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]>
175 lines
8.4 KiB
Markdown
175 lines
8.4 KiB
Markdown
# control-board
|
|
|
|
Step 1 of the control board MVP (`docs/plans/2026-09-12_control-board-mvp.md`).
|
|
This package scans your running pi agent sessions and writes one small JSON
|
|
status file per agent, plus one summary file, so a later web page can show
|
|
them all in one place. It reads pi session logs and checks tmux liveness. It
|
|
does not launch, stop, or talk to any agent. Board files are derived and
|
|
rewritable; they are not run records and are not evidence.
|
|
|
|
## States
|
|
|
|
| State | Plain-words meaning |
|
|
|---------|----------------------|
|
|
| working | The agent is in the middle of a turn: thinking or running a tool. A seat whose newest log entry is a tool call or a tool result is working even if its last words looked like a question. |
|
|
| waiting | The agent finished its turn with a text-only message. It is your move now. |
|
|
| error | The agent's last turn ended in an error, was aborted, or was cut off. Go look at it. |
|
|
| offline | There is no live tmux session for this agent right now, or its session exists but no longer runs `pi`. |
|
|
| idle | The agent is live but has not had a conversation yet. |
|
|
| unknown | The scanner could not ask tmux (missing or not answering). It does not assume the agent is alive. |
|
|
|
|
Liveness means a pane in the agent's tmux session is actually running `pi`
|
|
(`tmux list-panes -s -t '=<session>' -F '#{pane_current_command}'`). A tmux session that still
|
|
exists but only runs bash or some other program counts as offline, not
|
|
waiting.
|
|
|
|
## Commands
|
|
|
|
```
|
|
node src/cli.mjs scan [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print]
|
|
node src/cli.mjs serve [same flags] [--port N] [--host 127.0.0.1]
|
|
```
|
|
|
|
`scan` runs once and writes the status files. `serve` starts a small local
|
|
web server: open `http://127.0.0.1:7331/` in a browser. The page fetches
|
|
`/api/board` every 10 seconds; each fetch re-runs the scan, so the page is
|
|
never staler than that timer. There is no login, so the server refuses to
|
|
bind to anything but a loopback address.
|
|
|
|
- `--config PATH` — path to the system config file. Defaults to
|
|
`~/.config/mosaic-dev/config.json`. This file must exist and name an
|
|
absolute `dataRoot`, or the scanner refuses to run.
|
|
- `--repo PATH` — path to a project checkout to scan for repo agents
|
|
(`.pi/state/<agent>/sessions`). Defaults to the current directory.
|
|
- `--fleet PATH|none` — path to the fleet agents directory
|
|
(`<fleet>/<agent>/.pi/agent/sessions`, tmux socket `mosaic-fleet`).
|
|
Defaults to `~/.mosaic/fleet/agents`. Pass `none` to skip fleet agents.
|
|
- `--liveness tmux|assume-alive` — how to decide if an agent is alive.
|
|
`tmux` (default) checks the real tmux session. `assume-alive` treats
|
|
every agent as alive, useful for tests or environments without tmux.
|
|
- `--print` — (`scan`) also print a one-line-per-agent table to stdout. The
|
|
first column is `*` for a row waiting on you, `s` for a seen row, or blank
|
|
otherwise. The final summary line reads
|
|
`board: <dir> (N sessions, N waiting on you, N seen)`.
|
|
- `--port N` — (`serve`) port to listen on. Default `7331`; `0` picks a free port.
|
|
- `--host ADDR` — (`serve`) loopback address to bind. Default `127.0.0.1`.
|
|
Any non-loopback address is refused.
|
|
|
|
Routes served:
|
|
|
|
- `GET /` — the page.
|
|
- `GET /api/board` — rescan, returns `index.json`.
|
|
- `POST /api/seen` — mark or unmark a row as seen, then rescan and return
|
|
`index.json`. Body must be JSON (`Content-Type: application/json`, no
|
|
more than 4096 bytes): `{"project", "agent", "lastActivity", "seen"}`.
|
|
`seen` defaults to `true`; pass `false` to unsee. Bad input, a missing
|
|
header, or an oversized body gets a `400`.
|
|
- `GET /healthz`.
|
|
|
|
## Exit codes
|
|
|
|
- `0` — scan completed and status files were written, or the server stopped cleanly.
|
|
- `2` — refused: bad config, missing/invalid `dataRoot`, or bad arguments.
|
|
The message on stderr says why.
|
|
|
|
## Tests
|
|
|
|
```
|
|
node --test packages/control-board/tests/
|
|
```
|
|
|
|
## Output layout
|
|
|
|
```
|
|
<dataRoot>/board/
|
|
index.json # summary: counts, waiting-on-you, seen, registered, registrationStale, registrationErrors, all records
|
|
seen.json # Jason's "seen" marks (see below)
|
|
sessions/
|
|
<project>/
|
|
<agent>.json # one status record per agent
|
|
```
|
|
|
|
`seen.json` holds `{ "<project>/<agent>": "<lastActivity>" }`. It is written
|
|
only when Jason clicks "Seen" or "Unsee" on the page (via `POST
|
|
/api/seen`); a scan reads it but never writes it. A mark applies only while
|
|
the agent's newest message still has that exact `lastActivity` timestamp —
|
|
as soon as the agent writes anything new, `lastActivity` changes, the mark
|
|
no longer matches, and the row falls back into "Waiting on you" on its own.
|
|
Every row also shows three "what and where" fields, each taken from the
|
|
seat's registration when it has one, else derived from the session log and
|
|
tmux, else shown as the word "unknown", never guessed:
|
|
|
|
- **Task** — the session's first user message (collapsed to one line, 240
|
|
characters). pi logs carry no task envelope, so this is the only
|
|
assignment signal available. For fleet seats it is usually the
|
|
fleet-comms envelope the seat was started with.
|
|
- **Active project** — the basename of the nearest git checkout (a `.git`
|
|
directory or worktree file) above the workspace. Fleet seats that run in
|
|
`~/.mosaic` therefore show `.mosaic`. The grouping column "Project" is
|
|
unchanged: it still comes from where the seat's logs live, and `seen.json`
|
|
keys depend on it.
|
|
- **Workspace** (detail row, and the hover title of Active project) — the
|
|
current path of the tmux pane running `pi`; when the seat is offline or
|
|
tmux could not be asked, the `cwd` from the session log. The record says
|
|
which one it used (`workspaceSource`: `tmux-pane` or `session-cwd`).
|
|
|
|
A seat started through `scripts/mosaic launch <seat>` (see
|
|
`packages/seat/README.md`, #1504) has a registration at
|
|
`<dataRoot>/seats/<layout>/<seat>/registration.json`. The scan reads every one of
|
|
those and matches a record to a row by its sessions directory, never by
|
|
name alone. A registered task, project or workspace replaces the derived
|
|
value and the row's source field (`taskSource`, `activeProjectSource`,
|
|
`workspaceSource`) reads `registration`; the page shows a small source tag
|
|
next to the value and a "Registered" line in the detail with the start time,
|
|
harness, pid and tmux session. An empty task or a null project or workspace
|
|
in the record leaves the derived value in place. Rows with no registration
|
|
are exactly as before. The board only reads `seats/`; `mosaic launch` and
|
|
`mosaic seat task` are the only writers. A malformed record is listed in
|
|
`registrationErrors` on the index (and on stderr for `scan`) and skipped.
|
|
A record is written before the launch script's own checks run, so a refused
|
|
launch leaves a record whose pid is gone. The scan probes the recorded pid
|
|
on every pass; when it is gone the record is stale: the row keeps the
|
|
Registered line (marked stale, `registered.alive` false) but the derived
|
|
task, project and workspace win and the sources say so. Stale rows are
|
|
listed under `registrationStale` on the index. A record with no pid is
|
|
never stale.
|
|
|
|
Marked rows are listed under a collapsed "Seen (N)" section on the page,
|
|
each with an "Unsee" button, so nothing marked is ever out of reach. Each
|
|
project table has "Hide offline" and "Hide seen" checkboxes (both on by
|
|
default) with a note saying how many rows each one hides. While a box hides
|
|
something the project header reads "N of N" (shown of total); otherwise it
|
|
shows the plain total.
|
|
If `seen.json` exists but is not valid JSON (or not an object of string
|
|
values), the scan refuses rather than silently dropping every mark.
|
|
|
|
## Example status record
|
|
|
|
```json
|
|
{
|
|
"agent": "darkwing",
|
|
"project": "mosaic-stack",
|
|
"state": "waiting",
|
|
"waitingOnYou": true,
|
|
"seen": false,
|
|
"alive": true,
|
|
"tmux": { "socket": null, "session": "darkwing" },
|
|
"sessionFile": "/mnt/storage/src/mosaic-stack/.pi/state/darkwing/sessions/2026-09-12.jsonl",
|
|
"sessionId": "01a06e48-0718-71f2-a889-c263c4800fb9",
|
|
"cwd": "/mnt/storage/src/mosaic-stack",
|
|
"task": "Read agents/darkwing/work/RESTART.md",
|
|
"taskSource": "first-user-message",
|
|
"workspace": "/mnt/storage/src/mosaic-stack",
|
|
"workspaceSource": "tmux-pane",
|
|
"activeProject": "mosaic-stack",
|
|
"activeProjectSource": "workspace-git-root",
|
|
"registered": null,
|
|
"lastActivity": "2026-09-12T15:04:33.000Z",
|
|
"ageSeconds": 42,
|
|
"lastAssistantText": "Ready for the next step whenever you are.",
|
|
"lastError": null,
|
|
"skippedLines": 0,
|
|
"scannedAt": "2026-09-12T15:05:15.000Z"
|
|
}
|
|
```
|