# Hook Runtime Round 14 Plan

## Summary

Round 14 turns `spool` from a passive MCP service into an active memory
runtime by injecting itself into the AI client's hook chain. The work is
sequenced in five sub-rounds (R1–R5), each independently shippable.

The driving observation: spool already had retrieval, ledger, vault
writeback, and a Tauri facade, but users were not invoking any of it
during day-to-day sessions. Wakeup packets came back empty. Decisions
and incidents never made it to the ledger. The gap was not a missing
feature — it was a missing run-time integration. Round 14 closes that
gap by borrowing Trellis's *configurator* + template-distribution model
and re-targeting it at memory infrastructure rather than project rules.

The PRD that scopes this round lives at
`.trellis/tasks/05-04-hook-runtime/prd.md` (MVP-γ).

## Sub-Round Sequencing

| Round | Range | Status |
|-------|-------|--------|
| R1 | Installer skeleton + registration | ✅ Done |
| R2 | Hook suite + template distribution | ✅ Done |
| R3 | Tier 1 heuristics + transcript parsing | ✅ Done |
| R4 | MCP sampling reverse-call (Tier 2) | ✅ Done |
| R5 | Doctor + documentation closeout | ✅ Done (this doc) |

### R1 — Installer skeleton + registration

- `src/installers/{mod,shared,claude,templates}.rs` introduces the
  `Installer` trait, the `mcpServers` JSON merge primitive, and the
  Claude-specific configurator.
- `src/cli/args.rs` grows `spool mcp install / uninstall / doctor`
  subcommands with text/json output formats.
- `spool mcp install --client claude` end-to-end: builds the binary,
  drops it onto a stable absolute path, merges `mcpServers.spool` into
  Claude Code's config without disturbing other entries, snapshots a
  backup before each write.
- Uninstall reverses the same set of writes deterministically.

User-visible result: registration is stable. The "binary path drift"
class of failures from `target/debug/spool-mcp` cannot recur.

### R2 — Hook suite + template distribution

- `src/hook_runtime/` adds five hook entry points:
  `session_start`, `user_prompt`, `post_tool_use`, `stop`, `pre_compact`.
- `src/installers/templates/claude/` holds the embedded
  `commands/`, `hooks/`, and `skills/` bundles distributed alongside
  binary registration. Template resolution lives in
  `src/installers/templates.rs` (`include_str!` so the binary stays
  self-contained).
- `spool hook <which>` is a CLI fan-out for the same hooks, used by
  the shipped shell scripts.
- Trellis-aware degradation: when `.trellis/.developer` is present,
  the session-start hook emits a compact wakeup so the two systems do
  not stomp on each other.

User-visible result: any new Claude Code session starts with a
populated wakeup packet. `/spool:capture <text>` is available without
the user having to remember the underlying tool surface.

### R3 — Tier 1 heuristics + transcript parsing

- `src/distill/heuristic/{self_tag,extraction}.rs` split signals into
  two writes: explicit self-tags become `accepted`; pattern-derived
  candidates become `candidate`.
- `src/distill/transcript.rs` parses the Claude Code session JSONL
  (`~/.claude/projects/<sanitized-cwd>/<sid>.jsonl`).
- `src/distill/redact.rs` drops obvious secrets (API keys, bearer
  tokens) before any signal is persisted.
- `src/distill_queue.rs` provides the `.spool/distill-pending.queue`
  file format that `post_tool_use` writes into and `stop` drains.

User-visible result: ending a session produces ledger rows
automatically. Self-tagged content is wakeup-eligible at once;
heuristic candidates queue for review.

### R4 — MCP sampling reverse-call

R4 is split into two sub-phases by commit:

1. **R4a (tracked separately)** — synchronous Tier 1 path inside
   `memory_distill_pending`, queue draining, dedupe, and redact.
