@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.
@@ -0,0 +1,99 @@
1
+ # Gotchas
2
+
3
+ Non-obvious invariants and footguns. If something behaves strangely, check
4
+ here first. These are the things that have already cost time.
5
+
6
+ ## Embeddings
7
+
8
+ - **Embedding spaces are discriminated by vector _dimension_, not model tag.**
9
+ `recall` / `findDuplicates` skip entries whose vector length differs from the
10
+ query. The `model` column is informational only. Switching `THATCH_MODEL`
11
+ makes old memories _invisible_ to search (not corrupted, not deleted — just
12
+ skipped) until re-saved. There is no automatic re-embedding.
13
+ - **Embedding serialization honors `byteOffset`/`byteLength`.** transformers.js
14
+ can return a `Float32Array` that is a _view_ into a larger tensor buffer.
15
+ Serializing the whole backing buffer corrupts vectors. Always serialize the
16
+ view's own bytes, not the underlying buffer.
17
+ - **BGE asymmetric search**: queries get the prefix
18
+ `"Represent this sentence for searching relevant passages: "`; passages get
19
+ no prefix. `queryEmbed` vs `passageEmbed` — don't swap them.
20
+
21
+ ## Search vs recall
22
+
23
+ - **`search` scores; `recall` scores _and_ stamps telemetry.** `search` records
24
+ no usage. The prompt-aware recall nudge deliberately uses `db.search()` (not
25
+ `recall`) so nudges don't inflate the "used recently" signal. Only explicit
26
+ `thatch_memory_recall` / CLI `search` stamp `recall_count`/`last_recalled_at`.
27
+ - **`findSimilar` excludes the slug being written** (self-exclusion on
28
+ overwrite), so overwriting a memory doesn't warn against itself.
29
+
30
+ ## Writes
31
+
32
+ - **The write-time similarity warning never blocks the save.** `remember` always
33
+ proceeds; the warning lists >= 0.85-similar entries and asks the agent to
34
+ reconcile (merge or `dedup_mark_checked`). Don't add blocking logic here.
35
+ - **`overwrite: true` clears stale dedup verdicts** for that slug. Forgetting an
36
+ entry clears all verdicts involving it. Both make a pair eligible for
37
+ re-reporting by `find_duplicates`. This is intended.
38
+
39
+ ## Hooks (opencode)
40
+
41
+ - **`tool.execute.after` is a plugin hook, NOT a bus event.** It must stay on
42
+ the hook object returned by `server`. Moving it into the `event` handler
43
+ (where `session.created` lives) silently never fires it — the event bus has
44
+ no such event. This was dead for weeks because the failure was invisible.
45
+ - **Hook failures are logged with a `[thatch]` prefix and never swallowed.** Two
46
+ hooks were dead for weeks before failures were made visible. If you add a
47
+ hook, log on failure.
48
+ - **`chat.message` has two priority tiers**: extraction nudge first (returns
49
+ early), then the prompt-aware recall nudge. Don't run both in one turn.
50
+
51
+ ## Hooks (Claude Code / Cursor)
52
+
53
+ - **`PostToolBatch`/`postToolUse` must be silent** (no stdout). The agent loop
54
+ must not block on a payload that should be invisible until the next prompt.
55
+ Only `flush-tools` prints.
56
+ - **The recall nudge arrives at the _start_ of the next turn** in Claude
57
+ Code/Cursor, not the end of the current one like opencode's `chat.message`. A
58
+ file-backed queue bridges calls that have no shared state.
59
+ - **Cursor uses `conversation_id`** where Claude Code uses `session_id`.
60
+ `buffer-tool` normalizes the former to a safe filename and tries multiple
61
+ field names for the tool response.
62
+ - **`--json` flips the output shape.** `reminder --json` and `flush-tools
63
+ --json` emit `{ additional_context: "..." }` for Cursor; plain stdout for
64
+ Claude Code. The flag is baked into the installed hook command.
65
+
66
+ ## Sideband
67
+
68
+ - **Socket path = SHA-256 of the DB path**, under `os.tmpdir()`. The MCP server
69
+ and hook processes compute it independently — no out-of-band coordination.
70
+ Changing `THATCH_DB_PATH` moves the socket; a stale socket from a crash is
71
+ cleaned up on connection error.
72
+ - **Sideband failure never blocks.** Server down, stale socket, or a >2 s
73
+ timeout all return `null`, and `flush-tools` falls back to the static write
74
+ nudge. Never hard-fail the agent over a recall nudge.
75
+
76
+ ## Setup
77
+
78
+ - **Skills are always user-scoped**, even in project-local setup. The project's
79
+ `.claude/settings.json` and `CLAUDE.md` stay in the repo, but skills live
80
+ under `$CLAUDE_CONFIG_DIR/skills/` / `~/.cursor/skills/`. Installing into the
81
+ worktree would mutate the user's repo.
82
+ - **`appendBlock` leaves content alone if the markers don't parse.** If the
83
+ start marker is found but the end marker isn't, the whole block is skipped
84
+ rather than half-replaced. Fix the markers or delete the block manually.
85
+ - **The binary path is baked into installed hook commands.** `thatch setup`
86
+ resolves `<bin>` from PATH (or the script's absolute path) and writes it into
87
+ every hook command, so hooks survive after the setup session ends.
88
+
89
+ ## Tests
90
+
91
+ - **DB tests use real SQLite files in `mkdtempSync`, not `:memory:`.** WAL
92
+ behavior differs in-memory and would mask bugs. Temp dirs are removed in
93
+ `afterEach`.
94
+ - **`BgeEmbeddingModel`'s real `PipelineFactory` is untested** — downloading the
95
+ model violates the no-network rule. Lazy-load and retry logic are tested with
96
+ an injected mock factory. The real model is exercised only by real use.
97
+ - **`bun test` does not typecheck**, and `tsconfig.json` excludes `tests`. Test
98
+ type errors are editor-only noise unless you run `tsc` on the test files
99
+ directly. Keep test files type-clean anyway so the editor stays quiet.
@@ -1,27 +1,47 @@
1
- # MCP Parity: OpenCode Plugin vs Claude Code
1
+ # MCP Parity: OpenCode Plugin vs Claude Code vs Cursor
2
2
 
3
- Thatch supports two integration paths:
3
+ Thatch supports three integration paths sharing one core (`src/tool-defs.ts`):
4
4
 
5
5
  1. **OpenCode plugin** — runs inside opencode's Bun runtime. Full access to
6
- plugin hooks: system prompt injection, session events, tool buffering,
7
- compaction context, and skill installation.
6
+ plugin hooks: system prompt injection, session events, in-process tool
7
+ buffering, compaction context, and skill installation.
8
8
  2. **Claude Code MCP server** — runs as a stdio JSON-RPC process. Tools are
9
9
  exposed via MCP `tools/list` + `tools/call`. Session behavior is driven by
10
- Claude Code hooks (`SessionStart`, `UserPromptSubmit`).
10
+ Claude Code hooks (`SessionStart`, `PostToolBatch`, `UserPromptSubmit`).
11
+ 3. **Cursor MCP server** — same stdio MCP server as Claude Code. Session
12
+ behavior is driven by Cursor hooks (`sessionStart`, `postToolUse`,
13
+ `beforeSubmitPrompt`) in a flat `hooks.json` format.
11
14
 
12
- This document maps the feature parity and documents the gaps.
15
+ This document maps the feature parity and documents the remaining gaps.
13
16
 
14
17
  ## Parity matrix
15
18
 
16
- | Feature | OpenCode plugin | Claude Code MCP + hooks | Parity |
17
- |---------|----------------|------------------------|--------|
18
- | **Tools** (remember, recall, list, show, forget, store_list, find_dup, mark_checked) | Plugin tool registration | MCP `tools/list` + `tools/call` | **Full** |
19
- | **System prompt** (store names, usage rules, when-to-write) | `experimental.chat.system.transform` — dynamic, repo name baked in at runtime | CLAUDE.md instructions — static text appended by `thatch setup`; repo name auto-detected at runtime by the MCP server | **Approximate** — CLAUDE.md is static but the MCP server resolves the repo at startup, so tool behavior is identical |
20
- | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder` command; stdout becomes context for Claude | **Full** |
21
- | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | **Gap** — CLAUDE.md persists through compaction, so the agent retains the usage instructions, but the dynamic "re-familiarization" nudge is lost |
22
- | **Extraction nudge** (buffer tool calls, inject JSON payload for fact extraction) | `tool.execute.after` buffers non-thatch tool calls; `chat.message` flushes the buffer as a synthetic text part with the JSON payload | No clean equivalent. `PostToolUse` fires per-call but can't buffer across calls. A file-based approach (write to temp, flush on `UserPromptSubmit`) was considered but rejected as fragile | **Gap** — the `thatch-fact-extractor` skill is installed but not automatically triggered. Agents can still load it manually when they notice tool interactions worth extracting |
23
- | **Skills** (fact-extractor, dedup-classifier) | Installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init | Installed to `~/.claude/skills/` by `thatch setup` | **Full** — same SKILL.md format, different directory |
24
- | **Store detection** (repo identity from git remote) | `worktree` parameter from opencode plugin | `CLAUDE_PROJECT_DIR` environment variable (set by Claude Code for stdio MCP servers) | **Full** |
19
+ | Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
20
+ |---------|----------------|------------------------|---------------------|--------|
21
+ | **Tools** (8, single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | **Full** |
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
+ | **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
+ | **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 |
25
+ | **Extraction nudge** (buffer tool calls, inject JSON payload) | `tool.execute.after` buffers non-thatch tool calls in-process; `chat.message` flushes the buffer as a synthetic text part | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` drains the queue and prints the nudge | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Full** (file-backed) — timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor, not at the end of the current one like opencode's `chat.message` |
26
+ | **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 |
27
+ | **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) |
28
+ | **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** |
29
+ | **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 |
30
+
31
+ ## Tool name conventions
32
+
33
+ The shared tool definitions in `src/tool-defs.ts` use bare names
34
+ (`memory_remember`, `memory_recall`, ...). Each host prefixes them differently:
35
+
36
+ - **opencode**: `thatch_memory_remember` — the prefix is added by the
37
+ `tool()` wrapper in `src/tools.ts`.
38
+ - **Claude Code**: `mcp__thatch__memory_remember` — Claude Code auto-prefixes
39
+ with the server name; the MCP server exposes `memory_remember`.
40
+ - **Cursor**: `mcp__thatch__memory_remember` — same MCP server.
41
+
42
+ The CLAUDE.md / AGENTS.md instructions generated by `thatch setup` reference
43
+ both forms — the full `mcp__thatch__*` names for the host, and bare names for
44
+ readability.
25
45
 
