# Tools and lifecycle reference

This page is the detailed reference for the tools, commands, and run lifecycle of `pi-herdr-agents`. For installation and everyday use, see the [README](../README.md). Configuration keys are in [Configuration](configuration.md), and role authoring is in [Roles and custom agents](roles.md).

## What's Included

### Extensions

<!-- generated:tools:start -->
<!-- Generated by npm run docs:tools; edit the schema, not this block. -->

**Subagents** — 10 parent-session tools + 3 commands, plus 2 child-only tools:

| Tool | Sessions | Description |
| --- | --- | --- |
| `subagent` | parent, child | Launch a Pi subagent in a terminal pane, or in a new managed Git worktree when `worktree` is set. |
| `subagent_cancel` | parent, child | Cancel a running non-persistent subagent, including an interrupted one. |
| `subagent_interrupt` | parent, child | Send Escape to a running subagent's active turn. |
| `subagent_resume` | parent, child | Resume a saved subagent session in a new terminal pane, to continue a cancelled run or send follow-up work. |
| `subagent_send` | parent, child | Deliver one follow-up task to an idle persistent specialist. |
| `subagent_stop` | parent, child | Stop a persistent specialist after its active task finishes. |
| `subagents_list` | parent, child | List subagent role definitions, live persistent specialists, and the supervision mode. |
| `subagents_write_task_models` | parent only | Validate and atomically replace models.tasks and models.tasksMeta in the durable Pi agent config, preserving unrelated settings. |
| `worktree_list` | parent only | List managed worktrees, including orphans from other sessions. |
| `worktree_remove` | parent only | Remove one managed worktree by exact path, branch, or workspace ID, only when the user asks. |
<!-- generated:tools:end -->

| Skill | Description |
| ----- | ----------- |
| `pi-herdr-agents` | Operating guide for this host: launching, supervising, interrupting, cancelling and resuming children, worktrees, persistent specialists, model routing and configuration. Loaded by agents on demand; see the [operating skill](../skills/pi-herdr-agents/SKILL.md) |

<!-- generated:child-tools:start -->
<!-- Generated by npm run docs:tools; edit the schema, not this block. -->

| Pi child-only tool | Description |
| --- | --- |
| `caller_ping` | Send a help request to the parent agent and exit this session. |
| `subagent_done` | Call this tool when you have completed your task. |
<!-- generated:child-tools:end -->

<!-- generated:commands:start -->
<!-- Generated by npm run docs:tools; edit the schema, not this block. -->

| Command | Usage | Description |
| --- | --- | --- |
| `/subagent` | `/subagent <agent> <task>`<br>`/subagent list` | Spawn a subagent: /subagent \<agent> \<task>; list agents: /subagent list |
| `/subagents-init` | `/subagents-init [preferences]` | Draft task-category model preferences from the live registry; optional arguments set ranking preferences |
| `/worktree` | `/worktree <name> [task]`<br>`/worktree list`<br>`/worktree remove <target>` | Fork into a worktree, list retained worktrees, or explicitly remove one |
<!-- generated:commands:end -->

## Tool loading

The nine routine tools (`subagent`, `subagent_resume`, `subagent_interrupt`, `subagent_cancel`, `subagent_send`, `subagent_stop`, `subagents_list`, `worktree_list`, and `worktree_remove`) register with Pi's `deferred` [tool exposure](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#tool-exposure). Their schemas stay out of model requests until Pi's built-in `tool_search` loads them. The same applies to the `subagent` guidelines, which include the [model catalog](configuration.md#model-catalog-in-the-prompt). When a session starts, the host activates `tool_search` and leaves every other active or restored tool as it was. A search such as `tool_search({ query: "subagent" })` or `tool_search({ query: "worktree" })` declares the matching tools for the next model call. Pi records loaded tools in the transcript, so they stay declared after `/reload`, and after a resume on that branch.

Pi's tool policy decides which host tools exist, and the host never adds a tool the policy removed:

- A `--tools` allowlist keeps only the tools it names. Pi declares the named host tools at once, and `tool_search` can load only tools the allowlist kept.
- `--exclude-tools`, and in a child the role's `deny-tools` or `spawning: false`, remove tools from the session. Search cannot load a removed tool.
- When Pi's own `tool_search` is unavailable or turned off, the host activates every routine tool Pi kept when the session starts, as earlier releases did. This happens with `--no-extensions`, with `-builtin:tool-search` in the `extensions` setting, with `"-tool_search"` in the `defaultTools` setting, with `--exclude-tools tool_search`, with an allowlist that omits `tool_search`, and when another extension registers a tool named `tool_search`. An SDK session uses search only when it loads the tool-search extension as the built-in `builtin:tool-search`; see [Pi's SDK documentation](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/sdk.md).
- Pi does not report the per-invocation `--tools -tool_search` to extensions, so the host still activates search under it. Use `--exclude-tools tool_search` to turn search off for one run.
- The host recognizes Pi's search by the source Pi reports for it (`builtin:tool-search`), never by name alone, and activates only tools it registered itself.

Codemode scripts can call registered `deferred` tools without loading them, as Pi allows for every deferred tool. The removal rules above still decide which host tools are registered.

