@jeffober/thatch 0.1.25 → 0.1.27

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 (70) hide show
  1. package/README.md +7 -7
  2. package/artifacts/skills/common.md +1 -1
  3. package/artifacts/skills/thatch-change-walkthrough.md +2 -2
  4. package/artifacts/skills/thatch-code-archaeology.md +252 -0
  5. package/artifacts/skills/thatch-code-review.md +4 -4
  6. package/artifacts/skills/thatch-code-walkthrough.md +2 -2
  7. package/artifacts/skills/thatch-coding-workflow.md +79 -0
  8. package/artifacts/skills/{pr-description.md → thatch-pr-description.md} +1 -1
  9. package/artifacts/skills/thatch-review-context.md +9 -0
  10. package/artifacts/skills/thatch-review-followup.md +281 -0
  11. package/artifacts/skills/thatch-review-pedantic.md +16 -1
  12. package/artifacts/skills/thatch-review-response.md +365 -0
  13. package/artifacts/skills/thatch-review-synthesizer.md +92 -5
  14. package/artifacts/skills/{split-overlarge-pr.md → thatch-split-overlarge-pr.md} +1 -1
  15. package/artifacts/skills/{ticket-description.md → thatch-ticket-description.md} +3 -3
  16. package/bin/release +3 -3
  17. package/bin/thatch +8 -8
  18. package/docs/dev/README.md +1 -1
  19. package/docs/dev/skills.md +34 -9
  20. package/docs/plans/README.md +9 -6
  21. package/docs/plans/intuition-drives.md +147 -0
  22. package/docs/plans/nomenclater.md +89 -0
  23. package/docs/plans/prediction-consolidation.md +361 -0
  24. package/docs/qa/README.md +20 -3
  25. package/docs/user/README.md +33 -5
  26. package/package.json +1 -1
  27. package/src/db.ts +6 -6
  28. package/src/embeddings.ts +2 -2
  29. package/src/extract-queue.ts +4 -4
  30. package/src/extraction.ts +4 -4
  31. package/src/git.ts +1 -1
  32. package/src/hygiene.ts +1 -1
  33. package/src/index.ts +28 -28
  34. package/src/mcp.ts +5 -5
  35. package/src/prompts.ts +71 -65
  36. package/src/setup.ts +19 -19
  37. package/src/sideband.ts +3 -3
  38. package/src/skills.ts +105 -6
  39. package/src/tool-defs.ts +11 -11
  40. package/src/tools.ts +1 -1
  41. package/artifacts/skills/thatch-workflow-research.md +0 -91
  42. package/docs/in-progress/prediction-engine.md +0 -404
  43. package/docs/plans/001-memory-store.md +0 -106
  44. package/docs/plans/002-agent-driven-pipelines.md +0 -59
  45. package/docs/plans/003-memory-hygiene.md +0 -61
  46. package/docs/qa/use-cases/README.md +0 -51
  47. package/docs/qa/use-cases/UC-001-memory-roundtrip.md +0 -20
  48. package/docs/qa/use-cases/UC-002-dedup-cycle.md +0 -23
  49. package/docs/qa/use-cases/UC-003-extraction-nudge.md +0 -56
  50. package/docs/qa/use-cases/UC-004-cli-inspection.md +0 -22
  51. package/docs/qa/use-cases/UC-005-setup-install.md +0 -34
  52. package/docs/qa/use-cases/UC-006-cursor-hook-lifecycle.md +0 -32
  53. package/docs/qa/use-cases/UC-007-recall-nudge.md +0 -31
  54. package/docs/qa/use-cases/UC-008-hygiene-heartbeat.md +0 -26
  55. package/docs/qa/use-cases/UC-009-flush-tools-tiers.md +0 -33
  56. package/docs/qa/use-cases/UC-010-prime.md +0 -25
  57. package/docs/qa/use-cases/UC-011-write-time-similarity-warning.md +0 -24
  58. package/docs/qa/use-cases/UC-012-model-migration.md +0 -28
  59. package/docs/qa/use-cases/UC-013-compaction-context.md +0 -25
  60. package/docs/qa/use-cases/UC-014-skill-install-drift.md +0 -28
  61. package/docs/qa/use-cases/UC-015-env-override-matrix.md +0 -28
  62. package/docs/qa/use-cases/UC-016-concurrent-session-isolation.md +0 -37
  63. package/docs/qa/use-cases/UC-017-buffer-tool-vs-buffer-batch.md +0 -27
  64. package/docs/qa/use-cases/UC-018-mcp-startup-setup-detection.md +0 -38
  65. package/docs/qa/use-cases/UC-019-archived-memory-lifecycle.md +0 -36
  66. package/docs/qa/use-cases/UC-020-extraction-escalation.md +0 -52
  67. package/docs/qa/use-cases/UC-021-prediction-autofire.md +0 -21
  68. package/docs/qa/use-cases/UC-022-prediction-dedup.md +0 -19
  69. package/docs/qa/use-cases/UC-023-prediction-confidence-model.md +0 -23
  70. package/docs/qa/use-cases/UC-024-prediction-delete.md +0 -20
