@jeffober/thatch 0.1.40 → 0.1.41

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.
@@ -1,11 +1,9 @@
1
1
  # CLI (bin/thatch)
2
2
 
3
- The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, and the setup installer.
3
+ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, the setup installer, session archaeology over the opencode database, and a read-only window on the cross-session chat directory.
4
4
 
5
5
  ## Subcommands
6
6
 
7
- 15 subcommands:
8
-
9
7
  | Command | Args | Flags | Stdin | Purpose |
10
8
  |---------|------|-------|-------|---------|
11
9
  | `stores` | none | none | none | List all store names |
@@ -22,6 +20,9 @@ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, M
22
20
  | `flush-tools [--json]` | none | `--json` | JSON | Peek queue + extraction/recall/prediction/behavior/write nudge |
23
21
  | `flush-predictions [--json]` | none | `--json` | JSON | Standalone prediction-only nudge |
24
22
  | `setup --claude [--cursor] [--global]` | none | `--claude`, `--cursor`, `--global` | none | Install config + instructions + hooks + skills |
23
+ | `session list/get/transcript/search` | per subcommand | `-s/--session`, `--id`, `--after`, `--before`, `--regex`, `--limit` | none | Read-only archaeology on the opencode session database (JSONL output) |
24
+ | `chat list` | none | none | none | Registered chat sessions: name, human-readable age, project, topic |
25
+ | `chat tail [--once]` | none | `--once` | none | Follow cross-session chat: sent and read events (`--once` prints the backlog and exits) |
25
26
  | (unknown) | none | none | none | Print usage, exit 1 |
26
27
 
27
28
  ## Global behavior
@@ -41,6 +42,21 @@ Runs the thatch-project-primer skill via an external CLI. Searches `PATH` in ord
41
42
 
42
43
  Inherits stdio, exits with the child's exit code. Errors and exits 1 if none found.
43
44
 
45
+ ## chat
46
+
47
+ Read-only window on the cross-session chat directory ([cross-session-chat.md](cross-session-chat.md)):
48
+
49
+ - `chat list` renders the roster: display name, human-readable age since the
50
+ last heartbeat, project, and topic when set.
51
+ - `chat tail [--once]` follows the message stream. Sent lines have the shape
52
+ `[timestamp] from -> to: body` (broadcast rows render `broadcast` via the
53
+ `via_broadcast` marker); in follow mode, an inbox drain emits a read line
54
+ (`[timestamp] ${reader} read a message from ${sender}: preview` — the
55
+ reader is the recipient draining its inbox). The diff logic is
56
+ `chatTailDiff()`/`formatChatTailEvent()` in `src/chat.ts`, unit-tested
57
+ there; `--once` prints one snapshot and exits, because a snapshot has no
58
+ previous state to diff read events against.
59
+
44
60
  ## Environment variables
45
61
 
46
62
  | Variable | Purpose | Default |
@@ -59,7 +75,7 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
59
75
 
60
76
  ## Source files
61
77
 
62
- - `bin/thatch` — the CLI (Bun script, 486 lines)
78
+ - `bin/thatch` — the CLI (Bun script)
63
79
  - `bin/release` — release helper (bash, separate from the main CLI)
64
80
 
65
81
  ## Interactions with other features
@@ -70,6 +86,8 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
70
86
  - Extraction ([extraction.md](extraction.md)): `buffer-batch`, `buffer-tool`, `flush-tools` subcommands
71
87
  - Hygiene ([hygiene.md](hygiene.md)): `hygiene`, `reminder` subcommands
72
88
  - Multi-host ([multi-host.md](multi-host.md)): `mcp` subcommand starts the MCP server for Claude Code and Cursor
89
+ - Session archaeology: the `session` subcommand group (see the table above) reads the opencode session database via `src/session-db.ts`
90
+ - Cross-session chat ([cross-session-chat.md](cross-session-chat.md)): the `chat` subcommand group reads the shared chat directory and inbox
73
91
 
74
92
  ## Key invariants
75
93
 
