@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +245 -77
  2. package/package.json +16 -13
  3. package/src/abortable.ts +1 -1
  4. package/src/agent-file-toggle.ts +6 -5
  5. package/src/agent-manager.ts +489 -129
  6. package/src/agent-runner.ts +244 -93
  7. package/src/child-context.ts +19 -1
  8. package/src/completion-outbox.ts +22 -3
  9. package/src/cross-extension-rpc.ts +21 -42
  10. package/src/custom-agents.ts +11 -6
  11. package/src/default-agents.ts +21 -21
  12. package/src/detached-results.ts +18 -2
  13. package/src/enabled-models.ts +8 -7
  14. package/src/enterprise-local.ts +7 -6
  15. package/src/error-message.ts +11 -0
  16. package/src/git-path.ts +66 -0
  17. package/src/group-join.ts +4 -10
  18. package/src/home-paths.ts +27 -0
  19. package/src/index.ts +898 -297
  20. package/src/invocation-config.ts +4 -21
  21. package/src/memory.ts +9 -9
  22. package/src/mention-clone.ts +13 -8
  23. package/src/model-resolver.ts +92 -25
  24. package/src/model-scope.ts +2 -1
  25. package/src/model-selection.ts +170 -0
  26. package/src/model-tiers.ts +176 -0
  27. package/src/nested-tools.ts +99 -44
  28. package/src/notification-result.ts +118 -0
  29. package/src/output-file.ts +23 -1
  30. package/src/package-resources.ts +25 -10
  31. package/src/plan-mode-children.ts +176 -0
  32. package/src/process-budget.ts +6 -3
  33. package/src/prompts.ts +7 -2
  34. package/src/quota-admission.ts +35 -13
  35. package/src/resource-loader.ts +65 -0
  36. package/src/schedule.ts +94 -30
  37. package/src/settings.ts +63 -13
  38. package/src/skill-loader.ts +244 -93
  39. package/src/stale-ctx.ts +16 -0
  40. package/src/status-note.ts +28 -5
  41. package/src/types.ts +79 -2
  42. package/src/ui/agent-mention.ts +5 -2
  43. package/src/ui/agent-row.ts +13 -3
  44. package/src/ui/agent-widget.ts +55 -25
  45. package/src/ui/conversation-viewer.ts +10 -5
  46. package/src/ui/fallback-flag.ts +114 -0
  47. package/src/ui/fleet-list.ts +431 -86
  48. package/src/ui/fleet-state.ts +154 -0
  49. package/src/ui/notification-view.ts +22 -3
  50. package/src/ui/workflow-card.ts +6 -9
  51. package/src/ui/workflow-dialog.ts +36 -13
  52. package/src/workflow/host.ts +47 -50
  53. package/src/workflow/journal.ts +13 -1
  54. package/src/workflow/meta.ts +7 -4
  55. package/src/workflow/progress.ts +8 -3
  56. package/src/workflow/runtime.ts +48 -15
  57. package/src/workflow/saved.ts +21 -21
  58. package/src/workflow/task.ts +22 -7
  59. package/src/workflow/tool-description.ts +2 -2
  60. package/src/workflow/worker-source.ts +162 -40
  61. package/src/worktree.ts +309 -19
package/README.md CHANGED
@@ -14,14 +14,14 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
14
14
 
15
15
  ## Features
16
16
 
