@jeffober/thatch 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +56 -26
  2. package/artifacts/skills/common.md +1 -0
  3. package/artifacts/skills/thatch-clear-writing.md +103 -5
  4. package/artifacts/skills/thatch-code-archaeology.md +9 -0
  5. package/artifacts/skills/thatch-code-review.md +16 -8
  6. package/artifacts/skills/thatch-coordination.md +69 -0
  7. package/artifacts/skills/thatch-plan-refinement.md +33 -4
  8. package/artifacts/skills/thatch-pr-description.md +21 -13
  9. package/artifacts/skills/thatch-refine-personal.md +5 -0
  10. package/artifacts/skills/thatch-refine.md +7 -1
  11. package/artifacts/skills/thatch-review-context.md +55 -2
  12. package/artifacts/skills/thatch-review-followup.md +4 -4
  13. package/artifacts/skills/thatch-review-no-slop.md +15 -0
  14. package/artifacts/skills/thatch-review-response.md +1 -1
  15. package/artifacts/skills/thatch-review-synthesizer.md +21 -1
  16. package/bin/thatch +48 -15
  17. package/docs/assets/deepwiki-badge.svg +1 -0
  18. package/docs/dev/README.md +18 -10
  19. package/docs/dev/features/README.md +1 -1
  20. package/docs/dev/features/behavior-engine.md +10 -0
  21. package/docs/dev/features/cli.md +4 -1
  22. package/docs/dev/features/commands.md +40 -9
  23. package/docs/dev/features/cross-session-chat.md +194 -23
  24. package/docs/dev/features/database.md +4 -4
  25. package/docs/dev/features/notifications.md +1 -2
  26. package/docs/dev/features/nudge-pipeline.md +14 -0
  27. package/docs/dev/features/opencode-plugin.md +56 -15
  28. package/docs/dev/features/prediction-engine.md +71 -6
  29. package/docs/dev/features/qa-system.md +19 -3
  30. package/docs/dev/features/session-lifecycle.md +6 -6
  31. package/docs/dev/features/session-tabs.md +161 -0
  32. package/docs/dev/features/setup.md +5 -0
  33. package/docs/dev/features/watchers.md +62 -6
  34. package/docs/dev/gotchas.md +89 -0
  35. package/docs/dev/mcp-parity.md +5 -5
  36. package/docs/dev/setup-and-hooks.md +7 -2
  37. package/docs/dev/skills.md +12 -9
  38. package/docs/plans/generalized-session-heartbeat.md +212 -0
  39. package/docs/plans/prediction-consolidation.md +88 -68
  40. package/docs/user/README.md +29 -13
  41. package/docs/user/behavior-engine.md +7 -1
  42. package/docs/user/cli.md +5 -1
  43. package/docs/user/code-review.md +43 -2
  44. package/docs/user/commands.md +37 -10
  45. package/docs/user/cross-session-chat.md +76 -26
  46. package/docs/user/prediction-engine.md +23 -1
  47. package/docs/user/setup.md +16 -0
  48. package/docs/user/skills.md +9 -7
  49. package/docs/user/watchers.md +22 -7
  50. package/package.json +10 -2
  51. package/src/alerts.ts +1 -1
  52. package/src/capabilities.ts +20 -6
  53. package/src/chat.ts +358 -43
  54. package/src/commands.ts +123 -18
  55. package/src/db.ts +74 -3
  56. package/src/git.ts +3 -1
  57. package/src/hygiene.ts +5 -0
  58. package/src/mcp.ts +101 -9
  59. package/src/opencode/tui-plugin.ts +90 -0
  60. package/src/opencode/v2.ts +440 -66
  61. package/src/prediction.ts +90 -0
  62. package/src/prompts.ts +164 -29
  63. package/src/runtime.ts +298 -15
  64. package/src/scoring-engine.ts +40 -9
  65. package/src/session-db.ts +4 -4
  66. package/src/session-tab-shared.ts +231 -0
  67. package/src/setup.ts +9 -3
  68. package/src/skills.ts +6 -9
  69. package/src/tab-state.ts +125 -0
  70. package/src/tool-defs.ts +454 -76
  71. package/src/tools.ts +2 -0
  72. package/src/watchers.ts +263 -26
  73. package/docs/plans/intuition-drives.md +0 -147
  74. package/docs/plans/llm-alerts.md +0 -287
  75. package/docs/plans/thatch-actions.md +0 -179
  76. package/docs/plans/worktree-deletion-recovery.md +0 -246
@@ -160,6 +160,12 @@ here first. These are the things that have already cost time.
160
160
  - **`bun test` does not typecheck**, and `tsconfig.json` excludes `tests`. Test
161
161
  type errors are editor-only noise unless you run `tsc` on the test files
162
162
  directly. Keep test files type-clean anyway so the editor stays quiet.
163
+ - **`mise run test` (bare `bun test`) also executes the QA barrels**
164
+ (`tests/qa/*/index.test.ts`), which spawn REAL opencode sessions and spend
165
+ model tokens, one use case at a time - a full run can hang for many minutes.
166
+ The unit gate is `mise run check` (`bun test tests/*.test.ts`); QA runs
167
+ belong to the `mise run qa-auto` / `qa-live` tasks, which select a UC
168
+ subset by name.
163
169
 
164
170
  ## Config and notifications
165
171
 
@@ -293,3 +299,86 @@ instead of throwing "Cannot use a closed database". Two rules keep this intact:
293
299
  renderer quotes every description (`yamlQuote` in src/commands.ts) and a
294
300
  unit test pins the quoted form. Keep it that way for any new frontmatter
295
301
  field whose value is free-form prose.
