@jeffober/thatch 0.1.48 → 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/README.md +43 -4
- package/artifacts/skills/thatch-fact-extractor.md +1 -1
- package/artifacts/skills/thatch-memory-verify.md +2 -2
- package/bin/thatch +31 -16
- package/docs/dev/README.md +39 -13
- package/docs/dev/features/behavior-engine.md +3 -2
- package/docs/dev/features/cicd.md +1 -1
- package/docs/dev/features/compaction-recovery.md +1 -1
- package/docs/dev/features/cross-session-chat.md +12 -3
- package/docs/dev/features/extraction.md +10 -2
- package/docs/dev/features/hygiene.md +3 -1
- package/docs/dev/features/memory-store.md +1 -1
- package/docs/dev/features/multi-host.md +4 -2
- package/docs/dev/features/notifications.md +121 -4
- package/docs/dev/features/nudge-pipeline.md +16 -2
- package/docs/dev/features/opencode-plugin.md +133 -0
- package/docs/dev/features/qa-system.md +43 -0
- package/docs/dev/features/repo-identity.md +1 -1
- package/docs/dev/features/session-lifecycle.md +6 -3
- package/docs/dev/features/watchers.md +12 -5
- package/docs/dev/gotchas.md +94 -4
- package/docs/dev/mcp-parity.md +4 -3
- package/docs/dev/setup-and-hooks.md +7 -2
- package/docs/plans/llm-alerts.md +287 -0
- package/docs/plans/prediction-consolidation.md +3 -3
- package/docs/user/README.md +8 -4
- package/docs/user/memory.md +1 -1
- package/docs/user/notifications.md +63 -4
- package/docs/user/setup.md +4 -0
- package/docs/user/watchers.md +15 -7
- package/package.json +3 -1
- package/src/alerts.ts +360 -0
- package/src/capabilities.ts +174 -0
- package/src/chat.ts +80 -5
- package/src/commands.ts +44 -5
- package/src/config.ts +32 -1
- package/src/db.ts +157 -12
- package/src/embeddings.ts +43 -0
- package/src/extraction.ts +210 -0
- package/src/index.ts +77 -1339
- package/src/opencode/v1.ts +54 -0
- package/src/opencode/v2.ts +605 -0
- package/src/os-args.ts +99 -0
- package/src/prompts.ts +181 -31
- package/src/runtime.ts +2004 -0
- package/src/seed-behaviors.ts +1 -1
- package/src/session-db.ts +32 -0
- package/src/tool-defs.ts +340 -46
- package/src/tools.ts +16 -35
- package/src/watchers.ts +109 -14
package/src/os-args.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses the session id a harness was started to continue from an argument
|
|
5
|
+
* list (`opencode -s <id>` / `--session <id>` / `--session=<id>`). Pure:
|
|
6
|
+
* the caller supplies the list (see startupSessionId for where it comes
|
|
7
|
+
* from). The framework passes no session id to plugins and resuming a
|
|
8
|
+
* session fires no events, so the command line is the only signal that a
|
|
9
|
+
* fresh harness is the new home of a continued session.
|
|
10
|
+
*/
|
|
11
|
+
export function startupSessionIdFromArgv(argv: string[]): string | null {
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (arg === "-s" || arg === "--session") return argv[i + 1] ?? null;
|
|
15
|
+
if (arg.startsWith("--session=")) return arg.slice("--session=".length) || null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the harness was started with `-c`/`--continue` (continue the most
|
|
22
|
+
* recent session). Pure. Yargs-style boolean flags only: `--continue=false`
|
|
23
|
+
* reads as the negation, everything else as a plain switch. The session id
|
|
24
|
+
* itself is not on the command line - the harness resolves "most recent
|
|
25
|
+
* top-level session" at startup, which the plugin replicates via the SDK
|
|
26
|
+
* (continuesLastSessionId).
|
|
27
|
+
*/
|
|
28
|
+
export function continuesLastSessionFromArgv(argv: string[]): boolean {
|
|
29
|
+
for (const arg of argv) {
|
|
30
|
+
if (arg === "-c" || arg === "--continue") return true;
|
|
31
|
+
if (arg.startsWith("--continue=")) return arg.slice("--continue=".length) !== "false";
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Picks the session `opencode -c` continues: the most recently updated
|
|
38
|
+
* top-level session. Pure; the caller supplies the session list from
|
|
39
|
+
* client.session.list() (directory-scoped, like the plugin's own client).
|
|
40
|
+
* The server's list order is not trusted - the TUI re-sorts by
|
|
41
|
+
* time.updated before picking, and so does this.
|
|
42
|
+
*/
|
|
43
|
+
export function continuesLastSessionId(
|
|
44
|
+
sessions: { id: string; parentID?: string; time?: { updated?: number } }[],
|
|
45
|
+
): string | null {
|
|
46
|
+
return (
|
|
47
|
+
[...sessions]
|
|
48
|
+
.sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
|
|
49
|
+
.find((s) => !s.parentID)?.id ?? null
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The plugin does not run in the process's main thread: opencode's TUI
|
|
54
|
+
// spawns a worker thread (cli/tui/worker.js) to host the server, and worker
|
|
55
|
+
// threads get their own argv (just the worker script) with none of the CLI
|
|
56
|
+
// flags. The thread still shares the process's pid, so the OS-level command
|
|
57
|
+
// line for our own pid is the real `opencode -s ses_...` invocation. Read it
|
|
58
|
+
// from /proc on Linux or via ps on macOS. Returns [] when unavailable;
|
|
59
|
+
// callers treat that as "no session flag". The readers are injectable so
|
|
60
|
+
// tests can drive each branch without a real /proc or ps.
|
|
61
|
+
export interface OsArgsDeps {
|
|
62
|
+
/** Reads a file as UTF-8, throwing when it does not exist. */
|
|
63
|
+
readFile: (path: string) => string;
|
|
64
|
+
/** Runs `ps -p <pid> -o args=`; returns its exit code and stdout. */
|
|
65
|
+
ps: (pid: number) => { exitCode: number; stdout: string };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultOsArgsDeps: OsArgsDeps = {
|
|
69
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
70
|
+
ps: (pid) => {
|
|
71
|
+
const res = Bun.spawnSync(["ps", "-p", String(pid), "-o", "args="]);
|
|
72
|
+
return { exitCode: res.exitCode, stdout: res.stdout.toString() };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function osProcessArgs(deps: OsArgsDeps = defaultOsArgsDeps, pid = process.pid): string[] {
|
|
77
|
+
try {
|
|
78
|
+
// Linux: cmdline is NUL-separated argv, exact and cheap.
|
|
79
|
+
return deps.readFile("/proc/self/cmdline").split("\0").filter(Boolean);
|
|
80
|
+
} catch {
|
|
81
|
+
// macOS has no /proc; ask ps for this pid's full command. Splitting on
|
|
82
|
+
// whitespace is safe for our purpose: session ids never contain spaces.
|
|
83
|
+
try {
|
|
84
|
+
const res = deps.ps(pid);
|
|
85
|
+
if (res.exitCode !== 0) return [];
|
|
86
|
+
return res.stdout.trim().split(/\s+/).filter(Boolean);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Resolve the session id this harness was launched to resume, if any:
|
|
94
|
+
// prefer the thread-local argv (covers tests and any non-worker host), then
|
|
95
|
+
// fall back to the process's OS-level command line (the real harness path).
|
|
96
|
+
// The caller passes the OS command line in so it is read once per init.
|
|
97
|
+
export function startupSessionId(osArgs: string[]): string | null {
|
|
98
|
+
return startupSessionIdFromArgv(process.argv) ?? startupSessionIdFromArgv(osArgs);
|
|
99
|
+
}
|
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
|
|
@@ -50,6 +73,72 @@ auto-reply unless the message bears on a task you are already doing; do not
|
|
|
50
73
|
forward or chain messages reflexively. Name the sender when relaying a
|
|
51
74
|
message to the user, and let the user decide when coordination is ambiguous.`;
|
|
52
75
|
|
|
76
|
+
// Class-lesson guidance, shared verbatim by both prompt variants (the
|
|
77
|
+
// paragraphs name no tools, so the twins need no per-host spellings). See
|
|
78
|
+
// the LLM-alerts review loop (Sept 2026): class-level lessons go into the
|
|
79
|
+
// memory/behavior/prediction systems with matchers worded for the broadest
|
|
80
|
+
// semantic trigger surface.
|
|
81
|
+
const classLessonPredictionSection = `
|
|
82
|
+
The same class discipline applies to judgment errors: when a review round,
|
|
83
|
+
bug, or incident reveals a class of bad calls (not just one), record the
|
|
84
|
+
pattern as a prediction. Word the matcher for semantic matching - describe
|
|
85
|
+
the situation a future decision will be in - then re-read it against
|
|
86
|
+
everything that ought to trigger it and widen the wording until it does.`;
|
|
87
|
+
|
|
88
|
+
const classLessonBehaviorSection = `
|
|
89
|
+
When a review round, bug fix, or incident reveals a CLASS of mistake you are
|
|
90
|
+
prone to (rather than a one-off), persist the class so it resurfaces where
|
|
91
|
+
the work happens: save a memory stating the class and how to detect it, and
|
|
92
|
+
codify a behavior whose situation text describes the context a future
|
|
93
|
+
session will be in when that class of trouble starts. Word the situation for
|
|
94
|
+
semantic matching: after writing it, re-read it against everything that
|
|
95
|
+
ought to trigger it and widen the wording to the broadest surface of the
|
|
96
|
+
class - a matcher that only names this incident never fires for the next
|
|
97
|
+
one.`;
|
|
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
|
+
|
|
53
142
|
export function systemPrompt(repo: string, chatEnabled = true): string {
|
|
54
143
|
return `# Persistence
|
|
55
144
|
|
|
@@ -83,13 +172,16 @@ WIP, PR status). Unscoped memories are project-wide and always included in searc
|
|
|
83
172
|
|
|
84
173
|
## Session Startup
|
|
85
174
|
|
|
86
|
-
1. thatch_memory_recall "user preferences and personality"
|
|
87
|
-
2. thatch_memory_recall "project architecture and conventions"
|
|
88
|
-
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
|
|
89
178
|
4. thatch_memory_list and thatch_store_list to see what's available
|
|
90
179
|
|
|
91
180
|
Before diving into code, thatch_memory_recall with a query relevant to the area
|
|
92
|
-
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.
|
|
93
185
|
|
|
94
186
|
## Skills
|
|
95
187
|
|
|
@@ -111,7 +203,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
|
|
|
111
203
|
- \`thatch-ticket-description\` - draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
|
|
112
204
|
- \`thatch-split-overlarge-pr\` - split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
|
|
113
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.
|
|
114
|
-
- \`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.
|
|
115
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.
|
|
116
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.
|
|
117
209
|
|
|
@@ -139,15 +231,7 @@ whether you've discovered knowledge worth persisting. Use thatch_memory_recall
|
|
|
139
231
|
to check for duplicates, then thatch_memory_remember for new findings. Then
|
|
140
232
|
deliver your response.
|
|
141
233
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
When thatch_memory_show or thatch_memory_recall returns a memory with an old
|
|
145
|
-
updated_at date (more than a few weeks), consider loading the
|
|
146
|
-
thatch-memory-verify skill to fact-check its code-related claims against the
|
|
147
|
-
current codebase before relying on them. This is not mandatory for every old
|
|
148
|
-
memory. Use judgment: a memory about a stable architectural decision may be
|
|
149
|
-
fine for months, while a memory about file locations or function names is
|
|
150
|
-
more likely to drift.
|
|
234
|
+
${memoryPointersSection(opencodeToolName)}
|
|
151
235
|
|
|
152
236
|
## Background Task Completions
|
|
153
237
|
|
|
@@ -164,6 +248,12 @@ the same prompt. If the re-dispatch also fails or times out, note the gap and
|
|
|
164
248
|
move on. Do not retry more than once - repeated failures indicate an
|
|
165
249
|
intractable prompt, not a transient hang.
|
|
166
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
|
+
|
|
167
257
|
## Watchers
|
|
168
258
|
|
|
169
259
|
Use thatch_watch_create to watch a GitHub PR for events (comments, review
|
|
@@ -225,6 +315,8 @@ reviewing your changes, or asked you to review their changes or another LLM's
|
|
|
225
315
|
changes. Before creating a new prediction, query for an existing one and
|
|
226
316
|
reinforce or adjust it when possible.
|
|
227
317
|
|
|
318
|
+
${classLessonPredictionSection}
|
|
319
|
+
|
|
228
320
|
## Situational Behaviors
|
|
229
321
|
|
|
230
322
|
A model of your own operational rules is maintained by the behavior engine.
|
|
@@ -245,6 +337,8 @@ way that is about your own operational discipline (not a user preference):
|
|
|
245
337
|
2. Use thatch_behavior_feedback when surfaced behaviors fire to ham/spam them
|
|
246
338
|
3. Use thatch_behavior_delete to remove a rule created in error
|
|
247
339
|
|
|
340
|
+
${classLessonBehaviorSection}
|
|
341
|
+
|
|
248
342
|
Do not codify rules that make you lazier or less thorough. Rules should encode
|
|
249
343
|
discipline, not shortcuts.
|
|
250
344
|
|
|
@@ -277,6 +371,7 @@ config in memory.
|
|
|
277
371
|
- One topic per memory. Write for a future instance with zero current context.
|
|
278
372
|
- Confidence 1-2: single signal. 5-6: moderate. 9: explicitly stated.
|
|
279
373
|
10: hard constraint.
|
|
374
|
+
${artifactPointerBullet}
|
|
280
375
|
|
|
281
376
|
## What NOT to Store
|
|
282
377
|
|
|
@@ -395,13 +490,14 @@ WIP, PR status). Unscoped memories are project-wide and always included in searc
|
|
|
395
490
|
|
|
396
491
|
## Session Startup
|
|
397
492
|
|
|
398
|
-
1. \`memory_recall\` "user preferences and personality" (global store)
|
|
399
|
-
2. \`memory_recall\` "project architecture and conventions" (project store)
|
|
400
|
-
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
|
|
401
496
|
4. \`memory_list\` and \`store_list\` to see what's available
|
|
402
497
|
|
|
403
498
|
Before diving into code, \`memory_recall\` with a query relevant to the area
|
|
404
|
-
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.
|
|
405
501
|
|
|
406
502
|
## Skills
|
|
407
503
|
|
|
@@ -423,7 +519,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
|
|
|
423
519
|
- \`thatch-ticket-description\` - draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
|
|
424
520
|
- \`thatch-split-overlarge-pr\` - split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
|
|
425
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.
|
|
426
|
-
- \`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.
|
|
427
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.
|
|
428
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.
|
|
429
525
|
|
|
@@ -451,14 +547,7 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
|
|
|
451
547
|
check for duplicates, then memory_remember for new findings. Then deliver
|
|
452
548
|
your response.
|
|
453
549
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
When memory_show or memory_recall returns a memory with an old updated_at
|
|
457
|
-
date (more than a few weeks), consider loading the thatch-memory-verify skill
|
|
458
|
-
to fact-check its code-related claims against the current codebase before
|
|
459
|
-
relying on them. This is not mandatory for every old memory. Use judgment: a
|
|
460
|
-
memory about a stable architectural decision may be fine for months, while a
|
|
461
|
-
memory about file locations or function names is more likely to drift.
|
|
550
|
+
${memoryPointersSection(bareToolName)}
|
|
462
551
|
|
|
463
552
|
## User Decision Model
|
|
464
553
|
|
|
@@ -489,6 +578,8 @@ reviewing your changes, or asked you to review their changes or another LLM's
|
|
|
489
578
|
changes. Before creating a new prediction, query for an existing one and
|
|
490
579
|
reinforce or adjust it when possible.
|
|
491
580
|
|
|
581
|
+
${classLessonPredictionSection}
|
|
582
|
+
|
|
492
583
|
## Situational Behaviors
|
|
493
584
|
|
|
494
585
|
A model of your own operational rules is maintained by the behavior engine.
|
|
@@ -509,6 +600,8 @@ way that is about your own operational discipline (not a user preference):
|
|
|
509
600
|
2. Use behavior_feedback when surfaced behaviors fire to ham/spam them
|
|
510
601
|
3. Use behavior_delete to remove a rule created in error
|
|
511
602
|
|
|
603
|
+
${classLessonBehaviorSection}
|
|
604
|
+
|
|
512
605
|
Do not codify rules that make you lazier or less thorough. Rules should encode
|
|
513
606
|
discipline, not shortcuts.
|
|
514
607
|
|
|
@@ -540,6 +633,7 @@ hand-editable; do not store thatch config in memory.
|
|
|
540
633
|
- One topic per memory. Write for a future instance with zero current context.
|
|
541
634
|
- Confidence 1-2: single signal. 5-6: moderate. 9: explicitly stated.
|
|
542
635
|
10: hard constraint.
|
|
636
|
+
${artifactPointerBullet}
|
|
543
637
|
|
|
544
638
|
## What NOT to Store
|
|
545
639
|
|
|
@@ -622,6 +716,17 @@ export function versionWarningNudge(message: string): string {
|
|
|
622
716
|
return `[thatch] ${message} This is a system nudge, not user input. After informing the user, continue with their request.`;
|
|
623
717
|
}
|
|
624
718
|
|
|
719
|
+
/**
|
|
720
|
+
* The version-skew message: the on-disk package version is newer than the
|
|
721
|
+
* running one (the user upgraded but the host kept the old plugin loaded).
|
|
722
|
+
* Standalone so the wording is testable. Host-specific remedies (v2's
|
|
723
|
+
* "opencode plugin update", a shim-shape reminder) are planned but not yet
|
|
724
|
+
* appended by any caller - see docs/dev/features/opencode-plugin.md.
|
|
725
|
+
*/
|
|
726
|
+
export function skewWarningText(onDisk: string, running: string): string {
|
|
727
|
+
return `thatch was upgraded to v${onDisk} but this session is running v${running}. Restart opencode to apply the update.`;
|
|
728
|
+
}
|
|
729
|
+
|
|
625
730
|
// ---------------------------------------------------------------------------
|
|
626
731
|
// Prompt cores - behavior instruction bodies shared by nudges and /thatch
|
|
627
732
|
// command files (src/commands.ts). A core is the host-agnostic instruction
|
|
@@ -641,6 +746,13 @@ export type ToolNamer = (name: string) => string;
|
|
|
641
746
|
export const opencodeToolName: ToolNamer = (name) => `thatch_${name}`;
|
|
642
747
|
export const mcpToolName: ToolNamer = (name) => `mcp__thatch__${name}`;
|
|
643
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
|
+
|
|
644
756
|
/**
|
|
645
757
|
* The extraction instruction body. sessionIdArg is either ` with session_id
|
|
646
758
|
* "X"` (the nudge path: the sub-agent's session differs from the parent's)
|
|
@@ -676,7 +788,7 @@ export function reflectCore(tool: ToolNamer): string {
|
|
|
676
788
|
}
|
|
677
789
|
|
|
678
790
|
/**
|
|
679
|
-
* Extraction nudge with escalation. Both the opencode plugin (src/
|
|
791
|
+
* Extraction nudge with escalation. Both the opencode plugin (src/runtime.ts)
|
|
680
792
|
* and the Claude Code/Cursor CLI (bin/thatch flush-tools) call this with
|
|
681
793
|
* their respective tool name prefix. The missedCount parameter tracks
|
|
682
794
|
* consecutive nudges delivered without any memory_remember call in between,
|
|
@@ -824,7 +936,11 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
|
|
|
824
936
|
const verb = n === 1 ? "relates" : "relate";
|
|
825
937
|
const labels = matches.slice(0, 2).map((m) => `"${m.label}"`).join(", ");
|
|
826
938
|
const etc = n > 2 ? ", etc." : "";
|
|
827
|
-
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
|
+
);
|
|
828
944
|
}
|
|
829
945
|
|
|
830
946
|
// ---------------------------------------------------------------------------
|
|
@@ -837,7 +953,7 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
|
|
|
837
953
|
export function predictionNudge(items: PredictionNudgeItem[]): string {
|
|
838
954
|
const lines = items.map((p) => {
|
|
839
955
|
const verb = predictionVerb(p.evidence_count);
|
|
840
|
-
return `-
|
|
956
|
+
return `- ${formatWhenLine(p.confidence, p.evidence_count, p.matcher_description, p.statement, verb)}`;
|
|
841
957
|
});
|
|
842
958
|
return `[thatch] User decision model\n${lines.join("\n")}`;
|
|
843
959
|
}
|
|
@@ -851,7 +967,7 @@ export function predictionNudge(items: PredictionNudgeItem[]): string {
|
|
|
851
967
|
export function behaviorNudge(items: BehaviorNudgeItem[]): string {
|
|
852
968
|
const lines = items.map((b) => {
|
|
853
969
|
const verb = b.evidence_count === 0 ? "consider" : "do";
|
|
854
|
-
return `-
|
|
970
|
+
return `- ${formatWhenLine(b.confidence, b.evidence_count, b.matcher_description, b.statement, verb)}`;
|
|
855
971
|
});
|
|
856
972
|
return `[thatch] Situational behaviors\n${lines.join("\n")}`;
|
|
857
973
|
}
|
|
@@ -884,6 +1000,40 @@ ${lines.join("\n")}
|
|
|
884
1000
|
This is a system notification, not user input. Decide whether to act now or keep waiting - the user's instructions from when the watch was created govern how to handle it.${detailHint} This notification is not approval to advance other pending work, with one exception: if this watch was created to gate work the user already greenlit (for example "wait for CI, then merge"), it is the continuation signal for exactly that work - proceed with it.${cadence} If you act, tell the user what you did and why; if not, stop and wait.`;
|
|
885
1001
|
}
|
|
886
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Watcher re-arm notice for a session resuming after a restart. The session's
|
|
1005
|
+
* own dormant watcher definitions were re-armed into the live registry;
|
|
1006
|
+
* delivered as a synthetic part on the session's first message so the model
|
|
1007
|
+
* knows proactive notifications will resume without a re-registration step.
|
|
1008
|
+
* Expired definitions (TTL passed while the session was away) are reported,
|
|
1009
|
+
* not silently dropped - a watch the user expected to be waiting should be
|
|
1010
|
+
* known to be gone.
|
|
1011
|
+
*/
|
|
1012
|
+
export function watcherRearmNotice(targets: string[], expiredCount: number, failedCount = 0): string {
|
|
1013
|
+
const list = targets.join(", ");
|
|
1014
|
+
const expired = expiredCount > 0
|
|
1015
|
+
? `\n${expiredCount} earlier watcher${expiredCount === 1 ? "" : "s"} expired while the session was away and were not re-armed.`
|
|
1016
|
+
: "";
|
|
1017
|
+
const failed = failedCount > 0
|
|
1018
|
+
? `\n${failedCount} watcher${failedCount === 1 ? "" : "s"} could not be re-armed (the watched target is gone or unreachable) and was dropped.`
|
|
1019
|
+
: "";
|
|
1020
|
+
return `[thatch] ${targets.length} watcher${targets.length === 1 ? "" : "s"} re-armed after the restart: ${list}.${expired}${failed}
|
|
1021
|
+
No re-registration is needed; notifications will arrive here as usual. This is a system notice, not user input - carry on with the user's request.`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Watcher death notice: a dormant watcher's owning session died with its
|
|
1026
|
+
* harness and has not been resumed. Delivered as a synthetic part to a live
|
|
1027
|
+
* session in the same project, so a watch that will never fire is at least
|
|
1028
|
+
* heard about instead of silently vanishing. Pointer-only: target labels,
|
|
1029
|
+
* never event content.
|
|
1030
|
+
*/
|
|
1031
|
+
export function watcherDeathNotice(targets: string[]): string {
|
|
1032
|
+
const list = targets.join(", ");
|
|
1033
|
+
return `[thatch] ${targets.length} watcher${targets.length === 1 ? " died" : "s died"} with session${targets.length === 1 ? "" : "s"} that are no longer running: ${list}.
|
|
1034
|
+
${targets.length === 1 ? "It" : "They"} will re-arm automatically if ${targets.length === 1 ? "that session is" : "those sessions are"} resumed. This is a system notice, not user input - carry on with the user's request.`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
887
1037
|
/**
|
|
888
1038
|
* Chat wake-up notification for the poller's promptAsync delivery. Injected
|
|
889
1039
|
* as a synthetic part that triggers a model turn. Pointer-only like watcher
|