@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.
Files changed (62) hide show
  1. package/README.md +24 -10
  2. package/artifacts/skills/thatch-code-review.md +9 -7
  3. package/artifacts/skills/thatch-fact-extractor.md +2 -2
  4. package/artifacts/skills/thatch-review-economy.md +97 -0
  5. package/artifacts/skills/thatch-review-followup.md +1 -1
  6. package/artifacts/skills/thatch-review-synthesizer.md +15 -5
  7. package/bin/qa-run +38 -0
  8. package/bin/thatch +13 -7
  9. package/docs/dev/README.md +76 -22
  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 +7 -5
  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 +66 -14
  51. package/src/mcp.ts +59 -2
  52. package/src/prediction.ts +127 -0
  53. package/src/prompts.ts +155 -200
  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 +235 -9
  60. package/src/tools.ts +11 -1
  61. package/src/vector-math.ts +30 -0
  62. package/docs/qa/README.md +0 -175
@@ -24,9 +24,14 @@ Shared core
24
24
  ├── embeddings.ts → embedding model via transformers.js
25
25
  ├── git.ts → detect repo identity (store name)
26
26
  ├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
27
- ├── prompts.ts → system prompt, compaction, reminders, recall nudge, CLAUDE.md instructions
27
+ ├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, CLAUDE.md instructions
28
28
  ├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
29
- └── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
29
+ ├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
30
+ ├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
31
+ ├── prediction.ts → prediction engine wrapper (thin layer over ScoringEngine)
32
+ ├── behavior.ts → behavior engine wrapper (thin layer over ScoringEngine)
33
+ ├── seed-behaviors.ts → default behavior seeding on first run
34
+ └── vector-math.ts → blobToVector + cosineSimilarity helpers
30
35
 
31
36
  OpenCode plugin path
32
37
  ├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
@@ -44,7 +49,7 @@ MCP server path
44
49
  CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
45
50
 
46
51
  bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
47
- prime|buffer-batch|buffer-tool|flush-tools|setup
52
+ prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup
48
53
  ```
49
54
 
50
55
  ## Module responsibilities
@@ -55,16 +60,21 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
55
60
  | `tools.ts` | Thin opencode wrappers. Imports tool-defs, wraps each in opencode's `tool()` with a `thatch_` prefix. |
56
61
  | `mcp.ts` | Stdio JSON-RPC 2.0 server. Compiles zod schemas to JSON Schema via `z.toJSONSchema()` for `tools/list`. Validates args via `z.object().parse()` in `tools/call`. All logging to stderr (stdout is the transport). |
57
62
  | `index.ts` | OpenCode plugin entry. Wires DB, model, extraction; registers tools and hooks; installs skills. Internal state beyond the extraction pipeline: `extracting` set (parent IDs with an active direct-extraction child), `childMetrics` map (new/updated/deleted counts per child session), and `triggerExtraction(parentID)` (creates a child session via the SDK client and prompts it with the extraction payload). |
58
- | `setup.ts` | `thatch setup --claude` installer. Writes .mcp.json, appends to CLAUDE.md (idempotent), installs hooks in settings.json, installs skills. |
63
+ | `setup.ts` | `thatch setup --claude` / `--cursor` installer. Writes MCP config (`.mcp.json` / `.cursor/mcp.json`), appends to CLAUDE.md / AGENTS.md (idempotent), installs hooks in settings.json / hooks.json, installs skills. |
59
64
  | `hygiene.ts` | Hygiene report: pending dedup pairs, stale count, orphaned branch memories. Shared by the plugin's session-start hook and the CLI's `thatch reminder` command. |
60
65
  | `git.ts` | Parse `owner/repo` from git remote. Worktree-safe fallback chain. |
61
- | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. Prediction tables: matchers, predictions, edges, provenance. `scorePredictionNudge` is the shared auto-fire entry point for both host paths. |
66
+ | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. Prediction tables: matchers, predictions, edges, provenance. `scorePredictionNudge` is the shared auto-fire entry point for both host paths. Behavior tables: same four-table shape (matchers, behaviors, edges, provenance) with `scoreBehaviorNudge` as the shared entry point. |
62
67
  | `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
63
- | `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the extraction nudge carries. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `bin/thatch` imports `buildExtractionPayload`. `summarizeArgs` is used internally by `buildExtractionPayload`. |
68
+ | `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the `get_extraction_payload` tool returns. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `mcp.ts` imports `buildExtractionPayload` for the extraction payload provider. `summarizeArgs` is used internally by `buildExtractionPayload`. |
64
69
  | `extract-queue.ts` | File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of `extraction.ts`. |
