@jeffober/thatch 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -9
- package/bin/release +3 -3
- package/bin/thatch +167 -46
- package/docs/dev/README.md +69 -16
- package/docs/dev/gotchas.md +99 -0
- package/docs/dev/mcp-parity.md +113 -57
- package/docs/dev/setup-and-hooks.md +134 -0
- package/docs/dev/skills.md +109 -0
- package/docs/qa/README.md +9 -0
- package/docs/qa/use-cases/README.md +16 -0
- package/docs/qa/use-cases/UC-005-setup-install.md +34 -0
- package/docs/qa/use-cases/UC-006-cursor-hook-lifecycle.md +31 -0
- package/docs/qa/use-cases/UC-007-recall-nudge.md +31 -0
- package/docs/qa/use-cases/UC-008-hygiene-heartbeat.md +26 -0
- package/docs/qa/use-cases/UC-009-flush-tools-tiers.md +31 -0
- package/docs/qa/use-cases/UC-010-prime.md +25 -0
- package/docs/qa/use-cases/UC-011-write-time-similarity-warning.md +24 -0
- package/docs/qa/use-cases/UC-012-model-migration.md +28 -0
- package/docs/qa/use-cases/UC-013-compaction-context.md +25 -0
- package/docs/qa/use-cases/UC-014-skill-install-drift.md +28 -0
- package/docs/qa/use-cases/UC-015-env-override-matrix.md +28 -0
- package/docs/qa/use-cases/UC-016-concurrent-session-isolation.md +36 -0
- package/docs/qa/use-cases/UC-017-buffer-tool-vs-buffer-batch.md +27 -0
- package/docs/user/README.md +42 -7
- package/package.json +3 -2
- package/src/db.ts +19 -5
- package/src/git.ts +1 -1
- package/src/index.ts +89 -21
- package/src/mcp.ts +19 -0
- package/src/prompts.ts +114 -3
- package/src/setup.ts +171 -8
- package/src/sideband.ts +207 -0
- package/src/skills.ts +34 -10
package/src/sideband.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createServer, connect, type Server, type Socket } from "node:net";
|
|
6
|
+
import type { ThatchDB } from "./db";
|
|
7
|
+
import type { EmbeddingModel } from "./embeddings";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic socket path from the DB path. Both the MCP server and hook
|
|
11
|
+
* processes resolve the same DB path (from THATCH_DB_PATH or the default
|
|
12
|
+
* under XDG_CONFIG_HOME), so they independently arrive at the same socket
|
|
13
|
+
* path without any out-of-band coordination.
|
|
14
|
+
*/
|
|
15
|
+
export function sidebandSocketPath(dbPath: string): string {
|
|
16
|
+
const hash = createHash("sha256").update(dbPath).digest("hex").slice(0, 16);
|
|
17
|
+
return join(tmpdir(), `thatch-${hash}.sock`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SidebandMatch {
|
|
21
|
+
label: string;
|
|
22
|
+
score: number;
|
|
23
|
+
store: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MatchRequest {
|
|
27
|
+
method: "match";
|
|
28
|
+
text: string;
|
|
29
|
+
stores: string[];
|
|
30
|
+
threshold: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MatchResponse {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
matches?: SidebandMatch[];
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Serves embedding + semantic search requests over a Unix domain socket.
|
|
42
|
+
* The MCP server (long-lived, warm model) runs this so that one-shot hook
|
|
43
|
+
* processes can ask "does this prompt match any memories?" without loading
|
|
44
|
+
* the ~34 MB model themselves.
|
|
45
|
+
*
|
|
46
|
+
* The protocol is newline-delimited JSON: one request per connection, one
|
|
47
|
+
* response per connection. Requests are `{"method":"match","text":"...",
|
|
48
|
+
* "stores":[...],"threshold":N,"limit":N}`. Responses are `{"ok":true,
|
|
49
|
+
* "matches":[...]}` or `{"ok":false,"error":"..."}`.
|
|
50
|
+
*/
|
|
51
|
+
export class SidebandServer {
|
|
52
|
+
#server: Server | null = null;
|
|
53
|
+
#path: string;
|
|
54
|
+
#model: EmbeddingModel;
|
|
55
|
+
#db: ThatchDB;
|
|
56
|
+
|
|
57
|
+
constructor(path: string, model: EmbeddingModel, db: ThatchDB) {
|
|
58
|
+
this.#path = path;
|
|
59
|
+
this.#model = model;
|
|
60
|
+
this.#db = db;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Opens the socket. Cleans up any stale socket file from a crashed server. */
|
|
64
|
+
start(): void {
|
|
65
|
+
try {
|
|
66
|
+
unlinkSync(this.#path);
|
|
67
|
+
} catch {
|
|
68
|
+
// No stale socket — expected on first run.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.#server = createServer((socket) => {
|
|
72
|
+
this.#handleConnection(socket);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.#server.listen(this.#path);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Closes the socket and removes the socket file. */
|
|
79
|
+
stop(): void {
|
|
80
|
+
this.#server?.close();
|
|
81
|
+
this.#server = null;
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(this.#path);
|
|
84
|
+
} catch {
|
|
85
|
+
// Already gone.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get path(): string {
|
|
90
|
+
return this.#path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#handleConnection(socket: Socket): void {
|
|
94
|
+
let buf = "";
|
|
95
|
+
socket.on("data", (chunk: Buffer) => {
|
|
96
|
+
buf += chunk.toString();
|
|
97
|
+
const nl = buf.indexOf("\n");
|
|
98
|
+
if (nl < 0) return;
|
|
99
|
+
|
|
100
|
+
const line = buf.slice(0, nl).trim();
|
|
101
|
+
buf = buf.slice(nl + 1);
|
|
102
|
+
this.#handleRequest(line, socket);
|
|
103
|
+
});
|
|
104
|
+
socket.on("error", () => {
|
|
105
|
+
// Client disconnected abruptly — nothing to do.
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async #handleRequest(line: string, socket: Socket): Promise<void> {
|
|
110
|
+
let req: MatchRequest;
|
|
111
|
+
try {
|
|
112
|
+
req = JSON.parse(line);
|
|
113
|
+
} catch {
|
|
114
|
+
this.#respond(socket, { ok: false, error: "Invalid JSON" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (req.method !== "match") {
|
|
119
|
+
this.#respond(socket, { ok: false, error: `Unknown method: ${req.method}` });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const embedding = await this.#model.queryEmbed(req.text);
|
|
125
|
+
const results = this.#db.search(req.stores, embedding, { limit: req.limit });
|
|
126
|
+
const matches: SidebandMatch[] = results
|
|
127
|
+
.filter((r) => r._score >= req.threshold)
|
|
128
|
+
.map((r) => ({
|
|
129
|
+
label: r.label,
|
|
130
|
+
score: Math.round(r._score * 1000) / 1000,
|
|
131
|
+
store: r.store,
|
|
132
|
+
}));
|
|
133
|
+
this.#respond(socket, { ok: true, matches });
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
this.#respond(socket, { ok: false, error: String(err?.message ?? err) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#respond(socket: Socket, res: MatchResponse): void {
|
|
140
|
+
if (!socket.destroyed) {
|
|
141
|
+
socket.write(JSON.stringify(res) + "\n");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Connect to the sideband server and request semantic matches for a prompt.
|
|
148
|
+
* Returns null on any failure (server not running, stale socket, timeout) —
|
|
149
|
+
* callers must treat null as "skip the recall nudge" and fall back gracefully.
|
|
150
|
+
*/
|
|
151
|
+
export function sidebandMatch(
|
|
152
|
+
socketPath: string,
|
|
153
|
+
text: string,
|
|
154
|
+
stores: string[],
|
|
155
|
+
threshold: number,
|
|
156
|
+
limit: number,
|
|
157
|
+
timeoutMs = 2000,
|
|
158
|
+
): Promise<SidebandMatch[] | null> {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let settled = false;
|
|
161
|
+
const done = (val: SidebandMatch[] | null) => {
|
|
162
|
+
if (settled) return;
|
|
163
|
+
settled = true;
|
|
164
|
+
resolve(val);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const socket = connect(socketPath);
|
|
168
|
+
const req = JSON.stringify({ method: "match", text, stores, threshold, limit }) + "\n";
|
|
169
|
+
let buf = "";
|
|
170
|
+
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
socket.destroy();
|
|
173
|
+
done(null);
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
|
|
176
|
+
socket.on("error", () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
// Stale socket file left by a crashed server — clean it up so the next
|
|
179
|
+
// connection attempt doesn't hit ECONNREFUSED again.
|
|
180
|
+
try {
|
|
181
|
+
unlinkSync(socketPath);
|
|
182
|
+
} catch {
|
|
183
|
+
// Already gone or never existed.
|
|
184
|
+
}
|
|
185
|
+
done(null);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
socket.on("connect", () => {
|
|
189
|
+
socket.write(req);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
socket.on("data", (chunk: Buffer) => {
|
|
193
|
+
buf += chunk.toString();
|
|
194
|
+
const nl = buf.indexOf("\n");
|
|
195
|
+
if (nl < 0) return;
|
|
196
|
+
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
try {
|
|
199
|
+
const res = JSON.parse(buf.slice(0, nl)) as MatchResponse;
|
|
200
|
+
done(res.ok ? (res.matches ?? []) : null);
|
|
201
|
+
} catch {
|
|
202
|
+
done(null);
|
|
203
|
+
}
|
|
204
|
+
socket.destroy();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
package/src/skills.ts
CHANGED
|
@@ -72,6 +72,8 @@ Identify the application's runtime model early: Is it a CLI tool (process exits
|
|
|
72
72
|
## Reachability gate
|
|
73
73
|
For every potential finding, you MUST describe a concrete scenario where a real user triggers the problem through normal usage. "The code allows this" is not sufficient — show how a user actually encounters it given the application's runtime model. If the only trigger requires conditions that cannot occur in actual usage, it is not a finding.
|
|
74
74
|
|
|
75
|
+
Before claiming a value can hold a problematic state (NULL, orphaned, out-of-range, wrong-type), read the artifact that governs that state: the column definition, type declaration, FK/NOT NULL constraint, validation, or guard. Quote it in your finding. If a schema constraint, type, or guard makes the state unreachable, it is not a finding. "The join could drop rows" requires proving a droppable row can exist — cite the constraint that permits it.
|
|
76
|
+
|
|
75
77
|
## Intent verification
|
|
76
78
|
Before flagging behavior as a bug, verify intent:
|
|
77
79
|
1. **Trace callers.** Read every caller of the cited code. The behavior may be intentional given how the feature is actually used.
|
|
@@ -453,11 +455,13 @@ For each, walk through actual function calls, tracking data shape at each step.
|
|
|
453
455
|
|
|
454
456
|
### 4. Prove the producer chain
|
|
455
457
|
For every finding about invalid state, missing data, shape mismatches, or cross-module behavior, trace the full causal chain:
|
|
456
|
-
- Who produces the state or value?
|
|
458
|
+
- Who produces the state or value? Cite the file:line that writes the problematic value.
|
|
457
459
|
- Which functions transform it?
|
|
458
460
|
- Which consumer or branch fails?
|
|
459
461
|
- Which real entrypoint/workflow exercises that chain?
|
|
460
462
|
|
|
463
|
+
The producer must be a specific file:line in current code that writes the problematic value. "Source deletion could orphan this" is not a producer; the file:line where a row is written with a dangling reference is. If no such write site exists in the codebase, the state is unreachable — do not report it.
|
|
464
|
+
|
|
461
465
|
If you cannot identify a real producer in current code, or the only way to trigger the issue is by manually fabricating invalid state/data, do not report it as a real finding.
|
|
462
466
|
|
|
463
467
|
### 5. Identify the implicit FSM
|
|
@@ -490,6 +494,10 @@ For each new module or significant change:
|
|
|
490
494
|
- **DEAD_PATH**: Code path exists but cannot be reached given current callers/preconditions
|
|
491
495
|
|
|
492
496
|
For each finding, cite both sides of any contract (file:line for caller and callee).
|
|
497
|
+
|
|
498
|
+
## Worked non-finding (negative example)
|
|
499
|
+
|
|
500
|
+
Example non-finding: "INNER JOIN on source_id silently drops rows whose source was deleted." Before reporting, read the FK: if source_id is NOT NULL with a foreign key and deletion cascades to children, an orphaned row cannot exist and the drop is unreachable. Reporting it anyway is the canonical reachability failure. The fix is not to switch to LEFT JOIN (which would add dead code for an impossible case) but to verify the constraint that governs the state.
|
|
493
501
|
`;
|
|
494
502
|
|
|
495
503
|
const REVIEW_NO_SLOP = `---
|
|
@@ -649,25 +657,35 @@ For each finding from the specialists:
|
|
|
649
657
|
|
|
650
658
|
3. **Determine provenance.** Is the finding a new issue introduced by this change, or a pre-existing problem? Note pre-existing bugs separately.
|
|
651
659
|
|
|
652
|
-
|
|
660
|
+
**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.
|
|
661
|
+
|
|
662
|
+
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).
|
|
653
663
|
|
|
654
|
-
5. **Prove the causal chain** for any finding about state, data shape, cross-module contracts, or behavior:
|
|
664
|
+
5. **Prove the causal chain (behavioral findings only)** for any finding about state, data shape, cross-module contracts, or behavior:
|
|
655
665
|
- Identify the authoritative producer or source of the state/data.
|
|
656
666
|
- Identify the transforms between producer and consumer.
|
|
657
667
|
- Identify the consumer or branch where failure occurs.
|
|
658
668
|
- Identify the real entrypoint/workflow that exercises this chain.
|
|
659
669
|
If the only way to trigger the issue is by manually fabricating invalid data/state or bypassing the real producers/guards, reject the finding. If the citation is real but you cannot prove the producer chain, classify as UNVERIFIABLE rather than CONFIRMED.
|
|
660
670
|
|
|
661
|
-
|
|
671
|
+
5a. **Verify governing constraints for data-shape findings.** For findings claiming a field can be NULL/missing/orphaned/malformed, locate and read the field's definition (schema migration, model, type declaration). If a NOT NULL, FK, enum, or type constraint forecloses the claimed state, REJECT and cite the constraint. Do not classify such a finding CONFIRMED without having read the governing definition.
|
|
672
|
+
|
|
673
|
+
6. **Verify intent (behavioral findings only).** If the specialist flagged behavior as a bug but the code appears to work as designed, check whether the behavior is intentional:
|
|
662
674
|
- Read the callers of the cited code to see if the pattern makes sense in context.
|
|
663
675
|
- Check git log or git blame on the cited file for commit messages explaining the decision.
|
|
664
676
|
- Use thatch_memory_recall to search for documented rationale.
|
|
665
677
|
If the behavior is intentional or an accepted limitation, reject the finding.
|
|
666
678
|
|
|
679
|
+
4m. **Mechanical verification (pedantic, no-slop, breadcrumbs, docs, naming, style, comments).** For mechanical findings, verification means:
|
|
680
|
+
- The cited text exists at the cited location (already confirmed in step 2).
|
|
681
|
+
- The finding is branch-introduced or newly made relevant by the change. Pre-existing issues go in the pre-existing appendix, not rejected.
|
|
682
|
+
- The cited text violates the stated guideline, specialist taxonomy, or project norm. Identify the source of truth: the specific guideline, convention, or writing norm being violated.
|
|
683
|
+
Runtime reachability, producer chains, and intent verification do not apply. A mechanical finding is not a "bug" and does not need a "trigger scenario."
|
|
684
|
+
|
|
667
685
|
7. **Classify:**
|
|
668
|
-
- **CONFIRMED**:
|
|
669
|
-
- **REJECTED**:
|
|
670
|
-
- **UNVERIFIABLE**: The citation is correct but you cannot confirm the
|
|
686
|
+
- **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.
|
|
687
|
+
- **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.
|
|
688
|
+
- **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.
|
|
671
689
|
|
|
672
690
|
## Deduplication
|
|
673
691
|
|
|
@@ -683,10 +701,14 @@ Assign final severity based on YOUR verification:
|
|
|
683
701
|
- **MEDIUM**: Edge cases, UX friction, or issues where the citation is correct but the impact is limited or requires unusual conditions.
|
|
684
702
|
- **LOW**: Mechanical issues (stale docs, guideline violations, naming) that don't affect correctness.
|
|
685
703
|
|
|
704
|
+
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.
|
|
705
|
+
|
|
686
706
|
## Report format
|
|
687
707
|
|
|
688
708
|
You MUST produce the full report structure below. Do not simplify or omit sections. Every confirmed finding must include all numbered fields. If a section has no entries, write "None" — do not skip the section.
|
|
689
709
|
|
|
710
|
+
Confirmed LOW findings are mandatory. Do not summarize them away or omit them for being non-functional. Pedantic, no-slop, breadcrumbs, docs, naming, style, and comment findings are first-class review findings when confirmed. Group them under LOW, but include every one.
|
|
711
|
+
|
|
690
712
|
### Scope
|
|
691
713
|
- Branch/range reviewed
|
|
692
714
|
- Design context (if provided)
|
|
@@ -858,13 +880,15 @@ For the integration review unit (5+ points), dispatch a sub-agent focused on:
|
|
|
858
880
|
|
|
859
881
|
After all sub-agents complete, load the thatch-review-synthesizer skill to verify findings, deduplicate across specialists, classify (CONFIRMED/REJECTED/UNVERIFIABLE), and produce the final severity-grouped report.
|
|
860
882
|
|
|
883
|
+
Confirmed LOW findings are mandatory in the final report, including mechanical findings (pedantic, no-slop, breadcrumbs, docs, naming, style, comments). Do not omit them for being non-functional.
|
|
884
|
+
|
|
861
885
|
Alternatively, perform the synthesis yourself:
|
|
862
886
|
1. Read each finding's cited location to verify evidence accuracy.
|
|
863
887
|
2. Deduplicate findings flagged by multiple specialists.
|
|
864
888
|
3. Group findings by root cause where multiple findings stem from the same issue.
|
|
865
|
-
4. Classify each as CONFIRMED, REJECTED, or UNVERIFIABLE
|
|
889
|
+
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.
|
|
866
890
|
5. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
|
|
867
|
-
6. Produce a final report grouped by severity, with coverage gaps noted.
|
|
891
|
+
6. Produce a final report grouped by severity, with coverage gaps noted. Include every confirmed LOW finding.
|
|
868
892
|
|
|
869
893
|
## Specialist briefing template
|
|
870
894
|
|
|
@@ -876,7 +900,7 @@ When dispatching each sub-agent, include in the prompt:
|
|
|
876
900
|
- Any design context or specific concerns
|
|
877
901
|
- Explicit scope boundaries ("your scope is X; do NOT review Y")
|
|
878
902
|
- Instruction to produce markdown findings with: severity, category, file:line, finding, evidence, trigger scenario, reachability, source of truth, producer chain, provenance
|
|
879
|
-
- Instruction to apply the reachability gate and intent verification before reporting
|
|
903
|
+
- Instruction to apply the reachability gate (including reading and citing governing constraints for data-state claims) and intent verification before reporting
|
|
880
904
|
`;
|
|
881
905
|
|
|
882
906
|
// ---------------------------------------------------------------------------
|