@@ -0,0 +1,361 @@
1
+ # Prediction Consolidation and Compound Predictions
2
+
3
+ ## Synopsis
4
+
5
+ Post-processing layers for the prediction engine: fire tracking, co-fire-based
6
+ consolidation (dedup), and compound (tier-2) predictions minted from
7
+ co-firing constellations of existing predictions. Uses nudge-driven detection
8
+ and the existing edge model for fire behavior. No cron, no separate backend.
9
+
10
+ ## Background
11
+
12
+ The prediction engine ships matchers, predictions, edges, and provenance. It
13
+ auto-fires at chat.message and updates via agent tool calls. What it lacks
14
+ relative to nak's samskara system:
15
+
16
+ - **Fire tracking** -- no record of which predictions fire together on the same
17
+ turn. This is the prerequisite for both co-fire dedup and compound detection.
18
+ - **Consolidation** -- no corpus-level dedup pass. Write-time dedup
19
+ (findNearestPrediction at 0.85) catches near-identical statements at creation,
20
+ but behavioral duplicates (same preference, different wording) accumulate.
21
+ - **Compound predictions** -- no mechanism to mint a higher-order prediction
22
+ from a constellation of predictions that reliably co-fire. Nak's tier-2
23
+ samskaras fire at 77% genuine-engagement vs tier-1's 21% because compound
24
+ claims match broader contexts.
25
+
26
+ Reference: `docs/dev/samskara.md` in nak (1896 lines), studied July/August 2026.
27
+ The architectural difference between the two systems is load-bearing for this
28
+ plan (see Key Insight below).
29
+
30
+ ## Key Insight: the edge model carries fire behavior
31
+
32
+ Nak matches on `prediction_embedding` (the claim text). A tier-2 fires because
33
+ its compound claim text cosine-matches the user message. This is why nak needs
34
+ fire tracking, cohort_ids, lift computation, and a separate tier-2 detection
35
+ RPC -- the compound only fires if its own embedding matches.
36
+
37
+ Thatch matches on `matcher_embedding` (the context description) and follows
38
+ edges. A compound prediction with edges to its children's matchers fires
39
+ automatically whenever any child matcher fires. The compound inherits the fire
40
+ coverage of all its children. This is "firing in tandem" for free, via the
41
+ existing edge model.
42
+
43
+ The co-fire data is for *deciding what to compound*, not for *making the
44
+ compound fire*. This means:
45
+
46
+ 1. A compound needs no tier column, no special scoring path, no fire-path
47
+ changes. It is a prediction with multiple matcher edges.
48
+ 2. The existing `scorePredictions` already follows all edges and deduplicates by
49
+ prediction_id. A compound scores through its children's edges.
50
+ 3. The existing `prediction_update` tool already finds existing matchers by
51
+ cosine (0.85 threshold) and creates edges. Calling it multiple times with
52
+ different matcher texts but the same prediction statement creates a
53
+ multi-matcher compound naturally.
54
+
55
+ A v1 could even skip fire tracking and let the agent create compounds based on
56
+ semantic similarity between existing predictions (cosine on prediction
57
+ embeddings). Fire tracking makes detection data-driven instead of
58
+ embedding-driven, but the edge model carries the fire behavior either way.
59
+
60
+ ## Decisions
61
+
62
+ | Decision | Rationale |
63
+ |----------|-----------|
64
+ | No tier column | A compound is a prediction with multiple matcher edges. The edge model already handles multi-matcher predictions. Adding a tier column would special-case scoring and fire paths for no benefit. |
65
+ | Fire tracking as prerequisite | Co-fire data drives both consolidation and compound detection. Without it, detection is limited to semantic similarity. With it, detection is behavioral -- based on what actually fires together. |
66
+ | Nudge-driven, not cron-driven | Thatch has no cron. Detection runs at session start, folded into the existing hygiene nudge. This is the same pattern as `findDuplicates` for memories. |
67
+ | Agent is the minter | Like the existing prediction engine, the agent synthesizes compound statements via `prediction_update`. No separate LLM minter call. The nudge surfaces the candidate constellation; the agent decides whether to compound. |
68
+ | Lift for compound detection, ratio for dedup | Nak proved this: the rarer-member ratio (`cofires / min(fires_A, fires_B)`) catches duplicates (perfectly correlated) but fails for compounds because busy-but-independent pairs still reach ratio ~0.5. Lift (`cofires * cohorts / (fires_A * fires_B)`) separates genuine co-activation from base-rate binding. |
69
+ | Cosine band separates dedup from compound detection | Dedup operates at cosine >= 0.70 (near-duplicates). Compound detection operates at cosine in [0.30, 0.68) (related but distinct). The 0.68 top end stays below dedup's 0.70 floor so the two phases never fight over the same pair. |
70
+
71
+ ## Architecture
72
+
73
+ ### Phase 1: Fire Tracking
74
+
75
+ **New table: `prediction_fires`**
76
+
77
+ ```sql
78
+ CREATE TABLE IF NOT EXISTS prediction_fires (
79
+ id TEXT PRIMARY KEY,
80
+ prediction_id TEXT NOT NULL,
81
+ store TEXT NOT NULL,
82
+ session_id TEXT,
83
+ cohort_id TEXT NOT NULL,
84
+ score REAL NOT NULL,
85
+ fired_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
86
+ FOREIGN KEY (prediction_id) REFERENCES predictions(id) ON DELETE CASCADE
87
+ );
88
+ ```
89
+
90
+ `cohort_id` is a UUID generated per auto-fire invocation, shared across all
91
+ predictions that scored above threshold in the same `scorePredictionNudge`
92
+ call. This is the direct analog of nak's `samskara_fires.cohort_id`.
93
+
94
+ **Write path:** `scorePredictionNudge` currently returns `PredictionNudgeItem[]`
95
+ and is called from two sites (index.ts auto-fire, sideband.ts MCP path). Add a
96
+ `recordFires` method that takes the scored predictions + a generated cohort_id
97
+ and inserts one row per prediction. The callers generate the cohort_id before
98
+ calling `scorePredictionNudge` and pass it through for recording.
99
+
100
+ Both callers must record fires. The opencode path (index.ts) has the session_id.
101
+ The MCP path (sideband.ts) does not have a session_id in the sideband request --
102
+ pass null or derive from the socket connection.
103
+
104
+ **New DB methods:**
105
+ - `recordFires(cohortId, sessionId, items, store)` -- batch insert
106
+ - `fireCount(predictionId)` -- total fires for a prediction
107
+ - `cofireCount(predictionIdA, predictionIdB)` -- count shared cohorts
108
+ - `totalCohorts(store)` -- distinct cohort_id count (for lift denominator)
109
+
110
+ **Schema location:** `#initSchema` in db.ts, alongside existing prediction
111
+ tables. Same conventions (uuid PK, store column, idempotent CREATE TABLE IF
112
+ NOT EXISTS).
113
+
114
+ ### Phase 2: Consolidation (Co-fire Dedup)
115
+
116
+ **New DB method: `findPredictionDuplicates(store, threshold)`**
117
+
118
+ Two-pass, mirroring nak's `samskara_collapse_by_cofiring`:
119
+
120
+ **Primary pass (behavioral):** For each pair of predictions (A, B) in the store
121
+ where both have fire_count > 0:
122
+
123
+ ```
124
+ cofires(A, B) >= 3
125
+ cofires(A, B) / min(fires_A, fires_B) >= 0.5
126
+ cosine(embed_A, embed_B) >= 0.70
127
+ ```
128
+
129
+ The ratio normalization is the key: two predictions that always fire together
130
+ when either fires are duplicates. Two that co-fire but also fire independently
131
+ are adjacent-but-distinct. Cosine is a sanity floor against spurious co-fires.
132
+
133
+ **Safety cap (cosine-only):** If the prediction count exceeds a target (e.g.,
134
+ 100), fall through to pure embedding-cosine greedy merge (cosine >= 0.60)
135
+ regardless of co-fire data. This catches accumulation when the behavioral pass
136
+ finds nothing but the pool is still growing.
137
+
138
+ **Output:** A list of candidate merge pairs with {predictionA, predictionB,
139
+ cofires, ratio, cosine, firesA, firesB}. Surfaced via the hygiene nudge, not
140
+ auto-merged. The agent reviews and merges via prediction_delete + (optionally)
141
+ prediction_update to fold the loser's matchers into the winner.
142
+
143
+ The merge itself is agent-driven: the agent sees the candidate pair, reads both
144
+ predictions, deletes the weaker one (or uses a future `prediction_merge` tool
145
+ if one is added). This matches the memory dedup pattern
146
+ (`find_duplicates` + `dedup_mark_checked`).
147
+
148
+ **Integration point:** `hygiene.ts` -- add a line to `hygieneReport`:
149
+ `N prediction duplicate pairs pending review` when candidates exist.
150
+
151
+ **Nudge throttling:** Runs at session start only, same as the existing hygiene
152
+ report. The analysis is O(n^2) over the predictions table but n is small
153
+ (tens, not hundreds) so the cost is negligible.
154
+
155
+ ### Phase 3: Compound Prediction Detection
156
+
157
+ **New DB method: `findCoFireConstellations(store)`**
158
+
159
+ Returns candidate groups of 3-6 predictions that reliably co-fire but are
160
+ semantically distinct (not dedup candidates).
161
+
162
+ **Detection formula (lift-based, from nak):**
163
+
164
+ ```
165
+ eligible(A, B) when:
166
+ cofires(A, B) >= p_min_cofires -- 10
167
+ lift(A, B) >= p_min_lift -- 2.0
168
+ cosine(embed_A, embed_B) >= p_cosine_lo -- 0.30 (effectively inert)
169
+ cosine(embed_A, embed_B) < p_cosine_hi -- 0.68 (< dedup floor 0.70)
170
+
171
+ where lift(A, B) = cofires(A, B) * total_cohorts / (fires_A * fires_B)
172
+ ```
173
+
174
+ Lift is observed co-fires over co-fires expected under independence. Base-rate
175
+ binding (two busy predictions colliding because both fire often) sits at lift
176
+ ~1. Genuine co-activation runs several times chance. The `p_min_cofires = 10`
177
+ absolute-mass guard prevents small-sample variance (a pair firing 4x that
178
+ always co-fires scores a huge lift on 4 points).
179
+
180
+ **Group growth:**
181
+ 1. Seed: the strongest-lift eligible edge.
182
+ 2. Grow: every node sharing an eligible edge with BOTH seed members (not
183
+ either -- co-firing with one is adjacent, not part of the constellation).
184
+ Strongest combined lift first. Capped at 6 members. Reject groups < 3 (a
185
+ 2-member group is a dedup candidate, not a compound).
186
+ 3. Coverage skip: if the candidate's Jaccard overlap with any existing
187
+ compound's child-matchers is >= 0.60, skip to the next-strongest uncovered
188
+ seed. This prevents one compound on a dense region from masking every other
189
+ constellation.
190
+
191
+ **Precondition:** At least 8 predictions with fire_count > 0 before the
192
+ self-join runs. Avoids spending computation on a cold store.
193
+
194
+ **Output:** A list of candidate constellations, each with {members:
195
+ [{prediction_id, statement, matcher_description, cofire_weight}], lift,
196
+ group_size}. Surfaced via the hygiene nudge.
197
+
198
+ **Integration point:** `hygiene.ts` -- add: `N co-fire constellations pending
199
+ review` when candidates exist.
200
+
201
+ ### Phase 4: Compound Minting (Agent-Driven)
202
+
203
+ The nudge surfaces a candidate constellation. The prompt instructions tell the
204
+ agent:
205
+
206
+ 1. Read the member predictions and their matchers.
207
+ 2. Synthesize a generalized statement that captures the shared behavior without
208
+ collapsing to any single child.
209
+ 3. Call `prediction_update` once per child matcher, using the child's matcher
210
+ text and the new compound statement. Each call finds the existing child
211
+ matcher by cosine (0.85) and creates an edge to the new compound prediction.
212
+
213
+ The agent calls `prediction_update` with `signal: "create"` for each child
214
+ matcher. The first call creates the compound prediction + an edge from the
215
+ first child's matcher. Subsequent calls find the existing compound by
216
+ `findNearestPrediction` (0.85 store-wide dedup) and add edges from the other
217
+ children's matchers.
218
+
219
+ This works with the existing tool surface. No new tools needed for minting.
220
+
221
+ **Prompt instructions addition** (in prompts.ts, all three host variants):
222
+
223
+ After the existing prediction instructions, add guidance for compounds:
224
+
225
+ - When the hygiene nudge reports co-fire constellations, consider whether the
226
+ members share a generalizable behavior.
227
+ - If so, synthesize a compound statement and call `prediction_update` once per
228
+ child matcher with the compound statement. The tool handles matcher dedup and
229
+ edge creation.
230
+ - A compound should be strictly more general than any single child. If you
231
+ cannot generalize, do not compound.
232
+ - The compound inherits the fire coverage of its children's matchers. It will
233
+ fire whenever any child matcher fires.
234
+
235
+ ### Phase 5: Compound Summary (Optional, Future)
236
+
237
+ Nak generates a cached prose summary of top samskaras that rides every system
238
+ prompt. Thatch could do the same: a one-paragraph summary of the strongest
239
+ predictions, regenerated when the prediction set changes significantly.
240
+
241
+ This is lower priority. The per-turn auto-fire nudge already surfaces relevant
242
+ predictions. A compound summary would capture stable bias across every turn,
243
+ not just when a matcher fires. Defer until the compound predictions prove
244
+ useful in practice.
245
+
246
+ ## Data Flow
247
+
248
+ ```
249
+ chat.message (user text)
250
+ → embed promptText (shared with recall nudge)
251
+ → scorePredictionNudge([repo, global], embedding, 0.45)
252
+ → findMatchers (cosine >= 0.45)
253
+ → scorePredictions (follow edges, dedup by prediction_id)
254
+ → recordFires (NEW: persist cohort with shared cohort_id)
255
+ → return PredictionNudgeItem[]
256
+ → inject prediction nudge (existing)
257
+
258
+ session.created (top-level)
259
+ → hygieneReport
260
+ → findDuplicates (memories, existing)
261
+ → findPredictionDuplicates (NEW: co-fire dedup candidates)
262
+ → findCoFireConstellations (NEW: compound candidates)
263
+ → inject hygiene nudge with all signals
264
+
265
+ Agent reads hygiene nudge
266
+ → reviews dedup candidates → prediction_delete / merge
267
+ → reviews constellation candidates → prediction_update (compound minting)
268
+ ```
269
+
270
+ ## Dependencies
271
+
272
+ No new runtime dependencies. All detection is SQLite queries + JS cosine
273
+ computation, same as existing prediction methods.
274
+
275
+ **Schema:** One new table (`prediction_fires`), added to `#initSchema` in
276
+ db.ts.
277
+
278
+ **Code changes by file:**
279
+ - `src/db.ts` -- new table, 4-5 new methods (recordFires, fireCount,
280
+ cofireCount, totalCohorts, findPredictionDuplicates, findCoFireConstellations)
281
+ - `src/index.ts` -- generate cohort_id, pass to recordFires in auto-fire path
282
+ - `src/sideband.ts` -- same for MCP path
283
+ - `src/hygiene.ts` -- add prediction dedup + constellation counts to report
284
+ - `src/prompts.ts` -- compound minting guidance in all three prompt variants
285
+ - `tests/prediction.test.ts` -- tests for new DB methods
286
+ - `tests/plugin.test.ts` -- test that fire tracking records cohorts
287
+
288
+ ## Implementation Order
289
+
290
+ 1. **Fire tracking** (table + recordFires + caller wiring). Foundation for
291
+ everything else. Ship and let fire data accumulate.
292
+ 2. **Cosine dedup** (findPredictionDuplicates + hygiene integration). Does not
293
+ need fire tracking. Can ship in parallel with phase 1. Immediate value:
294
+ catches behavioral duplicates the write-time 0.85 dedup misses.
295
+ 3. **Co-fire dedup** (add co-fire ratio + cosine filter to
296
+ findPredictionDuplicates). Needs fire data to accumulate first. Wait a few
297
+ weeks after phase 1 ships.
298
+ 4. **Compound detection** (findCoFireConstellations + hygiene integration).
299
+ Needs more fire data than dedup (p_min_cofires = 10). Wait longer.
300
+ 5. **Compound minting** (prompt instructions + agent-driven via existing
301
+ tools). Ships with phase 4 -- the nudge surfaces candidates, the agent
302
+ compounds.
303
+
304
+ Phases 1-2 can ship together. Phases 3-5 follow once fire data accumulates.
305
+
306
+ ## Test Plan
307
+
308
+ - `prediction.test.ts`: test recordFires, fireCount, cofireCount,
309
+ totalCohorts with synthetic cohorts. Test findPredictionDuplicates with
310
+ known co-firing pairs. Test findCoFireConstellations with lift thresholds.
311
+ Use the existing `makeEmbed(seed, dim=384)` helper.
312
+ - `plugin.test.ts`: test that chat.message auto-fire records a cohort.
313
+ Follow the recall-nudge test pattern (mock.module transformers + server
314
+ hooks).
315
+ - `hygiene.test.ts` (if exists) or `plugin.test.ts`: test that hygiene report
316
+ includes prediction dedup/constellation counts when candidates exist.
317
+
318
+ ## Risks and Mitigations
319
+
320
+ **Risk: O(n^2) co-fire self-join at session start.**
321
+ Mitigation: n is small (tens of predictions per store). The self-join is over
322
+ `prediction_fires` grouped by cohort_id, not over embeddings. Add a
323
+ precondition gate (minimum prediction count, minimum fire count) before running
324
+ the analysis.
325
+
326
+ **Risk: Compound predictions fire too much, dominating the nudge.**
327
+ Mitigation: The existing `scorePredictionNudge` deduplicates by prediction_id
328
+ and slices to limit=5. A compound competes on equal footing with its children.
329
+ If a compound and a child both fire, the higher-scoring one wins the slot. The
330
+ compound's score is `cosine(matcher) * edge.weight * confidence` -- same
331
+ formula. No special treatment.
332
+
333
+ **Risk: Agent creates poor compounds (too generic, not useful).**
334
+ Mitigation: The prompt instructions include the "strictly more general" guard
335
+ and the "if you cannot generalize, do not compound" escape hatch. The compound
336
+ starts at confidence 0.5 (p0) and earns confidence through user feedback like
337
+ any prediction. Bad compounds get disconfirmed and stop dominating the nudge.
338
+
339
+ **Risk: Fire tracking bloats the DB.**
340
+ Mitigation: `prediction_fires` rows are small (id, prediction_id, store,
341
+ session_id, cohort_id, score, fired_at). At ~10 predictions per fire event and
342
+ ~100 fire events per session, that is ~1000 rows per session. Add a cleanup
343
+ pass (delete fires older than 90 days with no corresponding prediction) if
344
+ this becomes a concern. Low priority.
345
+
346
+ ## Notes
347
+
348
+ - The `populationP0` method already exists but is not wired into
349
+ `createPrediction`. Wiring it would make new predictions (including
350
+ compounds) start at the store's actual hit rate instead of flat 0.5. This is
351
+ a separate improvement, not part of this plan, but worth doing alongside
352
+ phase 1.
353
+ - The existing write-time dedup (`findNearestPrediction` at 0.85) catches
354
+ near-identical statements at creation. The co-fire dedup in phase 3 catches
355
+ behavioral duplicates that differ in wording. The two are complementary:
356
+ write-time catches semantic twins, co-fire catches behavioral twins.
357
+ - Nak's tier-2 decline ledger (`samskara_tier2_declines`, TTL'd 7 days) prevents
358
+ re-offering a declined constellation every sweep. Thatch's equivalent: a
359
+ `prediction_constellation_declines` table or a `dedup_pairs`-style
360
+ checked-pairs set. Defer until the agent is actually declining candidates and
361
+ the re-offer becomes annoying.
package/docs/qa/README.md CHANGED
@@ -152,7 +152,24 @@ correctness on `README.md` and `docs/` excluding `docs/plans/`; see
152
152
  mocking `fs` to force a write error).
