Add control board status scanner and MVP plan (#1503)

Step 1 of the control board MVP (decision MOSAIC-STACK-D-001): a plan page,
Gitea #1503, and packages/control-board, which reads each agent's newest pi
session log plus tmux liveness and writes one status file per agent under
<dataRoot>/board/. 23/23 tests; independent review approved after three
fixes (length stopReason as error, unknown liveness state, secrets-boundary
test). CURRENT.md now points at step 2, the page.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 07:26:03 -05:00
co-authored by Claude Fable 5.1
parent 1993039c76
commit b9f59a5903
10 changed files with 1003 additions and 15 deletions
+53
View File
@@ -0,0 +1,53 @@
#!/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]
// 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, tmuxIsAlive, ConfigError } from "./scan.mjs";
function parseArgs(argv) {
const opts = { command: argv[0], config: defaultConfigPath(), repo: process.cwd(), fleet: join(homedir(), ".mosaic", "fleet", "agents"), liveness: "tmux", print: false };
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 throw new ConfigError(`unknown argument: ${a}`);
}
if (opts.command !== "scan") throw new ConfigError("usage: mosaic-board scan [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print]");
if (!["tmux", "assume-alive"].includes(opts.liveness)) throw new ConfigError(`unknown liveness mode: ${opts.liveness}`);
return opts;
}
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" ? tmuxIsAlive : () => true;
const boardDir = join(dataRoot, "board");
const index = scan(specs, { boardDir, isAlive });
if (opts.print) {
for (const s of index.sessions) {
const flag = s.waitingOnYou ? "*" : " ";
const age = s.ageSeconds == null ? "-" : `${Math.round(s.ageSeconds / 60)}m`;
process.stdout.write(`${flag} ${s.state.padEnd(8)} ${s.project.padEnd(14)} ${s.agent.padEnd(16)} ${age.padStart(7)} ${s.lastAssistantText ? s.lastAssistantText.slice(0, 80) : ""}\n`);
}
}
process.stdout.write(`board: ${boardDir} (${index.sessions.length} sessions, ${index.waitingOnYou.length} waiting on you)\n`);
}
try {
main();
} catch (err) {
if (err instanceof ConfigError) {
process.stderr.write(`refused: ${err.message}\n`);
process.exit(2);
}
throw err;
}