@jeffober/thatch 0.2.5 → 0.2.7
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 +48 -25
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-clear-writing.md +11 -0
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +25 -15
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- package/artifacts/skills/thatch-refine-personal.md +5 -0
- package/artifacts/skills/thatch-refine.md +7 -1
- package/artifacts/skills/thatch-review-clarity.md +101 -0
- package/artifacts/skills/thatch-review-context.md +55 -2
- package/artifacts/skills/thatch-review-followup.md +4 -4
- package/artifacts/skills/thatch-review-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +22 -2
- package/bin/thatch +103 -33
- package/docs/dev/README.md +11 -8
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +23 -7
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +99 -27
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +41 -15
- package/docs/dev/features/prediction-engine.md +27 -1
- package/docs/dev/features/qa-system.md +19 -3
- package/docs/dev/features/session-lifecycle.md +6 -6
- package/docs/dev/features/session-tabs.md +11 -5
- package/docs/dev/gotchas.md +21 -0
- package/docs/dev/mcp-parity.md +3 -3
- package/docs/dev/skills.md +12 -8
- package/docs/user/README.md +22 -14
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +28 -15
- package/docs/user/code-review.md +46 -4
- package/docs/user/commands.md +27 -7
- package/docs/user/cross-session-chat.md +48 -20
- package/docs/user/prediction-engine.md +12 -1
- package/docs/user/skills.md +9 -6
- package/package.json +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +56 -24
- package/src/commands.ts +109 -18
- package/src/db.ts +23 -2
- package/src/mcp.ts +6 -2
- package/src/opencode/tui-plugin.ts +29 -9
- package/src/opencode/v2.ts +247 -66
- package/src/prompts.ts +55 -6
- package/src/runtime.ts +122 -17
- package/src/scoring-engine.ts +40 -9
- package/src/session-tab-shared.ts +55 -4
- package/src/skills.ts +5 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +55 -22
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Thatch's `/thatch/*` commands come in two families:
|
|
4
4
|
|
|
5
|
-
- **Actions** (`defrag`, `extract`, `hygiene`, `
|
|
6
|
-
|
|
5
|
+
- **Actions** (`defrag`, `extract`, `hygiene`, `peer-review`, `refine`,
|
|
6
|
+
`reflect`, `self-review`, `whois`) — user-invoked commands. The
|
|
7
|
+
nudge-backed ones (`defrag`, `extract`, `hygiene`, `reflect`) wrap
|
|
8
|
+
behaviors the nudges otherwise run on their own schedule; the rest
|
|
9
|
+
are self-contained, with no nudge counterpart.
|
|
7
10
|
- **Wrap-ups** (`compact`, `exit`) — greenlight-gated pre-compaction and
|
|
8
11
|
pre-exit checklists. opencode only: the greenlight check needs the
|
|
9
12
|
plugin's `command.execute.before` arming and TUI control routes.
|
|
@@ -12,11 +15,12 @@ User-facing guide: [../../user/commands.md](../../user/commands.md).
|
|
|
12
15
|
|
|
13
16
|
## Prompt cores
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
(`defragCore`, `hygieneCore`, `reflectCore`,
|
|
17
|
-
host-agnostic instruction text for one
|
|
18
|
-
the same cores with escalation tiers
|
|
19
|
-
and prompt renderers wrap them with
|
|
18
|
+
The nudge-backed action bodies are **prompt cores**: constants in
|
|
19
|
+
`src/prompts.ts` (`defragCore`, `hygieneCore`, `reflectCore`,
|
|
20
|
+
`extractionCore`) that hold the host-agnostic instruction text for one
|
|
21
|
+
behavior. The nudge envelopes wrap the same cores with escalation tiers
|
|
22
|
+
and the stop-and-wait ending; command and prompt renderers wrap them with
|
|
23
|
+
frontmatter and a user-message section.
|
|
20
24
|
The point is drift-proofing: a wording change lands in the nudge and the
|
|
21
25
|
command at once, and a test asserts each rendered file contains its core
|
|
22
26
|
verbatim.
|
|
@@ -30,13 +34,40 @@ per host: `thatch_find_duplicates` on opencode, `mcp__thatch__find_duplicates`
|
|
|
30
34
|
on MCP hosts. Each host's renderer passes its own namer
|
|
31
35
|
(`opencodeToolName` / `mcpToolName`).
|
|
32
36
|
|
|
37
|
+
## Skill-couching actions
|
|
38
|
+
|
|
39
|
+
A command body can **couch** a skill: wrap the skill's procedure with the
|
|
40
|
+
additional instructions the command needs. `refine`, `peer-review`,
|
|
41
|
+
`self-review`, and `whois` are not prompt cores: they have no nudge
|
|
42
|
+
counterpart to stay in sync with, so their bodies live directly in
|
|
43
|
+
`actionDefs` (src/commands.ts). `refine` triggers the `thatch-refine`
|
|
44
|
+
router skill and `whois` prints the chat identity; neither applies an
|
|
45
|
+
overlay.
|
|
46
|
+
|
|
47
|
+
`peer-review` and `self-review` share one pipeline couching
|
|
48
|
+
(`reviewPipelineCouching` in src/commands.ts): run the code review
|
|
49
|
+
machinery — the `thatch-code-review` coordinator where that skill is
|
|
50
|
+
installed (opencode), the inline specialist sequence where it is not
|
|
51
|
+
(Claude Code, Cursor) — and apply a mode overlay that reweights the
|
|
52
|
+
findings for whose work is under review. The roster and count for the
|
|
53
|
+
inline sequence come from `REVIEW_SPECIALISTS` in src/prompts.ts, the
|
|
54
|
+
same source as the install list in src/skills.ts and both host
|
|
55
|
+
instruction skill lists. `peer-review` weights org
|
|
56
|
+
velocity and walks the project's persistent memory (the one tool it
|
|
57
|
+
names, spelled through the `ToolNamer` so MCP renderings stay correct);
|
|
58
|
+
`self-review` weights the reputation lenses (no-slop, clarity,
|
|
59
|
+
technique, breadcrumbs) and iterates rounds until a round produces no meaningful
|
|
60
|
+
findings. The couching also names the MCP `Focus:` line as a target
|
|
61
|
+
source, because MCP prompts replace the file commands' `$ARGUMENTS`
|
|
62
|
+
section with a `Focus:` line.
|
|
63
|
+
|
|
33
64
|
## Rendering and sync
|
|
34
65
|
|
|
35
66
|
`src/commands.ts` is the single registry:
|
|
36
67
|
|
|
37
68
|
- `actionDefs(tool)` returns the action set with bodies rendered for one
|
|
38
69
|
host. Actions needing plugin-only capabilities are marked `opencodeOnly`
|
|
39
|
-
(`extract`:
|
|
70
|
+
(`extract` and `whois`: they need the invoking session's identity).
|
|
40
71
|
- `opencodeCommandDefs()` = wrap-up templates + all actions. Synced by the
|
|
41
72
|
plugin at init into `$XDG_CONFIG_HOME/opencode/command/thatch/`
|
|
42
73
|
(`installOpencodeCommands`), content-compare, self-healing.
|
|
@@ -52,7 +83,7 @@ on MCP hosts. Each host's renderer passes its own namer
|
|
|
52
83
|
|
|
53
84
|
A parity-guard test asserts the three sets are the same actions minus the
|
|
54
85
|
documented exclusions: opencode = Claude Code set + `compact`, `exit`,
|
|
55
|
-
`extract`; MCP prompts = Claude Code's set.
|
|
86
|
+
`extract`, `whois`; MCP prompts = Claude Code's set.
|
|
56
87
|
|
|
57
88
|
## The extract action and session IDs
|
|
58
89
|
|
|
@@ -146,10 +146,10 @@ mechanical barrier, a documented trust posture.
|
|
|
146
146
|
### Leave tombstones
|
|
147
147
|
|
|
148
148
|
Every exit path - `chat_unregister`, `session.deleted`, and a greenlit
|
|
149
|
-
`/thatch/exit` (the plugin unregisters the exiting session before
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
`/thatch/exit` (the plugin unregisters the exiting session before the
|
|
150
|
+
host close-out - v1 publishes `app.exit`, v2 closes the session's own
|
|
151
|
+
tab - so the roster stops advertising a host that is about to vanish)
|
|
152
|
+
- writes a
|
|
153
153
|
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
154
154
|
directory row. The tombstone is what makes a leave stick: all
|
|
155
155
|
registration paths consult it (the plugin's idle and prompt auto-registerers are
|
|
@@ -235,12 +235,17 @@ the top of the result teaches the paging call, and ids make it exact.
|
|
|
235
235
|
|
|
236
236
|
`chat_broadcast` posts one message to every other registered session at
|
|
237
237
|
once, one inbox row per recipient per part - the existing wake machinery
|
|
238
|
-
(grouping, gating, rate cap) treats each as ordinary mail.
|
|
239
|
-
are skipped and reported
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
(grouping, gating, rate cap) treats each as ordinary mail. Opencode rows
|
|
239
|
+
are skipped and reported unless they are OPEN (fresh heartbeat AND a tab
|
|
240
|
+
open now, per the open-tab reader below): a host that has stopped
|
|
241
|
+
heartbeat-ing will never read the mail, and a fresh-but-tab-less session's
|
|
242
|
+
wake can be swallowed without running a turn, so messaging it would spend
|
|
243
|
+
a model turn on a delivery that likely never fires. MCP sessions always
|
|
244
|
+
receive (they read mail at their next prompt). Every skip is listed by
|
|
245
|
+
name in the result's `skipped` field. The sender is excluded. It is a
|
|
246
|
+
separate tool rather than a
|
|
242
247
|
magic `chat_send` recipient because "send to all" changes the behavior
|
|
243
|
-
(fan-out,
|
|
248
|
+
(fan-out, skip classes, no address resolution), and a function that
|
|
244
249
|
changes behavior drastically on a parameter value is two functions.
|
|
245
250
|
|
|
246
251
|
Retries are idempotent by READ STATE within a short window: a send (or
|
|
@@ -269,7 +274,8 @@ fires no `session.deleted`, so its rows would linger - staleness catches
|
|
|
269
274
|
them: a row that has missed two consecutive beats (`CHAT_STALE_MS`, 60s
|
|
270
275
|
at the default interval) is stale, meaning its harness stopped. Two beats
|
|
271
276
|
rather than one so a single late poll cycle does not flap the roster.
|
|
272
|
-
Heartbeat age is the
|
|
277
|
+
Heartbeat age is the only WRITTEN liveness signal, by design (the tab
|
|
278
|
+
reader below is read-only). A host process id
|
|
273
279
|
cannot serve: the same session is re-hosted by a new process on every
|
|
274
280
|
`-s` resume, so a pid stamped on the row lies as soon as a different
|
|
275
281
|
harness beats it (it says dead while the heartbeat proves alive). RESTART
|
|
@@ -280,18 +286,57 @@ into, so without the grace they would read stale while alive-idle and
|
|
|
280
286
|
mail to them would not wake the tab. Sessions that emit activity
|
|
281
287
|
graduate to permanent hosting; still-silent ones (closed tabs) age out
|
|
282
288
|
at the grace and go stale. `isStale`
|
|
283
|
-
(src/chat.ts) is the single rule; `chatLiveness`, the `chat_send`
|
|
284
|
-
and the broadcast skip all call it, so they cannot disagree.
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
(src/chat.ts) is the single staleness rule; `chatLiveness`, the `chat_send`
|
|
290
|
+
note, and the broadcast skip all call it, so they cannot disagree.
|
|
291
|
+
|
|
292
|
+
### The open-tab reader: OPEN vs RECENT
|
|
293
|
+
|
|
294
|
+
Heartbeat age alone conflates presence with recency: a finished
|
|
295
|
+
conversation stays fresh until its directory's instance stops beating it
|
|
296
|
+
(days), and a closed tab leaves a fresh row. The refinement is a READ-ONLY
|
|
297
|
+
second signal, `src/tab-state.ts`: at roster render and broadcast time,
|
|
298
|
+
glob `<XDG_STATE_HOME>/opencode/*/tui/tabs.json` (one file per release
|
|
299
|
+
channel - "latest" today, hence the glob), parse BOTH scope modes
|
|
300
|
+
(`global.tabs` and per-path `cwd.*.tabs`; the scope depends on opencode's
|
|
301
|
+
`tabs.scope` config), and build the set of sessionIDs carrying a tab.
|
|
302
|
+
Missing files, bad JSON, and absent state trees degrade to an empty set -
|
|
303
|
+
an unknown tab state never reads as OPEN. `chatLiveness` takes the set and
|
|
304
|
+
returns `open` (fresh + tab), `recent` (fresh + no tab), `stale`;
|
|
305
|
+
MCP rows keep `active`/`idle`. Callers compose the raw set with confirmed
|
|
306
|
+
tab closes via `effectiveOpenTabs` (`chatOpenTabs` is the wired provider):
|
|
307
|
+
the `session.tab_closed` bridge event (the session_tab tool's confirmed
|
|
308
|
+
close) writes a `chat_tab_closed` runtime_state marker, and a FRESH marker
|
|
309
|
+
(inside `CHAT_TAB_CLOSED_OVERRIDE_MS`, 2 staleness windows) subtracts the
|
|
310
|
+
session from the set - so a residue tabs.json row cannot read OPEN right
|
|
311
|
+
after a confirmed close. The marker self-expires (the death path stops the
|
|
312
|
+
heartbeat, so the row goes stale on heartbeat age regardless) and is
|
|
313
|
+
superseded by any later beat.
|
|
314
|
+
|
|
315
|
+
Known limits, accepted: a killed window leaves tab rows behind (the file
|
|
316
|
+
is restart persistence by design), so dead-window residue reads OPEN until
|
|
317
|
+
its heartbeat ages out; tabs disabled degrades everything to RECENT.
|
|
318
|
+
|
|
319
|
+
`chat_list`
|
|
320
|
+
groups the result into Active, Recent, and Stale sections - the Recent
|
|
321
|
+
and Stale sections' explainers say what those states mean, and each row
|
|
322
|
+
renders its verdict -
|
|
323
|
+
and `chat_send` states the recipient's
|
|
287
324
|
liveness at send time, so a sender mailing a ghost learns it immediately
|
|
288
|
-
instead of waiting on a wake that will never fire
|
|
289
|
-
|
|
325
|
+
instead of waiting on a wake that will never fire, and a sender mailing a
|
|
326
|
+
tab-less session is told the wake is not guaranteed. Both roster surfaces
|
|
327
|
+
order rows with `sortChatRoster` (also src/chat.ts) and split fresh
|
|
328
|
+
opencode rows along the same open/recent boundary (`splitChatRoster`
|
|
329
|
+
takes the open-tab set): project first,
|
|
290
330
|
alphabetical, with no-project rows last; most recently seen first within
|
|
291
|
-
a project. The CLI roster
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
331
|
+
a project. The CLI roster guards the Recent and Stale sections behind
|
|
332
|
+
flags, and the two guards hide differently: without `--recent` the
|
|
333
|
+
tab-less rows collapse into a muted hidden-count note (revealed by the
|
|
334
|
+
flag), while without `--stale` stale rows are dropped entirely; with
|
|
335
|
+
`--stale [N|all]` they show under a one-day display cap that collapses
|
|
336
|
+
older rows into a
|
|
337
|
+
hidden-count note. The tool
|
|
338
|
+
surface shows all three sections unbounded so an agent always sees the
|
|
339
|
+
full directory.
|
|
295
340
|
`session.deleted` is the graceful-exit fast path that unregisters immediately.
|
|
296
341
|
|
|
297
342
|
### Delivery, re-nudge, and the rate cap
|
|
@@ -312,6 +357,22 @@ and a count, pointer-only like watcher notifications, with the model calling
|
|
|
312
357
|
`chat_read` for content. On success the poller stamps `delivered_at`
|
|
313
358
|
(re-stamping on re-nudges resets the re-nudge timer).
|
|
314
359
|
|
|
360
|
+
Before any wake attempt runs, the delivery guard (`guardDelivery` in
|
|
361
|
+
`src/runtime.ts`) checks that the runtime instance's own directory - the
|
|
362
|
+
bound directory of every session it hosts - still exists. A deleted
|
|
363
|
+
worktree (merged branch) would otherwise error every attempt, and opencode
|
|
364
|
+
surfaces that error to the user as a toast. The guard skips the attempt
|
|
365
|
+
with a `runtime:delivery` debug line instead, leaving the mail queued; the
|
|
366
|
+
check is per attempt, so a worktree recreated at the same path resumes
|
|
367
|
+
delivery. The existence check cannot say why a path is absent (deleted
|
|
368
|
+
tree, broken symlink, parent permission), so any miss classifies as gone -
|
|
369
|
+
deliberate, because surfacing non-ENOENT errors through the attempt would
|
|
370
|
+
buy the toast back for no recovery the pending-and-retry cycle does not
|
|
371
|
+
already provide. The guard wraps `promptSession` in
|
|
372
|
+
`createRuntime`, which covers chat nudges, watcher notifications and death
|
|
373
|
+
notices, rehydrate pings, chat echoes, and extraction dispatch at once;
|
|
374
|
+
MCP hosts never build a runtime, so they are unaffected.
|
|
375
|
+
|
|
315
376
|
The hard brake: at most a fixed number of wake prompts per recipient per
|
|
316
377
|
hour (default 6, in-memory on purpose - it is a loop guard, not accounting).
|
|
317
378
|
Without it, two agents politely acknowledging each other would ping-pong
|
|
@@ -475,7 +536,9 @@ caller's identity and mailbox, and is absent entirely when chat is
|
|
|
475
536
|
disabled - absence is silent by construction.
|
|
476
537
|
Staleness semantics differ by kind: an opencode row's stale age means its
|
|
477
538
|
harness stopped beating (broadcast skips it), while an mcp row's age only means
|
|
478
|
-
"between turns" (broadcast always delivers).
|
|
539
|
+
"between turns" (broadcast always delivers). Broadcast also skips fresh
|
|
540
|
+
opencode rows with no open tab (see the open-tab reader) - the skip
|
|
541
|
+
classes are reported by name either way.
|
|
479
542
|
|
|
480
543
|
The `/thatch/whois` command (opencode-only action, src/commands.ts) is the
|
|
481
544
|
user-facing identity report: it relays `chat_status` for the invoking
|
|
@@ -489,18 +552,27 @@ mutates anything.
|
|
|
489
552
|
|
|
490
553
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
491
554
|
threshold two missed beats (60s), re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
492
|
-
per hour, auto-row TTL 7 days
|
|
493
|
-
|
|
494
|
-
`
|
|
495
|
-
|
|
555
|
+
per hour, auto-row TTL 7 days, confirmed-close override window two
|
|
556
|
+
staleness windows (`CHAT_TAB_CLOSED_OVERRIDE_MS` in `src/tab-state.ts`).
|
|
557
|
+
Environment overrides: `THATCH_CHAT_SPLIT_PART_LEN`
|
|
558
|
+
(auto-split part budget, default 2000, clamped 500..9000),
|
|
559
|
+
`THATCH_CHAT_MAX_BODY_CHARS` (whole-body acceptance ceiling, default 20000), and
|
|
560
|
+
`THATCH_OPENCODE_STATE_ROOT` (the open-tab reader's state root, tests and
|
|
561
|
+
QA sandboxes only);
|
|
562
|
+
the split constants are resolved once at module load and sanitized to positive integers
|
|
496
563
|
within their clamps - invalid values silently fall back to the default.
|
|
497
564
|
|
|
498
565
|
## Source files
|
|
499
566
|
|
|
500
567
|
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
501
|
-
pool pick, slugify, counter bump, insert, prune, topic refresh
|
|
568
|
+
pool pick, slugify, counter bump, insert, prune, topic refresh, broadcast
|
|
569
|
+
targeting), ChatPoller
|
|
502
570
|
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
503
|
-
staleness helper
|
|
571
|
+
staleness helper, chatLiveness (the open/recent/active/idle/stale
|
|
572
|
+
verdicts)
|
|
573
|
+
- `src/tab-state.ts` - the read-only open-tab reader: per-channel
|
|
574
|
+
tabs.json glob (global + cwd scopes), confirmed-close override window,
|
|
575
|
+
the composed open-tabs provider
|
|
504
576
|
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
505
577
|
the base for every assigned name
|
|
506
578
|
- `src/db.ts` - the chat tables in schema init (including
|
|
@@ -96,7 +96,7 @@ prediction_matchers(
|
|
|
96
96
|
embedding BLOB,
|
|
97
97
|
model TEXT,
|
|
98
98
|
created_at TEXT NOT NULL,
|
|
99
|
-
updated_at TEXT NOT NULL
|
|
99
|
+
updated_at TEXT NOT NULL -- last change to the matcher row or its edge set
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -116,7 +116,7 @@ predictions(
|
|
|
116
116
|
confirm_count REAL NOT NULL DEFAULT 0,
|
|
117
117
|
disconfirm_count REAL NOT NULL DEFAULT 0,
|
|
118
118
|
created_at TEXT NOT NULL,
|
|
119
|
-
updated_at TEXT NOT NULL
|
|
119
|
+
updated_at TEXT NOT NULL -- written at creation, then last confidence change
|
|
120
120
|
)
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -167,7 +167,7 @@ behavior_matchers(
|
|
|
167
167
|
embedding BLOB,
|
|
168
168
|
model TEXT,
|
|
169
169
|
created_at TEXT NOT NULL,
|
|
170
|
-
updated_at TEXT NOT NULL
|
|
170
|
+
updated_at TEXT NOT NULL -- last change to the matcher row or its edge set
|
|
171
171
|
)
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -185,7 +185,7 @@ behaviors(
|
|
|
185
185
|
confirm_count REAL NOT NULL DEFAULT 0,
|
|
186
186
|
disconfirm_count REAL NOT NULL DEFAULT 0,
|
|
187
187
|
created_at TEXT NOT NULL,
|
|
188
|
-
updated_at TEXT NOT NULL
|
|
188
|
+
updated_at TEXT NOT NULL -- written at creation, then last confidence change
|
|
189
189
|
)
|
|
190
190
|
```
|
|
191
191
|
|
|
@@ -131,6 +131,20 @@ Tiers 2, 3, and 4 all fire independently. Any subset may inject. All share
|
|
|
131
131
|
one embedding computation, reused across three cosine scans against
|
|
132
132
|
different tables.
|
|
133
133
|
|
|
134
|
+
#### Snapshot semantics
|
|
135
|
+
|
|
136
|
+
A nudge block is computed once, when the message is submitted, and stays
|
|
137
|
+
attached to that message for the whole turn - including every model round
|
|
138
|
+
trip after mid-turn tool calls. Predictions, behaviors, or memories the
|
|
139
|
+
conversation mutates mid-turn do NOT refresh an already-injected block
|
|
140
|
+
(v1: the synthetic part lives on the stored message; v2:
|
|
141
|
+
`pendingInjections` re-injects the prompt-time text into every outbound
|
|
142
|
+
request of the turn, bound to the message it was computed for - see
|
|
143
|
+
[opencode-plugin.md](opencode-plugin.md)). The prediction and behavior
|
|
144
|
+
blocks therefore end with a snapshot footer naming the store state they
|
|
145
|
+
reflect and the live tool to re-check. The recall nudge carries no footer:
|
|
146
|
+
it lists memory labels without confidence claims.
|
|
147
|
+
|
|
134
148
|
### MCP path (`bin/thatch`, `flush-tools` subcommand)
|
|
135
149
|
|
|
136
150
|
The `flush-tools` subcommand fires on `UserPromptSubmit` (Claude Code) or
|
|
@@ -58,7 +58,7 @@ the shared runtime needs. It is the lifecycle-level sibling of `CoreContext`
|
|
|
58
58
|
| Capability | v1 source | v2 source | v2 strategy |
|
|
59
59
|
|---|---|---|---|
|
|
60
60
|
| bus events | `event` hook | `event.subscribe` (raw SSE) | client-side location filter; location-less events resolve via `session.get` (`{sessionID}` input) and drop unresolvable (mirrors v1's server-side filter); events for plugin-created extraction children pass by ID (they live in the project directory, which differs from the instance directory on below-root launches) |
|
|
61
|
-
| incoming message | `chat.message` hook | `session.hook("prompt")` | the hook awaits the runtime; nudge injections append to the OUTBOUND request's
|
|
61
|
+
| incoming message | `chat.message` hook | `session.hook("prompt")` | the hook awaits the runtime; nudge injections append to the OUTBOUND request's user message whose text matches the entry's stored prompt, in the context hook (v2 wire `Message` carries parts in `content`, not `parts`). Entries form a per-session set (cap 3, oldest evicted) so a prompt queued mid-turn keeps its own entry instead of evicting the running turn's; a prompt whose nudge computation comes up empty clears the session's whole set, so the running turn's remaining rounds lose their block in that one case (smaller than the staleness it prevents: an entry left behind would match its own prompt text in later requests' session history and replay a stale block there) |
|
|
62
62
|
| system prompt | `experimental.chat.system.transform` | `session.hook("context")` | mutates the request's system array; the runtime's plain strings convert to `{type: "text", text}` SystemPart objects at the boundary |
|
|
63
63
|
| compaction | `experimental.session.compacting` + `.autocontinue` + `session.compacted` | `session.hook("compaction")` | flag lands; context-injection surface unverified |
|
|
64
64
|
| wrap-up commands | `command.execute.before` + command files | `command.transform` + `CommandEditor.add` | registered in code: `execute` arms the greenlight check (`onCommandExecuteBefore`) and delivers the same prompt body the v1 file carries, substituting the invocation's prompt text for `$ARGUMENTS` (v2's `session.prompt` does no template expansion); the runtime installs only ACTION command files on v2 and REMOVES stale wrap-up files from earlier v1 runs (a file and a registered command with the same name would collide) |
|
|
@@ -66,14 +66,14 @@ the shared runtime needs. It is the lifecycle-level sibling of `CoreContext`
|
|
|
66
66
|
| tool buffering | `tool.execute.after` hook | `tool.hook("execute.after")` | wired: feeds the same extraction buffer; `Tool.Result.content` is flattened (string passes through, content-part arrays contribute their text parts); the title is derived from the tool name and args (`deriveTitle`), since v2's `Tool.Result.output` is a typed output value, not a title |
|
|
67
67
|
| noReply deliveries | `promptAsync` with `noReply` | none | gated off via `HostCapabilities.noReplyDelivery` -- chat echoes and the session-start reminder are skipped on v2 (delivering them would start real model turns: a feedback loop) |
|
|
68
68
|
| synthetic wake deliveries | `promptAsync` with `synthetic` parts | `session.synthetic` endpoint | watcher + chat wake nudges route to v2's synthetic endpoint (TUI-hidden), matching v1 |
|
|
69
|
-
| child sessions | `client.session.create/promptAsync/prompt/delete` | `session.create/prompt` | create+prompt supported (a missing id throws into the extraction fallback); delete
|
|
69
|
+
| child sessions | `client.session.create/promptAsync/prompt/delete` | `session.create/prompt` | create+prompt supported (a missing id throws into the extraction fallback); delete rides `session.remove` (upstream #52387, in v2.0.22), runtime-guarded like compact -- an older host keeps the documented gap (the session picker accumulates one `thatch-extraction` entry per extraction, and `-c` "continue last session" logic that picks the newest top-level session will land in an extraction child after any session that triggered extraction) |
|
|
70
70
|
| session status | `client.session.status` | none | returns `{}`; the wake gate treats unknown as idle and the event-fed status map does the gating |
|
|
71
71
|
| session messages | `client.session.messages` | `session.context` | mapped into the v1 `{info: {role}, parts}` shape, so the wrap-up greenlight check works (the promise domain has no `message` accessor) |
|
|
72
72
|
| session list | `client.session.list` | none | degrade (`-c` resume listing loses its data source) |
|
|
73
|
-
| compaction trigger | `client.tui.executeCommand("session_compact")` |
|
|
74
|
-
| toasts | `client.tui.showToast` |
|
|
75
|
-
|
|
|
76
|
-
| session tabs | n/a | rpc event bridge |
|
|
73
|
+
| compaction trigger | `client.tui.executeCommand("session_compact")` | `session.compact({sessionID})` | wired behind a runtime guard: upstream [#52385](https://github.com/anomalyco/opencode/pull/52385) exposed `session.compact` on the promise domain (in the installed v2.0.25), newer than the pinned dev types - an older host degrades to a logged no-op (the wrap-up checklist and flush still run, the automatic compaction is skipped) |
|
|
74
|
+
| toasts | `client.tui.showToast` | toast rpc event | wired: every runtime toast call site (alerts, extraction metrics, chat registration, blocked wrap-ups, watcher rearm notices) emits the bridge's `toast` event, which the `./tui` plugin shows via `ui.toast.show`; the pump never translates it (TUI-only by nature). Best-effort by contract: a TUI-less environment or an rpc-less SDK floor shows nothing, silently |
|
|
75
|
+
| wrap-up exit | `client.tui.publish` | exit-tab-closed rpc event | the wrap-up exit records the session's watcher deaths synchronously (the runtime, before the chat unregister - `sessionDied` places the death row by the chat row, so the order is load-bearing) and then emits `rpc.thatch-tabs.exit-tab-closed`, a TUI-ONLY close of the session's OWN tab (the `./tui` plugin's `ui.tabs.close`): the tab-scoped semantic upstream [anomalyco/opencode#50984](https://github.com/anomalyco/opencode/issues/50984) asks for, since the daemon hosts every tab and `app.exit` would take down unrelated sessions. The pump deliberately never translates this event (a second death pass would erase the durable death row the first one wrote; the session_tab_close tool's `tab-closed` is the only death-translating event). Degrades to a logged no-op without the rpc surface; a TUI with no tab for the session (headless run, tabs off) never acts - the event is ephemeral |
|
|
76
|
+
| session tabs | n/a | rpc event bridge | `thatch_session_tab` (v2Only) spawns a detached subordinate session (create -> move -> chat-register -> emit `rpc.thatch-tabs.tab-opened` -> prompt); the `./tui` CLI plugin consumes the event and calls `ui.tabs.open` (open-without-focus, idempotent). `thatch_session_tab_close` emits `rpc.thatch-tabs.tab-closed {sessionID, chatName}` - the pump translates it into the runtime's confirmed-death path. The registration's third event, `exit-tab-closed` (the wrap-up exit's), is TUI-only - see the wrap-up exit row |
|
|
77
77
|
| tab close | n/a | no session.deleted on v2 tab close | bounded 2026-10-07: watcher death detection cancels a closed tab's watchers once its pending events age past `THATCH_WATCH_DEATH_MINUTES` (default 120) while deliveries throw 404-class errors, and announces the death to live same-project sessions; the roster row itself still lingers until the reaper. NOTE: an earlier version of this row claimed deliveries "fail their idle gate" - wrong: the v2 gate opens (the status stub reads as idle) and the PROMPT fails; same leak, different link |
|
|
78
78
|
| wake gate after reload | `session.status` event map | map rehydrates empty | known gap: v2's fetchStatuses stub is structural (the promise context has no session.status), so in the reload window - until the session's next status event - the proactive-prompt gate is fully open |
|
|
79
79
|
| dispose | `dispose` hook | cleanup returned from `setup` | must be idempotent: v2 auto-reloads plugins on file change, and a non-idempotent cleanup doubles pollers/pumps/nudges. Volatile runtime state (extraction buffer + accepted entries, child bookkeeping, wrap-up arms, watcher definitions) is journaled to the `runtime_state` table - instance-scoped by the writing location's directory - and rehydrated on the next `setup()`: rows from the same process AND directory (a reload) all restore; child rows carry a kind marker (extraction vs task-dispatched sub-agent, defaulting to extraction for pre-marker rows) so only extraction children restore the `extracting`/`extractionChildren` sets; rows from a dead process are pruned unless the session is the `-c`/`-s` startup resume, which inherits recovery-safe state only (buffer requeued, no live child bookkeeping - the child died with the old process). Exception: a dead process's watcher definitions are kept as DORMANT rows (no polling, no delivery) instead of pruned - resuming the session re-arms them on its first message (a `chat.message` scan: own rows re-arm, same-directory rows of still-dead sessions earn the live session a one-line death notice gated on the owning pid being dead, rows past the watcher TTL plus a 24h grace age out). Restored sessions on v1 get a synthetic noReply re-attach notice (v2 has no turn-free delivery and skips it). The chat poller re-hosts the instance's journaled hosted set on reload, so pending chat mail wakes sessions the reload left asleep and heartbeats keep the reaper from reaping live registrations. The embedding model is refcounted per db path, so N location instances share one resident model |
|
|
@@ -90,20 +90,46 @@ suite runs green against a v2 serve.
|
|
|
90
90
|
Still unverified against the live binary (static analysis only):
|
|
91
91
|
|
|
92
92
|
- Whether `session.hook("prompt")` fires for `session.synthetic` inbox
|
|
93
|
-
items. If it does not, `pendingInjections` from the prior real turn
|
|
94
|
-
appended to the synthetic turn's outbound request.
|
|
93
|
+
items. If it does not, `pendingInjections` from the prior real turn would
|
|
94
|
+
be appended to the synthetic turn's outbound request. Structurally
|
|
95
|
+
bounded 2026-10-12: each `pendingInjections` entry is bound to the prompt
|
|
96
|
+
text it was computed from, and the context hook injects it only into a
|
|
97
|
+
request user message whose joined text equals that prompt - a replay onto
|
|
98
|
+
any other message (synthetic turn, missed prompt hook, failed
|
|
99
|
+
`onChatMessage`) is skipped. Text equality is the tightest binding
|
|
100
|
+
available, not an airtight one: an identical prompt re-submitted after a
|
|
101
|
+
missed prompt hook still matches and replays; the next prompt-hook run
|
|
102
|
+
that fires is the recovery. The prompt-time block still re-fires on every
|
|
103
|
+
round of its OWN turn (v1's persistent-synthetic-part semantics), and the
|
|
104
|
+
block carries a snapshot footer stating the labels are a submission-time
|
|
105
|
+
snapshot and pointing at the live re-check tool.
|
|
95
106
|
- Whether `SessionGenerate.messages` are fresh objects per model call within
|
|
96
|
-
a turn.
|
|
97
|
-
|
|
107
|
+
a turn. The context hook appends injections to the matched message's
|
|
108
|
+
content, which changes the text the binding check compares. With fresh
|
|
109
|
+
objects per call (what the tests assume), the prompt-time block re-fires
|
|
110
|
+
on every round of its own turn. With reused objects, round 1's
|
|
111
|
+
injections would change the matched message's text, so the binding check
|
|
112
|
+
would skip rounds 2 and on - within-turn replay would break after round
|
|
113
|
+
1 instead of duplicating.
|
|
114
|
+
- Entries outliving their session: `pendingInjections` is keyed by session
|
|
115
|
+
id and never swept on session end. Bounded - a per-session set capped at
|
|
116
|
+
3 entries, and the map itself dies with the adapter activation - but a
|
|
117
|
+
session's entries do persist after the session is gone, pre-existing.
|
|
118
|
+
- Cross-directory hook dispatch scope: the event pump filters events by
|
|
119
|
+
directory, but the session hooks (`prompt`, `context`, `compaction`)
|
|
120
|
+
have no such filter - whether a hook can fire for another directory's
|
|
121
|
+
session is unverified, pre-existing.
|
|
98
122
|
- An npm-installed copy (not the dev checkout, which has devDependencies
|
|
99
123
|
present) loading under v2 - the isolation rule's real-world proof.
|
|
100
124
|
|
|
101
|
-
The
|
|
102
|
-
|
|
103
|
-
[anomalyco/opencode#50984](https://github.com/anomalyco/opencode/issues/50984)
|
|
125
|
+
The wrap-up exit closes the session's own tab over the session-tab rpc
|
|
126
|
+
bridge - the tab-scoped close upstream
|
|
127
|
+
[anomalyco/opencode#50984](https://github.com/anomalyco/opencode/issues/50984)
|
|
128
|
+
asks for. Known degrade: a TUI with no tab for the session (headless run,
|
|
129
|
+
tabs off) never acts - the event is ephemeral, nobody is listening.
|
|
104
130
|
Install opencode-v2 (brew formula `anomalyco/tap/opencode-v2`; conflicts
|
|
105
|
-
with the v1 formula's binary name, so it cannot coexist) and re-run the
|
|
106
|
-
live suite against it.
|
|
131
|
+
with the v1 formula's binary name, so it cannot coexist) and re-run the
|
|
132
|
+
QA live suite against it.
|
|
107
133
|
|
|
108
134
|
## User setup compatibility
|
|
109
135
|
|
|
@@ -141,6 +141,8 @@ later — land below that floor and accumulate. The dedup pass finds them:
|
|
|
141
141
|
`[thatch] User decision model` header.
|
|
142
142
|
- 0-evidence predictions use "you may prefer"; predictions with evidence use
|
|
143
143
|
"you tend to".
|
|
144
|
+
- The block ends with a snapshot footer (see the block example below): the
|
|
145
|
+
labels are a submission-time snapshot, not a live view.
|
|
144
146
|
- Independent try/catch — a prediction failure does not block recall or
|
|
145
147
|
behavior.
|
|
146
148
|
- Toast: `[thatch] N predictions surfaced`.
|
|
@@ -159,11 +161,27 @@ later — land below that floor and accumulate. The dedup pass finds them:
|
|
|
159
161
|
The auto-fire block looks like:
|
|
160
162
|
|
|
161
163
|
```text
|
|
162
|
-
|
|
164
|
+
[thatch] User decision model
|
|
163
165
|
- [0.72 conf, 4 tests] When {matcher}: you tend to {prediction}
|
|
164
166
|
- [0.61 conf, 2 tests] When {matcher2}: you may prefer {prediction2}
|
|
167
|
+
Snapshot: these labels reflect the prediction store as of this message's
|
|
168
|
+
submission. Predictions updated after submission (including during this
|
|
169
|
+
turn) do not refresh it; re-check with thatch_prediction_query before
|
|
170
|
+
relying on them.
|
|
165
171
|
```
|
|
166
172
|
|
|
173
|
+
The footer's tool name follows the host: opencode sees
|
|
174
|
+
`thatch_prediction_query`; MCP hosts (Claude Code, Cursor) see
|
|
175
|
+
`mcp__thatch__prediction_query`. The renderer resolves the spelling
|
|
176
|
+
through the caller's `ToolNamer`.
|
|
177
|
+
|
|
178
|
+
The snapshot footer exists because the block is computed once when the
|
|
179
|
+
message is submitted and stays attached to it for the whole turn (v1:
|
|
180
|
+
synthetic part on the stored message; v2: `pendingInjections` re-injected
|
|
181
|
+
into every outbound request of the turn). Predictions the conversation
|
|
182
|
+
mutates mid-turn do not refresh the block; the footer tells the consumer
|
|
183
|
+
the labels are a submission-time snapshot and points at the live tool.
|
|
184
|
+
|
|
167
185
|
Prompt instructions: follow strong predictions silently, surface weak or
|
|
168
186
|
competing predictions to the user, update the model when the user responds.
|
|
169
187
|
|
|
@@ -280,6 +298,14 @@ prediction_dedup_pairs(
|
|
|
280
298
|
)
|
|
281
299
|
```
|
|
282
300
|
|
|
301
|
+
Timestamp semantics: a matcher's `updated_at` is the last change to the
|
|
302
|
+
matcher row OR its edge set — `createEdge` bumps it when a new edge lands,
|
|
303
|
+
and deleting an item bumps the matchers whose edges cascade away. The
|
|
304
|
+
matcher's own text may legitimately be older than its links (a corrected
|
|
305
|
+
prediction re-links the existing matcher without touching its text).
|
|
306
|
+
An item's `updated_at` is written at creation and then moved by the last
|
|
307
|
+
confidence change.
|
|
308
|
+
|
|
283
309
|
## Key invariants
|
|
284
310
|
|
|
285
311
|
1. **The firewall principle.** The model that uses predictions cannot grade
|
|
@@ -70,9 +70,11 @@ execution directly via bun assertions. These are fast and run in parallel.
|
|
|
70
70
|
Spawn real opencode sessions. Test the full pipeline with a real model. These
|
|
71
71
|
cost model tokens and have a 20-minute per-use-case timeout. Live use cases
|
|
72
72
|
check for `opencode` on PATH at runtime: if it is present they run; if not,
|
|
73
|
-
they skip with `[MANUAL]`.
|
|
73
|
+
they skip with `[MANUAL]`. Six (UC-006, UC-010, UC-013, UC-083, UC-084,
|
|
74
|
+
and UC-085) are
|
|
74
75
|
`manualOnly: true` for infrastructure reasons (Cursor hook lifecycle, nested
|
|
75
|
-
opencode session timeout, compaction trigger
|
|
76
|
+
opencode session timeout, compaction trigger, sub-agent support, skill
|
|
77
|
+
loading, and a PR with prior review comments). Run them locally with
|
|
76
78
|
`mise run qa-live`.
|
|
77
79
|
|
|
78
80
|
| UC | Name | What it tests |
|
|
@@ -86,6 +88,20 @@ opencode session timeout, compaction trigger). Run them locally with
|
|
|
86
88
|
| UC-013 | compaction-context | compaction hook injects context |
|
|
87
89
|
| UC-019 | archived-memory-lifecycle | archive -> search excludes -> unarchive -> search includes |
|
|
88
90
|
| UC-020 | extraction-escalation | polite -> insistent -> ALL-CAPS over missed nudges |
|
|
91
|
+
| UC-038 | no-save-run | an extraction child that finds nothing worth saving (no toast) |
|
|
92
|
+
| UC-043 | compaction-suppression | extraction nudges suppressed while an extraction child runs |
|
|
93
|
+
| UC-077 | child-session-drain | direct extraction: the child drains the parent's queue |
|
|
94
|
+
| UC-078 | child-session-error | direct extraction: the child errors, held entries requeue |
|
|
95
|
+
| UC-079 | child-session-deletion | direct extraction: the child is deleted, entries requeue |
|
|
96
|
+
| UC-080 | parent-session-deletion | parent deleted mid-extraction, cleanup completes |
|
|
97
|
+
| UC-083 | review-coordinator | the multi-agent review coordinator pipeline |
|
|
98
|
+
| UC-084 | review-walkthrough | the change-walkthrough skill on a real diff |
|
|
99
|
+
| UC-085 | review-followup | follow-up round verification against prior review comments |
|
|
100
|
+
| UC-086 | review-response | author-side response to incoming review comments |
|
|
101
|
+
| UC-087 | review-no-subagents | the solo review path on hosts without sub-agents |
|
|
102
|
+
| UC-098 | chat-cross-session | two real opencode sessions exchange chat messages |
|
|
103
|
+
| UC-119 | session-tab-spawn | the session_tab spawn flow end to end |
|
|
104
|
+
| UC-122 | review-mode-commands | /thatch/self-review and /thatch/peer-review overlay efficacy (seeded fixture) |
|
|
89
105
|
|
|
90
106
|
### mise tasks
|
|
91
107
|
|
|
@@ -235,7 +251,7 @@ No separate QA docs directory exists. QA use cases live in `tests/qa/` as execut
|
|
|
235
251
|
1. **Automatable use cases never spawn an LLM.** They test CLI, hooks, DB, and
|
|
236
252
|
tool execution directly.
|
|
237
253
|
2. **Live use cases spawn real opencode sessions and cost model tokens.** They
|
|
238
|
-
skip with `[MANUAL]` when `opencode` is not on PATH;
|
|
254
|
+
skip with `[MANUAL]` when `opencode` is not on PATH; six are
|
|
239
255
|
`manualOnly: true` for infrastructure reasons.
|
|
240
256
|
3. **`QA_DRY_RUN=1` lists use cases without executing.** Use this for CI and
|
|
241
257
|
coverage review.
|
|
@@ -73,12 +73,12 @@ When a parent session (no parentID) goes idle:
|
|
|
73
73
|
|
|
74
74
|
User-invoked slash commands, shipped as command markdown synced by the plugin (see `src/commands.ts`). The template instructs the model to flush pending fact extraction (`thatch_get_extraction_payload` + `thatch_extraction_done`), finish promised memory writes, and surface unaddressed todos -- then end its response with a greenlight token (`THATCH_COMPACT_READY` / `THATCH_EXIT_READY`) only when safe to proceed. Text typed after the command is forwarded into a labeled `# User Message` section ahead of the `# Pre-*-wrap-up` checklist section (opencode's `$ARGUMENTS` substitution): the headers give the user's words provenance, so they cannot be misread as part of the wrap-up instructions. opencode's substitution is purely mechanical with no default-value form (verified against v1.18.30), so the template cannot switch on emptiness; instead the section always carries a fallback line telling the model to treat an empty section as n/a (bare command, no user text).
|
|
75
75
|
|
|
76
|
-
1. `command.execute.before` arms the session in `pendingWrapUp` when the command runs
|
|
77
|
-
2. On the session's next idle, the plugin fetches the session's messages
|
|
78
|
-
3. Token present: trigger the
|
|
79
|
-
- compact: `client.tui.executeCommand
|
|
80
|
-
- exit: unregister the session from the chat directory
|
|
81
|
-
4. Token absent: warning toast pointing at the blockers in the response, then fall through -- the model may have buffered tool interactions that still need extraction
|
|
76
|
+
1. `command.execute.before` arms the session in `pendingWrapUp` when the command runs (on v2 the registered command's `execute` runs the same arming through `onCommandExecuteBefore`)
|
|
77
|
+
2. On the session's next idle, the plugin fetches the session's messages (v1: SDK client; v2: `session.context`, mapped into the v1 shape) and checks the final assistant message's trailing text for the token (trimmed, exact match)
|
|
78
|
+
3. Token present: trigger the host action and return early -- compaction is starting (the checklist drained the buffer) or the session is closing
|
|
79
|
+
- compact: v1 dispatches the TUI's `session_compact` command (`client.tui.executeCommand` -- the execute-command route only accepts legacy alias names; `session_compact` maps to the TUI's `session.compact` action, the same thing the built-in `/compact` runs). v2 calls `session.compact({sessionID})` on the promise domain directly (upstream #52385), runtime-guarded because the pinned dev types predate the surface -- an older host degrades to a logged no-op
|
|
80
|
+
- exit: record the session's watcher deaths first (`watchers.sessionDied` -- BEFORE the unregister below, because the death row is placed by the session's chat row and the unregister deletes it; the runtime also captures the project there, so the async death-notice delivery still places after the row is gone), unregister the session from the chat directory (a greenlit exit's host is about to vanish; other sessions must stop addressing mail to it), then close the session out on the host. v1 publishes the `app.exit` TUI keymap command directly (`client.tui.publish` -- no exit alias exists). v2 emits the session-tab bridge's `exit-tab-closed` event, a TUI-ONLY close of the session's OWN tab: the daemon hosts every tab, so `app.exit` would take down unrelated sessions, and the pump deliberately never translates this event (the deaths were already recorded; a second pass would erase the durable death row). The unregister is plugin-side only, after the death recording: the template deliberately does NOT ask the model to call `thatch_chat_unregister`, because a mid-turn unregister would delete the chat row before the death placement could use it. Compaction deliberately does NOT unregister - the session continues
|
|
81
|
+
4. Token absent: warning toast pointing at the blockers in the response, then fall through -- the model may have buffered tool interactions that still need extraction (on v2 the toast rides the bridge's toast event; a TUI-less environment shows nothing - the blockers themselves are in the model's response)
|
|
82
82
|
|
|
83
83
|
### Command file install
|
|
84
84
|
|
|
@@ -82,11 +82,17 @@ Key properties:
|
|
|
82
82
|
## The cross-entry contract
|
|
83
83
|
|
|
84
84
|
`src/session-tab-shared.ts` (SDK-free, per the isolation rule in
|
|
85
|
-
`src/index.ts`) holds the rpc definition (`thatch-tabs`: `tab-opened
|
|
86
|
-
`tab-closed` -
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
`src/index.ts`) holds the rpc definition (`thatch-tabs`: `tab-opened`;
|
|
86
|
+
`tab-closed` - emitted by the close tool (below) and translated by the v2
|
|
87
|
+
pump into the runtime's confirmed-death path, which also records a
|
|
88
|
+
`chat_tab_closed` marker the chat roster's open-tab reader uses to demote
|
|
89
|
+
the row instantly ([cross-session-chat.md](cross-session-chat.md));
|
|
90
|
+
`exit-tab-closed` - the
|
|
91
|
+
wrap-up exit's TUI-only close, which the runtime records the exit's deaths
|
|
92
|
+
itself before emitting, so the pump deliberately never translates that
|
|
93
|
+
one; and `toast` - every runtime toast's TUI-only feedback channel), the
|
|
94
|
+
subordinate prompt builder, and the validators. The TUI entrypoint may
|
|
95
|
+
import only `@opencode/plugin/tui` and this module.
|
|
90
96
|
|
|
91
97
|
## Interactions
|
|
92
98
|
|
package/docs/dev/gotchas.md
CHANGED
|
@@ -160,6 +160,12 @@ here first. These are the things that have already cost time.
|
|
|
160
160
|
- **`bun test` does not typecheck**, and `tsconfig.json` excludes `tests`. Test
|
|
161
161
|
type errors are editor-only noise unless you run `tsc` on the test files
|
|
162
162
|
directly. Keep test files type-clean anyway so the editor stays quiet.
|
|
163
|
+
- **`mise run test` (bare `bun test`) also executes the QA barrels**
|
|
164
|
+
(`tests/qa/*/index.test.ts`), which spawn REAL opencode sessions and spend
|
|
165
|
+
model tokens, one use case at a time - a full run can hang for many minutes.
|
|
166
|
+
The unit gate is `mise run check` (`bun test tests/*.test.ts`); QA runs
|
|
167
|
+
belong to the `mise run qa-auto` / `qa-live` tasks, which select a UC
|
|
168
|
+
subset by name.
|
|
163
169
|
|
|
164
170
|
## Config and notifications
|
|
165
171
|
|
|
@@ -361,3 +367,18 @@ instead of throwing "Cannot use a closed database". Two rules keep this intact:
|
|
|
361
367
|
chat canary). Local plugin specs handed to a loader explicitly
|
|
362
368
|
(`"plugin": ["./path"]`) can point at a directory on v1 - it is only the
|
|
363
369
|
AUTO-discovery scan that is files-only.
|
|
370
|
+
|
|
371
|
+
## A promptSession wrapper must stay synchronous or fire-and-forget delivery assertions misfire
|
|
372
|
+
|
|
373
|
+
- **Symptom**: tests that assert on wake delivery immediately after a hook
|
|
374
|
+
invocation see zero calls; deliveries land one microtask late.
|
|
375
|
+
- **Cause**: the delivery guard (`guardDelivery`, src/runtime.ts) sits on
|
|
376
|
+
every wake path, and its directory-existence check was first written as
|
|
377
|
+
`await stat(...)`. Fire-and-forget callers (`void caps.promptSession(...)`)
|
|
378
|
+
and the repo's mock contract (calls recorded "synchronously inside the
|
|
379
|
+
mock", asserted with no intervening await) observe the wrapped call
|
|
380
|
+
before an extra microtask resolves.
|
|
381
|
+
- **Fix**: the existence check is synchronous (`existsSync`), so the guard
|
|
382
|
+
adds no microtask between the gate and the wrapped call. If you ever
|
|
383
|
+
replace it with an async check, the callers' fire-and-forget timing -
|
|
384
|
+
and the tests that pin it - break again. (2026-10-12, wake-delivery-guard)
|