# Muster 🐑
Muster is the project manager for Herdr project spaces in Pi. A project is one Herdr space. A lane is one tab. An agent is a catalog row with a full launch profile. A packet is one harvested result named by its hash. The desk is the operator's queue.
It sits on top of [Bellwether](https://github.com/joelhooks/pi-bellwether), which owns the generic Herdr runtime: sockets, panes, agents, watches, wakes. Muster imports Bellwether's exported modules as a library and never reaches into its extension. Bellwether never imports Muster.
```text
herdr (terminals) → Bellwether (runtime) → Muster (projects, lanes, packets, agents, desk)
```
## Default shape
One writer, one gate, one accountable desk. The desk owns the project; each lane has one writer, a worker who reports directly to the desk. The desk may also be the writer. The land gate proves the exact candidate.
Bosses, the hawk, judges and arbiters are opt-in for a named risk. Record the risk on the lane or in the launch before opening the role: for example, many packets touching shared files, a long unattended run, or customer money. There is no standing judge slot.
Retros run only after an incident (a bad landing, outage or lost work), friction that repeats across lanes, or when Joel asks. The desk opens one by hand; lane counts, elapsed days and ordinary project reviews do not trigger one.
## Install
```bash
pi install git:github.com/joelhooks/pi-muster
```
Muster expects [Herdr](https://herdr.dev), pi-ratking (`@rat-king/pi-ratking`) and pi-until in the same Pi install. Muster traffic rides Rat King by name; pi-intercom is not needed. Muster always uses pi-ratking. Existing `policy.comms` values, including `intercom` and `network`, still decode but cannot select a transport. `project_status` reports ignored values and keeps the file unchanged. Without pi-ratking loaded, sends fail immediately with `pi-ratking not loaded`; there is no fallback. Two paths default to the author's rig and can be pointed elsewhere:
- `MUSTER_WORKER_WORKTREE`: the script that allocates, harvests, and removes worker clones.
- `MUSTER_DESK_EXTENSION`: the Pi extension a desk agent loads to read its queue.
## Communications
Muster emits `ratking/send` and receives `ratking/message`. Rows answer to `<project>/<row>`; rowless owners use the `ownerName` recorded on their rows. Owner posts and agent messages retain the deployed `{type: "owner" | "message", recipient, …}` JSON payload inside `kind: "data"`, so old and new Muster sessions can exchange mail during rollout. A session id inside that payload is local queue metadata, not a delivery fence.
pi-ratking owns names, DIDs, keys, leases, cursors and the reader. Muster neither provisions identities nor starts a mailbox consumer. `comms_doctor` reports pi-ratking's live status when available, otherwise its canonical claims, directory and cursor files. Live status reports `reader`, `leaseGeneration` and `leaseUntil`, including null names and DIDs after retirement. It takes precedence over missing file claims for env-given names. Without live status, a cursor with no claim is reported as unknown; a wrong session or dead claimant suggests a restart. Remote rows are probed on their own machine. The doctor never mints or pushes keys.
An owner pass backfills `ownerName` and pushes updated orchestration bindings to running remote rows. Workers read that binding before their launch-time `MUSTER_OWNER_NAME`. A restart rebinds the catalog and emits `ratking/retire` from the old Pi; the successor takes the same name. There is no successor ledger, scoped owner forward or stale-session quarantine. Local queue history stays intact.
Detached launch helpers run on the owner's machine without a Pi event bus. They append their result to that owner's local queue and never send. Initial work travels through argv/file pointers and requires first-turn journal proof.
`desk_phone` uses pi-ratking's record API (rat-king `819c4ab` or later). Item and update records go to the reserved `joel-phone` alias with encryption explicitly enabled, without a message wrapper. Answers arrive on `ratking/record`, never as agent text. Muster accepts them only when `verified` is true and the signed `did` matches the phone DID in the private `~/.config/muster/desk-inbox.json`; it never trusts the sender label alone. The same file's Switchboard DID must match this Pi's live pi-ratking status.
Phone state remains in `~/.local/state/muster/desk-inbox/pending.json`. Old pending threads and their message references remain readable. A send intent without a confirmed receipt is never resent blindly; `sync` retries recorded rulings and resolved updates, not uncertain initial sends. `poll` only reads state. Muster opens no mailbox, lease or second reader. The inert `comms-network.ts` compatibility stand-in stays until the desk finishes restarting older Pis. All vendor code and fixtures are gone. The old lazy item, receipt and quarantine schema imports were phone/network-only, not part of non-phone Rat King operations. Inbound Muster messages and phone records share a process-local 500-ID deduplication window before any follow-up or notification. Owner items also deduplicate against the local queue by URI and CID, including deliveries with a new message ID.
## Long-lived sessions
Muster records the package commit when the extension loads, without spawning Git. Every tool, including desk and Switchboard tools, checks for changes at most once per minute. A changed commit adds a stale-tools warning with both short SHAs and the commit count when Git can calculate it. `project_status` also marks its board header. After a dependency upgrade, restart Pi with `pi --session <session file>`, not `/reload`: stale reloads keep Muster tool names and return the restart command instead of silently losing them. Muster-managed agents restart with `agent_launch action: "restart"`, never `/reload`.
Outside a Git checkout, the check uses the package version and newest mtime of the extension's local source imports. Missing metadata or a failed check never blocks a tool. Startup reads version metadata only; it opens no socket or bus channel.
## Roster
Role models are data. Muster reads `~/.config/muster/roster.json` (or `MUSTER_ROSTER`) on every call, so one edited and synced file changes what every machine launches next. Without it, built-in defaults apply. Precedence: built-in, roster role, the alternate matching the chosen model, project policy, explicit `agent_launch` arguments.
```json
{
"version": 1,
"aliases": {
"opus": "claude-bridge/claude-opus-5-5",
"sol": "openai-codex/gpt-6.1-sol"
},
"roles": {
"boss": {
"model": "claude-bridge/claude-opus-5-5",
"alternates": [
{
"model": "openai-codex/gpt-6.1-sol",
"thinking": "high",
"compactAt": 200000,
"useFor": ["investigation", "root-causing bugs", "reviewing a builder's output"],
"avoidFor": ["front-end design", "long unattended builds"]
}
]
}
}
}
```
Use `opus` or `sol` in launch and policy models; roster aliases can override these built-ins or add names. Thinking suffixes such as `opus:high` are supported. Aliases resolve before alternate settings are selected. Sonnet is refused, including aliases that point to it, except front-desk's `claude-bridge/claude-sonnet-5-5` (Joel, 2026-10-04: "front desk needs to be opus 5 5 and sonnet 5 5"); exceptions live in `MODEL_EXCEPTIONS` in `src/models.ts`. Workers in any project may also opt into `claude-bridge/claude-sonnet-5-5` (Joel, 2026-10-05: "we can offer exploratry use of sonnet 55 for workers"), but only as an explicit launch `model`; a policy or roster default naming it is still refused (`ROLE_MODEL_EXCEPTIONS`). Fable runs only when a launch or role policy names its exact route, such as `claude-bridge/claude-fable-5-2` (Joel, 2026-11-09: "i think we wipl iltimately want desks on fable and workers on opus"). A Fable alias, a default substitution and a fallback route are still refused, and a restore never moves a session onto Fable unless its launch profile or the call names it. No role defaults to Fable; the judge defaults to Opus. `project_update` returns the aliases with the effective policy.
Every launch, fork, and restore runs `pi --list-models` once in the launch cwd, with a 20-second timeout, before writing a row or opening a pane. A missing authenticated model fails with working-route suggestions. A failed or empty listing permits launch with a `model check skipped` note.
Delivery requires Herdr `working`, two clean pane reads three seconds apart, and a newly appended matching user entry followed by a clean first assistant entry in the session journal (90 s bound); a missing or failed first turn returns `unproven` with a `FIRST_TURN` event and repair call. Long or multiline prompts use private durable file pointers, including on remote hosts; receipts include the pane's Pi path and version, or `unknown` if the probe fails. Only Pi-rendered `Error:` lines in the newest output above its editor count; prose, older errors, editor text, and footer text do not. Auth/model errors fail the launch and row, record the line in row events, and leave the pane open. Rate limits under the same prefix warn without failing. `project_status` checks only running, silent, nudged, and restarted rows whose Herdr status is not working. With `act: true`, it fails owned rows with a current model error. Finished rows keep their state. Model-failed rows require an explicit restore, not automatic adoption.
Role and alternate `skills` name standing skills (names or absolute paths); launch skills append after project policy skills, de-duplicated. Desk, boss, hawk and judge launches also load the package's `skills/muster`, unless `noSkills` is set. Workers keep `-ns` and search the installed catalog with `skill_find`, then read a matching `SKILL.md`.
## Tools
Owner side: `project_open`, `project_move`, `project_update`, `lane_open`, `lane_deliver`, `lane_close`, `agent_launch`, `agent_close`, `packet_verify`, `packet_land`, `desk_post`, `project_status`, `project_review`, `desk_inbox`, `desk_answer`, `desk_report`, `desk_rulings`, `thinking_set` (a session lowers or raises its own thinking level, as an opted-in hawk may do when the line stops).
### Deploy posture
VISION.md is canonical. Add a section like this; Muster prints this conservative template when it is missing and never edits the file:
```markdown
## Deploy posture
Level: 1 (prove)
Live proof frees the slot.
```
`project_update policy.deployLevel` is the fallback. No section or policy means level 1. A malformed section is reported and falls back. VISION wins over policy; status names a mismatch and shows each lane's effective level. Reads happen at call time.
| Level | Name | Deploy permission | WIP frees |
| --- | --- | --- | --- |
| 0 | locked | Cite a resolved approval desk item in evidence | Live proof |
| 1 | prove | Green gate | Live proof |
| 2 | ship-and-watch | Green gate, `Rollback: ...` and `Watch: ...` evidence lines | Deploy; desk follows watch evidence |
| 3 | jfdi | Green gate and `Rollback: ...` evidence line | Deploy; no proof step |
`lane_open deployLevel` only lowers permission and requires `deployRule`: `customer-facing`, `money`, `outbound-sends`, `irreversible`, `shared-infra` or `slow-rollback`. The tool refuses an override above that rule's cap. Customer-facing work, money or outbound sends cap permission at 1; irreversible work at 0; shared infrastructure at 2; rollback over five minutes or needing another person at 1. A later project-level decrease also lowers existing lanes.
`project_update` sets the sidebar headline and the project's policy: WIP and flow limits, silence limits and per-role model, thinking, compaction, and skills, merged over Muster's defaults.
## Delivery and flow
`packet_verify` records the base SHA and changed paths for commit packets and writes `~/.local/state/muster/reviews/<project slug>/<packet>.md` on the owner machine (private directory `0700`, file `0600`), outside the project's Brain: the lane brief, packet report, and merge-base diff, capped at 60 KiB. For a blind review, launch `agent_launch role:"judge"` with a prompt naming only that file. The reviewer numbers findings and answers the three coverage questions with observed command/path/line evidence or “not checked”. No worker transcript or pane text is included.
`packet_land` warns when the base moved under verified paths, naming every overlapping path and the count; an actual land gate refreshes that evidence and suppresses the warning. Old packets without a recorded base SHA get no stale-evidence warning. Pass `review: <absolute path>` to record blind-review findings. A commit packet over 200 changed lines without a recorded review gets `no blind review recorded`. Both warnings are alarms, not refusals. These input-freshness and blind-review ideas come from [eas4ai/sudus](https://github.com/eas4ai/sudus).
A lane is done when its work is live and proven, not merely merged. See [the definition of done and brief template](skills/muster/references/done.md). `packet_report` accepts `deploy`, `proof`, and `signals: { working, failing, where }` and includes them in its `.svx` report.
Committed `packet_land` outcomes start delivery at `landed`, with a timestamp. `lane_deliver({ slug, stage: "deployed" | "proven" | "waived", evidence })` records the next stage and its plain-word evidence. Proof may follow a waiver or deploy at any level. Delivery otherwise only moves forward within a cycle; another committed packet starts a new cycle. Docs or probes with nothing to deploy can be waived. Rejected and `no_changes` packets need no delivery. Old catalogs decode committed lanes as `proven` with evidence `before done-live`; new landed work is never backfilled.
Feature WIP includes open and draining work lanes, plus closed lanes still at `landed` or `deployed`, even if archived. At deploy levels 2 and 3, deployed lanes leave WIP even with an open tab; the flow line keeps them visible as `watching`. Role tabs and proposed (`open: false`) ideas do not count. `policy.wipLimit` defaults to 3; `null` disables it. At the limit, `lane_open` refuses and lists in-flight lanes with stage and age. Park ideas with `open: false`, or pass `override` containing Joel's words; the lane records them.
`project_status` also reports progress alarms as `watchdog: <row> ...`. The fast path checks artifact age, repeated tool errors and turns without an artifact. Only flagged worker/boss rows owned by the calling session can invoke the judge; the caller's own row is excluded, and `act: false` never invokes it. The watchdog records an alarm, never types, nudges or restarts. The owner decides what to do.
Tune `policy.watchdog` with `project_update`: `progressStallMin` (120), `repeatedErrors` (3), `errorWindow` (50 tail messages), `turnsWithoutArtifact` (30), and `model` (`cliproxy-codex/gpt-5.6-terra`). Fields are optional and merge individually. The named model must exist in Pi's registry; there is no fallback. The resolved model API must be one of `openai-completions`, `openai-responses`, `anthropic-messages`, `google-generative-ai` or `google-vertex`. Session-bound APIs such as `claude-bridge` are refused even under another provider name, because a side call could cross the owner's conversation. A judgment uses low thinking, 300 output tokens, a 15-second deadline, the brief (or next action), and at most 8 KB of redacted assistant/tool text. Unchanged artifact/error/flag evidence reuses the last judgment for `progressStallMin`. Each pass makes at most three new judgments, oldest artifact first; other flagged rows say `judge deferred` and get a turn after earlier rows enter cooldown. Remote probes retain the first 100 rows' silence timestamps, including closed rows, and spend at most about ten seconds on watchdog work before reporting `probe budget` for the remainder. Bounded session tails give conservative turn counts; unavailable probes or judges are alarms, not status failures.
Muster's phone records keep the item title plus explanation, or an update's text, within 100 words including the trim marker. Trimmed item detail stays in the card body.
The board header and turn feeds show one flow line: `WIP 2/3 · landed, not live: lexicon-pin 40m · oldest in flight 2h · last proven 35m ago`. `⚠ not flowing` marks WIP with no recent proof or stage movement for `policy.flowStallMin` (default 120), or a reported packet waiting longer than `policy.landWaitMin` (default 30). These limits are minutes and can be changed with `project_update`. No extra sounds or toasts.
Worker side, when `MUSTER_AGENT` is set: `owner_note`, `packet_report` and per-role compaction (`--compact-at`, `/compact-at`). A worker (`MUSTER_ROLE=worker`) gets no project-management or operator tools. `owner_inbox` and `owner_reply` remain for one release as refusals that name their Rat King replacements.
`muster-heavy -- <command>` registers a job and runs it immediately. It never queues, grants capacity, checks memory for admission, or holds an exclusive window. Local `packet_land` gates run through the same CLI. Old admission flags and environment settings are accepted and ignored, with one stderr line naming them. Old `grant` management commands are inert.
Jobs live in `~/.local/state/muster/jobs/<id>.json`: id, host, repo, cwd, full command, wrapper pid and start time. The wrapper is the process-tree root. A shared cache batches one `ps` across live jobs every five seconds. Status shows tree CPU percentage, current RSS and sampled peak RSS in KB. On exit the record has exit code, wall milliseconds, CPU-seconds and peak RSS. `/usr/bin/time` supplies final CPU accounting even for commands shorter than the sample interval. Darwin uses portable output; GNU/Linux includes the child exit field to distinguish signals from intentional exits such as 143. RSS peaks are sampled, so short spikes between samples are not measured. A dead wrapper without an exit is marked `exit: null, lost: true` on the next status or report pass.
Use `muster-heavy status [--json]` for live jobs and `muster-heavy report [--since 24h] [--json]` for per-repo and per-command count, total wall time, total CPU-seconds and maximum RSS. The report selects jobs by start time and includes lost-job counts. State and telemetry files stay private.
The status JSON keeps fleet-compute's placement fields. `holders` has one held entry per live job with `mode: "job"` and the full command, including any `fc-<runId>` marker. `slots` is live jobs plus one, not a capacity limit; `minFreeGB` reports `MUSTER_HEAVY_MIN_FREE_GB` (default 16 GB), which fleet-compute may use for placement. Local execution never consults that floor. `availableGB` counts free, inactive and speculative pages on macOS, and `MemAvailable` on Linux, rather than raw free memory. `loadLimit` is a large constant and `exclusivePending` is not held. `jobs` adds telemetry.
`muster-heavy gate --wait 1200 -- <command>` is unchanged: fleet-compute owns placement and queueing. `--wait` still applies to fleet routing, not local execution. `MUSTER_FLEET_COMPUTE` selects an absolute runner script; `off` or a missing runner falls back to immediate local execution. Installed `packet_land` uses fleet receipts to decide success and prove the committed tree. Keep `pack:check` and `smoke` local.
## Owner mail
Owner mail is Rat King mail. Workers and bosses use `owner_note({kind, title, body?, refs?, replyTo?})`. FYI, progress and done arrive quietly. Questions, blocked notices and packet actions mention and wake the owner resolved at send time. `owner_note` reads the `MUSTER_PROJECT` catalog row named by `MUSTER_AGENT`; if the catalog is unreadable or the row is absent, its receipt names the `MUSTER_OWNER` fallback. `replyTo` threads a post onto the Rat King message that carried its parent.
pi-ratking shows and wakes owner mail; Muster only claims the payloads it understands. Read open asks with `ratking({ action: "pending" })` and answer with `ratking({ action: "reply", replyTo })`. A send retries transient failures for up to about 3 minutes, then returns NOT DELIVERED; send it again. There is no local queue, sidecar or fallback. A launch result whose mail did not go out shows on the owner's next `project_status` for a day, with its log path.
Slice 5 removed the JSONL owner queue under `~/.local/state/muster/owner-queue/`. Its files stay on disk, unread. `owner_inbox` and `owner_reply` refuse with `OwnerInboxRetired` and `OwnerReplyRetired` for one release, so recurring passes that still name them can drop them at their next re-arm.
## Switchboard
One inbox over every project's desk queue. It reads and routes; project desks stay the authority for their own work. Start a session as the Switchboard with `pi --switchboard` (or `MUSTER_SWITCHBOARD=1`, or `/switchboard on`). Nothing starts on its own otherwise.
- The collapsed widget is a theme-colored flame graph, at most eight lines: four to six flame rows, project labels (`☠` means a dead desk), and one open-count summary. Blocked/approval/decision weights are 3/2/1; logarithmic age adds height and heat. Quiet projects show dim embers and yield space first. Below 40 columns, with `NO_COLOR`, `TERM=dumb`, or an uncolored theme, it shows only the plain summary.
- Flames flicker deterministically at about 7 fps, flare for 1.5 seconds on arrivals, and settle for 2 seconds on resolutions. Only the active visible Switchboard animates; the overlay pauses it. After 60 seconds without queue changes or input it freezes, resuming on either. Pi-tui 0.84.3 exposes no terminal-focus notification API; incoming CSI focus reports are honored if the host supplies them. Animation only invalidates the widget and requests a TUI render, never a model turn.
- The expanded `alt+s` overlay keeps the ranked `project#id` asks and ticker in its header, above the navigable project rows. It keeps the newest five queue events for an hour; resolutions show `✓ resolved`. On short terminals the header yields room to navigation.
- Queue changes repaint without a model turn, with a one-second timer as the watch backstop. Queue reads retain byte offsets and reset on truncation or replacement; ages tick from cached records. Fleet topology and desk liveness refresh every 30 seconds or on registry changes. `☠ no live desk` means no catalog desk or owner is in the live session list; an unavailable session list, as under ratking, shows no marker. The first refresh failure is logged with its message.
- Nudges are only a courtesy: writes before Switchboard registration, old extension versions, and external queue writers can miss them. Activation and filesystem reads show the entire open queue anyway.
- Optional `MUSTER_SWITCHBOARD_CALLS` (absolute report-items JSON path) and `MUSTER_SWITCHBOARD_CALLS_URL` add one display-only Joel calls line; without the path, nothing is read or watched.
- `alt+s` or `/switchboard` browses everything: `j`/`k` move, `space` folds, `enter` puts an `[project#id]` reference in the editor, `a` answers, `d` marks done.
- `desk_inbox` lists open items ranked blocked, approval, decision, oldest first. It is read-only; only `subscribe: true`, `--switchboard`, or `/switchboard` makes a session the Switchboard that is paged on every queue change, and never one Muster launched (any `MUSTER_ROLE`). `desk_answer` appends a resolving line to the item's own queue and nudges that project's desk.
## Relay diet
Packet reports still send one actionable Rat King message to the owner. The owner must verify and land them.
The Muster desk feed suppresses ids created by this session's `desk_post`, `desk_answer`, and `desk_rulings` tools. It reserves the queue id before writing, keeps it in memory, and saves a `muster-desk-self-post` custom session entry for reload. A failed attempt may leave an unused reserved id. Other sessions' items still arrive, even when their sender label matches. The queue and inbox remain unchanged; only self-delivery is suppressed.
With Bellwether's pane-close bus support, `agent_close` and `lane_close` notify Bellwether before closing a pane Muster opened. The `bellwether/pane-close/v1` event carries the pane id, recorded terminal id, reason and synchronous reply callback. Bellwether retires the owner's matching watches and withdraws their held wakes; close receipts report `watches retired: <ids>` (or `none`). Owners no longer need to cancel their own blocked-watch manually. With older Bellwether or no listener, list the pane's watches and cancel them first with `herdr_watch action=cancel id=<id>`. Fallback receipts name candidate ids from session receipts, not confirmed live watches; named agent targets need inspection too.
`~/.local/state/muster/relay-events.jsonl` holds best-effort metadata counters, one line per packet relay attempt or queue item delivered/skipped. Each line contains `{ts, session, kind, project, packetId?, itemId?}`; no bodies, titles or refs. `kind` is `packet_report`, `desk_note`, `desk_note_skipped_self`, or `watch_retired`. A synchronous pane-close acknowledgement emits one `watch_retired` line per retired id, stored in `itemId`; the fallback emits none. A batch delivered before a user turn records one `desk_note` per item, not one per card. These counts measure relay boundaries, not completed model turns. Write failures never fail a tool or feed delivery.
## Desk report
When a desk holds several decisions, it publishes one static feedback page instead of a chat digest. `desk_report` builds it from report items: one card per decision, radio sets per decision axis, and a copy-feedback button. The page is noindex and holds no links or addresses. `desk_rulings` turns the pasted feedback into one resolving line per desk item, plus one message for the owner. The contract is in [skills/muster/references/desk-report.md](skills/muster/references/desk-report.md). The page inlines ratstack's `app.css` when it is on the machine (`MUSTER_DESK_REPORT_CSS` overrides the path).
## State
- `<project>/.brain/data/muster/project.json`: the project, its lanes, agent rows, and packets. Schema-decoded on every read; written with a lock and an atomic rename.
- `<project>/.brain/data/muster/reports/`: packet reports as `.svx`, with title, packet id and lane frontmatter. Worker text is fenced as code for MDsveX; existing `.md` report paths still verify and land without migration. `closed/`: pane tails saved before a close or restart.
- `<project>/.brain/projects/muster/<slug>.svx`: a generated Brain board.
- `~/.local/state/herdr-desk/<slug>.jsonl`: the desk queue, one JSON line per item.
- `~/.local/state/muster/jobs/<id>.json`: private atomic job records, retained after exit.
- `~/.local/state/muster/jobs/samples.json`: shared five-second process telemetry cache; `sampling/` serializes cache writes only, never command execution. Legacy admission files are unused and left untouched.
- `~/.local/state/muster/projects.jsonl`: every project `project_open` has seen, so the Switchboard can find them all.
Muster refuses project state, briefs, and agent cwds under a temp dir unless the project was opened with `ephemeral: true`.
## Lifecycles
XState machines, persisted as the state value on each row:
- Agent: `planned → launching → running → reported → verified → landed → closed`; `running → silent → nudged → restarted`; `* → interrupted → restoring → running`; `failed`.
- Lane: `proposed → open → draining → closed`.
- Lane delivery: `none → landed → deployed → proven`; `landed → proven` permits a combined live deploy check; `none | landed | deployed → waived` records nothing to deploy.
- Project: `setup → active → reviewing → active | archived`.
An illegal transition is a typed `IllegalTransition` error. Sidebar tokens (`now`, `progress`, `agents`, `needs`, source `user:muster.v1`) are derived from these states, the headline, and the desk queue. In a space it owns, Muster keeps the space label a plain name.
## Checks
```bash
npm install --ignore-scripts
npm run check
npm test
npm run smoke
npm run pack:check
```
## License
MIT