2. **R4b (this round's main work)** — reverse `sampling/createMessage`
   support so the running AI client provides the LLM. The pipeline
   tries sampling first, records `fallback_used` + `sampling_attempted`,
   and always falls through to Tier 1 underneath.

R4b implementation notes:

- `src/distill/pipeline.rs` exposes a `SamplingClient` trait with an
  async `create_message`. `NoopSamplingClient` covers CLI-only
  contexts; `McpSamplingClient` handles the live MCP path.
- `src/mcp.rs` is rebuilt around three threads:
  - **reader** (OS thread, blocking `stdin`) parses each JSON line. If
    it is a *response* envelope (no `method`), it is routed through
    `StdSamplingChannel::pending` to the awaiting oneshot. Otherwise
    it is forwarded to the dispatcher.
  - **dispatcher** (main thread) handles tool calls inline. The
    `memory_distill_pending` handler enters a `current_thread` tokio
    runtime and `block_on`s the async pipeline. Sampling reverse-calls
    inside that `block_on` are fulfilled by the reader thread, which
    is *not* owned by the runtime, so the reader can keep delivering
    while we await.
  - **writer** (OS thread) drains a single `mpsc<Value>` channel to
    `stdout`. All outbound writes funnel through it, so dispatcher
    output and reverse-call requests never interleave bytes.
- The harness in `tests/mcp_smoke.rs` learned to mock the reverse-call
  via `call_tool_with_sampling`, with two new e2e cases: success
  (candidates persisted) and rejection (graceful Tier 1 fallback).

R4b shutdown bug fixed in this round: every `out_tx` clone the main
thread holds — including the one inside `sampling_channel` — must be
dropped before joining the writer, otherwise the writer's
`out_rx.recv()` never observes EOF and the child cannot exit when
stdin closes. That symptom looked like an stdio deadlock but was an
ownership/drop-order issue.

User-visible result: sampling-capable clients (Claude Code) get
LLM-grade extraction; non-capable clients silently fall through to
Tier 1.

### R5 — Doctor + documentation closeout

- `spool mcp doctor --client claude` emits seven check rows:
  `claude_config_exists`, `mcp_servers_spool_registered`,
  `spool_mcp_binary`, `spool_config_readable`,
  `claude_settings_hooks_registered`, `spool_hook_scripts`,
  `spool_skill_present`. Status uses an `Ok / Warn / Fail /
  NotApplicable` four-state system so partial installs surface as
  warnings rather than errors.
- ADR-0003 documents the sampling decision
  (`docs/adr/0003-mcp-sampling-async-distill.md`).
- `README.md` gains a "Quick start: Claude Code" section so a fresh
  user can wire spool in three commands.
- Backend spec docs are resynced:
  `.trellis/spec/backend/directory-structure.md` lists the new
  `hook_runtime/`, `distill/`, `installers/` modules;
  `.trellis/spec/backend/error-handling.md` documents the
  hook-failure-silent rule (D7 in the PRD).

## Test Plan

- `cargo test -q` (lib, all 350+ unit tests)
- `cargo test --test mcp_smoke` (10 cases including the two new
  sampling-aware e2e cases)
- `cargo test --test cli_smoke` (regression of the install/uninstall/
  doctor and lifecycle CLI surface)
- `cargo test --test daemon_smoke`
- `cargo check -q --bin spool --bin spool-mcp --bin spool-daemon`

## Assumptions

- Only Claude Code is wired in this round. Codex / Cursor / OpenCode /
  Pi / Gemini / Windsurf are deferred to round 14.B (Installer trait
  is already general enough to make those a copy + edit job).
- MCP `sampling/createMessage` is best-effort. Round 14 ships the
  Tier 1 fallback so unsupported clients still see a working pipeline.
- Hooks remain fork-exec for now. A daemon-mode hook entry can replace
  the CLI fan-out later if cold-start cost becomes painful; Round 14
  did not need it.
- Promotion (`candidate → canonical`) stays a manual user action.
  Auto-promotion is out of scope and would need its own ADR.

## Out of Scope (carry forward)

- Multi-client installers (#7.B)
- GUI installer flow
- Phase 4 retrieval intelligence (semantic, contradiction, confidence)
- Cross-device ledger sync (still user-managed via git/syncthing)
- Team / shared ledger (violates ADR-0001 single-user invariant)

## Completion Status

Last checked: `2026-05-07`
