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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +460 -2
  3. package/assets/CursorGuard.png +0 -0
  4. package/assets/Wren.png +0 -0
  5. package/assets/avatars/ATTRIBUTION.md +18 -0
  6. package/assets/avatars/female/01-explorer-eeb4a4.png +0 -0
  7. package/assets/avatars/female/02-manager-b16a5b.png +0 -0
  8. package/assets/avatars/female/02-manager-e7a391.png +0 -0
  9. package/assets/avatars/female/03-coder-92594b.png +0 -0
  10. package/assets/avatars/female/03-coder-e5a07e.png +0 -0
  11. package/assets/avatars/female/05-analyst-b16a5b.png +0 -0
  12. package/assets/avatars/female/05-analyst-eeb4a4.png +0 -0
  13. package/assets/avatars/female/08-tester-d78774.png +0 -0
  14. package/assets/avatars/female/08-tester-eeb4a4.png +0 -0
  15. package/assets/avatars/female/10-builder-92594b.png +0 -0
  16. package/assets/avatars/female/10-builder-e5a07e.png +0 -0
  17. package/assets/avatars/female/13-researcher-92594b.png +0 -0
  18. package/assets/avatars/female/13-researcher-e7a391.png +0 -0
  19. package/assets/avatars/female/14-writer-623d36.png +0 -0
  20. package/assets/avatars/female/14-writer-e5a07e.png +0 -0
  21. package/assets/avatars/female/16-fixer-e7a391.png +0 -0
  22. package/assets/avatars/female/17-deployer-92594b.png +0 -0
  23. package/assets/avatars/female/17-deployer-e5a07e.png +0 -0
  24. package/assets/avatars/female/23-mentor-b16a5b.png +0 -0
  25. package/assets/avatars/female/23-mentor-e7a391.png +0 -0
  26. package/assets/avatars/female/25-pioneer-d78774.png +0 -0
  27. package/assets/avatars/female/26-strategist-b16a5b.png +0 -0
  28. package/assets/avatars/female/31-gardener-e5a07e.png +0 -0
  29. package/assets/avatars/female/33-simplifier-b16a5b.png +0 -0
  30. package/assets/avatars/female/33-simplifier-eeb4a4.png +0 -0
  31. package/assets/avatars/female/34-optimizer-92594b.png +0 -0
  32. package/assets/avatars/female/34-optimizer-e7a391.png +0 -0
  33. package/assets/avatars/female/36-integrator-d78774.png +0 -0
  34. package/assets/avatars/female/36-integrator-eeb4a4.png +0 -0
  35. package/assets/avatars/female/37-compiler-b16a5b.png +0 -0
  36. package/assets/avatars/female/37-compiler-e7a391.png +0 -0
  37. package/assets/avatars/female/40-formatter-b16a5b.png +0 -0
  38. package/assets/avatars/female/42-shipper-623d36.png +0 -0
  39. package/assets/avatars/female/46-steward-623d36.png +0 -0
  40. package/assets/avatars/female/46-steward-d78774.png +0 -0
  41. package/assets/avatars/female/47-shepherd-b16a5b.png +0 -0
  42. package/assets/avatars/female/47-shepherd-eeb4a4.png +0 -0
  43. package/assets/avatars/female/48-tinkerer-92594b.png +0 -0
  44. package/assets/avatars/female/50-wrangler-eeb4a4.png +0 -0
  45. package/assets/avatars/index.html +4 -0
  46. package/assets/avatars/male/01-explorer-d78774.png +0 -0
  47. package/assets/avatars/male/06-planner-92594b.png +0 -0
  48. package/assets/avatars/male/06-planner-e7a391.png +0 -0
  49. package/assets/avatars/male/07-reviewer-623d36.png +0 -0
  50. package/assets/avatars/male/07-reviewer-e5a07e.png +0 -0
  51. package/assets/avatars/male/09-migrator-b16a5b.png +0 -0
  52. package/assets/avatars/male/09-migrator-e7a391.png +0 -0
  53. package/assets/avatars/male/11-debugger-623d36.png +0 -0
  54. package/assets/avatars/male/11-debugger-d78774.png +0 -0
  55. package/assets/avatars/male/12-designer-b16a5b.png +0 -0
  56. package/assets/avatars/male/12-designer-eeb4a4.png +0 -0
  57. package/assets/avatars/male/15-editor-d78774.png +0 -0
  58. package/assets/avatars/male/15-editor-eeb4a4.png +0 -0
  59. package/assets/avatars/male/16-fixer-b16a5b.png +0 -0
  60. package/assets/avatars/male/18-scout-623d36.png +0 -0
  61. package/assets/avatars/male/18-scout-d78774.png +0 -0
  62. package/assets/avatars/male/19-architect-b16a5b.png +0 -0
  63. package/assets/avatars/male/19-architect-eeb4a4.png +0 -0
  64. package/assets/avatars/male/20-operator-92594b.png +0 -0
  65. package/assets/avatars/male/20-operator-e7a391.png +0 -0
  66. package/assets/avatars/male/21-coordinator-623d36.png +0 -0
  67. package/assets/avatars/male/21-coordinator-e5a07e.png +0 -0
  68. package/assets/avatars/male/22-auditor-d78774.png +0 -0
  69. package/assets/avatars/male/22-auditor-eeb4a4.png +0 -0
  70. package/assets/avatars/male/25-pioneer-623d36.png +0 -0
  71. package/assets/avatars/male/26-strategist-eeb4a4.png +0 -0
  72. package/assets/avatars/male/28-librarian-623d36.png +0 -0
  73. package/assets/avatars/male/28-librarian-e5a07e.png +0 -0
  74. package/assets/avatars/male/30-tracker-b16a5b.png +0 -0
  75. package/assets/avatars/male/30-tracker-e7a391.png +0 -0
  76. package/assets/avatars/male/31-gardener-92594b.png +0 -0
  77. package/assets/avatars/male/32-refactorer-623d36.png +0 -0
  78. package/assets/avatars/male/32-refactorer-d78774.png +0 -0
  79. package/assets/avatars/male/35-profiler-623d36.png +0 -0
  80. package/assets/avatars/male/35-profiler-e5a07e.png +0 -0
  81. package/assets/avatars/male/38-parser-92594b.png +0 -0
  82. package/assets/avatars/male/38-parser-e5a07e.png +0 -0
  83. package/assets/avatars/male/39-linter-623d36.png +0 -0
  84. package/assets/avatars/male/39-linter-d78774.png +0 -0
  85. package/assets/avatars/male/40-formatter-eeb4a4.png +0 -0
  86. package/assets/avatars/male/41-patcher-92594b.png +0 -0
  87. package/assets/avatars/male/41-patcher-e7a391.png +0 -0
  88. package/assets/avatars/male/42-shipper-e5a07e.png +0 -0
  89. package/assets/avatars/male/43-maintainer-d78774.png +0 -0
  90. package/assets/avatars/male/43-maintainer-eeb4a4.png +0 -0
  91. package/assets/avatars/male/44-guardian-b16a5b.png +0 -0
  92. package/assets/avatars/male/44-guardian-e7a391.png +0 -0
  93. package/assets/avatars/male/45-sentinel-92594b.png +0 -0
  94. package/assets/avatars/male/45-sentinel-e5a07e.png +0 -0
  95. package/assets/avatars/male/48-tinkerer-e7a391.png +0 -0
  96. package/assets/avatars/male/49-hacker-623d36.png +0 -0
  97. package/assets/avatars/male/49-hacker-e5a07e.png +0 -0
  98. package/assets/avatars/male/50-wrangler-d78774.png +0 -0
  99. package/assets/avatars/manifest.json +1202 -0
  100. package/index.ts +11 -0
  101. package/package.json +65 -4
  102. package/src/config.ts +176 -0
  103. package/src/core/avatar.ts +92 -0
  104. package/src/core/claims.ts +302 -0
  105. package/src/core/clock.ts +31 -0
  106. package/src/core/directory.ts +48 -0
  107. package/src/core/format.ts +382 -0
  108. package/src/core/mailbox.ts +239 -0
  109. package/src/core/mesh.ts +781 -0
  110. package/src/core/registry.ts +306 -0
  111. package/src/deliverer.ts +86 -0
  112. package/src/events.ts +66 -0
  113. package/src/index.ts +323 -0
  114. package/src/rpc.ts +52 -0
  115. package/src/storage.ts +105 -0
  116. package/src/tools/args.ts +132 -0
  117. package/src/tools/claim.ts +87 -0
  118. package/src/tools/heartbeat.ts +152 -0
  119. package/src/tools/inbox.ts +76 -0
  120. package/src/tools/index.ts +62 -0
  121. package/src/tools/peers.ts +101 -0
  122. package/src/tools/send.ts +84 -0
  123. package/src/tools/status.ts +137 -0
  124. package/src/tools/types.ts +72 -0
  125. package/src/tools/wait.ts +96 -0
  126. package/src/tui/avatar.ts +188 -0
  127. package/src/tui/index.tsx +216 -0
  128. package/src/types.ts +147 -0
  129. 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
- # Temporary Holding Version
1
+ # opencode-crosstalk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ![The CursorGuard session: a crosstalk_peers call, a lease on src/app/app/app.hpp with a note and a 900s TTL, a request sent to Wren, and Wren's reply landing in an already-running turn](assets/CursorGuard.png)
32
+
33
+ ![The Wren session, the other half of the same exchange: the request it received, its answer naming CursorGuard, and the edit it is making in its own file while it still holds app.hpp](assets/Wren.png)
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
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.