@jeffober/thatch 0.1.47 → 0.2.0

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 (47) hide show
  1. package/README.md +43 -4
  2. package/artifacts/skills/thatch-plan-refinement.md +2 -0
  3. package/bin/thatch +31 -16
  4. package/docs/dev/README.md +28 -6
  5. package/docs/dev/features/behavior-engine.md +3 -2
  6. package/docs/dev/features/cicd.md +1 -1
  7. package/docs/dev/features/compaction-recovery.md +1 -1
  8. package/docs/dev/features/cross-session-chat.md +4 -2
  9. package/docs/dev/features/extraction.md +10 -2
  10. package/docs/dev/features/hygiene.md +3 -1
  11. package/docs/dev/features/multi-host.md +4 -2
  12. package/docs/dev/features/notifications.md +121 -4
  13. package/docs/dev/features/nudge-pipeline.md +4 -2
  14. package/docs/dev/features/opencode-plugin.md +133 -0
  15. package/docs/dev/features/qa-system.md +43 -0
  16. package/docs/dev/features/repo-identity.md +1 -1
  17. package/docs/dev/features/session-lifecycle.md +5 -2
  18. package/docs/dev/features/watchers.md +12 -5
  19. package/docs/dev/gotchas.md +36 -3
  20. package/docs/dev/mcp-parity.md +4 -3
  21. package/docs/dev/setup-and-hooks.md +6 -1
  22. package/docs/plans/llm-alerts.md +287 -0
  23. package/docs/plans/prediction-consolidation.md +3 -3
  24. package/docs/user/README.md +7 -3
  25. package/docs/user/notifications.md +63 -4
  26. package/docs/user/setup.md +4 -0
  27. package/docs/user/watchers.md +15 -7
  28. package/package.json +3 -1
  29. package/src/alerts.ts +360 -0
  30. package/src/capabilities.ts +174 -0
  31. package/src/chat.ts +63 -5
  32. package/src/commands.ts +44 -5
  33. package/src/config.ts +32 -1
  34. package/src/db.ts +80 -0
  35. package/src/embeddings.ts +43 -0
  36. package/src/extraction.ts +124 -0
  37. package/src/index.ts +38 -1345
  38. package/src/opencode/v1.ts +54 -0
  39. package/src/opencode/v2.ts +561 -0
  40. package/src/os-args.ts +99 -0
  41. package/src/prompts.ts +77 -1
  42. package/src/runtime.ts +1815 -0
  43. package/src/seed-behaviors.ts +1 -1
  44. package/src/session-db.ts +32 -0
  45. package/src/tool-defs.ts +185 -19
  46. package/src/tools.ts +16 -35
  47. package/src/watchers.ts +95 -14
@@ -0,0 +1,287 @@
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.
@@ -92,12 +92,12 @@ predictions that scored above threshold in the same `scorePredictionNudge`
92
92
  call. This is the direct analog of nak's `samskara_fires.cohort_id`.
93
93
 
94
94
  **Write path:** `scorePredictionNudge` currently returns `PredictionNudgeItem[]`
95
- and is called from two sites (index.ts auto-fire, sideband.ts MCP path). Add a
95
+ and is called from two sites (runtime.ts auto-fire, sideband.ts MCP path). Add a
96
96
  `recordFires` method that takes the scored predictions + a generated cohort_id
97
97
  and inserts one row per prediction. The callers generate the cohort_id before
98
98
  calling `scorePredictionNudge` and pass it through for recording.
99
99
 
100
- Both callers must record fires. The opencode path (index.ts) has the session_id.
100
+ Both callers must record fires. The opencode path (runtime.ts) has the session_id.
101
101
  The MCP path (sideband.ts) does not have a session_id in the sideband request --
102
102
  pass null or derive from the socket connection.
103
103
 
@@ -278,7 +278,7 @@ db.ts.
278
278
  **Code changes by file:**
279
279
  - `src/db.ts` -- new table, 4-5 new methods (recordFires, fireCount,
280
280
  cofireCount, totalCohorts, findPredictionDuplicates, findCoFireConstellations)
281
- - `src/index.ts` -- generate cohort_id, pass to recordFires in auto-fire path
281
+ - `src/runtime.ts` -- generate cohort_id, pass to recordFires in auto-fire path
282
282
  - `src/sideband.ts` -- same for MCP path
283
283
  - `src/hygiene.ts` -- add prediction dedup + constellation counts to report
284
284
  - `src/prompts.ts` -- compound minting guidance in all three prompt variants
@@ -13,7 +13,7 @@ This README is the overview. Each feature has its own guide:
13
13
  - [extraction.md](extraction.md): automatic fact extraction from tool calls
