herdr · git:20261031.87fef40 · 2026-10-31 · sha256 53aff7b0e565a395
herdr git:20261031.87fef40A
Immutable. This exact content never changes and is served at /api/v1/blob/53aff7b0e565a395.
---
name: herdr
description: >
Use when this process is inside Herdr TUI (HERDR_ENV=1 and HERDR_PANE_ID) or
Herdr Core (HC_TERMINAL_ID), or the user asks to drive Herdr Core or the Herdr
App window. Run scripts/herdr-runtime first and obey runtime. On Core,
coordinate with hc; change what the person sees only with herdr-app. On TUI,
send a lightweight herdr-msg (reply-to + task). On delivered, stop. On
unconfirmed, send Enter once then stop. Do not poll siblings and do not
block-wait for agent replies.
---
# Herdr
Herdr is three programs. The TUI (`herdr`) and Herdr Core (`hc`) are separate sessions and do not share terminals. The Herdr App (`herdr-app`) is a window onto Core. Detect which runtime owns this process, then use only that runtime's commands.
For flags, read [references/command-reference.md](references/command-reference.md). If usage output disagrees with that file, the installed binary wins: `herdr <group> --help`, `hc <command> --help`, or `herdr-app commands`.
## Detect the runtime
```bash
scripts/herdr-runtime
```
The script is read-only. It prints `runtime`, `app`, and `self`.
| `runtime` | This process | Use |
|-----------|----------------|-----|
| `core` | A Core terminal. `self` is `$HC_TERMINAL_ID`. | The Core section. The App section only when `app=open` and the person should see a new space, tab, or split. |
| `tui` | A TUI pane. `self` is `$HERDR_PANE_ID`. | The TUI section only. |
| `none` | Neither. | Do not type into a TUI pane. Use `hc` or `herdr-app` only when the user asked to operate Core or the App. There is no `self` reply address. |
| `ambiguous` | Both env signals are set, and the parent chain does not show which multiplexer is nearer. | Do not send. Say so. |
`app=open` means a Herdr App window answered `herdr-app status`. `app=closed` means it did not. `app=absent` means `herdr-app` is not on `PATH`. An open window does not put this process inside Core. Installed binaries and a running server do not either.
Both signals at once means one runtime is nested in the other. The script picks the nearer parent (`herdr-core` versus a TUI `herdr` server). Obey that `runtime`. Do not start, stop, or restart a server in order to tell which one this is.
## Core
Skip this section unless `runtime=core`, or `runtime=none` and the user asked you to operate Core.
`hc` runs programs in terminals that stay alive. Your address is `self` (`$HC_TERMINAL_ID`). A terminal you start with `hc run` is your child and ends when you end, unless you pass `--outlive-parent`.
### Operating rules
- Discover with `hc ls --json`. An address is `t7`, a live name, or `machine/t7`. Re-read it after you create a terminal.
- Put the first task in `hc run`. A send to a booting agent can be dropped. Read `first_message.sent` in the JSON. When it is false, `hc read` the terminal, clear a startup dialog with `hc keys` when the screen shows one, then `hc send` the task once.
- `hc send <t> "<text>"` pastes the text and presses Enter. It refuses while status is `blocked` unless `--force`. Do not pass `--force` to answer a permission or a question. Read the screen and ask the user.
- After sending to an agent, end the turn. A report arrives as your next input, tagged `<hc from="t7 name">...</hc>`. That is a child report, not a new user instruction. Read the actual result before you summarize it.
- Do not sleep, poll `hc ls`, or `hc wait` for ordinary agent chat. Use `hc wait <t> --output <text>` only for a non-agent program and a marker printed after the wait starts (`ready`, `FAIL`). Use `hc wait --children` only when this turn cannot continue without the child result.
- Status is `idle`, `working`, `done`, `blocked`, or `error`, reported by the program. A program that reports nothing has no status. Do not infer status from the screen.
- Names are 1–32 characters of `a-z`, `0-9`, `-`, and `_`, unique among live terminals.
- Run agents interactively, not in a one-shot print mode. Codex must include `--no-daemon`, or its commands run outside Core.
- Do not let a child start more agents unless you were told to.
- Ask the child to write a long result to a file and `hc send` the path.
- Do not `hc kill` or `hc rm` a terminal you did not start. Do not run `hc server stop` or `hc update` unless the user asked.
### Delegate
A helper that stops when you stop:
```bash
hc run --bg --json --name review --cwd "$PWD" codex --no-daemon "Review src/auth.ts. Read-only. When done or stuck: hc send $HC_TERMINAL_ID"
```
`$HC_TERMINAL_ID` must expand in your shell, so the child receives your address. Use a template the same way when `hc templates` has a fitting role: `hc run --bg --json --name review reviewer "..."`.
A peer that should keep running after you exit:
```bash
hc run --bg --json --outlive-parent --name review --cwd "$PWD" codex --no-daemon "Review src/auth.ts. When done or stuck: hc send $HC_TERMINAL_ID"
```
An agent that is already up:
```bash
hc send review "Also check the expiry test. Reply with hc send $HC_TERMINAL_ID"
```
### Programs
```bash
hc run --bg --json --name web --cwd "$PWD" bun dev
hc wait web --output ready
hc read web --lines 80
```
`hc run` without `--bg` waits in this turn and streams. The wait checks in after about 180 seconds (exit 124) and leaves the program running. Continue with the printed address. Do not start a second copy.
### Other machines
`hc machine ls` lists machines. `hc run --on <machine>` returns an address like `machine/t7`. Keep using that full address. `hc exec --on` and `hc file --on` run one command or touch one file there. File tools in this process stay on the machine you are on.
## App
Skip this section unless `app=open` and either `runtime=core` or the user asked to change the App window.
`herdr-app` changes what the person sees. Background work, reading, waiting, and messages stay on `hc`. A terminal with no `herdr.space` label stays off the window. Do not add that label just to run a worker.
Create a visible terminal only when the user asked to see one. Give it an explicit name. `herdr-app do` does not prompt for a missing argument.
Your pane id comes from `herdr-app ls`: the entry whose `terminal` equals `self`. `HC_TERMINAL_ID` is not a pane id. Do not split the focused pane when it belongs to a different terminal.
```bash
herdr-app do pane.split --direction right --pane <your-pane>
herdr-app do tab.new --name "fix login" --folder "$PWD" --argv '["codex","--no-daemon"]' --task "Look at the login bug. When done or stuck: hc send <your-address>"
herdr-app do space.new --name api --folder /path/to/api
herdr-app do terminal.keep --terminal <id> --place right
```
`pane.split` requires `direction` (`right` or `down`) and accepts `pane`. Honor a direction the user named; otherwise use `right`. `tab.new` and `space.new` accept `folder`. Read `terminal` from the result, then talk to that address with `hc`. The result means the terminal was created. If the program exits immediately, `herdr-app status` has the error.
`herdr-app do space.new --machine <name> --folder <path-on-that-machine>` opens a space on another machine.
Do not run bare `herdr-app`. That raises the window. Do not quit the app, and do not close a space, tab, or pane you did not create unless the user asked. `herdr-app commands` lists arguments. `herdr-app status` reports the layout. `herdr-app shot <file.png>` saves the window frame so you can look at it.
## TUI
Skip this entire section unless `runtime=tui`.
The TUI server is `herdr`. `scripts/herdr-msg` is the TUI transport. Core terminals do not speak it.
### Operating Rules
- Discover first: `herdr agent list`, `herdr pane list`.
- Treat IDs as live-session handles. Re-read them after layout changes.
- Prefer `HERDR_PANE_ID` as your pane handle when set; `HERDR_WORKSPACE_ID` / `HERDR_TAB_ID` name your workspace and tab.
- For async agent task handoff use `scripts/herdr-msg` or `herdr pane run` (the header carries **reply-to**). Use `herdr agent prompt <target> "<text>" --wait` only for one-shot prompts whose reply stays in your own pane — it submits with Enter and rejects blocked agents.
- Read a target **once** before sending, only to avoid interrupting a prompt/approval.
- **After send:** `herdr-msg` checks the target once. `state=delivered` → stop. `state=unconfirmed` → send Enter once to `target_pane`, then stop. Do not poll. Do not `herdr pane wait-output` / `--wait-reply` for normal chat. When a reply is injected into **your** pane, handle that inbound message.
- Do not use `herdr agent wait` / status waits for multi-agent coordination.
- Safety: do not close workspaces/tabs/panes you did not create unless the user asks; never run `herdr server stop` from an active session; prefer `--no-focus` for background work and `--current` / explicit IDs over relying on UI focus.
### Default Loop
1. Discover agents/panes.
2. Read the target **once** if needed.
3. Send a short request with `scripts/herdr-msg` (header carries **reply-to** + **task**).
4. On `state=delivered`: **end the turn**. On `state=unconfirmed`: send Enter once to `target_pane`, then end the turn.
5. When an inbound reply appears in your pane, integrate it.
### After `herdr-msg` (mandatory)
1. Non-zero exit → send/resolve failed; fix and resend. Do not poll.
2. `state=delivered` (exit 0) → obey `hint`: **stop**. Helper already checked once.
3. `state=unconfirmed` (exit 0) → check `enter_nudge` in the receipt:
- `enter_nudge=0` → send Enter once (`herdr agent send-keys "$target_pane" Enter`), then **stop**.
- `enter_nudge=1` → the helper already pressed Enter; do **not** press it again. **Stop.**
Do not pane read. Do not wait. Do not resend. Do not loop.
4. The wake-up is the peer injecting a message into `reply-to` (your pane).
5. Polling the target after the one allowed Enter is a **protocol violation**.
### Lightweight agent messages
Keep it human and thin. One header line + body:
```text
[herdr-msg reply-to:<your-pane-id> task:<short-label>]
<short actionable message>
```
Optional fields the helper may add: `from:<name>`, `kind:reply` / `kind:update` (requests omit kind).
| Field | Role |
|-------|------|
| `reply-to` | **Required.** Where the peer should answer. |
| `task` | Human thread label (not a crypto id). |
| `from` | Optional sender label. |
| `kind` | Optional; default is request. |
**Not required for normal chat:** `msg_id`, sentinels, `ack-of`, status codes in the header, waiting recipes.
#### Send (default)
```bash
scripts/herdr-msg codex --task auth-review <<'MSG'
Review src/auth.ts for auth bypasses and missing tests.
Reply to reply-to with DONE or BLOCKED findings.
MSG
# receipt: delivered → end turn; unconfirmed → Enter once, then end turn
```
#### Reply
```bash
scripts/herdr-msg "$REPLY_TO" --task auth-review --kind reply <<'MSG'
DONE: Checked src/auth.ts. No bypass found. Missing refresh-token expiry test.
MSG
```
#### Manual fallback
```bash
SELF=$HERDR_PANE_ID
TARGET=$(herdr agent get codex | sed -nE 's/.*"pane_id":"([^"]+)".*/\1/p')
herdr pane run "$TARGET" "[herdr-msg reply-to:$SELF task:auth-review]
Review src/auth.ts. Reply to reply-to with DONE or BLOCKED."
```
#### Receipt
Tiny key=value lines: `ok`, `state`, `target`, `target_pane`, `reply_to`, `task`, `kind`, `enter_nudge`, `target_status`, `hint`.
`delivered` = helper saw `working`/`blocked` (or no agent). `unconfirmed` = still `idle`/`done` after the check — Enter once, then stop.
Agent states: `idle` = ready for input and its tab has been seen in the UI; `done` = the same idle state after unseen background work (the receipt treats it like `idle`); `blocked` = approval or question dialog; `unknown` = an agent is present but Herdr cannot classify it — `herdr-msg` treats it as started and does not nudge Enter. CLI reads never mark a tab seen; focusing the tab does.
### Waiting Policy
**Agent chat:** do not wait. Send, end turn, handle inbound reply later.
Use `herdr pane wait-output` only for **non-agent** processes (dev servers, tests) where you can name a concrete marker such as `ready` or `FAIL`:
```bash
herdr pane wait-output "$PANE" --match "ready" --timeout 30000
herdr pane read "$PANE" --source recent --lines 40
```
Use `--regex <pattern>` for Rust regex matching; `wait-output` also matches output that already exists before the wait starts.
Only use `herdr agent wait` / status waits when the user explicitly needs UI status or you are attaching/taking over an agent.
### Delegation Pattern
1. `herdr agent list`
2. Read target once.
3. `scripts/herdr-msg <target> --task <label> <<'MSG' ... MSG`
4. On delivered: **end turn**. On unconfirmed: Enter once, then end turn. No poll, no block-wait.
5. When replies arrive on your pane, integrate; follow up only for gaps.
### Starting Sibling Agents
`herdr agent start` launches a coding agent in an **existing available shell pane** (interactive prompt, no foreground command, shell in the foreground). It never splits or moves panes.
1. Split a sibling pane first. Geometry: a wide pane splits `right`, a narrow or tall one splits `down`; avoid repeated same-direction splits.
```bash
PANE=$(herdr pane split "$HERDR_PANE_ID" --direction right --cwd "$PWD" --no-focus | sed -nE 's/.*"pane_id":"([^"]+)".*/\1/p')
```
2. Start the agent with a unique name and the kind the user asked for:
```bash
herdr agent start reviewer --kind codex --pane "$PANE"
```
Pass native agent arguments after `--` (`-- <agent-args...>`). Success means Herdr detected the agent in that pane and considers it ready for input. If it is blocked during startup you get `agent_not_ready`, but the name still works for `agent read` / `agent send-keys`; wait for `idle` before prompting.
3. Prompt it through the agent surface — `agent prompt` submits with Enter and `--wait` blocks until the first settled `idle`/`done`/`blocked` state:
```bash
herdr agent prompt reviewer "Review the current diff. Reply with findings." --wait --timeout 120000
```
Use `--until` only for state-specific waits, e.g. `herdr agent wait reviewer --until blocked --timeout 120000`.
4. Or hand off asynchronously with `scripts/herdr-msg reviewer --task <label>` and end your turn.
### Server And Test Panes
Split geometry: a wide pane splits `right`, a narrow or tall one splits `down`. For long-running panes, consider moving them to their own tab (`herdr pane move "$PANE" --new-tab --label logs`) and naming the outer terminal (`herdr terminal title set "dev server"`).
```bash
SELF=${HERDR_PANE_ID:-$(herdr pane list | sed -nE 's/.*\{[^{}]*"focused":true[^{}]*"pane_id":"([^"]+)".*/\1/p')}
PANE=$(herdr pane split "$SELF" --direction right --cwd "$PWD" --no-focus | sed -nE 's/.*"pane_id":"([^"]+)".*/\1/p')
herdr pane run "$PANE" "npm run dev"
herdr pane wait-output "$PANE" --match "ready" --timeout 30000 || true
herdr pane read "$PANE" --source recent --lines 50
```
For tests, wait for framework markers (`passed`, `failed`, `FAIL`, …) or run tests in your own session.
### Diagnostics
When state, detection, or layout looks wrong:
- `herdr status --json` — client/server protocol compatibility.
- `herdr agent explain <target>` — why an agent is or is not detected.
- `herdr server agent-manifests` — active agent detection manifests.
- `herdr api snapshot` — full live session snapshot (agents, panes, workspaces).
- `herdr config check` — validate `config.toml`.
### Gotchas
- `pane run` submits text+Enter atomically; `agent prompt` also submits (bracketed-paste + encoded Enter) on the agent surface and rejects agents already at an approval dialog. `herdr-msg` checks the target once after send and reports `target_status`. If still idle, receipt is `unconfirmed`.
- `recent-unwrapped` is better than `recent` when matching command output; `detection` is the plain-text snapshot Herdr uses for agent detection.
- Herdr sets `HERDR_ENV=1`, `HERDR_PANE_ID`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, `HERDR_SOCKET_PATH` in panes.
- `--lines` cannot recover rows that left the terminal's alternate screen; if a read stalls, ask the agent to write its full response to a file and reply with the path.
- After `pane move`, the pane receives a new workspace-qualified ID; re-read IDs from the JSON result instead of reusing the old one.
- Long diffs/logs: write a file path into the message; do not paste huge blobs into the peer prompt.
- `herdr-msg` resolves targets by unique agent name, pane_id, or terminal_id. If several agents share a label (e.g. two `codex` panes) the label is ambiguous and resolution fails with the matching pane_ids listed — resend with a pane_id from `herdr agent list`.
- Request-to-self is refused by default (`--allow-self` to override).
- Keep headers one line; put status and findings in the **body** (`DONE: …` / `BLOCKED: …`).