@@ -0,0 +1,256 @@
1
+ # Cross-Session Chat
2
+
3
+ Cross-session chat lets independent opencode sessions on one machine message
4
+ each other through thatch: a session registers in a shared directory, other
5
+ sessions see it in the list, and messages land in its inbox. An idle
6
+ recipient is woken with a prompt when mail arrives, so coordination does not
7
+ route through the human ("ask the other session whether the release is
8
+ green" instead of "Jeff, go ask").
9
+
10
+ User-facing behavior is documented in
11
+ [docs/user/cross-session-chat.md](../../user/cross-session-chat.md).
12
+
13
+ ## What it does
14
+
15
+ - `thatch_chat_register` / `thatch_chat_list` / `thatch_chat_send` /
16
+ `thatch_chat_read` / `thatch_chat_unregister` / `thatch_chat_broadcast`
17
+ tools (shared across hosts; opencode injects identity, MCP hosts
18
+ self-declare with the as argument)
19
+ - A shared SQLite directory and inbox in thatch.db, writable by any opencode
20
+ process on the machine
21
+ - A per-process poller that heartbeats hosted sessions and delivers wake
22
+ prompts via `client.session.promptAsync` with a synthetic part - the same
23
+ mechanism watchers use
24
+ - Anti-loop protection at two levels: prompt guidance (messages are
25
+ informational, no reflexive auto-reply) and a hard per-recipient nudge
26
+ rate cap
27
+
28
+ ## The shared-state, local-delivery decision
29
+
30
+ Watchers keep their registry in plugin memory, never SQLite, because the
31
+ process that registered a watcher is the only one that can deliver its
32
+ events (see [watchers.md](watchers.md)). Chat cannot work that way: the
33
+ sender's process and the recipient's process are usually different, so the
34
+ state they coordinate through must be shared.
35
+
36
+ The split:
37
+
38
+ - **State is shared.** `chat_sessions` and `chat_messages` live in thatch.db.
39
+ Every opencode process on the machine reads and writes the same tables.
40
+ Concurrent access is the already-solved case - multiple sessions share the
41
+ DB for memories today, under WAL plus a busy timeout.
42
+ - **Delivery is local.** Each process polls the inbox for messages addressed
43
+ to sessions it hosts (those it has seen `session.status` events for) and
44
+ prompts them through its own SDK client. A sender never prompts a session
45
+ in another process.
46
+
47
+ This preserves the watcher rationale's core property - no process prompts a
48
+ session it does not host - so each message has exactly one possible
49
+ deliverer. There is no claiming logic, no double-delivery race, and no
50
+ orphaned delivery state. The cost is the mirror of watchers': chat state
51
+ outlives a process, so liveness needs a signal beyond process lifetime.
52
+
53
+ ## How it works
54
+
55
+ ### Registration and identity
56
+
57
+ `chat_register` joins the directory. Without a name it draws one at random
58
+ from the built-in pool (`src/chat-names.ts`): whimsical geek-culture names
59
+ in the style of fnord's Nomenclater, statically baked in so assignment never
60
+ costs a model call. Pool draws redraw on the rare collision a concurrent
61
+ registration can cause, and skip any name a session already claimed by
62
+ custom registration - so the pool is the recommended path. With a name,
63
+ the session claims it as a custom name -
64
+ uniqueness is case-insensitive ("Landru" and "landru" are one name), so two
65
+ visually identical identities cannot coexist, and message addressing follows
66
+ the same rule. Databases created before the case-insensitive constraint are
67
+ rebuilt at schema init (first row wins per case group; message history
68
+ survives because the endpoints are not foreign keys).
69
+
70
+ Identity is the host session ID, which the model cannot know or forge.
71
+ Re-registering renames. Unregistered sessions are invisible and
72
+ unmessageable in both directions. Only top-level sessions should register -
73
+ sub-agent children are ephemeral - which the tool descriptions and system
74
+ prompt state; there is no mechanical barrier, a documented trust posture.
75
+
76
+ Names are claimable by any session (no impersonation defense). The trust
77
+ model is the same as the shared memory stores: every agent on this machine
78
+ is the operator's agent.
79
+
80
+ ### Topics
81
+
82
+ The optional topic is the roster's "who is working on what" signal: with a
83
+ dozen registered sessions, a name like "Kurn the Typechecker" says nothing,
84
+ but `topic:QAing the release` in `chat_list` tells a coordinator which
85
+ session to address. Topics are free text, sanitized to one roster line
86
+ (whitespace runs collapse, 80-character cap, advisory - oversizing degrades
87
+ rather than errors). The null/empty distinction is deliberate: omitting the
88
+ topic on re-register keeps the existing one; an empty topic clears it.
89
+ Broadcast senders benefit most: `chat_broadcast` answers "which of you is
90
+ working on X?" with the roster already in hand.
91
+
92
+ ### Messages
93
+
94
+ `chat_send` addresses a recipient by display name or session ID. Both
95
+ endpoints must be registered and distinct. `chat_read` drains the calling
96
+ session's inbox oldest-first and stamps rows read. Message history survives
97
+ unregistration; a departed sender degrades to an unknown name in the
98
+ reader's view (the endpoints are deliberately not foreign keys - leaving the
99
+ directory must not be blocked by history).
100
+
101
+ `chat_broadcast` posts one message to every other registered session at
102
+ once, one inbox row per recipient - the existing wake machinery (grouping,
103
+ gating, rate cap) treats each as ordinary mail. Stale sessions are skipped
104
+ and reported rather than messaged: a host that has stopped heartbeat-ing
105
+ will never read the mail, and dead mail to a dead process is just clutter.
106
+ The sender is excluded. It is a separate tool rather than a magic
107
+ `chat_send` recipient because "send to all" changes the behavior (fan-out,
108
+ stale skipping, no address resolution), and a function that changes
109
+ behavior drastically on a parameter value is two functions.
110
+
111
+ ### Polling, heartbeat, staleness
112
+
113
+ A `setInterval` loop (default 30s) runs one cycle per process: heartbeat the
114
+ hosted sessions' `last_seen`, then deliver. A crashed process fires no
115
+ `session.deleted`, so `chat_list` marks sessions whose `last_seen` is older
116
+ than the staleness threshold (default 10 minutes) as stale rather than
117
+ hiding them - the operator decides what to do with ghosts. `session.deleted`
118
+ is the graceful-exit fast path that unregisters immediately.
119
+
120
+ ### Delivery, re-nudge, and the rate cap
121
+
122
+ A message needs a wake prompt when it is unread and either never delivered
123
+ or delivered more than the re-nudge window ago (default 15 minutes).
124
+ Delivery is gated by the same `canDeliver` predicate watchers use - the
125
+ recipient must be idle and not compacting - and the idle event handler
126
+ flushes pending mail directly so it lands promptly instead of waiting for
127
+ the next cycle. Failed deliveries stay pending and retry. Only registered
128
+ recipients are ever selected: unregistering stops wake prompts for
129
+ kept-but-unread mail, which is the `chat_unregister` tool's promise.
130
+ Delivery is at-least-once - a crash after the wake prompt but before the
131
+ delivered_at stamp re-nudges the same batch on the next cycle.
132
+
133
+ One wake prompt covers all of a recipient's pending messages: sender names
134
+ and a count, pointer-only like watcher notifications, with the model calling
135
+ `chat_read` for content. On success the poller stamps `delivered_at`
136
+ (re-stamping on re-nudges resets the re-nudge timer).
137
+
138
+ The hard brake: at most a fixed number of wake prompts per recipient per
139
+ hour (default 6, in-memory on purpose - it is a loop guard, not accounting).
140
+ Without it, two agents politely acknowledging each other would ping-pong
141
+ forever, each wake triggering a reply that wakes the other.
142
+
143
+ ### Transcript echo
144
+
145
+ Plugin tools render in the opencode TUI as muted one-line generic entries,
146
+ with the output block behind a default-off toggle - so without help, a chat
147
+ exchange is invisible to the human watching the session. The plugin's
148
+ `tool.execute.after` hook builds a short echo for the conversational events
149
+ (`chatEchoText()` in `src/prompts.ts`: register, send, read, broadcast) and
150
+ delivers it as a non-synthetic, `noReply` promptAsync part: rendered as a
151
+ visible bubble in the transcript, no model turn started (the server's
152
+ prompt path returns before the completion loop). Failed calls never echo;
153
+ `chat_list` and `chat_unregister` stay on the muted tool line. Broadcasts
154
+ echo their fan-out count with the clipped body.
155
+
156
+ The trade-off: non-synthetic is what makes the TUI render the part, and it
157
+ also means later turns see the echo in context - a small duplication of the
158
+ tool call it mirrors, accepted for visibility. Echo bodies are clipped
159
+ (send: the body; read: the formatted inbox; broadcast: the body) so a
160
+ bubble stays cheap. Echo delivery is fire-and-forget: a failure must never
161
+ fail the tool call it follows. Because the opencode server fires the
162
+ `chat.message` hook for every prompt part before the `noReply`
163
+ early-return, the plugin's `chat.message` handler skips messages whose
164
+ visible text is entirely `[chat]`-prefixed bubbles (`isChatEchoParts`) -
165
+ otherwise every echo would run the nudge machinery for a message no model
166
+ turn reads. The accepted edge: a real user message whose every visible
167
+ part starts with the prefix is skipped the same way, losing that one turn's
168
+ advisory nudges.
169
+
170
+ ### CLI access
171
+
172
+ The user watches the conversation without an agent: `thatch chat list`
173
+ renders the roster with human-readable heartbeat ages, and
174
+ `thatch chat tail [--once]` follows the message stream. The tail diffs
175
+ `ChatStore.messageFeed()` snapshots via `chatTailDiff()` (unit-tested in
176
+ `tests/chat.test.ts`): sent lines on first view, then new sends and newly-
177
+ read messages per poll. `chat_messages.via_broadcast` marks fan-out rows so
178
+ the tail renders one `-> broadcast` line per recipient instead of what
179
+ would otherwise look like identical direct sends. The CLI takes no chat
180
+ write actions (the wake machinery owns those paths); the only writes it
181
+ can trigger are the schema migrations that run whenever any thatch
182
+ process opens the database.
183
+
184
+ ## Interactions with other features
185
+
186
+ - Watchers ([watchers.md](watchers.md)): chat reuses the delivery gate
187
+ (`canDeliver` over `sessionStatus` + `compacting`), the idle-flush
188
+ pattern, and the synthetic-part + toast notification shape. The state
189
+ model is the deliberate opposite - shared SQLite vs process memory - and
190
+ each feature's doc explains why its side of the split is right for its
191
+ problem.
192
+ - Session lifecycle ([session-lifecycle.md](session-lifecycle.md)):
193
+ `session.status` feeds the delivery gate and the hosted-session set;
194
+ `session.deleted` unregisters.
195
+ - Extraction pipeline ([extraction.md](extraction.md)): chat tool calls
196
+ are `thatch_*` tools and excluded from buffering like all thatch tools;
197
+ non-thatch tool calls made during a chat notification turn are buffered
198
+ and extracted like any other turn's.
199
+ - Multi-host ([multi-host.md](multi-host.md)): the chat tools are shared -
200
+ MCP hosts register with a self-declared identity (the `as` argument;
201
+ the stored session ID is synthetic, `mcp_<hash of name>`), and pending
202
+ mail surfaces at prompt time through the flush-tools hook line and
203
+ `chat_status`. Wake-up delivery remains opencode-only: no MCP server
204
+ can start a turn in the client's conversation.
205
+
206
+ ## Multi-host delivery tiers
207
+
208
+ Chat works on every host. Earlier designs for the MCP path - a JSONL
209
+ temp-file inbox, a socket or daemon - were rejected: the DB already is the
210
+ inbox, and MCP hosts cannot hold connections across turns, so both
211
+ degenerate to the turn-granularity polling the tools already provide. The
212
+ wake analog is the existing hook channel.
213
+
214
+ | Host | Send/receive | New-mail signal | Echo bubbles |
215
+ |-------------|--------------|------------------------------|--------------|
216
+ | opencode | yes | promptAsync wake (idle-gated)| yes |
217
+ | claude code | yes | flush-tools hook line | no |
218
+ | cursor | yes | flush-tools hook line | no |
219
+
220
+ MCP identity is the registered name: `chat_register` without a host
221
+ session stores a synthetic `mcp_<sha256(name)[0:12]>` session ID, so the
222
+ identity is stable across the ephemeral sessions those harnesses run.
223
+ `chat_status` is the quiet check (registered flag + pending/total); the
224
+ flush-tools hook line names the addressee sessions and prints nothing
225
+ when the project has no pending chat - absence is silent by construction.
226
+ Staleness semantics differ by kind: an opencode row's stale age means the
227
+ process is gone (broadcast skips it), while an mcp row's age only means
228
+ "between turns" (broadcast always delivers).
229
+
230
+ ## Defaults
231
+
232
+ Timing constants live in `src/chat.ts`: poll interval 30s, staleness
233
+ threshold 10 minutes, re-nudge window 15 minutes, nudge cap 6 per recipient
234
+ per hour. There are no environment overrides yet; add them the way
235
+ `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
236
+
237
+ ## Source files
238
+
239
+ - `src/chat.ts` - ChatStore (directory + inbox SQL, pool assignment),
240
+ ChatPoller (heartbeat, gated delivery, re-nudge, rate cap), staleness
241
+ helper
242
+ - `src/chat-names.ts` - the static display-name pool (nomenclater style)
243
+ - `src/db.ts` - the chat tables in schema init, the NOCASE collation
244
+ migration and topic column migration, delegated methods
245
+ - `src/tool-defs.ts` - the chat tool definitions
246
+ - `src/index.ts` - poller construction, delivery closure, idle flush,
247
+ session.deleted unregister, dispose, transcript echo in
248
+ tool.execute.after
249
+ - `src/prompts.ts` - `chatNotificationNudge()`, `chatEchoText()` /
250
+ `isChatEchoParts()`, system prompt Cross-Session Chat section, MCP
251
+ absent-tools note
252
+ - `tests/chat.test.ts` - store, pool, migration, echo-text, and poller unit
253
+ tests (temp-dir SQLite, injected delivery)
254
+ - `tests/qa/auto/uc-097-chat.ts` - full lifecycle against a mocked poller
255
+ - `tests/qa/live/uc-098-chat-cross-session.ts` - two real sessions exchange
256
+ a message through the shared DB
@@ -1,6 +1,6 @@
1
1
  # Database
2
2
 
3
- Thatch stores all data in a single SQLite file. The schema is organized around three concerns: memory entries, the prediction engine, and the behavior engine.
3
+ Thatch stores all data in a single SQLite file. The schema is organized around feature concerns: memory entries, the prediction engine, the behavior engine, and cross-session chat.
4
4
 
5
5
  ## Configuration
6
6
 
@@ -10,13 +10,14 @@ Thatch stores all data in a single SQLite file. The schema is organized around t
10
10
 
11
11
  ## Schema overview
12
12
 
13
- The tables fall into three groups:
13
+ The tables fall into these groups:
14
14
 
15
15
  | Group | Tables | Purpose |
16
16
  |-------|--------|---------|
17
17
  | Memory | `stores`, `entries`, `dedup_pairs` | Core memory CRUD + dedup verdicts |
18
18
  | Prediction engine | `prediction_matchers`, `predictions`, `prediction_edges`, `prediction_provenance` | User decision model |
19
19
  | Behavior engine | `behavior_matchers`, `behaviors`, `behavior_edges`, `behavior_provenance` | LLM self-discipline rules |
20
+ | Cross-session chat | `chat_sessions`, `chat_messages` | Session directory + message inbox (shared across opencode processes) |
20
21
 
21
22
  The prediction and behavior engines share the same four-table shape (matchers, items, edges, provenance) with different table names. They are separate because the semantics differ: predictions model what the user wants; behaviors model what the LLM should do.
22
23
 
@@ -213,6 +214,52 @@ behavior_provenance(
213
214
 
214
215
  - `signal`: "confirm" (ham), "disconfirm" (spam), or "codify".
215
216
 
217
+ ## Cross-session chat tables
218
+
219
+ ### chat_sessions
220
+
221
+ ```sql
222
+ chat_sessions(
223
+ session_id TEXT PRIMARY KEY,
224
+ name TEXT NOT NULL UNIQUE COLLATE NOCASE,
225
+ topic TEXT,
226
+ project TEXT,
227
+ registered_at TEXT NOT NULL,
228
+ last_seen TEXT NOT NULL
229
+ )
230
+ ```
231
+
232
+ - The machine-wide session directory. `last_seen` is the heartbeat: each
233
+ host process refreshes it for its own sessions every poll cycle, and a
234
+ stale value marks a crashed or closed process. Name uniqueness is
235
+ case-insensitive; databases whose chat_sessions table predates the NOCASE
236
+ constraint are rebuilt at schema init (first row wins per case group).
237
+
238
+ ### chat_messages
239
+
240
+ ```sql
241
+ chat_messages(
242
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
243
+ from_session TEXT NOT NULL,
244
+ to_session TEXT NOT NULL,
245
+ body TEXT NOT NULL,
246
+ created_at TEXT NOT NULL,
247
+ delivered_at TEXT,
248
+ read_at TEXT,
249
+ via_broadcast INTEGER NOT NULL DEFAULT 0
250
+ )
251
+ ```
252
+
253
+ - `delivered_at` is the last wake-prompt stamp (restamped on re-nudges);
254
+ `read_at` is set when the recipient drains its inbox. `via_broadcast`
255
+ marks rows a chat_broadcast fan-out created (the CLI tail renders them
256
+ as `-> broadcast`). The endpoints are
257
+ plain columns, not foreign keys: unregistering a session must not be
258
+ blocked by message history, and a departed sender degrades to an unknown
259
+ name in the reader's view.
260
+
261
+ See [cross-session-chat.md](cross-session-chat.md) for the delivery model.
262
+
216
263
  ## Schema migration
217
264
 
218
265
  `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing databases by an idempotent column migration at init. The migration uses `PRAGMA table_info` to check for the column's existence, then `ALTER TABLE ADD COLUMN` if missing. This handles databases created before these columns existed.