65
- | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles two methods: `match` (recall nudge) and `predictions` (prediction auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
66
- | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
67
- | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (22 skills: fact-extractor, dedup-classifier, project-primer, 7 review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr — work on both opencode and Claude Code) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
70
+ | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
71
+ | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
72
+ | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (23 skills: fact-extractor, dedup-classifier, project-primer, 8 review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr — work on all three hosts) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
73
+ | `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
74
+ | `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
75
+ | `behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names. |
76
+ | `seed-behaviors.ts` | Default behavior seeding on first run. Populates starter self-discipline rules. |
77
+ | `vector-math.ts` | `blobToVector` and `cosineSimilarity` helpers used across the codebase. |
68
78
 
69
79
  ## Plugin hooks
70
80
 
@@ -76,7 +86,7 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
76
86
  | `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
77
87
  | `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
78
88
  | `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
79
- | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge + JSON payload — the buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire: `db.scorePredictionNudge` scores the prompt against prediction matchers and injects a separate `[thatch] User decision model` nudge if any matchers clear the 0.45 threshold. Recall and prediction nudges fire independently (separate try/catch blocks, separate synthetic parts). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
89
+ | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. The buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
80
90
  | `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status` (idle): if the session is a child, drains its snapshot, fires a toast with `childMetrics`, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls `triggerExtraction` to create a direct-extraction child. `session.error`: requeues the parent's buffer (child died without draining). `session.deleted`: cleans up `childToParent`, `parentSnapshots`, `childMetrics`, and `extracting`. `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
81
91
  | `dispose` | Closes the DB. |
82
92
 
@@ -100,12 +110,14 @@ Two of these hooks were dead for weeks because failures were invisible.
100
110
  5. **Store creation is implicit.** First `remember` to a new store creates it.
101
111
  6. **Default recall scope is repo + global.** The tool layer hardcodes this.
102
112
  7. **Skills are plugin-owned files.** Installed to
103
- `$XDG_CONFIG_HOME/opencode/skills` (opencode) or `~/.claude/skills/`
104
- (Claude Code) — never into the worktree; drifted content is overwritten
105
- on plugin init or re-running `thatch setup`. Skills are split into
106
- `SHARED_SKILLS` (work on both hosts) and `OPENCODE_ONLY_SKILLS` (require
107
- sub-agent support, not installed for Claude Code). The opencode plugin
108
- installs both arrays; `thatch setup --claude` installs only shared.
113
+ `$XDG_CONFIG_HOME/opencode/skills` (opencode), `~/.claude/skills/`
114
+ (Claude Code), or `~/.cursor/skills/` (Cursor) — never into the
115
+ worktree; drifted content is overwritten on plugin init or re-running
116
+ `thatch setup`. Skills are split into `SHARED_SKILLS` (work on all
117
+ three hosts) and `OPENCODE_ONLY_SKILLS` (require sub-agent support,
118
+ not installed for Claude Code or Cursor). The opencode plugin
119
+ installs both arrays; `thatch setup --claude` and `--cursor` install
120
+ only shared.
109
121
  8. **Tool definitions are the single source of truth.** `tool-defs.ts` defines
110
122
  each tool once (name, zod schema, execute function). The opencode plugin
111
123
  wraps them in `tool()` with a `thatch_` prefix; the MCP server wraps them
@@ -138,6 +150,18 @@ Two of these hooks were dead for weeks because failures were invisible.
138
150
  relevance-gated (being tested moves it, not being ignored). See the
139
151
  prediction DB tables, auto-fire in `chat.message`, and the sideband
140
152
  `predictions` method for the MCP path.
153
+ 12. **Behavior engine mirrors the prediction engine but is self-graded.** Same
154
+ four-table data model, same Bayesian confidence, same auto-fire pipeline.
155
+ The difference: predictions model what the USER wants (graded by user
156
+ feedback); behaviors model what the LLM should do (graded by the LLM's own
157
+ ham/spam relevance judgment via `behavior_feedback`). The firewall
158
+ principle from predictions (the model that uses predictions cannot grade
159
+ them) does not apply: the LLM grading its own behavioral rules is the
160
+ point, not a violation. The ham/spam is a relevance judgment ("does this
161
+ rule apply here?"), not a value judgment ("is this a good rule?"). An
162
+ anti-laziness guard in the prompt prevents the agent from codifying
163
+ shortcuts. See the behavior DB tables, auto-fire in `chat.message`, and
164
+ the sideband `behaviors` method for the MCP path.
141
165
 
142
166
  ## Data flow
143
167
 
@@ -186,12 +210,13 @@ extraction cycle
186
210
  → nudge path (fallback): if triggerExtraction throws (create or prompt
187
211
  fails), the `extracting` set clears and the next chat.message sees
188
212
  pending entries with no extracting flag → peeks the buffer and injects
189
- a nudge part with JSON payload; missed nudges escalate (polite →
213
+ a nudge part with session ID and fetch tool name; missed nudges escalate (polite →
190
214
  insistent → ALL-CAPS) via the missedNudges counter
191
215
  → drain: thatch_memory_remember (or thatch_extraction_done) clears the
192
216
  buffer and resets the missed-nudge counter
193
- → agent loads thatch-fact-extractor skill, saves facts via
194
- thatch_memory_remember
217
+ → agent dispatches a sub-agent that calls thatch_get_extraction_payload
218
+ to fetch the queued interactions, loads thatch-fact-extractor skill,
219
+ saves facts via thatch_memory_remember
195
220
  → MCP path (Claude Code/Cursor): unchanged — no SDK client, no child
196
221
  sessions; extract-queue.ts + flush-tools drives the nudge via hooks
197
222
 
@@ -206,7 +231,9 @@ toast notifications (opencode-only, TUI-rendered)
206
231
  → recall matches: `[thatch] recalled N memories` (info variant, 3s) —
207
232
  fires when chat.message matches stored memories
208
233
  → prediction matches: `[thatch] N predictions surfaced` (info variant,
209
- 3s) — fires when chat.message matches decision-model patterns
234
+ 3s) — fires when chat.message matches decision-model patterns
235
+ → behavior matches: `[thatch] N behaviors surfaced` (info variant,
236
+ 3s) — fires when chat.message matches codified behavior matchers
210
237
 
211
238
  prompt-aware recall nudge (both paths)
212
239
  → opencode: chat.message hook embeds prompt text with warm in-process model,
@@ -236,7 +263,7 @@ prediction cycle (agent-driven, statistical model)
236
263
  confidence is relevance-gated (being tested moves it, not being ignored).
237
264
  → auto-fire (opencode): chat.message reuses the prompt embedding already
238
265
  computed for the recall nudge. db.scorePredictionNudge([repo, global],
239
- embedding, 0.45) finds matchers above threshold, follows edges to
266
+ embedding, 0.60) finds matchers above threshold, follows edges to
240
267
  predictions, scores by cosine * weight * confidence, dedups by
241
268
  prediction_id, and returns top 5. Injects a separate synthetic part
242
269
  with a [thatch] User decision model block. 0-evidence predictions use
@@ -251,6 +278,29 @@ prediction cycle (agent-driven, statistical model)
251
278
  thatch_prediction_list inspects the model with provenance;
252
279
  thatch_prediction_delete removes bad predictions (cascade clears edges
253
280
  and provenance).
281
+
282
+ behavior cycle (agent-driven, self-graded)
283
+ → formation: agent calls thatch_behavior_codify(situation, behavior, rationale)
284
+ when it recognizes a situation it should react to in a specific, repeatable
285
+ way. The tool embeds the situation text, finds or creates a behavior matcher
286
+ (0.85 dedup), finds or creates a behavior (0.85 store-wide dedup), links
287
+ them via an edge. Confidence starts at p0 (0.5) with 0 evidence.
288
+ → auto-fire (opencode): chat.message reuses the prompt embedding. db.
289
+ scoreBehaviorNudge([repo, global], embedding, 0.60) finds behavior matchers
290
+ above threshold, follows edges to behaviors, scores by cosine * weight *
291
+ confidence, dedups by behavior_id, returns top 5. Injects a separate
292
+ synthetic part with a [thatch] Situational behaviors block. 0-evidence
293
+ behaviors use "consider"; behaviors with evidence use "do".
294
+ → auto-fire (Claude Code/Cursor): flush-tools fires the behavior query via
295
+ the sideband socket's `behaviors` method in parallel with recall and
296
+ predictions. Same scoreBehaviorNudge entry point prevents scoring drift.
297
+ → consumption: agent evaluates each surfaced behavior against the current
298
+ situation. If relevant (ham), follows it and calls behavior_feedback with
299
+ relevant: true (confirm). If not relevant (spam), calls behavior_feedback
300
+ with relevant: false (disconfirm). The feedback adjusts the Bayesian
301
+ confidence the same way prediction signals do. thatch_behavior_list
302
+ inspects with provenance; thatch_behavior_delete removes bad rules
303
+ (cascade clears edges and provenance).
254
304
  ```
255
305
 
256
306
  ## Database
@@ -265,7 +315,11 @@ prediction cycle (agent-driven, statistical model)
265
315
  `prediction_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
266
316
  `predictions(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
267
317
  `prediction_edges(matcher_id, prediction_id, weight REAL, PK(matcher_id, prediction_id), FK CASCADE)`,
268
- `prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE)`.
318
+ `prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE)`,
319
+ `behavior_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
320
+ `behaviors(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
321
+ `behavior_edges(matcher_id, behavior_id, weight REAL, PK(matcher_id, behavior_id), FK CASCADE)`,
322
+ `behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)`.
269
323
  - `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