26
46
  ## Why the gaps exist
27
47
 
@@ -31,56 +51,92 @@ OpenCode's `experimental.session.compacting` hook lets the plugin append text
31
51
  to the compaction output — the agent sees a "you are using thatch, here's what
32
52
  you've learned" message after compaction. Claude Code's `PostCompact` hook is
33
53
  side-effects only (logging, external state); it has no `additionalContext`
34
- field and no decision control. `PreCompact` can block compaction but also
35
- can't inject context.
54
+ field and no decision control. Cursor has no equivalent hook.
36
55
 
37
- **Mitigation**: CLAUDE.md is loaded at session start and persists through
38
- compaction. The agent retains the usage instructions. The `SessionStart` hook
39
- with `source: "compact"` fires after compaction, so the recall reminder runs
40
- again — but `thatch reminder` currently doesn't distinguish compaction from
41
- other session-start sources.
56
+ **Mitigation**: CLAUDE.md / AGENTS.md is loaded at session start and persists
57
+ through compaction, so the agent retains the usage instructions. The
58
+ `SessionStart` / `sessionStart` hook with `source: "compact"` fires after
59
+ compaction in Claude Code, so the recall reminder runs again. There is no
60
+ explicit "re-familiarization" nudge for hosts without a compaction hook.
42
61
 
