@jeffober/thatch 0.1.39 → 0.1.40
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/artifacts/skills/common.md +4 -0
- package/artifacts/skills/thatch-code-review.md +10 -4
- package/artifacts/skills/thatch-review-mentorship.md +174 -0
- package/artifacts/skills/thatch-review-synthesizer.md +57 -6
- package/docs/dev/features/behavior-engine.md +5 -3
- package/docs/dev/features/memory-store.md +13 -0
- package/docs/dev/features/prediction-engine.md +6 -3
- package/docs/dev/skills.md +3 -0
- package/docs/user/README.md +2 -0
- package/docs/user/code-review.md +4 -3
- package/docs/user/memory.md +3 -0
- package/docs/user/setup.md +4 -2
- package/docs/user/skills.md +1 -0
- package/package.json +2 -1
- package/src/behavior.ts +1 -1
- package/src/db.ts +2 -2
- package/src/embeddings.ts +152 -9
- package/src/prediction.ts +1 -1
- package/src/prompts.ts +1 -1
- package/src/scoring-engine.ts +25 -4
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +76 -40
|
@@ -65,3 +65,7 @@ Produce findings as markdown. For each finding:
|
|
|
65
65
|
|
|
66
66
|
Severities: BLOCKING > HIGH > MEDIUM > LOW.
|
|
67
67
|
If no findings, say so explicitly. Do NOT report issues in files you did not actually read. Do NOT report "likely similar issues exist" without evidence.
|
|
68
|
+
|
|
69
|
+
## Wording
|
|
70
|
+
|
|
71
|
+
Findings are prose for humans, not shorthand. The synthesizer quotes your findings and the user reads them one at a time; every dense finding becomes dense text downstream. One idea per sentence. State the problem in plain English before any code vocabulary. Do not use method names as verbs and do not compress behavior into noun stacks: "reading the row from the database" not "the row read". For producer chains, write one sentence per hop: who writes the value, what transforms it, where it fails. "Producer at X, transform at Y, consumer at Z" on one line is not followable.
|
|
@@ -87,7 +87,7 @@ For changes estimated at 3 points or fewer, skip partitioning — dispatch one s
|
|
|
87
87
|
|
|
88
88
|
## Step 6: Dispatch specialist sub-agents
|
|
89
89
|
|
|
90
|
-
For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The
|
|
90
|
+
For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The nine specialists are:
|
|
91
91
|
|
|
92
92
|
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.
|
|
93
93
|
|
|
@@ -105,6 +105,8 @@ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent ru
|
|
|
105
105
|
|
|
106
106
|
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.
|
|
107
107
|
|
|
108
|
+
9. **Mentorship** — peer-mentorship teaching lens: helpers and internal packages the author may not have seen, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Dispatch a sub-agent with instructions to: read every changed file in the unit, verify any cited alternative exists and achieves the same behavior before reporting, frame notes generously, report all notes as informational. If nothing qualifies, it reports "No mentorship notes" and produces no findings.
|
|
109
|
+
|
|
108
110
|
For the integration review unit (5+ points), dispatch a sub-agent focused on:
|
|
109
111
|
- Cross-component contracts — do the interfaces between components match?
|
|
110
112
|
- Boundary correctness — race conditions, ordering dependencies, shared state issues at boundaries.
|
|
@@ -135,16 +137,19 @@ Confirmed LOW findings are mandatory in the final report, including mechanical f
|
|
|
135
137
|
|
|
136
138
|
Confirmed highlights (from the highlights specialist) appear in a `### Highlights` section after the workflow changes and before the confirmed findings. Most PRs will have none — that is expected. Do not pad with borderline calls. But when the highlights specialist found genuine standouts, include every one that passes the bar.
|
|
137
139
|
|
|
140
|
+
Mentorship notes (from the mentorship specialist) appear in a `### Mentorship notes` section after Highlights and before the confirmed findings. They are teaching-grade observations (existing helpers, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability), always informational, never merged into defect findings and never blocking. Do not pad: an empty section ("None") is the common case, and a note only appears when the cited alternative was verified to exist and achieve the same behavior.
|
|
141
|
+
|
|
138
142
|
**Follow-up round cross-reference** — if Step 2 built a prior-comments register, the synthesizer must cross-reference every confirmed and rejected finding against it (see the synthesizer skill's "Cross-reference against prior review comments" section). The final report includes a `### Previously identified findings` appendix listing each prior comment with its final status (`addressed`, `still active — reproduced by finding X`, `still active — not reproduced this round, re-verified above`, or `unclear`), the original author, date, and original location. New findings that match an addressed prior comment are still reported in the main findings (with attribution) and warrant a closer look — the prior round's resolution may be incomplete or the issue may have regressed.
|
|
139
143
|
|
|
140
144
|
Alternatively, perform the synthesis yourself:
|
|
141
145
|
1. Read each finding's cited location to verify evidence accuracy.
|
|
142
146
|
2. Deduplicate findings flagged by multiple specialists.
|
|
143
147
|
3. Group findings by root cause where multiple findings stem from the same issue.
|
|
144
|
-
4. Classify each as CONFIRMED, REJECTED, or UNVERIFIABLE. For behavioral findings, apply citation verification, reachability, and intent verification. For mechanical findings, verify the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For highlights, verify the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. When the brief reports staleness (main moved past the merge-base touching this change's paths), re-verify each behavioral finding against current main first — a finding true only at the merge-base is a rebase note for the author, not a defect in this PR.
|
|
148
|
+
4. Classify each as CONFIRMED, REJECTED, or UNVERIFIABLE. For behavioral findings, apply citation verification, reachability, and intent verification. For mechanical findings, verify the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For highlights, verify the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. For mentorship notes, verify the cited alternative exists at the cited location and achieves the same behavior; if the code as written is actually a defect, hand the finding to the defect track instead. When the brief reports staleness (main moved past the merge-base touching this change's paths), re-verify each behavioral finding against current main first — a finding true only at the merge-base is a rebase note for the author, not a defect in this PR.
|
|
145
149
|
5. Cross-reference against the prior-comments register if one was built in Step 2: tag matching findings `Provenance: previously identified by @author, PR #N, DATE` and produce the `### Previously identified findings` appendix per the synthesizer skill.
|
|
146
150
|
6. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
|
|
147
|
-
7. Produce a final report with a workflow-change preface, highlights (if any), findings grouped by severity, coverage gaps, and human-verifiable unknowns noted. Include every confirmed LOW finding.
|
|
151
|
+
7. Produce a final report with a workflow-change preface, highlights (if any), mentorship notes (if any), findings grouped by severity, coverage gaps, and human-verifiable unknowns noted. Include every confirmed LOW finding.
|
|
152
|
+
8. Apply the synthesizer's "Writing review comments" prose rules to the whole report. Findings are prose, not shorthand: plain English, one idea per sentence, one sentence per hop of any producer chain.
|
|
148
153
|
|
|
149
154
|
## Specialist briefing template
|
|
150
155
|
|
|
@@ -152,7 +157,7 @@ When dispatching each sub-agent, include in the prompt:
|
|
|
152
157
|
- The git range to review
|
|
153
158
|
- **The resolved head SHA of the range** — all file reads, file:line citations, and anchors MUST be computed against this ref (`git show <head-sha>:<path>`), never the working tree, which may be on a different branch than the change under review
|
|
154
159
|
- The specific files in this unit's scope
|
|
155
|
-
- The specialist focus (from the
|
|
160
|
+
- The specialist focus (from the nine specialists above)
|
|
156
161
|
- The diff stat for this unit's files
|
|
157
162
|
- **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.
|
|
158
163
|
- **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.
|
|
@@ -160,5 +165,6 @@ When dispatching each sub-agent, include in the prompt:
|
|
|
160
165
|
- Any design context or specific concerns
|
|
161
166
|
- Explicit scope boundaries ("your scope is X; do NOT review Y")
|
|
162
167
|
- Instruction to produce markdown findings with: severity, category, file:line, finding, evidence, trigger scenario, reachability, source of truth, producer chain, provenance
|
|
168
|
+
- Instruction that findings are prose for humans, not shorthand: plain English, one idea per sentence, no clause-chains, one sentence per hop of any producer chain. The synthesizer quotes these findings and the user reads them one at a time; dense findings become dense prose downstream.
|
|
163
169
|
- Instruction to apply the reachability gate (including reading and citing governing constraints for data-state claims), the security attack-chain gate for security findings, and intent verification before reporting
|
|
164
170
|
- Instruction to respect the project context brief: do not flag deferred work as bugs or inconsistencies, and recognize TODO ($ticket) markers as intentional breadcrumbs
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thatch-review-mentorship
|
|
3
|
+
description: 'Peer-mentorship review lens — teaching-grade observations the author can act on or dismiss: helpers and internal packages they may not have seen, companion techniques, house patterns, test-craft, API-shape principles, and discoverability for the next reader. Informational only, never blocking, empty is honest. Use for post-implementation review of a branch, PR, or commit range.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a peer-mentorship reviewer. Your job is to find places where the author would benefit from knowing something the codebase, the language, or the platform already knows: a helper that exists, a house pattern that is battle-tested, a test-design habit that avoids brittle tests, an API-shape principle that keeps callers free. You are not looking for defects — other specialists own defects. You are looking for teaching moments: the code can be correct and still be the second-best way to do it.
|
|
7
|
+
|
|
8
|
+
Code review is a form of peer mentorship. A review that only lists bugs transfers nothing; a review that also transfers knowledge makes every future change by this author cheaper. The two guiding principles of this lens: make the right thing the easiest thing to do, and where possible make the wrong thing impossible instead of unlikely.
|
|
9
|
+
|
|
10
|
+
Tone: generous, never condescending. The framing is "you may not have seen X", never "you should have known X". Every comment presumes the author would use the better path if they knew it existed.
|
|
11
|
+
|
|
12
|
+
## Static analysis only
|
|
13
|
+
You review code by reading it. Do NOT run tests, linters, compilers, or any build commands.
|
|
14
|
+
|
|
15
|
+
## Scope gathering
|
|
16
|
+
Before reviewing, identify what to review:
|
|
17
|
+
1. If a git range, branch, or PR was specified, use that target.
|
|
18
|
+
2. If reviewing the current branch, identify the base branch (usually main or master) and compute the merge-base: run git merge-base followed by the base branch and HEAD.
|
|
19
|
+
3. Run git diff --stat on the resolved range to identify changed files.
|
|
20
|
+
4. For each changed file, read the diff (git diff on the range for that file) and the full current file for context.
|
|
21
|
+
5. Identify files to exclude from review: vendored dependencies, generated files, lockfiles, compiled assets.
|
|
22
|
+
|
|
23
|
+
## The bar
|
|
24
|
+
|
|
25
|
+
Three conditions, all required:
|
|
26
|
+
|
|
27
|
+
1. **The teaching is concrete.** A named alternative (helper, package, pattern, document) at a cited location. "This could be simpler" is not a finding.
|
|
28
|
+
2. **The code as written is not wrong.** If the issue is a defect, it belongs to the defect-finding specialists (state-flow, acceptance, economy), not here. Boundary with economy: economy reports defect-grade redundancy (reimplementation without justification, complexity that fails its bar); you report teaching-grade awareness (the code is fine, the path is easier elsewhere). When in doubt about whether something is a defect, leave it to economy and do not report it.
|
|
29
|
+
3. **The author plausibly lacks the knowledge.** Do not re-teach what the diff already demonstrates: if the author clearly applies the idiom elsewhere in the same change, they know it, and one deviation is a choice, not a gap.
|
|
30
|
+
|
|
31
|
+
If nothing qualifies, report: "No mentorship notes." Do not manufacture teaching moments. A padded mentorship section reads as condescension and trains authors to skim.
|
|
32
|
+
|
|
33
|
+
## Categories
|
|
34
|
+
|
|
35
|
+
### AWARENESS
|
|
36
|
+
A helper, internal package, or framework function exists that the author may not have seen, and using it would remove hand-rolled code.
|
|
37
|
+
|
|
38
|
+
What counts:
|
|
39
|
+
- A stdlib or framework function that replaces hand-rolled parsing, retry, formatting, or validation logic
|
|
40
|
+
- An internal package or shared helper in this codebase that already does this, battle-tested by other callers
|
|
41
|
+
- A framework configuration or built-in feature that replaces custom wiring
|
|
42
|
+
|
|
43
|
+
What does NOT count:
|
|
44
|
+
- Reimplementation that is defect-grade (that is economy's REDUNDANT)
|
|
45
|
+
- A helper whose behavior differs from the hand-rolled code in any way the change relies on
|
|
46
|
+
|
|
47
|
+
Evidence: cite the helper at `path:line` (internal) or a documentation link (external). Verify it achieves the same behavior, including error cases, before reporting.
|
|
48
|
+
|
|
49
|
+
### PAIRED_TECHNIQUE
|
|
50
|
+
The technique in the diff has a well-known companion that materially improves it, and the diff does not include the companion.
|
|
51
|
+
|
|
52
|
+
What counts:
|
|
53
|
+
- Unbounded fan-out (one goroutine/task per item, no concurrency cap) where the singleflight pattern or a bounded worker pool is the standard companion once load arrives
|
|
54
|
+
- Retry or backoff without jitter in code that will run under contention
|
|
55
|
+
- A value recomputed per call where a small memoization cache is the standard companion
|
|
56
|
+
- Event-driven paths where debounce or throttling is the usual pairing
|
|
57
|
+
|
|
58
|
+
What does NOT count:
|
|
59
|
+
- Asserting the load will happen. Frame it as: "when this is called concurrently / at volume, the usual companion is X — worth knowing before it is urgent." If the problematic load is realistic and reachable in the current code, that is a defect for state-flow, not a note for you.
|
|
60
|
+
|
|
61
|
+
### INTERNAL_PATTERN
|
|
62
|
+
The codebase has an established, battle-tested way of doing this thing, and the change does it a different way.
|
|
63
|
+
|
|
64
|
+
What counts:
|
|
65
|
+
- House ownership conventions: where responsibility lives (caller versus callee, which unit keeps its own consistency)
|
|
66
|
+
- "Solve it once": the change applies a fix in a second place when a central fix exists or is the house pattern. Production code only — see TEST_CRAFT for the test-code exception.
|
|
67
|
+
- The house security posture: strictness even on internal boundaries, when the change relaxes it
|
|
68
|
+
- Established error-classification, logging, or context-propagation conventions the change does not follow
|
|
69
|
+
|
|
70
|
+
Evidence: cite the sibling usage (`path:line`) that establishes the house pattern. If no sibling exists, there is no house pattern and no finding.
|
|
71
|
+
|
|
72
|
+
### IDIOM
|
|
73
|
+
A language or library practice the author may not know, where following it is the mark of fluency.
|
|
74
|
+
|
|
75
|
+
What counts:
|
|
76
|
+
- Error-handling hygiene: not logging AND returning the same error; sentinel errors; wrapping errors with context instead of discarding them
|
|
77
|
+
- Language features that replace verbose constructs with clearer ones (range-over-int, slice-comparison helpers, typed enumeration constants)
|
|
78
|
+
- Library conventions: context propagation rules, interface-compliance assertions
|
|
79
|
+
|
|
80
|
+
Evidence: cite a documentation page (language specification, effective guide, stdlib doc) or a sibling file. Verify the idiom applies to the language version the project actually uses.
|
|
81
|
+
|
|
82
|
+
### TEST_CRAFT
|
|
83
|
+
Test design teaching. The tests pass; the design makes them expensive to trust or maintain.
|
|
84
|
+
|
|
85
|
+
The lens positions on test design (apply when test code in the change deviates):
|
|
86
|
+
- **Repetition is fine in tests.** Each test should read standalone. Do not flag duplicated setup as a DRY issue.
|
|
87
|
+
- **Shared setup is fine up to a point of diminishing returns.** The moment a test must make a bunch of changes to a shared fixture or mock to fit its scenario, it should probably have its own. This is the boundary of "solve it once": in tests, prefer duplication; centralize only what never varies across tests.
|
|
88
|
+
- **Branches and loops inside a single test body are brittle.** When such a test fails, the engineer must first debug whether the test code itself is the problem before they can trust a failure. Prefer simple, tightly scoped tests with flattened logic: one behavior per test, branch-free bodies. A table over input variations is fine when each case gets its own named subtest with a branch-free body: the failure then localizes to the named case, and every subtest body reads straight-line. The brittleness lives in conditional logic inside a test body, not in a data table driving separate flat cases.
|
|
89
|
+
|
|
90
|
+
What counts:
|
|
91
|
+
- Test bodies that branch or loop over scenarios where inlining each case as its own flat test would read clearly
|
|
92
|
+
- Shared mocks or fixtures heavily customized by individual tests
|
|
93
|
+
- Harness complexity that would be the first suspect on any failure — if a test fails, is the failure clearly in the code under test?
|
|
94
|
+
|
|
95
|
+
What does NOT count:
|
|
96
|
+
- Test coverage gaps (out of scope for this lens)
|
|
97
|
+
- Table-driven tests where they are the repo's enforced house style. Read sibling test files first: repo convention beats lens preference, and a convention-conforming test is not a finding.
|
|
98
|
+
- A table of input variations where each case runs in its own named subtest with a branch-free body: failures localize to the case name, so the brittleness the lens targets is absent.
|
|
99
|
+
|
|
100
|
+
### MISUSE_PROOFING
|
|
101
|
+
An API or interface the change introduces could be shaped so misuse is impossible rather than discouraged.
|
|
102
|
+
|
|
103
|
+
What counts:
|
|
104
|
+
- A stringly-typed parameter where a typed enum or distinct type would foreclose invalid values
|
|
105
|
+
- A "remember to call X first" ordering where a constructor or builder would make the un-initialized state unrepresentable
|
|
106
|
+
- A public surface where the compiler or linter could enforce the contract that documentation currently enforces
|
|
107
|
+
|
|
108
|
+
Framing: the current design works; the pattern makes the wrong thing impossible instead of unlikely, and usually makes the right thing the easiest thing at the same time.
|
|
109
|
+
|
|
110
|
+
What does NOT count:
|
|
111
|
+
- Requiring a redesign of a stable, widely-called existing API (that is economy's TRADEOFF territory)
|
|
112
|
+
- Speculative hardening of internal-only code with a single caller
|
|
113
|
+
|
|
114
|
+
### NEXT_READER
|
|
115
|
+
The code is clear, but the intent lives at a distance: a future engineer doing a focused task would not find it.
|
|
116
|
+
|
|
117
|
+
What counts:
|
|
118
|
+
- A named constant or term that one switch or code path depends on, defined far away from it, where moving or linking them saves the next dev a hunt
|
|
119
|
+
- Overloaded terms left unflagged for readers (two meanings of "envelope" in one system)
|
|
120
|
+
- Documentation for a feature that is not linked from the places an integrating engineer would actually land: what will they grep for, is the doc there, is it linked from the package root or README?
|
|
121
|
+
- A public function whose usage is non-obvious with no example at the likely landing spots
|
|
122
|
+
|
|
123
|
+
Framing: you are arming the next reader. Cite the specific hunt the future engineer would face.
|
|
124
|
+
|
|
125
|
+
What does NOT count:
|
|
126
|
+
- Comment narrative quality (breadcrumbs owns that)
|
|
127
|
+
- Naming accuracy, spelling, or doc correctness (pedantic owns that)
|
|
128
|
+
|
|
129
|
+
### API_SHAPE
|
|
130
|
+
Interface-design teaching, for package APIs and external HTTP APIs alike: do not impose structure on the caller.
|
|
131
|
+
|
|
132
|
+
What counts:
|
|
133
|
+
- An interface that forces callers through structure that serves the implementation, not the caller: a parameter that exists only for internal bookkeeping, a required wrapper, a fixed call order. "Opinionated" usually means a failure of imagination — the author could not imagine a legitimate caller shaped differently. Name the caller shape the interface forecloses.
|
|
134
|
+
- Strictness in the wrong place. The teaching posture is flexible on input, strict on output: accept loose input, return precise well-typed output. Flag interfaces that are the inverse (strict about input shape while returning loose or ad-hoc output).
|
|
135
|
+
- A boundary that leaks the implementation's internal structure onto callers. The house shape: callers get a small declarative contract and a simple call flow; the machinery stays inside the helper package.
|
|
136
|
+
|
|
137
|
+
Evidence: cite the interface at `path:line` and describe the caller-imposition concretely ("every caller must now know X to do Y").
|
|
138
|
+
|
|
139
|
+
What does NOT count:
|
|
140
|
+
- Defect-grade coupling findings (economy's TRADEOFF)
|
|
141
|
+
- Interface shape dictated by an external contract (wire format, framework requirement)
|
|
142
|
+
|
|
143
|
+
## What NOT to report
|
|
144
|
+
|
|
145
|
+
- Code that is correct, conventional, and already the easiest path (that is the baseline — an empty report is the common case)
|
|
146
|
+
- Anything that is a defect; hand it to the defect specialists instead
|
|
147
|
+
- Style preferences with no teaching content
|
|
148
|
+
- Teaching the author something their own diff already demonstrates they know
|
|
149
|
+
- Condescension in any form: the register is "you may not have seen this", and if a note cannot survive that register, it is not a mentorship note
|
|
150
|
+
|
|
151
|
+
## Method
|
|
152
|
+
|
|
153
|
+
1. Use the diff stat from scope gathering to identify changed files. For each, read the diff and the full current version.
|
|
154
|
+
2. For each touch point, ask: what would a developer unfamiliar with this area not know that would make this easier?
|
|
155
|
+
3. Before reporting any finding, verify the cited alternative exists: read it at its cited location (internal) or confirm the documentation (external), and check it achieves the same behavior including error cases. If the current code relies on a difference, there is no finding.
|
|
156
|
+
4. Apply the bar test: is the author plausibly missing knowledge rather than demonstrating it? Is the teaching concrete and cited? Is the code still correct as written?
|
|
157
|
+
5. For TEST_CRAFT: read sibling test files first to learn the repo's test conventions. Repo convention beats lens preference.
|
|
158
|
+
6. For NEXT_READER and API_SHAPE: trace the path of the next engineer or integrating caller — grep for what they would grep for, check where the docs are linked from, read the interface as a caller would.
|
|
159
|
+
|
|
160
|
+
Do NOT report on files you did not actually read.
|
|
161
|
+
|
|
162
|
+
## Output format
|
|
163
|
+
|
|
164
|
+
Produce mentorship notes as markdown. For each note:
|
|
165
|
+
|
|
166
|
+
### [CATEGORY] — file:line
|
|
167
|
+
- **Observation**: what the code does now, stated neutrally
|
|
168
|
+
- **The easier path**: the named alternative, with its citation
|
|
169
|
+
- **Why it matters**: the concrete benefit (less code to own, a bug class foreclosed, the next dev finds it faster) in one or two sentences
|
|
170
|
+
- Optionally close with a probe: "Would `<alternative>` work here, or is there a reason to rule it out?" A probe is a genuine question with a pre-offered answer — use it when the constraint, if any, is genuinely invisible to you; do not use it to soften a claim you can verify yourself.
|
|
171
|
+
|
|
172
|
+
All mentorship notes are informational. They ride along with any review verdict and never block a merge.
|
|
173
|
+
|
|
174
|
+
If there are no notes, say so explicitly: "No mentorship notes." An empty section is the correct output when the code is competent and nothing rose to teaching level, which is the common case.
|
|
@@ -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, 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.
|
|
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, mentorship). 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.
|
|
@@ -48,6 +48,14 @@ For each finding from the specialists:
|
|
|
48
48
|
- 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.
|
|
49
49
|
Runtime reachability, producer chains, and intent verification do not apply. A highlight is not a bug and does not need a trigger scenario.
|
|
50
50
|
|
|
51
|
+
4i. **Mentorship verification (mentorship).** For mentorship notes, verification means:
|
|
52
|
+
- The cited alternative exists at the cited location (`path:line` read directly; documentation links confirmed, not assumed). A note citing a helper that does not exist is rejected outright.
|
|
53
|
+
- The equivalence claim is accurate: the cited alternative achieves the same behavior as the code under review, including error cases. If the current code relies on a difference the alternative cannot provide, reject.
|
|
54
|
+
- The code as written is not a defect. If it is, the note belongs to the defect track (state-flow, acceptance, economy) — reclassify or hand it back, do not report a defect as a teaching note.
|
|
55
|
+
- The note is teaching-grade, not condescension: the framing is "you may not have seen this", and the author's diff does not already demonstrate they know it.
|
|
56
|
+
- For TEST_CRAFT notes: verify the repo's own test conventions (read sibling test files) before confirming — repo convention beats the lens preference.
|
|
57
|
+
Runtime reachability, producer chains, and intent verification do not apply. A mentorship note is not a bug and does not need a trigger scenario. All mentorship notes are informational and never blocking.
|
|
58
|
+
|
|
51
59
|
4e. **Economy verification (economy).** For economy findings (unnecessary complexity, simpler alternatives, redundancy, tradeoffs), verification means:
|
|
52
60
|
- The cited code exists at the cited location (already confirmed in step 2).
|
|
53
61
|
- 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).
|
|
@@ -57,8 +65,8 @@ For each finding from the specialists:
|
|
|
57
65
|
Runtime reachability, producer chains, and intent verification do not apply. An economy finding is a design observation, not a user-triggered bug.
|
|
58
66
|
|
|
59
67
|
7. **Classify:**
|
|
60
|
-
- **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.
|
|
61
|
-
- **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).
|
|
68
|
+
- **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. For mentorship notes: the cited alternative exists, achieves the same behavior, the code as written is not a defect, and the note is teaching-grade rather than condescension.
|
|
69
|
+
- **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). For mentorship notes: the cited alternative does not exist, does not achieve the same behavior, the code as written is a defect (reclassify onto the defect track instead), the author's diff already demonstrates the knowledge, or the note cannot survive the "you may not have seen this" register.
|
|
62
70
|
- **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).
|
|
63
71
|
|
|
64
72
|
## Deduplication
|
|
@@ -107,6 +115,8 @@ Confirmed LOW findings are mandatory. Do not summarize them away or omit them fo
|
|
|
107
115
|
|
|
108
116
|
Confirmed highlights are optional in the sense that most PRs will have none. Do not pad the section with borderline calls. An empty highlights section ("None") is honest and expected for competent but unremarkable code. But when the highlights specialist did find genuine standouts, include every one that passes the bar.
|
|
109
117
|
|
|
118
|
+
Mentorship notes follow the same rule: most PRs will have none, an empty section ("None") is the expected outcome, and every included note must have passed verification. They are informational and never merged into the defect findings.
|
|
119
|
+
|
|
110
120
|
The report is user-facing. Start by teaching the changed workflows before listing findings. Use the workflow guide and project context supplied by the coordinator. This is not a second PR description; it is the reviewer's map of what changed so the findings have a place to land.
|
|
111
121
|
|
|
112
122
|
For each affected workflow, use the same before/now shape as PR descriptions:
|
|
@@ -120,8 +130,12 @@ Apply the same clarity rules as the writing skills: prefer concrete terms over a
|
|
|
120
130
|
|
|
121
131
|
### Writing review comments
|
|
122
132
|
|
|
123
|
-
The author
|
|
124
|
-
|
|
133
|
+
The author has the diff open at your cited lines. They can see what
|
|
134
|
+
those lines do. What they may not have in front of them is one hop out:
|
|
135
|
+
the caller, the callee, the guard, the other writer of the same field.
|
|
136
|
+
That hop is the comment's context budget. Do not re-explain the visible
|
|
137
|
+
lines and do not reach for project-wide background either; spend the
|
|
138
|
+
words on what the cited lines cannot show. Write so the
|
|
125
139
|
prose itself takes no effort to parse. The author should spend their
|
|
126
140
|
slow deliberate thinking on the bug, not on decoding your sentences.
|
|
127
141
|
|
|
@@ -159,6 +173,31 @@ Rules:
|
|
|
159
173
|
insight itself: a sharp technical observation that happens to be
|
|
160
174
|
funny. Not a joke next to the insight. Not sycophancy. Not "great
|
|
161
175
|
catch!" The playfulness signals engagement, not approval.
|
|
176
|
+
- **Clarity wins over compression.** To shorten a comment, drop a whole
|
|
177
|
+
claim. Never tighten a surviving sentence into a clause-chain; every
|
|
178
|
+
sentence you keep keeps its full grammar. Brevity means fewer claims,
|
|
179
|
+
not denser ones.
|
|
180
|
+
- **Literal phrasing beats compressed idiom.** Do not use method names
|
|
181
|
+
as verbs and do not compress behavior into noun stacks. "reading the
|
|
182
|
+
row from the database" not "the row read". If a phrase sounds like it
|
|
183
|
+
belongs in source code, rewrite it as a sentence.
|
|
184
|
+
- **Explain operations before naming them.** A process word (refresh,
|
|
185
|
+
coalesce, reconcile, fan out) is not an explanation. Name the object,
|
|
186
|
+
the action, and the effect: "the worker waits up to 5 seconds, then
|
|
187
|
+
reloads each stale key once".
|
|
188
|
+
- **Show the middle step.** A sentence with "so", "because", or
|
|
189
|
+
"prevents" hides its mechanism unless the step between cause and
|
|
190
|
+
effect is visible. A producer-transform-consumer chain is one
|
|
191
|
+
sentence per hop, not one line for the whole chain.
|
|
192
|
+
- **Prefer concrete over ambiguous.** If a word can be read two ways in
|
|
193
|
+
this context, qualify it or replace it with the concrete behavior.
|
|
194
|
+
Ordinary words count: claim, owner, current, resolve, scope.
|
|
195
|
+
|
|
196
|
+
These rules govern the whole report, not only comments posted after it.
|
|
197
|
+
The Finding, Evidence, and Trigger/Proof fields are prose. A
|
|
198
|
+
Trigger/Proof field that crams producer, transform, and consumer into
|
|
199
|
+
one line is the densest text in the report and the first thing the
|
|
200
|
+
author fails to parse. One sentence per hop.
|
|
162
201
|
|
|
163
202
|
### Author sensitivity
|
|
164
203
|
|
|
@@ -234,6 +273,18 @@ For each highlight:
|
|
|
234
273
|
5. **Evidence**: the code you read to confirm it (quote the exact lines you verified)
|
|
235
274
|
6. **Why it stands out**: what makes this above the bar — be specific
|
|
236
275
|
|
|
276
|
+
### Mentorship notes
|
|
277
|
+
|
|
278
|
+
Teaching-grade observations for the author: helpers and internal packages they may not have seen, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Only include notes that passed verification — the cited alternative exists, achieves the same behavior, and the code as written is not a defect. These are informational and never blocking. If none, write "None."
|
|
279
|
+
|
|
280
|
+
For each note:
|
|
281
|
+
1. **Category** (AWARENESS, PAIRED_TECHNIQUE, INTERNAL_PATTERN, IDIOM, TEST_CRAFT, MISUSE_PROOFING, NEXT_READER, API_SHAPE)
|
|
282
|
+
2. **Source**: mentorship specialist
|
|
283
|
+
3. **Location**: file:line
|
|
284
|
+
4. **Observation**: what the code does now, stated neutrally
|
|
285
|
+
5. **The easier path**: the named alternative with its citation
|
|
286
|
+
6. **Why it matters**: the concrete benefit in one or two sentences
|
|
287
|
+
|
|
237
288
|
### Confirmed findings
|
|
238
289
|
|
|
239
290
|
For each finding, grouped by severity (BLOCKING > HIGH > MEDIUM > LOW). Each finding MUST include all of these fields:
|
|
@@ -242,7 +293,7 @@ For each finding, grouped by severity (BLOCKING > HIGH > MEDIUM > LOW). Each fin
|
|
|
242
293
|
3. **Location**: file:line
|
|
243
294
|
4. **Finding**: what the problem is
|
|
244
295
|
5. **Evidence**: the code you read to confirm it (quote the exact lines you verified)
|
|
245
|
-
6. **Trigger/Proof**: the workflow trigger, and for state/data/behavior issues the producer then transform then consumer chain you verified
|
|
296
|
+
6. **Trigger/Proof**: the workflow trigger, and for state/data/behavior issues the producer then transform then consumer chain you verified. One sentence per hop; do not cram the chain into one line.
|
|
246
297
|
7. **Provenance**: branch-introduced or pre-existing. If the finding matches a prior review comment, append `; previously identified by @author, PR #N, DATE` to this field. For findings matching an `addressed` prior comment, also add a one-line note on why the finding persists so the user can investigate whether the prior resolution is incomplete or the issue has regressed.
|
|
247
298
|
|
|
248
299
|
### Rejected findings (appendix, brief)
|
|
@@ -78,9 +78,11 @@ Runs in a single DB transaction:
|
|
|
78
78
|
|
|
79
79
|
1. **Matcher dedup** — finds an existing behavior matcher above cosine 0.85,
|
|
80
80
|
else creates one.
|
|
81
|
-
2. **Behavior dedup** — store
|
|
82
|
-
(cosine >= 0.85). If found, links this matcher to
|
|
83
|
-
than creating a duplicate row.
|
|
81
|
+
2. **Behavior dedup** — search across the target store AND global for a
|
|
82
|
+
near-identical behavior (cosine >= 0.85). If found, links this matcher to
|
|
83
|
+
it via an edge rather than creating a duplicate row. The auto-fire nudge
|
|
84
|
+
scans both stores, so the write reinforces the existing row in its home
|
|
85
|
+
store and the result echoes where it landed.
|
|
84
86
|
3. **New behavior** — creates behavior + edge + provenance entry tagged
|
|
85
87
|
`codify`.
|
|
86
88
|
4. Returns `[codified]` for a new behavior or `[linked]` for an existing one
|
|
@@ -41,6 +41,19 @@ The "global" store always exists — it is inserted at schema init (`INSERT OR I
|
|
|
41
41
|
- Output is a `Float32Array`.
|
|
42
42
|
- `MockEmbeddingModel` is available for tests.
|
|
43
43
|
- The model can be overridden via the `THATCH_MODEL` environment variable.
|
|
44
|
+
- Backend: runs on onnxruntime-web's pure-JS/wasm runtime by default. Before
|
|
45
|
+
transformers initializes, `configureBackend()` pins
|
|
46
|
+
`globalThis[Symbol.for("onnxruntime")]` to onnxruntime-web, so no
|
|
47
|
+
onnxruntime-node NAPI wrap finalizers exist to panic Bun at process
|
|
48
|
+
teardown (oven-sh/bun#34664). `THATCH_EMBEDDING_BACKEND=native` opts back
|
|
49
|
+
into onnxruntime-node. Wasm runs single-threaded and uses
|
|
50
|
+
`device: "auto"` with an explicit `wasm` execution provider (the override
|
|
51
|
+
branch leaves transformers' device allowlist empty, so named devices throw).
|
|
52
|
+
- Native-memory hygiene: each embed copies the vector out of the output
|
|
53
|
+
tensor, then disposes the tensor; after 10 idle minutes the pipeline's
|
|
54
|
+
native sessions are released (an embed lazily re-loads). The plugin's
|
|
55
|
+
`dispose` hook releases them at shutdown. All release paths are
|
|
56
|
+
best-effort and never throw.
|
|
44
57
|
|
|
45
58
|
### Write path (memory_remember)
|
|
46
59
|
|
|
@@ -85,9 +85,12 @@ Runs in a single DB transaction:
|
|
|
85
85
|
|
|
86
86
|
1. **Matcher dedup** — finds an existing matcher above cosine 0.85, else
|
|
87
87
|
creates one.
|
|
88
|
-
2. **Prediction dedup** — store
|
|
89
|
-
(cosine >= 0.85). If found, links this matcher to
|
|
90
|
-
creating a duplicate row.
|
|
88
|
+
2. **Prediction dedup** — search across the target store AND global for a
|
|
89
|
+
near-identical prediction (cosine >= 0.85). If found, links this matcher to
|
|
90
|
+
it via an edge rather than creating a duplicate row. The auto-fire nudge
|
|
91
|
+
scans both stores, so a prediction saved to both would otherwise surface
|
|
92
|
+
twice every turn; the write reinforces the existing row in its home store
|
|
93
|
+
and the result echoes where it landed.
|
|
91
94
|
3. **New prediction with a non-create signal** — applies the signal
|
|
92
95
|
immediately so the first confirm/disconfirm is not lost.
|
|
93
96
|
4. **`create` on an existing prediction** — links the edge and records
|
package/docs/dev/skills.md
CHANGED
|
@@ -46,6 +46,7 @@ description: Extract durable project facts ... Use when ...
|
|
|
46
46
|
| `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of behavior? |
|
|
47
47
|
| `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
|
|
48
48
|
| `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar against generic praise. |
|
|
49
|
+
| `thatch-review-mentorship` | Peer-mentorship lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, verified citations, never blocking. |
|
|
49
50
|
| `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. Starts the final report with a workflow-change preface, then cross-references findings against prior review comments when a follow-up round register is provided. |
|
|
50
51
|
| `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, linked docs/tickets followed from the change, memory) before fan-out. Prevents false positives about intentionally deferred work, and checks whether main moved past the merge-base touching the same paths (staleness signal for the synthesizer). Also fetches prior review comments on a connected PR/MR for follow-up-round detection and builds a register with preliminary addressed-check status per comment. |
|
|
51
52
|
| `thatch-code-archaeology` | Investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features, skeletons) before proposing changes. The research skill; pair with `thatch-coding-workflow`. |
|
|
@@ -92,6 +93,8 @@ The seven problem-finding review specialists share a framework interpolated via
|
|
|
92
93
|
ticket is closed or merged.
|
|
93
94
|
- **Output format** — `[SEVERITY] [CATEGORY] file:line` with finding, evidence
|
|
94
95
|
(quoted), trigger, reachability, source of truth, producer chain, provenance.
|
|
96
|
+
- **Wording** — findings are prose for humans, not shorthand: one idea per
|
|
97
|
+
sentence, no method names as verbs, one sentence per hop for producer chains.
|
|
95
98
|
|
|
96
99
|
The synthesizer reuses the same verification rigor but has its own structure
|
|
97
100
|
(it does not interpolate `REVIEW_COMMON`).
|
package/docs/user/README.md
CHANGED
|
@@ -278,6 +278,7 @@ response, walkthrough, and workflow skills that support the review pipeline:
|
|
|
278
278
|
| `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of the code's behavior? |
|
|
279
279
|
| `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
|
|
280
280
|
| `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar against generic praise. |
|
|
281
|
+
| `thatch-review-mentorship` | Peer-mentorship lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, never blocking. |
|
|
281
282
|
| `thatch-review-synthesizer` | Verifies and synthesizes findings from multiple specialists into a report that starts with workflow changes, then highlights, then deduplicated severity-grouped findings. |
|
|
282
283
|
| `thatch-review-context` | Gathers project context (PR descriptions, git archaeology, ticket references, linked docs/tickets, memory) before a review. Prevents false positives about intentionally deferred work. |
|
|
283
284
|
| `thatch-code-archaeology` | Investigates an existing feature, debugs an unfamiliar area, or begins a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features, skeletons) before proposing changes. Pairs with `thatch-coding-workflow`. |
|
|
@@ -409,6 +410,7 @@ Most configuration needs none. Two layers exist:
|
|
|
409
410
|
|----------|---------|-------------|
|
|
410
411
|
| `THATCH_DB_PATH` | `$XDG_CONFIG_HOME/thatch/thatch.db` | Override database location (the config file follows it) |
|
|
411
412
|
| `THATCH_MODEL` | `Xenova/bge-small-en-v1.5` | Override embedding model |
|
|
413
|
+
| `THATCH_EMBEDDING_BACKEND` | `wasm` | Set to `native` to run embeddings on onnxruntime-node (NAPI) instead of the wasm runtime |
|
|
412
414
|
|
|
413
415
|
Unchanged defaults: the database is created automatically, the embedding
|
|
414
416
|
model downloads once and is cached, the store name is auto-detected from
|
package/docs/user/code-review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Code Review
|
|
2
2
|
|
|
3
|
-
Thatch ships a multi-agent code review pipeline.
|
|
3
|
+
Thatch ships a multi-agent code review pipeline. Nine specialist
|
|
4
4
|
review lenses run in parallel, then a synthesizer verifies and
|
|
5
5
|
deduplicates their findings into a single report.
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ the review:
|
|
|
22
22
|
3. **Research affected workflows**: load `thatch-code-archaeology`,
|
|
23
23
|
trace the code paths the change touches.
|
|
24
24
|
4. **Estimate complexity**: partition the diff into review units.
|
|
25
|
-
5. **Dispatch specialists**: fan out the
|
|
25
|
+
5. **Dispatch specialists**: fan out the nine specialists as
|
|
26
26
|
parallel sub-agents, each with the context brief.
|
|
27
27
|
6. **Synthesize**: the synthesizer reads all specialist output,
|
|
28
28
|
verifies each finding (cited text exists, claim is accurate,
|
|
@@ -31,7 +31,7 @@ the review:
|
|
|
31
31
|
re-verifies behavioral findings against current main and reports
|
|
32
32
|
merge-base-only findings as rebase notes rather than defects.
|
|
33
33
|
|
|
34
|
-
## The
|
|
34
|
+
## The nine specialists
|
|
35
35
|
|
|
36
36
|
| Skill | What it checks |
|
|
37
37
|
|-------|-------------|
|
|
@@ -43,6 +43,7 @@ the review:
|
|
|
43
43
|
| `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of the code's behavior? |
|
|
44
44
|
| `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
|
|
45
45
|
| `thatch-review-highlights` | Positive finding detection: clever solutions, cleanup done along the way, documentation that helps. Medium-high bar. |
|
|
46
|
+
| `thatch-review-mentorship` | Peer-mentorship lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, never blocking. |
|
|
46
47
|
|
|
47
48
|
Each specialist is a self-contained static-analysis pass. They can run
|
|
48
49
|
individually or as part of the coordinated review.
|
package/docs/user/memory.md
CHANGED
|
@@ -49,6 +49,9 @@ store when recalling.
|
|
|
49
49
|
(MCP).
|
|
50
50
|
- `THATCH_MODEL`: Hugging Face model name for embeddings. Defaults to
|
|
51
51
|
`Xenova/bge-small-en-v1.5` (34 MB, 384-dimensional).
|
|
52
|
+
- `THATCH_EMBEDDING_BACKEND`: embedding compute backend. Defaults to
|
|
53
|
+
`wasm` (pure JavaScript/WebAssembly, no native addons). Set to
|
|
54
|
+
`native` to use the onnxruntime-node addon instead.
|
|
52
55
|
- `THATCH_RECALL_THRESHOLD`: cosine score threshold for the recall
|
|
53
56
|
nudge. Defaults to 0.55. Lower surfaces more matches (noisier);
|
|
54
57
|
higher surfaces fewer (stricter).
|
package/docs/user/setup.md
CHANGED
|
@@ -75,6 +75,7 @@ and `--json` output for hook commands.
|
|
|
75
75
|
|----------|---------|-----------------|
|
|
76
76
|
| `THATCH_DB_PATH` | `~/.config/thatch/thatch.db` | SQLite database path |
|
|
77
77
|
| `THATCH_MODEL` | `Xenova/bge-small-en-v1.5` | Hugging Face model name for embeddings |
|
|
78
|
+
| `THATCH_EMBEDDING_BACKEND` | `wasm` | Set to `native` to run embeddings on onnxruntime-node (NAPI) instead of the wasm runtime |
|
|
78
79
|
| `THATCH_RECALL_THRESHOLD` | `0.55` | Cosine threshold for recall nudge |
|
|
79
80
|
| `THATCH_PREDICTION_THRESHOLD` | `0.60` | Cosine threshold for prediction auto-fire |
|
|
80
81
|
| `THATCH_BEHAVIOR_THRESHOLD` | `0.60` | Cosine threshold for behavior auto-fire |
|
|
@@ -101,8 +102,9 @@ instructions are deployed without requiring you to manually re-run
|
|
|
101
102
|
cached in `node_modules/@huggingface/transformers/.cache/`.
|
|
102
103
|
- There is no web UI or dashboard. All interaction is through the
|
|
103
104
|
agent's tool calls and the `thatch` CLI.
|
|
104
|
-
- The MCP server is a long-lived process that
|
|
105
|
-
|
|
105
|
+
- The MCP server is a long-lived process that keeps the embedding model
|
|
106
|
+
loaded while it is in use, releases it after 10 idle minutes, and
|
|
107
|
+
re-loads it on demand. Hook commands connect to it via a Unix domain
|
|
106
108
|
socket (the sideband) to avoid loading the model themselves.
|
|
107
109
|
|
|
108
110
|
See [memory.md](memory.md) for the memory system,
|
package/docs/user/skills.md
CHANGED
|
@@ -45,6 +45,7 @@ Skills are split into shared (all hosts) and opencode-only:
|
|
|
45
45
|
| `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of the code's behavior? |
|
|
46
46
|
| `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
|
|
47
47
|
| `thatch-review-highlights` | Positive finding detection: notably clever solutions, cleanup done along the way, documentation that helps. Medium-high bar. |
|
|
48
|
+
| `thatch-review-mentorship` | Peer-mentorship lens: teaching-grade notes on unseen helpers/internal packages, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Informational only, never blocking. |
|
|
48
49
|
| `thatch-review-synthesizer` | Verifies and synthesizes findings from multiple specialists into a deduplicated, severity-grouped report. |
|
|
49
50
|
| `thatch-review-context` | Gathers project context (PRs, tickets, linked docs/designs followed from the change, TODOs, deferred work) before a review. Prevents false positives about intentionally deferred work. |
|
|
50
51
|
| `thatch-code-archaeology` | Investigates an existing feature, debugs an unfamiliar area, or begins a new ticket. The research skill; pair with `thatch-coding-workflow`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@huggingface/transformers": "^4.0.0",
|
|
27
|
+
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
|
|
27
28
|
"zod": "^4.1.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
package/src/behavior.ts
CHANGED
|
@@ -82,7 +82,7 @@ export class BehaviorEngine {
|
|
|
82
82
|
return this.#engine.createMatcher(store, description, embedding, model);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
findNearestBehavior(store: string, embedding: Float32Array, threshold: number): BehaviorRow | null {
|
|
85
|
+
findNearestBehavior(store: string | string[], embedding: Float32Array, threshold: number): BehaviorRow | null {
|
|
86
86
|
return this.#engine.findNearestItem(store, embedding, threshold) as BehaviorRow | null;
|
|
87
87
|
}
|
|
88
88
|
|
package/src/db.ts
CHANGED
|
@@ -612,7 +612,7 @@ export class ThatchDB {
|
|
|
612
612
|
return this.#predictions.createMatcher(store, description, embedding, model);
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
-
findNearestPrediction(store: string, embedding: Float32Array, threshold: number) {
|
|
615
|
+
findNearestPrediction(store: string | string[], embedding: Float32Array, threshold: number) {
|
|
616
616
|
return this.#predictions.findNearestPrediction(store, embedding, threshold);
|
|
617
617
|
}
|
|
618
618
|
|
|
@@ -672,7 +672,7 @@ export class ThatchDB {
|
|
|
672
672
|
return this.#behaviors.createBehaviorMatcher(store, description, embedding, model);
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
findNearestBehavior(store: string, embedding: Float32Array, threshold: number) {
|
|
675
|
+
findNearestBehavior(store: string | string[], embedding: Float32Array, threshold: number) {
|
|
676
676
|
return this.#behaviors.findNearestBehavior(store, embedding, threshold);
|
|
677
677
|
}
|
|
678
678
|
|
package/src/embeddings.ts
CHANGED
|
@@ -25,11 +25,99 @@ export interface EmbeddingModel {
|
|
|
25
25
|
*/
|
|
26
26
|
export type PipelineFactory = (modelName: string) => Promise<any>;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Embedding backend mode. "wasm" runs the ONNX model on onnxruntime-web's
|
|
30
|
+
* pure-JS/wasm runtime; "native" runs it on onnxruntime-node's NAPI addon.
|
|
31
|
+
*/
|
|
32
|
+
export type EmbeddingBackendMode = "wasm" | "native";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reads the backend override. Only "native" opts out; any other value,
|
|
36
|
+
* including unset, selects the wasm default. Exported for tests.
|
|
37
|
+
*/
|
|
38
|
+
export function backendMode(): EmbeddingBackendMode {
|
|
39
|
+
return process.env.THATCH_EMBEDDING_BACKEND === "native" ? "native" : "wasm";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ORT_SYMBOL = Symbol.for("onnxruntime");
|
|
43
|
+
|
|
44
|
+
let backendConfigured: Promise<EmbeddingBackendMode> | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Chooses the ONNX backend before transformers initializes. Memoized: the
|
|
48
|
+
* choice is made once at transformers' module init and cannot be changed
|
|
49
|
+
* afterwards. Exported for tests.
|
|
50
|
+
*
|
|
51
|
+
* "wasm" mode pins globalThis[Symbol.for("onnxruntime")] to onnxruntime-web
|
|
52
|
+
* so transformers' node build creates sessions on its pure-JS/wasm runtime
|
|
53
|
+
* instead of the onnxruntime-node NAPI addon. The NAPI addon leaves wrap
|
|
54
|
+
* finalizers that panic Bun at process teardown (oven-sh/bun#34664); the wasm
|
|
55
|
+
* runtime creates none. A backend already chosen by the host (symbol present)
|
|
56
|
+
* is respected, never clobbered. A failed onnxruntime-web import falls back
|
|
57
|
+
* to native rather than blocking embedding.
|
|
58
|
+
*/
|
|
59
|
+
export async function configureBackend(): Promise<EmbeddingBackendMode> {
|
|
60
|
+
backendConfigured ??= (async () => {
|
|
61
|
+
const mode = backendMode();
|
|
62
|
+
if (mode === "native" || ORT_SYMBOL in globalThis) return mode;
|
|
63
|
+
try {
|
|
64
|
+
const ort = await import("onnxruntime-web");
|
|
65
|
+
(globalThis as Record<symbol, unknown>)[ORT_SYMBOL] = ort;
|
|
66
|
+
// Single-threaded wasm: deterministic, and avoids worker_threads,
|
|
67
|
+
// where Bun has known shutdown crashes. Latency at one thread is fine
|
|
68
|
+
// for background embedding.
|
|
69
|
+
ort.env.wasm.numThreads = 1;
|
|
70
|
+
} catch {
|
|
71
|
+
// onnxruntime-web unavailable - native sessions still work.
|
|
72
|
+
}
|
|
73
|
+
return mode;
|
|
74
|
+
})();
|
|
75
|
+
return backendConfigured;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Test hook: clears the configureBackend memo and unpins the onnxruntime
|
|
80
|
+
* global, so a test can exercise the pinning path itself. Needed because the
|
|
81
|
+
* test process is shared across files: initializing the plugin server seeds
|
|
82
|
+
* behaviors, which embeds, which runs configureBackend and pins the symbol
|
|
83
|
+
* as a side effect. Whether that happened before a given test file runs
|
|
84
|
+
* depends on bun's file order, which differs between machines.
|
|
85
|
+
*/
|
|
86
|
+
export function resetBackendForTests(): void {
|
|
87
|
+
backendConfigured = null;
|
|
88
|
+
delete (globalThis as Record<symbol, unknown>)[ORT_SYMBOL];
|
|
89
|
+
}
|
|
90
|
+
|
|
28
91
|
const defaultPipelineFactory: PipelineFactory = async (modelName) => {
|
|
92
|
+
const mode = await configureBackend();
|
|
29
93
|
const { pipeline } = await import("@huggingface/transformers");
|
|
30
|
-
|
|
94
|
+
if (mode === "native") {
|
|
95
|
+
return pipeline("feature-extraction", modelName);
|
|
96
|
+
}
|
|
97
|
+
// The ORT_SYMBOL override branch leaves transformers' device allowlist
|
|
98
|
+
// empty, so any named device throws; "auto" bypasses the check and the
|
|
99
|
+
// explicit execution provider does the real work.
|
|
100
|
+
return pipeline("feature-extraction", modelName, {
|
|
101
|
+
device: "auto",
|
|
102
|
+
session_options: { executionProviders: ["wasm"] },
|
|
103
|
+
});
|
|
31
104
|
};
|
|
32
105
|
|
|
106
|
+
/**
|
|
107
|
+
* How long a loaded pipeline may sit unused before its native ONNX sessions
|
|
108
|
+
* are released while the runtime is still healthy. Long enough to span the
|
|
109
|
+
* gap between extraction bursts, short enough to matter before process exit.
|
|
110
|
+
*/
|
|
111
|
+
const DEFAULT_IDLE_TTL_MS = 10 * 60 * 1000;
|
|
112
|
+
|
|
113
|
+
export interface BgeEmbeddingModelOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Idle milliseconds before the pipeline's native sessions are released.
|
|
116
|
+
* A later embed lazily re-loads. 0 disables idle release.
|
|
117
|
+
*/
|
|
118
|
+
idleTtlMs?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
33
121
|
/**
|
|
34
122
|
* Lazy-loads an embedding model via @huggingface/transformers.
|
|
35
123
|
* Model files (~34 MB for the default) are downloaded once and cached by HF Hub.
|
|
@@ -37,12 +125,19 @@ const defaultPipelineFactory: PipelineFactory = async (modelName) => {
|
|
|
37
125
|
export class BgeEmbeddingModel implements EmbeddingModel {
|
|
38
126
|
#modelName: string;
|
|
39
127
|
#pipelineFactory: PipelineFactory;
|
|
128
|
+
#idleTtlMs: number;
|
|
40
129
|
#pipe: any = null;
|
|
41
130
|
#loading: Promise<void> | null = null;
|
|
131
|
+
#idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
42
132
|
|
|
43
|
-
constructor(
|
|
133
|
+
constructor(
|
|
134
|
+
modelName = "Xenova/bge-small-en-v1.5",
|
|
135
|
+
pipelineFactory?: PipelineFactory,
|
|
136
|
+
options: BgeEmbeddingModelOptions = {},
|
|
137
|
+
) {
|
|
44
138
|
this.#modelName = modelName;
|
|
45
139
|
this.#pipelineFactory = pipelineFactory ?? defaultPipelineFactory;
|
|
140
|
+
this.#idleTtlMs = options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
46
141
|
}
|
|
47
142
|
|
|
48
143
|
get loaded(): boolean {
|
|
@@ -75,12 +170,31 @@ export class BgeEmbeddingModel implements EmbeddingModel {
|
|
|
75
170
|
}
|
|
76
171
|
|
|
77
172
|
async #embed(text: string): Promise<Float32Array> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
173
|
+
// Clear any pending idle timer before touching the pipeline. The timer
|
|
174
|
+
// must never fire while an embed is in flight - its callback releases
|
|
175
|
+
// the pipeline this call is about to use.
|
|
176
|
+
this.#clearIdleTimer();
|
|
177
|
+
try {
|
|
178
|
+
await this.load();
|
|
179
|
+
const pipe = this.#pipe;
|
|
180
|
+
const output = await pipe(text, {
|
|
181
|
+
pooling: "mean",
|
|
182
|
+
normalize: true,
|
|
183
|
+
});
|
|
184
|
+
// Copy the vector before releasing the tensor: the real Tensor's data
|
|
185
|
+
// getter exposes memory the disposer frees.
|
|
186
|
+
const data = new Float32Array(output.data);
|
|
187
|
+
if (typeof output?.dispose === "function") {
|
|
188
|
+
try {
|
|
189
|
+
await output.dispose();
|
|
190
|
+
} catch {
|
|
191
|
+
// Tensor release is best-effort; the copied data is already valid.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return data;
|
|
195
|
+
} finally {
|
|
196
|
+
this.#armIdleTimer();
|
|
197
|
+
}
|
|
84
198
|
}
|
|
85
199
|
|
|
86
200
|
/**
|
|
@@ -89,11 +203,16 @@ export class BgeEmbeddingModel implements EmbeddingModel {
|
|
|
89
203
|
* the runtime is still alive. If instead they survive to Bun's worker
|
|
90
204
|
* teardown, Bun panics creating their errors (oven-sh/bun#34664).
|
|
91
205
|
*
|
|
206
|
+
* The idle timer releases on the same principle during normal operation:
|
|
207
|
+
* the plugin's dispose hook is best-effort because opencode can exit
|
|
208
|
+
* without running it, so sessions are also freed after a quiet period.
|
|
209
|
+
*
|
|
92
210
|
* Best-effort by design: dispose never throws, and a failed release is
|
|
93
211
|
* indistinguishable from never loading. An embed call after dispose just
|
|
94
212
|
* lazily re-loads the model.
|
|
95
213
|
*/
|
|
96
214
|
async dispose(): Promise<void> {
|
|
215
|
+
this.#clearIdleTimer();
|
|
97
216
|
// An in-flight load memoizes a rejecting promise on failure. Await it
|
|
98
217
|
// defensively so its error neither escapes dispose nor kills the teardown.
|
|
99
218
|
if (this.#loading) {
|
|
@@ -103,6 +222,10 @@ export class BgeEmbeddingModel implements EmbeddingModel {
|
|
|
103
222
|
// Load failed - nothing was loaded, so nothing to release.
|
|
104
223
|
}
|
|
105
224
|
}
|
|
225
|
+
await this.#release();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async #release(): Promise<void> {
|
|
106
229
|
const pipe = this.#pipe;
|
|
107
230
|
this.#pipe = null;
|
|
108
231
|
this.#loading = null;
|
|
@@ -110,7 +233,27 @@ export class BgeEmbeddingModel implements EmbeddingModel {
|
|
|
110
233
|
try {
|
|
111
234
|
await pipe.dispose();
|
|
112
235
|
} catch {
|
|
113
|
-
// Best-effort - see doc comment
|
|
236
|
+
// Best-effort - see dispose's doc comment.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#armIdleTimer(): void {
|
|
241
|
+
if (this.#idleTtlMs <= 0) return;
|
|
242
|
+
this.#clearIdleTimer();
|
|
243
|
+
this.#idleTimer = setTimeout(() => {
|
|
244
|
+
this.#idleTimer = null;
|
|
245
|
+
this.#release().catch(() => {
|
|
246
|
+
// Idle release is best-effort; a failed release retries on dispose.
|
|
247
|
+
});
|
|
248
|
+
}, this.#idleTtlMs);
|
|
249
|
+
// Never hold the host process open for an idle timer.
|
|
250
|
+
this.#idleTimer.unref?.();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#clearIdleTimer(): void {
|
|
254
|
+
if (this.#idleTimer) {
|
|
255
|
+
clearTimeout(this.#idleTimer);
|
|
256
|
+
this.#idleTimer = null;
|
|
114
257
|
}
|
|
115
258
|
}
|
|
116
259
|
}
|
package/src/prediction.ts
CHANGED
|
@@ -89,7 +89,7 @@ export class PredictionEngine {
|
|
|
89
89
|
return this.#engine.createMatcher(store, description, embedding, model);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
findNearestPrediction(store: string, embedding: Float32Array, threshold: number): PredictionRow | null {
|
|
92
|
+
findNearestPrediction(store: string | string[], embedding: Float32Array, threshold: number): PredictionRow | null {
|
|
93
93
|
return this.#engine.findNearestItem(store, embedding, threshold) as PredictionRow | null;
|
|
94
94
|
}
|
|
95
95
|
|
package/src/prompts.ts
CHANGED
|
@@ -338,7 +338,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
|
|
|
338
338
|
- \`thatch-code-walkthrough\` - produces a teaching walkthrough for a feature/workflow as it stands today with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + Key files) and calibrated prose rules. Also use proactively to draft high-level docs for new or undocumented features (suitable for pkg/README.md or docs/features/<name>.md). Load the skill for the format and prose rules, not just for the research method.
|
|
339
339
|
- \`thatch-review-context\` - gather project context (PRs, tickets, TODOs, deferred work) before a review.
|
|
340
340
|
- \`thatch-review-followup\` - alternate entrypoint for follow-up review rounds. Verifies whether the author's responses and code changes since your last review adequately addressed prior findings, offers to reply on resolved items, then optionally re-runs the full structured review.
|
|
341
|
-
- \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-economy\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` / \`-highlights\` -
|
|
341
|
+
- \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-economy\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` / \`-highlights\` / \`-mentorship\` - nine specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
|
|
342
342
|
- \`thatch-project-primer\` - investigate a new project and write foundational memories.
|
|
343
343
|
- \`thatch-session-reflection\` - record what you learned at end of session.
|
|
344
344
|
- \`thatch-fact-extractor\` - extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
|
package/src/scoring-engine.ts
CHANGED
|
@@ -118,7 +118,7 @@ export class ScoringEngine {
|
|
|
118
118
|
const rows = this.#db
|
|
119
119
|
.query(
|
|
120
120
|
`SELECT p.id, p.statement, p.rationale, p.confidence, p.confirm_count, p.disconfirm_count,
|
|
121
|
-
e.matcher_id, e.weight
|
|
121
|
+
p.embedding, e.matcher_id, e.weight
|
|
122
122
|
FROM ${edgesTable} e
|
|
123
123
|
JOIN ${itemsTable} p ON e.${itemForeignKey} = p.id
|
|
124
124
|
WHERE e.matcher_id IN (${placeholders})
|
|
@@ -128,6 +128,7 @@ export class ScoringEngine {
|
|
|
128
128
|
|
|
129
129
|
const matcherMap = new Map(matchers.map((m) => [m.id, m]));
|
|
130
130
|
const scored: ScoredItem[] = [];
|
|
131
|
+
const embeddings = new Map<string, Float32Array>();
|
|
131
132
|
for (const r of rows) {
|
|
132
133
|
const matcher = matcherMap.get(r.matcher_id);
|
|
133
134
|
if (!matcher) continue;
|
|
@@ -135,6 +136,7 @@ export class ScoringEngine {
|
|
|
135
136
|
const confidence = r.confidence as number;
|
|
136
137
|
const evidence = Math.round(r.confirm_count + r.disconfirm_count);
|
|
137
138
|
const score = matcher.score * (r.weight as number) * confidence;
|
|
139
|
+
if (r.embedding) embeddings.set(r.id as string, blobToVector(r.embedding));
|
|
138
140
|
scored.push({
|
|
139
141
|
matcher_id: r.matcher_id,
|
|
140
142
|
matcher_description: matcher.description,
|
|
@@ -153,9 +155,25 @@ export class ScoringEngine {
|
|
|
153
155
|
// so the nudge does not repeat the same item with different matcher
|
|
154
156
|
// contexts.
|
|
155
157
|
const seen = new Set<string>();
|
|
158
|
+
// Semantic dedup across stores: the same preference saved to both the
|
|
159
|
+
// project and global stores has different row ids but near-identical
|
|
160
|
+
// embeddings. Without this, the multi-store nudge scan surfaces it
|
|
161
|
+
// twice every turn. Sorted by score descending, so the first occurrence
|
|
162
|
+
// (highest-scoring copy) wins.
|
|
163
|
+
const SEMANTIC_DEDUP_COSINE = 0.85;
|
|
164
|
+
const seenEmbeddings: Float32Array[] = [];
|
|
156
165
|
return scored.filter((s) => {
|
|
157
166
|
if (seen.has(s.item_id)) return false;
|
|
158
167
|
seen.add(s.item_id);
|
|
168
|
+
const emb = embeddings.get(s.item_id);
|
|
169
|
+
if (emb) {
|
|
170
|
+
for (const other of seenEmbeddings) {
|
|
171
|
+
if (other.length === emb.length && cosineSimilarity(other, emb) >= SEMANTIC_DEDUP_COSINE) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
seenEmbeddings.push(emb);
|
|
176
|
+
}
|
|
159
177
|
return true;
|
|
160
178
|
});
|
|
161
179
|
}
|
|
@@ -219,18 +237,21 @@ export class ScoringEngine {
|
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
findNearestItem(
|
|
222
|
-
store: string,
|
|
240
|
+
store: string | string[],
|
|
223
241
|
embedding: Float32Array,
|
|
224
242
|
threshold: number,
|
|
225
243
|
): ItemRow | null {
|
|
226
244
|
const { itemsTable } = this.#cfg;
|
|
245
|
+
const stores = Array.isArray(store) ? store : [store];
|
|
246
|
+
if (stores.length === 0) return null;
|
|
247
|
+
const placeholders = stores.map(() => "?").join(", ");
|
|
227
248
|
const rows = this.#db
|
|
228
249
|
.query(
|
|
229
250
|
`SELECT id, store, statement, rationale, confidence, confirm_count, disconfirm_count, created_at, updated_at, embedding
|
|
230
251
|
FROM ${itemsTable}
|
|
231
|
-
WHERE store
|
|
252
|
+
WHERE store IN (${placeholders}) AND embedding IS NOT NULL`,
|
|
232
253
|
)
|
|
233
|
-
.all(
|
|
254
|
+
.all(...(stores as [string, ...string[]])) as any[];
|
|
234
255
|
|
|
235
256
|
let best: { row: any; score: number } | null = null;
|
|
236
257
|
for (const r of rows) {
|
package/src/skills.ts
CHANGED
package/src/tool-defs.ts
CHANGED
|
@@ -23,6 +23,26 @@ const MATCHER_DEDUP_COSINE = 0.85;
|
|
|
23
23
|
const PREDICTION_DEDUP_COSINE = 0.85;
|
|
24
24
|
const BEHAVIOR_DEDUP_COSINE = 0.85;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the write target for a store-scoped tool. detectRepo falls back
|
|
28
|
+
* to "unknown" when it cannot name a repo; rows written to a store called
|
|
29
|
+
* "unknown" are invisible to every auto-fire scan (which reads the project
|
|
30
|
+
* store and "global"), so an unidentified context falls back to global.
|
|
31
|
+
*/
|
|
32
|
+
function resolveStore(args: Record<string, unknown>, ctx: CoreContext): string {
|
|
33
|
+
const store = (args.store as string) || ctx.defaultStore;
|
|
34
|
+
return store === "unknown" ? "global" : store;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The stores whose items a write must dedup against: the target store plus
|
|
39
|
+
* global, matching the stores the auto-fire nudge scans. A duplicate in
|
|
40
|
+
* either store would otherwise surface twice every turn.
|
|
41
|
+
*/
|
|
42
|
+
function dedupScanStores(store: string): string[] {
|
|
43
|
+
return store === "global" ? ["global"] : [store, "global"];
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
// Minimum matcher cosine to consider a prediction relevant. Matches the
|
|
27
47
|
// auto-fire threshold in index.ts (PREDICTION_THRESHOLD). The query tool
|
|
28
48
|
// should not return predictions from near-zero-similarity matchers that
|
|
@@ -192,7 +212,7 @@ const rememberDef: ToolDef = {
|
|
|
192
212
|
),
|
|
193
213
|
},
|
|
194
214
|
async execute(args, ctx) {
|
|
195
|
-
const store = (args
|
|
215
|
+
const store = resolveStore(args, ctx);
|
|
196
216
|
const label = args.label as string;
|
|
197
217
|
const content = `# ${label}\n\n${args.content as string}`;
|
|
198
218
|
const embedding = await ctx.model.passageEmbed(content);
|
|
@@ -276,7 +296,7 @@ const listDef: ToolDef = {
|
|
|
276
296
|
),
|
|
277
297
|
},
|
|
278
298
|
async execute(args, ctx) {
|
|
279
|
-
const store = (args
|
|
299
|
+
const store = resolveStore(args, ctx);
|
|
280
300
|
const entries = ctx.db.listEntries(store);
|
|
281
301
|
|
|
282
302
|
if (entries.length === 0) return `No memories in "${store}".`;
|
|
@@ -306,7 +326,7 @@ const showDef: ToolDef = {
|
|
|
306
326
|
),
|
|
307
327
|
},
|
|
308
328
|
async execute(args, ctx) {
|
|
309
|
-
const store = (args
|
|
329
|
+
const store = resolveStore(args, ctx);
|
|
310
330
|
const entry = ctx.db.showEntry(store, args.label as string);
|
|
311
331
|
|
|
312
332
|
if (!entry) return `No memory labeled "${args.label as string}" found in store "${store}".`;
|
|
@@ -327,7 +347,7 @@ const forgetDef: ToolDef = {
|
|
|
327
347
|
),
|
|
328
348
|
},
|
|
329
349
|
async execute(args, ctx) {
|
|
330
|
-
const store = (args
|
|
350
|
+
const store = resolveStore(args, ctx);
|
|
331
351
|
const deleted = ctx.db.forgetEntry(store, args.label as string);
|
|
332
352
|
|
|
333
353
|
if (!deleted) return `No memory labeled "${args.label as string}" found in store "${store}".`;
|
|
@@ -366,7 +386,7 @@ const findDuplicatesDef: ToolDef = {
|
|
|
366
386
|
),
|
|
367
387
|
},
|
|
368
388
|
async execute(args, ctx) {
|
|
369
|
-
const store = (args
|
|
389
|
+
const store = resolveStore(args, ctx);
|
|
370
390
|
const threshold = (args.threshold as number) ?? 0.85;
|
|
371
391
|
const candidates = ctx.db.findDuplicates(store, threshold);
|
|
372
392
|
|
|
@@ -394,7 +414,7 @@ const markCheckedDef: ToolDef = {
|
|
|
394
414
|
),
|
|
395
415
|
},
|
|
396
416
|
async execute(args, ctx) {
|
|
397
|
-
const store = (args
|
|
417
|
+
const store = resolveStore(args, ctx);
|
|
398
418
|
ctx.db.markPairChecked(
|
|
399
419
|
store,
|
|
400
420
|
ctx.db.slugify(args.label_a as string),
|
|
@@ -551,7 +571,7 @@ const predictionUpdateDef: ToolDef = {
|
|
|
551
571
|
),
|
|
552
572
|
},
|
|
553
573
|
async execute(args, ctx) {
|
|
554
|
-
const store = (args
|
|
574
|
+
const store = resolveStore(args, ctx);
|
|
555
575
|
const matcherText = args.matcher as string;
|
|
556
576
|
const predictionText = args.prediction as string;
|
|
557
577
|
const signal = args.signal as "confirm" | "disconfirm" | "soft" | "create";
|
|
@@ -565,20 +585,23 @@ const predictionUpdateDef: ToolDef = {
|
|
|
565
585
|
// entire operation rather than leaving orphans (matcher without
|
|
566
586
|
// prediction, prediction without edge, edge without confidence).
|
|
567
587
|
return ctx.db.transaction(() => {
|
|
588
|
+
// Cross-store dedup: search the target store AND global for a
|
|
589
|
+
// near-identical prediction. The auto-fire nudge scans both stores,
|
|
590
|
+
// so two copies would surface twice every turn. When the prediction
|
|
591
|
+
// already exists in the other store, that store becomes the write
|
|
592
|
+
// target ("home") and the result echoes where it landed.
|
|
593
|
+
const existing = ctx.db.findNearestPrediction(dedupScanStores(store), predEmbed, PREDICTION_DEDUP_COSINE);
|
|
594
|
+
const home = existing?.store ?? store;
|
|
595
|
+
const storeTag = home === store ? "" : ` in ${home}`;
|
|
596
|
+
|
|
568
597
|
// Dedup matchers: find an existing matcher above the cosine threshold
|
|
569
598
|
// rather than always creating a new one.
|
|
570
|
-
let matcherId = ctx.db.findNearestMatcher(
|
|
571
|
-
if (!matcherId) matcherId = ctx.db.createMatcher(
|
|
572
|
-
|
|
573
|
-
// Store-wide dedup: search the entire store for a near-identical
|
|
574
|
-
// prediction, not just this matcher's edges. If found, link this
|
|
575
|
-
// matcher to the existing prediction via an edge rather than
|
|
576
|
-
// creating a second row with the same statement.
|
|
577
|
-
let predictionId = ctx.db.findNearestPrediction(store, predEmbed, PREDICTION_DEDUP_COSINE)?.id;
|
|
599
|
+
let matcherId = ctx.db.findNearestMatcher(home, matcherEmbed, MATCHER_DEDUP_COSINE)?.id;
|
|
600
|
+
if (!matcherId) matcherId = ctx.db.createMatcher(home, matcherText, matcherEmbed, ctx.model.name);
|
|
578
601
|
|
|
579
|
-
if (!
|
|
602
|
+
if (!existing) {
|
|
580
603
|
// No near-identical prediction exists; create one and link it.
|
|
581
|
-
predictionId = ctx.db.createPrediction(
|
|
604
|
+
const predictionId = ctx.db.createPrediction(home, predictionText, rationale, predEmbed, ctx.model.name);
|
|
582
605
|
ctx.db.createEdge(matcherId, predictionId, 1.0);
|
|
583
606
|
|
|
584
607
|
// When the signal is confirm/disconfirm/soft (not create), apply
|
|
@@ -593,9 +616,11 @@ const predictionUpdateDef: ToolDef = {
|
|
|
593
616
|
return `[created + ${signal}] "${updated?.statement ?? predictionText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
|
|
594
617
|
}
|
|
595
618
|
ctx.db.addProvenance(predictionId, "create", rationale);
|
|
596
|
-
return `[created] ${
|
|
619
|
+
return `[created] ${home} :: "${predictionText}" for "${matcherText}"`;
|
|
597
620
|
}
|
|
598
621
|
|
|
622
|
+
const predictionId = existing.id;
|
|
623
|
+
|
|
599
624
|
// Ensure an edge links this matcher to the existing prediction.
|
|
600
625
|
// createEdge uses ON CONFLICT DO NOTHING, so existing edge weights
|
|
601
626
|
// are preserved.
|
|
@@ -607,8 +632,8 @@ const predictionUpdateDef: ToolDef = {
|
|
|
607
632
|
// confidence-neutral, not a disconfirm.
|
|
608
633
|
if (signal === "create") {
|
|
609
634
|
ctx.db.addProvenance(predictionId, "create", rationale);
|
|
610
|
-
const
|
|
611
|
-
return `[linked] "${
|
|
635
|
+
const existingItem = ctx.db.getPrediction(predictionId);
|
|
636
|
+
return `[linked${storeTag}] "${existingItem?.statement ?? predictionText}" for "${matcherText}" confidence=${(existingItem?.confidence ?? 0).toFixed(2)} (${existingItem?.confirm_count ?? 0}/${existingItem?.disconfirm_count ?? 0})`;
|
|
612
637
|
}
|
|
613
638
|
|
|
614
639
|
// Signal is confirm, disconfirm, or soft. Map the tool's 4-value
|
|
@@ -616,7 +641,7 @@ const predictionUpdateDef: ToolDef = {
|
|
|
616
641
|
ctx.db.adjustConfidence(predictionId, signal === "soft" ? "soft" : signal === "confirm" ? "confirm" : "disconfirm");
|
|
617
642
|
ctx.db.addProvenance(predictionId, signal, rationale);
|
|
618
643
|
const updated = ctx.db.getPrediction(predictionId);
|
|
619
|
-
return `[${signal}] "${updated?.statement ?? predictionText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
|
|
644
|
+
return `[${signal}${storeTag}] "${updated?.statement ?? predictionText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
|
|
620
645
|
});
|
|
621
646
|
},
|
|
622
647
|
};
|
|
@@ -628,13 +653,15 @@ const predictionListDef: ToolDef = {
|
|
|
628
653
|
"confidence, and evidence count. For inspection and debugging.",
|
|
629
654
|
args: {
|
|
630
655
|
store: z.string().optional().describe(
|
|
631
|
-
"Which store to list. Defaults to the project store.",
|
|
656
|
+
"Which store to list. Defaults to the project store and global.",
|
|
632
657
|
),
|
|
633
658
|
},
|
|
634
659
|
async execute(args, ctx) {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
660
|
+
const stores = args.store
|
|
661
|
+
? [args.store as string]
|
|
662
|
+
: [ctx.defaultStore, "global"];
|
|
663
|
+
const predictions = stores.flatMap((s) => ctx.db.listPredictions(s));
|
|
664
|
+
if (predictions.length === 0) return `No predictions in "${stores.join('", "')}".`;
|
|
638
665
|
return predictions.map((p) => {
|
|
639
666
|
const matchers = p.matchers.map((m) => ` - "${m.description}" (w:${m.weight})`).join("\n");
|
|
640
667
|
const provenance = ctx.db.getProvenance(p.id);
|
|
@@ -662,7 +689,7 @@ const predictionDeleteDef: ToolDef = {
|
|
|
662
689
|
),
|
|
663
690
|
},
|
|
664
691
|
async execute(args, ctx) {
|
|
665
|
-
const store = (args
|
|
692
|
+
const store = resolveStore(args, ctx);
|
|
666
693
|
const statementText = args.statement as string;
|
|
667
694
|
const predEmbed = await ctx.model.passageEmbed(statementText);
|
|
668
695
|
const prediction = ctx.db.findNearestPrediction(store, predEmbed, PREDICTION_DEDUP_COSINE);
|
|
@@ -705,7 +732,7 @@ const behaviorCodifyDef: ToolDef = {
|
|
|
705
732
|
),
|
|
706
733
|
},
|
|
707
734
|
async execute(args, ctx) {
|
|
708
|
-
const store = (args
|
|
735
|
+
const store = resolveStore(args, ctx);
|
|
709
736
|
const situationText = args.situation as string;
|
|
710
737
|
const behaviorText = args.behavior as string;
|
|
711
738
|
const rationale = args.rationale as string;
|
|
@@ -714,21 +741,28 @@ const behaviorCodifyDef: ToolDef = {
|
|
|
714
741
|
const behaviorEmbed = await ctx.model.passageEmbed(behaviorText);
|
|
715
742
|
|
|
716
743
|
return ctx.db.transaction(() => {
|
|
717
|
-
|
|
718
|
-
|
|
744
|
+
// Cross-store dedup: same rationale as prediction_update. The
|
|
745
|
+
// auto-fire nudge scans the target store and global, so a duplicate
|
|
746
|
+
// in either store would surface twice every turn.
|
|
747
|
+
const existing = ctx.db.findNearestBehavior(dedupScanStores(store), behaviorEmbed, BEHAVIOR_DEDUP_COSINE);
|
|
748
|
+
const home = existing?.store ?? store;
|
|
749
|
+
const storeTag = home === store ? "" : ` in ${home}`;
|
|
719
750
|
|
|
720
|
-
let
|
|
751
|
+
let matcherId = ctx.db.findNearestBehaviorMatcher(home, matcherEmbed, BEHAVIOR_DEDUP_COSINE)?.id;
|
|
752
|
+
if (!matcherId) matcherId = ctx.db.createBehaviorMatcher(home, situationText, matcherEmbed, ctx.model.name);
|
|
753
|
+
|
|
754
|
+
let behaviorId = existing?.id;
|
|
721
755
|
if (!behaviorId) {
|
|
722
|
-
behaviorId = ctx.db.createBehavior(
|
|
756
|
+
behaviorId = ctx.db.createBehavior(home, behaviorText, rationale, behaviorEmbed, ctx.model.name);
|
|
723
757
|
ctx.db.createBehaviorEdge(matcherId, behaviorId, 1.0);
|
|
724
758
|
ctx.db.addBehaviorProvenance(behaviorId, "codify", rationale);
|
|
725
|
-
return `[codified] ${
|
|
759
|
+
return `[codified] ${home} :: "${behaviorText}" for "${situationText}"`;
|
|
726
760
|
}
|
|
727
761
|
|
|
728
762
|
ctx.db.createBehaviorEdge(matcherId, behaviorId, 1.0);
|
|
729
763
|
ctx.db.addBehaviorProvenance(behaviorId, "codify", rationale);
|
|
730
|
-
const
|
|
731
|
-
return `[linked] "${
|
|
764
|
+
const behavior = ctx.db.getBehavior(behaviorId);
|
|
765
|
+
return `[linked${storeTag}] "${behavior?.statement ?? behaviorText}" for "${situationText}" confidence=${(behavior?.confidence ?? 0).toFixed(2)} (${behavior?.confirm_count ?? 0}/${behavior?.disconfirm_count ?? 0})`;
|
|
732
766
|
});
|
|
733
767
|
},
|
|
734
768
|
};
|
|
@@ -761,7 +795,7 @@ const behaviorFeedbackDef: ToolDef = {
|
|
|
761
795
|
),
|
|
762
796
|
},
|
|
763
797
|
async execute(args, ctx) {
|
|
764
|
-
const store = (args
|
|
798
|
+
const store = resolveStore(args, ctx);
|
|
765
799
|
const behaviorText = args.behavior as string;
|
|
766
800
|
const relevant = args.relevant as boolean;
|
|
767
801
|
const contextText = args.context as string;
|
|
@@ -787,13 +821,15 @@ const behaviorListDef: ToolDef = {
|
|
|
787
821
|
"evidence count. For inspection and debugging.",
|
|
788
822
|
args: {
|
|
789
823
|
store: z.string().optional().describe(
|
|
790
|
-
"Which store to list. Defaults to the project store.",
|
|
824
|
+
"Which store to list. Defaults to the project store and global.",
|
|
791
825
|
),
|
|
792
826
|
},
|
|
793
827
|
async execute(args, ctx) {
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
828
|
+
const stores = args.store
|
|
829
|
+
? [args.store as string]
|
|
830
|
+
: [ctx.defaultStore, "global"];
|
|
831
|
+
const behaviors = stores.flatMap((s) => ctx.db.listBehaviors(s));
|
|
832
|
+
if (behaviors.length === 0) return `No behaviors in "${stores.join('", "')}".`;
|
|
797
833
|
return behaviors.map((b) => {
|
|
798
834
|
const matchers = b.matchers.map((m) => ` - "${m.description}" (w:${m.weight})`).join("\n");
|
|
799
835
|
const provenance = ctx.db.getBehaviorProvenance(b.id);
|
|
@@ -822,7 +858,7 @@ const behaviorDeleteDef: ToolDef = {
|
|
|
822
858
|
),
|
|
823
859
|
},
|
|
824
860
|
async execute(args, ctx) {
|
|
825
|
-
const store = (args
|
|
861
|
+
const store = resolveStore(args, ctx);
|
|
826
862
|
const statementText = args.statement as string;
|
|
827
863
|
const behaviorEmbed = await ctx.model.passageEmbed(statementText);
|
|
828
864
|
const behavior = ctx.db.findNearestBehavior(store, behaviorEmbed, BEHAVIOR_DEDUP_COSINE);
|