UX redesign: installer + provider rename + /learn-codebase + welcome card + SessionStart hint (#2255)

* feat(ux): claude-mem UX improvements with installer enhancements

Squashed PR #2156 commits for clean rebase onto main:
- feat(installer): add provider selection, model prompt, worker auto-start
- refactor: rename *Agent provider classes to *Provider
- feat: add /learn-codebase skill and viewer welcome card
- feat(worker): inject welcome hint when project has zero observations
- fix(pr-2156): address greptile review comments
- fix(pr-2156): address coderabbit review comments
- fix(pr-2156): persist CLAUDE_MEM_PROVIDER for non-claude in non-TTY mode
- fix(pr-2156): file-backed settings reads in installer + env-first SKILL doc

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: rebuild plugin artifacts after rebase onto v12.4.7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): strip claude-mem internals from learn-codebase

The learn-codebase skill, install next-step copy, WelcomeCard, and
welcome-hint previously walked the primary agent through worker endpoints
and synthetic observation payloads. The PostToolUse hook already captures
every Read/Edit the agent makes — the agent should have no awareness that
the memory layer exists. Collapse the skill to one instruction ("read every
source file in full") and rephrase touchpoints to describe only what the
user observes (Claude reading files), not what happens behind the scenes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): preflight version mismatch + settings-aware port resolution

Two related fixes for build-and-sync's worker restart step:

1. Read CLAUDE_MEM_WORKER_PORT from ~/.claude-mem/settings.json the same
   way the worker does, instead of computing the default port from the
   uid alone. Previously, users with a custom port saw a misleading
   "Worker not running" message because the restart POST hit the wrong
   port and got ECONNREFUSED.

2. Add a preflight check that aborts the sync when the running worker's
   reported version does not match the version we are about to build.
   Claude Code's plugin loader pins the worker to a specific cache
   version per session, so syncing into a newer cache directory has no
   effect until the user runs `claude plugin update thedotmack/claude-mem`
   to bump the pin. The preflight surfaces this explicitly with the exact
   command to run; --force bypasses it for intentional cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(learn-codebase): note sed for partial reads of large files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide

Removed prose comments from all tracked source. Preserved directives
(@ts-ignore, eslint-disable, biome-ignore, prettier-ignore, triple-slash
references, webpack magic, shebangs). Deleted two tests that asserted
on comment text rather than runtime behavior.

Net: 401 files, -14,587 / +389 lines, -10.4% bytes.

