* feat(buddy): hero pixel-art companions with signature Enter animations
Rebuild the buddy system as heroes-only. The 18 legacy rolled species are
removed; the hatch pool is now 7 hero forms — robinhood, kaio, strawhat,
merlin, kage, ember, corsair — each hand-pickable via /buddy set.
Every hero has 22x16 truecolor half-block pixel art (idle + action poses),
a line-art fallback for low-color terminals, a narrow-mode face, and a
signature effect that fires on every message submission: arrow with impact
thunk, charging full-width energy wave, stretchy punch that extends and
snaps back, twinkling sparkle stream, spinning shuriken, gradient fire
cone, and cannonball with smoke trail.
Engine: companion animation moves from a raw 500ms setInterval to the
shared animation clock (useAnimationFrame; pauses when hidden, respects
prefersReducedMotion), with a one-shot 50ms burst driver (useShotClock,
arm-then-anchor to avoid stale-tick draw-phase skips) and a general
ActionEffect system (pure draw/travel/impact functions, frame-tested).
Effects travel right-to-left toward the prompt — matching where the
sprite actually stands.
Commands: /buddy set <form|random>, /buddy name <name>, muted-buddy
feedback (silent no-op pets now explain themselves), and a hatch-message
fix so the announced species always matches the displayed sprite (the
message previously rolled with a different seed).
BREAKING: existing rolled pets transform into a hero on upgrade (name and
personality persist; speciesOverride pins are unaffected).
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(buddy): address CodeRabbit review on PR #1972
- useShotClock: consume an in-flight shot when playback becomes
ineligible mid-flight (mute/reduced-motion/resize), so re-enabling
can't resume a stale animation.
- /buddy unmute: emit a greeting reaction — the sprite reads
companionMuted non-reactively and its clock is paused while hidden,
so a config-only unmute left it invisible until an unrelated
re-render.
- /buddy name: strip ANSI escapes and control/format characters before
saving, and cap by display width (stringWidth) instead of UTF-16
length.
- CompanionSprite: track bubble age in sync-render state instead of an
effect-updated ref, so a fresh reaction can't render pre-faded.
- companion_intro already keyed on name+species (prior commit); tests
now pin exact faces for all seven heroes, separate idle/shoot pixel
frame counts, and decode-guard the charCode species constants.
- CompanionActionFX tests: deterministic companion fixture via
complete-config module mock; raw (untrimmed) output compared against
a rendered-null baseline so a spurious blank FX row fails.
- companion.test: re-register the real config module in afterAll
(mock.restore does not undo mock.module).
- Types: SPECIES_COLORS and FORM_FLAVOR are full Records (compile
error on a colorless/flavorless future hero); dead RARITY_COLORS
removed.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test(buddy): regression coverage for bubble age reset on reaction change
Renders CompanionSprite against a fake shared clock: ages the first
bubble past the fade threshold, swaps the reaction WITHOUT advancing
the clock, and asserts the fresh bubble renders unfaded. Fading is
detected structurally (border and text collapse to one color when
fading) so the test is independent of the active theme's exact values.
Requested by CodeRabbit on PR #1972.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix(typecheck): replace 'external' === 'ant' dead-code literals with isAntEmployee()
The build system replaces process.env.USER_TYPE with the string literal
'external' at build time. Dead-code elimination then removes branches
where 'external' === 'ant'. But TypeScript sees these as impossible
comparisons (TS2367) because the narrowed literal type 'external'
never equals 'ant', producing 90 type errors across 27 files.
Replace all 'external === 'ant'' with isAntEmployee() and
'external !== 'ant'' with !isAntEmployee(). The function already
exists in src/utils/buildConfig.ts and always returns false, so this
is a behavioral no-op that makes the intent explicit and type-safe.
The process.env.USER_TYPE === 'ant' pattern in other files is not
touched; it will be addressed in a follow-up.
Refs: #1486
* fix(build): replace isAntEmployee() calls with false at build time for DCE
The bundler cannot dead-code-eliminate branches guarded by isAntEmployee()
because it's an opaque function call. Extend the feature-flag preprocess
plugin to also replace isAntEmployee() with false during bundling, so
dynamic import() and require() calls gated behind ant-employee checks
are eliminated from the external build.
Also export IS_ANT_EMPLOYEE as a named constant for call-site readability
and documentation, with the function kept as a convenience wrapper.
* fix(build): use IS_ANT_EMPLOYEE constant for ant-only import/require guards
CodeRabbit review identified that isAntEmployee() is a runtime function call
that bundlers cannot evaluate for DCE. Replace all isAntEmployee() guards on
dynamic import()/require() calls of ant-internal modules with the
IS_ANT_EMPLOYEE boolean constant (exported as `false as const`), which the
build-time source transform can replace with a literal `false` for DCE.
Also extend the featureFlagPreprocessPlugin to replace IS_ANT_EMPLOYEE with
false during bundling, and clean up the resulting dead imports/exports
(`import { false, isAntEmployee }` → `import { isAntEmployee }`,
`export const false = false as const` → removed).
Affected ant-only modules (all missing from OpenClaude, must be DCE'd):
- sessionDataUploader.js, eventLoopStallDetector.js, sdkHeapDumpMonitor.js
- ccshareResume.js, cli/up.js, cli/rollback.js, cli/handlers/ant.js
- useFrustrationDetection.js, useAntOrgWarningNotification.js
- AntModelSwitchCallout.js, UndercoverAutoCallout.js
Inline base64 source maps had been checked into tracked src files. This strips those comments from the repository without changing runtime behavior or adding ongoing guardrails, per the requested one-time cleanup scope.
Constraint: Keep this change limited to tracked source cleanup only
Rejected: Add CI/source verification guard | user requested one-time cleanup only
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If these directives reappear, fix the producing transform instead of reintroducing repo-side cleanup code
Tested: rg -n "sourceMappingURL" ., bun run smoke, bun run verify:privacy, bun run test:provider, npm run test:provider-recommendation
Not-tested: bun run typecheck (repository has many pre-existing unrelated failures)
Co-authored-by: anandh8x <test@example.com>
Squash the current repository state back into one baseline commit while
preserving the README reframing and repository contents.
Constraint: User explicitly requested a single squashed commit with subject "asdf"
Confidence: high
Scope-risk: broad
Reversibility: clean
Directive: This commit intentionally rewrites published history; coordinate before future force-pushes
Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr
Not-tested: Fresh clone verification after push