@jeffober/thatch 0.1.20 → 0.1.22

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 CHANGED
@@ -67,7 +67,7 @@ Include thatch's instructions in your agent's system prompt manually; see
67
67
  - **Prediction engine**: a statistical model of user decision-making
68
68
  preferences. When a prompt matches learned contexts, predictions fire
69
69
  alongside the recall nudge.
70
- - **Skills**: 20 skills for memory workflows, structured multi-specialist
70
+ - **Skills**: 19 skills for memory workflows, structured multi-specialist
71
71
  code review, change walkthroughs, and writing tasks (PR descriptions,
72
72
  tickets, PR splitting).
73
73
 
@@ -39,7 +39,7 @@ Sections, in order: SYNOPSIS, PURPOSE, DESCRIPTION, WALK-THROUGH (conditional),
39
39
 
40
40
  **SYNOPSIS, PURPOSE, and DESCRIPTION are mandatory** - even for a one-line, single-concern change. A tiny PR gets short sections, not fewer sections. WALK-THROUGH and NOTES appear only when warranted; omit them when empty. robots.txt appears only after an automated reviewer has produced a false positive worth heading off (see its section); it is always the final section.
41
41
 
42
- **Scale the body to the diff's conceptual size**, not its structure's capacity. A 2-file, single-concern change (add a flag, guard a branch, rename a symbol, bump a constant) is a few lines per section. SYNOPSIS one line, PURPOSE one to three, DESCRIPTION one short paragraph, NOTES if needed. The layered DESCRIPTION machinery below is for changes with real before/after mechanics to teach; when the diff is self-evident once you know the intent, collapse it. If your draft for a small change runs past ~15 lines of body, you are padding. Cut until each remaining line earns its place. Most of this skill's guidance exists to tame large PRs; applying it wholesale to a small one produces exactly the bloat the skill warns against.
42
+ **Scale the body to the PR's complexity, not its size in files or lines.** Estimate complexity by how much new mental model the reviewer must build (see the point-based budgets in Length and style). A 1-point mechanical change gets short sections, not fewer sections. The layered DESCRIPTION machinery below is for changes with real before/after mechanics to teach; when the diff is self-evident once you know the intent, collapse it. Most of this skill's guidance exists to tame large PRs; applying it wholesale to a small one produces exactly the bloat the skill warns against.
43
43
 
44
44
  ### SYNOPSIS
45
45
 
@@ -77,7 +77,7 @@ When a ticket description is available, use its problem statement as the seed fo
77
77
 
78
78
  ### DESCRIPTION
79
79
 
80
- How much structure DESCRIPTION needs depends on the change's conceptual size.
80
+ How much structure DESCRIPTION needs depends on the PR's complexity.
81
81
 
82
82
  **Small, self-evident change** (add a flag, guard a branch, rename a symbol, bump a constant): collapse to a sentence or two stating what the PR adds/changes. Do not manufacture a "how the code behaves today" paragraph for a change whose intent already makes the diff obvious. That is padding, not scaffolding.
83
83
 
@@ -240,13 +240,20 @@ These are hard rules, not suggestions.
240
240
 
241
241
  Target for most PRs: the reader can take in the whole description in under 60 seconds. Err on the shorter side.
242
242
 
243
- Scale the body to the diff's conceptual size:
244
- - Trivial 2-file change: ~15-line ceiling. SYNOPSIS one line, PURPOSE one to three, DESCRIPTION one short paragraph, NOTES if needed.
245
- - Moderate refactor: one tight paragraph or bullet set per section.
246
- - Large change with WALK-THROUGH: as long as the flow needs, but no longer. Aim for 3-6 steps; more means you are over-decomposing. Each step earns its place.
247
- - Enormous PR: accept that the reader may need more than 60 seconds. Keep the same rules, but spend the extra words on context, concrete behavior, and reviewer safety. Do not chase a tiny body by compressing meaning.
243
+ Estimate the PR's **complexity** (how much new mental model the reviewer must build), not its size in files or lines. Complexity drives the word budget the way scrum points drive effort estimates:
248
244
 
249
- If your draft for a small change runs past ~15 lines of body, you are padding. Cut until each remaining line earns its place.
245
+ - **1 point** (mechanical: rename, flag, guard, bump): the reviewer needs to know what changed, not why or how. ~50-100 words. SYNOPSIS one line, PURPOSE 1-2 sentences, DESCRIPTION 1-2 sentences.
246
+ - **2-3 points** (new behavior, non-obvious mechanics): the reviewer needs to understand a before/after they cannot infer from the diff. ~100-250 words. SYNOPSIS 1-2 lines, PURPOSE 2-4 sentences, DESCRIPTION one tight paragraph or short three-layer.
247
+ - **5 points** (new primitive, multi-workflow): the reviewer needs a mental model they do not have yet. ~250-400 words. Full sections as needed, WALK-THROUGH if there is a lifecycle.
248
+ - **8 points** (architectural shift): the reviewer needs to re-orient their understanding of a subsystem. ~400-600 words. Accept the length, spend on context and reviewer safety.
249
+
250
+ Estimate by: how many new concepts, how many decision points, whether the before/after is non-obvious, whether multiple workflows are affected. Not by files changed or lines added. A 200-line mechanical rename is 1 point. A 50-line new primitive with non-obvious semantics is 3-5 points.
251
+
252
+ **When a section is too long, step up the abstraction ladder.** Do not compress terminology or cut connective tissue to hit a word count. Instead, replace a sequence of mechanism details with the principle that subsumes them. If you are explaining three mechanisms in sequence, find the one principle that makes all three obvious to someone who already knows the stack. The mechanism details belong in DESCRIPTION or the diff, not in PURPOSE or every section.
253
+
254
+ Example: instead of six claims about how a connection pool reuses connections, how session locks are session-scoped, how a lock taken by one statement does not guard the next, and how nothing reports the mismatch, write: "Some Postgres features attach to the connection, not the statement. The pool freely reuses connections between statements, so state set by one statement may not be there for the next, and nothing reports the mismatch." Same understanding, fewer words, no compression.
255
+
256
+ Brevity means fewer ideas, not compressed claims. If the section is still too long after stepping up, cut the lowest-value claim entirely. Do not cut the connective tissue that makes the remaining claims parse on first read.
250
257
 
251
258
  Inside sections, telegraphic bullet style is fine: lowercase start, abbreviations (`w/`, `1x`, `~`), parenthetical shorthand. Full sentences only when required for clarity. The section headers are the only formal structure.
252
259
 
@@ -401,7 +408,7 @@ That reading alone conveys the shape and scope of both changes. The prose is for
401
408
  11. **Verify links**: check ticket/PR links, repo file links, and external docs where tooling allows. Do not claim an unverified link was verified.
402
409
  12. **Verify prose style and core principle**: check each rule in Prose style. Cut design-narration, authoring-sequence, and buzzwordy abstractions. Verify plain ASCII. Verify first-use definitions for subsystem jargon. Verify project-private labels are translated before use. Verify plain English nouns (no code-domain nouns in prose). Verify conclusion-first in PURPOSE. Verify NOTES bullets are either short pointers or self-contained. Verify one-paragraph-one-physical-line for GitHub rendering. The robots.txt section is exempt from these prose rules below its human-warning line.
403
410
  13. **Clarity pass**: read the draft as a cold reviewer skimming the PR. Rewrite before returning it; do not tell the user you performed this pass. Check: does every project-specific term get plain-English meaning before or with the label? Are ambiguous terms qualified or replaced with concrete behavior? Did you replace code/comment shorthand with reader-facing behavior when the shorthand is less clear? Does every process word name object/action/effect? Does every contrast name old and new behavior? Does every causal sentence show the middle step? Did you preserve connective tissue instead of compressing meaning into noun stacks? If the draft got longer, cut a lower-value claim instead of compressing a high-value one.
404
- 14. **Verify length**: if the body exceeds the ceiling for its conceptual size, cut until each line earns its place. For enormous PRs, accept extra length only when it buys context, concrete behavior, or reviewer safety.
411
+ 14. **Verify length**: estimate the PR's complexity (see the point-based budgets in Length and style). If the body exceeds the budget, step up the abstraction ladder first (replace mechanism sequences with the principle that subsumes them). If still over, cut the lowest-value claim entirely. Do not compress terminology or cut connective tissue to hit a word count.
405
412
  15. **Submit** via `gh pr create --body "$(cat <<'EOF' ... EOF)"` to preserve formatting.
