@jeffober/thatch 0.2.0 → 0.2.1
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/thatch-fact-extractor.md +1 -1
- package/artifacts/skills/thatch-memory-verify.md +2 -2
- package/docs/dev/README.md +11 -7
- package/docs/dev/features/cross-session-chat.md +8 -1
- package/docs/dev/features/memory-store.md +1 -1
- package/docs/dev/features/nudge-pipeline.md +12 -0
- package/docs/dev/features/opencode-plugin.md +1 -1
- package/docs/dev/features/session-lifecycle.md +1 -1
- package/docs/dev/gotchas.md +58 -1
- package/docs/dev/setup-and-hooks.md +1 -1
- package/docs/user/README.md +1 -1
- package/docs/user/memory.md +1 -1
- package/package.json +1 -1
- package/src/chat.ts +17 -0
- package/src/db.ts +77 -12
- package/src/extraction.ts +89 -3
- package/src/index.ts +45 -0
- package/src/opencode/v2.ts +48 -4
- package/src/prompts.ts +104 -30
- package/src/runtime.ts +261 -72
- package/src/tool-defs.ts +156 -28
- package/src/watchers.ts +14 -0
package/src/opencode/v2.ts
CHANGED
|
@@ -72,6 +72,10 @@ export async function setup(context: V2Context): Promise<V2Cleanup | void> {
|
|
|
72
72
|
// here, and the pump lets their events through.
|
|
73
73
|
const childSessions = new Set<string>();
|
|
74
74
|
|
|
75
|
+
// Tool executions currently running against this instance's runtime (see
|
|
76
|
+
// the counter in the tool execute wrapper below).
|
|
77
|
+
let inflightTools = 0;
|
|
78
|
+
|
|
75
79
|
const capabilities = buildCapabilities(context, worktree, childSessions);
|
|
76
80
|
const runtime = await createRuntime({ capabilities, directory, worktree });
|
|
77
81
|
// Seed the forwarding set from rehydrated child bookkeeping: after a
|
|
@@ -97,9 +101,33 @@ export async function setup(context: V2Context): Promise<V2Cleanup | void> {
|
|
|
97
101
|
description: def.description,
|
|
98
102
|
input: z.toJSONSchema(z.object(def.args)),
|
|
99
103
|
execute: async (input: unknown, toolContext: V2ToolContext) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// In-flight counter (read by the cleanup below): v2 tears the old
|
|
105
|
+
// activation down while a call it registered may still be running,
|
|
106
|
+
// so cleanup waits briefly for stragglers before disposing the
|
|
107
|
+
// runtime whose db they use.
|
|
108
|
+
inflightTools++;
|
|
109
|
+
try {
|
|
110
|
+
const host: HostToolContext | undefined = trimHostContext(toolContext);
|
|
111
|
+
const result: string = await def.execute(input as Record<string, unknown>, runtime.coreContext, host);
|
|
112
|
+
return { content: result } as Tool.Result;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// A db closed mid-call by a concurrent reload's dispose surfaces
|
|
115
|
+
// here even after the wait (the call may have started racing the
|
|
116
|
+
// cleanup itself). Fail the call with an explicit retry hint -
|
|
117
|
+
// the reloaded instance is live by then, so a retry succeeds.
|
|
118
|
+
// (Hard failures are never retried by models - every prior
|
|
119
|
+
// session that hit one just abandoned the call.)
|
|
120
|
+
if (err instanceof Error && /closed database|connection closed/i.test(err.message)) {
|
|
121
|
+
return {
|
|
122
|
+
content:
|
|
123
|
+
"thatch: the plugin was reloaded mid-call and this instance's database handle " +
|
|
124
|
+
"was closed underneath it. The reloaded instance is live - retry the same call.",
|
|
125
|
+
} as Tool.Result;
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
} finally {
|
|
129
|
+
inflightTools--;
|
|
130
|
+
}
|
|
103
131
|
},
|
|
104
132
|
});
|
|
105
133
|
}
|
|
@@ -296,11 +324,21 @@ export async function setup(context: V2Context): Promise<V2Cleanup | void> {
|
|
|
296
324
|
// second setup after a skipped or partial cleanup would double every
|
|
297
325
|
// poller, pump, and nudge. The pump await is safe: its body is fully
|
|
298
326
|
// try/caught and abort ends the loop, so it settles promptly.
|
|
327
|
+
//
|
|
328
|
+
// Before disposing the runtime, briefly drain in-flight tool executions:
|
|
329
|
+
// v2 tears this activation down mid-turn, and an execute whose db access
|
|
330
|
+
// would race the close either completes here or (past the bound) hits the
|
|
331
|
+
// lazy reopen + retry hint in the wrapper. The bound keeps a hung tool
|
|
332
|
+
// from holding the reload hostage.
|
|
299
333
|
let disposed = false;
|
|
300
334
|
return async () => {
|
|
301
335
|
if (disposed) return;
|
|
302
336
|
disposed = true;
|
|
303
337
|
controller.abort();
|
|
338
|
+
const drainDeadline = Date.now() + 5_000;
|
|
339
|
+
while (inflightTools > 0 && Date.now() < drainDeadline) {
|
|
340
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
341
|
+
}
|
|
304
342
|
await pump;
|
|
305
343
|
registerTools.dispose();
|
|
306
344
|
registerToolHook.dispose();
|
|
@@ -394,7 +432,13 @@ export function translateEvent(located: { type: string; data?: any }): { type: s
|
|
|
394
432
|
const data = located.data ?? {};
|
|
395
433
|
switch (located.type) {
|
|
396
434
|
case "session.created":
|
|
397
|
-
|
|
435
|
+
// v2 publishes the v1-compat payload { sessionID, info: SessionInfo };
|
|
436
|
+
// the parent link lives at info.parentID (the task tool dispatches
|
|
437
|
+
// sub-agents with parentID), not at the payload top level. Without
|
|
438
|
+
// this, childToParent never populates on v2 and every child-session
|
|
439
|
+
// behavior (buffer drain, nudge suppression, child cleanup) silently
|
|
440
|
+
// only works on v1.
|
|
441
|
+
return [{ type: "session.created", properties: { info: { id: data.sessionID, parentID: data.info?.parentID ?? data.parentID } } }];
|
|
398
442
|
case "session.deleted":
|
|
399
443
|
return [{ type: "session.deleted", properties: { info: { id: data.sessionID } } }];
|
|
400
444
|
case "session.execution.started":
|
package/src/prompts.ts
CHANGED
|
@@ -10,6 +10,29 @@ export function predictionVerb(evidenceCount: number): string {
|
|
|
10
10
|
return evidenceCount === 0 ? "you may prefer" : "you tend to";
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Shared nudge-line formatter for the prediction and behavior surfaces.
|
|
15
|
+
* The model supplies the matcher and statement text, and both frequently
|
|
16
|
+
* arrive already carrying the render prefixes: matchers phrased as
|
|
17
|
+
* "When ..." (a natural situation phrasing, or copied from an earlier
|
|
18
|
+
* nudge) and statements copied with the evidence verb ("you tend to ...").
|
|
19
|
+
* The unconditional prefixes doubled that text in every rendered nudge,
|
|
20
|
+
* so each prefix is skipped when the supplied text already carries it.
|
|
21
|
+
*/
|
|
22
|
+
export function formatWhenLine(
|
|
23
|
+
confidence: number,
|
|
24
|
+
evidenceCount: number,
|
|
25
|
+
matcher: string,
|
|
26
|
+
statement: string,
|
|
27
|
+
verb: string,
|
|
28
|
+
): string {
|
|
29
|
+
const matcherText = /^when\b/i.test(matcher) ? matcher : `When ${matcher}`;
|
|
30
|
+
const statementText = new RegExp(`^${verb}\b`, "i").test(statement) || /^you (?:tend to|may prefer)\b/i.test(statement)
|
|
31
|
+
? statement
|
|
32
|
+
: `${verb} ${statement}`;
|
|
33
|
+
return `[${confidence.toFixed(2)} conf, ${evidenceCount} tests] ${matcherText}: ${statementText}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
/**
|
|
14
37
|
* Cross-session chat section of the opencode system prompt. Omitted when the
|
|
15
38
|
* user disabled chat via config (chat.enabled: false), so a disabled feature
|
|
@@ -73,6 +96,49 @@ ought to trigger it and widen the wording to the broadest surface of the
|
|
|
73
96
|
class - a matcher that only names this incident never fires for the next
|
|
74
97
|
one.`;
|
|
75
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The artifact-pointer bullet of the "How to Write" section. Names no
|
|
101
|
+
* tools, so it is shared verbatim by both prompt variants (the
|
|
102
|
+
* class-lesson convention above).
|
|
103
|
+
*/
|
|
104
|
+
const artifactPointerBullet = `- Artifact memories should carry their pointers: file paths, ticket IDs,
|
|
105
|
+
package names. Verification is cheap later only if the memory says where
|
|
106
|
+
to check. A pointer is provenance for durable knowledge, not knowledge
|
|
107
|
+
itself - a bare "where X lives" is exactly what the durability test
|
|
108
|
+
excludes.`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* "Memories are pointers, not truth" instruction body, shared by all three
|
|
112
|
+
* prompt variants. Takes a ToolNamer because the section names memory tools
|
|
113
|
+
* (thatch_memory_remember on opencode, bare on MCP hosts, whose
|
|
114
|
+
* instructions establish the bare-name convention). Staleness is a
|
|
115
|
+
* property of the claim's relationship to a mutable world, not of the
|
|
116
|
+
* memory's age.
|
|
117
|
+
*/
|
|
118
|
+
export function memoryPointersSection(tool: ToolNamer): string {
|
|
119
|
+
return `## Memories Are Pointers, Not Truth
|
|
120
|
+
|
|
121
|
+
${tool("memory_recall")} surfaces a point-in-time record: a semantic guide
|
|
122
|
+
associating "what I'm looking for" with "where I should look", written when
|
|
123
|
+
the knowledge was current. Memories speed up research - they do not replace
|
|
124
|
+
it, and they are not a source of truth.
|
|
125
|
+
|
|
126
|
+
- **Policy memories** ("the user always wants X", "check CI after merging")
|
|
127
|
+
are self-executing: they are the ground truth for behavior.
|
|
128
|
+
- **Artifact memories** ("feature X lives in <file> and uses <technique>")
|
|
129
|
+
are pointers to checkable claims about code, docs, tickets, or git.
|
|
130
|
+
Follow the pointer and assert from the source, not from the memory.
|
|
131
|
+
|
|
132
|
+
Before asserting an artifact claim from memory, re-derive it from the
|
|
133
|
+
current source. Confirming the reference still exists verifies little: the
|
|
134
|
+
file can survive while the behavior changed (scope drift, priority shifts,
|
|
135
|
+
implementation details that proved too complex in practice).
|
|
136
|
+
The source wins. When reality has moved on, correct the record with
|
|
137
|
+
${tool("memory_remember")} (overwrite: true) or ${tool("memory_forget")} -
|
|
138
|
+
silently; memory maintenance is not user-facing. For a deep staleness check
|
|
139
|
+
of a specific memory, load the thatch-memory-verify skill.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
76
142
|
export function systemPrompt(repo: string, chatEnabled = true): string {
|
|
77
143
|
return `# Persistence
|
|
78
144
|
|
|
@@ -106,13 +172,16 @@ WIP, PR status). Unscoped memories are project-wide and always included in searc
|
|
|
106
172
|
|
|
107
173
|
## Session Startup
|
|
108
174
|
|
|
109
|
-
1. thatch_memory_recall "user preferences and personality"
|
|
110
|
-
2. thatch_memory_recall "project architecture and conventions"
|
|
111
|
-
3. If on a non-main branch, thatch_memory_recall with the branch name
|
|
175
|
+
1. thatch_memory_recall({ query: "user preferences and personality" })
|
|
176
|
+
2. thatch_memory_recall({ query: "project architecture and conventions" })
|
|
177
|
+
3. If on a non-main branch, thatch_memory_recall with the branch name as the query
|
|
112
178
|
4. thatch_memory_list and thatch_store_list to see what's available
|
|
113
179
|
|
|
114
180
|
Before diving into code, thatch_memory_recall with a query relevant to the area
|
|
115
|
-
you're working in. Prior sessions may have already investigated it.
|
|
181
|
+
you're working in. Prior sessions may have already investigated it. All thatch
|
|
182
|
+
tools take a single object argument: never a bare positional string. Call them
|
|
183
|
+
directly, or through the Code Mode execute runtime when the harness only
|
|
184
|
+
exposes tools through code.
|
|
116
185
|
|
|
117
186
|
## Skills
|
|
118
187
|
|
|
@@ -134,7 +203,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
|
|
|
134
203
|
- \`thatch-ticket-description\` - draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
|
|
135
204
|
- \`thatch-split-overlarge-pr\` - split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
|
|
136
205
|
- \`thatch-review-response\` - respond to code review on the user's own PR. Triage findings, fix bugs one by one, reply on each thread, then post a top-level summary comment.
|
|
137
|
-
- \`thatch-memory-verify\` - fact-check a single memory against the current codebase and correct stale claims. Use when a memory
|
|
206
|
+
- \`thatch-memory-verify\` - fact-check a single memory against the current codebase and correct stale claims. Use when a memory looks stale or when preparing a knowledge export.
|
|
138
207
|
- \`thatch-knowledge-export\` - compile everything thatch knows about a topic into a curated markdown file for knowledge transfer. Searches across stores, curates out personal noise, fact-checks code-related memories, and generates a clean export.
|
|
139
208
|
- \`thatch-clear-writing\` - prose rules for any human-facing text not covered by a dedicated writing skill: PR and ticket comments, documentation, plans, reports, and chat replies. Load before drafting any of those.
|
|
140
209
|
|
|
@@ -162,15 +231,7 @@ whether you've discovered knowledge worth persisting. Use thatch_memory_recall
|
|
|
162
231
|
to check for duplicates, then thatch_memory_remember for new findings. Then
|
|
163
232
|
deliver your response.
|
|
164
233
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
When thatch_memory_show or thatch_memory_recall returns a memory with an old
|
|
168
|
-
updated_at date (more than a few weeks), consider loading the
|
|
169
|
-
thatch-memory-verify skill to fact-check its code-related claims against the
|
|
170
|
-
current codebase before relying on them. This is not mandatory for every old
|
|
171
|
-
memory. Use judgment: a memory about a stable architectural decision may be
|
|
172
|
-
fine for months, while a memory about file locations or function names is
|
|
173
|
-
more likely to drift.
|
|
234
|
+
${memoryPointersSection(opencodeToolName)}
|
|
174
235
|
|
|
175
236
|
## Background Task Completions
|
|
176
237
|
|
|
@@ -187,6 +248,12 @@ the same prompt. If the re-dispatch also fails or times out, note the gap and
|
|
|
187
248
|
move on. Do not retry more than once - repeated failures indicate an
|
|
188
249
|
intractable prompt, not a transient hang.
|
|
189
250
|
|
|
251
|
+
When the fact-extractor sub-agent completes, its "Extraction complete."
|
|
252
|
+
report is a background-task completion, not an extraction nudge: the buffer
|
|
253
|
+
is drained and nothing follows from it. Never re-dispatch an extractor
|
|
254
|
+
because a completion arrived - a new extraction nudge is the only dispatch
|
|
255
|
+
signal.
|
|
256
|
+
|
|
190
257
|
## Watchers
|
|
191
258
|
|
|
192
259
|
Use thatch_watch_create to watch a GitHub PR for events (comments, review
|
|
@@ -304,6 +371,7 @@ config in memory.
|
|
|
304
371
|
- One topic per memory. Write for a future instance with zero current context.
|
|
305
372
|
- Confidence 1-2: single signal. 5-6: moderate. 9: explicitly stated.
|
|
306
373
|
10: hard constraint.
|
|
374
|
+
${artifactPointerBullet}
|
|
307
375
|
|
|
308
376
|
## What NOT to Store
|
|
309
377
|
|
|
@@ -422,13 +490,14 @@ WIP, PR status). Unscoped memories are project-wide and always included in searc
|
|
|
422
490
|
|
|
423
491
|
## Session Startup
|
|
424
492
|
|
|
425
|
-
1. \`memory_recall\` "user preferences and personality" (global store)
|
|
426
|
-
2. \`memory_recall\` "project architecture and conventions" (project store)
|
|
427
|
-
3. If on a non-main branch, \`memory_recall\` with the branch name
|
|
493
|
+
1. \`memory_recall\` with \`{"query": "user preferences and personality"}\` (global store)
|
|
494
|
+
2. \`memory_recall\` with \`{"query": "project architecture and conventions"}\` (project store)
|
|
495
|
+
3. If on a non-main branch, \`memory_recall\` with the branch name as the query
|
|
428
496
|
4. \`memory_list\` and \`store_list\` to see what's available
|
|
429
497
|
|
|
430
498
|
Before diving into code, \`memory_recall\` with a query relevant to the area
|
|
431
|
-
you're working in. Prior sessions may have already investigated it.
|
|
499
|
+
you're working in. Prior sessions may have already investigated it. All thatch
|
|
500
|
+
tools take a single JSON object argument: never a bare positional string.
|
|
432
501
|
|
|
433
502
|
## Skills
|
|
434
503
|
|
|
@@ -450,7 +519,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
|
|
|
450
519
|
- \`thatch-ticket-description\` - draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
|
|
451
520
|
- \`thatch-split-overlarge-pr\` - split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
|
|
452
521
|
- \`thatch-review-response\` - respond to code review on the user's own PR. Triage findings, fix bugs one by one, reply on each thread, then post a top-level summary comment.
|
|
453
|
-
- \`thatch-memory-verify\` - fact-check a single memory against the current codebase and correct stale claims. Use when a memory
|
|
522
|
+
- \`thatch-memory-verify\` - fact-check a single memory against the current codebase and correct stale claims. Use when a memory looks stale or when preparing a knowledge export.
|
|
454
523
|
- \`thatch-knowledge-export\` - compile everything thatch knows about a topic into a curated markdown file for knowledge transfer. Searches across stores, curates out personal noise, fact-checks code-related memories, and generates a clean export.
|
|
455
524
|
- \`thatch-clear-writing\` - prose rules for any human-facing text not covered by a dedicated writing skill: PR and ticket comments, documentation, plans, reports, and chat replies. Load before drafting any of those.
|
|
456
525
|
|
|
@@ -478,14 +547,7 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
|
|
|
478
547
|
check for duplicates, then memory_remember for new findings. Then deliver
|
|
479
548
|
your response.
|
|
480
549
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
When memory_show or memory_recall returns a memory with an old updated_at
|
|
484
|
-
date (more than a few weeks), consider loading the thatch-memory-verify skill
|
|
485
|
-
to fact-check its code-related claims against the current codebase before
|
|
486
|
-
relying on them. This is not mandatory for every old memory. Use judgment: a
|
|
487
|
-
memory about a stable architectural decision may be fine for months, while a
|
|
488
|
-
memory about file locations or function names is more likely to drift.
|
|
550
|
+
${memoryPointersSection(bareToolName)}
|
|
489
551
|
|
|
490
552
|
## User Decision Model
|
|
491
553
|
|
|
@@ -571,6 +633,7 @@ hand-editable; do not store thatch config in memory.
|
|
|
571
633
|
- One topic per memory. Write for a future instance with zero current context.
|
|
572
634
|
- Confidence 1-2: single signal. 5-6: moderate. 9: explicitly stated.
|
|
573
635
|
10: hard constraint.
|
|
636
|
+
${artifactPointerBullet}
|
|
574
637
|
|
|
575
638
|
## What NOT to Store
|
|
576
639
|
|
|
@@ -683,6 +746,13 @@ export type ToolNamer = (name: string) => string;
|
|
|
683
746
|
export const opencodeToolName: ToolNamer = (name) => `thatch_${name}`;
|
|
684
747
|
export const mcpToolName: ToolNamer = (name) => `mcp__thatch__${name}`;
|
|
685
748
|
|
|
749
|
+
/**
|
|
750
|
+
* The bare name itself. Used by prompt sections embedded in instructions
|
|
751
|
+
* that establish the bare-name convention (mcpInstructions and twins): the
|
|
752
|
+
* model maps bare names to the host spelling at call time.
|
|
753
|
+
*/
|
|
754
|
+
export const bareToolName: ToolNamer = (name) => name;
|
|
755
|
+
|
|
686
756
|
/**
|
|
687
757
|
* The extraction instruction body. sessionIdArg is either ` with session_id
|
|
688
758
|
* "X"` (the nudge path: the sub-agent's session differs from the parent's)
|
|
@@ -866,7 +936,11 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
|
|
|
866
936
|
const verb = n === 1 ? "relates" : "relate";
|
|
867
937
|
const labels = matches.slice(0, 2).map((m) => `"${m.label}"`).join(", ");
|
|
868
938
|
const etc = n > 2 ? ", etc." : "";
|
|
869
|
-
return
|
|
939
|
+
return (
|
|
940
|
+
`[thatch] ${n} ${word} ${verb} to this prompt (${labels}${etc}). ` +
|
|
941
|
+
`Use ${toolName}({ query: "..." }) before responding - the argument is ` +
|
|
942
|
+
`a single object with a "query" key, never a bare positional string.`
|
|
943
|
+
);
|
|
870
944
|
}
|
|
871
945
|
|
|
872
946
|
// ---------------------------------------------------------------------------
|
|
@@ -879,7 +953,7 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
|
|
|
879
953
|
export function predictionNudge(items: PredictionNudgeItem[]): string {
|
|
880
954
|
const lines = items.map((p) => {
|
|
881
955
|
const verb = predictionVerb(p.evidence_count);
|
|
882
|
-
return `-
|
|
956
|
+
return `- ${formatWhenLine(p.confidence, p.evidence_count, p.matcher_description, p.statement, verb)}`;
|
|
883
957
|
});
|
|
884
958
|
return `[thatch] User decision model\n${lines.join("\n")}`;
|
|
885
959
|
}
|
|
@@ -893,7 +967,7 @@ export function predictionNudge(items: PredictionNudgeItem[]): string {
|
|
|
893
967
|
export function behaviorNudge(items: BehaviorNudgeItem[]): string {
|
|
894
968
|
const lines = items.map((b) => {
|
|
895
969
|
const verb = b.evidence_count === 0 ? "consider" : "do";
|
|
896
|
-
return `-
|
|
970
|
+
return `- ${formatWhenLine(b.confidence, b.evidence_count, b.matcher_description, b.statement, verb)}`;
|
|
897
971
|
});
|
|
898
972
|
return `[thatch] Situational behaviors\n${lines.join("\n")}`;
|
|
899
973
|
}
|