@jeffober/thatch 0.1.41 → 0.1.42

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.
@@ -7,15 +7,17 @@ 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
23
  liveness marker (fresh or stale), its project, its topic, and your unread
@@ -53,19 +55,14 @@ toggle reveals those output blocks if you want them.)
53
55
 
54
56
  ## How to use it
55
57
 
56
- Tell each session to register, then let them coordinate:
58
+ With auto-registration you usually do nothing: both sessions are already
59
+ in the directory. To introduce them, just tell each session who to talk to:
57
60
 
58
- > Register in the thatch chat. I'll have another session ask you about the
59
- > release status; answer it directly.
61
+ > Ask the other session whether the release has shipped; answer its
62
+ > questions directly.
60
63
 
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.)
64
+ Sessions find each other with `thatch_chat_list` and address each other by
65
+ their assigned names.
69
66
 
70
67
  Received messages are treated as informational, not as your instructions: an
71
68
  agent that gets mail will not treat it as approval to start work, and it
@@ -82,6 +79,58 @@ or closed process stops heartbeat-ing, and the session shows as stale in
82
79
  its inbox unread - a dead session never reads them. Explicitly closing a
83
80
  session (deleting it in the TUI) unregisters it immediately.
84
81
 
82
+ ## Turning chat off
83
+
84
+ Set `chat.enabled: false` in the thatch config file
85
+ (`~/.config/thatch/config.json`) - or ask an agent:
86
+ `config_set with chat: { enabled: false }`. When off:
87
+
88
+ - every chat tool refuses, stating that chat is disabled and how to
89
+ re-enable it
90
+ - the poller never starts, so nothing is woken or heartbeated
91
+ - the system prompt omits the chat section entirely, and the hook lines
92
+ print nothing - the feature stops existing as far as any agent can tell
93
+
94
+ Takes effect for the tools immediately (they re-read the config per
95
+ call); a restart applies it to the poller and prompt. Delete the setting
96
+ (or set it true) to turn chat back on.
97
+
98
+ ## Turning auto-registration off
99
+
100
+ Set `chat.autoRegister: false` in the same config file when you want the
101
+ old opt-in behavior: opencode sessions stay out of the directory until
102
+ they call `chat_register`. (The Claude Code / Cursor hook still ensures
103
+ its conversation's identity is registered on each prompt - that is the
104
+ host's identity anchor, not auto-joining; remove the hook with
105
+ `thatch setup` if you want those hosts out too.) Wake delivery for
106
+ registered sessions still works; there is just no automatic joining.
107
+
108
+ ## Leaving the directory
109
+
110
+ `chat_unregister` is a real exit on every host: a leave tombstone
111
+ suppresses re-joining (auto-registration on opencode, the hook's ensure on
112
+ other hosts) until the session explicitly calls `chat_register` again.
113
+ Without the tombstone, the next idle moment would silently re-register the
114
+ session under a fresh name and undo the leave. Rejoining mints a new name
115
+ (assigned names are never reused, even by the same session).
116
+
117
+ ## Security model
118
+
119
+ Chat messages are text written by OTHER agent sessions, delivered into a
120
+ reading agent's context verbatim - a prompt-injection surface by
121
+ construction. Three mitigations:
122
+
123
+ - **Framed at read time.** `chat_read` wraps its output in an explicit
124
+ untrusted-content frame (begin/end fences with a do-not-follow warning).
125
+ The frame exists only in the tool output; the persisted messages are
126
+ untouched.
127
+ - **Names are assigned, not claimed.** Display names are minted by thatch
128
+ with a never-reused counter, so a name cannot be grabbed by an unrelated
129
+ session and an old name cannot be resurrected by an impersonator.
130
+ - **No external content via wake.** The wake notification names senders
131
+ and counts only - bodies flow exclusively through the framed
132
+ `chat_read` surface.
133
+
85
134
  ## Requirements and limitations
86
135
 
87
136
  - **Wake-up delivery is opencode only.** The chat tools work everywhere
@@ -95,8 +144,7 @@ session (deleting it in the TUI) unregisters it immediately.
95
144
  - **Loop safety.** Thatch caps wake prompts per recipient per hour, so two
