# @qcts33/pi-herdr-subagents

Subagents for [pi](https://github.com/badlogic/pi-mono) running exclusively in [herdr](https://herdr.dev). Spawn, orchestrate, and manage ephemeral child runtimes in dedicated herdr tabs or panes. They run in the background by default, with optional foreground execution when the parent needs the result before continuing.

## How It Works

Call `subagent()` to run a child in its own terminal pane. It returns immediately by default; set `run_in_background: false` to wait for the child and receive its result directly. A live widget above the input shows all tracked agents with their projected state — for example `starting`, `active`, `waiting`, `interrupted`, `stalled`, `running`, or `finalizing`. The header summarizes **active** (processing) vs **open** (not processing). When every tracked subagent is open, the border switches to amber. Background results are **steered back** into the main session as async notifications.

```
╭─ Subagents ──────────────────── 1 active · 1 open ─╮
│ 00:23  Explore: Auth (explore)     active · bash 7m │
│ 00:45  Explore: DB (explore)            waiting 2m │
╰────────────────────────────────────────────────────╯
```

For parallel execution, just call `subagent` multiple times — they all run concurrently:

```typescript
subagent({ name: "Explore: Auth", agent: "explore", task: "Analyze auth module" });
subagent({ name: "Explore: DB", agent: "explore", task: "Map database schema" });
// Both return immediately, results steer back independently

// Foreground call: wait for the result before continuing
subagent({ name: "Implementer", task: "Make the change", run_in_background: false });
```

## Development

Run unit tests and lint locally:

```bash
npm test
npm run lint
```

Run the real end-to-end suite from inside herdr with an explicit test model and thinking level:

```bash
PI_TEST_MODEL="deepseek/deepseek-v4-flash" PI_TEST_THINKING=high PI_TEST_TIMEOUT=180000 npm run test:integration
```

The full suite launches real Pi sessions and can take several minutes. `PI_TEST_TIMEOUT` is the per-test timeout in milliseconds; use at least `180000` for the lifecycle suite. The integration harness deliberately uses temporary workspace paths containing spaces; on native Windows it drives the parent through a PowerShell script and Node-based fixtures so the same tests work from PowerShell or `cmd.exe` panes. Set `PI_TEST_WINDOWS_CWD` to an existing drive-letter or UNC directory to enable the optional native Windows cwd test.

`PI_TEST_MODEL` and `PI_TEST_THINKING` select the runtime for both the parent Pi sessions and their subagents (children inherit the parent runtime by default). Defaults are `deepseek/deepseek-v4-flash` and `high`. `PI_TEST_THINKING` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Use an exact registry model ID for `PI_TEST_MODEL`; do not append a thinking-level suffix such as `:low`.

## Install

Install the package from npm:

```bash
pi install npm:@qcts33/pi-herdr-subagents
```

This project does not install or load `HazAT/pi-interactive-subagents` automatically.

Releases are published manually from a clean `main` branch; for authentication, versioning, and troubleshooting see [RELEASING.md](../../RELEASING.md).

Start herdr, then run pi inside it:

```bash
herdr
pi
```

herdr is the only supported terminal environment. The extension requires `HERDR_ENV=1`, the `herdr` CLI, and Herdr's native agent API (`agent start`, `agent prompt`, `agent get`, and `agent send-keys`). Pi is the only supported child agent kind; there is no pane-command fallback.

Native agent startup waits for Pi readiness before delivering the task. If startup is slow, set `PI_SUBAGENT_AGENT_START_TIMEOUT_MS` (default `30000`, maximum `300000`). Every Fresh Ephemeral Child runtime uses Pi's `--no-session` mode and explicitly loads the completion extension, which records Completion evidence and acknowledges cancellation after descendant drain; Pi's configured Child extensions remain enabled alongside it. Completion, activity, cancellation, and launch-prompt data live only in short-lived operation artifacts under the OS temporary directory.

Subagent tabs and panes are created without stealing keyboard focus. Native agent operations target child panes by explicit ID, so focus and command delivery are independent. Every child uses the same automatic Completion lifecycle; this is independent of terminal focus.

## What's Included

### Extensions

**Subagents** — 2 main-session tools, plus 1 subagent-only tool:

| Tool              | Description                                                                                 |
| ----------------- | ------------------------------------------------------------------------------------------- |
| `subagent`        | Spawn a sub-agent; background by default or foreground with `run_in_background: false` |
| `subagent_cancel` | Cancel a running sub-agent: terminate, reclaim its herdr pane, and report the cancellation |

### Built-in Subagent Types

Exactly two built-in subagent types exist; they are fixed in the extension and cannot be extended or overridden:

| Type                | Default runtime | Role                                                                                 |
| ------------------- | --------------- | ------------------------------------------------------------------------------------ |
| **explore**         | Parent          | Read-only codebase search and analysis — maps files, patterns, conventions            |
| **general-purpose** | Parent          | Complex multi-step tasks requiring exploration and action — writes code, runs tests   |

Both types inherit the parent model and thinking level by default. The orchestrating agent can override either field for a specific task using an exact authenticated model ID and a supported Pi thinking level. Prefer changing thinking before changing models.

---

## Subagent Execution Modes

```
1. Agent calls subagent()          → returns immediately ("started")
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
```

For a foreground call (`run_in_background: false`), the parent tool call stays pending while the child runs:

```
1. Agent calls subagent()          → call remains pending
2. Sub-agent runs in herdr pane    → widget shows live status
3. Sub-agent finishes              → result returns from the tool call
4. Main agent processes result     → continues with the child summary
```

Multiple background subagents run concurrently — each steers its result back independently as it finishes. The live widget above the input tracks every agent still in flight:

```
╭─ Subagents ──────────────────── 1 active · 2 open ─╮
│ 01:23  Explore: Auth (explore)       active · write 7m │
│ 00:45  General (general-purpose)           stalled 4m │
│ 00:12  Explore: DB (explore)               starting…   │
╰─────────────────────────────────────────────────────────╯
```

Completion messages render with a colored background and are expandable with `Ctrl+O` to show the full summary. 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 agent 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.
- Child Pi runtimes do not create a transcript. The Completion sidecar carries the final summary and runtime details; operation artifacts are not Pi sessions.

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 / a running `subagent_cancel`); the row is **not** treated as active processing while the cancellation drain finishes
- `stalled` — pane inspection is unhealthy long enough that the parent can no longer trust the run
- `running` — fallback while the native Pi process is present but detailed turn state is not yet known
- `finalizing` — completion was observed and delivery is in progress; the process elapsed timer freezes here
- `draining` — a launch reservation or result handoff still owns the parent before its provisional settled boundary is visible
- `self-settled · draining` — this parent loop has ended provisionally while owned descendants, launch reservations, or result handoffs remain
- `cancelling · draining` — cancellation won the lifecycle cutover and descendant termination is still being supervised

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. Cancellation does **not** freeze that process clock; the interrupted state shows its own duration on the right while the pane remains open during the drain.

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.