270
324
  databases by an idempotent column migration at init (`PRAGMA table_info` +
271
325
  `ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =
@@ -0,0 +1,33 @@
1
+ # Feature Architecture
2
+
3
+ Per-feature architecture docs for thatch. Each doc covers what the feature
4
+ does, how it works, how it interacts with other features, and the source
5
+ files that implement it.
6
+
7
+ For the monolithic architecture overview, see
8
+ [../README.md](../README.md). For hook configuration and concrete artifact
9
+ paths, see [../setup-and-hooks.md](../setup-and-hooks.md). For feature
10
+ parity across hosts, see [../mcp-parity.md](../mcp-parity.md). For the skill
11
+ system, see [../skills.md](../skills.md). For non-obvious invariants, see
12
+ [../gotchas.md](../gotchas.md).
13
+
14
+ ## Docs
15
+
16
+ | Doc | Feature |
17
+ |-----|---------|
18
+ | [memory-store.md](memory-store.md) | Persistent memory store (CRUD, search, embeddings, stores, branches) |
19
+ | [multi-host.md](multi-host.md) | Three-host architecture (opencode, Claude Code, Cursor) |
20
+ | [extraction.md](extraction.md) | Extraction pipeline (buffering, child sessions, nudge escalation, file-backed queue) |
21
+ | [nudge-pipeline.md](nudge-pipeline.md) | Per-message nudge pipeline (extraction, recall, prediction, behavior tiers) |
22
+ | [prediction-engine.md](prediction-engine.md) | Prediction engine (user decision model, Bayesian confidence, auto-fire) |
23
+ | [behavior-engine.md](behavior-engine.md) | Behavior engine (LLM self-discipline rules, ham/spam, auto-fire) |
24
+ | [hygiene.md](hygiene.md) | Hygiene system (duplicate, stale, orphaned branch signals) |
25
+ | [deduplication.md](deduplication.md) | Deduplication system (find pairs, verdict lifecycle) |
26
+ | [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
27
+ | [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
28
+ | [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking) |
29
+ | [setup.md](setup.md) | Setup system (markers, checkSetup, auto-refresh, binary resolution) |
30
+ | [cli.md](cli.md) | CLI (subcommands, environment variables) |
31
+ | [database.md](database.md) | Database (schema, migrations, WAL, 11 tables) |
32
+ | [qa-system.md](qa-system.md) | QA system (93 use cases, automatable vs live, mise tasks) |
33
+ | [cicd.md](cicd.md) | CI/CD (CI pipeline, OIDC publishing, release helper) |
@@ -0,0 +1,271 @@
1
+ # Behavior Engine (LLM Self-Discipline Rules)
2
+
3
+ A statistical model of the LLM's own operational discipline. Captures "when
4
+ situation X arises, I should do Y." Mirrors the
5
+ [prediction engine](prediction-engine.md) architecture but serves a different
6
+ purpose: predictions model what the *user* wants; behaviors model what the
7
+ *LLM* should do.
8
+
9
+ ## What it does
10
+
11
+ - Four tools: `behavior_codify`, `behavior_feedback`, `behavior_list`,
12
+ `behavior_delete`. All defined in `src/tool-defs.ts`.
13
+ - Auto-fires at every user message: embeds the prompt, cosine-matches against
14
+ behavior matchers, injects scored behaviors as a synthetic nudge part.
15
+ - Ham/spam feedback (ham = relevant, spam = not relevant) adjusts Bayesian
16
+ confidence, training the classifier over time.
17
+ - Anti-laziness guard in prompt instructions: prevents codifying rules that
18
+ make the agent less thorough. Rules should encode discipline, not shortcuts.
19
+ - Default behavior seeding at startup: seeds several default behaviors into
20
+ the global store (session wrap-up, work finalization, research discipline,
21
+ day-turnover checks, dead-end capture, git archaeology). Idempotent across
22
+ releases via version stamps.
23
+
24
+ ## How it works
25
+
26
+ ### Data model: matchers, behaviors, edges, provenance
27
+
28
+ Four tables in SQLite — same shape as the prediction engine:
29
+
30
+ - **Behavior matchers** — situation descriptions (when to apply the rule).
31
+ Embedded, cosine-matched at `chat.message`. Capture "when."
32
+ - **Behaviors** — self-discipline rule statements (what to do). Capture
33
+ "what." Each carries a rationale explaining why the rule is worth
34
+ persisting.
35
+ - **Edges** — weighted matcher-to-behavior links. Many-to-many. The same
36
+ matcher can link to multiple behaviors; the same behavior can be reached
37
+ by multiple matchers.
38
+ - **Provenance** — audit trail of every signal applied to a behavior
39
+ (`codify`, `confirm` / ham, `disconfirm` / spam) with detail and timestamp.
40
+
41
+ ### Confidence model (Bayesian posterior)
42
+
43
+ Same formula and constants as the prediction engine — the constants live in
44
+ `src/scoring-engine.ts` and are shared by both engines:
45
+
46
+ ```text
47
+ confidence = (confirm_count + K * P0) / (confirm_count + disconfirm_count + K)
48
+ ```
49
+
50
+ - K = 5 (shrinkage parameter)
51
+ - P0 = 0.5 (prior)
52
+ - Ham feedback = confirm signal. Spam feedback = disconfirm signal.
53
+ - Same shrinkage and relevance-gated properties as predictions: a single ham
54
+ is plausible, not reliable. Untested behaviors stay at P0.
55
+
56
+ ### How it differs from the prediction engine
57
+
58
+ | Aspect | Prediction engine | Behavior engine |
59
+ |--------|-------------------|-----------------|
60
+ | Models | What the USER wants | What the LLM should do |
61
+ | Graded by | User feedback (confirm/disconfirm/soft) | LLM's own ham/spam relevance judgment |
62
+ | Firewall principle | Applies — the model that uses predictions cannot grade them | Does NOT apply — the LLM grading its own behaviors IS the point |
63
+ | Signal types | confirm, disconfirm, soft, create | confirm (ham), disconfirm (spam), codify |
64
+ | Default seeding | No | Yes (multiple default behaviors at startup) |
65
+
66
+ The firewall principle from predictions does not apply here. The LLM grading
67
+ its own behavioral rules is the point, not a violation. Ham/spam is a
68
+ relevance judgment ("does this rule apply here?"), not a value judgment ("is
69
+ this a good rule?"). The agent evaluates each surfaced behavior against the
70
+ current situation and records whether it was relevant.
71
+
72
+ ### Codify (`behavior_codify` tool)
73
+
74
+ Parameters: `situation` (triggering context), `behavior` (the rule),
75
+ `rationale` (why worth persisting).
76
+
77
+ Runs in a single DB transaction:
78
+
79
+ 1. **Matcher dedup** — finds an existing behavior matcher above cosine 0.85,
80
+ else creates one.
81
+ 2. **Behavior dedup** — store-wide search for a near-identical behavior
82
+ (cosine >= 0.85). If found, links this matcher to it via an edge rather
83
+ than creating a duplicate row.
84
+ 3. **New behavior** — creates behavior + edge + provenance entry tagged
85
+ `codify`.
86
+ 4. Returns `[codified]` for a new behavior or `[linked]` for an existing one
87
+ (with confidence and counts).
88
+
89
+ Confidence starts at P0 (0.5) with 0 evidence.
90
+
91
+ ### Feedback (`behavior_feedback` tool)
92
+
93
+ Parameters: `behavior` (statement, semantic match at cosine >= 0.85),
94
+ `relevant` (boolean), `context` (situation description for provenance).
95
+
96
+ - `relevant: true` = ham = confirm signal.
97
+ - `relevant: false` = spam = disconfirm signal.
98
+ - Adjusts confidence and records provenance tagged `ham:` or `spam:`.
99
+ - Returns updated confidence and confirm/disconfirm counts.
100
+ - Returns a "not found" message if no behavior matches.
101
+
102
+ ### Auto-fire (opencode: `chat.message` hook)
103
+
104
+ - Reuses the prompt embedding already computed for the recall nudge — same
105
+ embedding serves recall (tier 2), prediction (tier 3), and behavior (tier
106
+ 4). One embed, three lookups.
107
+ - Calls `db.scoreBehaviorNudge([repo, "global"], embedding,
108
+ BEHAVIOR_THRESHOLD)`.
109
+ - Threshold: 0.60 (env: `THATCH_BEHAVIOR_THRESHOLD`). Same as prediction —
110
+ surfacing a discipline rule is as disruptive as surfacing a preference.
111
+ - Injects `behaviorNudge(items)` as a separate synthetic text part with a
112
+ `[thatch] Situational behaviors` header.
113
+ - 0-evidence behaviors use "consider"; behaviors with evidence use "do".
114
+ - Independent try/catch — a behavior failure does not block recall or
115
+ prediction.
116
+ - Toast: `[thatch] N behaviors surfaced`.
117
+
118
+ ### Auto-fire (MCP: sideband)
119
+
120
+ - `flush-tools` fires the behavior query via the sideband socket's
121
+ `behaviors` method, in parallel with recall and predictions.
122
+ - The same `scoreBehaviorNudge` entry point prevents scoring drift between
123
+ host paths.
124
+ - Sideband failure returns `null`; the caller skips the behavior nudge
125
+ gracefully.
126
+
127
+ ### Default behavior seeding (`src/seed-behaviors.ts`)
128
+
129
+ Runs at startup in both the opencode plugin (`src/index.ts`) and the MCP
130
+ server (`src/mcp.ts`), after DB and model initialization.
131
+
132
+ Seeds multiple default behaviors into the global store — session wrap-up
133
+ (check for uncommitted changes, untracked files, stale artifacts), work
134
+ finalization (check before commits/merges), research discipline (investigate
135
+ before proposing changes), day-turnover checks (rebase staleness, new review
136
+ comments), dead-end capture (save red herrings as memories), and git
137
+ archaeology (understand history before debugging or planning).
138
+
139
+ Idempotent across releases via version stamps:
140
+
141
+ 1. For each default behavior, embeds the situation and behavior text.
142
+ 2. Checks whether a behavior matcher with cosine >= 0.85 already exists.
143
+ 3. If found, checks the linked behavior's rationale for a version stamp
144
+ (`seed-version:X.Y.Z`).
145
+ 4. If the stamp matches the current package version, skips — already seeded.
146
+ 5. If the stamp differs (older release seeded it) or is absent (manually
147
+ codified), deletes the old behavior and re-creates it with the current
148
+ version stamp. The matcher and edge cascade-delete with the behavior.
149
+ 6. Creates: behavior matcher (situation embedding), behavior (statement +
150
+ rationale embedding, with version stamp), edge (weight 1.0), provenance
151
+ entry tagged `codify`.
152
+
153
+ ### Anti-laziness guard
154
+
155
+ Prompt instructions in all host variants explicitly prevent the agent from
156
+ codifying rules that make it less thorough. The guard text — "Do not codify
157
+ rules that make you lazier or less thorough. Rules should encode discipline,
158
+ not shortcuts." — appears in `src/prompts.ts` in each host's system prompt.
159
+
160
+ ### Consumption
161
+
162
+ When surfaced behaviors appear in the nudge, the agent evaluates each against
163
+ the current situation:
164
+
165
+ - If relevant (ham): follows the rule and calls `behavior_feedback` with
166
+ `relevant: true` (confirm).
167
+ - If not relevant (spam): calls `behavior_feedback` with `relevant: false`
168
+ (disconfirm).
169
+
170
+ This trains the classifier so future nudges are more accurate. Behaviors
171
+ that consistently score as spam will drop in confidence and eventually fall
172
+ below the auto-fire threshold.
173
+
174
+ ## Interactions with other features
175
+
176
+ - **Nudge pipeline** ([nudge-pipeline.md](nudge-pipeline.md)): behavior
177
+ auto-fire is tier 4. Shares the prompt embedding with recall (tier 2) and
178
+ prediction (tier 3).
179
+ - **Prediction engine** ([prediction-engine.md](prediction-engine.md)): same
180
+ four-table data model, same Bayesian confidence, same scoring engine.
181
+ Different semantics and different grading — user feedback vs. self
182
+ feedback.
183
+ - **Scoring engine**: `src/scoring-engine.ts` provides the generic
184
+ four-table scoring base. `BehaviorEngine` (`src/behavior.ts`) is a thin
185
+ wrapper with behavior-specific table names.
186
+ - **Memory store** ([memory-store.md](memory-store.md)): same embedding
187
+ model (BGE-small-en-v1.5), same SQLite DB, different tables.
188
+ - **Sideband IPC** ([sideband.md](sideband.md)): MCP hosts query behaviors
189
+ via the sideband socket's `behaviors` method.
190
+
191
+ ## Source files
192
+
193
+ | File | Role |
194
+ |------|------|
195
+ | `src/behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names |
196
+ | `src/scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence (shared base for prediction and behavior) |
197
+ | `src/seed-behaviors.ts` | Default behavior seeding at startup — version-stamped, idempotent across releases |
198
+ | `src/db.ts` | Behavior tables, `scoreBehaviorNudge` delegation |
199
+ | `src/tool-defs.ts` | Four behavior tools (`codify`, `feedback`, `list`, `delete`) |
200
+ | `src/prompts.ts` | `behaviorNudge` formatting, verb selection ("consider" vs "do"), anti-laziness guard |
201
+ | `src/sideband.ts` | `behaviors` method for the MCP path |
202
+
203
+ ## Database tables
204
+
205
+ ```sql
206
+ behavior_matchers(
207
+ id TEXT PRIMARY KEY,
208
+ store TEXT NOT NULL REFERENCES stores(name),
209
+ description TEXT NOT NULL,
210
+ embedding BLOB,
211
+ model TEXT,
212
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
213
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
214
+ )
215
+
216
+ behaviors(
217
+ id TEXT PRIMARY KEY,
218
+ store TEXT NOT NULL REFERENCES stores(name),
219
+ statement TEXT NOT NULL,
220
+ rationale TEXT,
221
+ embedding BLOB,
222
+ model TEXT,
223
+ confidence REAL NOT NULL DEFAULT 0.5,
224
+ confirm_count REAL NOT NULL DEFAULT 0,
225
+ disconfirm_count REAL NOT NULL DEFAULT 0,
226
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
227
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
228
+ )
229
+
230
+ behavior_edges(
231
+ matcher_id TEXT NOT NULL,
232
+ behavior_id TEXT NOT NULL,
233
+ weight REAL NOT NULL DEFAULT 1.0,
234
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
235
+ PRIMARY KEY (matcher_id, behavior_id),
236
+ FOREIGN KEY (matcher_id) REFERENCES behavior_matchers(id) ON DELETE CASCADE,
237
+ FOREIGN KEY (behavior_id) REFERENCES behaviors(id) ON DELETE CASCADE
238
+ )
239
+
240
+ behavior_provenance(
241
+ id TEXT PRIMARY KEY,
242
+ behavior_id TEXT NOT NULL,
243
+ signal TEXT NOT NULL,
244
+ detail TEXT,
245
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
246
+ FOREIGN KEY (behavior_id) REFERENCES behaviors(id) ON DELETE CASCADE
247
+ )
248
+ ```
249
+
250
+ ## Key invariants
251
+
252
+ 1. **The firewall principle does NOT apply.** The LLM grading its own
253
+ behaviors is the point. Ham/spam is a relevance judgment, not a value
254
+ judgment.
255
+
256
+ 2. **Anti-laziness guard prevents codifying shortcuts.** Rules must encode
257
+ discipline, not shortcuts. The guard text is in the system prompt for all
258
+ host variants.
259
+
260
+ 3. **Same `scoreBehaviorNudge` entry point** in both host paths (opencode
261
+ `chat.message` hook and MCP sideband) prevents scoring drift.
262
+
263
+ 4. **Default behavior seeding is idempotent.** Version stamps in the
264
+ rationale text allow re-seeding when content changes across releases
265
+ without duplicating rows.
266
+
267
+ 5. **One embedding computation serves recall, prediction, and behavior.**
268
+ The prompt is embedded once at `chat.message`; three separate cosine
269
+ scans run against different tables.
270
+
271
+ 6. **Dedup at creation.** Cosine >= 0.85 for both matchers and behaviors.
@@ -0,0 +1,89 @@
1
+ # CI/CD
2
+
3
+ Thatch uses GitHub Actions for CI and publishing, plus a local release helper script. The quality gate is `mise run check` (typecheck + tests + markdownlint).
4
+
5
+ ## CI pipeline (.github/workflows/ci.yml)
6
+
7
+ Runs on every push/PR to main. Three jobs:
8
+ 1. **Typecheck** — `bunx tsc -p tsconfig.check.json` (includes test files that the build's `tsconfig.json` excludes)
9
+ 2. **Tests** — `bun test` (bun:test framework, no network, `:memory:` DB or temp dirs)
10
+ 3. **Markdownlint** — `bunx markdownlint-cli2` on `README.md` and `docs/**/*.md` (excluding `docs/plans/**`)
11
+
12
+ The CI gate is the never-merge-broken guard before a release. All three must pass.
13
+
14
+ ## Publish pipeline (.github/workflows/publish.yml)
15
+
16
+ Triggered by pushing a `v*` tag. Publishes to npm via OIDC trusted publishing — no stored npm token.
17
+
18
+ Key details:
19
+ - `id-token: write` permission lets npm authenticate through GitHub's OIDC exchange
20
+ - Prerequisite: a Trusted Publisher configured on npmjs.com for `@jeffober/thatch` pointing at the `sysread/thatch` repo
21
+ - npm >= 11.5.1 required (OIDC exchange needs it). The workflow installs setup-node@v4 (node 24) and `npm install -g npm@11`
22
+ - npm 12 shipped a sigstore bug that broke OIDC publishing; the pin prevents it
23
+ - The workflow deliberately sets NO `registry-url`, which would write a token-expecting `.npmrc` that preempts the OIDC exchange
24
+
25
+ ## Release helper (bin/release)
26
+
27
+ A bash script (`set -euo pipefail`) that bumps, checks, commits, tags, and pushes.
28
+
29
+ Args: exactly one required: `patch | minor | major`. Anything else prints an error and exits 1.
30
+
31
+ Steps:
32
+ 1. Read current version from `package.json` via `node -p`
33
+ 2. Compute next version (major resets minor/patch to 0; minor resets patch to 0)
34
+ 3. Print current/bump/next
35
+ 4. Pre-flight: if git tag `v$curr` exists, check npm registry for `@jeffober/thatch@$curr`. If npm returns E404 (tagged but not published = prior release failed), error with recovery instructions and exit 1. If npm check fails for non-404 reason, warn and proceed.
36
+ 5. Run `mise run check` (typecheck + tests + markdownlint)
37
+ 6. Bump `package.json` via `npm version $v --no-git-tag-version`
38
+ 7. Prompt `commit, tag, and push v$next? [y/N]`. On no: revert `package.json` and exit 0. On yes: git add, commit -m "v$next", tag `v$next`, push, push --tags.
39
+ 8. Print `done - CI will publish v$next to npm`
40
+
41
+ ## mise tasks
42
+
43
+ | Task | Purpose |
44
+ |------|---------|
45
+ | `test` | Run the test suite (`bun test`) |
46
+ | `test-watch` | Run tests in watch mode |
47
+ | `coverage` | Run tests with coverage report |
48
+ | `typecheck` | Typecheck src + tests (`bunx tsc -p tsconfig.check.json`) |
49
+ | `lint-md` | Lint markdown docs (`bunx markdownlint-cli2`) |
50
+ | `check` | The CI gate: typecheck + tests + markdownlint |
51
+ | `release` | Bump version, commit, tag, push (args: `patch\|minor\|major`) |
52
+ | `qa` | Run QA use cases (auto first, then live) |
53
+ | `qa-dry-run` | List QA use cases without spawning opencode sessions |
54
+ | `qa-auto` | Run only automatable QA use cases (no LLM, fast) |
55
+ | `qa-live` | Run only live-session QA use cases (spawns opencode, costs model tokens) |
56
+ | `cli` | Run the thatch CLI (list, show, search, forget) |
57
+
58
+ `mise.toml` pins Bun 1.3.14 and sets `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`.
59
+
60
+ ## Development tooling
61
+
62
+ - **Bun 1.3.14** (pinned via mise)
63
+ - **TypeScript** with `tsconfig.check.json` for typecheck (includes test files; build's `tsconfig.json` excludes them)
64
+ - **bun:test** framework (no network, `:memory:` DB or temp dirs, mock embeddings)
65
+ - **markdownlint-cli2** with custom config (`.markdownlint-cli2.jsonc`): disables MD013 (line length), MD032 (blanks around lists), MD036 (emphasis as heading), MD060 (table alignment). Keeps MD040 (code-block language), MD047 (trailing newline), heading hierarchy.
66
+ - **Zod** for tool schema validation and JSON Schema generation (`src/tool-defs.ts`)
67
+
68
+ ## Interactions with other features
69
+
70
+ - QA system ([qa-system.md](qa-system.md)): `mise run qa` runs the QA use cases
71
+ - All features: `mise run check` is the gate that validates everything before release
72
+
73
+ ## Source files
74
+
75
+ - `.github/workflows/ci.yml` — CI pipeline
76
+ - `.github/workflows/publish.yml` — publish pipeline (OIDC)
77
+ - `bin/release` — release helper script
78
+ - `mise.toml` — task definitions, tool pins, env vars
79
+ - `.markdownlint-cli2.jsonc` — markdownlint config
80
+ - `tsconfig.check.json` — typecheck config (includes tests)
81
+
82
+ ## Key invariants
83
+
84
+ - `mise run check` (typecheck + tests + markdownlint) must pass before any release. The release helper enforces this in step 5.
85
+ - OIDC publishing uses no stored npm token. The workflow sets NO `registry-url` because it would write a token-expecting `.npmrc` that preempts the OIDC exchange.
86
+ - npm is pinned to 11.x. npm 12 shipped a sigstore bug that broke OIDC publishing.
87
+ - The release helper checks for tagged-but-not-published state (tag exists, npm returns E404) and refuses to proceed with recovery instructions.
88
+ - CI runs on push/PR to main. Publish runs only on `v*` tags. The release helper bridges them: tag + push triggers publish.
89
+ - `tsconfig.check.json` includes test files for typecheck. The build's `tsconfig.json` excludes them.