crewbus 10.0.3 → 10.0.5
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/CHANGELOG.md +26 -0
- package/README.md +2 -2
- package/bin/crewbus.js +115 -4
- package/bin/lib/groups.js +94 -1
- package/bin/lib/mail.js +21 -0
- package/bin/lib/spawn.js +18 -11
- package/bin/lib/web.js +104 -16
- package/docs/GROUPS.md +7 -2
- package/docs/VERSION_SKEW.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
- Package is now published as unscoped `crewbus` (v10.0.2): `npm i -g crewbus`.
|
|
6
6
|
|
|
7
|
+
## 10.0.5 (2026-10-12)
|
|
8
|
+
|
|
9
|
+
PATCH RELEASE: Self-update command. No board-schema change (`board.json` stays v2); additive CLI surface only.
|
|
10
|
+
|
|
11
|
+
- **`crewbus update [--check] [--yes] [--version x.y.z]`**:
|
|
12
|
+
- Version check via `npm view <own-name>@latest version`, install via `npm install -g` — registry, auth, and proxy come from the user's npm config, never hardcoded.
|
|
13
|
+
- Package name always read from our own `package.json` (never argv); `--version` accepts strict `x.y.z` only.
|
|
14
|
+
- `--check` reports only; bare `update` refuses off-TTY without `--yes` (install scripts execute from the network); source checkouts are refused with `git pull` guidance.
|
|
15
|
+
- Already-latest and newer-than-registry are no-ops; explicit pins allow downgrades; post-install note covers the old-code process restart + `init --force` for wiring.
|
|
16
|
+
|
|
17
|
+
## 10.0.4 (2026-10-12)
|
|
18
|
+
|
|
19
|
+
PATCH RELEASE: Briefs view + gather endpoint, task-scoped spawn rosters, and mobile brief/thread/watcher screens. No board-schema change (`board.json` stays v2); all additions backward-compatible.
|
|
20
|
+
|
|
21
|
+
- **Dashboard Briefs view + `GET /api/gather?batch=`**:
|
|
22
|
+
- New Briefs section: one row per fan-out (subject, workers, checkpoint/final counts, acked fraction, relative last activity); click a row for its gathered thread (briefs, checkpoint timeline, finals with ack ticks, telemetry footer).
|
|
23
|
+
- Crews table gains a brief/groups column (batch short-id + group names, or "solo"); Recent activity gains a text filter + checkpoints/finals toggle.
|
|
24
|
+
- **Task-scoped spawn rosters**:
|
|
25
|
+
- `buildCrewContext` takes the task's groups only; groups from unrelated concurrent briefs collapse to a count (`N other groups exist on this board`) instead of leaking names cross-task. Documented in `docs/GROUPS.md`.
|
|
26
|
+
- **Mobile (M5)**:
|
|
27
|
+
- Boards card counts live workers (`alive === true`) instead of all pid-bearing workers.
|
|
28
|
+
- Triage groups inbox by brief thread with checkpoint/final/note chips.
|
|
29
|
+
- New Briefs screen (one row per batch, tap for thread via `/api/gather`).
|
|
30
|
+
- Foreground watcher (`src/lib/watch.js`): polls inbox approvals + board dead-man state, alerts once per new id with in-app banner + haptic. Foreground-only by scaffold rule (no notification plugins); killed-app push remains future work (needs the plugin rule lifted + EAS credentials + a relay sender).
|
|
31
|
+
- `/api/inbox` items now carry `checkpoint` (additive).
|
|
32
|
+
|
|
7
33
|
## 10.0.3 (2026-10-10)
|
|
8
34
|
|
|
9
35
|
PATCH RELEASE: Worker-record integrity, truthful presence, and multi-group collaboration in spawn briefs. No board-schema change (`board.json` stays v2); all prompt additions are backward-compatible (absent unless the spawn carries group/compute context).
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# crewbus v10.0.
|
|
1
|
+
# crewbus v10.0.5 — DM bus
|
|
2
2
|
|
|
3
3
|
[](https://github.com/eamonpluto/crewbus/actions/workflows/ci.yml)
|
|
4
4
|
|
|
@@ -55,7 +55,7 @@ env overrides below.)
|
|
|
55
55
|
Layout: `board.json`, `agents/<name>.json`, `dm/<recipient>/<id>.json`,
|
|
56
56
|
`delivered/<recipient>/<id>.json` (push markers, written by the plugin).
|
|
57
57
|
|
|
58
|
-
Versions: `package.json` (currently 10.0.
|
|
58
|
+
Versions: `package.json` (currently 10.0.5) is the source of truth for the
|
|
59
59
|
release version. `board.json`'s `version: 2` is the **board schema version**
|
|
60
60
|
(a different number on purpose — do not "align" them). New to the project?
|
|
61
61
|
Start with `docs/QUICKSTART.md` (5 minutes).
|
package/bin/crewbus.js
CHANGED
|
@@ -2869,16 +2869,21 @@ function cmdSpawn(args) {
|
|
|
2869
2869
|
if (alsoChannel && (!toGroupRaw || !String(toGroupRaw).trim())) fail("--also-channel needs --to-group <g,...> (it mirrors the brief into each group's channel)");
|
|
2870
2870
|
const groupNames = String(toGroupRaw || "").split(",").map((s) => String(s).trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40)).filter(Boolean);
|
|
2871
2871
|
// Crew roster for the prompt block (see buildCrewContext in lib/spawn.js):
|
|
2872
|
-
//
|
|
2873
|
-
//
|
|
2874
|
-
//
|
|
2872
|
+
// TASK-scoped, not board-scoped. Only the groups named in THIS spawn
|
|
2873
|
+
// appear in the brief — their lanes are what a worker may need to
|
|
2874
|
+
// contact. Groups from unrelated concurrent briefs collapse to a count
|
|
2875
|
+
// (discoverable via `crewbus group list`, never advertised cross-task:
|
|
2876
|
+
// ambient cross-task rosters are prompt noise and a misleading
|
|
2877
|
+
// invitation to DM crews working on unrelated tasks).
|
|
2875
2878
|
const rosterGroups = [];
|
|
2876
2879
|
for (const e of listJson(d.groups)) {
|
|
2877
2880
|
const g = e && e.data;
|
|
2878
2881
|
if (g && g.name && Array.isArray(g.members)) rosterGroups.push({ name: g.name, members: g.members.slice() });
|
|
2879
2882
|
}
|
|
2880
2883
|
rosterGroups.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
2881
|
-
const
|
|
2884
|
+
const taskGroups = rosterGroups.filter((g) => groupNames.includes(g.name));
|
|
2885
|
+
const offTask = rosterGroups.length - taskGroups.length;
|
|
2886
|
+
const crewFor = (to) => buildCrewContext({ name: to, from, subject, groups: taskGroups, myGroups: taskGroups.filter((g) => g.members.includes(to)).map((g) => g.name), offTask });
|
|
2882
2887
|
const model = getFlag(args, "--model");
|
|
2883
2888
|
const auto = args.includes("--auto");
|
|
2884
2889
|
requireAutoConfirm(args);
|
|
@@ -6321,6 +6326,10 @@ Setup:
|
|
|
6321
6326
|
project-local surface (claude/opencode/cursor); codex/grok/antigravity
|
|
6322
6327
|
are skipped (TOML or global-only surfaces — see docs/COMPATIBILITY.md).
|
|
6323
6328
|
Nothing else is widened.
|
|
6329
|
+
crewbus update [--check] [--yes] [--version x.y.z]
|
|
6330
|
+
self-update the global npm install to the registry latest
|
|
6331
|
+
(--check reports only; needs --yes off-TTY; runs the published
|
|
6332
|
+
install scripts — same trust as npm i -g crewbus)
|
|
6324
6333
|
|
|
6325
6334
|
Identity (first claim wins, token after that):
|
|
6326
6335
|
crewbus register --from <you> [--session <opencode-session-id>] [--token <t>] [--expires-in <dur>] [--service]
|
|
@@ -7332,6 +7341,107 @@ function cmdInstallCli() {
|
|
|
7332
7341
|
}
|
|
7333
7342
|
}
|
|
7334
7343
|
|
|
7344
|
+
// Self-update support: which package did this process install as? Read
|
|
7345
|
+
// from our own package.json next to bin/crewbus.js — the name is never
|
|
7346
|
+
// taken from argv, so there is no `update <other-pkg>` confusion/abuse.
|
|
7347
|
+
function ownPackage() {
|
|
7348
|
+
try {
|
|
7349
|
+
const here = path.dirname(path.resolve(process.argv[1] || ""));
|
|
7350
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, "..", "package.json"), "utf8"));
|
|
7351
|
+
if (pkg && typeof pkg.name === "string" && typeof pkg.version === "string") {
|
|
7352
|
+
return { name: pkg.name, version: pkg.version, dir: path.resolve(path.join(here, "..")) };
|
|
7353
|
+
}
|
|
7354
|
+
} catch {}
|
|
7355
|
+
return null;
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7358
|
+
// Zero-dep semver compare (release parts only; prerelease suffixes compare
|
|
7359
|
+
// equal — `npm view` reports the latest stable tag, so suffixes never meet
|
|
7360
|
+
// in the check path; --version pins accept them verbatim).
|
|
7361
|
+
function cmpSemver(a, b) {
|
|
7362
|
+
const parts = (s) => String(s || "").trim().replace(/^[=v]/, "").split(".").map((x) => {
|
|
7363
|
+
const n = Number(String(x).split("-")[0]);
|
|
7364
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
7365
|
+
});
|
|
7366
|
+
const pa = parts(a);
|
|
7367
|
+
const pb = parts(b);
|
|
7368
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
7369
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
7370
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
7371
|
+
}
|
|
7372
|
+
return 0;
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
function cmdUpdate(args) {
|
|
7376
|
+
const checkOnly = args.includes("--check");
|
|
7377
|
+
const yes = args.includes("--yes");
|
|
7378
|
+
const wantRaw = getFlag(args, "--version");
|
|
7379
|
+
if (wantRaw !== undefined && !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(String(wantRaw).trim())) {
|
|
7380
|
+
fail(`--version must be a plain x.y.z version (got "${wantRaw}") — no tags, URLs, or ranges`);
|
|
7381
|
+
}
|
|
7382
|
+
const own = ownPackage();
|
|
7383
|
+
if (!own) fail("cannot determine the installed package (no package.json next to the CLI) — reinstall with: npm i -g crewbus");
|
|
7384
|
+
try {
|
|
7385
|
+
if (fs.existsSync(path.join(own.dir, ".git"))) {
|
|
7386
|
+
fail(`running from a source checkout at ${own.dir} — update it with: git pull (npm would install the registry copy elsewhere; the global CLI is a separate install)`);
|
|
7387
|
+
}
|
|
7388
|
+
} catch (e) {
|
|
7389
|
+
if (e && e.message && e.message.startsWith("running from a source checkout")) throw e;
|
|
7390
|
+
}
|
|
7391
|
+
// The user's own npm: registry, auth, proxy and cache all come from
|
|
7392
|
+
// their npm config — never hardcoded here. shell:true on Windows so the
|
|
7393
|
+
// npm.cmd shim resolves (same convention as probeBinary in lib/launch.js).
|
|
7394
|
+
const npmShell = process.platform === "win32";
|
|
7395
|
+
try {
|
|
7396
|
+
execFileSync("npm", ["--version"], { stdio: ["ignore", "pipe", "pipe"], timeout: 15000, shell: npmShell });
|
|
7397
|
+
} catch {
|
|
7398
|
+
fail("npm was not found on PATH — install Node.js/npm first, or update manually: npm i -g crewbus@latest");
|
|
7399
|
+
}
|
|
7400
|
+
let latest = "";
|
|
7401
|
+
try {
|
|
7402
|
+
latest = String(execFileSync("npm", ["view", `${own.name}@latest`, "version"], { stdio: ["ignore", "pipe", "pipe"], timeout: 30000, shell: npmShell }) || "").trim();
|
|
7403
|
+
} catch {
|
|
7404
|
+
latest = "";
|
|
7405
|
+
}
|
|
7406
|
+
if (!latest) fail(`could not reach the npm registry (offline?) — latest ${own.name} version unknown; retry with network, or install explicitly: npm i -g ${own.name}@<x.y.z>`);
|
|
7407
|
+
const want = wantRaw !== undefined ? String(wantRaw).trim() : latest;
|
|
7408
|
+
const cmp = cmpSemver(own.version, want);
|
|
7409
|
+
if (cmp === 0) {
|
|
7410
|
+
console.log(`crewbus ${own.version} installed${wantRaw !== undefined ? ` (requested ${want})` : `, ${latest} latest`} — already up to date [board n/a]`);
|
|
7411
|
+
return;
|
|
7412
|
+
}
|
|
7413
|
+
if (cmp > 0 && wantRaw === undefined) {
|
|
7414
|
+
console.log(`crewbus ${own.version} installed is newer than registry latest ${latest} (dev install?) — nothing to do [board n/a]`);
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
if (checkOnly) {
|
|
7418
|
+
console.log(`crewbus ${own.version} installed, ${want} available — re-run without --check to update [board n/a]`);
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
const direction = cmp < 0 ? "upgrade" : "downgrade";
|
|
7422
|
+
if (!yes) {
|
|
7423
|
+
if (process.stdin && process.stdin.isTTY) {
|
|
7424
|
+
process.stderr.write(`This ${direction}s ${own.name} ${own.version} → ${want} via "npm install -g" (runs the published package's install scripts). Type "yes" to continue: `);
|
|
7425
|
+
let answer = "";
|
|
7426
|
+
try {
|
|
7427
|
+
answer = String(fs.readFileSync(0, "utf8") || "").trim().toLowerCase();
|
|
7428
|
+
} catch {}
|
|
7429
|
+
if (answer !== "yes" && answer !== "y") fail("update refused (confirmation not given) — re-run with --yes to confirm");
|
|
7430
|
+
} else {
|
|
7431
|
+
fail(`refusing to self-update non-interactively without --yes (npm install -g executes the published package's install scripts) — re-run with --yes to confirm`);
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
console.log(`${direction === "upgrade" ? "Updating" : "Downgrading"} ${own.name} ${own.version} → ${want} (npm install -g ${own.name}@${want})…`);
|
|
7435
|
+
try {
|
|
7436
|
+
execFileSync("npm", ["install", "-g", `${own.name}@${want}`], { stdio: "inherit", timeout: 300000, shell: npmShell });
|
|
7437
|
+
} catch (e) {
|
|
7438
|
+
const detail = String((e && e.message) || e);
|
|
7439
|
+
const busy = /EBUSY|EPERM/i.test(detail);
|
|
7440
|
+
fail(`npm install failed: ${detail.slice(0, 300)}${busy ? " — close running crewbus processes (web/serve/listen/watchers holding the install open) and retry" : ""}`);
|
|
7441
|
+
}
|
|
7442
|
+
console.log(`updated to ${want} — this process is still the old code: restart your terminal (and any crewbus web/serve/listen processes), then re-run init --force in your projects to refresh harness wiring [board n/a]`);
|
|
7443
|
+
}
|
|
7444
|
+
|
|
7335
7445
|
async function main() {
|
|
7336
7446
|
const [, , cmd, ...rest] = process.argv;
|
|
7337
7447
|
if (REMOVED.has(cmd)) {
|
|
@@ -7340,6 +7450,7 @@ async function main() {
|
|
|
7340
7450
|
switch (cmd) {
|
|
7341
7451
|
case "init": return cmdInit(rest);
|
|
7342
7452
|
case "install-cli": return cmdInstallCli();
|
|
7453
|
+
case "update": return cmdUpdate(rest);
|
|
7343
7454
|
case "register": return cmdRegister(rest);
|
|
7344
7455
|
case "acl": return cmdAcl(rest);
|
|
7345
7456
|
case "agents": return cmdAgents(rest);
|
package/bin/lib/groups.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { readJson, writeJson, listJson, fail, parseGroupList, cleanGroupName } from "./store.js";
|
|
10
|
-
import { readDMs, findMessageById, isVerified, scanAllMessages } from "./mail.js";
|
|
10
|
+
import { readDMs, findMessageById, isVerified, scanAllMessages, ackedIdSet } from "./mail.js";
|
|
11
11
|
import { readChannelPosts } from "./channels.js";
|
|
12
12
|
|
|
13
13
|
// parseGroupList + cleanGroupName live in store.js (canonical home).
|
|
@@ -197,6 +197,99 @@ export function gatherTelemetry(items) {
|
|
|
197
197
|
return { messages: items.length, tokensEst: Math.floor(chars / 4) };
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// Batch (brief) enumeration for the dashboard Briefs view: one row per
|
|
201
|
+
// fan-out/thread on the board — briefs, checkpoints, finals, ack state,
|
|
202
|
+
// last activity. Keys come from message batch stamps plus spawned
|
|
203
|
+
// workers' briefIds (single-recipient briefs carry no batch until
|
|
204
|
+
// replied to); a batch-less root with no replies and no worker pointing
|
|
205
|
+
// at it is a stray DM, not a brief, and is skipped. Stats mirror
|
|
206
|
+
// collectBatch + gatherTelemetry; pure read, no board IO.
|
|
207
|
+
export function listBatches(d) {
|
|
208
|
+
const all = [];
|
|
209
|
+
let subs = [];
|
|
210
|
+
try {
|
|
211
|
+
subs = fs.readdirSync(d.dm);
|
|
212
|
+
} catch {
|
|
213
|
+
subs = [];
|
|
214
|
+
}
|
|
215
|
+
for (const sub of subs) {
|
|
216
|
+
try {
|
|
217
|
+
if (!fs.statSync(path.join(d.dm, sub)).isDirectory()) continue;
|
|
218
|
+
} catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const m of readDMs(d, sub)) all.push(m);
|
|
222
|
+
}
|
|
223
|
+
for (const e of listJson(d.broadcast || path.join(d.root, "broadcast"))) {
|
|
224
|
+
const b = e.data;
|
|
225
|
+
if (b && b.id && b.from) {
|
|
226
|
+
if (!b.batch) b.batch = b.id;
|
|
227
|
+
all.push(b);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const childCount = new Map();
|
|
231
|
+
for (const m of all) {
|
|
232
|
+
if (!m.replyTo) continue;
|
|
233
|
+
childCount.set(m.replyTo, (childCount.get(m.replyTo) || 0) + 1);
|
|
234
|
+
}
|
|
235
|
+
const keys = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const addKey = (k) => {
|
|
238
|
+
if (k && !seen.has(k)) {
|
|
239
|
+
seen.add(k);
|
|
240
|
+
keys.push(k);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
for (const m of all) {
|
|
244
|
+
if (m.batch) addKey(m.batch);
|
|
245
|
+
else if (!m.replyTo && (childCount.get(m.id) || 0) > 0) addKey(m.id);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
for (const e of listJson(path.join(d.root, "agents"))) {
|
|
249
|
+
const a = e && e.data;
|
|
250
|
+
if (a && a.briefId) addKey(a.briefId);
|
|
251
|
+
}
|
|
252
|
+
} catch {}
|
|
253
|
+
const acked = ackedIdSet(d);
|
|
254
|
+
const rows = [];
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
const res = collectBatch(d, key);
|
|
257
|
+
if (!res) continue;
|
|
258
|
+
const briefs = res.items.filter((m) => !m.replyTo);
|
|
259
|
+
if (briefs.length === 0) continue;
|
|
260
|
+
briefs.sort((a, b) => String(a.at).localeCompare(String(b.at)) || String(a.id).localeCompare(String(b.id)));
|
|
261
|
+
const replies = res.items.filter((m) => !!m.replyTo);
|
|
262
|
+
const checkpoints = replies.filter((m) => m.checkpoint === true);
|
|
263
|
+
const finals = replies.filter((m) => m.checkpoint !== true);
|
|
264
|
+
const workers = [];
|
|
265
|
+
for (const b of briefs) {
|
|
266
|
+
const tos = Array.isArray(b.to) ? b.to : (b.to ? [b.to] : []);
|
|
267
|
+
for (const t of tos) {
|
|
268
|
+
if (t && !workers.includes(t)) workers.push(t);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let lastAt = briefs[0].at || "";
|
|
272
|
+
for (const m of res.items) {
|
|
273
|
+
if (String(m.at || "") > String(lastAt || "")) lastAt = m.at;
|
|
274
|
+
}
|
|
275
|
+
rows.push({
|
|
276
|
+
batch: key,
|
|
277
|
+
subject: briefs[0].subject || "",
|
|
278
|
+
from: briefs[0].from || "",
|
|
279
|
+
at: briefs[0].at || "",
|
|
280
|
+
workers,
|
|
281
|
+
briefCount: briefs.length,
|
|
282
|
+
replyCount: replies.length,
|
|
283
|
+
checkpointCount: checkpoints.length,
|
|
284
|
+
finalCount: finals.length,
|
|
285
|
+
ackedFinals: finals.filter((m) => acked.has(m.id)).length,
|
|
286
|
+
lastAt: lastAt || "",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
rows.sort((a, b) => String(b.lastAt).localeCompare(String(a.lastAt)) || String(a.batch).localeCompare(String(b.batch)));
|
|
290
|
+
return rows;
|
|
291
|
+
}
|
|
292
|
+
|
|
200
293
|
export function batchReplyIds(d, batch) {
|
|
201
294
|
const res = collectBatch(d, batch);
|
|
202
295
|
if (!res) return null;
|
package/bin/lib/mail.js
CHANGED
|
@@ -388,6 +388,27 @@ export function ackedIds(d, agent) {
|
|
|
388
388
|
return out;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
// Ack markers across every agent: message ids with at least one ack, by
|
|
392
|
+
// anyone. Powers batch-thread ack state (dashboard Briefs/gather) —
|
|
393
|
+
// orthogonal to per-agent ackedIds(), which scopes triage to one inbox.
|
|
394
|
+
export function ackedIdSet(d) {
|
|
395
|
+
const out = new Set();
|
|
396
|
+
let subs = [];
|
|
397
|
+
try {
|
|
398
|
+
subs = fs.readdirSync(path.join(d.root, "acked"));
|
|
399
|
+
} catch {
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
for (const sub of subs) {
|
|
403
|
+
try {
|
|
404
|
+
for (const f of fs.readdirSync(path.join(d.root, "acked", sub)).filter((f) => f.endsWith(".json"))) {
|
|
405
|
+
out.add(f.replace(/\.json$/, ""));
|
|
406
|
+
}
|
|
407
|
+
} catch {}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
391
412
|
export function splitCommand(cmd) {
|
|
392
413
|
const out = [];
|
|
393
414
|
let cur = "";
|
package/bin/lib/spawn.js
CHANGED
|
@@ -185,14 +185,19 @@ export function buildSpawnPrompt({ name, from, subject, body, replyId, rev, cwd,
|
|
|
185
185
|
|
|
186
186
|
// Crew context block for the spawn prompt: the roster a worker needs to
|
|
187
187
|
// talk peer-direct instead of star-topologizing every question through
|
|
188
|
-
// the lead. `groups` is the
|
|
189
|
-
// `myGroups` the group names this recipient
|
|
190
|
-
// line for my groups (the spawn call's
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
188
|
+
// the lead. `groups` is the TASK roster — the groups named in this spawn
|
|
189
|
+
// only — [{ name, members }]; `myGroups` the group names this recipient
|
|
190
|
+
// belongs to; `subject` the lane line for my groups (the spawn call's
|
|
191
|
+
// --subject); `offTask` how many board groups from unrelated concurrent
|
|
192
|
+
// briefs exist (collapsed to a count, never enumerated: cross-task
|
|
193
|
+
// rosters are prompt noise and a misleading invitation — "ask the group
|
|
194
|
+
// that owns a thing" only makes sense for sibling lanes of the same
|
|
195
|
+
// task). Workers may send to each other (and to-group) and tail any
|
|
196
|
+
// channel, but channel-post is lead|admin-only (see identity.js
|
|
197
|
+
// LEAD_PLUS) — so the block points reads at channels and writes at DMs.
|
|
198
|
+
// Returns bullet lines (no header); the caller renders them under a
|
|
199
|
+
// Crew: header. Pure: unit-tested, no board IO.
|
|
200
|
+
export function buildCrewContext({ name, from, subject, groups, myGroups, offTask }) {
|
|
196
201
|
const all = (groups || []).filter((g) => g && g.name);
|
|
197
202
|
const mineSet = new Set(myGroups || []);
|
|
198
203
|
const mine = all.filter((g) => mineSet.has(g.name));
|
|
@@ -200,6 +205,8 @@ export function buildCrewContext({ name, from, subject, groups, myGroups }) {
|
|
|
200
205
|
const lane = subject && String(subject).trim() ? String(subject).trim() : "the brief above";
|
|
201
206
|
const me = String(name || "you");
|
|
202
207
|
const lead = String(from || "lead");
|
|
208
|
+
const off = Math.max(0, Number(offTask) || 0);
|
|
209
|
+
const offNoun = (n) => `${n} other group${n === 1 ? "" : "s"}`;
|
|
203
210
|
const showMembers = (members) => {
|
|
204
211
|
const ms = (members || []).filter(Boolean).map(String);
|
|
205
212
|
return ms.slice(0, 8).join(",") + (ms.length > 8 ? "…" : "");
|
|
@@ -212,11 +219,11 @@ export function buildCrewContext({ name, from, subject, groups, myGroups }) {
|
|
|
212
219
|
}
|
|
213
220
|
if (others.length > 0) {
|
|
214
221
|
// Same one-liner as channels.js groupChannelName (grp-<group>, max 60).
|
|
215
|
-
out.push(`other groups on this
|
|
222
|
+
out.push(`other groups on this task: ${others.map((g) => `${g.name} (${showMembers(g.members)})`).join("; ")} — their channels: ${others.map((g) => `grp-${g.name}`.slice(0, 60)).join(", ")}.` + (off > 0 ? ` ${offNoun(off)} exist on this board: crewbus group list to discover.` : ""));
|
|
216
223
|
} else if (mine.length > 0) {
|
|
217
|
-
out.push(`no other groups on this board right now — discover live agents with: crewbus agents.`);
|
|
224
|
+
out.push(off > 0 ? `no other groups on this task. ${offNoun(off)} exist on this board: crewbus group list to discover.` : `no other groups on this board right now — discover live agents with: crewbus agents.`);
|
|
218
225
|
} else {
|
|
219
|
-
out.push(`discover who else is on this board with: crewbus agents (plus crewbus group list once groups exist).`);
|
|
226
|
+
out.push(off > 0 ? `${offNoun(off)} exist on this board (crewbus group list to discover).` : `discover who else is on this board with: crewbus agents (plus crewbus group list once groups exist).`);
|
|
220
227
|
}
|
|
221
228
|
out.push(`ask the group that owns a thing before asking ${lead}: crewbus send --from ${me} --to-group <group> --subject "..." --body "..." (reaches every member; ${lead} is for approvals and scope disputes only).`);
|
|
222
229
|
out.push(`read context freely: crewbus channel tail grp-<group> (a group's briefs + progress, reads need no token). Posting to channels needs lead rights — write everything via DMs.`);
|
package/bin/lib/web.js
CHANGED
|
@@ -17,9 +17,9 @@ import path from "node:path";
|
|
|
17
17
|
import http from "node:http";
|
|
18
18
|
import { boardDir, requireBoard, getFlag, fail, writeJson, listJson, cleanWebName, readJson, gitRevForBoard, newId, MAX_SPAWN, MAX_BODY_CHARS } from "./store.js";
|
|
19
19
|
import { readAgent, agentTokenMatches, authorizeCheck, mintToken, newSalt, hashToken, touchAgent } from "./identity.js";
|
|
20
|
-
import { readDMs, readVisible, ackedIds, findMessageById, deliverDMs, parseRecipients } from "./mail.js";
|
|
20
|
+
import { readDMs, readVisible, ackedIds, ackedIdSet, findMessageById, deliverDMs, parseRecipients } from "./mail.js";
|
|
21
21
|
import { workerStatus, pidAlive, killWorkers, bootWorker, buildSpawnPrompt, buildSpawnTarget, formatSpawnCmd, installCliToUserPath, ensureBoardCliShim } from "./spawn.js";
|
|
22
|
-
import { groupTelemetryData } from "./groups.js";
|
|
22
|
+
import { groupTelemetryData, collectBatch, gatherTelemetry, listBatches } from "./groups.js";
|
|
23
23
|
import { httpJson } from "./sync.js";
|
|
24
24
|
import { readChainRecords, verifyChainRecords, appendChainRecord, readHold, readBoardQuotas } from "./export.js";
|
|
25
25
|
import { launchDrivers, detectHarnessBinaries, validateLaunchPlan, advertiseEnv, HARNESS_MODELS, getDiscoveredModels, refreshDiscoveredModels } from "./launch.js";
|
|
@@ -112,7 +112,18 @@ export function boardSnapshot(d, activeWindowSec) {
|
|
|
112
112
|
.filter((x) => x && x.peer && typeof x.lastOk === "number")
|
|
113
113
|
.sort((a, b) => String(a.peer).localeCompare(String(b.peer)))
|
|
114
114
|
.map((p) => ({ peer: p.peer, lastOk: new Date(p.lastOk).toISOString() }));
|
|
115
|
-
|
|
115
|
+
// Brief batch per worker (for the Crews brief/group column): the batch
|
|
116
|
+
// stamp on the worker's brief DM, else the brief id itself. Built from
|
|
117
|
+
// the full pre-slice message scan so capped feeds never lose the join.
|
|
118
|
+
const batchByBriefId = new Map();
|
|
119
|
+
for (const m of recent) {
|
|
120
|
+
if (m && m.id && !batchByBriefId.has(m.id)) batchByBriefId.set(m.id, m.batch || m.id);
|
|
121
|
+
}
|
|
122
|
+
const workersWithBatch = workers.map((w) => ({
|
|
123
|
+
...w,
|
|
124
|
+
batch: w.briefId ? (batchByBriefId.get(w.briefId) || w.briefId) : undefined,
|
|
125
|
+
}));
|
|
126
|
+
return { board: d.root, at: new Date().toISOString(), agents, workers: workersWithBatch, broadcasts, recent: recent.slice(0, 30), ackedBy, groups, peers, briefs: listBatches(d) };
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
// Fleet console snapshots (served by `web`, all open reads like /api/board;
|
|
@@ -622,9 +633,10 @@ details.tech-details summary{cursor:pointer;font-weight:500;user-select:none}
|
|
|
622
633
|
|
|
623
634
|
<div class="sidebar-heading">VIEWS</div>
|
|
624
635
|
<nav id="approot-nav" aria-label="sections">
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
636
|
+
<button data-nav="sec-launch" class="active">Launch</button>
|
|
637
|
+
<button data-nav="sec-boards">Boards</button>
|
|
638
|
+
<button data-nav="sec-briefs">Briefs</button>
|
|
639
|
+
<button data-nav="sec-crews">Crews</button>
|
|
628
640
|
<button data-nav="sec-fleet">Fleet</button>
|
|
629
641
|
<button data-nav="sec-channels">Channels</button>
|
|
630
642
|
<button data-nav="sec-triage">Triage</button>
|
|
@@ -770,15 +782,23 @@ details.tech-details summary{cursor:pointer;font-weight:500;user-select:none}
|
|
|
770
782
|
<div class="card"><b id="c-peers">–</b><br>peers</div>
|
|
771
783
|
</div>
|
|
772
784
|
<h2>Recent activity</h2>
|
|
785
|
+
<div style="display:flex;gap:8px;margin-bottom:8px"><input id="feed-q" placeholder="filter route / message…" style="flex:1"><select id="feed-type"><option value="">all</option><option value="checkpoint">checkpoints</option><option value="final">final reports</option></select></div>
|
|
773
786
|
<table><tr><th>id</th><th>route</th><th>message</th></tr><tbody id="recent"></tbody></table>
|
|
774
787
|
<h2>Broadcasts</h2>
|
|
775
788
|
<table><tr><th>id</th><th>from</th><th>to</th><th>subject</th><th>body</th></tr><tbody id="bcast"></tbody></table>
|
|
776
789
|
</section>
|
|
777
790
|
|
|
791
|
+
<!-- SECTION: BRIEFS (ONE ROW PER FAN-OUT) -->
|
|
792
|
+
<section id="sec-briefs" data-section="Briefs" class="approot-hidden">
|
|
793
|
+
<h2>Briefs <span class="dim">one row per fan-out — click a row for its gathered thread</span></h2>
|
|
794
|
+
<table><tr><th>brief</th><th>workers</th><th>checkpoints</th><th>finals</th><th>acked</th><th>last activity</th></tr><tbody id="briefs"></tbody></table>
|
|
795
|
+
<div id="brief-thread"></div>
|
|
796
|
+
</section>
|
|
797
|
+
|
|
778
798
|
<!-- SECTION: CREWS (WORKERS & TEAMS) -->
|
|
779
799
|
<section id="sec-crews" data-section="Crews" class="approot-hidden">
|
|
780
800
|
<h2>Workers <button id="killall" class="danger">kill all</button></h2>
|
|
781
|
-
<table><tr><th>worker</th><th>state</th><th>pid</th><th>reply</th><th>log tail</th><th></th></tr><tbody id="workers"></tbody></table>
|
|
801
|
+
<table><tr><th>worker</th><th>state</th><th>pid</th><th>brief / groups</th><th>reply</th><th>log tail</th><th></th></tr><tbody id="workers"></tbody></table>
|
|
782
802
|
<h2>Agents</h2>
|
|
783
803
|
<table><tr><th>name</th><th>presence</th><th>last seen</th><th>session</th><th>DMs</th><th>unacked</th></tr><tbody id="agents"></tbody></table>
|
|
784
804
|
<h2>Groups</h2>
|
|
@@ -919,6 +939,10 @@ var _lt=document.getElementById('launch-token');
|
|
|
919
939
|
if(_lt){_lt.oninput=function(){try{localStorage.setItem('ab-tok',this.value.trim());}catch(_){}var t=document.getElementById('tok');if(t)t.value=this.value.trim();markIdent();};}
|
|
920
940
|
autoLoadOperatorCreds();
|
|
921
941
|
function stateOf(w){if(!w.known)return 'unknown';if(w.reply)return w.acked?'done · acked':'done · reply waiting';if(w.alive===true)return 'running';if(w.alive===false)return 'exited · no reply';return 'no pid';}
|
|
942
|
+
var _fq=document.getElementById('feed-q');
|
|
943
|
+
if(_fq){_fq.oninput=function(){renderRecent();};}
|
|
944
|
+
var _ft=document.getElementById('feed-type');
|
|
945
|
+
if(_ft){_ft.onchange=function(){renderRecent()};}
|
|
922
946
|
async function kill(names){
|
|
923
947
|
var c=creds();
|
|
924
948
|
if(!c.from||!c.token){say('set identity + token first');return;}
|
|
@@ -938,8 +962,7 @@ document.getElementById('killall').onclick=async function(){
|
|
|
938
962
|
if(!names.length){say('no live workers (kill needs a running pid)');return;}
|
|
939
963
|
kill(names);
|
|
940
964
|
};
|
|
941
|
-
document.getElementById('ackall').onclick=async function(){
|
|
942
|
-
var c=creds();
|
|
965
|
+
document.getElementById('ackall').onclick=async function(){ var c=creds();
|
|
943
966
|
if(!c.from||!c.token){say('set identity + token first');return;}
|
|
944
967
|
if(!confirm('ack everything unacked for '+c.from+'?'))return;
|
|
945
968
|
say('acking all for '+c.from+' …');
|
|
@@ -950,6 +973,37 @@ document.getElementById('ackall').onclick=async function(){
|
|
|
950
973
|
}catch(e){say('ack failed: '+e.message);}
|
|
951
974
|
refresh();
|
|
952
975
|
};
|
|
976
|
+
function renderRecent(){
|
|
977
|
+
var q=((document.getElementById('feed-q')||{}).value||'').toLowerCase();
|
|
978
|
+
var t=((document.getElementById('feed-type')||{}).value||'');
|
|
979
|
+
var known=window.__ackedBy||{};
|
|
980
|
+
var rows=(window.__recent||[]).filter(function(m){
|
|
981
|
+
if(t==='checkpoint'&&m.checkpoint!==true)return false;
|
|
982
|
+
if(t==='final'&&!(m.replyTo&&m.checkpoint!==true))return false;
|
|
983
|
+
if(!q)return true;
|
|
984
|
+
var to=Array.isArray(m.to)?m.to.join(','):String(m.to||'');
|
|
985
|
+
return (m.id+' '+(m.from||'')+' '+to+' '+(m.subject||'')+' '+(m.body||'')).toLowerCase().indexOf(q)!==-1;
|
|
986
|
+
});
|
|
987
|
+
document.getElementById('recent').innerHTML=rows.map(function(m){
|
|
988
|
+
var to=Array.isArray(m.to)?m.to.join(','):String(m.to||'');
|
|
989
|
+
var acks=(known[m.id]||[]).map(function(x){return '✓'+x;}).join(' ');
|
|
990
|
+
return '<tr><td>'+esc(m.id)+'</td><td>'+esc(m.from)+' → '+esc(short(to,40))+'</td><td>'+(m.subject?'<b>'+esc(m.subject)+'</b><br>':'')+esc(short(m.body,200))+'<div class="dim">'+(m.replyTo?('re: '+esc(m.replyTo)+' '):'')+(m.batch?('batch '+esc(m.batch)+' '):'')+esc(acks)+'</div></td></tr>';
|
|
991
|
+
}).join('')||'<tr><td colspan="3" class="dim">no messages yet</td></tr>';
|
|
992
|
+
}
|
|
993
|
+
async function showBrief(batch){
|
|
994
|
+
var box=document.getElementById('brief-thread');
|
|
995
|
+
box.innerHTML='<div class="dim">loading thread '+esc(batch)+'…</div>';
|
|
996
|
+
try{
|
|
997
|
+
var r=await fetch('/api/gather?batch='+encodeURIComponent(batch),{cache:'no-store'});
|
|
998
|
+
var g=await r.json();
|
|
999
|
+
if(!r.ok){box.innerHTML='<div class="dim">gather failed: '+esc(g.error||r.status)+'</div>';return;}
|
|
1000
|
+
box.innerHTML='<h3>'+esc(short(batch,32))+' <span class="dim">'+g.briefs+' briefs · '+g.replies+' replies · ~'+g.telemetry.tokensEst+' tok</span></h3>'+g.items.map(function(m){
|
|
1001
|
+
var kind=!m.replyTo?'<b>brief</b>':(m.checkpoint?'<span class="dim">checkpoint</span>':'<b>final'+(m.acked?' ✓':'')+'</b>');
|
|
1002
|
+
var to=Array.isArray(m.to)?m.to.join(','):String(m.to||'');
|
|
1003
|
+
return '<div class="card" style="margin:.5em 0"><div>'+kind+' <b>'+esc(m.from)+'</b> → '+esc(short(to,60))+' <span class="dim">'+esc(ago(m.at))+'</span></div>'+(m.subject?'<div><b>'+esc(m.subject)+'</b></div>':'')+'<div class="log" style="white-space:pre-wrap">'+esc(m.body)+'</div></div>';
|
|
1004
|
+
}).join('');
|
|
1005
|
+
}catch(e){box.innerHTML='<div class="dim">gather failed: '+esc(e.message)+'</div>';}
|
|
1006
|
+
}
|
|
953
1007
|
async function refresh(){
|
|
954
1008
|
try{
|
|
955
1009
|
var r=await fetch('/api/board',{cache:'no-store'});
|
|
@@ -977,8 +1031,11 @@ async function refresh(){
|
|
|
977
1031
|
var tail=(w.tail||[]).slice(-3).map(function(l){return '<div class="log">'+esc(l)+'</div>';}).join('')||'<span class="dim">no log</span>';
|
|
978
1032
|
var rep=w.reply?esc(w.reply.id)+'<div class="dim">'+esc(w.reply.head)+'</div>':'—';
|
|
979
1033
|
var btn=(w.known&&w.alive===true)?'<button class="danger" data-kill="'+esc(w.name)+'">kill</button>':'';
|
|
980
|
-
|
|
981
|
-
|
|
1034
|
+
var wgroups=(s.groups||[]).filter(function(g){return (g.members||[]).indexOf(w.name)!==-1;}).map(function(g){return g.name;}).join(',');
|
|
1035
|
+
var batch=w.batch||w.briefId||'—';
|
|
1036
|
+
var bcell='<div class="dim">'+esc(short(batch,20))+'</div>'+(wgroups?esc(wgroups):'<span class="dim">solo</span>');
|
|
1037
|
+
return '<tr data-worker="'+esc(w.name)+'" class="worker-row"><td><b>'+esc(w.name)+'</b><div class="dim">by '+esc(w.spawnedBy||'?')+'</div></td><td>'+esc(stateOf(w))+'</td><td>'+(w.pid===null||w.pid===undefined?'—':esc(String(w.pid)))+'</td><td>'+bcell+'</td><td>'+rep+'</td><td>'+tail+'</td><td>'+btn+'</td></tr>';
|
|
1038
|
+
}).join('')||'<tr><td colspan="7" class="dim">no spawned workers</td></tr>';
|
|
982
1039
|
Array.prototype.forEach.call(document.querySelectorAll('[data-kill]'),function(b){b.onclick=function(){kill([b.getAttribute('data-kill')]);};});
|
|
983
1040
|
document.getElementById('agents').innerHTML=s.agents.map(function(a){
|
|
984
1041
|
return '<tr><td><b>'+esc(a.name)+'</b></td><td>'+(a.active?'● active':'○ stale')+'</td><td title="'+esc(a.lastSeen||'?')+'">'+esc(ago(a.lastSeen))+'</td><td>'+esc(a.sessionId||'—')+'</td><td>'+a.dmCount+'</td><td>'+a.unacked+'</td></tr>';
|
|
@@ -987,11 +1044,14 @@ async function refresh(){
|
|
|
987
1044
|
var to=Array.isArray(b.to)?b.to.join(','):String(b.to||'');
|
|
988
1045
|
return '<tr><td>'+esc(b.id)+'</td><td>'+esc(b.from)+'</td><td>'+esc(short(to,80))+'</td><td>'+esc(b.subject||'')+'</td><td>'+esc(short(b.body,140))+'</td></tr>';
|
|
989
1046
|
}).join('')||'<tr><td colspan="5" class="dim">no broadcasts</td></tr>';
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1047
|
+
window.__recent=s.recent||[];
|
|
1048
|
+
window.__ackedBy=s.ackedBy||{};
|
|
1049
|
+
renderRecent();
|
|
1050
|
+
document.getElementById('briefs').innerHTML=(s.briefs||[]).map(function(b){
|
|
1051
|
+
var ws=(b.workers||[]).join(',');
|
|
1052
|
+
return '<tr data-batch="'+esc(b.batch)+'" class="brief-row"><td>'+(b.subject?'<b>'+esc(b.subject)+'</b><br>':'')+'<span class="dim">'+esc(short(b.batch,24))+' · from '+esc(b.from)+'</span></td><td>'+esc(ws.length>60?ws.slice(0,60)+'…':ws)+'<div class="dim">'+b.briefCount+' brief'+(b.briefCount===1?'':'s')+'</div></td><td>'+b.checkpointCount+'</td><td>'+b.finalCount+'</td><td>'+b.ackedFinals+'/'+b.finalCount+'</td><td>'+esc(ago(b.lastAt))+'</td></tr>';
|
|
1053
|
+
}).join('')||'<tr><td colspan="6" class="dim">no briefs yet</td></tr>';
|
|
1054
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-batch]'),function(r){r.onclick=function(){showBrief(r.getAttribute('data-batch'));};});
|
|
995
1055
|
try{
|
|
996
1056
|
var fr=await fetch('/api/fleet',{cache:'no-store'});
|
|
997
1057
|
var fl=await fr.json();
|
|
@@ -1870,12 +1930,40 @@ export async function handleWebDashboardRoute(req, res, url, d) {
|
|
|
1870
1930
|
id: m.id, from: m.from, at: m.at, subject: m.subject || "",
|
|
1871
1931
|
head: String(m.body || "").slice(0, 160),
|
|
1872
1932
|
replyTo: m.replyTo || "", batch: m.batch || "",
|
|
1933
|
+
checkpoint: m.checkpoint === true,
|
|
1873
1934
|
artifact: m.artifact || "", acked: known.has(m.id),
|
|
1874
1935
|
}));
|
|
1875
1936
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1876
1937
|
res.end(JSON.stringify({ board: d.root, agent, items }));
|
|
1877
1938
|
return true;
|
|
1878
1939
|
}
|
|
1940
|
+
if (req.method === "GET" && url.pathname === "/api/gather") {
|
|
1941
|
+
// Open read like /api/board (the transcript is already visible piece
|
|
1942
|
+
// by piece there); the dashboard twin of `gather --batch`: one brief
|
|
1943
|
+
// thread — briefs, checkpoint timeline, final reports — oldest first.
|
|
1944
|
+
const batch = String(url.searchParams.get("batch") || "").trim().slice(0, 200);
|
|
1945
|
+
if (!batch) {
|
|
1946
|
+
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
|
|
1947
|
+
res.end(JSON.stringify({ error: "pass ?batch=<batch-id>" }));
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
const got = collectBatch(d, batch);
|
|
1951
|
+
if (!got) {
|
|
1952
|
+
res.writeHead(404, { "content-type": "application/json; charset=utf-8" });
|
|
1953
|
+
res.end(JSON.stringify({ error: `unknown batch "${batch}"` }));
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
const known = ackedIdSet(d);
|
|
1957
|
+
const tele = gatherTelemetry(got.items);
|
|
1958
|
+
const items = got.items.map((m) => ({
|
|
1959
|
+
id: m.id, from: m.from, to: m.to, at: m.at, subject: m.subject || "",
|
|
1960
|
+
body: String(m.body || ""), replyTo: m.replyTo || "", batch: m.batch || "",
|
|
1961
|
+
checkpoint: m.checkpoint === true, acked: known.has(m.id),
|
|
1962
|
+
}));
|
|
1963
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1964
|
+
res.end(JSON.stringify({ board: d.root, batch, briefs: got.briefs, replies: got.replies, telemetry: tele, items }));
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1879
1967
|
if (req.method === "POST" && url.pathname === "/api/ack") {
|
|
1880
1968
|
// JSON-only like /api/kill (plain browser forms can't reach it),
|
|
1881
1969
|
// token-checked like the CLI, no --verify over HTTP.
|
package/docs/GROUPS.md
CHANGED
|
@@ -81,8 +81,13 @@ Groups share one board, so any member can already read anything. The
|
|
|
81
81
|
thing that decides whether groups actually collaborate is whether the
|
|
82
82
|
brief tells them the map. Since v10.0.x every `spawn` brief carries a
|
|
83
83
|
**Crew:** block (see `buildCrewContext` in `bin/lib/spawn.js`): your
|
|
84
|
-
group(s) + members + lane, the
|
|
85
|
-
channels, and the peer-DM norm.
|
|
84
|
+
group(s) + members + lane, the sibling groups of the same spawn +
|
|
85
|
+
members, their group channels, and the peer-DM norm. Groups from
|
|
86
|
+
unrelated concurrent briefs are deliberately NOT enumerated — they
|
|
87
|
+
collapse to a count (`N other groups exist on this board`), so
|
|
88
|
+
cross-task rosters stay discoverable (`crewbus group list`) without
|
|
89
|
+
becoming prompt noise or a misleading invitation. Conventions that
|
|
90
|
+
block assumes:
|
|
86
91
|
|
|
87
92
|
- **Peer-direct for questions.** `crewbus send --from you --to-group
|
|
88
93
|
<group> --subject "..." --body "..."` reaches every member of
|
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.5`),
|
|
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);
|