@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.
- package/README.md +24 -10
- package/artifacts/skills/thatch-code-review.md +9 -7
- 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/release +21 -0
- package/bin/thatch +17 -5
- package/docs/dev/README.md +71 -18
- 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 +6 -4
- 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 +55 -9
- package/src/mcp.ts +30 -1
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +135 -188
- 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 +172 -25
- package/src/vector-math.ts +30 -0
- 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).
|