@jeffober/thatch 0.2.3 → 0.2.5

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 (54) hide show
  1. package/README.md +8 -1
  2. package/artifacts/skills/thatch-clear-writing.md +103 -5
  3. package/artifacts/skills/thatch-coordination.md +69 -0
  4. package/artifacts/skills/thatch-pr-description.md +21 -13
  5. package/artifacts/skills/thatch-review-no-slop.md +15 -0
  6. package/bin/thatch +95 -44
  7. package/docs/assets/deepwiki-badge.svg +1 -0
  8. package/docs/dev/README.md +7 -2
  9. package/docs/dev/features/cli.md +2 -0
  10. package/docs/dev/features/cross-session-chat.md +118 -11
  11. package/docs/dev/features/notifications.md +1 -2
  12. package/docs/dev/features/opencode-plugin.md +18 -3
  13. package/docs/dev/features/prediction-engine.md +44 -5
  14. package/docs/dev/features/session-tabs.md +155 -0
  15. package/docs/dev/features/setup.md +12 -0
  16. package/docs/dev/features/watchers.md +62 -6
  17. package/docs/dev/gotchas.md +68 -0
  18. package/docs/dev/mcp-parity.md +2 -2
  19. package/docs/dev/setup-and-hooks.md +7 -2
  20. package/docs/dev/skills.md +1 -1
  21. package/docs/plans/generalized-session-heartbeat.md +212 -0
  22. package/docs/plans/prediction-consolidation.md +88 -68
  23. package/docs/user/README.md +12 -3
  24. package/docs/user/cli.md +5 -1
  25. package/docs/user/commands.md +11 -4
  26. package/docs/user/cross-session-chat.md +31 -8
  27. package/docs/user/prediction-engine.md +11 -0
  28. package/docs/user/setup.md +29 -0
  29. package/docs/user/skills.md +1 -1
  30. package/docs/user/watchers.md +22 -7
  31. package/package.json +10 -2
  32. package/src/alerts.ts +1 -1
  33. package/src/chat.ts +320 -27
  34. package/src/commands.ts +14 -0
  35. package/src/db.ts +51 -1
  36. package/src/git.ts +3 -1
  37. package/src/hygiene.ts +5 -0
  38. package/src/mcp.ts +95 -7
  39. package/src/opencode/tui-plugin.ts +70 -0
  40. package/src/opencode/v2.ts +197 -4
  41. package/src/prediction.ts +90 -0
  42. package/src/prompts.ts +110 -23
  43. package/src/runtime.ts +187 -9
  44. package/src/session-db.ts +4 -4
  45. package/src/session-tab-shared.ts +180 -0
  46. package/src/setup.ts +59 -3
  47. package/src/skills.ts +1 -0
  48. package/src/tool-defs.ts +412 -55
  49. package/src/tools.ts +2 -0
  50. package/src/watchers.ts +263 -26
  51. package/docs/plans/intuition-drives.md +0 -147
  52. package/docs/plans/llm-alerts.md +0 -287
  53. package/docs/plans/thatch-actions.md +0 -179
  54. package/docs/plans/worktree-deletion-recovery.md +0 -246