406
413
 
407
414
  ## Anti-patterns
@@ -15,7 +15,10 @@ You will be given a JSON payload with:
15
15
  1. Call thatch_memory_recall to check for existing memories related to the interactions.
16
16
  2. For each new fact, call thatch_memory_remember with the appropriate store.
17
17
  3. Do not save session-specific state, ephemeral debugging details, or info already in CLAUDE.md.
18
- 4. Write for a future session with zero current context. No "we", "our session", "just now".
18
+ 4. Apply the durability test before saving: will this still be true and useful in a future session that knows nothing about this branch, commit, or session? If not, skip it. Do not store point-in-time values (migration indices, line numbers, file counts), commit hashes or branch history narratives (git is the source of truth), or anything re-derivable from the codebase faster than recalling it.
19
+ 5. Write for a future session with zero current context. No "we", "our session", "just now".
20
+ 6. Call thatch_extraction_done to mark the run complete. Do this even when nothing was worth saving: the parent session holds the buffered interactions until a completion signal arrives, and a memory write is not one on a no-save run.
21
+ 7. Your final message must be exactly: "Extraction complete." — do not list or summarize what you saved. The memories are in the store; the parent session does not need a report.
19
22
 
20
23
  ## What to extract
21
24
 
@@ -44,7 +44,7 @@ Sections, in order: SYNOPSIS, PROBLEM, BACKGROUND (conditional), PROPOSED APPROA
44
44
 
45
45
  **SYNOPSIS, PROBLEM, PROPOSED APPROACH, and ACCEPTANCE CRITERIA are mandatory.** A small ticket gets short sections, not fewer sections. Conditional sections appear only when warranted; omit them when empty. Never emit an empty header.
46
46
 
47
- **Scale the body to the work's conceptual size.** A one-line fix gets a one-line PROBLEM, a one-line PROPOSED APPROACH, and one or two acceptance criteria. A multi-PR feature gets full sections with real depth. The implementer should be able to start work after reading the ticket, but they should not have to read a novel to fix a guard clause.
47
+ **Scale the body to the work's complexity, not its size in files or lines.** Estimate complexity by how much new mental model the implementer must build (see the point-based budgets in Length and style). A one-line fix gets a one-line PROBLEM, a one-line PROPOSED APPROACH, and one or two acceptance criteria. A multi-PR feature gets full sections with real depth. The implementer should be able to start work after reading the ticket, but they should not have to read a novel to fix a guard clause. Most of this skill's guidance exists to tame large tickets; applying it wholesale to a small one produces exactly the bloat the skill warns against.
48
48
 
49
49
  ### SYNOPSIS
50
50
 
@@ -66,11 +66,13 @@ The reader should know what the problem is before they read why it happens.
66
66
 
67
67
  No solution yet. The reader should finish this section agreeing there is something to address, even with zero code knowledge.
68
68
 
69
- Cover three things, each in its own paragraph:
69
+ Cover three things, each in its own paragraph when the ticket has the complexity to warrant it:
70
70
  1. **What is wrong**, concretely. Name the component, the behavior, and the failure mode.
71
71
  2. **Why it matters.** Incident history, risk rating, customer impact, or "preventive: blocks a class of bug we keep almost-hitting."
72
72
  3. **Hazard rating and motivation.** How likely is the bad outcome, and under what conditions? Then answer "why now?": if the risk is low, what motivates this ticket? A prior PR identified it as a follow-up, the API still permits a failure shape, or the next planned change would make it worse. The lead's question is "should I prioritize this?" - give them the answer. _PLAT-135's ticket did this well: it named the cross-wait, then said "why it never fired" and rated the window as once-per-process cold load._
73
73
 
74
+ A small ticket covers all three in a single paragraph; split into one paragraph per topic only when the content earns it.
75
+
74
76
  If the problem is preventive (no current defect), say so plainly. Then say why it is worth fixing despite the low risk.
75
77
 
76
78
  ### BACKGROUND (conditional)
@@ -89,6 +91,8 @@ Omit when the problem is self-contained. A one-line "follow-up from #6838" can l
89
91
 
90
92
  The design concept. Not a full implementation plan. Enough that the implementer knows the direction: "load key material outside the mutex" not "restructure ActiveKey to use cachedKey/publish helpers with a publish function that zeroizes the loser's DEK." The implementer writes the implementation plan; the ticket sets the compass heading.
91
93
 
94
+ Present each mechanism in one short paragraph: what it does, plus the constraint that justifies its existence. Keep a justification when it answers an objection the reader will have (a lock that looks redundant, a retry that looks pointless). Compress failure-mode sagas to their consequence: "a leaked lock blocks every later attempt" carries what a paragraph of driver internals would. Reasons of the form "why not X" (no metric, no kill switch, no singleflight) go in NOTES as declined alternatives.
95
+
92
96
  For investigative tickets, this is "investigate X and propose a solution" rather than a concrete approach. Say so explicitly so the implementer knows the ticket expects analysis before implementation.
93
97
 
94
98
  This is where the ticket diverges most from a PR description. A PR description describes a change that already happened. A ticket proposes one that hasn't. The prose is prescriptive ("the fix should...") rather than descriptive ("this PR moves...").
@@ -105,7 +109,9 @@ Bad criteria are vague:
105
109
  - "improve the locking situation" (untestable)
106
110
  - "restructure ActiveKey" (implementation detail, not a behavior)
107
111
 
108
- If the criterion has a test shape, name it: "SetMaxOpenConns(1) with the connection held by a tx that also resolves the key." This gives the implementer the test design without dictating the implementation.
112
+ If a criterion's test design is non-obvious, name the test shape: "SetMaxOpenConns(1) with the connection held by a tx that also resolves the key." This gives the implementer the test design without dictating the implementation. "Tested explicitly" is not a test shape.
113
+
114
+ Keep the list coarse: three to seven criteria cover most tickets. This section defines "done"; it is not a test inventory. Merge parallel checks that verify the same behavior under different triggers. Drop criteria that restate PROPOSED APPROACH as a checklist - the criterion must add an observable gate, not echo the design.
109
115
 
110
116
  ### RELEASE PLAN (conditional)
111
117
 
@@ -115,7 +121,7 @@ Rollout strategy for tickets that change production behavior. Only when the chan
115
121
  - Deploy sequence (which services, in what order).
116
122
  - Rollback path (what to do if it goes wrong).
117
123
 
118
- A pure internal refactor with no operator-visible behavior change does not need this section. A migration that gates on a backfill completing does.
124
+ A pure internal refactor with no operator-visible behavior change does not need this section. Neither does a change whose rollout is a normal deploy: if the only available bullets say "nothing special happens," omit the section - its absence is the nothing-special signal. A migration that gates on a backfill completing, or a revert that is not free, does need it.
119
125
 
120
126
  ### VALIDATION (conditional)
121
127
 
@@ -129,10 +135,12 @@ Only when prod verification is non-trivial. A pure refactor with good tests does
129
135
 
130
136
  ### DEPENDENCIES (conditional)
131
137
 
132
- Blocking relationships and ordering constraints. "Blocked by PLAT-126" is the tracker field; the _why_ belongs here: "PLAT-125 partitions by location_fingerprint, so it can only run after backfill completes (count == 0), not merely after PLAT-124 merges." That kind of nuance is what prevents a well-intentioned lead from reordering the queue.
138
+ The tracker's blocker links carry the what; this section carries only what links cannot. Write a bullet when there is a _why_ the link cannot express: "PLAT-125 partitions by location_fingerprint, so it can only run after backfill completes (count == 0), not merely after PLAT-124 merges." That kind of nuance is what prevents a well-intentioned lead from reordering the queue. "Blocked by PLAT-126" with nothing to add is the tracker field - set the link there and omit the bullet.
133
139
 
134
140
  Distinguish hard data dependencies (cannot run until X completes) from soft dependencies (should run after X for cleanliness, but technically works independently). Say which kind it is.
135
141
 
