@jeffober/thatch 0.1.27 → 0.1.29
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 +24 -10
- package/artifacts/skills/thatch-code-review.md +9 -7
- package/artifacts/skills/thatch-fact-extractor.md +2 -2
- package/artifacts/skills/thatch-review-economy.md +97 -0
- package/artifacts/skills/thatch-review-followup.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +15 -5
- package/bin/qa-run +38 -0
- package/bin/thatch +13 -7
- package/docs/dev/README.md +76 -22
- package/docs/dev/features/README.md +33 -0
- package/docs/dev/features/behavior-engine.md +271 -0
- package/docs/dev/features/cicd.md +89 -0
- package/docs/dev/features/cli.md +81 -0
- package/docs/dev/features/compaction-recovery.md +48 -0
- package/docs/dev/features/database.md +244 -0
- package/docs/dev/features/deduplication.md +102 -0
- package/docs/dev/features/extraction.md +253 -0
- package/docs/dev/features/hygiene.md +81 -0
- package/docs/dev/features/memory-store.md +154 -0
- package/docs/dev/features/multi-host.md +228 -0
- package/docs/dev/features/nudge-pipeline.md +282 -0
- package/docs/dev/features/prediction-engine.md +253 -0
- package/docs/dev/features/qa-system.md +205 -0
- package/docs/dev/features/session-lifecycle.md +110 -0
- package/docs/dev/features/setup.md +199 -0
- package/docs/dev/features/sideband.md +104 -0
- package/docs/dev/gotchas.md +3 -3
- package/docs/dev/mcp-parity.md +7 -5
- package/docs/dev/setup-and-hooks.md +4 -4
- package/docs/dev/skills.md +10 -8
- package/docs/plans/README.md +2 -2
- package/docs/user/README.md +58 -20
- package/docs/user/behavior-engine.md +85 -0
- package/docs/user/cli.md +98 -0
- package/docs/user/code-review.md +73 -0
- package/docs/user/deduplication.md +70 -0
- package/docs/user/default-behaviors.md +75 -0
- package/docs/user/extraction.md +74 -0
- package/docs/user/hygiene.md +76 -0
- package/docs/user/memory.md +81 -0
- package/docs/user/prediction-engine.md +87 -0
- package/docs/user/setup.md +110 -0
- package/docs/user/skills.md +97 -0
- package/package.json +2 -2
- package/src/behavior.ts +120 -0
- package/src/db.ts +160 -417
- package/src/embeddings.ts +0 -41
- package/src/extraction.ts +2 -2
- package/src/git.ts +3 -1
- package/src/index.ts +66 -14
- package/src/mcp.ts +59 -2
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +155 -200
- package/src/scoring-engine.ts +374 -0
- package/src/seed-behaviors.ts +339 -0
- package/src/setup.ts +2 -6
- package/src/sideband.ts +36 -8
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +235 -9
- package/src/tools.ts +11 -1
- package/src/vector-math.ts +30 -0
- package/docs/qa/README.md +0 -175
|
@@ -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.
|