@jeffober/thatch 0.2.5 → 0.2.6
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 +48 -25
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +16 -8
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- package/artifacts/skills/thatch-refine-personal.md +5 -0
- package/artifacts/skills/thatch-refine.md +7 -1
- package/artifacts/skills/thatch-review-context.md +55 -2
- package/artifacts/skills/thatch-review-followup.md +4 -4
- package/artifacts/skills/thatch-review-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +21 -1
- package/bin/thatch +21 -12
- package/docs/dev/README.md +11 -8
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +2 -1
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +85 -21
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +41 -15
- package/docs/dev/features/prediction-engine.md +27 -1
- package/docs/dev/features/qa-system.md +19 -3
- package/docs/dev/features/session-lifecycle.md +6 -6
- package/docs/dev/features/session-tabs.md +11 -5
- package/docs/dev/gotchas.md +21 -0
- package/docs/dev/mcp-parity.md +3 -3
- package/docs/dev/skills.md +11 -8
- package/docs/user/README.md +19 -12
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +2 -1
- package/docs/user/code-review.md +43 -2
- package/docs/user/commands.md +27 -7
- package/docs/user/cross-session-chat.md +47 -20
- package/docs/user/prediction-engine.md +12 -1
- package/docs/user/skills.md +8 -6
- package/package.json +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +39 -17
- package/src/commands.ts +109 -18
- package/src/db.ts +23 -2
- package/src/mcp.ts +6 -2
- package/src/opencode/tui-plugin.ts +29 -9
- package/src/opencode/v2.ts +247 -66
- package/src/prompts.ts +54 -6
- package/src/runtime.ts +122 -17
- package/src/scoring-engine.ts +40 -9
- package/src/session-tab-shared.ts +55 -4
- package/src/skills.ts +5 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +42 -21
package/bin/thatch
CHANGED
|
@@ -13,11 +13,12 @@ import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscript
|
|
|
13
13
|
import { chatTailDiff, chatTailBacklog, filterChatTailRows, formatChatTailJsonl, CHAT_TAIL_DEFAULT_LIMIT, parseChatTimeBound, CHAT_STALE_MS, CHAT_RENUDGE_MINUTES, chatLiveness, humanAge, splitChatRoster, sortChatRoster, mcpSessionID, isoMinutesAgo, renderChatParticipant, resolveRegisteredPredecessor, type ChatSessionRow, type ChatTailFilter } from "../src/chat";
|
|
14
14
|
import { chatEnabled, loadConfig } from "../src/config";
|
|
15
15
|
import { chatNotificationNudge } from "../src/prompts";
|
|
16
|
+
import { chatOpenTabs as chatOpenTabsProvider } from "../src/tab-state";
|
|
16
17
|
import { createDebugLog } from "../src/debug";
|
|
17
18
|
|
|
18
19
|
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
19
20
|
const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
|
|
20
|
-
const db = new ThatchDB(dbPath);
|
|
21
|
+
const db: ThatchDB = new ThatchDB(dbPath, { chatOpenTabs: chatOpenTabsProvider(() => db.chatTabClosures()) });
|
|
21
22
|
|
|
22
23
|
const [command, ...args] = process.argv.slice(2);
|
|
23
24
|
|
|
@@ -427,7 +428,9 @@ const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "T
|
|
|
427
428
|
* Renders the `chat list` roster as two sections - Active and Stale - each
|
|
428
429
|
* an aligned-column table under a header row. Section headers are bold,
|
|
429
430
|
* the stale explainer is muted italic; rows use green names, dim age,
|
|
430
|
-
* green/red status, and a muted italic topic.
|
|
431
|
+
* green/red status, and a muted italic topic. The STATUS column carries
|
|
432
|
+
* the liveness verdict: open (tab open now), recent (host alive, no open
|
|
433
|
+
* tab), active/idle (MCP rows), stale (host presumed gone).
|
|
431
434
|
* staleCapMs bounds how old a stale row is displayed (null = no bound);
|
|
432
435
|
* rows past the cap collapse into a muted hidden-count note so the reader
|
|
433
436
|
* knows rows exist that the window left out.
|
|
@@ -435,7 +438,7 @@ const CHAT_ROSTER_HEADER = ["NAME", "SESSION ID", "AGE", "STATUS", "PROJECT", "T
|
|
|
435
438
|
* NO_COLOR is unset), so piped output - grep, redirect, QA runners - stays
|
|
436
439
|
* clean plain text.
|
|
437
440
|
*/
|
|
438
|
-
function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null): string {
|
|
441
|
+
function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null, openTabs?: ReadonlySet<string>): string {
|
|
439
442
|
const colored = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
440
443
|
const paint = (code: string, s: string) => (colored ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
441
444
|
const { active, stale, staleHidden } = splitChatRoster(sortChatRoster(sessions), Date.now(), staleCapMs);
|
|
@@ -444,7 +447,7 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
444
447
|
r.name,
|
|
445
448
|
r.session_id,
|
|
446
449
|
humanAge(r.last_seen),
|
|
447
|
-
chatLiveness(r),
|
|
450
|
+
chatLiveness(r, Date.now(), openTabs),
|
|
448
451
|
r.project ?? "unknown",
|
|
449
452
|
r.topic ?? "",
|
|
450
453
|
];
|
|
@@ -455,11 +458,14 @@ function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null
|
|
|
455
458
|
const header = align(CHAT_ROSTER_HEADER).map((h) => paint("1", h)).join(" ");
|
|
456
459
|
const body = rows.map((r) => {
|
|
457
460
|
const [name, sessionID, age, status, project, topic] = align(cells(r));
|
|
461
|
+
// Green = present or reachable (open/active), yellow = alive host
|
|
462
|
+
// without a tab (recent, wake not guaranteed), red = stale.
|
|
463
|
+
const statusColor = status === "stale" ? "31" : status === "recent" ? "33" : "32";
|
|
458
464
|
return [
|
|
459
465
|
paint("32", name),
|
|
460
466
|
paint("2", sessionID),
|
|
461
467
|
paint("2", age),
|
|
462
|
-
paint(
|
|
468
|
+
paint(statusColor, status),
|
|
463
469
|
project,
|
|
464
470
|
topic ? paint("3;90", topic) : "",
|
|
465
471
|
].join(" ").trimEnd();
|
|
@@ -514,8 +520,11 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
514
520
|
process.exit(1);
|
|
515
521
|
} else staleCapMs = Number(staleNext) * 24 * 60 * 60 * 1000;
|
|
516
522
|
}
|
|
523
|
+
// The open-tab set is read once per invocation at render time: rows
|
|
524
|
+
// with a tab render "open", fresh rows without one "recent".
|
|
525
|
+
const openTabs = chatOpenTabsProvider(() => db.chatTabClosures())();
|
|
517
526
|
let sessions = db.listChatSessions();
|
|
518
|
-
if (!showStale) sessions = sessions.filter((r) => chatLiveness(r)
|
|
527
|
+
if (!showStale) sessions = sessions.filter((r) => chatLiveness(r, Date.now(), openTabs) !== "stale");
|
|
519
528
|
if (sessions.length === 0) {
|
|
520
529
|
console.log(
|
|
521
530
|
showStale
|
|
@@ -524,7 +533,7 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
524
533
|
);
|
|
525
534
|
break;
|
|
526
535
|
}
|
|
527
|
-
console.log(formatChatRoster(sessions, showStale ? staleCapMs : null));
|
|
536
|
+
console.log(formatChatRoster(sessions, showStale ? staleCapMs : null, openTabs));
|
|
528
537
|
break;
|
|
529
538
|
}
|
|
530
539
|
|
|
@@ -1050,7 +1059,7 @@ switch (command) {
|
|
|
1050
1059
|
const sessionID = data.session_id ?? data.conversation_id;
|
|
1051
1060
|
|
|
1052
1061
|
const { peekQueue, getMissedCount, incrementMissedCount } = await import("../src/extract-queue");
|
|
1053
|
-
const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge } = await import("../src/prompts");
|
|
1062
|
+
const { extractionNudge, claudeWriteNudge, claudeRecallNudge, predictionNudge, behaviorNudge, mcpToolName } = await import("../src/prompts");
|
|
1054
1063
|
const { detectSkew, readNpmCacheForUpdate } = await import("../src/version-check");
|
|
1055
1064
|
|
|
1056
1065
|
let repo = "unknown";
|
|
@@ -1144,10 +1153,10 @@ switch (command) {
|
|
|
1144
1153
|
parts.push(claudeRecallNudge(matches));
|
|
1145
1154
|
}
|
|
1146
1155
|
if (predictions && predictions.length > 0) {
|
|
1147
|
-
parts.push(predictionNudge(predictions));
|
|
1156
|
+
parts.push(predictionNudge(predictions, mcpToolName));
|
|
1148
1157
|
}
|
|
1149
1158
|
if (behaviors && behaviors.length > 0) {
|
|
1150
|
-
parts.push(behaviorNudge(behaviors));
|
|
1159
|
+
parts.push(behaviorNudge(behaviors, mcpToolName));
|
|
1151
1160
|
}
|
|
1152
1161
|
|
|
1153
1162
|
if (parts.length > 0) {
|
|
@@ -1296,13 +1305,13 @@ switch (command) {
|
|
|
1296
1305
|
if (repo === "unknown") repo = "global";
|
|
1297
1306
|
|
|
1298
1307
|
const { sidebandPredictions, sidebandSocketPath } = await import("../src/sideband");
|
|
1299
|
-
const { predictionNudge } = await import("../src/prompts");
|
|
1308
|
+
const { predictionNudge, mcpToolName } = await import("../src/prompts");
|
|
1300
1309
|
const sockPath = sidebandSocketPath(dbPath);
|
|
1301
1310
|
const threshold = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.60");
|
|
1302
1311
|
const predictions = await sidebandPredictions(sockPath, promptText, [repo, "global"], threshold, 5);
|
|
1303
1312
|
|
|
1304
1313
|
if (predictions && predictions.length > 0) {
|
|
1305
|
-
const output = predictionNudge(predictions);
|
|
1314
|
+
const output = predictionNudge(predictions, mcpToolName);
|
|
1306
1315
|
if (isJson) console.log(JSON.stringify({ additional_context: output }));
|
|
1307
1316
|
else console.log(output);
|
|
1308
1317
|
} else {
|
package/docs/dev/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Shared core
|
|
|
27
27
|
├── git.ts → detect repo identity (store name) + worktree-deletion recovery (repo_paths cache)
|
|
28
28
|
├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
|
|
29
29
|
├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, prompt cores, CLAUDE.md instructions
|
|
30
|
-
├── commands.ts → /thatch slash-command registry: actions
|
|
30
|
+
├── commands.ts → /thatch slash-command registry: actions (prompt cores + self-contained skill couchings) + wrap-ups, per-host sync
|
|
31
31
|
├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
|
|
32
32
|
├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
|
|
33
33
|
├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
|
|
@@ -91,7 +91,7 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
91
91
|
| `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. |
|
|
92
92
|
| `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. v2 also wires the session-tab host flow (create -> move -> chat-register -> rpc emit -> prompt) through the `CoreContext.sessionTabHost` seam. |
|
|
93
93
|
| `opencode/tui-plugin.ts` | The v2 TUI CLI plugin (the package's `./tui` entrypoint): listens for `rpc.thatch-tabs.tab-opened` and `rpc.thatch-tabs.tab-closed` events and opens/closes the subordinate session's tab via `ui.tabs.open`/`ui.tabs.close`. See [features/session-tabs.md](features/session-tabs.md). |
|
|
94
|
-
| `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. |
|
|
94
|
+
| `capabilities.ts` | The `HostCapabilities` seam: the host operations the shared runtime needs (session create/prompt/get/list/messages/delete, toast, compaction trigger, session close-out - app exit on v1, the session's own tab on v2). `capabilitiesFromClient` bridges the v1 SDK client. |
|
|
95
95
|
| `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. |
|
|
96
96
|
| `os-args.ts` | Pure argv / OS-command-line helpers for startup session resolution (`-s`/`-c`). SDK-free. |
|
|
97
97
|
| `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. |
|
|
@@ -108,10 +108,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
108
108
|
| `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. |
|
|
109
109
|
| `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). |
|
|
110
110
|
| `chat-names.ts` | The static display-name pool for the chat directory: whimsical geek-culture names in fnord's Nomenclater style, baked in so name assignment never costs a model call. The pool is the fallback base for registrations with no usable session title. |
|
|
111
|
+
| `tab-state.ts` | Read-only open-tab reader for the chat roster: globs every opencode channel's `tabs.json` (global + cwd scopes), subtracts fresh confirmed tab closes, and composes the provider the roster surfaces and broadcast targeting consume. Degrades to an empty set - never a false OPEN. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
111
112
|
| `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
|
|
112
113
|
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), watcher notification (`watcherNotificationNudge`), version warning (`versionWarningNudge`), chat wake notification (`chatNotificationNudge`), chat transcript echo text and echo recognizer (`chatEchoText` / `isChatEchoParts`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. Also the prompt cores (`defragCore`, `hygieneCore`, `reflectCore`, `extractionCore`) - host-agnostic behavior instruction bodies shared by nudges and `/thatch` commands, parameterized by a `ToolNamer` for per-host tool spellings. See [features/commands.md](features/commands.md). |
|
|
113
|
-
| `commands.ts` | Slash-command registry and sync. `actionDefs(tool)` renders the on-demand actions (defrag, extract, hygiene, reflect) from the prompt cores; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (`installOpencodeCommands`, synced by the plugin at init) and Claude Code (`installClaudeCommands`, synced by setup); `compilePrompts` in `mcp.ts` exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See [features/commands.md](features/commands.md). |
|
|
114
|
-
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (
|
|
114
|
+
| `commands.ts` | Slash-command registry and sync. `actionDefs(tool)` renders the on-demand actions: the nudge-backed ones (defrag, extract, hygiene, reflect) from the prompt cores, and the self-contained ones (refine, whois, and the review-mode couchings peer-review, self-review) inline in the registry; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (`installOpencodeCommands`, synced by the plugin at init) and Claude Code (`installClaudeCommands`, synced by setup); `compilePrompts` in `mcp.ts` exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See [features/commands.md](features/commands.md). |
|
|
115
|
+
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (everything that works on all three hosts: the memory workflows, the review specialists — whose roster comes from `REVIEW_SPECIALISTS` in prompts.ts — plus the synthesizer, context, followup, and response skills, the walkthrough and archaeology skills, the refinement family, the writing skills, and verification/export) and `OPENCODE_ONLY_SKILLS` (the code-review coordinator and thatch-coordination — they need sub-agent support, so they are not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
|
|
115
116
|
| `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
|
|
116
117
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
117
118
|
| `behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names. |
|
|
@@ -129,7 +130,7 @@ methods. The v1 hook shapes:
|
|
|
129
130
|
|------|-------------|
|
|
130
131
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
131
132
|
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
132
|
-
| `command.execute.before` | Arms the wrap-up greenlight check: `/thatch/compact` and `/thatch/exit` mark their session in `pendingWrapUp`. The next `session.status` idle resolves it by inspecting the final assistant message for the greenlight token and, when present, triggering the
|
|
133
|
+
| `command.execute.before` | Arms the wrap-up greenlight check: `/thatch/compact` and `/thatch/exit` mark their session in `pendingWrapUp`. The next `session.status` idle resolves it by inspecting the final assistant message for the greenlight token and, when present, triggering the host action (compact: v1 `session_compact` via `executeCommand`, v2 `session.compact` on the promise domain; exit: record the session's watcher deaths, unregister it from chat, then v1 `app.exit` via `publish` or v2 the exit-tab-closed rpc event closing the session's own tab). See [features/session-lifecycle.md](features/session-lifecycle.md). |
|
|
133
134
|
| `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
|
|
134
135
|
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. For `thatch_chat_*` tool calls, builds the transcript echo (`chatEchoText`) and posts it as a visible non-synthetic `noReply` part. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
135
136
|
| `chat.message` | Recall nudge: embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges fire independently in separate try/catch blocks with separate synthetic parts. There is deliberately NO extraction nudge here - extraction is plugin-driven at session idle (see the extraction feature doc); the old model-driven handshake raced its own state machine and was removed. Chat transcript echoes (non-synthetic `noReply` parts whose text is entirely `[chat]`-prefixed bubbles) are skipped before any tier — no model turn reads them, so nudging them wastes an embedding. Skipped entirely while the session is compacting (tool calls are blocked during summary generation), and for task-dispatched sub-agent sessions (`childToParent` without `extractionChildren`) - their tool lists may exclude the thatch tools and their work is driven by the dispatch prompt. |
|
|
@@ -268,14 +269,16 @@ extraction cycle
|
|
|
268
269
|
→ MCP path (Claude Code/Cursor): unchanged — no SDK client, no child
|
|
269
270
|
sessions; extract-queue.ts + flush-tools drives the nudge via hooks
|
|
270
271
|
|
|
271
|
-
toast notifications (opencode-only, TUI-rendered
|
|
272
|
+
toast notifications (opencode-only, TUI-rendered; the bridge's toast
|
|
273
|
+
event -> the TUI plugin's ui.toast.show on v2)
|
|
272
274
|
→ client.tui.showToast — best-effort, silently ignored if TUI not
|
|
273
275
|
connected (headless mode); model-invisible (goes to the user only,
|
|
274
276
|
not the conversation history — the inverse of synthetic nudge parts,
|
|
275
277
|
which are model-visible but TUI-hidden)
|
|
276
278
|
→ extraction metrics: `[thatch] new: N, updated: M, deleted: K` (success
|
|
277
|
-
variant, 4s) — fires when an extraction child goes idle
|
|
278
|
-
|
|
279
|
+
variant, 4s) — fires when an extraction child goes idle, but ONLY when
|
|
280
|
+
memories were actually written; no-save runs show nothing (no
|
|
281
|
+
notification fatigue)
|
|
279
282
|
→ recall matches: `[thatch] recalled N memories` (info variant, 3s) —
|
|
280
283
|
fires when chat.message matches stored memories
|
|
281
284
|
→ prediction matches: `[thatch] N predictions surfaced` (info variant,
|
|
@@ -23,7 +23,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
23
23
|
| [behavior-engine.md](behavior-engine.md) | Behavior engine (LLM self-discipline rules, ham/spam, auto-fire) |
|
|
24
24
|
| [hygiene.md](hygiene.md) | Hygiene system (duplicate, stale, orphaned branch signals) |
|
|
25
25
|
| [deduplication.md](deduplication.md) | Deduplication system (find pairs, verdict lifecycle) |
|
|
26
|
-
| [commands.md](commands.md) | Slash commands (on-demand actions
|
|
26
|
+
| [commands.md](commands.md) | Slash commands (on-demand actions: prompt cores and skill couchings, wrap-ups, host sync) |
|
|
27
27
|
| [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
|
|
28
28
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
29
29
|
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking, wrap-up commands) |
|
|
@@ -113,6 +113,8 @@ Parameters: `behavior` (statement, semantic match at cosine >= 0.85),
|
|
|
113
113
|
- Injects `behaviorNudge(items)` as a separate synthetic text part with a
|
|
114
114
|
`[thatch] Situational behaviors` header.
|
|
115
115
|
- 0-evidence behaviors use "consider"; behaviors with evidence use "do".
|
|
116
|
+
- The block ends with a snapshot footer (same as the prediction nudge): the
|
|
117
|
+
labels are a submission-time snapshot, not a live view.
|
|
116
118
|
- Independent try/catch — a behavior failure does not block recall or
|
|
117
119
|
prediction.
|
|
118
120
|
- Toast: `[thatch] N behaviors surfaced`.
|
|
@@ -250,6 +252,14 @@ behavior_provenance(
|
|
|
250
252
|
)
|
|
251
253
|
```
|
|
252
254
|
|
|
255
|
+
Timestamp semantics: a matcher's `updated_at` is the last change to the
|
|
256
|
+
matcher row OR its edge set — `createEdge` bumps it when a new edge lands
|
|
257
|
+
(atomically with the insert), and deleting an item bumps the matchers whose
|
|
258
|
+
edges cascade away (atomically with the delete). The matcher's own text may
|
|
259
|
+
legitimately be older than its links (a corrected behavior re-links the
|
|
260
|
+
existing matcher without touching its text). A behavior's `updated_at` is
|
|
261
|
+
written at creation and then moved by the last confidence change.
|
|
262
|
+
|
|
253
263
|
## Key invariants
|
|
254
264
|
|
|
255
265
|
1. **The firewall principle does NOT apply.** The LLM grading its own
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -51,7 +51,8 @@ Read-only window on the cross-session chat directory ([cross-session-chat.md](cr
|
|
|
51
51
|
(`formatChatRoster()` in `bin/thatch`; pipes and QA runners get plain
|
|
52
52
|
text). Rows are split into Active and Stale sections by
|
|
53
53
|
`splitChatRoster()`; the status column is `chatLiveness()`'s verdict
|
|
54
|
-
(`
|
|
54
|
+
(`open`/`recent`/`stale` for opencode rows - the open-tab reader feeds
|
|
55
|
+
the open/recent split - `active`/`idle` for MCP rows).
|
|
55
56
|
- `chat tail` follows the message stream as JSONL: one `ChatTailEvent`
|
|
56
57
|
per line via `formatChatTailJsonl()` (a bare `JSON.stringify`; no ANSI,
|
|
57
58
|
no markdown rendering, no local-time conversion - the tail is a log for
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Thatch's `/thatch/*` commands come in two families:
|
|
4
4
|
|
|
5
|
-
- **Actions** (`defrag`, `extract`, `hygiene`, `
|
|
6
|
-
|
|
5
|
+
- **Actions** (`defrag`, `extract`, `hygiene`, `peer-review`, `refine`,
|
|
6
|
+
`reflect`, `self-review`, `whois`) — user-invoked commands. The
|
|
7
|
+
nudge-backed ones (`defrag`, `extract`, `hygiene`, `reflect`) wrap
|
|
8
|
+
behaviors the nudges otherwise run on their own schedule; the rest
|
|
9
|
+
are self-contained, with no nudge counterpart.
|
|
7
10
|
- **Wrap-ups** (`compact`, `exit`) — greenlight-gated pre-compaction and
|
|
8
11
|
pre-exit checklists. opencode only: the greenlight check needs the
|
|
9
12
|
plugin's `command.execute.before` arming and TUI control routes.
|
|
@@ -12,11 +15,12 @@ User-facing guide: [../../user/commands.md](../../user/commands.md).
|
|
|
12
15
|
|
|
13
16
|
## Prompt cores
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
(`defragCore`, `hygieneCore`, `reflectCore`,
|
|
17
|
-
host-agnostic instruction text for one
|
|
18
|
-
the same cores with escalation tiers
|
|
19
|
-
and prompt renderers wrap them with
|
|
18
|
+
The nudge-backed action bodies are **prompt cores**: constants in
|
|
19
|
+
`src/prompts.ts` (`defragCore`, `hygieneCore`, `reflectCore`,
|
|
20
|
+
`extractionCore`) that hold the host-agnostic instruction text for one
|
|
21
|
+
behavior. The nudge envelopes wrap the same cores with escalation tiers
|
|
22
|
+
and the stop-and-wait ending; command and prompt renderers wrap them with
|
|
23
|
+
frontmatter and a user-message section.
|
|
20
24
|
The point is drift-proofing: a wording change lands in the nudge and the
|
|
21
25
|
command at once, and a test asserts each rendered file contains its core
|
|
22
26
|
verbatim.
|
|
@@ -30,13 +34,40 @@ per host: `thatch_find_duplicates` on opencode, `mcp__thatch__find_duplicates`
|
|
|
30
34
|
on MCP hosts. Each host's renderer passes its own namer
|
|
31
35
|
(`opencodeToolName` / `mcpToolName`).
|
|
32
36
|
|
|
37
|
+
## Skill-couching actions
|
|
38
|
+
|
|
39
|
+
A command body can **couch** a skill: wrap the skill's procedure with the
|
|
40
|
+
additional instructions the command needs. `refine`, `peer-review`,
|
|
41
|
+
`self-review`, and `whois` are not prompt cores: they have no nudge
|
|
42
|
+
counterpart to stay in sync with, so their bodies live directly in
|
|
43
|
+
`actionDefs` (src/commands.ts). `refine` triggers the `thatch-refine`
|
|
44
|
+
router skill and `whois` prints the chat identity; neither applies an
|
|
45
|
+
overlay.
|
|
46
|
+
|
|
47
|
+
`peer-review` and `self-review` share one pipeline couching
|
|
48
|
+
(`reviewPipelineCouching` in src/commands.ts): run the code review
|
|
49
|
+
machinery — the `thatch-code-review` coordinator where that skill is
|
|
50
|
+
installed (opencode), the inline specialist sequence where it is not
|
|
51
|
+
(Claude Code, Cursor) — and apply a mode overlay that reweights the
|
|
52
|
+
findings for whose work is under review. The roster and count for the
|
|
53
|
+
inline sequence come from `REVIEW_SPECIALISTS` in src/prompts.ts, the
|
|
54
|
+
same source as the install list in src/skills.ts and both host
|
|
55
|
+
instruction skill lists. `peer-review` weights org
|
|
56
|
+
velocity and walks the project's persistent memory (the one tool it
|
|
57
|
+
names, spelled through the `ToolNamer` so MCP renderings stay correct);
|
|
58
|
+
`self-review` weights the reputation lenses (no-slop, technique,
|
|
59
|
+
breadcrumbs) and iterates rounds until a round produces no meaningful
|
|
60
|
+
findings. The couching also names the MCP `Focus:` line as a target
|
|
61
|
+
source, because MCP prompts replace the file commands' `$ARGUMENTS`
|
|
62
|
+
section with a `Focus:` line.
|
|
63
|
+
|
|
33
64
|
## Rendering and sync
|
|
34
65
|
|
|
35
66
|
`src/commands.ts` is the single registry:
|
|
36
67
|
|
|
37
68
|
- `actionDefs(tool)` returns the action set with bodies rendered for one
|
|
38
69
|
host. Actions needing plugin-only capabilities are marked `opencodeOnly`
|
|
39
|
-
(`extract`:
|
|
70
|
+
(`extract` and `whois`: they need the invoking session's identity).
|
|
40
71
|
- `opencodeCommandDefs()` = wrap-up templates + all actions. Synced by the
|
|
41
72
|
plugin at init into `$XDG_CONFIG_HOME/opencode/command/thatch/`
|
|
42
73
|
(`installOpencodeCommands`), content-compare, self-healing.
|
|
@@ -52,7 +83,7 @@ on MCP hosts. Each host's renderer passes its own namer
|
|
|
52
83
|
|
|
53
84
|
A parity-guard test asserts the three sets are the same actions minus the
|
|
54
85
|
documented exclusions: opencode = Claude Code set + `compact`, `exit`,
|
|
55
|
-
`extract`; MCP prompts = Claude Code's set.
|
|
86
|
+
`extract`, `whois`; MCP prompts = Claude Code's set.
|
|
56
87
|
|
|
57
88
|
## The extract action and session IDs
|
|
58
89
|
|
|
@@ -146,10 +146,10 @@ mechanical barrier, a documented trust posture.
|
|
|
146
146
|
### Leave tombstones
|
|
147
147
|
|
|
148
148
|
Every exit path - `chat_unregister`, `session.deleted`, and a greenlit
|
|
149
|
-
`/thatch/exit` (the plugin unregisters the exiting session before
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
`/thatch/exit` (the plugin unregisters the exiting session before the
|
|
150
|
+
host close-out - v1 publishes `app.exit`, v2 closes the session's own
|
|
151
|
+
tab - so the roster stops advertising a host that is about to vanish)
|
|
152
|
+
- writes a
|
|
153
153
|
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
154
154
|
directory row. The tombstone is what makes a leave stick: all
|
|
155
155
|
registration paths consult it (the plugin's idle and prompt auto-registerers are
|
|
@@ -235,12 +235,17 @@ the top of the result teaches the paging call, and ids make it exact.
|
|
|
235
235
|
|
|
236
236
|
`chat_broadcast` posts one message to every other registered session at
|
|
237
237
|
once, one inbox row per recipient per part - the existing wake machinery
|
|
238
|
-
(grouping, gating, rate cap) treats each as ordinary mail.
|
|
239
|
-
are skipped and reported
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
(grouping, gating, rate cap) treats each as ordinary mail. Opencode rows
|
|
239
|
+
are skipped and reported unless they are OPEN (fresh heartbeat AND a tab
|
|
240
|
+
open now, per the open-tab reader below): a host that has stopped
|
|
241
|
+
heartbeat-ing will never read the mail, and a fresh-but-tab-less session's
|
|
242
|
+
wake can be swallowed without running a turn, so messaging it would spend
|
|
243
|
+
a model turn on a delivery that likely never fires. MCP sessions always
|
|
244
|
+
receive (they read mail at their next prompt). Every skip is listed by
|
|
245
|
+
name in the result's `skipped` field. The sender is excluded. It is a
|
|
246
|
+
separate tool rather than a
|
|
242
247
|
magic `chat_send` recipient because "send to all" changes the behavior
|
|
243
|
-
(fan-out,
|
|
248
|
+
(fan-out, skip classes, no address resolution), and a function that
|
|
244
249
|
changes behavior drastically on a parameter value is two functions.
|
|
245
250
|
|
|
246
251
|
Retries are idempotent by READ STATE within a short window: a send (or
|
|
@@ -269,7 +274,8 @@ fires no `session.deleted`, so its rows would linger - staleness catches
|
|
|
269
274
|
them: a row that has missed two consecutive beats (`CHAT_STALE_MS`, 60s
|
|
270
275
|
at the default interval) is stale, meaning its harness stopped. Two beats
|
|
271
276
|
rather than one so a single late poll cycle does not flap the roster.
|
|
272
|
-
Heartbeat age is the
|
|
277
|
+
Heartbeat age is the only WRITTEN liveness signal, by design (the tab
|
|
278
|
+
reader below is read-only). A host process id
|
|
273
279
|
cannot serve: the same session is re-hosted by a new process on every
|
|
274
280
|
`-s` resume, so a pid stamped on the row lies as soon as a different
|
|
275
281
|
harness beats it (it says dead while the heartbeat proves alive). RESTART
|
|
@@ -280,12 +286,43 @@ into, so without the grace they would read stale while alive-idle and
|
|
|
280
286
|
mail to them would not wake the tab. Sessions that emit activity
|
|
281
287
|
graduate to permanent hosting; still-silent ones (closed tabs) age out
|
|
282
288
|
at the grace and go stale. `isStale`
|
|
283
|
-
(src/chat.ts) is the single rule; `chatLiveness`, the `chat_send`
|
|
284
|
-
and the broadcast skip all call it, so they cannot disagree.
|
|
289
|
+
(src/chat.ts) is the single staleness rule; `chatLiveness`, the `chat_send`
|
|
290
|
+
note, and the broadcast skip all call it, so they cannot disagree.
|
|
291
|
+
|
|
292
|
+
### The open-tab reader: OPEN vs RECENT
|
|
293
|
+
|
|
294
|
+
Heartbeat age alone conflates presence with recency: a finished
|
|
295
|
+
conversation stays fresh until its directory's instance stops beating it
|
|
296
|
+
(days), and a closed tab leaves a fresh row. The refinement is a READ-ONLY
|
|
297
|
+
second signal, `src/tab-state.ts`: at roster render and broadcast time,
|
|
298
|
+
glob `<XDG_STATE_HOME>/opencode/*/tui/tabs.json` (one file per release
|
|
299
|
+
channel - "latest" today, hence the glob), parse BOTH scope modes
|
|
300
|
+
(`global.tabs` and per-path `cwd.*.tabs`; the scope depends on opencode's
|
|
301
|
+
`tabs.scope` config), and build the set of sessionIDs carrying a tab.
|
|
302
|
+
Missing files, bad JSON, and absent state trees degrade to an empty set -
|
|
303
|
+
an unknown tab state never reads as OPEN. `chatLiveness` takes the set and
|
|
304
|
+
returns `open` (fresh + tab), `recent` (fresh + no tab), `stale`;
|
|
305
|
+
MCP rows keep `active`/`idle`. Callers compose the raw set with confirmed
|
|
306
|
+
tab closes via `effectiveOpenTabs` (`chatOpenTabs` is the wired provider):
|
|
307
|
+
the `session.tab_closed` bridge event (the session_tab tool's confirmed
|
|
308
|
+
close) writes a `chat_tab_closed` runtime_state marker, and a FRESH marker
|
|
309
|
+
(inside `CHAT_TAB_CLOSED_OVERRIDE_MS`, 2 staleness windows) subtracts the
|
|
310
|
+
session from the set - so a residue tabs.json row cannot read OPEN right
|
|
311
|
+
after a confirmed close. The marker self-expires (the death path stops the
|
|
312
|
+
heartbeat, so the row goes stale on heartbeat age regardless) and is
|
|
313
|
+
superseded by any later beat.
|
|
314
|
+
|
|
315
|
+
Known limits, accepted: a killed window leaves tab rows behind (the file
|
|
316
|
+
is restart persistence by design), so dead-window residue reads OPEN until
|
|
317
|
+
its heartbeat ages out; tabs disabled degrades everything to RECENT.
|
|
318
|
+
|
|
319
|
+
`chat_list`
|
|
285
320
|
groups the result into Active and Stale sections - the stale section's
|
|
286
|
-
explainer says what stale means
|
|
321
|
+
explainer says what stale means, and each row renders its verdict -
|
|
322
|
+
and `chat_send` states the recipient's
|
|
287
323
|
liveness at send time, so a sender mailing a ghost learns it immediately
|
|
288
|
-
instead of waiting on a wake that will never fire
|
|
324
|
+
instead of waiting on a wake that will never fire, and a sender mailing a
|
|
325
|
+
tab-less session is told the wake is not guaranteed. Both roster surfaces
|
|
289
326
|
order rows with `sortChatRoster` (also src/chat.ts): project first,
|
|
290
327
|
alphabetical, with no-project rows last; most recently seen first within
|
|
291
328
|
a project. The CLI roster bounds
|
|
@@ -312,6 +349,22 @@ and a count, pointer-only like watcher notifications, with the model calling
|
|
|
312
349
|
`chat_read` for content. On success the poller stamps `delivered_at`
|
|
313
350
|
(re-stamping on re-nudges resets the re-nudge timer).
|
|
314
351
|
|
|
352
|
+
Before any wake attempt runs, the delivery guard (`guardDelivery` in
|
|
353
|
+
`src/runtime.ts`) checks that the runtime instance's own directory - the
|
|
354
|
+
bound directory of every session it hosts - still exists. A deleted
|
|
355
|
+
worktree (merged branch) would otherwise error every attempt, and opencode
|
|
356
|
+
surfaces that error to the user as a toast. The guard skips the attempt
|
|
357
|
+
with a `runtime:delivery` debug line instead, leaving the mail queued; the
|
|
358
|
+
check is per attempt, so a worktree recreated at the same path resumes
|
|
359
|
+
delivery. The existence check cannot say why a path is absent (deleted
|
|
360
|
+
tree, broken symlink, parent permission), so any miss classifies as gone -
|
|
361
|
+
deliberate, because surfacing non-ENOENT errors through the attempt would
|
|
362
|
+
buy the toast back for no recovery the pending-and-retry cycle does not
|
|
363
|
+
already provide. The guard wraps `promptSession` in
|
|
364
|
+
`createRuntime`, which covers chat nudges, watcher notifications and death
|
|
365
|
+
notices, rehydrate pings, chat echoes, and extraction dispatch at once;
|
|
366
|
+
MCP hosts never build a runtime, so they are unaffected.
|
|
367
|
+
|
|
315
368
|
The hard brake: at most a fixed number of wake prompts per recipient per
|
|
316
369
|
hour (default 6, in-memory on purpose - it is a loop guard, not accounting).
|
|
317
370
|
Without it, two agents politely acknowledging each other would ping-pong
|
|
@@ -475,7 +528,9 @@ caller's identity and mailbox, and is absent entirely when chat is
|
|
|
475
528
|
disabled - absence is silent by construction.
|
|
476
529
|
Staleness semantics differ by kind: an opencode row's stale age means its
|
|
477
530
|
harness stopped beating (broadcast skips it), while an mcp row's age only means
|
|
478
|
-
"between turns" (broadcast always delivers).
|
|
531
|
+
"between turns" (broadcast always delivers). Broadcast also skips fresh
|
|
532
|
+
opencode rows with no open tab (see the open-tab reader) - the skip
|
|
533
|
+
classes are reported by name either way.
|
|
479
534
|
|
|
480
535
|
The `/thatch/whois` command (opencode-only action, src/commands.ts) is the
|
|
481
536
|
user-facing identity report: it relays `chat_status` for the invoking
|
|
@@ -489,18 +544,27 @@ mutates anything.
|
|
|
489
544
|
|
|
490
545
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
491
546
|
threshold two missed beats (60s), re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
492
|
-
per hour, auto-row TTL 7 days
|
|
493
|
-
|
|
494
|
-
`
|
|
495
|
-
|
|
547
|
+
per hour, auto-row TTL 7 days, confirmed-close override window two
|
|
548
|
+
staleness windows (`CHAT_TAB_CLOSED_OVERRIDE_MS` in `src/tab-state.ts`).
|
|
549
|
+
Environment overrides: `THATCH_CHAT_SPLIT_PART_LEN`
|
|
550
|
+
(auto-split part budget, default 2000, clamped 500..9000),
|
|
551
|
+
`THATCH_CHAT_MAX_BODY_CHARS` (whole-body acceptance ceiling, default 20000), and
|
|
552
|
+
`THATCH_OPENCODE_STATE_ROOT` (the open-tab reader's state root, tests and
|
|
553
|
+
QA sandboxes only);
|
|
554
|
+
the split constants are resolved once at module load and sanitized to positive integers
|
|
496
555
|
within their clamps - invalid values silently fall back to the default.
|
|
497
556
|
|
|
498
557
|
## Source files
|
|
499
558
|
|
|
500
559
|
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
501
|
-
pool pick, slugify, counter bump, insert, prune, topic refresh
|
|
560
|
+
pool pick, slugify, counter bump, insert, prune, topic refresh, broadcast
|
|
561
|
+
targeting), ChatPoller
|
|
502
562
|
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
503
|
-
staleness helper
|
|
563
|
+
staleness helper, chatLiveness (the open/recent/active/idle/stale
|
|
564
|
+
verdicts)
|
|
565
|
+
- `src/tab-state.ts` - the read-only open-tab reader: per-channel
|
|
566
|
+
tabs.json glob (global + cwd scopes), confirmed-close override window,
|
|
567
|
+
the composed open-tabs provider
|
|
504
568
|
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
505
569
|
the base for every assigned name
|
|
506
570
|
- `src/db.ts` - the chat tables in schema init (including
|
|
@@ -96,7 +96,7 @@ prediction_matchers(
|
|
|
96
96
|
embedding BLOB,
|
|
97
97
|
model TEXT,
|
|
98
98
|
created_at TEXT NOT NULL,
|
|
99
|
-
updated_at TEXT NOT NULL
|
|
99
|
+
updated_at TEXT NOT NULL -- last change to the matcher row or its edge set
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -116,7 +116,7 @@ predictions(
|
|
|
116
116
|
confirm_count REAL NOT NULL DEFAULT 0,
|
|
117
117
|
disconfirm_count REAL NOT NULL DEFAULT 0,
|
|
118
118
|
created_at TEXT NOT NULL,
|
|
119
|
-
updated_at TEXT NOT NULL
|
|
119
|
+
updated_at TEXT NOT NULL -- written at creation, then last confidence change
|
|
120
120
|
)
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -167,7 +167,7 @@ behavior_matchers(
|
|
|
167
167
|
embedding BLOB,
|
|
168
168
|
model TEXT,
|
|
169
169
|
created_at TEXT NOT NULL,
|
|
170
|
-
updated_at TEXT NOT NULL
|
|
170
|
+
updated_at TEXT NOT NULL -- last change to the matcher row or its edge set
|
|
171
171
|
)
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -185,7 +185,7 @@ behaviors(
|
|
|
185
185
|
confirm_count REAL NOT NULL DEFAULT 0,
|
|
186
186
|
disconfirm_count REAL NOT NULL DEFAULT 0,
|
|
187
187
|
created_at TEXT NOT NULL,
|
|
188
|
-
updated_at TEXT NOT NULL
|
|
188
|
+
updated_at TEXT NOT NULL -- written at creation, then last confidence change
|
|
189
189
|
)
|
|
190
190
|
```
|
|
191
191
|
|
|
@@ -131,6 +131,20 @@ Tiers 2, 3, and 4 all fire independently. Any subset may inject. All share
|
|
|
131
131
|
one embedding computation, reused across three cosine scans against
|
|
132
132
|
different tables.
|
|
133
133
|
|
|
134
|
+
#### Snapshot semantics
|
|
135
|
+
|
|
136
|
+
A nudge block is computed once, when the message is submitted, and stays
|
|
137
|
+
attached to that message for the whole turn - including every model round
|
|
138
|
+
trip after mid-turn tool calls. Predictions, behaviors, or memories the
|
|
139
|
+
conversation mutates mid-turn do NOT refresh an already-injected block
|
|
140
|
+
(v1: the synthetic part lives on the stored message; v2:
|
|
141
|
+
`pendingInjections` re-injects the prompt-time text into every outbound
|
|
142
|
+
request of the turn, bound to the message it was computed for - see
|
|
143
|
+
[opencode-plugin.md](opencode-plugin.md)). The prediction and behavior
|
|
144
|
+
blocks therefore end with a snapshot footer naming the store state they
|
|
145
|
+
reflect and the live tool to re-check. The recall nudge carries no footer:
|
|
146
|
+
it lists memory labels without confidence claims.
|
|
147
|
+
|
|
134
148
|
### MCP path (`bin/thatch`, `flush-tools` subcommand)
|
|
135
149
|
|
|
136
150
|
The `flush-tools` subcommand fires on `UserPromptSubmit` (Claude Code) or
|