43
- ### Extraction nudge
62
+ ### Extraction nudge timing
44
63
 
45
64
  OpenCode's extraction pipeline works by:
46
65
 
47
66
  1. `tool.execute.after` buffers every non-thatch tool call into a per-session
48
- array (max 20 interactions).
67
+ in-memory array (max 20 interactions).
49
68
  2. On the next `chat.message`, the buffer is flushed and a synthetic text part
50
- carrying the JSON payload is injected into the conversation.
51
- 3. The agent sees the nudge, loads the `thatch-fact-extractor` skill, and
52
- saves durable facts via `thatch_memory_remember`.
53
-
54
- Claude Code hooks fire per-event with no cross-call state. `PostToolUse` can
55
- inject `additionalContext` alongside a single tool result, but there's no
56
- mechanism to buffer across multiple tool calls and flush them together. A
57
- file-based approach (write each interaction to `/tmp/thatch-<session>.jsonl`,
58
- flush on `UserPromptSubmit`) was considered but rejected because:
59
-
60
- - Session tracking is fragile (session IDs from hook input, cleanup on
61
- `SessionEnd`).
62
- - The 10K character cap on `additionalContext` limits the payload size.
63
- - The `UserPromptSubmit` hook fires before the model processes the prompt,
64
- so the nudge would arrive at the start of the next turn, not at the end of
65
- the current one — a different timing than opencode's `chat.message`.
66
-
67
- **Mitigation**: The `UserPromptSubmit` hook includes a write-nudge that
68
- reminds the agent to save new knowledge. The `thatch-fact-extractor` skill is
69
- installed and can be loaded manually. The agent can also call
70
- `thatch_memory_remember` directly when it recognizes a durable fact, which is
71
- the core workflow regardless of whether the extraction nudge fires.
69
+ carrying the JSON payload is injected into the conversation (end of the
70
+ current turn).
71
+ 3. The agent sees the nudge, loads the `thatch-fact-extractor` skill, and saves
72
+ durable facts via `thatch_memory_remember`.
73
+
74
+ Claude Code and Cursor use a **file-backed** queue (`src/extract-queue.ts`)
75
+ because hooks fire one-shot with no cross-call state:
76
+
77
+ 1. `PostToolBatch` (Claude Code) / `postToolUse` (Cursor) appends each tool
78
+ interaction to a per-session JSONL file under `$XDG_CACHE_HOME/thatch/queue/`
79
+ (max 20, oldest dropped). These commands are silent — no stdout — so the
80
+ agent loop is not delayed.
81
+ 2. `UserPromptSubmit` / `beforeSubmitPrompt` runs `thatch flush-tools`, which
82
+ drains the queue and prints the JSON payload. This arrives at the **start**
83
+ of the next turn (before the model processes the prompt), not at the end of
84
+ the current one.
85
+
86
+ The two paths produce the same JSON shape via `buildExtractionPayload()`, so
87
+ the fact-extractor skill receives an identical contract regardless of host.
88
+
89
+ ### Recall nudge without a warm model
90
+
91
+ The prompt-aware recall nudge needs the embedding model to embed the user's
92
+ prompt text. In opencode, the model is warm in-process — the `chat.message`
93
+ hook calls `model.queryEmbed()` directly. In Claude Code/Cursor, hooks are
94
+ one-shot `bun` spawns that can't reach the MCP server's in-memory model.
95
+
96
+ Loading the ~34 MB model on every `UserPromptSubmit` would add ~300-700 ms to
97
+ every prompt in the critical path before the agent responds. The sideband
98
+ socket eliminates this cost; see the next section.
99
+
100
+ ## Sideband socket architecture
101
+
102
+ ```text
103
+ Claude Code / Cursor session
104
+ ├── MCP server (long-lived, warm model)
105
+ │ ├── stdio JSON-RPC (tool calls)
106
+ │ └── Unix socket sideband (embed + search for hooks)
107
+ ├── UserPromptSubmit / beforeSubmitPrompt hook → thatch flush-tools [--json]
108
+ │ ├── drains the file-backed extraction queue (existing)
109
+ │ ├── connects to sideband socket
110
+ │ ├── sends prompt text
111
+ │ ├── receives match labels + scores
112
+ │ └── prints recall nudge or falls back to write nudge
113
+ ├── PostToolBatch → thatch buffer-batch (Claude Code)
114
+ │ └── postToolUse → thatch buffer-tool (Cursor)
115
+ └── SessionStart / sessionStart hook → thatch reminder [--json]
116
+ ```
117
+
118
+ The socket path is derived from a SHA-256 hash of the DB path — both the MCP
119
+ server and hook processes resolve the same DB path independently (from
120
+ `THATCH_DB_PATH` or the default under `XDG_CONFIG_HOME`), so they arrive at the
121
+ same socket path without out-of-band coordination. The path lives under
122
+ `os.tmpdir()`.
123
+
124
+ The protocol is newline-delimited JSON: one request per connection, one
125
+ response. Requests are `{"method":"match","text":"...","stores":[...],
126
+ "threshold":N,"limit":N}`. Responses are `{"ok":true,"matches":[...]}` or
127
+ `{"ok":false,"error":"..."}`.
128
+
129
+ Graceful degradation: if the socket isn't available (MCP server not running,
130
+ old version, stale socket from a crash, or a >2 s timeout), `sidebandMatch`
131
+ returns `null` and `flush-tools` falls back to the static write nudge. The
132
+ recall nudge is best-effort — its absence never blocks the agent's workflow. A
133
+ stale socket file left by a crash is cleaned up on connection error.
72
134
 
