opencode-crosstalk 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +460 -2
- package/assets/CursorGuard.png +0 -0
- package/assets/Wren.png +0 -0
- package/assets/avatars/ATTRIBUTION.md +18 -0
- package/assets/avatars/female/01-explorer-eeb4a4.png +0 -0
- package/assets/avatars/female/02-manager-b16a5b.png +0 -0
- package/assets/avatars/female/02-manager-e7a391.png +0 -0
- package/assets/avatars/female/03-coder-92594b.png +0 -0
- package/assets/avatars/female/03-coder-e5a07e.png +0 -0
- package/assets/avatars/female/05-analyst-b16a5b.png +0 -0
- package/assets/avatars/female/05-analyst-eeb4a4.png +0 -0
- package/assets/avatars/female/08-tester-d78774.png +0 -0
- package/assets/avatars/female/08-tester-eeb4a4.png +0 -0
- package/assets/avatars/female/10-builder-92594b.png +0 -0
- package/assets/avatars/female/10-builder-e5a07e.png +0 -0
- package/assets/avatars/female/13-researcher-92594b.png +0 -0
- package/assets/avatars/female/13-researcher-e7a391.png +0 -0
- package/assets/avatars/female/14-writer-623d36.png +0 -0
- package/assets/avatars/female/14-writer-e5a07e.png +0 -0
- package/assets/avatars/female/16-fixer-e7a391.png +0 -0
- package/assets/avatars/female/17-deployer-92594b.png +0 -0
- package/assets/avatars/female/17-deployer-e5a07e.png +0 -0
- package/assets/avatars/female/23-mentor-b16a5b.png +0 -0
- package/assets/avatars/female/23-mentor-e7a391.png +0 -0
- package/assets/avatars/female/25-pioneer-d78774.png +0 -0
- package/assets/avatars/female/26-strategist-b16a5b.png +0 -0
- package/assets/avatars/female/31-gardener-e5a07e.png +0 -0
- package/assets/avatars/female/33-simplifier-b16a5b.png +0 -0
- package/assets/avatars/female/33-simplifier-eeb4a4.png +0 -0
- package/assets/avatars/female/34-optimizer-92594b.png +0 -0
- package/assets/avatars/female/34-optimizer-e7a391.png +0 -0
- package/assets/avatars/female/36-integrator-d78774.png +0 -0
- package/assets/avatars/female/36-integrator-eeb4a4.png +0 -0
- package/assets/avatars/female/37-compiler-b16a5b.png +0 -0
- package/assets/avatars/female/37-compiler-e7a391.png +0 -0
- package/assets/avatars/female/40-formatter-b16a5b.png +0 -0
- package/assets/avatars/female/42-shipper-623d36.png +0 -0
- package/assets/avatars/female/46-steward-623d36.png +0 -0
- package/assets/avatars/female/46-steward-d78774.png +0 -0
- package/assets/avatars/female/47-shepherd-b16a5b.png +0 -0
- package/assets/avatars/female/47-shepherd-eeb4a4.png +0 -0
- package/assets/avatars/female/48-tinkerer-92594b.png +0 -0
- package/assets/avatars/female/50-wrangler-eeb4a4.png +0 -0
- package/assets/avatars/index.html +4 -0
- package/assets/avatars/male/01-explorer-d78774.png +0 -0
- package/assets/avatars/male/06-planner-92594b.png +0 -0
- package/assets/avatars/male/06-planner-e7a391.png +0 -0
- package/assets/avatars/male/07-reviewer-623d36.png +0 -0
- package/assets/avatars/male/07-reviewer-e5a07e.png +0 -0
- package/assets/avatars/male/09-migrator-b16a5b.png +0 -0
- package/assets/avatars/male/09-migrator-e7a391.png +0 -0
- package/assets/avatars/male/11-debugger-623d36.png +0 -0
- package/assets/avatars/male/11-debugger-d78774.png +0 -0
- package/assets/avatars/male/12-designer-b16a5b.png +0 -0
- package/assets/avatars/male/12-designer-eeb4a4.png +0 -0
- package/assets/avatars/male/15-editor-d78774.png +0 -0
- package/assets/avatars/male/15-editor-eeb4a4.png +0 -0
- package/assets/avatars/male/16-fixer-b16a5b.png +0 -0
- package/assets/avatars/male/18-scout-623d36.png +0 -0
- package/assets/avatars/male/18-scout-d78774.png +0 -0
- package/assets/avatars/male/19-architect-b16a5b.png +0 -0
- package/assets/avatars/male/19-architect-eeb4a4.png +0 -0
- package/assets/avatars/male/20-operator-92594b.png +0 -0
- package/assets/avatars/male/20-operator-e7a391.png +0 -0
- package/assets/avatars/male/21-coordinator-623d36.png +0 -0
- package/assets/avatars/male/21-coordinator-e5a07e.png +0 -0
- package/assets/avatars/male/22-auditor-d78774.png +0 -0
- package/assets/avatars/male/22-auditor-eeb4a4.png +0 -0
- package/assets/avatars/male/25-pioneer-623d36.png +0 -0
- package/assets/avatars/male/26-strategist-eeb4a4.png +0 -0
- package/assets/avatars/male/28-librarian-623d36.png +0 -0
- package/assets/avatars/male/28-librarian-e5a07e.png +0 -0
- package/assets/avatars/male/30-tracker-b16a5b.png +0 -0
- package/assets/avatars/male/30-tracker-e7a391.png +0 -0
- package/assets/avatars/male/31-gardener-92594b.png +0 -0
- package/assets/avatars/male/32-refactorer-623d36.png +0 -0
- package/assets/avatars/male/32-refactorer-d78774.png +0 -0
- package/assets/avatars/male/35-profiler-623d36.png +0 -0
- package/assets/avatars/male/35-profiler-e5a07e.png +0 -0
- package/assets/avatars/male/38-parser-92594b.png +0 -0
- package/assets/avatars/male/38-parser-e5a07e.png +0 -0
- package/assets/avatars/male/39-linter-623d36.png +0 -0
- package/assets/avatars/male/39-linter-d78774.png +0 -0
- package/assets/avatars/male/40-formatter-eeb4a4.png +0 -0
- package/assets/avatars/male/41-patcher-92594b.png +0 -0
- package/assets/avatars/male/41-patcher-e7a391.png +0 -0
- package/assets/avatars/male/42-shipper-e5a07e.png +0 -0
- package/assets/avatars/male/43-maintainer-d78774.png +0 -0
- package/assets/avatars/male/43-maintainer-eeb4a4.png +0 -0
- package/assets/avatars/male/44-guardian-b16a5b.png +0 -0
- package/assets/avatars/male/44-guardian-e7a391.png +0 -0
- package/assets/avatars/male/45-sentinel-92594b.png +0 -0
- package/assets/avatars/male/45-sentinel-e5a07e.png +0 -0
- package/assets/avatars/male/48-tinkerer-e7a391.png +0 -0
- package/assets/avatars/male/49-hacker-623d36.png +0 -0
- package/assets/avatars/male/49-hacker-e5a07e.png +0 -0
- package/assets/avatars/male/50-wrangler-d78774.png +0 -0
- package/assets/avatars/manifest.json +1202 -0
- package/index.ts +11 -0
- package/package.json +65 -4
- package/src/config.ts +176 -0
- package/src/core/avatar.ts +92 -0
- package/src/core/claims.ts +302 -0
- package/src/core/clock.ts +31 -0
- package/src/core/directory.ts +48 -0
- package/src/core/format.ts +382 -0
- package/src/core/mailbox.ts +239 -0
- package/src/core/mesh.ts +781 -0
- package/src/core/registry.ts +306 -0
- package/src/deliverer.ts +86 -0
- package/src/events.ts +66 -0
- package/src/index.ts +323 -0
- package/src/rpc.ts +52 -0
- package/src/storage.ts +105 -0
- package/src/tools/args.ts +132 -0
- package/src/tools/claim.ts +87 -0
- package/src/tools/heartbeat.ts +152 -0
- package/src/tools/inbox.ts +76 -0
- package/src/tools/index.ts +62 -0
- package/src/tools/peers.ts +101 -0
- package/src/tools/send.ts +84 -0
- package/src/tools/status.ts +137 -0
- package/src/tools/types.ts +72 -0
- package/src/tools/wait.ts +96 -0
- package/src/tui/avatar.ts +188 -0
- package/src/tui/index.tsx +216 -0
- package/src/types.ts +147 -0
- package/tui.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christoph Döllinger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,461 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-crosstalk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let the OpenCode sessions on one server **see each other, talk to each other, and
|
|
4
|
+
lease the files they are editing**.
|
|
5
|
+
|
|
6
|
+
Two agents in the same repository cannot discover each other, cannot say "I'm in
|
|
7
|
+
`src/auth/` right now", and — most expensively — cannot stop each other from
|
|
8
|
+
writing the same file at the same time. Which means the second agent usually
|
|
9
|
+
discovers the first one's work by reading a half-applied diff during the
|
|
10
|
+
postmortem. This plugin adds seven tools that make all three ordinary.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
crosstalk_status declare your role and see who else is here
|
|
14
|
+
crosstalk_peers who is running, what they declared, what they hold
|
|
15
|
+
crosstalk_send message a peer, a role, or everyone
|
|
16
|
+
crosstalk_inbox read replies, optionally blocking until one arrives
|
|
17
|
+
crosstalk_claim take an exclusive, expiring lease on a file
|
|
18
|
+
crosstalk_wait block until a peer is idle or a file is released
|
|
19
|
+
crosstalk_heartbeat send a message to a peer on a repeating interval
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
All seven share one namespace and one permission action (`crosstalk`), so a
|
|
23
|
+
project can allow or deny coordination with a single rule instead of seven.
|
|
24
|
+
|
|
25
|
+
## In action
|
|
26
|
+
|
|
27
|
+
Two real sessions in one checkout, with nobody relaying between them.
|
|
28
|
+
`CursorGuard` wants `src/app/app/app.hpp`. `Wren` is standing on it. The entire
|
|
29
|
+
negotiation:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Neither session stalled, and neither came to ask a human. One requested, one
|
|
36
|
+
answered with a hold and a warning about the three defaults it had already
|
|
37
|
+
shuffled — and both went back to their own files while the answer was in flight.
|
|
38
|
+
The lease is what made that a sentence instead of a silent overwrite; the
|
|
39
|
+
interesting part of these screenshots is the thing that *didn't* happen. Both
|
|
40
|
+
panes also carry the sidebar portrait, the role glyph, and the live summary each
|
|
41
|
+
session declared ([TUI avatars](#tui-avatars) below).
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
git clone https://github.com/MrDoe/opencode-crosstalk
|
|
47
|
+
cd opencode-crosstalk && npm install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Every project on your machine
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm run setup # symlink this checkout into ~/.opencode/plugins/crosstalk
|
|
54
|
+
npm run uninstall # remove the link again
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The global `plugins/` directory is discovered automatically, so this needs no
|
|
58
|
+
config edit, no manifest entry, and no negotiation with the plugin loader. It
|
|
59
|
+
activates the plugin for every project and workspace on the machine, and it links
|
|
60
|
+
rather than copies, so your edits to `src/` are live after a reload. The host
|
|
61
|
+
resolves the linked directory through its root `index.ts`, so that file stays:
|
|
62
|
+
remove it and the plugin is dropped silently, with no error and no opinion.
|
|
63
|
+
|
|
64
|
+
Verify with `opencode api get /api/plugin` (expect `opencode.crosstalk` active);
|
|
65
|
+
`opencode plugin list` shows it as `local`. The cheapest instrument is
|
|
66
|
+
`crosstalk_status` itself, which ends in a self-check line — see
|
|
67
|
+
[TUI avatars](#tui-avatars).
|
|
68
|
+
|
|
69
|
+
Install into exactly one global root. Linking both loads the plugin twice per
|
|
70
|
+
location; the second load detects the first through `globalThis`, warns, and
|
|
71
|
+
registers nothing, so nothing explodes — but you are now running two installs
|
|
72
|
+
that each think they own the job, which is the exact failure mode this plugin
|
|
73
|
+
exists to prevent. `npm run setup` uses `~/.opencode/plugins/`; the root the V2
|
|
74
|
+
docs name, `<config>/plugins/` (`~/.config/opencode/plugins/`), is honored as
|
|
75
|
+
well. Set `CROSSTALK_PLUGINS_DIR` to target another directory.
|
|
76
|
+
|
|
77
|
+
### One specific project
|
|
78
|
+
|
|
79
|
+
Point a project at it:
|
|
80
|
+
|
|
81
|
+
```jsonc title="opencode.jsonc"
|
|
82
|
+
{
|
|
83
|
+
"$schema": "https://opencode.ai/config.json",
|
|
84
|
+
"plugins": ["../opencode-crosstalk"]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or drop the package into `.opencode/plugins/` and it is discovered with no
|
|
89
|
+
configuration at all:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
your-project/.opencode/plugins/crosstalk/ → symlink or copy this package here
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Permissions
|
|
96
|
+
|
|
97
|
+
No permission rule is needed to *use* the tools: OpenCode's base policy allows
|
|
98
|
+
every action, so `crosstalk` already resolves to allow. Add a rule when you want
|
|
99
|
+
to control the plugin rather than enable it — one rule covers all seven tools:
|
|
100
|
+
|
|
101
|
+
```jsonc title="opencode.jsonc"
|
|
102
|
+
{
|
|
103
|
+
"permissions": [{ "action": "crosstalk", "resource": "*", "effect": "deny" }]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Note that a rule which *blocks* the action does not prompt and then decline — it
|
|
108
|
+
removes the tools from the catalog, and the model never learns they existed. If
|
|
109
|
+
you want a dialog box instead of silence, use `ask`.
|
|
110
|
+
|
|
111
|
+
No build step: the host resolves the plugin directory through the root `index.ts`,
|
|
112
|
+
a thin re-export of `src/index.ts`. There is no `dist/`, no bundler, and no
|
|
113
|
+
sourcemap archaeology.
|
|
114
|
+
|
|
115
|
+
## Using it
|
|
116
|
+
|
|
117
|
+
An agent that knows other sessions exist uses this on its own — the plugin
|
|
118
|
+
briefs every session in its system prompt. A session that is currently alone
|
|
119
|
+
gets a compact declaration nudge, so it acquires a name and a role before a peer
|
|
120
|
+
appears (an undeclared session is not addressable); as soon as one peer or one
|
|
121
|
+
active lease exists, the full coordination briefing takes over. A typical
|
|
122
|
+
exchange:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
crosstalk_status { name: "Riley", role: "migrator", goal: "port auth off sessions",
|
|
126
|
+
summary: "moving the token handlers now" }
|
|
127
|
+
crosstalk_peers {}
|
|
128
|
+
→ crosstalk peers (scope project, 1 found, 1 running):
|
|
129
|
+
- ses_9f2 (Alex) running role=reviewer summary="checking the token flow" "check my auth changes" claims /repo/src/auth/session.ts
|
|
130
|
+
|
|
131
|
+
crosstalk_peers { overlap: ["src/auth/session.ts"] }
|
|
132
|
+
→ crosstalk peers (scope project, 1 found, 1 running, overlap src/auth/session.ts):
|
|
133
|
+
- ses_9f2 (Alex) ... claims /repo/src/auth/session.ts
|
|
134
|
+
|
|
135
|
+
crosstalk_claim { action: "claim", resources: ["src/auth/session.ts"] }
|
|
136
|
+
→ crosstalk claim: refused — 1 resource already leased
|
|
137
|
+
- src/auth/session.ts held by ses_9f2 (reviewing), free in 4m
|
|
138
|
+
wait for them with crosstalk_wait, signal them with crosstalk_send, or pass force: true to steal.
|
|
139
|
+
nothing was claimed (atomic request).
|
|
140
|
+
|
|
141
|
+
crosstalk_send { to: "Alex", kind: "request",
|
|
142
|
+
text: "I need to rewrite session.ts. Are you done?" }
|
|
143
|
+
|
|
144
|
+
crosstalk_wait { for: "peer_idle", sessionID: "ses_9f2", timeoutSeconds: 120 }
|
|
145
|
+
→ crosstalk wait: peer idle after 1.2s — ses_9f2
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A message is stored in the recipient's mailbox and, when the session is live,
|
|
149
|
+
injected into its current turn — so the other agent finds out without anyone
|
|
150
|
+
having to poll, remember to poll, or schedule a poll. The wait returned in 1.2
|
|
151
|
+
seconds because it had something to wait on.
|
|
152
|
+
|
|
153
|
+
### Talking to sessions by name
|
|
154
|
+
|
|
155
|
+
Names are there for the user, too. Once the sessions declare themselves, you can
|
|
156
|
+
route work in plain language — *"Talk to Ada about this first."* — and the agent
|
|
157
|
+
you are talking to will address that session by name with
|
|
158
|
+
`crosstalk_send { to: "Ada", … }`. A name is unique among the peers a session
|
|
159
|
+
can see (a taken one is refused, with the holder named), shows up in
|
|
160
|
+
`crosstalk_peers` and in the sidebar, and travels with every message, so the
|
|
161
|
+
inbox always says who is speaking. Underneath the name, nothing changes: identity
|
|
162
|
+
is still the session id, and that is what stops one session releasing another's
|
|
163
|
+
leases. The nickname is a label, never a licence.
|
|
164
|
+
|
|
165
|
+
### TUI avatars
|
|
166
|
+
|
|
167
|
+
Sessions that declare themselves get a cartoon avatar in the OpenCode TUI
|
|
168
|
+
sidebar, with their name, their role, and their live status summary below it —
|
|
169
|
+
`🔧 CursorGuard` and `🔬 Wren` in the screenshots at the top.
|
|
170
|
+
|
|
171
|
+
The avatar is picked from a bundled pool of 92 characters (DiceBear *Personas*,
|
|
172
|
+
CC BY 4.0 — see [`assets/avatars/ATTRIBUTION.md`](assets/avatars/ATTRIBUTION.md);
|
|
173
|
+
browse the pool by opening `assets/avatars/index.html` in a browser):
|
|
174
|
+
|
|
175
|
+
- `crosstalk_status { name: "Rita", role: "coder", avatar: "👩" }` — the emoji is
|
|
176
|
+
a hint, not a request: 👩/👨 pick the character pool, and the skin tone is
|
|
177
|
+
baked into the art. There is no further negotiation.
|
|
178
|
+
- `crosstalk_status { summary: "Rewriting the session store so a reload keeps its leases." }`
|
|
179
|
+
— the live status line under the avatar: one or two short sentences (200
|
|
180
|
+
characters) on what the session is doing *right now*. It is wrapped to the
|
|
181
|
+
avatar's width, falls back to the declared goal until a summary is written,
|
|
182
|
+
and lands in `crosstalk_peers` output for the other sessions. Because it is
|
|
183
|
+
pushed to the TUI on every declaration (the same debounced `changed` event,
|
|
184
|
+
see below), refreshing it with the next `crosstalk_status` call updates the
|
|
185
|
+
sidebar within a second — keep it current as your task moves. Write it for the
|
|
186
|
+
peer who will read it, not for the user, who can already see your tabs.
|
|
187
|
+
- Freshness is visible, not enforced. A peer line annotates its summary's age
|
|
188
|
+
once it is more than five minutes old — `summary="handlers half done" (14m
|
|
189
|
+
ago)` — so a reader can tell a current summary from a stale one, and the
|
|
190
|
+
sidebar dims the same summary after a quarter hour of no refresh (a 30s
|
|
191
|
+
background tick, so it dims even for a session that emits nothing). A summary
|
|
192
|
+
restored from a snapshot written before this feature carries no age: it is
|
|
193
|
+
shown, annotated with nothing, and never dimmed. Archaeology at least admits to
|
|
194
|
+
being archaeology.
|
|
195
|
+
- The declaring session sees its own summary's age in `crosstalk_status` output
|
|
196
|
+
from five minutes on, and is nudged — *"your summary is 15m old — refresh
|
|
197
|
+
it"* — once it crosses the same quarter-hour the sidebar dims at. A session
|
|
198
|
+
that never declared a summary is still told to declare one. Being nagged is
|
|
199
|
+
the enforcement mechanism; there is no other.
|
|
200
|
+
- Every `crosstalk_status` call also returns a **self-check line**, so an
|
|
201
|
+
install problem is visible without reading server logs:
|
|
202
|
+
`self-check: snapshot v2 · storage crosstalk/fb18e52f…cd2d · 4 locations owned`.
|
|
203
|
+
It names the snapshot format the running store reports, the storage key (or
|
|
204
|
+
`persistence off`), and how many locations this process owns in-process.
|
|
205
|
+
Once the instance guard has turned a duplicate load away, the line grows a
|
|
206
|
+
`2 refused loads` tail — the tell for a plugin installed in two places at
|
|
207
|
+
once, which otherwise only shows up as one install silently clobbering the
|
|
208
|
+
other, hours later, in the worst possible way.
|
|
209
|
+
- `crosstalk_peers { overlap: ["src/auth/session.ts"] }` answers *who else
|
|
210
|
+
touches these paths* in one call: a peer matches when it holds a lease on the
|
|
211
|
+
exact key or listed the path in `workingOn`. Keys are exact, like every claim
|
|
212
|
+
key — asking about a directory never matches the files inside it — and each
|
|
213
|
+
side is normalized against its own directory, so `src/x.ts` typed by a
|
|
214
|
+
session in another worktree does not match this one. An empty result says
|
|
215
|
+
how many sessions were on the channel to begin with, so "nobody overlaps" is
|
|
216
|
+
never confused with "nobody is here".
|
|
217
|
+
- An avatar appears when the session has a task: the first `crosstalk_status`
|
|
218
|
+
that carries a role or a goal freezes one portrait onto the session, and
|
|
219
|
+
nothing changes it afterwards — a later role, name, or avatar hint cannot
|
|
220
|
+
move the artwork. Agents do not get a mid-session glow-up.
|
|
221
|
+
- The declared role picks the character when it matches a pool seed (`coder`,
|
|
222
|
+
`reviewer`, `explorer`, …); otherwise the name hashes to a stable one. The
|
|
223
|
+
portrait is stored on the session record, so it survives reloads and
|
|
224
|
+
restarts.
|
|
225
|
+
- Every avatar also carries an **emoticon** — one curated glyph for its role
|
|
226
|
+
(`coder` → 💻, `librarian` → 📚, 46 roles), chosen from tools, objects, and
|
|
227
|
+
people and never a smiley. It is derived from the manifest entry, so the same
|
|
228
|
+
character always shows the same glyph.
|
|
229
|
+
- That glyph is shown beside the avatar's name in the sidebar **and** written
|
|
230
|
+
into the session title, because the tab header has no slot a plugin can render
|
|
231
|
+
into — its label *is* the title. `💻 Refactor the session store`.
|
|
232
|
+
|
|
233
|
+
The title write is deliberately narrow: only a session that declared a name, and
|
|
234
|
+
only one in the TUI's own directory or below it, is retitled; the write is
|
|
235
|
+
idempotent (a title already carrying its glyph is left alone, so the
|
|
236
|
+
`session.renamed` event it causes is a no-op), a stale glyph is replaced rather
|
|
237
|
+
than stacked, and an emoji you typed yourself is never stripped. Renames are
|
|
238
|
+
durable server state, so a decorated title outlives the TUI that wrote it — and
|
|
239
|
+
outlives several of the bugs it could have had.
|
|
240
|
+
|
|
241
|
+
The server plugin exposes the session directory over RPC (a `directory` method
|
|
242
|
+
plus a debounced `changed` event); the CLI plugin in `tui.ts` / `src/tui/`
|
|
243
|
+
renders it, location-scoped, into `sidebar.content`. The image uses OpenTUI's
|
|
244
|
+
**block** renderer on purpose: `sidebar.content` sits inside a scrollbox and the
|
|
245
|
+
terminal we target (VS Code) announces sixel support but never paints the
|
|
246
|
+
payload — it is confidently and loudly wrong. On a kitty-graphics terminal,
|
|
247
|
+
switching `protocol` in `src/tui/index.tsx` from `blocks` to `auto` gives crisp
|
|
248
|
+
pixels.
|
|
249
|
+
|
|
250
|
+
### Drop-in AGENTS.md snippet
|
|
251
|
+
|
|
252
|
+
Copy this block into a project's `AGENTS.md` to teach its sessions how to
|
|
253
|
+
coordinate without stalling. The `<!-- … -->` markers make it easy to find and
|
|
254
|
+
replace later, so upgrading this file later is a find-and-replace rather than an
|
|
255
|
+
archaeological dig.
|
|
256
|
+
|
|
257
|
+
```markdown
|
|
258
|
+
<!-- opencode-crosstalk:begin -->
|
|
259
|
+
## OpenCode Crosstalk
|
|
260
|
+
|
|
261
|
+
Other OpenCode sessions in this workspace are reachable through the
|
|
262
|
+
`opencode-crosstalk` plugin: `crosstalk_status`, `crosstalk_peers`,
|
|
263
|
+
`crosstalk_send`, `crosstalk_inbox`, `crosstalk_claim`, `crosstalk_heartbeat`,
|
|
264
|
+
`crosstalk_wait`.
|
|
265
|
+
Write in **English only** and keep everything token-tight: one short sentence
|
|
266
|
+
per status, message, or claim note (who, what, where) — no reports, no
|
|
267
|
+
summaries, no pleasantries. Keep working; only stop for coordination that
|
|
268
|
+
prevents a real collision.
|
|
269
|
+
|
|
270
|
+
- **Declare once — briefly — then keep moving.** `crosstalk_status` sets a unique
|
|
271
|
+
human name (so the user can say "tell George…" and peers can address you), your
|
|
272
|
+
role, and a goal of a few words; `crosstalk_peers` shows active sessions and
|
|
273
|
+
their leases. Work that does not overlap theirs needs no coordination.
|
|
274
|
+
- **Keep a live summary under your avatar.** `crosstalk_status { summary: … }` —
|
|
275
|
+
one or two short sentences on what you are doing right now — is shown under
|
|
276
|
+
your avatar in the sidebar and read by peers through `crosstalk_peers`, so
|
|
277
|
+
refresh it whenever your task changes.
|
|
278
|
+
- **Talk before you collide — as a notice, not an essay.** If you need something a
|
|
279
|
+
peer holds, `crosstalk_send` one short precise sentence (`to: "George"` or a
|
|
280
|
+
session id) and continue elsewhere; replies are injected into live turns (use
|
|
281
|
+
`crosstalk_inbox` to catch up). Never force a claim.
|
|
282
|
+
- **Lease what you are editing now.** `crosstalk_claim` takes an exclusive
|
|
283
|
+
expiring lease on exact paths — no globs (`resources`, `note`, `ttlSeconds`);
|
|
284
|
+
keep the note to a few words. `renew` if the work runs long, `release` when
|
|
285
|
+
done; a refusal names the holder.
|
|
286
|
+
- **Only sessions in this project are reachable** — peers marked "not addressable"
|
|
287
|
+
(location unknown or a different project) cannot receive mail, so do not try.
|
|
288
|
+
|
|
289
|
+
Installed globally — `opencode api get /api/plugin` shows `opencode.crosstalk`
|
|
290
|
+
active.
|
|
291
|
+
<!-- opencode-crosstalk:end -->
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Design notes
|
|
295
|
+
|
|
296
|
+
**Discovery is event-driven.** The V2 plugin context exposes a narrow slice of
|
|
297
|
+
the session API: no `session.list()`, no `session.active()`.
|
|
298
|
+
`ctx.event.subscribe()` is the only way a plugin learns that other sessions
|
|
299
|
+
exist, so the presence registry is a reducer over `session.created`,
|
|
300
|
+
`session.status`, `session.execution.*`, `session.idle`, `session.renamed`,
|
|
301
|
+
`session.moved`, `session.forked`, and `session.deleted`. One consequence worth
|
|
302
|
+
knowing: a session that was already mid-turn when the plugin loaded stays
|
|
303
|
+
invisible until it emits something, and it shows up on its next event. It is a
|
|
304
|
+
slow handshake, not a handshake failure.
|
|
305
|
+
|
|
306
|
+
**Delivery is retried, not just queued.** A plugin instance belongs to one
|
|
307
|
+
location, so injecting into a session owned by another location can fail. The
|
|
308
|
+
message stays in the recipient's mailbox, and every crosstalk tool first retries
|
|
309
|
+
any injection that is still outstanding for the calling session — so a peer
|
|
310
|
+
recovers the moment it does anything at all, without anyone polling. Retries are
|
|
311
|
+
bounded (50 per session) and expire with the message TTL. Mail older than a day
|
|
312
|
+
is not news, it is noise with a timestamp.
|
|
313
|
+
|
|
314
|
+
**Liveness has two thresholds.** An agent can sit inside one model call for
|
|
315
|
+
minutes without emitting anything, so a peer is only flagged *stale* after
|
|
316
|
+
`staleAfterMs` (10 min) and only *evicted* after `evictAfterMs` (1 h). Silence
|
|
317
|
+
is not evidence.
|
|
318
|
+
|
|
319
|
+
**A claim is a hard lease, not a note.** Paths are normalized
|
|
320
|
+
(`./src/a.ts` = `src//a.ts`, and relative paths resolve against the holder's
|
|
321
|
+
directory so two worktrees do not shadow each other), leases expire, and a batch
|
|
322
|
+
request is atomic: one busy file means nothing is taken. The all-or-nothing part
|
|
323
|
+
is the entire point — a partial claim is a claim you have to remember you hold.
|
|
324
|
+
|
|
325
|
+
**A claim key is an exact path, not a glob.** This is the section people skip
|
|
326
|
+
and then lose an hour to. There is no pattern matching: `src/auth/**` is stored
|
|
327
|
+
as the literal key `/repo/src/auth/**` and protects nothing underneath it, so
|
|
328
|
+
list the files you mean — or claim the directory itself and accept that it is
|
|
329
|
+
one key, not a subtree. Leasing a directory covers the directory and nothing
|
|
330
|
+
inside it, which is a sentence worth reading twice.
|
|
331
|
+
|
|
332
|
+
**Messages are not persisted.** Peers and leases survive a plugin reload through
|
|
333
|
+
`ctx.storage`; mailboxes do not. Replaying stale mail after a restart is worse
|
|
334
|
+
than an empty inbox: the recipient cannot tell a fresh request from a two-hour-old
|
|
335
|
+
one, and will act on the wrong one with complete confidence.
|
|
336
|
+
|
|
337
|
+
**Sessions carry a human name, but identity stays the session id.** A session
|
|
338
|
+
may give itself a unique nickname with `crosstalk_status { name: "George" }` so
|
|
339
|
+
the user can say "tell George…" and peers can address it by name in
|
|
340
|
+
`crosstalk_send`. Names are case-insensitively unique among visible peers — a
|
|
341
|
+
taken name is refused and names the holder — and travel with each message
|
|
342
|
+
(`fromName`), so the inbox shows who is talking even mid-session.
|
|
343
|
+
|
|
344
|
+
**The channel is walled to sessions provably in your project.** `scope` still
|
|
345
|
+
controls what a *listing* shows, but communication is stricter: a peer is
|
|
346
|
+
addressable only when its project (or directory, under `scope: "location"`) is
|
|
347
|
+
provably equal to yours. Unknown locations stay listed — marked
|
|
348
|
+
"not addressable" — but sends, role/`all` broadcasts, `crosstalk_wait`, and
|
|
349
|
+
injection retries all refuse them, so a session in another project folder can
|
|
350
|
+
neither be messaged nor have mail injected into it. Listing is permissive;
|
|
351
|
+
communication is not.
|
|
352
|
+
|
|
353
|
+
## Options
|
|
354
|
+
|
|
355
|
+
All optional; set them with the object form of `plugins`:
|
|
356
|
+
|
|
357
|
+
```jsonc
|
|
358
|
+
{
|
|
359
|
+
"plugins": [
|
|
360
|
+
{
|
|
361
|
+
"package": "../opencode-crosstalk",
|
|
362
|
+
"options": {
|
|
363
|
+
"scope": "project",
|
|
364
|
+
"claimTtlMs": 300000,
|
|
365
|
+
"codemode": false
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
| Option | Default | Meaning |
|
|
373
|
+
| --- | --- | --- |
|
|
374
|
+
| `namespace` | `crosstalk` | Tool namespace. Dots are rejected: the host rewrites them to `_`. |
|
|
375
|
+
| `permission` | `crosstalk` | Permission action for all seven tools. |
|
|
376
|
+
| `codemode` | `false` | Expose the tools through Code Mode instead of directly. |
|
|
377
|
+
| `scope` | `project` | Peer visibility: `project`, `location`, or `server`. |
|
|
378
|
+
| `staleAfterMs` | `600000` | Silence after which a peer is flagged stale. |
|
|
379
|
+
| `evictAfterMs` | `3600000` | Silence after which a peer is dropped. Never below `staleAfterMs`. |
|
|
380
|
+
| `maxMessages` | `100` | Retained messages per mailbox. |
|
|
381
|
+
| `messageTtlMs` | `86400000` | Message lifetime. |
|
|
382
|
+
| `claimTtlMs` | `300000` | Default lease duration. |
|
|
383
|
+
| `maxWaitMs` | `120000` | Ceiling on any blocking call. |
|
|
384
|
+
| `pollMs` | `1000` | Poll interval for the barrier waits. |
|
|
385
|
+
| `heartbeatMs` | `10000` | Progress heartbeat while blocked; `0` disables. |
|
|
386
|
+
| `persist` | `true` | Persist peers and leases through `ctx.storage`. |
|
|
387
|
+
| `storageKey` | `crosstalk` | Storage key prefix. |
|
|
388
|
+
| `announce` | `true` | Brief every session in the system prompt; compact while the session is alone. |
|
|
389
|
+
|
|
390
|
+
Bad values degrade to the default with a warning; a typo never costs an agent
|
|
391
|
+
its tools mid-session. `parseOptions` never throws — a missing bracket is a log
|
|
392
|
+
line, not an outage, because losing your tools halfway through a turn is a worse
|
|
393
|
+
outcome than losing a setting.
|
|
394
|
+
|
|
395
|
+
### How the permission action and `codemode` are used
|
|
396
|
+
|
|
397
|
+
Both were confirmed against the host's own tool snapshot, which reads roughly
|
|
398
|
+
`tool.options?.permission ?? <effective tool name>` when it filters the catalog,
|
|
399
|
+
and splits tools on `options.codemode === false` (direct) versus everything else
|
|
400
|
+
(Code Mode catalog). So:
|
|
401
|
+
|
|
402
|
+
- `permission: "crosstalk"` makes the action that gates all seven tools. Without
|
|
403
|
+
it, the action would fall back to the effective tool name — `crosstalk_status`,
|
|
404
|
+
`crosstalk_claim`, and so on — needing seven rules instead of one.
|
|
405
|
+
- `codemode: false` is what makes the tools directly callable. Setting it to
|
|
406
|
+
`true` moves them into the Code Mode catalog only, where they are reached
|
|
407
|
+
through `execute` rather than called by name. That adds a second gate: the
|
|
408
|
+
`execute` action with resource `*` decides whether Code Mode is available at
|
|
409
|
+
all, so blocking either one is enough to make the tools unreachable.
|
|
410
|
+
- A rule that *blocks* the action keeps the tools out of the catalog entirely,
|
|
411
|
+
so the model never sees them — no prompt, no fallback, just a plugin-shaped
|
|
412
|
+
silence. `ask` behaves as usual and prompts.
|
|
413
|
+
|
|
414
|
+
## Development
|
|
415
|
+
|
|
416
|
+
```sh
|
|
417
|
+
npm install
|
|
418
|
+
npm run check # the gate: typecheck + 279 unit tests, both always run
|
|
419
|
+
npm run test:e2e # live smoke test, needs OPENCODE_E2E=1 and a usable model
|
|
420
|
+
npm run setup # link the plugin in globally
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`npm run check` is the whole gate and is what to keep green before pushing. It
|
|
424
|
+
calls `scripts/check.mjs`, which runs the typecheck and the test suite
|
|
425
|
+
**independently** — one failing never suppresses the other's output — and exits
|
|
426
|
+
non-zero if either failed, naming both codes. `npm test` and `npm run typecheck`
|
|
427
|
+
remain available for a single leg. The tests need no network and no server.
|
|
428
|
+
|
|
429
|
+
The e2e test skips with a reason — rather than failing — when the configured
|
|
430
|
+
provider cannot serve the request, so a credits problem never looks like a
|
|
431
|
+
plugin regression. It spawns real `opencode run` sessions and spends real money,
|
|
432
|
+
so it stays behind `OPENCODE_E2E=1` and wants `OPENCODE_E2E_MODEL` pointed at a
|
|
433
|
+
specific model.
|
|
434
|
+
|
|
435
|
+
`src/core` imports nothing from OpenCode: the presence registry, mailboxes, and
|
|
436
|
+
claim table take an injected clock and are tested directly. `src/index.ts`
|
|
437
|
+
(server) and `src/tui/index.tsx` (CLI) are the only places that talk to the
|
|
438
|
+
host; `src/rpc.ts` is the shared contract and imports only types. `scripts/setup.mjs`
|
|
439
|
+
and `scripts/check.mjs` are plain JavaScript sitting outside `tsconfig.json`,
|
|
440
|
+
so neither is part of the typecheck.
|
|
441
|
+
|
|
442
|
+
## Skills
|
|
443
|
+
|
|
444
|
+
`.opencode/skills/` carries four project skills OpenCode advertises to every
|
|
445
|
+
session here; load one with the `skill` tool when it fits:
|
|
446
|
+
|
|
447
|
+
| Skill | Use it for |
|
|
448
|
+
| --- | --- |
|
|
449
|
+
| `crosstalk-coordination` | running several sessions — assigning phases, relaying findings, push waves |
|
|
450
|
+
| `crosstalk-team-workflow` | executing an assignment — leases, a green gate, honest landing reports |
|
|
451
|
+
| `test-driven-development` | any new code: failing test first, watch it fail, then minimal code |
|
|
452
|
+
| `requesting-code-review` | dispatching a reviewer on a commit range before merging |
|
|
453
|
+
|
|
454
|
+
The first two are written for this repository. The last two are vendored from
|
|
455
|
+
[obra/superpowers](https://github.com/obra/superpowers) under MIT; each file
|
|
456
|
+
carries its attribution footer and any edits made while vendoring.
|
|
457
|
+
`.opencode/skills/README.md` is the index.
|
|
458
|
+
|
|
459
|
+
## License
|
|
460
|
+
|
|
461
|
+
MIT © 2026 Christoph Döllinger — see [LICENSE](LICENSE).
|
|
Binary file
|
package/assets/Wren.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Avatar attribution
|
|
2
|
+
|
|
3
|
+
The avatar images in this directory are generated with [DiceBear](https://www.dicebear.com/)
|
|
4
|
+
from the **Personas** style.
|
|
5
|
+
|
|
6
|
+
- **Style:** Personas by [Draftbit](https://draftbit.com/) — <https://personas.draftbit.com/>
|
|
7
|
+
- **License:** [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
|
8
|
+
- **Remix:** via [DiceBear](https://www.dicebear.com/styles/personas/) (`@dicebear/styles@10.6.0`)
|
|
9
|
+
|
|
10
|
+
Each PNG was generated deterministically from a role seed (e.g. `coder`, `reviewer`),
|
|
11
|
+
a gender pool (female / male) and a skin tone from the Personas palette. The resolved
|
|
12
|
+
style options for every file are recorded in `manifest.json`.
|
|
13
|
+
|
|
14
|
+
Generation parameters: DiceBear `personas` (`@dicebear/styles@10.6.0`), 256 px,
|
|
15
|
+
transparent background, one of 50 role seeds, a gender pool (female = long-hair
|
|
16
|
+
variants with no facial hair; male = short-hair variants with 65% facial hair),
|
|
17
|
+
and a skin tone from the Personas palette. The pool was curated by hand after
|
|
18
|
+
generation.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|