@jeffober/thatch 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +24 -10
  2. package/artifacts/skills/thatch-code-review.md +9 -7
  3. package/artifacts/skills/thatch-review-economy.md +97 -0
  4. package/artifacts/skills/thatch-review-followup.md +1 -1
  5. package/artifacts/skills/thatch-review-synthesizer.md +15 -5
  6. package/bin/qa-run +38 -0
  7. package/bin/release +21 -0
  8. package/bin/thatch +17 -5
  9. package/docs/dev/README.md +71 -18
  10. package/docs/dev/features/README.md +33 -0
  11. package/docs/dev/features/behavior-engine.md +271 -0
  12. package/docs/dev/features/cicd.md +89 -0
  13. package/docs/dev/features/cli.md +81 -0
  14. package/docs/dev/features/compaction-recovery.md +48 -0
  15. package/docs/dev/features/database.md +244 -0
  16. package/docs/dev/features/deduplication.md +102 -0
  17. package/docs/dev/features/extraction.md +253 -0
  18. package/docs/dev/features/hygiene.md +81 -0
  19. package/docs/dev/features/memory-store.md +154 -0
  20. package/docs/dev/features/multi-host.md +228 -0
  21. package/docs/dev/features/nudge-pipeline.md +282 -0
  22. package/docs/dev/features/prediction-engine.md +253 -0
  23. package/docs/dev/features/qa-system.md +205 -0
  24. package/docs/dev/features/session-lifecycle.md +110 -0
  25. package/docs/dev/features/setup.md +199 -0
  26. package/docs/dev/features/sideband.md +104 -0
  27. package/docs/dev/gotchas.md +3 -3
  28. package/docs/dev/mcp-parity.md +6 -4
  29. package/docs/dev/setup-and-hooks.md +4 -4
  30. package/docs/dev/skills.md +10 -8
  31. package/docs/plans/README.md +2 -2
  32. package/docs/user/README.md +58 -20
  33. package/docs/user/behavior-engine.md +85 -0
  34. package/docs/user/cli.md +98 -0
  35. package/docs/user/code-review.md +73 -0
  36. package/docs/user/deduplication.md +70 -0
  37. package/docs/user/default-behaviors.md +75 -0
  38. package/docs/user/extraction.md +74 -0
  39. package/docs/user/hygiene.md +76 -0
  40. package/docs/user/memory.md +81 -0
  41. package/docs/user/prediction-engine.md +87 -0
  42. package/docs/user/setup.md +110 -0
  43. package/docs/user/skills.md +97 -0
  44. package/package.json +2 -2
  45. package/src/behavior.ts +120 -0
  46. package/src/db.ts +160 -417
  47. package/src/embeddings.ts +0 -41
  48. package/src/extraction.ts +2 -2
  49. package/src/git.ts +3 -1
  50. package/src/index.ts +55 -9
  51. package/src/mcp.ts +30 -1
  52. package/src/prediction.ts +127 -0
  53. package/src/prompts.ts +135 -188
  54. package/src/scoring-engine.ts +374 -0
  55. package/src/seed-behaviors.ts +339 -0
  56. package/src/setup.ts +2 -6
  57. package/src/sideband.ts +36 -8
  58. package/src/skills.ts +1 -0
  59. package/src/tool-defs.ts +172 -25
  60. package/src/vector-math.ts +30 -0
  61. package/docs/qa/README.md +0 -175