142
+ If every bullet would restate a tracker link, omit the section and set the links.
143
+
136
144
  ### NOTES
137
145
 
138
146
  Always a `## NOTES` markdown header. Never a bare `Notes:` line ending a section.
@@ -195,12 +203,24 @@ These are hard rules, not suggestions.
195
203
 
196
204
  ## Length and style
197
205
 
198
- Target: the implementer can start work after one read. The lead can prioritize after reading SYNOPSIS and PROBLEM.
206
+ Target: the implementer can start work after one read. The lead can prioritize after reading SYNOPSIS and PROBLEM. Err on the shorter side.
207
+
208
+ Estimate the ticket's **complexity** (how much new mental model the implementer must build), not its size in files or lines. Complexity drives the word budget the way scrum points drive effort estimates:
209
+
210
+ - **1 point** (one guard, one flag, one constant): the implementer needs to know what to change, not a lesson in the subsystem. ~100-200 words. SYNOPSIS one line, PROBLEM 2-4 sentences (still rate the hazard, even if it is "no current defect"), PROPOSED APPROACH 1-2 sentences, one to three acceptance criteria.
211
+ - **2-3 points** (new function, restructured flow, multi-file refactor): the implementer needs a before/after they cannot infer from the code alone. ~200-400 words. Full mandatory sections; BACKGROUND only when prior work or project context earns it.
212
+ - **5 points** (new feature, migration sequence): the implementer needs a mental model they do not have yet. ~400-600 words. RELEASE PLAN, VALIDATION, and DEPENDENCIES when prod coordination warrants.
213
+ - **8 points** (multi-PR effort, subsystem redesign, constraint-dense work): ~800-1100 words. Complex tickets legitimately need more text; spend it on context and starting state, not on repetition. If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is probably a design doc wearing a ticket costume; split it.
199
214
 
200
- Scale the body to the work's conceptual size:
201
- - Small fix (one guard, one flag, one constant): one paragraph per mandatory section, one to three acceptance criteria. ~20-line ceiling.
202
- - Moderate change (new function, restructured flow, multi-file refactor): full sections with real depth. No hard ceiling, but each paragraph earns its place.
203
- - Large change (new feature, multi-PR effort, migration sequence): full sections plus RELEASE PLAN and VALIDATION. May run a full page. If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is probably a design doc wearing a ticket costume; split it.
215
+ Estimate by: how many new concepts the implementer must learn, whether the problem and approach have non-obvious mechanics, whether the work spans multiple workflows or systems. Not by files the change will touch or sections present. A mechanical multi-file rename is 1 point. A one-line fix with subtle semantics is 2-3.
216
+
217
+ **When a ticket runs long, take the section-level levers first.** Omit RELEASE PLAN, VALIDATION, DEPENDENCIES, or NOTES when they carry nothing a normal deploy or the tracker links already say. Coarsen ACCEPTANCE CRITERIA. Compress PROPOSED APPROACH failure sagas to their consequences. These levers remove duplication, not knowledge.
218
+
219
+ **Then step up the abstraction ladder.** Do not compress terminology or cut connective tissue to hit a word count. Instead, replace a sequence of mechanism details with the principle that subsumes them.
220
+
221
+ Example: instead of six claims about how a connection pool reuses connections, how session locks are session-scoped, how a lock taken by one statement does not guard the next, and how nothing reports the mismatch, write: "Some Postgres features attach to the connection, not the statement. The pool freely reuses connections between statements, so state set by one statement may not be there for the next, and nothing reports the mismatch." Same understanding, fewer words, no compression. The mechanism details belong in the linked design doc or the code itself, not in the ticket.
222
+
223
+ Brevity means fewer ideas, not compressed claims. If the ticket is still too long after the levers and the ladder, cut the lowest-value claim entirely. Do not cut the connective tissue that makes the remaining claims parse on first read. The knowledge build-up - each section introducing what the next one needs - survives every trim; what goes is redundancy.
204
224
 
205
225
  Inside sections, telegraphic bullet style is fine for criteria and plans: lowercase start, abbreviations (`w/`, `1x`, `~`), parenthetical shorthand. Full sentences for PROBLEM and BACKGROUND prose.
206
226
 
@@ -231,6 +251,8 @@ Read project context when the request, existing ticket, code comments, branch na
231
251
 
232
252
  Use available source material: linked tickets, linked PRs, nearby docs, code comments, commit messages, and project memory. Build a private term map: term -> plain behavior -> source. Do not dump the map into the ticket. Use it to write the shortest useful BACKGROUND.
233
253
 
254
+ When rewriting an existing ticket, mine the old text for facts - harm statements, platform behaviors, traps already discovered - but draft the new description from the sources, not from the old text. Editing the old ticket inherits its phrasing and its habits; the rewrite should stand on the plan docs, the code, and the mined facts.
255
+
234
256
  If a term or dependency controls the work and you cannot resolve it from available context, stop and ask the user. Do not guess. Do not write around the missing definition with vaguer words.
235
257
 
236
258
  Put project context in BACKGROUND only when it helps the implementer start work or helps a lead schedule it correctly. Omit generic backstory.
@@ -389,14 +411,14 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
389
411
  1. **Gather**: read the code the ticket references. Check `git log` for recent changes to the same files. Identify the problem, the prior work, and the proposed direction.
390
412
  2. **Search before create**: search Linear/Jira for likely duplicate tickets. If one exists, surface it before drafting or filing.
391
413
  3. **Codebase comparison**: verify the problem still exists in the current code. If it was already fixed, tell the user. If it was partially mitigated, note the mitigation.
392
- 4. **Read project context for planned work**: if the ticket is part of a larger project, depends on prior work, uses unexplained project terms, or has scheduling dependencies, read the linked tickets, PRs, docs, comments, and memory before drafting. Build a private term map. If a term or dependency controls the work and you cannot resolve it, ask the user.
414
+ 4. **Read project context for planned work**: if the ticket is part of a larger project, depends on prior work, uses unexplained project terms, or has scheduling dependencies, read the linked tickets, PRs, docs, comments, and memory before drafting. Build a private term map. If a term or dependency controls the work and you cannot resolve it, ask the user. When rewriting an existing ticket, mine the old text for facts but draft fresh.
393
415
  5. **Draft PROBLEM first**, in one breath, without looking at code. If you cannot state the harm in three sentences, you do not understand the problem yet. Go back and read.
394
416
  6. **Write BACKGROUND** (if needed): what prior work led here, what was done, what was left behind, and what project context an engineer new to this area needs to start.
395
- 7. **Write PROPOSED APPROACH**: the compass heading, not the map. Prescriptive voice.
396
- 8. **Write ACCEPTANCE CRITERIA**: observable, testable bullets. Name the test shape where relevant.
397
- 9. **Write RELEASE PLAN** (if needed): flags, ordering, rollback.
417
+ 7. **Write PROPOSED APPROACH**: the compass heading, not the map. Prescriptive voice. One short paragraph per mechanism, each with the constraint that justifies it; compress failure sagas to their consequence; move "why not X" reasoning to NOTES.
418
+ 8. **Write ACCEPTANCE CRITERIA**: observable, testable bullets. Coarse: three to seven, merging parallel checks and dropping restatements of the approach. Name the test shape only where the test design is non-obvious.
419
+ 9. **Write RELEASE PLAN** (if needed): flags, ordering, rollback. Omit when the rollout is a normal deploy.
398
420
  10. **Write VALIDATION** (if needed): monitoring, signals, smoke tests.
399
- 11. **Write DEPENDENCIES** (if needed): what blocks this, what this blocks, and why.
421
+ 11. **Write DEPENDENCIES** (if needed): only the why that tracker blocker links cannot express. Omit when the links carry it; set the links instead.
400
422
  12. **Write SYNOPSIS last** - it is a compression of PROBLEM + the core approach.
401
423
  13. **Bold and italicize the save points** in the allowed locations. Bold phrases that name the work (components, mechanisms, behaviors). Italicize phrases that orient to significance (conclusions, payoffs), at most one per paragraph. Apply the scan check.
402
424
  14. **Add NOTES** for non-goals, declined alternatives, and scope limits. Use `## NOTES` header. Omit if empty.
