Files
stack/packages/seat/README.md
T
jason.woltjeandClaude Fable 5.1 17153fe140 control board: stale registrations and a launch-test leak (#1504)
Two defects in 69f99323, reported by the professor session and verified.

The darkwing launch test's flock-contention spawn ran without the fixture
config, so launch.sh re-entered scripts/mosaic against the real data root
and wrote fixture records for darkwing, dewey and filbert there. That
spawn now names the fixture config, and both launch test files set
MOSAIC_CONFIG to a nonexistent path and clear MOSAIC_LAUNCH_REGISTERED
process-wide, so a spawn that forgets fails instead of polluting.

A registration is written before the launch script's own checks, so a
refused launch left a record with a dead pid that the board honoured. The
scanner now probes the recorded pid (pidAlive, signal 0); a gone pid makes
the record stale: still on the Registered line with alive false, derived
task, project and workspace win, index gains registrationStale, CLI
summary gains a stale count.

Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15,
launch scripts 5/5. Sonnet review APPROVED.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-12 11:04:49 -05:00

147 lines
7.0 KiB
Markdown

# seat
`mosaic launch <seat>` 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 <seat> <text>` changes the task on
that record. 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 <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH]
[--harness NAME] [--repo PATH] [--config PATH] [-- args...]
scripts/mosaic seat task <seat> <text> [--layout repo|fleet|unknown] [--config PATH]
```
- `<seat>` is a name under `<repo>/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=<record path>`. 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.
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
`<dataRoot>/seats/<layout>/<seat>/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 `<dataRoot>/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
}
```
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` (`<repo>/agents/<seat>` with `<repo>/.git`), `fleet`
(`<seatDir>/.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.
- `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 `<dataRoot>/seats/<layout>/<seat>/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 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 <seat-dir>`.
## 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.