@jeffober/thatch 0.1.5 → 0.1.8

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 CHANGED
@@ -2,12 +2,18 @@
2
2
 
3
3
  [![CI](https://github.com/sysread/thatch/actions/workflows/ci.yml/badge.svg)](https://github.com/sysread/thatch/actions/workflows/ci.yml)
4
4
 
5
- Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config.
6
-
7
- Works with **OpenCode** (as a plugin) and **Claude Code** (as an MCP server).
8
- Your AI agent can remember information across sessions. Stores are per-repo
9
- with a shared `global` store. No API keys, no cloud services — everything runs
10
- on your machine.
5
+ Persistent memory and operational methodology for AI coding agents — local
6
+ embeddings, SQLite stores, zero config.
7
+
8
+ Works with **OpenCode** (as a plugin), **Claude Code** (as an MCP server), and
9
+ **Cursor** (as an MCP server).
10
+ Each session inherits the accumulated knowledge of every session before it —
11
+ project architecture, conventions, gotchas, user preferences — so your agent
12
+ starts with context instead of a blank slate. Stores are per-repo with a
13
+ shared `global` store. On top of the memory layer, thatch ships a suite of
14
+ skills that prime, maintain, and reflect on memory, plus a structured
15
+ multi-agent code review framework. No API keys, no cloud services —
16
+ everything runs on your machine.
11
17
 
12
18
  ## Quick start
13
19
 
@@ -51,7 +57,7 @@ thatch setup --claude
51
57
  1. Writes `.mcp.json` — registers the MCP server (Claude Code spawns `thatch mcp` as a stdio process)
52
58
  2. Appends instructions to `CLAUDE.md` — session startup, when to write, what to store
53
59
  3. Installs hooks in `.claude/settings.json` — `SessionStart` runs `thatch reminder` (recall nudge + hygiene), `UserPromptSubmit` reminds you to save new knowledge
54
- 4. Installs skill files to `~/.claude/skills/` — `thatch-fact-extractor`, `thatch-dedup-classifier`, and `thatch-project-primer`
60
+ 4. Installs skill files to `~/.claude/skills/` — memory workflow skills (project primer, fact extractor, dedup classifier, session reflection) and code review skills (5 review specialists + synthesizer). See [Skills](#skills) below.
55
61
 
56
62
  Restart Claude Code and thatch's tools are available as `mcp__thatch__*`.
57
63
 
@@ -88,6 +94,38 @@ the bare `thatch` command so the config survives updates.
88
94
  | `thatch_find_duplicates` | `mcp__thatch__find_duplicates` | Surface pairs of suspiciously similar memories |
89
95
  | `thatch_dedup_mark_checked` | `mcp__thatch__dedup_mark_checked` | Record a reviewed pair so it stops being re-reported |
90
96
 
97
+ ## Skills
98
+
99
+ Thatch ships skills that encode operational methodology — practices your
100
+ agent follows to maintain its own memory and review code systematically.
101
+
102
+ ### Memory workflows
103
+
104
+ | Skill | What it does |
105
+ |-------|-------------|
106
+ | `thatch-project-primer` | Bootstrap a project's memory store by investigating the codebase and writing foundational memories |
107
+ | `thatch-fact-extractor` | Extract durable facts from recent tool interactions and persist them to memory |
108
+ | `thatch-dedup-classifier` | Classify and consolidate similar memories surfaced by `thatch_find_duplicates` |
109
+ | `thatch-session-reflection` | Record what was learned during a session — project facts, user preferences, tool tips |
110
+
111
+ ### Code review
112
+
113
+ | Skill | What it does |
114
+ |-------|-------------|
115
+ | `thatch-review-pedantic` | Mechanical correctness — spelling, naming, doc accuracy, specs, stale artifacts |
116
+ | `thatch-review-acceptance` | Behavioral and product review — UX coherency, behavioral delta, integration effects |
117
+ | `thatch-review-state-flow` | Data flow and contracts — module boundaries, implicit state machines, error propagation |
118
+ | `thatch-review-no-slop` | AI writing anti-pattern detection — change narration, fourth wall breaks, hedging |
119
+ | `thatch-review-breadcrumbs` | Comment narrative — do comments form a coherent outline of the code's behavior? |
120
+ | `thatch-review-synthesizer` | Verify specialist findings against actual code, deduplicate, produce severity-grouped report |
121
+ | `thatch-code-review` | Multi-agent review coordinator — triage, partition, dispatch specialists in parallel (opencode only) |
122
+
123
+ The five review specialists run independently and can be used standalone. The
124
+ synthesizer verifies their findings against the actual code — reading each
125
+ cited location to confirm the evidence matches — before producing a final
126
+ report. The coordinator (opencode only) orchestrates the full pipeline by
127
+ dispatching specialist sub-agents in parallel.
128
+
91
129
  ## CLI
92
130
 
93
131
  The `thatch` CLI ships with the package (installed to your npm global bin by
@@ -141,6 +179,17 @@ Thatch auto-detects your repo identity from `git remote get-url origin`
141
179
  locally) and stored in `~/.config/thatch/thatch.db` (SQLite, WAL mode).
142
180
  Search is brute-force cosine similarity — fast enough for thousands of entries.
143
181
 
182
+ ### Memory capture
183
+
184
+ Thatch doesn't call an LLM to extract facts. Instead, it buffers recent tool
185
+ interactions and nudges the host agent to persist durable knowledge itself,
186
+ using the `thatch-fact-extractor` skill. The agent's own model decides what's
187
+ worth remembering — keeping thatch dependency-free and the agent in control.
188
+ On OpenCode, the plugin hooks `tool.execute.after` and `chat.message` to
189
+ buffer and flush. On Claude Code, `PostToolBatch` and `UserPromptSubmit`
190
+ hooks drive a file-backed queue. Both paths produce the same payload for the
191
+ fact-extractor skill.
192
+
144
193
  ## OpenCode vs Claude Code parity
145
194
 
146
195
  | Feature | OpenCode (plugin) | Claude Code (MCP + hooks) |
@@ -149,7 +198,7 @@ Search is brute-force cosine similarity — fast enough for thousands of entries
149
198
  | System prompt | Injected at runtime with repo name | Static CLAUDE.md (repo auto-detected by MCP server) |
150
199
  | Session-start reminder | `session.created` event | `SessionStart` hook → `thatch reminder` |
151
200
  | Compaction context | `experimental.session.compacting` hook | **Gap**: no equivalent (CLAUDE.md persists through compaction) |
152
- | Extraction nudge | `tool.execute.after` + `chat.message` | **Gap**: no cross-call buffering (hooks fire per-event) |
201
+ | Extraction nudge | `tool.execute.after` + `chat.message` | `PostToolBatch` + `UserPromptSubmit` (file-backed queue) |
153
202
  | Skills | `$XDG_CONFIG_HOME/opencode/skills` | `~/.claude/skills/` |
154
203
  | Store detection | `worktree` param from plugin | `CLAUDE_PROJECT_DIR` env var |
155
204
 
@@ -174,10 +223,12 @@ and is cached. No data leaves your machine.
174
223
  bun install # deps
175
224
  bun test # full suite: zero network, zero external deps
176
225
  bun test --watch # watch mode
226
+ mise run check # typecheck + bun test + markdownlint (the CI gate)
177
227
  ```
178
228
 
179
229
  Tests never reach outside the sandbox: temp-directory SQLite files, mock
180
- embeddings, no network.
230
+ embeddings, no network. `mise run check` is the full quality gate; `mise run
231
+ typecheck` or `mise run lint-md` run those alone.
181
232
 
182
233
  ## License
183
234
 
package/bin/release CHANGED
@@ -22,9 +22,9 @@ echo " bump: $v"
22
22
  echo " next: $next"
23
23
  echo
24
24
 
25
- echo "▸ running tests..."
26
- bun test
27
- echo " ✓ tests passed"
25
+ echo "▸ running checks (typecheck + tests + markdown lint)..."
26
+ mise run check
27
+ echo " ✓ checks passed"
28
28
  echo
29
29
 
30
30
  echo "▸ bumping version..."
package/bin/thatch CHANGED
@@ -7,7 +7,7 @@ import { detectRepo } from "../src/git";
7
7
  import { BgeEmbeddingModel } from "../src/embeddings";
8
8
  import { hygieneReport } from "../src/hygiene";
9
9
  import { claudeSessionStartReminder } from "../src/prompts";
10
- import { setupClaudeCode } from "../src/setup";
10
+ import { setupClaudeCode, setupCursor } from "../src/setup";
11
11
 
12
12
  const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
13
13
  const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
@@ -22,13 +22,14 @@ function usage(): never {
22
22
  thatch show <label> [store] Display a memory by label
23
23
  thatch forget <label> [store] Remove a memory by label
24
24
  thatch search <query> [store] Semantic search (cosine similarity)
25
- thatch mcp Start the stdio MCP server (for Claude Code)
26
- thatch reminder Print session-start reminder (for SessionStart hook)
25
+ thatch mcp Start the stdio MCP server (for Claude Code, Cursor, etc.)
26
+ thatch reminder [--json] Print session-start reminder (for SessionStart hook)
27
27
  thatch hygiene Print the hygiene report (standalone)
28
- thatch prime Prime project memory (runs the primer skill via opencode or claude)
29
- thatch buffer-batch Append a PostToolBatch payload to the queue (hook only)
30
- thatch flush-tools Drain the queue + print an extraction nudge (hook only)
31
- thatch setup [--claude] [--global] [--force]
28
+ thatch prime Prime project memory (runs the primer skill via opencode, cursor, or claude)
29
+ thatch buffer-batch Append a PostToolBatch payload to the queue (Claude Code hook only)
30
+ thatch buffer-tool Append a single postToolUse interaction to the queue (Cursor hook only)
31
+ thatch flush-tools [--json] Drain the queue + print an extraction nudge (hook only)
32
+ thatch setup --claude [--cursor] [--global]
32
33
  Install config + instructions + hooks + skills
33
34
 
34
35
  Stores default to the repo detected from git remote. Use "global" for the
@@ -44,6 +45,11 @@ async function defaultStore(): Promise<string> {
44
45
  }
45
46
  }
46
47
 
48
+ /** Resolve the project directory from host-specific env vars or cwd. */
49
+ function projectDir(): string {
50
+ return process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
51
+ }
52
+
47
53
  switch (command) {
48
54
  case "stores":
49
55
  console.log(db.listStores().join("\n"));
@@ -138,21 +144,27 @@ switch (command) {
138
144
  }
139
145
 
140
146
  case "reminder": {
147
+ const isJson = args.includes("--json");
141
148
  const repo = await defaultStore();
142
- const worktree = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
149
+ const worktree = projectDir();
143
150
  let hygiene: string | null = null;
144
151
  try {
145
152
  hygiene = await hygieneReport(db, repo, worktree);
146
153
  } catch (err) {
147
154
  console.error(`[thatch] hygiene report failed: ${err}`);
148
155
  }
149
- console.log(claudeSessionStartReminder(repo, hygiene));
156
+ const reminder = claudeSessionStartReminder(repo, hygiene);
157
+ if (isJson) {
158
+ console.log(JSON.stringify({ additional_context: reminder }));
159
+ } else {
160
+ console.log(reminder);
161
+ }
150
162
  break;
151
163
  }
152
164
 
153
165
  case "hygiene": {
154
166
  const repo = await defaultStore();
155
- const worktree = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
167
+ const worktree = projectDir();
156
168
  const hygiene = await hygieneReport(db, repo, worktree);
157
169
  console.log(hygiene ?? "Store is healthy.");
158
170
  break;
@@ -161,6 +173,7 @@ switch (command) {
161
173
  case "prime": {
162
174
  const primerPrompt = "Load the thatch-project-primer skill and follow its instructions to prime this project's memory store.";
163
175
  const opencodeBin = Bun.which("opencode");
176
+ const agentBin = Bun.which("agent");
164
177
  const claudeBin = Bun.which("claude");
165
178
 
166
179
  if (opencodeBin) {
@@ -170,6 +183,15 @@ switch (command) {
170
183
  stderr: "inherit",
171
184
  });
172
185
  process.exit(proc.exitCode ?? 1);
186
+ } else if (agentBin) {
187
+ // Cursor CLI: -p for non-interactive print mode, --approve-mcps to
188
+ // auto-approve the thatch MCP server (no interactive UI in -p mode).
189
+ console.error("Priming project memory via cursor agent...");
190
+ const proc = Bun.spawnSync([agentBin, "-p", primerPrompt, "--approve-mcps"], {
191
+ stdout: "inherit",
192
+ stderr: "inherit",
193
+ });
194
+ process.exit(proc.exitCode ?? 1);
173
195
  } else if (claudeBin) {
174
196
  console.error("Priming project memory via claude...");
175
197
  const proc = Bun.spawnSync([claudeBin, primerPrompt], {
@@ -178,7 +200,7 @@ switch (command) {
178
200
  });
179
201
  process.exit(proc.exitCode ?? 1);
180
202
  } else {
181
- console.error("Error: neither 'opencode' nor 'claude' found on PATH.");
203
+ console.error("Error: none of 'opencode', 'agent', or 'claude' found on PATH.");
182
204
  console.error("Install one of them to use thatch prime.");
183
205
  process.exit(1);
184
206
  }
@@ -188,10 +210,11 @@ switch (command) {
188
210
  case "setup": {
189
211
  const isGlobal = args.includes("--global");
190
212
  const isClaude = args.includes("--claude");
213
+ const isCursor = args.includes("--cursor");
191
214
 
192
- if (!isClaude) {
193
- console.error("Currently only --claude is supported.");
194
- console.error("Usage: thatch setup --claude [--global]");
215
+ if (!isClaude && !isCursor) {
216
+ console.error("Specify at least one host: --claude and/or --cursor.");
217
+ console.error("Usage: thatch setup --claude [--cursor] [--global]");
195
218
  process.exit(1);
196
219
  }
197
220
 
@@ -201,30 +224,47 @@ switch (command) {
201
224
  // If not on PATH (local dev checkout), use the absolute path to this script.
202
225
  const thatchOnPath = Bun.which("thatch");
203
226
  const thatchBin = thatchOnPath ?? resolve(process.argv[1] ?? process.execPath);
204
- const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
227
+ const dir = projectDir();
205
228
 
206
- const result = setupClaudeCode(thatchBin, isGlobal, projectDir);
229
+ if (isClaude) {
230
+ const result = setupClaudeCode(thatchBin, isGlobal, dir);
207
231
 
208
- console.log(`Thatch setup complete (${isGlobal ? "global" : "project-local"}).`);
209
- console.log();
210
- if (result.mcpConfig) {
211
- console.log(` MCP config: ${result.mcpConfig}`);
232
+ console.log(`Thatch setup complete for Claude Code (${isGlobal ? "global" : "project-local"}).`);
233
+ console.log();
234
+ if (result.mcpConfig) {
235
+ console.log(` MCP config: ${result.mcpConfig}`);
236
+ }
237
+ console.log(` Instructions: ${result.claudeMd}`);
238
+ console.log(` Hooks: ${result.settings}`);
239
+ const skillsDir = result.skills[0]?.path
240
+ ? result.skills[0].path.split("/").slice(0, -1).join("/")
241
+ : "(no skills installed)";
242
+ console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
243
+ console.log();
244
+ if (result.mcpAddCommand) {
245
+ console.log("To register the MCP server globally, run:");
246
+ console.log(` ${result.mcpAddCommand}`);
247
+ console.log();
248
+ }
249
+ console.log("Restart Claude Code for the changes to take effect.");
212
250
  }
213
- console.log(` Instructions: ${result.claudeMd}`);
214
- console.log(` Hooks: ${result.settings}`);
215
- // Skills dir is not currently surfaced on SetupResult — derive it from
216
- // the skills file paths (they all share the same parent directory).
217
- const skillsDir = result.skills[0]?.path
218
- ? result.skills[0].path.split("/").slice(0, -1).join("/")
219
- : "(no skills installed)";
220
- console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
221
- console.log();
222
- if (result.mcpAddCommand) {
223
- console.log("To register the MCP server globally, run:");
224
- console.log(` ${result.mcpAddCommand}`);
251
+
252
+ if (isCursor) {
253
+ const result = setupCursor(thatchBin, isGlobal, dir);
254
+
225
255
  console.log();
256
+ console.log(`Thatch setup complete for Cursor (${isGlobal ? "global" : "project-local"}).`);
257
+ console.log();
258
+ console.log(` MCP config: ${result.mcpConfig}`);
259
+ console.log(` Instructions: ${result.agentsMd}`);
260
+ console.log(` Hooks: ${result.hooks}`);
261
+ const skillsDir = result.skills[0]?.path
262
+ ? result.skills[0].path.split("/").slice(0, -1).join("/")
263
+ : "(no skills installed)";
264
+ console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
265
+ console.log();
266
+ console.log("Restart Cursor for the changes to take effect.");
226
267
  }
227
- console.log("Restart Claude Code for the changes to take effect.");
228
268
  break;
229
269
  }
230
270
 
@@ -245,35 +285,116 @@ switch (command) {
245
285
  break;
246
286
  }
247
287
 
288
+ case "buffer-tool": {
289
+ // Cursor postToolUse hook: append a single tool interaction to the
290
+ // session's JSONL queue. Cursor fires postToolUse per-tool (no batch
291
+ // equivalent), so this receives one tool at a time. Silent on success.
292
+ // Session ID comes as `conversation_id` in Cursor (vs `session_id` in
293
+ // Claude Code). The tool result field name is not explicitly documented,
294
+ // so we try the most likely candidates.
295
+ try {
296
+ const raw = await Bun.stdin.text();
297
+ const data = JSON.parse(raw) as {
298
+ conversation_id?: string;
299
+ session_id?: string;
300
+ tool_name?: string;
301
+ tool_input?: Record<string, unknown>;
302
+ tool_use_id?: string;
303
+ tool_result?: string | unknown[];
304
+ tool_response?: string | unknown[];
305
+ result_json?: string;
306
+ result?: string | unknown[];
307
+ output?: string;
308
+ };
309
+ const sessionID = data.conversation_id ?? data.session_id;
310
+ if (sessionID && data.tool_name) {
311
+ const { appendBatch } = await import("../src/extract-queue");
312
+ const response = data.tool_result ?? data.tool_response ?? data.result_json ?? data.result ?? data.output ?? "";
313
+ appendBatch(sessionID, [{
314
+ tool_name: data.tool_name,
315
+ tool_input: data.tool_input ?? {},
316
+ tool_use_id: data.tool_use_id,
317
+ tool_response: response,
318
+ }]);
319
+ }
320
+ } catch (err) {
321
+ console.error(`[thatch buffer-tool] failed: ${err}`);
322
+ }
323
+ break;
324
+ }
325
+
248
326
  case "flush-tools": {
249
- // UserPromptSubmit hook: drain the queue. With buffered interactions,
250
- // print the extraction nudge carrying the payload so the agent can load
251
- // the thatch-fact-extractor skill and persist durable facts. With an
252
- // empty queue, fall back to the static write-nudge so the original
253
- // workflow contract (a per-prompt "did you learn anything?" reminder)
254
- // is preserved on conversational turns that did no tool use.
327
+ // UserPromptSubmit (Claude Code) / beforeSubmitPrompt (Cursor) hook.
328
+ // Three priority tiers:
329
+ // 1. Extraction nudge — buffered tool interactions are queued for fact
330
+ // extraction (carries the JSON payload for thatch-fact-extractor).
331
+ // 2. Recall nudge — the user's prompt semantically matches existing
332
+ // memories. Uses the sideband socket to ask the warm MCP server to
333
+ // embed the prompt and search, avoiding a cold model load here.
334
+ // 3. Write nudge — static "did you learn anything?" fallback for
335
+ // conversational turns with no tool use and no memory matches.
336
+ // --json wraps output as { additional_context: "..." } for Cursor hooks.
337
+ // Without --json, stdout is plain text (Claude Code style).
338
+ const isJson = args.includes("--json");
255
339
  try {
256
340
  const raw = await Bun.stdin.text();
257
- const data = JSON.parse(raw) as { session_id?: string };
341
+ const data = JSON.parse(raw) as {
342
+ session_id?: string;
343
+ conversation_id?: string;
344
+ prompt?: string;
345
+ message?: string;
346
+ text?: string;
347
+ user_input?: string;
348
+ input?: string;
349
+ };
350
+ const sessionID = data.session_id ?? data.conversation_id;
258
351
 
259
352
  const { flushQueue } = await import("../src/extract-queue");
260
353
  const { buildExtractionPayload } = await import("../src/extraction");
261
- const { claudeExtractionNudge, claudeWriteNudge } = await import("../src/prompts");
354
+ const { claudeExtractionNudge, claudeWriteNudge, claudeRecallNudge } = await import("../src/prompts");
262
355
 
263
356
  let repo = "unknown";
264
357
  try { repo = await detectRepo(); } catch {}
265
358
 
266
- const interactions = data.session_id ? flushQueue(data.session_id) : [];
359
+ // Tier 1: extraction nudge.
360
+ const interactions = sessionID ? flushQueue(sessionID) : [];
267
361
  if (interactions.length > 0) {
268
- console.log(claudeExtractionNudge(interactions.length, buildExtractionPayload(interactions, repo)));
269
- } else {
270
- console.log(claudeWriteNudge());
362
+ const output = claudeExtractionNudge(interactions.length, buildExtractionPayload(interactions, repo));
363
+ if (isJson) console.log(JSON.stringify({ additional_context: output }));
364
+ else console.log(output);
365
+ break;
271
366
  }
367
+
368
+ // Tier 2: recall nudge via sideband. The prompt text field name varies
369
+ // by host (Claude Code: "prompt", Cursor: undocumented). Try candidates.
370
+ const promptText = data.prompt ?? data.message ?? data.text ?? data.user_input ?? data.input;
371
+ if (promptText && promptText.length >= 10) {
372
+ const { sidebandMatch, sidebandSocketPath } = await import("../src/sideband");
373
+ const sockPath = sidebandSocketPath(dbPath);
374
+ const threshold = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55");
375
+ const matches = await sidebandMatch(sockPath, promptText, [repo, "global"], threshold, 5);
376
+ if (matches && matches.length > 0) {
377
+ const output = claudeRecallNudge(matches);
378
+ if (isJson) console.log(JSON.stringify({ additional_context: output }));
379
+ else console.log(output);
380
+ break;
381
+ }
382
+ }
383
+
384
+ // Tier 3: static write nudge.
385
+ const output = claudeWriteNudge();
386
+ if (isJson) console.log(JSON.stringify({ additional_context: output }));
387
+ else console.log(output);
272
388
  } catch (err) {
273
389
  console.error(`[thatch flush-tools] failed: ${err}`);
274
390
  try {
275
391
  const { claudeWriteNudge } = await import("../src/prompts");
276
- console.log(claudeWriteNudge());
392
+ const fallback = claudeWriteNudge();
393
+ if (isJson) {
394
+ console.log(JSON.stringify({ additional_context: fallback }));
395
+ } else {
396
+ console.log(fallback);
397
+ }
277
398
  } catch {
278
399
  // If even the fallback import fails, do nothing — silent failure is
279
400
  // preferable to blocking the user's prompt over an unrelated hook.
@@ -2,22 +2,30 @@
2
2
 
3
3
  ## Architecture
4
4
 
5
- Thatch has two integration paths sharing a common core:
5
+ Thatch has three integration paths sharing a common core:
6
6
 
7
7
  1. **OpenCode plugin** — runs inside opencode's Bun runtime. Full access to
8
8
  plugin hooks: system prompt injection, session events, tool buffering,
9
9
  compaction context.
10
10
  2. **Claude Code MCP server** — runs as a stdio JSON-RPC process. Tools
11
11
  exposed via MCP; session behavior driven by Claude Code hooks.
12
+ 3. **Cursor MCP server** — same stdio MCP server as Claude Code; session
13
+ behavior driven by Cursor hooks in a flat `hooks.json` format.
12
14
 
13
- ```
15
+ For feature parity and gaps across the three, see [mcp-parity.md](mcp-parity.md).
16
+ For the concrete files and hook events each host writes, see
17
+ [setup-and-hooks.md](setup-and-hooks.md). For non-obvious invariants, see
18
+ [gotchas.md](gotchas.md). For the skill system, see [skills.md](skills.md).
19
+
20
+ ```text
14
21
  Shared core
15
22
  ├── tool-defs.ts → single source of truth: zod schemas + execute logic
16
- ├── db.ts → SQLite CRUD, cosine search, dedup verdicts
23
+ ├── db.ts → SQLite CRUD, cosine search (recall + search), dedup verdicts
17
24
  ├── embeddings.ts → embedding model via transformers.js
18
25
  ├── git.ts → detect repo identity (store name)
19
26
  ├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
20
- ├── prompts.ts → system prompt, compaction, reminders, CLAUDE.md instructions
27
+ ├── prompts.ts → system prompt, compaction, reminders, recall nudge, CLAUDE.md instructions
28
+ ├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
21
29
  └── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
22
30
 
23
31
  OpenCode plugin path
@@ -26,11 +34,15 @@ OpenCode plugin path
26
34
 
27
35
  MCP server path
28
36
  ├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
29
- │ z.object().parse() for validation, dispatches to tool-defs
30
- └── setup.ts → `thatch setup --claude`: writes .mcp.json, CLAUDE.md,
31
- settings.json hooks, installs skills
32
-
33
- bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|setup
37
+ │ z.object().parse() for validation, dispatches to tool-defs;
38
+ │ opens sideband socket for warm-model match queries
39
+ ├── sideband.ts → Unix socket: SidebandServer (embed + search via warm model)
40
+ │ and sidebandMatch (thin client for hook processes)
41
+ └── setup.ts → `thatch setup --claude` / `--cursor`: writes .mcp.json,
42
+ CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
43
+
44
+ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
45
+ prime|buffer-batch|buffer-tool|flush-tools|setup
34
46
  ```
35
47
 
36
48
  ## Module responsibilities
@@ -44,10 +56,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
44
56
  | `setup.ts` | `thatch setup --claude` installer. Writes .mcp.json, appends to CLAUDE.md (idempotent), installs hooks in settings.json, installs skills. |
45
57
  | `hygiene.ts` | Hygiene report: pending dedup pairs, stale count, orphaned branch memories. Shared by the plugin's session-start hook and the CLI's `thatch reminder` command. |
46
58
  | `git.ts` | Parse `owner/repo` from git remote. Worktree-safe fallback chain. |
47
- | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search, dedup-pair verdict tracking. |
59
+ | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. |
48
60
  | `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
49
61
  | `extraction.ts` | Buffers non-thatch tool interactions per session and serializes them into the JSON payload the extraction nudge carries. (opencode-only — no MCP equivalent.) |
50
- | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, Claude Code CLAUDE.md instructions, Claude Code hook text. |
62
+ | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Socket path is a hash of the DB path — both processes compute it independently. |
63
+ | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
51
64
  | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (10 skills: fact-extractor, dedup-classifier, project-primer, 5 review specialists, review synthesizer, session reflection — work on both opencode and Claude Code) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
52
65
 
53
66
  ## Plugin hooks
@@ -57,9 +70,10 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
57
70
  | Hook | What it does |
58
71
  |------|-------------|
59
72
  | `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
60
- | `experimental.session.compacting` | Appends re-familiarization context so a compacted session still knows thatch exists. |
73
+ | `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
74
+ | `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. |
61
75
  | `tool.execute.after` | Buffers every non-`thatch_*` tool call into the session's extraction buffer. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
62
- | `chat.message` | If the session has buffered interactions, flushes them and injects a synthetic text part carrying the extraction nudge + JSON payload. |
76
+ | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, flushes them as a synthetic text part carrying the extraction nudge + JSON payload; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
63
77
  | `event` (`session.created`) | Sends the session-start reminder via `client.session.prompt`, carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. The session id is at `event.properties.info.id`. |
64
78
  | `dispose` | Closes the DB. |
65
79
 
@@ -97,7 +111,7 @@ Two of these hooks were dead for weeks because failures were invisible.
97
111
 
98
112
  ## Data flow
99
113
 
100
- ```
114
+ ```text
101
115
  thatch_memory_remember(label, content)
102
116
  → model.passageEmbed("# label\n\ncontent") → Float32Array
103
117
  → db.findSimilar(store, embedding) — write-time collision check (no telemetry)
@@ -129,6 +143,16 @@ extraction cycle (agent-driven)
129
143
  → next chat.message flushes the buffer into a nudge part with JSON payload
130
144
  → agent loads thatch-fact-extractor skill, saves facts via thatch_memory_remember
131
145
 
146
+ prompt-aware recall nudge (both paths)
147
+ → opencode: chat.message hook embeds prompt text with warm in-process model,
148
+ searches db.search([repo, global]), pushes nudge part if matches ≥ threshold
149
+ → Claude Code/Cursor: flush-tools connects to MCP server's sideband socket,
150
+ warm server embeds + searches, returns labels; hook prints nudge or falls
151
+ back to write nudge if socket unavailable or no matches
152
+ → threshold: THATCH_RECALL_THRESHOLD env (default 0.55) — lower than
153
+ findDuplicates' 0.85 because "relates to" is a weaker signal than "duplicate"
154
+ → no telemetry stamped: uses db.search(), not db.recall()
155
+
132
156
  hygiene heartbeat (session start)
133
157
  → hygieneReport(db, repo, worktree): pending dedup pairs; entries neither
134
158
  updated nor recalled in 90+ days; memories scoped to branches that no
@@ -169,14 +193,43 @@ hygiene heartbeat (session start)
169
193
  ```bash
170
194
  bun install # deps
171
195
  bun test # full suite; no network, no real config dirs
196
+ mise run check # typecheck + bun test + markdownlint (the CI gate)
172
197
  opencode # self-hosts via .opencode/plugins/thatch.ts
173
198
  ```
174
199
 
200
+ The markdownlint gate lints `README.md` and `docs/` (excluding the historical
201
+ `docs/plans/`) via `.markdownlint-cli2.jsonc`. It disables three rules that
202
+ conflict with intentional house style (line length, table alignment, and the
203
+ use-case template's bold-label + tight-list format). Run `mise run lint-md`
204
+ alone to check docs without the test suite. Run `mise run typecheck` for
205
+ `tsc` alone (uses `tsconfig.check.json`, which includes the test files that
206
+ the build's `tsconfig.json` excludes).
207
+
175
208
  ## Release
176
209
 
177
210
  ```bash
178
211
  mise run release patch|minor|major
179
212
  ```
180
213
 
181
- Bumps version, tags, pushes; GitHub Actions publishes to npm via OIDC trusted
182
- publishing (no tokens — see `.github/workflows/publish.yml`).
214
+ `bin/release` runs the full flow:
215
+
216
+ 1. Runs `bun test` (aborts on failure — no broken releases).
217
+ 2. `npm version <bump> --no-git-tag-version` bumps `package.json`.
218
+ 3. Prompts to commit, tag (`v<next>`), and push; declining reverts
219
+ `package.json` and exits cleanly.
220
+ 4. On confirm: commits, tags `v<next>`, pushes commits and the tag.
221
+
222
+ Publishing is tag-driven and **passwordless**. Pushing a `v*` tag triggers
223
+ `.github/workflows/publish.yml`, which publishes to npm via **OIDC trusted
224
+ publishing** — no stored npm token. The `id-token: write` permission lets npm
225
+ authenticate through GitHub's OIDC exchange instead. Prerequisites:
226
+
227
+ - A Trusted Publisher configured on npmjs.com for `@jeffober/thatch` pointing at
228
+ the `sysread/thatch` repo and this workflow file.
229
+ - npm >= 11.5.1 (the OIDC exchange needs it). `publish.yml` installs
230
+ `setup-node@v4` (node 24) and `npm install -g npm@latest`; it deliberately
231
+ sets **no `registry-url`**, which would write a token-expecting `.npmrc` that
232
+ preempts the OIDC exchange.
233
+
234
+ CI (`.github/workflows/ci.yml`) runs `bun test` on every push/PR to `main` —
235
+ the never-merge-broken guard before a release.