@@ -404,7 +426,7 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
404
426
  16. **Verify links**: check repo file links, external docs, tickets, PRs, and design artifacts where tooling allows. Do not claim an unverified link was verified.
405
427
  17. **Verify prose style and core principle**: check each rule in Prose style. Cut design-narration, authoring-sequence, and buzzwordy abstractions. Verify plain ASCII. Verify prescriptive voice (not descriptive). Verify first-use definitions for subsystem jargon in BACKGROUND or PROBLEM. Verify plain English nouns (no code-domain nouns in prose). Verify conclusion-first in PROBLEM. Verify NOTES bullets are either short pointers or self-contained.
406
428
  18. **Clarity pass**: read the draft as a cold lead skimming a backlog, then as an implementer picking up the work cold. Rewrite before returning it; do not tell the user you performed this pass. Check: does every project-specific term get plain-English meaning before or with the label? Are ambiguous terms qualified or replaced with specific behavior? Does every process word name object/action/effect? Does every contrast name current and proposed behavior? Does every causal sentence show the middle step? Did you preserve connective tissue instead of compressing meaning into noun stacks? If the draft got longer, cut a lower-value claim instead of compressing a high-value one.
407
- 19. **Verify length**: if the body exceeds what the work's conceptual size warrants, cut until each line earns its place. If PROPOSED APPROACH dominates, consider splitting into a design doc plus ticket.
429
+ 19. **Verify length**: estimate the ticket's complexity (see the point-based budgets in Length and style). If the body exceeds the budget, take the section-level levers first (omit conditional sections that duplicate the tracker or a normal deploy, coarsen acceptance criteria, compress approach sagas), then step up the abstraction ladder (replace mechanism sequences with the principle that subsumes them). If still over, cut the lowest-value claim entirely. Do not compress terminology or cut connective tissue to hit a word count. If PROPOSED APPROACH dominates, consider splitting into a design doc plus ticket.
408
430
 
409
431
  ## Anti-patterns
410
432
 
@@ -427,3 +449,7 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
427
449
  - **Conclusion buried at the end.** PROBLEM that builds to its point in the last sentence forces the reader to absorb the explanation before knowing what is being explained. Lead with the problem, then explain why.
428
450
  - **Hazard rating without motivation.** "No incident has hit this shape" answers "how bad?" but not "why prioritize this?" Answer "why now?" so the lead can decide whether to schedule it.
429
451
  - **NOTES bullets that are both wordy and terse.** A bullet that compresses a paragraph of rationale into one dense sentence reads as a private note. Either flag the thing and point to the full explanation, or write a self-contained sentence or two. Not the middle thing.
452
+ - **RELEASE PLAN that argues the release is boring.** If every bullet says the rollout is routine, omit the section. Its absence is the nothing-special signal.
453
+ - **DEPENDENCIES that restate tracker links.** "Blocked by PLAT-126" with nothing to add is the tracker field. Set the link; keep the section only for the why a link cannot express.
454
+ - **Test inventory as acceptance criteria.** A dozen bullets covering every code path is a test plan, not a definition of done. Keep three to seven observable gates; merge parallel checks.
455
+ - **Failure sagas in PROPOSED APPROACH.** Paragraphs of driver internals, pool lifecycle, or library quirks. Compress to the consequence; the implementer discovers the saga at the keyboard.
@@ -58,11 +58,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
58
58
  | `setup.ts` | `thatch setup --claude` installer. Writes .mcp.json, appends to CLAUDE.md (idempotent), installs hooks in settings.json, installs skills. |
59
59
  | `hygiene.ts` | Hygiene report: pending dedup pairs, stale count, orphaned branch memories. Shared by the plugin's session-start hook and the CLI's `thatch reminder` command. |
60
60
  | `git.ts` | Parse `owner/repo` from git remote. Worktree-safe fallback chain. |
61
- | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. |
61
+ | `db.ts` | SQLite schema, CRUD for entries/stores, brute-force cosine search (`search` = pure scoring, `recall` = search + telemetry stamping), dedup-pair verdict tracking. Prediction tables: matchers, predictions, edges, provenance. `scorePredictionNudge` is the shared auto-fire entry point for both host paths. |
62
62
  | `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
63
63
  | `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the extraction nudge carries. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `bin/thatch` imports `buildExtractionPayload`. `summarizeArgs` is used internally by `buildExtractionPayload`. |
64
64
  | `extract-queue.ts` | File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of `extraction.ts`. |
65
- | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Socket path is a hash of the DB path — both processes compute it independently. |
65
+ | `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles two methods: `match` (recall nudge) and `predictions` (prediction auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
66
66
  | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
67
67
  | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (18 skills: fact-extractor, dedup-classifier, project-primer, 6 review specialists, review synthesizer, review context, workflow research, change walkthrough, code walkthrough, session reflection, pr-description, ticket-description, split-overlarge-pr — work on both opencode and Claude Code) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
68
68
 
@@ -74,9 +74,9 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
74
74
  |------|-------------|
75
75
  | `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
76
76
  | `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
77
- | `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. |
77
+ | `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
78
78
  | `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
79
- | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge + JSON payload — the buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
79
+ | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge + JSON payload — the buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire: `db.scorePredictionNudge` scores the prompt against prediction matchers and injects a separate `[thatch] User decision model` nudge if any matchers clear the 0.45 threshold. Recall and prediction nudges fire independently (separate try/catch blocks, separate synthetic parts). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
80
80
  | `event` (`session.created`) | Sends the session-start reminder via `client.session.prompt`, carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. The session id is at `event.properties.info.id`. |
81
81
  | `dispose` | Closes the DB. |
82
82
 
@@ -121,6 +121,23 @@ Two of these hooks were dead for weeks because failures were invisible.
121
121
  case (agent forgot to save, next user turn arrives with buffer still
122
122
  pending). This is an experiment — model reliability on meta-instructions
123
123
  is uncertain.
124
+ 10. **Background completion narration is suppressed, not prevented.** When a
125
+ background sub-agent completes, opencode injects a `<task_result>` block
126
+ into the parent session and triggers a full model generation. Thatch
127
+ cannot cancel this turn (no pre-response hook). Two mitigations: the
128
+ fact-extractor skill's return value is constrained to the literal
129
+ "Extraction complete." so the injected block has no narratable content,
130
+ and the system prompt's "Background Task Completions" section instructs
131
+ the model not to narrate completions or treat them as approval to act.
132
+ 11. **Prediction engine is a statistical model, not an LLM call.** The query
133
+ (embed prompt, cosine-match against matchers, score linked predictions)
134
+ is mechanical — same shape as `thatch_memory_recall` but against different
135
+ tables. The agent drives formation and evaluation via tools
136
+ (`thatch_prediction_update` with confirm/disconfirm/soft/create signals)
137
+ guided by system prompt instructions. No wall-clock decay; confidence is
138
+ relevance-gated (being tested moves it, not being ignored). See the
139
+ prediction DB tables, auto-fire in `chat.message`, and the sideband
140
+ `predictions` method for the MCP path.
124
141
 
125
142
  ## Data flow
126
143
 