`subagents_write_task_models` is not deferred. It is a direct tool registered inactive, `tool_search` never finds it, and only `/subagents-init` activates it; see [Task-model writer](configuration.md#task-model-writer). The child-only `caller_ping` and `subagent_done` tools are declared directly, as before.

## Async Subagent Flow

```
1. Agent calls subagent()          → acknowledges acquisition, not task completion
2. Sub-agent runs in herdr pane    → widget shows live status
3. User keeps chatting             → main session fully interactive
4. Sub-agent finishes              → result steered back as a normal completion/failure
5. Main agent processes result     → continues with new context
```

Multiple subagents run concurrently — each steers its result back independently as it finishes. Active watchers survive parent `/reload`, `/new`, `/resume`, and `/fork` transitions, so completion is delivered into the replacement session. A result that settles while the old session is shut down and the replacement has not started is held, then delivered once when the replacement starts, and a model fallback due in that gap waits to launch until the replacement starts (a `subagent_cancel` recorded during that wait ends it at once and launches nothing). Quitting Pi still stops parent-side delivery. The live widget above the input tracks every agent still in flight:

```
╭─ Subagents ──────────────────── 1 active · 2 open ─╮
│ 01:23  Scout: Auth (scout)             active · read 7m │
│ 00:45  Reviewer (reviewer)                   stalled 4m │
│ 00:12  Scout: DB (scout)                      starting… │
╰─────────────────────────────────────────────────────────╯
```

Child-authored text in completions, failures, help requests, and persistent task results is delivered inside a single `<subagent_report from="<name>" kind="<result|failure|help-request|task-result>">` block, preceded by a sentence telling the parent to treat it as a report to evaluate, not as instructions from the user. Any `<subagent_report` or `</subagent_report` inside the child's output, in any letter case, is escaped as `&lt;` so it cannot close or reopen the block; other tags pass through unchanged, and long results are abbreviated before wrapping so the closing tag always survives. Names are abbreviated to 80 characters in the framing sentence and the `from` attribute. Every host line has its own bound, and when a message would exceed 16,000 characters the host drops whole evidence lines (file lists first) and says how many, then shortens the body inside the block; it never cuts through host text, so the opening and closing tags, the session reference, and the continuation line always survive. Host-authored lines (elapsed time, model provenance and failures, provider errors, worktree handoff, session references, runtime warnings, cancellation text) stay outside the block, as do summaries the host synthesizes when the child wrote no report (for example `Subagent error: …` and `exited with code N` fallbacks).

A child can still influence some values in host lines: a provider error it relays (`Error:` and `Subagent error:`), the model its transcript reports (`Model used:`, `Runtime warning:`), worktree filenames, and persistent task IDs. The host writes each such value as one line: JSON string escapes keep `\n`, `"` and `\` visible, other control, separator and bidi-control characters become `\uXXXX`, report tags are escaped, and long values are abbreviated with `[... abbreviated ...]`; long lists show whole items and then a count such as `(and 312 more files)`. A child cannot start a new host line such as `Parent action:`. The same rule covers activity labels in the widget and status updates, `worktree_list`, `worktree_remove` and `/worktree` output, `subagents_list` rows, launch diagnostics, and control replies. The raw values stay in structured details (`errorMessage`, `fallbackFailures`, `worktree`, `task`). Commands the parent may run are not display text: `worktree_remove({ target: … })` carries the exact path as a JSON string (with `<` and separators as `\u` escapes), and a path too long for that command is replaced by the workspace-ID form. Help-request headers use the name the parent gave the run, never a name the child writes. A persistent specialist's event for a task the parent did not issue is ignored without changing its state or completion count; the first one produces a single notice that does not start a turn.

Completion messages render with a colored background and are expandable with `Ctrl+O`. In the expanded view of a result or a help request, the child's report sits between a `┌ Child report from <name> (<kind>)` rule and a `└ end of child report` rule. The child's text is split on every line separator (CR, LF, NEL, VT, FF, U+2028, U+2029) and its terminal controls such as ESC are shown as `\uXXXX` before each line is prefixed with `│`, so host evidence below it cannot be mistaken for child text. Results larger than 16,000 characters are abbreviated in the parent context while preserving their beginning, conclusion, and session path; the complete result remains in the child session. The extension includes that bounded result and a continuation instruction directly in the single custom `subagent_result` message that triggers or steers Pi, avoiding empty turns caused by a separate context-free wake-up. The renderer uses the unadorned bounded result from structured details. Completed rows are removed from the widget as soon as their result is delivered or suppressed.

### In-progress status updates

The widget projects each sub-agent from a **process + turn lifecycle**:

- **Herdr pane inspection** is the coarse authority for whether the child process is present and whether Herdr reports it as idle, working, blocked, or done.
- **Child activity snapshots** enrich the label with Pi-only detail (tool name, streaming, etc.) when available.
- Session JSONL is still used for transcript, resume, lineage, and result extraction — not for liveness.

Projected labels include:

- `starting` — launched; pane/activity confirmation is still settling
- `active` — processing work (agent turn, provider request, streaming, or tool execution)
- `blocked` — Herdr reports the child as blocked
- `waiting` — turn finished; the process is intentionally open for more input or another stage
- `interrupted` — the current turn was cancelled (Escape / `subagent_interrupt`); the process stays open and is **not** treated as active processing
- `stalled` — pane inspection is unhealthy long enough that the parent can no longer trust the run
- `running` — fallback when only coarse process presence is known
- `finalizing` — completion was observed and delivery is in progress; the process elapsed timer freezes here
- `cancelling…` / `cancel unconfirmed` — a `subagent_cancel` is terminating the run, or its termination could not be confirmed and the run stays live

The widget header counts **active** vs **open**:

- **active** — `active`, `starting`, `running`, or `blocked`
- **open** — everything else still tracked (`waiting`, `interrupted`, `stalled`, `finalizing`, …)

When `activeCount === 0` (every tracked row is open), the border uses an amber accent. Process elapsed time (`MM:SS` on the left) freezes when the process reaches finalizing/completed/failed. Interrupt does **not** freeze that process clock; the interrupted state shows its own duration on the right while the process remains open.

A fixed internal watchdog marks a run as `stalled` when pane inspection fails or the pane disappears without a completion sidecar; valid long-running `active` or `waiting` states do not become `stalled` just because time passes. When a run enters `stalled` or recovers from it, the parent agent receives a steer message so it can react. All other status transitions stay in the widget only.

**Interactive subagents stay silent.** Long-running user-driven subagents (for example, an interactive planning role or a bare `interactive: true` fork) do not wake the parent session on `stalled`/`recovered` transitions — the user is working directly in the subagent's pane, and a steer message there would just burn an orchestrator turn on a no-op "still waiting" ping. The widget still updates normally, and activity snapshots are still recorded/classified regardless of the `interactive` setting. By default, agents with `auto-exit: true` are treated as autonomous and get stall pings; agents without it are treated as interactive and stay quiet. Override per-agent with `interactive: true|false` in frontmatter, or per-spawn with `interactive: true|false` on the tool call.

### Children that launch children

An `auto-exit` child that launches its own subagents stays open until each of them has delivered its result, help request, or cancellation to it. The child can end its turn right after launching ("waiting for the children"): it does not exit at that settlement. Each delivery is a steer that starts a new turn, and when that turn settles with nothing left outstanding the child exits as usual, so its own result reflects what the grandchildren returned. A child that launches nothing exits at its first settlement, as before.

- A grandchild that fails, is cancelled, or crashes still delivers a terminal result, so it never blocks the exit once that result is delivered.
- Persistent specialists are long-lived by design and never hold the exit.
- A run that was aborted (Escape or `subagent_interrupt`) stays open, and a run that settles on a provider error still exits and reports the error, whatever is outstanding.
- A grandchild that hangs without ever delivering keeps the child open. The child sees the usual stall advisories and can `subagent_cancel` it; the parent can also cancel or interrupt the child itself.
- Children that are not `auto-exit` call `subagent_done` themselves and are unaffected.

## Spawning Subagents

Examples that set `agent` assume a role pack or a project/global definition
supplies that role; this package ships none.

```typescript
// Explicit fast-tier runtime for bounded reconnaissance
subagent({ name: "Scout", agent: "scout", model: "<provider>/<fast-tier-id>", thinking: "low", task: "Analyze the codebase..." });

// Force a full-context fork for this spawn
subagent({ name: "Fix", fork: true, model: "<provider>/<mid-tier-id>", thinking: "medium", task: "Fix the bug where..." });

// Explicit frontier-tier runtime for architecture work
subagent({ name: "Planner", agent: "planner", model: "<provider>/<frontier-tier-id>", thinking: "high", task: "Work through the design with me" });

// Explicit mid-tier runtime with a custom working directory
subagent({ name: "Designer", agent: "game-designer", model: "<provider>/<mid-tier-id>", thinking: "medium", cwd: "agents/game-designer", task: "..." });

// Isolated ticket branch in a Herdr-managed Git worktree
subagent({
  name: "Ticket 123",
  agent: "worker",
  model: "<provider>/<mid-tier-id>",
  thinking: "medium",
  worktree: { branch: "ticket/123", base: "main" },
  task: "Implement ticket 123, test it, and commit the result",
});
```

### Parameters

<!-- generated:subagent-params:start -->
<!-- Generated by npm run docs:tools; edit the schema, not this block. -->

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Short label shown in the widget and pane title. |
| `task` | string | yes | Task/prompt for the sub-agent |
| `agent` | string | no | Role definition to load defaults from (see subagents_list). Omit for a bare agent. |
| `systemPrompt` | string | no | Role/system-prompt text for a bare spawn. Named agents keep their definition body. |
| `model` | string | no | Exact authenticated provider/model-id, a comma-separated fallback list, or task:\<category> as the whole value. Worktree launches take one model: a task: category uses its first candidate, and fallback lists are rejected. For ordinary review, prefer a different authenticated model family. When no other authenticated model family is available, ordinary review may use a same-family reviewer in a fresh standalone session. Disclose that this review is context-isolated, not cross-family independent. Cross-family verification must not use this fallback. |
| `thinking` | `"off"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | no | Pi thinking level, within the chosen model's supported range. Use low for mechanical work, medium for ordinary work, and high+ for hard work. |
| `skills` | string | no | Comma-separated skills (overrides agent default) |
| `tools` | string | no | Comma-separated tools (overrides agent default) |
| `cwd` | string | no | Working directory. With `worktree`, the source repository; the child then starts at the new worktree root. |
| `worktree` | object \| null | no | Run in a new managed Git worktree. Omit or pass null for a plain pane in cwd. |
| `worktree.branch` | string | yes | Unique new branch name. |
| `worktree.base` | string | no | Git revision to branch from. Defaults to the committed HEAD. |
| `fork` | boolean | no | `true` copies this conversation into the child; `false` starts it fresh. Omit to use the role's session-mode. |
| `persistent` | boolean | no | Keep the child alive between tasks. Send later tasks with subagent_send. |
| `accept` | object | no | Optional acceptance check. The result reports `Check: \<verdict>`: accepted, rejected, error, skipped, or none. Only for auto-exit, non-persistent children. |
| `accept.run` | string | yes | Shell command run with `sh -c`; exit 0 passes. The child runs it and retries on failure. The host then reruns it in the child's checkout, and that run decides the verdict. |
| `accept.maxRetries` | integer | no | Extra child turns after a failed check. Default 1. |
| `accept.timeoutSec` | integer | no | Per-run timeout in seconds. Default 600. A timeout gives verdict `error`. |
| `accept.paths` | string[] | no | Check files, such as tests. The result lists any the child changed, without affecting the verdict. |
| `interactive` | boolean | no | True when the user drives the child in its own pane. Interactive children do not wake the parent on stall or recovery. Defaults to the role's `interactive` setting, else the opposite of `auto-exit`. |
<!-- generated:subagent-params:end -->

Defaults and behavior the schema does not carry:

- `persistent` defaults to `false`.
- `interactive` defaults to the agent's `interactive` frontmatter, otherwise the inverse of `auto-exit`.
- `model` resolves tool argument → agent frontmatter → per-agent config → global config → parent. `task:<category>` routing is tool-only, and worktrees use its first authenticated candidate.
- `thinking` defaults to the role's thinking level when set, otherwise the parent's.
- `cwd` is the source repository when `worktree` is set (see [Role Folders](roles.md#role-folders)); `worktree.base` defaults to committed `HEAD`.
- `fork`: `true` forces fork, `false` forces standalone. Omit to inherit the agent `session-mode` frontmatter.
- `accept` defaults: `maxRetries` 1 (at most 5), `timeoutSec` 600 (at most 3,600), `paths` none. It is refused before launch for a persistent or non-auto-exit child. See [Accept checks](#accept-checks).

A bare spawn's `systemPrompt` is not passed to Pi as a system prompt. The host
prepends it as a role block to the child's first message, which is delivered
through a task artifact file referenced with `@path`. A full-context fork
(`fork: true`) receives only the raw task, so its `systemPrompt` is dropped. Set
`fork: false` when a bare child must receive reference or role text through
`systemPrompt`.

### Naming coordinated children

Before launching a new group, choose a short task slug and label each new child
`<task>-<role>[-n]`, such as `login-api` or `login-test2`. Roles are `plan`,
`research`, `ui`, `api`, `build`, `test`, `review`, `browser`, `security`,
`perf`, and `merge`. Leave existing labels unchanged. After the final launch,
print a markdown table with the header row `| name | agent kind | role | model |
worktree |`, a separator row, and one row per child (worktree `none` when there
is none), and use each name in prompts, handoffs, and results. Set
[`guidelines.launchRoster`](configuration.md#launch-roster) to `false` to drop
the table instruction while keeping the rest of this naming guidance.

### Isolated worktree runs

Use one worktree per parallel independent writing task; a single or sequential writer can work in the parent checkout, and read-only agents use ordinary panes. Omit `worktree` for an ordinary pane; clients whose generated tool schema requires every property may send `worktree: null` with the same effect. `cwd` selects the source Git repository, `branch` must be unique, and `base` is resolved to an exact commit before creation. If `cwd` is a linked checkout, Herdr provisioning uses the principal checkout while the requested checkout supplies the base SHA and manifest provenance. A successful launch from that linked checkout does not itself authorize cleanup there: cleanup checks the canonical principal/source repository under the invoking parent session's cwd, not `manifest.sourceCwd` or shared Git identity. If cleanup is needed, start the parent Pi session rooted at the principal checkout or an ancestor containing it, then use the normal explicit cleanup flow; changing directories inside an existing Pi session does not change its session cwd. If `base` is omitted, the source checkout's committed `HEAD` is used. Parent-checkout changes that have not been committed are not copied.

Choose a worktree from the task, not from a role name: the extension emits no
role-specific worktree warnings. Read-only scouting and review normally use an
ordinary pane; to inspect or review an existing worker result, start an ordinary
child in that retained worktree path. Do not infer that a role cannot write
because its `tools` omit `write` or `edit`: a `read,bash` allowlist is not an
enforced read-only boundary because shell commands can mutate files. Report-only
roles must restrict Bash to safe inspection and avoid artifact-generating
verification in the reviewed checkout. Herdr worktree workspaces persist until
explicitly removed.

The child starts at the returned worktree root. Tell writing agents to test and commit when you want a commit-based handoff, and tell them not to push, merge, switch branches, or remove the worktree. The parent owns review and integration.

Successful, failed, and help-requesting worktree runs retain their workspace and root shell. A reviewer's disposable pane can close without closing that root, tab, or checkout. Completion includes the worktree path, Herdr workspace, branch, base/head SHAs, commits ahead, changed and untracked files, and clean/dirty/conflicted state. Here, `clean` means no uncommitted files; the branch may still contain commits. If Git inspection fails, state is reported as unknown rather than guessed.

An ownership manifest is written under the parent session's `artifacts/<session-id>/worktree-runs/` directory before Herdr creates resources. V1 does not automatically recover watchers after a full process restart, and `subagent_resume` does not reattach the managed worktree lifecycle.

The extension does **not** push, create a PR, merge, cherry-pick, or remove the worktree or branch automatically. For task selection, lifecycle states, review commands, failure recovery, and safe cleanup, read [Worktree subagents](worktree-subagents.md). The [research report](research/worktree-subagent-orchestration.md) records the rationale and deferred roadmap.

### Launch deadlines and uncertainty

Launch, pane inspection, and Git handoff subprocesses run asynchronously with
finite budgets. The acknowledgement follows provisioning and command dispatch;
it does not wait for the child's task. These are separate operation limits, not
an end-to-end launch deadline.

| Operation | Budget |
| --- | --- |
| Ordinary Herdr request | 10 seconds |
| Pane placement, including queue wait | 30 seconds |
| Worktree acquisition, including advisory snapshots and optional grouping | 120 seconds |
| Recovery inspection after uncertain worktree creation | 6 seconds total |
| Each primary-workspace snapshot | 3 seconds |
| Cosmetic pane or tab rename | 1 second |
| Shell or Pi readiness | 10 seconds |
| Pane-absence confirmation | 5 seconds |
| Git base resolution | 10 seconds |
| Git principal-checkout resolution | 15 seconds |
| Complete Git handoff inspection | 15 seconds |

Herdr and Git clients capture at most 1 MiB per output stream. Failed, timed-out,
oversized, or malformed Git inspection produces unknown handoff fields, not a
clean-state claim. Readiness queries consume the remaining readiness budget and
cannot establish success after it expires.

A definite failure before command dispatch, such as an unwritable launch script,
is a normal launch failure. Ordinary rollback must confirm pane absence before
model fallback can proceed. Worktree launches retain the checkout and report the
failure without supervising a child that never started.

A timed-out Herdr client does not prove that the server cancelled its operation.
Uncertain creation or dispatch never advances the model fallback list. A launch
with no identified owner fails with the observed resource identities and any
retained manifest. The host never adopts or closes a resource by its label.

When a real initialized owner exists but dispatch or rollback is uncertain, the
acknowledgement carries `status: "uncertain"` and `launchFailure`. The host keeps
that owner controlled and supervised until termination is confirmed. It attempts
owned termination, retains unconfirmed owners, and delivers a result after exit
confirmation. If both supervision and termination probes keep failing, retry
waits increase from 1 second to a 30-second cap. They never trigger model
fallback or discard the owner. Suppression or confirmed cancellation releases
the retry wait immediately. Worktree roots and caller-provided panes remain retained. A
persistent specialist with an uncertain launch cannot accept follow-up tasks.
Do not blindly retry an uncertain launch. See [recovery](troubleshooting.md#an-acquisition-or-dispatch-is-uncertain).

Explicit cleanup Git commands and macOS process enumeration remain bounded
synchronous operations. Filesystem and Pi SDK operations can also run
synchronously. This change does not make all host I/O nonblocking.

### Accept checks

Pass `accept` to turn a finished run into an accepted or rejected outcome. The host runs the caller's command; it ships no checks and no default command ([ADR-0017](adr/0017-accept-check.md)).

```typescript
subagent({
  name: "fix-parser",
  model: "<provider>/<mid-tier-id>",
  thinking: "medium",
  worktree: { branch: "fix/parser" },
  task: "Fix the date parser and commit",
  accept: { run: "npm test", maxRetries: 1, paths: ["test"] },
});
```

1. **Child loop.** When the child's turn ends normally, it runs `sh -c <run>` in its own checkout. On a failure with retries left, the last 4,000 characters of output go back to it as a check-failure message, and it works one more turn. With the default `maxRetries: 1` the child runs the check at most twice. The child reads the spec from a file named by `PI_SUBAGENT_ACCEPT_FILE`; the command is never in the launch environment or script.
2. **Parent re-check.** After the child exits, the parent runs the check once more, and that run alone decides the verdict. The child's own report is evidence. It runs only for a completed, uncancelled, exit-0 run that did not ask for help, in the worktree for a worktree child and in the child's `cwd` otherwise. The check's environment drops every `PI_SUBAGENT_*` variable. A timeout sends SIGTERM to the check's process group, then SIGKILL after 5 seconds. A check never outlives the session that runs it: the child's shutdown and the parent's quit send SIGTERM, then SIGKILL after 1 second, and a process that exits kills any check group it still runs. Callers cannot skip this re-check; use `timeoutSec` for a slow suite.
3. **Delivery.** Every result and help request carries a `Check:` host line directly under its status line, outside the `subagent_report` block. Unchecked runs read `Check: none`.

| Verdict | Meaning |
| --- | --- |
| `accepted` | The parent re-check exited 0. |
| `rejected` | The parent re-check exited non-zero. |
| `error` | The parent re-check gave no answer: it timed out, was killed by a signal, its directory was missing, or it could not start. |
| `skipped` | A check was requested, but the run was cancelled, asked for help, hit a provider error, or exited non-zero. |
| `none` | No check was requested. |

Example lines: `Check: accepted (2 tries)`, `Check: rejected after 2 tries (exit 1)`, `Check: rejected (child reported a pass; parent re-check exit 1)`, `Check: error (timed out after 600s)`, `Check: skipped (cancelled)`. The line adds `· shared checkout` when the re-check ran in an ordinary child's cwd, which other writers and the user may share, and `· check files changed by the child: …` when a worktree child changed files under `paths` (encoded names, from the handoff's changed and untracked files). A changed check file never changes the verdict; the caller decides what it means. For `rejected` and `error`, the last 1,500 characters of the re-check output follow the report as one encoded host line, `Check output (last 1,500 characters, encoded): …`. Structured details carry `accept` with the verdict, tries, exit code, timeout, the command's SHA-256, the re-check directory, changed check files, and up to 4,000 raw output characters.

A rejected verdict changes neither the sidebar status nor the worktree manifest state, and it never starts a model fallback: a rejected run is a completed negative result. Each fallback attempt is a fresh child with a fresh retry budget. A cancel during the parent re-check stops it and delivers the cancelled result with verdict `skipped`. `subagent_resume` and `/worktree` handoffs carry no check, so they report `Check: none`.

Each model attempt also appends one line to the [outcome ledger](configuration.md#outcome-ledger).

## Persistent specialists

Set `persistent: true` on a `subagent` launch to create one logical specialist with one v1 session generation. Its resolved tools, denied tools, model, thinking level, and optional worktree binding are snapshotted at launch and do not change when work is sent later. `subagents_list` shows each live specialist's logical ID, generation ID, state, completed-task count, and effective policy.

The initial task and each `subagent_send({ id|name, message })` task are dispatched with a task ID. Parent-owned in-process bookkeeping prevents a successful task-result or help notification from being sent again when its ledger acknowledgment fails; the acknowledgment alone is retried, including across `/reload`. This bookkeeping does not survive a full process crash and is not a durable exactly-once messaging guarantee. A specialist accepts one task at a time. Sends while it is working are recorded as `rejected-busy`; no queue is retained. After a task result arrives, it is idle and accepts the next task while its process remains open. Once process exit is observed, sends and stop requests are rejected even while the final Git handoff is pending. A persistent child's `caller_ping` ends its current task with a help request and keeps the session alive. Answer with `subagent_send`: the answer starts a new task, which the child takes up after the run that asked for help finishes.

The child's activity file follows the current task. `subagent_done` or `caller_ping` writes `phase: "done"` for that task and freezes the file against the trailing events of the same run; when the next `subagent_send` task starts, the child records `latestEvent: "input"` with `phase: "active"` and then reflects the task's turns and tools, ending in `done` again. The file schema is unchanged and `sequence` keeps increasing across tasks. A non-persistent child's `done` snapshot stays final.

Use `subagent_stop({ id|name })` to request graceful shutdown. If a task is active, stop becomes `stop-pending` and the task reaches its terminal outcome first. A stop that arrives after the task ended, for example after a help request, aborts the child's leftover run. An uncertain launch's initial task does not hold stop pending. Its bounded exit wait starts immediately. The parent reports `stopped` only after process-exit evidence is confirmed, then closes an ordinary pane it created and releases the name. If confirmation times out, the specialist is `stalled` in an unconfirmed-stop state: `subagent_send` rejects follow-up work while retaining evidence. If publishing the stop directive fails, it likewise enters a retryable failed-stop state rather than remaining permanently stop-pending. Request `subagent_stop` again to publish another directive and make another bounded exit check, or spawn a new specialist. Timeout notices belong to one stop attempt and wait for a live parent API across reload and session-replacement gaps. Notification failures are contained and retried. A new stop attempt, verified process exit, or quitting cancels the old notice, including pending evidence reads and retry timers. If exit is confirmed after a stop timeout or publication failure, the parent receives one terminal stopped update and a stopped ledger entry. A pane or process disappearance without a stop directive produces one facts-only crash notice; persistent sessions cannot be resumed in v1, so spawn a new specialist. There is no automatic restart, replay, or revival.

A persistent completion sidecar records the child's intended outcome before
shutdown. It does not prove process exit. The host retains lifetime ownership
until a verified process identity has exited, the shell has emitted its post-exit
marker, or pane absence confirms termination when no identity is known alive.
A known-live identity prevents a stale marker or missing pane from establishing
exit. Task events can still arrive while shutdown is pending.

A persistent specialist with a worktree holds that lease for its entire lifetime. It cannot be re-bound to another checkout. Otherwise it runs in an ordinary pane.

## Interrupting a running subagent

Use `subagent_interrupt` to cancel the active turn of a running Pi-backed subagent:

```typescript
subagent_interrupt({ id: "abcd1234" });
// or
subagent_interrupt({ name: "Scout" });
```

This sends Escape to the child pane, cancelling the in-progress model turn. The subagent session stays alive — the pane, session file, and background polling all remain intact. After the interrupt, the widget immediately labels the child as `interrupted` (counted as **open**, not active processing). Stale pre-interrupt activity snapshots are ignored so a lagging Herdr/`active` reading cannot overwrite the interrupt. The process elapsed timer keeps running because the pane is still open; only the interrupted-state duration freezes relative to the interrupt request. If the child starts work later, newer observations return it to `active`; completion, failure, and `caller_ping` still flow through normally.

`id` and `name` are each optional, but execution requires one usable target: an exact running ID or an exact, unambiguous display name. When both are supplied, `id` is used. Duplicate names are rejected.

This is a turn-level interrupt, not a method for forcibly terminating a subagent session. To end the run, use `subagent_cancel`.

## Cancelling a running subagent

Use `subagent_cancel` to end one ordinary (non-persistent) managed run, including an interrupted one:

```typescript
subagent_cancel({ id: "abcd1234" });
// or
subagent_cancel({ name: "Scout" });
```

Target resolution matches `subagent_interrupt`: an exact running ID or an exact, unambiguous display name. Persistent specialists are rejected; use `subagent_stop`, whose graceful v1 semantics are unchanged.

The cancel intent is recorded before anything is aborted or killed. From then on the run never advances its model shortlist or starts another execution attempt, even when terminating the pane makes the watcher observe a lost pane, and even when a fallback launch was already in flight. The result reports one status:

| Status | Meaning |
| --- | --- |
| `confirmed` | Termination is confirmed. One cancelled result is delivered automatically. |
| `requested` | A launch or fallback acquisition is still in flight. Its owner is terminated as soon as it is acquired; no later model is tried. |
| `unconfirmed` | Termination failed (the error is reported). The run stays live, owned, and supervised, and nothing is delivered or cleaned up. Call `subagent_cancel` again to retry. |
| `already-terminal` | The run already took a natural result or was retired; nothing was cancelled. |

If a watcher fails while cancellation remains unconfirmed, the host retries
observation of the same owner with bounded backoff. A failed watcher is not exit
evidence and does not trigger another kernel termination attempt. Once real
completion evidence arrives, the producer makes one confirmation attempt. If
termination is still unconfirmed, an explicit cancel retry or shutdown releases
that wait.

Repeated cancels join an in-flight termination and keep the first request time. The parent receives exactly one `subagent_result` whose details carry `error: "cancelled"` and a `cancellation` record (`requestedAt`, `termination`, `confirmedAt`). Its message says the run was cancelled and lists any models already attempted. It is never presented as a provider failure. A child's natural result taken before the cancel stays authoritative. A cancel while a provider error is still eligible for fallback wins and stops that fallback. A cancel while the parent re-runs an [accept check](#accept-checks) stops that check and delivers the cancelled result with `Check: skipped (cancelled)`.

Termination follows surface ownership:

- **Ordinary pane:** the pane is closed. Confirmation is Herdr reporting it absent; closing a Herdr pane terminates its terminal session, but this is not a separate OS process check. Other panes, tabs, and user panes are never closed.
- **Managed worktree:** the retained root pane, workspace, checkout, branch, commits, and manifest are kept. At launch, the child records its own process identity (PID, kernel start time, boot ID, and PID namespace) beside its session; the parent accepts it only while that process is alive in the parent's PID namespace as the Herdr pane shell or a descendant of it. SIGTERM goes only to that identity, re-verified immediately before the signal. Confirmation requires that identity to no longer exist, or the pane to be gone while it is not known alive. Command-line text and Herdr's foreground list are never evidence, because Pi rewrites its process title. A live identity (for example a suspended Pi), or one whose SIGTERM fails while it is still alive, leaves termination `unconfirmed` even if the pane is gone. An identity that was not captured (including on non-Linux hosts), is unreadable, or whose PID now names another process is never signalled and leaves termination `unconfirmed` unless pane absence confirms it. Every Herdr query and capture wait is bounded by the cancel's deadline (5 seconds); an answer that arrives after the deadline is ignored, even if it settles before the timer runs, and never changes a reported outcome. A retry re-checks, and if the launch capture expired it captures the identity again within its own deadline. The manifest becomes `cancelled` only after confirmed termination (a parent shutdown while unconfirmed records the plain shutdown state), and the normal worktree handoff is delivered. No Git cleanup is performed. See [Worktree subagents](worktree-subagents.md).

Do not poll after cancelling; the cancelled result arrives as a steer message. Design rationale: [ADR-0014](adr/0014-operator-cancel-terminal-intent.md).

## caller_ping — Child-to-Parent Help Request

The `caller_ping` tool lets a Pi-backed subagent request help from its parent agent. Ordinary children **exit** and the parent can resume them with `subagent_resume`. Persistent specialists record a help-request outcome, stay alive, and accept a reply through `subagent_send`.

**`caller_ping` parameters:**

- `message` (required): What you need help with

**`subagent_resume` parameters (Pi-backed sessions):**

- `sessionPath` (required): Path to the child session `.jsonl` file
- `name` (optional): Display name for the resumed pane (defaults to `Resume`)
- `message` (optional): Follow-up prompt to send after resuming
- `autoExit` (optional): Whether the resumed session should auto-exit after its next response fully settles. Defaults to `true` for autonomous follow-up work; set `false` when resuming for an interactive handoff.

Each public child stores a session-adjacent versioned launch-policy sidecar. Public resume restores its resolved tool allowlist and denied subagent tools rather than looking up the current role, so later role changes cannot widen a child. An intentionally unrestricted launch remains unrestricted (no `--tools` argument); a restricted launch restores its exact allowlist. The `autoExit` override still controls whether `subagent_done` is available, while `caller_ping` remains available. Missing, malformed, or unsupported policy fails closed before a pane is created with recovery guidance. Public resume rejects managed-worktree child sessions; use their retained workspace instead. Unknown policy owners, including legacy workflow sidecars, fail closed.

**Interaction flow:**

1. Child calls `caller_ping({ message: "Not sure which schema to use" })`
2. Ordinary child sessions exit (like `subagent_done`); persistent specialists stay alive.
3. Parent receives a steer notification: *"Sub-agent Worker needs help"* followed by the message inside a `<subagent_report from="Worker" kind="help-request">` block
4. The parent resumes an ordinary child with `subagent_resume`, or replies to a persistent specialist with `subagent_send`.
5. The child picks up with the parent's guidance

**Example:**

```typescript
// Inside a worker subagent
await caller_ping({
  message: "Found two conflicting migration files — should I use v1 or v2?"
});
// Session exits here. Parent receives the ping, then resumes this session
// with guidance like "Use v2, v1 is deprecated"
```

> **Note:** `caller_ping` is only available inside Pi-backed subagent contexts. Calling it from a standalone Pi session returns an error. For a worktree child, the help handoff retains the workspace, but `subagent_resume` does not reattach worktree tracking; continue the work in the retained workspace.

## Child-context hint (`PI_SUBAGENT_ID`)

Every fresh or resumed child launched by this extension, including `fork: true`
children and persistent specialists, runs with `PI_SUBAGENT_ID` set to its run
ID. The extension uses it to register the child protocol tools and to omit
parent-only surfaces (`worktree_list`, `worktree_remove`,
`subagents_write_task_models`, and `/subagents-init`). A `/worktree <name>`
handoff starts an ordinary interactive Pi session and does not set it. Pi's own
`/fork` and `/clone` run in the same process and keep that process's
environment: the variable is absent in a top-level session and still present
inside a child.

Role packs may read `PI_SUBAGENT_ID` as a context hint, for example to avoid
restoring parent-only session state inside a delegated child. It is not an
authentication or security boundary: any process can set it, and shell commands
and nested processes a child runs inherit it, including a `pi` process started
from the child's Bash tool. Do not grant or deny privileges based on it. There
is no other parent/child protocol.

## The `/worktree` Workflow

`/worktree <worktree> [task]` creates a Herdr-managed worktree from the current committed branch and launches a new interactive Pi session there with the active conversation branch. The original session remains available. Use `/worktree list` or `worktree_list({})` to inspect managed worktrees, including cross-session orphans, whose canonical source repositories are inside the session's cwd subtree. This is a new-process handoff, not an in-place move of the existing shell or Pi process.

The destination workspace is focused only after Pi startup is confirmed. Every fresh launch, including the handoff, sets `PI_HERDR_AGENTS_SESSION` to its session file; confirmation requires a Pi process in the root pane with the worktree cwd whose launch-time environment carries that value. Pi rewrites its process title, so command-line text is never evidence. Nothing inside Pi reads the variable, and it is not a child-context hint. Confirmation reads Linux `/proc`; see [Worktree subagents](worktree-subagents.md).

### Explicit worktree cleanup

Parent sessions can call `worktree_remove({ target: "<path|branch|workspace-id>", preserve: true })` or `/worktree remove <target> [--preserve]`. Preservation is optional and never implied: dirty work is blocked unless explicitly committed first or preserved as a WIP commit. The result reports its SHA even if removal later fails or is refused. A failed preservation commit restores the pre-preservation index and never proceeds to removal. Inventory and removal reports disclose exact ignored-file counts: enumeration is streamed rather than buffered as one listing. Ignored files do not block cleanup and are not captured by preservation; failed counting still blocks removal.

Eligibility is rechecked at removal time: canonical source-repository cwd containment, registered linked checkout, no detected process holder, known live child, or persistent lease, and clean Git state with no untracked files or conflicts. A successful launch from a linked checkout does not itself authorize its removal; cleanup uses the canonical principal/source repository under the invoking parent session's cwd, not `manifest.sourceCwd` or shared Git identity. Start the parent Pi session rooted at that principal checkout or an ancestor containing it, then use normal explicit cleanup; `cd` inside an existing Pi session does not change the session cwd. A source repository that is a Git submodule is located through the `core.worktree` setting of its shared Git directory under the superproject's `.git/modules`, not through that directory's parent. Unknown inspection, identity disagreements, detached HEAD, locked checkouts, and initialized submodules block removal. Out-of-scope repositories are never eligible. Open worktree workspaces use Herdr removal. The source repository's primary workspace is never closed automatically. If this process recorded that worktree creation appeared to open it and a read taken just before the report still shows it untouched, removal adds `wX appears to have been opened by worktree creation; if you haven't used it, close it with herdr workspace close wX`; a missing or wrong-typed field, another open workspace for the repository, another session, or a restarted process reports nothing. Orphans use Git removal and registration pruning after checkout absence is verified. Owned reachable manifests are marked `removed`; manifests from other sessions are not required or rewritten. Stale removed manifests never govern a recreated checkout. Missing or dangling manifest paths do not affect unrelated checkouts; undecidable or conflicting manifest identity still blocks removal. A failed manifest update after removal is reported as a warning. Symlinked ancestors are supported; checkout symlinks escaping the canonical managed root are blocked. Process inspection covers observable same-user processes across sessions, regardless of runtime name, exempting only Herdr-confirmed idle retained shells, never runtimes at the same PID. Unreadable individual process details, including a same-user process whose cwd cannot be determined (any error other than ENOENT or ESRCH), produce non-blocking warnings without an override flag. Scanning continues so another observable holder still blocks removal. An undetermined cwd is not proof the process is idle. Human and structured inventory/removal results disclose incomplete coverage, including warnings seen before a later recheck or failure. Same-user inspection is permission-limited, and other-user processes are not inspected: a protected process could hold the checkout undetected. Warnings aggregate counts and bounded PID samples, not commands or environments. Cleanup path canonicalization uses lstat/stat/readlink only and does not `readdir` a managed entry until containment under the managed root is proven, so a protected-directory symlink cannot freeze Pi. Linux uses `/proc` and resolves `/proc/<pid>/cwd` with the no-open resolver only; macOS uses same-user `lsof` cwd records resolved without opening and warns for unreadable individual records. Unsupported platforms, failed global enumeration (including a failed `lsof` with partial output), and other unverifiable eligibility evidence still block removal. Cleanup Git and Herdr calls have 30-second timeouts.

A worktree launch may add three asynchronous Herdr snapshots (`worktree list` before create, `worktree list` after create, and `pane list` for the opened primary), each bounded at 3 seconds. Together they can add up to 9 seconds of wall time within the worktree-acquisition budget without blocking the event loop. A failed snapshot claims nothing and is returned as a launch diagnostic, shown in the `subagent` acknowledgement and the `/worktree` notice; it is not written to the console. Claims are kept only in this process and are not written to the manifest. Ordinary non-worktree launches add no Herdr calls. Explicit removal adds a primary-workspace report only when this process holds a claim for the removed source repository: `worktree list`, `workspace get`, and `pane list`, each asynchronous and bounded at 3 seconds, taken after the checkout is removed. That is at most three calls and 9 seconds of wall time, without blocking the event loop. These report calls sit on top of the existing cleanup Git and Herdr calls, which stay at 30 seconds each.

Cleanup never deletes or rewrites branches, uses force flags, or runs automatically. It never closes the source repository's primary workspace. Session startup does not scan worktree inventory, avoiding blocking Pi initialization; use `/worktree list` or `worktree_list` for an explicit inventory. Child sessions retain `/worktree list` and `/worktree <name>`, but receive neither cleanup tools nor the remove subcommand. Their repository-local listing labels detached entries `(detached HEAD)`; a detached sibling does not prevent inspection of named branches. See [cleanup and recovery](worktree-subagents.md#cleanup) for details.

## Tools Widget

Every sub-agent session displays a compact one-line tools widget summarizing available and denied tools:

```
[scout] — 12 tools · 4 denied
```

## Tool reference

Parameters for every public tool, generated from the registered schemas.

<!-- generated:tool-reference:start -->
<!-- Generated by npm run docs:tools; edit the schema, not this block. -->

### `caller_ping`

Registered in: child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | string | yes | What you need help with |

### `subagent`

Registered in: parent, child sessions.

See [Parameters](#parameters).

### `subagent_cancel`

Registered in: parent, child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Exact running subagent id |
| `name` | string | no | Exact unambiguous running subagent display name |

### `subagent_done`

Registered in: child sessions.

No parameters.

### `subagent_interrupt`

Registered in: parent, child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Exact running subagent id |
| `name` | string | no | Exact running subagent display name |

### `subagent_resume`

Registered in: parent, child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `sessionPath` | string | yes | Path to the session .jsonl file to resume |
| `name` | string | no | Display name for the terminal tab. Default: 'Resume' |
| `message` | string | no | Message to send after resuming. |
| `autoExit` | boolean | no | Exit after the response. Defaults to true; set false to leave the session open for the user. |

### `subagent_send`

Registered in: parent, child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Exact persistent specialist logical ID |
| `name` | string | no | Exact unambiguous persistent specialist name |
| `message` | string | yes | The next task |

### `subagent_stop`

Registered in: parent, child sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Exact persistent specialist logical ID |
| `name` | string | no | Exact unambiguous persistent specialist name |

### `subagents_list`

Registered in: parent, child sessions.

No parameters.

### `subagents_write_task_models`

Registered in: parent sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `tasks` | object | yes | — |
| `tasks.coding` | string[] | no | Implementation workers |
| `tasks.review` | string[] | no | Code reviewers |
| `tasks.recon` | string[] | no | Reconnaissance scouts |
| `tasks.qa` | string[] | no | Software and test runners |
| `tasks.architecture` | string[] | no | Planning and diagnosis |
| `tasks.docs` | string[] | no | Documentation workers |
| `tasksMeta` | object | yes | — |
| `tasksMeta.generatedAt` | string | yes | — |
| `tasksMeta.method` | `"research"` \| `"registry-only"` | yes | — |
| `expectedConfigRevision` | `"missing"` \| string | no | Revision of the config the proposal was read from: "sha256:" plus 64 lowercase hex digits of the exact file bytes, or "missing" when the file is absent. A mismatch fails without writing; re-read, re-propose, and re-approve. Omit for an unconditional write. |
| `basis` | object \| object | no | What the ranking rests on, as submitted by the proposer and not independently verified: {"kind":"registry-only"}, or {"kind":"research"} with the http(s) sources consulted in this run, how each informed the ranking, and the remaining uncertainty. kind must equal tasksMeta.method. Not saved. Omit for a legacy write. |
| `basis.kind` | `"registry-only"` \| `"research"` | yes | — |
| `basis.sources` | object[] | no | — |
| `basis.sources[].url` | string | yes | — |
| `basis.sources[].influence` | string | yes | — |
| `basis.uncertainty` | string | no | — |

### `worktree_list`

Registered in: parent sessions.

No parameters.

### `worktree_remove`

Registered in: parent sessions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `target` | string | yes | — |
| `preserve` | boolean | no | — |
<!-- generated:tool-reference:end -->
