@jeffober/thatch 0.1.48 → 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.
- package/README.md +43 -4
- package/bin/thatch +31 -16
- package/docs/dev/README.md +28 -6
- package/docs/dev/features/behavior-engine.md +3 -2
- package/docs/dev/features/cicd.md +1 -1
- package/docs/dev/features/compaction-recovery.md +1 -1
- package/docs/dev/features/cross-session-chat.md +4 -2
- package/docs/dev/features/extraction.md +10 -2
- package/docs/dev/features/hygiene.md +3 -1
- package/docs/dev/features/multi-host.md +4 -2
- package/docs/dev/features/notifications.md +121 -4
- package/docs/dev/features/nudge-pipeline.md +4 -2
- package/docs/dev/features/opencode-plugin.md +133 -0
- package/docs/dev/features/qa-system.md +43 -0
- package/docs/dev/features/repo-identity.md +1 -1
- package/docs/dev/features/session-lifecycle.md +5 -2
- package/docs/dev/features/watchers.md +12 -5
- package/docs/dev/gotchas.md +36 -3
- package/docs/dev/mcp-parity.md +4 -3
- package/docs/dev/setup-and-hooks.md +6 -1
- package/docs/plans/llm-alerts.md +287 -0
- package/docs/plans/prediction-consolidation.md +3 -3
- package/docs/user/README.md +7 -3
- package/docs/user/notifications.md +63 -4
- package/docs/user/setup.md +4 -0
- package/docs/user/watchers.md +15 -7
- package/package.json +3 -1
- package/src/alerts.ts +360 -0
- package/src/capabilities.ts +174 -0
- package/src/chat.ts +63 -5
- package/src/commands.ts +44 -5
- package/src/config.ts +32 -1
- package/src/db.ts +80 -0
- package/src/embeddings.ts +43 -0
- package/src/extraction.ts +124 -0
- package/src/index.ts +38 -1345
- package/src/opencode/v1.ts +54 -0
- package/src/opencode/v2.ts +561 -0
- package/src/os-args.ts +99 -0
- package/src/prompts.ts +77 -1
- package/src/runtime.ts +1815 -0
- package/src/seed-behaviors.ts +1 -1
- package/src/session-db.ts +32 -0
- package/src/tool-defs.ts +185 -19
- package/src/tools.ts +16 -35
- 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 (
|
|
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 (
|
|
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/
|
|
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
|
package/docs/user/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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.
|
package/docs/user/setup.md
CHANGED
|
@@ -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
|
package/docs/user/watchers.md
CHANGED
|
@@ -111,9 +111,21 @@ Other tools:
|
|
|
111
111
|
|
|
112
112
|
## Lifetime
|
|
113
113
|
|
|
114
|
-
Watchers
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
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",
|