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.
@@ -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
- | `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`) |
12
- | 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 |
13
- | `doctor` FAIL on hooks/MCP entries | Wiring edited or installed for another harness | Re-run `init --harness <name>`, then follow the printed follow-ups |
14
- | 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 |
15
- | `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 |
16
- | 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 |
17
- | Bench budget missed in CI | Loaded runner, not a regression | Re-run; budgets are generous on purpose — only consistent misses across runs count |
18
-
19
- Windows note: detached `spawn` workers are killed as a whole process tree on
20
- Windows; elsewhere termination follows the recorded pid. Drive-root guards
21
- mention `C:\` because that is where a failed cwd resolution lands — the fix
22
- is the same on every OS: set `CREWBUS_DIR` explicitly.
23
-
24
- Still stuck? File a bug with the template in `.github/ISSUE_TEMPLATE/bug.md`
25
- including the `doctor` output and the `[board <path>]` echoes.
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.
@@ -1,6 +1,6 @@
1
1
  # Version skew (mixed-relay operation)
2
2
 
3
- How crewbus versions interoperate. Evidence: `package.json:2` (`10.0.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);
@@ -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.1 · package <code>@eamonpluto/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>
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 &amp; publish</h2>
274
- <pre><code>npm i -g @eamonpluto/crewbus # global install
274
+ <pre><code>npm i -g crewbus # global install
275
275
  crewbus init --harness &lt;name&gt; --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 &lt;name&gt; --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>@eamonpluto/crewbus</code>.</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>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 @eamonpluto/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 &amp; 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>
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 &amp; 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 @eamonpluto/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 @eamonpluto/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>
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/@eamonpluto/crewbus">@eamonpluto/crewbus</a></li>
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>@eamonpluto/crewbus</code></span>
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) agentToSession.set(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
- fs.writeFileSync(p, JSON.stringify({
121
- name: agent,
122
- firstSeen: (prev && prev.firstSeen) || at,
123
- lastSeen: at,
124
- sessionId: (prev && prev.sessionId) || undefined,
125
- lastDir: (prev && prev.lastDir) || undefined,
126
- spawnedPid: (prev && prev.spawnedPid) || undefined,
127
- spawnedAt: (prev && prev.spawnedAt) || undefined,
128
- spawnedBy: (prev && prev.spawnedBy) || undefined,
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
- writeJsonAtomic(ap, {
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
- token: (prev && prev.token) || undefined,
398
- tokenHash: (prev && prev.tokenHash) || undefined,
399
- salt: (prev && prev.salt) || undefined,
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 "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewbus",
3
- "version": "10.0.2",
3
+ "version": "10.0.3",
4
4
  "description": "Zero-dependency message bus for AI coding-agent crews: message another agent, inserted into context, just a tool call.",
5
5
  "type": "module",
6
6
  "bin": {