By default, subagents publish Completion evidence at Pi's `agent_settled` boundary. The parent closes the child surface, delivers the result, and receives `stalled`/`recovered` notifications when supervision needs attention. Every new attempt is Fresh and independent. A Completion destination is invoked at most once per operation; if the parent rejects the handoff, the extension reports a parent-side delivery failure and does not retry it. A failed Fresh attempt is terminal; if the work is still needed, start a new Fresh `subagent` call and provide the necessary context in its task. `/reload` preserves the parent-local registry, nested ownership, pending handoffs, and drain state, while a new parent process does not recover old outcomes. Existing operation records, claims, lineage metadata, and stale Completion artifacts are ignored rather than migrated.

#### Configuration

Status display is enabled by default. To disable it, create `config.json` in the extension directory:

```json
{
  "status": {
    "enabled": false
  }
}
```

Subagent model and thinking selection is done per call: omit both to inherit the parent runtime, or pass an exact authenticated `provider/model-id` and a supported Pi thinking level on the tool call.

`config.json` is gitignored so local overrides don't get committed.

---

## Spawning Subagents

```typescript
// Named agent with defaults from agent definition or config.json
subagent({ name: "Explore", agent: "explore", task: "Analyze the codebase..." });

// Read-only investigation with the built-in explore type
subagent({ name: "Researcher", agent: "explore", task: "Investigate the API rate limits" });

// Custom working directory
subagent({ name: "Designer", agent: "general-purpose", cwd: "agents/game-designer", task: "..." });
```

### Parameters