73
135
  ## Architecture implications
74
136
 
75
137
  The shared tool definitions in `src/tool-defs.ts` are the single source of
76
- truth for both paths. The opencode plugin wraps them in `tool()` with zod
77
- schemas; the MCP server wraps them in `z.object()` for validation and
78
- `z.toJSONSchema()` for the protocol response. Tool names differ:
79
-
80
- - **opencode**: `thatch_memory_remember` (prefixed at the wrapper level)
81
- - **MCP/Claude Code**: `mcp__thatch__memory_remember` (Claude Code auto-prefixes
82
- with the server name; the MCP server exposes `memory_remember`)
83
-
84
- The `thatch setup` command generates CLAUDE.md instructions that reference
85
- both forms — the full `mcp__thatch__*` names for Claude Code, and bare names
86
- for readability.
138
+ truth for all three paths. The opencode plugin wraps them in `tool()` with zod
139
+ schemas and a `thatch_` prefix; the MCP server (used by both Claude Code and
140
+ Cursor) wraps them in `z.object()` for validation and `z.toJSONSchema()` for
141
+ the protocol response. See `docs/dev/setup-and-hooks.md` for the concrete
142
+ files, hook event names, and command shapes each host writes.
@@ -0,0 +1,134 @@
1
+ # Setup and Hook Configuration
2
+
3
+ How `thatch setup` wires each host, and which hook events each path relies on.
4
+ For feature parity and gaps, see `mcp-parity.md`. This document is the concrete
5
+ artifact reference.
6
+
7
+ ## OpenCode (plugin)
8
+
9
+ No `setup` command — opencode loads the plugin from `opencode.json`:
10
+
11
+ ```jsonc
12
+ { "plugin": ["@jeffober/thatch"] }
13
+ ```
14
+
15
+ The plugin entry (`src/index.ts`) registers these hooks in code:
16
+
17
+ | Hook | Input | Output | Behavior |
18
+ |------|-------|--------|----------|
19
+ | `experimental.chat.system.transform` | `{}` | `{ system: string[] }` | Pushes the system prompt (store names, usage rules). |
20
+ | `experimental.session.compacting` | `{ sessionID }` | `{ context: string[] }` | Marks the session as compacting, pushes re-familiarization context. |
21
+ | `experimental.compaction.autocontinue` | `{ sessionID }` | `{ enabled: boolean }` | Clears the compacting flag so `chat.message` nudges resume post-compaction. |
22
+ | `tool.execute.after` | `{ tool, sessionID, callID, args }` | `{ title, output, metadata }` | Buffers non-`thatch_` tool calls into the in-memory extraction ring buffer (max 20). |
23
+ | `chat.message` | `{ sessionID, messageID }` | `{ message, parts }` | Two tiers: extraction nudge if pending, else prompt-aware recall nudge. Skipped while the session is compacting. |
24
+ | `event` (`session.created`) | `{ event: { type, properties: { info: { id } } } }` | — | Calls `client.session.prompt` with the reminder + hygiene heartbeat. |
25
+ | `dispose` | — | — | Closes the DB. |
26
+
27
+ Skills install to `$XDG_CONFIG_HOME/opencode/skills` at plugin init — shared
28
+ **and** opencode-only (the coordinator needs sub-agents).
29
+
30
+ ## Claude Code (MCP server + hooks)
31
+
32
+ `thatch setup --claude` (project-local) or `thatch setup --claude --global`.
33
+
34
+ ### Artifacts written (Claude Code)
35
+
36
+ | Artifact | Project-local | Global (`--global`) |
37
+ |----------|--------------|---------------------|
38
+ | MCP config | `.mcp.json` (`mcpServers.thatch`, stdio, `["mcp"]`) | none — prints `claude mcp add --scope user thatch -- <bin> mcp` |
39
+ | Instructions | `CLAUDE.md` (idempotent `appendBlock`) | `$CLAUDE_CONFIG_DIR/CLAUDE.md` |
40
+ | Hooks | `.claude/settings.json` | `$CLAUDE_CONFIG_DIR/settings.json` |
41
+ | Skills | `$CLAUDE_CONFIG_DIR/skills/` (always user-scoped) | same |
42
+
43
+ `CLAUDE_CONFIG_DIR` overrides the default `~/.claude` for all user-scoped
44
+ paths. Project-local keeps `.mcp.json`, `CLAUDE.md`, and `.claude/settings.json`
45
+ in the repo; only skills are user-scoped.
46
+
47
+ ### Hook events (nested `settings.json`)
48
+
49
+ ```jsonc
50
+ {
51
+ "hooks": {
52
+ "SessionStart": { "hooks": [{ "type": "command", "command": "<bin> reminder" }] },
53
+ "PostToolBatch": { "hooks": [{ "type": "command", "command": "<bin> buffer-batch" }] },
54
+ "UserPromptSubmit": { "hooks": [{ "type": "command", "command": "<bin> flush-tools" }] }
55
+ }
56
+ }
57
+ ```
58
+
59
+ | Event | Command | Output | Role |
60
+ |-------|---------|--------|------|
61
+ | `SessionStart` | `thatch reminder` | plain text to stdout (becomes context) | Recall instructions + hygiene heartbeat |
62
+ | `PostToolBatch` | `thatch buffer-batch` | **silent** (no stdout) | Appends a batch of tool calls to the file-backed JSONL queue |
63
+ | `UserPromptSubmit` | `thatch flush-tools` | nudge text to stdout | Drains queue (extraction nudge), else recall nudge via sideband, else write nudge |
64
+
65
+ `PostToolBatch` is silent so the agent loop is not delayed; the buffered
66
+ content is invisible until `UserPromptSubmit` flushes it.
67
+
68
+ ## Cursor (MCP server + hooks)
69
+
70
+ `thatch setup --cursor` (project-local) or `thatch setup --cursor --global`.
71
+ Cursor uses the same stdio MCP server as Claude Code; only the hooks differ.
72
+
73
+ ### Artifacts written (Cursor)
74
+
75
+ | Artifact | Project-local | Global (`--global`) |
76
+ |----------|--------------|---------------------|
77
+ | MCP config | `.cursor/mcp.json` | `$CURSOR_CONFIG_DIR/mcp.json` (or `~/.cursor/mcp.json`) |
78
+ | Instructions | `AGENTS.md` (idempotent `appendBlock`, Cursor-marked) | `$CURSOR_CONFIG_DIR/AGENTS.md` |
79
+ | Hooks | `.cursor/hooks.json` (flat) | `$CURSOR_CONFIG_DIR/hooks.json` |
80
+ | Skills | `$CURSOR_CONFIG_DIR/skills/` (shared only) | same |
81
+
82
+ Cursor has no documented env override like `CLAUDE_CONFIG_DIR`; `CURSOR_CONFIG_DIR`
83
+ is honored for symmetry and forward-compatibility. Cursor has no equivalent of
84
+ `claude mcp add --scope user` — writing `~/.cursor/mcp.json` directly is enough.
85
+
86
+ ### Hook events (flat `hooks.json`)
87
+
88
+ ```jsonc
89
+ {
90
+ "version": 1,
91
+ "hooks": {
92
+ "sessionStart": [{ "command": "<bin> reminder --json" }],
93
+ "postToolUse": [{ "command": "<bin> buffer-tool" }],
94
+ "beforeSubmitPrompt": [{ "command": "<bin> flush-tools --json" }]
95
+ }
96
+ }
97
+ ```
98
+
99
+ | Event | Command | Output | Role |
100
+ |-------|---------|--------|------|
101
+ | `sessionStart` | `thatch reminder --json` | `{ additional_context: "..." }` | Recall + heartbeat, JSON-wrapped for Cursor |
102
+ | `postToolUse` | `thatch buffer-tool` | **silent** | Appends a **single** tool call to the file-backed queue |
103
+ | `beforeSubmitPrompt` | `thatch flush-tools --json` | JSON `additional_context` | Drains queue, else recall via sideband, else write nudge |
104
+
105
+ Differences from Claude Code:
106
+
107
+ - **Flat format** (`{version, hooks:{event:[{command}]}}`) vs nested.
108
+ - `postToolUse` fires **per tool** (Cursor has no `PostToolBatch`); `buffer-tool`
109
+ reads `conversation_id` (vs `session_id`) and normalizes it to a safe filename.
110
+ - `--json` on `reminder` and `flush-tools` so Cursor parses the output as
111
+ `additional_context`.
112
+
113
+ ## Idempotence and drift
114
+
115
+ All setup operations are idempotent:
116
+
117
+ - **Instructions**: `appendBlock` searches for start/end markers and replaces
118
+ the block between them, so re-running setup updates drifted content without
119
+ clobbering surrounding text. No marker found → append.
120
+ - **Hooks**: `replaceThatchHooks` / `replaceCursorThatchHooks` filter out any
121
+ hook group whose command contains `thatch`, then add the current ones. A
122
+ legacy `thatch echo` hook is replaced with `flush-tools`. Non-thatch hooks are
123
+ preserved.
124
+ - **MCP config**: existing `mcpServers` are preserved; only the `thatch` entry
125
+ is set.
126
+ - **Skills**: `installSkills` only writes when on-disk content differs from the
127
+ definition (drift detection). Skills are plugin-owned — local edits are
128
+ overwritten on the next plugin init or `thatch setup`.
129
+
130
+ ## Binary resolution
131
+
132
+ `thatch setup` resolves `<bin>` from PATH, falling back to the absolute path of
133
+ the running script. This is baked into every installed hook command, so the
134
+ hooks keep working after the session that ran setup ends.
@@ -0,0 +1,109 @@
1
+ # Skills
2
+
3
+ Thatch installs SKILL.md files that give the agent task-specific workflows. All
4
+ skill content is defined in `src/skills.ts` and installed to a host-specific
5
+ directory. See `mcp-parity.md` for which skills each host gets.
6
+
7
+ ## Skill file format
8
+
9
+ Each skill is a single `SKILL.md` with YAML frontmatter:
10
+
11
+ ```markdown
12
+ ---
13
+ name: thatch-fact-extractor
14
+ description: Extract durable project facts ... Use when ...
15
+ ---
16
+
17
+ <role and instructions>
18
+ ```
19
+
20
+ - `name` must match the directory name (`<skillsDir>/<name>/SKILL.md`).
21
+ - `description` drives when the agent loads the skill (both opencode and
22
+ Cursor auto-discover skills and use the description for relevance).
23
+
24
+ ## The 11 skills
25
+
26
+ **Shared (10)** — installed everywhere; no sub-agents required:
27
+
28
+ | Skill | Role |
29
+ |-------|------|
30
+ | `thatch-fact-extractor` | Turn buffered tool interactions into `thatch_memory_remember` calls. |
31
+ | `thatch-dedup-classifier` | Classify and resolve `find_duplicates` pairs/clusters. |
32
+ | `thatch-project-primer` | Investigate a new project and write foundational memories. |
33
+ | `thatch-review-pedantic` | Mechanical correctness: spelling, naming, doc accuracy, specs, stale artifacts. |
34
+ | `thatch-review-acceptance` | Behavioral/product review: UX, behavioral delta, integration effects. |
35
+ | `thatch-review-state-flow` | Data flow and contracts: module boundaries, implicit FSMs, error propagation. |
36
+ | `thatch-review-no-slop` | AI writing anti-patterns: change narration, fourth-wall breaks, em dashes, filler. |
37
+ | `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of behavior? |
38
+ | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. |
39
+ | `thatch-session-reflection` | End-of-session memory recording (project, user, tools, self). |
40
+
41
+ **opencode-only (1)** — the coordinator needs sub-agent support:
42
+
43
+ | Skill | Role |
44
+ |-------|------|
45
+ | `thatch-code-review` | Resolve review target, estimate complexity, partition, dispatch the 5 specialists in parallel, synthesize. |
46
+
47
+ ## REVIEW_COMMON
48
+
49
+ The five review specialists share a framework interpolated via `${REVIEW_COMMON}`:
50
+
51
+ - **Static analysis only** — no running tests/linters/compilers.
52
+ - **Scope gathering** — resolve the git range, `git diff --stat`, read changed
53
+ files in full for context.
54
+ - **Runtime model** — CLI/long-lived server/library/batch — this determines
55
+ which bug classes are realistic.
56
+ - **Reachability gate** — every finding must describe a concrete normal-usage
57
+ trigger; "the code allows this" is not enough.
58
+ - **Intent verification** — trace callers, check git history, and `thatch_memory_recall`
59
+ for design rationale before flagging a behavior as a bug.
60
+ - **Output format** — `[SEVERITY] [CATEGORY] file:line` with finding, evidence
61
+ (quoted), trigger, reachability, source of truth, producer chain, provenance.
62
+
63
+ The synthesizer reuses the same verification rigor but has its own structure
64
+ (it does not interpolate `REVIEW_COMMON`).
65
+
66
+ ## The two arrays
67
+
68
+ ```ts
69
+ const SHARED_SKILLS: SkillDef[] = [ /* 10 skills above */ ];
70
+ const OPENCODE_ONLY_SKILLS: SkillDef[] = [ /* code-review coordinator */ ];
71
+ ```
72
+
73
+ `installSkills(skillsDir, skills?)` defaults to `SHARED_SKILLS`. The opencode
74
+ plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`; `thatch setup --claude`
75
+ and `--cursor` pass only the shared set.
76
+
77
+ ## Install mechanics
78
+
79
+ - **Location**: `$XDG_CONFIG_HOME/opencode/skills` (opencode),
80
+ `$CLAUDE_CONFIG_DIR/skills` (Claude Code), `~/.cursor/skills` (Cursor).
81
+ Always user-scoped — never into the worktree.
82
+ - **Trigger**: opencode installs at plugin init; Claude Code/Cursor install via
83
+ `thatch setup`.
84
+ - **Drift detection**: `installSkills` only writes when the on-disk content
85
+ differs from the definition. This is how skill improvements ship with new
86
+ versions without users manually deleting files.
87
+ - **Idempotent**: re-running init or setup overwrites drifted content but leaves
88
+ unrelated skill files alone.
89
+
90
+ ## Adding a skill
91
+
92
+ 1. Write the `SKILL.md` content as a template literal in `src/skills.ts`.
93
+ Interpolate `${REVIEW_COMMON}` if it's a review specialist.
94
+ 2. Add a `{ name, content }` entry to `SHARED_SKILLS` (or `OPENCODE_ONLY_SKILLS`
95
+ if it needs sub-agents).
96
+ 3. If the skill name appears in a tool's expected workflow (e.g. the extraction
97
+ nudge references `thatch-fact-extractor`), update that prompt in `src/prompts.ts`.
98
+ 4. The opencode plugin and `thatch setup` pick it up automatically on next init.
99
+
100
+ ## Memory review skills in practice
101
+
102
+ - **Quick single lens**: load any specialist directly and point it at a branch.
103
+ - **Full review on opencode**: load `thatch-code-review` — it dispatches all 5
104
+ specialists in parallel, then synthesizes.
105
+ - **Full review on Claude Code/Cursor**: run each specialist in sequence, then
106
+ run `thatch-review-synthesizer` to verify and aggregate.
107
+
108
+ The coordinator is the only skill that can't be used without sub-agents; that's
109
+ why it lives in `OPENCODE_ONLY_SKILLS`.
package/docs/qa/README.md CHANGED
@@ -24,8 +24,17 @@ network access and no writes outside temp directories.
24
24
  ```bash
25
25
  bun test # all tests
26
26
  bun test --watch # watch mode
27
+ mise run check # typecheck + bun test + markdownlint (the CI gate)
28
+ mise run typecheck # tsc alone
29
+ mise run lint-md # markdownlint only
27
30
  ```