@@ -235,8 +282,9 @@ Embeddings are raw Float32Array bytes stored as BLOBs. Serialization honors `byt
235
282
 
236
283
  - Memory store ([memory-store.md](memory-store.md)): `entries` table is the core; `stores` table is the registry
237
284
  - Deduplication ([deduplication.md](deduplication.md)): `dedup_pairs` table tracks reviewed pairs
238
- - Prediction engine ([prediction-engine.md](prediction-engine.md)): 4 prediction tables
239
- - Behavior engine ([behavior-engine.md](behavior-engine.md)): 4 behavior tables
285
+ - Prediction engine ([prediction-engine.md](prediction-engine.md)): the prediction tables
286
+ - Behavior engine ([behavior-engine.md](behavior-engine.md)): the behavior tables
287
+ - Cross-session chat ([cross-session-chat.md](cross-session-chat.md)): the chat tables, shared across opencode processes
240
288
  - Hygiene ([hygiene.md](hygiene.md)): `staleEntryCount` uses `recall_count` and `last_recalled_at`; `branchesInStore` uses `branch` column
241
289
 
242
290
  ## Source files
@@ -16,11 +16,16 @@ Thatch supports three host agents from a single codebase:
16
16
  - **Cursor MCP**---same stdio MCP server, `--json` hook output format,
17
17
  `mcp__thatch__` tool prefix, system prompt in AGENTS.md
18
18
 
19
- All three paths share the tool definitions from `src/tool-defs.ts`: 18
20
- host-agnostic tools plus opencode-only tools (currently seven:
21
- `get_session_info`, `session_search`/`session_get`, and the
22
- `watch_create`/`watch_branch_create`/`watch_list`/`watch_cancel` watcher tools), which the MCP server
23
- filters out. The shared tools take args and a `CoreContext`
19
+ All three paths share the tool definitions from `src/tool-defs.ts`: the
20
+ host-agnostic tools plus the opencode-only tools, which the MCP server
21
+ filters out. The opencode-only set covers host-session capabilities MCP
22
+ hosts lack: `get_session_info`, `session_search`/`session_get` (no session
23
+ concept or session database), the `watch_create`/`watch_branch_create`/
24
+ `watch_list`/`watch_cancel` watcher tools (no poller or proactive-prompt
25
+ channel), and the `chat_register`/`chat_list`/`chat_send`/`chat_read`/
26
+ `chat_unregister`/`chat_broadcast` chat tools (no session identity or
27
+ wake-up channel). The
28
+ shared tools take args and a `CoreContext`
24
29
  (`{ db, model, defaultStore }`) and return a string. The host-specific
25
30
  wrapping---prefixing, transport, session hooks---lives outside the core.
26
31
 
@@ -74,9 +79,10 @@ at session start.
74
79
  so the agent sees `mcp__thatch__memory_remember`. Thatch itself does not apply
75
80
  this prefix.
76
81
 
77
- **Skills**: installed to `$CLAUDE_CONFIG_DIR/skills/`---shared skills only.
78
- The code-review coordinator skill is opencode-only because it requires
79
- sub-agent dispatch, which Claude Code does not support.
82
+ **Skills**: installed to the scope's skills dir (repo `.claude/skills/` for
83
+ project-local setup, `$CLAUDE_CONFIG_DIR/skills/` for `--global`)---shared
84
+ skills only. The code-review coordinator skill is opencode-only because it
85
+ requires sub-agent dispatch, which Claude Code does not support.
80
86
 
81
87
  **Session behavior**: driven by external CLI hook processes (`bin/thatch`).
82
88
  The hooks are registered in `.claude/settings.json` during setup. Each hook
@@ -182,6 +188,8 @@ behavior, with some features having no MCP counterpart.
182
188
  | Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
183
189
  | `get_session_info` (session identity) | Yes | No | No |
184
190
  | `session_search` / `session_get` (conversation archaeology) | Yes (tools) | CLI only (`thatch session ...`) | CLI only (`thatch session ...`) |
191
+ | `watch_*` (PR and branch watchers) | Yes (tools) | No | No |
192
+ | `chat_*` (cross-session chat) | Yes | No | No |
185
193
 
186
194
  For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
187
195
 
@@ -231,4 +239,4 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
231
239
  - The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
232
240
  - The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
233
241
  - The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts` (opencode's list includes opencode-only tools; the MCP lists exclude them). The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
234
- - opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, PR watchers) have no MCP counterpart. The `thatch session` CLI subcommands work anywhere - they only need an opencode.db file to read.
242
+ - opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, PR watchers, cross-session chat) have no MCP counterpart. The `thatch session` CLI subcommands work anywhere - they only need an opencode.db file to read.
@@ -52,8 +52,9 @@ Four artifacts, all idempotent:
52
52
  → `thatch reminder`, `PostToolBatch` → `thatch buffer-batch`,
