# 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 '=' -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//sessions`). Defaults to the current directory. - `--fleet PATH|none` — path to the fleet agents directory (`//.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: (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`. - `POST /api/reply` — send one line of text to one registered seat through `tools/tmux/agent-send.sh` (#1505). Body: `{"agent": "/", "text"}`, JSON, at most 4096 bytes, text at most 2000 characters. The server rescans, finds the row, and runs `agent-send.sh -s -S :control-board [-L ] -m ` with the session and socket from the row's registration; `MOSAIC_TMUX_SOCKET` is stripped from the tool's environment so the registration is the only source. The text is sent as typed, followed on its own line by a fixed trailer: `(control-board: answer in your own session as usual; the board reads your transcript. Do not agent-send to control-board.)`. The board is a sender without a pane: replies to it are read from the seat's transcript, never sent, and `agent-send.sh -s control-board` refuses because no such session exists (Jason's refinement after the first real exchange, 2026-09-12). It answers `200` with `{delivered, exitCode, signal, stdout, stderr, agent, session, socket, sentAt}` whenever the tool ran, delivered or not; a non-zero exit is reported with the tool's stderr, never retried and never routed around. Refusals before the tool runs: `400` bad body, empty or blank text, text too long; `404` unknown row; `409` the row has no registration, the registration is stale (pid gone), or it has no tmux session; `500` the tool could not be started. One seat per request; no queue, no history, no broadcast, no raw `send-keys`. - `GET /healthz`. On the page, the detail of a row with a live registration and a tmux session has a one-line reply box and a Send button; every other row's detail says "reply needs a registered seat". After Send the row shows `delivered