Files
stack/packages/seat/src/cli.mjs
T
jason.woltjeandClaude Opus 5.5 af4203ca92 feat(board): session attention, Discord rows, task attribution and relaunch activity (rows 18, 22, #1511, #1512)
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]>
2026-09-26 14:54:18 -05:00

122 lines
5.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Usage:
// mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH]
// [--harness NAME] [--repo PATH] [--config PATH] [-- args...]
// mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]
//
// `seat task` records who set the task as taskSetBy: `--by NAME`, else
// $MOSAIC_AGENT_NAME, else "unknown". This is what the caller claimed; it is
// not verified and it authorizes nothing.
//
// `launch` writes <dataRoot>/seats/<layout>/<seat>/registration.json, then replaces
// itself with the seat's launch.sh, unchanged, with everything after `--` as
// its arguments and MOSAIC_LAUNCH_REGISTERED set to the record path. A launch
// script that sees that variable is already registered and must not call
// mosaic launch again. The exit code is the launch script's own.
//
// Exit codes: the launch script's code (launch); 0 ok; 1 operation failed;
// 2 invalid data or configuration; 4 usage.
import { rmSync } from "node:fs";
import {
SeatError, defaultConfigPath, loadDataRoot, seatsDir, resolveSeat, tmuxContext,
makeRegistration, writeRegistration, updateTask, resolveSetBy,
} from "./seat.mjs";
const USAGE = [
"usage: mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...]",
" mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]",
].join("\n");
export const REGISTERED_ENV = "MOSAIC_LAUNCH_REGISTERED";
function parseLaunch(argv) {
const opts = { seat: null, task: "", project: undefined, workspace: undefined, harness: null, repo: process.cwd(), config: defaultConfigPath(), args: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const next = () => {
if (i + 1 >= argv.length) throw new SeatError(`missing value for ${a}`, 4);
return argv[++i];
};
if (a === "--") { opts.args = argv.slice(i + 1); break; }
else if (a === "--task") opts.task = next();
else if (a === "--project") opts.project = next();
else if (a === "--workspace") opts.workspace = next();
else if (a === "--harness") opts.harness = next();
else if (a === "--repo") opts.repo = next();
else if (a === "--config") opts.config = next();
else if (a.startsWith("--")) throw new SeatError(`unknown argument: ${a}\n${USAGE}`, 4);
else if (opts.seat === null) opts.seat = a;
else throw new SeatError(`unexpected argument: ${a} (put launch script arguments after --)`, 4);
}
if (opts.seat === null) throw new SeatError(USAGE, 4);
return opts;
}
function parseSeatTask(argv) {
const opts = { seat: null, task: null, layout: null, by: null, config: defaultConfigPath() };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--config" || a === "--layout" || a === "--by") {
if (i + 1 >= argv.length) throw new SeatError(`missing value for ${a}`, 4);
opts[a.slice(2)] = argv[++i];
} else if (a.startsWith("--")) throw new SeatError(`unknown argument: ${a}\n${USAGE}`, 4);
else if (opts.seat === null) opts.seat = a;
else if (opts.task === null) opts.task = a;
else throw new SeatError(`unexpected argument: ${a}`, 4);
}
if (opts.seat === null || opts.task === null) throw new SeatError(USAGE, 4);
return opts;
}
function launch(argv) {
const opts = parseLaunch(argv);
const resolved = resolveSeat(opts.seat, { repo: opts.repo });
const seats = seatsDir(loadDataRoot(opts.config));
// Register first, then replace this process with the launch script (same
// pid, same terminal, same process group). The launch script's exit code
// and signals are then the shell's to see directly, and the tmux pane's
// foreground command stays the harness itself, which the control board's
// liveness check depends on.
const record = makeRegistration({
resolved, task: opts.task, project: opts.project, workspace: opts.workspace,
harness: opts.harness, tmux: tmuxContext(), pid: process.pid,
});
const path = writeRegistration(seats, record);
process.stderr.write(`mosaic launch: registered ${resolved.seat} (${resolved.layout} layout) at ${path}\n`);
try {
process.execve(resolved.launchScript, [resolved.launchScript, ...opts.args], { ...process.env, [REGISTERED_ENV]: path });
} catch (err) {
rmSync(path, { force: true });
throw new SeatError(`could not run ${resolved.launchScript}: ${err.message}`, 1);
}
return 0;
}
function seatTask(argv) {
const opts = parseSeatTask(argv);
// Attribution is resolved (and an invalid value refused) before any read
// or write, so a refusal leaves the record exactly as it was.
const setBy = resolveSetBy({ by: opts.by, env: process.env });
const seats = seatsDir(loadDataRoot(opts.config));
const record = updateTask(seats, opts.seat, opts.task, { layout: opts.layout, setBy });
process.stdout.write(`mosaic seat task: ${opts.seat} (${record.layout} layout) task set (${opts.task.length} characters, set by ${setBy})\n`);
return 0;
}
function main(argv) {
if (process.env[REGISTERED_ENV] && argv[0] === "launch") {
throw new SeatError(`${REGISTERED_ENV} is already set; refusing to register a seat from inside a registered launch`, 4);
}
if (argv[0] === "launch") return launch(argv.slice(1));
if (argv[0] === "seat" && argv[1] === "task") return seatTask(argv.slice(2));
throw new SeatError(USAGE, 4);
}
try {
process.exitCode = main(process.argv.slice(2));
} catch (err) {
if (!(err instanceof SeatError)) throw err;
process.stderr.write(`refused: ${err.message}\n`);
process.exitCode = err.exitCode;
}