96
145
  agents acknowledging each other cannot ping-pong forever even if both
97
146
  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
147
  - **Message retention.** Messages are kept as history; unregistering does
102
- not delete them. There is no automatic pruning yet.
148
+ not delete them. Auto-registered sessions whose host has been gone for a
149
+ week are pruned from the directory (their names are never reused), and
150
+ unread mail addressed to them is removed on a later sweep.
@@ -1,10 +1,11 @@
1
1
  # Watchers
2
2
 
3
- Watchers monitor external sources - today, GitHub pull requests and
4
- branches - and inject a notification into the conversation when
5
- something happens. You ask the agent to watch a PR or a branch; thatch
6
- polls it in the background; the agent gets woken up when a watched
7
- event occurs. No one has to keep asking "any updates yet?"
3
+ Watchers monitor external sources - today, GitHub pull requests,
4
+ branches, and local shell commands - and inject a notification into
5
+ the conversation when something happens. You ask the agent to watch a
6
+ PR, a branch, or a command; thatch polls it in the background; the
7
+ agent gets woken up when a watched event occurs. No one has to keep
8
+ asking "any updates yet?"
8
9
 
9
10
  ## What it does
10
11
 
@@ -34,18 +35,47 @@ Branch watched events:
34
35
  on the branch, optionally filtered by workflow name (substring,
35
36
  case-insensitive)
36
37
 
38
+ Command watched events:
39
+
40
+ - `command_success` - the watched command exited 0. The agent calls
41
+ `thatch_watch_command_create` with a shell command that works as a
42
+ **condition variable, not a data pipe**: the watcher re-runs it every
43
+ poll and reads only the exit code - the command's output is
44
+ discarded, never delivered. A command watch is one-shot by design:
45
+ the first exit 0 fires one notification, then the watcher cancels
46
+ itself. The command must be a fast, idempotent status check (a
47
+ marker-file test, a health endpoint curl, a `gh run list` query).
48
+ Blocking waits like `gh run watch` or `tail -f` are not refused -
49
+ the tool guidance tells the agent not to use them, and a blocking
50
+ command is killed at the per-run timeout each cycle and
51
+ treated as not-done-yet. The command runs once at registration as
52
+ validation: a command that already exits 0 is refused (the condition
53
+ is already met), as is a command-not-found.
54
+
37
55
  Notifications are delivered as synthetic parts: the model sees them,
38
56
  the transcript does not. A notification triggers a model turn, so the
39
57
  agent can act on the event even while you are away.
40
58
 
41
59
  ## Privacy model
42
60
 
43
- Notifications carry pointer data only: who commented, what happened,
44
- and a URL. Comment bodies, diff contents, and CI logs never enter the
45
- context from the notification itself. This is deliberate - GitHub
46
- comments are written by strangers, and untrusted text flowing into a
47
- model's context is a prompt-injection vector. The agent fetches details
48
- on demand with `gh` when it decides to act.
61
+ Notifications carry pointer data and machine status: who acted, what
62
+ happened, a URL, and - for CI - the check or workflow name with its
63
+ conclusion (success, failure). Comment bodies, diff contents, and CI
64
+ logs never enter the context from the notification itself. This is
65
+ deliberate - GitHub comments are written by strangers, and untrusted
66
+ text flowing into a model's context is a prompt-injection vector.
67
+ Machine status fields come from the GitHub API rather than
68
+ user-written text, so they are safe to deliver. The agent fetches
69
+ everything else on demand with `gh` when it decides to act.
70
+
71
+ Command watches follow the same rule with a stricter boundary: the
72
+ notification carries the exit code and duration only. The command's
73
+ stdout and stderr are discarded - command output can be external
74
+ content (a curl response, a build log), so it never rides a
75
+ notification. The agent re-runs the command or reads logs itself when
76
+ it acts. The command runs in the project directory as the local user;
77
+ each run is killed at a 30-second timeout
78
+ (`THATCH_WATCH_COMMAND_TIMEOUT_SECONDS` overrides it).
49
79
 
50
80
  ## How to use it
51
81
 
@@ -56,13 +86,21 @@ Ask for it in conversation:
56
86
 
57
87
  The handling policy lives in your instruction. State it when you ask -