153
153
  - `bin/thatch` has no automated tests; it is a thin shell over `db.ts`.
154
154
 
155
- ## Use cases
155
+ ## QA use-case suite
156
156
 
157
- See `docs/qa/use-cases/` for manual end-to-end scenarios with preconditions,
158
- steps, and expected results.
157
+ End-to-end QA scenarios live in `tests/qa/` as bun test files, not docs.
158
+ Each use case is a `uc-NNN-*.test.ts` file that defines preconditions,
159
+ steps, and expected results as string constants, then registers itself
160
+ via `registerUseCase` from `tests/qa/runner.ts`.
161
+
162
+ Most use cases shell out to `opencode run` with an isolated repo copy
163
+ (temp DB, temp config, temp home). Automatable use cases override the
164
+ `run` function with direct CLI/bun assertions. Manual-only use cases
165
+ (compaction, visual TUI) are skipped automatically.
166
+
167
+ Run the suite:
168
+
169
+ ```bash
170
+ mise run qa # full run (spawns opencode sessions, costs model tokens)
171
+ mise run qa-dry-run # list use cases without spawning sessions
172
+ ```
173
+
174
+ The regular test suite (`mise run check`) excludes `tests/qa/` by using
175
+ a flat glob (`bun test tests/*.test.ts`).
@@ -199,7 +199,9 @@ Seven specialist review lenses, each a self-contained static-analysis pass:
199
199
  | `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar against generic praise. |
200
200
  | `thatch-review-synthesizer` | Verifies and synthesizes findings from multiple specialists into a report that starts with workflow changes, then highlights, then deduplicated severity-grouped findings. |
201
201
  | `thatch-review-context` | Gathers project context (PR descriptions, git archaeology, ticket references, memory) before a review. Prevents false positives about intentionally deferred work. |
202
- | `thatch-workflow-research` | Researches code workflows and features affected by a change or planned change. Reads code flows, comments, git history, and produces a guide for reviewers or planners. |
202
+ | `thatch-code-archaeology` | Investigates an existing feature, debugs an unfamiliar area, or begins a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features, skeletons) before proposing changes. Pairs with `thatch-coding-workflow`. |
203
+ | `thatch-review-followup` | Alternate entrypoint for follow-up review rounds. Verifies whether the author's responses and code changes since your last review adequately addressed your prior findings, offers to reply on resolved items, then optionally re-runs the full structured review. |
204
+ | `thatch-review-response` | Author-side review response: triage findings on your own PR, fix bugs one by one, reply on each thread, post a top-level summary comment. |
203
205
  | `thatch-change-walkthrough` | Explains a change to the user as a teaching walkthrough: researches each affected workflow at the merge-base, teaches current behavior, then overlays the modifications with file:line citations. |
204
206
  | `thatch-code-walkthrough` | Explains a feature, module, or workflow to the user as a teaching walkthrough with file:line citations. Also used to draft high-level docs for new or undocumented features. |
205
207
 
@@ -207,9 +209,9 @@ Seven specialist review lenses, each a self-contained static-analysis pass:
207
209
 
208
210
  | Skill | Purpose |
209
211
  |-------|---------|
210
- | `pr-description` | Drafts PR descriptions with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, project-context research, clarity checks, and bold/italic emphasis for scanning. |
211
- | `ticket-description` | Drafts ticket or issue descriptions (Linear or Jira) with clear sections, project-context research, clarity checks, and bold/italic emphasis for scanning. |
212
- | `split-overlarge-pr` | Splits already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main. |
212
+ | `thatch-pr-description` | Drafts PR descriptions with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, project-context research, clarity checks, and bold/italic emphasis for scanning. |
213
+ | `thatch-ticket-description` | Drafts ticket or issue descriptions (Linear or Jira) with clear sections, project-context research, clarity checks, and bold/italic emphasis for scanning. |
214
+ | `thatch-split-overlarge-pr` | Splits already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main. |
213
215
 
214
216
  ### opencode-only skills
215
217
 
@@ -224,7 +226,9 @@ Seven specialist review lenses, each a self-contained static-analysis pass:
224
226
  | Memory skills (4) | Yes | Yes | Yes |
225
227
  | Review specialists (7) | Yes | Yes | Yes |
226
228
  | Review synthesizer | Yes | Yes | Yes |
227
- | Review context + workflow research | Yes | Yes | Yes |
229
+ | Review context + code archaeology | Yes | Yes | Yes |
230
+ | Review followup | Yes | Yes | Yes |
231
+ | Review response (author-side) | Yes | Yes | Yes |
228
232
  | Walkthrough skills (2) | Yes | Yes | Yes |
229
233
  | Writing skills (3) | Yes | Yes | Yes |
230
234
  | Code review coordinator | Yes | No (requires sub-agents) | No (requires sub-agents) |
@@ -258,6 +262,30 @@ specialist in sequence, then synthesize:
258
262
  4. Load thatch-review-synthesizer, verify and aggregate all findings.
259
263
  ```
