@jeffober/thatch 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +48 -25
  2. package/artifacts/skills/common.md +1 -0
  3. package/artifacts/skills/thatch-code-archaeology.md +9 -0
  4. package/artifacts/skills/thatch-code-review.md +16 -8
  5. package/artifacts/skills/thatch-plan-refinement.md +33 -4
  6. package/artifacts/skills/thatch-refine-personal.md +5 -0
  7. package/artifacts/skills/thatch-refine.md +7 -1
  8. package/artifacts/skills/thatch-review-context.md +55 -2
  9. package/artifacts/skills/thatch-review-followup.md +4 -4
  10. package/artifacts/skills/thatch-review-response.md +1 -1
  11. package/artifacts/skills/thatch-review-synthesizer.md +21 -1
  12. package/bin/thatch +21 -12
  13. package/docs/dev/README.md +11 -8
  14. package/docs/dev/features/README.md +1 -1
  15. package/docs/dev/features/behavior-engine.md +10 -0
  16. package/docs/dev/features/cli.md +2 -1
  17. package/docs/dev/features/commands.md +40 -9
  18. package/docs/dev/features/cross-session-chat.md +85 -21
  19. package/docs/dev/features/database.md +4 -4
  20. package/docs/dev/features/nudge-pipeline.md +14 -0
  21. package/docs/dev/features/opencode-plugin.md +41 -15
  22. package/docs/dev/features/prediction-engine.md +27 -1
  23. package/docs/dev/features/qa-system.md +19 -3
  24. package/docs/dev/features/session-lifecycle.md +6 -6
  25. package/docs/dev/features/session-tabs.md +11 -5
  26. package/docs/dev/gotchas.md +21 -0
  27. package/docs/dev/mcp-parity.md +3 -3
  28. package/docs/dev/skills.md +11 -8
  29. package/docs/user/README.md +19 -12
  30. package/docs/user/behavior-engine.md +7 -1
  31. package/docs/user/cli.md +2 -1
  32. package/docs/user/code-review.md +43 -2
  33. package/docs/user/commands.md +27 -7
  34. package/docs/user/cross-session-chat.md +47 -20
  35. package/docs/user/prediction-engine.md +12 -1
  36. package/docs/user/skills.md +8 -6
  37. package/package.json +1 -1
  38. package/src/capabilities.ts +20 -6
  39. package/src/chat.ts +39 -17
  40. package/src/commands.ts +109 -18
  41. package/src/db.ts +23 -2
  42. package/src/mcp.ts +6 -2
  43. package/src/opencode/tui-plugin.ts +29 -9
  44. package/src/opencode/v2.ts +247 -66
  45. package/src/prompts.ts +54 -6
  46. package/src/runtime.ts +122 -17
  47. package/src/scoring-engine.ts +40 -9
  48. package/src/session-tab-shared.ts +55 -4
  49. package/src/skills.ts +5 -9
  50. package/src/tab-state.ts +125 -0
  51. package/src/tool-defs.ts +42 -21
@@ -1,9 +1,11 @@
1
1
  # Slash commands
2
2
 
3
3
  Thatch ships `/thatch/*` commands you run yourself, on top of the nudges the
4
- plugin fires on its own schedule. Nudges and commands share the same
5
- instruction text, so a behavior works the same whether it fires automatically
6
- or you invoke it.
4
+ plugin fires on its own schedule. The nudge-backed commands (`defrag`,
5
+ `extract`, `hygiene`, `reflect`) share their instruction text with the
6
+ nudges, so a behavior works the same whether it fires automatically or you
7
+ invoke it. The remaining commands (`refine`, `self-review`, `peer-review`,
8
+ `whois`) stand alone.
7
9
 
8
10
  ## Commands
9
11
 
@@ -26,9 +28,24 @@ or you invoke it.
26
28
  project type's requirements: the agent classifies the project (team,
27
29
  personal, shared library, spike, infrastructure, or open source
28
30
  contribution), confirms the pick, and runs the refinement loop with
29
- that type's expectations applied. Text after the command can name the
30
- type directly (for example, `/thatch/refine shared-lib`) to skip
31
- classification.
31
+ that type's expectations applied. Project type relaxes round counts
32
+ and documentation bars, never the deep-mode triggers. Text after the
33
+ command can name the type directly (for example, `/thatch/refine
34
+ shared-lib`) to skip classification.
35
+ - `/thatch/peer-review` — review another author's change: the full review
36
+ pipeline with findings weighted for org velocity. Text after the command
37
+ names the review target (for example, `/thatch/peer-review 1234` for a
38
+ PR). The review walks the project's persistent memory for
39
+ incidents, constraints, and footguns the author could not have known,
40
+ and checks them against the change. Settled designs are not
41
+ re-litigated, and "if someone changes this later it breaks" findings
42
+ are never reported. See [code-review.md](code-review.md).
43
+ - `/thatch/self-review` — review your own completed work before
44
+ submitting it: the full review pipeline weighted for reputation and
45
+ simplicity. Takes the same target argument as peer-review. Rounds
46
+ iterate (fix or decline each finding, then re-review the updated
47
+ state) until a round produces no meaningful findings, at most three.
48
+ See [code-review.md](code-review.md).
32
49
  - `/thatch/compact` and `/thatch/exit` — the wrap-up commands. They run a