@@ -178,6 +195,33 @@ hygiene heartbeat (session start)
178
195
  longer exist (skipped when worktree isn't a git repo)
179
196
  → non-zero signals appended to the session-start reminder; the agent tends
180
197
  the store when convenient — the plugin never deletes memories itself
198
+
199
+ prediction cycle (agent-driven, statistical model)
200
+ → formation: agent calls thatch_prediction_update(matcher, prediction, signal)
201
+ with signal = create|confirm|disconfirm|soft. The tool embeds the matcher
202
+ text (raw, no header prepend — unlike memory_remember), finds or creates
203
+ a matcher (0.85 dedup), finds or creates a prediction (0.85 store-wide
204
+ dedup), links them via an edge, and adjusts confidence via a Bayesian
205
+ posterior: (confirm + K*P0) / (confirm + disconfirm + K), K=5, P0=0.5.
206
+ Soft signals count as 0.25 of a full signal. No wall-clock decay;
207
+ confidence is relevance-gated (being tested moves it, not being ignored).
208
+ → auto-fire (opencode): chat.message reuses the prompt embedding already
209
+ computed for the recall nudge. db.scorePredictionNudge([repo, global],
210
+ embedding, 0.45) finds matchers above threshold, follows edges to
211
+ predictions, scores by cosine * weight * confidence, dedups by
212
+ prediction_id, and returns top 5. Injects a separate synthetic part
213
+ with a [thatch] User decision model block. 0-evidence predictions use
214
+ "you may prefer"; predictions with evidence use "you tend to".
215
+ → auto-fire (Claude Code/Cursor): flush-tools fires the prediction query
216
+ via the sideband socket's `predictions` method in parallel with the
217
+ recall nudge. Same scorePredictionNudge entry point prevents scoring
218
+ drift between host paths.
219
+ → consumption: agent follows strong predictions silently, surfaces
220
+ ambiguous/competing predictions to the user, and calls
221
+ thatch_prediction_update to reinforce or weaken after the user responds.
222
+ thatch_prediction_list inspects the model with provenance;
223
+ thatch_prediction_delete removes bad predictions (cascade clears edges
224
+ and provenance).
181
225
  ```
182
226
 
183
227
  ## Database
@@ -188,7 +232,11 @@ hygiene heartbeat (session start)
188
232
  `entries(slug, store, label, content, embedding BLOB, model, branch,
189
233
  confidence, archived, created_at, updated_at, recall_count,
190
234
  last_recalled_at, PK(slug, store))`,
191
- `dedup_pairs(store, slug_a, slug_b, status, checked_at, PK(store, slug_a, slug_b))`.
235
+ `dedup_pairs(store, slug_a, slug_b, status, checked_at, PK(store, slug_a, slug_b))`,
236
+ `prediction_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
237
+ `predictions(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
238
+ `prediction_edges(matcher_id, prediction_id, weight REAL, PK(matcher_id, prediction_id), FK CASCADE)`,
239
+ `prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE)`.
192
240
  - `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
