Acceptance rule (plan page, bf641e22): a seat mid-tool-call is working,
never waiting. The scanner already met it through pi's stopReason values;
deriveState now also checks the content for a toolCall block (working),
after the error stop reasons and before "stop" (waiting). Thinking blocks
do not keep a text turn from being waiting. Three JSONL fixture tests and
three state-table cases pin the rule. Live check on the real board:
orch-01 and rev-code-01 mid-tool-call are working, velma's finished
text-only turn is waiting. Sonnet review: APPROVED.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
129 lines
5.7 KiB
Markdown
129 lines
5.7 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 list, seen list, 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.
|
|
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",
|
|
"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"
|
|
}
|
|
```
|