@jeffober/thatch 0.1.41 → 0.1.43

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.
@@ -3,10 +3,11 @@
3
3
  Watchers are the general mechanism for event-driven notifications from
4
4
  external sources: the model registers a watch on something outside the
5
5
  session, thatch polls it in the background, and the plugin prompts the
6
- session when a watched event happens. Two sources ship: GitHub pull
7
- requests (source `pr`) and GitHub branches (source `branch`, for
8
- watching CI and workflow runs on main). The registry, poller, and
9
- delivery are source-agnostic - a new source adds a fetch-and-diff
6
+ session when a watched event happens. Three sources ship: GitHub pull
7
+ requests (source `pr`), GitHub branches (source `branch`, for watching
8
+ CI and workflow runs on main), and local shell commands (source
9
+ `command`, for waiting on arbitrary conditions). The registry, poller,
10
+ and delivery are source-agnostic - a new source adds a fetch-and-diff
10
11
  function pair and event types.
11
12
 
12
13
  User-facing behavior is documented in [docs/user/watchers.md](../../user/watchers.md).
@@ -14,13 +15,16 @@ User-facing behavior is documented in [docs/user/watchers.md](../../user/watcher
14
15
  ## What it does
15
16
 
16
17
  - `thatch_watch_create` (PRs) / `thatch_watch_branch_create` (branches) /
17
- `thatch_watch_list` / `thatch_watch_cancel` tools (all opencode-only)
18
+ `thatch_watch_command_create` (commands) / `thatch_watch_list` /
19
+ `thatch_watch_cancel` tools (all opencode-only)
18
20
  - A background poller in the plugin process diffs each watcher's target
19
21
  against its last-seen state
20
22
  - Delivery prompts the session with a synthetic part - the same
21
23
  mechanism opencode uses for background task completions
22
- - Notifications carry pointer data only (author, URL, counts); the
23
- model fetches content on demand with `gh`
24
+ - Notifications carry pointer data plus machine status (author, URL,
25
+ check names and conclusions, exit codes); external content is fetched
26
+ on demand with `gh` or by re-running a watched command, never
27
+ delivered
24
28
 
25
29
  ## The process-lifetime decision
26
30
 
@@ -72,6 +76,12 @@ attempt delivery. Poll errors are per-watcher; one bad PR never
72
76
  blocks the others. The diff is a pure function, so it is unit-tested
73
77
  without any network access.
74
78
 
79
+ The interval is surfaced to the model through the registry's
80
+ `pollSeconds` getter: watch-creation output states when the first
81
+ poll lands, and notifications carry a cadence line, so a model
82
+ choosing between a watcher and an in-turn poll compares real numbers
83
+ even when `THATCH_WATCH_POLL_SECONDS` overrides the default.
84
+
75
85
  Event detection:
76
86
 
77
87
  - comments by id monotonicity (fetch is sorted descending)
@@ -89,6 +99,57 @@ Event detection:
89
99
  A diff emits at most 10 events per watcher per cycle, so comment
90
100
  floods collapse into one batch.
91
101
 
102
+ ### One-shot watches
103
+
104
+ `watch_create` and `watch_branch_create` accept `once: true`. The
105
+ watcher cancels itself when its first matching event is detected, not
106
+ when it is delivered - the event still queues and delivers through the
107
+ normal pending path, so a session busy at fire time does not lose the
108
+ notification. Cancellation at detection time is what keeps a "tell me
109
+ when this run finishes" request from leaving a standing watch polling
110
+ a target nobody is waiting on. `watch_list` marks one-shot watchers
111
+ with a `[once]` tag, and the registration output states the mode.
112
+
113
+ ### Command watches
114
+
115
+ The command source stretches the fetch/diff recipe in two ways:
116
+
117
+ 1. **The fetch has side effects.** `runWatchedCommand()` executes the
118
+ watched command via `bash -c` in the project directory each cycle.
119
+ This is the point - the command IS the condition - but it means
120
+ command watchers must not block: the tool description and system
121
+ prompt state the contract (fast, idempotent status check; never
122
+ `gh run watch` or `tail -f`), and a per-run kill timeout
123
+ (default 30s, `THATCH_WATCH_COMMAND_TIMEOUT_SECONDS`) bounds a
124
+ hung command's cost to one partial poll cycle - other watchers
125
+ wait out the kill rather than stalling, but a blocking command
126
+ still burns its timeout slice every cycle. A timed-out run means
127
+ "not done yet": the last-seen exit is left untouched and the watch
128
+ keeps polling.
129
+ 2. **The diff is a trivial exit-code check.** The command is a
130
+ condition variable, not a data pipe: stdout is drained (a full
131
+ pipe buffer would deadlock the child) but discarded, and stderr is
132
+ kept only for registration-time error messages. The notification
133
+ carries the exit code and duration; command output can be external
134
+ content, so it never rides a notification. This is the strictest
135
+ application of the notification content rule. The pipe drains are also
136
+ raced against a hard deadline, because a
137
+ command that backgrounds a long-lived child leaves a grandchild
138
+ holding the pipe write-ends past the kill - without the race, the
139
+ drains never resolve and the shared poll cycle stalls forever.
140
+
141
+ `createCommand()` runs the command once at registration as baseline
142
+ validation, like `createPr()`'s baseline fetch: exit 0 at baseline
143
+ means the condition is already met (refused, so the caller proceeds
144
+ now), exit 127 means the command does not exist (refused with the
145
+ stderr tail), and a spawn failure fails registration with a real
146
+ error. A baseline run that times out records the 124 sentinel as the
147
+ last-seen exit - the killed process may have exited 0 while dying, and
148
+ the watcher's first real exit 0 must fire. Registered command
149
+ watchers carry `once: true` inherently - "run until it returns 0,
150
+ then notify, then stop" is the whole concept. The `CommandRunner` is
151
+ injectable like `GhRunner`, so tests never spawn processes.
152
+
92
153
  ### Delivery
93
154
 
94
155
  Events queue in an in-memory pending map keyed by session. Delivery
@@ -101,9 +162,15 @@ on the session with a synthetic text part built by
101
162
  `watcherNotificationNudge()`. This triggers a full model turn - the
102
163
  model reads the notification and decides whether to act. The
103
164
  notification text borrows the background-task-completion framing
104
- (system event, not user input, not approval to advance other work),
105
- following the same load-bearing wording discipline as the extraction
106
- nudge.
165
+ (system event, not user input, not approval to advance other work)
166
+ with one carve-out: a watch registered to gate work the user already
167
+ greenlit (for example "wait for CI, then merge") makes the
168
+ notification the continuation signal for exactly that work. The
169
+ carve-out lives in the nudge itself, not only the system prompt,
170
+ because the nudge is what the model reads at the act-or-wait decision
171
+ point. The wrapper also states that check-run and workflow
172
+ conclusions are already in the event summaries, so gh fetches are
173
+ needed only for logs and bodies.
107
174
 
108
175
  Failed or gated deliveries stay pending and retry on the next poll
109
176
  cycle or when the session next goes idle (the event hook calls
@@ -114,7 +181,10 @@ cycle or when the session next goes idle (the event hook calls
114
181
  `pr_comment`, `pr_review_comment`, `pr_review_reply`,
115
182
  `pr_review_resolved`, `pr_commit`, `pr_status`, `pr_description`,
116
183
  `pr_ci`. The tool's `events` argument selects a subset; the default is
117
- all eight.
184
+ all eight. Branch watchers add `branch_commit`, `branch_ci`, and
185
+ `branch_workflow` (the full vocabulary is `WATCHER_EVENT_TYPES` in
186
+ src/watchers.ts, test-enforced); command watchers register the single
187
+ `command_success` type.
118
188
 
119
189
  ## Interactions with other features
120
190
 
@@ -134,28 +204,48 @@ all eight.
134
204
  - `THATCH_WATCH_POLL_SECONDS` - poll interval (default 60)
135
205
  - `THATCH_WATCH_TTL_MINUTES` - watcher time-to-live (default 480)
136
206
  - `THATCH_WATCH_MAX_PER_SESSION` - active watchers per session
137
- (default 5)
207
+ (default 5; all sources share the budget)
208
+ - `THATCH_WATCH_COMMAND_TIMEOUT_SECONDS` - per-run kill timeout for
209
+ watched commands (default 30)
138
210
 
139
211
  ## Source files
140
212
 
141
- - `src/watchers.ts` - registry, poller, gh CLI runner, diff functions
142
- - `src/tool-defs.ts` - the three watch tool definitions
213
+ - `src/watchers.ts` - registry, poller, gh CLI runner, command runner,
214
+ diff functions
215
+ - `src/tool-defs.ts` - the watch tool definitions (see `TOOL_DEFS`)
143
216
  - `src/index.ts` - registry construction, delivery closure,
144
217
  session.status tracking, session.deleted cleanup, dispose
145
218
  - `src/prompts.ts` - `watcherNotificationNudge()`, system prompt
146
219
  Watchers section
147
220
  - `tests/watchers.test.ts` - registry, poll, and diff unit tests
148
- (mocked gh, no network)
221
+ (mocked gh and command runners, no network)
222
+ - `tests/tool-defs.test.ts` - watch tool execute-function tests
149
223
  - `tests/qa/auto/uc-095-watchers.ts` - end-to-end lifecycle against a
150
224
  mocked gh runner
225
+ - `tests/qa/auto/uc-101-command-watchers.ts` - command-watch lifecycle
226
+ against a mocked command runner
151
227
 
152
228
  ## Adding a new source type
153
229
 
154
- The registry is a discriminated union over sources: `PrWatcher` and
155
- `BranchWatcher` implement the same lifecycle (id, session, repo,
156
- events, expiry, snapshot), and `poll()` dispatches to the source's
157
- fetch/diff pair. Adding a third source means: new event types, a
158
- `fetchXState`/`diffXState` pair, a new arm of the `Watcher` union, a
159
- `#pollOne` branch, and the tool surface for registering it. Keep the
160
- pointer-only notification rule: external content enters the context on
161
- explicit fetch, never via notification.
230
+ The registry is a discriminated union over sources: `PrWatcher`,
231
+ `BranchWatcher`, and `CommandWatcher` implement the same lifecycle
232
+ (id, session, events, expiry, snapshot), and `poll()` dispatches to
233
+ the source's fetch/diff pair. Adding a new source means: new event
234
+ types, a fetch function and diff (or a trivial inline diff like the
235
+ command source's), a new arm of the `Watcher` union, a `#pollOne`
236
+ branch, and the tool surface for registering it. Keep the
237
+ notification content rule: external content enters the context on
238
+ explicit fetch, never via notification. Machine status fields (names,
239
+ conclusions, counts, exit codes) come from the API rather than
240
+ user-written text and belong in event summaries.
241
+
242
+ Two caveats from the command source, worth carrying to future
243
+ sources:
244
+
245
+ - If the source's fetch does anything beyond reading an API, inject a
246
+ runner (like `GhRunner`/`CommandRunner`) so tests never spawn real
247
+ work, and bound it with a timeout so a hung fetch cannot stall the
248
+ shared sequential poll cycle.
249
+ - If the source has no natural URL (a local command has none), emit
250
+ an empty `url` in the event; `watcherNotificationNudge()` omits
251
+ empty URLs rather than rendering a dangling space.
@@ -51,6 +51,15 @@ here first. These are the things that have already cost time.
51
51
  the hook object returned by `server`. Moving it into the `event` handler
52
52
  (where `session.created` lives) silently never fires it — the event bus has
53
53
  no such event. This was dead for weeks because the failure was invisible.
54
+ - **`client.tui.executeCommand` only accepts legacy alias names**
55
+ (`session_compact`, `agent_cycle`, ...). Unknown commands publish
56
+ `{command: undefined}` and silently no-op — no error surfaces. For TUI
57
+ commands without an alias (e.g. `app.exit`), publish
58
+ `tui.command.execute` directly via `client.tui.publish`.
59
+ - **Command markdown written by the plugin is invisible until the next server
60
+ start.** opencode loads config (including command discovery) before
61
+ plugins, so `installOpencodeCommands` self-heals on every load but a
62
+ first-ever install only lands after a restart.
54
63
  - **`tool.execute.after` excludes `skill` and `task` tools**, not just
55
64
  `thatch_*`. Buffering them creates a feedback loop: the nudge triggers a
56
65
  skill load, which gets buffered, which triggers another nudge on the next
@@ -18,10 +18,11 @@ This document maps the feature parity and documents the remaining gaps.
18
18
 
19
19
  | Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
20
20
  |---------|----------------|------------------------|---------------------|--------|
21
- | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR and branch watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The `chat_*` tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel |
21
+ | **Tools** (single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR, branch, and command watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The `chat_*` tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel |
22
22
  | **System prompt** (store names, usage rules) | `experimental.chat.system.transform` — dynamic, repo baked in at runtime | CLAUDE.md static text appended by `thatch setup`; repo auto-detected by the MCP server at startup | AGENTS.md static text appended by `thatch setup`; repo auto-detected at startup | **Approximate** — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
23
23
  | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
24
24
  | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
25
+ | **Wrap-up commands** (`/thatch/compact`, `/thatch/exit`) | Command markdown synced to `$XDG_CONFIG_HOME/opencode/command/thatch/` at plugin init; `command.execute.before` arms the session and the next idle resolves the greenlight token into a TUI action (`session.compact` via `executeCommand`, `app.exit` via `publish`) | No equivalent — no plugin host to arm the check or trigger the action | No equivalent | **Gapped** — needs the plugin's SDK client and TUI control routes |
25
26
  | **Extraction** (buffer tool calls, extract durable facts) | `tool.execute.after` buffers non-thatch, non-skill, non-task tool calls in-process; **direct extraction** is the primary path — when the parent session goes idle with pending buffer entries, the plugin creates a child session via the SDK (`client.session.create` with `parentID`) and prompts it to call `get_extraction_payload` (nudge suppressed via the `extracting` set); the `chat.message` nudge path is a **fallback** used when direct extraction throws | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` peeks the queue and prints the nudge with session ID + fetch tool name (does not drain — the queue persists until a memory write or `extraction_done`) | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Partial** — opencode uses direct extraction (parent idle → child session) as the primary path, with the nudge as fallback; MCP hosts are nudge-only. Both paths use `get_extraction_payload` to fetch the queued interactions as a tool response, keeping the full payload out of the main session's context window. Nudge timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor; opencode's direct extraction fires at **end of turn** (session idle), and the nudge fallback fires at the start of the next turn like MCP |
26
27
  | **Extraction feedback** (toast notifications) | `client.tui.showToast` fires when the extraction child goes idle — shows `[thatch] new: N, updated: M, deleted: K` (success) or `extraction complete — nothing to save` (info); best-effort, silently ignored in headless mode | No TUI connection — MCP hosts have no equivalent | No TUI connection | **Gapped** — MCP hosts cannot show toast notifications |
27
28
  | **Prompt-aware recall nudge** | `chat.message` hook embeds the prompt with the in-process warm model, searches `db.search()`, pushes a nudge part if matches ≥ threshold | `UserPromptSubmit` hook → `thatch flush-tools` connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — sideband socket gives cold hook processes access to the warm MCP server's model |
@@ -61,6 +61,7 @@ description: Extract durable project facts ... Use when ...
61
61
  | `thatch-review-response` | Author-side review response: triage findings, fix bugs one by one, reply on each thread, post a top-level summary comment. |
62
62
  | `thatch-memory-verify` | Fact-check a single memory against the current codebase and correct stale claims. Uses git archaeology to preserve historical context when changes were intentional. |
63
63
  | `thatch-knowledge-export` | Compile everything thatch knows about a topic into a curated markdown file for knowledge transfer. Searches across stores, curates out personal noise, fact-checks code-related memories via `thatch-memory-verify`. |
64
+ | `thatch-clear-writing` | Prose rules for any human-facing text not covered by a dedicated writing skill: PR and ticket comments, documentation, plans, reports, and chat replies. Clarity over compression; dedicated skills win when loaded. |
64
65
 
65
66
  **opencode-only** — the coordinator needs sub-agent support:
66
67
 
@@ -11,7 +11,7 @@ This README is the overview. Each feature has its own guide:
11
11
 
12
12
  - [memory.md](memory.md): persistent memory store
13
13
  - [extraction.md](extraction.md): automatic fact extraction from tool calls
14
- - [watchers.md](watchers.md): event-driven notifications (PR and branch watching)
14
+ - [watchers.md](watchers.md): event-driven notifications (PR, branch, and command watching)
15
15
  - [cross-session-chat.md](cross-session-chat.md): sessions messaging each other on one machine
16
16
  - [notifications.md](notifications.md): banner and voice notifications, user config
17
17
  - [prediction-engine.md](prediction-engine.md): user decision model
@@ -179,6 +179,7 @@ search` - JSONL output designed for piping to `jq` or `grep`.
179
179
  |------|-------------|
180
180
  | `thatch_watch_create` | Watch a GitHub PR for events (comments, review comments and replies, thread resolutions, commits, status changes, description edits, CI completions). The plugin polls in the background and prompts the session when a watched event happens. |
181
181
  | `thatch_watch_branch_create` | Watch a GitHub branch (typically main) for commit landings, CI check-run completions, and workflow runs, with an optional workflow-name filter. |
182
+ | `thatch_watch_command_create` | Wait on any local condition via a shell command that exits 0 when the wait is over. One-shot: one notification on the first exit 0, then it cancels itself. |
182
183
  | `thatch_watch_list` | List this session's active watchers. |
183
184
  | `thatch_watch_cancel` | Cancel one of this session's watchers by id. |
184
185
 
@@ -189,8 +190,8 @@ requirements.
189
190
 
190
191
  | Tool | What it does |
191
192
  |------|-------------|
192
- | `thatch_chat_register` | Join the cross-session chat directory, so other opencode sessions on this machine can message you. Without a name, one is drawn from the built-in pool (recommended - cannot collide); with a name, it is claimed case-insensitively. Pass a topic (one line about what the session is working on) so the roster tells others who to talk to. Safe to call again - same name is a no-op, a new name renames, a new topic updates. |
193
- | `thatch_chat_list` | List registered sessions with a liveness marker (fresh = its process is alive, stale = likely gone), each session's project and topic, and your unread count. |
193
+ | `thatch_chat_register` | Join the cross-session chat directory, so other sessions on this machine can message you. Display names are assigned by thatch, never chosen (a slug plus a never-reused counter), so a name always refers to the same session. Call with no arguments to join, rejoin after unregistering, or check your name. On hosts without session context (Claude Code, Cursor), pass the name your thatch hook printed as `as`. |
194
+ | `thatch_chat_list` | List registered sessions with a liveness marker (fresh = its process is alive, stale = likely gone), how long ago each last checked in, each session's project and checkout kind (project root or linked worktree), its topic, and your unread count. |
194
195
  | `thatch_chat_send` | Send a message to another registered session, by name or session id. The recipient is nudged when its session is idle. |
195
196
  | `thatch_chat_read` | Drain your inbox: all unread messages oldest-first, marked read. Senders identified by display name. |
196
197
  | `thatch_chat_unregister` | Leave the chat directory. |
@@ -231,6 +232,20 @@ Beyond the tools, thatch hooks into opencode itself:
231
232
  notification (`[thatch] recalled 3 memories`, `[thatch] 2 predictions
232
233
  surfaced`, or `[thatch] 1 behavior surfaced`). The toast is ephemeral. It
233
234
  fades after a few seconds.
235
+ - **Wrap-up commands.** `/thatch/compact` and `/thatch/exit` (opencode only)
236
+ run a pre-flight checklist before you compact or close a session: the agent
237
+ flushes pending fact extraction, finishes any promised memory writes, and
238
+ surfaces todos or follow-ups it never addressed. The agent ends its
239
+ response with a greenlight token only when the checklist is clean. Thatch
240
+ watches for the token and then runs the compaction or exits opencode.
241
+ Without the token nothing fires -- a toast points you at the items the
242
+ agent listed, and you re-run the command once they're handled. Text typed
243
+ after the command is passed to the agent ahead of the checklist (say
244
+ goodbye or hand off context: `/thatch/exit Good work - see you tomorrow`).
245
+ A greenlit `/thatch/exit` also unregisters the session from the chat
246
+ directory, so other sessions stop sending it mail. The commands
247
+ self-install into `~/.config/opencode/command/thatch/` at plugin startup;
248
+ a first install becomes available after the next opencode start.
234
249
  - **Compaction context.** When opencode compacts a long session, thatch injects
235
250
  a reminder so the summarized session still knows memory tools exist.
236
251
  - **Prediction auto-fire.** When a prompt matches learned contexts, thatch
@@ -313,6 +328,7 @@ response, walkthrough, and workflow skills that support the review pipeline:
313
328
  | `thatch-pr-description` | Drafts PR descriptions with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, project-context research, clarity checks, and bold/italic emphasis for scanning. |
314
329
  | `thatch-ticket-description` | Drafts ticket or issue descriptions (Linear or Jira) with clear sections, project-context research, clarity checks, and bold/italic emphasis for scanning. |
315
330
  | `thatch-split-overlarge-pr` | Splits already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main. |
331
+ | `thatch-clear-writing` | Prose rules for every human-facing text the dedicated writing skills do not cover: PR and ticket comments, documentation, plans, reports, and chat replies. Clarity over compression. |
316
332
 
317
333
  ### opencode-only skills
318
334
 
package/docs/user/cli.md CHANGED
@@ -47,27 +47,81 @@ Read-only access to the chat directory your opencode sessions share
47
47
  ```bash
48
48
  thatch chat list # who is registered: name, age, project, topic
49
49
  thatch chat tail # follow sent and read events, live
50
- thatch chat tail --once # print the messages sent so far and exit
50
+ thatch chat tail --once # print the last 20 messages and exit
51
+ thatch chat tail --once --limit 50 # a longer backlog (or --limit all)
52
+ thatch chat tail --match "rebase" --match "payments" # body must match both
53
+ thatch chat tail --from al --to bob # substring match on names
54
+ thatch chat tail --since 2026-09-12 --until "2026-09-13 09:00" # a window
51
55
  ```
52
56
 
53
- `chat list` shows every registered session with how long since its last
54
- heartbeat (a large age means the session's process is probably gone), its
55
- project, and the topic it registered with - the fastest way to answer
56
- "which session should I talk to about X?"
57
-
58
- `chat tail` prints the conversation as it happens:
57
+ `chat list` shows every registered session as aligned columns under a
58
+ header row: how long since its last heartbeat (a large age means the
59
+ session's process is probably gone), its status (`fresh` or `STALE`),
60
+ its project, and the topic it registered with - the fastest way to
61
+ answer "which session should I talk to about X?" Color is added only
62
+ when the output is a terminal; a pipe prints plain aligned text.
63
+
64
+ `chat tail` prints the conversation as styled chat cards (labels on a
65
+ colored background, names in color, timestamps dimmed; a drawn rule
66
+ separates cards, with blank lines on either side). After each name, the
67
+ session's registered topic appears in muted italics - so the tail tells
68
+ you not just who is talking but what they are working on. In a
69
+ terminal, message bodies render as markdown: if `glow` (or `gum`) is
70
+ installed, bodies with headings, lists, code blocks, and emphasis are
71
+ styled; plain-text bodies print exactly as sent. Piped output - and a
72
+ system without either tool - always gets the raw body.
59
73
 
60
74
  ```text
61
- [2026-09-12T14:02:11Z] Kurn the Typechecker -> Marlowe the Cherry Picker: CI is green on main
62
- [2026-09-12T21:07:52Z] Marlowe the Cherry Picker -> broadcast: rebasing payments, hold off
63
- [2026-09-12T21:08:03Z] Marlowe the Cherry Picker read a message from Kurn the Typechecker: direct ping
75
+ From Al Go Rithm <cross-session messaging between opencode sessions>
76
+ To Brute the Dream Farrier <diagnosing watcher underuse>
77
+ When 2026-09-12 14:02 America/Denver
78
+
79
+ CI is green on main
80
+ ____________________________________________________________
81
+
82
+ To broadcast
83
+ When 2026-09-12 21:07 America/Denver
84
+
85
+ rebasing payments, hold off
86
+ ____________________________________________________________
87
+
88
+ From Kurn the Typechecker <release QA>
89
+ read Marlowe the Cherry Picker <payments refactor>
90
+ When 2026-09-12 21:08 America/Denver
91
+
92
+ direct ping
64
93
  ```
65
94
 
66
95
  Follow mode runs until Ctrl-C. Read events appear only in follow mode
67
- (they fire when an inbox is drained after the tail started). The chat
68
- itself flows through the agents (the CLI never sends, reads, or
69
- registers on a session's behalf; that happens from opencode sessions
70
- via the `thatch_chat_*` tools).
96
+ (they fire when an inbox is drained after the tail started). When
97
+ timestamps are in your local timezone. The chat itself flows through the
98
+ agents (the CLI never sends, reads, or registers on a session's behalf;
99
+ that happens from opencode sessions via the `thatch_chat_*` tools).
100
+
101
+ The backlog shows the last 20 messages before follow mode takes over.
102
+ When messages are hidden, tail prints a one-line note on stderr saying
103
+ how many there were; `--limit N` raises or lowers the backlog and
104
+ `--limit all` shows everything. All the filters apply to live events
105
+ too, so a filtered tail keeps watching the same way:
106
+
107
+ - `--match PATTERN` filters message bodies. Repeat it to AND patterns
108
+ together: every one must match the message body.
109
+ - `--from NAME` and `--to NAME` match participant names (case doesn't
110
+ matter). A departed sender can only be found as the `unknown`
111
+ rendering the tail shows. Broadcast cards say `broadcast` in the To
112
+ slot, but the row underneath keeps the real recipient: `--to <name>`
113
+ finds the fan-out rows that reached them, while `--to broadcast`
114
+ matches nothing.
115
+ - `--since DT` and `--until DT` bound the window; a message counts if
116
+ it was sent at or after `--since` and before `--until`. The date
117
+ format is `YYYY-MM-DD` with an optional `HH:MM` in your local
118
+ timezone.
119
+
120
+ A `--until` already in the past exits after the backlog (a closed
121
+ window has nothing left to follow); one in the future follows until
122
+ the window closes. Note that the limit hides sent cards, not their
123
+ reads: a hidden message that gets read during follow mode still emits
124
+ its read card.
71
125
 
72
126
  ## Priming a new project
73
127
 
@@ -7,19 +7,22 @@ between tabs all afternoon.
7
7
 
8
8
  ## What it does
9
9
 
10
- Each session opts in with `thatch_chat_register`. The recommended path is to
11
- register without a name: thatch assigns one drawn from a built-in pool of
12
- whimsical names ("Kurn the Typechecker", "Marlowe the Cherry Picker",
13
- "Labcoat 3"), so you never think about naming and it can never collide. You
14
- can also claim a custom name; names are unique case-insensitively, so
15
- "Landru" and "landru" are the same name. Pass a topic while you are at it -
16
- one line about what the session is working on ("QAing the v1.41 release",
17
- "planning the payments refactor") - because that is what turns the roster
18
- into a who-is-working-on-what board. Once registered:
10
+ opencode sessions join the directory automatically, on their first idle
11
+ moment. Names are assigned by thatch, never chosen: a lowercase slug of the
12
+ session's title plus a counter (a session titled "Fix auth bug" becomes
13
+ `fix-auth-bug-00001`). The counter only ever increments, so a name is
14
+ minted exactly once per machine - pruning an old session never reissues its
15
+ name to someone else, and the same name always refers to the same session.
16
+ The live title rides along as the session's topic (refreshed as the
17
+ auto-titler converges), so the roster reads as a who-is-working-on-what
18
+ board without anyone filling in forms. A session can also join or rejoin
19
+ explicitly with `thatch_chat_register` (no arguments) - useful after
20
+ `chat_unregister`, or just to check your assigned name. Once registered:
19
21
 
20
22
  - `thatch_chat_list` shows every registered session on the machine, with a
21
- liveness marker (fresh or stale), its project, its topic, and your unread
22
- count
23
+ liveness marker (fresh or stale), how long ago it last checked in, its
24
+ project, whether it runs in the project root or a linked git worktree,
25
+ its topic, and your unread count
23
26
  - `thatch_chat_send` delivers a message to another registered session, by
24
27
  name or session id
25
28
  - `thatch_chat_read` drains the session's inbox
@@ -53,19 +56,14 @@ toggle reveals those output blocks if you want them.)
53
56
 
54
57
  ## How to use it
55
58
 
56
- Tell each session to register, then let them coordinate:
59
+ With auto-registration you usually do nothing: both sessions are already
60
+ in the directory. To introduce them, just tell each session who to talk to:
57
61
 
58
- > Register in the thatch chat. I'll have another session ask you about the
59
- > release status; answer it directly.
62
+ > Ask the other session whether the release has shipped; answer its
63
+ > questions directly.
60
64
 
61
- and in the other session:
62
-
63
- > Register in the thatch chat, then ask the other session whether the
64
- > release has shipped.
65
-
66
- (Registering without arguments is the normal path - each session gets a
67
- pool name automatically. To recognize sessions at a glance, ask for a
68
- custom name instead, or just run `thatch_chat_list` and read the roster.)
65
+ Sessions find each other with `thatch_chat_list` and address each other by
66
+ their assigned names.
69
67
 
70
68
  Received messages are treated as informational, not as your instructions: an
71
69
  agent that gets mail will not treat it as approval to start work, and it
@@ -80,7 +78,63 @@ A session shows as fresh while its opencode process is running. A crashed
80
78
  or closed process stops heartbeat-ing, and the session shows as stale in
81
79
  `thatch_chat_list` after ten minutes. Messages to a stale session wait in
82
80
  its inbox unread - a dead session never reads them. Explicitly closing a
83
- session (deleting it in the TUI) unregisters it immediately.
81
+ session (deleting it in the TUI) unregisters it immediately, and a
82
+ greenlit `/thatch/exit` unregisters the session too: the wrap-up
83
+ checklist calls `chat_unregister`, and the plugin removes the row itself
84
+ when the exit token passes the greenlight check, so other sessions stop
85
+ addressing mail to a process that is about to vanish.
86
+
87
+ ## Turning chat off
88
+
89
+ Set `chat.enabled: false` in the thatch config file
90
+ (`~/.config/thatch/config.json`) - or ask an agent:
91
+ `config_set with chat: { enabled: false }`. When off:
92
+
93
+ - every chat tool refuses, stating that chat is disabled and how to
94
+ re-enable it
95
+ - the poller never starts, so nothing is woken or heartbeated
96
+ - the system prompt omits the chat section entirely, and the hook lines
97
+ print nothing - the feature stops existing as far as any agent can tell
98
+
99
+ Takes effect for the tools immediately (they re-read the config per
100
+ call); a restart applies it to the poller and prompt. Delete the setting
101
+ (or set it true) to turn chat back on.
102
+
103
+ ## Turning auto-registration off
104
+
105
+ Set `chat.autoRegister: false` in the same config file when you want the
106
+ old opt-in behavior: opencode sessions stay out of the directory until
107
+ they call `chat_register`. (The Claude Code / Cursor hook still ensures
108
+ its conversation's identity is registered on each prompt - that is the
109
+ host's identity anchor, not auto-joining; remove the hook with
110
+ `thatch setup` if you want those hosts out too.) Wake delivery for
111
+ registered sessions still works; there is just no automatic joining.
112
+
113
+ ## Leaving the directory
114
+
115
+ `chat_unregister` is a real exit on every host: a leave tombstone
116
+ suppresses re-joining (auto-registration on opencode, the hook's ensure on
117
+ other hosts) until the session explicitly calls `chat_register` again.
118
+ Without the tombstone, the next idle moment would silently re-register the
119
+ session under a fresh name and undo the leave. Rejoining mints a new name
120
+ (assigned names are never reused, even by the same session).
121
+
122
+ ## Security model
123
+
124
+ Chat messages are text written by OTHER agent sessions, delivered into a
125
+ reading agent's context verbatim - a prompt-injection surface by
126
+ construction. Three mitigations:
127
+
128
+ - **Framed at read time.** `chat_read` wraps its output in an explicit
129
+ untrusted-content frame (begin/end fences with a do-not-follow warning).
130
+ The frame exists only in the tool output; the persisted messages are
131
+ untouched.
132
+ - **Names are assigned, not claimed.** Display names are minted by thatch
133
+ with a never-reused counter, so a name cannot be grabbed by an unrelated
134
+ session and an old name cannot be resurrected by an impersonator.
135
+ - **No external content via wake.** The wake notification names senders
136
+ and counts only - bodies flow exclusively through the framed
137
+ `chat_read` surface.
84
138
 
85
139
  ## Requirements and limitations
86
140
 
@@ -95,8 +149,7 @@ session (deleting it in the TUI) unregisters it immediately.
95
149
  - **Loop safety.** Thatch caps wake prompts per recipient per hour, so two
96
150
  agents acknowledging each other cannot ping-pong forever even if both
97
151
  models decide to be chatty.
98
- - **Names are claimable.** Any session can pick any unused name (uniqueness
99
- is case-insensitive). There is no impersonation defense - the assumption
100
- is that every agent on the machine is yours.
101
152
  - **Message retention.** Messages are kept as history; unregistering does
102
- not delete them. There is no automatic pruning yet.
153
+ not delete them. Auto-registered sessions whose host has been gone for a
154
+ week are pruned from the directory (their names are never reused), and
155
+ unread mail addressed to them is removed on a later sweep.
@@ -68,6 +68,7 @@ Skills are split into shared (all hosts) and opencode-only:
68
68
  | `thatch-pr-description` | Drafts PR descriptions with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, using bold and italic emphasis for scanning. |
69
69
  | `thatch-ticket-description` | Drafts ticket or issue descriptions (Linear or Jira) with clear sections and bold/italic emphasis for scanning. |
70
70
  | `thatch-split-overlarge-pr` | Splits already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main. |
71
+ | `thatch-clear-writing` | Prose rules for every human-facing text the dedicated writing skills do not cover: PR and ticket comments, documentation, plans, reports, and chat replies. Clarity over compression. |
71
72
 
72
73
  ### Workflow skills
73
74