| Parameter              | Type    | Default        | Description                                                                                       |
| ---------------------- | ------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `name`                 | string  | required       | Display name (shown in widget and pane title)                                                     |
| `task`                 | string  | required       | Task prompt for the sub-agent                                                                     |
| `run_in_background`    | boolean | `true*`         | `false` waits for and returns the result; `true` returns immediately and steers the result. Omit to preserve background compatibility. |
| `agent`                | string  | —              | Built-in subagent type: `'explore'` or `'general-purpose'`                                        |
| `model`                | string  | parent         | Exact authenticated `provider/model-id`; omit to inherit the parent                               |
| `thinking`             | string  | parent level   | Pi thinking level (`off` through `max`); omit to inherit the parent                                |
| `systemPrompt`         | string  | —              | Append to system prompt                                                                           |
| `skills`               | string  | —              | Comma-separated skill names                                                                       |
| `tools`                | string  | —              | Comma-separated tool names                                                                        |
| `cwd`                  | string  | —              | Working directory for the sub-agent (see [Role Folders](#role-folders))                           |

---

## Cancelling a running subagent

Use `subagent_cancel` to terminate a running Pi-backed subagent:

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

This is a full cancellation, not a turn-level interrupt. The extension writes a durable cancellation request, sends Escape to the child pane, and the child drains its own active descendants before publishing its cancellation acknowledgement. Once the acknowledgement is observed, the watch arc **closes the child's herdr pane**, releases the registry entry, and delivers a `cancelled` steer to the parent conversation — the cancelled attempt never produces a result. Cancelling a subagent that is already finalizing (or already cancelled) is a no-op with an explanatory message.

A cancelled child's surface is reclaimed only after its acknowledgement so its descendant subtree can drain first. The `interrupted` widget label marks the row during that drain window; the row disappears once the pane is reclaimed.

> **Note:** Only Pi-backed subagents are supported. Agent definitions with a legacy `cli` setting are no longer supported.

---

## Ephemeral Child runtimes

Every subagent starts as a Fresh Ephemeral Child runtime with Pi session persistence disabled (`--no-session`). There is no context-inheritance mode: the child receives only its standalone task prompt and explicit role instructions; required context belongs in the task. Completion evidence and the structured result are exchanged through short-lived operation artifacts, not a Pi transcript.

### Completion lifecycle

By default, a child requests Completion evidence at Pi's `agent_settled` boundary. For a child that owns Nested Subagents, this is only the Self-settled boundary: final evidence waits until direct descendants and pending Descendant result handoffs drain. Intermediate `agent_end` events do not complete the operation, provider retries are allowed, and an aborted final run publishes no automatic evidence.

## Tool Access Control

The two built-in types have fixed tool access:

| Type            | Tools                                                                 | Spawning                                             |
| --------------- | --------------------------------------------------------------------- | ---------------------------------------------------- |
| general-purpose | Full toolset                                                          | Allowed — may spawn further subagents                |
| explore         | `read`, `bash`, `web_search`, `web_fetch`                            | Denied — no `subagent` / `subagent_cancel`        |

### Direct self-spawn

A Subagent cannot launch a direct Child runtime using its own type (`general-purpose` → `general-purpose` is rejected). The rejected tool call tells it to choose the other built-in type instead; if none fits, it must complete the task directly.

---

## Role Folders

The `cwd` parameter lets sub-agents start in a specific directory with its own configuration:

```
project/
├── agents/
│   ├── game-designer/
│   │   └── CLAUDE.md          ← "You are a game designer..."
│   ├── sre/
│   │   ├── CLAUDE.md          ← "You are an SRE specialist..."
│   │   └── .pi/skills/        ← SRE-specific skills
│   └── narrative/
│       └── CLAUDE.md          ← "You are a narrative designer..."
```

```typescript
subagent({ name: "Game Designer", cwd: "agents/game-designer", task: "Design the combat system" });
subagent({ name: "SRE", cwd: "agents/sre", task: "Review deployment pipeline" });
```

---

## Tools Widget

Every sub-agent runtime displays a compact tools widget showing available and denied tools. Toggle with `Ctrl+J`:

```
[explore] — 10 tools · 2 denied  (Ctrl+J)           ← collapsed
[explore] — 10 available  (Ctrl+J to collapse)       ← expanded
  read, bash, web_search, web_fetch, ...
  denied: subagent, subagent_cancel, ...
```

---

## Requirements

- [pi](https://github.com/badlogic/pi-mono) — the coding agent
- [herdr](https://herdr.dev) — the required terminal workspace

```bash
herdr
pi
```

Other multiplexers and terminal backends are not supported.

---

## Acknowledgements

The sub-agent status supervision and run cancellation features were inspired by [RepoPrompt](https://repoprompt.com/)'s sub-agent snapshot polling and run cancellation features.

---

## License

MIT
