<!--
  scaffold: status-board.md
  bucket: work-management (Bucket 2), with the boot reset in operator-interface (Bucket 1)
  what belongs here: the daily status board discipline.
    - Where the board lives and who reads it.
    - The update rule: every dispatch, return, status tick and operator ruling.
    - The boot reset with the day's plan.
    - The write path (pa status set | patch | show | lane | notice | done, and pa dispatch for each dispatch), never a hand edit.
    - The schema v2 sections (lanes, For me, Done) and the lanes seam.
    - The check (pa board check), at boot, after compaction and at session end.
-->

# Daily status board

The status board is the operator's live view of the day: what is running, what is waiting on the operator, what closed. The operator keeps the workspace **Status** page open (second in the nav, after Home) and reads it instead of asking. A stale or wrong board is worse than none, because the operator trusts it.

## Where it lives

Each PA instance has its own board at `agents/<paName>/workspace/status-board.json` in the operator repo. The Status page shows the running PA's board; `?agent=<name>` shows another instance's. The PA name resolves the way the workspace resolves it everywhere else (`paName` in preferences, else the single agent folder holding `next-session.md`). Every board reader and writer (the page's read and Dismiss, every `pa status` verb, `pa dispatch` and `pa board check`) turns the name into the board's path through one resolver: the name, whether it comes from `?agent=`, `--agent` or preferences, must be an agent folder name (lowercase letters, digits and hyphens), the workspace folder must really be `agents/<name>/workspace/` under the repo root (no symlink on the way), and the board file must lie inside it. Anything else is refused before a file is read or written: the page's routes answer 400, the CLI exits 1. A missing folder or board keeps its usual answer (the page shows no board yet; `pa status set` creates the folders, only inside that fence).

## The update rule

The PA updates the board, through the CLI, at every one of these moments, in the same turn as the event:

1. **Every dispatch.** `pa dispatch open` adds the item as `running`, holding the dispatch id in `ids`, in the same call that writes the spawn-log entry; `pa dispatch stage` refreshes it and `pa dispatch close` moves it to `done`. Patch its `where` (local, a lane host, cloud) with `pa status patch --id <d-id>` when it differs.
2. **Every return.** The item moves on: `review`, `blocked`, `done`, or back to `queued` for the next stage, with a one-line `note` on the outcome.
3. **Every status tick.** When the PA checks on work in flight (a lane poll, a CI read, a scheduled tick), the board's `updatedAt` and the touched rows are refreshed even if nothing changed state, so the freshness pill tells the truth.
4. **Every operator ruling.** A question the operator answers leaves the on-me list (`--on-me --remove`), and whatever the ruling unblocks changes status in the same write. A new question for the operator goes onto the on-me list the moment it is asked.

**At session boot the board is reset with the day's plan.** After the boot reads (see `operator-interface/session-boot-and-end.md`), the PA writes a fresh board with `pa status set`: the headline for the day, one group per workstream in priority order, every item in flight or planned, the open on-me questions, and an empty `done` list.

## The write path

The PA never hand-edits the JSON. The CLI reads the clock, validates against the schema and writes atomically (temp file, then rename), so a bad write never lands and the page never reads a half-written file. Every writer (the `pa status` verbs, `pa dispatch` and the page's Dismiss) reads, changes and writes the board inside one lock, `status-board.json.lock` beside it, so two writers never lose each other's change; a write that cannot get the lock within a few seconds writes nothing and says so. A lock left by a writer on the same host counts as stale only when that writer's process is gone; a live writer is never broken, however long it holds. A long hold is made visible instead: a writer refused by the lock names the holder's pid and the lock's age, and `pa board check` warns once a lock held by a live writer on this host is more than ten minutes old, naming the manual clear, `rm` of the lock file after checking that pid is not a board writer; nothing clears it automatically. A lock from another host, or one with no readable owner, counts as stale after ten seconds. A stale lock is removed only under a short break mutex, `status-board.json.lock.break`, and only while it is still the lock judged stale, so recovering it never lets two writers in. A crash or a kill can leave the lock file behind: the next writer recovers it within its wait, and nothing is lost, because the board itself is only ever replaced whole. Neither file is ever committed. An option's value never starts with `--`, in the separate form or the `--flag=value` form, and `--started`, `--estimate` and `--ended` take an ISO 8601 date-time with a zone on a real calendar date, such as `2030-01-15T09:30:00Z` (`2030-02-31T09:30:00Z` is refused).

| Verb | Use |
|---|---|
| `pa status set [--file <path>]` | Replace the whole board (the boot reset, or a reorder of priorities). Reads stdin when `--file` is absent. |
| `pa status patch` | Merge one item (`--id` or `--match "<title>"`) or one on-me entry (`--on-me`), set a status, set the headline, append to `done` (`--add-done`). An id shared by two items is refused; use `--match`. |
| `pa status show [--json]` | Print the board, to check it before telling the operator anything. |
| `pa status patch --lane <key>` | Merge one item into a lane (`--id` or `--match`): adds it queued last, or as the running item with `--slot running` (refused while the lane has one), moves it from another lane, or moves it to `done` with `--status done`. Item flags as for `patch`, plus `--after "<task>"`, `--project`, `--started <iso>`, `--estimate <iso>` and `--lane-title` for a lane that is new. |
| `pa status lane --key <key>` | Add or change a lane: `--title`, `--paused` or `--resume`, `--remove`, or `--file <path>` (`-` for stdin) to replace its running item and queue from `{ title?, state?, running?, queued? }` in one write. |
| `pa status notice` | Add or change a For-me notice: `--kind Risk\|Money\|Security --text "..."` (`--id`, else one from the clock), `--link`, `--dismiss`, `--undismiss`, `--remove`. |
| `pa status done` | `--id <id>` moves the item holding the id to `done` with `endedAt`, once: the lane item, else the open group item a v1 board puts in Main (no copy is left in Main); `--title "..."` adds a done entry; `--prune` drops entries older than 24 hours. |
| `pa dispatch open \| stage \| close` | Write a dispatch's spawn-log entry and its board item in one call: open adds a `running` item with the id in `ids`, stage refreshes it, close moves it to `done`. A failed board patch exits 2, keeps the spawn-log entry and says what `pa board check` then does; stage and close on a closed dispatch exit 1. See `spawn-log.md`. |

Bare `pa status` is unchanged: it is still the environment probe.

## Schema v2: lanes, For me, Done

A v2 board carries `"schemaVersion": 2` and four lists beside the headline:

- `onMe`: what only the operator can do or decide. Each entry keeps its exact `question`, verbatim, with `options`, `since`, `workstream`, `ids` and `link`.
- `forMe`: notices the operator should see, for risk, money or security only (`kind` is `Risk`, `Money` or `Security`); routine news stays off. Each has a unique `id`, a `text` and, once the operator dismisses it on the page, `dismissedAt`. A dismiss is saved in the board file (`POST /__api/status-board/dismiss`, body `{ "id": "<id>" }`) and does not touch `updatedAt`.
- `lanes`: where work runs. Each lane has a lowercase slug `key`, a `title`, an optional `state` of `paused`, an optional `running` item and a `queued` list in order. A queued item may carry `after`, the plain words of the task it waits on, shown at the card's top right. An item may carry `project`, `startedAt` and `estimatedFinishAt`; the page shows "not set" rather than a guessed time when there is no estimate.
- `done`: finished items with `endedAt` (else `updatedAt`). The page and `/__api/status-board` show the last 24 hours before the board's `updatedAt`; older entries stay in the file until `pa status done --prune`.

**The lanes seam.** Core names one lane, `main`, which every read supplies, first, when the board has none. Any other lane comes from whatever writes it into `lanes` through `pa status patch --lane <key>` or `pa status lane --key <key>`: an extension that runs work on another host keeps its own lane current that way, and the page draws every lane it finds, in board order, between Main and Done. A lane with nothing in it reads "Lane idle"; a paused one "Lane paused".

**v1 boards still read.** A board with no `schemaVersion` is v1 and is migrated on read: `onMe` and `done` kept, every open group item carried into `main` in board order (the first running one as its running item), `forMe` empty, the `groups` list kept beside the lanes so `pa dispatch` and `pa status patch --group` still work. On a v2 board an open group item that no lane holds is carried into `main` the same way. Writing a lane or a notice, `pa status done` and `pa status done --prune` turn a v1 board into v2.

## The check: `pa board check`

`pa board check [--json] [--hook-envelope] [--agent <name>]` compares the board with the spawn-log, using the shared open rule (`spawn-log.md`), and prints one line per problem, then a summary line. It is read-only: it never writes a file.

It reports:

- the board's `updatedAt` 90 minutes old or more (the page's red pill) while a dispatch is open, or no board at all while one is;
- an open dispatch that no board item holds in its `ids`: a group item, or on a v2 board a lane's running item or one of its queued items, in any lane, paused or not (a mention in a title does not count, and neither does an on-me entry, a For-me notice or a `done` entry);
- a dispatch open 24 to 48 hours: close it or check it;
- when the instance keeps initiatives records (`initiatives/<id>/initiative.json`): an open dispatch with no `initiative`, a tagged open dispatch with no `stage` (a stage is a non-empty string, or an object with a numeric `n` and `m` and an optional `label`, such as `{ "n": 2, "m": 5, "label": "Build" }`), and an open question (`initiatives/<id>/questions/*.json` with `status: "open"`) whose id is in no on-me entry's `ids`.

Status or outcome words the package does not know are warnings and never fail the check. The human output gives one line per word: its count, then at most five dispatch ids and "+N more"; `--json` lists every one.

| Exit | Meaning |
|---|---|
| 0 | Clean (warnings may still be listed) |
| 1 | Usage error |
| 2 | One or more problems |
| 3 | The check could not run: no repo root, the spawn-log or the board unreadable or not valid JSON, or the board failing the schema |

`--json` prints one object: `ok`, `exitCode`, `agent`, `board` (`path`, `present`, `updatedAt`, `ageMinutes`), `spawnLog`, `open` (`live` and `stale` lists of `{ id, description, launchedAt, initiative, stage }`, where `stage` is the string, the object as `{ n, m, label }` (`label` only when it is a non-empty string), or `null`), `problems` (`{ kind, dispatchId or questionId, message }`) and `unrecognisedStatuses` (`{ id, word }`).

**At boot and after compaction.** A new instance runs `pa board check --hook-envelope` as a SessionStart hook with the matcher `startup|resume|clear|compact`. The same entry is in both settings files, the estate root's `.claude/settings.json` and the agent folder's `agents/<paName>/.claude/settings.json` (where `pa session` starts), so it runs whichever folder the session starts in. The command is a short `node -e` launcher: it walks up from `$CLAUDE_PROJECT_DIR` to the nearest `node_modules/@stravica-ai/digital-operator-pa/bin/pa` and imports it, so it needs neither `pa` nor pnpm on the hook PATH. The hook prints the board's age, the open dispatches, the untagged ones and those with no board item as session context, under 2,000 characters, and always exits 0. If the check cannot start (no installed package above the folder, a broken install), the context says `board check could not run: <reason>` instead. When it reports a problem, fix the board before the next action.

**At session end.** `pa board check` must pass before the session-end commit.

**On the Status page.** The page shows a banner naming the open dispatches that are not on the board, from the same check.

## Shape rules

- Titles say what the thing is; ids go in `ids`, never in the title.
- Group labels are workstream labels. Group order is priority order.
- Lane order is board order, after Main; a lane's queue order is the order work runs in.
- On a card the one-liner fits one line (about 33 characters at the 280 px lane minimum); longer text is cut with an ellipsis and shown whole in the task window.
- Statuses: `queued`, `running`, `review`, `blocked`, `onMe`, `done`. On a card the status badge is the item's own status when it has one, else its place in the lane (running or queued).
- The freshness pill turns amber at 45 minutes and red at 90 without an update. A red pill during working hours means the update rule was missed.
- camelCase keys, no em-dashes, no operator names in the product surface.

<!-- OPERATOR: add operator-specific board conventions here (workstream label wording, which lane hosts appear in `where`, extra moments that count as a status tick). -->
