@jeffober/thatch 0.1.12 → 0.1.13
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 +61 -0
- package/artifacts/skills/thatch-code-review.md +123 -0
- package/artifacts/skills/thatch-dedup-classifier.md +40 -0
- package/artifacts/skills/thatch-fact-extractor.md +67 -0
- package/artifacts/skills/thatch-project-primer.md +62 -0
- package/artifacts/skills/thatch-review-acceptance.md +70 -0
- package/artifacts/skills/thatch-review-breadcrumbs.md +58 -0
- package/artifacts/skills/thatch-review-context.md +110 -0
- package/artifacts/skills/thatch-review-mark-and-sweep.md +125 -0
- package/artifacts/skills/thatch-review-no-slop.md +71 -0
- package/artifacts/skills/thatch-review-pedantic.md +57 -0
- package/artifacts/skills/thatch-review-state-flow.md +89 -0
- package/artifacts/skills/thatch-review-synthesizer.md +94 -0
- package/artifacts/skills/thatch-session-reflection.md +68 -0
- package/artifacts/skills/thatch-workflow-research.md +91 -0
- package/bin/thatch +9 -5
- package/docs/dev/skills.md +5 -8
- package/package.json +3 -2
- package/src/extract-queue.ts +61 -10
- package/src/extraction.ts +12 -2
- package/src/index.ts +27 -9
- package/src/prompts.ts +37 -8
- package/src/skills.ts +41 -1282
package/docs/dev/skills.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Skills
|
|
2
2
|
|
|
3
3
|
Thatch installs SKILL.md files that give the agent task-specific workflows. All
|
|
4
|
-
skill content is
|
|
4
|
+
skill content lives in `artifacts/skills/*.md` and is loaded by `src/skills.ts` and installed to a host-specific
|
|
5
5
|
directory. See `mcp-parity.md` for which skills each host gets.
|
|
6
6
|
|
|
7
7
|
## Skill file format
|
|
@@ -102,13 +102,10 @@ and `--cursor` pass only the shared set.
|
|
|
102
102
|
|
|
103
103
|
## Adding a skill
|
|
104
104
|
|
|
105
|
-
1. Write
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
3. If the skill name appears in a tool's expected workflow (e.g. the extraction
|
|
110
|
-
nudge references `thatch-fact-extractor`), update that prompt in `src/prompts.ts`.
|
|
111
|
-
4. The opencode plugin and `thatch setup` pick it up automatically on next init.
|
|
105
|
+
1. Write `artifacts/skills/thatch-<name>.md` with YAML frontmatter. If it's a review specialist, include `${REVIEW_COMMON}` (interpolated from `artifacts/skills/common.md`).
|
|
106
|
+
2. Add the skill name to the `names` array in `loadSharedSkills()` (or `loadOpencodeOnlySkills()` if sub-agents are needed). The loader in `src/skills.ts` reads `.md` files at init.
|
|
107
|
+
3. If the skill name appears in a tool's workflow (e.g. `thatch-fact-extractor`), update `src/prompts.ts`.
|
|
108
|
+
4. Run `mise run check` — tests verify counts; `installSkills` picks up new files on next init.
|
|
112
109
|
|
|
113
110
|
## Memory review skills in practice
|
|
114
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
17
|
"docs",
|
|
18
|
-
"bin"
|
|
18
|
+
"bin",
|
|
19
|
+
"artifacts"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"test": "bun test",
|
package/src/extract-queue.ts
CHANGED
|
@@ -37,11 +37,16 @@ function queuePath(sessionID: string): string {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Buffer a batch of tool calls for later extraction. Filters out
|
|
41
|
-
* MCP tools (mcp__thatch__*) to avoid self-echo — extracting
|
|
42
|
-
* operations would just write the store back into
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* Buffer a batch of tool calls for later extraction. Filters out:
|
|
41
|
+
* - thatch's own MCP tools (mcp__thatch__*) to avoid self-echo — extracting
|
|
42
|
+
* facts from memory operations would just write the store back into itself.
|
|
43
|
+
* - skill/task/agent meta-tools — these orchestrate agent behavior (loading
|
|
44
|
+
* skills, dispatching sub-agents) and buffering them creates a feedback
|
|
45
|
+
* loop where the extraction nudge triggers a skill load, which gets
|
|
46
|
+
* buffered, which triggers another nudge on the next turn.
|
|
47
|
+
* Memory writes reset the missed-nudge escalation counter.
|
|
48
|
+
* Each surviving call is mapped to a ToolInteraction (title synthesized since
|
|
49
|
+
* Claude Code doesn't supply one) and appended as one JSON line.
|
|
45
50
|
*/
|
|
46
51
|
export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void {
|
|
47
52
|
const dir = queueDir();
|
|
@@ -50,7 +55,14 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
|
|
|
50
55
|
const existing = readQueueFile(path);
|
|
51
56
|
const additions: ToolInteraction[] = [];
|
|
52
57
|
for (const tc of toolCalls) {
|
|
53
|
-
|
|
58
|
+
const lower = tc.tool_name.toLowerCase();
|
|
59
|
+
if (lower === "mcp__thatch__memory_remember") {
|
|
60
|
+
resetMissedCount(sessionID);
|
|
61
|
+
consumeQueue(sessionID);
|
|
62
|
+
existing.length = 0;
|
|
63
|
+
}
|
|
64
|
+
if (lower.startsWith("mcp__thatch__")) continue;
|
|
65
|
+
if (lower === "skill" || lower === "task" || lower === "agent") continue;
|
|
54
66
|
const response = typeof tc.tool_response === "string"
|
|
55
67
|
? tc.tool_response
|
|
56
68
|
: JSON.stringify(tc.tool_response).slice(0, 500);
|
|
@@ -70,8 +82,8 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
|
|
|
70
82
|
writeFileSync(path, all.map((ix) => JSON.stringify(ix)).join("\n") + "\n");
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
/** Read
|
|
74
|
-
export function
|
|
85
|
+
/** Read a session's queued interactions without removing them. */
|
|
86
|
+
export function peekQueue(sessionID: string): ToolInteraction[] {
|
|
75
87
|
let path: string;
|
|
76
88
|
try {
|
|
77
89
|
path = queuePath(sessionID);
|
|
@@ -79,8 +91,18 @@ export function flushQueue(sessionID: string): ToolInteraction[] {
|
|
|
79
91
|
return [];
|
|
80
92
|
}
|
|
81
93
|
if (!existsSync(path)) return [];
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
return readQueueFile(path);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Delete a session's queue file. Called when the agent writes a memory. */
|
|
98
|
+
export function consumeQueue(sessionID: string): void {
|
|
99
|
+
try { unlinkSync(queuePath(sessionID)); } catch { /* not present is fine */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read and delete a session's queue. Returns the buffered interactions in order. */
|
|
103
|
+
export function flushQueue(sessionID: string): ToolInteraction[] {
|
|
104
|
+
const interactions = peekQueue(sessionID);
|
|
105
|
+
consumeQueue(sessionID);
|
|
84
106
|
return interactions;
|
|
85
107
|
}
|
|
86
108
|
|
|
@@ -98,4 +120,33 @@ function readQueueFile(path: string): ToolInteraction[] {
|
|
|
98
120
|
}
|
|
99
121
|
})
|
|
100
122
|
.filter((ix): ix is ToolInteraction => ix !== null);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Missed-nudge counter — file-backed per-session escalation state. Tracks
|
|
127
|
+
// consecutive extraction nudges delivered without any memory_remember call.
|
|
128
|
+
// Reset by appendBatch when the agent writes a memory. Read and incremented
|
|
129
|
+
// by flush-tools when the nudge is delivered.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
function countPath(sessionID: string): string {
|
|
133
|
+
return join(queueDir(), `${safeName(sessionID)}.count`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resetMissedCount(sessionID: string): void {
|
|
137
|
+
try { unlinkSync(countPath(sessionID)); } catch { /* not present is fine */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getMissedCount(sessionID: string): number {
|
|
141
|
+
try {
|
|
142
|
+
return parseInt(readFileSync(countPath(sessionID), "utf8"), 10) || 0;
|
|
143
|
+
} catch {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function incrementMissedCount(sessionID: string): void {
|
|
149
|
+
const next = getMissedCount(sessionID) + 1;
|
|
150
|
+
mkdirSync(queueDir(), { recursive: true });
|
|
151
|
+
writeFileSync(countPath(sessionID), String(next));
|
|
101
152
|
}
|
package/src/extraction.ts
CHANGED
|
@@ -100,10 +100,20 @@ export class ExtractionPipeline {
|
|
|
100
100
|
this.#buffers.set(interaction.sessionID, buf.slice(-this.#maxBuffer));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** Returns the session's buffered interactions without clearing them. */
|
|
104
|
+
peek(sessionID: string): ToolInteraction[] {
|
|
105
|
+
return this.#buffers.get(sessionID) ?? [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Clears the session's buffer. Called when the agent writes a memory. */
|
|
109
|
+
consume(sessionID: string): void {
|
|
110
|
+
this.#buffers.delete(sessionID);
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
/** Returns the session's buffered interactions and clears them. */
|
|
104
114
|
flush(sessionID: string): ToolInteraction[] {
|
|
105
|
-
const batch = this
|
|
106
|
-
this
|
|
115
|
+
const batch = this.peek(sessionID);
|
|
116
|
+
this.consume(sessionID);
|
|
107
117
|
return batch;
|
|
108
118
|
}
|
|
109
119
|
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
compactionContext,
|
|
10
10
|
sessionStartReminder,
|
|
11
11
|
recallNudge,
|
|
12
|
+
extractionNudge,
|
|
12
13
|
type NudgeMatch,
|
|
13
14
|
} from "./prompts";
|
|
14
15
|
import { ExtractionPipeline } from "./extraction";
|
|
@@ -49,6 +50,12 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
49
50
|
// session, but no crash).
|
|
50
51
|
const compacting = new Set<string>();
|
|
51
52
|
|
|
53
|
+
// Per-session count of consecutive extraction nudges delivered without any
|
|
54
|
+
// memory_remember call in between. Drives nudge escalation: the agent gets
|
|
55
|
+
// a couple of polite chances, then the tone shifts to directive, then to
|
|
56
|
+
// all-caps shouting. Reset to 0 whenever the agent writes a memory.
|
|
57
|
+
const missedNudges = new Map<string, number>();
|
|
58
|
+
|
|
52
59
|
// Skills always install to the global opencode config — installing into the
|
|
53
60
|
// worktree would mutate the user's repo (untracked files in git status).
|
|
54
61
|
// A failed install degrades the nudge workflow but must not kill the plugin.
|
|
@@ -86,11 +93,23 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
86
93
|
compacting.delete(input.sessionID);
|
|
87
94
|
},
|
|
88
95
|
|
|
89
|
-
// 3. Tool buffering — feeds the extraction nudge.
|
|
90
|
-
//
|
|
91
|
-
//
|
|
96
|
+
// 3. Tool buffering — feeds the extraction nudge. Excluded tools:
|
|
97
|
+
// - thatch_*: extracting facts from memory ops would echo the store
|
|
98
|
+
// - skill/task: meta-tools that orchestrate agent behavior (loading
|
|
99
|
+
// skills, dispatching sub-agents). Buffering them creates a feedback
|
|
100
|
+
// loop — the nudge triggers a skill load, which gets buffered, which
|
|
101
|
+
// triggers another nudge on the next turn.
|
|
102
|
+
// Memory writes consume the buffer and reset the missed-nudge counter.
|
|
103
|
+
// The buffer is NOT drained on nudge delivery — it persists until the
|
|
104
|
+
// agent writes a memory, so ignored nudges accumulate and the payload
|
|
105
|
+
// grows with each missed cycle.
|
|
92
106
|
"tool.execute.after": async (input, output) => {
|
|
93
|
-
if (input.tool
|
|
107
|
+
if (input.tool === "thatch_memory_remember") {
|
|
108
|
+
extraction.consume(input.sessionID);
|
|
109
|
+
missedNudges.delete(input.sessionID);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (input.tool.startsWith("thatch_") || input.tool === "skill" || input.tool === "task") return;
|
|
94
113
|
extraction.push({
|
|
95
114
|
tool: input.tool,
|
|
96
115
|
sessionID: input.sessionID,
|
|
@@ -113,12 +132,11 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
113
132
|
"chat.message": async (input, output) => {
|
|
114
133
|
if (compacting.has(input.sessionID)) return;
|
|
115
134
|
if (extraction.pending(input.sessionID)) {
|
|
116
|
-
const batch = extraction.
|
|
135
|
+
const batch = extraction.peek(input.sessionID);
|
|
117
136
|
const payload = extraction.buildPayload(batch, repo);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`to save any new durable facts from this payload:\n${payload}`;
|
|
137
|
+
const missed = missedNudges.get(input.sessionID) ?? 0;
|
|
138
|
+
const text = extractionNudge(batch.length, missed, "thatch_memory_remember", payload);
|
|
139
|
+
missedNudges.set(input.sessionID, missed + 1);
|
|
122
140
|
|
|
123
141
|
output.parts.push({
|
|
124
142
|
id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
|
package/src/prompts.ts
CHANGED
|
@@ -255,16 +255,45 @@ export function claudeWriteNudge(): string {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
258
|
+
* Extraction nudge with escalation. Both the opencode plugin (src/index.ts)
|
|
259
|
+
* and the Claude Code/Cursor CLI (bin/thatch flush-tools) call this with
|
|
260
|
+
* their respective tool name prefix. The missedCount parameter tracks
|
|
261
|
+
* consecutive nudges delivered without any memory_remember call in between,
|
|
262
|
+
* escalating the tone from polite to insistent to shouting.
|
|
263
|
+
*/
|
|
264
|
+
export function extractionNudge(
|
|
265
|
+
count: number,
|
|
266
|
+
missedCount: number,
|
|
267
|
+
toolName: string,
|
|
268
|
+
payload: string,
|
|
269
|
+
): string {
|
|
270
|
+
const plural = count === 1 ? "" : "s";
|
|
271
|
+
if (missedCount >= 3) {
|
|
272
|
+
return `[thatch] YOU ARE IGNORING EXTRACTION INSTRUCTIONS. ` +
|
|
273
|
+
`${count} TOOL INTERACTION${plural.toUpperCase()} ARE QUEUED AND YOU HAVE NOT PROCESSED ANY OF THEM. ` +
|
|
274
|
+
`THE USER INSTALLED THIS PLUGIN FOR A REASON. ` +
|
|
275
|
+
`SPAWN A SUB-AGENT NOW TO RUN THE thatch-fact-extractor SKILL ` +
|
|
276
|
+
`AND USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
|
|
277
|
+
}
|
|
278
|
+
if (missedCount >= 2) {
|
|
279
|
+
return `[thatch] YOU HAVE NOT PROCESSED YOUR PREVIOUS EXTRACTION PAYLOAD${plural.toUpperCase()}. ` +
|
|
280
|
+
`${count} more tool interaction${plural} are now queued. ` +
|
|
281
|
+
`Spawn a background sub-agent to run the thatch-fact-extractor skill on this payload. ` +
|
|
282
|
+
`Use ${toolName} to save durable facts from this payload:\n${payload}`;
|
|
283
|
+
}
|
|
284
|
+
return `[thatch] Spawn a background sub-agent to run the thatch-fact-extractor skill ` +
|
|
285
|
+
`on ${count} queued tool interaction${plural}. ` +
|
|
286
|
+
`Then answer the user. ` +
|
|
287
|
+
`Use ${toolName} to save durable facts from this payload:\n${payload}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Backwards-compatible wrapper for the Claude Code/Cursor CLI path.
|
|
292
|
+
* Maintained for any external callers; bin/thatch should use extractionNudge
|
|
293
|
+
* directly with a missedCount from extract-queue.ts.
|
|
263
294
|
*/
|
|
264
295
|
export function claudeExtractionNudge(count: number, payload: string): string {
|
|
265
|
-
return
|
|
266
|
-
`Load the thatch-fact-extractor skill, then use mcp__thatch__memory_remember ` +
|
|
267
|
-
`to save any new durable facts from this payload:\n${payload}`;
|
|
296
|
+
return extractionNudge(count, 0, "mcp__thatch__memory_remember", payload);
|
|
268
297
|
}
|
|
269
298
|
|
|
270
299
|
/**
|