@@ -1,287 +0,0 @@
1
- # Thatch Alerts: Notify the User When the LLM Pauses or Finishes
2
-
3
- ## Synopsis
4
-
5
- Add an opt-in alert layer to the thatch opencode plugin that sends a desktop
6
- notification when the LLM (a) is blocked on a human decision -- an interactive
7
- question or a permission request -- or (b) finishes a round of real work. Bookkeeping
8
- rounds driven by thatch's own nudges and background-task completions never notify.
9
- One code path serves both installed opencode lines (v1.18.x and v2.x). The brain
10
- lives in the server plugin, which is a single instance per project directory, so
11
- deduplication and debouncing per session are free.
12
-
13
- ## Background
14
-
15
- Jeff's workflow: ask a question, switch to another window, let the LLM work. He
16
- needs a notification when the work finishes or when it stalls waiting for him.
17
- Nothing else deserves a notification.
18
-
19
- What exists today and why it is not enough:
20
-
21
- - opencode v2's built-in TUI attention plugin (`internal:notifications`) notifies
22
- on question, permission, done, and error. It has no per-event configuration, and
23
- its "done" fires on every turn end -- including turns triggered by thatch nudges
24
- and background-task completions. That turns async thatch activity into
25
- notification spam.
26
- - The v1 line has no TUI plugin system at all. The community notifier plugin
27
- Jeff used on v1 is orphaned (config remnants in `~/.config/opencode`, upstream
28
- issue mohak34/opencode-notifier#108).
29
- - Thatch already ships `thatch_notify_user` (voice + banner, session-labeled), but
30
- it is a tool the LLM invokes on request. It cannot fire on host-level events,
31
- and it must not fire on every turn end.
32
-
33
- opencode's attention API (system notification + sound, focus-aware) exists only in
34
- the TUI plugin context, so a pure server plugin cannot use it. Shipping a thatch
35
- TUI plugin would deliver notifications through the terminal but would reintroduce
36
- the double-TUI problem: one plugin instance per attached TUI means one notification
37
- per TUI, and the same v2 session can live in two TUIs. Server-side delivery avoids
38
- that by construction.
39
-
40
- ## Goals
41
-
42
- - Notify on `question.asked` and `permission.asked` (the blocking cases).
43
- - Notify when a round of real work ends (busy -> idle transition).
44
- - Stay silent on rounds whose only tool calls were thatch or meta tools, and on
45
- turns triggered by synthetic input (nudges, task completions, watcher wake-ups).
46
- - Stay silent on recoverable errors and user-initiated aborts; notify when a
47
- session dies in a state nothing will wake.
48
- - Work identically on opencode v1.18.x and v2.x from one code path.
49
- - Deduplicate and debounce per session in one process.
50
-
51
- ## Non-goals (deferred)
52
-
53
- - TUI plugin delivery via opencode's attention API (v2-only, focus-aware, but
54
- per-TUI instances defeat dedup). Revisit as a v2 enhancement: a TUI plugin could
55
- report focus state to the server plugin for focus gating.
56
- - Focus gating in general. Server-side delivery cannot see terminal focus; banners
57
- may land while the terminal is focused. Accepted: Jeff's stated workflow is to be
58
- away from the terminal, and one unified code path is worth the noise.
59
- - Reload-state persistence. v2 auto-reload discards plugin memory on every save to
60
- the dev shim tree (see the PR #16 design). Worst case here is one missed or
61
- duplicate alert at a reload boundary, which Jeff has ruled acceptable. No
62
- `runtime_state` journaling for alerts.
63
- - Claude Code / Cursor hosts. The MCP server has no event stream. Claude Code's
64
- `Notification` and `Stop` hooks could feed the same classifier later; a parity
65
- row documents the gap.
66
-
67
- ## Design
68
-
69
- ### Architecture
70
-
71
- One actor owns all alert state: the thatch plugin instance inside the shared
72
- opencode server process (per project directory). Both v1 and v2 load exactly one
73
- instance per directory no matter how many clients are attached, so per-session
74
- dedup and debounce are plain in-memory maps. Delivery reuses the platform
75
- dispatcher that `thatch_notify_user` already uses.
76
-
77
- ```text
78
- event hook (question/permission/session.*)
79
- |
80
- v
81
- per-session alert state machine ---> classifier (real work?)
82
- | |
83
- v v
84
- src/notify.ts dispatcher <--- config (alerts section)
85
- (osascript / notify-send / say)
86
- ```
87
-
88
- ### Event capture
89
-
90
- | Event | Action |
91
- |---|---|
92
- | `question.asked` | Notify "needs your answer". Track request id; clear on `question.replied` / `question.rejected`. |
93
- | `permission.asked` | Notify "needs approval". Track request id; clear on `permission.replied`. |
94
- | `session.status` busy or retry | Mark the session active. Clear any recorded error (a retry means opencode is still working). |
95
- | `session.status` idle (from active) | Evaluate the round: classify, then notify or stay silent. |
96
- | `session.error` | Record the error on the session. Do not notify yet -- the status transition decides. |
97
-
98
- Both lines publish `question.asked` / `permission.asked` on the event bus with the
99
- full request payload (sessionID included), and `session.status` carries
100
- `{ type: "idle" | "busy" | "retry" }`. Verify both against the installed versions
101
- before implementation (see Verification).
102
-
103
- ### Per-session state machine
104
-
105
- State kept per session id: `active` (bool), `lastError` (name + message or null),
106
- `pendingAsks` (request ids already notified). Transitions mirror the built-in v2
107
- plugin's proven pattern, with error handling changed to match Jeff's intent:
108
-
109
- - error followed by busy/retry -> recovered, clear the error, keep going quiet.
110
- - error followed directly by idle -> nothing will wake the session; notify
111
- "needs attention" instead of a done notification.
112
- - `MessageAbortedError` -> always silent (the user pressed abort; they know).
113
- - busy -> idle with no error -> the done path: classify the round, notify only
114
- real work.
115
-
116
- Notification state is rebuilt empty on plugin setup. Reload or restart mid-turn
117
- means the busy->idle transition is never observed and that round stays silent.
118
- Accepted.
119
-
120
- ### Round classification (shared classifier)
121
-
122
- At idle, inspect the round's tool calls and skip when the round did no real work:
123
-
124
- 1. Skip sessions with a `parentID` (subagent children never notify).
125
- 2. Skip when the triggering user message is synthetic-only. This covers thatch
126
- nudge turns, background-task completions, and watcher wake-ups. Without this
127
- rule, zero-tool-call narration turns (the typical completion-injection turn)
128
- would still notify.
129
- 3. Otherwise inspect the last assistant message's tool parts. Skip when every tool
130
- call is in the meta set: `thatch_*`, the dispatch tools `task` / `agent` /
131
- `subagent` (name differs by version -- match the set, never one name), `skill`,
132
- `todowrite`, `question`, and `execute` calls whose code only wraps thatch tools
133
- (reuse the `unwrapExecuteThatchCalls` scan from `src/extraction.ts`).
134
- 4. Real tool calls present -> notify "work finished", titled with the session
135
- title (every notification identifies its session -- standing preference).
136
-
137
- The meta-tool exclusion and the execute-unwrap scan already exist in the
138
- extraction buffer's non-bufferable filter. Extract them into one shared pure
139
- function (in `src/extraction.ts` or a sibling) consumed by both the buffer and
140
- this classifier, so the aggregator-tool lessons live in one place. Unit tests for
141
- the shared classifier extend the existing meta-tool buffer tests.
142
-
143
- ### Config
144
-
145
- New `alerts` section in `~/.config/thatch/config.json` (schema in `src/config.ts`,
146
- written via `config_set`, hand-editable, atomic save -- same as `notifications`):
147
-
148
- ```jsonc
149
- {
150
- "alerts": {
151
- "pause": { "mode": "banner" }, // question + permission
152
- "done": { "mode": "banner" }, // real-work turn end
153
- "error": { "mode": "banner" } // stalled session
154
- }
155
- }
156
- ```
157
-
158
- `mode` accepts `both` / `banner` / `voice` / `none`, defaulting to `banner` for
159
- every event. Voice is opt-in per event: turn-end voice would fire far too often to
160
- be the default, and `thatch_notify_user` remains the LLM-invoked voice channel.
161
- Delivery goes through `sendNotification` in `src/notify.ts`, which already owns
162
- per-platform dispatch (darwin: `osascript` banner + `/usr/bin/say`; linux:
163
- `notify-send` / `spd-say` / `espeak`) and the injectable spawner tests need.
164
- Spoken alerts carry the session-identifying source label, per the standing rule.
165
-
166
- ### Version support
167
-
168
- The event hook, session status events, and the notify dispatcher exist on both
169
- lines. Divergence is confined to two seams, both already abstracted in
170
- `src/capabilities.ts` / the v1-v2 adapter layer:
171
-
172
- - Message reading for the classifier: v2 exposes session context/messages through
173
- the promise client; v1 has its own message-list shape. Map both into one
174
- internal part view (tool calls + synthetic flags) in the capabilities layer.
175
- - Tool naming: the dispatch tool is `task` (v1) vs `subagent` (v2); the classifier
176
- matches the set.
177
-
178
- ## Verification (before implementation)
179
-
180
- 1. SSE probe against the installed v1.18.x serve: confirm `question.asked`,
181
- `permission.asked`, and `session.status` appear on `/api/event` and carry the
182
- session's directory as `location` (the plugin event hook drops events whose
183
- location does not match).
184
- 2. Same probe against installed v2.x.
185
- 3. Confirm the message-list shape on both lines exposes tool parts (with tool
186
- names) and synthetic flags, so the classifier can read them.
187
- 4. Confirm one plugin instance per directory in the shared server (both lines) --
188
- this is the dedup guarantee, so verify rather than assume.
189
-
190
- ## Implementation milestones
191
-
192
- 1. Extract the shared round classifier (meta set + execute-unwrap) from the
193
- extraction filter; unit tests first.
194
- 2. Alert state machine in the plugin event hook (`src/index.ts` or a new
195
- `src/alerts.ts`): pause tracking, active/error/idle transitions.
196
- 3. Capabilities-layer message mapping for both lines.
197
- 4. `alerts` config section + `config_get` / `config_set` wiring + dispatcher
198
- calls.
199
- 5. QA use cases + docs.
200
-
201
- ## Testing
202
-
203
- - Unit: classifier cases (all-meta skip, real work notify, execute-wrapped unwrap,
204
- dispatch-tool set, synthetic-only skip); state-machine transitions (busy->idle
205
- notify once, error->retry quiet, error->idle notify, abort silent, dedup by
206
- request id); config parsing and defaults.
207
- - QA (`tests/qa/auto/`): drive a session through the plugin harness with the
208
- injectable spawner capturing `sendNotification` calls; assert pause and done
209
- alerts fire, nudge turns stay silent. Import the file from the suite's
210
- `index.test.ts` barrel.
211
- - Gate: `mise run check` and `mise run qa-auto` green before commit.
212
-
213
- ## Docs impact
214
-
215
- - `docs/user/notifications.md` -- alerts section: what fires, how to configure
216
- `alerts`, limitations (no focus gating, opencode-only).
217
- - `docs/dev/features/notifications.md` -- architecture: event capture, state
218
- machine, shared classifier, version seams.
219
- - `docs/dev/README.md` -- module table row if a new module lands.
220
- - `docs/dev/mcp-parity.md` -- row for the opencode-only alerts surface.
221
- - `docs/dev/gotchas.md` -- entry if the v1/v2 event or message-shape seams cost
222
- debugging time.
223
-
224
- ## Decisions already made
225
-
226
- - Server-plugin delivery over TUI-plugin delivery (dedup by construction, unified
227
- across versions).
228
- - Reload state loss accepted; no `runtime_state` journaling.
229
- - Watcher wake-up turns stay silent; the LLM can call `thatch_notify_user` when it
230
- decides something is important (tool already exists).
231
- - User aborts stay silent; unrecovered errors notify.
232
- - Banner default, voice opt-in per event.
233
- - Retire the orphaned v1 notifier config during rollout; leave v2's built-in
234
- attention settings untouched unless doubles annoy (its TUI-side notifications
235
- only fire when blurred).
236
-
237
- ## Deviations from the plan as written
238
-
239
- Recorded during implementation and review pass 1 (crash-override-00004);
240
- the review's blocking findings were real and fixed:
241
-
242
- - **The round's tool side comes from live bookkeeping, not the message
243
- list.** The plan's classifier read tool calls from messages and reused
244
- `unwrapExecuteThatchCalls` there; message parts do not carry tool
245
- arguments, so the unwrap was impossible at that seam (and was silently
246
- dropped -- the round-1 review caught the plan Mandate going unmet). The
247
- shipped design marks real work at the extraction buffer's push site
248
- (`sessionRealWork`), which already classifies meta tools and unwraps
249
- execute-wrapped thatch calls. `RoundShape` carries only the trigger side
250
- (synthetic) and the round error. `deriveRoundShape` is exported and pure
251
- (the review found the inline closure untestable, which is exactly where
252
- the blocking bug hid).
253
- - **The round delimiter is the host's idle marker on v2** (review B1):
254
- v2.0.16 appends a `{type: "idle"}` marker message at every turn end, and
255
- the plan's "newest non-assistant message is the trigger" scan picked the
256
- marker -- every v2 round looked empty and done alerts were dead. The
257
- round is now everything after the previous marker (the host's turn
258
- definition; also fixes mid-round steers on v2). V1 keeps the trigger
259
- scan (no markers; mid-round steers under-classify -- accepted v1
260
- limitation).
261
- - **A v2 execution failure now delivers the verdict immediately** (review
262
- B2): failed is the only terminal event for a failed busy period -- no
263
- idle follows it -- so the adapter translates it to the v1-shaped pair
264
- (`session.error` + `session.status` idle). Interrupts translate to an
265
- error named by reason (`user` -> MessageAbortedError silent, `shutdown`
266
- -> SessionShutdownError silent, `inactivity` -> SessionInactivityError ->
267
- needs-attention) + idle.
268
- - **Unknown round shape stays silent**, not "notify". The original choice
269
- (never miss a completion) made every adapter-level test whose client mock
270
- lacks a message surface spawn a real osascript banner during `mise run
271
- check` -- the state machine reached live delivery through the pump's
272
- busy->idle path. Silence on a failed fetch is also the better product
273
- behavior: alerts are best-effort, and the same fetch failure already
274
- breaks the wrap-up greenlight check, so the session degrades consistently.
275
- - **v1 event names verified from source, not SSE probe**: v1.18.9 publishes
276
- `question.asked`/`question.replied`/`question.rejected`
277
- (`packages/schema/src/v1/question.ts`) and `permission.asked`/
278
- `permission.replied` (`packages/schema/src/v1/permission.ts`); the
279
- installed v1 SDK's `types.gen.d.ts` is stale (no question events, and
280
- `permission.updated` is a legacy type with no publish site). The events
281
- flow through the plugin event hook as untyped `{type, properties}`, so
282
- SDK staleness is harmless. Reply events carry `requestID` on both lines
283
- (the review killed a defensive `permissionID` fallback as dead code).
284
- - **v2 idle translation reused**: v2's SSE stream delivers the idle signal
285
- as `session.execution.*` events (the existing `translateEvent` mapping),
286
- so pause events needed translation cases there rather than a new
287
- subscription path.
@@ -1,179 +0,0 @@
1
- # Thatch Actions: Prompt Cores Shared by Nudges and /thatch Commands
2
-
3
- ## Synopsis
4
-
5
- Extract the behavior instructions currently embedded in nudge text into
6
- prompt-core constants in `src/prompts.ts`, then build `/thatch/*` user-facing
7
- commands on top of the same constants. The nudge and the command become two
8
- envelopes around one instruction body: the nudge adds escalation tiers and
9
- stop-and-wait framing, the command adds user-invocation framing. First
10
- payoff is `/thatch/defrag`, but the structure covers any behavior the nudges
11
- currently ask the agent to perform on demand.
12
-
13
- ## Background
14
-
15
- Nudges are instruction-carrying messages injected by hooks. Several encode a
16
- multi-step behavior the agent must execute (dispatch a sub-agent, fetch the
17
- extraction payload, run a skill, ack). The same behaviors have no user-facing
18
- invocation path: the user cannot say "drain the extraction queue now" or
19
- "consolidate duplicate memories now" -- they can only wait for a nudge or
20
- describe the desired behavior in prose and hope the agent maps it to the right
21
- skill + tool sequence.
22
-
23
- Two existing pieces make this cheap to add:
24
-
25
- - `src/commands.ts` already ships `/thatch/compact` and `/thatch/exit` as
26
- markdown command files synced into the opencode config dir, and already
27
- composes them from shared fragments (`sharedChecklist`,
28
- `userMessageSection`).
29
- - The "Shared prose sync across prompt variants" problem is documented:
30
- prompt prose lives in parallel functions with no single source of truth,
31
- and drift in unasserted sections passes tests. This plan makes the shared
32
- behavior text a true constant.
33
-
34
- ## Design: prompt cores and envelopes
35
-
36
- A **prompt core** is a constant string in `src/prompts.ts` holding the
37
- host-agnostic instruction body for one behavior: which tools to call, in what
38
- order, with what arguments, and what "done" looks like.
39
-
40
- An **envelope** wraps a core with delivery-specific framing:
41
-
42
- - The nudge envelope adds: escalation tier wording, the load-bearing
43
- stop-and-wait ending (see "Extraction nudge stop-and-wait wording is
44
- load-bearing" -- the stop-and-wait text is nudge-only and must NOT leak
45
- into action envelopes, because an action IS user input), and the
46
- "system nudge, not user input" disclaimer.
47
- - The action envelope adds: frontmatter description, the `# User Message`
48
- section with `$ARGUMENTS`, and any greenlight-token mechanics (wrap-up
49
- commands only).
50
-
51
- Cores are plain string constants, not template functions, so both envelopes
52
- interpolate them identically. Where a core needs host-specific tool names
53
- (`thatch_memory_remember` vs `mcp__thatch__memory_remember`), the core takes
54
- the opencode spelling and MCP consumers apply the documented token
55
- substitution, matching the existing systemPrompt/mcpInstructions convention.
56
-
57
- ## Actions v1
58
-
59
- | Command | Core behavior | Existing machinery |
60
- |---|---|---|
61
- | `/thatch/defrag` | Run `thatch_find_duplicates`, classify each pair with the thatch-dedup-classifier skill, consolidate (remember/overwrite or forget), mark pairs checked | find_duplicates + dedup classifier skill |
62
- | `/thatch/extract` | Fetch the extraction payload, run the fact-extractor skill, call `extraction_done` | get_extraction_payload + fact-extractor skill |
63
- | `/thatch/hygiene` | Run the hygiene report, act on stale and orphaned branch-scoped memories | `hygieneReport()` + `/thatch hygiene` CLI |
64
- | `/thatch/reflect` | Run the session-reflection skill over the current session | thatch-session-reflection skill |
65
-
66
- The wrap-up commands (`compact`, `exit`) stay as-is; their checklist text
67
- becomes a core reused by both templates (already true via `sharedChecklist`).
68
-
69
- `/thatch/extract` interacts with opencode's direct extraction (parent idle
70
- spawns the extractor child; the nudge is only a fallback). The action must
71
- reuse the same accept/complete semantics and must not fight the `extracting`
72
- set: if direct extraction is already running for the session, the action
73
- should report that and stop rather than double-dispatch.
74
-
75
- ## Host parity
76
-
77
- Commands are markdown or MCP prompts, synced per host. Behavior parity is the
78
- goal; format differences are documented, not hidden.
79
-
80
- - **opencode** (existing): `installOpencodeCommands` syncs markdown to
81
- `~/.config/opencode/command/thatch/` on every plugin load.
82
- - **Claude Code**: native user-level slash commands read markdown from
83
- `~/.claude/commands/thatch/` (user scope) or `.claude/commands/thatch/`
84
- (project scope), with the same `$ARGUMENTS` substitution. The sync lives in
85
- `setup.ts` alongside the existing installers and follows their patterns:
86
- idempotent content-compare like `installSkills`, both global and local
87
- scopes, reported in the setup output next to the skills report.
88
- (Deviation from the draft below: no `checkSetup` entry. That function
89
- answers one question - "is thatch set up at all" - and command files
90
- self-heal on every setup run, so flagging their absence as a startup
91
- warning would be noise.) Differences: no plugin, so no
92
- `command.execute.before` arming -- greenlight-token wrap-up commands do not
93
- ship to Claude Code; and tool names use the `mcp__thatch__` prefix. MCP
94
- hosts do have hooks now (session-start reminders, chat-wake stop hooks), so
95
- hook wiring is not the barrier; the missing piece is only the plugin's TUI
96
- control routes.
97
- - **Cursor**: no file-based commands. The stdio MCP server (`mcp.ts`) gains
98
- MCP Prompts support (`prompts/list`, `prompts/get`), which Cursor surfaces
99
- as slash commands. The dispatch switch currently handles `initialize`,
100
- `tools/list`, `tools/call`, and `ping`; prompts get a `compileTools`-style
101
- `compilePrompts()` and two new cases. Prompt arguments use MCP's
102
- `{{argument}}`-style templating, so each action core gets an MCP-prompt
103
- rendering alongside the markdown rendering.
104
-
105
- README gains "What works in Claude Code" and "What works in Cursor" sections
106
- listing per-host support without restating the shared docs. The parity matrix
107
- in `docs/dev/mcp-parity.md` gets an Actions row next to the existing
108
- Wrap-up commands row.
109
-
110
- ## Session ID plumbing
111
-
112
- Since the plan was drafted, `HostToolContext` (tool-defs.ts) landed: every
113
- tool's `execute` receives the invoking session's ID as a third parameter on
114
- the opencode path, and the watch tools already consume it. `extraction_done`
115
- also already has the optional `session_id` with the omit-vs-parent rule in
116
- its description. The remaining work is narrower than first planned:
117
-
118
- - `get_extraction_payload`: `session_id` becomes optional. `execute` uses
119
- `args.session_id ?? host?.sessionID`. On the opencode path the wrapper
120
- supplies the invoking session, so omitted means "this session."
121
- - MCP path: the host context is omitted (no session concept), so an omitted
122
- `session_id` returns a clear error telling the model to pass the parent
123
- session's ID from the extraction nudge.
124
-
125
- One wrinkle to document: the zod arg shape is shared between hosts, so
126
- optionality is global -- you cannot make the arg required on MCP and optional
127
- on opencode at the schema level. Enforcement happens in `execute` per host,
128
- and the arg description must carry the rule so the model self-corrects.
129
-
130
- The rule the LLM must apply, stated everywhere the tools are described:
131
-
132
- > Omit `session_id` when acting on the session you are running in. Pass the
133
- > parent session's `session_id` when running inside a sub-agent that was
134
- > dispatched to process another session's extraction queue.
135
-
136
- This rule appears in: the arg descriptions in `tool-defs.ts`, the extraction
137
- prompt core (used by both nudge and action envelopes), the fact-extractor
138
- skill, and the dev docs. Tests cover both paths: omit-resolves-to-current
139
- and explicit-parent-ID (the existing sub-agent drain bug's regression path).
140
-
141
- ## Decisions
142
-
143
- - Prompt cores live in `src/prompts.ts`, not a new module; `commands.ts`
144
- imports them. One prompt module, consistent with the existing module map.
145
- - Actions are generated, not hand-written: each action has one core and a
146
- per-host renderer, so a wording change cannot drift between nudge and
147
- command.
148
- - No `--dry-run` flag for defrag in v1; the dedup classifier skill already
149
- asks for pair-by-pair verdicts, which is the interactive safety net.
150
- - MCP Prompts for Cursor are additive to `mcp.ts` and do not change the
151
- tools/list contract.
152
-
153
- ## Testing
154
-
155
- - `tests/commands.test.ts` (or the existing plugin tests): command-file
156
- content assertions per host, including that each action's markdown contains
157
- its core text verbatim.
158
- - Claude setup tests: command sync writes, updates only on content change,
159
- and `checkSetup` flags a missing or stale command file.
160
- - MCP tests: `prompts/list` returns every action; `prompts/get` interpolates
161
- arguments; an omitted-session-ID `get_extraction_payload` call errors with
162
- the pass-the-parent-ID message (no host context on the MCP path).
163
- - Plugin tests: `get_extraction_payload` without `session_id` resolves via
164
- the host context to the invoking session; with an explicit parent ID,
165
- drains the parent's queue (the existing sub-agent drain bug's regression
166
- path).
167
- - Parity guard: a test asserts every opencode command def has a
168
- corresponding MCP prompt (and vice versa) except the documented exclusions
169
- (wrap-up greenlight commands on non-opencode hosts).
170
-
171
- ## Docs
172
-
173
- - `docs/dev/features/commands.md` and `docs/user/commands.md` (two-tier):
174
- action list, host parity matrix, session ID rule.
175
- - `docs/dev/mcp-parity.md`: Actions row in the parity matrix; revisit the
176
- Wrap-up commands row if greenlight arming ever lands on MCP hosts.
177
- - README: "What works in Claude Code" / "What works in Cursor" sections.
178
- - QA use cases: one auto use case for `/thatch/defrag` end-to-end, one for
179
- `/thatch/extract` with omitted session ID.