One cumulative control-board, webui and seat state. The four rows edit the
same files (scan.mjs, page.html, README.md, app.js), so they land together,
each on its own receipt:
- Row 18, Discord connector rows on the board (#1509): R3 approved by
Darkwing and Dewey, Gitea comment 26257, manifest 254403b8. Jason
accepted the visual test.
- Row 22, board attention status (#1503): Filbert approved R1, comment
26248, manifest e40b58ec; restart receipt 26249.
- #1511, task attribution (row 6 code phase): R2 approved by Filbert and
Dewey, manifest d4c96395. docs/TOOLS.md carries the approved --by usage
line (tools-usage.patch 86bcba3c).
- #1512, relaunch activity (row 6 pilot): R1 approved by Darkwing and
Dewey, candidate manifest 47769fad. All seven source files match it.
Row 16, internal development bootstrap (#1510): the seven files outside
shared records match Filbert's R1 pins, receipt 26204 (agents/researcher/*,
scripts/test-darkwing-launch.mjs, the bootstrap plan).
packages/webui/src/public/app.js is committed at its #1512 R1 pin ce7d79a4.
The working copy holds Dewey's unreviewed return-flow candidate on top of
that, and it stays uncommitted.
Also: the four row briefs and Darkwing's evidence records under
agents/darkwing/work, including the 2026-09-26 tree manifest and the #1512
re-run against 21e3e908. Serial acceptance command: 397/397, three runs.
The failures that only show when tests run concurrently are in #1509 engine
tests, and they reproduce on clean HEAD.
Suites on the exact staged tree: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63; package union 397/397
(serial); test-darkwing-launch 5/5.
Shared records (BUILD-LOG, QUEUE, CURRENT, DEFERRED, SESSIONS, AGENTS.md,
agents/README.md) follow in Sage's records commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
74 lines
4.0 KiB
JavaScript
Executable File
74 lines
4.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Usage:
|
|
// node packages/control-board/src/cli.mjs scan [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print]
|
|
// node packages/control-board/src/cli.mjs serve [same flags] [--port N] [--host 127.0.0.1]
|
|
// Exit 0 on success, 2 on a config refusal or bad usage.
|
|
import { join } from "node:path";
|
|
import { homedir } from "node:os";
|
|
import { loadConfig, defaultConfigPath, discoverRepoAgents, discoverFleetAgents, scan, tmuxInspect, ConfigError } from "./scan.mjs";
|
|
import { startServer } from "./serve.mjs";
|
|
|
|
const USAGE = "usage: mosaic-board scan|serve [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print] [--port N] [--host 127.0.0.1]";
|
|
|
|
function parseArgs(argv) {
|
|
const opts = { command: argv[0], config: defaultConfigPath(), repo: process.cwd(), fleet: join(homedir(), ".mosaic", "fleet", "agents"), liveness: "tmux", print: false, port: 7331, host: "127.0.0.1" };
|
|
for (let i = 1; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
const next = () => {
|
|
if (i + 1 >= argv.length) throw new ConfigError(`missing value for ${a}`);
|
|
return argv[++i];
|
|
};
|
|
if (a === "--config") opts.config = next();
|
|
else if (a === "--repo") opts.repo = next();
|
|
else if (a === "--fleet") opts.fleet = next();
|
|
else if (a === "--liveness") opts.liveness = next();
|
|
else if (a === "--print") opts.print = true;
|
|
else if (a === "--port") {
|
|
opts.port = Number(next());
|
|
if (!Number.isInteger(opts.port) || opts.port < 0 || opts.port > 65535) throw new ConfigError("--port must be an integer 0..65535");
|
|
} else if (a === "--host") opts.host = next();
|
|
else throw new ConfigError(`unknown argument: ${a}`);
|
|
}
|
|
if (opts.command !== "scan" && opts.command !== "serve") throw new ConfigError(USAGE);
|
|
if (!["tmux", "assume-alive"].includes(opts.liveness)) throw new ConfigError(`unknown liveness mode: ${opts.liveness}`);
|
|
return opts;
|
|
}
|
|
|
|
async function main() {
|
|
const opts = parseArgs(process.argv.slice(2));
|
|
const { dataRoot } = loadConfig(opts.config);
|
|
const specs = [...discoverRepoAgents(opts.repo), ...(opts.fleet === "none" ? [] : discoverFleetAgents(opts.fleet))];
|
|
const isAlive = opts.liveness === "tmux" ? tmuxInspect : () => true;
|
|
const boardDir = join(dataRoot, "board");
|
|
const seatsDir = join(dataRoot, "seats");
|
|
if (opts.command === "serve") {
|
|
const server = await startServer({ host: opts.host, port: opts.port, specs, boardDir, isAlive, seatsDir, discordDataRoot: dataRoot });
|
|
const addr = server.address();
|
|
process.stdout.write(`control board: http://${opts.host}:${addr.port}/ (${specs.length} agents; board files in ${boardDir}; Ctrl-C to stop)\n`);
|
|
const stop = () => server.close(() => process.exit(0));
|
|
process.on("SIGINT", stop);
|
|
process.on("SIGTERM", stop);
|
|
return;
|
|
}
|
|
const index = scan(specs, { boardDir, isAlive, seatsDir, discordDataRoot: dataRoot });
|
|
for (const line of index.discoveryErrors) process.stderr.write(`${line}\n`);
|
|
for (const line of index.registrationErrors) process.stderr.write(`registration skipped: ${line}\n`);
|
|
if (opts.print) {
|
|
for (const s of index.sessions) {
|
|
const flag = s.waitingOnYou ? "*" : s.seen ? "s" : " ";
|
|
const age = s.relaunchedAt ? "relaunch" : s.ageSeconds == null ? "-" : `${Math.round(s.ageSeconds / 60)}m`;
|
|
const preview = s.relaunchedAt ? `relaunched at ${s.relaunchedAt}, no messages since` : s.lastAssistantText ? s.lastAssistantText.slice(0, 80) : "";
|
|
process.stdout.write(`${flag} ${s.state.padEnd(8)} ${s.project.padEnd(14)} ${s.agent.padEnd(16)} ${age.padStart(7)} ${preview}\n`);
|
|
}
|
|
}
|
|
process.stdout.write(`board: ${boardDir} (${index.sessions.length} sessions, ${index.waitingOnYou.length} waiting on you, ${index.seen.length} seen, ${index.registered.length} registered, ${index.registrationStale.length} stale)\n`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
if (err instanceof ConfigError) {
|
|
process.stderr.write(`refused: ${err.message}\n`);
|
|
process.exit(2);
|
|
}
|
|
throw err;
|
|
});
|