302
+
303
+ ## In-memory maps gate nothing durable: reloads and restarts wipe them
304
+
305
+ - **Any guard whose correctness depends on a runtime.ts Map silently
306
+ disappears for sessions whose events arrive after a plugin reload or a
307
+ daemon restart.** The chat auto-registration child check learned this the
308
+ hard way: `childToParent` kept extraction children out of the roster, but
309
+ an instance that lost the map to a reload registered every event-bearing
310
+ child, and v2 cannot delete sessions (sessionDelete is a no-op), so the
311
+ poller heartbeat those corpse rows forever and the stale-row reaper never
312
+ reaped them (81 `thatch-extraction` rows by 2026-10-07). The fix pattern:
313
+ the plugin writes a DURABLE marker at creation time
314
+ (`chat_machinery`, src/db.ts), and every registration path checks the
315
+ marker plus a stable title (`isMachinerySessionTitle`) instead of
316
+ trusting in-memory state alone. If you add a new plugin-created session
317
+ kind, mark it in the same table and route its events through the same
318
+ guards - do not rely on Maps surviving a reload.
319
+
320
+ ## A running TUI never adopts a NEW ./tui plugin entrypoint - restart the TUI
321
+
322
+ - **The v2 TUI discovers its CLI plugins at process startup only.** The
323
+ server side hot-reloads plugin code (the digest watcher), and the TUI
324
+ hot-reloads the SOURCE of plugins it already loaded - but a plugin that
325
+ GAINS a `./tui` entrypoint (or a fresh install of one) is a discovery-
326
+ level change: a TUI process started before the entrypoint existed never
327
+ loads it. Symptom (2026-10-08, the thog TUI): `thatch_session_tab` ran
328
+ fine server-side (the session, the chat registration, the response all
329
+ real) but the tab never appeared in the strip - the TUI had no
330
+ `ui.tabs` consumer loaded, so the rpc event landed nowhere. The
331
+ persisted `tabs.json` (in `$XDG_STATE_HOME/opencode/<channel>/tui/`)
332
+ proved the registration logic was correct the whole time: a FRESH TUI
333
+ (the docker sandbox attach) showed the tab immediately.
334
+ - **Fix**: convert the install to the directory shim (docs/user/setup.md),
335
+ THEN restart the TUI. A restart ALONE fixes nothing when the install is
336
+ a FILE shim - the file shape never loads a `./tui` entrypoint, whatever
337
+ the process age. First diagnostic: `ls ~/.config/opencode/plugins/` -
338
+ a FILE (`thatch.ts`) means no TUI consumer has ever loaded there,
339
+ restart or not; the directory form (`thatch/index.ts` + `tui.ts`) is
340
+ required. (2026-10-08, the thog TUI: the restart-and-respawn test
341
+ DISPROVED the stale-process theory and pointed here - the conversion
342
+ was the missing install step.)
343
+ - **Fix**: restart the TUI window once after adopting (or changing the
344
+ shape of) a `./tui` entrypoint. DIAGNOSTIC WARNING: `tabs.json` cannot
345
+ discriminate here - it is SHARED across windows and written by ANY tab
346
+ open, manual (a picker navigation) or event-driven - an entry for the
347
+ session proves only that SOMETHING opened it, not that the spawn-time
348
+ event worked. The discriminating test: restart the TUI and re-spawn; a
349
+ tab appearing post-restart confirms the stale-process diagnosis; a tab
350
+ still absent is a real bug worth a report.
351
+
352
+ ## The two opencode lines discover plugins differently: no one shim shape auto-loads on both
353
+
354
+ - **v1 auto-discovers plugin FILES only** - its config plugin scan globs
355
+ `{plugin,plugins}/*.{ts,js}`, which never matches a directory. **v2
356
+ auto-discovers files AND directories** (and symlinks), and its activation
357
+ dies on duplicate ids - so a plugins directory holding BOTH `thatch.ts`
358
+ and `thatch/` loads the server plugin twice on v2 and dies with
359
+ "duplicate instance plugin ids" before any plugin code runs. There is no
360
+ single on-disk shape that auto-loads on both lines as a directory-based
361
+ multi-entry plugin. Consequences: the dev-checkout install uses the file
362
+ shim for v1-only or v2-only use, or the directory shim for v2's `./tui`
363
+ entrypoint (session tabs) - deleting the file shim in the same step; the
364
+ QA fixture keeps the FILE shim because the QA suite is headless (no TUI
365
+ process ever loads `./tui`, and the file shape is the only one that loads
366
+ on both lines - a directory shim deterministically killed UC-100's v1
367
+ chat canary). Local plugin specs handed to a loader explicitly
368
+ (`"plugin": ["./path"]`) can point at a directory on v1 - it is only the
369
+ AUTO-discovery scan that is files-only.
370
+
371
+ ## A promptSession wrapper must stay synchronous or fire-and-forget delivery assertions misfire
372
+
373
+ - **Symptom**: tests that assert on wake delivery immediately after a hook
374
+ invocation see zero calls; deliveries land one microtask late.
375
+ - **Cause**: the delivery guard (`guardDelivery`, src/runtime.ts) sits on
376
+ every wake path, and its directory-existence check was first written as
377
+ `await stat(...)`. Fire-and-forget callers (`void caps.promptSession(...)`)
378
+ and the repo's mock contract (calls recorded "synchronously inside the
379
+ mock", asserted with no intervening await) observe the wrapped call
380
+ before an extra microtask resolves.
381
+ - **Fix**: the existence check is synchronous (`existsSync`), so the guard
382
+ adds no microtask between the gate and the wrapped call. If you ever
383
+ replace it with an async check, the callers' fire-and-forget timing -
384
+ and the tests that pin it - break again. (2026-10-12, wake-delivery-guard)
@@ -18,20 +18,20 @@ This document maps the feature parity and documents the remaining gaps.
18
18
 
