Add mosaic launch <seat> with seat registration for the control board (#1504)

New package packages/seat and wrapper scripts/mosaic. `launch <seat>` writes
<dataRoot>/seats/<layout>/<seat>/registration.json and then execs the seat's
launch.sh unchanged; `seat task <seat> <text>` edits the task only. The board
reads registrations, matches by sessions directory, and lets a registered
task, project or workspace override the derived value with a source tag.
The four repository launch scripts register themselves unless already
registered or run with --check. Fleet launchers untouched; one-liner on the
plan page.

Review found the record path keyed by seat name alone (repo and fleet
"darkwing" would collide); fixed by keying on layout. Also: the Pi pin
refusal now names installed and required versions.

Tests: seat 15, control-board 89, launch scripts 5, registry 69, config 24.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 10:56:17 -05:00
co-authored by Claude Fable 5.1
parent 01d9a19612
commit 69f99323c7
25 changed files with 1766 additions and 46 deletions
+17 -2
View File
@@ -16,13 +16,19 @@ test('Rocko uses Sonnet, isolated resume IDs, current context and launch refusal
writeFileSync(join(root, file), `fixture ${file}\n`);
}
writeFileSync(join(root, 'scripts/common.sh'), 'load_config() { MOSAIC_DATA_ROOT="$PWD"; }\n');
// Seat registration (packages/seat): the fixture is its own data root.
cpSync(join(source, 'scripts/mosaic'), join(root, 'scripts/mosaic'));
cpSync(join(source, 'packages/seat/src'), join(root, 'packages/seat/src'), { recursive: true });
mkdirSync(join(root, '.git'));
writeFileSync(join(root, 'config.json'), JSON.stringify({ configVersion: 1, dataRoot: root }));
const registration = join(root, 'seats/repo/rocko/registration.json');
writeFileSync(join(root, 'bin/claude'), `#!/usr/bin/env node
const fs = require('node:fs');
if (process.argv[2] === '--version') { console.log('fixture Claude'); process.exit(0); }
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), cwd: process.cwd(), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION}));
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), cwd: process.cwd(), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION, registered: process.env.MOSAIC_LAUNCH_REGISTERED}));
if (process.env.FAKE_CLAUDE_FAIL) process.exit(7);
`, { mode: 0o755 });
const env = { ...process.env, PATH: `${join(root, 'bin')}:${process.env.PATH}`, MOSAIC_LAUNCH_INCARNATION: 'do-not-inherit' };
const env = { ...process.env, PATH: `${join(root, 'bin')}:${process.env.PATH}`, MOSAIC_LAUNCH_INCARNATION: 'do-not-inherit', MOSAIC_CONFIG: join(root, 'config.json'), MOSAIC_LAUNCH_REGISTERED: '' };
const run = (args = '', tty = true, extraEnv = {}) => spawnSync(tty ? 'script' : 'bash', tty
? ['-q', '-e', '-c', `bash agents/rocko/launch.sh ${args}`, '/dev/null']
: [join(root, 'agents/rocko/launch.sh'), ...args.split(' ').filter(Boolean)],
@@ -33,10 +39,19 @@ if (process.env.FAKE_CLAUDE_FAIL) process.exit(7);
ok(run('--check', false));
assert.equal(existsSync(state), false);
assert.equal(existsSync(join(root, 'capture.json')), false);
assert.equal(existsSync(join(root, 'seats')), false);
ok(run());
let captured = capture();
assert.equal(captured.cwd, root);
assert.equal(captured.agent, 'rocko');
// Registered before Claude started; the record names the Claude harness.
assert.equal(captured.registered, registration);
const record = JSON.parse(readFileSync(registration, 'utf8'));
assert.equal(record.seat, 'rocko');
assert.equal(record.harness, 'claude-code');
assert.equal(record.layout, 'repo');
assert.equal(record.workspace, root);
assert.ok(Number.isInteger(record.pid) && record.pid > 0);
assert.equal(captured.incarnation, undefined);
assert.equal(captured.args[captured.args.indexOf('--model') + 1], 'sonnet');
assert.equal(captured.args[captured.args.indexOf('--name') + 1], 'Rocko');