@jeffober/thatch 0.2.4 → 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 +56 -26
- package/artifacts/skills/common.md +1 -0
- package/artifacts/skills/thatch-clear-writing.md +103 -5
- package/artifacts/skills/thatch-code-archaeology.md +9 -0
- package/artifacts/skills/thatch-code-review.md +16 -8
- package/artifacts/skills/thatch-coordination.md +69 -0
- package/artifacts/skills/thatch-plan-refinement.md +33 -4
- package/artifacts/skills/thatch-pr-description.md +21 -13
- 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-no-slop.md +15 -0
- package/artifacts/skills/thatch-review-response.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +21 -1
- package/bin/thatch +48 -15
- package/docs/assets/deepwiki-badge.svg +1 -0
- package/docs/dev/README.md +18 -10
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/behavior-engine.md +10 -0
- package/docs/dev/features/cli.md +4 -1
- package/docs/dev/features/commands.md +40 -9
- package/docs/dev/features/cross-session-chat.md +194 -23
- package/docs/dev/features/database.md +4 -4
- package/docs/dev/features/notifications.md +1 -2
- package/docs/dev/features/nudge-pipeline.md +14 -0
- package/docs/dev/features/opencode-plugin.md +56 -15
- package/docs/dev/features/prediction-engine.md +71 -6
- 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 +161 -0
- package/docs/dev/features/setup.md +5 -0
- package/docs/dev/features/watchers.md +62 -6
- package/docs/dev/gotchas.md +89 -0
- package/docs/dev/mcp-parity.md +5 -5
- package/docs/dev/setup-and-hooks.md +7 -2
- package/docs/dev/skills.md +12 -9
- package/docs/plans/generalized-session-heartbeat.md +212 -0
- package/docs/plans/prediction-consolidation.md +88 -68
- package/docs/user/README.md +29 -13
- package/docs/user/behavior-engine.md +7 -1
- package/docs/user/cli.md +5 -1
- package/docs/user/code-review.md +43 -2
- package/docs/user/commands.md +37 -10
- package/docs/user/cross-session-chat.md +76 -26
- package/docs/user/prediction-engine.md +23 -1
- package/docs/user/setup.md +16 -0
- package/docs/user/skills.md +9 -7
- package/docs/user/watchers.md +22 -7
- package/package.json +10 -2
- package/src/alerts.ts +1 -1
- package/src/capabilities.ts +20 -6
- package/src/chat.ts +358 -43
- package/src/commands.ts +123 -18
- package/src/db.ts +74 -3
- package/src/git.ts +3 -1
- package/src/hygiene.ts +5 -0
- package/src/mcp.ts +101 -9
- package/src/opencode/tui-plugin.ts +90 -0
- package/src/opencode/v2.ts +440 -66
- package/src/prediction.ts +90 -0
- package/src/prompts.ts +164 -29
- package/src/runtime.ts +298 -15
- package/src/scoring-engine.ts +40 -9
- package/src/session-db.ts +4 -4
- package/src/session-tab-shared.ts +231 -0
- package/src/setup.ts +9 -3
- package/src/skills.ts +6 -9
- package/src/tab-state.ts +125 -0
- package/src/tool-defs.ts +454 -76
- package/src/tools.ts +2 -0
- package/src/watchers.ts +263 -26
- package/docs/plans/intuition-drives.md +0 -147
- package/docs/plans/llm-alerts.md +0 -287
- package/docs/plans/thatch-actions.md +0 -179
- package/docs/plans/worktree-deletion-recovery.md +0 -246
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)
|
|
@@ -45,6 +45,10 @@ OpenCode plugin path
|
|
|
45
45
|
│ lazy-imports the adapter matching the host
|
|
46
46
|
├── opencode/v1.ts → v1 adapter: @opencode-ai/plugin hooks object
|
|
47
47
|
├── opencode/v2.ts → v2 adapter: @opencode/plugin promise-context domains
|
|
48
|
+
├── opencode/tui-plugin.ts → v2 TUI CLI plugin (./tui entrypoint):
|
|
49
|
+
│ consumes the session-tab rpc events, drives ui.tabs
|
|
50
|
+
├── session-tab-shared.ts → SDK-free cross-entry contract: session-tab rpc
|
|
51
|
+
│ definition, subordinate prompt builder, validators
|
|
48
52
|
├── capabilities.ts → HostCapabilities seam: the host operations the
|
|
49
53
|
│ runtime needs; capabilitiesFromClient bridges the
|
|
50
54
|
│ v1 SDK client
|
|
@@ -85,8 +89,9 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
85
89
|
| `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). |
|
|
86
90
|
| `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). |
|
|
87
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. |
|
|
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
|
-
| `
|
|
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
|
+
| `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, session close-out - app exit on v1, the session's own tab on v2). `capabilitiesFromClient` bridges the v1 SDK client. |
|
|
90
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. |
|
|
91
96
|
| `os-args.ts` | Pure argv / OS-command-line helpers for startup session resolution (`-s`/`-c`). SDK-free. |
|
|
92
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. |
|
|
@@ -98,15 +103,16 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
98
103
|
| `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`. |
|
|
99
104
|
| `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. |
|
|
100
105
|
| `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 [
|
|
106
|
+
| `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 [features/notifications.md](features/notifications.md). |
|
|
102
107
|
| `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. |
|
|
103
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. |
|
|
104
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). |
|
|
105
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). |
|
|
106
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. |
|
|
107
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). |
|
|
108
|
-
| `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). |
|
|
109
|
-
| `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]`. |
|
|
110
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. |
|
|
111
117
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
112
118
|
| `behavior.ts` | Thin wrapper around `ScoringEngine` with behavior-specific table names. |
|
|
@@ -124,7 +130,7 @@ methods. The v1 hook shapes:
|
|
|
124
130
|
|------|-------------|
|
|
125
131
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
126
132
|
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
127
|
-
| `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). |
|
|
128
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. |
|
|
129
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. |
|
|
130
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. |
|
|
@@ -263,14 +269,16 @@ extraction cycle
|
|
|
263
269
|
→ MCP path (Claude Code/Cursor): unchanged — no SDK client, no child
|
|
264
270
|
sessions; extract-queue.ts + flush-tools drives the nudge via hooks
|
|
265
271
|
|
|
266
|
-
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)
|
|
267
274
|
→ client.tui.showToast — best-effort, silently ignored if TUI not
|
|
268
275
|
connected (headless mode); model-invisible (goes to the user only,
|
|
269
276
|
not the conversation history — the inverse of synthetic nudge parts,
|
|
270
277
|
which are model-visible but TUI-hidden)
|
|
271
278
|
→ extraction metrics: `[thatch] new: N, updated: M, deleted: K` (success
|
|
272
|
-
variant, 4s) — fires when an extraction child goes idle
|
|
273
|
-
|
|
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)
|
|
274
282
|
→ recall matches: `[thatch] recalled N memories` (info variant, 3s) —
|
|
275
283
|
fires when chat.message matches stored memories
|
|
276
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
|
|
@@ -88,6 +89,8 @@ Read-only window on the cross-session chat directory ([cross-session-chat.md](cr
|
|
|
88
89
|
| `THATCH_CHILD_RECONCILE_MS` | Reload window before a restored extraction child is judged finished (test knob) | `10000` |
|
|
89
90
|
| `CLAUDE_PROJECT_DIR` | Claude Code project dir | `process.cwd()` |
|
|
90
91
|
| `CURSOR_PROJECT_DIR` | Cursor project dir | falls through |
|
|
92
|
+
| `THATCH_WATCH_DEATH_MINUTES` | Pending-age threshold for watcher death detection | `120` |
|
|
93
|
+
| `THATCH_PROJECT_DIR` | Pinned project dir (written by project-local `thatch setup` MCP configs) | falls through |
|
|
91
94
|
| `CLAUDE_CONFIG_DIR` | Claude config dir | `~/.claude` |
|
|
92
95
|
| `CURSOR_CONFIG_DIR` | Cursor config dir | `~/.cursor` |
|
|
93
96
|
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Enable async extraction | unset |
|
|
@@ -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
|
|
|
@@ -86,6 +86,29 @@ worktree, a `.git` directory means the project root, no `.git` reads as
|
|
|
86
86
|
undetected. The roster shows it as a `loc:` token so sessions coordinating
|
|
87
87
|
on a shared tree can tell who sits where.
|
|
88
88
|
|
|
89
|
+
### Machinery sessions never auto-register
|
|
90
|
+
|
|
91
|
+
Sessions the plugin itself creates as machinery are not chat
|
|
92
|
+
participants and never join the directory: the direct-extraction child
|
|
93
|
+
(`triggerExtraction`, titled `thatch-extraction`) is the current case.
|
|
94
|
+
The primary guard is durable, not in-memory: child creation writes a
|
|
95
|
+
`chat_machinery` row (src/db.ts), and both auto-register paths (prompt
|
|
96
|
+
and idle) refuse a marked session. This matters because the in-memory
|
|
97
|
+
`childToParent` mapping that normally keeps children out is lost to a
|
|
98
|
+
plugin reload or daemon restart - an instance that processes the child's
|
|
99
|
+
events without the mapping would otherwise register it, and since v2
|
|
100
|
+
cannot delete sessions (see docs/dev/features/opencode-plugin.md), the
|
|
101
|
+
row never cleans up. The poller heartbeats every registered row of the
|
|
102
|
+
project it serves, so a machinery corpse stayed fresh forever and the
|
|
103
|
+
stale-row reaper could not reap it (81 corpse rows by 2026-10-07). The
|
|
104
|
+
idle path deletes any corpse row it finds for a marked session
|
|
105
|
+
(self-healing) and additionally refuses a session whose fetched title is
|
|
106
|
+
the machinery title (`isMachinerySessionTitle`, src/chat.ts) - the title
|
|
107
|
+
is the one signal that survives even a lost marker. Markers age out with
|
|
108
|
+
the 7-day auto-row TTL; session ids are never reused, so an expired
|
|
109
|
+
marker cannot resurrect a row. Explicit `chat_register` is unaffected:
|
|
110
|
+
a model deliberately joining the directory is never machinery.
|
|
111
|
+
|
|
89
112
|
### Startup registration and reclaim
|
|
90
113
|
|
|
91
114
|
A session continued via `opencode -s <id>` gets no event when it comes
|
|
@@ -123,10 +146,10 @@ mechanical barrier, a documented trust posture.
|
|
|
123
146
|
### Leave tombstones
|
|
124
147
|
|
|
125
148
|
Every exit path - `chat_unregister`, `session.deleted`, and a greenlit
|
|
126
|
-
`/thatch/exit` (the plugin unregisters the exiting session before
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
153
|
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
131
154
|
directory row. The tombstone is what makes a leave stick: all
|
|
132
155
|
registration paths consult it (the plugin's idle and prompt auto-registerers are
|
|
@@ -168,15 +191,78 @@ unregistration; a departed sender degrades to an unknown name in the
|
|
|
168
191
|
reader's view (the endpoints are deliberately not foreign keys - leaving the
|
|
169
192
|
directory must not be blocked by history).
|
|
170
193
|
|
|
194
|
+
Bodies over the part budget are not refused: `splitChatBody` (src/chat.ts)
|
|
195
|
+
auto-splits them into parts, and the first part is prefixed "(message 1 of
|
|
196
|
+
N)" while every part but the last is suffixed "(continued in next
|
|
197
|
+
message)", so a reader assembles the batch before acting on it. Cuts
|
|
198
|
+
prefer blank-line paragraph boundaries and back off a hard cut so a
|
|
199
|
+
surrogate pair never splits mid-character. The parts of one send are
|
|
200
|
+
inserted in ONE transaction - a crash cannot leave a partial batch - and
|
|
201
|
+
the tool output reports the split ("SENT AS N PARTS") so a caller cannot
|
|
202
|
+
mistake it for a single delivery.
|
|
203
|
+
|
|
204
|
+
Two budgets govern the split, and the docs state both honestly:
|
|
205
|
+
|
|
206
|
+
- The part budget is `CHAT_SPLIT_PART_LEN`, default 2000 UTF-16 units
|
|
207
|
+
(`THATCH_CHAT_SPLIT_PART_LEN` to override, clamped to 500..9000). The
|
|
208
|
+
DEFAULT is sized to the worst host we have observed: a version-skewed
|
|
209
|
+
opencode install whose tool-result renderer clipped at exactly 2000
|
|
210
|
+
characters with no saved-output pointer, cutting each part off before
|
|
211
|
+
the model saw it. Current opencode v2 (pinned tag v2.0.24) passes
|
|
212
|
+
50 KiB and 2000 lines per tool result (packages/core/src/tool-output.ts
|
|
213
|
+
MAX_LINES/MAX_BYTES, host-configurable via
|
|
214
|
+
`tool_output.max_lines/max_bytes`), so a host on a known-good version
|
|
215
|
+
can safely raise the override.
|
|
216
|
+
- The whole body is capped by `CHAT_MAX_BODY_CHARS`, default 20,000
|
|
217
|
+
characters (`THATCH_CHAT_MAX_BODY_CHARS`), checked up front at accept; the
|
|
218
|
+
number of parts derives from it (ceil(total/partLen)). The refusal
|
|
219
|
+
reports the attempted count so a caller can split deterministically.
|
|
220
|
+
The grouped chat_read of a split batch renders as ONE tool result, so
|
|
221
|
+
the ceiling keeps a worst-case batch read (at most ten 2000-char parts
|
|
222
|
+
plus framing) comfortably inside the 50 KiB v2 envelope while still
|
|
223
|
+
bounding the context bomb the old 10k refusal used to block.
|
|
224
|
+
|
|
225
|
+
Read-side clipping: the chat_read drain shows each message up to
|
|
226
|
+
`CHAT_READ_PAGE_LEN` (2000) characters; longer rows are shown clipped
|
|
227
|
+
with a per-message marker that names the unit - "[clipped: showing 2000
|
|
228
|
+
of 9050 characters" - and the deterministic recovery path. Every drain
|
|
229
|
+
line carries the message id, and `chat_read` accepts `message` (a row id)
|
|
230
|
+
and `offset` (a character offset) to page through one message a page at a
|
|
231
|
+
time without draining or marking anything read. This is the recovery for
|
|
232
|
+
both a drain clip and a HOST-side tool-result clip (the version-skew host
|
|
233
|
+
above): even when a marker or a whole message is cut, the frame note at
|
|
234
|
+
the top of the result teaches the paging call, and ids make it exact.
|
|
235
|
+
|
|
171
236
|
`chat_broadcast` posts one message to every other registered session at
|
|
172
|
-
once, one inbox row per recipient - the existing wake machinery
|
|
173
|
-
gating, rate cap) treats each as ordinary mail.
|
|
174
|
-
and reported
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
once, one inbox row per recipient per part - the existing wake machinery
|
|
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
|
|
247
|
+
magic `chat_send` recipient because "send to all" changes the behavior
|
|
248
|
+
(fan-out, skip classes, no address resolution), and a function that
|
|
249
|
+
changes behavior drastically on a parameter value is two functions.
|
|
250
|
+
|
|
251
|
+
Retries are idempotent by READ STATE within a short window: a send (or
|
|
252
|
+
per-recipient broadcast batch) whose content matches an existing row on
|
|
253
|
+
sender, recipient, body, and delivery kind within `CHAT_DEDUPE_WINDOW_MS`
|
|
254
|
+
(10s) succeeds without a second insert - but ONLY while a matching row is
|
|
255
|
+
still UNREAD in the recipient's inbox. The mail is there; a retry adds
|
|
256
|
+
nothing. Once every matching row was read, the identical body DELIVERS
|
|
257
|
+
again: presumptively an intentional resend after the first was consumed
|
|
258
|
+
(a repeated "ok", a resend the reader asked for). The tool output says
|
|
259
|
+
why it suppressed ("still UNREAD in the recipient's inbox"). This exists
|
|
260
|
+
because a harness that times out a chat_send which actually committed
|
|
261
|
+
retries the call, and the identical body otherwise lands twice (observed
|
|
262
|
+
2026-10-07). The same body outside the window is always a new message.
|
|
263
|
+
For a split batch, ANY unread part suppresses the retry - idempotent even
|
|
264
|
+
while the recipient is mid-read (the poller keeps nudging the unread
|
|
265
|
+
parts), and only a fully read batch lets an intentional repeat through.
|
|
180
266
|
|
|
181
267
|
### Polling, heartbeat, staleness
|
|
182
268
|
|
|
@@ -188,7 +274,8 @@ fires no `session.deleted`, so its rows would linger - staleness catches
|
|
|
188
274
|
them: a row that has missed two consecutive beats (`CHAT_STALE_MS`, 60s
|
|
189
275
|
at the default interval) is stale, meaning its harness stopped. Two beats
|
|
190
276
|
rather than one so a single late poll cycle does not flap the roster.
|
|
191
|
-
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
|
|
192
279
|
cannot serve: the same session is re-hosted by a new process on every
|
|
193
280
|
`-s` resume, so a pid stamped on the row lies as soon as a different
|
|
194
281
|
harness beats it (it says dead while the heartbeat proves alive). RESTART
|
|
@@ -199,12 +286,43 @@ into, so without the grace they would read stale while alive-idle and
|
|
|
199
286
|
mail to them would not wake the tab. Sessions that emit activity
|
|
200
287
|
graduate to permanent hosting; still-silent ones (closed tabs) age out
|
|
201
288
|
at the grace and go stale. `isStale`
|
|
202
|
-
(src/chat.ts) is the single rule; `chatLiveness`, the `chat_send`
|
|
203
|
-
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`
|
|
204
320
|
groups the result into Active and Stale sections - the stale section's
|
|
205
|
-
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
|
|
206
323
|
liveness at send time, so a sender mailing a ghost learns it immediately
|
|
207
|
-
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
|
|
208
326
|
order rows with `sortChatRoster` (also src/chat.ts): project first,
|
|
209
327
|
alphabetical, with no-project rows last; most recently seen first within
|
|
210
328
|
a project. The CLI roster bounds
|
|
@@ -231,6 +349,22 @@ and a count, pointer-only like watcher notifications, with the model calling
|
|
|
231
349
|
`chat_read` for content. On success the poller stamps `delivered_at`
|
|
232
350
|
(re-stamping on re-nudges resets the re-nudge timer).
|
|
233
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
|
+
|
|
234
368
|
The hard brake: at most a fixed number of wake prompts per recipient per
|
|
235
369
|
hour (default 6, in-memory on purpose - it is a loop guard, not accounting).
|
|
236
370
|
Without it, two agents politely acknowledging each other would ping-pong
|
|
@@ -360,11 +494,23 @@ parent pid against that table (`chatDerivedIdentity` on CoreContext,
|
|
|
360
494
|
freshness-capped at 600s to bound pid reuse). A second model-proof layer
|
|
361
495
|
is `CLAUDE_CODE_SESSION_ID`, which the host sets in the stdio server's
|
|
362
496
|
environment at spawn. `resolveChatIdentity` priority: opencode host
|
|
363
|
-
context > ppid mapping > env id > claimed `as`.
|
|
497
|
+
context > ppid mapping > env id > claimed `as`. The `as` fallback is also directory-bound: the
|
|
498
|
+
claimed identity must belong to the caller's project (the store slug
|
|
499
|
+
every registration writes as the row's project column) - a cross-project
|
|
500
|
+
or null-project claim is refused with a re-register escape hatch.
|
|
501
|
+
Cursor's hooks and MCP
|
|
364
502
|
servers share one workspace process (ambiguous ppid, no per-session env),
|
|
365
503
|
so Cursor keeps the claimed-`as` path, documented in the user doc's
|
|
366
504
|
security model.
|
|
367
505
|
|
|
506
|
+
**Server project resolution.** The MCP server resolves its own project
|
|
507
|
+
directory as `THATCH_PROJECT_DIR ?? CURSOR_PROJECT_DIR ??
|
|
508
|
+
CLAUDE_PROJECT_DIR ?? cwd` (a superset of detectRepo's chain), maps
|
|
509
|
+
`unknown` to `global` exactly like the hook, and project-local setups
|
|
510
|
+
pin `THATCH_PROJECT_DIR` into the written MCP config so the resolution
|
|
511
|
+
is cwd-independent. Global-scope registrations are never pinned - one
|
|
512
|
+
entry serves every project.
|
|
513
|
+
|
|
368
514
|
**Continuation adoption (Claude Code).** Claude Code forks the session id
|
|
369
515
|
on resume - and has been observed to fork spontaneously (agent-team
|
|
370
516
|
conversion, self-update, config reload) - which would strand the old
|
|
@@ -382,21 +528,43 @@ caller's identity and mailbox, and is absent entirely when chat is
|
|
|
382
528
|
disabled - absence is silent by construction.
|
|
383
529
|
Staleness semantics differ by kind: an opencode row's stale age means its
|
|
384
530
|
harness stopped beating (broadcast skips it), while an mcp row's age only means
|
|
385
|
-
"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.
|
|
534
|
+
|
|
535
|
+
The `/thatch/whois` command (opencode-only action, src/commands.ts) is the
|
|
536
|
+
user-facing identity report: it relays `chat_status` for the invoking
|
|
537
|
+
session's handle, then joins the roster row for topic, project, and the
|
|
538
|
+
`loc:` worktree token - the topic being the live TUI session title, it is
|
|
539
|
+
how the operator maps a roster handle to a visible tab. An argument looks
|
|
540
|
+
up another session's row instead. It reports only; it never reads mail or
|
|
541
|
+
mutates anything.
|
|
386
542
|
|
|
387
543
|
## Defaults
|
|
388
544
|
|
|
389
545
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
390
546
|
threshold two missed beats (60s), re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
391
|
-
per hour, auto-row TTL 7 days
|
|
392
|
-
|
|
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
|
|
555
|
+
within their clamps - invalid values silently fall back to the default.
|
|
393
556
|
|
|
394
557
|
## Source files
|
|
395
558
|
|
|
396
559
|
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
397
|
-
pool pick, slugify, counter bump, insert, prune, topic refresh
|
|
560
|
+
pool pick, slugify, counter bump, insert, prune, topic refresh, broadcast
|
|
561
|
+
targeting), ChatPoller
|
|
398
562
|
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
399
|
-
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
|
|
400
568
|
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
401
569
|
the base for every assigned name
|
|
402
570
|
- `src/db.ts` - the chat tables in schema init (including
|
|
@@ -418,5 +586,8 @@ them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
|
418
586
|
- `tests/qa/auto/uc-097-chat.ts` - full lifecycle against a mocked poller
|
|
419
587
|
- `tests/qa/auto/uc-099-chat-cli.ts` - `chat list` roster and `chat tail`
|
|
420
588
|
JSONL contract over a seeded DB
|
|
589
|
+
- `tests/qa/auto/uc-116-chat-send-dedupe.ts` - a retried send is suppressed
|
|
590
|
+
- `tests/qa/auto/uc-119-chat-split-long-messages.ts` - an oversized body is
|
|
591
|
+
auto-split into marker-decorated parts
|
|
421
592
|
- `tests/qa/live/uc-098-chat-cross-session.ts` - two real sessions exchange
|
|
422
593
|
a message through the shared DB
|
|
@@ -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
|
|
|
@@ -10,8 +10,7 @@ Two delivery paths share the dispatcher and the config file:
|
|
|
10
10
|
- **Agent-initiated**: the LLM calls `notify_user`. Works on every host.
|
|
11
11
|
- **Automatic alerts** (opencode-only): the plugin runtime watches session
|
|
12
12
|
events and notifies when the LLM pauses for human input or finishes a
|
|
13
|
-
round of real work. See the Alerts section below
|
|
14
|
-
`docs/plans/llm-alerts.md` for the design decisions.
|
|
13
|
+
round of real work. See the Alerts section below for the design decisions.
|
|
15
14
|
|
|
16
15
|
## Source files
|
|
17
16
|
|
|
@@ -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
|