# Bellwether 🐏🔔
Bellwether (`@joelhooks/pi-bellwether`) is the owned Pi package for generic Herdr runtime control.
It replaces the loaded `pi-herdr` fork after a separate settings cutover. The fork remains rollback source. Bellwether does not depend on or edit it.
## Runtime
Bellwether keeps Pi startup side-effect free. It resolves the Herdr socket only when a tool or command runs.
- Effect 4.0.0-beta.99 owns socket path resolution, one newline-delimited JSON request per socket, Schema decoding, typed errors, timeouts, interruption, and cleanup.
- XState 5.32.5 owns each watch lifecycle: `starting -> running -> matched | timedOut | targetGone | failed | cancelled`.
- A Pi is prompted by Rat King mail, not keystrokes: `herdr_agent prompt` to a Pi emits `bellwether/pi-prompt/v1` for its Muster owner to claim, and refuses with `PromptUnrouted` when nobody does. Pi turn state comes from Rat King turn records, so `agent_state` watches refuse Pi panes.
- Each watch owns one direct Herdr wait socket. Watches do not shell out, spawn `herdr`, or pool connections.
- Agent-state watches also use bounded one-request `agent.get` probes every five seconds. A crashed TUI that returns to a live shell settles as `targetGone` even when Herdr emits no release event.
- Active watches render in a compact Pi widget with a live spinner, phase, target, and elapsed time. The widget hides when no watch is active.
- Inside a Herdr pane, active watches also drive the Herdr sidebar under source `user:bellwether.v1` with a three-minute lease: pane `$wait` (for example `⏳ reviewer · 4m`). Bellwether publishes no workspace tokens: Muster publishes `$agents` from Rat King turn records. Herdr merges tokens across sources, so Bellwether clears only its own pane token. No request is sent while no watch is active. Add `["$wait"]` to `ui.sidebar.agents.rows` to show it.
- Herdr `error.code` determines timeout and target-loss states. Error prose does not.
`herdr_ping_wait` remains an explicit degraded crash and turn fallback. It is the only child-process wait path. `herdr_watch` never calls it.
## Structured tools
### `herdr_layout`
Actions:
- `current`, `overview`
- `workspace_list`, `workspace_create`, `workspace_focus`, `workspace_rename`
- `tab_list`, `tab_create`, `tab_focus`
- `pane_list`, `pane_layout`, `pane_split`
`overview` returns one bounded snapshot for an explicit workspace or the caller's current workspace. It includes caller identity, scoped panes and agents, and session-owned active watches tied to those panes. Each list returns at most 64 entries with total, returned, and omitted counts. A failed section appears in `partialFailures`; Bellwether never substitutes a globally focused workspace for a missing caller.
### `herdr_pane`
Actions: `get`, `rename`, `run`, `read`, `send_text`, `send_keys`, `close`.
`rename` changes the pane display label. Pass `clearLabel: true` to clear it. Agent identity naming remains a separate `herdr_agent rename` action.
There is no `wait_output` action. `close` requires `confirm: true` and refuses the pane that hosts the current Pi process.
### `herdr_agent`
Actions: `list`, `get`, `start`, `prompt`, `read`, `send_keys`, `focus`, `rename`.
Agent startup uses `timeoutSeconds`; `120` means two minutes. There is no ambiguous public `timeout` field. Herdr's direct `agent.start` response may confirm `interactive_ready: true`; only then does Bellwether report readiness as `proven`. Otherwise it reports the launch as submitted with readiness `unknown`, preserving `launch_pending`, `interactive_ready`, agent state, and stable identity in details.
There is no public `wait` action or `wait` parameter. `prompt` first resolves the target to its stable pane ID, then performs a bounded delivery handshake. Bellwether asks Herdr for `working` state with one absolute 30-second wall-clock deadline. A successful response is proof that Herdr observed the agent alive and working. If the target is already working, Bellwether submits atomically without asking Herdr to wait for a later turn transition; the prompt response must still report working. This avoids a false timeout while a long current turn queues the prompt. Herdr 0.7.5 can return `agent_prompt_stalled` after five seconds even when Pi starts working just after its fixed gate. Bellwether then spends only the unused part of the original 30-second deadline on `agent.wait`, without resubmitting the prompt. If neither path observes `working`, the call returns a timeout. It does not wait for completion and starts no watch.
Start and prompt failures return `isError: true` with cloneable details: the primary Herdr error, failed stage, known pane identity, and prompt submission state. Bellwether makes at most one extra `pane.read` diagnostic request with a 1.5-second client deadline. Evidence is ANSI-stripped and capped at 12 lines / 2 KiB. Terminal evidence is marked `UNTRUSTED`; a diagnostic failure never replaces the primary error. An aborted call performs no further diagnostic I/O.
### `herdr_watch`
Actions: `start`, `list`, `status`, `cancel`.
`list` returns active watches only. Add `history: true` to include the retained terminal receipts. `status` still retrieves a finished watch by ID.
Status and wake text include the target, failure code, and observed agent state or terminal match. Terminal evidence prefers the matched line and is limited to 12 lines / 2 KiB, with ANSI controls stripped and truncation marked. Full structured receipts remain in `details`. Read more output only when the excerpt is insufficient; an observed state or matching line does not prove task completion.
For `start`, optional `socketPath` selects a direct Unix socket instead of the resolved local socket. Use an externally managed forwarded socket to watch another Herdr instance; Bellwether does not create or manage the forward. The wait and identity probes use that path. Status and list receipts show the override. Reload and `/herdr-resume` preserve it without extending the deadline. A missing or refused custom socket settles as `failed` with the path and `forward down`, not `targetGone`.
Initial kinds:
- `agent_state`
- `pane_output`
`start` returns a cloneable running receipt immediately. The public timeout field is `timeoutSeconds`; `7200` means two hours. Bellwether converts seconds to Herdr milliseconds once at the extension boundary. Bellwether exposes no ambiguous public `timeout` field. Wake policies are `agent`, `notify`, and `silent`. Agent-state watches race the event-driven wait against a five-second liveness probe. Explicit `agent_not_found`, `agent_not_running`, or identity replacement settles as `targetGone`; transient probe failures do not override the wait. Cancel and non-reload `session_shutdown` close exact owned sockets and suppress late wakes. Bellwether stops terminal actors and retains only the newest 64 terminal receipts per session.
`/reload` is different: Bellwether writes one versioned suspension entry into the current Pi branch, closes its exact sockets and fallback child processes, then restores active waits in the new extension instance. Absolute deadlines do not restart. Only `reason: "reload"` restores them automatically; new, resumed, and forked sessions never revive stale waits. The newest suspension entry is authoritative, including an empty or malformed one.
`/quit` writes the same suspension entry. After a process restart against the same session file (`pi --session <file>`), `/herdr-resume` restores the newest entry on request: it skips waits whose absolute deadline already passed and waits that are already active, and it never runs by itself. Run it before the first `/reload` in the new process, because a reload writes a fresh (possibly empty) entry that becomes the newest.
The approved first cut intentionally supports only `agent_state` and `pane_output`. `workflow_receipt` is not a watch kind. Bellwether has no workflow-watch adapter. Add one only when a concrete consumer and result contract exist.
## Wakes
Watch and degraded ping-wait settlements send signed `sh.mschf.agentgraph.wake` records to the owner's live Rat King name (`src/wake-record.ts`). pi-ratking owns urgency, batching, and delivery through pi-until.
- Without a live self name, Bellwether sends one local follow-up with the original custom type and details. It does not batch.
- A refused send or a missing delivery result after 15 seconds falls back to that same local follow-up and warns the owner.
- Records cap text at 8192 characters, terminal evidence at 12 lines / 2048 characters, and encoded size at 16 KB. They omit socket routing.
- Cancelling a mailed wake sends a `cancelled` record for the same watch. pi-ratking withdraws it if still held. Local follow-ups cannot be withdrawn.
- Notify, silent, and already-reported settlements remain local only.
## Pane-close wakes
An owner closing a pane can emit `bellwether/pane-close/v1` on `pi.events`:
```ts
{
paneId: string,
terminalId?: string,
reason: string,
reply?: (result: { retired: string[] }) => void
}
```
Bellwether subscribes at extension load but performs no I/O until a watch starts. It never publishes this event. The listener cancels matching local active watches, including gated watches, and withdraws held wakes. Matching uses the stable pane ID, a named agent's observed pane ID, or the optional terminal ID after pane renumbering. Pane-output watches resolve their terminal identity once; agent-state watches learn it from their existing liveness probe. A gated pane target can match directly before any probe runs.
The retained receipt is `cancelled` with `failure: "pane closed by owner: <reason>"`; cancellation does not wake. If a watch already settled as `targetGone` (or another terminal status), it is retired when Bellwether has a mailed or in-flight wake to withdraw. Already-delivered wakes cannot be recalled. `reply` receives `{ retired }` synchronously. Invalid payloads are ignored with a payload-free debug log.
## Slash commands
Bellwether keeps bounded human commands:
- `/herdr-status`
- `/herdr-agents`
- `/herdr-read <agent target>`
- `/herdr-focus <agent target>`
- `/herdr-stop <agent target>`
- `/herdr-resume` — restore waits from the newest suspension entry after a process restart; expired waits are skipped.
The old split send/submit and combined start commands are gone.
## Install and smoke test
```bash
pi install git:github.com/joelhooks/pi-bellwether
PI_OFFLINE=1 pi -e /path/to/pi-bellwether --help
```
## Settings cutover
Do not change global Pi settings while building or reviewing this package.
After parity review, replace the `pi-extensions/packages/pi-herdr` entry in `~/.pi/agent/settings.json` with the reviewed Bellwether checkout or package reference. Start a fresh Pi session, verify the four structured tools, run one bounded read and one cancellable watch, then keep the fork unloaded as rollback.
## Development
```bash
npm install --ignore-scripts
npm run check
npm test
npm run smoke
npm run pack:check
npm audit --omit=dev
pi-notes brain check
```