# seat `mosaic launch ` starts a seat through its existing launch script, unchanged, and leaves one registration record that the control board reads instead of guessing. `mosaic seat task ` changes the task on that record and notes who set it. Nothing else. No provider or auth registry, no roster schema change, no stopping or killing of seats, one record per seat. Issue #1504. Plain ESM, no dependencies, Node 24 or newer. ## Commands The repository wrapper is `scripts/mosaic`. It is not on PATH and is not the npm-global `mosaic` CLI from the estate tooling, which has no `launch` or `seat` command; run it by path. ``` scripts/mosaic launch [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...] scripts/mosaic seat task [--by NAME] [--layout repo|fleet|unknown] [--config PATH] ``` - `` is a name under `/agents/` (`--repo` defaults to the current directory) or a path to a seat directory such as `~/.mosaic/fleet/agents/orch-01`. Either way the directory must hold an executable `launch.sh`. - Everything after `--` goes to the launch script untouched. - `launch` writes the registration, then replaces itself with the launch script (`process.execve`). The seat keeps the same pid, terminal and process group as if you had run `launch.sh` yourself, the launch script's exit code is yours, and the tmux pane's foreground command stays the harness, which the control board's liveness check depends on. Node marks `process.execve` experimental (present since 24); it is the only way to keep the seat's pid, so the package accepts that and pins Node 24 or newer. The seat tests exercise it directly (exit passthrough). - The launch script receives `MOSAIC_LAUNCH_REGISTERED=`. A launch script that sees this variable is already registered and must not call `mosaic launch` again; `mosaic launch` refuses to run when it is set. - `seat task` records who set the task in `taskSetBy` (#1511): `--by NAME` if given, else `$MOSAIC_AGENT_NAME` if set and non-empty, else the word `unknown`. An explicit value must be one token of 1 to 64 characters (letters, digits, `.` `_` `@` `:` `-`, starting with a letter or digit); anything else refuses with exit 4 before the record is touched. A set but malformed `MOSAIC_AGENT_NAME` also refuses rather than being reported as `unknown`; `--by` is the explicit way past it. The value is never echoed back in a refusal. **This is what the caller said, not a verified identity.** Nothing checks it, nothing grants on it; the board displays it next to the task and does no more with it. `launch --task` does not set the field. The four repository seats (`agents/darkwing`, `agents/dewey`, `agents/filbert`, `agents/rocko`) register themselves: their `launch.sh` re-enters through `scripts/mosaic launch` unless already registered or called with `--check`. So `agents/darkwing/launch.sh` and `scripts/mosaic launch darkwing --task "..."` are the same path; the second form is how you attach a task. A `--check` run never writes a record. ## The record `/seats///registration.json`, directory 0700, file 0600, written atomically. `layout` is `repo`, `fleet` or `unknown` (see below); it is part of the path because a seat name alone is not unique, and `seat task` refuses a bare name that is registered in more than one layout until `--layout` says which. `dataRoot` comes from `~/.config/mosaic-dev/config.json` (or `$MOSAIC_CONFIG`); a missing or unreadable config refuses the launch. The record is rewritten on every launch. It is written before the launch script runs, so a launch the script itself refuses (a failed `--check`-style precondition, a missing context file) still leaves a record with a pid that is no longer running; the next launch replaces it, and the board treats a record whose pid is gone as stale (shown, but the derived values win). It is a launch record, not a run record: it is not evidence, it holds one seat's latest launch only, and it lives outside `/board/` because the board never writes here and a scan never changes it. ```json { "version": 1, "seat": "darkwing", "project": "mosaic-stack", "task": "Control board: seat registration (#1504)", "workspace": "/mnt/storage/src/mosaic-stack", "tmux": { "socket": null, "session": "darkwing" }, "harness": "pi", "startedAt": "2026-09-12T16:20:11.000Z", "pid": 431734, "sessionsDir": "/mnt/storage/src/mosaic-stack/.pi/state/darkwing/sessions", "seatDir": "/mnt/storage/src/mosaic-stack/agents/darkwing", "launchScript": "/mnt/storage/src/mosaic-stack/agents/darkwing/launch.sh", "layout": "repo", "updatedAt": null } ``` After `scripts/mosaic seat task darkwing "Row 6 (#1511)" --by jason` the same record also carries `"taskSetBy": "jason"` and a non-null `updatedAt`; `startedAt` and every other field are unchanged. Fields asked for in the brief: `seat`, `project`, `task` (empty unless `--task` or a later `seat task`), `workspace`, `tmux` (session name and socket, from the `TMUX` variable of the pane the launch ran in; null outside tmux), `harness` (only what `--harness` says; the repository launch scripts pass `pi` or `claude-code`), `startedAt`, `pid`. Fields added, and why: - `sessionsDir`: how the board matches a record to a row. Seat names are not unique across layouts (there is a `darkwing` in this repository and a `darkwing` in the fleet), so the record names the sessions directory the board already scans, and only an exact match counts. - `seatDir`, `launchScript`, `layout`: what was launched and how the paths were derived, so a wrong record can be traced without re-running anything. `layout` is `repo` (`/agents/` with `/.git`), `fleet` (`/.pi` exists) or `unknown` (nothing derived, `sessionsDir` null, the record is still written but the board cannot match it). - `updatedAt`: set only by `seat task`, so a task change is distinguishable from a relaunch. - `taskSetBy`: set only by `seat task` (see above). Optional in the shape: a record written before the field existed loads unchanged, the board shows `unknown` for it, and nothing rewrites it until the next `seat task` or launch. The record version stays 1; a value that is not one bounded token makes the record unreadable (refused, not misread). - `version`: so a later shape change can be refused rather than misread. `project` and `workspace` are derived only for the repo layout (the repository's basename and root). For the fleet layout they are null unless `--project` and `--workspace` are given, because the roster has no project field and all fleet seats run in `~/.mosaic`; the board then keeps its own derived values for those rows. ## How the board uses it `packages/control-board` reads every `/seats///registration.json` on each scan. A registered task, project or workspace wins over the derived value, and the row's `taskSource`, `activeProjectSource` or `workspaceSource` says `registration`. An empty task or a null project or workspace in the record leaves the derived value in place. Rows without a registration are unchanged. A malformed record is reported in `registrationErrors` on the index and skipped; it never takes the board down and it is never treated as absent silently. A record whose pid is no longer running is stale: still shown on the Registered line, but it does not override anything. The row's `taskSetBy` is the record's setter only while the task shown is the registered one (`taskSource` `registration`); otherwise it is null, so a stale or empty registration never attributes a transcript-derived task, and a Discord connector's fixed task never borrows a native record's setter. The repository launch scripts are the only launchers that register. Fleet seats under `~/.mosaic` stay on their own launchers (Jason's ruling, 2026-09-12); a fleet seat can still be registered by hand with `scripts/mosaic launch `. ## Exit codes `launch` exits with the launch script's own code once the script runs. Before that: 1 the launch script could not be started (the record is removed again), 2 invalid data or configuration (missing config, no such seat, no executable `launch.sh`, bad record on disk), 4 usage. `seat task`: 0 ok, 1 no registration for that seat, 2 as above, 4 usage. ## Tests ``` node --test packages/seat/tests/ ``` The repository launch scripts are covered end to end by `scripts/test-darkwing-launch.mjs` (darkwing, dewey, filbert) and `scripts/test-rocko-launch.mjs`, which run each `launch.sh` in a fixture root that is also its own data root, so no registration reaches a real one.