@jeffober/thatch 0.1.4 → 0.1.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.
- package/README.md +60 -9
- package/bin/thatch +167 -46
- package/docs/dev/README.md +73 -18
- package/docs/dev/gotchas.md +99 -0
- package/docs/dev/mcp-parity.md +113 -57
- package/docs/dev/setup-and-hooks.md +133 -0
- package/docs/dev/skills.md +109 -0
- package/docs/qa/README.md +9 -0
- package/docs/qa/use-cases/README.md +16 -0
- package/docs/qa/use-cases/UC-005-setup-install.md +34 -0
- package/docs/qa/use-cases/UC-006-cursor-hook-lifecycle.md +31 -0
- package/docs/qa/use-cases/UC-007-recall-nudge.md +31 -0
- package/docs/qa/use-cases/UC-008-hygiene-heartbeat.md +26 -0
- package/docs/qa/use-cases/UC-009-flush-tools-tiers.md +31 -0
- package/docs/qa/use-cases/UC-010-prime.md +25 -0
- package/docs/qa/use-cases/UC-011-write-time-similarity-warning.md +24 -0
- package/docs/qa/use-cases/UC-012-model-migration.md +28 -0
- package/docs/qa/use-cases/UC-013-compaction-context.md +25 -0
- package/docs/qa/use-cases/UC-014-skill-install-drift.md +28 -0
- package/docs/qa/use-cases/UC-015-env-override-matrix.md +28 -0
- package/docs/qa/use-cases/UC-016-concurrent-session-isolation.md +36 -0
- package/docs/qa/use-cases/UC-017-buffer-tool-vs-buffer-batch.md +27 -0
- package/docs/user/README.md +101 -4
- package/package.json +3 -2
- package/src/db.ts +19 -5
- package/src/git.ts +1 -1
- package/src/index.ts +70 -21
- package/src/mcp.ts +19 -0
- package/src/prompts.ts +111 -0
- package/src/setup.ts +171 -8
- package/src/sideband.ts +207 -0
- package/src/skills.ts +732 -9
package/README.md
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sysread/thatch/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Persistent memory for AI coding agents — local
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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/` —
|
|
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` |
|
|
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/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
|
|
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
|
|
31
|
-
thatch
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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("
|
|
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
|
|
227
|
+
const dir = projectDir();
|
|
205
228
|
|
|
206
|
-
|
|
229
|
+
if (isClaude) {
|
|
230
|
+
const result = setupClaudeCode(thatchBin, isGlobal, dir);
|
|
207
231
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
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 {
|
|
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
|
-
|
|
359
|
+
// Tier 1: extraction nudge.
|
|
360
|
+
const interactions = sessionID ? flushQueue(sessionID) : [];
|
|
267
361
|
if (interactions.length > 0) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.log(
|
|
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
|
-
|
|
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.
|
package/docs/dev/README.md
CHANGED
|
@@ -2,23 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Architecture
|
|
4
4
|
|
|
5
|
-
Thatch has
|
|
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
|
|
21
|
-
|
|
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
|
|
29
|
+
└── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
|
|
22
30
|
|
|
23
31
|
OpenCode plugin path
|
|
24
32
|
├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
|
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,11 +56,12 @@ 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
|
-
| `
|
|
51
|
-
| `
|
|
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. |
|
|
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
|
|
54
67
|
|
|
@@ -59,7 +72,7 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
59
72
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
60
73
|
| `experimental.session.compacting` | Appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
61
74
|
| `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` |
|
|
75
|
+
| `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). |
|
|
63
76
|
| `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
77
|
| `dispose` | Closes the DB. |
|
|
65
78
|
|
|
@@ -85,7 +98,10 @@ Two of these hooks were dead for weeks because failures were invisible.
|
|
|
85
98
|
7. **Skills are plugin-owned files.** Installed to
|
|
86
99
|
`$XDG_CONFIG_HOME/opencode/skills` (opencode) or `~/.claude/skills/`
|
|
87
100
|
(Claude Code) — never into the worktree; drifted content is overwritten
|
|
88
|
-
on plugin init or re-running `thatch setup`.
|
|
101
|
+
on plugin init or re-running `thatch setup`. Skills are split into
|
|
102
|
+
`SHARED_SKILLS` (work on both hosts) and `OPENCODE_ONLY_SKILLS` (require
|
|
103
|
+
sub-agent support, not installed for Claude Code). The opencode plugin
|
|
104
|
+
installs both arrays; `thatch setup --claude` installs only shared.
|
|
89
105
|
8. **Tool definitions are the single source of truth.** `tool-defs.ts` defines
|
|
90
106
|
each tool once (name, zod schema, execute function). The opencode plugin
|
|
91
107
|
wraps them in `tool()` with a `thatch_` prefix; the MCP server wraps them
|
|
@@ -94,7 +110,7 @@ Two of these hooks were dead for weeks because failures were invisible.
|
|
|
94
110
|
|
|
95
111
|
## Data flow
|
|
96
112
|
|
|
97
|
-
```
|
|
113
|
+
```text
|
|
98
114
|
thatch_memory_remember(label, content)
|
|
99
115
|
→ model.passageEmbed("# label\n\ncontent") → Float32Array
|
|
100
116
|
→ db.findSimilar(store, embedding) — write-time collision check (no telemetry)
|
|
@@ -126,6 +142,16 @@ extraction cycle (agent-driven)
|
|
|
126
142
|
→ next chat.message flushes the buffer into a nudge part with JSON payload
|
|
127
143
|
→ agent loads thatch-fact-extractor skill, saves facts via thatch_memory_remember
|
|
128
144
|
|
|
145
|
+
prompt-aware recall nudge (both paths)
|
|
146
|
+
→ opencode: chat.message hook embeds prompt text with warm in-process model,
|
|
147
|
+
searches db.search([repo, global]), pushes nudge part if matches ≥ threshold
|
|
148
|
+
→ Claude Code/Cursor: flush-tools connects to MCP server's sideband socket,
|
|
149
|
+
warm server embeds + searches, returns labels; hook prints nudge or falls
|
|
150
|
+
back to write nudge if socket unavailable or no matches
|
|
151
|
+
→ threshold: THATCH_RECALL_THRESHOLD env (default 0.55) — lower than
|
|
152
|
+
findDuplicates' 0.85 because "relates to" is a weaker signal than "duplicate"
|
|
153
|
+
→ no telemetry stamped: uses db.search(), not db.recall()
|
|
154
|
+
|
|
129
155
|
hygiene heartbeat (session start)
|
|
130
156
|
→ hygieneReport(db, repo, worktree): pending dedup pairs; entries neither
|
|
131
157
|
updated nor recalled in 90+ days; memories scoped to branches that no
|
|
@@ -166,14 +192,43 @@ hygiene heartbeat (session start)
|
|
|
166
192
|
```bash
|
|
167
193
|
bun install # deps
|
|
168
194
|
bun test # full suite; no network, no real config dirs
|
|
195
|
+
mise run check # typecheck + bun test + markdownlint (the CI gate)
|
|
169
196
|
opencode # self-hosts via .opencode/plugins/thatch.ts
|
|
170
197
|
```
|
|
171
198
|
|
|
199
|
+
The markdownlint gate lints `README.md` and `docs/` (excluding the historical
|
|
200
|
+
`docs/plans/`) via `.markdownlint-cli2.jsonc`. It disables three rules that
|
|
201
|
+
conflict with intentional house style (line length, table alignment, and the
|
|
202
|
+
use-case template's bold-label + tight-list format). Run `mise run lint-md`
|
|
203
|
+
alone to check docs without the test suite. Run `mise run typecheck` for
|
|
204
|
+
`tsc` alone (uses `tsconfig.check.json`, which includes the test files that
|
|
205
|
+
the build's `tsconfig.json` excludes).
|
|
206
|
+
|
|
172
207
|
## Release
|
|
173
208
|
|
|
174
209
|
```bash
|
|
175
210
|
mise run release patch|minor|major
|
|
176
211
|
```
|
|
177
212
|
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
`bin/release` runs the full flow:
|
|
214
|
+
|
|
215
|
+
1. Runs `bun test` (aborts on failure — no broken releases).
|
|
216
|
+
2. `npm version <bump> --no-git-tag-version` bumps `package.json`.
|
|
217
|
+
3. Prompts to commit, tag (`v<next>`), and push; declining reverts
|
|
218
|
+
`package.json` and exits cleanly.
|
|
219
|
+
4. On confirm: commits, tags `v<next>`, pushes commits and the tag.
|
|
220
|
+
|
|
221
|
+
Publishing is tag-driven and **passwordless**. Pushing a `v*` tag triggers
|
|
222
|
+
`.github/workflows/publish.yml`, which publishes to npm via **OIDC trusted
|
|
223
|
+
publishing** — no stored npm token. The `id-token: write` permission lets npm
|
|
224
|
+
authenticate through GitHub's OIDC exchange instead. Prerequisites:
|
|
225
|
+
|
|
226
|
+
- A Trusted Publisher configured on npmjs.com for `@jeffober/thatch` pointing at
|
|
227
|
+
the `sysread/thatch` repo and this workflow file.
|
|
228
|
+
- npm >= 11.5.1 (the OIDC exchange needs it). `publish.yml` installs
|
|
229
|
+
`setup-node@v4` (node 24) and `npm install -g npm@latest`; it deliberately
|
|
230
|
+
sets **no `registry-url`**, which would write a token-expecting `.npmrc` that
|
|
231
|
+
preempts the OIDC exchange.
|
|
232
|
+
|
|
233
|
+
CI (`.github/workflows/ci.yml`) runs `bun test` on every push/PR to `main` —
|
|
234
|
+
the never-merge-broken guard before a release.
|