58
88
  the notification turn reads your policy from the conversation and
59
- follows it. The notification says what happened and where, not what to
60
- do about it.
89
+ follows it. The notification says what happened and where, not what
90
+ to do about it.
91
+
92
+ For waiting on CI, pick the tool that fits the wait. A short bounded
93
+ wait (a couple of minutes) can simply be polled in the conversation.
94
+ Watchers are the better fit for longer or open-ended waits, and for
95
+ post-merge builds on main; the creation output states when the first
96
+ poll lands so the choice compares real numbers.
61
97
 
62
98
  Other tools:
63
99
 
64
100
  - `thatch_watch_branch_create` - watch a branch (typically main) for
65
101
  commits, CI check runs, and workflow runs
102
+ - `thatch_watch_command_create` - wait on any local condition via a
103
+ shell command that exits 0 when the wait is over
66
104
  - `thatch_watch_list` - shows the session's active watchers
67
105
  - `thatch_watch_cancel` - cancels one by id
68
106
 
@@ -72,7 +110,13 @@ Watchers are in-memory and process-scoped:
72
110
 
73
111
  - Ending the opencode process (or restarting it) drops all watchers.
74
112
  - A watcher expires after 8 hours by default.
75
- - Each session can hold up to 5 active watchers.
113
+ - Each session can hold up to 5 active watchers (all sources share
114
+ the budget).
115
+ - A one-shot watch cancels itself after its first event - a single
116
+ "tell me when this run finishes" request leaves nothing polling
117
+ afterwards. Standing watches (the default) keep going until they
118
+ are cancelled, expire, or the process ends. Command watches are
119
+ always one-shot.
76
120
 
77
121
  If opencode restarts while a watch is active, re-register it - the
78
122
  session's conversation survives, so the policy you stated is still
@@ -83,8 +127,9 @@ there.
83
127
  - **opencode only.** MCP hosts (Claude Code, Cursor) have no way for a
84
128
  plugin to start a conversation turn, so the watch tools are not
85
129
  available there.
86
- - **Requires the `gh` CLI** to be installed and authenticated. Watch
87
- creation reports a clear error if it is not.
130
+ - **Requires the `gh` CLI** to be installed and authenticated for PR
131
+ and branch watches. Watch creation reports a clear error if it is
132
+ not. Command watches need only `bash`, not `gh`.
88
133
  - **Polling, not webhooks.** Events appear on the next poll cycle
89
134
  (60 seconds by default), not instantly.
90
135
  - **No delivery during an active turn.** If the agent is mid-turn when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@opencode-ai/plugin": "^1.17.13",
32
+ "@opencode-ai/sdk": "1.17.13",
32
33
  "@types/bun": "^1.3.14",
33
34
  "markdownlint-cli2": "^0.23.0",
34
35
  "typescript": "^5.7.0",
package/src/chat-names.ts CHANGED
@@ -7,14 +7,13 @@
7
7
  * Klingons, Discworld, Sandman, D&D, pulp detectives with software puns,
8
8
  * hackerspeak callsigns, AI puns, golden-age sci-fi, spoofed famous robots,
9
9
  * unnamed-cast extras). fnord generates its names with an LLM at runtime;
10
- * thatch bakes the pool in - static contents, no model call - while the
11
- * draw itself is random (see ChatStore.assign).
10
+ * thatch bakes the pool in - static contents, no model call.
12
11
  *
13
- * A session may still claim a custom name (chat_register's optional arg);
14
- * the pool is the default and keeps the directory colorful with zero
15
- * bikeshedding. Pool names are case-insensitively distinct from each other,
16
- * matching the directory's NOCASE uniqueness rule - so pool assignment can
17
- * never collide with another pool name, only with a custom claim.
12
+ * The pool is the fallback base for registrations with no usable session
13
+ * title (ChatStore.register slugifies a random pool name). Pool names are
14
+ * case-insensitively distinct from each other; slugify is lossy, but a
15
+ * shared base is harmless - the per-base counter still yields unique
16
+ * names.
18
17
  *
19
18
  * Names are deliberately short: they appear inside wake prompts and
20
19
  * chat_list output, so every character costs context.