53
53
  `UserPromptSubmit` → `thatch flush-tools`. Nested format:
54
54
  `{hooks:{Event:{hooks:[{type,command}]}}}`.
55
- 4. **Skills** --- installs to `$CLAUDE_CONFIG_DIR/skills/` (always
56
- user-scoped, even for project-local setup). `SHARED_SKILLS` only --- the
55
+ 4. **Skills** --- installs to the scope's skills dir: the repo's
56
+ `.claude/skills/` for project-local, `$CLAUDE_CONFIG_DIR/skills/` for
57
+ `--global`. `SHARED_SKILLS` only --- the
57
58
  code-review coordinator requires sub-agent dispatch, which Claude Code
58
59
  does not support.
59
60
 
@@ -73,7 +74,9 @@ Four artifacts, same idempotent pattern:
73
74
  `{version:1, hooks:{event:[{command}]}}`. Three hooks: `sessionStart`
74
75
  → `thatch reminder --json`, `postToolUse` → `thatch buffer-tool`,
75
76
  `beforeSubmitPrompt` → `thatch flush-tools --json`.
76
- 4. **Skills** --- installs to `$CURSOR_CONFIG_DIR/skills/` (shared only).
77
+ 4. **Skills** --- installs to the scope's skills dir: the repo's
78
+ `.cursor/skills/` for project-local, `$CURSOR_CONFIG_DIR/skills/` for
79
+ `--global` (shared only).
77
80
 