@@ -0,0 +1,154 @@
1
+ # Memory Store
2
+
3
+ The memory store is thatch's core feature: a SQLite-backed store of memories with semantic search via local embeddings. Everything else thatch does — extraction, hygiene, nudges, predictions, behaviors — reads from or writes to this store.
4
+
5
+ ## What it does
6
+
7
+ The store persists memories keyed by label, scoped to a project store or the shared "global" store. Each memory is a labeled chunk of text an agent writes for a future session of itself to read.
8
+
9
+ - Stores memories keyed by label within a store. The store name is auto-detected from the git remote — the `owner/repo` slug — or falls back to `"global"` for cross-project knowledge.
10
+ - Semantic search via cosine similarity over BGE-small-en-v1.5 embeddings (384 dimensions, ~34 MB). The model runs locally and is lazy-loaded.
11
+ - Six tools: `memory_remember`, `memory_recall`, `memory_list`, `memory_show`, `memory_forget`, and `store_list`. All are defined in `src/tool-defs.ts`.
12
+ - Branch scoping. Memories can be scoped to a git branch for work-in-progress context, or left unscoped for project-wide knowledge. Branch-scoped memories are recalled alongside project-wide ones when the branch matches.
13
+ - Confidence grading on a 1–10 scale, stored per memory.
14
+ - Archived flag. Archived memories are long-term records excluded from search and hygiene by default. Pass `includeArchived: true` to surface them.
15
+ - Write-time duplicate detection. When a new memory is similar to existing ones (cosine ≥ 0.85), the save returns a warning listing the near-duplicates. The save always proceeds — the warning asks the agent to reconcile, never blocks.
16
+ - Asymmetric search. Query text gets a BGE-specific prefix for retrieval; stored passages get no prefix. This follows the BGE training convention for asymmetric search.
17
+ - Multi-model dimension check. Entries embedded by a model with a different vector dimension are silently skipped during search, not ranked. The `model` column is informational — dimension is the discriminator.
18
+ - Recall tracking. Each memory carries `recall_count` and `last_recalled_at`, stamped when an agent-initiated search returns it. The hygiene system uses these to detect stale memories.
19
+
20
+ ## How it works
21
+
22
+ ### Store detection
23
+
24
+ `src/git.ts` parses `owner/repo` from the git remote URL. The resolution chain is worktree-safe:
25
+
26
+ 1. Parse `owner/repo` from `git remote get-url origin`.
27
+ 2. Fall back to the basename of the git common dir (worktree-safe).
28
+ 3. Fall back to the directory basename.
29
+ 4. Falls back to `"unknown"` if none of the above yield a result.
30
+
31
+ The "global" store always exists — it is inserted at schema init (`INSERT OR IGNORE INTO stores (name) VALUES ('global')`). Store creation is implicit: the first `memory_remember` to a new store calls `ensureStore`, which creates it via `INSERT OR IGNORE`.
32
+
33
+ ### Embedding model
34
+
35
+ `src/embeddings.ts` defines the `BgeEmbeddingModel` class.
36
+
37
+ - Model: `Xenova/bge-small-en-v1.5` — 384 dimensions, ~34 MB.
38
+ - Lazy-loaded via the `@huggingface/transformers` `pipeline("feature-extraction")` API. The first load downloads from Hugging Face Hub; subsequent loads use the cached model.
39
+ - Query prefix: `"Represent this sentence for searching relevant passages: "` — the BGE asymmetric search convention. Queries get the prefix; passages do not.
40
+ - Mean pooling and L2 normalization are applied via pipeline options.
41
+ - Output is a `Float32Array`.
42
+ - `MockEmbeddingModel` is available for tests.
43
+ - The model can be overridden via the `THATCH_MODEL` environment variable.
44
+
45
+ ### Write path (memory_remember)
46
+
47
+ ```text
48
+ agent calls memory_remember(label, content, opts)
49
+ → model.passageEmbed("# label\n\ncontent") → Float32Array
50
+ → db.findSimilar(store, embedding, { excludeSlug }) → write-time collision check
51
+ → db.remember(store, label, content, embedding, model.name, opts)
52
+ overwrite:false → atomic INSERT (PK constraint rejects duplicates)
53
+ overwrite:true → upsert + clear stale dedup verdicts for that slug
54
+ → return confirmation + warning if similar memories found
55
+ ```
56
+
57
+ The tool prepends `# ${label}\n\n` to the content before embedding. The stored `content` column holds this full formatted text — it is the exact input that was embedded, so a future re-embed requires no reconstruction.
58
+
59
+ The `findSimilar` call is a write-time collision check with no telemetry. It excludes the slug being written (self-exclusion on overwrite). If it finds entries with cosine ≥ 0.85, the return value includes a warning listing them.
60
+
61
+ The save always proceeds. The warning asks the agent to reconcile — merge into one entry (overwrite + forget the other) or record that they are genuinely distinct via `dedup_mark_checked`.
62
+
63
+ On overwrite, the upsert clears stale dedup verdicts: any `dedup_pairs` rows referencing that slug are deleted, since the content change may resolve or create duplicates.
64
+
65
+ ### Read path (memory_recall)
66
+
67
+ ```text
68
+ agent calls memory_recall(query, opts)
69
+ → model.queryEmbed(query) → Float32Array
70
+ → db.recall([repo, "global"], queryEmbedding, { branch?, limit })
71
+ → db.search() → cosine scoring, sort desc, top-N
72
+ → stamp recall_count + last_recalled_at on returned rows
73
+ → return formatted results with scores
74
+ ```
75
+
76
+ The tool layer hardcodes `[repo, "global"]` as the default search scope. Omitting the `store` parameter searches both. Passing a specific store searches only that store.
77
+
78
+ Entries with a mismatched embedding dimension are skipped during search — the `emb.length !== queryEmbedding.length` guard in `db.search()`. This handles the case where a different embedding model was used for older entries.
79
+
80
+ ### Search vs. recall
81
+
82
+ Two methods, one scoring engine, different telemetry policy:
83
+
84
+ | Method | Telemetry | Used by |
85
+ |--------|-----------|---------|
86
+ | `db.search()` | None | Prompt-aware recall nudge (see [nudge-pipeline.md](nudge-pipeline.md)) |
87
+ | `db.recall()` | Stamps `recall_count` / `last_recalled_at` | `thatch_memory_recall` tool, CLI `thatch search` |
88
+
89
+ The distinction is deliberate. The recall nudge checks whether memories relate to a prompt — the plugin is looking, not the agent. Stamping telemetry on a nudge would inflate the "used recently" signal that hygiene reporting depends on. Only explicit agent-initiated recall stamps telemetry.
90
+
91
+ ### Default recall scope
92
+
93
+ The tool layer hardcodes `[repo, "global"]` as the default search scope. This means a `memory_recall` call with no `store` argument searches both the project store and the global store. There is no way to search only the project store without the global store via the tool — pass an explicit `store` argument to scope to one.
94
+
95
+ ### Archived memories
96
+
97
+ The `archived` column is an integer flag: `0` (default) is live, `1` is archived.
98
+
99
+ - `search()`, `findDuplicates()`, and the stale-entry count in hygiene all filter `WHERE archived = 0`.
100
+ - To search archived memories: pass `includeArchived: true` to `memory_recall`.
101
+ - To archive: write with `archived: true`. To unarchive: write with `archived: false`.
102
+ - Updating an already-archived memory requires an explicit `archived` parameter. The guard prevents accidental unarchival — the caller must consciously pass `archived: true` to keep it archived or `archived: false` to unarchive it.
103
+
104
+ ### Slug generation
105
+
106
+ `slugify()` converts a label to a slug: lowercase, whitespace to hyphen, Unicode letters and digits preserved. All-symbol labels fall back to a hash so no label ever maps to an empty slug. ASCII labels produce the same slugs as earlier releases.
107
+
108
+ The slug plus store name form the composite primary key: `PRIMARY KEY (slug, store)`.
109
+
110
+ ### CLI access
111
+
112
+ The `bin/thatch` CLI provides read-only access to the store:
113
+
114
+ | Command | Description |
115
+ |---------|-------------|
116
+ | `thatch stores` | List all stores |
117
+ | `thatch list [store]` | List memory labels in a store |
118
+ | `thatch show <label> [store]` | Display one memory |
119
+ | `thatch forget <label> [store]` | Delete one memory |
120
+ | `thatch search <query> [store]` | Semantic cosine search (limit 10; `"all"` searches project + global) |
121
+
122
+ ## Interactions with other features
123
+
124
+ - **Extraction pipeline** ([extraction.md](extraction.md)): the extraction sub-agent writes memories via `memory_remember`. The extraction buffer drains on any `memory_remember` call.
125
+ - **Deduplication** ([deduplication.md](deduplication.md)): `find_duplicates` scans stored memories for cosine-similar pairs. `dedup_mark_checked` records verdicts in the `dedup_pairs` table. Overwriting or forgetting a memory clears its dedup verdicts.
126
+ - **Hygiene** ([hygiene.md](hygiene.md)): staleness checks use `recall_count` and `last_recalled_at` to find memories nobody has read recently. Duplicate checks use cosine search. Orphaned-branch detection finds branch-scoped memories whose branch no longer exists.
127
+ - **Nudge pipeline** ([nudge-pipeline.md](nudge-pipeline.md)): the recall nudge uses `db.search()` — not `db.recall()` — to avoid inflating telemetry. The nudge fires when search results exceed the similarity threshold (default 0.55).
128
+ - **Prediction engine** ([prediction-engine.md](prediction-engine.md)) and **behavior engine** ([behavior-engine.md](behavior-engine.md)): these share the same embedding model but store vectors in separate tables (`prediction_matchers`, `prediction_predictions`, `behavior_matchers`, `behavior_behaviors`). They do not read from or write to the `entries` table.
129
+
130
+ ## Source files
131
+
132
+ | File | Responsibility |
133
+ |------|---------------|
134
+ | `src/db.ts` | SQLite schema, CRUD for entries and stores, brute-force cosine search (`search` vs. `recall`), dedup verdict tracking, prediction and behavior tables |
135
+ | `src/embeddings.ts` | `BgeEmbeddingModel` (lazy-load, query/passage embed, mean pooling, L2 normalization) and `MockEmbeddingModel` for tests |
136
+ | `src/git.ts` | Detect repo identity (store name) from git remote; worktree-safe fallback chain |
137
+ | `src/tool-defs.ts` | Tool definitions — single source of truth for all tools, including the six memory tools |
138
+ | `bin/thatch` | CLI subcommands: `stores`, `list`, `show`, `forget`, `search` |
139
+
140
+ ## Key invariants
141
+
142
+ 1. **No global mutable state.** Every module accepts dependencies explicitly. `ThatchDB` takes a path; `BgeEmbeddingModel` takes an optional pipeline factory; tools receive a `ctx` with `db` and `model`.
143
+
144
+ 2. **Embedding is a separate concern.** `db.ts` stores and retrieves BLOBs and compares vectors handed to it. It knows nothing about embedding models — it never calls `passageEmbed` or `queryEmbed`. The tool layer is the bridge: it embeds, then hands the vector to the DB.
145
+
146
+ 3. **Embedding spaces are discriminated by vector dimension, not model tag.** The `model` column is written on every save and returned on every read, but no code path filters or branches on it. Dimension mismatch is the only guard. This means a same-dimension model switch produces silently wrong cosine scores, while a different-dimension switch produces silently skipped entries.
147
+
148
+ 4. **Store creation is implicit.** The first `memory_remember` to a new store creates it. There is no explicit "create store" operation.
149
+
150
+ 5. **Default recall scope is repo + global.** The tool layer hardcodes `[ctx.defaultStore, "global"]` when no `store` argument is provided. This is intentional — cross-project knowledge in the global store is always searched alongside project-specific memories.
151
+
152
+ 6. **The write-time similarity warning never blocks the save.** `findSimilar` returns a warning, `remember` proceeds regardless. The warning asks the agent to reconcile after the fact.
153
+
154
+ 7. **`search()` records no usage. `recall()` stamps telemetry. This is deliberate.** The recall nudge (plugin checking relevance) must not inflate the "used recently" signal. Only agent-initiated recall counts as actual usage.
@@ -0,0 +1,228 @@
1
+ # Multi-Host Support
2
+
3
+ Thatch runs as both an opencode plugin and an MCP server for Claude Code and
4
+ Cursor. Three integration paths share a common core---the tool definitions in
5
+ `src/tool-defs.ts`---but differ in how they deliver system prompts, prefix tool
6
+ names, and handle session lifecycle events.
7
+
8
+ ## What it does
9
+
10
+ Thatch supports three host agents from a single codebase:
11
+
12
+ - **opencode plugin**---in-process, direct event hooks, warm embedding model,
13
+ `thatch_` tool prefix
14
+ - **Claude Code MCP**---stdio MCP server, external CLI hooks,
15
+ `mcp__thatch__` tool prefix, system prompt in CLAUDE.md
16
+ - **Cursor MCP**---same stdio MCP server, `--json` hook output format,
17
+ `mcp__thatch__` tool prefix, system prompt in AGENTS.md
18
+
19
+ All three paths expose the same 18 tool definitions from `src/tool-defs.ts`.
20
+ The tools are host-agnostic: each takes args and a `CoreContext`
21
+ (`{ db, model, defaultStore }`) and returns a string. The host-specific
22
+ wrapping---prefixing, transport, session hooks---lives outside the core.
23
+
24
+ ## The three paths
25
+
26
+ ### opencode plugin (`src/index.ts`)
27
+
28
+ Runs inside opencode's Bun runtime as a loaded plugin. No setup command is
29
+ needed---opencode discovers the plugin from `opencode.json` and loads it at
30
+ startup.
31
+
32
+ **System prompt**: injected at runtime via the
33
+ `experimental.chat.system.transform` hook. The hook calls `systemPrompt(repo)`
34
+ and pushes the result into `output.system` every turn. No files are written.
35
+ The repo name is baked in at runtime, so the prompt always reflects the current
36
+ worktree.
37
+
38
+ **Tools**: registered through opencode's `tool()` wrapper in `src/tools.ts`,
39
+ which adds the `thatch_` prefix. The agent sees `thatch_memory_remember`,
40
+ `thatch_memory_recall`, etc.
41
+
42
+ **Skills**: installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init.
43
+ Both the shared skill array and the opencode-only skill array are installed.
44
+ See [../skills.md](../skills.md) for the skill system.
45
+
46
+ **Capabilities unique to this path**:
47
+
48
+ - Full access to plugin hooks: system prompt injection, session events, tool
49
+ buffering, compaction context
50
+ - TUI toast notifications---best-effort, silently ignored in headless mode
51
+ - Direct extraction via child sessions (see [session-lifecycle.md](session-lifecycle.md))
52
+ - Compaction recovery (see [compaction-recovery.md](compaction-recovery.md))
53
+ - Background sub-agents (experimental)---the code-review coordinator dispatches
54
+ specialist sub-agents through opencode's agent infrastructure
55
+
56
+ ### Claude Code MCP server (`src/mcp.ts`)
57
+
58
+ Runs as a stdio JSON-RPC 2.0 process spawned by Claude Code. The MCP server
59
+ exposes tools via `tools/list` and `tools/call`. It is long-lived for the
60
+ session duration, keeping the embedding model warm in memory.
61
+
62
+ **System prompt**: static text appended to `CLAUDE.md` by
63
+ `thatch setup --claude`. The text is written between marker constants
64
+ (`THATCH_MARKER` / `THATCH_END_MARKER`) using the `appendBlock` helper in
65
+ `src/setup.ts`. The block is idempotent---re-running setup replaces the content
66
+ between the markers rather than duplicating it. Claude Code loads `CLAUDE.md`
67
+ at session start.
68
+
69
+ **Tools**: the MCP server exposes bare names (`memory_remember`,
70
+ `memory_recall`). Claude Code's MCP client applies the `mcp__thatch__` prefix,
71
+ so the agent sees `mcp__thatch__memory_remember`. Thatch itself does not apply
72
+ this prefix.
73
+
74
+ **Skills**: installed to `$CLAUDE_CONFIG_DIR/skills/`---shared skills only.
75
+ The code-review coordinator skill is opencode-only because it requires
76
+ sub-agent dispatch, which Claude Code does not support.
77
+
78
+ **Session behavior**: driven by external CLI hook processes (`bin/thatch`).
79
+ The hooks are registered in `.claude/settings.json` during setup. Each hook
80
+ spawns a short-lived `thatch` process that communicates with the long-lived MCP
81
+ server via the sideband socket (see [sideband.md](sideband.md)) for warm-model
82
+ access. This avoids loading the ~34 MB embedding model in every one-shot hook
83
+ process.
84
+
85
+ **Setup**: `thatch setup --claude` writes `.mcp.json` (server config),
86
+ `CLAUDE.md` (instructions block), `.claude/settings.json` (hooks), and
87
+ installs skills. See [setup.md](setup.md) for the setup system.
88
+
89
+ ### Cursor MCP server (same `src/mcp.ts`)
90
+
91
+ The same stdio MCP server as Claude Code. The differences are in hook format,
92
+ system prompt file, and tool batching granularity.
93
+
94
+ **System prompt**: static text appended to `AGENTS.md` by
95
+ `thatch setup --cursor`, using the same `appendBlock` helper and marker pair.
96
+ Cursor loads `AGENTS.md` at session start.
97
+
98
+ **Tools**: same bare names over MCP, same `mcp__thatch__` prefix applied by
99
+ Cursor's MCP client.
100
+
101
+ **Skills**: installed to `$CURSOR_CONFIG_DIR/skills/`---shared skills only.
102
+
103
+ **Session behavior**: driven by Cursor hooks in flat `hooks.json` format
104
+ (`.cursor/hooks.json`). Hook output uses the `--json` flag, producing
105
+ `{ additional_context: "..." }` objects that Cursor injects into the agent's
106
+ context. The `postToolUse` hook fires per-tool---there is no batch equivalent
107
+ of Claude Code's `PostToolBatch`. Tool buffering uses `thatch buffer-tool` on
108
+ each individual tool call. Cursor uses `conversation_id` instead of
109
+ `session_id` for session tracking.
110
+
111
+ **Setup**: `thatch setup --cursor` writes `.cursor/mcp.json`, `AGENTS.md`,
112
+ `.cursor/hooks.json`, and installs skills.
113
+
114
+ ## System prompt delivery
115
+
116
+ Three mechanisms, one per host. The prompt functions all live in
117
+ `src/prompts.ts`.
118
+
119
+ 1. **opencode**: the `experimental.chat.system.transform` hook calls
120
+ `systemPrompt(repo)` and pushes the result into `output.system` at runtime.
121
+ No files are written. The prompt is generated fresh every turn.
122
+
123
+ 2. **Claude Code**: `claudeInstructions()` produces the text that
124
+ `thatch setup --claude` appends to `CLAUDE.md` between
125
+ `THATCH_MARKER` / `THATCH_END_MARKER`. The file is loaded once at session
126
+ start by Claude Code.
127
+
128
+ 3. **Cursor**: `cursorInstructions()` produces the text that
129
+ `thatch setup --cursor` appends to `AGENTS.md` between the same markers.
130
+ Loaded once at session start by Cursor.
131
+
132
+ The three prompt functions are near-identical. They differ only in host name
133
+ string, config file reference (`OPENCODE.md` / `CLAUDE.md` / `AGENTS.md`), and
134
+ tool name prefix in the tool list line. Their shared prose sections---"When to
135
+ Write", "What NOT to Store", "Before Responding", "Stores", "Skills"---are
136
+ verbatim copies with these token substitutions.
137
+
138
+ There is no single-source-of-truth template that generates all three. Each
139
+ variant is an independent string constant in `src/prompts.ts`. Editing shared
140
+ prose in one variant requires mirroring the edit in the other two, or the
141
+ variants silently drift.
142
+
143
+ The tool list line in all three prompts must be kept in sync with
144
+ `TOOL_DEFS` in `src/tool-defs.ts`. The historical failure mode: a tool is added
145
+ to `TOOL_DEFS` but only two of the three prompt functions are updated, creating
146
+ an asymmetry a reviewer can catch by diffing the tool lines.
147
+
148
+ ## Tool name prefixing
149
+
150
+ The 18 tool definitions in `src/tool-defs.ts` use bare names:
151
+ `memory_remember`, `memory_recall`, `memory_list`, etc. Each host applies its
152
+ own prefix.
153
+
154
+ | Host | Prefix | Applied by | Example |
155
+ |------|--------|------------|---------|
156
+ | opencode | `thatch_` | `tool()` wrapper in `src/tools.ts` | `thatch_memory_remember` |
157
+ | Claude Code | `mcp__thatch__` | Claude Code's MCP client | `mcp__thatch__memory_remember` |
158
+ | Cursor | `mcp__thatch__` | Cursor's MCP client | `mcp__thatch__memory_remember` |
159
+
160
+ The MCP server (`src/mcp.ts`) always exposes bare names. The
161
+ `mcp__thatch__` prefix is a convention of the MCP client, not of thatch.
162
+ The opencode prefix is applied in-process by the `tool()` wrapper.
163
+
164
+ ## Feature availability differences
165
+
166
+ Not all features are available on all hosts. The opencode plugin has direct
167
+ access to session events, child sessions, and compaction hooks. The MCP hosts
168
+ rely on external hook processes and the sideband socket for equivalent
169
+ behavior, with some features having no MCP counterpart.
170
+
171
+ | Feature | opencode | Claude Code | Cursor |
172
+ |---------|----------|-------------|--------|
173
+ | Direct extraction (child sessions) | Yes | No | No |
174
+ | Compaction recovery | Yes | No | No |
175
+ | TUI toast notifications | Yes | No | No |
176
+ | Sideband socket | No (in-process) | Yes | Yes |
177
+ | code-review coordinator skill | Yes | No | No |
178
+ | Background sub-agents | Yes (experimental) | No | No |
179
+ | Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
180
+
181
+ For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
182
+
183
+ ## Interactions with other features
184
+
185
+ All 18 tools are shared across all hosts via the single source of truth in
186
+ `src/tool-defs.ts`. Individual tool behavior is documented in
187
+ [memory-store.md](memory-store.md), [extraction.md](extraction.md),
188
+ [prediction-engine.md](prediction-engine.md),
189
+ [behavior-engine.md](behavior-engine.md), and
190
+ [deduplication.md](deduplication.md).
191
+
192
+ The **nudge pipeline** (see [nudge-pipeline.md](nudge-pipeline.md)) runs
193
+ in-process for opencode. For MCP hosts, it runs via the sideband socket---hook
194
+ processes query the long-lived MCP server for memory matches, predictions, and
195
+ behavior nudges through a Unix domain socket. See [sideband.md](sideband.md)
196
+ for the socket protocol.
197
+
198
+ The **extraction pipeline** (see [extraction.md](extraction.md)) uses an
199
+ in-memory ring buffer for opencode. For MCP hosts, it uses a file-backed JSONL
200
+ queue that hook processes append to and the MCP server drains.
201
+
202
+ The **setup system** (see [setup.md](setup.md)) handles Claude Code and Cursor
203
+ installation. opencode auto-installs skills and injects the system prompt at
204
+ plugin init---no setup command is needed.
205
+
206
+ **Skills** (see [../skills.md](../skills.md)) install both the shared and
207
+ opencode-only arrays for opencode. MCP hosts receive shared skills only, since
208
+ the opencode-only skills require sub-agent dispatch or in-process hooks.
209
+
210
+ ## Source files
211
+
212
+ | File | Role |
213
+ |------|------|
214
+ | `src/index.ts` | opencode plugin entry---hooks, system prompt injection, session events |
215
+ | `src/mcp.ts` | MCP server (shared by Claude Code and Cursor)---stdio JSON-RPC, tool dispatch |
216
+ | `src/setup.ts` | Setup installer for Claude Code and Cursor---markers, hooks, skills |
217
+ | `src/prompts.ts` | All three system prompt variants---`systemPrompt()`, `claudeInstructions()`, `cursorInstructions()` |
218
+ | `src/tools.ts` | Thin opencode tool wrappers---imports tool-defs, adds `thatch_` prefix via `tool()` |
219
+ | `src/tool-defs.ts` | Single source of truth for all 18 tool definitions---name, description, zod schema, execute |
220
+ | `bin/thatch` | CLI hook commands for MCP hosts---`reminder`, `buffer-tool`, `flush-tools`, `setup` |
221
+
222
+ ## Key invariants
223
+
224
+ - All three hosts share the same 18 tool definitions from `src/tool-defs.ts`. Adding a tool requires updating `TOOL_DEFS` and all three prompt functions.
225
+ - The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
226
+ - The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
227
+ - The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts`. The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
228
+ - opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts) have no MCP counterpart.
@@ -0,0 +1,282 @@
1
+ # Per-Message Nudge Pipeline
2
+
3
+ Runs on every user message. Four priority tiers share one embedding
4
+ computation. The pipeline injects *synthetic parts*---text the model sees in
5
+ the conversation but the user does not see in the TUI. This is the inverse of
6
+ toast notifications, which are TUI-visible but model-invisible.
7
+
8
+ The nudge pipeline is thatch's core innovation. It is not a passive
9
+ store-and-retrieve system. It is a nudge layer that changes agent behavior by
10
+ injecting context at the right moment. See [extraction.md](extraction.md) for
11
+ the extraction nudge, [prediction-engine.md](prediction-engine.md) for the
12
+ prediction auto-fire, and [behavior-engine.md](behavior-engine.md) for the
13
+ behavior auto-fire.
14
+
15
+ ## What it does
16
+
17
+ On every user message, thatch runs through four priority tiers:
18
+
19
+ 1. **Extraction nudge**---if tool interactions are buffered, tell the agent to
20
+ extract memories.
21
+ 2. **Recall nudge**---semantically search stored memories for context
22
+ relevant to the user's prompt.
23
+ 3. **Prediction nudge**---surface user decision-model predictions relevant to
24
+ the current situation.
25
+ 4. **Behavior nudge**---surface LLM self-discipline rules relevant to the
26
+ current situation.
27
+
28
+ Tiers 2--4 fire independently and share one embedding computation. Tier 1
29
+ returns early, skipping tiers 2--4.
30
+
31
+ ## How it works
32
+
33
+ Two host paths deliver the same four tiers through different mechanisms. The
34
+ opencode path runs in-process with direct event hooks. The MCP path runs via
35
+ external CLI hook processes that communicate with the long-lived MCP server
36
+ through the sideband socket.
37
+
38
+ ### opencode path (`src/index.ts`, `chat.message` hook)
39
+
40
+ #### Compaction guard
41
+
42
+ If the session is in the `compacting` set, thatch checks whether the incoming
43
+ message is a compaction summary. If it is, all nudges are suppressed---tools
44
+ are blocked during summary generation, so a nudge that says "call
45
+ `thatch_memory_recall`" would trigger a blocked-tool error. If the message is
46
+ not a compaction summary, compaction has already failed (the auto-continue
47
+ never fired). Thatch clears the stale `compacting` flag and proceeds normally.
48
+ See [compaction-recovery.md](compaction-recovery.md).
49
+
50
+ #### Tier 1---Extraction nudge (fallback path)
51
+
52
+ Fires only when direct extraction was never triggered or threw (the session is
53
+ not in the `extracting` set) **and** the in-memory buffer has pending
54
+ interactions.
55
+
56
+ - Injects `extractionNudge(count, missed, "thatch_memory_remember", sessionID)`
57
+ as a synthetic text part.
58
+ - Increments the `missedNudges` counter for escalation: polite at 0, insistent
59
+ at 2, ALL-CAPS shouting at 3+.
60
+ - Returns early. Tiers 2--4 are skipped.
61
+
62
+ See [extraction.md](extraction.md) for the full extraction pipeline, including
63
+ the accept/complete/requeue lifecycle and the `extracting` set.
64
+
65
+ #### Tier 2---Recall nudge
66
+
67
+ - Extracts user prompt text from non-synthetic text parts. Skips if the text
68
+ is shorter than `MIN_PROMPT_LEN` (10 characters, hardcoded). Trivially short
69
+ prompts like "yes" or "ok" match too broadly to be useful.
70
+ - Embeds the prompt with the warm in-process model: `model.queryEmbed(text)`.
71
+ - Searches memories via `db.search([repo, "global"], embedding, { limit: 5 })`.
72
+ This uses `db.search`, not `db.recall`, to avoid inflating recall telemetry.
73
+ The agent has not actually read the memories yet---the nudge only checks
74
+ whether they relate to the prompt.
75
+ - Filters results to `_score >= RECALL_THRESHOLD` (0.55, env:
76
+ `THATCH_RECALL_THRESHOLD`).
77
+ - If matches survive: injects `recallNudge(matches)` as a synthetic text part
78
+ and fires a toast notification via `client.tui.showToast`.
79
+ - Wrapped in its own try/catch. A recall failure does not block prediction or
80
+ behavior tiers.
81
+
82
+ #### Tier 3---Prediction auto-fire
83
+
84
+ - Reuses the same embedding from tier 2. No extra model call.
85
+ - Scores prediction matchers via
86
+ `db.scorePredictionNudge([repo, "global"], embedding, PREDICTION_THRESHOLD)`.
87
+ This is a mechanical query: cosine-match against matchers, then score linked
88
+ predictions by Bayesian confidence.
89
+ - Threshold: 0.60 (env: `THATCH_PREDICTION_THRESHOLD`). Higher than recall
90
+ because surfacing a preference nudge is more disruptive---the agent may act
91
+ on it or surface it to the user.
92
+ - If matches: injects `predictionNudge(items)` as a synthetic text part and
93
+ fires a toast.
94
+ - Independent try/catch.
95
+
96
+ See [prediction-engine.md](prediction-engine.md) for the confidence model,
97
+ matcher/prediction/edge data model, and the ambiguity-surfacing behavior.
98
+
99
+ #### Tier 4---Behavior auto-fire
100
+
101
+ - Reuses the same embedding. One more cosine scan, this time against
102
+ `behavior_matchers`.
103
+ - Scores via
104
+ `db.scoreBehaviorNudge([repo, "global"], embedding, BEHAVIOR_THRESHOLD)`.
105
+ - Threshold: 0.60 (env: `THATCH_BEHAVIOR_THRESHOLD`).
106
+ - If matches: injects `behaviorNudge(items)` as a synthetic text part and
107
+ fires a toast.
108
+ - Independent try/catch.
109
+
110
+ See [behavior-engine.md](behavior-engine.md) for the ham/spam feedback loop
111
+ and the self-discipline rule data model.
112
+
113
+ #### Tier independence
114
+
115
+ Tiers 2, 3, and 4 all fire independently. Any subset may inject. All share
116
+ one embedding computation. The embedding is computed once, after the
117
+ extraction tier returns early, and is reused across three cosine scans against
118
+ different tables.
119
+
120
+ ### MCP path (`bin/thatch`, `flush-tools` subcommand)
121
+
122
+ The `flush-tools` subcommand fires on `UserPromptSubmit` (Claude Code) or
123
+ `beforeSubmitPrompt` (Cursor). It reads JSON from stdin:
124
+
125
+ ```json
126
+ {
127
+ "session_id": "...",
128
+ "conversation_id": "...",
129
+ "prompt": "...",
130
+ "message": "...",
131
+ "text": "...",
132
+ "user_input": "...",
133
+ "input": "..."
134
+ }
135
+ ```
136
+
137
+ Session ID is `session_id ?? conversation_id`. Prompt text is the first
138
+ present field from `prompt`, `message`, `text`, `user_input`, `input`.
139
+
140
+ Three priority tiers, first match wins:
141
+
142
+ #### Tier 1---Extraction nudge
143
+
144
+ - `peekQueue(sessionID)` peeks the file-backed JSONL queue without draining
145
+ it. The queue persists until the agent calls `memory_remember`, which
146
+ triggers `consumeQueue`.
147
+ - If interactions are pending: increments the missed count, prints
148
+ `extractionNudge(count, missed, "mcp__thatch__memory_remember", sessionID)`.
149
+ - Breaks. Tiers 2--3 are skipped.
150
+
151
+ See [extraction.md](extraction.md) for the file-backed queue and the
152
+ `buffer-batch` subcommand that populates it.
153
+
154
+ #### Tier 2---Recall + prediction + behavior via sideband
155
+
156
+ - Extracts prompt text. Skips if shorter than 10 characters.
157
+ - Connects to the sideband socket and fetches all three in parallel:
158
+ `sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`.
159
+ - Same thresholds as opencode: 0.55 recall, 0.60 prediction, 0.60 behavior.
160
+ - Concatenates any non-empty results: `claudeRecallNudge`,
161
+ `predictionNudge`, `behaviorNudge`.
162
+ - Prints to stdout. Breaks.
163
+
164
+ The sideband socket lets short-lived hook processes use the warm embedding
165
+ model in the long-lived MCP server, avoiding a cold ~34 MB model load on every
166
+ prompt. See [sideband.md](sideband.md) for the socket protocol.
167
+
168
+ #### Tier 3---Static write nudge
169
+
170
+ Fallback for conversational turns with no tool use and no memory matches.
171
+ Prints `claudeWriteNudge()`:
172
+
173
+ > After responding, check: did you learn new project knowledge, user
174
+ > preferences, or corrections worth persisting? If so, save to thatch.
175
+
176
+ On any error, falls back to the static write nudge. If that import also fails,
177
+ silent failure---the hook never blocks the user's prompt over an unrelated
178
+ error.
179
+
180
+ ### `flush-predictions` subcommand
181
+
182
+ Standalone prediction fire---queries the sideband for scored predictions
183
+ only. Not installed as a hook. Exists for testing and for hosts that want
184
+ prediction-only output. `flush-tools` already fires predictions in tier 2.
185
+
186
+ ### Output format
187
+
188
+ - **Claude Code**: plain text to stdout. Claude Code feeds hook stdout into
189
+ the agent's context.
190
+ - **Cursor**: `--json` flag wraps output as `{ additional_context: "..." }`.
191
+ Cursor injects `additional_context` into the session.
192
+
193
+ ## Thresholds
194
+
195
+ All thresholds are env-overridable.
196
+
197
+ | Threshold | Default | Env var |
198
+ |-----------|---------|---------|
199
+ | Recall nudge | 0.55 | `THATCH_RECALL_THRESHOLD` |
200
+ | Prediction auto-fire | 0.60 | `THATCH_PREDICTION_THRESHOLD` |
201
+ | Behavior auto-fire | 0.60 | `THATCH_BEHAVIOR_THRESHOLD` |
202
+ | Min prompt length | 10 chars | hardcoded (`MIN_PROMPT_LEN`) |
203
+
204
+ The recall threshold (0.55) is lower than `findDuplicates`'s 0.85 because
205
+ "relates to" is a weaker signal than "duplicate." The prediction and behavior
206
+ thresholds (0.60) are higher than recall because surfacing a preference or
207
+ self-discipline rule is more disruptive than surfacing a memory---the agent
208
+ may act on it or interrupt the user.
209
+
210
+ ## Toast notifications (opencode only)
211
+
212
+ `client.tui.showToast` is best-effort. If the TUI is not connected (headless
213
+ mode), the call is silently ignored. Toasts are model-invisible: they go to
214
+ the user only, not into the conversation history.
215
+
216
+ | Trigger | Message | Variant | Duration |
217
+ |---------|---------|---------|----------|
218
+ | Recall matches | recalled N memories | info | 3s |
219
+ | Prediction matches | N predictions surfaced | info | 3s |
220
+ | Behavior matches | N behaviors surfaced | info | 3s |
221
+ | Extraction child goes idle | new: N, updated: M, deleted: K | success | 4s |
222
+
223
+ The extraction metrics toast fires only when memories were actually written.
224
+ No-save runs produce no toast to avoid notification fatigue.
225
+
226
+ ## Synthetic nudge parts (opencode only)
227
+
228
+ Nudges are injected via `client.session.prompt` with `noReply: true` and
229
+ `synthetic: true`. The `synthetic` flag is a TUI-visibility marker, not a
230
+ model-routing flag. The model sees the nudge text in the conversation. The
231
+ user does not see it in the TUI transcript. This asymmetry is what makes the
232
+ nudge-injection pattern work: the model can act on the nudge without
233
+ cluttering the user's view.
234
+
235
+ Do not confuse `synthetic` with `ignored`. A part marked `ignored` is dropped
236
+ from the LLM-bound message. A part marked `synthetic` is only hidden from the
237
+ TUI display. The opencode framework's history serializer filters on
238
+ `!part.ignored`, not `!part.synthetic`.
239
+
240
+ ## Interactions with other features
241
+
242
+ - **Extraction pipeline** ([extraction.md](extraction.md)): tier 1 is the
243
+ extraction nudge. The `extracting` set suppresses tier 1 when direct
244
+ extraction is active.
245
+ - **Memory store** ([memory-store.md](memory-store.md)): tier 2 searches
246
+ stored memories via `db.search()`. This path records no telemetry---the
247
+ agent has not read the memories yet.
248
+ - **Prediction engine** ([prediction-engine.md](prediction-engine.md)): tier
249
+ 3 scores prediction matchers by cosine, then scores linked predictions by
250
+ Bayesian confidence.
251
+ - **Behavior engine** ([behavior-engine.md](behavior-engine.md)): tier 4
252
+ scores behavior matchers by cosine, then scores linked behaviors by
253
+ ham/spam confidence.
254
+ - **Sideband IPC** ([sideband.md](sideband.md)): MCP hosts use the sideband
255
+ socket for warm-model access. The hook process sends the prompt text; the
256
+ MCP server embeds it and runs the cosine scans.
257
+ - **Compaction recovery** ([compaction-recovery.md](compaction-recovery.md)):
258
+ the nudge pipeline is suppressed during compaction. The compaction guard
259
+ clears stale flags when compaction fails.
260
+ - **Session lifecycle** ([session-lifecycle.md](session-lifecycle.md)):
261
+ direct extraction (`triggerExtraction`) suppresses tier 1 by adding the
262
+ session to the `extracting` set.
263
+
264
+ ## Source files
265
+
266
+ | File | Role |
267
+ |------|------|
268
+ | `src/index.ts` | opencode: `chat.message` hook (all 4 tiers), compaction guard, extraction fallback |
269
+ | `bin/thatch` | MCP: `flush-tools` and `flush-predictions` subcommands |
270
+ | `src/sideband.ts` | MCP: sideband client helpers (`sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`) |
271
+ | `src/prompts.ts` | All nudge formatting functions (`recallNudge`, `claudeRecallNudge`, `predictionNudge`, `behaviorNudge`, `extractionNudge`, `claudeWriteNudge`) |
272
+
273
+ ## Key invariants
274
+
275
+ - Tier 1 (extraction) returns early. Tiers 2-4 are skipped when tier 1 fires.
276
+ - Tiers 2-4 share one embedding computation. One embed, three cosine scans against different tables.
277
+ - Each tier is wrapped in its own try/catch. A failure in one tier does not block the others.
278
+ - The recall nudge uses `db.search()`, not `db.recall()`, to avoid inflating telemetry. The agent has not read the memories yet.
279
+ - The recall threshold (0.55) is lower than prediction/behavior (0.60) because "relates to" is weaker than "should surface to user."
280
+ - Nudges are suppressed during compaction. Tools are blocked during summary generation, so a nudge saying "call `memory_recall`" would trigger a blocked-tool error.
281
+ - `synthetic` means TUI-invisible, not model-invisible. The model sees the nudge text; the user does not. Do not confuse with `ignored`, which drops the part from the LLM-bound message.
282
+ - MCP host hooks must be silent on success. Only `flush-tools` prints. Any stdout from other hooks delays the agent loop.