14
14
  - [watchers.md](watchers.md): event-driven notifications (PR, branch, and command watching)
15
15
  - [cross-session-chat.md](cross-session-chat.md): sessions messaging each other on one machine
16
- - [notifications.md](notifications.md): banner and voice notifications, user config
16
+ - [notifications.md](notifications.md): banner and voice notifications, automatic LLM alerts (pause / done / error), user config
17
17
  - [prediction-engine.md](prediction-engine.md): user decision model
18
18
  - [behavior-engine.md](behavior-engine.md): agent self-discipline rules
19
19
  - [default-behaviors.md](default-behaviors.md): what ships automatically
@@ -56,7 +56,9 @@ For local development before publishing, use a file path:
56
56
  { "plugin": ["./path/to/thatch/src/index.ts"] }
57
57
  ```
58
58
 
59
- Or place the thatch repo in `.opencode/plugins/` for auto-loading.
59
+ The same file loads on both opencode lines (1.x via its `server` export,
60
+ 2.x via its default export). Or place the thatch repo in
61
+ `.opencode/plugins/` for auto-loading.
60
62
 
61
63
  ### Claude Code
62
64
 
@@ -442,7 +444,9 @@ Most configuration needs none. Two layers exist:
442
444
 
443
445
  - **Preferences the agent manages** live in
444
446
  `~/.config/thatch/config.json` (beside the database): notification mode,
445
- voice, and sound. Ask your agent ("set notifications to banner only") or
447
+ voice, sound, and the automatic LLM alerts (pause / done / error, each
448
+ independently set to banner, voice, both, or none). Ask your agent ("set
449
+ notifications to banner only", "silence the done alert") or
446
450
  edit the file by hand. See [notifications.md](notifications.md).
447
451
  - **Environment variables** for infrastructure:
448
452
 
@@ -2,11 +2,19 @@
2
2
 
3
3
  Thatch can notify you out-of-band when something happens worth leaving the
4
4
  terminal for: a CI run finishing, a merge or deploy completing, a watched PR
5
- receiving a comment. The notification is a desktop banner, a spoken voice
6
- announcement, or both, depending on your preferences.
5
+ receiving a comment, or your LLM finishing its work while you are in another
6
+ window. The notification is a desktop banner, a spoken voice announcement, or
7
+ both, depending on your preferences.
7
8
 
8
- This works on every host (opencode, Claude Code, Cursor) because it is plain
9
- child processes, not host plumbing.
9
+ Two mechanisms:
10
+
11
+ - **Agent-initiated** (`thatch_notify_user`): the LLM decides something is
12
+ worth interrupting you for and calls the tool. Works on every host
13
+ (opencode, Claude Code, Cursor) because it is plain child processes, not
14
+ host plumbing.
15
+ - **Automatic alerts** (opencode only): the plugin watches the session and
16
+ notifies you when the LLM pauses for your input or finishes a round of real
17
+ work -- no tool call required. See the Alerts section below.
10
18
 
11
19
  ## The tools
12
20
 
@@ -74,6 +82,48 @@ The agent can manage the whole file for you through `config_get` /
74
82
  `config_set`; edits merge at the field level, so the agent changing your
75
83
  voice cannot wipe your mode.
76
84
 
85
+ ## Alerts: notifications without a tool call
86
+
87
+ On opencode, the plugin watches your session and alerts you automatically:
88
+
89
+ | Alert | Fires when | Default |
90
+ |-------|-----------|---------|
91
+ | `pause` | The LLM asks you an interactive question, or requests a permission it needs approved. | banner |
92
+ | `done` | A round of real work finishes -- meaning the turn started from a prompt you typed AND ran at least one real tool. | banner |
93
+ | `error` | The session fails with nothing to recover it, or opencode's inactivity sweeper kills a still-running session (a project directory quiet for 60 minutes gets its executions interrupted). Failures opencode retries through are silent, and so are turns you aborted yourself. | banner |
94
+
95
+ Rounds that stay silent even when they ran real tools: anything triggered by
96
+ async agent activity rather than a prompt you typed -- thatch's own nudges,
97
+ background-task completions, and watcher wake-ups. On opencode v2 those
98
+ wakes are invisible by design (the injected message is hidden from the TUI,
99
+ and plugin toasts are unavailable), so a watcher-driven round produces no
100
+ visible feedback at all -- that is why the silence rule exists, and why the
101
+ escape hatch matters: the LLM sees the watcher event in its context and can
102
+ call `thatch_notify_user` when the outcome is worth interrupting you for.
103
+ A round delegated entirely to subagents stays silent too (the dispatch is
104
+ bookkeeping; the child sessions never alert).
105
+
106
+ Banners carry the session title, so when you juggle concurrent sessions you
107
+ know which one spoke. Voice is opt-in per alert: set the mode to `voice` or
108
+ `both` on the events you want spoken.
109
+
110
+ ```json
111
+ {
112
+ "alerts": {
113
+ "pause": { "mode": "both" },
114
+ "done": { "mode": "banner" },
115
+ "error": { "mode": "both" }
116
+ }
117
+ }
118
+ ```
119
+
120
+ Each event's `mode` takes the same values as `notifications.mode`. Configure
121
+ them the same two ways -- ask your agent, or edit `config.json` directly.
122
+
123
+ When you want the LLM itself to decide something is worth interrupting you
124
+ for (a decision it cannot make alone), that is what `thatch_notify_user` is
125
+ for -- the automatic alerts and the tool are independent channels.
126
+
77
127
  ## Limitations
78
128
 
79
129
  - Banners are best-effort by nature of the OS. The agent can only report
@@ -82,3 +132,12 @@ voice cannot wipe your mode.
82
132
  are short by design.
83
133
  - There is no debounce. If an agent polls a status in a tight loop, it should
84
134
  notify once at the end, not per poll.
135
+ - Alerts are opencode-only. Claude Code and Cursor have no plugin event
136
+ stream to watch; their hosts can only notify through
137
+ `thatch_notify_user`.
138
+ - Alerts cannot tell whether your terminal is focused. A `done` banner can
139
+ land while you are looking at the TUI; set the event's mode to `none` if
140
+ that bothers you more than an occasional redundant banner.
141
+ - A plugin reload or restart mid-round (a v2 plugin upgrade, a save to a dev
142
+ shim tree) loses that round's in-memory state and stays silent. The next
143
+ round alerts normally.
@@ -40,6 +40,10 @@ Before publishing, use a file path:
40
40
  { "plugin": ["./path/to/thatch/src/index.ts"] }
41
41
  ```
