@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.
Files changed (50) hide show
  1. package/README.md +43 -4
  2. package/artifacts/skills/thatch-fact-extractor.md +1 -1
  3. package/artifacts/skills/thatch-memory-verify.md +2 -2
  4. package/bin/thatch +31 -16
  5. package/docs/dev/README.md +39 -13
  6. package/docs/dev/features/behavior-engine.md +3 -2
  7. package/docs/dev/features/cicd.md +1 -1
  8. package/docs/dev/features/compaction-recovery.md +1 -1
  9. package/docs/dev/features/cross-session-chat.md +12 -3
  10. package/docs/dev/features/extraction.md +10 -2
  11. package/docs/dev/features/hygiene.md +3 -1
  12. package/docs/dev/features/memory-store.md +1 -1
  13. package/docs/dev/features/multi-host.md +4 -2
  14. package/docs/dev/features/notifications.md +121 -4
  15. package/docs/dev/features/nudge-pipeline.md +16 -2
  16. package/docs/dev/features/opencode-plugin.md +133 -0
  17. package/docs/dev/features/qa-system.md +43 -0
  18. package/docs/dev/features/repo-identity.md +1 -1
  19. package/docs/dev/features/session-lifecycle.md +6 -3
  20. package/docs/dev/features/watchers.md +12 -5
  21. package/docs/dev/gotchas.md +94 -4
  22. package/docs/dev/mcp-parity.md +4 -3
  23. package/docs/dev/setup-and-hooks.md +7 -2
  24. package/docs/plans/llm-alerts.md +287 -0
  25. package/docs/plans/prediction-consolidation.md +3 -3
  26. package/docs/user/README.md +8 -4
  27. package/docs/user/memory.md +1 -1
  28. package/docs/user/notifications.md +63 -4
  29. package/docs/user/setup.md +4 -0
  30. package/docs/user/watchers.md +15 -7
  31. package/package.json +3 -1
  32. package/src/alerts.ts +360 -0
  33. package/src/capabilities.ts +174 -0
  34. package/src/chat.ts +80 -5
  35. package/src/commands.ts +44 -5
  36. package/src/config.ts +32 -1
  37. package/src/db.ts +157 -12
  38. package/src/embeddings.ts +43 -0
  39. package/src/extraction.ts +210 -0
  40. package/src/index.ts +77 -1339
  41. package/src/opencode/v1.ts +54 -0
  42. package/src/opencode/v2.ts +605 -0
  43. package/src/os-args.ts +99 -0
  44. package/src/prompts.ts +181 -31
  45. package/src/runtime.ts +2004 -0
  46. package/src/seed-behaviors.ts +1 -1
  47. package/src/session-db.ts +32 -0
  48. package/src/tool-defs.ts +340 -46
  49. package/src/tools.ts +16 -35
  50. 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 has an old \`updated_at\` date or when preparing a knowledge export.
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
- ## Memory Verification
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 has an old \`updated_at\` date or when preparing a knowledge export.
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
- ## Memory Verification
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/index.ts)
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 `[thatch] ${n} ${word} ${verb} to this prompt (${labels}${etc}). Use ${toolName} before responding.`;
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 `- [${p.confidence.toFixed(2)} conf, ${p.evidence_count} tests] When ${p.matcher_description}: ${verb} ${p.statement}`;
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 `- [${b.confidence.toFixed(2)} conf, ${b.evidence_count} tests] When ${b.matcher_description}: ${verb} ${b.statement}`;
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