33
50
  pre-flight checklist (flush extraction, finish promised memory writes,
34
51
  surface loose ends) before a compaction or an exit, gated by a greenlight
@@ -45,11 +62,14 @@ or you invoke it.
45
62
  | `/thatch/reflect` | yes | yes | yes (as an MCP prompt) |
46
63
  | `/thatch/whois` | yes | no | no |
47
64
  | `/thatch/refine` | yes | yes | yes (as an MCP prompt) |
65
+ | `/thatch/peer-review` | yes | yes | yes (as an MCP prompt) |
66
+ | `/thatch/self-review` | yes | yes | yes (as an MCP prompt) |
48
67
  | `/thatch/compact`, `/thatch/exit` | yes | no | no |
49
68
 
50
69
  Why the gaps: `/thatch/extract` and `/thatch/whois` need the invoking
51
70
  session's ID, which only the opencode plugin can supply. The wrap-up
52
- commands trigger TUI actions through the plugin. Cursor has no file-based
71
+ commands trigger host actions (the compaction and the session close-out)
72
+ through the plugin. Cursor has no file-based
53
73
  commands; its prompts come from the MCP server (`prompts/list` and
54
74
  `prompts/get`).
55
75
 
@@ -25,13 +25,20 @@ explicitly with `thatch_chat_register` (no arguments) - useful after
25
25
  `chat_unregister`, or just to check your assigned name. Once registered:
26
26
 
27
27
  - `thatch_chat_list` shows every registered session on the machine, grouped
28
- into Active and Stale sections: active sessions can be woken; stale ones
29
- have missed two heartbeats, about a minute (the opencode process hosting
30
- them has probably stopped). Each row carries how long ago the session last
28
+ into Active and Stale sections: active sessions' harness is alive; stale
29
+ ones have missed two heartbeats, about a minute (the opencode process
30
+ hosting them has probably stopped). Each active row also carries a
31
+ liveness verdict: `open` means the session has an opencode tab open
32
+ right now, `recent` means the harness is alive but no tab is open. Each
33
+ row carries how long ago the session last
31
34
  checked in, its project, whether it runs in the project root or a linked
32
35
  git worktree, its topic, and your unread count
33
36
  - `thatch_chat_send` delivers a message to another registered session, by
34
37
  name or session id, and states the recipient's liveness at send time.
38
+ For a recipient with no open tab it says so plainly: the message waits
39
+ in the inbox, but the wake is not guaranteed to start a turn
40
+ (daemon-resident sessions like subordinates usually still pick the
41
+ message up on their next activity).
35
42
  An identical resend within a few seconds is treated as a client retry:
36
43
  while the first copy is still unread it succeeds but delivers nothing
37
44
  new (the tool output says so); once the recipient has read it, the same
@@ -46,13 +53,16 @@ explicitly with `thatch_chat_register` (no arguments) - useful after
46
53
  the rest with the `message` and `offset` arguments (each drain line
47
54
  carries the message id). Paging never marks anything read
48
55
  - `thatch_chat_broadcast` delivers one message to every other live
49
- registered session at once (a long body is split per recipient, exactly
50
- like a split `chat_send`)
56
+ registered session that has a tab open, plus every MCP session (a long
57
+ body is split per recipient, exactly like a split `chat_send`)
51
58
  - `thatch_chat_unregister` leaves the directory
52
59
 
53
60
  When mail arrives for an idle session, thatch wakes it with a notification -
54
61
  the agent gets a model turn even with nobody at the keyboard, reads its
