@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,253 @@
1
+ # Prediction Engine (User Decision Model)
2
+
3
+ A statistical model of the user's decision-making preferences. Captures "when
4
+ X, the user tends to Y." The agent queries the model when facing judgment
5
+ calls and updates it based on user feedback.
6
+
7
+ ## What it does
8
+
9
+ - Four tools: `prediction_query`, `prediction_update`, `prediction_list`,
10
+ `prediction_delete`. All defined in `src/tool-defs.ts`.
11
+ - Auto-fires at every user message: embeds the prompt, cosine-matches against
12
+ matchers, injects scored predictions as a synthetic nudge part.
13
+ - Bayesian confidence model — a graded score that is reinforced or weakened
14
+ over time, not binary like memory.
15
+ - Matcher-prediction edges: many-to-many with weights. The same matcher can
16
+ produce different predictions in different contexts.
17
+ - Ambiguity surfacing: strong predictions are followed silently; ambiguous or
18
+ competing predictions are surfaced to the user.
19
+ - No wall-clock decay: confidence moves only when tested (user feedback);
20
+ untested predictions stay at the prior.
21
+
22
+ ## How it works
23
+
24
+ ### Data model: matchers, predictions, edges, provenance
25
+
26
+ Four tables in SQLite, mirroring the behavior engine's shape:
27
+
28
+ - **Matchers** — context patterns (situation descriptions). Embedded,
29
+ cosine-matched at `chat.message`. Capture "when" — describes the situation.
30
+ - **Predictions** — user preference statements with graded confidence.
31
+ Capture "what" — the user's tendency.
32
+ - **Edges** — weighted matcher-to-prediction links. Many-to-many. The same
33
+ matcher can produce different predictions in different contexts (the weight
34
+ encodes context-dependency). The same prediction can be reached by multiple
35
+ matchers.
36
+ - **Provenance** — audit trail of every signal applied to a prediction
37
+ (`confirm` / `disconfirm` / `soft` / `create`) with detail and timestamp.
38
+
39
+ ### Confidence model (Bayesian posterior)
40
+
41
+ ```text
42
+ confidence = (confirm_count + K * P0) / (confirm_count + disconfirm_count + K)
43
+ ```
44
+
45
+ - K = 5 (shrinkage parameter)
46
+ - P0 = 0.5 (prior)
47
+ - Soft signal weight: 0.25 — a soft disconfirm counts as one-quarter of a
48
+ full disconfirm.
49
+ - `effective_confidence = confidence`. The shrinkage handles thin evidence: a
50
+ single confirm is plausible, not reliable.
51
+ - No wall-clock decay. Confidence is relevance-gated: being tested
52
+ (encountered + user feedback) is what moves it. Untested predictions stay at
53
+ P0.
54
+
55
+ ### Scoring at query time
56
+
57
+ ```text
58
+ score = cosine(user_text, matcher.embedding) × edge.weight × prediction.effective_confidence
59
+ ```
60
+
61
+ Returns all predictions from matching matchers so the LLM can read conflicts.
62
+ The LLM is the conflict detector. Prompt instruction: "if competing
63
+ predictions offer conflicting guidance, surface to the user."
64
+
65
+ ### Query (`prediction_query` tool)
66
+
67
+ - Embeds the context string.
68
+ - Finds the top 5 matchers by cosine.
69
+ - Filters to cosine >= 0.60 (`PREDICTION_QUERY_THRESHOLD`, matching the
70
+ auto-fire threshold).
71
+ - Follows edges to predictions, scores them.
72
+ - Returns one line per prediction: `[conf, N tests] When {matcher}: {verb}
73
+ {statement}`.
74
+ - Verb reflects evidence count: "you may prefer" (0 evidence) vs "you tend to"
75
+ (has evidence).
76
+ - Read-only. Embeds the context.
77
+
78
+ ### Update (`prediction_update` tool)
79
+
80
+ Parameters: `matcher` (situation description), `prediction` (preference
81
+ statement), `signal` (`confirm` / `disconfirm` / `soft` / `create`),
82
+ `rationale` (why).
83
+
84
+ Runs in a single DB transaction:
85
+
86
+ 1. **Matcher dedup** — finds an existing matcher above cosine 0.85, else
87
+ creates one.
88
+ 2. **Prediction dedup** — store-wide search for a near-identical prediction
89
+ (cosine >= 0.85). If found, links this matcher to it via an edge rather than
90
+ creating a duplicate row.
91
+ 3. **New prediction with a non-create signal** — applies the signal
92
+ immediately so the first confirm/disconfirm is not lost.
93
+ 4. **`create` on an existing prediction** — links the edge and records
94
+ provenance but does NOT adjust confidence. `create` is confidence-neutral.
95
+ 5. **`confirm` / `disconfirm` / `soft`** — maps to `adjustConfidence` and
96
+ records provenance.
97
+
98
+ Returns a status line with final confidence and confirm/disconfirm counts.
99
+ Embeds both the matcher and prediction text.
100
+
101
+ ### Auto-fire (opencode: `chat.message` hook)
102
+
103
+ - Reuses the prompt embedding already computed for the recall nudge — no extra
104
+ model call.
105
+ - Calls `db.scorePredictionNudge([repo, "global"], embedding,
106
+ PREDICTION_THRESHOLD)`.
107
+ - Threshold: 0.60 (env: `THATCH_PREDICTION_THRESHOLD`). Higher than recall
108
+ (0.55) because surfacing a preference nudge is more disruptive.
109
+ - Injects `predictionNudge(items)` as a separate synthetic text part with a
110
+ `[thatch] User decision model` header.
111
+ - 0-evidence predictions use "you may prefer"; predictions with evidence use
112
+ "you tend to".
113
+ - Independent try/catch — a prediction failure does not block recall or
114
+ behavior.
115
+ - Toast: `[thatch] N predictions surfaced`.
116
+
117
+ ### Auto-fire (MCP: sideband)
118
+
119
+ - `flush-tools` fires the prediction query via the sideband socket's
120
+ `predictions` method, in parallel with recall and behavior.
121
+ - The same `scorePredictionNudge` entry point prevents scoring drift between
122
+ host paths.
123
+ - Sideband failure returns `null`; the caller skips the prediction nudge
124
+ gracefully.
125
+
126
+ ### System prompt injection
127
+
128
+ The auto-fire block looks like:
129
+
130
+ ```text
131
+ ## User decision model
132
+ - [0.72 conf, 4 tests] When {matcher}: you tend to {prediction}
133
+ - [0.61 conf, 2 tests] When {matcher2}: you may prefer {prediction2}
134
+ ```
135
+
136
+ Prompt instructions: follow strong predictions silently, surface weak or
137
+ competing predictions to the user, update the model when the user responds.
138
+
139
+ ### Ambiguity surfacing
140
+
141
+ The killer feature. Three regimes:
142
+
143
+ - **Strong prediction** (high confidence + evidence) — the agent follows
144
+ silently, does not interrupt.
145
+ - **No prediction** — the agent asks the user (same as today).
146
+ - **Ambiguous or competing predictions** — the agent surfaces naturally:
147
+ "I think you usually prefer X, but I'm not sure here — what do you want?"
148
+
149
+ When the user responds to a surfaced prediction, the agent calls
150
+ `prediction_update` to reinforce or weaken. The value is fewer interruptions,
151
+ not more.
152
+
153
+ ### Firewall principle
154
+
155
+ The model that uses predictions cannot grade its own predictions. The user's
156
+ feedback is the signal. The query is mechanical (a statistical model). The
157
+ formation and evaluation are agent-driven (via tools, guided by prompt
158
+ instructions). This separation prevents the agent from reinforcing its own
159
+ biases.
160
+
161
+ ### Relevance-gated decay
162
+
163
+ Confidence does not decay by wall-clock time. It moves only when tested — when
164
+ the user encounters a situation matching the matcher and provides feedback.
165
+ Untested predictions stay at P0 (0.5). A prediction that was strongly confirmed
166
+ a year ago but never tested since retains its confidence. The design assumes
167
+ that user preferences are stable, not time-sensitive.
168
+
169
+ ## Interactions with other features
170
+
171
+ - **Nudge pipeline** ([nudge-pipeline.md](nudge-pipeline.md)): prediction
172
+ auto-fire is tier 3. Shares the prompt embedding with recall (tier 2) and
173
+ behavior (tier 4).
174
+ - **Behavior engine** ([behavior-engine.md](behavior-engine.md)): same
175
+ four-table data model, same Bayesian confidence, same scoring engine.
176
+ Different semantics — predictions model user preferences; behaviors model
177
+ LLM discipline.
178
+ - **Scoring engine**: `src/scoring-engine.ts` provides the generic four-table
179
+ scoring base. `PredictionEngine` (`src/prediction.ts`) is a thin wrapper with
180
+ prediction-specific table names.
181
+ - **Memory store** ([memory-store.md](memory-store.md)): same embedding model
182
+ (BGE-small-en-v1.5), same SQLite DB, different tables.
183
+ - **Sideband IPC** ([sideband.md](sideband.md)): MCP hosts query predictions
184
+ via the sideband socket's `predictions` method.
185
+
186
+ ## Source files
187
+
188
+ | File | Role |
189
+ |------|------|
190
+ | `src/prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names |
191
+ | `src/scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence (shared base for prediction and behavior) |
192
+ | `src/db.ts` | Prediction tables, `scorePredictionNudge` |
193
+ | `src/tool-defs.ts` | Four prediction tools (`query`, `update`, `list`, `delete`) |
194
+ | `src/prompts.ts` | `predictionNudge` formatting, verb selection |
195
+ | `src/sideband.ts` | `predictions` method for the MCP path |
196
+
197
+ ## Database tables
198
+
199
+ ```sql
200
+ prediction_matchers(
201
+ id TEXT PRIMARY KEY,
202
+ store TEXT,
203
+ description TEXT,
204
+ embedding BLOB,
205
+ model TEXT,
206
+ created_at TEXT,
207
+ updated_at TEXT
208
+ )
209
+
210
+ predictions(
211
+ id TEXT PRIMARY KEY,
212
+ store TEXT,
213
+ statement TEXT,
214
+ rationale TEXT,
215
+ embedding BLOB,
216
+ model TEXT,
217
+ confidence REAL,
218
+ confirm_count REAL,
219
+ disconfirm_count REAL,
220
+ created_at TEXT,
221
+ updated_at TEXT
222
+ )
223
+
224
+ prediction_edges(
225
+ matcher_id INTEGER,
226
+ prediction_id INTEGER,
227
+ weight REAL,
228
+ PRIMARY KEY (matcher_id, prediction_id),
229
+ FOREIGN KEY (matcher_id) REFERENCES prediction_matchers(id) ON DELETE CASCADE,
230
+ FOREIGN KEY (prediction_id) REFERENCES predictions(id) ON DELETE CASCADE
231
+ )
232
+
233
+ prediction_provenance(
234
+ id TEXT PRIMARY KEY,
235
+ prediction_id INTEGER,
236
+ signal TEXT,
237
+ detail TEXT,
238
+ created_at TEXT,
239
+ FOREIGN KEY (prediction_id) REFERENCES predictions(id) ON DELETE CASCADE
240
+ )
241
+ ```
242
+
243
+ ## Key invariants
244
+
245
+ 1. **The firewall principle.** The model that uses predictions cannot grade
246
+ them. User feedback is the only signal.
247
+ 2. **No wall-clock decay.** Confidence is relevance-gated — being tested moves
248
+ it, not the passage of time.
249
+ 3. **Dedup at creation.** Cosine >= 0.85 for both matchers and predictions.
250
+ 4. **Same `scorePredictionNudge` entry point** in both host paths (opencode
251
+ `chat.message` hook and MCP sideband) prevents scoring drift.
252
+ 5. **Matcher text is embedded raw.** No header prepend — unlike
253
+ `memory_remember`, which prepends `# label\n\n` before embedding.
@@ -0,0 +1,205 @@
1
+ # QA System
2
+
3
+ Thatch ships 93 QA use cases that verify end-to-end behavior. Each use case is
4
+ a `bun:test` file that runs in an isolated environment — a temp-directory copy
5
+ of the repo, not the working tree itself. Use cases are split into
6
+ **automatable** (no LLM, fast) and **live** (spawns opencode, costs model
7
+ tokens).
8
+
9
+ ## What it does
10
+
11
+ - 93 use cases: 73 automatable + 20 live
12
+ - Automatable use cases run without an LLM — they test CLI behavior, hooks, DB
13
+ operations, and tool execution directly via bun assertions
14
+ - Live use cases spawn real opencode sessions — they test the full
15
+ nudge/extraction/skill pipeline with a real model
16
+ - `mise` tasks for running subsets: `qa` (all), `qa-auto` (fast), `qa-live`
17
+ (model), `qa-dry-run` (list only)
18
+ - Each use case follows a structured format: preconditions, steps, expected
19
+ results
20
+
21
+ ## How it works
22
+
23
+ ### Use case structure
24
+
25
+ Each use case file (`tests/qa/auto/uc-NNN-name.ts` or
26
+ `tests/qa/live/uc-NNN-name.ts`) imports `registerUseCase` and `UseCase` from
27
+ `../runner`, defines the scenario with `preconditions`, `steps`, and `expected`
28
+ as string arrays joined by newlines, then calls `registerUseCase(useCase)`.
29
+
30
+ Most use cases use the default `runViaOpencode` (no custom `run`). Automatable
31
+ use cases override `run` with direct CLI/bun assertions. Manual-only use cases
32
+ set `manualOnly: true`.
33
+
34
+ Individual use case files are `.ts` (not `.test.ts`) so bun does not discover
35
+ them as standalone test files. Each directory has an `index.test.ts` **barrel
36
+ file** that imports all use case modules. Bun's `--concurrent` flag
37
+ parallelizes tests within a single file, not across files. The barrel pattern
38
+ registers all use cases in one suite, enabling `--concurrent
39
+ --max-concurrency 5` to run 5 use cases at once (33s vs 88s sequential for the
40
+ auto suite).
41
+
42
+ ### Automatable use cases (73)
43
+
44
+ Run without an LLM. Test CLI behavior, hooks, DB operations, and tool
45
+ execution directly via bun assertions. These are fast and run in parallel.
46
+
47
+ | UC | Name | What it tests |
48
+ |----|------|---------------|
49
+ | UC-004 | cli-inspection | `stores`, `list`, `show`, `forget`, `search` subcommands |
50
+ | UC-005 | setup-install | setup installs correct files for Claude Code and Cursor |
51
+ | UC-008 | hygiene-heartbeat | hygiene report at session start (duplicates, stale, orphaned) |
52
+ | UC-009 | flush-tools-tiers | three priority tiers: extraction, recall, write nudge |
53
+ | UC-011 | write-time-similarity-warning | `memory_remember` warns about near-duplicates |
54
+ | UC-012 | model-migration | switching `THATCH_MODEL` makes old entries invisible |
55
+ | UC-014 | skill-install-drift | `installSkills` overwrites drifted content |
56
+ | UC-015 | env-override-matrix | `THATCH_RECALL_THRESHOLD`, `THATCH_PREDICTION_THRESHOLD`, `THATCH_BEHAVIOR_THRESHOLD` |
57
+ | UC-016 | concurrent-session-isolation | sessions don't interfere with each other |
58
+ | UC-017 | buffer-tool-vs-buffer-batch | Cursor per-tool vs Claude Code batch queueing |
59
+ | UC-018 | mcp-startup-setup-detection | `checkSetup` at MCP server startup |
60
+ | UC-021 | prediction-autofire | prediction nudge fires at `chat.message` |
61
+ | UC-022 | prediction-dedup | cosine >= 0.85 dedup for matchers and predictions |
62
+ | UC-023 | prediction-confidence-model | Bayesian posterior with confirm/disconfirm/soft |
63
+ | UC-024 | prediction-delete | semantic-match deletion with cascade |
64
+ | UC-025 | behavior-autofire | behavior nudge fires at `chat.message` |
65
+ | UC-026 | behavior-dedup | cosine >= 0.85 dedup for matchers and behaviors |
66
+ | UC-027 | behavior-confidence-model | ham/spam adjusts Bayesian confidence |
67
+ | UC-028 | behavior-delete | semantic-match deletion with cascade |
68
+
69
+ ### Live use cases (20)
70
+
71
+ Spawn real opencode sessions. Test the full pipeline with a real model. These
72
+ cost model tokens and have a 20-minute per-use-case timeout. All 20 are marked
73
+ `manualOnly: true` — the opencode binary is not available in CI, and three
74
+ (UC-006, UC-010, UC-013) are manual-only for fundamental reasons (Cursor hook
75
+ lifecycle, nested opencode session timeout, compaction trigger). Run them
76
+ locally with `mise run qa-live`.
77
+
78
+ | UC | Name | What it tests |
79
+ |----|------|---------------|
80
+ | UC-001 | memory-roundtrip | remember -> recall -> show -> forget |
81
+ | UC-002 | dedup-cycle | `find_duplicates` -> classify -> merge -> `mark_checked` |
82
+ | UC-003 | extraction-nudge | tool use -> buffer -> nudge -> `extraction_done` |
83
+ | UC-006 | cursor-hook-lifecycle | Cursor hooks: `sessionStart`, `postToolUse`, `beforeSubmitPrompt` |
84
+ | UC-007 | recall-nudge | prompt-aware recall at `chat.message` |
85
+ | UC-010 | prime | `thatch prime` runs the project-primer skill |
86
+ | UC-013 | compaction-context | compaction hook injects context |
87
+ | UC-019 | archived-memory-lifecycle | archive -> search excludes -> unarchive -> search includes |
88
+ | UC-020 | extraction-escalation | polite -> insistent -> ALL-CAPS over missed nudges |
89
+
90
+ ### mise tasks
91
+
92
+ | Task | Command | Purpose |
93
+ |------|---------|---------|
94
+ | `qa` | `bun test tests/qa/auto/index.test.ts --concurrent --max-concurrency 5 && bun test tests/qa/live/index.test.ts` | Run all QA (auto first, then live) |
95
+ | `qa-dry-run` | `QA_DRY_RUN=1 bun test tests/qa/auto/index.test.ts tests/qa/live/index.test.ts --concurrent --max-concurrency 5` | List use cases without spawning sessions |
96
+ | `qa-auto` | `bin/qa-run auto` | Run automatable only (fast, no LLM). Pass UC names as args to select a subset. |
97
+ | `qa-live` | `bin/qa-run live` | Run live only (spawns opencode, costs tokens). Pass UC names as args to select a subset. |
98
+
99
+ The `qa` task runs auto first via `&&`. Auto failures stop before live runs.
100
+ The `qa-live` task does not use `--concurrent` — live sessions are
101
+ resource-heavy and benefit from sequential execution.
102
+
103
+ ### Runner architecture
104
+
105
+ `tests/qa/runner.ts` provides the `registerUseCase` function and the `UseCase`
106
+ type. The runner index files (`tests/qa/auto/index.test.ts` and
107
+ `tests/qa/live/index.test.ts`) import all use case files in their directory,
108
+ which self-register via `registerUseCase`. The runner then executes each use
109
+ case.
110
+
111
+ The `QA_DRY_RUN=1` environment variable causes the runner to print use case
112
+ names and descriptions without executing them. This is useful for CI and for
113
+ reviewing coverage without spawning sessions.
114
+
115
+ ### Isolation model
116
+
117
+ The runner creates a **master copy** of the repo at `/tmp/thatch-qa/master/`
118
+ via `git archive HEAD` (tracked files only — no `.git`, no `node_modules`, no
119
+ untracked artifacts). `node_modules` is symlinked from the real repo to share
120
+ deps including the HF embedding model cache. opencode's own npm deps
121
+ (`~/.config/opencode/node_modules/`) are copied into the master so each use
122
+ case has them without a warm-up session.
123
+
124
+ Each use case gets a per-use-case copy of the master via `createFixture(name)`,
125
+ with isolated `HOME`, `XDG_CONFIG_HOME`, `THATCH_DB_PATH`, and
126
+ `THATCH_QUEUE_DIR` environment variables. The repo is read-only during QA —
127
+ sessions run inside `/tmp/thatch-qa/<uc-name>/` copies.
128
+
129
+ The `thatch-qa` skill (`.opencode/skills/thatch-qa/SKILL.md`) instructs the
130
+ LLM on how to execute a single use case in the isolated directory. The skill's
131
+ safety contract is intentionally fixed text — do not paraphrase it.
132
+
133
+ ### Execution modes
134
+
135
+ 1. **Live session** (default): no custom `run` function → `runViaOpencode`
136
+ spawns `opencode run --dir <ctx.dir> --model <MODEL> --auto` with the use
137
+ case content as prompt. Requires `VENICE_API_KEY`. Default model:
138
+ `venice/zai-org-glm-5-2` (override with `QA_MODEL` env var). Uses
139
+ `Bun.spawn()` (not `Bun.$`) so the process can be killed on timeout (590s
140
+ SIGTERM, SIGKILL in finally). The 20-minute per-test timeout is the
141
+ wall-clock budget for a complete LLM-driven agent session — do not reduce
142
+ it.
143
+ 2. **Automatable**: custom `run(ctx)` function with direct CLI/bun assertions.
144
+ No opencode session, no API key.
145
+ 3. **Manual-only**: `manualOnly: true` → test is skipped, prints `[MANUAL]`.
146
+
147
+ ### Relationship to the quality gate
148
+
149
+ `mise run check` uses a flat glob (`tests/*.test.ts`) that excludes
150
+ `tests/qa/`. QA tests spawn opencode sessions and cannot run in the normal
151
+ quality gate. The QA files are typechecked by `tsconfig.check.json` (which
152
+ includes all of `tests/`), just not executed during `check`. See
153
+ [cicd.md](cicd.md) for the full CI pipeline.
154
+
155
+ ## Interactions with other features
156
+
157
+ - **All features**: QA use cases verify end-to-end behavior of every feature
158
+ - [cicd.md](cicd.md): `mise run check` uses a flat glob (`tests/*.test.ts`)
159
+ that excludes `tests/qa/`; CI runs `bun test` which includes the QA barrels;
160
+ `mise run qa` runs the full QA suite separately
161
+ - [setup.md](setup.md): UC-005 and UC-018 test setup installation and detection
162
+ - [nudge-pipeline.md](nudge-pipeline.md): UC-007, UC-009, UC-021, UC-025 test
163
+ nudge tiers
164
+ - [extraction.md](extraction.md): UC-003, UC-020 test extraction nudge and
165
+ escalation
166
+ - [hygiene.md](hygiene.md): UC-008 tests the hygiene heartbeat
167
+ - [memory-store.md](memory-store.md): UC-001, UC-004, UC-011, UC-012, UC-019
168
+ test memory CRUD, search, warnings, migration, archival
169
+ - [deduplication.md](deduplication.md): UC-002 tests the dedup cycle
170
+ - [prediction-engine.md](prediction-engine.md): UC-021 through UC-024 test
171
+ auto-fire, dedup, confidence, delete
172
+ - [behavior-engine.md](behavior-engine.md): UC-025 through UC-028 test
173
+ auto-fire, dedup, confidence, delete
174
+ - [../skills.md](../skills.md): UC-014 tests skill drift detection
175
+ - [multi-host.md](multi-host.md): UC-006, UC-017 test Cursor hooks and
176
+ buffer-tool vs buffer-batch
177
+
178
+ ## Source files
179
+
180
+ | File | Role |
181
+ |------|------|
182
+ | `tests/qa/runner.ts` | `registerUseCase`, `UseCase` type, `ensureMaster`, `createFixture`, `runViaOpencode`, execution logic |
183
+ | `tests/qa/auto/index.test.ts` | Automatable use case barrel — imports all auto use case modules |
184
+ | `tests/qa/live/index.test.ts` | Live use case barrel — imports all live use case modules |
185
+ | `tests/qa/auto/uc-NNN-*.ts` | 73 automatable use case files |
186
+ | `tests/qa/live/uc-NNN-*.ts` | 20 live use case files |
187
+
188
+ No separate QA docs directory exists. QA use cases live in `tests/qa/` as executable tests, not docs.
189
+
190
+ ## Key invariants
191
+
192
+ 1. **Automatable use cases never spawn an LLM.** They test CLI, hooks, DB, and
193
+ tool execution directly.
194
+ 2. **Live use cases spawn real opencode sessions and cost model tokens.** All
195
+ 20 are marked `manualOnly` — the opencode binary is not available in CI.
196
+ 3. **`QA_DRY_RUN=1` lists use cases without executing.** Use this for CI and
197
+ coverage review.
198
+ 4. **Each use case follows the structured format**: preconditions, steps,
199
+ expected.
200
+ 5. **Use cases self-register via `registerUseCase`** — no central registry to
201
+ maintain. Add a new `.ts` file and import it from the barrel.
202
+ 6. **The repo is read-only during QA.** Sessions run inside
203
+ `/tmp/thatch-qa/<uc-name>/` copies created from `git archive HEAD`.
204
+ 7. **The 20-minute test timeout is the agent-session budget, not test
205
+ execution time.** Reducing it will cause live use cases to time out.
@@ -0,0 +1,110 @@
1
+ # Session Lifecycle Management (opencode)
2
+
3
+ opencode emits bus events for session lifecycle changes. Thatch subscribes to these events to manage the extraction pipeline, send session-start reminders, and handle child session cleanup.
4
+
5
+ ## What it does
6
+
7
+ - Session-start reminder + hygiene heartbeat for top-level sessions
8
+ - Child session tracking (`childToParent` map, `parentSnapshots`)
9
+ - Direct extraction: trigger child session when parent goes idle with pending buffer
10
+ - Extraction child lifecycle: created on parent idle, drains parent's snapshot on completion, deleted after
11
+ - Sub-agent lifecycle: task-dispatched sub-agents complete accepted entries but are not deleted
12
+ - Session error recovery: child errors requeue parent's accepted entries
13
+ - Session deletion recovery: child deletion requeues; parent deletion completes accepted entries
14
+
15
+ ## How it works
16
+
17
+ ### Event handler (`src/index.ts`, event hook)
18
+
19
+ Subscribes to all session bus events. Dispatches based on `event.type`.
20
+
21
+ ### session.created with parentID (child session)
22
+
23
+ 1. Record `childToParent.set(childID, parentID)`
24
+ 2. Snapshot the parent's current pending buffer: `parentSnapshots.set(childID, [...extraction.peek(parentID)])`
25
+ 3. Return early -- child sessions don't get the session-start reminder
26
+
27
+ This snapshot is used by `consumeSnapshot` for snapshot-aware drain: when the child writes memories, only the snapshot entries are removed from the parent's buffer, preserving interleaved-turn entries that were added after the snapshot was taken.
28
+
29
+ ### session.created without parentID (top-level session)
30
+
31
+ - Send session-start reminder via `client.session.prompt` with `noReply: true` and `synthetic: true`
32
+ - The reminder includes the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero
33
+ - See [hygiene.md](hygiene.md)
34
+
35
+ ### session.status idle -- extraction child
36
+
37
+ When a child created by `triggerExtraction` goes idle:
38
+
39
+ 1. Drain the parent's snapshot from pending buffer via `consumeSnapshot` (if still present -- a no-save run's entries need draining so they don't replay; a save run already drained them via `tool.execute.after`)
40
+ 2. Never drains the entire buffer -- interleaved-turn entries survive
41
+ 3. Fire a toast with extraction metrics (new/updated/deleted counts) -- only if memories were actually written
42
+ 4. `completeAccepted(parentID)`, reset `missedNudges`
43
+ 5. Clean up all maps: `extracting`, `childToParent`, `parentSnapshots`, `childMetrics`, `extractionChildren`
44
+ 6. `consume(childID)` -- drain child's own buffer
45
+ 7. Delete the child session via `client.session.delete`
46
+
47
+ ### session.status idle -- task-dispatched sub-agent
48
+
49
+ When a task-dispatched sub-agent (not created by `triggerExtraction`) goes idle:
50
+
51
+ - `completeAccepted(parentID)` -- complete the parent's accepted entries (from the nudge-path `extraction_done` accept)
52
+ - Reset `missedNudges`
53
+ - Does NOT drain the buffer or delete the session -- the task tool that dispatched the sub-agent needs to read its output
54
+
55
+ ### session.status idle -- parent session
56
+
57
+ When a parent session (no parentID) goes idle:
58
+
59
+ - If not compacting, not already extracting, and buffer has pending interactions: `triggerExtraction(sessionID)`
60
+ - On failure: log error, clear `extracting` flag (nudge path takes over as fallback on next chat.message)
61
+
62
+ ### session.error (child session)
63
+
64
+ - `requeueAccepted(parentID)` -- move parent's accepted entries back to pending (the extractor never processed them)
65
+ - Clean up all maps for the child
66
+
67
+ ### session.deleted -- child
68
+
69
+ - `requeueAccepted(parentID)` -- entries go back to pending (never processed)
70
+ - Clean up all maps for the child
71
+
72
+ ### session.deleted -- parent
73
+
74
+ - `completeAccepted(id)` -- a deleted parent takes its accepted entries with it
75
+ - Clear `extracting` for the parent
76
+
77
+ ### session.compacted
78
+
79
+ - Clear the `compacting` flag so chat.message nudges resume
80
+ - See [compaction-recovery.md](compaction-recovery.md)
81
+
82
+ ### Internal state maps
83
+
84
+ - **childToParent**: `Map<childID, parentID>` -- maps child sessions to their parents
85
+ - **parentSnapshots**: `Map<childID, Interaction[]>` -- snapshot of parent's buffer at child creation time
86
+ - **childMetrics**: `Map<childID, {new, updated, deleted}>` -- extraction metrics per child
87
+ - **extracting**: `Set<parentID>` -- parent IDs with an active direct-extraction child (suppresses nudge)
88
+ - **extractionChildren**: `Set<childID>` -- distinguishes extraction children from task-dispatched sub-agents
89
+ - **compacting**: `Set<sessionID>` -- sessions currently being compacted (suppresses nudges)
90
+ - **missedNudges**: `Map<sessionID, number>` -- extraction nudge escalation counter
91
+
92
+ ## Interactions with other features
93
+
94
+ - Extraction pipeline ([extraction.md](extraction.md)): direct extraction is triggered by `session.status idle`; child lifecycle managed here
95
+ - Nudge pipeline ([nudge-pipeline.md](nudge-pipeline.md)): `extracting` set suppresses tier 1; `compacting` set suppresses all tiers
96
+ - Hygiene ([hygiene.md](hygiene.md)): hygiene report runs at `session.created` for top-level sessions
97
+ - Compaction recovery ([compaction-recovery.md](compaction-recovery.md)): `session.compacted` event clears the compacting flag
98
+ - Memory store ([memory-store.md](memory-store.md)): child sessions write memories via `memory_remember`, which triggers drain via `tool.execute.after`
99
+
100
+ ## Source files
101
+
102
+ - `src/index.ts` -- event handler (all session events), `triggerExtraction`, `cleanupChild`, internal state maps
103
+
104
+ ## Key invariants
105
+
106
+ - Child sessions don't get the session-start reminder (early return in `session.created` with parentID).
107
+ - `consumeSnapshot` is snapshot-aware: removes only entries captured at dispatch time, preserving interleaved-turn entries.
108
+ - Extraction children are deleted after going idle; task-dispatched sub-agents are NOT deleted (task tool reads output).
109
+ - Child errors requeue the parent's accepted entries (never processed). Child deletion also requeues.
110
+ - Parent deletion completes accepted entries (takes them with it).