260
264
 
265
+ For a **follow-up round** (the author responded or pushed changes after your
266
+ review), load the re-check skill:
267
+
268
+ ```text
269
+ Load thatch-review-followup and check whether my prior review comments
270
+ on this PR were adequately addressed.
271
+ ```
272
+
273
+ It verifies whether each finding was resolved (by code change, proof, or
274
+ follow-up ticket with a risk explanation), offers to reply on the resolved
275
+ ones, then optionally hands off to the coordinator for a fresh full review.
276
+
277
+ For **responding to review on your own PR**, load the author-side skill:
278
+
279
+ ```text
280
+ Load thatch-review-response and help me work through the review comments
281
+ on my PR.
282
+ ```
283
+
284
+ It triages every finding (legitimate, intentional, false positive, unlikely
285
+ edge case), collapses comments sharing a root cause, works through each bug
286
+ with you, drafts replies on each thread, then posts a top-level summary
287
+ comment so reviewers can see what changed without re-reading the full diff.
288
+
261
289
  These files are plugin-owned: local edits are overwritten the next time the
262
290
  plugin initializes (this is how skill improvements ship with new versions).
263
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/db.ts CHANGED
@@ -344,7 +344,7 @@ export class ThatchDB {
344
344
  /**
345
345
  * Brute-force cosine similarity search across the given stores.
346
346
  * When branch is specified, includes project-wide (branch IS NULL) plus
347
- * branch-specific entries. Does NOT stamp recall telemetry — the prompt-aware
347
+ * branch-specific entries. Does NOT stamp recall telemetry - the prompt-aware
348
348
  * nudge uses this to check whether memories relate to a prompt without
349
349
  * polluting recall_count/last_recalled_at (the agent hasn't actually read
350
350
  * them yet, only the plugin has checked for relevance).
@@ -388,7 +388,7 @@ export class ThatchDB {
388
388
 
389
389
  // Entries embedded by a different model live in a different vector space;
390
390
  // comparing them would produce NaN or nonsense scores, so they're skipped
391
- // rather than ranked. Dimension is the discriminator — model tags are
391
+ // rather than ranked. Dimension is the discriminator - model tags are
392
392
  // informational only.
393
393
  const scored = rows.flatMap((row) => {
394
394
  row.archived = !!row.archived;
@@ -403,7 +403,7 @@ export class ThatchDB {
403
403
 
404
404
  /**
405
405
  * Semantic recall for agent-initiated searches. Delegates to search() for
406
- * cosine scoring, then stamps recall telemetry — retrieval is the "used
406
+ * cosine scoring, then stamps recall telemetry - retrieval is the "used
407
407
  * recently" signal hygiene reporting keys on.
408
408
  */
409
409
  recall(
@@ -427,7 +427,7 @@ export class ThatchDB {
427
427
  }
428
428
 
429
429
  /**
430
- * Entries in a store semantically close to the given embedding — the
430
+ * Entries in a store semantically close to the given embedding - the
431
431
  * write-time collision check. Unlike recall(), this records no telemetry:
432
432
  * it's the plugin looking, not the agent using.
433
433
  */
@@ -931,7 +931,7 @@ export class ThatchDB {
931
931
  }
932
932
 
933
933
  // ---------------------------------------------------------------------------
934
- // Hygiene — signals for the session-start heartbeat. Staleness means
934
+ // Hygiene - signals for the session-start heartbeat. Staleness means
935
935
  // neither written nor recalled since the cutoff; recall telemetry keeps
936
936
  // actively-used old memories out of the count.
937
937
  // ---------------------------------------------------------------------------
@@ -1002,7 +1002,7 @@ function blobToVector(blob: Uint8Array): Float32Array {
1002
1002
  /**
1003
1003
  * Cosine similarity between two Float32Array vectors of equal dimension.
1004
1004
  * Returns a value in [-1, 1]. Normalized embeddings will be close to [0, 1].
1005
- * Throws on dimension mismatch — comparing vectors from different embedding
1005
+ * Throws on dimension mismatch - comparing vectors from different embedding
1006
1006
  * spaces is always a caller bug; callers filter by length first.
1007
1007
  */
1008
1008
  export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
package/src/embeddings.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // BGE-small-en-v1.5 requires a query prefix for asymmetric search.
2
- // Passage (memory content) gets no prefix — the model was trained
2
+ // Passage (memory content) gets no prefix - the model was trained
3
3
  // to encode passages without instruction.
4
4
  const QUERY_PREFIX =
5
5
  "Represent this sentence for searching relevant passages: ";
@@ -86,7 +86,7 @@ export class BgeEmbeddingModel implements EmbeddingModel {
86
86
  /**
87
87
  * Mock embedding model for tests. Returns deterministic vectors derived from
88
88
  * a hash of the input text via xorshift, so identical texts embed identically
89
- * and distinct texts land near-orthogonal — mirroring how unrelated content
89
+ * and distinct texts land near-orthogonal - mirroring how unrelated content
90
90
  * behaves in a real embedding space. Never loads a model or touches the network.
91
91
  */
92
92
  export class MockEmbeddingModel implements EmbeddingModel {
@@ -27,7 +27,7 @@ export function queueDir(): string {
27
27
  return join(cacheHome, "thatch", "queue");
28
28
  }
29
29
 
30
- /** Sanitize a session id for use as a filename — UUIDs are already safe. */
30
+ /** Sanitize a session id for use as a filename - UUIDs are already safe. */
31
31
  function safeName(sessionID: string): string {
32
32
  return sessionID.replace(/[^a-zA-Z0-9_-]/g, "_");
33
33
  }
@@ -38,9 +38,9 @@ function queuePath(sessionID: string): string {
38
38
 
39
39
  /**
40
40
  * Buffer a batch of tool calls for later extraction. Filters out:
41
- * - thatch's own MCP tools (mcp__thatch__*) to avoid self-echo — extracting
41
+ * - thatch's own MCP tools (mcp__thatch__*) to avoid self-echo - extracting
42
42
  * facts from memory operations would just write the store back into itself.
43
- * - skill/task/agent meta-tools — these orchestrate agent behavior (loading
43
+ * - skill/task/agent meta-tools - these orchestrate agent behavior (loading
44
44
  * skills, dispatching sub-agents) and buffering them creates a feedback
45
45
  * loop where the extraction nudge triggers a skill load, which gets
46
46
  * buffered, which triggers another nudge on the next turn.
@@ -116,7 +116,7 @@ function readQueueFile(path: string): ToolInteraction[] {
116
116
  }
117
117
 
118
118
  // ---------------------------------------------------------------------------
119
- // Missed-nudge counter — file-backed per-session escalation state. Tracks
119
+ // Missed-nudge counter - file-backed per-session escalation state. Tracks
120
120
  // consecutive extraction nudges delivered without any memory_remember call.
121
121
  // Reset by appendBatch when the agent writes a memory. Read and incremented
122
122
  // by flush-tools when the nudge is delivered.
package/src/extraction.ts CHANGED
@@ -64,7 +64,7 @@ export function truncate(text: string, maxLen: number): string {
64
64
  * thatch-fact-extractor skill expects. Shared by the opencode plugin path
65
65
  * (in-memory buffer) and the Claude Code CLI path (file-backed queue) so the
66
66
  * extractor sees an identical contract regardless of host. The agent itself
67
- * does the actual fact extraction — this function only collects and serializes.
67
+ * does the actual fact extraction - this function only collects and serializes.
68
68
  */
69
69
  export function buildExtractionPayload(
70
70
  interactions: ToolInteraction[],
@@ -113,7 +113,7 @@ export class ExtractionPipeline {
113
113
 
114
114
  /**
115
115
  * Accept the session's buffer: move it to a holding area so the nudge
116
- * stops replaying while a dispatched extractor works. AMQP-style accept —
116
+ * stops replaying while a dispatched extractor works. AMQP-style accept -
117
117
  * the entries are not dropped until a completion signal arrives (the
118
118
  * extractor writes a memory, calls extraction_done, or goes idle). If the
119
119
  * extractor dies first, requeueAccepted puts them back.
@@ -135,7 +135,7 @@ export class ExtractionPipeline {
135
135
  * Move accepted entries back to pending: the extractor errored or its
136
136
  * session was deleted before completing, so the facts were never
137
137
  * processed. Prepended so they replay in original order. Not capped at
138
- * maxBuffer — a silent drop here would defeat the point of requeueing;
138
+ * maxBuffer - a silent drop here would defeat the point of requeueing;
139
139
  * the next push trims back to the cap.
140
140
  */
141
141
  requeueAccepted(sessionID: string): void {
@@ -155,7 +155,7 @@ export class ExtractionPipeline {
155
155
  * Remove entries that were in the snapshot (by reference identity), keeping
156
156
  * any that arrived after the snapshot was taken. Used by the child-parent
157
157
  * drain: when a sub-agent writes a memory, only the entries present at
158
- * dispatch time should be drained — not the entire buffer, which may now
158
+ * dispatch time should be drained - not the entire buffer, which may now
159
159
  * contain interleaved-turn entries the parent accumulated while the
160
160
  * sub-agent was running.
161
161
  */
package/src/git.ts CHANGED
@@ -84,7 +84,7 @@ export async function detectRepo(cwd?: string): Promise<string> {
84
84
  /**
85
85
  * Local branch names in the given directory's repository. Used to detect
86
86
  * branch-scoped memories whose branch no longer exists. Returns [] outside
87
- * a git repo — callers must treat that as "unknown", not "no branches",
87
+ * a git repo - callers must treat that as "unknown", not "no branches",
88
88
  * or every branch-scoped memory would look orphaned.
89
89
  */
90
90
  export async function listBranches(cwd: string): Promise<string[]> {
package/src/hygiene.ts CHANGED
@@ -27,7 +27,7 @@ export async function hygieneReport(
27
27
  }
28
28
 
29
29
  // listBranches returns [] outside a git repo, which would make every
30
- // branch-scoped memory look orphaned — skip the check in that case.
30
+ // branch-scoped memory look orphaned - skip the check in that case.
31
31
  const scoped = db.branchesInStore(repo);
32
32
  if (scoped.length > 0) {
33
33
  const live = await listBranches(worktree);