@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/README.md
CHANGED
|
@@ -21,7 +21,10 @@ everything runs on your machine.
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
On next start, OpenCode npm-installs thatch and its tools are available
|
|
24
|
-
immediately.
|
|
24
|
+
immediately. Works with both opencode 1.x and 2.x (the same package supports
|
|
25
|
+
both plugin APIs). Until the next release ships, npm's `latest` still
|
|
26
|
+
targets 1.x only. For async extraction (child sessions run in the
|
|
27
|
+
background):
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
30
|
export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
@@ -30,6 +33,39 @@ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
|
30
33
|
Without this env var, extraction still works - the child session runs
|
|
31
34
|
synchronously (fire-and-forget) instead of asynchronously.
|
|
32
35
|
|
|
36
|
+
### What works in opencode 2.x
|
|
37
|
+
|
|
38
|
+
The v2 plugin API is missing a few surfaces the v1 API had. On 2.x:
|
|
39
|
+
|
|
40
|
+
- **No toasts.** Toasts are dropped on v2 (no publish surface). The
|
|
41
|
+
model-facing nudges they accompanied - watcher and chat wake nudges -
|
|
42
|
+
still arrive; the extraction-result and other TUI-only toasts have no
|
|
43
|
+
replacement.
|
|
44
|
+
- **Plugin state survives reloads.** Editing the plugin (or `opencode
|
|
45
|
+
plugin update`) reloads it, but extraction buffers, watcher
|
|
46
|
+
registrations, and armed wrap-ups are journaled and restored; a process
|
|
47
|
+
restart restores the state of a resumed (`-c`/`-s`) session. Other
|
|
48
|
+
sessions' watcher registrations survive as dormant definitions: resuming
|
|
49
|
+
such a session re-arms its watches on the first message (expired ones
|
|
50
|
+
are dropped and reported), and a live session in the same project gets a
|
|
51
|
+
one-line notice that a dead session's watch will re-arm if resumed.
|
|
52
|
+
- **Extraction children are visible.** The fact-extractor child sessions are
|
|
53
|
+
top-level sessions the host cannot delete, so they accumulate in the
|
|
54
|
+
session picker, and "continue last session" (`-c`) can land in one after
|
|
55
|
+
any session that triggered extraction.
|
|
56
|
+
- **`/thatch/compact` does not auto-compact.** The checklist and memory
|
|
57
|
+
flush run, but the compaction itself cannot be triggered from the plugin
|
|
58
|
+
API - run `/compact` yourself after the wrap-up completes. `/thatch/exit`
|
|
59
|
+
cannot auto-exit for the same reason.
|
|
60
|
+
- **No `-c` session listing.** The chat resume listing degrades.
|
|
61
|
+
|
|
62
|
+
Everything else - tools, memory, nudges, chat, watchers - behaves the
|
|
63
|
+
same on both versions, with one caveat: v2 has no turn-free delivery
|
|
64
|
+
(noReply), so the features that lean on it - the chat transcript echo
|
|
65
|
+
bubble, the session-start reminder, and the post-reload re-attach
|
|
66
|
+
notice - do not render on 2.x. Their model-facing content still
|
|
67
|
+
arrives; only the bubble rendering is lost.
|
|
68
|
+
|
|
33
69
|
Then **prime your project memory** by running `thatch prime` in your project directory.
|
|
34
70
|
This launches an `opencode` session to build an initial map of the code base
|
|
35
71
|
and seed the memory.
|
|
@@ -169,10 +205,13 @@ Thatch gives your agent:
|
|
|
169
205
|
the agent lists them and nothing fires -- you decide when to retry.
|
|
170
206
|
- **Notifications + user config** -- the agent can ping you out-of-band when a
|
|
171
207
|
long-running outcome lands: a desktop banner, a spoken voice
|
|
172
|
-
announcement, or both (macOS and Linux).
|
|
208
|
+
announcement, or both (macOS and Linux). On opencode, thatch also watches
|
|
209
|
+
the session and alerts you automatically when the LLM pauses for your
|
|
210
|
+
input (a question or permission prompt) or finishes a round of real work
|
|
211
|
+
-- bookkeeping rounds stay silent. Preferences live in a
|
|
173
212
|
hand-editable config file (`~/.config/thatch/config.json`) that the agent
|
|
174
|
-
manages through `config_get`/`config_set` -- ask it to change your voice
|
|
175
|
-
|
|
213
|
+
manages through `config_get`/`config_set` -- ask it to change your voice,
|
|
214
|
+
quiet notifications entirely, or tune which alerts fire.
|
|
176
215
|
|
|
177
216
|
Plus **skills** for memory workflows, structured multi-specialist code
|
|
178
217
|
review, review response, plan refinement, change and feature walkthroughs,
|
package/bin/thatch
CHANGED
|
@@ -49,11 +49,13 @@ function usage(): never {
|
|
|
49
49
|
Full conversation as OpenAI chat-completions JSONL
|
|
50
50
|
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
51
51
|
JSONL of matching parts across sessions (decoded content)
|
|
52
|
-
thatch chat list [--stale N|all] Registered chat sessions
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
thatch chat list [--stale N|all] Registered chat sessions. Default shows
|
|
53
|
+
only active (fresh-heartbeat) sessions:
|
|
54
|
+
name, session id, age, status, project,
|
|
55
|
+
topic. --stale adds the stale section:
|
|
56
|
+
N bounds it to sessions seen in the
|
|
57
|
+
last N days (default 1); all shows
|
|
58
|
+
every stale session.
|
|
57
59
|
thatch chat tail [--once] [--limit N|all] [--match RE]
|
|
58
60
|
[--from NAME] [--to NAME] [--since DT] [--until DT]
|
|
59
61
|
Follow cross-session chat as a JSONL
|
|
@@ -415,7 +417,7 @@ async function sessionCommand(rest: string[]): Promise<void> {
|
|
|
415
417
|
// ---------------------------------------------------------------------------
|
|
416
418
|
|
|
417
419
|
/** Column headers for the `chat list` roster, in display order. */
|
|
418
|
-
const CHAT_ROSTER_HEADER = ["NAME", "AGE", "STATUS", "PROJECT", "TOPIC"];
|
|
420
|
+
const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "TOPIC"];
|
|
419
421
|
|
|
420
422
|
/**
|
|
421
423
|
* Renders the `chat list` roster as two sections - Active and Stale - each
|
|
@@ -436,6 +438,7 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
436
438
|
const renderTable = (rows: ChatSessionRow[]) => {
|
|
437
439
|
const cells = (r: ChatSessionRow) => [
|
|
438
440
|
r.name,
|
|
441
|
+
r.session_id,
|
|
439
442
|
humanAge(r.last_seen),
|
|
440
443
|
chatLiveness(r),
|
|
441
444
|
r.project ?? "unknown",
|
|
@@ -447,9 +450,10 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
447
450
|
const align = (line: string[]) => line.map((c, i) => (i < line.length - 1 ? c.padEnd(widths[i]) : c));
|
|
448
451
|
const header = align(CHAT_ROSTER_HEADER).map((h) => paint("1", h)).join(" ");
|
|
449
452
|
const body = rows.map((r) => {
|
|
450
|
-
const [name, age, status, project, topic] = align(cells(r));
|
|
453
|
+
const [name, sessionID, age, status, project, topic] = align(cells(r));
|
|
451
454
|
return [
|
|
452
455
|
paint("32", name),
|
|
456
|
+
paint("2", sessionID),
|
|
453
457
|
paint("2", age),
|
|
454
458
|
paint(status === "stale" ? "31" : "32", status),
|
|
455
459
|
project,
|
|
@@ -491,21 +495,32 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
491
495
|
// sessions from burying the fresh signal. --stale all restores the
|
|
492
496
|
// unbounded view. A bad value dies loudly rather than silently
|
|
493
497
|
// shrinking the roster.
|
|
494
|
-
|
|
498
|
+
// Default: active sessions only. --stale [N|all] opts into the
|
|
499
|
+
// stale section - bare --stale keeps the one-day display cap, N
|
|
500
|
+
// bounds it, all removes it. The value is parsed by hand (not
|
|
501
|
+
// flagValue) because bare --stale is meaningful.
|
|
502
|
+
const showStale = restArgs.includes("--stale");
|
|
503
|
+
const staleIdx = restArgs.indexOf("--stale");
|
|
504
|
+
const staleNext = showStale ? restArgs[staleIdx + 1] : undefined;
|
|
495
505
|
let staleCapMs: number | null = 24 * 60 * 60 * 1000;
|
|
496
|
-
if (
|
|
497
|
-
if (
|
|
498
|
-
else if (!/^\d+(\.\d+)?$/.test(
|
|
499
|
-
console.error(`error: --stale expects a positive number of days or "all", got "${
|
|
506
|
+
if (staleNext !== undefined) {
|
|
507
|
+
if (staleNext === "all") staleCapMs = null;
|
|
508
|
+
else if (!/^\d+(\.\d+)?$/.test(staleNext) || Number(staleNext) <= 0) {
|
|
509
|
+
console.error(`error: --stale expects a positive number of days or "all", got "${staleNext}"`);
|
|
500
510
|
process.exit(1);
|
|
501
|
-
} else staleCapMs = Number(
|
|
511
|
+
} else staleCapMs = Number(staleNext) * 24 * 60 * 60 * 1000;
|
|
502
512
|
}
|
|
503
|
-
|
|
513
|
+
let sessions = db.listChatSessions();
|
|
514
|
+
if (!showStale) sessions = sessions.filter((r) => chatLiveness(r) === "fresh");
|
|
504
515
|
if (sessions.length === 0) {
|
|
505
|
-
console.log(
|
|
516
|
+
console.log(
|
|
517
|
+
showStale
|
|
518
|
+
? "No sessions registered. Sessions join the directory automatically on their first idle (opencode), or via chat_register."
|
|
519
|
+
: "No active sessions. (Stale sessions are hidden; run with --stale to include them.)",
|
|
520
|
+
);
|
|
506
521
|
break;
|
|
507
522
|
}
|
|
508
|
-
console.log(formatChatRoster(sessions, staleCapMs));
|
|
523
|
+
console.log(formatChatRoster(sessions, showStale ? staleCapMs : null));
|
|
509
524
|
break;
|
|
510
525
|
}
|
|
511
526
|
|
package/docs/dev/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Development
|
|
2
2
|
|
|
3
|
+
Feature docs describe behavior in opencode v1 `client.*` terms; the opencode v2 equivalents and degrades are catalogued in [features/opencode-plugin.md](features/opencode-plugin.md).
|
|
4
|
+
|
|
3
5
|
## Architecture
|
|
4
6
|
|
|
5
7
|
Thatch has three integration paths sharing a common core:
|
|
@@ -38,8 +40,21 @@ Shared core
|
|
|
38
40
|
└── vector-math.ts → blobToVector + cosineSimilarity helpers
|
|
39
41
|
|
|
40
42
|
OpenCode plugin path
|
|
41
|
-
├── index.ts → plugin entry:
|
|
42
|
-
|
|
43
|
+
├── index.ts → dual-shape plugin entry: merged default export
|
|
44
|
+
│ ({id, setup, server}) loads on opencode v1 AND v2;
|
|
45
|
+
│ lazy-imports the adapter matching the host
|
|
46
|
+
├── opencode/v1.ts → v1 adapter: @opencode-ai/plugin hooks object
|
|
47
|
+
├── opencode/v2.ts → v2 adapter: @opencode/plugin promise-context domains
|
|
48
|
+
├── capabilities.ts → HostCapabilities seam: the host operations the
|
|
49
|
+
│ runtime needs; capabilitiesFromClient bridges the
|
|
50
|
+
│ v1 SDK client
|
|
51
|
+
├── runtime.ts → shared plugin runtime: nudges, system prompt
|
|
52
|
+
│ injection, session events, extraction triggers,
|
|
53
|
+
│ child-session bookkeeping (host-agnostic)
|
|
54
|
+
├── os-args.ts → pure argv/OS-command-line helpers (startup session
|
|
55
|
+
│ resolution; SDK-free)
|
|
56
|
+
├── tools.ts → thin opencode tool() wrappers (v1); tool-defs.ts
|
|
57
|
+
│ owns buildCoreContext (shared per-call context)
|
|
43
58
|
├── extraction.ts → in-memory ring buffer + shared payload builders
|
|
44
59
|
├── watchers.ts → in-memory watcher registry + poller (opencode-only,
|
|
45
60
|
│ event-driven PR notifications delivered as prompts)
|
|
@@ -67,9 +82,13 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
67
82
|
| Module | Responsibility |
|
|
68
83
|
|--------|---------------|
|
|
69
84
|
| `tool-defs.ts` | **Single source of truth** for all tools. Each tool has a name, description, zod schema (args), and execute function. Framework-agnostic — neither opencode nor MCP specific. |
|
|
70
|
-
| `tools.ts` | Thin opencode wrappers.
|
|
85
|
+
| `tools.ts` | Thin opencode wrappers. `buildCoreContext` constructs the shared per-call context; `createTools` wraps each tool-def in opencode's `tool()` with a `thatch_` prefix (v1 only). |
|
|
71
86
|
| `mcp.ts` | Stdio JSON-RPC 2.0 server. Compiles zod schemas to JSON Schema via `z.toJSONSchema()` for `tools/list`. Validates args via `z.object().parse()` in `tools/call`. All logging to stderr (stdout is the transport). |
|
|
72
|
-
| `index.ts` | OpenCode plugin entry
|
|
87
|
+
| `index.ts` | Dual-shape OpenCode plugin entry: the merged default export (`{ id, setup, server }`) loads on opencode v1 (reads `default.server`) and v2 (reads `default.setup`, strips excess keys). Lazy-imports the host adapter so each host's SDK resolves only under its own runtime. |
|
|
88
|
+
| `opencode/v1.ts` + `opencode/v2.ts` | Host adapters. v1 builds HostCapabilities from the PluginInput client and returns the hooks object; v2 registers through the promise-context domains (ToolEditor, session.hook, event.subscribe) and degrades the surfaces v2 lacks. |
|
|
89
|
+
| `capabilities.ts` | The `HostCapabilities` seam: the host operations the shared runtime needs (session create/prompt/get/list/messages/delete, toast, compaction trigger, app exit). `capabilitiesFromClient` bridges the v1 SDK client. |
|
|
90
|
+
| `runtime.ts` | Shared plugin runtime: wires DB, model, extraction; the nudge tiers, system prompt injection, session event handling, child-session bookkeeping, wrap-up resolution. Host-agnostic: consumed by both adapters. |
|
|
91
|
+
| `os-args.ts` | Pure argv / OS-command-line helpers for startup session resolution (`-s`/`-c`). SDK-free. |
|
|
73
92
|
| `setup.ts` | `thatch setup --claude` / `--cursor` installer. Writes MCP config (`.mcp.json` / `.cursor/mcp.json`), appends to CLAUDE.md / AGENTS.md (idempotent), installs hooks in settings.json / hooks.json, installs skills. |
|
|
74
93
|
| `hygiene.ts` | Hygiene report: pending dedup pairs, stale count, orphaned branch memories. Shared by the plugin's session-start hook and the CLI's `thatch reminder` command. |
|
|
75
94
|
| `git.ts` | Parse `owner/repo` from git remote. Worktree-safe fallback chain, plus recovery from the `repo_paths` cache when the directory is deleted, and the spawn-cwd fallback. See [features/repo-identity.md](features/repo-identity.md). |
|
|
@@ -78,7 +97,8 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
78
97
|
| `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the `get_extraction_payload` tool returns. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `mcp.ts` imports `buildExtractionPayload` for the extraction payload provider. `summarizeArgs` is used internally by `buildExtractionPayload`. |
|
|
79
98
|
| `extract-queue.ts` | File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of `extraction.ts`. |
|
|
80
99
|
| `config.ts` | User config file (`~/.config/thatch/config.json`, beside the DB): Zod section schemas (strict — unknown keys fail validation), load with missing/invalid fallback to defaults, field-level merge, atomic save (temp + rename). Backs the `config_get`/`config_set` tools. |
|
|
81
|
-
| `notify.ts` | Out-of-band notification dispatch: darwin (`osascript` banner + `/usr/bin/say` voice), linux (`notify-send` + `spd-say`/`espeak`), other platforms report unsupported. Injectable spawner (`CoreContext.spawner`) so tests never fire real commands. Backs the `notify_user` tool. |
|
|
100
|
+
| `notify.ts` | Out-of-band notification dispatch: darwin (`osascript` banner + `/usr/bin/say` voice), linux (`notify-send` + `spd-say`/`espeak`), other platforms report unsupported. Injectable spawner (`CoreContext.spawner`) so tests never fire real commands. Backs the `notify_user` tool and the alert state machine. |
|
|
101
|
+
| `alerts.ts` | LLM alert state machine (opencode-only): pause/done/error decisions from session events, per-session dedup (no timer-based debounce - one alert per busy->idle transition, per request id for pauses). Fed by `runtime.ts`'s event hook; delivers through `notify.ts`. Plain memory by design (a reload loses at most one transition). See [../plans/llm-alerts.md](../plans/llm-alerts.md). |
|
|
82
102
|
| `session-db.ts` | Read-only access to the opencode session database (opencode.db): session timeline (`session list`), full part/message retrieval (`session get`), OpenAI chat-completions transcript builder (`session transcript`), and decoded-content search (`session search`). Backs the `thatch session` CLI subcommands and the opencode-only `session_search`/`session_get` tools. Path resolution honors `OPENCODE_DB`, then the XDG default; the db opens read-only so a thatch bug cannot corrupt host history. |
|
|
83
103
|
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only). Three sources: GitHub PRs and branches via `gh api` (injected `GhRunner`), and local shell commands via `bash -c` (injected `CommandRunner`) run as condition variables - only the exit code is read, never the command's output. Diffs against last-seen state, queues events (pointer data plus machine status like check conclusions and exit codes, never external content) per session, and delivers them through a plugin-supplied callback when the session is idle. The `pollSeconds` getter surfaces the poll cadence to tool output and notifications. Process-lifetime state by design - no SQLite. |
|
|
84
104
|
| `chat.ts` | Cross-session chat between opencode sessions on one machine (opencode-only). `ChatStore` holds the shared directory and inbox in SQLite so any process can read/write them; `ChatPoller` heartbeats hosted sessions and delivers wake prompts through an injected callback when the recipient is idle - only the recipient's host process delivers, so there is no cross-process double-delivery. Heartbeat staleness flags crashed sessions; a per-recipient nudge rate cap is the anti-loop hard brake. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
@@ -96,7 +116,9 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
96
116
|
|
|
97
117
|
## Plugin hooks
|
|
98
118
|
|
|
99
|
-
`
|
|
119
|
+
The v1 adapter (`src/opencode/v1.ts`) registers these opencode integration
|
|
120
|
+
points; the v2 adapter routes its domain hooks into the same shared runtime
|
|
121
|
+
methods. The v1 hook shapes:
|
|
100
122
|
|
|
101
123
|
| Hook | What it does |
|
|
102
124
|
|------|-------------|
|
|
@@ -128,8 +128,9 @@ Parameters: `behavior` (statement, semantic match at cosine >= 0.85),
|
|
|
128
128
|
|
|
129
129
|
### Default behavior seeding (`src/seed-behaviors.ts`)
|
|
130
130
|
|
|
131
|
-
Runs at startup in both the opencode plugin (`src/
|
|
132
|
-
server (`src/mcp.ts`), after DB and
|
|
131
|
+
Runs at startup in both the opencode plugin (`src/runtime.ts`, shared by
|
|
132
|
+
the v1 and v2 adapters) and the MCP server (`src/mcp.ts`), after DB and
|
|
133
|
+
model initialization.
|
|
133
134
|
|
|
134
135
|
Seeds multiple default behaviors into the global store — session wrap-up
|
|
135
136
|
(check for uncommitted changes, untracked files, stale artifacts), work
|
|
@@ -13,7 +13,7 @@ The CI gate is the never-merge-broken guard before a release. All three must pas
|
|
|
13
13
|
|
|
14
14
|
## Publish pipeline (.github/workflows/publish.yml)
|
|
15
15
|
|
|
16
|
-
Triggered by pushing a `v*` tag. Publishes to npm via OIDC trusted publishing — no stored npm token.
|
|
16
|
+
Triggered by pushing a `v*` tag. Publishes to npm via OIDC trusted publishing — no stored npm token. The tag push does not trigger ci.yml, so the publish workflow re-runs the full CI gate (typecheck, tests, markdownlint) across the same dual-Bun matrix as its own release gate.
|
|
17
17
|
|
|
18
18
|
Key details:
|
|
19
19
|
- `id-token: write` permission lets npm authenticate through GitHub's OIDC exchange
|
|
@@ -37,7 +37,7 @@ During summary generation, tools are blocked. A nudge that instructs the agent t
|
|
|
37
37
|
|
|
38
38
|
## Source files
|
|
39
39
|
|
|
40
|
-
- `src/
|
|
40
|
+
- `src/runtime.ts` -- all four hooks/events (experimental.session.compacting, experimental.compaction.autocontinue, session.compacted event, chat.message compaction guard on v1; session.hook("compaction")/("prompt") on v2), shared by both adapters
|
|
41
41
|
- `src/prompts.ts` -- compactionContext function
|
|
42
42
|
|
|
43
43
|
## Key invariants
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Cross-Session Chat
|
|
2
2
|
|
|
3
|
+
Unless noted, `client.*` names below are the opencode v1 mapping; the v2 equivalents and degrades are in [opencode-plugin.md](opencode-plugin.md).
|
|
4
|
+
|
|
3
5
|
Cross-session chat lets independent opencode sessions on one machine message
|
|
4
6
|
each other through thatch: sessions join a shared directory (automatically,
|
|
5
7
|
under assigned names), see each other in the list, and messages land in
|
|
@@ -394,8 +396,8 @@ them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
|
394
396
|
chat_name_counters and the auto column), the NOCASE collation migration
|
|
395
397
|
and topic column migration, delegated methods
|
|
396
398
|
- `src/tool-defs.ts` - the chat tool definitions
|
|
397
|
-
- `src/
|
|
398
|
-
registration (`osProcessArgs`, `startupSessionId`), idle
|
|
399
|
+
- `src/runtime.ts` - poller construction, delivery closure, `-s` startup
|
|
400
|
+
registration (`osProcessArgs`, `startupSessionId` from `src/os-args.ts`), idle
|
|
399
401
|
auto-registration + topic refresh, idle flush,
|
|
400
402
|
session.deleted unregister, dispose, transcript echo in
|
|
401
403
|
tool.execute.after
|
|
@@ -36,7 +36,7 @@ directly for conversation-derived knowledge.
|
|
|
36
36
|
After every tool execution, non-`thatch_*`, non-`skill`, non-`task` tool
|
|
37
37
|
calls are buffered for later extraction.
|
|
38
38
|
|
|
39
|
-
**opencode** — `tool.execute.after` hook in `src/
|
|
39
|
+
**opencode** — `tool.execute.after` hook (v1) / `tool.hook` (v2) in `src/runtime.ts`:
|
|
40
40
|
|
|
41
41
|
- In-memory ring buffer per session (`ExtractionPipeline` in
|
|
42
42
|
`src/extraction.ts`)
|
|
@@ -50,6 +50,14 @@ calls are buffered for later extraction.
|
|
|
50
50
|
itself; `skill`/`task` meta-tools would create a feedback loop (extraction
|
|
51
51
|
triggers a skill load, which gets buffered, which triggers another
|
|
52
52
|
extraction)
|
|
53
|
+
- Code Mode `execute` calls are **unwrapped before filtering**
|
|
54
|
+
(`unwrapExecuteThatchCalls` in `src/extraction.ts`): when the call's code
|
|
55
|
+
invokes `tools.thatch_*` tools, the wrapped tools' hook semantics run
|
|
56
|
+
(ack, drain, metrics) and the call itself is never buffered. Matching on
|
|
57
|
+
the outer tool name alone re-queued the pipeline's own dispatch/ack
|
|
58
|
+
traffic every cycle — each extraction run queued the next one, producing
|
|
59
|
+
an infinite dispatch/ack loop (observed live, September 2026). Execute
|
|
60
|
+
calls that touch no thatch tools buffer as normal.
|
|
53
61
|
|
|
54
62
|
**MCP hosts** — `bin/thatch`, `src/extract-queue.ts`:
|
|
55
63
|
|
|
@@ -234,7 +242,7 @@ continues working while the child extracts.
|
|
|
234
242
|
|------|----------------|
|
|
235
243
|
| `src/extraction.ts` | In-memory ring buffer (`ExtractionPipeline`), shared payload builders (`buildExtractionPayload`, `deriveTitle`, `summarizeArgs`) |
|
|
236
244
|
| `src/extract-queue.ts` | File-backed JSONL queue for MCP hosts |
|
|
237
|
-
| `src/
|
|
245
|
+
| `src/runtime.ts` | opencode hooks: `tool.execute.after`, `session.status` idle, `session.created`, `session.error`, `session.deleted`, `chat.message` (nudge tier 1) -- shared by the v1/v2 adapters |
|
|
238
246
|
| `bin/thatch` | `buffer-batch`, `buffer-tool`, `flush-tools` subcommands |
|
|
239
247
|
| `src/prompts.ts` | `extractionNudge` (with escalation), `extractionDirectPrompt` |
|
|
240
248
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Hygiene System
|
|
2
2
|
|
|
3
|
+
Unless noted, `client.*` names below are the opencode v1 mapping; the v2 equivalents and degrades are in [opencode-plugin.md](opencode-plugin.md).
|
|
4
|
+
|
|
3
5
|
Store maintenance signals surfaced at session start. The plugin never deletes
|
|
4
6
|
memories itself — it surfaces problems and lets the agent tend the store.
|
|
5
7
|
|
|
@@ -52,7 +54,7 @@ instructions for the agent to tend the store when convenient.
|
|
|
52
54
|
|
|
53
55
|
`hygieneReport(db, repo, worktree)` in `src/hygiene.ts` is shared by:
|
|
54
56
|
|
|
55
|
-
- The opencode plugin's session-start hook (`src/
|
|
57
|
+
- The opencode plugin's session-start hook (`src/runtime.ts`)
|
|
56
58
|
- The CLI's `thatch reminder` and `thatch hygiene` subcommands (`bin/thatch`)
|
|
57
59
|
|
|
58
60
|
## Interactions with other features
|
|
@@ -32,7 +32,7 @@ wrapping---prefixing, transport, session hooks---lives outside the core.
|
|
|
32
32
|
|
|
33
33
|
## The three paths
|
|
34
34
|
|
|
35
|
-
### opencode plugin (`src/
|
|
35
|
+
### opencode plugin (`src/opencode/v1.ts` + `src/opencode/v2.ts`)
|
|
36
36
|
|
|
37
37
|
Runs inside opencode's Bun runtime as a loaded plugin. No setup command is
|
|
38
38
|
needed---opencode discovers the plugin from `opencode.json` and loads it at
|
|
@@ -228,7 +228,9 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
|
228
228
|
|
|
229
229
|
| File | Role |
|
|
230
230
|
|------|------|
|
|
231
|
-
| `src/index.ts` |
|
|
231
|
+
| `src/index.ts` | dual-shape plugin entry (merged default export loads on opencode v1 and v2) |
|
|
232
|
+
| `src/opencode/v1.ts` + `src/opencode/v2.ts` | host adapters: hooks (v1) vs promise-context domains (v2) |
|
|
233
|
+
| `src/runtime.ts` | shared plugin runtime: nudges, system prompt injection, session events |
|
|
232
234
|
| `src/mcp.ts` | MCP server (shared by Claude Code and Cursor)---stdio JSON-RPC, tool dispatch |
|
|
233
235
|
| `src/setup.ts` | Setup installer for Claude Code and Cursor---markers, hooks, skills |
|
|
234
236
|
| `src/prompts.ts` | All three system prompt variants---`systemPrompt()`, `claudeInstructions()`, `cursorInstructions()` |
|
|
@@ -5,19 +5,28 @@ hand-editable user config file. Both are plain child processes and file I/O,
|
|
|
5
5
|
so the feature works identically on every host -- opencode, Claude Code, and
|
|
6
6
|
Cursor -- with no host-specific plumbing and no new npm dependencies.
|
|
7
7
|
|
|
8
|
+
Two delivery paths share the dispatcher and the config file:
|
|
9
|
+
|
|
10
|
+
- **Agent-initiated**: the LLM calls `notify_user`. Works on every host.
|
|
11
|
+
- **Automatic alerts** (opencode-only): the plugin runtime watches session
|
|
12
|
+
events and notifies when the LLM pauses for human input or finishes a
|
|
13
|
+
round of real work. See the Alerts section below and
|
|
14
|
+
`docs/plans/llm-alerts.md` for the design decisions.
|
|
15
|
+
|
|
8
16
|
## Source files
|
|
9
17
|
|
|
10
18
|
| File | Role |
|
|
11
19
|
|------|------|
|
|
12
|
-
| `src/config.ts` | Zod section schemas, config file load/save/merge, platform defaults
|
|
20
|
+
| `src/config.ts` | Zod section schemas, config file load/save/merge, platform defaults, `alertMode`. |
|
|
13
21
|
| `src/notify.ts` | Per-platform dispatch for banner + voice, with an injectable spawner. |
|
|
14
22
|
| `src/tool-defs.ts` | `config_get`, `config_set`, `notify_user` tool definitions (shared registry, non-opencodeOnly). |
|
|
23
|
+
| `src/alerts.ts` | Alert state machine: pause/done/error decisions from session events, per-session dedup (no timer-based debounce - one alert per busy->idle transition, per request id for pauses). |
|
|
15
24
|
|
|
16
25
|
## Config design
|
|
17
26
|
|
|
18
27
|
The config is one JSON file at `~/.config/thatch/config.json`, placed next to
|
|
19
28
|
`thatch.db` by deriving the path from `THATCH_DB_PATH` (or the XDG default the
|
|
20
|
-
same way `
|
|
29
|
+
same way `runtime.ts` and `mcp.ts` derive the DB path). Tests pass an explicit
|
|
21
30
|
dbPath to keep writes inside a tempdir.
|
|
22
31
|
|
|
23
32
|
Key decisions:
|
|
@@ -81,13 +90,110 @@ Details that matter:
|
|
|
81
90
|
reporting stays honest. There is no debounce; polling agents notify once at
|
|
82
91
|
the end.
|
|
83
92
|
|
|
93
|
+
## Alerts: the automatic notification path (opencode only)
|
|
94
|
+
|
|
95
|
+
`src/alerts.ts` holds an in-memory state machine per session. The plugin
|
|
96
|
+
runtime (both the v1 and v2 adapters feed it the same event stream) drives it
|
|
97
|
+
from the shared `onEvent` hook; delivery reuses `sendNotification` with the
|
|
98
|
+
`alerts` config section choosing the channel per event (`alertMode`, default
|
|
99
|
+
`banner`). Delivery is fire-and-forget (a spoken alert awaits the sentence
|
|
100
|
+
for seconds and must never stall the event pump); the machine owns the
|
|
101
|
+
per-event `none` gate and the per-delivery config re-read is what makes
|
|
102
|
+
config changes apply without a restart.
|
|
103
|
+
|
|
104
|
+
Events consumed:
|
|
105
|
+
|
|
106
|
+
| Event | Machine input |
|
|
107
|
+
|-------|---------------|
|
|
108
|
+
| `question.asked` / `permission.asked` | Pause alert, deduped per request id; cleared by the matching replied/rejected event. |
|
|
109
|
+
| `session.status` busy | Marks the session active and resets the round. |
|
|
110
|
+
| `session.status` retry (v1 only) | Clears a recorded error -- opencode recovered on its own -- but preserves the real-work flag (the tool calls before the retry happened). |
|
|
111
|
+
| `session.error` / `session.execution.failed` | Records the error name; on v2 the adapter emits this paired with the idle event, so the verdict always follows. |
|
|
112
|
+
| `session.status` idle (from active) | The verdict: error alert, done alert, or silence. A recorded error delivers its verdict WITHOUT the message fetch (the fetch rides the same host API whose failure plausibly accompanied the failure). |
|
|
113
|
+
| `session.deleted` | Drops the state. |
|
|
114
|
+
|
|
115
|
+
The verdict combines two sources, because neither alone can answer "was
|
|
116
|
+
this a real round of work the user asked for?":
|
|
117
|
+
|
|
118
|
+
- **The tool side is live bookkeeping, not message archaeology.** The
|
|
119
|
+
runtime calls `sessionRealWork` at exactly the extraction buffer's push
|
|
120
|
+
site -- the buffer's filter is the one classification of "this call was
|
|
121
|
+
not bookkeeping" (`isMetaToolName`: `thatch_*`, `skill`, `task`,
|
|
122
|
+
`agent`, `subagent`; execute-wrapped thatch calls are unwrapped and never
|
|
123
|
+
reach the push). Message parts do not carry tool arguments, so the
|
|
124
|
+
execute-unwrap cannot be redone from messages. `todowrite` and `question`
|
|
125
|
+
reach the push (the buffer wants their facts) but are excluded from the
|
|
126
|
+
alert signal -- see `isNoWorkTool` in `src/alerts.ts`; they deliberately
|
|
127
|
+
do NOT live in `isMetaToolName`, and moving them there would silently
|
|
128
|
+
stop the buffer from queueing them.
|
|
129
|
+
- **The trigger side comes from the message list** via the pure
|
|
130
|
+
`deriveRoundShape` (exported from `src/alerts.ts`, fed
|
|
131
|
+
`HostCapabilities.sessionMessages` output). The round delimiter is v2's
|
|
132
|
+
own idle marker: v2 appends a `{type: "idle"}` marker message at every
|
|
133
|
+
turn end (`SessionMessage.Idle`), so the round is everything after the
|
|
134
|
+
previous marker -- the host's turn definition, immune to mid-round
|
|
135
|
+
steers. V1 has no markers and falls back to "newest non-assistant message
|
|
136
|
+
triggered the round" (a mid-round steer under-classifies there; accepted
|
|
137
|
+
v1 limitation).
|
|
138
|
+
|
|
139
|
+
Silence rules, in verdict order:
|
|
140
|
+
|
|
141
|
+
- **Unknown shape** -- a failed or empty message fetch stays silent.
|
|
142
|
+
Alerts are best-effort: a spurious banner on every broken fetch costs
|
|
143
|
+
more than a rare missed completion, and the same fetch failing also
|
|
144
|
+
breaks the wrap-up greenlight check, so the session degrades
|
|
145
|
+
consistently. This rule also keeps adapter-level tests (whose client
|
|
146
|
+
mocks have no message surface) from ever spawning a real banner.
|
|
147
|
+
- **Synthetic-trigger silence** -- nudges, background-task completions, and
|
|
148
|
+
watcher wake-ups are synthetic deliveries (v2: their own message kind; v1:
|
|
149
|
+
all-synthetic text parts on the user message). A round they triggered
|
|
150
|
+
never fires the DONE alert, even when it does real work. It is a silence
|
|
151
|
+
on the done verdict only: a synthetic round that ends in a real error
|
|
152
|
+
still banners the error. The rule exists because async thatch activity
|
|
153
|
+
would otherwise produce a banner every time.
|
|
154
|
+
- **Abort and shutdown silence** -- an error name matching
|
|
155
|
+
/abort|shutdown|interrupt/i means the user interrupted the turn
|
|
156
|
+
themselves (`MessageAbortedError`, or v2's user-reason interrupt
|
|
157
|
+
translation), the process is leaving (`SessionShutdownError`), or the
|
|
158
|
+
execution was superseded by a newer one (the catch-all for unknown
|
|
159
|
+
interrupt reasons). Sources: the event-level error, the message-level
|
|
160
|
+
error on the last assistant message, and v2's interrupted-reason
|
|
161
|
+
translation.
|
|
162
|
+
- **Error alert** -- any other recorded error (event-level, or message-level
|
|
163
|
+
on the last assistant message) notifies needs-attention with the error
|
|
164
|
+
name in the message, instead of done. Notably `SessionInactivityError`:
|
|
165
|
+
v2's location-activity sweeper interrupts still-executing sessions when a
|
|
166
|
+
project directory goes quiet for an hour -- the one banner most likely to
|
|
167
|
+
reach Jeff away from the terminal.
|
|
168
|
+
- **No real work** -- a round whose only tool calls were bookkeeping (or
|
|
169
|
+
that made none, or whose tool executions all crashed at the host layer)
|
|
170
|
+
stays silent. A round delegated entirely to subagents also stays silent:
|
|
171
|
+
dispatch tools are meta, and child sessions never alert.
|
|
172
|
+
|
|
173
|
+
State is plain memory, deliberately not journaled to `runtime_state`: a v2
|
|
174
|
+
reload mid-turn (plugin save, upgrade) loses that turn's busy->idle
|
|
175
|
+
transition and stays silent. Accepted by design -- the PR #16 journaling
|
|
176
|
+
exists because extraction and wrap-up state corrupt sessions when lost, and
|
|
177
|
+
an alert cannot corrupt anything.
|
|
178
|
+
|
|
179
|
+
Subagent child sessions never alert: the runtime's idle branch handles
|
|
180
|
+
children (extraction completion) and returns before the alert verdict, and
|
|
181
|
+
child errors requeue extraction instead. Pause asks from children still
|
|
182
|
+
notify -- a subagent blocking on a question blocks the parent too.
|
|
183
|
+
|
|
184
|
+
Deduplication across TUIs is structural: the runtime is one instance per
|
|
185
|
+
project directory in the shared server regardless of how many TUIs are
|
|
186
|
+
attached, so per-session state is exact by construction. Delivering from the
|
|
187
|
+
TUI side (opencode's attention API) would fire once per attached TUI and was
|
|
188
|
+
rejected for that reason (plus v1 has no TUI plugin surface at all).
|
|
189
|
+
|
|
84
190
|
## Prompt surface
|
|
85
191
|
|
|
86
192
|
- `systemPrompt()` and `mcpInstructions()` list the three new tools and carry
|
|
87
193
|
a short "Notifications" section: when to notify, the source-label rule, and
|
|
88
194
|
the config_get-before-config_set instruction.
|
|
89
195
|
- `notify_user` is not `opencodeOnly`: it needs no host capabilities, so MCP
|
|
90
|
-
hosts get it too. Nothing was added to `src/tools.ts` or
|
|
196
|
+
hosts get it too. Nothing was added to `src/tools.ts` or the plugin runtime.
|
|
91
197
|
|
|
92
198
|
## Tests
|
|
93
199
|
|
|
@@ -99,8 +205,19 @@ Details that matter:
|
|
|
99
205
|
platform, plus tool-level tests (merge-no-wipe, echo, `mode: none` no-op,
|
|
100
206
|
configured voice honored) with `THATCH_DB_PATH` pointed at a tempdir.
|
|
101
207
|
- `tests/tool-defs.test.ts` -- registry count and name list (hardcoded literals that fail loudly when the surface changes).
|
|
208
|
+
- `tests/alerts.test.ts` -- the alert state machine: `deriveRoundShape`
|
|
209
|
+
(idle-marker delimiter, v1 fallback, synthetic trigger detection), the
|
|
210
|
+
real-work flag, pause dedup and re-arm, abort/shutdown silence,
|
|
211
|
+
error-with-name, retry recovery, `mode: none` per event, config
|
|
212
|
+
round-trip (isolated tempdir).
|
|
102
213
|
|
|
103
214
|
## QA coverage
|
|
104
215
|
|
|
105
216
|
`tests/qa/auto/uc-059-tool-prefixing.ts` asserts the full bare-name list; the
|
|
106
|
-
three new names are in its `expected` array.
|
|
217
|
+
three new names are in its `expected` array. `uc-108-llm-alerts.ts` drives
|
|
218
|
+
the full alert event flow against a spy notifier, including the adapter
|
|
219
|
+
seam: raw v2 bus events through `translateEvent` and real-shaped
|
|
220
|
+
`session.context` lists through `mapSessionContextMessages` +
|
|
221
|
+
`deriveRoundShape` (the seam where the idle-marker bug hid). A live
|
|
222
|
+
banner-by-ear check remains a manual verification against a running
|
|
223
|
+
session.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Per-Message Nudge Pipeline
|
|
2
2
|
|
|
3
|
+
Unless noted, `client.*` names below are the opencode v1 mapping; the v2 equivalents and degrades are in [opencode-plugin.md](opencode-plugin.md).
|
|
4
|
+
|
|
3
5
|
Runs on every user message. Four priority tiers share one embedding
|
|
4
6
|
computation. The pipeline injects *synthetic parts*---text the model sees in
|
|
5
7
|
the conversation but the user does not see in the TUI. This is the inverse of
|
|
@@ -35,7 +37,7 @@ opencode path runs in-process with direct event hooks. The MCP path runs via
|
|
|
35
37
|
external CLI hook processes that communicate with the long-lived MCP server
|
|
36
38
|
through the sideband socket.
|
|
37
39
|
|
|
38
|
-
### opencode path (`src/
|
|
40
|
+
### opencode path (`src/runtime.ts`, `onChatMessage` - v1 `chat.message` hook, v2 `session.hook("prompt")`)
|
|
39
41
|
|
|
40
42
|
#### Compaction guard
|
|
41
43
|
|
|
@@ -265,7 +267,7 @@ TUI display. The opencode framework's history serializer filters on
|
|
|
265
267
|
|
|
266
268
|
| File | Role |
|
|
267
269
|
|------|------|
|
|
268
|
-
| `src/
|
|
270
|
+
| `src/runtime.ts` | opencode: per-message nudges (all 4 tiers), compaction guard, extraction fallback |
|
|
269
271
|
| `bin/thatch` | MCP: `flush-tools` and `flush-predictions` subcommands |
|
|
270
272
|
| `src/sideband.ts` | MCP: sideband client helpers (`sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`) |
|
|
271
273
|
| `src/prompts.ts` | All nudge formatting functions (`recallNudge`, `claudeRecallNudge`, `predictionNudge`, `behaviorNudge`, `extractionNudge`, `claudeWriteNudge`) |
|