78
81
  ### Marker system (`appendBlock`)
79
82
 
@@ -184,8 +187,12 @@ example, an older `thatch echo` hook is replaced with the current
184
187
  1. **All operations are idempotent.** Re-running setup updates drifted content
185
188
  without clobbering. Instructions use markers, hooks filter by `thatch`
186
189
  string, MCP config preserves existing servers, skills diff before writing.
187
- 2. **Skills are always user-scoped**, even in project-local setup. Claude Code
188
- and Cursor load skills from the user config dir, not the project.
190
+ 2. **Skills follow the setup scope.** Project-local installs write skills to
191
+ the repo's `.claude/skills/` / `.cursor/skills/` (Claude Code and Cursor
192
+ both discover project skills there); `--global` writes to the user config
193
+ dirs. Setup reports the directory plus added/updated/removed/unchanged
194
+ counts, and notes (without touching) any thatch skills found in the
195
+ opposite scope.
189
196
  3. **`appendBlock` leaves content alone if markers do not parse.** A start
190
197
  marker without an end marker means the file was edited externally. Writing
191
198
  would risk clobbering content after the start marker.
@@ -124,10 +124,13 @@ here first. These are the things that have already cost time.
124
124
 
125
125
  ## Setup
126
126
 
127
- - **Skills are always user-scoped**, even in project-local setup. The project's
128
- `.claude/settings.json` and `CLAUDE.md` stay in the repo, but skills live
129
- under `$CLAUDE_CONFIG_DIR/skills/` / `~/.cursor/skills/`. Installing into the
130
- worktree would mutate the user's repo.
127
+ - **Skills follow the setup scope.** Project-local installs write skills to
128
+ the repo's `.claude/skills/` / `.cursor/skills/` (they version with the
129
+ project); `--global` writes to `$CLAUDE_CONFIG_DIR/skills/` /
130
+ `~/.cursor/skills/`. A run never touches the opposite scope: if thatch
131
+ skills already exist there (e.g. user-scope copies from an older local
132
+ setup), setup reports them in a note but leaves them alone, so they can
133
+ drift stale until refreshed or deleted.
131
134
  - **`appendBlock` leaves content alone if the markers don't parse.** If the
