@jeffober/thatch 0.1.13 → 0.1.14
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 +11 -0
- package/artifacts/skills/thatch-code-review.md +6 -4
- package/docs/user/README.md +11 -0
- package/package.json +1 -1
- package/src/extract-queue.ts +1 -1
- package/src/index.ts +49 -0
- package/src/prompts.ts +23 -6
- package/src/tool-defs.ts +28 -0
package/README.md
CHANGED
|
@@ -32,6 +32,17 @@ No API keys, no cloud services; everything runs on your machine.
|
|
|
32
32
|
On next start, OpenCode npm-installs thatch (putting the `thatch` CLI on your
|
|
33
33
|
PATH as a side effect) and its tools are available immediately.
|
|
34
34
|
|
|
35
|
+
Set this environment variable so the extraction nudge can run the fact-extractor
|
|
36
|
+
as a **background** sub-agent instead of blocking your turn:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without it, opencode hides the task tool's `background` parameter and every
|
|
43
|
+
extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
|
|
44
|
+
`[env]`, or `.envrc`.
|
|
45
|
+
|
|
35
46
|
**Self-hosting** (local development):
|
|
36
47
|
|
|
37
48
|
```jsonc
|
|
@@ -10,13 +10,15 @@ This entire review is a static analysis exercise. Do NOT run tests, linters, com
|
|
|
10
10
|
|
|
11
11
|
## Step 1: Resolve the review target
|
|
12
12
|
|
|
13
|
+
First, run `git fetch origin` (NOT `git pull` — fetch updates remote-tracking refs without touching the working tree or current branch). This refreshes `origin/main` and other remote-tracking refs to their true remote state. Local branches (e.g. `main`) only advance on checkout/pull and may be stale on a long-lived feature checkout, so resolve the base against the remote tracking ref, not the local branch.
|
|
14
|
+
|
|
13
15
|
Identify what to review:
|
|
14
|
-
- If a branch was specified, identify the base branch (usually main or master), compute the merge-base, and review the range merge-base..HEAD.
|
|
15
|
-
- If a PR number was specified, use gh pr view to resolve the head and base, fetch both refs, and review merge-base..head.
|
|
16
|
+
- If a branch was specified, identify the base branch (usually main or master), compute the merge-base against the remote tracking ref (e.g. `git merge-base HEAD origin/main`), and review the range merge-base..HEAD.
|
|
17
|
+
- If a PR number was specified, use `gh pr view` to resolve the head and base, fetch both refs, and review merge-base..head.
|
|
16
18
|
- If a git range was specified (A..B form), use it directly.
|
|
17
|
-
- If no target was specified, review the current branch against its base.
|
|
19
|
+
- If no target was specified, review the current branch against its base, resolving the base from `origin/main` (or the appropriate remote tracking ref) after fetching.
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Also fetch any refs not locally reachable so branches and PRs that were never checked out can be reviewed (e.g. `git fetch origin <branch>` for a PR head).
|
|
20
22
|
|
|
21
23
|
Run git diff --stat on the resolved range and git log --oneline to understand the change.
|
|
22
24
|
|
package/docs/user/README.md
CHANGED
|
@@ -20,6 +20,17 @@ Publish to npm and add thatch to your opencode config:
|
|
|
20
20
|
|
|
21
21
|
OpenCode installs the plugin and its dependencies automatically on next start.
|
|
22
22
|
|
|
23
|
+
Set this environment variable so the extraction nudge can dispatch the
|
|
24
|
+
fact-extractor as a **background** sub-agent instead of blocking your turn:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Without it, opencode hides the task tool's `background` parameter and the
|
|
31
|
+
extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
|
|
32
|
+
`[env]`, or `.envrc`.
|
|
33
|
+
|
|
23
34
|
For local development before publishing, use a file path:
|
|
24
35
|
|
|
25
36
|
```jsonc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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/extract-queue.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
|
|
|
56
56
|
const additions: ToolInteraction[] = [];
|
|
57
57
|
for (const tc of toolCalls) {
|
|
58
58
|
const lower = tc.tool_name.toLowerCase();
|
|
59
|
-
if (lower === "mcp__thatch__memory_remember") {
|
|
59
|
+
if (lower === "mcp__thatch__memory_remember" || lower === "mcp__thatch__extraction_done") {
|
|
60
60
|
resetMissedCount(sessionID);
|
|
61
61
|
consumeQueue(sessionID);
|
|
62
62
|
existing.length = 0;
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
56
56
|
// all-caps shouting. Reset to 0 whenever the agent writes a memory.
|
|
57
57
|
const missedNudges = new Map<string, number>();
|
|
58
58
|
|
|
59
|
+
// Parent-child session mapping for cross-session buffer drain. opencode
|
|
60
|
+
// creates real child sessions with their own IDs for sub-agents; without
|
|
61
|
+
// this map, a memory_remember call in a child (e.g. a background
|
|
62
|
+
// fact-extractor task) drains the child's empty buffer but leaves the
|
|
63
|
+
// parent's buffer untouched, causing the nudge to replay indefinitely.
|
|
64
|
+
// Populated from session.created events that carry a parentID.
|
|
65
|
+
const childToParent = new Map<string, string>();
|
|
66
|
+
|
|
59
67
|
// Skills always install to the global opencode config — installing into the
|
|
60
68
|
// worktree would mutate the user's repo (untracked files in git status).
|
|
61
69
|
// A failed install degrades the nudge workflow but must not kill the plugin.
|
|
@@ -103,8 +111,34 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
103
111
|
// The buffer is NOT drained on nudge delivery — it persists until the
|
|
104
112
|
// agent writes a memory, so ignored nudges accumulate and the payload
|
|
105
113
|
// grows with each missed cycle.
|
|
114
|
+
//
|
|
115
|
+
// Fix A: a memory_remember call in a child session (sub-agent) also
|
|
116
|
+
// drains the parent's buffer. Without this, dispatching the
|
|
117
|
+
// fact-extractor as a background task writes memories in the child
|
|
118
|
+
// but never clears the parent's queue — the nudge replays every turn.
|
|
119
|
+
//
|
|
120
|
+
// Fix C: thatch_extraction_done is an explicit acknowledgment tool the
|
|
121
|
+
// model calls in the parent session after dispatching. Belt-and-
|
|
122
|
+
// suspenders for fix A: covers the case where the sub-agent errors out
|
|
123
|
+
// or the host doesn't expose parent-child session relationships.
|
|
106
124
|
"tool.execute.after": async (input, output) => {
|
|
107
125
|
if (input.tool === "thatch_memory_remember") {
|
|
126
|
+
extraction.consume(input.sessionID);
|
|
127
|
+
missedNudges.delete(input.sessionID);
|
|
128
|
+
// Fix A: if this is a child session, also drain the parent so the
|
|
129
|
+
// parent's extraction nudge stops replaying after a sub-agent writes
|
|
130
|
+
// memories on its behalf.
|
|
131
|
+
const parentID = childToParent.get(input.sessionID);
|
|
132
|
+
if (parentID) {
|
|
133
|
+
extraction.consume(parentID);
|
|
134
|
+
missedNudges.delete(parentID);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Fix C: explicit acknowledgment tool drains the buffer without
|
|
139
|
+
// requiring a memory write. The model calls this after dispatching
|
|
140
|
+
// the fact-extractor to a sub-agent.
|
|
141
|
+
if (input.tool === "thatch_extraction_done") {
|
|
108
142
|
extraction.consume(input.sessionID);
|
|
109
143
|
missedNudges.delete(input.sessionID);
|
|
110
144
|
return;
|
|
@@ -183,7 +217,22 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
183
217
|
|
|
184
218
|
// 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
|
|
185
219
|
// best-effort: a failure there must not cost the reminder itself.
|
|
220
|
+
//
|
|
221
|
+
// Also tracks parent-child session relationships for fix A (cross-session
|
|
222
|
+
// buffer drain). session.created with a parentID records the mapping;
|
|
223
|
+
// session.deleted cleans it up to avoid unbounded growth.
|
|
186
224
|
event: async ({ event }) => {
|
|
225
|
+
if (event.type === "session.created") {
|
|
226
|
+
const info = event.properties.info;
|
|
227
|
+
if (info.parentID) {
|
|
228
|
+
childToParent.set(info.id, info.parentID);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (event.type === "session.deleted") {
|
|
232
|
+
childToParent.delete(event.properties.info.id);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
187
236
|
if (event.type !== "session.created") return;
|
|
188
237
|
const id = event.properties.info.id;
|
|
189
238
|
|
package/src/prompts.ts
CHANGED
|
@@ -136,7 +136,7 @@ Tools are prefixed in Claude Code: \`mcp__thatch__memory_remember\`,
|
|
|
136
136
|
\`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
|
|
137
137
|
\`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
|
|
138
138
|
\`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
|
|
139
|
-
\`mcp__thatch__dedup_mark_checked\`.
|
|
139
|
+
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
|
|
140
140
|
Bare names used below for readability.
|
|
141
141
|
|
|
142
142
|
## Stores
|
|
@@ -268,21 +268,38 @@ export function extractionNudge(
|
|
|
268
268
|
payload: string,
|
|
269
269
|
): string {
|
|
270
270
|
const plural = count === 1 ? "" : "s";
|
|
271
|
+
// opencode's task tool has a `background` parameter (requires
|
|
272
|
+
// OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true) that launches the sub-agent
|
|
273
|
+
// asynchronously — the call returns immediately and the session is notified
|
|
274
|
+
// on completion. Naming the parameter gives the model a direct hit on the tool
|
|
275
|
+
// schema it sees. MCP hosts (Claude Code, Cursor) keep generic sub-agent
|
|
276
|
+
// wording because their background mechanisms differ and are not exposed as a
|
|
277
|
+
// per-call parameter in the same way.
|
|
278
|
+
const isOpencode = toolName.startsWith("thatch_");
|
|
279
|
+
const dispatch = isOpencode
|
|
280
|
+
? "Dispatch a task with background: true"
|
|
281
|
+
: "Spawn a background sub-agent";
|
|
282
|
+
const drainTool = isOpencode ? "thatch_extraction_done" : "mcp__thatch__extraction_done";
|
|
283
|
+
const acknowledge = `After dispatching, call ${drainTool} to acknowledge.`;
|
|
284
|
+
|
|
271
285
|
if (missedCount >= 3) {
|
|
272
286
|
return `[thatch] YOU ARE IGNORING EXTRACTION INSTRUCTIONS. ` +
|
|
273
287
|
`${count} TOOL INTERACTION${plural.toUpperCase()} ARE QUEUED AND YOU HAVE NOT PROCESSED ANY OF THEM. ` +
|
|
274
288
|
`THE USER INSTALLED THIS PLUGIN FOR A REASON. ` +
|
|
275
|
-
|
|
276
|
-
|
|
289
|
+
`${dispatch.toUpperCase()} NOW TO RUN THE thatch-fact-extractor SKILL. ` +
|
|
290
|
+
`${acknowledge.toUpperCase()} ` +
|
|
291
|
+
`USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
|
|
277
292
|
}
|
|
278
293
|
if (missedCount >= 2) {
|
|
279
294
|
return `[thatch] YOU HAVE NOT PROCESSED YOUR PREVIOUS EXTRACTION PAYLOAD${plural.toUpperCase()}. ` +
|
|
280
295
|
`${count} more tool interaction${plural} are now queued. ` +
|
|
281
|
-
|
|
296
|
+
`${dispatch} to run the thatch-fact-extractor skill on this payload. ` +
|
|
297
|
+
`${acknowledge} ` +
|
|
282
298
|
`Use ${toolName} to save durable facts from this payload:\n${payload}`;
|
|
283
299
|
}
|
|
284
|
-
return `[thatch]
|
|
300
|
+
return `[thatch] ${dispatch} to run the thatch-fact-extractor skill ` +
|
|
285
301
|
`on ${count} queued tool interaction${plural}. ` +
|
|
302
|
+
`${acknowledge} ` +
|
|
286
303
|
`Then answer the user. ` +
|
|
287
304
|
`Use ${toolName} to save durable facts from this payload:\n${payload}`;
|
|
288
305
|
}
|
|
@@ -312,7 +329,7 @@ Tools are prefixed in Cursor: \`mcp__thatch__memory_remember\`,
|
|
|
312
329
|
\`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
|
|
313
330
|
\`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
|
|
314
331
|
\`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
|
|
315
|
-
\`mcp__thatch__dedup_mark_checked\`.
|
|
332
|
+
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
|
|
316
333
|
Bare names used below for readability.
|
|
317
334
|
|
|
318
335
|
## Stores
|
package/src/tool-defs.ts
CHANGED
|
@@ -340,6 +340,33 @@ const markCheckedDef: ToolDef = {
|
|
|
340
340
|
},
|
|
341
341
|
};
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Acknowledge that the extraction nudge has been handled. Drains the
|
|
345
|
+
* extraction buffer so the nudge does not replay on the next turn.
|
|
346
|
+
*
|
|
347
|
+
* The actual draining happens in the host's post-tool hook
|
|
348
|
+
* (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) —
|
|
349
|
+
* this tool's execute function is a no-op confirmation. The tool exists so
|
|
350
|
+
* the model can call it in the parent session after dispatching the
|
|
351
|
+
* fact-extractor to a sub-agent, giving the hook a recognizable tool name
|
|
352
|
+
* to key on. Without it, the buffer only drains when a memory_remember
|
|
353
|
+
* call lands in the same session (or a tracked child session), which fails
|
|
354
|
+
* if the sub-agent errors out or the host doesn't expose parent-child
|
|
355
|
+
* session relationships.
|
|
356
|
+
*/
|
|
357
|
+
const extractionDoneDef: ToolDef = {
|
|
358
|
+
name: "extraction_done",
|
|
359
|
+
description:
|
|
360
|
+
"Acknowledge that the extraction nudge has been dispatched to a sub-agent. " +
|
|
361
|
+
"Drains the extraction buffer so the nudge does not replay. " +
|
|
362
|
+
"Call this in the parent session after dispatching a background task " +
|
|
363
|
+
"to run the thatch-fact-extractor skill.",
|
|
364
|
+
args: {},
|
|
365
|
+
async execute() {
|
|
366
|
+
return "[acknowledged] extraction buffer drained";
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
343
370
|
/**
|
|
344
371
|
* All tool definitions, in the order they should be presented to the agent.
|
|
345
372
|
* The opencode plugin wraps each in `tool()`; the MCP server exposes them
|
|
@@ -354,4 +381,5 @@ export const TOOL_DEFS: ToolDef[] = [
|
|
|
354
381
|
listStoresDef,
|
|
355
382
|
findDuplicatesDef,
|
|
356
383
|
markCheckedDef,
|
|
384
|
+
extractionDoneDef,
|
|
357
385
|
];
|