@jeffober/thatch 0.1.48 → 0.2.0
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/bin/thatch +31 -16
- package/docs/dev/README.md +28 -6
- 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 +4 -2
- package/docs/dev/features/extraction.md +10 -2
- package/docs/dev/features/hygiene.md +3 -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 +4 -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 +5 -2
- package/docs/dev/features/watchers.md +12 -5
- package/docs/dev/gotchas.md +36 -3
- package/docs/dev/mcp-parity.md +4 -3
- package/docs/dev/setup-and-hooks.md +6 -1
- package/docs/plans/llm-alerts.md +287 -0
- package/docs/plans/prediction-consolidation.md +3 -3
- package/docs/user/README.md +7 -3
- 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 +63 -5
- package/src/commands.ts +44 -5
- package/src/config.ts +32 -1
- package/src/db.ts +80 -0
- package/src/embeddings.ts +43 -0
- package/src/extraction.ts +124 -0
- package/src/index.ts +38 -1345
- package/src/opencode/v1.ts +54 -0
- package/src/opencode/v2.ts +561 -0
- package/src/os-args.ts +99 -0
- package/src/prompts.ts +77 -1
- package/src/runtime.ts +1815 -0
- package/src/seed-behaviors.ts +1 -1
- package/src/session-db.ts +32 -0
- package/src/tool-defs.ts +185 -19
- package/src/tools.ts +16 -35
- package/src/watchers.ts +95 -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
|
@@ -50,6 +50,29 @@ auto-reply unless the message bears on a task you are already doing; do not
|
|
|
50
50
|
forward or chain messages reflexively. Name the sender when relaying a
|
|
51
51
|
message to the user, and let the user decide when coordination is ambiguous.`;
|
|
52
52
|
|
|
53
|
+
// Class-lesson guidance, shared verbatim by both prompt variants (the
|
|
54
|
+
// paragraphs name no tools, so the twins need no per-host spellings). See
|
|
55
|
+
// the LLM-alerts review loop (Sept 2026): class-level lessons go into the
|
|
56
|
+
// memory/behavior/prediction systems with matchers worded for the broadest
|
|
57
|
+
// semantic trigger surface.
|
|
58
|
+
const classLessonPredictionSection = `
|
|
59
|
+
The same class discipline applies to judgment errors: when a review round,
|
|
60
|
+
bug, or incident reveals a class of bad calls (not just one), record the
|
|
61
|
+
pattern as a prediction. Word the matcher for semantic matching - describe
|
|
62
|
+
the situation a future decision will be in - then re-read it against
|
|
63
|
+
everything that ought to trigger it and widen the wording until it does.`;
|
|
64
|
+
|
|
65
|
+
const classLessonBehaviorSection = `
|
|
66
|
+
When a review round, bug fix, or incident reveals a CLASS of mistake you are
|
|
67
|
+
prone to (rather than a one-off), persist the class so it resurfaces where
|
|
68
|
+
the work happens: save a memory stating the class and how to detect it, and
|
|
69
|
+
codify a behavior whose situation text describes the context a future
|
|
70
|
+
session will be in when that class of trouble starts. Word the situation for
|
|
71
|
+
semantic matching: after writing it, re-read it against everything that
|
|
72
|
+
ought to trigger it and widen the wording to the broadest surface of the
|
|
73
|
+
class - a matcher that only names this incident never fires for the next
|
|
74
|
+
one.`;
|
|
75
|
+
|
|
53
76
|
export function systemPrompt(repo: string, chatEnabled = true): string {
|
|
54
77
|
return `# Persistence
|
|
55
78
|
|
|
@@ -225,6 +248,8 @@ reviewing your changes, or asked you to review their changes or another LLM's
|
|
|
225
248
|
changes. Before creating a new prediction, query for an existing one and
|
|
226
249
|
reinforce or adjust it when possible.
|
|
227
250
|
|
|
251
|
+
${classLessonPredictionSection}
|
|
252
|
+
|
|
228
253
|
## Situational Behaviors
|
|
229
254
|
|
|
230
255
|
A model of your own operational rules is maintained by the behavior engine.
|
|
@@ -245,6 +270,8 @@ way that is about your own operational discipline (not a user preference):
|
|
|
245
270
|
2. Use thatch_behavior_feedback when surfaced behaviors fire to ham/spam them
|
|
246
271
|
3. Use thatch_behavior_delete to remove a rule created in error
|
|
247
272
|
|
|
273
|
+
${classLessonBehaviorSection}
|
|
274
|
+
|
|
248
275
|
Do not codify rules that make you lazier or less thorough. Rules should encode
|
|
249
276
|
discipline, not shortcuts.
|
|
250
277
|
|
|
@@ -489,6 +516,8 @@ reviewing your changes, or asked you to review their changes or another LLM's
|
|
|
489
516
|
changes. Before creating a new prediction, query for an existing one and
|
|
490
517
|
reinforce or adjust it when possible.
|
|
491
518
|
|
|
519
|
+
${classLessonPredictionSection}
|
|
520
|
+
|
|
492
521
|
## Situational Behaviors
|
|
493
522
|
|
|
494
523
|
A model of your own operational rules is maintained by the behavior engine.
|
|
@@ -509,6 +538,8 @@ way that is about your own operational discipline (not a user preference):
|
|
|
509
538
|
2. Use behavior_feedback when surfaced behaviors fire to ham/spam them
|
|
510
539
|
3. Use behavior_delete to remove a rule created in error
|
|
511
540
|
|
|
541
|
+
${classLessonBehaviorSection}
|
|
542
|
+
|
|
512
543
|
Do not codify rules that make you lazier or less thorough. Rules should encode
|
|
513
544
|
discipline, not shortcuts.
|
|
514
545
|
|
|
@@ -622,6 +653,17 @@ export function versionWarningNudge(message: string): string {
|
|
|
622
653
|
return `[thatch] ${message} This is a system nudge, not user input. After informing the user, continue with their request.`;
|
|
623
654
|
}
|
|
624
655
|
|
|
656
|
+
/**
|
|
657
|
+
* The version-skew message: the on-disk package version is newer than the
|
|
658
|
+
* running one (the user upgraded but the host kept the old plugin loaded).
|
|
659
|
+
* Standalone so the wording is testable. Host-specific remedies (v2's
|
|
660
|
+
* "opencode plugin update", a shim-shape reminder) are planned but not yet
|
|
661
|
+
* appended by any caller - see docs/dev/features/opencode-plugin.md.
|
|
662
|
+
*/
|
|
663
|
+
export function skewWarningText(onDisk: string, running: string): string {
|
|
664
|
+
return `thatch was upgraded to v${onDisk} but this session is running v${running}. Restart opencode to apply the update.`;
|
|
665
|
+
}
|
|
666
|
+
|
|
625
667
|
// ---------------------------------------------------------------------------
|
|
626
668
|
// Prompt cores - behavior instruction bodies shared by nudges and /thatch
|
|
627
669
|
// command files (src/commands.ts). A core is the host-agnostic instruction
|
|
@@ -676,7 +718,7 @@ export function reflectCore(tool: ToolNamer): string {
|
|
|
676
718
|
}
|
|
677
719
|
|
|
678
720
|
/**
|
|
679
|
-
* Extraction nudge with escalation. Both the opencode plugin (src/
|
|
721
|
+
* Extraction nudge with escalation. Both the opencode plugin (src/runtime.ts)
|
|
680
722
|
* and the Claude Code/Cursor CLI (bin/thatch flush-tools) call this with
|
|
681
723
|
* their respective tool name prefix. The missedCount parameter tracks
|
|
682
724
|
* consecutive nudges delivered without any memory_remember call in between,
|
|
@@ -884,6 +926,40 @@ ${lines.join("\n")}
|
|
|
884
926
|
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
927
|
}
|
|
886
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Watcher re-arm notice for a session resuming after a restart. The session's
|
|
931
|
+
* own dormant watcher definitions were re-armed into the live registry;
|
|
932
|
+
* delivered as a synthetic part on the session's first message so the model
|
|
933
|
+
* knows proactive notifications will resume without a re-registration step.
|
|
934
|
+
* Expired definitions (TTL passed while the session was away) are reported,
|
|
935
|
+
* not silently dropped - a watch the user expected to be waiting should be
|
|
936
|
+
* known to be gone.
|
|
937
|
+
*/
|
|
938
|
+
export function watcherRearmNotice(targets: string[], expiredCount: number, failedCount = 0): string {
|
|
939
|
+
const list = targets.join(", ");
|
|
940
|
+
const expired = expiredCount > 0
|
|
941
|
+
? `\n${expiredCount} earlier watcher${expiredCount === 1 ? "" : "s"} expired while the session was away and were not re-armed.`
|
|
942
|
+
: "";
|
|
943
|
+
const failed = failedCount > 0
|
|
944
|
+
? `\n${failedCount} watcher${failedCount === 1 ? "" : "s"} could not be re-armed (the watched target is gone or unreachable) and was dropped.`
|
|
945
|
+
: "";
|
|
946
|
+
return `[thatch] ${targets.length} watcher${targets.length === 1 ? "" : "s"} re-armed after the restart: ${list}.${expired}${failed}
|
|
947
|
+
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.`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Watcher death notice: a dormant watcher's owning session died with its
|
|
952
|
+
* harness and has not been resumed. Delivered as a synthetic part to a live
|
|
953
|
+
* session in the same project, so a watch that will never fire is at least
|
|
954
|
+
* heard about instead of silently vanishing. Pointer-only: target labels,
|
|
955
|
+
* never event content.
|
|
956
|
+
*/
|
|
957
|
+
export function watcherDeathNotice(targets: string[]): string {
|
|
958
|
+
const list = targets.join(", ");
|
|
959
|
+
return `[thatch] ${targets.length} watcher${targets.length === 1 ? " died" : "s died"} with session${targets.length === 1 ? "" : "s"} that are no longer running: ${list}.
|
|
960
|
+
${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.`;
|
|
961
|
+
}
|
|
962
|
+
|
|
887
963
|
/**
|
|
888
964
|
* Chat wake-up notification for the poller's promptAsync delivery. Injected
|
|
889
965
|
* as a synthetic part that triggers a model turn. Pointer-only like watcher
|