132
135
  start marker is found but the end marker isn't, the whole block is skipped
133
136
  rather than half-replaced. Fix the markers or delete the block manually.
@@ -18,7 +18,7 @@ This document maps the feature parity and documents the remaining gaps.
18
18
 
19
19
  | Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
20
20
  |---------|----------------|------------------------|---------------------|--------|
21
- | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR and branch watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel |
21
+ | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR and branch watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The `chat_*` tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel |
22
22
  | **System prompt** (store names, usage rules) | `experimental.chat.system.transform` — dynamic, repo baked in at runtime | CLAUDE.md static text appended by `thatch setup`; repo auto-detected by the MCP server at startup | AGENTS.md static text appended by `thatch setup`; repo auto-detected at startup | **Approximate** — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
23
23
  | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
24
24
  | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
@@ -27,7 +27,7 @@ This document maps the feature parity and documents the remaining gaps.
27
27
  | **Prompt-aware recall nudge** | `chat.message` hook embeds the prompt with the in-process warm model, searches `db.search()`, pushes a nudge part if matches ≥ threshold | `UserPromptSubmit` hook → `thatch flush-tools` connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — sideband socket gives cold hook processes access to the warm MCP server's model |
28
28
  | **Prediction auto-fire** (user decision model) | `chat.message` hook scores the prompt embedding against prediction matchers; injects a `[thatch] User decision model` nudge alongside the recall nudge (separate synthetic part, same embedding call) | `UserPromptSubmit` hook → `thatch flush-tools` fires prediction query via the sideband socket's `predictions` method in parallel with the recall nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — same `scorePredictionNudge` entry point in both paths |