42
42
 
43
+ The same file loads on both opencode lines: opencode 1.x reads its `server`
44
+ export, opencode 2.x reads its default export (the merged plugin
45
+ definition). No path change when you switch opencode versions.
46
+
43
47
  Or place the thatch repo in `.opencode/plugins/` for auto-loading.
44
48
 
45
49
  ## Claude Code
@@ -111,9 +111,21 @@ Other tools:
111
111
 
112
112
  ## Lifetime
113
113
 
114
- Watchers are in-memory and process-scoped:
115
-
116
- - Ending the opencode process (or restarting it) drops all watchers.
114
+ Watchers poll from the plugin process, and their definitions survive
115
+ restarts:
116
+
117
+ - Ending the opencode process (or restarting it) stops the polling.
118
+ The watch definitions are kept, though: when you resume the session
119
+ that created them (session picker, or reopening the tab), the
120
+ watches re-arm automatically on the session's first message, and a
121
+ notice in the conversation says what was re-armed. There is no
122
+ re-registration step - the policy you stated when creating the
123
+ watch is still there.
124
+ - Watch definitions whose 8-hour TTL expired while the session was
125
+ away are not re-armed; the re-arm notice reports how many expired.
126
+ - If a watched session is never resumed, a live session in the same
127
+ project hears a one-line notice that the watch died with its
128
+ session, so a watch that will never fire is at least explained.
117
129
  - A watcher expires after 8 hours by default.
118
130
  - Each session can hold up to 5 active watchers (all sources share
119
131
  the budget).
@@ -123,10 +135,6 @@ Watchers are in-memory and process-scoped:
123
135
  are cancelled, expire, or the process ends. Command watches are
124
136
  always one-shot.
125
137
 
126
- If opencode restarts while a watch is active, re-register it - the
127
- session's conversation survives, so the policy you stated is still
128
- there.
129
-
130
138
  ## Requirements and limitations
131
139
 
132
140
  - **opencode only.** MCP hosts (Claude Code, Cursor) have no way for a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.47",
3
+ "version": "0.2.0",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -30,6 +30,8 @@
30
30
  "devDependencies": {
31
31
  "@opencode-ai/plugin": "^1.17.13",
32
32
  "@opencode-ai/sdk": "1.17.13",
33
+ "@opencode/client": "2.0.14",
34
+ "@opencode/plugin": "2.0.14",
33
35
  "@types/bun": "^1.3.14",
34
36
  "markdownlint-cli2": "^0.23.0",
35
37
  "typescript": "^5.7.0",