193
241
  databases by an idempotent column migration at init (`PRAGMA table_info` +
194
242
  `ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =
@@ -18,12 +18,13 @@ This document maps the feature parity and documents the remaining gaps.
18
18
 
19
19
  | Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
20
20
  |---------|----------------|------------------------|---------------------|--------|
21
- | **Tools** (9, single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | **Full** |
21
+ | **Tools** (13, single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | **Full** |
22
22
  | **System prompt** (store names, usage rules) | `experimental.chat.system.transform` — dynamic, repo baked in at runtime | CLAUDE.md static text appended by `thatch setup`; repo auto-detected by the MCP server at startup | AGENTS.md static text appended by `thatch setup`; repo auto-detected at startup | **Approximate** — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
23
23
  | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
24
24
  | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
25
25
  | **Extraction nudge** (buffer tool calls, inject JSON payload) | `tool.execute.after` buffers non-thatch, non-skill, non-task tool calls in-process; `chat.message` peeks the buffer (does not drain it) and injects a synthetic text part | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` peeks the queue and prints the nudge (does not drain — the queue persists until a memory write or `extraction_done`) | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Full** (file-backed) — timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor, not at the end of the current one like opencode's `chat.message` |
26
26
  | **Prompt-aware recall nudge** | `chat.message` hook embeds the prompt with the in-process warm model, searches `db.search()`, pushes a nudge part if matches ≥ threshold | `UserPromptSubmit` hook → `thatch flush-tools` connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — sideband socket gives cold hook processes access to the warm MCP server's model |
27
+ | **Prediction auto-fire** (user decision model) | `chat.message` hook scores the prompt embedding against prediction matchers; injects a `[thatch] User decision model` nudge alongside the recall nudge (separate synthetic part, same embedding call) | `UserPromptSubmit` hook → `thatch flush-tools` fires prediction query via the sideband socket's `predictions` method in parallel with the recall nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — same `scorePredictionNudge` entry point in both paths |
27
28
  | **Skills** | Installed to `$XDG_CONFIG_HOME/opencode/skills` at plugin init (shared **+** opencode-only) | Installed to `$CLAUDE_CONFIG_DIR/skills/` by `thatch setup` (shared only) | Installed to `~/.cursor/skills/` by `thatch setup` (shared only) | **Full** — same SKILL.md format; the code-review coordinator is opencode-only (needs sub-agents) |
28
29
  | **Store detection** (repo identity from git remote) | `worktree` parameter from the opencode plugin | `CLAUDE_PROJECT_DIR` env (set by Claude Code for stdio MCP servers) | `CURSOR_PROJECT_DIR` then `CLAUDE_PROJECT_DIR` then cwd | **Full** |
29
30
  | **Setup detection at startup** | n/a — plugin auto-installs at init | `checkSetup` in MCP server: detects missing or broken instructions in CLAUDE.md | Same as Claude Code (checks AGENTS.md) | **Full** — warns the agent to tell the user to run `thatch setup` |
@@ -28,7 +28,7 @@ file/IPC/CLI contracts ready to graduate into `bun:test` integration tests.
28
28
  | [UC-017](UC-017-buffer-tool-vs-buffer-batch.md) | `buffer-batch` (Claude, `tool_calls[]`+`session_id`) vs `buffer-tool` (Cursor, single+`conversation_id`) |
29
29
  | [UC-018](UC-018-mcp-startup-setup-detection.md) | MCP startup detects missing/broken setup instructions and warns the agent |
30
30
  | [UC-019](UC-019-archived-memory-lifecycle.md) | Archive, search exclusion, update guard, unarchive |
31
- | [UC-020](UC-020-extraction-escalation.md) | Nudge escalation (polite → insistent → ALL-CAPS), buffer persistence, `extraction_done` drain |
31
+ | [UC-020](UC-020-extraction-escalation.md) | Nudge escalation (polite → insistent → ALL-CAPS), buffer persistence, `extraction_done` accept/complete |
32
32
  | [UC-021](UC-021-prediction-autofire.md) | Prediction auto-fire: matcher scores prompt, prediction nudge injected alongside recall |
33
33
  | [UC-022](UC-022-prediction-dedup.md) | Prediction create-on-existing: dedup finds match, links edge without disconfirming |
34
34
  | [UC-023](UC-023-prediction-confidence-model.md) | Bayesian confidence math: create, confirm, soft (weak disconfirm), disconfirm signals |
@@ -18,13 +18,15 @@ client and a synthetic tool call sequence._
18
18
  - The agent loads `thatch-fact-extractor` and saves durable facts via
19
19
  `thatch_memory_remember` — or saves nothing if the activity was routine.
20
20
  - The buffer is **not** drained on nudge delivery — it persists until the agent
21
- writes a memory or calls `thatch_extraction_done`. If the nudge is ignored,
21
+ writes a memory or accepts it by calling `thatch_extraction_done`. Accepting
22
+ quiets the nudge while holding the entries until the extractor completes; a
23
+ child extractor that errors or is deleted requeues them. If the nudge is ignored,
22
24
  the next message carries a repeat nudge, escalating in urgency:
23
25
  - 1st-2nd miss: polite tone
24
26
  - 3rd consecutive miss (missedCount=2): insistent (directive) tone
25
27
  - 4th+ consecutive miss (missedCount>=3): ALL-CAPS tone
26
- The counter resets when the buffer drains (memory write or
27
- `thatch_extraction_done`).
28
+ The counter resets when the buffer drains (memory write) or is accepted
29
+ (`thatch_extraction_done`).
28
30
  - A `thatch_memory_remember` call in a child sub-agent session also drains the
29
31
  parent's buffer (via the `childToParent` Map), so dispatching the
30
32
  fact-extractor as a background task clears the parent's queue.
@@ -1,6 +1,6 @@
1
1
  # UC-020: Extraction nudge escalation and acknowledgment
2
2
 
3
- _Automatable: missedNudges counter + escalation tiers + extraction_done drain
3
+ _Automatable: missedNudges counter + escalation tiers + extraction_done accept
4
4
  are testable with the mock client and a synthetic tool call sequence._
5
5
 
6
6
  **Preconditions**
@@ -27,10 +27,12 @@ are testable with the mock client and a synthetic tool call sequence._
27
27
  persists until a memory is written or `extraction_done` is called. The nudge
28
28
  repeats each turn with escalated tone (and the payload grows if new tool
29
29
  activity adds entries between nudges).
30
- - Step 6: `thatch_extraction_done` drains the buffer and resets the
31
- `missedNudges` counter. The tool returns `"[acknowledged] extraction buffer
32
- drained"`.
33
- - Step 7: no extraction nudge appears (buffer is empty). If the user prompt
30
+ - Step 6: `thatch_extraction_done` in the parent accepts the buffer (moves it
31
+ to a holding area) and resets the `missedNudges` counter. The tool returns
32
+ `"[acknowledged]"`. Held entries drop when the extractor completes (child
33
+ memory write, child `extraction_done`, or child idle); if the child errors
34
+ or is deleted first, the entries requeue and the nudge replays.
35
+ - Step 7: no extraction nudge appears (buffer is accepted). If the user prompt
34
36
  semantically matches existing memories, a recall nudge may appear instead.
35
37
  - Alternative drain path: if a child sub-agent (dispatched via the `task`
36
38
  tool) writes a memory via `thatch_memory_remember`, the parent's buffer is
@@ -108,6 +108,15 @@ Stores are created automatically — no setup required.
108
108
  |------|-------------|
109
109
  | `thatch_extraction_done` | Acknowledge that the extraction nudge was dispatched to a sub-agent. Drains the buffer so the nudge does not replay. Called after dispatching a background task to run the fact-extractor skill. |
110
110
 
111
+ ### Prediction tools
112
+
113
+ | Tool | What it does |
114
+ |------|-------------|
115
+ | `thatch_prediction_query` | Query the user decision model for scored predictions matching a context. Returns predictions with confidence and evidence count. |
116
+ | `thatch_prediction_update` | Create, reinforce, or weaken a prediction. Takes a matcher (context description), a prediction (preference statement), and a signal (confirm/disconfirm/soft/create). |
117
+ | `thatch_prediction_list` | List all predictions in a store with matchers, confidence, and provenance. |
118
+ | `thatch_prediction_delete` | Delete a prediction by semantic match. Edges and provenance are cascade-deleted. |
119
+
111
120
  ## Automatic behaviors
112
121
 
113
122
  Beyond the tools, thatch hooks into opencode itself:
@@ -133,6 +142,12 @@ Beyond the tools, thatch hooks into opencode itself:
133
142
  thatch never saves memories on its own.
134
143
  - **Compaction context.** When opencode compacts a long session, thatch injects
135
144
  a reminder so the summarized session still knows memory tools exist.
145
+ - **Prediction auto-fire.** When a prompt matches learned contexts, thatch
146
+ injects a `User decision model` block alongside the recall nudge. The block
147
+ lists scored predictions (confidence, evidence count) that the agent can
148
+ follow silently (strong prediction), surface to the user (ambiguous), or
149
+ use to update the model after the user responds. No extra model call —
150
+ the prediction search reuses the same prompt embedding as the recall nudge.
136
151
 
137
152
  ### Setup detection (Claude Code and Cursor)
138
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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/extraction.ts CHANGED
@@ -91,6 +91,7 @@ export function buildExtractionPayload(
91
91
  */
92
92
  export class ExtractionPipeline {
93
93
  #buffers = new Map<string, ToolInteraction[]>();
94
+ #accepted = new Map<string, ToolInteraction[]>();
94
95
  #maxBuffer = 20;
95
96
 
96
97
  /** Record a tool execution for later extraction. */
@@ -110,6 +111,46 @@ export class ExtractionPipeline {
110
111
  this.#buffers.delete(sessionID);
111
112
  }
112
113
 
114
+ /**
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 —
117
+ * the entries are not dropped until a completion signal arrives (the
118
+ * extractor writes a memory, calls extraction_done, or goes idle). If the
119
+ * extractor dies first, requeueAccepted puts them back.
120
+ */
121
+ accept(sessionID: string): void {
122
+ const buf = this.#buffers.get(sessionID) ?? [];
123
+ if (buf.length === 0) return;
124
+ this.#buffers.delete(sessionID);
125
+ const existing = this.#accepted.get(sessionID) ?? [];
126
+ this.#accepted.set(sessionID, [...existing, ...buf]);
127
+ }
128
+
129
+ /** Drop accepted entries: the extractor finished (save or no-save). */
130
+ completeAccepted(sessionID: string): void {
131
+ this.#accepted.delete(sessionID);
132
+ }
133
+
134
+ /**
135
+ * Move accepted entries back to pending: the extractor errored or its
136
+ * session was deleted before completing, so the facts were never
137
+ * processed. Prepended so they replay in original order. Not capped at
138
+ * maxBuffer — a silent drop here would defeat the point of requeueing;
139
+ * the next push trims back to the cap.
140
+ */
141
+ requeueAccepted(sessionID: string): void {
142
+ const accepted = this.#accepted.get(sessionID);
143
+ if (!accepted || accepted.length === 0) return;
144
+ this.#accepted.delete(sessionID);
145
+ const buf = this.#buffers.get(sessionID) ?? [];
146
+ this.#buffers.set(sessionID, [...accepted, ...buf]);
147
+ }
148
+
149
+ /** Returns the session's accepted (held) interactions. Test/introspection. */
150
+ peekAccepted(sessionID: string): ToolInteraction[] {
151
+ return this.#accepted.get(sessionID) ?? [];
152
+ }
153
+
113
154
  /**
114
155
  * Remove entries that were in the snapshot (by reference identity), keeping
115
156
  * any that arrived after the snapshot was taken. Used by the child-parent
package/src/index.ts CHANGED
@@ -51,9 +51,12 @@ export const server: Plugin = async ({ client, worktree }) => {
51
51
  // Sessions currently being compacted. chat.message nudges are skipped while
52
52
  // a session is in this set — the agent can't call tools during summary
53
53
  // generation, so a recall or extraction nudge would cause a blocked-tool
54
- // error. Cleared by experimental.compaction.autocontinue. If compaction
55
- // fails, the entry leaks (graceful degradation: nudges stay off for that
56
- // session, but no crash).
54
+ // error. Cleared by experimental.compaction.autocontinue (success), the
55
+ // session.compacted event (redundant belt-and-suspenders), or chat.message
56
+ // itself when a non-compaction message arrives (compaction failure fallback
57
+ // — autocontinue never fired, so the next real user message clears the
58
+ // stale flag). If all three somehow miss, the flag leaks (graceful
59
+ // degradation: nudges stay off for that session, but no crash).
57
60
  const compacting = new Set<string>();
58
61
 
59
62
  // Per-session count of consecutive extraction nudges delivered without any
@@ -126,27 +129,34 @@ export const server: Plugin = async ({ client, worktree }) => {
126
129
  // agent writes a memory, so ignored nudges accumulate and the payload
127
130
  // grows with each missed cycle.
128
131
  //
129
- // A memory_remember call in a child session (sub-agent) also drains
130
- // the parent's buffer — but only the entries that existed at dispatch
131
- // time (the snapshot). Entries from interleaved turns survive so their
132
- // facts aren't silently dropped. Without this, dispatching the
133
- // fact-extractor as a background task writes memories in the child but
134
- // never clears the parent's queue — the nudge replays every turn.
132
+ // Buffer lifecycle is AMQP-style accept/complete, so a failed
133
+ // extractor does not silently drop unprocessed interactions:
134
+ // - thatch_extraction_done in the parent ACCEPTS the buffer: it moves
135
+ // to a holding area and the nudge quiets, but entries are not
136
+ // dropped yet.
137
+ // - COMPLETE drops the held entries. Signals: a memory_remember or
138
+ // extraction_done call in a child session of that parent (the
139
+ // extractor confirming it processed them), or the child going idle
140
+ // (a no-save run that never acks still counts as processed).
141
+ // - REQUEUE returns the held entries to pending. Signals: the child
142
+ // session errors or is deleted before completing.
135
143
  //
136
- // thatch_extraction_done is an explicit acknowledgment tool the model
137
- // calls in the parent session after dispatching. Covers the case where
138
- // the sub-agent errors out or the host doesn't expose parent-child
139
- // session relationships.
144
+ // A memory_remember call in a child session (sub-agent) also drains
145
+ // the parent's pending buffer — but only the entries that existed at
146
+ // dispatch time (the snapshot). Entries from interleaved turns survive
147
+ // so their facts aren't silently dropped.
140
148
  "tool.execute.after": async (input, output) => {
141
149
  if (input.tool === "thatch_memory_remember") {
142
150
  extraction.consume(input.sessionID);
143
151
  missedNudges.delete(input.sessionID);
144
- // If this is a child session, drain only the parent's snapshot entries
145
- // (those present at dispatch time). Interleaved-turn entries survive.
146
- // If no snapshot was recorded (e.g. session.created event never
147
- // arrived), fall back to draining the entire parent buffer.
152
+ // If this is a child session, complete the parent's accepted entries
153
+ // (the extractor confirmed it is alive and saving) and drain the
154
+ // parent's snapshot entries from the pending buffer. If no snapshot
155
+ // was recorded (e.g. session.created event never arrived), fall back
156
+ // to draining the entire parent buffer.
148
157
  const parentID = childToParent.get(input.sessionID);
149
158
  if (parentID) {
159
+ extraction.completeAccepted(parentID);
150
160
  const snapshot = parentSnapshots.get(input.sessionID);
151
161
  if (snapshot) {
152
162
  extraction.consumeSnapshot(parentID, snapshot);
@@ -158,11 +168,21 @@ export const server: Plugin = async ({ client, worktree }) => {
158
168
  }
159
169
  return;
160
170
  }
161
- // Explicit acknowledgment tool drains the buffer without requiring a
162
- // memory write. The model calls this after dispatching the
163
- // fact-extractor to a sub-agent.
171
+ // thatch_extraction_done has two roles depending on the session:
172
+ // - parent, after dispatching the fact-extractor: ACCEPT the buffer
173
+ // (quiet the nudge, hold the entries for completion).
174
+ // - child extractor, at the end of its run: COMPLETE the parent's
175
+ // accepted entries, including no-save runs that write no memory,
176
+ // and drop the child's own buffer (its work is done).
164
177
  if (input.tool === "thatch_extraction_done") {
165
- extraction.consume(input.sessionID);
178
+ const parentID = childToParent.get(input.sessionID);
179
+ if (parentID) {
180
+ extraction.completeAccepted(parentID);
181
+ missedNudges.delete(parentID);
182
+ extraction.consume(input.sessionID);
183
+ } else {
184
+ extraction.accept(input.sessionID);
185
+ }
166
186
  missedNudges.delete(input.sessionID);
167
187
  return;
168
188
  }
@@ -192,7 +212,18 @@ export const server: Plugin = async ({ client, worktree }) => {
192
212
  // a summary, so a nudge that says "use thatch_memory_recall" triggers a
193
213
  // blocked-tool error.
194
214
  "chat.message": async (input, output) => {
195
- if (compacting.has(input.sessionID)) return;
215
+ if (compacting.has(input.sessionID)) {
216
+ // The session is marked as compacting. If this message is the
217
+ // compaction summary generation itself (has a compaction-type part),
218
+ // suppress nudges — tools are blocked during summary generation and a
219
+ // nudge would cause a blocked-tool error. If it is NOT a compaction
220
+ // message, compaction has already failed (autocontinue never fired)
221
+ // and the user is sending a new message. Clear the stale flag and
222
+ // proceed normally — tools are available again.
223
+ const isCompactionMsg = (output.parts as any[]).some((p) => p.type === "compaction");
224
+ if (isCompactionMsg) return;
225
+ compacting.delete(input.sessionID);
226
+ }
196
227
  if (extraction.pending(input.sessionID)) {
197
228
  const batch = extraction.peek(input.sessionID);
198
229
  const payload = extraction.buildPayload(batch, repo);
@@ -276,7 +307,13 @@ export const server: Plugin = async ({ client, worktree }) => {
276
307
  // snapshots the parent's current buffer — so the child's later
277
308
  // memory_remember drains only those snapshot entries, not the parent's
278
309
  // entire buffer (which may have grown from interleaved turns).
279
- // session.deleted cleans both maps to avoid unbounded growth.
310
+ //
311
+ // The accepted-buffer lifecycle completes or requeues on child lifecycle
312
+ // events: idle means the extractor finished (a no-save run writes no
313
+ // memory but still processed the payload); error or deletion before
314
+ // completion means the facts were never extracted, so the entries go
315
+ // back to pending. session.deleted also cleans both maps to avoid
316
+ // unbounded growth.
280
317
  event: async ({ event }) => {
281
318
  if (event.type === "session.created") {
282
319
  const info = event.properties.info;
@@ -285,9 +322,39 @@ export const server: Plugin = async ({ client, worktree }) => {
285
322
  parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
286
323
  }
287
324
  }
325
+ if (event.type === "session.error") {
326
+ const childID = event.properties.sessionID;
327
+ const parentID = childID ? childToParent.get(childID) : undefined;
328
+ if (parentID) extraction.requeueAccepted(parentID);
329
+ return;
330
+ }
331
+ if (event.type === "session.status" && event.properties.status?.type === "idle") {
332
+ const childID = event.properties.sessionID;
333
+ const parentID = childID ? childToParent.get(childID) : undefined;
334
+ if (parentID) {
335
+ extraction.completeAccepted(parentID);
336
+ missedNudges.delete(parentID);
337
+ }
338
+ return;
339
+ }
288
340
  if (event.type === "session.deleted") {
289
- childToParent.delete(event.properties.info.id);
290
- parentSnapshots.delete(event.properties.info.id);
341
+ const id = event.properties.info.id;
342
+ // A child deleted before completing never processed its payload.
343
+ const parentID = childToParent.get(id);
344
+ if (parentID) extraction.requeueAccepted(parentID);
345
+ childToParent.delete(id);
346
+ parentSnapshots.delete(id);
347
+ // A deleted parent takes its accepted entries with it.
348
+ extraction.completeAccepted(id);
349
+ return;
350
+ }
351
+
352
+ // session.compacted fires on successful compaction. Redundant with
353
+ // experimental.compaction.autocontinue, but belt-and-suspenders: if
354
+ // autocontinue didn't fire or wasn't installed, the event still clears
355
+ // the compacting flag so nudges resume.
356
+ if (event.type === "session.compacted") {
357
+ compacting.delete(event.properties.sessionID);
291
358
  return;
292
359
  }
293
360
 
package/src/prompts.ts CHANGED
@@ -54,7 +54,9 @@ workflows. The host auto-discovers them, but reach for them proactively:
54
54
 
55
55
  **One signal is enough.** Do not wait for confirmation.
56
56
  Save immediately on: feedback, preferences, corrections, emotional signals,
57
- new project knowledge, conventions, architectural decisions, investigation results.
57
+ new project knowledge, conventions, architectural decisions, investigation
58
+ results that produced durable knowledge (not point-in-time facts about the
59
+ current state of the code).
58
60
 
59
61
  ## Before Responding
60
62
 
@@ -64,6 +66,16 @@ whether you've discovered knowledge worth persisting. Use thatch_memory_recall
64
66
  to check for duplicates, then thatch_memory_remember for new findings. Then
65
67
  deliver your response.
66
68
 
69
+ ## Background Task Completions
70
+
71
+ When a background task completes and its result is injected into the session,
72
+ the completion is informational — it is not user input and not approval to
73
+ proceed with anything you were discussing. Do not narrate what the task did or
74
+ summarize its output. If you were mid-conversation with the user, continue
75
+ that thread. If the user's last message has been fully addressed, stop and
76
+ wait. Do not treat the completion as a trigger to advance work the user has
77
+ not greenlit.
78
+
67
79
  ## User Decision Model
68
80
 
69
81
  A statistical model of the user's decision-making preferences is maintained by
@@ -100,8 +112,9 @@ reinforce or adjust it when possible.
100
112
  component relationships, conventions, operational playbooks
101
113
  - **Branch-scoped**: branch purpose, feature design decisions, PR status,
102
114
  temporary workarounds
103
- - **Insights**: ALWAYS save non-obvious gotchas, patterns, and architectural
104
- lessons discovered during implementation. Never prompt the user; just do it.
115
+ - **Insights**: Save non-obvious gotchas, patterns, and architectural
116
+ lessons discovered during implementation, but only if they remain true
117
+ across sessions. Never prompt the user; just do it.
105
118
 
106
119
  ## How to Write
107
120
 
@@ -116,6 +129,16 @@ reinforce or adjust it when possible.
116
129
  Session-specific context, incomplete/unverified info, anything already in
117
130
  CLAUDE.md or OPENCODE.md, speculative conclusions.
118
131
 
132
+ Apply the durability test: will this still be true and useful in a future
133
+ session that knows nothing about this branch, commit, or session? If not,
134
+ do not save it. Do not store:
135
+ - Point-in-time values (migration indices, line numbers, file counts) that
136
+ will be stale by next session
137
+ - Commit hashes and branch-specific history narratives. Git is the source
138
+ of truth for this; do not duplicate it in memory
139
+ - Anything re-derivable from the codebase faster than recalling it (current
140
+ file locations, type signatures, function names)
141
+
119
142
  ## Archived Memories
120
143
 
121
144
  Memories can be marked \`archived\` — a flag for stable, long-term historical
@@ -228,7 +251,9 @@ workflows. The host auto-discovers them, but reach for them proactively:
228
251
 
229
252
  **One signal is enough.** Do not wait for confirmation.
230
253
  Save immediately on: feedback, preferences, corrections, emotional signals,
231
- new project knowledge, conventions, architectural decisions, investigation results.
254
+ new project knowledge, conventions, architectural decisions, investigation
255
+ results that produced durable knowledge (not point-in-time facts about the
256
+ current state of the code).
232
257
 
233
258
  ## Before Responding
234
259
 
@@ -274,8 +299,9 @@ reinforce or adjust it when possible.
274
299
  component relationships, conventions, operational playbooks
275
300
  - **Branch-scoped**: branch purpose, feature design decisions, PR status,
276
301
  temporary workarounds
277
- - **Insights**: ALWAYS save non-obvious gotchas, patterns, and architectural
278
- lessons discovered during implementation. Never prompt the user; just do it.
302
+ - **Insights**: Save non-obvious gotchas, patterns, and architectural
303
+ lessons discovered during implementation, but only if they remain true
304
+ across sessions. Never prompt the user; just do it.
279
305
 
280
306
  ## How to Write
281
307
 
@@ -290,6 +316,16 @@ reinforce or adjust it when possible.
290
316
  Session-specific context, incomplete/unverified info, anything already in
291
317
  CLAUDE.md, speculative conclusions.
292
318
 
319
+ Apply the durability test: will this still be true and useful in a future
320
+ session that knows nothing about this branch, commit, or session? If not,
321
+ do not save it. Do not store:
322
+ - Point-in-time values (migration indices, line numbers, file counts) that
323
+ will be stale by next session
324
+ - Commit hashes and branch-specific history narratives. Git is the source
325
+ of truth for this; do not duplicate it in memory
326
+ - Anything re-derivable from the codebase faster than recalling it (current
327
+ file locations, type signatures, function names)
328
+
293
329
  ## Archived Memories
294
330
 
295
331
  Memories can be marked \`archived\` — a flag for stable, long-term historical
@@ -390,7 +426,9 @@ export function extractionNudge(
390
426
  return `[thatch] ${dispatch} to run the thatch-fact-extractor skill ` +
391
427
  `on ${count} queued tool interaction${plural}. ` +
392
428
  `${acknowledge} ` +
393
- `Then answer the user. ` +
429
+ `This is a system nudge, not user input. After dispatching and acknowledging, ` +
430
+ `stop: do not advance pending work on this nudge's account. If you are ` +
431
+ `waiting for the user to respond, continue waiting. ` +
394
432
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
395
433
  }
396
434
 
@@ -464,7 +502,9 @@ workflows. The host auto-discovers them, but reach for them proactively:
464
502
 
465
503
  **One signal is enough.** Do not wait for confirmation.
466
504
  Save immediately on: feedback, preferences, corrections, emotional signals,
467
- new project knowledge, conventions, architectural decisions, investigation results.
505
+ new project knowledge, conventions, architectural decisions, investigation
506
+ results that produced durable knowledge (not point-in-time facts about the
507
+ current state of the code).
468
508
 
469
509
  ## Before Responding
470
510
 
@@ -510,8 +550,9 @@ reinforce or adjust it when possible.
510
550
  component relationships, conventions, operational playbooks
511
551
  - **Branch-scoped**: branch purpose, feature design decisions, PR status,
512
552
  temporary workarounds
513
- - **Insights**: ALWAYS save non-obvious gotchas, patterns, and architectural
514
- lessons discovered during implementation. Never prompt the user; just do it.
553
+ - **Insights**: Save non-obvious gotchas, patterns, and architectural
554
+ lessons discovered during implementation, but only if they remain true
555
+ across sessions. Never prompt the user; just do it.
515
556
 
516
557
  ## How to Write
517
558
 
@@ -526,6 +567,16 @@ reinforce or adjust it when possible.
526
567
  Session-specific context, incomplete/unverified info, anything already in
527
568
  AGENTS.md, speculative conclusions.
528
569
 
570
+ Apply the durability test: will this still be true and useful in a future
571
+ session that knows nothing about this branch, commit, or session? If not,
572
+ do not save it. Do not store:
573
+ - Point-in-time values (migration indices, line numbers, file counts) that
574
+ will be stale by next session
575
+ - Commit hashes and branch-specific history narratives. Git is the source
576
+ of truth for this; do not duplicate it in memory
577
+ - Anything re-derivable from the codebase faster than recalling it (current
578
+ file locations, type signatures, function names)
579
+
529
580
  ## Archived Memories
530
581
 
531
582
  Memories can be marked \`archived\` — a flag for stable, long-term historical
package/src/tool-defs.ts CHANGED
@@ -352,29 +352,35 @@ const markCheckedDef: ToolDef = {
352
352
  };
353
353
 
354
354
  /**
355
- * Acknowledge that the extraction nudge has been handled. Drains the
356
- * extraction buffer so the nudge does not replay on the next turn.
355
+ * Extraction-buffer acknowledgment, with AMQP-style accept/complete roles.
357
356
  *
358
- * The actual draining happens in the host's post-tool hook
357
+ * Called in a PARENT session after dispatching the fact-extractor, it accepts
358
+ * the buffer: entries move to a holding area and the nudge quiets, but they
359
+ * are not dropped until the extractor completes. Called in a CHILD extractor
360
+ * at the end of its run, it completes the parent's accepted entries —
361
+ * including no-save runs that write no memory. If the child errors or is
362
+ * deleted before either signal, the host requeues the entries so the facts
363
+ * are not lost.
364
+ *
365
+ * The actual state changes happen in the host's post-tool hook
359
366
  * (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) —
360
367
  * this tool's execute function is a no-op confirmation. The tool exists so
361
- * the model can call it in the parent session after dispatching the
362
- * fact-extractor to a sub-agent, giving the hook a recognizable tool name
363
- * to key on. Without it, the buffer only drains when a memory_remember
364
- * call lands in the same session (or a tracked child session), which fails
365
- * if the sub-agent errors out or the host doesn't expose parent-child
366
- * session relationships.
368
+ * the model has a recognizable tool name to key on. In the MCP path the
369
+ * file-backed queue is consumed on this call (or on any memory_remember),
370
+ * which is durable across interruption because the queue persists on disk
371
+ * until then.
367
372
  */
368
373
  const extractionDoneDef: ToolDef = {
369
374
  name: "extraction_done",
370
375
  description:
371
- "Acknowledge that the extraction nudge has been dispatched to a sub-agent. " +
372
- "Drains the extraction buffer so the nudge does not replay. " +
373
- "Call this in the parent session after dispatching a background task " +
374
- "to run the thatch-fact-extractor skill.",
376
+ "Acknowledge extraction-buffer work. In a parent session, call after " +
377
+ "dispatching the fact-extractor to a sub-agent: accepts the buffer " +
378
+ "(quiets the nudge) while keeping entries until the extractor completes. " +
379
+ "In the fact-extractor sub-agent, call at the end of the run to mark the " +
380
+ "entries complete, even when nothing was worth saving.",
375
381
  args: {},
376
382
  async execute() {
377
- return "[acknowledged] extraction buffer drained";
383
+ return "[acknowledged]";
378
384
  },
379
385
  };
380
386