@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.
- package/README.md +21 -11
- package/bin/release +4 -1
- package/bin/thatch +189 -33
- package/docs/dev/README.md +6 -4
- package/docs/dev/features/cli.md +20 -9
- package/docs/dev/features/cross-session-chat.md +125 -66
- package/docs/dev/features/multi-host.md +4 -3
- package/docs/dev/features/watchers.md +113 -23
- package/docs/dev/mcp-parity.md +1 -1
- package/docs/user/README.md +3 -2
- package/docs/user/cli.md +57 -9
- package/docs/user/cross-session-chat.md +72 -24
- package/docs/user/watchers.md +61 -16
- package/package.json +2 -1
- package/src/chat-names.ts +6 -7
- package/src/chat.ts +479 -148
- package/src/config.ts +34 -1
- package/src/db.ts +54 -6
- package/src/index.ts +87 -24
- package/src/prompts.ts +114 -50
- package/src/tool-defs.ts +294 -97
- package/src/tools.ts +2 -0
- package/src/watchers.ts +358 -23
|
@@ -7,15 +7,17 @@ between tabs all afternoon.
|
|
|
7
7
|
|
|
8
8
|
## What it does
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
59
|
-
>
|
|
61
|
+
> Ask the other session whether the release has shipped; answer its
|
|
62
|
+
> questions directly.
|
|
60
63
|
|
|
61
|
-
and
|
|
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.
|
|
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.
|
package/docs/user/watchers.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Watchers
|
|
2
2
|
|
|
3
|
-
Watchers monitor external sources - today, GitHub pull requests
|
|
4
|
-
branches - and inject a notification into
|
|
5
|
-
something happens. You ask the agent to watch a
|
|
6
|
-
polls it in the background; the
|
|
7
|
-
event occurs. No one has to keep
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
87
|
-
creation reports a clear error if it is
|
|
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.
|
|
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
|
|
11
|
-
* draw itself is random (see ChatStore.assign).
|
|
10
|
+
* thatch bakes the pool in - static contents, no model call.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|