28
31
 
32
+ `mise run check` is the canonical quality gate (also run by CI on push/PR to
33
+ `main`). Typecheck uses `tsconfig.check.json` (which includes the test files
34
+ that the build's `tsconfig.json` excludes). Markdownlint enforces structural
35
+ correctness on `README.md` and `docs/` excluding `docs/plans/`; see
36
+ `.markdownlint-cli2.jsonc` for rule config.
37
+
29
38
  ## Test files and coverage
30
39
 
31
40
  ### `tests/db.test.ts`
@@ -4,12 +4,28 @@ Manual end-to-end QA scenarios for thatch. Each use case has preconditions,
4
4
  steps, and expected results. These cover behavior the automated suite can't:
5
5
  real opencode sessions, the real embedding model, and the CLI.
6
6
 
7
+ Newer use cases carry an _Automatable_ note flagging which are pure
8
+ file/IPC/CLI contracts ready to graduate into `bun:test` integration tests.
9
+
7
10
  | Use case | Covers |
8
11
  |----------|--------|
9
12
  | [UC-001](UC-001-memory-roundtrip.md) | Remember/recall across sessions, store naming, duplicate rejection |
10
13
  | [UC-002](UC-002-dedup-cycle.md) | Full dedup review cycle: find, classify, resolve, mark checked |
11
14
  | [UC-003](UC-003-extraction-nudge.md) | Tool buffering → extraction nudge → agent-driven fact saving |
12
15
  | [UC-004](UC-004-cli-inspection.md) | CLI commands, arg validation, env overrides |
16
+ | [UC-005](UC-005-setup-install.md) | `thatch setup --claude`/`--cursor`, project-local + `--global`, idempotent re-run, legacy hook migration |
17
+ | [UC-006](UC-006-cursor-hook-lifecycle.md) | Cursor hook lifecycle: `buffer-tool` (`conversation_id`), `flush-tools --json`, queue drain |
18
+ | [UC-007](UC-007-recall-nudge.md) | Prompt-aware recall nudge — opencode in-process + Claude/Cursor sideband paths; no telemetry |
19
+ | [UC-008](UC-008-hygiene-heartbeat.md) | Hygiene heartbeat: stale 90d, orphaned branches, pending dedup; advisory, agent-driven |
20
+ | [UC-009](UC-009-flush-tools-tiers.md) | `flush-tools` three-tier priority (extraction > recall > write) + sideband degradation |
21
+ | [UC-010](UC-010-prime.md) | `thatch prime` CLI dispatch chain (opencode/Cursor/claude), child exit code |
22
+ | [UC-011](UC-011-write-time-similarity-warning.md) | Write-time warning never blocks; merge or mark-distinct reconciliation |
23
+ | [UC-012](UC-012-model-migration.md) | `THATCH_MODEL` change: old memories skipped (not corrupted) until re-saved |
24
+ | [UC-013](UC-013-compaction-context.md) | Compaction context re-familiarization (opencode-only) |
25
+ | [UC-014](UC-014-skill-install-drift.md) | Skill install: 10 shared vs 11 opencode; drift overwritten on re-setup |
26
+ | [UC-015](UC-015-env-override-matrix.md) | `THATCH_DB_PATH`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`, `THATCH_QUEUE_DIR`, `THATCH_RECALL_THRESHOLD`, `THATCH_MODEL` |
27
+ | [UC-016](UC-016-concurrent-session-isolation.md) | Per-session extraction isolation (in-memory + file queue); shared stateless sideband |
28
+ | [UC-017](UC-017-buffer-tool-vs-buffer-batch.md) | `buffer-batch` (Claude, `tool_calls[]`+`session_id`) vs `buffer-tool` (Cursor, single+`conversation_id`) |
13
29
 
14
30
  ## Template
15
31
 
@@ -0,0 +1,34 @@
1
+ # UC-005: Setup install (Claude Code and Cursor)
2
+
3
+ **Preconditions**
4
+ - Thatch installed and `bun` on PATH
5
+ - Target config dirs writable (`$XDG_CONFIG_HOME`, `$CLAUDE_CONFIG_DIR`, `~/.cursor`)
6
+
7
+ **Steps**
8
+ 1. From a project root: `thatch setup --claude`
9
+ 2. From the same project root: `thatch setup --cursor`
10
+ 3. Repeat both commands (re-run idempotence and drift recovery)
11
+ 4. Add a non-thatch hook to `.claude/settings.json` and `.cursor/hooks.json`,
12
+ then re-run setup
13
+ 5. From elsewhere: `thatch setup --cursor --global`, then `thatch setup --claude --global`
14
+
15
+ **Expected**
16
+ - `--claude` project-local writes `.mcp.json` (`mcpServers.thatch`, stdio + `["mcp"]`),
17
+ appends a thatch block to `CLAUDE.md` bracketed by start/end markers (replaced, not
18
+ duplicated, on re-run), writes `.claude/settings.json` hooks (`SessionStart` -> `thatch reminder`,
19
+ `PostToolBatch` -> `thatch buffer-batch`, `UserPromptSubmit` -> `thatch flush-tools`), and installs
20
+ **10 shared skills** (no code-review coordinator) to `$CLAUDE_CONFIG_DIR/skills/` — user-scoped
21
+ even in project-local mode.
22
+ - `--cursor` project-local writes `.cursor/mcp.json`, appends to `AGENTS.md`, writes
23
+ `.cursor/hooks.json` in the **flat format** (`{version:1, hooks:{...}}`): `sessionStart` ->
24
+ `thatch reminder --json`, `postToolUse` -> `thatch buffer-tool`, `beforeSubmitPrompt` ->
25
+ `thatch flush-tools --json`; and installs 10 shared skills to `~/.cursor/skills/`.
26
+ - Re-run is idempotent: instructions are not duplicated, thatch hooks are replaced (not appended),
27
+ and non-thatch hooks are preserved. A legacy `thatch echo` hook is replaced with `flush-tools`.
28
+ - `--global` (Claude) writes `~/.claude/CLAUDE.md` + `~/.claude/settings.json` but **no project
29
+ `.mcp.json`** — it prints a `claude mcp add --scope user` command instead. `--global` (Cursor)
30
+ writes `~/.cursor/mcp.json`, `~/.cursor/AGENTS.md`, `~/.cursor/hooks.json`, `~/.cursor/skills/`.
31
+
32
+ _Automatable: yes — file presence and shape assertions on `.mcp.json`, `hooks.json`,
33
+ `CLAUDE.md`/`AGENTS.md` blocks, and the skills directory. `setup.test.ts` already covers the
34
+ unit contract; this is the end-to-end runbook version._