29
29
  | **Behavior auto-fire** (self-discipline rules) | `chat.message` hook scores the prompt embedding against behavior matchers; injects a `[thatch] Situational behaviors` nudge (separate synthetic part, same embedding call) | `UserPromptSubmit` hook → `thatch flush-tools` fires behavior query via the sideband socket's `behaviors` method in parallel with recall and predictions | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — same `scoreBehaviorNudge` entry point in both paths |
30
- | **Skills** | Installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init (shared **+** opencode-only) | Installed to `$CLAUDE_CONFIG_DIR/skills/` by `thatch setup` (shared only) | Installed to `~/.cursor/skills/` by `thatch setup` (shared only) | **Full** — same SKILL.md format; the code-review coordinator is opencode-only (needs sub-agents) |
30
+ | **Skills** | Installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init (shared **+** opencode-only) | Installed by `thatch setup` (shared only) to the repo's `.claude/skills/` (local) or `$CLAUDE_CONFIG_DIR/skills/` (global) | Installed by `thatch setup` (shared only) to the repo's `.cursor/skills/` (local) or `~/.cursor/skills/` (global) | **Full** — same SKILL.md format; the code-review coordinator is opencode-only (needs sub-agents) |
31
31
  | **Store detection** (repo identity from git remote) | `worktree` parameter from the opencode plugin | `CLAUDE_PROJECT_DIR` env (set by Claude Code for stdio MCP servers) | `CURSOR_PROJECT_DIR` then `CLAUDE_PROJECT_DIR` then cwd | **Full** |
