@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.
- package/README.md +7 -7
- package/artifacts/skills/common.md +1 -1
- package/artifacts/skills/thatch-change-walkthrough.md +2 -2
- package/artifacts/skills/thatch-code-archaeology.md +252 -0
- package/artifacts/skills/thatch-code-review.md +4 -4
- package/artifacts/skills/thatch-code-walkthrough.md +2 -2
- package/artifacts/skills/thatch-coding-workflow.md +79 -0
- package/artifacts/skills/{pr-description.md → thatch-pr-description.md} +1 -1
- package/artifacts/skills/thatch-review-context.md +9 -0
- package/artifacts/skills/thatch-review-followup.md +281 -0
- package/artifacts/skills/thatch-review-pedantic.md +16 -1
- package/artifacts/skills/thatch-review-response.md +365 -0
- package/artifacts/skills/thatch-review-synthesizer.md +92 -5
- package/artifacts/skills/{split-overlarge-pr.md → thatch-split-overlarge-pr.md} +1 -1
- package/artifacts/skills/{ticket-description.md → thatch-ticket-description.md} +3 -3
- package/bin/release +3 -3
- package/bin/thatch +8 -8
- package/docs/dev/README.md +1 -1
- package/docs/dev/skills.md +34 -9
- package/docs/plans/README.md +9 -6
- package/docs/plans/intuition-drives.md +147 -0
- package/docs/plans/nomenclater.md +89 -0
- package/docs/plans/prediction-consolidation.md +361 -0
- package/docs/qa/README.md +20 -3
- package/docs/user/README.md +33 -5
- package/package.json +1 -1
- package/src/db.ts +6 -6
- package/src/embeddings.ts +2 -2
- package/src/extract-queue.ts +4 -4
- package/src/extraction.ts +4 -4
- package/src/git.ts +1 -1
- package/src/hygiene.ts +1 -1
- package/src/index.ts +28 -28
- package/src/mcp.ts +5 -5
- package/src/prompts.ts +71 -65
- package/src/setup.ts +19 -19
- package/src/sideband.ts +3 -3
- package/src/skills.ts +105 -6
- package/src/tool-defs.ts +11 -11
- package/src/tools.ts +1 -1
- package/artifacts/skills/thatch-workflow-research.md +0 -91
- package/docs/in-progress/prediction-engine.md +0 -404
- package/docs/plans/001-memory-store.md +0 -106
- package/docs/plans/002-agent-driven-pipelines.md +0 -59
- package/docs/plans/003-memory-hygiene.md +0 -61
- package/docs/qa/use-cases/README.md +0 -51
- package/docs/qa/use-cases/UC-001-memory-roundtrip.md +0 -20
- package/docs/qa/use-cases/UC-002-dedup-cycle.md +0 -23
- package/docs/qa/use-cases/UC-003-extraction-nudge.md +0 -56
- package/docs/qa/use-cases/UC-004-cli-inspection.md +0 -22
- package/docs/qa/use-cases/UC-005-setup-install.md +0 -34
- package/docs/qa/use-cases/UC-006-cursor-hook-lifecycle.md +0 -32
- package/docs/qa/use-cases/UC-007-recall-nudge.md +0 -31
- package/docs/qa/use-cases/UC-008-hygiene-heartbeat.md +0 -26
- package/docs/qa/use-cases/UC-009-flush-tools-tiers.md +0 -33
- package/docs/qa/use-cases/UC-010-prime.md +0 -25
- package/docs/qa/use-cases/UC-011-write-time-similarity-warning.md +0 -24
- package/docs/qa/use-cases/UC-012-model-migration.md +0 -28
- package/docs/qa/use-cases/UC-013-compaction-context.md +0 -25
- package/docs/qa/use-cases/UC-014-skill-install-drift.md +0 -28
- package/docs/qa/use-cases/UC-015-env-override-matrix.md +0 -28
- package/docs/qa/use-cases/UC-016-concurrent-session-isolation.md +0 -37
- package/docs/qa/use-cases/UC-017-buffer-tool-vs-buffer-batch.md +0 -27
- package/docs/qa/use-cases/UC-018-mcp-startup-setup-detection.md +0 -38
- package/docs/qa/use-cases/UC-019-archived-memory-lifecycle.md +0 -36
- package/docs/qa/use-cases/UC-020-extraction-escalation.md +0 -52
- package/docs/qa/use-cases/UC-021-prediction-autofire.md +0 -21
- package/docs/qa/use-cases/UC-022-prediction-dedup.md +0 -19
- package/docs/qa/use-cases/UC-023-prediction-confidence-model.md +0 -23
- 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
|
-
##
|
|
155
|
+
## QA use-case suite
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
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`).
|
package/docs/user/README.md
CHANGED
|
@@ -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-
|
|
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 +
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/src/extract-queue.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|