17
- - **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
17
+ - **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`, `stop_subagent`) — feels native
18
18
  - **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 10) and completion notifications that reach the parent at its next boundary, several at once when they finish together
19
19
  - **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons. Configurable via `/agents → Settings → Widget`: `all` (every agent), `background` (default — hides foreground runs, which already render inline as the `Agent` tool result), or `off`
20
- - **FleetView** — Claude Code-style navigable list of `main` + every running subagent rendered below the editor (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓` to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
20
+ - **FleetView** — one-line summary below the editor by default; `ctrl+shift+a` expands the Claude Code-style navigable list of `main` + every running subagent (earliest-launched first). `ctrl+shift+k` twice within 3s stops every running agent and workflow. Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓` to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
21
21
  - **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). In pi's fullscreen mode it is a borderless, full-terminal observer with mouse support. Steer a running agent inline by pressing `Enter` to open a composer, typing, then `Enter` to send (`Esc` or an empty submit returns) — the message appears as a user message and redirects the agent after its current tool. Press `b` on a running foreground `Agent` call to hand that run to the background (see [Sending a foreground agent to the background](#sending-a-foreground-agent-to-the-background)). Stop a still-running agent by pressing `x` (then `x` again to confirm) — steer and stop both work for background agents too. Assistant text renders as Markdown; `m` cycles that between off, assistant-only and everything (see [Viewer markdown](#persistent-settings))
22
22
  - **Custom agent types** — define agents in `.pi/agents/<name>.md` or `.agents/agents/<name>.md` (project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions, and Claude Code-compatible colored name badges
23
23
  - **Package-shipped agents** — a pi package can declare its own subagents (and workflow scripts) with `"pi": { "subagents": { "agents": ["./agents"] } }` in its `package.json`, so `pi install` is the whole setup: no copying files into `.pi/agents/` on every machine or container start. Only packages pi has actually installed are read (never `node_modules`), package definitions rank below every agent you wrote, and `/agents` shows them read-only with Eject and Disable. Narrow or switch it off with `packageAgents` / `packageWorkflows`. **[Full guide](https://github.com/tintinweb/pi-subagents/blob/master/docs/packages.md)**
24
- - **Nested subagents** — opt-in, default-off delegation: a custom agent that sets `allowed_subagents` gets its own ownership-scoped `Agent`, `get_subagent_result`, and `steer_subagent` tools, depth-capped from the main session (default 2). It can control only its own children, they are stopped when it finishes, and their transcripts and token spend roll up to it. The allowlist is a privilege boundary — a child runs with its own tools, so pick it as carefully as `tools:` itself
24
+ - **Nested subagents** — opt-in, default-off delegation: a custom agent that sets `allowed_subagents` gets its own ownership-scoped `Agent`, `get_subagent_result`, `steer_subagent`, and `stop_subagent` tools, depth-capped from the main session (default 2). It can control only its own children, they are stopped when it finishes, and their transcripts and token spend roll up to it. The allowlist is a privilege boundary — a child runs with its own tools, so pick it as carefully as `tools:` itself
25
25
  - **Agent mentions** — subagents are first-class: type `@explore also check the RPC path` at the prompt and it goes to that agent instead of the main model, without a word of it entering the chat. One syntax covers the whole lifecycle — message it while it runs, resume it once it has finished, reopen its session from disk long after that, or start it if it never ran. Mentioning an agent that isn't running spawns it through an off-screen clone of the conversation, so it gets Claude Code's context-written prompt and a real `Agent` tool call without a word of it reaching the chat; `direct` mode starts it here from your text instead, with no model call at all. The orchestrator can `name` an agent so you address it as `@auth-audit`, and handles work in `steer_subagent`/`get_subagent_result` too. `@` completes live agents, resumable ones, and startable types alongside pi's file completion; `@main` forces text back to the main model. Toggle via `/agents → Settings → Agent mentions`
26
26
  - **Scripted workflows** — a `SubagentWorkflow` tool that runs a deterministic JavaScript script orchestrating many subagents: `agent()`, `parallel()`, `pipeline()`, `phase()`, `log()` and `args`, with a pure-literal `meta` block declaring the phases. `pipeline()` has no barrier between stages, so one item can be in a later stage while another is still in the first — unlike `parallel()`, which idles every fast agent until the slowest finishes. Runs in the background with a live card, inspectable via `/agents → Workflows` or by selecting the run in FleetView. `agent()` also takes `gate: "npm test"` to verify a child by running a command (inside its worktree, when isolated) rather than asking another model, and `resume: "<label>"` to continue a child instead of re-paying its context. Scripts run in a `node:vm` sandbox on a worker thread where `Date.now()`, `Math.random()` and `eval` throw. On by default, but it stands down for company: if another extension already provides a `Workflow` or `SubagentWorkflow` tool, this one warns and disables itself for the session rather than offering the model two orchestrators. Pin it either way with `"workflowsEnabled"` in `subagents.json` or `/agents → Settings → Workflows`. A script written for Claude Code's `Workflow` tool runs here unchanged: same globals, `schema` returns a validated object exactly as it does there, `budget` is present and always reports no token target (pi has no such directive) so its `budget.total`-guarded patterns still take the branch they were written for, and nested `workflow()` composes saved workflows one level deep. **[Full guide](https://github.com/tintinweb/pi-subagents/blob/master/docs/workflows.md)**
27
27
  - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
@@ -32,8 +32,9 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
32
32
  - **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
33
33
  - **Persistent agent memory** — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
34
34
  - **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
35
- - **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
35
+ - **Skill preloading** — inject named skills into agent system prompts through Pi's configured resource discovery, including installed package skills; project resources participate only after project trust and package/user precedence stays Pi-native
36
36
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
37
+ - **Stops are reported** — a run that settles while the session's own agents are still running or queued shows the count and what is armed to wake the parent, e.g. `Run ended with 3 agents running or queued, armed: 2 agent completion notices wake the parent; 1 foreground or RPC-spawned agent will not`. Background agents (`Agent` with `run_in_background`, `@handle` mentions, scheduled jobs) wake it in every join mode; foreground agents and RPC spawns without `isBackground` notify nobody, and when only those are pending the notice is a warning with `armed: no agent completion notice wakes the parent`. It speaks for agents only, beside the monitor and todo notices. Two edges: after a session switch the notice is spooled and lands when that session is live again, and a held notice can be dropped when `pi -p` tears down. Nested and workflow children are not counted
37
38
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
38
39
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) and a per-message spend ledger (`subagents:usage`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
39
40
  - **Cross-extension RPC** — other pi extensions can spawn, stop, steer, and join subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`, `subagents:rpc:consume`, `subagents:rpc:steer`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on session start. **[Full reference](https://github.com/tintinweb/pi-subagents/blob/master/docs/rpc.md)**
@@ -54,7 +55,7 @@ pi -e ./src/index.ts
54
55
 
55
56
  Requires pi **1.1.0 or newer**: pi 0.86 and 0.87 moved the system prompt and conversation history into the session transcript, and the `@handle` conversation clone and the agent transcript files are built on that model. The `peerDependencies` range declares it, so npm flags an older pi at install time.
56
57
 
57
- ### In-process factory and clock
58
+ ### In-process factory, clock and home
58
59
 
59
60
  `src/index.ts` exports `createSubagentsExtension(options: SubagentsExtensionOptions = {}): ExtensionFactory`.
60
61
  The default export is `createSubagentsExtension()` and keeps native time and timer reference behavior.
@@ -70,8 +71,12 @@ declare const clock: Clock;
70
71
  const extension = createSubagentsExtension({ clock });
71
72
  ```
72
73
 
73
- `SubagentsExtensionOptions` has one optional field, `clock?: Clock`. The exported clock contract matches
74
- session-kit's scheduling boundary:
74
+ `SubagentsExtensionOptions` accepts `clock?: Clock` and `homeDir?: string`. An explicit home binds
75
+ user-scoped discovery, settings and child-session paths without mutating `HOME`; omitted homes keep
76
+ call-time host defaults. Explicit agent/session directory overrides still win. The boundary audit and
77
+ test seams are documented [here](docs/home-injection.md).
78
+
79
+ The exported clock contract matches session-kit's scheduling boundary:
75
80
 
76
81
  ```ts
77
82
  interface TimerHandle { cancel(): void; unref?(): void }
@@ -99,14 +104,27 @@ holds, workflow execution timers, child-shutdown watchdogs, detached-spool filen
99
104
  auto-hide and schedule-store lock retries retain native time. Queued result waits use the manager's
100
105
  start gate when present; legacy queued resumes retain the clock-driven fallback.
101
106
 
102
- `npm run check` runs Biome, TypeScript, the full Vitest suite and `test:bun`. The four session scenario
107
+ `npm test` and `test:coverage` launch Vitest through the shared
108
+ `scripts/run-vitest.ts` launcher. It gives the child a temporary `HOME`, `USERPROFILE`,
109
+ `XDG_CONFIG_HOME`, `PI_CODING_AGENT_DIR` and `PI_SUBAGENTS_CONFIG_DIR` before imports,
110
+ then removes that home on exit. Operator `agentModels`/model-tier policy must not affect
111
+ fixture dispatch; runtime tier defaults and operator files remain unchanged. The command-level
112
+ regression runs the real `npm test` against a hostile fixture home with an unavailable
113
+ `@default` tier and checks the existing Agent boundary assertions. `test:watch` and
114
+ `test:e2e` retain their operator environment. The explicit live smoke command
115
+ `PI_E2E_LIVE=1 npm run test:e2e` needs the operator's configured Pi login; it is not a
116
+ hermetic verification gate. Without that opt-in, `test:e2e` runs the scripted faux suite.
117
+
118
+ `npm run check` runs Biome, TypeScript, the full Vitest suite and `test:bun`, the same full suite on Bun 1.4.4 through
119
+ `scripts/run-vitest.ts bun` (no native `bun test` subset). The four session scenario
103
120
  files (`test/{session-scenarios,fleet-session,viewer-session,worktree-isolation-e2e}.test.ts`) run under
104
- both Vitest and Bun 1.3.14 on core `v1.3.0-nredd.6` release tarballs. They use exported `createHarness`
121
+ both engines on core `v1.3.0-nredd.10` release tarballs. They use exported `createHarness`
105
122
  and `createInteractiveHarness`/`VirtualTerminal`, real child sessions and git, one injected extension
106
123
  clock, promise-held tools/streams and raw SGR mouse input. Homes, settings and provider credentials
107
124
  are fixtures; no installed pi, tmux or real-clock sleeps are needed. Existing old-host strict-mode
108
125
  coverage explicitly removes the optional SDK export; the shared faux runtime uses the host's real
109
- `withBeforeProviderRequest` facade. The full older suite remains Vitest-only.
126
+ `withBeforeProviderRequest` facade. Maintainer guidance on public index callback
127
+ coverage and retained safety guards is [here](../../docs/subagents-index-continuation.md).
110
128
 
111
129
  ### Other hosts
112
130
 
@@ -129,7 +147,7 @@ Agent({
129
147
  })
130
148
  ```
131
149
 
132
- Agents run in the background by default: the call returns an ID immediately and notifies you on completion, carrying a preview of the result (use `get_subagent_result` for the full text). Pass `run_in_background: false` to block until the agent finishes and get its full output inline; while it blocks, `ctrl+b` hands it to the background (see below).
150
+ Agents run in the background by default: the call returns an ID immediately and notifies you on completion, carrying the whole result up to [`notificationResultChars`](#completion-notice-results) (16k chars by default; past that the head and tail, with `get_subagent_result` for the rest). Pass `run_in_background: false` to block until the agent finishes and get its full output inline; while it blocks, `ctrl+b` hands it to the background (see below).
133
151
 
134
152
  ### Sending a foreground agent to the background
135
153
 
@@ -163,9 +181,9 @@ Schedule formats:
163
181
  - **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
164
182
  - **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
165
183
 
166
- When a schedule fires, the spawn runs in background and its completion notification arrives the same way as a manually-spawned background agent's — your parent agent reasons about the result the same way. The job stores the resolved thinking level when you create it: an explicit `Agent({ thinking })` wins over the agent definition then, while an omitted value keeps the definition's default. Editing the agent file later does not replace that stored level.
184
+ When a schedule fires, the spawn runs in background and its completion notification arrives the same way as a manually-spawned background agent's — your parent agent reasons about the result the same way. Model selection runs at fire time with the manual-dispatch precedence: stored `model` request (including a captured configured tier), current `agentModels[type]`, `agentModels["*"]`, definition, parent. A stored explicit model that no longer resolves or is out of scope fails the job instead of being replaced. Tier jobs retain the `@tier` request, including a configured tier, and re-resolve its binding at fire time. Other quota-parked jobs retain the original explicit input, not a snapshot of the configured or parent model. The job stores the resolved thinking level when you create it: an explicit `Agent({ thinking })` wins over the agent definition then, while an omitted value keeps the definition's default. Editing the agent file later does not replace that stored level.
167
185
 
168
- **Session switches.** `/new`, `/resume`, `/fork` and `/reload` do not stop running background agents. They keep showing in the fleet list of the reloaded session while they run (open, stop and steer work from there), finish on their own, and the session that spawned them gets the completion notification when you return to it (immediately if it is already open). Held notices survive the switch too: unread results are transferred to the session spool and re-enter the same boundary outbox on return, without repeating their history entries. The preview is in the notification; the full output is in the transcript file it names, since `get_subagent_result` cannot reach an agent from a previous session. Quitting pi still aborts everything.
186
+ **Session switches.** `/new`, `/resume`, `/fork` and `/reload` do not stop running background agents. They keep showing in the fleet list of the reloaded session while they run (open, stop and steer work from there), finish on their own, and the session that spawned them gets the completion notification when you return to it (immediately if it is already open). Held notices survive the switch too: unread results are transferred to the session spool and re-enter the same boundary outbox on return, without repeating their history entries. The notification carries the result as a live one would, up to [`notificationResultChars`](#completion-notice-results); past that the full output is in the transcript file it names, since `get_subagent_result` cannot reach an agent that finished in a previous session. While a survivor is still running, `get_subagent_result` and `steer_subagent` reach it by id, handle or name from the reloaded session. Quitting pi still aborts everything. `router/*` routing is unaffected by the switch. A step that routes through another extension's virtual model bound to the old session is re-prompted up to three times with backoff; if it never recovers the agent ends `error` with `interrupted by a session reload` and a bounded summary of its latest tool activity, not `No output`. Earlier prose is labeled as progress if tools ran after it. The activity summary names only built-in tools (other names become `other tool`) and completion status, never tool arguments or output; inspect the transcript to recover artifacts. Usage and compaction observers bound to the old session are dropped rather than failing the run (any other observer error is logged once and also kept from failing it), and the structured-output retry and forced final report are not attempted on a runtime that is still stale, so `subagents:usage` and `subagents:compacted` stop for an agent detached by a switch.
169
187
 
170
188
  Schedules are **session-scoped**: they reset on `/new` and restore on `/resume`. List and cancel via `/agents → Scheduled jobs` (creation is the `Agent` tool's job — there is no parallel manual-create wizard). Storage at `<cwd>/.pi/subagent-schedules/<sessionId>.json` with PID-based file locking for cross-instance safety.
171
189
 
@@ -179,13 +197,13 @@ Restrictions:
179
197
 
180
198
  ### Quota admission
181
199
 
182
- With a subscription router installed (e.g. [pi-subscription-router](https://github.com/nredd/pi-subscription-router)), a fresh `Agent` dispatch first asks it whether the model is out of included quota, over `pi.events`: `router:rpc:admit` `{ requestId, model: "provider/id" }`, replied on `router:rpc:admit:reply:<requestId>` as `{ success, data: { ok, resolvedModel?, resetAt?, wait? } }` (`resolvedModel` is ignored; `wait: false` is the router's exhaustion policy saying fail now, absent means wait).
200
+ With a subscription router installed (e.g. [pi-subscription-router](https://github.com/nredd/pi-subscription-router)), a fresh `Agent` dispatch first asks it whether the model is out of included quota, over `pi.events`: `router:rpc:admit` `{ requestId, model: "provider/id" }`, replied on `router:rpc:admit:reply:<requestId>` as `{ success, data: { ok, resolvedModel?, resetAt?, wait? } }` (`resolvedModel` labels the admitted model for tiers; `wait: false` is the router's exhaustion policy saying fail now, absent means wait).
183
201
 
184
202
  - Admitted, or no router answers within 100ms: the agent starts as usual
185
203
  - Refused, background, reset known, `wait` not `false`, scheduling on: the dispatch is parked as a one-shot scheduled job a minute after the reset, persisted like any schedule. When it fires it asks again and re-parks at the new reset while still blocked; if the refusal can no longer park (`wait: false`, no reset, or a reset already past) the job is not started and ends with `lastStatus: "error"` and an error event. Only an explicit admission, or no router answering, starts it. If pi was closed through the reset, it asks again 10s after the session starts, once the router has warmed its usage cache
186
204
  - Refused otherwise (foreground, `inherit_context`, scheduling off, unknown reset, or `wait: false`): the call fails with the reset time and no agent starts
187
205
 
188
- Resumes, cron and interval jobs, and RPC or workflow spawns are not gated. A `router/*` virtual model still routes each request inside the child.
206
+ Live resumes keep their existing selection. Tier requests are admitted on every fresh entry path, including RPC, mentions, nested and workflow children, and every scheduled fire. Only top-level `Agent` background calls and scheduled fires can persist a quota wait; other refused starts error. A `router/*` virtual model still routes and fails over inside the child.
189
207
 
190
208
  ## UI
191
209
 
@@ -219,20 +237,43 @@ The status, elapsed time and tokens are read from the agent record on every rend
219
237
 
220
238
  ### FleetView
221
239
 
222
- While subagents are running, a Claude Code-style navigable list renders **below** the editor:
240
+ FleetView starts **collapsed**, rendering exactly one line below the editor, with no overflow rows or blank spacer. Always-collapsed is deliberate: it saves space even for one agent and avoids resizing when a fan-out crosses a threshold. `ctrl+shift+a` toggles it; the settled explicit choice is recorded as a versioned `subagents:fleet-state` custom entry, outside model context.
241
+
242
+ Auto-repeat events are ignored. A 300ms quiet period coalesces rapid toggles (net-no-change bursts write nothing); shutdown flushes the final choice before the activation loses its context. This bounds writes per burst, not all-time size of the host's append-only journal. The last valid choice on the active branch survives ticks, widget resets, `/reload`, and restarting/resuming that session. A fresh session defaults to collapsed. Temporary arrow navigation writes no state entry. This uses the fleet's existing focus-aware terminal-input handler, like its arrows and the foreground `ctrl+b` handoff; there is no subagent key-rebinding setting. Todo's `ctrl+shift+t`, monitors' `ctrl+shift+m`, and pi's `ctrl+o` keep their existing roles.
243
+
244
+ ```text
245
+ agents: 3 running, 2 queued, 4 done · ~$28.70 · ctrl+shift+a to expand
246
+ [1 failed, 2 parked, 1 fallback] agents: 3 running, 1 failed, 2 parked · ctrl+shift+a to expand
247
+ ```
248
+
249
+ Counts cover top-level records, including running starts and queued/waiting agents without a session, plus workflow containers (not their children). `steered` counts as done; errors and aborted runs count as failed; stopped runs stay distinct. Successful/stopped records retain the existing four-second linger.
250
+
251
+ Unacknowledged errors and aborted runs do not expire from the fleet: the UI journals minimal failure identities and acknowledgements, replayed from the active session branch on reload or disk resume. Terminal callbacks and detached-result ingress record failures explicitly, including failures between render ticks. It retains at most 128 detailed alerts (id, agent type, short sanitized description, terminal state and timestamp), never complete records/results/sessions; four durable per-kind overflow counters preserve exact outstanding totals beyond that cap. Overflow contributes to both collapsed and expanded headers, but has no individual navigation rows. Specific overflow ids can still be acknowledged through result consumption. The existing session journal is the identity authority: overflow deduplication/acknowledgement scans it backwards in O(branch length) time and O(1) extra space; checkpoints restore bounded state without replaying all failures. No additional file/global registry or unbounded UI id/ack set is kept. Live record and workflow-owner observation uses weak keys, not retained records; recreated workflow display projections do not rescan acknowledged/overflow identities every tick. Each failure/ack transition writes one bounded checkpoint (up to 128 minimal details plus counters), trading journal bytes for bounded checkpoint replay; no all-time compaction is claimed. Old failures cannot leave a healthy-looking header or hide the widget.
252
+
253
+ Acknowledged means the existing `resultConsumed` flag was set by inline result delivery, `get_subagent_result`, RPC consume, or successfully closing the failed agent's FleetView conversation. A session-less/restored failure is acknowledged by selecting it to read its error or minimal-metadata toast. Restored workflow failures without a live inspector use the same toast acknowledgement. Merely delivering a completion notice, ticking, or a viewer-open failure does not acknowledge it. Failed workflow containers stay until their inspector closes successfully; paused runs stay blocked until resumed/stopped.
254
+
255
+ Enabled quota-parked schedules awaiting dispatch count as parked even with no agent record, and clear when dispatched, disabled or removed.
256
+
257
+ **Stop all:** `ctrl+shift+k` while the prompt editor has focus (the list collapsed or expanded) shows `Press ctrl+shift+k again within 3s to stop every running agent and workflow.`; a second press inside those three seconds stops every running or queued top-level agent, background survivor and workflow run, and says how many. Nothing else is touched: a later press re-arms instead, an auto-repeat never confirms, and a scheduled job that has not fired is not running. Claude Code's chord is `ctrl+x ctrl+k`, but pi binds `ctrl+x` to copy. It is the same stop as `/agents -> Stop all running`, and needs FleetView enabled.
258
+
259
+ Failed/parked/paused-workflow/fallback counts lead in a shared, uncut flag; at narrow widths these compact to `[E1 P2 B1 F1]` (failed, parked, blocked/paused, fallback). Compact counts above 99 read `99+`; spaces yield before alerts, so all four still fit at 20 columns (`[E99+P99+B99+F99+]`). Below 20 columns alerts yield in reverse priority (fallback, paused, parked), never ahead of the failure flag; if even its count cannot fit, the marker is `!`, never the misleading `[fb]`. Cost uses record-borne estimates only when `showCost` is on; it is the sum for the agents currently summarized, not a session ledger, and excludes workflow-container spend. Narrow lines drop trailing detail and the hint before leading alerts. An empty fleet still hides entirely; the standalone summary renderer has a one-line zero-count form.
260
+
261
+ At an empty prompt, `↓` or `←` temporarily reveals the list and starts navigation. `Esc`, selecting `main`, or typing returns to the explicit disclosure choice; dialogs and conversation overlays keep their keys. `ctrl+shift+a` while navigating collapses and returns to the editor. The separate above-editor activity widget (`widgetMode`) and transcript/result/notice renderers are unchanged.
262
+
263
+ Expanded, the existing navigable list renders **below** the editor:
223
264
 
224
265
  ```
225
- esc to interrupt · ← for agents · ↓ to manage
266
+ esc to interrupt · ← for agents · ↓ to manage · ctrl+shift+a to collapse
226
267
 
227
268
  ● main
228
269
  ○ workflow audit-src 12/40 agents · 32s · ↓ 26.4k tokens
229
- ○ general-purpose Sleep then report 1 11s · ↓ 13.1k tokens
230
- ○ general-purpose Sleep then report 2 11s · ↓ 13.1k tokens
231
- ↓ 3 more
270
+ ○ general-purpose Sleep then report 1 ▶ running · 11s · ↓ 13.1k tokens
271
+ ○ general-purpose Sleep then report 2 ▶ running · 11s · ↓ 13.1k tokens
272
+ ↓ 3 more: 2 running, 1 failed
232
273
  ```
233
274
 
234
- Running [workflows](#subagentworkflow) appear as a single `workflow` row above the agents, carrying their agent counts in place of a description. `Enter` on one opens the same two-pane inspector `/agents → Workflows` does, rather than a conversation overlay. A run's own agents are *not* listed separately — they belong to the run, which reports for them, so they are filtered out of the fleet list, the above-editor widget, the `/agents` menus and `@handle` resolution exactly as nested children are. They are also outside the `maxConcurrent` pool: the run has its own concurrency cap, and routing a fan-out through the session pool as well would let one workflow starve everything else. The agents are ordered earliest-launched first, and only agents you can actually open are shown (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press `↓` (or `←`) to move focus from the prompt into the list — the selected row is marked `●`, the rest `○`. The row block stays at five lines once the roster reaches five rows; overflowing lists show three rows plus `↑ N more` / `↓ N more` indicators (blank at an edge), without moving the content below it. The selected row renders in the theme's primary text color rather than the muted/dim treatment of the others; an agent with a configured `color` shows its badge there too, bolded. `↑`/`↓` move the selection, `Enter` opens the selected agent's live conversation overlay (it auto-updates as the agent works), and `Esc` (or `↑` above `main`) returns to the prompt. Selecting `main` returns to the normal view. Inside the overlay, press `Enter` to steer the running agent — type a message and `Enter` to send it (`Esc` or an empty submit returns), and it redirects the agent the same way the `steer_subagent` tool does. On a running foreground `Agent` call, press `b` to hand that run to the background; the hint only shows where the handoff is available. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures arrow keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.
235
- With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (`sonnet 4.6 · thinking: high · 11s · ↓ 13.1k tokens`), and a running row says what its in-flight model call is doing (see [Live activity](#live-activity)). On a virtual model such as `router/default` the row reads `router default (routing)` until the first response, then the physical model that answered. The thinking level shown is the session's selected level; a router chain entry's own `thinking` override is not visible to it.
275
+ Running [workflows](#subagentworkflow) appear as a single `workflow` row above the agents, carrying their agent counts in place of a description. `Enter` on one opens the same two-pane inspector `/agents → Workflows` does, rather than a conversation overlay. A run's own agents are *not* listed separately — they belong to the run, which reports for them, so they are filtered out of the fleet list, the above-editor widget, the `/agents` menus and `@handle` resolution exactly as nested children are. They are also outside the `maxConcurrent` pool: the run has its own concurrency cap, and routing a fan-out through the session pool as well would let one workflow starve everything else. The agents are ordered earliest-launched first, and only agents you can actually open, or outstanding failures you can inspect, are shown (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press `↓` (or `←`) to move focus from the prompt into the list — the selected row is marked `●`, the rest `○`. The row block stays at five lines once the roster reaches five rows; overflowing lists show three rows plus `↑ N more: 2 running` / `↓ N more: 1 queued, 1 failed` indicators that count the hidden rows by state, without moving the content below it. Unused indicator slots are padded below the content, so `main`, visible rows and nonempty indicators stay contiguous even at a scroll edge. Each agent row carries its state as a glyph and a word in a fixed first column (`▶ running`, `◦ queued`, `✓ done`, `✓ limit`, `✗ failed`, `✗ aborted`, `■ stopped`), so colour is never the only cue; a spawner's `name` leads the description (`pr-1412: Spec review ...`), the configured thinking level reads `think:high` so it cannot be mistaken for the live `thinking · 4s` activity, and columns are sized from every row, not the visible window. Model, thinking, activity, elapsed, token and cost cells all end on their shared column boundary, including when another row has no activity. A configured-model fallback keeps its shared leading `[fallback]` flag even with `showModel` off; column fitting reserves space for flags on hidden rows too, so scrolling cannot shift the tail. On a narrow terminal the description truncates first, then the activity, thinking and model columns drop (in that order). If a fallback row still cannot fit its stats, the shared leading flag compacts to `[fb]` or `!`, never a sliced fragment. For every row, after optional columns and any flag compaction, state words shrink to glyphs, separators and column padding collapse to spaces, and navigation padding disappear as needed. Elapsed, tokens and cost stay whole whenever they physically fit beside the marker (if present); if space is still insufficient, the state glyph and then elapsed yield before tokens or cost may clip. The selected row renders in the theme's primary text color rather than the muted/dim treatment of the others; an agent with a configured `color` shows its badge there too, bolded. `↑`/`↓` move the selection, `Enter` opens the selected agent's live conversation overlay (it auto-updates as the agent works), and `Esc` (or `↑` above `main`) returns to the prompt. Selecting `main` returns to the normal view. Inside the overlay, press `Enter` to steer the running agent — type a message and `Enter` to send it (`Esc` or an empty submit returns), and it redirects the agent the same way the `steer_subagent` tool does. On a running foreground `Agent` call, press `b` to hand that run to the background; the hint only shows where the handoff is available. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures arrow keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.
276
+ With [`showModel`](#persistent-settings) on, each row names the model and thinking level it runs at (`▶ running · sonnet 4.6 · think:high · 11s · ↓ 13.1k tokens`, and the above-editor widget writes the level the same way), and a running row says what its in-flight model call is doing (see [Live activity](#live-activity)). On a virtual model such as `router/default` the row reads `router default (routing)` until the first response, then the physical model that answered. The thinking level shown is the session's selected level; a router chain entry's own `thinking` override is not visible to it.
236
277
 
237
278
  ### Fullscreen conversation observer
238
279
 
@@ -352,6 +393,8 @@ Individual agent results render Claude Code-style in the conversation:
352
393
 
353
394
  Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
354
395
 
396
+ For a new `Agent` call, transcript directory and initial entry creation must succeed before the child is queued or started. A setup error fails the call without launching a child; subsequent streaming writes remain best effort. Nested `Agent` calls report the setup error inline.
397
+
355
398
  By default, foreground and background agents each stream their full conversation to a per-subagent transcript — a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications collapse to one line, with pi 1.0 drawing the `▸`/`▾` marker and toggling on click (the renderers draw none of their own):
356
399
 
357
400
  ```
@@ -361,13 +404,15 @@ By default, foreground and background agents each stream their full conversation
361
404
 
362
405
  Expanded, a notice adds `↻3 · 3 tools · 12.4k token · 4.1s`, the result preview, and the transcript as an OSC 8 hyperlink labelled `transcript ↗` (the path follows it as plain text). A group expands to one block per agent, plus a total when `showCost` is on. The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
363
406
 
407
+ A failed agent whose session survived is resumable: a provider or credential error (e.g. a transient `!command` API key failure) ends its turn but keeps the whole transcript. Its collapsed notice reads `✗ desc · resume <id> to continue · ...`, and both the expanded notice and the `<task-notification>` `<summary>` say `transcript intact; resume agent <id> to continue`. A run that failed before it had a session carries no such hint.
408
+
364
409
  ## Default Agent Types
365
410
 
366
411
  | Type | Tools | Model | Prompt Mode | Description |
367
412
  |------|-------|-------|-------------|-------------|
368
413
  | `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
369
- | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
370
- | `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
414
+ | `Explore` | read, grep, find, ls | `@cheap` | `replace` (standalone) | Fast codebase exploration (read-only); an unbound/unavailable tier visibly inherits the parent by default, or refuses under `modelResolution` `error` |
415
+ | `Plan` | read, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
371
416
 
372
417
  The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
373
418
 
@@ -399,7 +444,7 @@ An unreadable or unparseable agent file is skipped, not fatal — a warning name
399
444
  color: red
400
445
  description: Security Code Reviewer
401
446
  tools: read, grep, find, bash
402
- model: claude-opus-4-6
447
+ model: anthropic/claude-opus-4-6
403
448
  thinking: high
404
449
  max_turns: 30
405
450
  ---
@@ -436,28 +481,30 @@ All fields are optional — sensible defaults for everything.
436
481
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
437
482
  | `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
438
483
  | `isolation` | — | Set to `worktree` to run in an isolated git worktree, or `off` to refuse one even when the caller passes `isolation: "worktree"` (frontmatter is authoritative). Workflow scripts are the exception: as in Claude Code, only their `agent()` option decides. `none`, `no`, and `false` are accepted spellings of `off` |
439
- | `model` | inherit parent | Default model — `provider/modelId`, [machine-local alias](#machine-local-model-aliases), or fuzzy name (`"haiku"`, `"sonnet"`). An explicit `Agent({ model })` selection overrides this default. Resolved tolerantly (`.`/`-` and a trailing date stamp are interchangeable) and falls back to the same model under another provider if the named one doesn't have it |
484
+ | `model` | inherit parent | Default exact `provider/modelId`, [tier](#model-tiers) such as `@cheap`, or [legacy alias](#machine-local-model-aliases). An explicit `Agent({ model })` selection overrides this default. Configured values are never fuzzy matched |
440
485
  | `thinking` | inherit | Default thinking level: off, minimal, low, medium, high, xhigh, max — an explicit `Agent({ thinking })` overrides it for that run. Actual availability depends on your pi version and model; pi clamps unsupported levels down |
441
486
  | `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
442
487
  | `persist_session` | `subagents.json` `rememberAgents` (default `true`) | Persist this subagent as a normal pi session instead of keeping the session in memory only; overrides the `rememberAgents` project default in both directions. It records its spawning session as parent, so it nests under it in `/resume`. The subagent's `.output` transcript is still written either way unless `output_transcript: false` |
443
488
  | `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write this subagent's `.output` transcript; when set, overrides the `subagents.json` `outputTranscript` default. Set `false` to write no transcript file or path. Governs only the transcript — independent of `persist_session`, `isolation: worktree`, and `memory:` |
444
489
  | `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd. A session outside the parent's session directory (this override, or `isolation: worktree`) is listed separately, so it shows as a root instead of nesting |
445
- | `allowed_subagents` | none | Opt in to scoped nested `Agent`, `get_subagent_result`, and `steer_subagent` tools. Omitted / empty / `none` / `false` = no nesting; `all` (or `"*"` / `true`) = any enabled agent; comma-separated list = only those agent types |
490
+ | `allowed_subagents` | none | Opt in to scoped nested `Agent`, `get_subagent_result`, `steer_subagent`, and `stop_subagent` tools. Omitted / empty / `none` / `false` = no nesting; `all` (or `"*"` / `true`) = any enabled agent; comma-separated list = only those agent types |
446
491
  | `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
447
492
  | `inherit_context` | `false` | Fork parent conversation into agent |
448
493
  | `run_in_background` | — | Pin this agent to background (`true`) or foreground (`false`). Omit to follow `backgroundByDefault` |
449
494
  | `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
450
495
  | `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
451
496
 
452
- Frontmatter is authoritative for `max_turns`, `inherit_context`, `run_in_background`, `isolated`, and `isolation`. An agent file's `model` and `thinking` are portable defaults: an explicit `Agent({ model, thinking })` selection overrides either for that run. With no explicit thinking level, the definition wins, then the parent session supplies the fallback; model selection additionally follows the [`agentModels`](#per-agent-models) precedence. Other `Agent` tool parameters only fill fields the agent config leaves unspecified. Other spawn paths differ per field:
497
+ Frontmatter is authoritative for `max_turns`, `inherit_context`, `run_in_background`, `isolated`, and `isolation`. An agent file's `model` and `thinking` are portable defaults: an explicit `Agent({ model, thinking })` selection overrides either for that run. With no explicit thinking level, the definition wins; if it has none either, no level is passed and pi picks one at session creation (the per-model setting, else `defaultThinkingLevel`, clamped to the model), which does not follow the parent's live level. Only the `@mention` clone path starts from the parent's `ctx.thinkingLevel`. Model selection additionally follows the [`agentModels`](#per-agent-models) precedence. Other `Agent` tool parameters only fill fields the agent config leaves unspecified. Other spawn paths differ per field:
453
498
 
454
499
  - `isolated` holds on every spawn path, including `@handle` mentions, workflows and [cross-extension RPC](https://github.com/tintinweb/pi-subagents/blob/master/docs/rpc.md).
455
500
  - `isolation` holds on every path except workflows: as in Claude Code, only the script's `agent()` option decides there.
456
501
  - `inherit_context` applies only to `Agent` tool and nested spawns, including default-mode `@handle` mentions, which spawn through the `Agent` tool. As in Claude Code, where only a fork inherits the conversation, every other spawn starts fresh unless its caller asks: workflow children and scheduled runs never inherit, and an RPC caller opts in with `inheritContext`.
457
502
 
458
- **Fuzzy names pick the newest model.** A bare family name such as `"opus"` matches several ids; exact ids and [aliases](#machine-local-model-aliases) still win first, otherwise the candidates are narrowed to pi's `enabledModels` (when any candidate is enabled), then ranked by parsed version, newest first, then by match score. Versions are read from `claude-<family>-<n>[-<n>]` (`claude-opus-4-6`, `claude-opus-5`; a trailing `-YYYYMMDD` is a date, not a minor) and `gpt-<n>[.<n>]-<variant>` (`gpt-6.1-sol`); ids with no recognised scheme rank after every versioned one. Previously the shortest id won, so `"opus"` resolved to `claude-opus-5` even with `claude-opus-4-6` enabled and `claude-opus-5` not.
503
+ **Explicit non-tier tool inputs can use fuzzy names.** These pick the newest matching model; configured pins and tier/alias members remain exact. Numeric version runs retain their order and multiplicity before scope filtering or ranking: `sonnet-5-5` can match `claude-5-5-sonnet`, never `claude-4-5-sonnet`, `claude-5-sonnet` or `claude-5-6-sonnet`. A missing requested version is an error, not a neighbouring generation. Dots and dashes are equivalent; a major-only query can still select the newest minor within that major. A bare family name such as `"opus"` matches several ids; exact ids and [aliases](#machine-local-model-aliases) still win first, otherwise the candidates are narrowed to pi's `enabledModels` (when any candidate is enabled), then ranked by parsed version, newest first, then by match score. Ties use the lexicographically smallest case-sensitive `provider/model` identity, independently of registry order; exact id ties use the same ordering without scope filtering. An unresolved explicit input lists that same narrowed candidate set, so it does not suggest models the dispatch path would reject. Versions are read from `claude-<family>-<n>[-<n>]` (`claude-opus-4-6`, `claude-opus-5`; a trailing `-YYYYMMDD` is a date, not a minor) and `gpt-<n>[.<n>]-<variant>` (`gpt-6.1-sol`); ids with no recognised scheme rank after every versioned one. Previously the shortest id won, so `"opus"` resolved to `claude-opus-5` even with `claude-opus-4-6` enabled and `claude-opus-5` not.
459
504
 
460
- **Forgiving `model:` resolution.** A `model:` pin is matched against pi's model registry tolerantly, so cosmetic id variations don't silently drop the agent back to the parent's model: `.` and `-` are treated as equivalent in version numbers (`claude-haiku-4.5` ≡ `claude-haiku-4-5`), a trailing `-YYYYMMDD` date stamp is optional (`anthropic/claude-haiku-4-5-20251001` matches an undated registry id and vice-versa), and a `provider/modelId` whose named provider doesn't carry that model retries the bare id against every provider. Precedence is **exact → fuzzy under the named provider → same model under any provider → unavailable**, so an exact match always wins and dated snapshots aren't conflated. If nothing resolves, the pin can't run and the agent inherits the parent model — `/agents → Agent types` flags this case as `(unavailable, fallback: inherit)` and shows the resolved target `(→ provider/id)` when resolution lands on a different provider or version than configured. (This is distinct from [Model Scope](#model-scope) enforcement, which matches the `enabledModels` allowlist by *exact* entry.)
505
+ Agent spawn/results and completion notices echo the resolved `provider/model` identity from the run's retained selection, including nested results and resumes. Scheduled and quota-parked results echo none: they reselect at fire time. A literal virtual model is labelled as a routing alias with an unknown physical response model, not as proof of a physical leaf. Tier notes retain their existing admitted/answered provenance. Only identity strings are echoed, never registry model objects or credentials; identity alone does not trigger a fallback warning.
506
+
507
+ **Exact configuration, convenient tool inputs.** Agent definitions, `agentModels`, tier members and legacy alias candidates resolve exactly, without version ranking or provider retry. An unavailable configured pin inherits the parent with a visible note by default; `modelResolution.onUnavailable: "error"` refuses it instead. Explicit literal/fuzzy tool inputs retain fuzzy matching and unresolved names remain errors; `@tier` and legacy aliases follow `modelResolution`. Built-in Explore uses `@cheap`; bind it globally or in the enterprise manifest. Unbound tiers inherit with disclosure by library default; personal configurations should set both resolution policies to `error`. During enterprise migration only, built-in Explore's unbound `@cheap` can use the existing `modelAliases["claude-haiku-4-5"]` exact candidates as a static cheap tier. An explicit `cheap` binding always wins, including when unavailable; no neighbouring tier or newer model is inferred. User-pinned legacy Explore definitions retain the exact `anthropic/claude-haiku-4-5` compatibility binding. Users who pin Explore explicitly through `agentModels` (e.g. the owner's `agentModels.Explore`) are unaffected. The router's shipped `examples/subscription-router.json` supplies an opt-in ordered `cheap` profile (Haiku then Luna); no profiles are registered implicitly.
461
508
 
462
509
  ### Nested subagents
463
510
 
@@ -473,7 +520,7 @@ allowed_subagents: support-file-finder, support-callsite-tracer # or `all`
473
520
 
474
521
  **The allowlist is a privilege boundary, not just a routing hint.** A child runs with *its own* `tools:`, `extensions:`, and `isolated:` — the parent's restrictions are not inherited — so delegation grants the parent the union of what the listed agents can do. The read-only agent above can write and run commands through any listed agent that can, and `all` reaches every enabled agent including `general-purpose`. Choose the list as carefully as you would choose `tools:` itself; that is the main reason this is default-off.
475
522
 
476
- `allowed_subagents` is runtime-enforced. A comma-separated list restricts nesting to those types; `all` (or `"*"` / `true`, matching how `extensions:` and `skills:` take booleans) allows any enabled agent; omitted, empty, `none`, or `false` means no nested tools are injected at all. Unknown, disabled, and out-of-list types are rejected rather than falling back — regardless of the project's [fallback agent](#persistent-settings) setting, so a configured fallback can never hand a nested caller an agent outside its allowlist — and a nested `model:` is validated against [Model Scope](#model-scope) exactly like a top-level spawn. Result, resume, and steering operations are ownership-scoped, so a parent can control only its own children. Nested records remain internal to that parent and do not appear in top-level tools, lifecycle events, or agent UI — so when a parent finishes, is stopped, or ends a resumed turn, its nested children are stopped with it. They do write their own `.output` transcript (subject to the same `output_transcript` gate), filed under the root session's directory alongside their ancestors', so a nested run can still be inspected after the fact. Their token usage is folded into every ancestor's totals up to the top-level agent (lifecycle events, completion notifications, `/agents`), so nested spend stays attributable at any depth even though the children themselves stay hidden. A nested result that ends `stopped`, `aborted`, or `steered` is labelled as partial, the same guarantee top-level results carry.
523
+ `allowed_subagents` is runtime-enforced. A comma-separated list restricts nesting to those types; `all` (or `"*"` / `true`, matching how `extensions:` and `skills:` take booleans) allows any enabled agent; omitted, empty, `none`, or `false` means no nested tools are injected at all. The same holds when a listed allowlist resolves to no enabled agent (every name missing or disabled, or `all` over an empty registry): the child gets no nested tools and the prompt says nesting is off, instead of an `Agent` tool whose only advertised type is `none`. Unknown, disabled, and out-of-list types are rejected rather than falling back — regardless of the project's [fallback agent](#persistent-settings) setting, so a configured fallback can never hand a nested caller an agent outside its allowlist — and a nested `model:` is validated against [Model Scope](#model-scope) exactly like a top-level spawn. Result, resume, steering, and stop operations are ownership-scoped, so a parent can control only its own children. Nested records remain internal to that parent and do not appear in top-level tools, lifecycle events, or agent UI — so when a parent finishes, is stopped, or ends a resumed turn, its nested children are stopped with it. They do write their own `.output` transcript (subject to the same `output_transcript` gate), filed under the root session's directory alongside their ancestors', so a nested run can still be inspected after the fact. Their token usage is folded into every ancestor's totals up to the top-level agent (lifecycle events, completion notifications, `/agents`), so nested spend stays attributable at any depth even though the children themselves stay hidden. A nested result that ends `stopped`, `aborted`, or `steered` is labelled as partial, the same guarantee top-level results carry.
477
524
 
478
525
  **No background work inside a subagent.** A nested `Agent` call always blocks and returns the child's result inline. `run_in_background: true` returns an error (a frontmatter `run_in_background: true` on the nested type is ignored and the child runs in the foreground), and the `workflow` / `Workflow` / `SubagentWorkflow` tools, including another extension's, return an error explaining why when a subagent calls them. A child gets a refusing `SubagentWorkflow` stand-in for this (one short tool definition), since the real tool is excluded and core would otherwise answer "Tool SubagentWorkflow not found" before the refusal could run; an isolated or `extensions: false` child gets no workflow tool at all. Both would deliver their completion to a session that ends the moment the subagent returns its answer, and a detached child is aborted with its owner anyway.
479
526
 
@@ -521,6 +568,8 @@ tools: "*, ext:mcp/search"
521
568
  isolated: true # hermetic: built-ins only, no extensions/skills/context
522
569
  ```
523
570
 
571
+ The embedded `Explore` and `Plan` definitions use only `read`, `grep`, `find`, and `ls`, with `extensions: false`. They deliberately omit `bash`: a prompt cannot make an unrestricted shell read-only. Their tool restriction is an enforced child-tool surface, not an operating-system or filesystem sandbox; custom overrides must choose any extension selectors deliberately.
572
+
524
573
  A few rules the examples don't make obvious:
525
574
 
526
575
  - `extensions:` is the sole loading authority. `ext:foo` in `tools:` narrows what surfaces; it can't load `foo` on its own. Mismatches fire `extension-error:…` warnings.
@@ -547,7 +596,11 @@ A few rules the examples don't make obvious:
547
596
  | `none` with `isolated: true` or `extensions: false` | no | `none` |
548
597
  | `none`, or only `ext:` entries | yes | `no built-ins, extension tools only` |
549
598
 
550
- The last two rows are separate because zero built-ins is not zero tools: `tools: none` alongside `extensions:` still surfaces every extension tool, so calling it `none` would understate what the agent can do. Without the `+ extension tools` flag an agent that can search the web through an extension (e.g. `Plan`) read as web-less.
599
+ When the effective built-in set lacks `bash` (including `disallowed_tools: bash`), the shared suffix adds `; no built-in shell: cannot run bash commands`. This applies to every type in full, compact and custom-template type lists. It does not claim extensions cannot execute commands: an extension may supply `ssh_exec` or another command tool. Put stronger restrictions in the agent description's **first sentence** so compact mode preserves them, e.g. `no shell/ssh: cannot run commands`. Shell-less prompts should require a FIRST-turn explanation, before tool calls, when a task needs unavailable commands. This is prompt guidance, not a dispatcher classifier.
600
+
601
+ `Agent` has no per-call tool-override flag. Use a shell-capable type for command work; do not ask a restricted researcher to acquire `bash`.
602
+
603
+ The last two rows are separate because zero built-ins is not zero tools: `tools: none` alongside `extensions:` still surfaces every extension tool, so calling it `none` would understate what the agent can do. Without the `+ extension tools` flag, a custom agent configured to search the web through an extension would read as web-less.
551
604
 
552
605
  ### Shipping agents in a pi package
553
606
 
@@ -598,7 +651,7 @@ Launch a sub-agent.
598
651
  | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
599
652
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
600
653
  | `run_in_background` | boolean | no | Defaults to `true`; `false` blocks and returns the result inline, until [`ctrl+b`](#sending-a-foreground-agent-to-the-background) hands it to the background |
601
- | `resume` | string | no | Agent ID to resume a previous session. The resumed run gets a fresh `max_turns` budget; its existing model and thinking level remain in effect, so this call's `model` and `thinking` are ignored |
654
+ | `resume` | string | no | Agent ID to resume a previous session. The resumed run gets a fresh `max_turns` budget; its existing model and thinking level remain in effect, so this call's `model` and `thinking` are ignored. An evicted agent whose session was persisted ([`rememberAgents`](#persistent-settings)) is reopened from disk like `@handle`, under a NEW agent id with the model selected anew and this call's `max_turns` applied. It runs only in the background: with `run_in_background: false` the call is refused and says so. The old id keeps resolving to the newest id of that conversation, so a second resume while it runs is answered with that id and status instead of reopening the file twice, and the chain survives the new record's own eviction. The result names the reopened session file, which is the transcript; no `.output` file is written for it |
602
655
  | `isolated` | boolean | no | No extension/MCP tools |
603
656
  | `isolation` | `"off"` \| `"worktree"` | no | `worktree` runs in an isolated git worktree; `off` (the default) does not. Absent from the schema entirely when `worktreeIsolation: false` |
604
657
  | `cwd` | string | no | Working directory for the agent: absolute, `~/`-prefixed, or relative to the session cwd. Default: the session cwd. With `isolation: "worktree"`, the worktree comes from the repo containing it; see [Worktree Isolation](#worktree-isolation). Not combinable with `schedule` |
@@ -657,13 +710,25 @@ Cancelling a `wait: true` call (for example, with `Esc`) stops only the wait. Th
657
710
 
658
711
  ### `steer_subagent`
659
712
 
660
- Send a steering message to a running agent. The message interrupts after the current tool execution. A queued agent (waiting on `maxConcurrent`) accepts it too: the result says it was queued, and it is delivered into the agent's first turn when it starts.
713
+ Send a message to an agent, like Claude Code's `SendMessage`. A running agent is interrupted after the current tool execution. A queued agent (waiting on `maxConcurrent`) accepts it too: the result says it was queued, and it is delivered into the agent's first turn when it starts.
714
+
715
+ A finished agent (`completed`, `steered`, `aborted` at the turn limit, or `error`) is resumed instead: the call runs `Agent` with `resume` set to that agent, `run_in_background: true` and `message` as the prompt, so the result is a background resume's (`Agent resumed in background.`, the agent id, a completion notice to follow) and its full history is kept. That includes reopening an evicted agent's persisted session under a new id (see `resume`). A stopped agent (`stopped`, by the user or by [`stop_subagent`](#stop_subagent)) is refused, not resumed. An agent whose own file sets `run_in_background: false` resumes in the foreground, as `Agent` would. Nested agents keep their own scoped `steer_subagent`, which still only steers a running child.
661
716
 
662
717
  | Parameter | Type | Required | Description |
663
718
  |-----------|------|----------|-------------|
664
719
  | `agent_id` | string | yes | Agent ID to steer |
665
720
  | `message` | string | yes | Message to inject into agent conversation |
666
721
 
722
+ ### `stop_subagent`
723
+
724
+ Stop a running or queued background agent, like Claude Code's `TaskStop`. A steer asking an agent to stop is read only after its current tool call and can be ignored; this cannot. It goes through the same stop as a viewer's `x`: the run is aborted at once, its current tool call gets the abort signal, and it starts no further tool call, so the record ends `stopped`. The tool returns that state (type, tool uses, tokens). A running agent then sends one completion notice, flagged `stopped by the parent with stop_subagent` rather than `STOPPED BY THE USER`, carrying its partial output and, under worktree isolation, the branch its changes were saved to. It counts as read like any `stopped` notice (see [Completion notice results](#completion-notice-results)). A queued agent never starts, frees its place in the queue, and sends no notice; it still emits `subagents:failed` with `status: "stopped"` so event listeners see it end.
725
+
726
+ `agent_id` takes an id, handle or name, like `steer_subagent`, and reaches a background agent that survived a session switch. A `SubagentWorkflow` Task ID stops that run and its agents, and its notice says `Stopped by the parent with stop_subagent`. An unknown id, or one that is no longer running, is an error that lists every running or queued agent (id, `@handle`, status, description) and workflow run. Ownership is unchanged: a nested or workflow child is not reachable from the top-level tool. A nested parent gets its own scoped `stop_subagent` for its own children. Like the other orchestration tools, it is excluded from a child's tools unless the child sets `allowed_subagents`. A stopped agent is not resumed by `steer_subagent`. The tool declares `readOnlyHint: true`, so [Plan mode](../plan-mode/README.md) admits it by default: it edits nothing and starts nothing, it only ends work already running, so a planning session can still stand a fan-out down. A second stop of the same agent says it is already `stopped`, and of a workflow still unwinding, that it is already stopping. Runs of another extension's `workflow` tool are outside this package, and the tool description says so: `@quintinshaw/pi-dynamic-workflows` stops its own with `workflow_control` action `stop`.
727
+
728
+ | Parameter | Type | Required | Description |
729
+ |-----------|------|----------|-------------|
730
+ | `agent_id` | string | yes | Agent ID, handle or name to stop, or a `SubagentWorkflow` Task ID |
731
+
667
732
  ## Commands
668
733
 
669
734
  | Command | Description |
@@ -711,12 +776,14 @@ The `/agents` command opens an interactive menu:
711
776
 
712
777
  ```
713
778
  Running agents (2) — 1 running, 1 done ← only shown when agents exist
779
+ Stop all running (1) ← only shown while something runs or is queued
714
780
  Agent types (6) ← unified list: defaults + custom
715
781
  Create new agent ← manual wizard or AI-generated
716
782
  Settings ← max concurrency (background + foreground), max turns, grace turns, join mode
717
783
  ```
718
784
 
719
785
  - **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering composer, then `Enter` again to send a message that redirects the agent (same mechanism as the `steer_subagent` tool; `Esc` or an empty submit returns). On a live foreground run, press `b` to hand the same session and in-flight work to the background without stopping or restarting it — the blocked `Agent` call returns the agent ID and the run notifies on completion like any background agent. Press `x` (then `x` again to confirm) to stop/abort it — including **background** agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground `Agent` call, but no longer reaches one that has been handed off — `x` here does). A stopped agent reports its partial output flagged as incomplete, not as a completion. `m` cycles how much of the transcript renders as Markdown — see [Viewer markdown](#persistent-settings).
786
+ - **Stop all running** — after a confirmation, stops every running or queued top-level agent, background survivor of a session switch, and workflow run (with its agents), the same as the FleetView stop-all chord. A scheduled job that has not fired yet is not running and is left alone.
720
787
  - **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `▪` ([package](docs/packages.md)), `✕` (disabled), explained by a legend naming only the badges actually on screen. Each row shows the agent's model, and the highlighted agent's full description appears below the list — prefixed with the declaring package's name for a `▪` agent, since the badge alone doesn't say *which* package. The model column flags `(unavailable, fallback: inherit)` when a configured model can't be resolved (it would silently inherit the parent model), and shows `(→ provider/id)` when it resolves to a different provider or version than configured. Select an agent to manage it:
721
788
  - **Default agents** (no override): Eject (export as `.md`), Disable
722
789
  - **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
@@ -729,7 +796,7 @@ Settings ← max concurrency (background + fo
729
796
 
730
797
  ## Record retention
731
798
 
732
- A finished agent's record (and the session behind `get_subagent_result` and `steer_subagent`) is kept for **10 minutes once its result was consumed** (fetched with `get_subagent_result`, or delivered inline to a blocking caller) and **60 minutes from completion while nobody has read it**. After that the record is evicted, and the manager keeps an in-memory note for the rest of the session: both tools answer `Agent "<id>" (<description>) completed, evicted after N min; transcript: <path>` instead of `Agent not found`. The path is the `.output` transcript when one was written, else the session file. Notes are cleared on `/new` and `/resume`, like the records. For tests, `PI_SUBAGENTS_CONSUMED_TTL_MIN` and `PI_SUBAGENTS_UNREAD_TTL_MIN` override the two windows in minutes (fractions and `0` allowed; read when the manager starts); the sweep then runs as often as the shorter window needs, but not more than once a second.
799
+ A finished agent's record (and the session behind `get_subagent_result` and `steer_subagent`) is kept for **10 minutes once its result was consumed** (fetched with `get_subagent_result`, or delivered inline to a blocking caller) and **60 minutes from completion while nobody has read it**. After that the record is evicted, and the manager keeps an in-memory note for the rest of the session: `get_subagent_result` answers `Agent "<id>" (<description>) completed, evicted after N min; transcript: <path>` instead of `Agent not found`, and an `Agent` `resume` or `steer_subagent` that has no persisted session to reopen prefixes that note with `Cannot resume agent "<id>": no saved session is left to reopen.` A `resume` or `steer_subagent` of an evicted agent whose session was persisted reopens it instead (see `resume` above). The path is the `.output` transcript when one was written, else the session file. A nested or workflow child leaves no note, so it stays `Agent not found` to these top-level tools after eviction as before it. When the sweep evicts a top-level agent whose result was **never read**, the parent also gets a `subagent-reaped` message on its next turn (no turn is triggered) carrying that note, so the loss is announced instead of discovered by a later lookup; a read result, a failure whose whole text the completion notice already carried, an agent whose latest run never started (stopped while queued, refused on an already-aborted signal, or failed before the runner began; a resumed one included), a result handed to a blocking caller inline (a foreground resume included), a nested or workflow child, and a record cleared at a session boundary send nothing, and neither does a sweep on a manager a session switch left behind for its running survivors (its `pi` is stale, so the send is dropped; not announced from there, and the new session's manager does not know those records). Running and queued agents are never evicted. Notes are cleared on `/new` and `/resume`, like the records. A background agent that finishes while its session is switched away (`/new`, `/resume`, `/reload`) leaves the same note, kept once its result is delivered to the reopened session, so the id in its notice still resolves. When it also has a saved session, the delivery registers it as reopenable too, so `resume` or `steer_subagent` on that id continues the conversation instead of reporting that no session is left; a name already held by a live agent, an evicted conversation or a reserved survivor stays with its holder, and the id then answers `Cannot resume agent "<id>": its name "<handle>" now belongs to another agent, so its saved session was not kept reopenable.` For tests, `PI_SUBAGENTS_CONSUMED_TTL_MIN` and `PI_SUBAGENTS_UNREAD_TTL_MIN` override the two windows in minutes (fractions and `0` allowed; read when the manager starts); the sweep then runs as often as the shorter window needs, but not more than once a second.
733
800
 
734
801
  ## Graceful Max Turns
735
802
 
@@ -795,6 +862,68 @@ When background agents complete, they notify the main agent. As in Claude Code,
795
862
  **Configuration:**
796
863
  - Configure join mode in `/agents` → Settings → Join mode
797
864
 
865
+ ### Completion notice results
866
+
867
+ A completion notice carries the agent's whole result, as Claude Code's does, up to `notificationResultChars` per agent (default `16000`). For a failed agent that is its error plus any partial output, the same text `get_subagent_result` returns. A result delivered whole counts as read, like a foreground result: no `get_subagent_result` is needed and the record gets the 10-minute read retention instead of 60 minutes unread. **A failure (`error` or `aborted`) never counts as read from a notice**, however short: delivering a completion notice does not acknowledge a failure in [FleetView](#fleetview), so it stays flagged, with the 60-minute unread retention, until `get_subagent_result`, RPC consume or closing its FleetView conversation acknowledges it. A `stopped` agent's partial output is not a failure and counts as read like any other.
868
+
869
+ The cap counts the raw result characters, before XML escaping (`<` is one char, not the four of `&lt;`); the truncation marker is extra. Past the cap the notice keeps the head and the tail of the result around one marker line, `...(truncated: N of M chars omitted; full result in <output-file>, or use get_subagent_result)...`, and the result stays unread. Notices that land in one message, a `group` batch or completions coalesced at one boundary, share a budget of four caps (64k chars by default): each result takes what it needs up to an even split of what is left, so a short result is never cut to make room for a long one. A notice spooled across a session switch carries the same body and is clipped when it is delivered, so survivors that land together after `/resume` share that budget too. Their records live in the orphaned instance, so they are not marked read.
870
+
871
+ **Trust model:** the `<result>` is the subagent's output, and a subagent reads files, web pages and tool output written by anyone, so the result can carry text crafted to look like instructions. It is XML-escaped, so it cannot close its own `<result>` or forge another notice's tags, and a `<result-note>` line before it tells the parent to evaluate it as data, not follow it. That is framing, not a sandbox: a full result puts more third-party text in the parent's context than a 500-char preview did, so treat agents that read untrusted input accordingly, or set `notificationResultChars: 0`.
872
+
873
+ Only the model-facing `<result>` grows; the rendered notification box keeps its 500-char preview. `notificationResultChars: 0` restores the old result: a 500-char preview (300 per `group` member) with a pointer to `get_subagent_result`, and nothing is marked read. The `<result-note>` line stays in either mode. Set it from `/agents` → Settings → Notice result chars (0 to 1000000).
874
+
875
+ ## Model tiers
876
+
877
+ Use `model: "@cheap"` in an agent, `agentModels`, `Agent({ model: "@cheap" })`, RPC or workflow `agent()` options. Explicit tool models still win. Tiers are independent of `enabledModels`, fuzzy matching, model versions and provider naming conventions.
878
+
879
+ Bindings live ONLY in global `~/.pi/agent/subagents.json` or the `subagents` object of an owner-only `enterprise.local.json`. Project tier definitions and resolution policies are ignored with a warning. Keys match `^[a-z0-9][a-z0-9._-]*$` and are case sensitive. Each nonempty list contains either exactly one registered `router/<profile>`, or only exact physical `provider/id` members. No mixtures, nested tiers, fuzzy entries or implicit neighbouring tiers. Exact model IDs may contain revision markers such as `@20240620`; a bare `@other-tier` binding entry is still rejected. Invalid bindings are dropped with a warning; references to them are `unmapped`.
880
+
881
+ Routed personal setup (configure the router profiles separately):
882
+
883
+ ```json
884
+ {
885
+ "modelTiers": { "cheap": ["router/cheap"], "default": ["router/default"], "deep": ["router/deep"], "frontier": ["router/frontier"], "review": ["router/review"] },
886
+ "modelResolution": { "onUnmapped": "error", "onUnavailable": "error" },
887
+ "agentModels": { "*": "@default", "Explore": "@cheap", "code-reviewer": "@review" }
888
+ }
889
+ ```
890
+
891
+ A routed tier starts on its virtual model, not a physical chain member. The router owns headroom ranking, admission and mid-run failover. Exhaustion never tries `router/default` or another tier: it follows the existing quota parking/error rules. Missing router profiles are `unavailable`, not fuzzy guesses.
892
+
893
+ GPT-only setup, with no Anthropic dependency or router required:
894
+
895
+ ```json
896
+ { "modelTiers": { "cheap": ["openai/gpt-6-luna"] }, "agentModels": { "Explore": "@cheap" } }
897
+ ```
898
+
899
+ Enterprise manifest (machine-local, permissions `0600`):
900
+
901
+ ```json
902
+ { "version": 1, "subagents": { "modelTiers": { "cheap": ["example-provider/example-wire-model"] }, "agentModels": { "Explore": "@cheap" } } }
903
+ ```
904
+
905
+ Enterprise `frontier` binding, two exact flagship members in order (placeholders):
906
+
907
+ ```json
908
+ { "version": 1, "subagents": { "modelTiers": { "frontier": ["example-provider/example-flagship-a", "example-provider/example-flagship-b"] } } }
909
+ ```
910
+
911
+ `@frontier` is the tier for the hardest design and architecture work. Skills and agents name `@frontier` on every profile; only this binding differs. Leave it unbound when no flagship is served: an unbound `@frontier` follows `modelResolution.onUnmapped`, so the work default inherits the parent model with a visible note, and the personal `"error"` refuses it. It never falls back to `@deep` or another tier.
912
+
913
+ Ordered static list:
914
+
915
+ ```json
916
+ { "modelTiers": { "cheap": ["provider-a/fast-model", "provider-b/fast-model"] } }
917
+ ```
918
+
919
+ Static tiers choose the first credentialed, admitted member and retain skipped members with `no-credentials` or `quota` reasons. With a router installed, each member is admitted separately. Without one, admission fails open and disclosure explicitly says `static; no quota signal`.
920
+
921
+ `modelResolution.onUnmapped` and `onUnavailable` each accept `error` or `inherit`; both library defaults are `inherit`, with a visible note. Neither policy applies to quota exhaustion. Whitespace-only model inputs mean unset; `@` alone and unknown/case-mismatched tier names are visibly unmapped.
922
+
923
+ The `Agent` tool guideline and its `model` parameter list the configured tier keys, e.g. `Configured tiers: @cheap, @deep, @default, @frontier, @review. Prefer a tier over a model id.`, never their members. With no tiers configured they keep the generic tier / `provider/modelId` / fuzzy-name text.
924
+
925
+ Results disclose `Model: @cheap -> provider/id via router/cheap` before the first response using admission's `resolvedModel`, then follow the answering model. Mid-run changes say `switched ... within tier`. Shared leading `[@cheap]` flags identify healthy tiers neutrally; warning flags and grouped alert counts cover only actual switches, parent inheritance, unavailable or unmapped outcomes. Structured `modelTier` outcomes survive records, events, transcripts, eviction and workflow journals. Reopened evicted sessions select/admit anew and retain old outcomes separately as `previousModelTier` and `previousModelFallback`; live resumes keep the existing session's selection. Scheduled tier jobs store `@cheap`, not today's physical pick, and resolve it again when they fire.
926
+
798
927
  ## Machine-local model aliases
799
928
 
800
929
  `~/.pi/agent/enterprise.local.json` is an optional, owner-only (`0600`) machine-local manifest for provider configuration that must never enter a repository. Its `subagents.modelAliases` section maps a logical name to ordered canonical `provider/modelId` candidates. The extension loads it only from the user agent directory, never from a project, and only accepts manifest version `1`. The same `modelAliases` key is also accepted in the global `~/.pi/agent/subagents.json` (when both define it, the manifest's whole set replaces the global one); a project `.pi/subagents.json` value is ignored with a warning, so a cloned repo cannot redirect which provider a subagent runs on.
@@ -810,17 +939,21 @@ When background agents complete, they notify the main agent. As in Claude Code,
810
939
  }
811
940
  ```
812
941
 
813
- Alias candidates are resolved in order against authenticated models; Pi sessions and schedules retain only the selected canonical ID. Missing, malformed, group-readable, or unsupported manifests are ignored with a safe path-and-reason warning. Aliases may not contain `/`; candidates must be canonical IDs, so aliases cannot recurse. A request whose alias has no available candidate fails with its alias and candidate IDs, never credential data.
942
+ Alias candidates are resolved in order against authenticated models. Agent records retain the requested input and selection outcome alongside the selected model; schedules retain the explicit request and resolve it again at fire time. Missing, malformed, group-readable, or unsupported manifests are ignored with a safe path-and-reason warning. Aliases may not contain `/`; candidates must be canonical IDs, so aliases cannot recurse. Legacy aliases are exact static tiers with admission and skip disclosure, not fuzzy shortcuts. An unavailable alias follows `modelResolution.onUnavailable`, never exposing credential data.
814
943
 
815
944
  ## Per-agent models
816
945
 
817
- `agentModels` in the global `~/.pi/agent/subagents.json` (or the `subagents` section of `enterprise.local.json`) maps an agent `type`, or `"*"` for every type, to a model: any form `Agent({ model })` takes (`provider/modelId`, alias, fuzzy name). Types match case-insensitively.
946
+ `agentModels` in the global `~/.pi/agent/subagents.json` (or the `subagents` section of `enterprise.local.json`) maps an agent `type`, or `"*"` for every type, to an exact `provider/modelId`, `@tier`, or legacy alias. Agent types match case-insensitively; tier names do not.
818
947
 
819
948
  ```json
820
- { "agentModels": { "Plan": "opus", "Explore": "haiku", "*": "sonnet" } }
949
+ { "agentModels": { "Plan": "@deep", "Explore": "@cheap", "*": "@default" } }
821
950
  ```
822
951
 
823
- Precedence, highest first: the `Agent` tool's `model` parameter, `agentModels[type]`, `agentModels["*"]`, the agent definition's `model:`, the parent's model. It applies on every spawn path (top-level, nested, workflow `agent()`). A value that does not resolve falls back down the chain silently, like a definition default. Like `modelAliases` it chooses providers, so a project `.pi/subagents.json` value is ignored with a warning.
952
+ Precedence, highest first: the `Agent` tool's `model` parameter, `agentModels[type]`, `agentModels["*"]`, the agent definition's `model:`, the parent's model. The shared `selectModel` module applies it on every fresh spawn: top-level and nested `Agent`, workflow `agent()`, scheduled jobs, RPC, direct `@mention` and reopened evicted sessions. `/agents` labels use the same configured selection. An unresolved configured value falls directly to the parent, not a lower-priority configured default, with disclosure: the spawn path records `Note: configured model "<input>" did not resolve; running on parent model <provider>/<id>.` on the agent, and every surface reads it from there: the `Agent` result text and expanded row, `get_subagent_result`, the completion notice, nested `Agent` results, workflow rows and `agent()` results (text, error and a `modelNote` field), the widget, fleet list and conversation viewer, completion events, and agents started by scheduler fire, cross-extension RPC or `@mention`. A live-session resume retains its record's selection and note, ignoring new model inputs and config. An evicted conversation's `modelTier`, `modelFallback` and every model note describe the continuation's actual outcome; `previousModelTier` and `previousModelFallback` separately retain the prior run's outcomes, never as substitutes for a missing or changed binding. Explicit unresolved literal/fuzzy model names are hard errors on every fresh dispatch, including scheduled fires; tiers and legacy aliases follow `modelResolution`. Configured inputs and alias members are exact; only explicit non-tier tool inputs retain fuzzy ranking and provider retry. Tier failures obey `modelResolution`. A workflow `agent({ schema })` result stays pure JSON: the note is a sibling `_modelNote` on an object payload and a line in the run log. Like `modelAliases` it chooses providers, so a project `.pi/subagents.json` value is ignored with a warning.
953
+
954
+ - **Intentional blank-input change:** empty and whitespace-only model strings mean unset at every precedence level and on every entry path, falling through to the next default without recording a `param` or `definition` request. Previously RPC/workflow explicit blanks and runner-based configured blanks reached fuzzy resolution (and could select an available model); Agent/nested/scheduled paths skipped `""` but resolved whitespace. Nonblank inputs retain their original spelling and matching.
955
+
956
+ At narrow terminal widths, the workflow dialog reserves its agent-list fallback disclosure: `[fallback]` compacts to `[fb]`, then `!`, rather than showing a clipped fragment. The detail pane middle-elides unusually long fallback notes so both the configured input and final diagnosis stay visible. The full note remains available in the result text, the record, the run log and `get_subagent_result`.
824
957
 
825
958
  ## Model Scope
826
959
 
@@ -847,12 +980,12 @@ When on, each subagent spawn's effective model is validated against pi's own `en
847
980
 
848
981
  ## Persistent Settings
849
982
 
850
- Runtime tuning values set via `/agents` → Settings (max concurrency, max foreground concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, strict agent files on/off, package agents on/off, package workflows on/off, agent mentions on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off, usage reporting on/off, cost display on/off, model display on/off, viewer markdown off/assistant/all) persist across pi restarts. Two files, merged on load:
983
+ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreground concurrency, default max turns, grace turns, nested depth, notice result chars, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, strict agent files on/off, package agents on/off, package workflows on/off, agent mentions on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off, usage reporting on/off, cost display on/off, model display on/off, viewer markdown off/assistant/all) persist across pi restarts. Two files, merged on load:
851
984
 
852
985
  - **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
853
986
  - **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
854
987
 
855
- **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
988
+ **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, notice result chars `16000`, join mode `smart`, defaults enabled).
856
989
 
857
990
  **Process budget** (`maxActiveSubagents`, default `0` = off; `nestedSubagentReserve`, default `2`): the process-wide ceiling described under [Process budget](#process-budget). Global configuration only — not written by `/agents`, and ignored with a warning in a project file.
858
991
 
@@ -868,7 +1001,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreg
868
1001
 
869
1002
  **Agent mentions** (`agentMentions`, default `"model"`): whether [`@handle message`](#agent-mentions) at the prompt addresses that subagent instead of the main model — messaging, resuming or starting it — and whether `@` offers agents alongside pi's file completion. `"model"` and `"direct"` differ only in [who starts an agent that isn't running](#starting-a-new-agent): an off-screen clone of this conversation, via a `<system-reminder>` and a real `Agent` call, or this extension, immediately and with no model call. Messaging and resuming are direct in both. `"off"` gates all three actions plus the suggestion list, so `@` means only "attach a file" again and every `@…` prompt reaches the main model verbatim. Toggle via `/agents → Settings → Agent mentions`; applied live. The booleans this setting used to take are still read — `true` as `"model"`, `false` as `"off"`.
870
1003
 
871
- **Background by default** (`backgroundByDefault`, default `true`): what an `Agent` call that doesn't say means. On — following Claude Code — the agent runs detached, the call returns its ID immediately, and a completion notification carries a preview of the result (`get_subagent_result` for the full text). Set `false` to restore the previous behaviour, where an unqualified spawn blocked the turn and returned its output inline. An explicit `run_in_background` on the call, or in an agent file's frontmatter, overrides this in both directions; the setting only decides what "unspecified" means. **Top-level only** — a nested spawn (an agent spawning its own) always defaults to foreground, because a detached child is stopped when its parent settles and has no notification path of its own. Toggle via `/agents → Settings → Background by default`; applied live.
1004
+ **Background by default** (`backgroundByDefault`, default `true`): what an `Agent` call that doesn't say means. On — following Claude Code — the agent runs detached, the call returns its ID immediately, and a completion notification carries the result up to [`notificationResultChars`](#completion-notice-results) (`get_subagent_result` for anything it had to cut). Set `false` to restore the previous behaviour, where an unqualified spawn blocked the turn and returned its output inline. An explicit `run_in_background` on the call, or in an agent file's frontmatter, overrides this in both directions; the setting only decides what "unspecified" means. **Top-level only** — a nested spawn (an agent spawning its own) always defaults to foreground, because a detached child is stopped when its parent settles and has no notification path of its own. Toggle via `/agents → Settings → Background by default`; applied live.
872
1005
 
873
1006
  **Remember agents** (`rememberAgents`, default `true`): whether subagents persist their pi session, which is what lets [`@handle`](#agent-mentions) reopen an agent's conversation after its in-memory record has been evicted. Two visible consequences of the default: top-level subagents write a session file, and they nest under the session that spawned them in pi's `/resume`. Agents spawned by another agent are excluded — they get no handle, so nothing could reopen their transcript. A custom agent's `persist_session` frontmatter overrides this per agent, in both directions. Toggle via `/agents → Settings → Remember agents`; with it off, handles expire with their record (roughly ten minutes past completion) and `@explore` then starts a fresh agent rather than resuming — the behaviour before this setting existed.
874
1007
 
@@ -893,10 +1026,11 @@ The `~` marks it as pi's estimate rather than a billed figure. **A cost is shown
893
1026
 
894
1027
  Independent of `reportUsage`: this one is what you read, that one is what your session counts. Toggle via `/agents → Settings → Show cost`; applied live.
895
1028
 
896
- **Show model** (`showModel`, default `false`): whether the widget's running rows and the [FleetView](#fleetview) rows name the model driving each agent and the thinking level it is running at. Toggling it refreshes both:
1029
+ **Show model** (`showModel`, default `false`): whether the widget's running rows and the [FleetView](#fleetview) rows name the model driving each agent and the thinking level it is running at. Toggling it refreshes both. The widget row, then the FleetView row. Both write the level `think:high` so it cannot read as a duplicate of the live `thinking · 4s` activity:
897
1030
 
898
1031
  ```text
899
- ├─ ⠹ Explore inspect code · sonnet 4.6 · thinking: high · ↻3 · 8.2k token · 4.1s
1032
+ ├─ ⠹ Explore inspect code · sonnet 4.6 · think:high · ↻3 · 8.2k token · 4.1s
1033
+ ○ Explore inspect code ▶ running · sonnet 4.6 · think:high · thinking · 4s · 41s · ↓ 8.2k tokens
900
1034
  ```
901
1035
 
902
1036
  Off by default because the row already carries the description, turns, tool uses, tokens and elapsed time, and every character it gains is one the description loses on a narrow terminal. The other surfaces show the pair either way: the `Agent` tool result names the model beside its tags, and the conversation viewer's `↳` row spells out the canonical `provider/model-id`.
@@ -927,6 +1061,8 @@ Turn `all` on for tools that genuinely emit Markdown, and off again for a diff o
927
1061
 
928
1062
  Leaving it unset is not quite the same as `true`. Unset means *auto*: on, unless another extension already provides a workflow tool, in which case this one warns and stands down for the session. Two orchestrators in one tool spec is a worse default than none — the model has to guess which to call and pays for both descriptions to find out — and the extension that was installed on purpose is the one that should survive. Setting `workflowsEnabled` explicitly pins the answer: `true` keeps ours whatever else is loaded, `false` is off regardless.
929
1063
 
1064
+ Child sessions do not start a second subagents manager, even when their extension loader imports a separate copy of the package. Their SDK refusal tool still explains why nested workflows are unavailable; the parent keeps its own workflow tool and manager. This holds across a child session's `reload()` too.
1065
+
930
1066
  The match is on the exact tool names `Workflow` (Claude Code's) and `SubagentWorkflow` (ours), never a substring, so a `list_workflows` or `github_workflow_run` from some CI integration does not silently take the feature down. The check runs at `session_start` and nowhere earlier, because `getAllTools` throws during extension loading and load order means a check at registration time could not see an extension that has not loaded yet — so the tool is registered first and withdrawn from the active set through `setActiveTools`, which rebuilds the system prompt before any turn runs. When the other extension took the `SubagentWorkflow` name itself, pi's first-registration-wins rule already dropped ours, so there is nothing to withdraw and only the menu and the CLI flag come down.
931
1067
 
932
1068
  **Tool description** (`toolDescriptionMode`, default `"full"`): which Agent tool description the LLM sees. `"full"` is the rich Claude Code-style prompt (~1,400 tokens with the default agents); `"compact"` is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.
@@ -962,24 +1098,33 @@ Every project now starts with concurrency 16 and grace 10, without ever touching
962
1098
 
963
1099
  ## Events
964
1100
 
965
- Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
1101
+ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react. A queued background resume's `subagents:created` includes `queuedAfterSettleSeq` (the preceding terminal event's `settleSeq`) and `queuedResumeSeq` (the new queue entry's sequence). A listener can match the preceding settle before marking an existing agent queued and reject late announcements after a later settle. Lifecycle events also carry `wakesParent` for completion-notice eligibility. A foreground handoff's `subagents:created` carries `backgroundHandoff` and the same `runSeq` as its `started`, so listeners can update eligibility without accepting a stale handoff from an earlier run.
966
1102
 
967
1103
  | Event | When | Key fields |
968
1104
  |-------|------|------------|
969
- | `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started` | `id`, `type`, `description`, `isBackground` (always `true`) |
970
- | `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
971
- | `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result` |
972
- | `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | identical payload to `subagents:completed` — both are built by the same formatter, so `error` and `status` are present on that row too, just empty |
1105
+ | `subagents:created` | `Agent`-tool background spawn, a detached resume, or a foreground `Agent` call handed to the background (after its `started`) — **not** cross-extension RPC, scheduler, or `@handle` spawns, which are first seen at `subagents:started`, or at `subagents:failed` when stopped while still queued | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `isBackground` (always `true`); plus `modelFallback`/`modelNote` when already known, see below |
1106
+ | `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below); plus `modelFallback`/`modelNote` only when the spawn path already knew it, see below |
1107
+ | `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `status`, `durationMs`, `tokens` (display total, `{ input, output, total }` — see the note below), `usage` (the run's spend as a pi `Usage`: token components including `cacheRead`, plus `cost.total` in USD; absent when nothing was spent), `toolUses`, `result`; plus `modelFallback` (`{ input, used, also?, via? }`: `also` and `via` retain older disclosure details; `via` is `"parent"` or `"agent"`) and `modelNote` when a configured model did not resolve and the agent ran on another |
1108
+ | `subagents:failed` | Agent errored, stopped, or aborted (background and foreground), including a top-level agent stopped while still queued, which never emitted `started` | identical payload to `subagents:completed` — both are built by the same formatter, so `error` and `status` are present on that row too, just empty |
973
1109
  | `subagents:steered` | Steering message accepted from any path (`steer_subagent`, `@handle`, the viewers, `subagents:rpc:steer`) — fires for a *queued* steer as well as a delivered one | `id`, `message` |
974
- | `subagents:usage` | Every assistant message any agent spends on — top-level, nested and workflow children alike, live while it runs | Always: `id`, `type`, `description`, `depth`, `usage` (that one message's spend as a pi `Usage`; never emitted for a message that spent nothing). Omitted when unknown, never `undefined`: `model` (`provider/id`) and `thinking` (the effective level) until the session has reported them; `requestedThinking`, only when the agent did not get the level it asked for; `parentAgentId`, nested agents only; `workflowId`, a workflow's agents only |
975
- | `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
976
- | `subagents:disposed` | After actual shutdown disposal of this activation's manager and child sessions. Not emitted when a session switch detaches surviving background work; those agents can still emit `subagents:usage`. Other detached activations may also still report usage, so this is not a process-global barrier. On quit, flush **synchronously** (`writeFileSync`, not `fs.promises`): `emit` does not await listeners and pi exits once shutdown handlers return | `{}` |
1110
+ | `subagents:usage` | Every assistant message any agent spends on — top-level, nested and workflow children alike, live while it runs (not for an agent detached by `/new`, `/resume`, `/fork` or `/reload`: its observers are dropped after the switch) | Always: `id`, `type`, `description`, `depth`, `usage` (that one message's spend as a pi `Usage`; never emitted for a message that spent nothing). Omitted when unknown, never `undefined`: `model` (`provider/id`) and `thinking` (the effective level) until the session has reported them; `requestedThinking`, only when the agent did not get the level it asked for; `parentAgentId`, nested agents only; `workflowId`, a workflow's agents only; `modelFallback` and `modelNote` when a configured model did not resolve |
1111
+ | `subagents:compacted` | Agent's session successfully compacted (not emitted for an agent detached by a session switch) | `id`, `type`, `description`, `handle`/`name`/`nameBase`/`previousId`/`spawnSeq` (see below), `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`; plus `modelFallback`/`modelNote` when known |
1112
+ | `subagents:disposed` | After actual shutdown disposal of this activation's manager and child sessions. Not emitted when a session switch detaches surviving background work. Those detached agents no longer emit `subagents:usage` or `subagents:compacted` either (their `pi` is stale, so the observers are dropped), so a spend ledger under-counts them; their completion still arrives through the session spool. On quit, flush **synchronously** (`writeFileSync`, not `fs.promises`): `emit` does not await listeners and pi exits once shutdown handlers return | `{}` |
977
1113
  | `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
978
1114
  | `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
1115
+ | `subagents:started` (survivor) | Re-announcement of each background agent still running under this session's file (after `/reload`, or a `/resume` back into the session that spawned them), at every `session_start` and whenever `subagents:announce_survivors` (`{}`) is emitted; `spawnSeq`/`runSeq` are omitted because the orphaned manager counts its own sequence. A survivor's later `subagents:completed` / `:failed` is emitted when its result is delivered, without `settleSeq` | same as `subagents:started` |
1116
+ | `subagents:announce_survivors` | Request, not a notification: a listener that bound after `session_start` (pi-todo sends it once its session is bound) asks this activation to re-emit its survivor announcements | `{}` |
1117
+ | `subagents:other_session_survivor` | At every `session_start` and on `subagents:announce_survivors`: each background agent still running under a session file other than the current one, whatever switch left it (`/fork`, a fork of a fork, `/resume` of another session, `/new`). Its result spools to that file, so it wakes that session, not this one; no `wakesParent`, and no `completed` / `failed` follows here unless that session is later resumed. A listener re-asks to learn that one finished. Listeners must not treat it as armed | same as `subagents:started`, minus `wakesParent`, `spawnSeq` and `runSeq`, plus `sessionFile` (the session it wakes) |
979
1118
  | `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `{}` (empty object) |
980
1119
  | `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
981
1120
  | `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
982
1121
 
1122
+ **Agent identity on events.** `created`, `started`, `completed`, `failed` and `compacted` carry `handle`, the agent's type-derived `@handle` (`explore`, `explore-2`), and `name`, the second handle drawn from the spawner's `name` (`auth-audit` for `name: "Auth Audit"`), and `nameBase`, that name before collision numbering: a retry spawned as `Auth Audit` while the first try still holds `auth-audit` gets `name: "auth-audit-2"` and `nameBase: "auth-audit"`. An evicted agent reopened under a new id also carries `previousId`, the id it continues. Each is omitted when the agent has none; `previousId` cannot be set through `subagents:rpc:spawn`. `spawnSeq` orders the agent's creation, before any time queued, and `completed`/`failed` add `settleSeq` from the same counter (one per extension activation), so a listener can tell an agent spawned after another settled from one queued behind it. A listener can then match an agent by what the parent typed instead of only its opaque `id`; pi-todo uses them to show a todo `owner`'s live agent state ([owner match rule](https://github.com/nredd/pi-harness/tree/main/packages/todo/docs/overlay.md#owner-match-rule)).
1123
+
1124
+ **Tier provenance on events.** Payloads that disclose fallback provenance also carry `modelTier` and `modelNote` for tier selections. Outcomes are `routed`, `static`, `unmapped`, `unavailable` or `exhausted`; successful outcomes include the tier, bound profile/member, admitted model and latest answering model, and static outcomes list skipped members and whether a quota signal was used. `subagents:record` history entries and detached completion spools retain the same outcome. Persisted `modelTier.model` contains identity only (`provider`, `id`, `name`, `api`), never provider URLs, headers or other credential-bearing model overlays.
1125
+
1126
+ **Model fallback on events.** When a configured model did not resolve, `modelFallback` (`{ input, used, also?, via? }`) and a ready-made `modelNote` ride on `created`, `started`, `compacted`, `usage` and `completed`/`failed` payloads. The shared spawn path records selection before emitting `started`, including RPC, `@mention` and schedules with no explicit model. All later events read the same retained provenance.
1127
+
983
1128
  The four agent-lifecycle events — `subagents:started`, `:completed`, `:failed`, `:compacted` — are emitted for **top-level agents only**; nested subagents and a workflow's children report through the parent or workflow that owns them. `subagents:usage` is the one event that is not scoped, on purpose: it is a ledger, not a lifecycle. It fires once per message for every agent, so summing `usage.cost.total` across it gives what this session's subagents spent, each message counted exactly once under the agent that spent it — unlike the ancestor records, which fold a nested child's spend into every parent. It fires regardless of `reportUsage`, which decides only whether the parent session's own totals include the spend.
984
1129
 
985
1130
  `tokens.total` = `input + output + cacheWrite`. `cacheRead` is excluded — each turn's `cacheRead` is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it as a measure of work done. Use `contextUsage.percent` (surfaced as `(NN%)` in the widget) for current context size.
@@ -1041,9 +1186,9 @@ pi.events.emit("subagents:rpc:spawn", {
1041
1186
  });
1042
1187
  ```
1043
1188
 
1044
- `options` is the manager's spawn-option object, not the `Agent` tool's parameter schema — the background flag is `isBackground`, and the tool's snake_case `run_in_background` is forwarded verbatim and ignored. Every RPC spawn returns its id immediately and runs detached either way; `isBackground: true` is what makes the agent occupy one of the `maxConcurrent` slots (and queue behind them when they are full). It does not affect `subagents:created`, which is never emitted for an RPC spawn at all — the first event you see for one is `subagents:started`. Leaving it unset starts the agent immediately regardless of that limit, though not of the [process budget](#process-budget) when it is on. `maxConcurrentForeground` never applies here whatever `isBackground` says: it bounds only spawns a caller is blocking on inline, and every RPC spawn is detached. A top-level RPC spawn renders in the widget and FleetView while it runs, with the same live tool activity and turn counter an `Agent`-tool spawn gets — only an explicit `isBackground: false` is dropped by the widget's default `background` mode, the way a foreground `Agent` call is. Nested spawns stay hidden from both.
1189
+ `options` is the manager's spawn-option object, not the `Agent` tool's parameter schema — the background flag is `isBackground`, and the tool's snake_case `run_in_background` is forwarded verbatim and ignored. Every RPC spawn returns its id immediately and runs detached either way; `isBackground: true` is what makes the agent occupy one of the `maxConcurrent` slots (and queue behind them when they are full). It does not affect `subagents:created`, which is never emitted for an RPC spawn at all — the first event you see for one is normally `subagents:started`; an agent stopped while still queued emits only `subagents:failed` (see [docs/rpc.md](docs/rpc.md)). Leaving it unset starts the agent immediately regardless of that limit, though not of the [process budget](#process-budget) when it is on. `maxConcurrentForeground` never applies here whatever `isBackground` says: it bounds only spawns a caller is blocking on inline, and every RPC spawn is detached. A top-level RPC spawn renders in the widget and FleetView while it runs, with the same live tool activity and turn counter an `Agent`-tool spawn gets — only an explicit `isBackground: false` is dropped by the widget's default `background` mode, the way a foreground `Agent` call is. Nested spawns stay hidden from both.
1045
1190
 
1046
- `options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are resolved against `ctx.modelRegistry` at the RPC boundary, so cross-extension callers can forward serializable values without losing auth context. Resolution is fuzzy, so a bare `"sonnet"` can land on a provider you never named: with [Model Scope](#model-scope) on, an override that resolves outside `enabledModels` is refused with an error envelope listing the allowed models, exactly as a caller-supplied `Agent({ model })` is. `null` means unset — the agent inherits, same as omitting the field.
1191
+ `options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are validated against `ctx.modelRegistry` at the RPC boundary and selected by the shared spawn path, which records the original input and source. Cross-extension callers can forward serializable values without losing auth context. Resolution is fuzzy, so a bare `"sonnet"` can land on a provider you never named: with [Model Scope](#model-scope) on, an override that resolves outside `enabledModels` is refused with an error envelope listing the allowed models, exactly as a caller-supplied `Agent({ model })` is. `null` means unset — the agent inherits, same as omitting the field.
1047
1192
 
1048
1193
  `options.cwd` (absolute path to an existing directory — anything else returns an error envelope; `null` means unset) runs the agent in a different working directory than the parent session. Its tools operate there and the prompt's environment block describes it, but **`.pi` config still loads from the parent session's project** — the target directory's `.pi` extensions never execute, and its agents/skills/settings are not picked up. Combined with `isolation: "worktree"`, the worktree is created *from* the target directory's repo, the agent works at the equivalent subdirectory inside the copy (a monorepo-package cwd stays scoped to that package), and the resulting `pi-agent-*` branch lands in that repo — the completion message names it. On session end, worktree registrations are pruned in every repo that received one; only a hard crash can leave a stale entry (then: `git worktree prune` in the target repo). Agents with `memory:` keep reading/writing the parent project's memory.
1049
1194
 
@@ -1104,6 +1249,31 @@ Memory uses a `MEMORY.md` index file and individual memory files with frontmatte
1104
1249
 
1105
1250
  The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
1106
1251
 
1252
+ ## Plan mode
1253
+
1254
+ A child started while the parent session is in [`pi-plan-mode`](../plan-mode/README.md)'s Plan mode plans too, whatever its own tools say, as Claude Code keeps a subagent spawned in plan mode in plan mode. Before every spawn the manager asks Plan mode on the parent's `pi.events` (`pi-plan-mode:state-query`, answered synchronously); while Plan is active the answer carries Plan's tool policy and the child runs under it:
1255
+
1256
+ - `edit` and `write` are refused at call time, `bash` and `powershell` run only Plan's reviewed read-only commands (`git status` runs, `rm -rf x` does not), and extension tools run only when Plan would admit them (trusted `readOnlyHint`, or selected in the Plan policy). Tool schemas stay visible, as in the planner, so the child's prompt cache and a later resume are unaffected
1257
+ - The child never needs Plan mode loaded itself: the policy is enforced by this extension, including for `extensions: false` and `isolated` agents. Explore and Plan keep their read-only surfaces unchanged
1258
+ - Every spawn path asks: the `Agent` tool, `@handle` mentions, RPC, schedules created before Plan and `SubagentWorkflow` stages, each as it starts. Nested `allowed_subagents` children also inherit their owner's restriction, even after the parent leaves Plan
1259
+ - The decision is per run. A child spawned during Plan keeps the restriction after you approve and leave Plan, since it was briefed as a planner; resuming it after Plan gets its normal surface back, and resuming any agent during Plan restricts that run. Both hold for `Agent` `resume` and for `steer_subagent` on a finished agent, which resumes through the same path. An agent started before Plan keeps its surface while it runs
1260
+ - `isolation: "worktree"` is refused while planning: a worktree exists to make edits
1261
+ - Nothing Plan's model starts may outlive Plan unrestricted: `Agent` with `schedule` is refused while planning, and so is parking a quota-blocked background dispatch (it fails with the reset instead). Both would fire as fresh spawns after Plan
1262
+ - `steer_subagent` and the `subagents:rpc:steer` channel are refused while planning for a running or queued agent that runs without Plan's restrictions (one started before Plan), so a steer from Plan's model or another extension's tool cannot drive it into edits. Its running gate is never changed underneath it: wait for it to finish and steer it then (that resumes it read-only), stop it with `stop_subagent` (Plan admits it and never asks the gate), or leave Plan first. An agent already running under Plan stays steerable
1263
+ - A malformed answer to the state query (wrong version, no boolean `active`) never counts as Plan being off: it restricts like an unusable one. That is how a version-skewed `pi-plan-mode` (a future v2) or another extension answering `pi-plan-mode:state-query` shows up: the child is limited to `read`, `grep`, `find` and `ls`, its other calls fail with `Plan mode is active and sent no tool policy this subagent understands`, and schedules, parking and steers of pre-Plan agents are refused, even with Plan off
1264
+ - The child's prompt says it is in Plan mode, and its expanded `Agent` row and conversation viewer tags read `plan`
1265
+ - Extension tools are classified from the child's own tool metadata (`sourceInfo`): a tool the planner selected is admitted only from the same source path, so a project-scope copy, or a project-scope `readOnlyHint`, stays blocked
1266
+ - An answer saying Plan is active but carrying no policy this extension understands fails closed to `read`, `grep`, `find` and `ls`. Without `pi-plan-mode` nothing answers and nothing changes
1267
+ - Plan mode and this extension shake hands (`pi-plan-mode:gate-query`): Plan blocks `Agent`, `SubagentWorkflow` and `steer_subagent` while active unless this gate answers (only those three, by name: `get_subagent_result`, `stop_subagent` and any other tool that starts no child stay available in Plan), and once Plan mode has shown itself a state query nobody answers refuses the spawn (a resume runs fail-closed). Needs `@nredd/pi-plan-mode` newer than `0.59.3-nredd.3` with this extension newer than `0.22.0-nredd.8`; update both together
1268
+
1269
+ What Plan does not reach:
1270
+
1271
+ - A child queued before Plan starts later with its full surface: the decision was made when it was spawned. Plan's model cannot steer it meanwhile
1272
+ - A schedule created before Plan fires during Plan as a fresh spawn, and is restricted then like any other
1273
+ - Steering from `/agents`, the conversation viewer and `@handle` is the user, not Plan's model, and is not refused
1274
+ - The handshake is hardening, not a sandbox. Every extension shares `pi.events`, and a well-formed `{ version: 1, active: false }` reply from one of them cannot be told apart from Plan's own. Any active answer still wins, but a spoofed one hides a Plan that stayed silent, so the child runs unrestricted. Extension code runs in-process with full access anyway
1275
+ - A nested child of a pre-Plan owner is restricted when Plan is on at its own spawn
1276
+
1107
1277
  ## Worktree Isolation
1108
1278
 
1109
1279
  Set `isolation: worktree` to run an agent in a temporary git worktree:
@@ -1112,11 +1282,13 @@ Set `isolation: worktree` to run an agent in a temporary git worktree:
1112
1282
  Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
1113
1283
  ```
1114
1284
 
1115
- The agent gets a full, isolated copy of the repository. The worktree directory is removed on completion either way — what differs is whether a branch is left behind:
1285
+ The agent gets a full, isolated copy of the repository. On successful cleanup the worktree directory is removed; whether a branch is left behind depends on the work:
1116
1286
  - **No changes:** worktree is cleaned up automatically, no branch
1117
- - **Changes made:** changes are committed to a new branch (`pi-agent-<id>`), and the result names the branch and the `git merge` command for it. The branch is the only artifact — the worktree path is gone, so nothing points into it
1287
+ - **Changes made:** changes are committed to a new branch (`pi-agent-<id>`), and the result names the branch and the `git merge` command for it. After successful cleanup the branch is the only artifact — the worktree path is gone, so nothing points into it
1118
1288
  - **Agent committed its own work:** the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)
1119
1289
 
1290
+ If Git cannot inspect, commit, create the branch, or remove the worktree, cleanup stops without force-removing it. The result reports the error and worktree path (and the branch if already created); inspect and recover the tree before removing it. A failed status check does not establish whether changes exist. Git removal also refuses uncommitted work that appears after the status check. Git refuses to remove any worktree with submodule state (a gitlink in the index, registered in `.gitmodules` or not, or a `modules` dir in the worktree's git dir), so such a worktree is removed with `--force` only after the outer tree and every nested repository re-verify clean; that is decided from the repository state, not git's locale-dependent message. Status checks pass `--untracked-files=all` and removal pins `status.showUntrackedFiles=all`, so a `status.showUntrackedFiles=no` config cannot hide new files from cleanup. A nested repository with uncommitted files (untracked included), a stash, or commits no remote-tracking ref contains, on a branch, a tag or HEAD (all deleted with the worktree), keeps the worktree and reports `submodule work would be lost on removal`. Offline a fetched tag cannot be told from a local one, so a tag on an unpublished commit fails safe. Nested clones under a gitignored path, which no gitlink records, are found through `git status --ignored` and inspected the same way, including ones nested inside a submodule or another ignored clone. Status checks also pass `--ignore-submodules=none`, so `submodule.<name>.ignore` or `diff.ignoreSubmodules` cannot hide a moved gitlink; if `git add -A` then stages nothing (it skips a gitlink hidden by `ignore = all`), cleanup keeps the worktree and says the changes could not be staged. Status checks first clear `assume-unchanged` and `skip-worktree` index bits (a skip-worktree file absent from disk is left alone), so an edit those bits hide is committed, not deleted; a regular file with a mode change staged in the index (vs HEAD, or on a newly added file) that the file on disk no longer matches (e.g. `update-index --chmod`, or a `chmod` after `git add`; a plain unstaged `chmod` is committed normally) cannot be told apart from a later on-disk change and `add -A` would silently restage the disk mode, so cleanup fails safe before staging: it keeps the worktree and reports `mode staged only in the index; worktree kept:` with the paths (skipped under `core.fileMode=false`). Path listings used for these decisions (`ls-files`, `ls-tree`, `diff --raw`, `status --ignored`) run with `core.quotePath=true` and without `-z`, and quoted names are decoded back by `unquoteGitPath`, so a multibyte name split across `pi.exec`'s output chunks cannot be mis-decoded to U+FFFD; nested git dirs are read from `.git` rather than `rev-parse` output. A name git prints raw non-ASCII or that is not valid UTF-8 cannot be inspected, so the worktree is kept with an error. A submodule HEAD at the sha the base commit records is not at risk, and neither is a leftover module git dir at a recorded sha. A cleanup failure marks a normally successful agent as failed, including a structured workflow child; a stopped agent stays stopped and reports the failure.
1291
+
1120
1292
  The agent's system prompt names the worktree as an isolated copy and tells it to work only there, even if other instructions name the main checkout — otherwise an inherited parent prompt or a task prompt mentioning the project path walks it straight back out of the copy. This is a directive, not a sandbox: an agent with shell access can still `cd` out, so don't rely on `isolation` alone to protect the main checkout.
1121
1293
 
1122
1294
  The automatic preservation commit uses `--no-verify`, so local pre-commit hooks can't block it — the commit is local-only and never pushed, and pre-push/server-side hooks still apply.
@@ -1153,25 +1325,11 @@ skills: api-conventions, error-handling
1153
1325
  ---
1154
1326
  ```
1155
1327
 
1156
- **Discovery roots** (checked in this order, first match wins):
1157
-
1158
- | Scope | Path | Source |
1159
- |---|---|---|
1160
- | Project | `<cwd>/.pi/skills/` | Pi-standard |
1161
- | Project | `<cwd>/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
1162
- | User | `$PI_CODING_AGENT_DIR/skills/` (default `~/.pi/agent/skills/`) | Pi-standard |
1163
- | User | `~/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
1164
- | User | `~/.pi/skills/` | Legacy (pre-Pi) |
1165
-
1166
- **Per root, a skill named `foo` resolves to the first of:**
1167
-
1168
- - `<root>/foo.md` — flat file at the top level
1169
- - `<root>/foo/SKILL.md` — directory skill (top-level)
1170
- - `<root>/*/.../foo/SKILL.md` — directory skill, found by recursive descent
1328
+ Named preloads use Pi's configured-resource resolution and parser. Trusted project discovery, the configured agent directory, and installed package manifests use Pi's precedence; a trusted project's ancestor `.agents/skills` roots participate when Pi discovers them. Pi-disabled resources, including `skills: ["-skills/review"]`, remain disabled and cannot return through compatibility lookup. Compatibility lookup otherwise retains the historical user roots (`<home>/.agents/skills`, `<home>/.pi/skills`) and explicit top-level `<name>.md` files. A package skill hidden behind a rejected project symlink alias is recovered through a scratch-cwd Pi resolution, so the project's package filters (`skills` allowlists and exclusions, relative or absolute sources) still decide whether it loads. Package discovery remains inspect-only: unavailable configured npm or Git sources are skipped, never installed or lifecycle-executed.
1171
1329
 
1172
- Recursion skips dotfile directories and `node_modules`. A directory that itself contains a `SKILL.md` is treated as a single skill — we don't descend into it. Traversal is byte-order sorted for deterministic resolution across filesystems.
1330
+ **Name matching:** Pi matches a skill by its frontmatter `name:`. The compatibility lookup runs only when Pi finds no enabled or disabled skill of that name, and matches by file or directory name instead, so a `review/` directory declaring `name: other` answers both `other` and `review`. Per root, `foo` resolves to the first of `<root>/foo.md`, `<root>/foo/SKILL.md`, then `<root>/*/.../foo/SKILL.md` by recursive descent. Descent skips dotfile directories and `node_modules`, never enters a directory that has its own `SKILL.md`, and is byte-order sorted. An unreadable match is skipped and the walk continues. Names with path-traversal characters are rejected: anything but a leading letter or digit followed by letters, digits, `.`, `_` or `-`, or longer than 128 chars. The project compatibility roots (`.pi/skills`, each `.agents/skills`) are walked only for a trusted project.
1173
1331
 
1174
- **Security:** project roots reject symlinks at every layer (the `.pi/` or `.agents/` directory, root, flat file, skill directory, `SKILL.md` inside a skill directory), so a cloned repository cannot point a skill at files outside it — intentional deviation from Pi. User roots belong to you and follow symlinks like Pi (linked roots, directories and files; broken links and directory cycles are skipped), so skills linked into `~/.agents/skills/` by an installer or dotfile manager load. Skill names with path-traversal characters (`..`, `/`, `\`, spaces, leading dot, >128 chars) are rejected.
1332
+ **Project links:** automatically discovered project roots reject symlinks at `<cwd>/.pi`, `.pi/skills`, each ancestor `.agents`, `.agents/skills`, and their skill entries. This filter runs before this extension accepts project candidates; a separate untrusted Pi resolution restores only a user or package candidate that Pi's realpath deduplication hid behind a rejected project alias, retaining Pi's package-identity precedence. A candidate whose path cannot be canonicalized (raced away or unreadable) is never restored, and the rest of the preload continues. User compatibility roots and installed package resources retain Pi's normal link handling, so a project or global package's `skills/<name>` symlink that points outside the package is followed and injected. The former loader refused links under project roots only; packages were never covered by that refusal, and this matches Pi. An explicit injected home excludes the runtime process's ambient `~/.agents/skills` result while preserving `<home>/.agents/skills`, so ambient content cannot enter a child prompt. The host still scans the ambient location; this extension filters returned resources rather than providing filesystem isolation.
1175
1333
 
1176
1334
  ## Tool Denylist
1177
1335
 
@@ -1192,6 +1350,7 @@ This is useful for creating agents that inherit extension tools but should not h
1192
1350
  docs/ # Long-form guides (shipped to npm; README links out to them)
1193
1351
  workflows.md # SubagentWorkflow: writing, editing, saving and re-running scripts
1194
1352
  rpc.md # Cross-extension integration: pi.events, subagents:rpc:*, manager registry
1353
+ home-injection.md # Injected home boundaries, call-time defaults and filesystem canaries
1195
1354
  examples/
1196
1355
  workflows/ # Runnable examples, executed by test/workflow-examples.test.ts
1197
1356
  agent-tool-description.md
@@ -1216,18 +1375,23 @@ src/
1216
1375
  nested-tools.ts # Delegation tools handed to subagents (nested spawn/collect/steer)
1217
1376
  process-budget.ts # Process-wide running-agent budget shared by every manager (maxActiveSubagents)
1218
1377
  child-context.ts # AsyncLocalStorage flag marking work done for a child session
1378
+ plan-mode-children.ts # Plan mode for children spawned while the parent plans (#437)
1219
1379
  abortable.ts # Race a wait against Esc without cancelling the background child
1220
1380
  model-call.ts # In-flight model call tracking: thinking / writing / stalled?
1221
1381
  completion-outbox.ts # Deliver completion notifications at the parent's next boundary, coalesced
1382
+ notification-result.ts # How much of a result a notice carries: per-agent cap, batch budget, head+tail clip
1222
1383
  clock.ts # Shared scheduler/outbox/admission/retention/UI clock and native timers
1223
1384
  detached-results.ts # Session-scoped spool and registry for background agents surviving reload/switch
1224
1385
  group-join.ts # `group` join mode: one notification per turn's batch, with timeout
1225
1386
  status-note.ts # Honest status note + salvaged partial output for non-normal outcomes
1387
+ error-message.ts # The text a caught value reports: an Error's message, else String(value)
1226
1388
  usage.ts # Token usage shapes, accumulators, session-stats readers
1227
1389
 
1228
1390
  # Invocation surface
1229
- invocation-config.ts # Shared tool-parameter schemas (isolation, join, thinking, ...)
1230
- model-resolver.ts # Model resolution: exact provider/modelId, aliases, `agentModels`, fuzzy fallback ranked by version
1391
+ invocation-config.ts # Shared tool-parameter schemas (isolation, cwd, thinking, ...)
1392
+ model-selection.ts # Shared precedence, scope and resolution outcome; stored on the agent record
1393
+ model-tiers.ts # Exact static/routed tiers, admission, policy and durable disclosure
1394
+ model-resolver.ts # Model matching: exact provider/modelId, aliases, fuzzy fallback ranked by version
1231
1395
  enabled-models.ts # Read pi's enabledModels settings (project over global)
1232
1396
  model-scope.ts # scopeModels allowlist policy, shared by top-level and nested tools
1233
1397
  mention.ts # `@handle message` grammar: suggestion triggers and send parsing
@@ -1239,10 +1403,13 @@ src/
1239
1403
  schedule-store.ts # PID-locked, session-scoped, atomic schedule persistence
1240
1404
 
1241
1405
  # Context & environment
1406
+ home-paths.ts # Injected home normalization and Pi agent-directory defaults
1407
+ resource-loader.ts # Fresh scoped settings paths for Pi's real resource/package loader
1242
1408
  memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
1243
- skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
1409
+ skill-loader.ts # Preload named skills through Pi's configured resource discovery (trust-gated, inspect-only)
1244
1410
  output-file.ts # Streaming output file transcripts for agent sessions
1245
1411
  worktree.ts # Git worktree isolation (create, cleanup, prune)
1412
+ git-path.ts # Decode git's C-quoted paths (`unquoteGitPath`) for worktree cleanup
1246
1413
  prompts.ts # Config-driven system prompt builder
1247
1414
  context.ts # Parent conversation context for inherit_context
1248
1415
  settings.ts # Persistent settings (~/.pi/agent/subagents.json + .pi/subagents.json)
@@ -1260,7 +1427,8 @@ src/
1260
1427
  agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
1261
1428
  agent-row.ts # The Agent tool row: live one-line summary, expanded prompt/tags/transcript
1262
1429
  notification-view.ts # Completion notice renderer: one-line collapsed, stats/preview/link expanded
1263
- fleet-list.ts # FleetView: navigable agent list below the editor
1430
+ fleet-list.ts # FleetView: one-line summary/disclosure and navigable list below the editor
1431
+ fleet-state.ts # Versioned journal checkpoints, bounded failure details and exact overflow counters
1264
1432
  conversation-viewer.ts # Live conversation overlay for viewing agent sessions
1265
1433
  viewer-keys.ts # Viewer scroll keys resolved through user keybindings
1266
1434
  agent-mention.ts # `@` roster (running, resumable, and startable agents) + popup rows