19
19
  | Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
20
20
  |---------|----------------|------------------------|---------------------|--------|
21
- | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR, branch, and command watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The `chat_*` tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel |
21
+ | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only and v2-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only and v2-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR, branch, and command watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The `chat_*` tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel. `session_tab` and `session_tab_close` are v2-opencode-only — they drive TUI session tabs over the v2 rpc-event bridge |
22
22
  | **System prompt** (store names, usage rules) | `experimental.chat.system.transform` — dynamic, repo baked in at runtime | CLAUDE.md static text appended by `thatch setup`; repo auto-detected by the MCP server at startup | AGENTS.md static text appended by `thatch setup`; repo auto-detected at startup | **Approximate** — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
23
23
  | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
24
24
  | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
25
- | **Wrap-up commands** (`/thatch/compact`, `/thatch/exit`) | Command markdown synced to `$XDG_CONFIG_HOME/opencode/command/thatch/` at plugin init; `command.execute.before` arms the session and the next idle resolves the greenlight token into a TUI action (`session.compact` via `executeCommand`, `app.exit` via `publish`) | No equivalent — no plugin host to arm the check or trigger the action | No equivalent | **Gapped** — needs the plugin's SDK client and TUI control routes |
26
- | **On-demand actions** (`/thatch/defrag`, `extract`, `hygiene`, `reflect`, `refine`) | Command markdown synced at plugin init; bodies rendered from the shared prompt cores with `thatch_` tool spellings; `extract` is opencode-only (needs session identity via `get_session_info` and the plugin's payload provider) | Command markdown synced by `thatch setup --claude` into `<claudeDir>/commands/thatch/`; same cores with `mcp__thatch__` spellings; `extract` excluded | MCP Prompts (`prompts/list` + `prompts/get`) from the same cores; Cursor surfaces them as slash commands; `extract` excluded | **Full** for the shared actions (same cores, per-host tool spellings); `extract` opencode-only |
25
+ | **Wrap-up commands** (`/thatch/compact`, `/thatch/exit`) | Command markdown synced to `$XDG_CONFIG_HOME/opencode/command/thatch/` at plugin init; `command.execute.before` arms the session and the next idle resolves the greenlight token into the host action (compact: the TUI compact command on v1, `session.compact` on v2; exit: `app.exit` on v1, the session's own tab closing on v2) | No equivalent — no plugin host to arm the check or trigger the action | No equivalent | **Gapped** — needs the plugin's SDK client and host action routes |
26
+ | **On-demand actions** (`/thatch/defrag`, `extract`, `hygiene`, `peer-review`, `refine`, `reflect`, `self-review`, `whois`) | Command markdown synced at plugin init; nudge-backed bodies rendered from the shared prompt cores with `thatch_` tool spellings, self-contained bodies (refine, whois, and the review-mode couchings peer-review/self-review) inline in the registry; `extract` and `whois` are opencode-only (they need session identity) | Command markdown synced by `thatch setup --claude` into `<claudeDir>/commands/thatch/`; same bodies with `mcp__thatch__` spellings; `extract` and `whois` excluded | MCP Prompts (`prompts/list` + `prompts/get`) from the same bodies; Cursor surfaces them as slash commands; `extract` and `whois` excluded | **Full** for the shared actions (same bodies, per-host tool spellings); `extract` and `whois` opencode-only |
27
27
  | **Extraction** (buffer tool calls, extract durable facts) | `tool.execute.after` buffers non-meta tool calls in-process (`isMetaToolName` in `src/extraction.ts`: thatch_*, skill, task, agent, subagent — execute calls unwrapping only thatch tools are also excluded); **direct extraction** is the primary path — when the parent session goes idle with pending buffer entries, the plugin creates a child session via the SDK (`client.session.create` with `parentID`) and prompts it to call `get_extraction_payload` (nudge suppressed via the `extracting` set); the `chat.message` nudge path is a **fallback** used when direct extraction throws | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` peeks the queue and prints the nudge with session ID + fetch tool name (does not drain — the queue persists until a memory write or `extraction_done`) | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Partial** — opencode uses direct extraction (parent idle → child session) as the primary path, with the nudge as fallback; MCP hosts are nudge-only. Both paths use `get_extraction_payload` to fetch the queued interactions as a tool response, keeping the full payload out of the main session's context window. Nudge timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor; opencode's direct extraction fires at **end of turn** (session idle), and the nudge fallback fires at the start of the next turn like MCP |
28
- | **Extraction feedback** (toast notifications) | `client.tui.showToast` fires when the extraction child goes idle — shows `[thatch] new: N, updated: M, deleted: K` (success) or `extraction complete — nothing to save` (info); best-effort, silently ignored in headless mode | No TUI connection — MCP hosts have no equivalent | No TUI connection | **Gapped** — MCP hosts cannot show toast notifications |
28
+ | **Extraction feedback** (toast notifications) | `client.tui.showToast` on v1; the session-tab bridge's toast event -> the TUI plugin's `ui.toast.show` on v2 — fires when the extraction child goes idle, but ONLY when memories were actually written (no toast for no-save runs, to avoid notification fatigue): shows `[thatch] new: N, updated: M, deleted: K` (success); best-effort, silently ignored in headless mode | No TUI connection — MCP hosts have no equivalent | No TUI connection | **Gapped** — MCP hosts cannot show toast notifications |
29
29
  | **Automatic alerts** (notify on LLM pause / turn end) | Plugin event hook drives the `alerts.ts` state machine: `question.asked`/`permission.asked` → pause alert; `session.status` idle with a real-work round → done alert; unrecovered errors → needs-attention. Delivered via the shared `notify.ts` dispatcher, channel per event from the `alerts` config section | No event stream — only `thatch_notify_user` when the agent chooses | No event stream — only `thatch_notify_user` when the agent chooses | **Gapped** — MCP hosts have no plugin event hook to watch; Claude Code's `Notification`/`Stop` hooks could feed the same classifier later |
30
30
  | **Prompt-aware recall nudge** | `chat.message` hook embeds the prompt with the in-process warm model, searches `db.search()`, pushes a nudge part if matches ≥ threshold | `UserPromptSubmit` hook → `thatch flush-tools` connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — sideband socket gives cold hook processes access to the warm MCP server's model |
31
31
  | **Prediction auto-fire** (user decision model) | `chat.message` hook scores the prompt embedding against prediction matchers; injects a `[thatch] User decision model` nudge alongside the recall nudge (separate synthetic part, same embedding call) | `UserPromptSubmit` hook → `thatch flush-tools` fires prediction query via the sideband socket's `predictions` method in parallel with the recall nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — same `scorePredictionNudge` entry point in both paths |
32
32
  | **Behavior auto-fire** (self-discipline rules) | `chat.message` hook scores the prompt embedding against behavior matchers; injects a `[thatch] Situational behaviors` nudge (separate synthetic part, same embedding call) | `UserPromptSubmit` hook → `thatch flush-tools` fires behavior query via the sideband socket's `behaviors` method in parallel with recall and predictions | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — same `scoreBehaviorNudge` entry point in both paths |
33
33
  | **Skills** | Installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init (shared **+** opencode-only) | Installed by `thatch setup` (shared only) to the repo's `.claude/skills/` (local) or `$CLAUDE_CONFIG_DIR/skills/` (global) | Installed by `thatch setup` (shared only) to the repo's `.cursor/skills/` (local) or `~/.cursor/skills/` (global) | **Full** — same SKILL.md format; the code-review coordinator is opencode-only (needs sub-agents) |
34
- | **Store detection** (repo identity from git remote) | `worktree` parameter from the opencode plugin | `CLAUDE_PROJECT_DIR` env (set by Claude Code for stdio MCP servers) | `CURSOR_PROJECT_DIR` then `CLAUDE_PROJECT_DIR` then cwd | **Full** |
34
+ | **Store detection** (repo identity from git remote) | `worktree` parameter from the opencode plugin | `THATCH_PROJECT_DIR` (pinned by project-local setup) → `CLAUDE_PROJECT_DIR` env → cwd | `THATCH_PROJECT_DIR` (pinned by project-local setup) → `CURSOR_PROJECT_DIR` then `CLAUDE_PROJECT_DIR` then cwd | **Full** |
35
35
  | **Setup detection at startup** | n/a — plugin auto-installs at init | `checkSetup` in MCP server: detects missing or broken instructions in CLAUDE.md | Same as Claude Code (checks AGENTS.md) | **Full** — warns the agent to tell the user to run `thatch setup` |
36
36
  | **Hook config format** | n/a — plugin registers hooks in code | Nested: `.claude/settings.json` `{hooks:{Event:{hooks:[{type,command}]}}}` | Flat: `.cursor/hooks.json` `{version:1,hooks:{event:[{command}]}}` | n/a |
37
37
 
@@ -80,7 +80,7 @@ project and every contributor gets them).
80
80
  |-------|---------|--------|------|
81
81
  | `SessionStart` | `thatch reminder` | plain text to stdout (becomes context) | Recall instructions + hygiene heartbeat + chat identity/mail line (reads the hook's stdin `session_id`) |
82
82
  | `PostToolBatch` | `thatch buffer-batch` | **silent** (no stdout) | Appends a batch of tool calls to the file-backed JSONL queue |
83
- | `UserPromptSubmit` | `thatch flush-tools` | nudge text to stdout | Peeks queue (extraction nudge), else fires recall, prediction, and behavior nudges via sideband in parallel, else write nudge |
83
+ | `UserPromptSubmit` | `thatch flush-tools` | nudge text to stdout | Peeks queue (extraction nudge; degraded to a restart instruction under version skew - the old server may lack the referenced tools), else fires recall, prediction, and behavior nudges via sideband in parallel, else write nudge |
84
84
  | `Stop` | `thatch chat-notify` | `{ hookSpecificOutput: { additionalContext } }` when chat mail is unread, else `{}` | Post-turn chat wake: the turn continues so the model reads its mail (Claude Code's analog of opencode's poller wake); host loop protections (`stop_hook_active`, 8-consecutive cap) plus the delivered stamp bound repeats |
85
85
 
86
86
  `PostToolBatch` is silent so the agent loop is not delayed; the buffered
@@ -122,7 +122,7 @@ is honored for symmetry and forward-compatibility. Cursor has no equivalent of
122
122
  |-------|---------|--------|------|
123
123
  | `sessionStart` | `thatch reminder --json` | `{ additional_context: "..." }` | Recall + heartbeat + chat identity/mail line, JSON-wrapped for Cursor |
124
124
  | `postToolUse` | `thatch buffer-tool` | **silent** | Appends a **single** tool call to the file-backed queue |
125
- | `beforeSubmitPrompt` | `thatch flush-tools --json` | JSON `additional_context` | Peeks queue, else recall, prediction, and behavior via sideband, else write nudge |
125
+ | `beforeSubmitPrompt` | `thatch flush-tools --json` | JSON `additional_context` | Peeks queue (extraction nudge; degraded to a restart instruction under version skew), else recall, prediction, and behavior via sideband, else write nudge |
126
126
  | `stop` | `thatch chat-notify` | `{ followup_message }` or `{}` | Post-turn chat wake: when chat mail is unread, Cursor auto-submits the follow-up as the next user message (Cursor's analog of opencode's poller wake); `loop_limit: 3` bounds consecutive follow-ups |
127
127
 
128
128
  Differences from Claude Code:
@@ -179,6 +179,11 @@ The host is determined from env vars set by the host process:
179
179
 
180
180
  Cursor takes priority because Cursor also sets `CLAUDE_PROJECT_DIR` as an alias.
181
181
 
182
+ Separately from host detection, the project directory used for store/chat
183
+ identity resolution resolves as `THATCH_PROJECT_DIR ?? CURSOR_PROJECT_DIR
184
+ ?? CLAUDE_PROJECT_DIR ?? cwd` in the MCP server; project-local `thatch
185
+ setup` runs pin `THATCH_PROJECT_DIR` into the written MCP config.
186
+
182
187
  ### What it checks
183
188
 
184
189
  `checkSetup` looks for the instruction markers (start and end) in the host's
@@ -47,18 +47,18 @@ description: Extract durable project facts ... Use when ...
47
47
  | `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
48
48
  | `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar against generic praise. |
49
49
  | `thatch-review-technique` | Technique lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, verified citations, never blocking. |
50
- | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. Starts the final report with a workflow-change preface, then cross-references findings against prior review comments when a follow-up round register is provided. |
51
- | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, linked docs/tickets followed from the change, memory) before fan-out. Prevents false positives about intentionally deferred work, and checks whether main moved past the merge-base touching the same paths (staleness signal for the synthesizer). Also fetches prior review comments on a connected PR/MR for follow-up-round detection and builds a register with preliminary addressed-check status per comment. |
50
+ | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. Starts the final report with a workflow-change preface, then cross-references findings against prior review comments when a follow-up round register is provided, and against sibling PR registers when related PRs were detected (cross-PR notes appendix). |
51
+ | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, linked docs/tickets followed from the change, memory) before fan-out. Prevents false positives about intentionally deferred work, and checks whether main moved past the merge-base touching the same paths (staleness signal for the synthesizer). Also fetches prior review comments on a connected PR/MR for follow-up-round detection and builds a register with preliminary addressed-check status per comment. Also discovers related PRs (stacks chained through base branches, release trains ordered by tracker relations) and builds a sibling register with targeted consistency verdicts. |
52
52
  | `thatch-code-archaeology` | Investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features, skeletons) before proposing changes. The research skill; pair with `thatch-coding-workflow`. |
53
53
  | `thatch-review-followup` | Alternate entrypoint for follow-up review rounds. Verifies whether the author's responses and code changes since your last review round adequately addressed your prior findings, offers to reply on resolved items, then optionally re-runs the full structured review. |
54
54
  | `thatch-change-walkthrough` | Explain a change to the user as a teaching walkthrough: resolve the delta, research each affected workflow at the merge-base, teach current behavior, then overlay the modifications with file:line citations and analogies. |
55
55
  | `thatch-code-walkthrough` | Explain a feature, module, or workflow to the user as a teaching walkthrough: identify the code area (optionally from a branch or PR), research how it works, teach it with file:line citations and analogies, list the key files. |
56
56
  | `thatch-session-reflection` | End-of-session memory recording (project, user, tools, self). |
57
57
  | `thatch-coding-workflow` | Plan and execute code changes with a task-list-driven workflow. Use when implementing features, fixing bugs, or making multi-file changes. |
58
- | `thatch-plan-refinement` | Refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). The generic core loop. |
59
- | `thatch-refine` | Classify the project (audience, lifecycle, surface) and refine a plan under that type's requirements. Entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. |
58
+ | `thatch-plan-refinement` | Refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). The generic core loop. Plans with UI or session state carry a state-and-transition matrix; template ports carry a deltas-from-template ledger; a revision self-check runs between revise and re-review; user-extended runs rotate reviewer lenses and stop on a round with no build-changing findings. Deep-mode triggers are binding. |
59
+ | `thatch-refine` | Classify the project (audience, lifecycle, surface) and refine a plan under that type's requirements. Entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. Deep-mode triggers in the core skill are binding; type deltas relax round counts, never mode selection. |
60
60
  | `thatch-refine-team-app` | Refinement deltas for multi-developer team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate. |
61
- | `thatch-refine-personal` | Refinement deltas for solo projects with no downstream consumers: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. |
61
+ | `thatch-refine-personal` | Refinement deltas for solo projects with no downstream consumers: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. Relaxes round counts only, never mode selection. |
62
62
  | `thatch-refine-shared-lib` | Refinement deltas for changes touching a library's API surface: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable. |
63
63
  | `thatch-refine-spike` | Refinement deltas for throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on, state the question and the time box. |
64
64
  | `thatch-refine-infra` | Refinement deltas for CI, Terraform, and k8s changes: blast radius across pipelines and environments, rollback plan and staged verification are mandatory, pattern-following is near-absolute. |
@@ -69,17 +69,17 @@ description: Extract durable project facts ... Use when ...
69
69
  | `thatch-review-response` | Author-side review response: triage findings, fix bugs one by one, reply on each thread, post a top-level summary comment. |
70
70
  | `thatch-memory-verify` | Fact-check a single memory against the current codebase and correct stale claims. Uses git archaeology to preserve historical context when changes were intentional. |
71
71
  | `thatch-knowledge-export` | Compile everything thatch knows about a topic into a curated markdown file for knowledge transfer. Searches across stores, curates out personal noise, fact-checks code-related memories via `thatch-memory-verify`. |
72
- | `thatch-clear-writing` | Prose rules for any human-facing text not covered by a dedicated writing skill: PR and ticket comments, documentation, plans, reports, and chat replies. Clarity over compression; dedicated skills win when loaded. |
72
+ | `thatch-clear-writing` | Prose rules for any human-facing text not covered by a dedicated writing skill: PR and ticket comments, documentation, plans, reports, chat replies, and code comments. Clarity over compression; dedicated skills win when loaded. |
73
73
 
74
74
  **opencode-only** — the coordinator needs sub-agent support:
75
75
 
76
76
  | Skill | Role |
77
77
  |-------|------|
78
- | `thatch-code-review` | Resolve review target (incl. VCS detection and connected PR/MR lookup for follow-up round detection), gather project context, research affected workflows, estimate complexity, partition, dispatch the review specialists in parallel, synthesize with a workflow-change preface and prior-comment cross-reference. |
78
+ | `thatch-code-review` | Resolve review target (incl. VCS detection and connected PR/MR lookup for follow-up round detection), gather project context, research affected workflows, estimate complexity, partition, dispatch the review specialists in parallel, synthesize with a workflow-change preface, prior-comment cross-reference, and sibling-register cross-reference. Also the machinery behind the `/thatch/self-review` and `/thatch/peer-review` mode commands, which run it with a weighting overlay (inline, without sub-agents, on hosts that lack the coordinator). |
79
79
 
80
80
  ## REVIEW_COMMON
81
81
 
82
- The seven problem-finding review specialists share a framework interpolated via `${REVIEW_COMMON}` (the eighth specialist, highlights, finds positive things and uses its own structure):
82
+ The seven problem-finding review specialists share a framework interpolated via `${REVIEW_COMMON}` (the technique and highlights specialists do not use it: technique reports informational notes and highlights finds positive things, each with its own structure):
83
83
 
84
84
  - **Static analysis only** — no running tests/linters/compilers.
85
85
  - **Scope gathering** — resolve the git range, `git diff --stat`, read changed
@@ -93,6 +93,9 @@ The seven problem-finding review specialists share a framework interpolated via
93
93
  - **Project context awareness** — if a context brief is provided (from the
94
94
  coordinator or from loading `thatch-review-context`), use it to avoid false
95
95
  positives about intentionally deferred work.
96
+ - **Sibling PR context** — when the brief's sibling register is present
97
+ (related PRs detected), check it before flagging anything as referenced but
98
+ missing; a stack member or release-train sibling may deliver the piece.
96
99
  - **Workflow context awareness** — if a workflow guide is provided (from the
97
100
  coordinator or from loading `thatch-code-archaeology`), use it to understand
98
101
  the purpose and evolution of the code, distinguishing intentional behavior
@@ -143,7 +146,7 @@ and `--cursor` pass only the shared set.
143
146
 
144
147
  ## Adding a skill
145
148
 
146
- 1. Write `artifacts/skills/thatch-<name>.md` with YAML frontmatter. If it's a problem-finding review specialist, include `${REVIEW_COMMON}` (interpolated from `artifacts/skills/common.md`). The highlights specialist does not use REVIEW_COMMON — it finds positive things, not problems, so the problem-finding framework does not apply.
149
+ 1. Write `artifacts/skills/thatch-<name>.md` with YAML frontmatter. If it's a problem-finding review specialist, include `${REVIEW_COMMON}` (interpolated from `artifacts/skills/common.md`). The technique and highlights specialists do not use REVIEW_COMMON — technique reports informational notes and highlights finds positive things, so the problem-finding framework does not apply to either.
147
150
  2. Add the skill name to the `names` array in `loadSharedSkills()` (or `loadOpencodeOnlySkills()` if sub-agents are needed). The loader in `src/skills.ts` reads `.md` files at init.
148
151
  3. If the skill name appears in a tool's workflow (e.g. `thatch-fact-extractor`), update `src/prompts.ts`.
149
152
  4. Run `mise run check` — tests verify counts; `installSkills` picks up new files on next init.
@@ -0,0 +1,212 @@
1
+ # Plan: watcher death detection for closed v2 tabs
2
+
3
+ Status: v3.1 - empirical premise VERIFIED live (2026-10-07: prompt_async
4
+ to a closed v2 tab returns 404; no silent success; the session row
5
+ persists, so the pre-cancel re-check keys on the throw class). The
6
+ build's gate is open. Two refinement rounds converged (round 2: BUILD,
7
+ no blockers).
8
+
9
+ ## Problem
10
+
11
+ v2 tab close fires no `session.deleted`, so the closed tab's entry in
12
+ the runtime's `sessionStatus` map (src/runtime.ts:217-222, keys "leave
13
+ only in session.deleted") never leaves, the chat poller keeps hosting
14
+ and heartbeat-ing the dead tab's row (src/runtime.ts:363-368,
15
+ src/chat.ts:879-883), and its watchers poll until the 8h TTL with
16
+ deliveries queuing forever. The session.deleted handler itself names
17
+ the failure mode: "a leaked status key would keep heartbeat-ing a dead
18
+ session as permanently fresh" (src/runtime.ts:1891).
19
+
20
+ ## Rejected: heartbeat-staleness death detection (the original plan)
21
+
22
+ The original design gated the heartbeat stamp on `fetchStatuses`
23
+ presence so a closed tab would age out. Round 1 proved the premise
24
+ inverted: the live status map carries only ACTIVE (mid-turn) sessions -
25
+ the host deletes idle entries (thatch's own wake-gate contract,
26
+ src/chat.ts:366-368; host source session/status.ts:42-46) - so absence
27
+ means "idle OR closed". Gating stamps on presence would mark every
28
+ idle-open tab stale in 60s (regressing e486148, whose grace exists
29
+ precisely because idle-open tabs emit nothing) and death-cancel the
30
+ watchers of every idle session. And on v2 - the only host with the bug
31
+ - `fetchStatuses` is a permanent `{}` stub (src/opencode/v2.ts:524;
32
+ "session.status exists in v2's schema but nothing publishes it",
33
+ v2.ts:417), so the mechanism does not exist where the bug lives.
34
+ Chat-off sessions (chatPoller.start gated on chatOn,
35
+ src/runtime.ts:407-409) get no stamps at all - a third false-death
36
+ path.
37
+
38
+ Conclusion: the heartbeat cannot carry tab-death detection on v2. The
39
+ heartbeat keeps its current role (roster liveness for chat, unconditional
40
+ stamps for hosted sessions); death detection needs a different clock.
41
+
42
+ ## New design: delivery-persistence death detection
43
+
44
+ The signal that DOES distinguish a closed v2 tab from a live one, with
45
+ no host API: **what happens to a pending watcher event.**
46
+
47
+ - A live v2 session with queued events consumes them: the delivery gate
48
+ opens (on v2 the stub map reads as idle) and `promptAsync` succeeds -
49
+ there is no busy-rejection on the v2 prompt path. Queues drain.
50
+ - A closed v2 session: every `deliver()` attempt throws (the session no
51
+ longer exists server-side) and the queue retries forever
52
+ (src/watchers.ts:1296-1298 - failed delivery stays pending).
53
+
54
+ So: **death = a session's pending watcher queue has been non-empty for
55
+ longer than the threshold AND recent delivery attempts have THROWN**
56
+ (not been skipped - a skipped attempt is a busy/compacting session and
57
+ must never count). The heartbeat is untouched; this is one extra clock
58
+ on one queue, per session.
59
+
60
+ - Threshold: 2 hours default (env `THATCH_WATCH_DEATH_MINUTES`). Far
61
+ above any legitimate busy turn; far below the 8h TTL, which remains
62
+ the hard backstop. The failure counter counts CONSECUTIVE thrown
63
+ deliveries per session (side map keyed by session id - WatcherEvent
64
+ carries no timestamp); a SKIPPED attempt (busy/compacting) or a
65
+ success RESETS it - skips are never death evidence.
66
+ - Pre-cancel re-check (round 2): when the threshold fires, re-verify
67
+ the session via `caps.sessionGet` before cancelling - only a
68
+ not-found-class result confirms death. A wedged server or 500-loop
69
+ throws exactly like a dead session does, and without the re-check a
70
+ two-hour API outage would mass-cancel every hosted watcher.
71
+ - v1 is out of scope: v1 fires session.deleted on tab close (the
72
+ known-gaps table), so v1 tabs already clean up; crashed v1 daemons
73
+ are covered by the dormant-row machinery.
74
+ - On death: cancel the session's watchers (watchers.cancelSession -
75
+ also drops the pending queue, src/watchers.ts:1195-1201), emit a
76
+ death notice via the existing promptAsync path to other live
77
+ same-project sessions (`watcherDeathNotice`, src/prompts.ts:1051;
78
+ dedup per dead session like notifiedWatcherDeaths,
79
+ src/runtime.ts:801-806), and persist a `watcher_death` runtime_state
80
+ row (generic runtimeStatePut/All/Delete - no new db helpers).
81
+ - Reader for the persisted row (round 2): the rehydration switch
82
+ (src/runtime.ts:609-713) must learn the `watcher_death` kind
83
+ (foreign-pid rows KEPT like dormant watcher rows, not deleted) and
84
+ the dormant-scan path (src/runtime.ts:779-825) surfaces pending
85
+ death rows to the scanning session, deduping against
86
+ `notifiedWatcherDeaths`, deleting the row on surfacing. Without this
87
+ the persistence half is write-only: unknown foreign-pid kinds are
88
+ deleted on restart (:615-641) and `dormantScanned` runs once per
89
+ session per process.
90
+ - Scope guard: only sessions this process HOSTS via the
91
+ `hostedSessionIds` helper (src/runtime.ts:363-370) are eligible -
92
+ sibling v1 processes and MCP sessions are never touched.
93
+ - Residual case (round 2, documented): a tab closed MID-TURN whose
94
+ host never emits a terminal execution event keeps its mapped status
95
+ `busy` forever - deliveries skip forever, death never fires, TTL
96
+ covers it. Accepted; the notice will not fire for that shape.
97
+ - The roster zombie (a closed tab's row shows Active for days; the
98
+ 7-day auto-reaper bounds auto rows) is NOT fixed by this plan - no v2-
99
+ reachable signal distinguishes it. Documented as a known limitation;
100
+ the reviewer confirmed no last_seen consumer treats staleness as a
101
+ trigger today, so the honest heartbeat stays unconditional.
102
+ - Death notice payload (session-tab coordination, wanda-warpdrive-00009
103
+ 2026-10-07): the notice carries the dead session's CHAT NAME and
104
+ session id, not just the watch targets, so a coordinator session
105
+ running the thatch-coordination skill can mark its task-list entry
106
+ failed and respawn keyed by chat name, no SDK lookup.
107
+
108
+ ### Confirmed-close fast path (integration with session-tab)
109
+
110
+ The session-tab tool (docs/dev/features/session-tabs.md; merged to main
111
+ as 5abb381) emits `rpc.thatch-tabs.tab-closed {sessionID,
112
+ chatName?}` from its TUI plugin when a close is TOOL-initiated (shared
113
+ module session-tab-shared.ts; user-initiated closes stay out of scope
114
+ there - the delivery-persistence heuristic covers those). When the
115
+ event lands for a session with watchers, treat it as a CONFIRMED
116
+ close: skip the 2h heuristic entirely and run the death path
117
+ immediately. The rpc definition is imported from session-tab-shared
118
+ (same package). Fallback layering: confirmed close (instant) >
119
+ delivery-persistence heuristic (2h) > TTL (8h).
120
+
121
+ ## Empirical premise: VERIFIED (2026-10-07, live probe via the session-tab smoke flow)
122
+
123
+ Jeff closed a real v2 tab (session chatting as rosie-unit-one-00007,
124
+ session ses_ee7ec0baeffeziwE83AT2mgLxI) with a command watch armed
125
+ before the close. Probe results against the serve daemon's /api surface
126
+ (Basic auth: username `opencode`, password from
127
+ ~/.config/opencode/service.json; unprefixed /session/* paths are the
128
+ TUI SPA fallback - 200 HTML for anything, do not trust them):
129
+
130
+ - POST /api/session/{id}/prompt_async -> HTTP **404** (clean, no body):
131
+ the delivery throw is real and 404-class.
132
+ - Message count in the closed session before/after: 0 / 0 - **no silent
133
+ success, no headless turn**.
134
+ - GET /api/session/{id} -> 200 with the full record: the session ROW
135
+ persists server-side after tab close, so existence checks cannot
136
+ confirm death.
137
+
138
+ Consequence for design point 2's pre-cancel re-check: key on the throw
139
+ CLASS (404 = route/session gone) not on sessionGet existence - a
140
+ sessionGet 200 is expected even for dead tabs. Wedged-server 5xx loops
141
+ still do not count as death; only 404-class delivery throws do.
142
+
143
+ Location-scoping nuance (wanda-warpdrive-00009, post-probe): the v2
144
+ prompt route is session-location-middleware'd, so the probe's
145
+ cross-location 404 may encode location scoping rather than closed-ness
146
+ per se. The death signal keys on the HOSTING poller's own deliveries
147
+ throwing - same-location throws are the evidence that counts, and the
148
+ session-tab `tab-closed` event papers over the ambiguous cases. The
149
+ pre-cancel re-check and the threshold both run against the hosting
150
+ instance's observed throw class.
151
+
152
+ ## Surfaces touched
153
+
154
+ ### Pending-event durability (from the 2026-10-07 reload-loss incident)
155
+
156
+ A reload between DETECTION and DELIVERY silently eats notifications:
157
+ pending events live only in the in-memory `#pending` queue, and one-shot
158
+ watchers delete their journal row at detection time ("ends at first
159
+ detection, not first delivery") - so the durability layer is gone
160
+ exactly when undelivered events exist (observed: watch_qn449jio's CI
161
+ notification lost in the 09b805f reload window). Fix folded into this
162
+ build: journal PENDING EVENTS per session (alongside the watcher
163
+ definitions), delete them on successful delivery - which the death
164
+ detection needs anyway for its pending-age signal to survive reloads.
165
+ (Found by robot-devil-00012's code-verified analysis; my own watcher
166
+ was not one-shot, so its exact deletion path stays unproven until a
167
+ THATCH_DEBUG=1 repro.)
168
+
169
+ ## Surfaces touched
170
+
171
+ - `src/watchers.ts` - per-session delivery-failure tracking (consecutive
172
+ throws + oldest-pending age) and a death-scan hook the poller calls.
173
+ - `src/runtime.ts` - wire the death callback: notice delivery to live
174
+ same-project sessions, the `watcher_death` runtime_state row, dedup.
175
+ - `src/db.ts` - runtime_state row helpers for the death record.
176
+ - `tests/watchers.test.ts` - death detection (throwing deliver + old
177
+ pending vs skipped-deliver + old pending), threshold env, MCP-owner
178
+ exclusion.
179
+ - `tests/qa/auto/uc-118-watcher-death-detection.ts` (new,
180
+ barrel-imported).
181
+ - Docs: `docs/user/watchers.md` (death notices), `docs/dev/features/
182
+ watchers.md` (detection subsection), `docs/dev/features/
183
+ opencode-plugin.md` known-gaps row (bounded by death detection),
184
+ `docs/user/cli.md` + `docs/dev/features/cli.md` (new env var).
185
+
186
+ ## Acceptance criteria
187
+
188
+ 1. A closed v2 tab's watchers are cancelled within the threshold once
189
+ deliveries have been failing; a live same-project session hears the
190
+ death notice.
191
+ 2. A busy or idle LIVE session never triggers death (skipped delivery
192
+ attempts never count; successful delivery drains the queue).
193
+ 3. A transient delivery failure (< threshold) cancels nothing.
194
+ 4. MCP-owner and non-hosted sessions are never touched.
195
+ 5. `mise run check` + `mise run qa-auto` green.
196
+
197
+ ## Review rounds
198
+
199
+ - Round 1 (fresh reviewer): DO-NOT-BUILD on the heartbeat mechanism -
200
+ inverted premise (status absence means idle, not dead; v2 stub
201
+ returns {}), chat-off and sleep false-death paths, dormantScanned
202
+ once-per-session guard, incomplete death cleanup. The revision
203
+ replaces the mechanism with delivery-persistence detection
204
+ (v2-scoped) and drops the roster-zombie claim.
205
+ - Round 2 (fresh reviewer): BUILD conditional on the empirical probe;
206
+ all premises verified (thrown deliveries stay pending at
207
+ src/watchers.ts:1344-1349, live sessions drain at runtime.ts:1866-
208
+ 1874), with three required adjustments folded in: the death-row
209
+ reader + rehydration handling, the pre-cancel existence re-check,
210
+ and the known-gaps doc correction. Consensus to build - AFTER the
211
+ probe.
212
+