32
32
  | **Setup detection at startup** | n/a — plugin auto-installs at init | `checkSetup` in MCP server: detects missing or broken instructions in CLAUDE.md | Same as Claude Code (checks AGENTS.md) | **Full** — warns the agent to tell the user to run `thatch setup` |
33
33
  | **Hook config format** | n/a — plugin registers hooks in code | Nested: `.claude/settings.json` `{hooks:{Event:{hooks:[{type,command}]}}}` | Flat: `.cursor/hooks.json` `{version:1,hooks:{event:[{command}]}}` | n/a |
@@ -49,11 +49,12 @@ the TUI is not connected (headless mode).
49
49
  | MCP config | `.mcp.json` (`mcpServers.thatch`, stdio, `["mcp"]`) | none — prints `claude mcp add --scope user thatch -- <bin> mcp` |
50
50
  | Instructions | `CLAUDE.md` (idempotent `appendBlock`) | `$CLAUDE_CONFIG_DIR/CLAUDE.md` |
51
51
  | Hooks | `.claude/settings.json` | `$CLAUDE_CONFIG_DIR/settings.json` |
52
- | Skills | `$CLAUDE_CONFIG_DIR/skills/` (always user-scoped) | same |
52
+ | Skills | `.claude/skills/` (in the repo) | `$CLAUDE_CONFIG_DIR/skills/` |
53
53
 
54
54
  `CLAUDE_CONFIG_DIR` overrides the default `~/.claude` for all user-scoped
55
- paths. Project-local keeps `.mcp.json`, `CLAUDE.md`, and `.claude/settings.json`
56
- in the repo; only skills are user-scoped.
55
+ paths. Project-local keeps everything in the repo: `.mcp.json`, `CLAUDE.md`,
56
+ `.claude/settings.json`, and `.claude/skills/` (so skills version with the
57
+ project and every contributor gets them).
57
58
 
58
59
  ### Hook events (nested `settings.json`)
59
60
 
@@ -88,7 +89,7 @@ Cursor uses the same stdio MCP server as Claude Code; only the hooks differ.
88
89
  | MCP config | `.cursor/mcp.json` | `$CURSOR_CONFIG_DIR/mcp.json` (or `~/.cursor/mcp.json`) |
89
90
  | Instructions | `AGENTS.md` (idempotent `appendBlock`, Cursor-marked) | `$CURSOR_CONFIG_DIR/AGENTS.md` |
90
91
  | Hooks | `.cursor/hooks.json` (flat) | `$CURSOR_CONFIG_DIR/hooks.json` |
91
- | Skills | `$CURSOR_CONFIG_DIR/skills/` (shared only) | same |
92
+ | Skills | `.cursor/skills/` (in the repo) | `$CURSOR_CONFIG_DIR/skills/` |
92
93
 
93
94
  Cursor has no documented env override like `CLAUDE_CONFIG_DIR`; `CURSOR_CONFIG_DIR`
94
95
  is honored for symmetry and forward-compatibility. Cursor has no equivalent of
@@ -46,7 +46,7 @@ description: Extract durable project facts ... Use when ...
46
46
  | `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of behavior? |
47
47
  | `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
48
48
  | `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar against generic praise. |
49
- | `thatch-review-mentorship` | Peer-mentorship lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, verified citations, never blocking. |
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, verified citations, never blocking. |
50
50
  | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. Starts the final report with a workflow-change preface, then cross-references findings against prior review comments when a follow-up round register is provided. |
51
51
  | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, linked docs/tickets followed from the change, memory) before fan-out. Prevents false positives about intentionally deferred work, and checks whether main moved past the merge-base touching the same paths (staleness signal for the synthesizer). Also fetches prior review comments on a connected PR/MR for follow-up-round detection and builds a register with preliminary addressed-check status per comment. |
52
52
  | `thatch-code-archaeology` | Investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features, skeletons) before proposing changes. The research skill; pair with `thatch-coding-workflow`. |
@@ -112,9 +112,10 @@ and `--cursor` pass only the shared set.
112
112
 
113
113
  ## Install mechanics
114
114
 
115
- - **Location**: `$XDG_CONFIG_HOME/opencode/skills` (opencode),
116
- `$CLAUDE_CONFIG_DIR/skills` (Claude Code), `~/.cursor/skills` (Cursor).
117
- Always user-scoped — never into the worktree.
115
+ - **Location**: `$XDG_CONFIG_HOME/opencode/skills` (opencode);
116
+ scope-dependent for MCP hosts — the repo's `.claude/skills/` or
117
+ `.cursor/skills/` for project-local `thatch setup`, the host config
118
+ dirs (`$CLAUDE_CONFIG_DIR/skills`, `~/.cursor/skills`) for `--global`.
118
119
  - **Trigger**: opencode installs at plugin init; Claude Code/Cursor install via
119
120
  `thatch setup`.
120
121
  - **Drift detection**: `installSkills` only writes when the on-disk content