@jeffober/thatch 0.2.3 → 0.2.5
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 +8 -1
- package/artifacts/skills/thatch-clear-writing.md +103 -5
- package/artifacts/skills/thatch-coordination.md +69 -0
- package/artifacts/skills/thatch-pr-description.md +21 -13
- package/artifacts/skills/thatch-review-no-slop.md +15 -0
- package/bin/thatch +95 -44
- package/docs/assets/deepwiki-badge.svg +1 -0
- package/docs/dev/README.md +7 -2
- package/docs/dev/features/cli.md +2 -0
- package/docs/dev/features/cross-session-chat.md +118 -11
- package/docs/dev/features/notifications.md +1 -2
- package/docs/dev/features/opencode-plugin.md +18 -3
- package/docs/dev/features/prediction-engine.md +44 -5
- package/docs/dev/features/session-tabs.md +155 -0
- package/docs/dev/features/setup.md +12 -0
- package/docs/dev/features/watchers.md +62 -6
- package/docs/dev/gotchas.md +68 -0
- package/docs/dev/mcp-parity.md +2 -2
- package/docs/dev/setup-and-hooks.md +7 -2
- package/docs/dev/skills.md +1 -1
- package/docs/plans/generalized-session-heartbeat.md +212 -0
- package/docs/plans/prediction-consolidation.md +88 -68
- package/docs/user/README.md +12 -3
- package/docs/user/cli.md +5 -1
- package/docs/user/commands.md +11 -4
- package/docs/user/cross-session-chat.md +31 -8
- package/docs/user/prediction-engine.md +11 -0
- package/docs/user/setup.md +29 -0
- package/docs/user/skills.md +1 -1
- package/docs/user/watchers.md +22 -7
- package/package.json +10 -2
- package/src/alerts.ts +1 -1
- package/src/chat.ts +320 -27
- package/src/commands.ts +14 -0
- package/src/db.ts +51 -1
- package/src/git.ts +3 -1
- package/src/hygiene.ts +5 -0
- package/src/mcp.ts +95 -7
- package/src/opencode/tui-plugin.ts +70 -0
- package/src/opencode/v2.ts +197 -4
- package/src/prediction.ts +90 -0
- package/src/prompts.ts +110 -23
- package/src/runtime.ts +187 -9
- package/src/session-db.ts +4 -4
- package/src/session-tab-shared.ts +180 -0
- package/src/setup.ts +59 -3
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +412 -55
- 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
|
@@ -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,7 +89,8 @@ 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. |
|
|
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). |
|
|
89
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. |
|
|
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. |
|
|
@@ -98,7 +103,7 @@ 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). |
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -88,6 +88,8 @@ Read-only window on the cross-session chat directory ([cross-session-chat.md](cr
|
|
|
88
88
|
| `THATCH_CHILD_RECONCILE_MS` | Reload window before a restored extraction child is judged finished (test knob) | `10000` |
|
|
89
89
|
| `CLAUDE_PROJECT_DIR` | Claude Code project dir | `process.cwd()` |
|
|
90
90
|
| `CURSOR_PROJECT_DIR` | Cursor project dir | falls through |
|
|
91
|
+
| `THATCH_WATCH_DEATH_MINUTES` | Pending-age threshold for watcher death detection | `120` |
|
|
92
|
+
| `THATCH_PROJECT_DIR` | Pinned project dir (written by project-local `thatch setup` MCP configs) | falls through |
|
|
91
93
|
| `CLAUDE_CONFIG_DIR` | Claude config dir | `~/.claude` |
|
|
92
94
|
| `CURSOR_CONFIG_DIR` | Cursor config dir | `~/.cursor` |
|
|
93
95
|
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Enable async extraction | unset |
|
|
@@ -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
|
|
@@ -168,15 +191,73 @@ 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. Stale sessions
|
|
174
|
-
and reported rather than messaged: a host that has stopped
|
|
175
|
-
will never read the mail, and dead mail to a dead process is
|
|
176
|
-
The sender is excluded. It is a separate tool rather than a
|
|
177
|
-
`chat_send` recipient because "send to all" changes the behavior
|
|
178
|
-
stale skipping, no address resolution), and a function that
|
|
179
|
-
behavior drastically on a parameter value is two functions.
|
|
237
|
+
once, one inbox row per recipient per part - the existing wake machinery
|
|
238
|
+
(grouping, gating, rate cap) treats each as ordinary mail. Stale sessions
|
|
239
|
+
are skipped and reported rather than messaged: a host that has stopped
|
|
240
|
+
heartbeat-ing will never read the mail, and dead mail to a dead process is
|
|
241
|
+
just clutter. The sender is excluded. It is a separate tool rather than a
|
|
242
|
+
magic `chat_send` recipient because "send to all" changes the behavior
|
|
243
|
+
(fan-out, stale skipping, no address resolution), and a function that
|
|
244
|
+
changes behavior drastically on a parameter value is two functions.
|
|
245
|
+
|
|
246
|
+
Retries are idempotent by READ STATE within a short window: a send (or
|
|
247
|
+
per-recipient broadcast batch) whose content matches an existing row on
|
|
248
|
+
sender, recipient, body, and delivery kind within `CHAT_DEDUPE_WINDOW_MS`
|
|
249
|
+
(10s) succeeds without a second insert - but ONLY while a matching row is
|
|
250
|
+
still UNREAD in the recipient's inbox. The mail is there; a retry adds
|
|
251
|
+
nothing. Once every matching row was read, the identical body DELIVERS
|
|
252
|
+
again: presumptively an intentional resend after the first was consumed
|
|
253
|
+
(a repeated "ok", a resend the reader asked for). The tool output says
|
|
254
|
+
why it suppressed ("still UNREAD in the recipient's inbox"). This exists
|
|
255
|
+
because a harness that times out a chat_send which actually committed
|
|
256
|
+
retries the call, and the identical body otherwise lands twice (observed
|
|
257
|
+
2026-10-07). The same body outside the window is always a new message.
|
|
258
|
+
For a split batch, ANY unread part suppresses the retry - idempotent even
|
|
259
|
+
while the recipient is mid-read (the poller keeps nudging the unread
|
|
260
|
+
parts), and only a fully read batch lets an intentional repeat through.
|
|
180
261
|
|
|
181
262
|
### Polling, heartbeat, staleness
|
|
182
263
|
|
|
@@ -360,11 +441,23 @@ parent pid against that table (`chatDerivedIdentity` on CoreContext,
|
|
|
360
441
|
freshness-capped at 600s to bound pid reuse). A second model-proof layer
|
|
361
442
|
is `CLAUDE_CODE_SESSION_ID`, which the host sets in the stdio server's
|
|
362
443
|
environment at spawn. `resolveChatIdentity` priority: opencode host
|
|
363
|
-
context > ppid mapping > env id > claimed `as`.
|
|
444
|
+
context > ppid mapping > env id > claimed `as`. The `as` fallback is also directory-bound: the
|
|
445
|
+
claimed identity must belong to the caller's project (the store slug
|
|
446
|
+
every registration writes as the row's project column) - a cross-project
|
|
447
|
+
or null-project claim is refused with a re-register escape hatch.
|
|
448
|
+
Cursor's hooks and MCP
|
|
364
449
|
servers share one workspace process (ambiguous ppid, no per-session env),
|
|
365
450
|
so Cursor keeps the claimed-`as` path, documented in the user doc's
|
|
366
451
|
security model.
|
|
367
452
|
|
|
453
|
+
**Server project resolution.** The MCP server resolves its own project
|
|
454
|
+
directory as `THATCH_PROJECT_DIR ?? CURSOR_PROJECT_DIR ??
|
|
455
|
+
CLAUDE_PROJECT_DIR ?? cwd` (a superset of detectRepo's chain), maps
|
|
456
|
+
`unknown` to `global` exactly like the hook, and project-local setups
|
|
457
|
+
pin `THATCH_PROJECT_DIR` into the written MCP config so the resolution
|
|
458
|
+
is cwd-independent. Global-scope registrations are never pinned - one
|
|
459
|
+
entry serves every project.
|
|
460
|
+
|
|
368
461
|
**Continuation adoption (Claude Code).** Claude Code forks the session id
|
|
369
462
|
on resume - and has been observed to fork spontaneously (agent-team
|
|
370
463
|
conversion, self-update, config reload) - which would strand the old
|
|
@@ -384,12 +477,23 @@ Staleness semantics differ by kind: an opencode row's stale age means its
|
|
|
384
477
|
harness stopped beating (broadcast skips it), while an mcp row's age only means
|
|
385
478
|
"between turns" (broadcast always delivers).
|
|
386
479
|
|
|
480
|
+
The `/thatch/whois` command (opencode-only action, src/commands.ts) is the
|
|
481
|
+
user-facing identity report: it relays `chat_status` for the invoking
|
|
482
|
+
session's handle, then joins the roster row for topic, project, and the
|
|
483
|
+
`loc:` worktree token - the topic being the live TUI session title, it is
|
|
484
|
+
how the operator maps a roster handle to a visible tab. An argument looks
|
|
485
|
+
up another session's row instead. It reports only; it never reads mail or
|
|
486
|
+
mutates anything.
|
|
487
|
+
|
|
387
488
|
## Defaults
|
|
388
489
|
|
|
389
490
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
390
491
|
threshold two missed beats (60s), re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
391
|
-
per hour, auto-row TTL 7 days.
|
|
392
|
-
|
|
492
|
+
per hour, auto-row TTL 7 days. Environment overrides: `THATCH_CHAT_SPLIT_PART_LEN`
|
|
493
|
+
(auto-split part budget, default 2000, clamped 500..9000) and
|
|
494
|
+
`THATCH_CHAT_MAX_BODY_CHARS` (whole-body acceptance ceiling, default 20000);
|
|
495
|
+
both are resolved once at module load and sanitized to positive integers
|
|
496
|
+
within their clamps - invalid values silently fall back to the default.
|
|
393
497
|
|
|
394
498
|
## Source files
|
|
395
499
|
|
|
@@ -418,5 +522,8 @@ them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
|
418
522
|
- `tests/qa/auto/uc-097-chat.ts` - full lifecycle against a mocked poller
|
|
419
523
|
- `tests/qa/auto/uc-099-chat-cli.ts` - `chat list` roster and `chat tail`
|
|
420
524
|
JSONL contract over a seeded DB
|
|
525
|
+
- `tests/qa/auto/uc-116-chat-send-dedupe.ts` - a retried send is suppressed
|
|
526
|
+
- `tests/qa/auto/uc-119-chat-split-long-messages.ts` - an oversized body is
|
|
527
|
+
auto-split into marker-decorated parts
|
|
421
528
|
- `tests/qa/live/uc-098-chat-cross-session.ts` - two real sessions exchange
|
|
422
529
|
a message through the shared DB
|
|
@@ -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
|
|
|
@@ -71,9 +71,10 @@ the shared runtime needs. It is the lifecycle-level sibling of `CoreContext`
|
|
|
71
71
|
| session messages | `client.session.messages` | `session.context` | mapped into the v1 `{info: {role}, parts}` shape, so the wrap-up greenlight check works (the promise domain has no `message` accessor) |
|
|
72
72
|
| session list | `client.session.list` | none | degrade (`-c` resume listing loses its data source) |
|
|
73
73
|
| compaction trigger | `client.tui.executeCommand("session_compact")` | none | degrade (no `session.compact` on the promise domain's Pick, and the built-in `/compact` is a TUI palette action calling the server endpoint directly - the server command registry only knows config/plugin-registered names; the wrap-up checklist and flush still run, the automatic compaction is skipped) |
|
|
74
|
-
| toasts | `client.tui.showToast` | none reachable | degrade (the `tui.toast.show` event has no producer surface from the promise context;
|
|
75
|
-
| TUI app exit | `client.tui.publish` | none | degrade (the wrap-up exit's checklist and flush run; the exit does not - upstream [anomalyco/opencode#50984](https://github.com/anomalyco/opencode/issues/50984). On v2 the right target closes THIS session's tab (tab-scoped), not `app.exit`: the daemon hosts every tab; the
|
|
76
|
-
|
|
|
74
|
+
| toasts | `client.tui.showToast` | none reachable | degrade (the `tui.toast.show` event has no producer surface from the promise context; the `./tui` companion entrypoint now EXISTS for the session-tab feature - extending it to publish toasts is the remaining lift) |
|
|
75
|
+
| TUI app exit | `client.tui.publish` | none | degrade (the wrap-up exit's checklist and flush run; the exit does not - upstream [anomalyco/opencode#50984](https://github.com/anomalyco/opencode/issues/50984). On v2 the right target closes THIS session's tab (tab-scoped), not `app.exit`: the daemon hosts every tab; the `./tui` entrypoint's `ui.tabs.close` is the known lift candidate and the session-tab rpc bridge is the transport) |
|
|
76
|
+
| session tabs | n/a | rpc event bridge | NEW: `thatch_session_tab` (v2Only) spawns a detached subordinate session (create -> move -> chat-register -> emit `rpc.thatch-tabs.tab-opened` -> prompt); the `./tui` CLI plugin consumes the event and calls `ui.tabs.open` (open-without-focus, idempotent). The deferred `thatch_session_tab_close` will emit `rpc.thatch-tabs.tab-closed {sessionID, chatName?}` - the shape is reserved and consumed by the generalized-session-heartbeat plan |
|
|
77
|
+
| tab close | n/a | no session.deleted on v2 tab close | bounded 2026-10-07: watcher death detection cancels a closed tab's watchers once its pending events age past `THATCH_WATCH_DEATH_MINUTES` (default 120) while deliveries throw 404-class errors, and announces the death to live same-project sessions; the roster row itself still lingers until the reaper. NOTE: an earlier version of this row claimed deliveries "fail their idle gate" - wrong: the v2 gate opens (the status stub reads as idle) and the PROMPT fails; same leak, different link |
|
|
77
78
|
| wake gate after reload | `session.status` event map | map rehydrates empty | known gap: v2's fetchStatuses stub is structural (the promise context has no session.status), so in the reload window - until the session's next status event - the proactive-prompt gate is fully open |
|
|
78
79
|
| dispose | `dispose` hook | cleanup returned from `setup` | must be idempotent: v2 auto-reloads plugins on file change, and a non-idempotent cleanup doubles pollers/pumps/nudges. Volatile runtime state (extraction buffer + accepted entries, child bookkeeping, wrap-up arms, watcher definitions) is journaled to the `runtime_state` table - instance-scoped by the writing location's directory - and rehydrated on the next `setup()`: rows from the same process AND directory (a reload) all restore; child rows carry a kind marker (extraction vs task-dispatched sub-agent, defaulting to extraction for pre-marker rows) so only extraction children restore the `extracting`/`extractionChildren` sets; rows from a dead process are pruned unless the session is the `-c`/`-s` startup resume, which inherits recovery-safe state only (buffer requeued, no live child bookkeeping - the child died with the old process). Exception: a dead process's watcher definitions are kept as DORMANT rows (no polling, no delivery) instead of pruned - resuming the session re-arms them on its first message (a `chat.message` scan: own rows re-arm, same-directory rows of still-dead sessions earn the live session a one-line death notice gated on the owning pid being dead, rows past the watcher TTL plus a 24h grace age out). Restored sessions on v1 get a synthetic noReply re-attach notice (v2 has no turn-free delivery and skips it). The chat poller re-hosts the instance's journaled hosted set on reload, so pending chat mail wakes sessions the reload left asleep and heartbeats keep the reaper from reaping live registrations. The embedding model is refcounted per db path, so N location instances share one resident model |
|
|
79
80
|
|
|
@@ -112,15 +113,29 @@ key (`plugin` vs `plugins`) is auto-migrated by v2. The only user-owned file
|
|
|
112
113
|
with a content change is the dev-session shim, and its new dual-shape content
|
|
113
114
|
loads under both hosts.
|
|
114
115
|
|
|
116
|
+
The session-tab feature adds a second user-owned artifact: the TUI-side
|
|
117
|
+
plugin entry. A local FILE plugin shim carries a `server` entrypoint only
|
|
118
|
+
(v2's `plugin/module.ts`), and the TUI's plugin discovery loads directories
|
|
119
|
+
and symlinks, never plain files - so the dev-checkout install must be a
|
|
120
|
+
DIRECTORY shim: `thatch/index.ts` (the dual-shape re-export, unchanged) plus
|
|
121
|
+
`thatch/tui.ts` (re-exporting the TUI plugin). DELETE the old `thatch.ts`
|
|
122
|
+
file in the same step - a file and a directory shim coexisting dies with
|
|
123
|
+
`duplicate instance plugin ids: thatch` before any plugin code runs (the
|
|
124
|
+
duplicate check is in activation, so nothing inside the plugin can
|
|
125
|
+
self-heal it).
|
|
126
|
+
|
|
115
127
|
## Source files
|
|
116
128
|
|
|
117
129
|
- `src/index.ts` -- dual entry, merged default export, lazy wrappers
|
|
118
130
|
- `src/opencode/v1.ts` -- v1 hooks adapter
|
|
119
131
|
- `src/opencode/v2.ts` -- v2 promise-context adapter + capability impl
|
|
132
|
+
- `src/opencode/tui-plugin.ts` -- v2 TUI CLI plugin (the `./tui` entrypoint: consumes the session-tab rpc events, drives `ui.tabs`)
|
|
133
|
+
- `src/session-tab-shared.ts` -- the cross-entry session-tab contract (rpc definition, prompt builder, validators)
|
|
120
134
|
- `src/capabilities.ts` -- `HostCapabilities`, `capabilitiesFromClient`
|
|
121
135
|
- `src/runtime.ts` -- the shared runtime (all behavioral logic)
|
|
122
136
|
- `src/os-args.ts` -- pure argv helpers
|
|
123
137
|
- `tests/opencode-v2.test.ts` -- v2 adapter contract tests (mocked context)
|
|
138
|
+
- `tests/session-tab-shared.test.ts` -- session-tab validators, rpc definition shape, TUI guard logic
|
|
124
139
|
- `tests/plugin.test.ts` -- v1 adapter contract tests (mocked client)
|
|
125
140
|
|
|
126
141
|
## Interactions with other features
|
|
@@ -6,8 +6,9 @@ calls and updates it based on user feedback.
|
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
`prediction_delete`. All defined in
|
|
9
|
+
- Tools: `prediction_query`, `prediction_update`, `prediction_list`,
|
|
10
|
+
`prediction_delete`, `prediction_mark_checked`. All defined in
|
|
11
|
+
`src/tool-defs.ts`.
|
|
11
12
|
- Auto-fires at every user message: embeds the prompt, cosine-matches against
|
|
12
13
|
matchers, injects scored predictions as a synthetic nudge part.
|
|
13
14
|
- Bayesian confidence model — a graded score that is reinforced or weakened
|
|
@@ -101,6 +102,33 @@ Runs in a single DB transaction:
|
|
|
101
102
|
Returns a status line with final confidence and confirm/disconfirm counts.
|
|
102
103
|
Embeds both the matcher and prediction text.
|
|
103
104
|
|
|
105
|
+
### Dedup pass (corpus-level consolidation)
|
|
106
|
+
|
|
107
|
+
Write-time dedup (0.85, both stores) catches near-identical statements at
|
|
108
|
+
creation. Wording-different duplicates — the same preference restated weeks
|
|
109
|
+
later — land below that floor and accumulate. The dedup pass finds them:
|
|
110
|
+
|
|
111
|
+
- `findPredictionDuplicates(store, threshold = 0.70)` in `src/prediction.ts`:
|
|
112
|
+
pairwise cosine over the store's predictions, skipping pairs already
|
|
113
|
+
adjudicated in `prediction_dedup_pairs`. The threshold is band placement:
|
|
114
|
+
[0.85, 1] is write-time territory, below 0.70 short-statement embeddings
|
|
115
|
+
are too noisy to call duplicates.
|
|
116
|
+
- Pairs are keyed on the canonical SORTED id pair — (A,B) and (B,A) are one
|
|
117
|
+
row, the same contract as the memory side's `dedup_pairs`.
|
|
118
|
+
- `prediction_mark_checked(id_a, id_b, status)` records the verdict:
|
|
119
|
+
`duplicate` (merged or treat as one) or `distinct` (both legitimate).
|
|
120
|
+
Distinct verdicts are the load-bearing case — a destroyed duplicate is
|
|
121
|
+
gone, but a judged-distinct pair re-surfaces every session start unless
|
|
122
|
+
recorded.
|
|
123
|
+
- The hygiene report surfaces `N prediction duplicate pairs pending review`;
|
|
124
|
+
the agent merges (fold the loser's matchers with `prediction_update`, then
|
|
125
|
+
`prediction_delete` the loser) or judges distinct, and marks the pair
|
|
126
|
+
either way. Scans are per-store: cross-store writes are edge-linked at
|
|
127
|
+
write time instead of duplicated.
|
|
128
|
+
- The co-fire refinement (behavioral evidence layered onto this method once
|
|
129
|
+
fire tracking exists) is designed in
|
|
130
|
+
[../../plans/prediction-consolidation.md](../../plans/prediction-consolidation.md).
|
|
131
|
+
|
|
104
132
|
### Auto-fire (opencode: `chat.message` hook)
|
|
105
133
|
|
|
106
134
|
- Reuses the prompt embedding already computed for the recall nudge — no extra
|
|
@@ -190,11 +218,11 @@ that user preferences are stable, not time-sensitive.
|
|
|
190
218
|
|
|
191
219
|
| File | Role |
|
|
192
220
|
|------|------|
|
|
193
|
-
| `src/prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names |
|
|
221
|
+
| `src/prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names, plus the corpus dedup pass (`findPredictionDuplicates`, pair verdicts) |
|
|
194
222
|
| `src/scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence (shared base for prediction and behavior) |
|
|
195
223
|
| `src/db.ts` | Prediction tables, `scorePredictionNudge` |
|
|
196
|
-
| `src/tool-defs.ts` |
|
|
197
|
-
| `src/prompts.ts` | `predictionNudge` formatting, verb selection |
|
|
224
|
+
| `src/tool-defs.ts` | Five prediction tools (`query`, `update`, `list`, `delete`, `mark_checked`) |
|
|
225
|
+
| `src/prompts.ts` | `predictionNudge` formatting, verb selection, dedup-merge guidance |
|
|
198
226
|
| `src/sideband.ts` | `predictions` method for the MCP path |
|
|
199
227
|
|
|
200
228
|
## Database tables
|
|
@@ -241,6 +269,15 @@ prediction_provenance(
|
|
|
241
269
|
created_at TEXT,
|
|
242
270
|
FOREIGN KEY (prediction_id) REFERENCES predictions(id) ON DELETE CASCADE
|
|
243
271
|
)
|
|
272
|
+
|
|
273
|
+
prediction_dedup_pairs(
|
|
274
|
+
store TEXT,
|
|
275
|
+
id_a TEXT,
|
|
276
|
+
id_b TEXT,
|
|
277
|
+
status TEXT,
|
|
278
|
+
checked_at TEXT,
|
|
279
|
+
PRIMARY KEY (store, id_a, id_b)
|
|
280
|
+
)
|
|
244
281
|
```
|
|
245
282
|
|
|
246
283
|
## Key invariants
|
|
@@ -254,3 +291,5 @@ prediction_provenance(
|
|
|
254
291
|
`chat.message` hook and MCP sideband) prevents scoring drift.
|
|
255
292
|
5. **Matcher text is embedded raw.** No header prepend — unlike
|
|
256
293
|
`memory_remember`, which prepends `# label\n\n` before embedding.
|
|
294
|
+
6. **Adjudicated dedup pairs never re-surface.** Verdict rows are keyed on
|
|
295
|
+
the canonical sorted id pair; `findPredictionDuplicates` skips them.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Session tabs (coordinated subordinates)
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`thatch_session_tab` (opencode v2 only) lets a coordinating LLM spawn a
|
|
6
|
+
detached subordinate LLM session in a new TUI tab. The subordinate is a full
|
|
7
|
+
peer session: it runs its own agent loop across many turns, in a directory
|
|
8
|
+
of the coordinator's choosing, while the coordinator keeps working. The user
|
|
9
|
+
sees the subordinate's tab appear beside the coordinator's, unfocused.
|
|
10
|
+
|
|
11
|
+
This is NOT the `task` tool: task subagents run inside the dispatcher's turn
|
|
12
|
+
and return their result inline. A tab subordinate outlives the dispatching
|
|
13
|
+
turn and is supervised over cross-session chat.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
coordinator session (LLM)
|
|
19
|
+
| tool call: thatch_session_tab {prompt, title, worktree|directory}
|
|
20
|
+
v
|
|
21
|
+
v2 adapter (src/opencode/v2.ts, buildSessionTabHost via CoreContext.sessionTabHost)
|
|
22
|
+
| validate (tool-defs: title, exactly-one location arg, fs existence,
|
|
23
|
+
| worktree same-main-checkout identity)
|
|
24
|
+
| 1. session.create({title, metadata: {thatch: {...}}, location})
|
|
25
|
+
| 2. worktree flow: session.move({directory: worktree}) (idle -> steer = immediate)
|
|
26
|
+
| 3. db.registerChatSession(subordinate) (participant, never machinery-marked)
|
|
27
|
+
| 4. rpc emit rpc.thatch-tabs.tab-opened {sessionID, directory}
|
|
28
|
+
| 5. session.prompt({text: preamble + task})
|
|
29
|
+
v
|
|
30
|
+
SSE event feed (isOpenCodeEvent passes rpc.*; fans out across all locations)
|
|
31
|
+
v
|
|
32
|
+
TUI CLI plugin (src/opencode/tui-plugin.ts, loaded from package exports "./tui")
|
|
33
|
+
| data.listen -> type filter -> directory guard (mirrors the built-in
|
|
34
|
+
| tui.* handlers: the event routes by the coordinator instance's
|
|
35
|
+
| directory) -> data.session.sync -> ui.tabs.open(sessionID)
|
|
36
|
+
v
|
|
37
|
+
Tab strip: subordinate tab appears, unfocused
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Key properties:
|
|
41
|
+
|
|
42
|
+
- **v2-only by construction.** The ToolDef carries `v2Only: true`; the v1
|
|
43
|
+
adapter (`src/tools.ts`) and the MCP server (`src/mcp.ts`) filter it. The
|
|
44
|
+
system prompt lists the tool only when the runtime is built with
|
|
45
|
+
`v2Tools: true` - a prompt listing a nonexistent tool is the
|
|
46
|
+
first-call-mistake class.
|
|
47
|
+
- **The event routes by the coordinator's location**, so the tab opens in
|
|
48
|
+
the coordinator's window under any `tabs.scope` - even when the session
|
|
49
|
+
lives in a worktree or temp dir. Tab ownership is by window, not by
|
|
50
|
+
session location.
|
|
51
|
+
- **The move precedes the emit**: a failed move leaves no stranded open tab
|
|
52
|
+
(placement is identical either way because routing is by coordinator
|
|
53
|
+
location).
|
|
54
|
+
- **No ack**: a headless run, a connecting TUI, or `tabs.mode: off` means
|
|
55
|
+
the event lands nowhere (harmlessly). The tool response says "tab
|
|
56
|
+
requested"; the session exists and is findable in the sessions list
|
|
57
|
+
regardless.
|
|
58
|
+
- **The subordinate is a chat participant**, deliberately never marked
|
|
59
|
+
machinery: pre-registration (idempotent, like an explicit chat_register)
|
|
60
|
+
gives the coordinator its chat name in the tool response, and the
|
|
61
|
+
auto-register converges on the same name. Gated on the chat-enabled
|
|
62
|
+
config like every other registration path: with chat off the
|
|
63
|
+
subordinate still spawns, but no roster row is written and the tool
|
|
64
|
+
response reports no chat name. `validateTitle` rejects the
|
|
65
|
+
machinery title so `isMachinerySessionTitle` can never misfire. A
|
|
66
|
+
mid-flow failure (create, move, emit, prompt) throws with the created
|
|
67
|
+
session id and the completed steps in the message - v2 has no session
|
|
68
|
+
delete, so the stranded session can at least be named and reported.
|
|
69
|
+
- **The worktree flow boots a second per-location thatch instance** (shared
|
|
70
|
+
db): the subordinate's pollers, nudges, extraction, wrap-up, and alerts
|
|
71
|
+
run in the worktree location's instance, and chat wake for the idle
|
|
72
|
+
subordinate depends on that instance being alive. The watcher poller
|
|
73
|
+
handoff is per location (`WatcherRegistry.#live` is keyed by directory):
|
|
74
|
+
the subordinate's instance booting never stops the coordinator's
|
|
75
|
+
poller - each location's instance polls only its own directory's
|
|
76
|
+
watchers. The pump caches the subordinate's location from the
|
|
77
|
+
tab-opened payload's directory field (its final, post-move directory),
|
|
78
|
+
never from the rpc envelope's location stamp (that is the publisher's
|
|
79
|
+
directory), so the subordinate's location-less execution events route
|
|
80
|
+
to the instance that owns it.
|
|
81
|
+
|
|
82
|
+
## The cross-entry contract
|
|
83
|
+
|
|
84
|
+
`src/session-tab-shared.ts` (SDK-free, per the isolation rule in
|
|
85
|
+
`src/index.ts`) holds the rpc definition (`thatch-tabs`: `tab-opened`,
|
|
86
|
+
`tab-closed` - the latter emitted by the close tool (below) and consumed
|
|
87
|
+
by the generalized-session-heartbeat plan), the subordinate prompt builder,
|
|
88
|
+
and the validators. The TUI entrypoint may import only
|
|
89
|
+
`@opencode/plugin/tui` and this module.
|
|
90
|
+
|
|
91
|
+
## Interactions
|
|
92
|
+
|
|
93
|
+
- **Chat**: the subordinate registers in its final directory's repo slug (a
|
|
94
|
+
worktree resolves to the main checkout's slug), so coordinator and
|
|
95
|
+
subordinate share a chat scope across the move.
|
|
96
|
+
- **Extraction/wrap-up/alerts**: the subordinate is a normal session owned
|
|
97
|
+
by its location's instance; the coordinator observes it via chat, not the
|
|
98
|
+
event pump (its post-move events fail the pump's directory filter
|
|
99
|
+
deliberately - the `childSessions` forwarding set is for extraction
|
|
100
|
+
children, and adding moved sessions would double-process their events).
|
|
101
|
+
- **Session metadata**: the created session carries
|
|
102
|
+
`metadata.thatch.{coordinatedBy, coordinatorSessionID, worktree?}`. The
|
|
103
|
+
host inherits parent metadata onto children - the subordinate's own
|
|
104
|
+
extraction children carry the marker too; treat inherited markers as
|
|
105
|
+
inherited, not first-party.
|
|
106
|
+
|
|
107
|
+
## Closing a subordinate's tab
|
|
108
|
+
|
|
109
|
+
`thatch_session_tab_close {session}` (the target: the subordinate's chat
|
|
110
|
+
name or session id, resolved name-first through the chat roster) emits
|
|
111
|
+
`rpc.thatch-tabs.tab-closed {sessionID, chatName?}` over the same rpc
|
|
112
|
+
bridge. The TUI plugin consumes it symmetrically with open: the directory
|
|
113
|
+
guard applies, and `ui.tabs.close(sessionID)` closes the tab in every
|
|
114
|
+
window showing one (windows not showing it no-op). Close is TAB-LEVEL: the
|
|
115
|
+
session survives, the strip's reopen stack can restore it, and chat history
|
|
116
|
+
is untouched.
|
|
117
|
+
|
|
118
|
+
Two consumers ride the event: the death-detection machinery
|
|
119
|
+
(generalized-session-heartbeat) treats a tool-initiated close as a
|
|
120
|
+
CONFIRMED close - the closed session's watchers cancel immediately instead
|
|
121
|
+
of waiting on the 2h delivery-failure heuristic - and the coordinator's own
|
|
122
|
+
workflow closes dispatched subordinates once their work is verified. The
|
|
123
|
+
pump routes the close by the CLOSED session's own directory (resolved from
|
|
124
|
+
the pump's session-location cache, or live through the session API), not
|
|
125
|
+
by the event's location stamp: the stamp names the publishing coordinator
|
|
126
|
+
instance, while the watchers and status bookkeeping live in the instance
|
|
127
|
+
that owns the session - its post-move location.
|
|
128
|
+
|
|
129
|
+
## Install requirement (the `./tui` entrypoint)
|
|
130
|
+
|
|
131
|
+
A local FILE plugin shim gets a `server` entrypoint only
|
|
132
|
+
(`packages/core/src/plugin/module.ts` at v2), and the TUI's plugin discovery
|
|
133
|
+
loads directories and symlinks, never plain files. The dev-checkout install
|
|
134
|
+
must therefore be a directory shim (`thatch/index.ts` + `thatch/tui.ts`),
|
|
135
|
+
and the old file shim must be DELETED in the same step - coexisting shims
|
|
136
|
+
die with `duplicate instance plugin ids: thatch` before any plugin code can
|
|
137
|
+
run. npm installs need the `exports` map to include `.` (the bare-specifier
|
|
138
|
+
path) alongside `./tui`, and `@opencode/plugin` is an optional peer
|
|
139
|
+
dependency for the TUI entry's runtime resolution.
|
|
140
|
+
|
|
141
|
+
## Testing
|
|
142
|
+
|
|
143
|
+
- Unit: `tests/session-tab-shared.test.ts` (validators, definition shape,
|
|
144
|
+
guard logic), `tests/opencode-v2.test.ts` (the execute flows' ordering via
|
|
145
|
+
the `makeContext` double with `session.move` + the `rpc` domain).
|
|
146
|
+
- QA: uc-059 (tool list + MCP leak check), uc-014/uc-060 (skill counts),
|
|
147
|
+
uc-119 (the live spawn flow through the docker sandbox).
|
|
148
|
+
- Live smoke (recorded 2026-10-08, ALL SCENARIOS PASSED): headless legs -
|
|
149
|
+
directory variant, worktree variant (the move persisted in the session
|
|
150
|
+
row + metadata), headless no-tab; TUI-visual legs (a real container
|
|
151
|
+
attach) - the subordinate's tab appeared unfocused, the title rendered,
|
|
152
|
+
the subordinate responded, and the persisted strip state matched; kill +
|
|
153
|
+
re-attach restored both tabs (restart persistence). Sandbox note: mkdir
|
|
154
|
+
the /qa host dir BEFORE `docker run -v` - a missing host path is created
|
|
155
|
+
inside the Docker VM, invisible to the mac.
|
|
@@ -18,6 +18,8 @@ resolution fit together.
|
|
|
18
18
|
hooks, and skills for Cursor
|
|
19
19
|
- `--global` flag --- installs to user-scoped config dirs instead of
|
|
20
20
|
project-local
|
|
21
|
+
- `--skills-only` flag --- refreshes just the skill files (same host and
|
|
22
|
+
scope rules), skipping MCP config, instructions, hooks, and commands
|
|
21
23
|
- **Marker system** --- idempotent replacement of the thatch instruction block
|
|
22
24
|
in `CLAUDE.md` or `AGENTS.md` via start/end markers
|
|
23
25
|
- **`checkSetup`** --- detects installed, not-installed, or markers-broken at
|
|
@@ -35,6 +37,11 @@ resolution fit together.
|
|
|
35
37
|
|
|
36
38
|
At least one of `--claude` or `--cursor` is required. Both can be passed
|
|
37
39
|
together. `--global` applies to whichever host or hosts are selected.
|
|
40
|
+
`--skills-only` narrows the run to the skill install (`setupSkillsOnly`
|
|
41
|
+
in `src/setup.ts`): it reuses the same path resolution and other-scope
|
|
42
|
+
probe as the full setup paths but only calls `installSkills`, so it is
|
|
43
|
+
safe to run in an already-set-up host when only the skill files need
|
|
44
|
+
refreshing.
|
|
38
45
|
|
|
39
46
|
### Claude Code setup (`setupClaudeCode` in `src/setup.ts`)
|
|
40
47
|
|
|
@@ -108,6 +115,11 @@ Cursor takes priority because Cursor also sets `CLAUDE_PROJECT_DIR` as an
|
|
|
108
115
|
alias. Without the priority check, Cursor sessions would be misidentified as
|
|
109
116
|
Claude Code.
|
|
110
117
|
|
|
118
|
+
Separately, the project directory for store/chat identity resolution
|
|
119
|
+
follows `THATCH_PROJECT_DIR ?? CURSOR_PROJECT_DIR ?? CLAUDE_PROJECT_DIR
|
|
120
|
+
?? cwd` in the MCP server; project-local setups pin `THATCH_PROJECT_DIR`
|
|
121
|
+
into the written MCP config.
|
|
122
|
+
|
|
111
123
|
`checkSetup` looks for start and end markers in the host's instructions file
|
|
112
124
|
--- `CLAUDE.md` for Claude Code, `AGENTS.md` for Cursor. It checks local first,
|
|
113
125
|
then global. Local takes priority.
|