@jeffober/thatch 0.1.6 → 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/bin/release +3 -3
- package/docs/dev/README.md +3 -2
- package/docs/dev/setup-and-hooks.md +3 -2
- package/package.json +1 -1
- package/src/index.ts +24 -2
- package/src/prompts.ts +3 -3
- package/src/skills.ts +17 -5
package/bin/release
CHANGED
|
@@ -22,9 +22,9 @@ echo " bump: $v"
|
|
|
22
22
|
echo " next: $next"
|
|
23
23
|
echo
|
|
24
24
|
|
|
25
|
-
echo "▸ running tests..."
|
|
26
|
-
|
|
27
|
-
echo " ✓
|
|
25
|
+
echo "▸ running checks (typecheck + tests + markdown lint)..."
|
|
26
|
+
mise run check
|
|
27
|
+
echo " ✓ checks passed"
|
|
28
28
|
echo
|
|
29
29
|
|
|
30
30
|
echo "▸ bumping version..."
|
package/docs/dev/README.md
CHANGED
|
@@ -70,9 +70,10 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
70
70
|
| Hook | What it does |
|
|
71
71
|
|------|-------------|
|
|
72
72
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
73
|
-
| `experimental.session.compacting` |
|
|
73
|
+
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
74
|
+
| `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. |
|
|
74
75
|
| `tool.execute.after` | Buffers every non-`thatch_*` tool call into the session's extraction buffer. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
75
|
-
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, flushes them as a synthetic text part carrying the extraction nudge + JSON payload; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). |
|
|
76
|
+
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions, flushes them as a synthetic text part carrying the extraction nudge + JSON payload; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
|
|
76
77
|
| `event` (`session.created`) | Sends the session-start reminder via `client.session.prompt`, carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. The session id is at `event.properties.info.id`. |
|
|
77
78
|
| `dispose` | Closes the DB. |
|
|
78
79
|
|
|
@@ -17,9 +17,10 @@ The plugin entry (`src/index.ts`) registers these hooks in code:
|
|
|
17
17
|
| Hook | Input | Output | Behavior |
|
|
18
18
|
|------|-------|--------|----------|
|
|
19
19
|
| `experimental.chat.system.transform` | `{}` | `{ system: string[] }` | Pushes the system prompt (store names, usage rules). |
|
|
20
|
-
| `experimental.session.compacting` | `{}` | `{ context: string[] }` |
|
|
20
|
+
| `experimental.session.compacting` | `{ sessionID }` | `{ context: string[] }` | Marks the session as compacting, pushes re-familiarization context. |
|
|
21
|
+
| `experimental.compaction.autocontinue` | `{ sessionID }` | `{ enabled: boolean }` | Clears the compacting flag so `chat.message` nudges resume post-compaction. |
|
|
21
22
|
| `tool.execute.after` | `{ tool, sessionID, callID, args }` | `{ title, output, metadata }` | Buffers non-`thatch_` tool calls into the in-memory extraction ring buffer (max 20). |
|
|
22
|
-
| `chat.message` | `{ sessionID, messageID }` | `{ message, parts }` | Two tiers: extraction nudge if pending, else prompt-aware recall nudge. |
|
|
23
|
+
| `chat.message` | `{ sessionID, messageID }` | `{ message, parts }` | Two tiers: extraction nudge if pending, else prompt-aware recall nudge. Skipped while the session is compacting. |
|
|
23
24
|
| `event` (`session.created`) | `{ event: { type, properties: { info: { id } } } }` | — | Calls `client.session.prompt` with the reminder + hygiene heartbeat. |
|
|
24
25
|
| `dispose` | — | — | Closes the DB. |
|
|
25
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
41
41
|
const model = new BgeEmbeddingModel(modelName);
|
|
42
42
|
const extraction = new ExtractionPipeline();
|
|
43
43
|
|
|
44
|
+
// Sessions currently being compacted. chat.message nudges are skipped while
|
|
45
|
+
// a session is in this set — the agent can't call tools during summary
|
|
46
|
+
// generation, so a recall or extraction nudge would cause a blocked-tool
|
|
47
|
+
// error. Cleared by experimental.compaction.autocontinue. If compaction
|
|
48
|
+
// fails, the entry leaks (graceful degradation: nudges stay off for that
|
|
49
|
+
// session, but no crash).
|
|
50
|
+
const compacting = new Set<string>();
|
|
51
|
+
|
|
44
52
|
// Skills always install to the global opencode config — installing into the
|
|
45
53
|
// worktree would mutate the user's repo (untracked files in git status).
|
|
46
54
|
// A failed install degrades the nudge workflow but must not kill the plugin.
|
|
@@ -64,11 +72,20 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
64
72
|
output.system.push(sys);
|
|
65
73
|
},
|
|
66
74
|
|
|
67
|
-
// 2. Compaction context — re-familiarizes after compaction.
|
|
68
|
-
|
|
75
|
+
// 2. Compaction context — re-familiarizes after compaction. The flag
|
|
76
|
+
// suppresses chat.message nudges during summary generation (tool
|
|
77
|
+
// calls are blocked there).
|
|
78
|
+
"experimental.session.compacting": async (input, output) => {
|
|
79
|
+
compacting.add(input.sessionID);
|
|
69
80
|
output.context.push(compact);
|
|
70
81
|
},
|
|
71
82
|
|
|
83
|
+
// 2b. Clear the compacting flag after compaction succeeds so chat.message
|
|
84
|
+
// nudges resume for the synthetic auto-continue turn and beyond.
|
|
85
|
+
"experimental.compaction.autocontinue": async (input) => {
|
|
86
|
+
compacting.delete(input.sessionID);
|
|
87
|
+
},
|
|
88
|
+
|
|
72
89
|
// 3. Tool buffering — feeds the extraction nudge. Thatch's own tools are
|
|
73
90
|
// excluded: extracting facts from memory operations would just echo the
|
|
74
91
|
// store back into itself.
|
|
@@ -89,7 +106,12 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
89
106
|
// b. Recall nudge: the user's prompt semantically matches existing
|
|
90
107
|
// memories — surface that prior knowledge exists before the agent
|
|
91
108
|
// responds. Uses the in-process warm model (no sideband needed).
|
|
109
|
+
//
|
|
110
|
+
// Skipped during compaction: the agent can't call tools while generating
|
|
111
|
+
// a summary, so a nudge that says "use thatch_memory_recall" triggers a
|
|
112
|
+
// blocked-tool error.
|
|
92
113
|
"chat.message": async (input, output) => {
|
|
114
|
+
if (compacting.has(input.sessionID)) return;
|
|
93
115
|
if (extraction.pending(input.sessionID)) {
|
|
94
116
|
const batch = extraction.flush(input.sessionID);
|
|
95
117
|
const payload = extraction.buildPayload(batch, repo);
|
package/src/prompts.ts
CHANGED
|
@@ -64,9 +64,9 @@ CLAUDE.md or OPENCODE.md, speculative conclusions.
|
|
|
64
64
|
|
|
65
65
|
export function compactionContext(repo: string): string {
|
|
66
66
|
return `Thatch persistent memory is active with stores "${repo}" and "global".
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
When summarizing, preserve context about what's been learned and decided
|
|
68
|
+
this session — conventions, architectural decisions, and prior knowledge
|
|
69
|
+
retrieved from memory. This ensures continuity after compaction.`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export function sessionStartReminder(repo: string, hygiene?: string | null): string {
|
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 = `---
|
|
@@ -660,6 +668,8 @@ For each finding from the specialists:
|
|
|
660
668
|
- Identify the real entrypoint/workflow that exercises this chain.
|
|
661
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.
|
|
662
670
|
|
|
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
|
+
|
|
663
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:
|
|
664
674
|
- Read the callers of the cited code to see if the pattern makes sense in context.
|
|
665
675
|
- Check git log or git blame on the cited file for commit messages explaining the decision.
|
|
@@ -673,9 +683,9 @@ For each finding from the specialists:
|
|
|
673
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."
|
|
674
684
|
|
|
675
685
|
7. **Classify:**
|
|
676
|
-
- **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,
|
|
677
|
-
- **REJECTED**: For behavioral findings: the citation is wrong, the claim is inaccurate, the bug cannot manifest in the actual runtime context,
|
|
678
|
-
- **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. Mechanical findings should rarely be UNVERIFIABLE — if the text exists and violates the guideline, it is CONFIRMED.
|
|
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.
|
|
679
689
|
|
|
680
690
|
## Deduplication
|
|
681
691
|
|
|
@@ -691,6 +701,8 @@ Assign final severity based on YOUR verification:
|
|
|
691
701
|
- **MEDIUM**: Edge cases, UX friction, or issues where the citation is correct but the impact is limited or requires unusual conditions.
|
|
692
702
|
- **LOW**: Mechanical issues (stale docs, guideline violations, naming) that don't affect correctness.
|
|
693
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
|
+
|
|
694
706
|
## Report format
|
|
695
707
|
|
|
696
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.
|
|
@@ -888,7 +900,7 @@ When dispatching each sub-agent, include in the prompt:
|
|
|
888
900
|
- Any design context or specific concerns
|
|
889
901
|
- Explicit scope boundaries ("your scope is X; do NOT review Y")
|
|
890
902
|
- Instruction to produce markdown findings with: severity, category, file:line, finding, evidence, trigger scenario, reachability, source of truth, producer chain, provenance
|
|
891
|
-
- 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
|
|
892
904
|
`;
|
|
893
905
|
|
|
894
906
|
// ---------------------------------------------------------------------------
|