crewbus 10.0.2 → 10.0.3
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/AGENTS.template.md +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +9 -2
- package/bin/crewbus.js +145 -55
- package/bin/lib/groups.js +4 -1
- package/bin/lib/identity.js +22 -26
- package/bin/lib/mail.js +5 -24
- package/bin/lib/spawn.js +95 -29
- package/bin/lib/store.js +3 -1
- package/bin/lib/web.js +78 -13
- package/docs/GROUPS.md +102 -77
- package/docs/TROUBLESHOOTING.md +26 -25
- package/docs/VERSION_SKEW.md +1 -1
- package/documentation.html +3 -3
- package/index.html +5 -5
- package/opencode/plugins/dm-watch.js +38 -18
- package/opencode/tools/dm-send.js +14 -6
- package/package.json +1 -1
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
# Troubleshooting matrix
|
|
2
|
-
|
|
3
|
-
Start with `crewbus doctor` — it prints the resolved board,
|
|
4
|
-
`CREWBUS_DIR` state, cwd, and git rev, and exits 1 when wiring is broken.
|
|
5
|
-
|
|
6
|
-
| Symptom | Likely cause | Fix |
|
|
7
|
-
|---|---|---|
|
|
8
|
-
| Two agents see different mail / empty inbox looks wrong | Split boards: each session resolved a different `.crewbus/` | Compare the `[board <path>]` echoes on `send`/`inbox`; converge with `export CREWBUS_DIR=<board>` (or per-call `--board <path>`) |
|
|
9
|
-
| `no board at <path>` on `agents`/`inbox`/`listen` | Read commands never create boards (by design) | Run `crewbus init` (or `send`/`register`) at the project root first |
|
|
10
|
-
| `drive root` refusal (`/.crewbus`, `C:\.crewbus`) | cwd resolution failed (detached harness worktree) | Pass an explicit `--board` path or set `CREWBUS_DIR`; see one Windows note below |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Windows
|
|
21
|
-
|
|
22
|
-
is
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
# Troubleshooting matrix
|
|
2
|
+
|
|
3
|
+
Start with `crewbus doctor` — it prints the resolved board,
|
|
4
|
+
`CREWBUS_DIR` state, cwd, and git rev, and exits 1 when wiring is broken.
|
|
5
|
+
|
|
6
|
+
| Symptom | Likely cause | Fix |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| Two agents see different mail / empty inbox looks wrong | Split boards: each session resolved a different `.crewbus/` | Compare the `[board <path>]` echoes on `send`/`inbox`; converge with `export CREWBUS_DIR=<board>` (or per-call `--board <path>`) |
|
|
9
|
+
| `no board at <path>` on `agents`/`inbox`/`listen` | Read commands never create boards (by design) | Run `crewbus init` (or `send`/`register`) at the project root first |
|
|
10
|
+
| `drive root` refusal (`/.crewbus`, `C:\.crewbus`) | cwd resolution failed (detached harness worktree) | Pass an explicit `--board` path or set `CREWBUS_DIR`; see one Windows note below |
|
|
11
|
+
| Group has one member like `cam-1-cam-2-cam-3` | PowerShell array-splatted an unquoted `--add a,b,c` (npm shim rejoins with spaces) | Quote comma lists on PowerShell: `--add "cam-1,cam-2,cam-3"` (same for `--to`, `--to-group`). List flags also accept whitespace separation, so a space-joined list still parses as several names |
|
|
12
|
+
| `wrong token` / auth failure | Token missing or from a different name | Tokens print once and never re-print — reuse the `register` output saved at claim time (or `token rotate --from <name>` with the current token); export `CREWBUS_TOKEN`; names are lowercase (`Alice` == `alice`) |
|
|
13
|
+
| Push never arrives (non-opencode harness) | Hooks not trusted / harness not restarted after `init` | Re-run `crewbus init --harness <name>` (merges, never overwrites), approve MCP/trust prompts (`/hooks`, `/hooks-trust`), restart harness |
|
|
14
|
+
| `doctor` FAIL on hooks/MCP entries | Wiring edited or installed for another harness | Re-run `init --harness <name>`, then follow the printed follow-ups |
|
|
15
|
+
| Duplicate or replayed DMs | Two push paths racing, or pruned markers | Markers in `delivered/` + `cursors/` dedupe across restarts; `redeliver --from <name> --all` after re-registering a dead watcher; `prune` keeps surviving markers so cursors never replay |
|
|
16
|
+
| `spawn` refuses a name | A live worker still holds it | `spawn-status --to <name>` to inspect; `spawn-kill` the stale worker; stale (dead-pid) names are reusable |
|
|
17
|
+
| Sync diverges / old mail returns | Deletions don't replicate; mtime skew | `prune` stays local by design; sync converges on message union + newer-mtime presence — rerun `sync`, check clocks |
|
|
18
|
+
| Bench budget missed in CI | Loaded runner, not a regression | Re-run; budgets are generous on purpose — only consistent misses across runs count |
|
|
19
|
+
|
|
20
|
+
Windows note: detached `spawn` workers are killed as a whole process tree on
|
|
21
|
+
Windows; elsewhere termination follows the recorded pid. Drive-root guards
|
|
22
|
+
mention `C:\` because that is where a failed cwd resolution lands — the fix
|
|
23
|
+
is the same on every OS: set `CREWBUS_DIR` explicitly.
|
|
24
|
+
|
|
25
|
+
Still stuck? File a bug with the template in `.github/ISSUE_TEMPLATE/bug.md`
|
|
26
|
+
including the `doctor` output and the `[board <path>]` echoes.
|
package/docs/VERSION_SKEW.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Version skew (mixed-relay operation)
|
|
2
2
|
|
|
3
|
-
How crewbus versions interoperate. Evidence: `package.json:2` (`10.0.
|
|
3
|
+
How crewbus versions interoperate. Evidence: `package.json:2` (`10.0.3`),
|
|
4
4
|
`CHANGELOG.md` (10.0.0 2026-10-08; 7.0.0 2026-10-05; 6.4.0–6.0.0; 4.1.0/4.0.0 baseline).
|
|
5
5
|
Capabilities: `RELAY_CAPS` + `launch` per `packages/contracts/board-caps.json:6-8`.
|
|
6
6
|
Cell behavior is the NEW side's verified behavior (`bin/lib/sync.js` refs);
|
package/documentation.html
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<main>
|
|
66
66
|
|
|
67
67
|
<h1>crewbus</h1>
|
|
68
|
-
<p class="sub">A zero-dependency local DM bus for AI coding agents — v10.0.
|
|
68
|
+
<p class="sub">A zero-dependency local DM bus for AI coding agents — v10.0.2 · package <code>crewbus</code> · <a href="https://github.com/eamonpluto/crewbus/actions/workflows/ci.yml"><img src="https://github.com/eamonpluto/crewbus/actions/workflows/ci.yml/badge.svg" alt="CI"></a></p>
|
|
69
69
|
<p class="sub">Lead audience: developers running <strong>mixed-harness crews</strong> (opencode, Claude Code, Codex, Antigravity, grok-build, Cursor). <code>package.json</code> is the release version source of truth; <code>board.json</code>'s <code>version: 2</code> is the board <em>schema</em> version, not the release.</p>
|
|
70
70
|
|
|
71
71
|
<h2 id="what">1 · What it is</h2>
|
|
@@ -271,7 +271,7 @@ crewbus doctor # confirm all-ok</code></pre>
|
|
|
271
271
|
<p><code>npm test</code> runs 20 suites — smoke + harness + compact + tokenfile + digest + control-plane + launch + web-launch + pair-api + web-panel + web-approot + client-runtime + runtime-wiring + desktop-sidecar + mobile-client + approvals (×3) + fault-injection + integration — all green. <code>npm run bench</code> asserts scale budgets and <code>npm run bench:load</code> the synthetic 10–10k load test separately. <code>sync-embeds.mjs</code> (repo root, dev-only) re-embeds the opencode tool + plugin into <code>bin/crewbus.js</code> for global installs — run it after editing either file (CI enforces <code>--check</code>).</p>
|
|
272
272
|
|
|
273
273
|
<h2 id="publish">15 · Install & publish</h2>
|
|
274
|
-
<pre><code>npm i -g
|
|
274
|
+
<pre><code>npm i -g crewbus # global install
|
|
275
275
|
crewbus init --harness <name> --portable # portable wiring
|
|
276
276
|
# maintainer: npm test && npm publish (needs npm login + 2FA approval)</code></pre>
|
|
277
277
|
|
|
@@ -292,7 +292,7 @@ crewbus init --harness <name> --portable # portable wiring
|
|
|
292
292
|
<li><strong>3.0.0</strong> — token-bound identity (first claim mints, <code>--token</code>/<code>CREWBUS_TOKEN</code>; breaking); presence (<code>agents --active</code>); <code>prune</code> retention; <code>spawn</code> + <code>spawn-status</code>/<code>spawn-kill</code>; <code>ack</code>/<code>thread</code>/<code>inbox --unacked</code>; <code>web</code> dashboard; <code>stop --all</code>.</li>
|
|
293
293
|
<li><strong>2.4.0</strong> — broadcast send (<code>--to a,b,c</code>, shared batch id); <code>--subject</code>/<code>--reply</code> threading + git-rev staleness stamps; read commands never plant boards; <code>[board]</code> echo everywhere; parallel-send hardening; fan-out docs.</li>
|
|
294
294
|
<li><strong>2.3.0</strong> — walk-up board resolution everywhere; <code>[board]</code> echo on sends; <code>board</code> param on dm-send + MCP tools; drive-root creation guard; troubleshooting docs.</li>
|
|
295
|
-
<li><strong>2.2.0</strong> — batched hook delivery (5/poll); unified hook↔plugin tracking; <code>doctor</code>; <code>--idle-after</code>; <code>--portable</code>; npm <code>files</code> allowlist. Published as <code
|
|
295
|
+
<li><strong>2.2.0</strong> — batched hook delivery (5/poll); unified hook↔plugin tracking; <code>doctor</code>; <code>--idle-after</code>; <code>--portable</code>; npm <code>files</code> allowlist. Published as <code>crewbus</code>.</li>
|
|
296
296
|
<li><strong>2.1.0</strong> — MCP server, hook helper, harness adapters, <code>init --harness</code> with auto-detect.</li>
|
|
297
297
|
<li><strong>2.0.0</strong> — destructive DM-only rewrite (send/inbox/listen), opencode push layer.</li>
|
|
298
298
|
<li><strong>1.1.0</strong> — legacy task-board model.</li>
|
package/index.html
CHANGED
|
@@ -2600,7 +2600,7 @@ kbd {
|
|
|
2600
2600
|
<div class="terminal-stage">
|
|
2601
2601
|
<div class="terminal" role="img" aria-label="Terminal session: installing crewbus, registering a lead, and launching a multi-harness task">
|
|
2602
2602
|
<div class="term-bar"><span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span><span class="term-title">terminal — multi-harness launch in 60 seconds</span></div>
|
|
2603
|
-
<div class="term-body"><div class="term-line"><span class="c-prompt">$ </span><span class="c-cmd">npm i -g
|
|
2603
|
+
<div class="term-body"><div class="term-line"><span class="c-prompt">$ </span><span class="c-cmd">npm i -g crewbus</span></div><div class="term-line"><span class="c-prompt">$ </span><span class="c-cmd">crewbus init</span> <span class="c-flag">--harness claude,antigravity,codex</span></div><div class="term-line"><span class="c-ok">board ready at ./.crewbus — multi-harness adapters & hooks configured</span></div><div class="term-line"><span class="c-prompt">$ </span><span class="c-cmd">crewbus register</span> <span class="c-flag">--from lead</span></div><div class="term-line"><span class="c-out">first claim wins — token minted (saved to logs/lead.token, mode 0600)</span></div><div class="term-line"><span class="c-prompt">$ </span><span class="c-cmd">crewbus launch</span> <span class="c-flag">--from lead --harness claude,antigravity --model claude-3-7-sonnet --workers 2</span> <span class="c-str">"Verify auth flow and benchmark."</span></div><div class="term-line"><span class="c-ok">2 workers booted across 2 harnesses · replies thread back · ack to close loop</span> <span class="term-caret"></span></div></div>
|
|
2604
2604
|
<p class="term-cap">Real commands, real output shape — paste them as-is against any board.</p>
|
|
2605
2605
|
</div>
|
|
2606
2606
|
</div>
|
|
@@ -3314,8 +3314,8 @@ crewbus spawn --from lead --to alice,bob --subject "brief: cards" \
|
|
|
3314
3314
|
<div class="cta-panel">
|
|
3315
3315
|
<h2>Install crewbus. Brief your first crew today.</h2>
|
|
3316
3316
|
<p class="lede">One package, one minute, six harnesses. MIT licensed, tested on Linux, macOS and Windows.</p>
|
|
3317
|
-
<div class="install-box">npm i -g
|
|
3318
|
-
<p class="dim small" style="margin-top: 18px; position: relative;">Staying current is one command — <code>npm i -g
|
|
3317
|
+
<div class="install-box">npm i -g crewbus<br>crewbus init --harness opencode<br>crewbus register --from lead<br>crewbus doctor</div>
|
|
3318
|
+
<p class="dim small" style="margin-top: 18px; position: relative;">Staying current is one command — <code>npm i -g crewbus@latest</code> — and boards carry a schema version, so old checkouts keep working. <a class="top-link" href="#top" style="margin-top: 0;">↑ Back to top</a></p>
|
|
3319
3319
|
<p class="dim small" style="margin-top: 10px; position: relative;">Hacking on it? Clone the repo, run <code>npm test</code>, and read the module map — MIT licensed, zero dependencies, no build step.</p>
|
|
3320
3320
|
<div class="btn-row" style="justify-content: center;">
|
|
3321
3321
|
<a class="btn btn-primary" href="documentation.html">Read the manual</a>
|
|
@@ -3361,11 +3361,11 @@ crewbus spawn --from lead --to alice,bob --subject "brief: cards" \
|
|
|
3361
3361
|
<li><a href="https://github.com/eamonpluto/crewbus">GitHub</a></li>
|
|
3362
3362
|
<li><a href="https://github.com/eamonpluto/crewbus/actions/workflows/ci.yml">CI: Linux · macOS · Windows</a></li>
|
|
3363
3363
|
<li><a href="CHANGELOG.md">Changelog</a></li>
|
|
3364
|
-
<li><a href="https://www.npmjs.com/package
|
|
3364
|
+
<li><a href="https://www.npmjs.com/package/crewbus">crewbus</a></li>
|
|
3365
3365
|
</ul>
|
|
3366
3366
|
</div>
|
|
3367
3367
|
</div>
|
|
3368
|
-
<span>crewbus · one primitive: message another agent · MIT · package <code
|
|
3368
|
+
<span>crewbus · one primitive: message another agent · MIT · package <code>crewbus</code></span>
|
|
3369
3369
|
<span>v10.0.2 · zero dependencies · Node 18+</span>
|
|
3370
3370
|
</footer>
|
|
3371
3371
|
</div>
|
|
@@ -65,6 +65,13 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
65
65
|
const deliveredDir = path.join(root, "delivered");
|
|
66
66
|
|
|
67
67
|
const agentToSession = new Map(); // agent -> sessionID
|
|
68
|
+
const sessionMeta = new Map(); // agent -> { sessionID, confirmedAt }
|
|
69
|
+
// A mapped session is proof of life only while fresh: entries age out
|
|
70
|
+
// after TTL without a confirmed delivery or re-registration, so dead
|
|
71
|
+
// workers stop being heartbeated (and stop reading "active") instead
|
|
72
|
+
// of lingering forever. Their mail then waits for pull until the live
|
|
73
|
+
// session re-registers — the same fallback as a refused push.
|
|
74
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
68
75
|
const processed = new Set(); // "<agent>/<msgId>"
|
|
69
76
|
const lastBeat = new Map(); // agent -> epoch ms of last presence write
|
|
70
77
|
|
|
@@ -97,7 +104,14 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
97
104
|
for (const f of files) {
|
|
98
105
|
if (!f.endsWith(".json")) continue;
|
|
99
106
|
const doc = readJsonSafe(path.join(agentsDir, f));
|
|
100
|
-
if (doc && doc.name && doc.sessionId)
|
|
107
|
+
if (doc && doc.name && doc.sessionId) {
|
|
108
|
+
agentToSession.set(doc.name, doc.sessionId);
|
|
109
|
+
const meta = sessionMeta.get(doc.name);
|
|
110
|
+
if (!meta || meta.sessionID !== doc.sessionId) {
|
|
111
|
+
// (Re)registration: a new session id is fresh proof of life.
|
|
112
|
+
sessionMeta.set(doc.name, { sessionID: doc.sessionId, confirmedAt: Date.now() });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
@@ -117,23 +131,15 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
117
131
|
if (prev && prev.lastSeen && now - new Date(prev.lastSeen).getTime() < 60000) return;
|
|
118
132
|
const at = new Date().toISOString();
|
|
119
133
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
briefId: (prev && prev.briefId) || undefined,
|
|
130
|
-
spawnedWorktree: (prev && prev.spawnedWorktree) || undefined,
|
|
131
|
-
spawnedBranch: (prev && prev.spawnedBranch) || undefined,
|
|
132
|
-
spawnedLifetime: (prev && prev.spawnedLifetime) || undefined,
|
|
133
|
-
token: (prev && prev.token) || undefined,
|
|
134
|
-
tokenHash: (prev && prev.tokenHash) || undefined,
|
|
135
|
-
salt: (prev && prev.salt) || undefined,
|
|
136
|
-
}, null, 2) + "\n");
|
|
134
|
+
// Merge over prev, never enumerate (see mergeAgentDoc in
|
|
135
|
+
// bin/lib/identity.js — this file is standalone so the merge is
|
|
136
|
+
// inlined): a heartbeat must not drop fields it doesn't own.
|
|
137
|
+
const merged = { ...(prev && typeof prev === "object" ? prev : {}) };
|
|
138
|
+
for (const [k, v] of Object.entries({ name: agent, firstSeen: (prev && prev.firstSeen) || at, lastSeen: at })) {
|
|
139
|
+
if (v === undefined) delete merged[k];
|
|
140
|
+
else merged[k] = v;
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n");
|
|
137
143
|
} catch {}
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -328,6 +334,10 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
328
334
|
await client.session.prompt({ path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
|
|
329
335
|
}
|
|
330
336
|
advanceCursor(agent, msg.id);
|
|
337
|
+
// A completed push is the only proof of life this watcher can
|
|
338
|
+
// observe directly — refresh the mapping TTL so live sessions
|
|
339
|
+
// stay pushable across idle stretches.
|
|
340
|
+
sessionMeta.set(agent, { sessionID, confirmedAt: Date.now() });
|
|
331
341
|
} catch (e) {
|
|
332
342
|
const detail = e && e.message ? e.message : String(e);
|
|
333
343
|
releaseClaim(agent, msg.id);
|
|
@@ -360,6 +370,16 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
360
370
|
for (const agent of agents) {
|
|
361
371
|
const sessionID = agentToSession.get(agent);
|
|
362
372
|
if (!sessionID) continue; // nobody live for this name — mail waits for pull
|
|
373
|
+
const meta = sessionMeta.get(agent);
|
|
374
|
+
if (!meta || meta.sessionID !== sessionID || Date.now() - meta.confirmedAt > SESSION_TTL_MS) {
|
|
375
|
+
// Stale mapping: no confirmed delivery and no re-registration
|
|
376
|
+
// within TTL. Drop it (clears the doc's sessionId too) so the
|
|
377
|
+
// agent stops reading "active" and its mail waits for pull until
|
|
378
|
+
// the live session re-registers.
|
|
379
|
+
invalidateSession(agent, sessionID);
|
|
380
|
+
sessionMeta.delete(agent);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
363
383
|
heartbeat(agent);
|
|
364
384
|
let files = [];
|
|
365
385
|
try {
|
|
@@ -382,22 +382,30 @@ export default tool({
|
|
|
382
382
|
const tokenHint = minted.created ? ` identity '${from}' claimed, token ${minted.token} (set CREWBUS_TOKEN=${minted.token})` : "";
|
|
383
383
|
const rev = gitRev(root);
|
|
384
384
|
const at = new Date().toISOString();
|
|
385
|
-
// upsert sender with live session routing for the watcher plugin
|
|
385
|
+
// upsert sender with live session routing for the watcher plugin.
|
|
386
|
+
// Merge over prev, never enumerate: a hand-built record here once
|
|
387
|
+
// wiped spawn bookkeeping (spawnedPid/By, briefId) and roles off
|
|
388
|
+
// every worker that reported through this tool.
|
|
386
389
|
const ap = path.join(root, "agents", from + ".json");
|
|
387
390
|
let prev = null;
|
|
388
391
|
try {
|
|
389
392
|
prev = JSON.parse(fs.readFileSync(ap, "utf8"));
|
|
390
393
|
} catch {}
|
|
391
|
-
|
|
394
|
+
const merged = { ...(prev && typeof prev === "object" ? prev : {}) };
|
|
395
|
+
for (const [k, v] of Object.entries({
|
|
392
396
|
name: from,
|
|
393
397
|
firstSeen: (prev && prev.firstSeen) || new Date().toISOString(),
|
|
394
398
|
lastSeen: new Date().toISOString(),
|
|
395
399
|
sessionId: (context && context.sessionID) || (prev && prev.sessionId) || undefined,
|
|
396
400
|
lastDir: context.worktree || context.directory || undefined,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
+
})) {
|
|
402
|
+
if (v === undefined) delete merged[k];
|
|
403
|
+
else merged[k] = v;
|
|
404
|
+
}
|
|
405
|
+
for (const k of Object.keys(merged)) {
|
|
406
|
+
if (merged[k] === undefined) delete merged[k];
|
|
407
|
+
}
|
|
408
|
+
writeJsonAtomic(ap, merged);
|
|
401
409
|
const isAll = recipients.length === 1 && recipients[0] === "@all";
|
|
402
410
|
const mirrorChannels = (batch) => {
|
|
403
411
|
if (args.also_channel !== true) return "";
|