55
- inbox, and decides what to do. The notification names the senders and the
62
+ inbox, and decides what to do. If the session's directory has been deleted
63
+ (a merged branch's worktree, for example), the wake is skipped quietly:
64
+ no error appears, the mail stays queued, and it delivers normally once the
65
+ directory exists again. The notification names the senders and the
56
66
  count only; the agent reads the actual messages itself. A split message
57
67
  arrives as one grouped notification ("N unread messages from X"), and the
58
68
  parts carry their own markers so the reader assembles the whole before
@@ -61,10 +71,14 @@ the last is suffixed "(continued in next message)".
61
71
 
62
72
  Broadcasts are for announcements and open questions ("which of you is
63
73
  working on the payments code?", "main just moved, rebase if you are based
64
- on it"). Every live session gets the message and spends a model turn on it,
74
+ on it"). Every OPEN session gets the message and spends a model turn on it,
65
75
  so broadcast sparingly - for anything with a known audience, `chat_send` is
66
- the polite tool. Stale sessions are skipped and reported: a dead session
67
- never reads a broadcast.
76
+ the polite tool. Sessions without an open tab are skipped and reported by
77
+ name: a stale session will never read the mail, and a fresh-but-tab-less
78
+ session (a closed tab, or a headless subordinate) is not reliably wakeable,
79
+ so messaging it would spend a turn on a delivery that likely never runs.
80
+ With opencode tabs disabled, broadcasts reach only MCP sessions - that is
81
+ the honest reading of "no tab signal", never a false fan-out.
68
82
 
69
83
  ## Watching the conversation
70
84
 
@@ -149,21 +163,34 @@ host the sessions run in.
149
163
 
150
164
  ## Liveness
151
165
 
152
- A session shows as fresh while the opencode process hosting it is
153
- running: hosted sessions keep beating (idle ones included), so the
154
- freshness is real. When that process dies, its sessions stop beating and
155
- show as stale about a minute later (two missed heartbeats), grouped into
156
- a separate Stale section. A laptop that slept reads the same way until
157
- its processes catch up.
166
+ Liveness has three levels, and the roster shows which one a row is in.
167
+
168
+ The base signal is the heartbeat: a session shows as fresh while the
169
+ opencode process hosting it is running - hosted sessions keep beating
170
+ (idle ones included), so the freshness is real. When that process dies,
171
+ its sessions stop beating and show as stale about a minute later (two
172
+ missed heartbeats), grouped into a separate Stale section. A laptop that
173
+ slept reads the same way until its processes catch up.
174
+
175
+ The second signal, read at roster time, is opencode's own tab state: a
176
+ fresh row whose session has a tab open right now shows as `open`, one
177
+ without shows as `recent`. `recent` covers finished-but-recently-active
178
+ conversations, closed tabs, and daemon-resident sessions that never had a
179
+ window (subordinates) - the harness is alive, but presence is unknown.
180
+ With tabs disabled (or no opencode state on the machine) every fresh row
181
+ shows as `recent`: an unknown tab state never reads as open.
182
+
158
183
  Messages to a stale session wait in its inbox unread - a dead session
159
- never reads them, and `chat_send` says so at send time. Continuing a
184
+ never reads them, and `chat_send` says so at send time. Messages to a
185
+ `recent` session are delivered, but `chat_send` warns that the wake is
186
+ not guaranteed without an open tab. Continuing a
160
187
  session with `opencode -s <id>` reclaims its name and row, and any mail
161
188
  that queued while it was down is delivered at startup. Explicitly
162
189
  closing a session (deleting it in the TUI) unregisters it immediately,
163
- and a greenlit `/thatch/exit` unregisters the session too: the wrap-up
164
- checklist calls `chat_unregister`, and the plugin removes the row itself
165
- when the exit token passes the greenlight check, so other sessions stop
166
- addressing mail to a process that is about to vanish.
190
+ and a greenlit `/thatch/exit` unregisters the session too: the plugin
191
+ removes the row itself when the exit token passes the greenlight check,
192
+ so other sessions stop
193
+ addressing mail to a session that is closing out.
167
194
 
168
195
  ## Turning chat off
169
196
 
@@ -46,10 +46,21 @@ like:
46
46
  flag tech debt before reviewing the diff
47
47
  - [0.61 conf, 2 tests] When choosing test scope: you may prefer
48
48
  integration tests over unit tests
49
+ Snapshot: these labels reflect the prediction store as of this message's
50
+ submission. Predictions updated after submission (including during this
51
+ turn) do not refresh it; re-check with thatch_prediction_query before
52
+ relying on them.
49
53
  ```
50
54
 
51
55
  The agent reads this and decides whether to follow the prediction,
52
- surface it to you, or ignore it. You never see the nudge directly.
56
+ surface it to you, or ignore it. You never see the nudge directly. The
57
+ snapshot line matters when the agent corrects the model mid-conversation
58
+ (you say "no, that's wrong" and it calls `thatch_prediction_delete`):
59
+ the block it saw at the start of the turn is a submission-time snapshot
60
+ and does not update. The agent should re-query with
61
+ `thatch_prediction_query` instead of trusting the block's labels. On
62
+ Claude Code and Cursor the tool name appears with its MCP spelling
63
+ (`mcp__thatch__prediction_query`).
53
64
 
54
65
  ## Tools
55
66
 
@@ -48,11 +48,11 @@ Skills are split into shared (all hosts) and opencode-only:
48
48
  | `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar. |
49
49
  | `thatch-review-technique` | Technique lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, never blocking. |
50
50
  | `thatch-review-synthesizer` | Verifies and synthesizes findings from multiple specialists into a deduplicated, severity-grouped report. |
51
- | `thatch-review-context` | Gathers project context (PRs, tickets, linked docs/designs followed from the change, TODOs, deferred work) before a review. Prevents false positives about intentionally deferred work. |
51
+ | `thatch-review-context` | Gathers project context (PRs, tickets, linked docs/designs followed from the change, TODOs, deferred work, related PRs) before a review. Prevents false positives about intentionally deferred work and about pieces delivered by sibling PRs in a stack or release train. |
52
52
  | `thatch-code-archaeology` | Investigates an existing feature, debugs an unfamiliar area, or begins a new ticket. The research skill; pair with `thatch-coding-workflow`. |
53
53
  | `thatch-review-followup` | Verifies whether the author's responses and code changes since your last review adequately addressed your prior findings. |
54
54
  | `thatch-review-response` | Author-side review response: triage findings on your own PR, fix bugs one by one, reply on each thread, post a top-level summary comment. |
55
- | `thatch-code-review` | Multi-agent review coordinator (opencode-only). Dispatches parallel sub-agents for specialist fan-out and synthesis. |
55
+ | `thatch-code-review` | Multi-agent review coordinator (opencode-only). Dispatches parallel sub-agents for specialist fan-out and synthesis. Also the machinery behind the `/thatch/self-review` and `/thatch/peer-review` mode commands, which couch it with a weighting overlay and run the specialists inline on hosts where the coordinator is not installed. |
56
56
 
57
57
  ### Walkthrough skills
58
58
 
@@ -75,15 +75,15 @@ Skills are split into shared (all hosts) and opencode-only:
75
75
  | Skill | Purpose |
76
76
  |-------|---------|
77
77
  | `thatch-coding-workflow` | Plans and executes code changes with a task-list-driven workflow: complexity triage, milestone planning, research before coding, post-coding verification. Pairs with `thatch-code-archaeology` (research first, then plan). |
78
- | `thatch-plan-refinement` | Refines a plan before it is implemented: a fresh-context reviewer subagent re-checks the plan each round until consensus; deep mode adds specialist lenses (reuse, alternatives, hidden problems, safe-to-modify, intent archaeology). |
78
+ | `thatch-plan-refinement` | Refines a plan before it is implemented: a fresh-context reviewer subagent re-checks the plan each round until consensus; deep mode adds specialist lenses (reuse, alternatives, hidden problems, safe-to-modify, intent archaeology). Plans with UI or session state must include a state-and-transition matrix; a revision self-check runs after each revision; extended runs rotate reviewer lenses and stop on a round with no build-changing findings. |
79
79
 
80
80
  ### Plan refinement skills
81
81
 
82
82
  | Skill | Purpose |
83
83
  |-------|---------|
84
- | `thatch-refine` | Classifies the project (audience, lifecycle, surface) and refines a plan under that type's requirements. The entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. |
84
+ | `thatch-refine` | Classifies the project (audience, lifecycle, surface) and refines a plan under that type's requirements. The entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. Deep-mode triggers in the core skill are binding; type deltas relax round counts, never mode selection. |
85
85
  | `thatch-refine-team-app` | Team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate. |
86
- | `thatch-refine-personal` | Solo projects: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. |
86
+ | `thatch-refine-personal` | Solo projects: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. Relaxes round counts, never mode selection. |
87
87
  | `thatch-refine-shared-lib` | Library API surfaces: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable. |
88
88
  | `thatch-refine-spike` | Throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on. |
89
89
  | `thatch-refine-infra` | CI, Terraform, and k8s changes: blast radius, mandatory rollback plan and staged verification, pattern-following is near-absolute. |
@@ -104,7 +104,9 @@ window.
104
104
  re-installs from its canonical source). Edit the canonical source
105
105
  in the thatch repo instead.
106
106
  - The opencode-only coordinator (`thatch-code-review`) requires
107
- sub-agent support. It is not available on Claude Code or Cursor.
107
+ sub-agent support. It is not available on Claude Code or Cursor; the
108
+ `/thatch/self-review` and `/thatch/peer-review` commands run the
109
+ same specialists inline there instead.
108
110
  - Skill content is markdown read at runtime, not compiled into the
109
111
  binary. If the skill file is missing or corrupted, the skill will
110
112
  not load (silent failure).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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",
@@ -54,7 +54,13 @@ export interface HostCapabilities {
54
54
  * only the title carries over.
55
55
  */
56
56
  sessionCreate(input: { parentID: string; title: string }): Promise<{ id: string }>;
57
- /** Delete a session (child-session cleanup). Degrades to a no-op resolve on v2. */
57
+ /**
58
+ * Delete a session (extraction-child cleanup, so the picker stays clean
59
+ * and `-c` resume does not land in a child). v1: client delete. v2:
60
+ * session.remove on the promise domain (upstream #52387), runtime-guarded
61
+ * because the pinned dev types predate the surface - an older host
62
+ * degrades to a no-op (best-effort by the caller's contract).
63
+ */
58
64
  sessionDelete(id: string): Promise<void>;
59
65
  /**
60
66
  * Prompt a session with text parts. `sync` mode blocks until the child
@@ -90,15 +96,23 @@ export interface HostCapabilities {
90
96
  /**
91
97
  * Trigger the host's compaction for a session (the wrap-up compact
92
98
  * action). v1 dispatches the TUI's session_compact command (the legacy
93
- * alias route; the id is ignored). v2 has no compaction trigger reachable
94
- * from the promise context and degrades to a logged no-op.
99
+ * alias route; the id is ignored). v2 calls session.compact on the
100
+ * promise domain (upstream #52385), runtime-guarded because the pinned
101
+ * dev types predate that surface; an older host degrades to a logged
102
+ * no-op.
95
103
  */
96
104
  compactSession(sessionID: string): Promise<void>;
97
105
  /**
98
- * Exit the host application (the wrap-up exit action). v1 publishes the
99
- * TUI's app.exit command; v2 has no TUI surface and degrades to a no-op.
106
+ * End the session's presence on the host (the wrap-up exit action). The
107
+ * runtime records the session's watcher deaths and unregisters it from
108
+ * chat BEFORE calling here, so implementations only close out the host
109
+ * side. v1 publishes the TUI's app.exit command - the id is unused
110
+ * because the whole app is leaving. v2 emits the session-tab bridge's
111
+ * exit-tab-closed event, a TUI-only close of the session's OWN tab (the
112
+ * daemon hosts every tab, so an app-wide exit would take down unrelated
113
+ * sessions too); a host with no bridge degrades to a logged no-op.
100
114
  */
101
- exitHost(): Promise<void>;
115
+ exitHost(sessionID: string): Promise<void>;
102
116
  }
103
117
 
104
118
  /**
package/src/chat.ts CHANGED
@@ -351,18 +351,28 @@ export function humanAge(iso: string, now = Date.now()): string {
351
351
  return `${Math.floor(minutes / (24 * 60))}d ago`;
352
352
  }
353
353
 
354
- /** The liveness verdict for one roster row, from heartbeat age alone:
355
- * - "fresh"/"stale" for opencode rows (poller-driven heartbeat; stale means * the hosting harness has missed two beats and is presumed gone)
356
- * - "active"/"idle" for MCP rows (turn-driven; idle between prompts is the
357
- * NORMAL state, so an idle MCP row is reachable, not dead)
358
- * Heartbeat age is the only signal on purpose. A process id cannot serve:
359
- * the same session is re-hosted by a new process on every `-s` resume, so
360
- * a pid stamped on the row lies as soon as a different harness beats it.
361
- * One clock, one rule. */
362
- export function chatLiveness(row: ChatSessionRow, now = Date.now()): "fresh" | "stale" | "active" | "idle" {
354
+ /** The liveness verdict for one roster row, from heartbeat age plus (for
355
+ * opencode rows) the open-tab set:
356
+ * - "open" - fresh heartbeat AND a tab is open now (session presence)
357
+ * - "recent" - fresh heartbeat, no known tab: an instance that saw this
358
+ * session is alive and beating it, but the session itself may or may
359
+ * not be wakeable (a closed tab's wake can be swallowed; a daemon-
360
+ * resident subordinate runs headless and may still pick up mail)
361
+ * - "stale" - the hosting harness has missed two beats and is presumed
362
+ * gone
363
+ * - "active"/"idle" for MCP rows (turn-driven; idle between prompts is
364
+ * the NORMAL state, so an idle MCP row is reachable, not dead)
365
+ * Heartbeat age alone decides staleness - a process id cannot serve: the
366
+ * same session is re-hosted by a new process on every `-s` resume, so a
367
+ * pid stamped on the row lies as soon as a different harness beats it.
368
+ * The tab set is a READ-ONLY refinement (src/tab-state.ts) consumed at
369
+ * render/broadcast time; omitting it degrades every fresh opencode row
370
+ * to "recent" - honest, never a false "open". */
371
+ export function chatLiveness(row: ChatSessionRow, now = Date.now(), openTabs?: ReadonlySet<string>): "open" | "recent" | "stale" | "active" | "idle" {
363
372
  const stale = isStale(row, now);
364
373
  if (row.host_kind === "mcp") return stale ? "idle" : "active";
365
- return stale ? "stale" : "fresh";
374
+ if (stale) return "stale";
375
+ return openTabs?.has(row.session_id) ? "open" : "recent";
366
376
  }
367
377
 
368
378
  /** Orders roster rows for display: project first (so a shared-tree reader
@@ -858,10 +868,16 @@ export class ChatStore {
858
868
  }
859
869
 
860
870
  /**
861
- * Posts a message to every other registered session at once. Stale
862
- * sessions are skipped, not messaged - a host process that has stopped
863
- * heartbeat-ing will never read the mail, and a broadcast is for reaching
864
- * live agents. Each recipient gets its own inbox row per part, so the
871
+ * Posts a message to every other registered session at once. Opencode
872
+ * rows are skipped unless they are OPEN - a fresh heartbeat AND a tab
873
+ * open right now. Both skip classes are honest: a STALE row's host
874
+ * process has stopped heartbeat-ing and will never read the mail, and a
875
+ * RECENT row (fresh heartbeat, no open tab) is not reliably wakeable -
876
+ * the wake can be swallowed without a tab to receive it, so the fan-out
877
+ * would spend a model turn on a delivery that likely never runs. MCP
878
+ * rows always receive (they read mail at their next prompt). Every skip
879
+ * is listed by name in `skipped` so the caller can see who was left
880
+ * out and why. Each recipient gets its own inbox row per part, so the
865
881
  * existing wake machinery (grouping, gating, rate cap) treats the
866
882
  * broadcast as ordinary per-recipient mail. An oversized body is
867
883
  * auto-split (splitChatBody) and every recipient gets every part inside
@@ -871,6 +887,7 @@ export class ChatStore {
871
887
  broadcast(
872
888
  fromSession: string,
873
889
  body: string,
890
+ openTabs?: ReadonlySet<string>,
874
891
  ): { ok: true; recipients: string[]; skipped: string[]; deduped: number; parts: number } | { ok: false; error: string } {
875
892
  const trimmed = body.trim();
876
893
  if (!trimmed) return { ok: false, error: `Message body cannot be empty (attempted ${body.length} characters).` };
@@ -885,6 +902,9 @@ export class ChatStore {
885
902
  };
886
903
  }
887
904
  const parts = splitChatBody(trimmed);
905
+ // No provider = no tab signal: every fresh opencode row degrades to
906
+ // RECENT (honest - an unknown tab state never reads as open).
907
+ const open = openTabs ?? new Set<string>();
888
908
  const recipients: string[] = [];
889
909
  const skipped: string[] = [];
890
910
  let deduped = 0;
@@ -894,10 +914,12 @@ export class ChatStore {
894
914
  this.#db.transaction(() => {
895
915
  for (const row of this.list()) {
896
916
  if (row.session_id === fromSession) continue;
897
- // Staleness means "host process gone" only for opencode rows; an
898
- // mcp row between turns reads its mail at the next prompt, so it
917
+ // Two honest skip classes for opencode rows: STALE (host process
918
+ // gone - never reads the mail) and RECENT (fresh heartbeat, no
919
+ // open tab - the wake is not guaranteed to run a turn). An mcp
920
+ // row between turns reads its mail at the next prompt, so it
899
921
  // always receives.
900
- if (row.host_kind === "opencode" && isStale(row)) {
922
+ if (row.host_kind === "opencode" && (isStale(row) || !open.has(row.session_id))) {
901
923
  skipped.push(row.name);
902
924
  continue;
903
925
  }
package/src/commands.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import {
4
+ REVIEW_SPECIALISTS,
4
5
  defragCore,
5
6
  extractionCore,
6
7
  hygieneCore,
@@ -24,12 +25,17 @@ import {
24
25
  *
25
26
  * Two command families live here:
26
27
  *
27
- * - Actions (/thatch/defrag, extract, hygiene, reflect): user-invoked
28
- * wrappers around a behavior that the nudges otherwise only run on their
29
- * own schedule. Each action body is a prompt core from src/prompts.ts, so
30
- * nudge and command wording cannot drift.
28
+ * - Actions (/thatch/defrag, extract, hygiene, peer-review, refine,
29
+ * reflect, self-review, whois): user-invoked commands. The nudge-backed
30
+ * ones (defrag, extract, hygiene, reflect) render prompt cores from
31
+ * src/prompts.ts, so nudge and command wording cannot drift. The rest
32
+ * (peer-review, refine, self-review, whois) are self-contained here:
33
+ * they have no nudge counterpart. peer-review and self-review couch the
34
+ * code review pipeline with a mode overlay: the same machinery, with
35
+ * findings weighted for whose work is under review.
31
36
  * - Wrap-ups (/thatch/compact, exit): greenlight-token commands that need
32
- * the opencode plugin's TUI control routes, so they are opencode-only.
37
+ * the opencode plugin's host action routes (the compact trigger and the
38
+ * session close-out), so they are opencode-only.
33
39
  */
34
40
 
35
41
  /** Greenlight token ending /thatch/compact's assistant response when compaction is safe. */
@@ -39,11 +45,13 @@ export const COMPACT_READY_TOKEN = "THATCH_COMPACT_READY";
39
45
  export const EXIT_READY_TOKEN = "THATCH_EXIT_READY";
40
46
 
41
47
  /**
42
- * The plugin triggers compaction by running the TUI's compact action. The
43
- * execute-command route only accepts legacy alias names, and "session_compact"
44
- * maps to the same session.compact action the built-in /compact command runs.
45
- * There is no exit alias, so the exit path publishes the TUI keymap command
46
- * ("app.exit") directly via /tui/publish.
48
+ * The greenlight token's host action, per host: v1 drives the TUI
49
+ * (compaction via the "session_compact" legacy alias through
50
+ * executeCommand, the exit via the "app.exit" keymap command through
51
+ * /tui/publish). v2 calls session.compact on the plugin's promise domain
52
+ * and emits the session-tab bridge's TUI-only exit-tab-closed event; the
53
+ * runtime records the exiting session's watcher deaths before either host
54
+ * action runs (src/runtime.ts).
47
55
  */
48
56
 
49
57
  const sharedChecklist = `1. Flush pending persistence. Call thatch_get_extraction_payload; if it returns buffered tool interactions, process them now (write any memories worth keeping), then call thatch_extraction_done to mark them complete. Complete any memory writes you promised earlier but have not made.
@@ -55,10 +63,42 @@ $ARGUMENTS
55
63
 
56
64
  (That section carries the text typed after the command. When it is empty, treat the user message as n/a - the command was run bare.)`;
57
65
 
66
+ /**
67
+ * How the review-mode commands (peer-review, self-review) run the code
68
+ * review machinery on the host at hand. opencode has the
69
+ * thatch-code-review coordinator skill and sub-agent dispatch; Claude Code
70
+ * and Cursor have neither, so the same procedure runs inline through the
71
+ * shared specialist skills. The mode overlay that differentiates the two
72
+ * commands lives in each command's own body and applies in both paths:
73
+ * sub-agent briefings on opencode, specialist passes inline elsewhere.
74
+ *
75
+ * Both couchings below are shared consts interpolated into the two bodies
76
+ * so the commands' procedure and target resolution cannot drift; each body
77
+ * keeps only its mode rules. The specialist roster and count come from
78
+ * REVIEW_SPECIALISTS in src/prompts.ts, the same source as the install
79
+ * list in src/skills.ts.
80
+ */
81
+ const reviewPipelineCouching = `If the thatch-code-review skill is available, load it and follow its steps, with one addition: apply the mode rules below throughout - to context gathering, every specialist briefing, and the synthesis.
82
+
83
+ If that skill is not available on this host, run the same procedure yourself, in order:
84
+ - Resolve the review range. Compute the merge-base against the base branch's remote tracking ref (for example origin/main), not the working branch's own upstream, and cite every file:line against the range's head SHA with git show, never the working tree. A PR or MR number resolves through the host's CLI (gh pr view, glab mr view) plus fetching the head ref (git fetch origin refs/pull/N/head on GitHub, refs/merge-requests/N/head on GitLab, or the source branch). A repository with no remote reviews the working tree's recent commits instead.
85
+ - Build the context brief with the thatch-review-context skill.
86
+ - Partition large diffs into review units.
87
+ - Run the ${REVIEW_SPECIALISTS.length} specialist skills (${REVIEW_SPECIALISTS.join(", ")}) on each unit.
88
+ - Synthesize with thatch-review-synthesizer.
89
+ The mode rules below apply in every specialist pass and in the synthesis, on every host. If the specialist skills are not installed on this host, tell the user to run thatch setup for it.`;
90
+
91
+ /**
92
+ * How the review-mode commands resolve what to review. Shared so the two
93
+ * commands accept the same target syntax and default identically.
94
+ */
95
+ const reviewTargetCouching = `The user message may name the review target: a branch, a PR or MR number, or a git range. On MCP hosts the Focus: line carries that text instead. If no target is given, review the current branch against its base.`;
96
+
58
97
  /**
59
98
  * One on-demand action: the command name (minus the thatch/ prefix), the
60
99
  * description shown in the command menu, and the instruction body (a prompt
61
- * core). opencodeOnly marks actions that need a session identity or other
100
+ * core for the nudge-backed actions, self-contained text for the rest).
101
+ * opencodeOnly marks actions that need a session identity or other
62
102
  * plugin-only capability; those are excluded from Claude Code command files
63
103
  * and MCP prompts.
64
104
  */
@@ -71,8 +111,11 @@ export interface ActionDef {
71
111
 
72
112
  /**
73
113
  * The on-demand action set, shared by every host that renders actions. The
74
- * bodies come from prompt cores so a wording change lands in the nudge and
75
- * the command at once. `tool` renders tool names in the host's spelling.
114
+ * nudge-backed bodies (defrag, extract, hygiene, reflect) come from prompt
115
+ * cores in src/prompts.ts so a wording change lands in the nudge and the
116
+ * command at once; the rest (peer-review, refine, self-review, whois) are
117
+ * self-contained here because they have no nudge counterpart. `tool`
118
+ * renders tool names in the host's spelling.
76
119
  */
77
120
  export function actionDefs(tool: ToolNamer): ActionDef[] {
78
121
  return [
@@ -100,6 +143,25 @@ After dispatching, call thatch_extraction_done to acknowledge.`,
100
143
  description: "Tend the memory store: stale entries, orphans, pending dedup pairs",
101
144
  body: hygieneCore(),
102
145
  },
146
+ {
147
+ name: "peer-review",
148
+ description: "Review another author's change (PR, MR, branch, or git range) through the full review pipeline",
149
+ // Skill-couching action (refine precedent): no nudge counterpart, so
150
+ // the body lives here. The memory-walk tool name is spelled through
151
+ // the namer so MCP renderings carry the mcp__thatch__ form.
152
+ body: `Review a change authored by someone else: a PR, MR, or branch on origin. The review runs the full thatch review pipeline with findings weighted for org velocity.
153
+
154
+ ${reviewTargetCouching}
155
+
156
+ ${reviewPipelineCouching}
157
+
158
+ ## Peer review mode rules
159
+
160
+ - Weight for org velocity. Findings that speed up or unblock the whole team outrank findings about individual lines. The highest-value findings: a simpler method that achieves the same outcome; an existing function or helper that already does what this change reimplements; documentation that is accurate and extremely clearly written; unintended consequences for callers, sibling features, or contracts the author could not see.
161
+ - Walk the project's memory. The author does not have this project's persistent memory; you do. During context gathering, call ${tool("memory_recall")} with several phrasings about the touched area: past incidents, design constraints, rejected approaches, known footguns. Check each recalled item against the change and report the ones that apply. Frame these findings as sharing knowledge the author could not have had, not as catching mistakes.
162
+ - Do not re-litigate settled designs. When the context brief or memory shows a design decision was agreed (in a ticket discussion, a prior review acceptance, recorded docs, or a memory of the agreement), treat the decision as settled. Findings may address how the change implements that decision. Findings that argue the decision itself was wrong are out of scope for this review.
163
+ - Never report findings of the form "if someone changes this in the future, it will silently break." This review does not solve for engineers who change code without reading it. It solves for engineers and LLMs who WILL read the code. For them, verify the change leaves a breadcrumb trail to wherever this organization records how a feature works (a context module, a feature doc, a README). Flag a missing trail. Do not flag hypothetical future misuse.`,
164
+ },
103
165
  {
104
166
  name: "refine",
105
167
  description: "Refine a plan under the project type's requirements (classifies the project first)",
@@ -115,6 +177,26 @@ The user message may name a project type (team-app, personal, shared-lib, spike,
115
177
  description: "Persist what this session learned as memories",
116
178
  body: reflectCore(tool),
117
179
  },
180
+ {
181
+ name: "self-review",
182
+ description: "Review the user's own completed work before submitting it",
183
+ // Skill-couching action (see peer-review). Names no tools, so the
184
+ // MCP spelling test exempt list covers it.
185
+ body: `Review the user's own completed work before submitting it: as a PR or MR on a shared project, or as a direct commit on a personal project. The review runs the full thatch review pipeline with findings weighted to protect the user's reputation as a conscientious developer.
186
+
187
+ The work under review was authored by the user, or by their agent acting in the user's name. Two goals: the work is sound, and it does not read as sloppy. Reviewers and teammates read the comments, docs, and commit messages as the user's own.
188
+
189
+ ${reviewTargetCouching}
190
+
191
+ ${reviewPipelineCouching}
192
+
193
+ ## Self review mode rules
194
+
195
+ - The reputation lenses weigh heaviest. NoSlop, Technique, and Breadcrumbs findings outrank same-severity findings from the other lenses. AI-writing tells, vague comments, and a missing narrative are exactly what this review exists to catch before anyone else sees them.
196
+ - Overweight simplicity on shared projects. Prefer findings that make the design easier to understand, easier to test, and more reliable: fewer assumptions, less surface area for bugs and side effects.
197
+ - Technique and highlights findings are decisions, not footnotes. Each one that would produce a simpler, clearer, or more reliable design is surfaced for an adopt-or-dismiss decision before submission. Work through them with the user one at a time; do not leave them as informational asides.
198
+ - Iterate rounds. After each round, work the findings with the user: fix each, or explicitly decline it with a reason. Then run another full round on the updated state. A round is meaningful when it produces a MEDIUM or higher finding, or a technique or slop finding worth adopting. A round producing only LOW mechanical findings gets them fixed but does not trigger another round. Stop at the first round with no meaningful findings, and never run more than three rounds.`,
199
+ },
118
200
  {
119
201
  name: "whois",
120
202
  description: "Report this session's chat identity (handle, topic, project), or look up another session's",
@@ -154,8 +236,18 @@ ${userMessageSection}
154
236
  ${action.body}`;
155
237
  }
156
238
 
239
+ /** The wrap-up command's menu/palette description. Host-neutral on purpose:
240
+ * what actually happens after the greenlight differs per host (v1 exits the
241
+ * app, v2 closes the session's tab), and the template text the MODEL reads
242
+ * is shared verbatim by both hosts. */
243
+ export function wrapUpCommandDescription(kind: "compact" | "exit"): string {
244
+ return kind === "compact"
245
+ ? "Flush thatch persistence, check for loose ends, then compact if clear"
246
+ : "Flush thatch persistence, check for loose ends, then end the session if clear";
247
+ }
248
+
157
249
  const COMPACT_TEMPLATE = `---
158
- description: ${yamlQuote("Flush thatch persistence, check for loose ends, then compact if clear")}
250
+ description: ${yamlQuote(wrapUpCommandDescription("compact"))}
159
251
  ---
160
252
  ${userMessageSection}
161
253
 
@@ -172,7 +264,7 @@ ${COMPACT_READY_TOKEN}
172
264
  If anything is outstanding, list the items concisely so the user can address them, and do NOT include the token. The session will only be compacted when the token is present.`;
173
265
 
174
266
  const EXIT_TEMPLATE = `---
175
- description: ${yamlQuote("Flush thatch persistence, check for loose ends, then exit opencode if clear")}
267
+ description: ${yamlQuote(wrapUpCommandDescription("exit"))}
176
268
  ---
177
269
  ${userMessageSection}
178
270
 
@@ -181,13 +273,12 @@ ${userMessageSection}
181
273
  Work through this checklist before responding:
182
274
 
183
275
  ${sharedChecklist}
184
- 3. Leave the chat directory. Call thatch_chat_unregister for this session - the session is exiting, so other sessions must stop addressing mail to it.
185
276
 
186
277
  Do not start new work beyond this checklist. If every item is handled and nothing needs the user's attention first, end your final response with exactly this token as the very last line, with no formatting around it:
187
278
 
188
279
  ${EXIT_READY_TOKEN}
189
280
 
190
- If anything is outstanding, list the items concisely so the user can address them, and do NOT include the token. opencode will only exit when the token is present.`;
281
+ If anything is outstanding, list the items concisely so the user can address them, and do NOT include the token. The session will only close when the token is present.`;
191
282
 
192
283
  /** One command: the command name (minus the thatch/ prefix) and its full markdown file content. */
193
284
  export interface CommandDef {
@@ -221,7 +312,7 @@ export function opencodeCommandDefs(): CommandDef[] {
221
312
  /**
222
313
  * The Claude Code command set: the actions minus the opencode-only ones.
223
314
  * The wrap-up commands are excluded too - their greenlight check needs the
224
- * plugin's command.execute.before arming and TUI control routes.
315
+ * plugin's command.execute.before arming and host action routes.
225
316
  */
226
317
  export function claudeCommandDefs(): CommandDef[] {
227
318
  return actionDefs(mcpToolName)