Verified: typecheck passes, build passes, test count unchanged from
baseline (22 pre-existing fails, all unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(installer): move runtime setup into npx, eliminate hook dead air

Smart-install ran 3 times during a fresh install — the worst run was silent,
fired by Claude Code's Setup hook after `claude plugin install`, producing
~30s of dead air that looked like the plugin was hung.

This change makes `npx claude-mem install` the single place heavy work
happens, with a visible spinner. Hooks become runtime-only.

- New `src/npx-cli/install/setup-runtime.ts` module: ensureBun, ensureUv,
  installPluginDependencies, read/writeInstallMarker, isInstallCurrent.
  Marker schema preserved exactly ({version, bun, uv, installedAt}) so
  ContextBuilder and BranchManager readers keep working.
- `npx claude-mem install`: ungated copy/register/enable for every IDE,
  inserts a "Setting up runtime" task with honest "first install can take
  ~30s" spinner. The claude-code shell-out to `claude plugin install` is
  removed — npx already populated everything Claude reads.
- New `npx claude-mem repair` command for post-`claude plugin update`
  recovery, force-reinstalls runtime.
- Setup hook now runs `plugin/scripts/version-check.js` (29ms wall) instead
  of smart-install. Mismatch prints "run: npx claude-mem repair" on stderr.
  Always exits 0 (non-blocking, per CLAUDE.md exit-code strategy).
- SessionStart loses the smart-install entry; 2 hooks remain (worker start,
  context fetch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(installer): delete smart-install sources, retarget tests

- Delete scripts/smart-install.js + plugin/scripts/smart-install.js (both
  are source files kept in sync manually; both must go).
- Delete tests/smart-install.test.ts (covered surface is gone).
- tests/plugin-scripts-line-endings: drop smart-install.js entry.
- tests/infrastructure/plugin-distribution: retarget two assertions at
  version-check.js (the new Setup hook script).
- New tests/setup-runtime.test.ts: 9 tests covering marker read/write,
  isInstallCurrent semantics. Marker schema invariant verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(installer): describe npx-driven setup + version-check Setup hook

Sweep public docs and architecture notes to reflect the new flow:
npx installer does Bun/uv setup with a visible spinner; Setup hook runs
sub-100ms version-check.js; users hit `npx claude-mem repair` after a
`claude plugin update`.

- docs/architecture-overview.md: hook lifecycle table + npx flow paragraph
- docs/public/configuration.mdx: tree + hook config example
- docs/public/development.mdx: build output line
- docs/public/hooks-architecture.mdx: full rewrite of pre-hook section,
  timing table, performance table
- docs/public/architecture/{overview,hooks,worker-service}.mdx: tree
  comments, JSON config example, Bun requirement section

docs/reports/* untouched (historical incident reports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): mergeSettings writes via USER_SETTINGS_PATH

Greptile P1 (#2156): `settingsFilePath()` only resolved
`process.env.CLAUDE_MEM_DATA_DIR`, while `getSetting()` reads via
`USER_SETTINGS_PATH` which `resolveDataDir()` populates from BOTH the env
var AND a `CLAUDE_MEM_DATA_DIR` entry persisted in
`~/.claude-mem/settings.json`. Result: a user with the data dir saved in
settings.json but not exported in their shell would have provider/model
settings silently written to `~/.claude-mem/settings.json` while
`getSetting()` read from `/custom/path/settings.json` — read/write split.

Drop `settingsFilePath()` and the now-unused `homedir` import; reuse the
already-imported `USER_SETTINGS_PATH` constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): parse --provider, --model, --no-auto-start install flags

Greptile P1 (#2156): InstallOptions has fields `provider`, `model`,
`noAutoStart`, but the install case in the npx-cli switch only parsed
`--ide`. The other three flags were silently dropped — `npx claude-mem
install --provider gemini` was a no-op.

Extract a `parseInstallOptions(argv)` helper, share it between the bare
`npx claude-mem` and `npx claude-mem install` paths, and validate
`--provider` against the allowed set. Update help text accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): pipe runtime-setup output, always show IDE multiselect

Two issues caught in a docker test of the installer:

1. The bun.sh installer, uv installer, and `bun install` were using
   stdio: 'inherit', dumping their stdout/stderr through clack's spinner
   region — visible as raw "downloading uv 0.11.8…" / "Checked 58
   installs across 38 packages…" text streaming under the spinner. Switch
   to stdio: 'pipe' and surface captured stderr only on failure (via a
   shared describeExecError() helper that includes stdout when stderr is
   empty). Spinner stays clean on the happy path.

2. promptForIDESelection() silently picked claude-code when no IDEs were
   detected, never showing the user the multiselect. On a fresh machine
   with no IDEs present yet (e.g. our docker test container), the user
   never got to choose. Now: always show the full IDE list when
   interactive; mark detected ones with [detected] hints and pre-select
   them; show a warn line if zero are detected explaining they should pick
   what they plan to use. Non-TTY callers still get the silent
   claude-code default at the call site (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): skip marketplace work for claude-code-only, offer to install Claude Code

Two related UX fixes from a docker test:

**Delay between "Saved Claude model=…" and "Plugin files copied OK"**

After dropping the needsManualInstall gate, every install was unconditionally
running `copyPluginToMarketplace` (which copied the entire root node_modules
tree — thousands of files, dozens of seconds) and `runNpmInstallInMarketplace`
(npm install --production) even when only claude-code was selected. Neither
is needed for claude-code: that path uses the plugin cache dir + the
installed_plugins.json + enabledPlugins flag, all of which we already write.

- Drop `node_modules` from `copyPluginToMarketplace`'s allowed-entries list;
  the dependency-install task populates it on the destination side anyway.
- Re-introduce `needsMarketplace = selectedIDEs.some(id => id !== 'claude-code')`
  scoped *only* to `copyPluginToMarketplace`, `runNpmInstallInMarketplace`,
  and the pre-install `shutdownWorkerAndWait` (also pointless for claude-code-
  only flows since we're not overwriting the worker's running cache dir
  source). All other tasks (cache copy, register, enable, runtime setup) stay
  unconditional.

**Claude Code missing → silent install of an IDE that isn't there**

When the user picked claude-code on a machine without it (e.g. a fresh
container), the install completed but `claude` was unavailable and the only
hint was a generic warn line. Replace with an explicit pre-flight prompt:

  Claude Code is not installed. Claude-mem works best in Claude Code, but
  also works with the IDEs below.
  ? Install Claude Code now?
    ◆ Yes — install Claude Code (recommended)
    ◯ No — pick another IDE below
    ◯ Cancel installation

If the user picks "Yes", run `curl -fsSL https://claude.ai/install.sh | bash`
(or the PowerShell equivalent on Windows), then re-detect IDEs and proceed
with claude-code pre-selected. If the install fails or the user picks "No",
the multiselect still appears with claude-code visible (just unmarked
[detected]), so they can opt in or pick another IDE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): detect Claude Code via `claude` CLI, not ~/.claude dir

The directory `~/.claude` can exist (e.g. mounted in Docker, or created
by tooling) without Claude Code actually being installed. Detect the
`claude` command in PATH instead so the installer correctly offers to
install Claude Code when missing.

* docs(learn-codebase): add reviewer note explaining the cost tradeoff

The skill intentionally reads every file in full to build a cognitive
cache that pays off across the rest of the project. Add a brief note
so reviewers (human or bot) understand the tradeoff before flagging
the unbounded read as a cost issue.

* fix: address Greptile P1 feedback on welcome hint and learn-codebase

- SearchRoutes: skip welcome hint when caller passes ?full=true so
  explicit full-context requests aren't intercepted by the hint.
- learn-codebase: replace `sed` instruction with the Read tool's
  offset/limit parameters, since Bash is gated in Claude Code by
  default.

* feat(install): ASCII-animated logo splash on interactive install

Plays a ~1s bloom animation of the claude-mem sunburst logomark when
the installer starts in an interactive terminal — geometrically rendered
via 12 ray curves around a center disc, in the brand orange. The
wordmark and tagline type on alongside the final frame.

Auto-skipped on non-TTY, in CI, when NO_COLOR or CLAUDE_MEM_NO_BANNER
is set, or when the terminal is too narrow.

Inspired by ghostty +boo.

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): three-act choreography renderer with radial gradient and diff redraw

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): update preview script to support small/medium/hero tier selection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): add COLORTERM=truecolor to test-installer sandbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): auto-apply PATH for Claude Code with spinner UX

The Claude Code install.sh prints a Setup notes block telling users to
manually edit "your shell config file" to add ~/.local/bin to PATH —
which left fresh installs unable to launch claude from the command line.

After a successful install, detect ~/.local/bin/claude on disk and, if
the dir is missing from PATH, append the right export line to .zshrc /
.bash_profile / .bashrc / fish config (idempotent, marked with a
comment). Also updates process.env.PATH for the current install run.

Wraps the curl|bash install in a clack spinner (interactive only) so the
~4 minute native-build download doesn't look frozen — output is captured
silently and dumped on failure for debuggability. Non-interactive mode
keeps inherited stdio for CI logs.

Verified end-to-end in the test-installer docker sandbox: spinner
animates, .bashrc gets the export, fresh login shell resolves claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): video-frame ASCII renderer with three-act choreography

Generator switched from a single Jimp-rendered logo to pre-extracted
video frames concatenated with \x01 separators and gzip-deflated, ported
from ghostty's boo wire format. Renderer rewritten around three acts
(ignite → stagger bloom → text reveal + breathe) with adaptive sizing,
radial gradient, and diff-based redraw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(onboarding): unify install / SessionStart / viewer around one first-success moment

Three surfaces now point at the same north-star moment — open the viewer, do
anything in Claude Code, watch an observation appear within seconds — with the
same verbatim timing and privacy lines, and a single canonical "how it works"
explainer instead of three diverging copies.

- Canonical explainer at src/services/worker/onboarding-explainer.md served via
  GET /api/onboarding/explainer; mirrored into plugin/skills/how-it-works/SKILL.md
- SessionStart welcome hint rewritten as third-person status (no imperatives
  Claude tries to execute), pinned with a default-value regression test
- Post-install Next Steps reframed as "two paths": passive default + optional
  /learn-codebase front-load; drops /mem-search and /knowledge-agent from this
  surface; adds verbatim timing + privacy lines and /how-it-works link
- /api/stats response gains firstObservationAt for the viewer stat row
- Viewer WelcomeCard branches on observationCount === 0: empty state shows live
  worker-connection dot + "waiting for activity"; has-data state shows
  observations · projects · since [date] and two example prompts. v2 dismiss key
- jimp added to package.json to fix pre-existing banner-frame build break

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(banner): play unconditionally; only honor CLAUDE_MEM_NO_BANNER

The 128-col / TTY / CI / NO_COLOR gates silently swallowed the banner in
narrower terminals, CI logs, and any non-TTY pipe — including Docker runs
where -it should preserve the experience but column width was the wrong
gate. Remove the implicit gates; keep the explicit opt-out only.

If a frame wraps in a narrow terminal, that's better than the banner
not playing at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(banner): restore 15:33 gating logic per user request

Reverts eb6fc157. Restores isBannerEnabled to the state at commit
8e448015 (2026-04-30 15:33): TTY check, !CI, !NO_COLOR, !CLAUDE_MEM_NO_BANNER,
and cols >= BANNER.width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): wrap remaining slow steps with spinners

Each IDE installer (Cursor, Gemini CLI, OpenCode, Windsurf, OpenClaw,
Codex CLI, MCP integrations) now runs inside a clack task spinner with
per-step progress messages instead of silent dynamic-import + cpSync.
Pre-overwrite worker shutdown (up to 10s) and the post-install health
probe (up to 3s) also get spinners.

Internal console.log/error/warn from each IDE installer is buffered
during the spinner; if the install fails, captured output is replayed
afterward via log.warn so users can see what broke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): observation count + IDE pre-selection regressions

WelcomeCard's "no observations yet" empty state was triggered when a
project filter narrowed the feed to zero rows, even with thousands of
observations elsewhere. Source the count from global stats.database
to match firstObservationAt's scope.

Restore initialValues: [] in the IDE multiselect — pre-selecting every
detected IDE was the exact regression #2106 was filed for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): trichotomy worker state + cache fallback for script path

ensureWorkerStarted now returns 'ready' | 'warming' | 'dead' instead of
boolean. The spawned-but-still-warming case (common in Docker cold
starts and slow first-time inits) was being misreported as 'did not
start', which contradicted the next-steps panel saying 'still starting
up'. Install task message and Next Steps headline now agree on the
actual state.

Also fixes the actual root cause of 'Worker did not start' on
claude-code-only installs: the worker script path was hardcoded to the
marketplace dir, which is left empty when no non-claude-code IDE is
selected. Now falls back to pluginCacheDirectory(version) when the
marketplace copy isn't present.

Verified end-to-end in docker/claude-mem with --ide claude-code,
--ide cursor, and a fresh container — install task and headline
agree on 'Worker ready at http://localhost:<port>' in all cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: align CLAUDE.md and public docs with current code

Sweep across CLAUDE.md and 10 high-traffic docs/public/ MDX files to
remove point-in-time references and align with the actual current
shape of the codebase. Highlights:

- Hardcoded port 37777 → per-user formula (37700 + uid % 100) on the
  front-door pages (introduction, installation, configuration,
  architecture/overview, architecture/worker-service, troubleshooting,
  hooks-architecture, platform-integration).
- Default model 'sonnet' → 'claude-haiku-4-5-20251001' (matches
  SettingsDefaultsManager).
- Node 18 → 20 (matches package.json engines).
- Lifecycle hook count corrected (5 events).
- Removed the nonexistent 'Smart Install' component and pre-built
  directory tree referencing files that no longer exist
  (context-hook.ts, save-hook.ts, cleanup-hook.ts, etc.); replaced
  with the real worker dispatcher shape.
- Removed CLAUDE.md '#2101' issue tag (kept the design rationale).
- Replaced obsolete hooks.json example with a description of the real
  bun-runner.js / worker-service.cjs hook event shape.

Lower-traffic doc pages still hardcode 37777 — left for a separate
global pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): land strip-comments around real parsers (postcss, remark, parse5)

Each language gets a real parser to locate comments, then we splice ranges
out of the original source. The library never serializes — that's how
remark-stringify produced 243 reformat-noise diffs in the first attempt
versus the 21 real strip targets here.

  JS/TS/JSX  -> ts.createSourceFile + getLeadingCommentRanges
  CSS/SCSS   -> postcss.parse + walkComments + node.source offsets
  MD/MDX     -> remark-parse (+ remark-mdx) + AST html / mdx-expression nodes
  HTML       -> parse5 with sourceCodeLocationInfo
  shell/py   -> kept hand-rolled hash stripper (no library worth the dep)

Preserves: shebangs, @ts-* directives, eslint-disable, biome-ignore,
prettier-ignore, triple-slash refs, webpack magic, /*! license keep,
@strip-comments-keep file marker. JS/TS handler runs a parse-roundtrip
check and refuses to write if syntax errors increased (catches the
worker-utils.ts breakage class from the 2026-04-29 attempt).

npm scripts:
  strip-comments         (apply)
  strip-comments:check   (CI-style, exits non-zero if changes needed)
  strip-comments:dry-run (list, no writes)

Verified --check on this repo: 21 changes, -4.0% bytes, no parse-error
regressions, no reformat-suspect false positives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide via parser-backed tool

21 files changed, -17,550 bytes (-4.0%) of narrative comments removed
across .ts / .tsx / .js / .mjs and the .gitignore. JS/TS comments stripped
via ts.createSourceFile + getLeadingCommentRanges — same canonical lexer,
same behavior as the 2026-04-29 strip, no reformat noise.

Preexisting baseline (unchanged):
  typecheck: 16 errors at HEAD, 16 errors after strip (line numbers shift,
             no new error classes — verified via diff of sorted error lists)
  build:     fails at HEAD with CrushHooksInstaller.js unresolved import
             (preexisting, unrelated to this strip)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): drop Crush integration references after extract

The Crush integration was extracted to its own branch on May 1, but the
import at install.ts:280 (and the case block + ide-detection entry +
McpIntegrations config + npx-cli help text) still referenced the now-
removed CrushHooksInstaller.js, breaking the build.

Removes:
- case 'crush' block in install.ts
- crush entry in ide-detection.ts
- CRUSH_CONFIG and registration in McpIntegrations.ts
- 'crush' from the IDE Identifiers help line in index.ts

Rebuilds worker-service.cjs to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(banner): mark generated banner-frames.ts with @strip-comments-keep

Without this, every build/strip cycle ping-pongs five lines of doc
comments in and out of the auto-generated output. The keep-marker tells
strip-comments.ts to skip the file entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): drop banner-frame regen from build script

generate-banner-frames.mjs requires PNG frames in /tmp/cmem-banner-frames
that only exist after the maintainer runs ffmpeg locally on the source
video. CI has neither the video nor the frames, so the build broke on
Windows. The output (src/npx-cli/banner-frames.ts) is committed, so the
regen is a one-shot dev step — not a build step. Run the script directly
when the video changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): unstick the spinner — kill claim-self-lock, wake on fail, auto-broadcast

Three surgical changes that cure the stuck-spinner bug at the source.

Phase 1.1 (L9): claimNextMessage no longer self-excludes its own worker_pid.
A single UPDATE-RETURNING grabs the oldest pending row by id. Removes the
LiveWorkerPidsProvider plumbing that was never injected — Supervisor enforces
single-worker via PID file, so the multi-worker SQL was defending against a
configuration the project does not support.

Phase 1.2 (L19): SessionManager.markMessageFailed wraps PendingMessageStore.markFailed
and emits 'message' on the per-session EventEmitter. The iterator's waitForMessage
now wakes immediately on re-pend instead of parking for 3 minutes. ResponseProcessor
and SessionRoutes routed through the new wrapper.

Phase 1.3 (L24): PendingMessageStore takes an optional onMutate callback fired
from every mutator (enqueue, claimNextMessage, confirmProcessed, markFailed,
transitionMessagesTo, clearFailedOlderThan). SessionManager wires it; WorkerService
passes broadcastProcessingStatus. Ten manual broadcast calls deleted across
SessionCleanupHelper, SessionEventBroadcaster, SessionRoutes, DataRoutes, and
worker-service. Caller discipline becomes structurally impossible to forget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): delete dead code — legacy routes, processPendingQueues, decorative guards

Pure deletions. Phase 2 of kill-the-asshole-gates.

- Legacy /sessions/:sessionDbId/* routes (handleSessionInit, handleObservations,
  handleSummarize, handleSessionStatus, handleSessionDelete, handleSessionComplete)
  bypassed all five ingest gates and were a parallel write path. Folded the
  initializeSession + broadcastNewPrompt + syncUserPrompt + ensureGeneratorRunning
  + broadcastSessionStarted work into the canonical /api/sessions/init handler so
  the hook makes one round trip instead of two.
- processPendingQueues (~104 lines, zero callers) — replaced in Phase 6 by a
  one-statement startup sweep.
- spawnInProgress Map and crashRecoveryScheduled Set — decorative dedupe over
  generatorPromise and stillExists checks that already provide the real safety.
- STALE_GENERATOR_THRESHOLD_MS — pre-empted live generators and raced with the
  finally block; the 3min idle timeout already kills zombies.
- MAX_SESSION_WALL_CLOCK_MS — ran a SELECT on every observation to enforce 24h.
  Runaway-spend protection lives in the API key, not in claude-mem.
- Missing-id 400 in shared.ts ingestObservation — Zod already enforces min(1)
  on contentSessionId and toolName at the route schema.
- SessionCompletionHandler import + completionHandler field on SessionRoutes
  (orphaned after handler deletions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): SQL-backed getTotalQueueDepth — single source of truth

Was: iterate this.sessions.values() and sum getPendingCount per session.
Now: SELECT COUNT(*) FROM pending_messages WHERE status IN ('pending','processing').

The in-memory sessions Map drifted from the DB rows whenever a generator exited
without confirm/fail, leading to false-positive isProcessing in the UI. Phase 1.3's
auto-broadcast fires on every mutation, but it broadcast a stale Map count.
Reading from the DB makes the UI's spinner state match what the queue actually holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): typed abortReason replaces wasAborted boolean

Was: a boolean wasAborted that lumped every abort together. The finally block
branched on !wasAborted, so any abort skipped restart — including idle aborts
with pending work, which is exactly the case where we DO want to restart.

Now: ActiveSession.abortReason is a typed enum 'idle' | 'shutdown' | 'overflow'
| 'restart-guard'. The finally block consumes the reason and only skips restart
for 'shutdown' and 'restart-guard'. Idle and overflow aborts fall through, so
if pending work exists they trigger restart correctly.

Dropped 'stale' and 'wall-clock' from the union — Phase 2 deleted those paths.
Natural-completion abort (post-success) intentionally has no reason; it's not
gating restart logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): unify the two generator-exit finally blocks

Was: worker-service.ts:startSessionProcessor and SessionRoutes:ensureGeneratorRunning
each had their own ~70-line finally block with divergent restart-guard handling.
The worker-service path called terminateSession on RestartGuard trip and orphaned
pending rows (the L16 bug); the SessionRoutes path drained them. Two places to
update when rules changed.

Now: handleGeneratorExit in src/services/worker/session/GeneratorExitHandler.ts
owns the contract:
  1. Always kill the SDK subprocess if alive.
  2. Always drain processingMessageIds via sessionManager.markMessageFailed
     (which wakes the iterator — Phase 1.2).
  3. shutdown / restart-guard reasons: drain pending rows via
     transitionMessagesTo('failed'), finalize, remove from Map. Fixes L16.
  4. pendingCount=0: finalize normally and remove from Map.
  5. pendingCount>0: backoff respawn via per-session respawnTimer (no global Set;
     Phase 2.4 deleted that). RestartGuard trip drains to 'abandoned'.

Both finally blocks are now ~10-line wrappers that translate local state into the
canonical abortReason and delegate. Restored completionHandler injection into
SessionRoutes (was dropped in Phase 2 cleanup; needed by the unified helper for
finalizeSession).

Behavior change: SessionRoutes' previous "keep idle session in memory" was
deliberately replaced by the plan's "remove from Map on natural completion" —
next observation reinitializes via getMessageIterator → initializeSession.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(worker): startup orphan sweep — reset 'processing' rows at boot

When the worker dies (crash, kill, restart), any pending_messages rows it left
in 'processing' state are by definition orphans (the only worker is dead).
Single SQL UPDATE at boot resets them to 'pending' so the iterator can claim
them again. Replaces the deleted processPendingQueues function (Phase 2.2).

Runs in initializeBackground after dbManager.initialize() and before the
initializationComplete middleware releases blocked HTTP requests, so no
in-flight request can race the sweep. NOT on a periodic timer — after boot,
every 'processing' row has a live consumer and a periodic sweep would race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): simplify enqueue catch, replace memorySessionId throw with re-pend

7.1: queueObservation's catch was logging two ERROR-level messages and rethrowing.
The rethrow is correct (FK violations / disk full / schema drift should crash
loudly), but the verbose ERROR logging pretended the error was recoverable.
Reduced to one INFO line + rethrow.

7.2: ResponseProcessor's memorySessionId guard was throwing if the SDK hadn't
included session_id on the first user-yield, terminal-failing the entire batch.
Now warns and re-pends in-flight messages via sessionManager.markMessageFailed
(which wakes the iterator — Phase 1.2). The next iteration tries again with
memorySessionId hopefully captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): mirror builds to installed-version cache for hot reload

When package.json bumps past Claude Code's installed pin, sync-marketplace
wrote new code to cache/<buildVersion>/ but the worker loaded from
cache/<installedVersion>/, so worker:restart reloaded the same old code.

Replace the exit-on-mismatch preflight with a mirror step: when versions
differ, also rsync plugin/ into cache/<installedVersion>/ so worker:restart
hot-reloads new code without a Claude Code session restart. The
build-version cache still gets written for the eventual
`claude plugin update`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: delete dead barrel files and orphan utilities

- src/sdk/index.ts (re-exports parser+prompts; nothing imported the barrel)
- src/services/Context.ts (re-exports ./context/index.js; no importers)
- src/services/integrations/index.ts (no importers)
- src/services/worker/Search.ts (3-line barrel of ./search/index.js)
- src/services/infrastructure/index.ts: drop CleanupV12_4_3 re-export
- src/utils/error-messages.ts (getWorkerRestartInstructions never imported)
- src/types/transcript.ts (170 LoC of types, zero importers)
- src/npx-cli/_preview.ts (banner dev preview, no script wires it)

Build + tests still pass; observations still flowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(parser): drop unused detectLanguage

Only the user-grammar-aware variant detectLanguageWithUserGrammars()
is actually called.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(types): drop unused SdkSessionRecord + ObservationWithContext

Both interfaces in src/types/database.ts had zero importers anywhere
in src or tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): drop unused getDetectedIDEs + claudeMemDataDirectory

getDetectedIDEs has no callers — install.ts uses detectInstalledIDEs
directly. claudeMemDataDirectory has no callers either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ProcessManager): drop dead orphan-reaper + signal-handler helpers

Each had zero callers in src/ or tests/:
  - cleanupOrphanedProcesses + enumerateOrphanedProcesses
  - ORPHAN_PROCESS_PATTERNS + ORPHAN_MAX_AGE_MINUTES
  - forceKillProcess
  - waitForProcessesExit
  - createSignalHandler
  - resetWorkerRuntimePathCache

The orphan reaper was retired in PATHFINDER Plan 02 ("OS process groups
replace hand-rolled reapers", commit 94d592f2) — these were the leftover
pieces. shutdown.ts uses the supervisor's own kill-pgid path instead.

parseElapsedTime kept (covered by tests/infrastructure/process-manager.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete 11 unreferenced DX/forensic scripts

None of these are referenced by package.json npm scripts or docs/.
All last touched on Apr 29 only as part of the comment-stripping
pass — the feature code itself is older and orphaned:

  analyze-transformations-smart.js
  debug-transcript-structure.ts
  dump-transcript-readable.ts
  endless-mode-token-calculator.js
  extract-prompts-to-yaml.cjs
  extract-rich-context-examples.ts
  find-silent-failures.sh
  fix-all-timestamps.ts
  format-transcript-context.ts
  test-transcript-parser.ts
  transcript-to-markdown.ts

These are standalone tools — runtime behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete unused extraction/ and types/ subdirs

- scripts/extraction/{extract-all-xml.py, filter-actual-xml.py, README.md}
  point at ~/Scripts/claude-mem/ — the user's pre-relocation path that no
  longer exists. Zero references in package.json, src/, or tests/.
- scripts/types/export.ts duplicates ObservationRecord etc. and has no
  importers (CodexCliInstaller imports transcripts/types, not this).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(BranchManager): drop dead getInstalledPluginPath

OpenCodeInstaller has its own (used) getInstalledPluginPath; the
BranchManager copy never had any external callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ChromaSyncState): unexport DocKind (used internally only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gemini): drop stale earliestPendingTimestamp / processingMessageIds

Both fields were removed from ActiveSession in earlier queue-engine
cleanup. Tests had been silently keeping them because the mock sessions
use 'as any' to bypass strict typing, so the dead fields rode along
without complaint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 3 unused module-level constants

- src/npx-cli/banner.ts: CURSOR_HOME, CLEAR_DOWN (banner uses
  CLEAR_SCREEN which combines clear-down + cursor-home into a single
  CSI sequence; the standalone constants were leftovers).
- src/services/worker/BranchManager.ts: DEFAULT_SHELL_TIMEOUT_MS
  (BranchManager only uses GIT_COMMAND_TIMEOUT_MS / NPM_INSTALL_TIMEOUT_MS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(opencode-plugin): drop dead workerPost helper

Only the fire-and-forget variant (workerPostFireAndForget) is actually
called. workerPost was the await-result version with no remaining caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 8 truly-unused interface fields

Verified each by grepping for `.field`, `"field"`, `'field'`, and
`field:` patterns across src/ + tests/ + plugin/scripts. Where the
only remaining usage was the assignment site, removed the assignments too.

- GitHubStarsData: watchers_count, forks_count (only stargazers_count read)
- TableColumnInfo: dflt_value (PRAGMA returns it but no caller reads it)
- IndexInfo: seq (PRAGMA returns it but no caller reads it)
- ObservationRecord: source_files (legacy field, no readers)
- HookResult.hookSpecificOutput: permissionDecisionReason
- WatchTarget: rescanIntervalMs (set in config, never read)
- ShutdownResult: confirmedStopped (write-only — assigned but no
  reader; updated all 3 return sites to drop it)
- ModePrompts: language_instruction (multilingual support never wired)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): reuse InstallOptions type instead of inline duplicate

parseInstallOptions had its return type written out inline as an
anonymous duplicate of InstallOptions. Use the canonical type
(import type — zero bundle cost).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(integrations): drop unused Platform type alias

The detectPlatform() function that returned this type was deleted earlier
in the branch (along with getScriptExtension that consumed it). The type
itself outlived its consumer; only string literals "Platform:" survive in
console.log diagnostics, which don't reference the alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): broadcast processing_status when summarize is queued

broadcastSummarizeQueued was an empty no-op even though
handleSummarizeByClaudeId calls it after enqueueing. The PendingMessageStore
onMutate callback already fires broadcastProcessingStatus on enqueue, but
calling it explicitly from broadcastSummarizeQueued ensures the spinner
ticks on the moment a summary is requested even if the onMutate chain has
any timing race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): keep spinner on while summary generates

ClaudeProvider's SDK can pull multiple synthetic prompts (e.g.
observation + summarize) before producing responses. Each pull pushed
an ID to session.processingMessageIds. When the SDK's first
observation response came back, ResponseProcessor.confirmProcessed
deleted ALL pending message rows — including the still-in-flight
summary — so getTotalQueueDepth dropped to 0 and the spinner turned
off, even though the summary took another ~22s to actually generate.

Tag each in-flight message with its type ({id, type}) so the response
processor can pop only the FIFO message of the matching type
(observation vs summarize). The summary row stays in 'processing'
until its own response arrives, keeping the spinner lit through the
entire summary window.

Also updates Gemini/OpenRouter providers and GeneratorExitHandler for
the new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): clear summary from queue on any SDK response

Switch ResponseProcessor from type-aware FIFO matching to strict FIFO
popping (each SDK response → 1 in-flight message consumed). This way
the summary always clears when the SDK responds, even when the
response is unparseable or the summary doesn't actually generate
content — preventing stuck spinner / queue-depth-stuck-at-1.

Spinner behavior is preserved: messages enqueued after the summary
keep the queue depth elevated, and only when the SDK has responded
to every prompt does the queue drain to zero.

Also: when the consumed message is a 'summarize' and parsing fails,
treat it as best-effort and confirmProcessed (no retry) — summaries
that can't be parsed shouldn't keep retrying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): redesign welcome card and remove source filters

The first-start welcome card now explains the three feed card types
(observation/summary/prompt) with color-coded badges, points users at
the gear icon for settings and the project dropdown for filtering, and
plugs /mem-search for recall — replacing the old two-line "ask:" prompts.

Source filter tabs (Claude/Codex/etc.) are removed from the header.
Filtering by AI provider was nonsense from a user POV; the project
dropdown is the only header filter now. Source tracking is also
stripped from useSSE, usePagination, App state, and CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): keep welcome card in feed column, swap rows for 3 squares

Two visible problems in the previous design: the card stretched
edge-to-edge while feed cards sit in a centered 650px column, and
the body was a stack of long horizontal rows that scanned line-by-line.

Both fixed: Feed now accepts a pinnedTop slot so the welcome card
renders inside the same .feed-content column as observation cards.
Body is now a 3-column grid of square feature blocks — Live feed,
Tune it, Recall it — each with a custom inline SVG illustration
(stacked cards with color-coded stripes, gear+sliders, magnifier
over cards). Old text-row sections (welcome-card-types,
welcome-card-tips, welcome-card-section, welcome-card-tip-icon)
are removed. Squares stack to one column under 600px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): convert welcome card to glassy modal with stylized logo

Card now opens as a centered modal with a frosted/glass backdrop
(blur + saturate) so it doubles as a proper help dialog when reopened
from the header's question-mark button. Removed the observation count,
project count, and "since" date — those don't make sense for a
first-launch surface and felt out of place in a help context.

Header art swapped from the small webp logomark to the new
high-resolution sun/sunburst PNG (claude-mem-logo-stylized.png),
shipped as a checked-in asset in src/ui and plugin/ui.

Bigger throughout: 28px h2, 16px tagline, 88px illustrations,
26px feature padding, 1:1 aspect-ratio squares. Backdrop click and
Esc both close. Mobile collapses the grid to one column and drops
the aspect-ratio constraint.

Reverted the unused pinnedTop slot on Feed.tsx since the welcome
card is now a true overlay rather than an in-feed pinned card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): make welcome modal actually glassy

Previous version had a 55%-opacity black backdrop that almost fully
blocked the underlying UI — the "glass" was just a dark plate.

Now the backdrop is fully transparent (no darkening at all), the
panel itself drops to 55% bg-card opacity with its existing
backdrop-filter blur(28px) saturate(170%), and the feature squares
drop to 35% bg-tertiary so they layer as glass-on-glass over the
already-blurred panel. The header and feed below now read clearly
through the modal's frosted blur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): bulletproof square features via padding-bottom + clamp() fluid type

Squares were rendering taller than wide because aspect-ratio is treated
as a minimum — content can push the box past 1:1. Switched to the
classic padding-bottom: 100% trick: percentage padding resolves against
the parent's width, so the box is ALWAYS W × W regardless of content.
Inner content sits in an absolutely-positioned flex column that can't
push the shell taller.

Whole modal is now desktop-first and fluid via clamp() — no media-query
stair-steps for type, padding, gaps, border-radius, illustration size,
or modal width. Single mobile breakpoint at <600px collapses the grid
to one column and reverts the padding-bottom trick so each feature can
grow to natural content height.

Tightened the three feature descriptions so they fit comfortably inside
the square at the desktop size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(viewer): 15% black overlay + heavier modal shadow for elevation

Backdrop goes from transparent to rgba(0,0,0,0.15) — just enough
darkening to push the modal visually forward without burying the
underlying UI. Modal shadow stacked: 40px/120px ambient + 16px/48px
contact, both deeper, plus the existing inset 1px highlight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): clear pending_messages queue on build-and-sync

Rewrites scripts/clear-failed-queue.ts to talk directly to SQLite via
bun:sqlite — the previous HTTP endpoints (/api/pending-queue/*) were
removed during the queue engine rewrite, so the script was orphaned.
Wires `npm run queue:clear` into `build-and-sync` so each rebuild
starts with a clean queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): collapse parser to binary valid/invalid + clearPendingForSession model

- Parser: { valid: true, observations, summary } | { valid: false } — drops kind/skipped enum dispatch
- ResponseProcessor: two branches only (parseable → store + clearPendingForSession; else → no-op)
- Drop processingMessageIds + per-message claim/confirm/markFailed lifecycle across 3 providers
- PendingMessageStore: 226 → 140 lines; remove markFailed/transitionMessagesTo/confirmProcessed/clearFailedOlderThan/getAllPending/peekPendingTypes... wait keep peekPendingTypes
- Schema migration v31+v32: drop retry_count, failed_at_epoch, completed_at_epoch, worker_pid columns
- SessionQueueProcessor: delete two 1s recovery sleeps (let iterator end on error)
- Server.ts/SettingsRoutes.ts: replace four magic-number setTimeout exit-flush patterns with flushResponseThen helper
- GeneratorExitHandler: 183 → 117 lines (drain in-flight loop gone)

Net: -181 lines. No more silent data loss via maxRetries=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): address review comments batch 1

- install.ts: needsMarketplace true when claude-code selected (P1, was no-op)
- install.ts: throw on invalid --model so CLI exits non-zero
- install.ts: skip worker health checks + adapt next-step copy when --no-auto-start
- install.ts: repair regenerates plugin cache when missing
- index.ts: readFlag rejects missing/flag-shaped values
- index.ts: route flag-first invocations (e.g. `--provider claude`) to install
- banner.ts: fail-open if frame payload decode throws
- SearchRoutes.ts: 5s TTL cache for settings reads on hot hook path (P2)
- detect-error-handling-antipatterns.ts: trailing-brace strip whitespace-tolerant
- investigate-timestamps.ts: compute Dec 2025 epochs at runtime (was Dec 2024)
- regenerate-claude-md.ts: include workingDir in fallback walker so root is covered
- sync-marketplace.cjs: parseWorkerPort validates 1..65535 before http.request
- sync-to-marketplace.sh: resolve SOURCE_DIR from script location, not cwd
- Dockerfile.test-installer: bash --login sources .bashrc via .bash_profile
- docs/configuration.mdx: drop nonexistent .worker.port file refs, use settings.json
- docs/architecture-overview.md: dynamic port + queue model after parser collapse
- docs/architecture/worker-service.mdx: dynamic port example + drop port-file claim
- docs/platform-integration.mdx: WORKER_BASE_URL pattern, drop hardcoded 37777
- install/public/install.sh: Node 20 floor (was 18) to match docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): reset claimed messages to pending on early-return paths

ResponseProcessor returns early in two cases:
- parser invalid (unparseable response)
- memorySessionId not yet captured

Both paths previously left the just-claimed message in `status='processing'`,
which counts toward `getPendingCount`. The generator-exit handler then sees
`pendingCount > 0` and respawns the generator, looping until the restart
guard trips and `clearPendingForSession` deletes the message — silent data
loss.

Calling `resetProcessingToPending` on these paths lets the next generator
pass re-claim the message and try again, instead of burning the restart
budget on no-op respawns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): swebench fallback row + troubleshooting port path

- evals/swebench/run-batch.py: append fallback prediction row when
  orchestrator future raises, preserving "never drop an instance" guarantee
- docs/troubleshooting.mdx: drop nonexistent .worker.port / worker.port file
  references; use settings.json + /api/health for port discovery

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): memoize per-project observation count for welcome-hint hot path

handleContextInject runs on every PostToolUse hook (after every Read/Edit).
The welcome-hint block ran a COUNT(*) on observations for every call once
CLAUDE_MEM_WELCOME_HINT_ENABLED was true. Observation counts are
monotonically increasing — once a project has any observations it always
will — so cache the positive result in a Set and skip the COUNT(*) on
subsequent requests.

Combined with the 5s settings TTL added earlier, the steady-state cost on
the hook hot path drops to a Set lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): use clearProcessingForSession on AI-success path

clearPendingForSession deletes ALL rows for the session. On the success
path of processAgentResponse, that's wrong: messages that arrived as
'pending' during the (1-5s) AI response latency get deleted along with
the 'processing' row we just consumed. In a hook burst (three quick
PostToolUse hooks), B and C land while A is in flight; A's success then
nukes B and C — silent data loss.

Add a status-scoped clearProcessingForSession to PendingMessageStore +
SessionManager, and use it in ResponseProcessor's success path. The
unconditional clearPendingForSession remains correct in
GeneratorExitHandler for hard-stop / restart-guard-trip paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(pr-2255): use clearProcessingForSession on AI-success path"

This reverts commit a08995299c30cbad36bddc3e5bddda7af8604b35.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-05-02 16:05:56 -07:00
committed by GitHub
parent 28b40c05f2
commit 9e2973059a
452 changed files with 6189 additions and 21059 deletions
+531
View File
@@ -0,0 +1,531 @@
# Installer Streamline — Eliminate 30s Silent Dead Air
**Goal:** Move all heavy install work (Bun/uv install, `bun install` in plugin cache) into the `npx claude-mem install` flow with a visible spinner. Make hooks runtime-only — never installers.
**Net effect:**
- `smart-install.js` runs in normal Claude Code lifecycle: 3 → 0 (or 1 via `npx claude-mem repair` after `claude plugin update`)
- 30s silent dead air → visible spinner during `npx`
- `npx claude-mem repair` becomes the canonical recovery entry point
- ~420 lines of code deleted (smart-install.js × 2 + tests + docs)
**Out of scope:** `bun-runner.js` deletion (independent rework with Windows/stdin verification needs — ship later).
---
## Phase 0 — Documentation Discovery (already complete)
These facts came from a discovery agent + direct file reads. Each implementation phase below cites them by line number; do not re-derive.
### Allowed APIs / patterns to copy
| Item | Location | What to copy |
|---|---|---|
| NPX command dispatcher | `src/npx-cli/index.ts:39141` | Manual `switch (command)` on `process.argv.slice(2)`. Each case dynamic-imports its handler. |
| `install` case (template for `repair`) | `src/npx-cli/index.ts:4652` | `const { runInstallCommand } = await import('./commands/install.js'); await runInstallCommand({ ide: ideValue });` |
| Plugin cache dir helper | `src/npx-cli/utils/paths.ts:3234` | `pluginCacheDirectory(version)``~/.claude/plugins/cache/thedotmack/claude-mem/{version}/` |
| `.install-version` marker readers | `src/services/context/ContextBuilder.ts:36,45` and `src/services/worker/BranchManager.ts:173,228` | These read/delete the marker. Marker schema (`{ version, bun, uv, installedAt }`) MUST be preserved. |
| `clack` task pattern | `src/npx-cli/commands/install.ts:604664` | `runTasks([{ title, task: async (message) => { … return 'Done OK' } }])` |
### Anti-patterns / API methods that DO NOT exist (avoid inventing)
- There is no existing `version-check.js` helper in `plugin/scripts/`. Phase 4 must create it.
- `package.json#files` already globs `plugin/scripts/*.js` (line 50), so deleting `plugin/scripts/smart-install.js` requires no `package.json` change.
- `scripts/smart-install.js` and `plugin/scripts/smart-install.js` are **both source files** kept in sync manually — there is no build step that copies one to the other. Both must be deleted in Phase 5.
- `runSmartInstall()` (install.ts:325345) shells `node smart-install.js`. After Phase 1 you can call the new module directly — do NOT shell out.
- The `claude plugin install` exec at install.ts:113 has **only one caller** in the entire repo. Safe to remove.
### File inventory used by this plan
| File | Lines | Disposition |
|---|---|---|
| `src/npx-cli/commands/install.ts` | 761 | Edited heavily (Phase 2) |
| `src/npx-cli/index.ts` | 147 | One case added (Phase 3) |
| `plugin/hooks/hooks.json` | 93 | Setup hook command rewritten, SessionStart smart-install entry deleted (Phase 4) |
| `scripts/smart-install.js` | 264 | DELETED (Phase 5) |
| `plugin/scripts/smart-install.js` | ≈264 | DELETED (Phase 5) |
| `tests/smart-install.test.ts` | 310 | DELETED (Phase 5) |
| `tests/plugin-scripts-line-endings.test.ts` | 33 | One array entry removed (Phase 5) |
| `plugin/scripts/version-check.js` | NEW | CREATED (Phase 4) |
| `src/npx-cli/install/setup-runtime.ts` | NEW | CREATED (Phase 1) |
| Docs (`docs/public/*.mdx`, `docs/architecture-overview.md`) | misc | Light edit (Phase 6) |
---
## Phase 1 — Create `src/npx-cli/install/setup-runtime.ts`
**What to implement:** Port the smart-install.js logic to a TypeScript module that takes a target directory parameter (so it can install into the plugin cache dir, not just the marketplace dir). Three exported functions plus internal helpers.
**File to create:** `src/npx-cli/install/setup-runtime.ts`
**API surface (these names are used by Phase 2 and Phase 3 — do not rename):**
```ts
export async function ensureBun(): Promise<{ bunPath: string; version: string }>;
export async function ensureUv(): Promise<{ uvPath: string; version: string }>;
export async function installPluginDependencies(targetDir: string, bunPath: string): Promise<void>;
export function readInstallMarker(targetDir: string): { version: string; bun?: string; uv?: string; installedAt?: string } | null;
export function writeInstallMarker(targetDir: string, version: string, bunVersion: string, uvVersion: string): void;
export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean;
```
**Reference implementation to port from:** `scripts/smart-install.js:1264`. Map old → new:
| smart-install.js | setup-runtime.ts |
|---|---|
| `getBunPath()` / `isBunInstalled()` / `installBun()` (lines 42152) | private helpers consumed by `ensureBun()` |
| `getUvPath()` / `isUvInstalled()` / `installUv()` (lines 77194) | private helpers consumed by `ensureUv()` |
| `needsInstall()` (lines 196205) | `isInstallCurrent()` + `readInstallMarker()` |
| `installDeps()` (lines 207226) | `installPluginDependencies(targetDir, bunPath)` — accepts target dir as parameter |
| `verifyCriticalModules()` (lines 228246) | private helper called inside `installPluginDependencies` |
| `MARKER` constant (line 32) | derive inside each function: `join(targetDir, '.install-version')` |
| Top-level `try { … }` (lines 248264) | DELETE — caller orchestrates |
**Key behavioral differences from smart-install.js:**
- All functions take `targetDir` as a parameter (was a top-level `ROOT` constant).
- `ensureBun()` / `ensureUv()` return their version strings rather than logging — caller decides what to display.
- All functions throw on failure with descriptive `Error.message`. The `clack` `runTasks` wrapper in Phase 2 catches and renders.
- `console.error` calls in install/uninstall paths become structured: throw a single `Error` with the manual install instructions in the message body.
- Marker schema is preserved exactly (`{ version, bun, uv, installedAt }`) so existing readers in `ContextBuilder.ts:36` and `BranchManager.ts:173,228` continue to work.
**Verification checklist:**
- [ ] `bun build src/npx-cli/install/setup-runtime.ts --target=node` succeeds (or whatever the project's TS check command is — confirm via `package.json#scripts`)
- [ ] Marker file format is byte-identical to smart-install.js output (write a marker, diff against a marker written by the old code)
- [ ] `grep -rn "ROOT" src/npx-cli/install/setup-runtime.ts` returns nothing — no top-level constants
**Anti-pattern guards:**
- ❌ Do not invent a `bunInstall.ts` or `uvInstall.ts` split — keep all three in one file. They share helper code (paths, version probing).
- ❌ Do not import from `plugin/scripts/smart-install.js` — it gets deleted in Phase 5.
- ❌ Do not change the marker schema. Existing readers depend on `{ version }` field.
---
## Phase 2 — Rework `src/npx-cli/commands/install.ts`
**What to implement:** Drop `needsManualInstall` gating (always run copy/register/enable for every IDE), add a new unconditional "Setting up runtime" task before `setupIDEs`, neuter the claude-code `execSync` shell-out, delete `runSmartInstall()`, and add a `runRepairCommand()` export.
**File to edit:** `src/npx-cli/commands/install.ts`
### Edit 2A — Add import for setup-runtime (top of file, after other imports)
Insert after line 10 (after the `ensureWorkerStarted` import):
```ts
import {
ensureBun,
ensureUv,
installPluginDependencies,
writeInstallMarker,
isInstallCurrent,
} from '../install/setup-runtime.js';
```
### Edit 2B — Delete `runSmartInstall()` function
**Delete lines 325345** (the entire `function runSmartInstall(): boolean { … }` block).
### Edit 2C — Drop `needsManualInstall` gating, ungate the runTasks block
**Line 589** currently reads:
```ts
const needsManualInstall = selectedIDEs.some((id) => id !== 'claude-code');
```
**Delete line 589.** Update line 593's `if (needsManualInstall) {` to just `{` (or unwrap the block — preferred). The `runTasks` block at lines 604664 now runs unconditionally.
**Within that runTasks block:** delete the "Setting up Bun and uv" task entry (lines 656663). Replace its slot with the new "Setting up runtime" task (Edit 2D).
### Edit 2D — Insert "Setting up runtime" task
Replace the deleted "Setting up Bun and uv" task (lines 656663) with:
```ts
{
title: 'Setting up runtime (first install can take ~30s)',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
const cacheDir = pluginCacheDirectory(version);
if (!isInstallCurrent(cacheDir, version)) {
message('Installing plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
}
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
```
Place this AFTER the "Installing dependencies" (npm install) task — same ordering position the deleted task occupied.
### Edit 2E — Neuter the claude-code shell-out in `setupIDEs`
**Lines 110123 currently:**
```ts
case 'claude-code': {
try {
execSync(
'claude plugin marketplace add thedotmack/claude-mem && claude plugin install claude-mem',
{ stdio: 'inherit' },
);
log.success('Claude Code: plugin installed via CLI.');
} catch (error: unknown) {
console.error('[install] Claude Code plugin install error:', );
log.error('Claude Code: plugin install failed. Is `claude` CLI on your PATH?');
failedIDEs.push(ideId);
}
break;
}
```
**Replace with:**
```ts
case 'claude-code': {
log.success('Claude Code: plugin registered (cache + settings written by npx).');
break;
}
```
The cache dir, marketplace registration, plugin registration, and `enabledPlugins` flag have all been written by the (now ungated) runTasks block before `setupIDEs` is called. `claude plugin install` was duplicating that work and triggering the silent Setup hook — both reasons to drop it.
### Edit 2F — Add `runRepairCommand()` export
After `runInstallCommand()` (after line 761), append:
```ts
export async function runRepairCommand(): Promise<void> {
const version = readPluginVersion();
const cacheDir = pluginCacheDirectory(version);
if (isInteractive) {
p.intro(pc.bgCyan(pc.black(' claude-mem repair ')));
} else {
console.log('claude-mem repair');
}
log.info(`Version: ${pc.cyan(version)}`);
await runTasks([
{
title: 'Setting up runtime',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
message('Reinstalling plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
]);
if (isInteractive) {
p.outro(pc.green('claude-mem repair complete.'));
} else {
console.log('claude-mem repair complete.');
}
}
```
`runRepairCommand` always runs the install (no `isInstallCurrent` short-circuit) — the user invoked `repair` because something is wrong, so don't gate on the marker.
**Verification checklist:**
- [ ] `grep -n "needsManualInstall" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "runSmartInstall" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "claude plugin install" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "claude plugin marketplace add" src/npx-cli/commands/install.ts` returns nothing
- [ ] `runRepairCommand` is exported and TypeScript compiles
- [ ] `runInstallCommand` still exports the same `InstallOptions` shape (Phase 3 needs it untouched)
**Anti-pattern guards:**
- ❌ Do not delete `runNpmInstallInMarketplace()` — it's still needed for the marketplace dir copy step (other IDEs use that dir).
- ❌ Do not delete `copyPluginToMarketplace()` — non-claude-code IDEs read from `marketplaceDirectory()`.
- ❌ Do not delete the `if (alreadyInstalled)` overwrite-confirm block (lines 538562) — user-facing UX preserved.
---
## Phase 3 — Wire `npx claude-mem repair`
**What to implement:** Add a `repair` case to the npx-cli command dispatcher.
**File to edit:** `src/npx-cli/index.ts`
### Edit 3A — Add `repair` case
In the `switch` block (lines 39141), copy the `install` case pattern from lines 4652 and adapt:
```ts
case 'repair': {
const { runRepairCommand } = await import('./commands/install.js');
await runRepairCommand();
break;
}
```
Place it adjacent to the `install` case for discoverability.
### Edit 3B — Help text update (if applicable)
If `src/npx-cli/index.ts` has a help/usage block (look for `case 'help':` or default case), add `repair` to the list of commands with description: `Repair claude-mem runtime (re-runs Bun/uv setup and bun install in plugin cache).`
**Verification checklist:**
- [ ] `npx claude-mem repair --help` (after build) shows the command
- [ ] `npx claude-mem repair` runs `runRepairCommand` end to end on a corrupted cache (delete `.install-version` then run; should reinstall)
- [ ] Help/usage output (if it exists) lists `repair`
**Anti-pattern guards:**
- ❌ Do not add CLI flag parsing for `repair` (no flags needed).
- ❌ Do not duplicate the `runRepairCommand` body in `index.ts` — dynamic import only.
---
## Phase 4 — Strip smart-install from hooks; add `version-check.js`
**What to implement:** Replace the Setup hook's `node smart-install.js` call with a fast version-marker check. Delete the SessionStart smart-install hook entry entirely.
### Edit 4A — Create `plugin/scripts/version-check.js`
**File to create:** `plugin/scripts/version-check.js` (new)
```js
#!/usr/bin/env node
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
function resolveRoot() {
if (process.env.CLAUDE_PLUGIN_ROOT) {
const root = process.env.CLAUDE_PLUGIN_ROOT;
if (existsSync(join(root, 'package.json'))) return root;
}
try {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const candidate = dirname(scriptDir);
if (existsSync(join(candidate, 'package.json'))) return candidate;
} catch {}
return null;
}
const ROOT = resolveRoot();
if (!ROOT) process.exit(0);
try {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const markerPath = join(ROOT, '.install-version');
if (!existsSync(markerPath)) {
console.error('claude-mem: runtime not yet set up — run: npx claude-mem repair');
process.exit(0);
}
const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
if (marker.version !== pkg.version) {
console.error(`claude-mem: upgraded to v${pkg.version} — run: npx claude-mem repair`);
}
} catch {
console.error('claude-mem: install marker unreadable — run: npx claude-mem repair');
}
process.exit(0);
```
**Behavior:**
- Sub-100ms (two synchronous file reads + JSON.parse + string compare).
- Always exits 0 (non-blocking) per the project's exit-code strategy in CLAUDE.md.
- Stderr message tells the user exactly what to run if a mismatch is detected.
### Edit 4B — Rewrite Setup hook command in `plugin/hooks/hooks.json`
**Lines 415** — replace the existing Setup hook command. Current command ends with `node "$_R/scripts/smart-install.js"`. Change it to `node "$_R/scripts/version-check.js"`. Everything before that (PATH export, `_R` resolution, cygpath) stays.
Concretely: the only change to line 11 is the trailing `smart-install.js``version-check.js`.
### Edit 4C — Delete SessionStart smart-install entry in `plugin/hooks/hooks.json`
**Lines 1740** — the SessionStart hook array currently has THREE hook entries:
1. `node "$_R/scripts/smart-install.js"` (lines 2126) — DELETE this entire entry
2. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" start` (lines 2732) — KEEP
3. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" hook claude-code context` (lines 3338) — KEEP
After edit, the SessionStart `hooks` array has 2 entries instead of 3.
**Verification checklist:**
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.Setup[0].hooks[0].command' | grep version-check.js` succeeds
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.SessionStart[0].hooks | length'` returns `2`
- [ ] `grep -c "smart-install" plugin/hooks/hooks.json` returns `0`
- [ ] `node plugin/scripts/version-check.js` exits 0 in <500ms (time it)
- [ ] On a fresh checkout (no `.install-version` marker), version-check stderr says "run: npx claude-mem repair"
**Anti-pattern guards:**
- ❌ Do not change the exit code from 0 — Windows Terminal tab management depends on it (CLAUDE.md exit-code strategy).
- ❌ Do not call out to Bun in version-check.js — Node-only, since this runs before we know Bun exists.
- ❌ Do not add fancy logic (semver compare, partial recovery). String equality is correct: any version mismatch warrants a repair.
---
## Phase 5 — Delete dead code
**What to implement:** Delete smart-install source files and update tests.
### Edit 5A — Delete files
```
rm scripts/smart-install.js
rm plugin/scripts/smart-install.js
rm tests/smart-install.test.ts
```
### Edit 5B — Trim `tests/plugin-scripts-line-endings.test.ts`
**Line 12 (the `SHEBANG_SCRIPTS` array):** remove the `'smart-install.js'` entry. Keep the rest of the array intact.
If the array becomes empty after the removal, also remove the entry — but per discovery report it has multiple entries, so just delete the one line.
### Edit 5C — Add new test for setup-runtime module (optional but recommended)
**File to create:** `tests/setup-runtime.test.ts`
Cover:
- `readInstallMarker` returns `null` for missing file
- `writeInstallMarker` produces a JSON object matching the smart-install.js schema (`{ version, bun, uv, installedAt }`)
- `isInstallCurrent` returns `false` for missing marker, `false` for version mismatch, `true` for match
- (Skip Bun/uv install integration tests — those need a sandbox and fall outside this PR's scope.)
If you skip this, document why in the PR description.
**Verification checklist:**
- [ ] `find . -name "smart-install*" -not -path "*/node_modules/*"` returns no results
- [ ] `grep -rn "smart-install" tests/` returns no results
- [ ] `npm test` (or whatever the project uses) passes
- [ ] If `tests/setup-runtime.test.ts` was added, it passes
**Anti-pattern guards:**
- ❌ Do not delete `tests/plugin-scripts-line-endings.test.ts` entirely — it tests other scripts too.
- ❌ Do not delete `tests/bun-runner.test.ts` — bun-runner.js stays in this PR.
---
## Phase 6 — Update docs
**What to implement:** Sweep documentation to reflect the new install flow.
### Edit 6A — `docs/architecture-overview.md:36`
Update reference to smart-install. New copy: "On first install, `npx claude-mem install` sets up Bun and uv globally and runs `bun install` in the plugin cache. The Setup hook then runs a sub-100ms version check on every Claude Code startup; if the plugin was upgraded externally, the user is prompted to run `npx claude-mem repair`."
### Edit 6B — `docs/public/configuration.mdx:139,163` and `docs/public/development.mdx:42`
Replace any mention of smart-install behavior with the version-check + repair model. Two-sentence patches; preserve surrounding context.
### Edit 6C — `docs/public/hooks-architecture.mdx` (11 references)
This is the largest doc change. Walk each reference (lines 77, 103, 119, 127, 432, 695696, 703 per discovery report). Update text describing the Setup hook to say it runs `version-check.js` (sub-100ms) instead of `smart-install.js`. Update SessionStart description to reflect 2 entries (worker start + context fetch) instead of 3.
### Edit 6D — `docs/public/architecture/` references (lines 149, 193)
Same pattern — replace smart-install lifecycle description with the npx-installer + version-check model.
### Edit 6E — Skip CHANGELOG
CLAUDE.md says: "No need to edit the changelog ever, it's generated automatically." Don't touch it.
### Edit 6F — Skip docs/reports/
Those are historical incident reports. Do not retroactively edit them — they describe past behavior.
**Verification checklist:**
- [ ] `grep -rn "smart-install" docs/public/` returns no results
- [ ] `grep -rn "smart-install" docs/architecture-overview.md` returns no results
- [ ] (Optional) Render docs locally via Mintlify dev server and visually scan the architecture page
**Anti-pattern guards:**
- ❌ Do not edit `docs/reports/*.md` — those are dated incident reports, leave them alone.
- ❌ Do not edit CHANGELOG.md.
---
## Phase 7 — Build, test, manual verify
**What to implement:** End-to-end validation. This phase is run by the implementer before opening the PR.
### Edit 7A — Build
```bash
npm run build-and-sync
```
This must succeed. If TypeScript fails on the new `setup-runtime.ts`, fix in place.
### Edit 7B — Test suite
```bash
npm test
```
Must be green. Likely failures to anticipate:
- `plugin-scripts-line-endings.test.ts` if the `'smart-install.js'` entry was missed in Phase 5
- Any test that imports from `scripts/smart-install.js` (discovery report says only `tests/smart-install.test.ts`, which Phase 5 deletes)
### Edit 7C — Manual fresh-install verification
1. On a clean machine (or after `rm -rf ~/.claude/plugins/marketplaces/thedotmack ~/.claude/plugins/cache/thedotmack ~/.claude-mem`):
```bash
npx claude-mem install
```
Confirm:
- Spinner says "Setting up runtime (first install can take ~30s)"
- No silent dead air
- Worker starts at the end
2. Open Claude Code in any project. Confirm:
- Setup hook fires fast (<200ms total)
- SessionStart fires fast (no smart-install delay)
- No "claude plugin install" output
3. Simulate a stale install:
```bash
rm ~/.claude/plugins/cache/thedotmack/claude-mem/<version>/.install-version
```
Open a new Claude Code session. Confirm version-check.js prints the "run: npx claude-mem repair" message to stderr.
4. Run repair:
```bash
npx claude-mem repair
```
Confirm spinner runs through Bun/uv check + bun install + marker write, then exits clean.
### Edit 7D — Commit and open PR
Per the PR creation flow in the user's outer task. Don't auto-merge; the user wants a review loop.
**Verification checklist:**
- [ ] `npm run build-and-sync` exits 0
- [ ] `npm test` exits 0
- [ ] Manual fresh install completes with visible spinner, no silent dead air
- [ ] Setup hook fires <200ms after rebuild
- [ ] `npx claude-mem repair` runs end-to-end
**Anti-pattern guards:**
- ❌ Do not skip the manual verification — the whole point of this PR is UX (eliminating dead air). Type checks won't catch a regression.
- ❌ Do not bump the version — version bump is handled separately by the version-bump skill.
---
## Summary of file changes
| Type | Path | Phase |
|---|---|---|
| Created | `src/npx-cli/install/setup-runtime.ts` | 1 |
| Edited | `src/npx-cli/commands/install.ts` | 2 |
| Edited | `src/npx-cli/index.ts` | 3 |
| Created | `plugin/scripts/version-check.js` | 4 |
| Edited | `plugin/hooks/hooks.json` | 4 |
| Deleted | `scripts/smart-install.js` | 5 |
| Deleted | `plugin/scripts/smart-install.js` | 5 |
| Deleted | `tests/smart-install.test.ts` | 5 |
| Edited | `tests/plugin-scripts-line-endings.test.ts` | 5 |
| Created | `tests/setup-runtime.test.ts` (optional) | 5 |
| Edited | `docs/architecture-overview.md` | 6 |
| Edited | `docs/public/configuration.mdx` | 6 |
| Edited | `docs/public/development.mdx` | 6 |
| Edited | `docs/public/hooks-architecture.mdx` | 6 |
| Edited | `docs/public/architecture/*.md` | 6 |
Estimated diff: **+250 / 500 lines** (net deletion).
+367
View File
@@ -0,0 +1,367 @@
# Onboarding UX Overhaul
Three surfaces, one product voice, one first-success moment. Each phase is self-contained and can be executed in a fresh chat with `/do`.
## North Star
Pull the user toward this single moment: **open the viewer in a browser, do anything in Claude Code, watch an observation appear within seconds.** All three surfaces aim at it from different angles.
## Cross-Cutting Facts (read this first, every phase)
- **Test runner:** `bun test`. Test command: `npm run test`. Tests live in `tests/`. Pattern templates: `tests/sqlite/observations.test.ts:1-60` (in-memory SQLite + bun:test), `tests/install-non-tty.test.ts:1-95` (regex assertions over install.ts source).
- **Build:** `npm run build-and-sync` runs full build (banner frames + plugin manifests + `scripts/build-hooks.js`) → marketplace sync → worker restart. Viewer compiles via esbuild to `plugin/ui/viewer-bundle.js`; HTML template (which holds ALL CSS) at `src/ui/viewer-template.html`.
- **Settings defaults:** `src/shared/SettingsDefaultsManager.ts:70-131`. Merge logic at `loadFromFile()` lines 161-205 — missing keys auto-pick up new defaults, explicit values are respected. Forward-compatible.
- **`CLAUDE_MEM_WELCOME_HINT_ENABLED` already defaults to `'true'`** (`SettingsDefaultsManager.ts:104`). Single reader at `SearchRoutes.ts:294`. Goal 5 from the brief is already done — we replace "flip the default" with "pin it with a regression test."
- **Timing line, identical wording everywhere:** `Memory injection starts on your second session in a project.`
- **Privacy line, identical wording everywhere:** `Everything stays in ~/.claude-mem on this machine.`
---
## Phase 0 — Documentation Discovery (DONE; for reference)
Discovery already completed. Allowed APIs and signatures established:
### Install.ts patterns (`src/npx-cli/commands/install.ts`)
- `log` helper at lines 41-46 — methods `info | success | warn | error`, conditionally routes to `p.log.*` (interactive) vs `console.log/warn/error` (non-interactive, 2-space indent).
- `p` is `* as p from '@clack/prompts'`. Used: `p.note(body, title)`, `p.outro(msg)`, `p.intro`, `p.log.*`, `p.tasks`, `p.spinner`, `p.select/multiselect/confirm/password`, `p.isCancel`, `p.cancel`.
- `pc` is `picocolors` default import. Available: `pc.cyan/green/yellow/red/bold/underline/dim/bgCyan/black`. **`pc.dim` exists** (already in use at line 663).
- `getSetting('CLAUDE_MEM_WORKER_PORT')` returns string; convert with `Number()` when needed.
- Health probe pattern at lines 843-864: `fetch('http://127.0.0.1:${port}/api/health', { signal: AbortSignal.timeout(3000) })`, non-throwing.
- Existing `summaryLines` block (826-841) and `nextSteps` block (866-896) — both have parallel interactive (`p.note`) and non-interactive (`console.log`) branches.
### Settings (`src/shared/SettingsDefaultsManager.ts`)
- `CLAUDE_MEM_WELCOME_HINT_ENABLED: 'true'` at line 104.
- Merge: defaults first, then file overrides, then env overrides (lines 194-201).
- Install does NOT pre-seed this key — only seeds prompted settings (provider, model). Existing users without explicit value automatically get the new default.
- `SettingsRoutes.ts:84-117` — flag is NOT in the user-updatable allowlist (read-only via UI).
- Test template: `tests/install-non-tty.test.ts` (regex over source); SettingsDefaultsManager has no dedicated test file — would be created if needed.
### SessionStart hint (`src/services/worker/http/routes/SearchRoutes.ts`)
- `WELCOME_HINT_TEMPLATE` at lines 14-27. Used at line 301: `WELCOME_HINT_TEMPLATE.replace('{viewer_url}', viewerUrl)`.
- Gating logic at lines 293-306. Only fires when `hintEnabled && !full && observationCount === 0`.
- Output is plain text injected as SessionStart `additionalContext` via the SessionStart hook (`src/cli/handlers/context.ts`).
### Viewer (`src/ui/viewer/`)
- `useSSE()` at `src/ui/viewer/hooks/useSSE.ts:1-148` exposes `{ observations, summaries, prompts, projects, sources, projectsBySource, isProcessing, queueDepth, isConnected }`. Auto-reconnects; new observations prepended via `'new_observation'` SSE event.
- `WelcomeCard` mounted in `src/ui/viewer/App.tsx:128-130`, currently receives only `onDismiss`. App has access to all SSE state (lines 51-67).
- All viewer CSS lives in `src/ui/viewer-template.html`; existing `.welcome-card*` styles at lines 1443-1561; existing `.status-dot` + `@keyframes pulse` at lines 754-764.
- Stats endpoints: `/api/stats` (`DataRoutes.ts:204-242`) returns `{database: {observations, sessions, summaries}, worker: {...}}`. `/api/projects` (`DataRoutes.ts:244-260`) returns `ProjectCatalog`. **No `firstObservationAt` field currently — Phase 4 adds it.**
- `/api/how-it-works` is NOT a static explainer — it queries observations tagged with the `'how-it-works'` concept (`SearchManager.ts:836-884`). Useless on a fresh install. Phase 1 adds a true static explainer.
### Skills (`plugin/skills/`)
- 9 existing skills, each a directory with `SKILL.md` and YAML frontmatter (`name`, `description`). No central registry — discovered by directory convention.
- Template to copy: `plugin/skills/mem-search/SKILL.md`.
### Anti-patterns to avoid
- DO NOT call `/api/how-it-works` for an onboarding explainer — wrong endpoint.
- DO NOT add new viewer CSS files — all styles in `src/ui/viewer-template.html`.
- DO NOT add new viewer routes for stats unless strictly needed — extend `/api/stats` instead.
- DO NOT seed `CLAUDE_MEM_WELCOME_HINT_ENABLED` in `install.ts` — defaults already handle it.
- DO NOT pass imperatives ("you should run X") in the SessionStart hint — Claude will try to execute. Use third-person narration ("`/learn-codebase` is available if…").
---
## Phase 1 — Canonical Onboarding Explainer
**Why:** All three surfaces need a single source of truth for the 90-second "what is this" explainer. `/api/how-it-works` does not serve this purpose. We'll create a real static explainer and link to it from everywhere.
### Tasks
1. Create `src/services/worker/onboarding-explainer.md` — single canonical content. ~150 words, three sections:
- **What it does:** Every Read/Edit/Bash Claude makes turns into a compressed observation. Observations get summarized at session end. Relevant ones get auto-injected into future prompts.
- **When it kicks in:** Memory injection starts on your second session in a project. *(verbatim timing line)*
- **Where data lives:** Everything stays in ~/.claude-mem on this machine. *(verbatim privacy line)*
2. Add new route `GET /api/onboarding/explainer` in `src/services/worker/http/routes/SearchRoutes.ts`:
- Read the markdown file at boot (cache like `cachedSkillMd` pattern in `Server.ts:18-33`).
- Serve as `text/markdown; charset=utf-8`.
- Register in `setupRoutes()` next to the other `/api/context/*` routes.
3. Create `plugin/skills/how-it-works/SKILL.md`:
- Copy frontmatter shape from `plugin/skills/mem-search/SKILL.md:1-4`.
- `name: how-it-works`
- `description: Explain how claude-mem captures observations, when memory injection kicks in, and where data lives. Use when the user asks "how does claude-mem work?" or "what is this thing doing?".`
- Body: same content as the markdown explainer (or fetch `/api/onboarding/explainer` at runtime).
- Wire into `scripts/build-hooks.js` verification list (lines 336-348) so build fails if the file is missing.
### Verification
- `npm run build-and-sync` succeeds; new SKILL.md present in `plugin/skills/how-it-works/`.
- `curl http://localhost:$PORT/api/onboarding/explainer` returns the markdown.
- Worker boot log includes a "Cached onboarding explainer at boot" entry (mirroring the SKILL.md cache log).
### Anti-pattern guards
- Do NOT alter `/api/how-it-works`. It serves a different (concept-tagged search) purpose.
- Do NOT inline the explainer text into install.ts / WelcomeCard / WELCOME_HINT_TEMPLATE — link, don't duplicate.
---
## Phase 2 — SessionStart Welcome Hint Rewrite
**Why:** Current copy reads as a marketing intercept inside Claude's context, leads with imperatives Claude tries to execute, and doesn't set the truthful "today seeds, tomorrow injects" expectation.
### Tasks
1. Rewrite `WELCOME_HINT_TEMPLATE` at `src/services/worker/http/routes/SearchRoutes.ts:14-27`. Target:
```
# claude-mem status
This project has no memory yet. The current session will seed it; subsequent sessions will receive auto-injected context for relevant past work.
Memory injection starts on your second session in a project.
`/learn-codebase` is available if the user wants to front-load the entire repo into memory in a single pass (~5 minutes on a typical repo, optional). Otherwise memory builds passively as work happens.
Live activity: {viewer_url}
How it works: `/how-it-works`
This message disappears once the first observation lands.
```
Constraints: third-person narration referring to "the user", not imperatives directed at Claude. Title is "status", not "Welcome".
2. **Pin the default with a test.** In a new file `tests/shared/welcome-hint-default.test.ts`:
- Assert `SettingsDefaultsManager.getAllDefaults().CLAUDE_MEM_WELCOME_HINT_ENABLED === 'true'`.
- Assert that an empty settings file resolves to `'true'`.
- Assert that an explicit `'false'` is preserved through `loadFromFile`.
3. No install.ts seeding change — defaults already flow through.
4. Audit existing welcome-hint tests (memory note: "4/4 tests pass"). Likely in `tests/worker/SearchManager.timeline-anchor.test.ts` per discovery; if those tests assert the old template body verbatim, update them to match the new copy. If they only assert the gating logic, leave alone.
### Verification
- `bun test tests/shared/welcome-hint-default.test.ts` passes.
- `bun test tests/worker/` (or whichever file holds the welcome-hint tests) passes.
- Manual: in a fresh project with zero observations, start a Claude Code session — SessionStart context includes the new status note. New text contains the verbatim timing line and points at `{viewer_url}` and `/how-it-works`.
- Manual: in a project with observations, the hint does NOT appear (gating still works).
### Anti-pattern guards
- Do NOT use the word "Welcome" or any second-person imperatives ("you should…", "go to…"). Claude will try to "help" by executing them.
- Do NOT exceed ~10 lines — this is injected into Claude's context for every fresh-project session.
---
## Phase 3 — Post-Install Next Steps Rewrite
**Why:** Current 4-bullet menu treats `/learn-codebase`, `/mem-search`, and `/knowledge-agent` as parallel options. They aren't — `/learn-codebase` is the only first-session move and even it's optional. Lead with proof (live viewer), give two paths, defuse the privacy concern.
### Tasks
1. Replace the `nextSteps` array at `src/npx-cli/commands/install.ts:866-878`. Target body when worker is ready:
```
${pc.green('✓')} Worker running at ${pc.underline(`http://localhost:${actualPort}`)}
${pc.bold('First success:')} keep that URL open in a browser, then open Claude Code in any project. Observations stream in as Claude reads, edits, and runs commands.
${pc.bold('Two paths from here:')}
${pc.cyan('A.')} Just start working. Memory builds passively from your first prompt. (Recommended.)
${pc.cyan('B.')} Front-load it: open Claude Code and run ${pc.bold('/learn-codebase')} to ingest the whole repo (~5 min, optional).
Memory injection starts on your second session in a project.
Everything stays in ${pc.cyan('~/.claude-mem')} on this machine.
${pc.dim('How it works: /how-it-works · Disable first-session hint: CLAUDE_MEM_WELCOME_HINT_ENABLED=false')}
${pc.dim('Note: close all Claude Code sessions before uninstalling, or ~/.claude-mem will be recreated by active hooks.')}
```
Worker-not-ready branch: keep the existing `pc.yellow('!')` warning + retry hint, then append the same "First success" / "Two paths" / timing / privacy lines (substituting `workerPort` for `actualPort`).
2. Drop `/mem-search` and `/knowledge-agent` lines from this surface entirely. (They reappear in WelcomeCard for users who do open the viewer.)
3. Keep both `isInteractive` (uses `p.note(nextSteps.join('\n'), 'Next Steps')`) and non-interactive (`console.log` per line, 2-space indent) branches in sync. The array shape stays the same — only the strings change.
4. Verify `pc.dim` renders correctly under the clack `p.note` box (it does — line 663 already uses it).
### Verification
- `npm run build` succeeds.
- Manual interactive run: `npx claude-mem install` in a fresh dir shows the new Next Steps block inside the clack box.
- Manual non-interactive run: `CI=true npx claude-mem install` (or pipe through cat) shows the same content with 2-space indent and no clack boxes.
- Update `tests/install-non-tty.test.ts` regex assertions to match the new strings (existing pattern: `expect(installSource).toContain(...)`).
### Anti-pattern guards
- Do NOT add new commands to this surface. The point is reduction.
- Do NOT lose the uninstall caveat — demote, don't delete.
- Do NOT reorder so the worker URL becomes a footnote — it's the single most important payload here.
---
## Phase 4 — Extend `/api/stats` with `firstObservationAt`
**Why:** The viewer micro-stat row (Phase 5) needs a "since [date]" value. No HTTP endpoint currently exposes the earliest observation timestamp. Smallest possible backend change to enable Phase 5.
### Tasks
1. Add a `firstObservationAt: string | null` field to the stats response in `src/services/worker/http/routes/DataRoutes.ts:204-242` (`handleGetStats`).
2. Add a SQL helper next to `getRecentObservations` (`src/services/sqlite/observations/recent.ts:6-20`):
```ts
export function getFirstObservationCreatedAt(db: SessionStore): string | null {
// SELECT created_at FROM observations ORDER BY created_at_epoch ASC LIMIT 1
}
```
Match the existing prepared-statement pattern in that directory.
3. Wire the helper into `handleGetStats` and surface as ISO string (or `null` if no observations). Verify the existing TypeScript type for the stats response is updated.
### Verification
- `bun test tests/sqlite/observations.test.ts` still passes.
- New unit test in `tests/sqlite/observations.test.ts` (or a new file) covering `getFirstObservationCreatedAt` for empty + non-empty DB.
- `curl http://localhost:$PORT/api/stats` returns the new field.
### Anti-pattern guards
- Do NOT add new endpoints — extend the existing `/api/stats` payload.
- Do NOT add per-project earliest-timestamp logic; the viewer stat row is global ("X observations · Y projects · since [date]").
---
## Phase 5 — Viewer WelcomeCard Rewrite
**Why:** Current card is generic and doesn't differentiate the empty state (the moment the user is asking "is anything happening?") from the data state (the moment the user is asking "what can I do here?").
### Tasks
1. **App.tsx wiring** (`src/ui/viewer/App.tsx:128-130`). Pass new props to `WelcomeCard`:
```tsx
<WelcomeCard
onDismiss={...}
observationCount={allObservations.length}
projectCount={projects.length}
isConnected={isConnected}
firstObservationAt={stats.firstObservationAt} // new — fetched from /api/stats
/>
```
If a stats fetch hook doesn't already exist, add one (`useStats()` at `src/ui/viewer/hooks/useStats.ts`) that polls `/api/stats` on mount and on each new SSE observation.
2. **WelcomeCard.tsx rewrite** (`src/ui/viewer/components/WelcomeCard.tsx`):
- Bump localStorage key to `claude-mem-welcome-dismissed-v2` (keep helpers in same file). v1 dismissals should NOT carry over — the card is meaningfully different.
- Branch on `observationCount === 0`:
- **Empty state:**
- Headline: "No observations yet."
- Body: "Open Claude Code in any project — entries stream in here as Claude reads, edits, and runs commands."
- Live status row with a `<span class="welcome-card-status-dot" data-connected={isConnected ? 'true' : 'false'} />` and label "Connected to worker · waiting for activity" / "Reconnecting…" based on `isConnected`.
- Footer: "How it works" link + dismiss button (existing behavior).
- **Has-data state:**
- Headline: "claude-mem"
- Body: "Persistent memory across Claude Code sessions."
- Stat row: `${observationCount} observations · ${projectCount} projects · since ${formatDate(firstObservationAt)}`.
- Two example prompts (cut from four):
- `<code>ask:</code> did we already solve X?`
- `<code>/mem-search</code> dig into past work`
- Footer: "How it works" + "Read the docs" links + dismiss.
- "How it works" link points to `/api/onboarding/explainer` (opens in new tab as raw markdown — acceptable for v1; or a small modal showing the markdown rendered).
3. **CSS additions** in `src/ui/viewer-template.html` next to `.welcome-card-*` styles (lines 1443-1561). Reuse existing `@keyframes pulse` (line 754). Add:
- `.welcome-card-status-dot` (8×8 circle, error color + pulse when disconnected, success color + no animation when `data-connected="true"`).
- `.welcome-card-stats` (single-row, dim text, dot separators using `·`).
- `.welcome-card-empty` adjustments (slightly larger lede, status row layout).
4. **Auto-dismiss on first observation:** in App.tsx, add an effect that flips the card from empty→has-data view automatically when `observationCount` crosses 0→1. The card should NOT auto-dismiss permanently on the first observation — it just transitions states. The user explicitly dismisses with the X.
### Verification
- `npm run build-and-sync` succeeds; viewer bundle rebuilds.
- Open viewer in a fresh-install state: empty card shows, dot animates (or is solid green if connected).
- In Claude Code, do one Read in a project. The viewer card flips to has-data state without a manual refresh, stat row populates.
- Dismiss persists across reload (localStorage v2 key).
- Header "Show help" button still re-opens the card.
- Tests: a small unit test for `getStoredWelcomeDismissed` / `setStoredWelcomeDismissed` against the new v2 key (extend the helper logic — pure functions are easy to test even without React Testing Library).
### Anti-pattern guards
- Do NOT add a new CSS file. All styles in `viewer-template.html`.
- Do NOT poll `/api/stats` on every render — once on mount + on `'new_observation'` SSE event is enough.
- Do NOT auto-permanently-dismiss on first observation; users may want to keep the card visible for the example prompts.
- Do NOT inline the explainer text into the card — link to `/api/onboarding/explainer`.
---
## Phase 6 — Drift Audit
**Why:** Three surfaces, two verbatim lines, one explainer source. Catch any divergence before it ships.
### Tasks
1. Grep for the timing line and assert it appears verbatim in:
- `src/services/worker/http/routes/SearchRoutes.ts` (Phase 2)
- `src/npx-cli/commands/install.ts` (Phase 3)
- `src/services/worker/onboarding-explainer.md` (Phase 1)
```bash
grep -rn "Memory injection starts on your second session in a project" src/ plugin/
# expect 3+ matches
```
2. Same for the privacy line (`Everything stays in ~/.claude-mem on this machine.`).
3. Confirm `/how-it-works` slash reference appears in install.ts and SearchRoutes.ts; SKILL.md exists at `plugin/skills/how-it-works/SKILL.md`.
4. Confirm WelcomeCard does NOT inline the explainer body — only the link.
5. Confirm no surface still says "/knowledge-agent" or "/mem-search" in install.ts post-install copy.
### Verification
- All grep checks pass.
- Manual: read all three surfaces side by side. Each is distinctly framed (install = two paths + first-success; SessionStart = third-person status; viewer = empty/has-data with live dot). The two verbatim lines and the `/how-it-works` link are the only repeated content.
---
## Phase 7 — End-to-End Smoke Test (manual)
**Why:** The acceptance criterion in the brief is a single coherent flow. This phase walks it.
### Steps
1. Fresh install:
```bash
rm -rf ~/.claude-mem
npx claude-mem install
```
Verify: install Next Steps shows the new "Two paths" + first-success + timing + privacy + `/how-it-works` block.
2. Open the viewer at the printed URL. Verify: empty state shows, dot is green (connected) or red+pulsing (disconnected briefly).
3. Open Claude Code in any project. Type a prompt that causes one Read.
- Verify in Claude Code: SessionStart context contains the new status note, NOT a "Welcome" block. Claude does not act on the bullets — at most relays them.
- Verify in viewer: card flips to has-data state, stat row populates, observation appears in the feed.
4. End the session. Start a second Claude Code session in the same project.
- Verify: SessionStart context this time contains injected past observations (not the welcome hint, since `observationCount > 0`).
5. Click the "How it works" link from the viewer card. Verify: it loads `/api/onboarding/explainer` markdown.
### Verification
All four observable beats in the acceptance criterion happen as described, in order, without any surface contradicting another on facts (timing, privacy, command names).
---
## Execution Order Summary
1. **Phase 1** (explainer + skill) — unblocks everything else by establishing the canonical content source.
2. **Phase 4** (`/api/stats` extension) — unblocks Phase 5; tiny isolated backend change, do it early.
3. **Phase 2** (SessionStart hint rewrite + default-pinning test) — independent, do in parallel with Phase 3.
4. **Phase 3** (install.ts Next Steps rewrite) — independent of Phase 2.
5. **Phase 5** (WelcomeCard rewrite) — depends on Phases 1 + 4.
6. **Phase 6** (drift audit) — runs after all copy changes land.
7. **Phase 7** (manual smoke) — final gate before commit/PR.
Phases 2, 3, and 4 are independent and could be parallelized in three short sessions if desired.
## Out of Scope (do not touch)
- Splash banner / installer animation work that just shipped on this branch.
- `/learn-codebase`, `/mem-search`, `/knowledge-agent` skill internals — only how we reference them.
- New viewer pages or routes beyond `/api/onboarding/explainer` and the `firstObservationAt` field on `/api/stats`.
- Public docs in `docs/public/` — covered by the existing Mintlify deploy; only update if a doc page directly contradicts the new copy.