@jeffober/thatch 0.1.41 → 0.1.42
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 +21 -11
- package/bin/release +4 -1
- package/bin/thatch +189 -33
- package/docs/dev/README.md +6 -4
- package/docs/dev/features/cli.md +20 -9
- package/docs/dev/features/cross-session-chat.md +125 -66
- package/docs/dev/features/multi-host.md +4 -3
- package/docs/dev/features/watchers.md +113 -23
- package/docs/dev/mcp-parity.md +1 -1
- package/docs/user/README.md +3 -2
- package/docs/user/cli.md +57 -9
- package/docs/user/cross-session-chat.md +72 -24
- package/docs/user/watchers.md +61 -16
- package/package.json +2 -1
- package/src/chat-names.ts +6 -7
- package/src/chat.ts +479 -148
- package/src/config.ts +34 -1
- package/src/db.ts +54 -6
- package/src/index.ts +87 -24
- package/src/prompts.ts +114 -50
- package/src/tool-defs.ts +294 -97
- package/src/tools.ts +2 -0
- package/src/watchers.ts +358 -23
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Cross-Session Chat
|
|
2
2
|
|
|
3
3
|
Cross-session chat lets independent opencode sessions on one machine message
|
|
4
|
-
each other through thatch:
|
|
5
|
-
|
|
6
|
-
recipient is woken with a prompt when mail arrives, so
|
|
7
|
-
route through the human ("ask the other session
|
|
8
|
-
green" instead of "Jeff, go ask").
|
|
4
|
+
each other through thatch: sessions join a shared directory (automatically,
|
|
5
|
+
under assigned names), see each other in the list, and messages land in
|
|
6
|
+
inboxes. An idle recipient is woken with a prompt when mail arrives, so
|
|
7
|
+
coordination does not route through the human ("ask the other session
|
|
8
|
+
whether the release is green" instead of "Jeff, go ask").
|
|
9
9
|
|
|
10
10
|
User-facing behavior is documented in
|
|
11
11
|
[docs/user/cross-session-chat.md](../../user/cross-session-chat.md).
|
|
@@ -14,8 +14,11 @@ User-facing behavior is documented in
|
|
|
14
14
|
|
|
15
15
|
- `thatch_chat_register` / `thatch_chat_list` / `thatch_chat_send` /
|
|
16
16
|
`thatch_chat_read` / `thatch_chat_unregister` / `thatch_chat_broadcast`
|
|
17
|
-
tools (shared across hosts; opencode injects identity, MCP hosts
|
|
18
|
-
|
|
17
|
+
tools (shared across hosts; opencode injects identity, MCP hosts pass the
|
|
18
|
+
name their thatch hook printed as the `as` argument)
|
|
19
|
+
- Automatic registration: the plugin inserts a top-level session's
|
|
20
|
+
directory row on its first idle event, with an assigned never-reused
|
|
21
|
+
name (`chat.autoRegister: false` opts out)
|
|
19
22
|
- A shared SQLite directory and inbox in thatch.db, writable by any opencode
|
|
20
23
|
process on the machine
|
|
21
24
|
- A per-process poller that heartbeats hosted sessions and delivers wake
|
|
@@ -54,40 +57,63 @@ outlives a process, so liveness needs a signal beyond process lifetime.
|
|
|
54
57
|
|
|
55
58
|
### Registration and identity
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
Sessions are auto-registered by the plugin: the first `session.status`
|
|
61
|
+
idle event for a top-level session inserts its directory row (unless
|
|
62
|
+
`chat.autoRegister: false`). Names are assigned, never claimed:
|
|
63
|
+
`<slug>-<counter>` where the slug is the session title lowercased and
|
|
64
|
+
hyphenated (pool-draw slug when the title is still opencode's placeholder)
|
|
65
|
+
and the counter comes from a per-base counter row
|
|
66
|
+
(`chat_name_counters`, src/db.ts) that only ever increments - drawn
|
|
67
|
+
atomically via INSERT ... ON CONFLICT ... RETURNING. A name is therefore
|
|
68
|
+
minted exactly once per machine: pruning an auto-registered row (host
|
|
69
|
+
silent for CHAT_AUTO_TTL_DAYS = 7 days, swept hourly by the poller) can
|
|
70
|
+
never reissue its name, which is what makes the prune safe. The session's
|
|
71
|
+
live title rides the topic column, refreshed on every idle by
|
|
72
|
+
`refreshAutoTopic` - auto rows only; legacy rows keep their old model-set topics.
|
|
73
|
+
|
|
74
|
+
`chat_register` (no arguments) is the explicit path: idempotent ensure for
|
|
75
|
+
opencode (identity is the host session ID, which the model cannot know or
|
|
76
|
+
forge), a fresh identity per conversation on MCP hosts, or a reclaim of an
|
|
77
|
+
existing name via `as`. Re-registering keeps the name. Unregistered
|
|
78
|
+
sessions are invisible and unmessageable in both directions. Only
|
|
79
|
+
top-level sessions should register - sub-agent children are ephemeral -
|
|
80
|
+
which the tool descriptions and system prompt state; there is no
|
|
81
|
+
mechanical barrier, a documented trust posture.
|
|
82
|
+
|
|
83
|
+
### Leave tombstones
|
|
84
|
+
|
|
85
|
+
Every exit path - `chat_unregister` and `session.deleted` alike - writes a
|
|
86
|
+
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
87
|
+
directory row. The tombstone is what makes a leave stick: both
|
|
88
|
+
registration paths consult it (the plugin's idle auto-registerer is
|
|
89
|
+
suppressed outright; the MCP hook's ensure-register prints the leave line
|
|
90
|
+
instead of rejoining), so a leave cannot be silently undone by the next
|
|
91
|
+
idle moment. The gate lives in `ChatStore.register` via `#tombstoneGate`
|
|
92
|
+
(src/chat.ts), which also closes the in-flight race: an auto-register
|
|
93
|
+
IIFE whose session was deleted during its title-fetch await finds the
|
|
94
|
+
tombstone and stops instead of resurrecting a dead session. An explicit
|
|
95
|
+
`chat_register` clears the tombstone before re-registering (fresh name -
|
|
96
|
+
assigned identities are never reused), and tombstones age out in the
|
|
97
|
+
hourly prune (7-day TTL; a resumed session after expiry simply
|
|
98
|
+
auto-registers under a fresh name).
|
|
99
|
+
|
|
100
|
+
Because names are system-minted and never recycled, the impersonation
|
|
101
|
+
surface shrinks to whatever the operator runs: every agent on this machine
|
|
78
102
|
is the operator's agent.
|
|
79
103
|
|
|
80
104
|
### Topics
|
|
81
105
|
|
|
82
|
-
The
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
The topic is the roster's "who is working on what" signal. For
|
|
107
|
+
auto-registered sessions it is the live session title, refreshed on every
|
|
108
|
+
idle event - the auto-titler usually lands a real title within a turn or
|
|
109
|
+
two, so the roster converges to the actual work with nobody filling in
|
|
110
|
+
forms. Legacy rows (from the claimed-names era) keep whatever topic was
|
|
111
|
+
set at their registration: free text, sanitized to one roster line
|
|
86
112
|
(whitespace runs collapse, 80-character cap, advisory - oversizing degrades
|
|
87
|
-
rather than errors).
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
rather than errors). There is no tool path to set a topic anymore - topics
|
|
114
|
+
are title-derived for everything the current code mints. Broadcast senders
|
|
115
|
+
benefit most: `chat_broadcast` answers "which of you is working on X?"
|
|
116
|
+
with the roster already in hand.
|
|
91
117
|
|
|
92
118
|
### Messages
|
|
93
119
|
|
|
@@ -167,19 +193,36 @@ turn reads. The accepted edge: a real user message whose every visible
|
|
|
167
193
|
part starts with the prefix is skipped the same way, losing that one turn's
|
|
168
194
|
advisory nudges.
|
|
169
195
|
|
|
196
|
+
### The untrusted-content frame
|
|
197
|
+
|
|
198
|
+
Message bodies are other agents' text delivered verbatim into the reading
|
|
199
|
+
model's context - a prompt-injection surface by construction. The watcher
|
|
200
|
+
privacy model solves the same surface with pointer-only notifications, but
|
|
201
|
+
chat's payload IS the body, so `chat_read` frames the boundary instead:
|
|
202
|
+
`chatInboxFrame()` (src/prompts.ts) wraps the message lines in
|
|
203
|
+
begin/end fences with an explicit do-not-follow warning. The frame exists only in
|
|
204
|
+
the tool output - persisted rows are untouched - and pairs with the wake
|
|
205
|
+
nudge's anti-loop rule and the isChatEchoParts skip as the feature's three
|
|
206
|
+
injection-hygiene layers.
|
|
207
|
+
|
|
170
208
|
### CLI access
|
|
171
209
|
|
|
172
210
|
The user watches the conversation without an agent: `thatch chat list`
|
|
173
|
-
renders the roster with human-readable heartbeat ages, and
|
|
174
|
-
|
|
175
|
-
`
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
renders the roster with human-readable heartbeat ages, and `thatch chat
|
|
212
|
+
tail` follows the message stream as cards. The backlog renders only the
|
|
213
|
+
last `CHAT_TAIL_DEFAULT_LIMIT` (20) messages via `chatTailBacklog()`;
|
|
214
|
+
`filterChatTailRows()` narrows every feed snapshot (backlog and follow
|
|
215
|
+
polls alike) by body regexes, participant-name substrings, and a
|
|
216
|
+
half-open time window. The diff itself is `chatTailDiff()` (unit-tested
|
|
217
|
+
in `tests/chat.test.ts`): sent cards on first view, then new sends and
|
|
218
|
+
newly-read messages per poll, with the diff state seeded from the full
|
|
219
|
+
feed so neither the limit nor a mid-follow rename/unregister can
|
|
220
|
+
resurface old rows as sent events. `chat_messages.via_broadcast` marks
|
|
221
|
+
fan-out rows so the tail renders one `-> broadcast` card per recipient
|
|
222
|
+
instead of what would otherwise look like identical direct sends. The
|
|
223
|
+
CLI takes no chat write actions (the wake machinery owns those paths);
|
|
224
|
+
the only writes it can trigger are the schema migrations that run
|
|
225
|
+
whenever any thatch process opens the database.
|
|
183
226
|
|
|
184
227
|
## Interactions with other features
|
|
185
228
|
|
|
@@ -197,14 +240,23 @@ process opens the database.
|
|
|
197
240
|
non-thatch tool calls made during a chat notification turn are buffered
|
|
198
241
|
and extracted like any other turn's.
|
|
199
242
|
- Multi-host ([multi-host.md](multi-host.md)): the chat tools are shared -
|
|
200
|
-
MCP hosts
|
|
201
|
-
the
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
can start a turn in the client's conversation.
|
|
243
|
+
MCP hosts get their identity from the thatch hook, which ensure-registers
|
|
244
|
+
the conversation's stable session ID (`mcp_<hash of host session id>`)
|
|
245
|
+
and prints the assigned name plus unread count on every prompt; chat
|
|
246
|
+
tools take that name as `as`. Wake-up delivery remains opencode-only: no
|
|
247
|
+
MCP server can start a turn in the client's conversation.
|
|
205
248
|
|
|
206
249
|
## Multi-host delivery tiers
|
|
207
250
|
|
|
251
|
+
Chat works on every host, and can be disabled entirely: `chat.enabled:
|
|
252
|
+
false` in the thatch config makes every chat tool refuse (a `[disabled]`
|
|
253
|
+
reply naming the re-enable path), stops the poller at init, omits the
|
|
254
|
+
system prompt's chat section, and silences the hook lines. The tools
|
|
255
|
+
re-read the config per call, so a toggle applies immediately; the poller
|
|
256
|
+
gate is read once at init and needs a restart. The hook lines gate on the
|
|
257
|
+
same config, so a disabled feature never advertises itself through any
|
|
258
|
+
surface.
|
|
259
|
+
|
|
208
260
|
Chat works on every host. Earlier designs for the MCP path - a JSONL
|
|
209
261
|
temp-file inbox, a socket or daemon - were rejected: the DB already is the
|
|
210
262
|
inbox, and MCP hosts cannot hold connections across turns, so both
|
|
@@ -217,12 +269,15 @@ wake analog is the existing hook channel.
|
|
|
217
269
|
| claude code | yes | flush-tools hook line | no |
|
|
218
270
|
| cursor | yes | flush-tools hook line | no |
|
|
219
271
|
|
|
220
|
-
MCP identity is the
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
272
|
+
MCP identity is anchored by the host hook: `chatHookLine(sessionID)` in
|
|
273
|
+
`bin/thatch` ensure-registers `mcp_<sha256(host session id)[0:12]>` on
|
|
274
|
+
every prompt and prints the assigned name plus unread count, so the
|
|
275
|
+
identity is stable across the ephemeral conversations those harnesses run
|
|
276
|
+
(and a conversation without the hook gets a fresh per-conversation
|
|
277
|
+
identity from `chat_register`). `chat_status` is the quiet check
|
|
278
|
+
(registered flag + pending/total); the flush-tools hook line names the
|
|
279
|
+
caller's identity and mailbox, and is absent entirely when chat is
|
|
280
|
+
disabled - absence is silent by construction.
|
|
226
281
|
Staleness semantics differ by kind: an opencode row's stale age means the
|
|
227
282
|
process is gone (broadcast skips it), while an mcp row's age only means
|
|
228
283
|
"between turns" (broadcast always delivers).
|
|
@@ -231,19 +286,23 @@ process is gone (broadcast skips it), while an mcp row's age only means
|
|
|
231
286
|
|
|
232
287
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
233
288
|
threshold 10 minutes, re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
234
|
-
per hour. There are no environment overrides yet; add
|
|
235
|
-
`THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
289
|
+
per hour, auto-row TTL 7 days. There are no environment overrides yet; add
|
|
290
|
+
them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
236
291
|
|
|
237
292
|
## Source files
|
|
238
293
|
|
|
239
|
-
- `src/chat.ts` - ChatStore (directory + inbox SQL,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- `src/
|
|
244
|
-
|
|
294
|
+
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
295
|
+
slugify, counter bump, insert, prune, topic refresh), ChatPoller
|
|
296
|
+
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
297
|
+
staleness helper
|
|
298
|
+
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
299
|
+
the fallback base for title-less registrations
|
|
300
|
+
- `src/db.ts` - the chat tables in schema init (including
|
|
301
|
+
chat_name_counters and the auto column), the NOCASE collation migration
|
|
302
|
+
and topic column migration, delegated methods
|
|
245
303
|
- `src/tool-defs.ts` - the chat tool definitions
|
|
246
|
-
- `src/index.ts` - poller construction, delivery closure, idle
|
|
304
|
+
- `src/index.ts` - poller construction, delivery closure, idle
|
|
305
|
+
auto-registration + topic refresh, idle flush,
|
|
247
306
|
session.deleted unregister, dispose, transcript echo in
|
|
248
307
|
tool.execute.after
|
|
249
308
|
- `src/prompts.ts` - `chatNotificationNudge()`, `chatEchoText()` /
|
|
@@ -21,7 +21,8 @@ host-agnostic tools plus the opencode-only tools, which the MCP server
|
|
|
21
21
|
filters out. The opencode-only set covers host-session capabilities MCP
|
|
22
22
|
hosts lack: `get_session_info`, `session_search`/`session_get` (no session
|
|
23
23
|
concept or session database), the `watch_create`/`watch_branch_create`/
|
|
24
|
-
`watch_list`/`watch_cancel` watcher tools (no
|
|
24
|
+
`watch_command_create`/`watch_list`/`watch_cancel` watcher tools (no
|
|
25
|
+
poller or proactive-prompt
|
|
25
26
|
channel), and the `chat_register`/`chat_list`/`chat_send`/`chat_read`/
|
|
26
27
|
`chat_unregister`/`chat_broadcast` chat tools (no session identity or
|
|
27
28
|
wake-up channel). The
|
|
@@ -188,7 +189,7 @@ behavior, with some features having no MCP counterpart.
|
|
|
188
189
|
| Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
|
|
189
190
|
| `get_session_info` (session identity) | Yes | No | No |
|
|
190
191
|
| `session_search` / `session_get` (conversation archaeology) | Yes (tools) | CLI only (`thatch session ...`) | CLI only (`thatch session ...`) |
|
|
191
|
-
| `watch_*` (PR and
|
|
192
|
+
| `watch_*` (PR, branch, and command watchers) | Yes (tools) | No | No |
|
|
192
193
|
| `chat_*` (cross-session chat) | Yes | No | No |
|
|
193
194
|
|
|
194
195
|
For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
|
|
@@ -239,4 +240,4 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
|
239
240
|
- The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
|
|
240
241
|
- The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
|
|
241
242
|
- The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts` (opencode's list includes opencode-only tools; the MCP lists exclude them). The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
|
|
242
|
-
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`,
|
|
243
|
+
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, all watchers (PR, branch, and command), cross-session chat wake-up) have no MCP counterpart. The `thatch session` CLI subcommands work anywhere - they only need an opencode.db file to read.
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
Watchers are the general mechanism for event-driven notifications from
|
|
4
4
|
external sources: the model registers a watch on something outside the
|
|
5
5
|
session, thatch polls it in the background, and the plugin prompts the
|
|
6
|
-
session when a watched event happens.
|
|
7
|
-
requests (source `pr`)
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
session when a watched event happens. Three sources ship: GitHub pull
|
|
7
|
+
requests (source `pr`), GitHub branches (source `branch`, for watching
|
|
8
|
+
CI and workflow runs on main), and local shell commands (source
|
|
9
|
+
`command`, for waiting on arbitrary conditions). The registry, poller,
|
|
10
|
+
and delivery are source-agnostic - a new source adds a fetch-and-diff
|
|
10
11
|
function pair and event types.
|
|
11
12
|
|
|
12
13
|
User-facing behavior is documented in [docs/user/watchers.md](../../user/watchers.md).
|
|
@@ -14,13 +15,16 @@ User-facing behavior is documented in [docs/user/watchers.md](../../user/watcher
|
|
|
14
15
|
## What it does
|
|
15
16
|
|
|
16
17
|
- `thatch_watch_create` (PRs) / `thatch_watch_branch_create` (branches) /
|
|
17
|
-
`
|
|
18
|
+
`thatch_watch_command_create` (commands) / `thatch_watch_list` /
|
|
19
|
+
`thatch_watch_cancel` tools (all opencode-only)
|
|
18
20
|
- A background poller in the plugin process diffs each watcher's target
|
|
19
21
|
against its last-seen state
|
|
20
22
|
- Delivery prompts the session with a synthetic part - the same
|
|
21
23
|
mechanism opencode uses for background task completions
|
|
22
|
-
- Notifications carry pointer data
|
|
23
|
-
|
|
24
|
+
- Notifications carry pointer data plus machine status (author, URL,
|
|
25
|
+
check names and conclusions, exit codes); external content is fetched
|
|
26
|
+
on demand with `gh` or by re-running a watched command, never
|
|
27
|
+
delivered
|
|
24
28
|
|
|
25
29
|
## The process-lifetime decision
|
|
26
30
|
|
|
@@ -72,6 +76,12 @@ attempt delivery. Poll errors are per-watcher; one bad PR never
|
|
|
72
76
|
blocks the others. The diff is a pure function, so it is unit-tested
|
|
73
77
|
without any network access.
|
|
74
78
|
|
|
79
|
+
The interval is surfaced to the model through the registry's
|
|
80
|
+
`pollSeconds` getter: watch-creation output states when the first
|
|
81
|
+
poll lands, and notifications carry a cadence line, so a model
|
|
82
|
+
choosing between a watcher and an in-turn poll compares real numbers
|
|
83
|
+
even when `THATCH_WATCH_POLL_SECONDS` overrides the default.
|
|
84
|
+
|
|
75
85
|
Event detection:
|
|
76
86
|
|
|
77
87
|
- comments by id monotonicity (fetch is sorted descending)
|
|
@@ -89,6 +99,57 @@ Event detection:
|
|
|
89
99
|
A diff emits at most 10 events per watcher per cycle, so comment
|
|
90
100
|
floods collapse into one batch.
|
|
91
101
|
|
|
102
|
+
### One-shot watches
|
|
103
|
+
|
|
104
|
+
`watch_create` and `watch_branch_create` accept `once: true`. The
|
|
105
|
+
watcher cancels itself when its first matching event is detected, not
|
|
106
|
+
when it is delivered - the event still queues and delivers through the
|
|
107
|
+
normal pending path, so a session busy at fire time does not lose the
|
|
108
|
+
notification. Cancellation at detection time is what keeps a "tell me
|
|
109
|
+
when this run finishes" request from leaving a standing watch polling
|
|
110
|
+
a target nobody is waiting on. `watch_list` marks one-shot watchers
|
|
111
|
+
with a `[once]` tag, and the registration output states the mode.
|
|
112
|
+
|
|
113
|
+
### Command watches
|
|
114
|
+
|
|
115
|
+
The command source stretches the fetch/diff recipe in two ways:
|
|
116
|
+
|
|
117
|
+
1. **The fetch has side effects.** `runWatchedCommand()` executes the
|
|
118
|
+
watched command via `bash -c` in the project directory each cycle.
|
|
119
|
+
This is the point - the command IS the condition - but it means
|
|
120
|
+
command watchers must not block: the tool description and system
|
|
121
|
+
prompt state the contract (fast, idempotent status check; never
|
|
122
|
+
`gh run watch` or `tail -f`), and a per-run kill timeout
|
|
123
|
+
(default 30s, `THATCH_WATCH_COMMAND_TIMEOUT_SECONDS`) bounds a
|
|
124
|
+
hung command's cost to one partial poll cycle - other watchers
|
|
125
|
+
wait out the kill rather than stalling, but a blocking command
|
|
126
|
+
still burns its timeout slice every cycle. A timed-out run means
|
|
127
|
+
"not done yet": the last-seen exit is left untouched and the watch
|
|
128
|
+
keeps polling.
|
|
129
|
+
2. **The diff is a trivial exit-code check.** The command is a
|
|
130
|
+
condition variable, not a data pipe: stdout is drained (a full
|
|
131
|
+
pipe buffer would deadlock the child) but discarded, and stderr is
|
|
132
|
+
kept only for registration-time error messages. The notification
|
|
133
|
+
carries the exit code and duration; command output can be external
|
|
134
|
+
content, so it never rides a notification. This is the strictest
|
|
135
|
+
application of the notification content rule. The pipe drains are also
|
|
136
|
+
raced against a hard deadline, because a
|
|
137
|
+
command that backgrounds a long-lived child leaves a grandchild
|
|
138
|
+
holding the pipe write-ends past the kill - without the race, the
|
|
139
|
+
drains never resolve and the shared poll cycle stalls forever.
|
|
140
|
+
|
|
141
|
+
`createCommand()` runs the command once at registration as baseline
|
|
142
|
+
validation, like `createPr()`'s baseline fetch: exit 0 at baseline
|
|
143
|
+
means the condition is already met (refused, so the caller proceeds
|
|
144
|
+
now), exit 127 means the command does not exist (refused with the
|
|
145
|
+
stderr tail), and a spawn failure fails registration with a real
|
|
146
|
+
error. A baseline run that times out records the 124 sentinel as the
|
|
147
|
+
last-seen exit - the killed process may have exited 0 while dying, and
|
|
148
|
+
the watcher's first real exit 0 must fire. Registered command
|
|
149
|
+
watchers carry `once: true` inherently - "run until it returns 0,
|
|
150
|
+
then notify, then stop" is the whole concept. The `CommandRunner` is
|
|
151
|
+
injectable like `GhRunner`, so tests never spawn processes.
|
|
152
|
+
|
|
92
153
|
### Delivery
|
|
93
154
|
|
|
94
155
|
Events queue in an in-memory pending map keyed by session. Delivery
|
|
@@ -101,9 +162,15 @@ on the session with a synthetic text part built by
|
|
|
101
162
|
`watcherNotificationNudge()`. This triggers a full model turn - the
|
|
102
163
|
model reads the notification and decides whether to act. The
|
|
103
164
|
notification text borrows the background-task-completion framing
|
|
104
|
-
(system event, not user input, not approval to advance other work)
|
|
105
|
-
|
|
106
|
-
|
|
165
|
+
(system event, not user input, not approval to advance other work)
|
|
166
|
+
with one carve-out: a watch registered to gate work the user already
|
|
167
|
+
greenlit (for example "wait for CI, then merge") makes the
|
|
168
|
+
notification the continuation signal for exactly that work. The
|
|
169
|
+
carve-out lives in the nudge itself, not only the system prompt,
|
|
170
|
+
because the nudge is what the model reads at the act-or-wait decision
|
|
171
|
+
point. The wrapper also states that check-run and workflow
|
|
172
|
+
conclusions are already in the event summaries, so gh fetches are
|
|
173
|
+
needed only for logs and bodies.
|
|
107
174
|
|
|
108
175
|
Failed or gated deliveries stay pending and retry on the next poll
|
|
109
176
|
cycle or when the session next goes idle (the event hook calls
|
|
@@ -114,7 +181,10 @@ cycle or when the session next goes idle (the event hook calls
|
|
|
114
181
|
`pr_comment`, `pr_review_comment`, `pr_review_reply`,
|
|
115
182
|
`pr_review_resolved`, `pr_commit`, `pr_status`, `pr_description`,
|
|
116
183
|
`pr_ci`. The tool's `events` argument selects a subset; the default is
|
|
117
|
-
all eight.
|
|
184
|
+
all eight. Branch watchers add `branch_commit`, `branch_ci`, and
|
|
185
|
+
`branch_workflow` (the full vocabulary is `WATCHER_EVENT_TYPES` in
|
|
186
|
+
src/watchers.ts, test-enforced); command watchers register the single
|
|
187
|
+
`command_success` type.
|
|
118
188
|
|
|
119
189
|
## Interactions with other features
|
|
120
190
|
|
|
@@ -134,28 +204,48 @@ all eight.
|
|
|
134
204
|
- `THATCH_WATCH_POLL_SECONDS` - poll interval (default 60)
|
|
135
205
|
- `THATCH_WATCH_TTL_MINUTES` - watcher time-to-live (default 480)
|
|
136
206
|
- `THATCH_WATCH_MAX_PER_SESSION` - active watchers per session
|
|
137
|
-
(default 5)
|
|
207
|
+
(default 5; all sources share the budget)
|
|
208
|
+
- `THATCH_WATCH_COMMAND_TIMEOUT_SECONDS` - per-run kill timeout for
|
|
209
|
+
watched commands (default 30)
|
|
138
210
|
|
|
139
211
|
## Source files
|
|
140
212
|
|
|
141
|
-
- `src/watchers.ts` - registry, poller, gh CLI runner,
|
|
142
|
-
|
|
213
|
+
- `src/watchers.ts` - registry, poller, gh CLI runner, command runner,
|
|
214
|
+
diff functions
|
|
215
|
+
- `src/tool-defs.ts` - the watch tool definitions (see `TOOL_DEFS`)
|
|
143
216
|
- `src/index.ts` - registry construction, delivery closure,
|
|
144
217
|
session.status tracking, session.deleted cleanup, dispose
|
|
145
218
|
- `src/prompts.ts` - `watcherNotificationNudge()`, system prompt
|
|
146
219
|
Watchers section
|
|
147
220
|
- `tests/watchers.test.ts` - registry, poll, and diff unit tests
|
|
148
|
-
(mocked gh, no network)
|
|
221
|
+
(mocked gh and command runners, no network)
|
|
222
|
+
- `tests/tool-defs.test.ts` - watch tool execute-function tests
|
|
149
223
|
- `tests/qa/auto/uc-095-watchers.ts` - end-to-end lifecycle against a
|
|
150
224
|
mocked gh runner
|
|
225
|
+
- `tests/qa/auto/uc-101-command-watchers.ts` - command-watch lifecycle
|
|
226
|
+
against a mocked command runner
|
|
151
227
|
|
|
152
228
|
## Adding a new source type
|
|
153
229
|
|
|
154
|
-
The registry is a discriminated union over sources: `PrWatcher
|
|
155
|
-
`BranchWatcher` implement the same lifecycle
|
|
156
|
-
events, expiry, snapshot), and `poll()` dispatches to
|
|
157
|
-
fetch/diff pair. Adding a
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
230
|
+
The registry is a discriminated union over sources: `PrWatcher`,
|
|
231
|
+
`BranchWatcher`, and `CommandWatcher` implement the same lifecycle
|
|
232
|
+
(id, session, events, expiry, snapshot), and `poll()` dispatches to
|
|
233
|
+
the source's fetch/diff pair. Adding a new source means: new event
|
|
234
|
+
types, a fetch function and diff (or a trivial inline diff like the
|
|
235
|
+
command source's), a new arm of the `Watcher` union, a `#pollOne`
|
|
236
|
+
branch, and the tool surface for registering it. Keep the
|
|
237
|
+
notification content rule: external content enters the context on
|
|
238
|
+
explicit fetch, never via notification. Machine status fields (names,
|
|
239
|
+
conclusions, counts, exit codes) come from the API rather than
|
|
240
|
+
user-written text and belong in event summaries.
|
|
241
|
+
|
|
242
|
+
Two caveats from the command source, worth carrying to future
|
|
243
|
+
sources:
|
|
244
|
+
|
|
245
|
+
- If the source's fetch does anything beyond reading an API, inject a
|
|
246
|
+
runner (like `GhRunner`/`CommandRunner`) so tests never spawn real
|
|
247
|
+
work, and bound it with a timeout so a hung fetch cannot stall the
|
|
248
|
+
shared sequential poll cycle.
|
|
249
|
+
- If the source has no natural URL (a local command has none), emit
|
|
250
|
+
an empty `url` in the event; `watcherNotificationNudge()` omits
|
|
251
|
+
empty URLs rather than rendering a dangling space.
|
package/docs/dev/mcp-parity.md
CHANGED
|
@@ -18,7 +18,7 @@ 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 and
|
|
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 |
|
|
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 |
|
package/docs/user/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This README is the overview. Each feature has its own guide:
|
|
|
11
11
|
|
|
12
12
|
- [memory.md](memory.md): persistent memory store
|
|
13
13
|
- [extraction.md](extraction.md): automatic fact extraction from tool calls
|
|
14
|
-
- [watchers.md](watchers.md): event-driven notifications (PR and
|
|
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
16
|
- [notifications.md](notifications.md): banner and voice notifications, user config
|
|
17
17
|
- [prediction-engine.md](prediction-engine.md): user decision model
|
|
@@ -179,6 +179,7 @@ search` - JSONL output designed for piping to `jq` or `grep`.
|
|
|
179
179
|
|------|-------------|
|
|
180
180
|
| `thatch_watch_create` | Watch a GitHub PR for events (comments, review comments and replies, thread resolutions, commits, status changes, description edits, CI completions). The plugin polls in the background and prompts the session when a watched event happens. |
|
|
181
181
|
| `thatch_watch_branch_create` | Watch a GitHub branch (typically main) for commit landings, CI check-run completions, and workflow runs, with an optional workflow-name filter. |
|
|
182
|
+
| `thatch_watch_command_create` | Wait on any local condition via a shell command that exits 0 when the wait is over. One-shot: one notification on the first exit 0, then it cancels itself. |
|
|
182
183
|
| `thatch_watch_list` | List this session's active watchers. |
|
|
183
184
|
| `thatch_watch_cancel` | Cancel one of this session's watchers by id. |
|
|
184
185
|
|
|
@@ -189,7 +190,7 @@ requirements.
|
|
|
189
190
|
|
|
190
191
|
| Tool | What it does |
|
|
191
192
|
|------|-------------|
|
|
192
|
-
| `thatch_chat_register` | Join the cross-session chat directory, so other
|
|
193
|
+
| `thatch_chat_register` | Join the cross-session chat directory, so other sessions on this machine can message you. Display names are assigned by thatch, never chosen (a slug plus a never-reused counter), so a name always refers to the same session. Call with no arguments to join, rejoin after unregistering, or check your name. On hosts without session context (Claude Code, Cursor), pass the name your thatch hook printed as `as`. |
|
|
193
194
|
| `thatch_chat_list` | List registered sessions with a liveness marker (fresh = its process is alive, stale = likely gone), each session's project and topic, and your unread count. |
|
|
194
195
|
| `thatch_chat_send` | Send a message to another registered session, by name or session id. The recipient is nudged when its session is idle. |
|
|
195
196
|
| `thatch_chat_read` | Drain your inbox: all unread messages oldest-first, marked read. Senders identified by display name. |
|
package/docs/user/cli.md
CHANGED
|
@@ -47,7 +47,11 @@ Read-only access to the chat directory your opencode sessions share
|
|
|
47
47
|
```bash
|
|
48
48
|
thatch chat list # who is registered: name, age, project, topic
|
|
49
49
|
thatch chat tail # follow sent and read events, live
|
|
50
|
-
thatch chat tail --once # print the
|
|
50
|
+
thatch chat tail --once # print the last 20 messages and exit
|
|
51
|
+
thatch chat tail --once --limit 50 # a longer backlog (or --limit all)
|
|
52
|
+
thatch chat tail --match "rebase" --match "payments" # body must match both
|
|
53
|
+
thatch chat tail --from al --to bob # substring match on names
|
|
54
|
+
thatch chat tail --since 2026-09-12 --until "2026-09-13 09:00" # a window
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
`chat list` shows every registered session with how long since its last
|
|
@@ -55,19 +59,63 @@ heartbeat (a large age means the session's process is probably gone), its
|
|
|
55
59
|
project, and the topic it registered with - the fastest way to answer
|
|
56
60
|
"which session should I talk to about X?"
|
|
57
61
|
|
|
58
|
-
`chat tail` prints the conversation as
|
|
62
|
+
`chat tail` prints the conversation as styled chat cards (labels on a
|
|
63
|
+
colored background, names in color, timestamps dimmed; a drawn rule
|
|
64
|
+
separates cards, with blank lines on either side). After each name, the
|
|
65
|
+
session's registered topic appears in muted italics - so the tail tells
|
|
66
|
+
you not just who is talking but what they are working on:
|
|
59
67
|
|
|
60
68
|
```text
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
From Al Go Rithm <cross-session messaging between opencode sessions>
|
|
70
|
+
To Brute the Dream Farrier <diagnosing watcher underuse>
|
|
71
|
+
When 2026-09-12 14:02 America/Denver
|
|
72
|
+
|
|
73
|
+
CI is green on main
|
|
74
|
+
____________________________________________________________
|
|
75
|
+
|
|
76
|
+
To broadcast
|
|
77
|
+
When 2026-09-12 21:07 America/Denver
|
|
78
|
+
|
|
79
|
+
rebasing payments, hold off
|
|
80
|
+
____________________________________________________________
|
|
81
|
+
|
|
82
|
+
From Kurn the Typechecker <release QA>
|
|
83
|
+
read Marlowe the Cherry Picker <payments refactor>
|
|
84
|
+
When 2026-09-12 21:08 America/Denver
|
|
85
|
+
|
|
86
|
+
direct ping
|
|
64
87
|
```
|
|
65
88
|
|
|
66
89
|
Follow mode runs until Ctrl-C. Read events appear only in follow mode
|
|
67
|
-
(they fire when an inbox is drained after the tail started).
|
|
68
|
-
|
|
69
|
-
registers on a session's behalf;
|
|
70
|
-
via the `thatch_chat_*` tools).
|
|
90
|
+
(they fire when an inbox is drained after the tail started). When
|
|
91
|
+
timestamps are in your local timezone. The chat itself flows through the
|
|
92
|
+
agents (the CLI never sends, reads, or registers on a session's behalf;
|
|
93
|
+
that happens from opencode sessions via the `thatch_chat_*` tools).
|
|
94
|
+
|
|
95
|
+
The backlog shows the last 20 messages before follow mode takes over.
|
|
96
|
+
When messages are hidden, tail prints a one-line note on stderr saying
|
|
97
|
+
how many there were; `--limit N` raises or lowers the backlog and
|
|
98
|
+
`--limit all` shows everything. All the filters apply to live events
|
|
99
|
+
too, so a filtered tail keeps watching the same way:
|
|
100
|
+
|
|
101
|
+
- `--match PATTERN` filters message bodies. Repeat it to AND patterns
|
|
102
|
+
together: every one must match the message body.
|
|
103
|
+
- `--from NAME` and `--to NAME` match participant names (case doesn't
|
|
104
|
+
matter). A departed sender can only be found as the `unknown`
|
|
105
|
+
rendering the tail shows. Broadcast cards say `broadcast` in the To
|
|
106
|
+
slot, but the row underneath keeps the real recipient: `--to <name>`
|
|
107
|
+
finds the fan-out rows that reached them, while `--to broadcast`
|
|
108
|
+
matches nothing.
|
|
109
|
+
- `--since DT` and `--until DT` bound the window; a message counts if
|
|
110
|
+
it was sent at or after `--since` and before `--until`. The date
|
|
111
|
+
format is `YYYY-MM-DD` with an optional `HH:MM` in your local
|
|
112
|
+
timezone.
|
|
113
|
+
|
|
114
|
+
A `--until` already in the past exits after the backlog (a closed
|
|
115
|
+
window has nothing left to follow); one in the future follows until
|
|
116
|
+
the window closes. Note that the limit hides sent cards, not their
|
|
117
|
+
reads: a hidden message that gets read during follow mode still emits
|
|
118
|
+
its read card.
|
|
71
119
|
|
|
72
120
|
## Priming a new project
|
|
73
121
|
|