@jeffober/thatch 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +24 -10
  2. package/artifacts/skills/thatch-code-review.md +9 -7
  3. package/artifacts/skills/thatch-review-economy.md +97 -0
  4. package/artifacts/skills/thatch-review-followup.md +1 -1
  5. package/artifacts/skills/thatch-review-synthesizer.md +15 -5
  6. package/bin/qa-run +38 -0
  7. package/bin/release +21 -0
  8. package/bin/thatch +17 -5
  9. package/docs/dev/README.md +71 -18
  10. package/docs/dev/features/README.md +33 -0
  11. package/docs/dev/features/behavior-engine.md +271 -0
  12. package/docs/dev/features/cicd.md +89 -0
  13. package/docs/dev/features/cli.md +81 -0
  14. package/docs/dev/features/compaction-recovery.md +48 -0
  15. package/docs/dev/features/database.md +244 -0
  16. package/docs/dev/features/deduplication.md +102 -0
  17. package/docs/dev/features/extraction.md +253 -0
  18. package/docs/dev/features/hygiene.md +81 -0
  19. package/docs/dev/features/memory-store.md +154 -0
  20. package/docs/dev/features/multi-host.md +228 -0
  21. package/docs/dev/features/nudge-pipeline.md +282 -0
  22. package/docs/dev/features/prediction-engine.md +253 -0
  23. package/docs/dev/features/qa-system.md +205 -0
  24. package/docs/dev/features/session-lifecycle.md +110 -0
  25. package/docs/dev/features/setup.md +199 -0
  26. package/docs/dev/features/sideband.md +104 -0
  27. package/docs/dev/gotchas.md +3 -3
  28. package/docs/dev/mcp-parity.md +6 -4
  29. package/docs/dev/setup-and-hooks.md +4 -4
  30. package/docs/dev/skills.md +10 -8
  31. package/docs/plans/README.md +2 -2
  32. package/docs/user/README.md +58 -20
  33. package/docs/user/behavior-engine.md +85 -0
  34. package/docs/user/cli.md +98 -0
  35. package/docs/user/code-review.md +73 -0
  36. package/docs/user/deduplication.md +70 -0
  37. package/docs/user/default-behaviors.md +75 -0
  38. package/docs/user/extraction.md +74 -0
  39. package/docs/user/hygiene.md +76 -0
  40. package/docs/user/memory.md +81 -0
  41. package/docs/user/prediction-engine.md +87 -0
  42. package/docs/user/setup.md +110 -0
  43. package/docs/user/skills.md +97 -0
  44. package/package.json +2 -2
  45. package/src/behavior.ts +120 -0
  46. package/src/db.ts +160 -417
  47. package/src/embeddings.ts +0 -41
  48. package/src/extraction.ts +2 -2
  49. package/src/git.ts +3 -1
  50. package/src/index.ts +55 -9
  51. package/src/mcp.ts +30 -1
  52. package/src/prediction.ts +127 -0
  53. package/src/prompts.ts +135 -188
  54. package/src/scoring-engine.ts +374 -0
  55. package/src/seed-behaviors.ts +339 -0
  56. package/src/setup.ts +2 -6
  57. package/src/sideband.ts +36 -8
  58. package/src/skills.ts +1 -0
  59. package/src/tool-defs.ts +172 -25
  60. package/src/vector-math.ts +30 -0
  61. package/docs/qa/README.md +0 -175
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/sysread/thatch/actions/workflows/ci.yml/badge.svg)](https://github.com/sysread/thatch/actions/workflows/ci.yml)
4
4
 
5
- Persistent memory and useful dev skills for AI coding agents. Works with
5
+ Persistent memory and operational methodology for AI coding agents. Works with
6
6
  **OpenCode** (as a plugin), **Claude Code** (as a local MCP server), and
7
7
  **Cursor** (as a local MCP server).
8
8
 
@@ -31,7 +31,8 @@ Without this env var, extraction still works - the child session runs
31
31
  synchronously (fire-and-forget) instead of asynchronously.
32
32
 
33
33
  Then **prime your project memory** by running `thatch prime` in your project directory.
34
- This will launch an `opencode` session to build an initial map of the code base and its architecture to seed the memory.
34
+ This launches an `opencode` session to build an initial map of the code base
35
+ and seed the memory.
35
36
 
36
37
  ### Claude Code and Cursor
37
38
 
@@ -65,14 +66,27 @@ Include thatch's instructions in your agent's system prompt manually; see
65
66
 
66
67
  ## What's inside
67
68
 
68
- - **Memory tools**: save, search, list, show, and forget memories with
69
- semantic search (local embeddings, SQLite, brute-force cosine).
70
- - **Prediction engine**: a statistical model of user decision-making
69
+ Thatch gives your agent three layers of persistent context:
70
+
71
+ - **Memory** -- save, search, and recall knowledge across sessions with
72
+ local embeddings (bge-small-en-v1.5) and SQLite. Every project gets its
73
+ own store; a shared global store holds cross-project knowledge. The agent
74
+ writes and reads memories through tools -- thatch never saves anything on
75
+ its own.
76
+ - **Prediction engine** -- a statistical model of the user's decision-making
71
77
  preferences. When a prompt matches learned contexts, predictions fire
72
- alongside the recall nudge.
73
- - **Skills**: 23 skills for memory workflows, structured multi-specialist
74
- code review, review response, change walkthroughs, and writing tasks (PR
75
- descriptions, tickets, PR splitting).
78
+ alongside the recall nudge. Confidence is graded (Bayesian posterior) and
79
+ reinforced or weakened by user feedback. The agent follows strong
80
+ predictions silently and surfaces ambiguous ones to the user.
81
+ - **Behavior engine** -- a self-discipline model where the agent codifies
82
+ its own operational rules ("when X, I do Y"). Rules auto-fire when similar
83
+ situations arise. The agent ham/spams each surfaced rule to train the
84
+ classifier. Confidence adjusts the same way as predictions. An anti-laziness
85
+ guard in the prompt prevents the agent from codifying shortcuts.
86
+
87
+ Plus **24 skills** for memory workflows, structured multi-specialist code
88
+ review, review response, change and feature walkthroughs, and writing tasks
89
+ (PR descriptions, tickets, PR splitting).
76
90
 
77
91
  See the [user guide](docs/user/README.md) for the full tool list, CLI
78
92
  commands, configuration, environment variables, and detailed setup for each
@@ -98,7 +112,7 @@ embeddings, no network.
98
112
  - [User guide](docs/user/README.md) -- setup, tools, configuration, CLI
99
113
  - [Development](docs/dev/README.md) -- architecture, module responsibilities
100
114
  - [MCP parity](docs/dev/mcp-parity.md) -- OpenCode plugin vs MCP feature comparison
101
- - [QA & tests](docs/qa/README.md) -- test conventions, use cases
115
+ - [QA tests](tests/qa/) -- executable use cases (auto/ and live/ subdirs)
102
116
  - [Design docs](docs/plans/) -- design-decision snapshots and in-progress plans
103
117
 
104
118
  ## License
@@ -53,7 +53,7 @@ Dispatch a sub-agent using the Task tool to research the code workflows affected
53
53
 
54
54
  The sub-agent produces a code guide with one section per affected workflow or feature. Wait for it to complete and collect the guide.
55
55
 
56
- This guide gives specialists the code-level context (purpose, data flow, evolutions, constraints) they need to avoid false positives about intentional behavior and long-standing design decisions, and reduces duplicate code-tracing across the seven specialist lenses.
56
+ This guide gives specialists the code-level context (purpose, data flow, evolutions, constraints) they need to avoid false positives about intentional behavior and long-standing design decisions, and reduces duplicate code-tracing across the eight specialist lenses.
57
57
 
58
58
  For small changes (a single file or trivial diff), you may do this research inline instead of dispatching a sub-agent. Use your judgment: if the workflow research would require reading more than 3-4 files, dispatch a sub-agent to preserve your context for orchestration.
59
59
 
@@ -83,7 +83,7 @@ For changes estimated at 3 points or fewer, skip partitioning — dispatch one s
83
83
 
84
84
  ## Step 6: Dispatch specialist sub-agents
85
85
 
86
- For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The seven specialists are:
86
+ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The eight specialists are:
87
87
 
88
88
  1. **Pedantic** — mechanical correctness: spelling, naming, doc accuracy, specs, guidelines, stale artifacts, structural conventions. Dispatch a sub-agent with instructions to: read every changed file in the unit, check comments/docs/naming/specs/style, check new file and directory placements against the repo's existing layout, report findings.
89
89
 
@@ -91,13 +91,15 @@ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent ru
91
91
 
92
92
  3. **State Flow** — data flow and contracts: module boundaries, implicit FSMs, error propagation, separation of concerns. Dispatch a sub-agent with instructions to: trace contracts across module boundaries, trace end-to-end paths, identify implicit state machines, check error paths.
93
93
 
94
- 4. **NoSlop** — AI writing anti-pattern detection: change narration, fourth wall breaks, em dashes, hedging, filler. Dispatch a sub-agent with instructions to: read every changed file, scan all comments/docs/strings for slop.
94
+ 4. **Economy** — design simplicity and maintainability: is the complexity earned? Evaluates both the overall change design (the forest) and individual touch points (the trees) for unnecessary complexity, redundancy, and simpler available alternatives. Dispatch a sub-agent with instructions to: read the full diff and understand the goal, evaluate the overall design for unnecessary machinery, check each touch point for concrete simpler alternatives, verify any flagged complexity is not constraint-driven before reporting.
95
95
 
96
- 5. **Breadcrumbs** — comment narrative evaluation: do comments form a coherent outline? Dispatch a sub-agent with instructions to: read every changed file in full, evaluate the comment narrative, flag gaps.
96
+ 5. **NoSlop** — AI writing anti-pattern detection: change narration, fourth wall breaks, em dashes, hedging, filler. Dispatch a sub-agent with instructions to: read every changed file, scan all comments/docs/strings for slop.
97
97
 
98
- 6. **MarkAndSweep** — mechanical change completeness: renames, flag removals, API substitutions. Dispatch a sub-agent with instructions to: extract old identifiers from removed diff lines, sweep the whole repo with git grep for stragglers, verify touchpoint neatness (dead branches, orphaned imports, stale comments, config/test residue). Self-selects: if the diff shows no mechanical change patterns, it reports "No mechanical change detected" and produces no findings.
98
+ 6. **Breadcrumbs** — comment narrative evaluation: do comments form a coherent outline? Dispatch a sub-agent with instructions to: read every changed file in full, evaluate the comment narrative, flag gaps.
99
99
 
100
- 7. **Highlights** — positive finding detection: notably clever solutions, cleanup done along the way, documentation that meaningfully helps. Dispatch a sub-agent with instructions to: read every changed file in the unit and the before-state with git show, look for things that rise above baseline competence. Medium-high bar: no generic praise, no baseline competence. If nothing rises above the bar, it reports "No highlights" and produces no findings.
100
+ 7. **MarkAndSweep** — mechanical change completeness: renames, flag removals, API substitutions. Dispatch a sub-agent with instructions to: extract old identifiers from removed diff lines, sweep the whole repo with git grep for stragglers, verify touchpoint neatness (dead branches, orphaned imports, stale comments, config/test residue). Self-selects: if the diff shows no mechanical change patterns, it reports "No mechanical change detected" and produces no findings.
101
+
102
+ 8. **Highlights** — positive finding detection: notably clever solutions, cleanup done along the way, documentation that meaningfully helps. Dispatch a sub-agent with instructions to: read every changed file in the unit and the before-state with git show, look for things that rise above baseline competence. Medium-high bar: no generic praise, no baseline competence. If nothing rises above the bar, it reports "No highlights" and produces no findings.
101
103
 
102
104
  For the integration review unit (5+ points), dispatch a sub-agent focused on:
103
105
  - Cross-component contracts — do the interfaces between components match?
@@ -144,7 +146,7 @@ Alternatively, perform the synthesis yourself:
144
146
  When dispatching each sub-agent, include in the prompt:
145
147
  - The git range to review
146
148
  - The specific files in this unit's scope
147
- - The specialist focus (from the seven specialists above)
149
+ - The specialist focus (from the eight specialists above)
148
150
  - The diff stat for this unit's files
149
151
  - **The project context brief** from Step 2, filtered to what is relevant to this unit's scope. Explicitly list any deferred work that falls within this unit's files, and call out TODO ($ticket) markers the specialists should recognize as intentional.
150
152
  - **The workflow guide** from Step 3, filtered to the workflows relevant to this unit's scope. Use it to understand the purpose and evolution of the code before flagging issues. It provides the code-level context (flows, contracts, history, constraints) that prevents false positives about intentional behavior and long-standing design decisions.
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: thatch-review-economy
3
+ description: Design simplicity and maintainability review — is the complexity earned? Evaluates both the overall change design (the forest) and individual touch points (the trees) for unnecessary complexity, redundancy, and simpler available alternatives. Use for post-implementation review of a branch, PR, or commit range.
4
+ ---
5
+
6
+ You are a design economy review agent. You evaluate whether the complexity in changed code is earned. Every abstraction layer, indirection, generic interface, and moving part should justify itself against the actual use case. You look at both the forest (the overall design of the change) and the trees (individual touch points where code was modified).
7
+ ${REVIEW_COMMON}
8
+ ## Your focus
9
+
10
+ You care about:
11
+ - **Earned complexity**: Each piece of machinery should pay for itself in clarity, correctness, or capability. Complexity that does not serve the goal is a maintenance burden. When evaluating whether complexity is earned, weigh three concrete costs: how hard is it to debug a failure through this code, how much mental work does it take to understand what the change does and why, and how much does it slow down the next person who touches this area.
12
+ - **Simpler alternatives**: At specific touch points, is there a concrete simpler approach that achieves the same behavior? Not "I would have written it differently" — a named alternative with less code, fewer moving parts, or less indirection. The simpler alternative must be at least as debuggable and at least as easy to reason about.
13
+ - **Separation of concerns**: Does the change mix responsibilities that should be separate, or split things that should be together? An unnecessary module boundary is indirection that makes debugging harder (the failure crosses a seam that did not need to exist). A blurred boundary is coupling that makes the code harder to reason about (one module knows too much about another's internals). State-flow checks whether boundaries are correct for data flow; you check whether the boundary structure itself is more complex than the problem requires.
14
+ - **Redundancy and the duplication-dependency tradeoff**: Does the change reimplement functionality already available in the codebase, framework, or standard library? That is a clear finding. The harder question is the inverse: does the change introduce a shared abstraction to avoid duplication, and is that abstraction worse than the duplication it replaced? This is not a binary call. It depends on what kind of reuse is happening:
15
+ - **Private reuse within a package** (two functions in the same file or package sharing a helper) is generally good. No new dependencies, same ownership, the helper is easy to find and change. Do not flag this.
16
+ - **Shared abstraction that warps behavior** (a function gains a parameter that changes what it does depending on who calls it) is generally bad. The "shared" thing is now two functions wearing a trench coat. Flag it: two separate functions would be clearer than one function with a behavioral switch.
17
+ - **Reuse that creates new cross-package or cross-module dependencies** is a judgment call with real costs in both directions. The abstraction removes duplication but adds a dependency that couples the consumers. This is not a hard finding. Surface it as a tradeoff for the user to decide: present what the abstraction buys (less duplicated code) and what it costs (the coupling, the difficulty of changing one consumer without considering the other, the debugging path crossing a module boundary).
18
+ - **Speculative generality**: Code built for hypothetical future needs that are not in the current requirements, context brief, or ticket scope.
19
+
20
+ You do NOT care about:
21
+ - Style preferences or formatting
22
+ - Correctness of data flow (state-flow handles that)
23
+ - User experience or behavioral delta (acceptance handles that)
24
+ - Spelling, naming, or doc accuracy (pedantic handles that)
25
+ - Whether the code works (other reviewers handle logic)
26
+ - Test quality or coverage
27
+
28
+ ## The bar
29
+
30
+ Economy findings are design observations, not user-triggered bugs. The reachability gate and producer chain from REVIEW_COMMON do not apply in their standard form. Use "N/A — design observation" for the trigger and producer chain fields.
31
+
32
+ Before flagging, you must:
33
+ 1. **Identify the simpler alternative concretely.** "This could be simpler" is not a finding. Name the approach: "a single map lookup replaces this three-level nested conditional" or "the stdlib `strconv.Atoi` replaces this hand-rolled parser."
34
+ 2. **Verify the simpler alternative achieves the SAME behavior.** Not similar behavior — the same behavior, including error cases and edge cases. If the simpler approach changes behavior, it is not a valid finding.
35
+ 3. **Verify the simpler alternative is at least as debuggable and at least as easy to reason about.** A refactor that removes lines but makes a failure harder to trace is not an improvement. Name where a bug would manifest in both approaches; if the simpler one makes the failure path less clear, it is not a valid finding.
36
+ 4. **Check why the complexity exists.** Read callers, consult the workflow guide and context brief, check git history. If a constraint (backward compatibility, performance requirement, error handling contract, security boundary, separation-of-concerns decision, coupling limitation) drives the complexity, it is earned. Do not flag it.
37
+
38
+ ## What NOT to flag
39
+
40
+ - Code that follows the project's existing conventions (even if those conventions are verbose)
41
+ - Complexity driven by an identifiable constraint (cite the constraint if you find one)
42
+ - Code that is simply correct and clean (that is the baseline)
43
+ - Pre-existing complexity outside the change's scope (only flag complexity the change introduces or makes worse)
44
+ - Differences in personal style ("I would have used a struct instead of a map")
45
+ - Complexity in test code that makes tests clearer (test readability beats test brevity; duplication in tests is preferable to shared test helpers that hide setup and add framework surface)
46
+ - Duplication that is preferable to a shared abstraction, when the choice is clear: the shared abstraction would create coupling between unrelated callers, make independent changes harder, or add indirection that makes debugging less clear. When the choice is NOT clear (cross-module dependency creation with real tradeoffs in both directions), do not suppress it either — surface it as a TRADEOFF finding instead
47
+
48
+ ## Method
49
+
50
+ ### 1. Read the change at the forest level
51
+ Read the full diff and the commit messages. Understand the goal of the change from the context brief and PR description. Then ask:
52
+ - What is the minimum set of concepts, types, and moving parts needed to achieve this goal?
53
+ - Does the change introduce more than that?
54
+ - Are there abstraction layers, interfaces, or indirections that do not serve the actual use case?
55
+ - Does the change mix responsibilities that should be separate, or split things that should be together? Each unnecessary boundary is a seam a future debugger must cross. Each blurred boundary is coupling a future reader must untangle.
56
+ - If someone needs to debug a failure in this change, how many layers will they have to trace through? Could the same behavior be structured so a failure surfaces closer to its cause?
57
+ - How much mental work does it take to understand what the change does and why? If a competent developer would need to read three files to understand a single behavior, is that complexity earned by a real constraint, or is it structural accident?
58
+
59
+ For each potential forest-level finding, verify that the complexity is not constraint-driven before reporting.
60
+
61
+ ### 2. Read the change at the tree level
62
+ For each modified function, class, or block:
63
+ - Is there a concrete simpler approach that achieves the same behavior?
64
+ - Could a framework or stdlib feature replace this code?
65
+ - Is this guard, handler, or branch necessary, or does it handle a case the surrounding code already prevents?
66
+ - Could two code paths be collapsed into one?
67
+ - If this code was extracted into a shared helper to avoid duplication, what kind of reuse is it? Private reuse within the same package is generally fine. A shared helper that warps its behavior based on a parameter (doing different things for different callers) is a finding — two separate functions would be clearer. A shared helper that creates a new cross-module dependency is a TRADEOFF — surface the pros and cons, do not declare a verdict.
68
+
69
+ For each potential tree-level finding, verify the simpler alternative achieves the same behavior (including error cases) before reporting.
70
+
71
+ ### 3. Check for redundancy
72
+ For any new function, helper, or utility introduced by the change:
73
+ - Does the codebase already have an equivalent?
74
+ - Does the framework or stdlib provide this?
75
+ - Is the new code justified by a difference from existing options that matters for the use case?
76
+
77
+ ### 4. Verify constraints
78
+ Before reporting any finding, check whether the complexity is driven by a constraint:
79
+ - Read the callers of the cited code. Do they depend on the current interface shape?
80
+ - Consult the workflow guide. Does it document a constraint that explains the complexity?
81
+ - Check git history on the cited file. Was the complexity introduced to solve a specific problem?
82
+ - Use thatch_memory_recall to search for documented design decisions.
83
+
84
+ If you find a constraint that justifies the complexity, do not report it.
85
+
86
+ ## Category taxonomy
87
+
88
+ - **OVERENGINEERED**: The solution uses more machinery than the problem requires — extra abstraction, indirection, or generality that does not serve the actual use case. This is a forest-level finding: the overall design of the change is more complex than needed.
89
+ - **SIMPLER_AVAILABLE**: At a specific touch point, a concrete simpler approach achieves the same behavior with less code, fewer moving parts, or less indirection. This is a tree-level finding: the simpler alternative must be named, not just hinted at.
90
+ - **REDUNDANT**: The change reimplements functionality already available in the codebase, framework, or standard library, without a justification that matters for the use case. Also covers the clear-cut inverse: a shared abstraction that warps behavior via a parameter (the function does different things depending on who calls it). Two separate functions would be clearer than one function with a behavioral switch. Do not flag private reuse within a package — that is generally good.
91
+ - **TRADEOFF**: A design choice where the costs are real in both directions and the right answer depends on context the reviewer cannot fully resolve. The most common case: a shared abstraction that creates a new cross-module or cross-package dependency to avoid duplication. The abstraction removes duplicated code but couples the consumers — a change to one may require considering the other, and a debugging path now crosses a module boundary. Present what the abstraction buys and what it costs. Do not declare a verdict. Frame the finding as a question for the user, not a fix to apply. Always LOW severity.
92
+
93
+ For each finding, the source of truth is the simpler alternative you identified (cite it concretely) and any constraints that would justify the current complexity (cite them if found, and withhold the finding if they do).
94
+
95
+ ## Worked non-finding (negative example)
96
+
97
+ Example non-finding: "This three-layer wrapper hierarchy (Handler -> Validator -> Processor) could be collapsed into a single class." Before reporting, read the callers: if Handler is called from three places, Validator from two different Handlers, and Processor is shared with another module, the layers are earning their keep through reuse. The complexity is structural, not speculative. Reporting it anyway is the canonical economy false positive — complexity that looks unnecessary in isolation but is justified by how the code is actually used. The fix is not to collapse the hierarchy but to verify the reuse pattern before flagging.
@@ -275,7 +275,7 @@ After all findings have been dealt with (adequately addressed and replied to, or
275
275
  ### How to offer
276
276
 
277
277
  Present the offer as a choice:
278
- 1. **Re-run the full structured review**: load `thatch-code-review` on the current HEAD. It will dispatch all seven specialists on the merge-base..HEAD range. The existing follow-up detection will cross-reference your prior comments. On Claude Code or Cursor (no sub-agent support), run each specialist skill in sequence, then run `thatch-review-synthesizer` to aggregate.
278
+ 1. **Re-run the full structured review**: load `thatch-code-review` on the current HEAD. It will dispatch all eight specialists on the merge-base..HEAD range. The existing follow-up detection will cross-reference your prior comments. On Claude Code or Cursor (no sub-agent support), run each specialist skill in sequence, then run `thatch-review-synthesizer` to aggregate.
279
279
  2. **End the re-review here**: the user confirmed their concerns were addressed (or decided how to handle the unaddressed ones) and does not want another full round.
280
280
 
281
281
  The offer is just an offer. The user may decline. They may have only wanted to confirm their concerns were addressed.
@@ -3,7 +3,7 @@ name: thatch-review-synthesizer
3
3
  description: Verify and synthesize findings from multiple review specialist skills into a single deduplicated, severity-grouped report. Use after running one or more thatch-review-* specialist skills.
4
4
  ---
5
5
 
6
- You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, no-slop, breadcrumbs, mark-and-sweep, highlights). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
6
+ You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, economy, no-slop, breadcrumbs, mark-and-sweep, highlights). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
7
7
 
8
8
  ## Static analysis only
9
9
  You review code by reading it. Do NOT run tests, linters, compilers, or any build commands.
@@ -18,7 +18,7 @@ For each finding from the specialists:
18
18
 
19
19
  3. **Determine provenance.** Is the finding a new issue introduced by this change, or a pre-existing problem? Note pre-existing bugs separately.
20
20
 
21
- **Finding type determines which steps apply next.** Steps 4-6 apply to behavioral findings (from acceptance, state-flow). Mechanical findings (from pedantic, no-slop, breadcrumbs — docs, naming, style, comments, prose) skip steps 4-6 and use the mechanical verification criteria in step 4m instead.
21
+ **Finding type determines which steps apply next.** Steps 4-6 apply to behavioral findings (from acceptance, state-flow). Economy findings (from economy — design observations about unnecessary complexity) skip steps 4-6 and use the economy verification criteria in step 4e instead. Mechanical findings (from pedantic, no-slop, breadcrumbs — docs, naming, style, comments, prose) skip steps 4-6 and use the mechanical verification criteria in step 4m instead.
22
22
 
23
23
  4. **Check runtime model applicability (behavioral findings only).** Can the finding realistically manifest in normal usage given the application's runtime model? A finding that requires conditions impossible in the actual runtime context is not a real bug (e.g., state accumulation in a short-lived process, concurrency in single-threaded code).
24
24
 
@@ -50,10 +50,18 @@ For each finding from the specialists:
50
50
  - For CLEANUP highlights: verify the bad code existed in the before-state (use git show with the base commit). If the "cleaned up" code was actually just new code the author wrote, it does not count.
51
51
  Runtime reachability, producer chains, and intent verification do not apply. A highlight is not a bug and does not need a trigger scenario.
52
52
 
53
+ 4e. **Economy verification (economy).** For economy findings (unnecessary complexity, simpler alternatives, redundancy, tradeoffs), verification means:
54
+ - The cited code exists at the cited location (already confirmed in step 2).
55
+ - For OVERENGINEERED and SIMPLER_AVAILABLE findings: the simpler alternative is concretely identified (the specialist named a specific approach, not a vague "this could be simpler"), the simpler alternative achieves the same behavior (read the cited code and reason about whether the simpler approach handles the same inputs, error cases, and edge cases — if it changes behavior, reject), and the complexity is not constraint-driven (check the context brief, workflow guide, and callers — if a constraint explains it, reject).
56
+ - For REDUNDANT findings: the reimplemented functionality actually exists where the specialist says it does, and the difference between the new code and the existing option does not matter for the use case. For the inverse (shared abstraction that warps behavior): verify the function actually changes behavior based on a parameter or caller — if both callers get the same behavior, it is not warping, and the finding is rejected.
57
+ - For TRADEOFF findings: verify both sides of the tradeoff are real. The abstraction does remove duplication (check that the consumers would otherwise have duplicated code). The abstraction does create a dependency (check that the consumers are in different packages or modules, or that the helper's interface constrains independent evolution). If only one side is real (e.g., the abstraction removes duplication but the consumers are in the same file), downgrade to REDUNDANT or reject. TRADEOFF findings are always LOW severity and framed as a question for the user, not a fix to apply.
58
+ - The complexity is branch-introduced or made worse by the change: pre-existing complexity outside the change's scope goes in the pre-existing appendix, not as a new finding.
59
+ Runtime reachability, producer chains, and intent verification do not apply. An economy finding is a design observation, not a user-triggered bug.
60
+
53
61
  7. **Classify:**
54
- - **CONFIRMED**: For behavioral findings: the cited code matches, the claim is accurate, the bug is reachable through realistic usage, you proved the workflow/producer chain where applicable, the behavior is not intentional, and for data-shape findings you read and cited the governing constraint confirming the claimed state is reachable. For mechanical findings: the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For highlights: the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence.
55
- - **REJECTED**: For behavioral findings: the citation is wrong, the claim is inaccurate, the bug cannot manifest in the actual runtime context, the behavior is an intentional design decision (explain why briefly), or a governing constraint (NOT NULL, FK, type, guard) forecloses the claimed state. Reject findings that rely on manually seeded invalid state/data with no real producer path. For mechanical findings: the cited text does not exist, does not violate the stated guideline, is unchanged legacy outside the touched scope, or the finding duplicates another confirmed finding. For highlights: the cited text does not exist, the claim is inaccurate, or the highlight does not meet the bar (generic praise, baseline competence, a compliment that could apply to any PR).
56
- - **UNVERIFIABLE**: The citation is correct but you cannot confirm the claim without deeper tracing. This is the default for plausible behavioral claims that lack a proven trigger path, producer chain, or authoritative source of truth. A data-shape finding that lacks the governing-constraint citation is UNVERIFIABLE, not CONFIRMED. Mechanical findings should rarely be UNVERIFIABLE — if the text exists and violates the guideline, it is CONFIRMED. Highlights should rarely be UNVERIFIABLE — either the code does what the highlight claims and it meets the bar (CONFIRMED), or it does not (REJECTED).
62
+ - **CONFIRMED**: For behavioral findings: the cited code matches, the claim is accurate, the bug is reachable through realistic usage, you proved the workflow/producer chain where applicable, the behavior is not intentional, and for data-shape findings you read and cited the governing constraint confirming the claimed state is reachable. For mechanical findings: the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For economy findings (OVERENGINEERED, SIMPLER_AVAILABLE, REDUNDANT): the simpler alternative is concrete, achieves the same behavior, no constraint justifies the current complexity, and the complexity is branch-introduced. For economy TRADEOFF findings: both sides of the tradeoff are real (the abstraction removes duplication AND creates a cross-module dependency), and the finding is framed as a question, not a verdict. For highlights: the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence.
63
+ - **REJECTED**: For behavioral findings: the citation is wrong, the claim is inaccurate, the bug cannot manifest in the actual runtime context, the behavior is an intentional design decision (explain why briefly), or a governing constraint (NOT NULL, FK, type, guard) forecloses the claimed state. Reject findings that rely on manually seeded invalid state/data with no real producer path. For mechanical findings: the cited text does not exist, does not violate the stated guideline, is unchanged legacy outside the touched scope, or the finding duplicates another confirmed finding. For economy findings: a constraint justifies the complexity (cite it), the simpler alternative does not achieve the same behavior, or the complexity predates the change. For economy TRADEOFF findings: only one side of the tradeoff is real (e.g., the abstraction removes duplication but the consumers are in the same package with no cross-module dependency), or the specialist declared a verdict instead of presenting the tradeoff. For highlights: the cited text does not exist, the claim is inaccurate, or the highlight does not meet the bar (generic praise, baseline competence, a compliment that could apply to any PR).
64
+ - **UNVERIFIABLE**: The citation is correct but you cannot confirm the claim without deeper tracing. This is the default for plausible behavioral claims that lack a proven trigger path, producer chain, or authoritative source of truth. A data-shape finding that lacks the governing-constraint citation is UNVERIFIABLE, not CONFIRMED. Mechanical findings should rarely be UNVERIFIABLE — if the text exists and violates the guideline, it is CONFIRMED. Economy findings should rarely be UNVERIFIABLE — either the simpler alternative achieves the same behavior and no constraint justifies the complexity (CONFIRMED), or it does not (REJECTED). Highlights should rarely be UNVERIFIABLE — either the code does what the highlight claims and it meets the bar (CONFIRMED), or it does not (REJECTED).
57
65
 
58
66
  ## Deduplication
59
67
 
@@ -89,6 +97,8 @@ Assign final severity based on YOUR verification:
89
97
  - **MEDIUM**: Edge cases, UX friction, or issues where the citation is correct but the impact is limited or requires unusual conditions.
90
98
  - **LOW**: Mechanical issues (stale docs, guideline violations, naming) that don't affect correctness.
91
99
 
100
+ Economy findings are never BLOCKING — they are maintainability observations, not correctness bugs. TRADEOFF findings are always LOW — they are questions for the user, not fixes to apply. Cap HIGH for significant unnecessary complexity that substantially increases maintenance burden or bug surface. MEDIUM for moderate complexity that adds friction. LOW for minor redundancy (a needless wrapper, a small reimplemented utility) and for TRADEOFF findings.
101
+
92
102
  A data-shape or reachability finding that lacks the governing-constraint citation (the schema, type, or guard definition you read to confirm the state is reachable) is capped at UNVERIFIABLE. It cannot be classified CONFIRMED or assigned a severity. This removes the path where a plausible-but-unchecked claim lands at MEDIUM.
93
103
 
94
104
  ## Report format
package/bin/qa-run ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Wrapper for running a subset of QA use cases by UC name.
5
+ # Usage: qa-run <auto|live> [uc-NNN ...]
6
+ # Examples:
7
+ # qa-run auto uc-004 uc-008
8
+ # qa-run live uc-001 uc-003
9
+
10
+ mode="${1:-auto}"
11
+ shift
12
+
13
+ if [ "$#" -eq 0 ]; then
14
+ # No args: run all
15
+ if [ "$mode" = "auto" ]; then
16
+ exec bun test tests/qa/auto/index.test.ts --concurrent --max-concurrency 5
17
+ else
18
+ exec bun test tests/qa/live/index.test.ts
19
+ fi
20
+ fi
21
+
22
+ # Build a regex from UC name args: uc-004 uc-008 -> ^(UC-004|UC-008)
23
+ # Test names are capitalized (UC-NNN), args may be lowercase.
24
+ pattern=""
25
+ for arg in "$@"; do
26
+ # Capitalize the first two chars (uc -> UC) to match test names
27
+ uc=$(echo "$arg" | sed 's/^uc/UC/')
28
+ if [ -n "$pattern" ]; then
29
+ pattern="${pattern}|"
30
+ fi
31
+ pattern="${pattern}${uc}"
32
+ done
33
+
34
+ if [ "$mode" = "auto" ]; then
35
+ exec bun test tests/qa/auto/index.test.ts --concurrent --max-concurrency 5 --test-name-pattern "^(${pattern})"
36
+ else
37
+ exec bun test tests/qa/live/index.test.ts --test-name-pattern "^(${pattern})"
38
+ fi
package/bin/release CHANGED
@@ -43,6 +43,27 @@ if git rev-parse -q --verify "refs/tags/v$curr" >/dev/null 2>&1; then
43
43
  fi
44
44
  fi
45
45
 
46
+ branch=$(git rev-parse --abbrev-ref HEAD)
47
+ ci_json=$(gh run list --branch "$branch" --workflow ci.yml --limit 1 --json status,conclusion 2>/dev/null || echo "[]")
48
+ ci_status=$(echo "$ci_json" | jq -r '.[0].status // "unknown"')
49
+ ci_conclusion=$(echo "$ci_json" | jq -r '.[0].conclusion // "unknown"')
50
+
51
+ if [[ "$ci_status" != "completed" ]]; then
52
+ echo "ERROR: CI is still running on '$branch' (status: $ci_status)."
53
+ echo " Wait for it to finish before releasing."
54
+ exit 1
55
+ fi
56
+
57
+ if [[ "$ci_conclusion" != "success" ]]; then
58
+ echo "ERROR: CI is red on '$branch' (conclusion: $ci_conclusion)."
59
+ echo " Fix the failures before releasing."
60
+ echo " View: gh run list --branch $branch --workflow ci.yml"
61
+ exit 1
62
+ fi
63
+
64
+ echo " ✓ CI green on '$branch'"
65
+ echo
66
+
46
67
  echo "▸ running checks (typecheck + tests + markdown lint)..."
47
68
  mise run check
48
69
  echo " ✓ checks passed"
package/bin/thatch CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
+ import pkg from "../package.json" with { type: "json" };
5
6
  import { ThatchDB } from "../src/db";
6
7
  import { detectRepo } from "../src/git";
7
8
  import { BgeEmbeddingModel } from "../src/embeddings";
@@ -17,6 +18,7 @@ const [command, ...args] = process.argv.slice(2);
17
18
 
18
19
  function usage(): never {
19
20
  console.error(`USAGE
21
+ thatch --version, -v Print version and exit
20
22
  thatch stores List all stores
21
23
  thatch list [store] List memory labels in a store
22
24
  thatch show <label> [store] Display a memory by label
@@ -52,6 +54,11 @@ function projectDir(): string {
52
54
  }
53
55
 
54
56
  switch (command) {
57
+ case "--version":
58
+ case "-v":
59
+ console.log(pkg.version);
60
+ process.exit(0);
61
+
55
62
  case "stores":
56
63
  console.log(db.listStores().join("\n"));
57
64
  break;
@@ -351,7 +358,7 @@ switch (command) {
351
358
  const sessionID = data.session_id ?? data.conversation_id;
352
359
 
353
360
  const { peekQueue, getMissedCount, incrementMissedCount } = await import("../src/extract-queue");
354
- const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge } = await import("../src/prompts");
361
+ const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge } = await import("../src/prompts");
355
362
 
356
363
  let repo = "unknown";
357
364
  try { repo = await detectRepo(); } catch {}
@@ -376,14 +383,16 @@ switch (command) {
376
383
  // Both recall and predictions share the same sideband round-trip.
377
384
  const promptText = data.prompt ?? data.message ?? data.text ?? data.user_input ?? data.input;
378
385
  if (promptText && promptText.length >= 10) {
379
- const { sidebandMatch, sidebandPredictions, sidebandSocketPath } = await import("../src/sideband");
386
+ const { sidebandMatch, sidebandPredictions, sidebandBehaviors, sidebandSocketPath } = await import("../src/sideband");
380
387
  const sockPath = sidebandSocketPath(dbPath);
381
388
  const recallThreshold = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55");
382
- const predThreshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.45");
389
+ const predThreshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
390
+ const behaviorThreshold = parseFloat(process.env.THATCH_BEHAVIOR_THRESHOLD ?? "0.60");
383
391
 
384
- const [matches, predictions] = await Promise.all([
392
+ const [matches, predictions, behaviors] = await Promise.all([
385
393
  sidebandMatch(sockPath, promptText, [repo, "global"], recallThreshold, 5),
386
394
  sidebandPredictions(sockPath, promptText, [repo, "global"], predThreshold, 5),
395
+ sidebandBehaviors(sockPath, promptText, [repo, "global"], behaviorThreshold, 5),
387
396
  ]);
388
397
 
389
398
  const parts: string[] = [];
@@ -393,6 +402,9 @@ switch (command) {
393
402
  if (predictions && predictions.length > 0) {
394
403
  parts.push(predictionNudge(predictions));
395
404
  }
405
+ if (behaviors && behaviors.length > 0) {
406
+ parts.push(behaviorNudge(behaviors));
407
+ }
396
408
 
397
409
  if (parts.length > 0) {
398
410
  const output = parts.join("\n\n");
@@ -458,7 +470,7 @@ switch (command) {
458
470
  const { sidebandPredictions, sidebandSocketPath } = await import("../src/sideband");
459
471
  const { predictionNudge } = await import("../src/prompts");
460
472
  const sockPath = sidebandSocketPath(dbPath);
461
- const threshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.45");
473
+ const threshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
462
474
  const predictions = await sidebandPredictions(sockPath, promptText, [repo, "global"], threshold, 5);
463
475
 
464
476
  if (predictions && predictions.length > 0) {
@@ -24,9 +24,14 @@ Shared core
24
24
  ├── embeddings.ts → embedding model via transformers.js
25
25
  ├── git.ts → detect repo identity (store name)
26
26
  ├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
27
- ├── prompts.ts → system prompt, compaction, reminders, recall nudge, CLAUDE.md instructions
27
+ ├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, CLAUDE.md instructions
28
28
  ├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
29
- └── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
29
+ ├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
30
+ ├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
31
+ ├── prediction.ts → prediction engine wrapper (thin layer over ScoringEngine)
32
+ ├── behavior.ts → behavior engine wrapper (thin layer over ScoringEngine)
33
+ ├── seed-behaviors.ts → default behavior seeding on first run
34
+ └── vector-math.ts → blobToVector + cosineSimilarity helpers
30
35
 
31
36
  OpenCode plugin path
32
37
  ├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
@@ -44,7 +49,7 @@ MCP server path
44
49
  CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
45
50
 
46
51
  bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
47
- prime|buffer-batch|buffer-tool|flush-tools|setup
52
+ prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup
48
53
  ```
49
54
 
50
55
  ## Module responsibilities
@@ -55,16 +60,21 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
55
60
  | `tools.ts` | Thin opencode wrappers. Imports tool-defs, wraps each in opencode's `tool()` with a `thatch_` prefix. |
56
61
  | `mcp.ts` | Stdio JSON-RPC 2.0 server. Compiles zod schemas to JSON Schema via `z.toJSONSchema()` for `tools/list`. Validates args via `z.object().parse()` in `tools/call`. All logging to stderr (stdout is the transport). |
57
62
  | `index.ts` | OpenCode plugin entry. Wires DB, model, extraction; registers tools and hooks; installs skills. Internal state beyond the extraction pipeline: `extracting` set (parent IDs with an active direct-extraction child), `childMetrics` map (new/updated/deleted counts per child session), and `triggerExtraction(parentID)` (creates a child session via the SDK client and prompts it with the extraction payload). |
58
- | `setup.ts` | `thatch setup --claude` installer. Writes .mcp.json, appends to CLAUDE.md (idempotent), installs hooks in settings.json, installs skills. |
63
+ | `setup.ts` | `thatch setup --claude` / `--cursor` installer. Writes MCP config (`.mcp.json` / `.cursor/mcp.json`), appends to CLAUDE.md / AGENTS.md (idempotent), installs hooks in settings.json / hooks.json, installs skills. |
59
64
  | `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
65
  | `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. Prediction tables: matchers, predictions, edges, provenance. `scorePredictionNudge` is the shared auto-fire entry point for both host paths. |
66
+ | `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. Behavior tables: same four-table shape (matchers, behaviors, edges, provenance) with `scoreBehaviorNudge` as the shared entry point. |
62
67
  | `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
63
68
  | `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the `get_extraction_payload` tool returns. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `mcp.ts` imports `buildExtractionPayload` for the extraction payload provider. `summarizeArgs` is used internally by `buildExtractionPayload`. |
64
69
  | `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. 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
- | `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
- | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (22 skills: fact-extractor, dedup-classifier, project-primer, 7 review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-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]`. |
70
+ | `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 three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
71
+ | `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
72
+ | `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (23 skills: fact-extractor, dedup-classifier, project-primer, 8 review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr — work on all three hosts) and `OPENCODE_ONLY_SKILLS` (1 skill: code-review coordinator — requires sub-agent support, not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
73
+ | `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
74
+ | `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
75
+ | `behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names. |
76
+ | `seed-behaviors.ts` | Default behavior seeding on first run. Populates starter self-discipline rules. |
77
+ | `vector-math.ts` | `blobToVector` and `cosineSimilarity` helpers used across the codebase. |
68
78
 
69
79
  ## Plugin hooks
70
80
 
@@ -76,7 +86,7 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
76
86
  | `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
77
87
  | `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
88
  | `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. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. 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 and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. 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). |
89
+ | `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. 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`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
80
90
  | `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then 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. `session.status` (idle): if the session is a child, drains its snapshot, fires a toast with `childMetrics`, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls `triggerExtraction` to create a direct-extraction child. `session.error`: requeues the parent's buffer (child died without draining). `session.deleted`: cleans up `childToParent`, `parentSnapshots`, `childMetrics`, and `extracting`. `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
81
91
  | `dispose` | Closes the DB. |
82
92
 
@@ -100,12 +110,14 @@ Two of these hooks were dead for weeks because failures were invisible.
100
110
  5. **Store creation is implicit.** First `remember` to a new store creates it.
101
111
  6. **Default recall scope is repo + global.** The tool layer hardcodes this.
102
112
  7. **Skills are plugin-owned files.** Installed to
103
- `$XDG_CONFIG_HOME/opencode/skills` (opencode) or `~/.claude/skills/`
104
- (Claude Code) — never into the worktree; drifted content is overwritten
105
- on plugin init or re-running `thatch setup`. Skills are split into
106
- `SHARED_SKILLS` (work on both hosts) and `OPENCODE_ONLY_SKILLS` (require
107
- sub-agent support, not installed for Claude Code). The opencode plugin
108
- installs both arrays; `thatch setup --claude` installs only shared.
113
+ `$XDG_CONFIG_HOME/opencode/skills` (opencode), `~/.claude/skills/`
114
+ (Claude Code), or `~/.cursor/skills/` (Cursor) — never into the
115
+ worktree; drifted content is overwritten on plugin init or re-running
116
+ `thatch setup`. Skills are split into `SHARED_SKILLS` (work on all
117
+ three hosts) and `OPENCODE_ONLY_SKILLS` (require sub-agent support,
118
+ not installed for Claude Code or Cursor). The opencode plugin
119
+ installs both arrays; `thatch setup --claude` and `--cursor` install
120
+ only shared.
109
121
  8. **Tool definitions are the single source of truth.** `tool-defs.ts` defines
110
122
  each tool once (name, zod schema, execute function). The opencode plugin
111
123
  wraps them in `tool()` with a `thatch_` prefix; the MCP server wraps them
@@ -138,6 +150,18 @@ Two of these hooks were dead for weeks because failures were invisible.
138
150
  relevance-gated (being tested moves it, not being ignored). See the
139
151
  prediction DB tables, auto-fire in `chat.message`, and the sideband
140
152
  `predictions` method for the MCP path.
153
+ 12. **Behavior engine mirrors the prediction engine but is self-graded.** Same
154
+ four-table data model, same Bayesian confidence, same auto-fire pipeline.
155
+ The difference: predictions model what the USER wants (graded by user
156
+ feedback); behaviors model what the LLM should do (graded by the LLM's own
157
+ ham/spam relevance judgment via `behavior_feedback`). The firewall
158
+ principle from predictions (the model that uses predictions cannot grade
159
+ them) does not apply: the LLM grading its own behavioral rules is the
160
+ point, not a violation. The ham/spam is a relevance judgment ("does this
161
+ rule apply here?"), not a value judgment ("is this a good rule?"). An
162
+ anti-laziness guard in the prompt prevents the agent from codifying
163
+ shortcuts. See the behavior DB tables, auto-fire in `chat.message`, and
164
+ the sideband `behaviors` method for the MCP path.
141
165
 
142
166
  ## Data flow
143
167
 
@@ -207,7 +231,9 @@ toast notifications (opencode-only, TUI-rendered)
207
231
  → recall matches: `[thatch] recalled N memories` (info variant, 3s) —
208
232
  fires when chat.message matches stored memories
209
233
  → prediction matches: `[thatch] N predictions surfaced` (info variant,
210
- 3s) — fires when chat.message matches decision-model patterns
234
+ 3s) — fires when chat.message matches decision-model patterns
235
+ → behavior matches: `[thatch] N behaviors surfaced` (info variant,
236
+ 3s) — fires when chat.message matches codified behavior matchers
211
237
 
212
238
  prompt-aware recall nudge (both paths)
213
239
  → opencode: chat.message hook embeds prompt text with warm in-process model,
@@ -237,7 +263,7 @@ prediction cycle (agent-driven, statistical model)
237
263
  confidence is relevance-gated (being tested moves it, not being ignored).
238
264
  → auto-fire (opencode): chat.message reuses the prompt embedding already
239
265
  computed for the recall nudge. db.scorePredictionNudge([repo, global],
240
- embedding, 0.45) finds matchers above threshold, follows edges to
266
+ embedding, 0.60) finds matchers above threshold, follows edges to
241
267
  predictions, scores by cosine * weight * confidence, dedups by
242
268
  prediction_id, and returns top 5. Injects a separate synthetic part
243
269
  with a [thatch] User decision model block. 0-evidence predictions use
@@ -252,6 +278,29 @@ prediction cycle (agent-driven, statistical model)
252
278
  thatch_prediction_list inspects the model with provenance;
253
279
  thatch_prediction_delete removes bad predictions (cascade clears edges
254
280
  and provenance).
281
+
282
+ behavior cycle (agent-driven, self-graded)
283
+ → formation: agent calls thatch_behavior_codify(situation, behavior, rationale)
284
+ when it recognizes a situation it should react to in a specific, repeatable
285
+ way. The tool embeds the situation text, finds or creates a behavior matcher
286
+ (0.85 dedup), finds or creates a behavior (0.85 store-wide dedup), links
287
+ them via an edge. Confidence starts at p0 (0.5) with 0 evidence.
288
+ → auto-fire (opencode): chat.message reuses the prompt embedding. db.
289
+ scoreBehaviorNudge([repo, global], embedding, 0.60) finds behavior matchers
290
+ above threshold, follows edges to behaviors, scores by cosine * weight *
291
+ confidence, dedups by behavior_id, returns top 5. Injects a separate
292
+ synthetic part with a [thatch] Situational behaviors block. 0-evidence
293
+ behaviors use "consider"; behaviors with evidence use "do".
294
+ → auto-fire (Claude Code/Cursor): flush-tools fires the behavior query via
295
+ the sideband socket's `behaviors` method in parallel with recall and
296
+ predictions. Same scoreBehaviorNudge entry point prevents scoring drift.
297
+ → consumption: agent evaluates each surfaced behavior against the current
298
+ situation. If relevant (ham), follows it and calls behavior_feedback with
299
+ relevant: true (confirm). If not relevant (spam), calls behavior_feedback
300
+ with relevant: false (disconfirm). The feedback adjusts the Bayesian
301
+ confidence the same way prediction signals do. thatch_behavior_list
302
+ inspects with provenance; thatch_behavior_delete removes bad rules
303
+ (cascade clears edges and provenance).
255
304
  ```
256
305
 
257
306
  ## Database
@@ -266,7 +315,11 @@ prediction cycle (agent-driven, statistical model)
266
315
  `prediction_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
267
316
  `predictions(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
268
317
  `prediction_edges(matcher_id, prediction_id, weight REAL, PK(matcher_id, prediction_id), FK CASCADE)`,
269
- `prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE)`.
318
+ `prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE)`,
319
+ `behavior_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
320
+ `behaviors(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
321
+ `behavior_edges(matcher_id, behavior_id, weight REAL, PK(matcher_id, behavior_id), FK CASCADE)`,
322
+ `behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)`.
270
323
  - `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
271
324
  databases by an idempotent column migration at init (`PRAGMA table_info` +
272
325
  `ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =