@cad0p/pi-subagents-tintinweb 0.14.2 → 0.22.0

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.
package/CHANGELOG.md CHANGED
@@ -5,20 +5,39 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.22.0] - 2026-10-21
9
+
10
+ > **⚠️ Breaking — `persist_session` changed meaning.** It used to mean "persist this subagent at all"; without it the run used an in-memory session and left nothing behind. It now means "retain this subagent's session file after it settles": every run writes a session file while it runs, so a crashed or killed run stays recoverable, and the file is deleted when the agent record is removed unless `persist_session: true`. Migration: an agent that relied on the old default to keep a run off disk must set `output_transcript: false`, avoid `persist_session`, `isolation: worktree` and `memory:`, or run pi with `--no-session`, which keeps subagent sessions in memory as before.
9
11
 
10
12
  ### Added
11
13
 
12
14
  - **`checkpoint` tool — subagent-authored progress checkpoints the parent pulls on demand.** A new subagent-facing tool (available to every subagent by default; not added to `EXCLUDED_TOOL_NAMES`) the subagent calls at milestones to save a one-sentence summary of what it just did and what it's doing next. The latest summary is surfaced inline in `get_subagent_result`'s output; the full history lives in a `.checkpoints.md` file appended in `## Turn N/M — Xs elapsed` format. The `.checkpoints.md` file is gated on `record.outputFile` (skipped when `output_transcript: false` suppresses the `.output` transcript, though the in-memory `lastCheckpoint` is still updated), and checkpoints are pull-only — no new model call, no mid-run push to the parent. The tool returns a brief `Checkpoint saved (turn N/M, Xs).` confirmation to the subagent.
13
15
 
16
+
17
+ - **Subagents load pi's built-in extensions, scoped and trusted** (closes [#61](https://github.com/cad0p/pi-subagents-tintinweb/issues/61), [#63](https://github.com/cad0p/pi-subagents-tintinweb/pull/63)). The child's `DefaultResourceLoader` is built by this extension, not by pi's CLI, so it started with none of pi's built-in extensions: `builtin:<name>` in `extensions:` resolved to nothing and codemode, tool search and MCP were out of reach for every subagent. The three factories are now forwarded as `builtin: true` resources, the parent's project-trust decision applies to the child, and `--no-extensions` / `--no-mcp` keep them out. Tool scope also covers calls made through `ctx.executeTool()` (codemode scripts) and `tool_search`, which bypass the `tools:` allowlist. A `defaultActive: false` built-in (`codemode`, `tool_search`) is declared in the child only when the parent session has it active or the agent names its extension with `ext:` — "as in the main session", not "everything that loaded". Append-mode agents no longer inherit a `<tools>` block advertising tools they do not have: `retargetToolsSection()` rewrites it from the tools the child actually declares.
18
+
19
+ - **Opt-in nested subagent delegation** (closes [#67](https://github.com/cad0p/pi-subagents-tintinweb/issues/67), [#69](https://github.com/cad0p/pi-subagents-tintinweb/pull/69)). `allowed_subagents` gives an agent its own `Agent`, `get_subagent_result`, `steer_subagent` and `checkpoint` tools, injected into that child's session only; `max_subagent_depth` per agent and `maxSubagentDepth` in `subagents.json` cap the tree, and a per-agent cap can only tighten. An agent at the cap gets no nested tools, so depth 0/1 means "nesting off" rather than "nesting always fails". Nested spawns resolve agents from a registry built for their own config root, share the `scopeModels` policy with the top-level tool, are background-only and return an id, and do not take a `maxConcurrent` slot (their parent already holds one, and queueing a child behind its own parent deadlocks). A parent's children are stopped when it settles, including at the end of a resumed turn. Ownership metadata is stripped from cross-extension spawns: `rootSessionId` names a transcript directory, so a forged value is a path-traversal primitive.
20
+
21
+ - **Subagent spend reaches the session, and cost is shown** (closes [#68](https://github.com/cad0p/pi-subagents-tintinweb/issues/68), [#70](https://github.com/cad0p/pi-subagents-tintinweb/pull/70)). Subagents run in their own pi session, so nothing they spent appeared in the parent's `getSessionStats()`: a session that delegated most of its work read as nearly free in the footer, the statusline and `/cost`. `reportUsage` (off by default) makes each `Agent` / `get_subagent_result` / `steer_subagent` result carry the spend accumulated since the last one, which pi folds into the session totals under `/cost`'s "Tools/summaries" bucket; tokens include `cacheRead`, cost is pi's own per-message figure, and the context-window percentage is untouched. A background agent has no result of its own to ride on, so its spend lands on the next call, and a session that ends with nothing pending reports nothing. Nested children are counted once each, on the top-level result. `showCost` (off by default) puts `~$0.0042` beside the token counts in the widget, FleetView, the conversation viewer, `get_subagent_result` and completion notifications, omitted entirely when pi has no pricing data. `showModel` names the model and thinking level in the widget's running rows, and every surface now reports the *effective* model and level read back from the child session, keeping the request beside a clamped level (`low (asked max)`). New events: `subagents:usage` fires once per assistant message for every agent, nested included, and is not gated by `reportUsage`; `subagents:disposed` fires at the end of `session_shutdown` carrying the reason; `subagents:completed` / `:failed` carry the run's spend as a pi `Usage`. Cross-extension callers gain `subagents:rpc:consume` and `subagents:rpc:steer`.
22
+
23
+ - **`fallbackSubagent`, `worktreeIsolation` and `strictAgentFiles` settings, `isolation: "off"`, and unknown-frontmatter warnings** (closes [#62](https://github.com/cad0p/pi-subagents-tintinweb/issues/62), [#64](https://github.com/cad0p/pi-subagents-tintinweb/pull/64)). `fallbackSubagent` decides what an unknown, disabled or case-ambiguous `subagent_type` does: unset keeps the historical route to general-purpose, an agent name routes there with a note in the result, and `none` fails closed naming the available types. It applies to the `Agent` tool, the scheduler at fire time and cross-extension RPC; `resume` is exempt because it replays a stored session. `isolation` now accepts `"off"` — the one optional parameter whose only legal value had a side effect, so a model that filled every field kept spawning worktrees — and frontmatter `isolation: off` vetoes a caller's request. `worktreeIsolation: false` refuses worktrees project-wide and drops the parameter from the tool schema. `strictAgentFiles` aborts startup on an unreadable or unparseable `agents/*.md` instead of skipping it. Agent files with unknown frontmatter keys now warn once per file per key instead of being silently ignored.
24
+
25
+ - **`docs/fork-differences.md`** (closes [#71](https://github.com/cad0p/pi-subagents-tintinweb/issues/71), [#72](https://github.com/cad0p/pi-subagents-tintinweb/pull/72)). What this fork deliberately does differently from upstream, in both directions with the reason for each, so a difference can be told from a missing port and the next merge has a list to check against instead of re-deriving the decisions.
26
+
14
27
  ### Changed
15
28
 
16
29
  - **`get_subagent_result` redesigned — surfaces the latest checkpoint and the checkpoint/transcript file paths, and drops the `verbose` parameter.** The parent LLM previously had two ways to inspect a running background subagent: a mid-run `get_subagent_result` that returned `"Agent is still running… check back later"` (zero useful signal), and a post-completion `verbose: true` that dumped the entire conversation transcript (tool results truncated to 200 chars, unstructured firehose). Both are replaced by a single pull-based shape: the running header now carries `turn N/M, Xs elapsed` and the latest checkpoint summary (if any), with both file paths (`.checkpoints.md` and `.output`) for drill-in; the completed header keeps the Status + stats + status-note line and result preview, then appends the latest checkpoint and file paths. The footer discourages polling on the running shape (`Do not poll repeatedly`) and drops it on the completed shape (the agent is done). The `verbose` parameter is removed from the schema — read the `.output` transcript file directly for full detail. The conversation-viewer's truncation hint, which pointed at `verbose`, now points at the transcript file. The `resultConsumed` / `cancelNudge` logic is preserved (it suppresses the completion notification when the parent reads the result).
17
30
  - **BREAKING: `get_subagent_result` no longer accepts a `verbose` parameter.** The `verbose: true` branch that dumped the agent's full conversation (the `--- Agent Conversation ---` block) is gone; the schema no longer declares it. Migration: orchestrator prompts that passed `verbose: true` should read the `.output` transcript file path that `get_subagent_result` now surfaces instead — it's the full, untruncated transcript.
18
31
  - **BREAKING: The `Agent` tool no longer accepts a `model` parameter.** The orchestrator previously could pass `model: "claude-sonnet-4"` to override the subagent's model at spawn time, but `resolveModel` matched against auth-configured models without verifying billing — so a model with valid auth but no payment method produced a raw `401 CreditsError` ("No payment method") with no actionable guidance for the orchestrator to recover. The `model` param is removed; subagents now inherit the parent session's model (always authenticated and billed) unless a custom agent's frontmatter pins a specific model via `model:` in `.pi/agents/*.md`, `.agents/agents/*.md`, or `~/.pi/agent/agents/*.md`. Cross-extension RPC (`options.model`) and the scheduler (`job.model`) retain their `model` string fields — programmatic callers can still pass a model, and the `resolveModel` fuzzy matcher stays load-bearing for frontmatter pins. The `scopeModels` feature loses its "caller-supplied → hard error" branch (no longer reachable) and now only warns on frontmatter-pinned or parent-inherited out-of-scope models. Migration: orchestrator prompts that passed `model:` on the `Agent` tool should drop it; per-task model selection now goes through a custom agent's frontmatter `model:` pin.
19
32
 
33
+
34
+ - **`reportUsage` and `showCost` now default to on** (closes [#79](https://github.com/cad0p/pi-subagents-tintinweb/issues/79), [#80](https://github.com/cad0p/pi-subagents-tintinweb/pull/80)). Upstream ships both off, so out of the box a session that delegated most of its work still read as nearly free. Subagent spend now counts towards the session's own totals — the footer, statusline and `/cost` include it, attributed to the **Tools/summaries** bucket — and the estimated cost appears beside the subagent token counts in the widget, FleetView, the viewer and the result report. Both remain settings, because turning `reportUsage` on does move numbers a statusline may already be reading; `showModel` deliberately stays off in both, since its cost is a narrow terminal row rather than a wrong number. Recorded in `docs/fork-differences.md`. Note where a setting lives: `/agents → Settings` writes the **project** file (`<cwd>/.pi/subagents.json`), while the machine-wide default is the hand-edited `~/.pi/agent/subagents.json` — a toggle made in one repo follows you nowhere, which is the other half of why the default was wrong.
35
+
36
+ - **BREAKING: every subagent run now writes a pi session file while it runs, and `persist_session` decides only whether it survives** (closes [#65](https://github.com/cad0p/pi-subagents-tintinweb/issues/65), [#66](https://github.com/cad0p/pi-subagents-tintinweb/pull/66)). Without `persist_session` the child session was in memory, so a crash, a kill or an interrupt left nothing for pi's `/resume` and nothing to inspect. Every run now writes a session file; retention is decided when the agent record is removed (the 10-minute eviction, a session switch, or a clean shutdown once the run has settled), which is also the window in which a completed record can still resume the agent. An in-flight run keeps its file even at shutdown, because that is the case crash recovery exists for. The child's session header records the spawning session file as `parentSession`, so subagent sessions nest under their spawner in pi's `/resume` picker. A host session with no file of its own (`--no-session`, or an SDK host using an in-memory session) gets in-memory children and no parent link. Migration: see the breaking note above.
37
+
20
38
  ### Fixed
21
39
 
40
+ - **A nested-delegation agent no longer warns that its own keys are unknown** (closes [#81](https://github.com/cad0p/pi-subagents-tintinweb/issues/81), [#82](https://github.com/cad0p/pi-subagents-tintinweb/pull/82)). `allowed_subagents` and `max_subagent_depth` were read by the parser but missing from `KNOWN_FRONTMATTER_KEYS`, so an agent file using them printed `Ignoring unknown frontmatter key` at every load while nesting worked — the warning that exists to say a key does nothing, saying the opposite. Both are in the set now, and a new test derives the check from the parser (`fm.<key>` for every field it reads) instead of from a fixture that drifted with it: the previous fixture listed every key except these two.
22
41
  - **A result read synchronously via `get_subagent_result` no longer also arrives as a completion notification — and mid-turn completions are now injected at the next possible moment** ([#13](https://github.com/cad0p/pi-subagents-tintinweb/issues/13)). When a background subagent finished while the parent was mid-turn (e.g. blocked in a long tool call), the completion notification was sent after only a 200ms hold — a window the parent physically could not call `get_subagent_result` within — and pi's fire-and-forget `sendMessage` made the queued followUp un-retractable, so the report was delivered twice and the notification's `triggerTurn` burned an extra assistant turn on "that's just the notification, already covered above". Notification sends are now parked while the main session is mid-turn (tracked via `turn_start`/`turn_end`), so `resultConsumed`/`cancelNudge` stays effective until the send; an armed nudge whose grace window expires inside a *new* turn re-parks instead of firing. Idle-session behavior is unchanged. Terminal `get_subagent_result` results now also carry the notification details and render as markdown in the TUI (same header/body renderer as the notification), so the synchronous read looks like the notification it suppresses.
23
42
  The mid-turn delivery itself is the second half of the fix: parked notifications are released at the next main-session message boundary (`message_end` / `tool_execution_end`) and sent through the steering queue (`deliverAs: "steer"`), which pi's agent loop drains right after such a boundary — before the parent's next LLM call — so a background completion lands right after the parent's current tool call instead of only when the whole turn ends. `turn_end` remains the fallback release point (delivered as a new turn, as before), which also covers the loop's error/abort path where the steering queue is never drained again. The send closures re-check `resultConsumed` at fire time, so a `get_subagent_result(wait: true)` call that resolves mid-turn still suppresses the push; a pull that happens *after* the boundary already fired simply re-reads the same report in-context (same turn, no extra turn).
24
43
  - **`isolation: "worktree"` agents are now told to stay in the worktree.** The isolation was advisory-only: the agent's cwd was set to the temporary worktree, but append-mode agents inherit the parent's system prompt verbatim — which names the *shared checkout* as the working directory, in detail, before everything else. Observed failure: two parallel worktree agents both `cd`'d to the main checkout for every operation (edits, `git checkout -b`, commit, push), raced on its HEAD — one agent's branch ended up stacked on the other's — and left the main tree checked out on an agent branch, silently redirecting the parent's later `git pull`/`git log`. The system prompt now carries a two-line `<worktree_isolation>` directive (worktree spawns only) naming the copy and the shared parent directory and telling the agent to never edit the latter, even when inherited instructions name it. Plumbed as `RunOptions.worktree` → `PromptExtras.worktree`; rendered by `buildAgentPrompt` in both append and replace modes. Enforcement (blocking bash commands that reference the parent path) is deliberately out of scope — a bash-capable agent can always `cd`, so this fixes the prompt-level root cause, not the theoretical one.
package/CONTRIBUTING.md CHANGED
@@ -53,6 +53,9 @@ npm run build # tsc
53
53
  All four must pass. `npm run lint:fix` will auto-fix most style issues, and
54
54
  `npm run test:e2e` runs the end-to-end suite if your change touches that surface.
55
55
 
56
+ For the full pre-review checklist — manual TUI/session checks and evidence
57
+ expectations — see [`docs/verification-checklist.md`](docs/verification-checklist.md).
58
+
56
59
  Other guidelines:
57
60
 
58
61
  - Keep PRs focused — one logical change per PR. Unrelated refactors make review
@@ -61,6 +64,11 @@ Other guidelines:
61
64
  - Match the surrounding code style (enforced by biome).
62
65
  - Do not edit `CHANGELOG.md`. Changelog entries are added by the maintainer.
63
66
  - Update the README when you add or change user-facing behavior.
67
+ - If your change adds an upstream feature, removes something listed there, or
68
+ establishes a new fork-only guarantee, update
69
+ [`docs/fork-differences.md`](docs/fork-differences.md) in the same PR. That page
70
+ is what keeps the next upstream merge from re-introducing a decision we already
71
+ made.
64
72
 
65
73
  ## Questions?
66
74
 
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @cad0p/pi-subagents-tintinweb
2
2
 
3
- A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
3
+ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Spawn them as background runs, steer them mid-run, resume completed sessions, and define your own custom agent types.
4
+
5
+ > **This is a fork of [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents).** It tracks upstream but is not a mirror: some upstream features are deliberately not ported (workflows, `@handle` mentions, package-shipped agents, foreground mode, join modes), and some behaviour here has no upstream counterpart (background-only spawning, the `checkpoint` tool, the session-retention model, parent-mirrored built-in activation). Before filing a difference as a bug, check [`docs/fork-differences.md`](docs/fork-differences.md) — it lists both directions with the reason for each.
4
6
 
5
7
  <img width="600" alt="pi-subagents screenshot" src="https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png" />
6
8
 
@@ -11,10 +13,10 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
11
13
  ## Features
12
14
 
13
15
  - **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
14
- - **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
15
- - **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`
16
+ - **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4)
17
+ - **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` (default — every agent) or `off` (hide the widget)
16
18
  - **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`
17
- - **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). 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. Stop a still-running agent by pressing `x` (then `x` again to confirm) — both work for background agents too
19
+ - **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). 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. Stop a still-running agent by pressing `x` (then `x` again to confirm) — both work for any running agent
18
20
  - **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
19
21
  - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
20
22
  - **Session resume** — pick up where an agent left off, preserving full conversation context
@@ -26,10 +28,10 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
26
28
  - **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
27
29
  - **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)
28
30
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
29
- - **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
31
+ - **Markdown completion notifications** — each background completion arrives as its own markdown report (status line, agent ID, transcript path, result body) in pi's standard extension-message box, rendered by pi's default custom-message component. The same text is what the parent model reads; delivery goes through pi's steering queue, so `steeringMode: "all"` delivers several completions in one turn
30
32
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
31
33
  - **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on session start
32
- - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
34
+ - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same steering-queue notification path as manual background completions; manage via `/agents → Scheduled jobs`
33
35
  - **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Frontmatter-pinned or parent-inherited out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
34
36
 
35
37
  ## Install
@@ -53,11 +55,10 @@ Agent({
53
55
  subagent_type: "Explore",
54
56
  prompt: "Find all files that handle authentication",
55
57
  description: "Find auth files",
56
- run_in_background: true,
57
58
  })
58
59
  ```
59
60
 
60
- Foreground agents block until complete and return results inline. Background agents return an ID immediately and notify you on completion.
61
+ The call returns an agent ID immediately and the result arrives through the completion notification (and `get_subagent_result` on demand). Resume a completed agent by ID to continue its work.
61
62
 
62
63
  ### Scheduling
63
64
 
@@ -79,21 +80,24 @@ Schedule formats:
79
80
  - **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
80
81
  - **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
81
82
 
82
- When a schedule fires, the spawn runs in background and its completion notification arrives in the conversation through the same `subagent-notification` followUp path as a manually-spawned background agent — your parent agent reasons about the result the same way.
83
+ When a schedule fires, the spawn runs in background and its completion notification arrives in the conversation through the same steering-queue path as a manually-spawned background agent — your parent agent reasons about the result the same way.
84
+
85
+ Interval delays are capped by the JS timer ceiling (2,147,483,647 ms, ~24.8 days): longer intervals are rejected at creation, while a one-shot further out stays scheduled and arms at a later scheduler start once it falls within range.
83
86
 
84
87
  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.
85
88
 
89
+ Malformed store records are skipped rather than crashing the menu: a job whose shape is invalid (missing id, unknown agent type, unparseable cron) is hidden from the scheduler but kept verbatim in the store file, with a one-time warning naming the file and count. Fix such an entry by editing the file, or delete it to discard the job. Duplicate ids are kept too: the first record stays live and the shadowed duplicate is stored verbatim but never armed — deleting the id removes both records. Entries nested past a safe depth are dropped with a warning, and a non-array `jobs` container or non-object file is rewritten on the next change.
90
+
86
91
  **Disable the feature entirely**: `/agents → Settings → Scheduling → disabled` removes `schedule` from the `Agent` tool spec (no LLM-context cost), hides the menu entry, and stops any active scheduler. The schema-level removal takes effect on the next pi session; the runtime kill is immediate. Re-enable from the same menu.
87
92
 
88
93
  Restrictions:
89
94
  - `schedule` cannot be combined with `inherit_context` (no parent conversation exists at fire time) or `resume` (schedules create fresh agents).
90
- - `run_in_background` is forced to `true`.
91
95
  - Scheduled fires bypass the `maxConcurrent` queue so a 5-minute interval cannot be deferred behind long-running manual agents.
92
- - **Headless `pi -p` doesn't wait for scheduled subagents.**
96
+ - **Headless `pi -p` does not wait for background subagents** — the process exits at the parent settle and running children are aborted before their reports can be delivered. Tracked in [#35](https://github.com/cad0p/pi-subagents-tintinweb/issues/35).
93
97
 
94
98
  ## UI
95
99
 
96
- The extension renders a persistent widget above the editor showing active agents. By default it shows background runs only (`widgetMode: background`) — foreground agents already render inline as the `Agent` tool result, so the widget would otherwise double-render them. Switch to `all` (every agent) or `off` (hide the widget) via `/agents → Settings → Widget`:
100
+ The extension renders a persistent widget above the editor showing active agents. It defaults to `all` (every agent); switch to `off` to hide the widget via `/agents → Settings → Widget`:
97
101
 
98
102
  ```
99
103
  ● Agents
@@ -129,25 +133,29 @@ Individual agent results render Claude Code-style in the conversation:
129
133
 
130
134
  | State | Example |
131
135
  |-------|---------|
132
- | **Running** | `⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%)` / `⎿ searching, reading 3 files…` |
133
- | **Completed** | `✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s` / `⎿ Done` |
134
- | **Wrapped up** | `✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s` / `⎿ Wrapped up (turn limit)` |
135
- | **Stopped** | `■ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Stopped` |
136
+ | **Background** | `⎿ Running in background (ID: 56493b20-4d5d-4de)` |
136
137
  | **Error** | `✗ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
137
138
  | **Aborted** | `✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3)` / `⎿ Aborted (max turns exceeded)` |
138
139
 
139
- Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
140
+ The `Agent` tool row only ever launches a background run. Rows recorded before foreground mode was removed (`running`/`completed`/`steered`/`stopped`) replay through the error/aborted tail without a result body; completion notifications render their full report directly.
140
141
 
141
- 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 render as styled boxes:
142
+ By default, each `Agent`-tool spawn streams its 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); RPC and scheduled spawns write no transcript. 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` (whether the pi session file is retained after the run — see [Session Persistence](#session-persistence)), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files). Note that every run writes a pi session file *while* it runs, so keeping a completed run off disk means `output_transcript: false` **and** no `persist_session: true` (the session file is deleted when the agent record is removed) **and** none of `isolation: worktree` / `memory:`. Background agent completion notifications render as markdown reports inside pi's standard extension-message box (`[subagent-notification]` label):
142
143
 
143
144
  ```
144
- ✓ Find auth files completed
145
- ↻3 · 3 tool uses · 12.4k token · 4.1s
146
- ⎿ Found 5 files related to authentication...
147
- transcript: .pi/output/agent-abc123.jsonl
145
+ **✓ Subagent completed: Find auth files** · ↻3≤30 · 3 tool uses · 12.4k token · 4.1s
146
+
147
+ Agent: 56493b20-4d5d-4de
148
+ Transcript: /var/folders/.../tasks/56493b20-4d5d-4de.output
149
+
150
+ Result:
151
+
152
+ Found 5 files related to authentication:
153
+ - src/auth.ts
148
154
  ```
149
155
 
150
- Group completions render each agent as a separate block. The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
156
+ The notification report is markdown-wrapped child output with terminal control sequences (OSC/CSI escapes, CR, and invisible format characters) stripped from the header, metadata, and body, and unpaired UTF-16 surrogates replaced with U+FFFD (a removal can instead join a split pair back into its character; the result is always well-formed), before the notification is rendered or persisted; markdown structure and body text are otherwise unchanged. The LLM-facing tool text is sanitized per surface: `get_subagent_result` collapses its single-line metadata (agent id, type, status, description, and the checkpoint/transcript paths) through the single-line sanitizer, while its result and error bodies — along with its not-found ids and error strings — stay raw; the spawn result (`Agent started in background.` / `Agent resumed in background.`, with its `Type:`, `Description:`, `Output file:`, and queue-position lines) is returned raw as well. Per-invocation model name and invocation tags are display-only fields on the `Agent` stats line and in the conversation viewer, sanitized for display; they are not part of the tool result text (a configured agent type's default model does appear in the full `Agent` tool description, which lists each type with its model and tools).
157
+
158
+ Each completion is its own notification, delivered through pi's steering queue. To receive several completions in a single parent turn, set pi's `steeringMode: "all"` (pi's default `"one-at-a-time"` delivers one per turn).
151
159
 
152
160
  ## Default Agent Types
153
161
 
@@ -215,20 +223,24 @@ All fields are optional — sensible defaults for everything.
215
223
  | `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
216
224
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
217
225
  | `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
218
- | `isolation` | — | Set to `worktree` to run in an isolated git worktree |
226
+ | `isolation` | — | `worktree` runs the agent in an isolated git worktree. `off` (also `none` / `no` / `false`) refuses one even when the caller asks — frontmatter outranks tool-call params |
219
227
  | `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`). 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 |
228
+ | `scoped_models` | inherit parent's scope | Child session's model scope: `inherit` (or omit; must stand alone) uses the parent session's `/scoped-models` scope, `none` gives the child no scope, or a comma-separated list of `provider/modelId` entries replaces it with an explicit chain. Feeds `ctx.scopedModels` for in-session extensions — e.g. `pi-fallback-provider`'s automatic fallback after exhausted retries. Alias: `inherit_scoped_models`. See [Model Scope](#model-scope) |
220
229
  | `thinking` | inherit | off, minimal, low, medium, high, xhigh, max — actual availability depends on your pi version and model; pi clamps unsupported levels down |
221
230
  | `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
222
- | `persist_session` | `false` | Persist this subagent as a normal pi session instead of keeping the session in memory only. The subagent's `.output` transcript is still written either way unless `output_transcript: false` |
231
+ | `persist_session` | `false` | Keep this subagent's pi session file after it settles. Every run writes a session file while it runs (so a crashed run stays recoverable); without this flag the file is deleted when the agent record is removed — the 10-minute eviction or a session switch. The subagent's `.output` transcript is a separate knob, still written unless `output_transcript: false`. See [Session Persistence](#session-persistence) |
223
232
  | `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:` |
224
- | `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 |
233
+ | `session_dir` | pi default | Directory for this agent's pi session file (written during the run; retained after it only with `persist_session: true`). Omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
225
234
  | `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) |
226
235
  | `inherit_context` | `false` | Fork parent conversation into agent |
227
- | `run_in_background` | `false` | Run in background by default |
228
236
  | `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
237
+ | `allowed_subagents` | — | Opt this agent into nested delegation: `all`, or a comma-separated list of agent types it may spawn. Omitted = no nested tools at all |
238
+ | `max_subagent_depth` | inherit | Per-agent nesting cap. Can only tighten the project's `maxSubagentDepth`, never raise it |
229
239
  | `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
230
240
 
231
- Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified.
241
+ Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified. `model` and `thinking` are frontmatter-only — the `Agent` tool doesn't expose them. When an agent file leaves them unset, the subagent inherits the parent session's model and uses pi's configured thinking level for that model (per-model setting, else `defaultThinkingLevel`).
242
+
243
+ A key this extension doesn't read (a typo, or a field from another tool such as `run_in_background`, `join_mode`, or `name:`) is a silent no-op in the field-by-field parser, so it is reported once per file per key at load — same warning channel as a malformed file — instead of being dropped quietly. A file whose frontmatter cannot be parsed is skipped with a warning naming the path (and, when it was overriding another agent, the file that now wins); `strictAgentFiles` turns that skip into a startup failure.
232
244
 
233
245
  **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.)
234
246
 
@@ -246,9 +258,11 @@ tools: "*, ext:mcp/search" # built-ins plus one extension tool
246
258
 
247
259
  extensions: false # no extensions load
248
260
  extensions: [mcp] # only mcp loads
261
+ extensions: [builtin:mcp, builtin:codemode] # pi's built-in MCP + codemode
249
262
  extensions: ["*", "/abs/foo.ts"] # all defaults plus one path-loaded extension
250
263
 
251
264
  exclude_extensions: pi-notify # everything except pi-notify (with extensions: true)
265
+ exclude_extensions: builtin:mcp # everything except pi's built-in MCP
252
266
 
253
267
  # Specialist: load one extension, expose only one of its tools, keep built-ins
254
268
  extensions: [mcp]
@@ -262,6 +276,9 @@ A few rules the examples don't make obvious:
262
276
  - `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.
263
277
  - Any `ext:` entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So `tools: "*, ext:mcp/search"` exposes only `search` from `mcp`, nothing from any other extension.
264
278
  - Extension names match case-insensitively (`[Mcp]` = `[mcp]`); tool names in `ext:foo/bar` stay case-sensitive.
279
+ - pi's built-in extensions (`builtin:mcp`, `builtin:codemode`, `builtin:tool-search`) load in subagents as they do in the main session, so with `extensions: true` **every subagent starts its own set of `mcp.json` servers**. Address them by the full `builtin:` name — bare `[mcp]` matches a third-party extension named `mcp`, not the built-in — and narrow their tools with `ext:builtin:mcp/<tool>`. MCP tools are reached through codemode (or `tool_search` for `deferred` servers), so an `ext:` allowlist must name that bridge too, as pi's `--tools read,codemode,'mcp__*'` does: `tools: "read, ext:builtin:codemode, ext:builtin:mcp/mcp__radius__search"`. Opt out per agent with `exclude_extensions: builtin:mcp`, or everywhere with `-builtin:mcp` in pi's `extensions` setting; starting pi with `--no-mcp` keeps MCP out of subagents too, and `--no-extensions` keeps all three built-ins out. MCP tools stay undeclared until codemode or `tool_search` reaches them, as in the main session, and a codemode script can only call tools inside the agent's scope. `codemode` and `tool_search` themselves are `defaultActive: false` in pi: listing `builtin:codemode` under `extensions:` loads the extension but leaves the tool dormant unless the parent session has it active or the agent names it (`tools: "read, ext:builtin:codemode"`).
280
+ - Subagents inherit the parent session's **project trust**: in a project you did not trust, a subagent loads none of its `.pi` extensions or settings and never starts the servers in its `.pi/mcp.json`. A subagent spawned into another directory (`Agent` tool `cwd`) still uses the parent project's settings but treats that directory as untrusted.
281
+ - An append-mode agent's prompt inherits the parent's, so the `<tools>` block it carries is rewritten to list exactly the tools that agent has. A subagent without codemode or `tool_search` is no longer told it has them, and one that loads them is told about them.
265
282
  - An installed **package** extension matches by its package short name (`@scope/pi-subagents` → `[pi-subagents]`), in addition to its path-derived name (a package whose entry is `src/index.ts` also answers to `[src]`). Prefer the package name — the path-derived one is incidental.
266
283
  - Plain `tools:` typos fail loudly: `tools: reed, grep` fires `tools-error:…` instead of silently producing an under-tooled agent.
267
284
  - `exclude_extensions:` wins over `extensions:` and over `ext:` selectors — an excluded extension never loads and a `tools: ext:` entry can't pull it back. Plain names only (no paths, no `*`); a name matching nothing fires an `extension-error:…` warning.
@@ -279,17 +296,17 @@ Launch a sub-agent.
279
296
  | `prompt` | string | yes | The task for the agent |
280
297
  | `description` | string | yes | Short 3-5 word summary (shown in UI) |
281
298
  | `subagent_type` | string | yes | Agent type (built-in or custom) |
282
- | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
283
299
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
284
- | `run_in_background` | boolean | no | Run without blocking |
285
300
  | `resume` | string | no | Agent ID to resume a previous session |
286
301
  | `isolated` | boolean | no | No extension/MCP tools |
287
- | `isolation` | `"worktree"` | no | Run in an isolated git worktree |
302
+ | `isolation` | `"worktree"` or `"off"` | no | `worktree` runs in an isolated git worktree; `off` declines one (the default) |
288
303
  | `inherit_context` | boolean | no | Fork parent conversation into agent |
289
304
 
305
+ Press ctrl+o to show the initial prompt in the tool row (the collapsed row shows only the type and description).
306
+
290
307
  ### `get_subagent_result`
291
308
 
292
- Check status and retrieve results from a background agent.
309
+ Check a subagent's status and retrieve its result. Use the agent ID returned by the `Agent` tool.
293
310
 
294
311
  | Parameter | Type | Required | Description |
295
312
  |-----------|------|----------|-------------|
@@ -297,13 +314,15 @@ Check status and retrieve results from a background agent.
297
314
 
298
315
  ### `steer_subagent`
299
316
 
300
- Send a steering message to a running agent. The message interrupts after the current tool execution.
317
+ Send a steering message to a subagent. The message interrupts after the current tool execution.
301
318
 
302
319
  | Parameter | Type | Required | Description |
303
320
  |-----------|------|----------|-------------|
304
- | `agent_id` | string | yes | Agent ID to steer |
321
+ | `agent_id` | string | yes | Agent ID to steer (must be running or queued) |
305
322
  | `message` | string | yes | Message to inject into agent conversation |
306
323
 
324
+ The message shows as a one-line preview in the tool row; press ctrl+o to expand the full text.
325
+
307
326
  ### `checkpoint`
308
327
 
309
328
  Subagent-facing: a subagent calls this at milestones to save a one-sentence progress summary for the parent. The latest summary is surfaced inline in `get_subagent_result`'s output; the full history lives in a `.checkpoints.md` file alongside the `.output` transcript.
@@ -324,10 +343,10 @@ The `/agents` command opens an interactive menu:
324
343
  Running agents (2) — 1 running, 1 done ← only shown when agents exist
325
344
  Agent types (6) ← unified list: defaults + custom
326
345
  Create new agent ← manual wizard or AI-generated
327
- Settings ← max concurrency, max turns, grace turns, join mode
346
+ Settings ← concurrency, turns, scheduling, failure preview, fleet view, widget, …
328
347
  ```
329
348
 
330
- - **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), or 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). A stopped agent reports its partial output flagged as incomplete, not as a completion.
349
+ - **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), or press `x` (then `x` again to confirm) to stop/abort it. A stopped agent reports its partial output flagged as incomplete, not as a completion.
331
350
  - **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Each row shows the agent's model, and the highlighted agent's full description appears below the list. 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:
332
351
  - **Default agents** (no override): Eject (export as `.md`), Disable
333
352
  - **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
@@ -336,7 +355,7 @@ Settings ← max concurrency, max turns, grac
336
355
  - **Eject** — writes the embedded default config as a `.md` file to project or personal location, so you can customize it
337
356
  - **Disable/Enable** — toggle agent availability. Disabled agents stay visible in the list (marked `✕`) and can be re-enabled
338
357
  - **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file). Any name is allowed, including default agent names (overrides them)
339
- - **Settings** — configure max concurrency, default max turns, grace turns, and join mode at runtime
358
+ - **Settings** — runtime tuning for max concurrency, default max turns, grace turns, scheduling, scope models, failure preview max chars, disable defaults, output transcript, fleet view, widget, and tool description
340
359
 
341
360
  ## Graceful Max Turns
342
361
 
@@ -357,22 +376,22 @@ Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
357
376
 
358
377
  Background agents are subject to a configurable concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. The widget shows queued agents as a collapsed count.
359
378
 
360
- Foreground agents bypass the queue — they block the parent anyway.
379
+ Resumed runs (`resume`) are exempt: a resume starts immediately, occupies a pool slot while it runs, and is never queued — a burst of resumes can therefore push the actual concurrency above `maxConcurrent` by design.
380
+
381
+ ## Session Persistence
361
382
 
362
- ## Join Strategies
383
+ Every subagent run is written to a normal pi session file while it runs — default agents included — so a crashed, killed, or interrupted run leaves a transcript pi's `/resume` can recover. The child's session header records the parent session file as `parentSession`, so subagent sessions nest under the run that spawned them in pi's `/resume` picker.
363
384
 
364
- When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
385
+ Retention is decided when the run settles, not when the session is created:
365
386
 
366
- | Mode | Behavior |
367
- |------|----------|
368
- | `smart` (default) | 2+ background agents spawned in the same turn are auto-grouped into a single consolidated notification. Solo agents notify individually. |
369
- | `async` | Each agent sends its own notification on completion (original behavior). Best when results need incremental processing. |
370
- | `group` | Force grouping even when spawning a single agent. Useful when you know more agents will follow. |
387
+ | `persist_session` | Behavior |
388
+ |---|---|
389
+ | `false` (default) | The session file is deleted when the agent record is removed: the 10-minute eviction after completion, `clearCompleted()` on session switch, or a clean shutdown once the run has settled. Until then the completed record can still `resume` the agent, which is why the file outlives the run itself. |
390
+ | `true` | The session file is kept indefinitely. |
371
391
 
372
- **Timeout behavior:** When agents are grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.
392
+ Shutdown keeps an in-flight run's file — a run killed mid-flight is the case this exists for — and deletes a settled one's, because the manager's map is discarded and nothing would ever revisit it. `session_dir:` picks the directory the file is written to. On a host session with no file of its own (`--no-session`, or an SDK host using an in-memory session) subagent sessions are in-memory too and explicitly not recoverable. A resumed agent keeps the same session file and the same retention decision as its first run.
373
393
 
374
- **Configuration:**
375
- - Configure join mode in `/agents` → Settings → Join mode
394
+ > **Migration — `persist_session` changed meaning.** It used to mean "persist this subagent at all" (without it, the run used an in-memory session and left nothing behind). It now means "retain this subagent's session file after it settles". An agent with `persist_session: false` — or no `persist_session` at all — now writes a session file *during* the run; that file is deleted when the agent record is removed, so completed runs still leave nothing behind unless pi crashes mid-run (which is the point). Agents with `persist_session: true` behave as before, plus the parent link.
376
395
 
377
396
  ## Model Scope
378
397
 
@@ -393,18 +412,36 @@ When on, each subagent spawn's effective model is validated against pi's own `en
393
412
 
394
413
  **No-op safety:** if `enabledModels` is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
395
414
 
415
+ **Child-session scope.** Subagents inherit the parent session's resolved scope by default, so in-session extensions that read `ctx.scopedModels` — notably [`pi-fallback-provider`](https://github.com/cad0p/pi-fallback-provider)'s automatic fallback after exhausted retries — work inside subagent sessions too. A custom agent's `scoped_models:` frontmatter overrides this per agent: `none` opts out (empty scope, no in-session cycling or fallback) and a comma-separated `provider/modelId` list replaces the inherited scope with an explicit chain (entries resolve like `model:`; unresolved entries are skipped with a warning). Isolated agents load no extensions, so their scope stays empty. This is independent of the `scopeModels` setting above: `scopeModels` only *warns* when a spawn's model falls outside the user's allowlist; `scoped_models` decides what the child session itself carries.
416
+
396
417
  ## Persistent Settings
397
418
 
398
- Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
419
+ Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, scheduling on/off, scope models on/off, strict agent files on/off, fallback agent, disable defaults on/off, output transcript on/off, worktree isolation on/off, report usage on/off, show cost on/off, show model on/off, fleet view on/off, failure preview max chars, tool description full/compact/custom, widget all/off) persist across pi restarts. Two files, merged on load:
399
420
 
400
421
  - **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
401
422
  - **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
402
423
 
403
- **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, join mode `smart`, defaults enabled).
424
+ **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, defaults enabled).
404
425
 
405
426
  **Disable defaults** (`disableDefaultAgents`, default `false`): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
406
427
 
407
- **Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's `.output` transcript. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files.
428
+ **Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's `.output` transcript. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files. A run's pi session file is written during the run regardless; `persist_session` decides whether it survives the agent record's removal (see [Session Persistence](#session-persistence)).
429
+
430
+ **Fallback agent** (`fallbackSubagent`, default `general-purpose`): the agent used when a caller-supplied `subagent_type` doesn't resolve to exactly one enabled agent — unknown, disabled, or ambiguous because two agents differ only by case. Name any enabled agent to route those calls there instead, or set `none` for **strict**, fail-closed dispatch: the call is refused with an error listing the available types, and nothing spawns. Strict mode matters most for background and scheduled calls, which would otherwise start executing a substituted agent before the caller learns anything. Also settable from `/agents → Settings → Fallback agent`. The boolean `false` is accepted as a spelling of `none`, because it would otherwise be dropped as the wrong type and silently leave the permissive default in place. Every other value is read as an agent name, so a mistaken `off` fails loudly at dispatch rather than meaning one thing in the settings file and another in the resolver. A fallback agent that is itself unknown or disabled is a misconfiguration and is reported rather than quietly replaced. The fallback applies to the `Agent` tool, the scheduler at fire time, and cross-extension RPC; a scheduled job stores the type the caller actually named, and a resume is exempt because it replays a stored session and ignores `subagent_type`. A job whose type was already unknown when the store loaded is hidden from the scheduler there and kept verbatim in the store file, so it never reaches dispatch: the fire-time policy covers the registry changes that happen while a job is armed.
431
+
432
+ **Worktree isolation** (`worktreeIsolation`, default `true`): whether `isolation: worktree` may create a worktree at all. Set `false` on a repo where a copy costs too much time or disk: a requested worktree is then dropped and the agent runs in the main checkout, on every path — agent files, scheduled jobs, and cross-extension RPC. The refusal is immediate; the `isolation` parameter and its usage note are also dropped from the Agent tool spec (next pi session) so the model is never told a capability exists. Custom tool descriptions opt in to the matching prose with `{{isolationGuideline}}`.
433
+
434
+ **Strict agent files** (`strictAgentFiles`, default `false`): when on, an unreadable or unparseable `agents/*.md` aborts extension load, naming the file, instead of being skipped with a warning. Startup only, by design — mid-session reloads (one per `Agent` call) keep warning, so a bad edit at 3pm cannot kill the session on the next unrelated spawn. For a checked-in `.pi/agents/`, failing at startup is the point: the alternative is running a *different* agent than the file names.
435
+
436
+ **Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on nested subagent delegation, counted from the main session — main is 0, its subagents are 1, their children are 2. `0` or `1` disables nesting project-wide. An agent only gets the nested tools (`Agent`, `get_subagent_result`, `steer_subagent`) when its own frontmatter opts in with `allowed_subagents` *and* it sits below the cap; its `max_subagent_depth` can tighten the project cap but never raise it. A nested child does not take a `maxConcurrent` slot — its parent already holds one — and it is aborted when its parent settles, since only the parent can reach it.
437
+
438
+ **Report usage** (`reportUsage`, default `true`): subagents run in their own pi session, so without this the parent's footer, statusline and `/cost` show only what the main model spent — a session that delegated most of its work reads as nearly free. With this on, each `Agent` / `get_subagent_result` / `steer_subagent` result carries the spend accumulated since the last one, and pi folds it into the session totals under `/cost`'s "Tools/summaries" bucket. Tokens include `cacheRead` (pi bills the cached prefix on every call), cost is pi's own per-message figure, and the context-window percentage is unaffected. Two consequences worth knowing: an agent that finishes in the background has no result of its own to ride on, so its spend lands on the **next** call rather than the moment it finishes, and a session that ends with nothing pending reports nothing. Nested children are counted once each, on the top-level result. On by default here, unlike upstream: a session total that omits the delegated work is wrong about what the session cost, and the setting stays available for anyone tracking the old numbers. Turning it on does move numbers a statusline may already be reading, which is the one reason it is a setting rather than unconditional.
439
+
440
+ **Show cost** (`showCost`, default `true`): `~$0.0042` beside the subagent token counts in the widget, FleetView, the conversation viewer, `get_subagent_result` and completion notifications. Independent of `reportUsage`: that one changes the session's totals, this one is what a human reads. Omitted entirely when pi has no pricing data for the model, so `$0.00` never means "measured, free".
441
+
442
+ **Show model** (`showModel`, default `false`): name the model and thinking level in the widget's running rows. 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 conversation viewer and the `Agent` result row show the pair either way (the result row names the model the spawn resolved, not the child's read-back). What they show is the *effective* value, read back from the child session rather than re-derived from the request — and when pi clamps a level the request is kept beside it (`low (asked max)`), so a clamp is never presented as what was asked for.
443
+
444
+ **Failure preview** (`failurePreviewMaxChars`, default `65536`): caps the `Result:` body of `error`/`stopped` completion notifications, plus the metadata `Error:` line that carries a long truncated error alongside a partial result. The untruncated text remains available via `get_subagent_result` and the session record (and in the transcript when one exists).
408
445
 
409
446
  **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.
410
447
 
@@ -417,7 +454,7 @@ Launch an autonomous agent. Available types:
417
454
  Custom agents live in .pi/agents/ or {{agentDir}}/agents/.
418
455
  ```
419
456
 
420
- Placeholders: `{{typeList}}` (full per-agent descriptions), `{{compactTypeList}}` (first sentence each), `{{agentDir}}`, `{{scheduleGuideline}}` (expands with its own leading newline + `- ` bullet when scheduling is on — place it directly after your last rule line; empty when scheduling is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to `"full"` with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.
457
+ Placeholders: `{{typeList}}` (full per-agent descriptions), `{{compactTypeList}}` (first sentence each), `{{agentDir}}`, `{{scheduleGuideline}}` (expands with its own leading newline + `- ` bullet when scheduling is on — place it directly after your last rule line; empty when scheduling is off), `{{isolationGuideline}}` (same shape, empty when `worktreeIsolation` is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to `"full"` with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.
421
458
 
422
459
  **Starting point:** copy [`examples/agent-tool-description.md`](examples/agent-tool-description.md) — it reproduces the default full description exactly (a CI test keeps it in sync), so you can trim from a known-good baseline instead of writing from scratch.
423
460
 
@@ -443,10 +480,10 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
443
480
 
444
481
  | Event | When | Key fields |
445
482
  |-------|------|------------|
446
- | `subagents:created` | Background agent registered | `id`, `type`, `description`, `isBackground` |
483
+ | `subagents:created` | Background agent registered | `id`, `type`, `description` |
447
484
  | `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
448
- | `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
449
- | `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | same as completed + `error`, `status` |
485
+ | `subagents:completed` | Agent finished successfully | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
486
+ | `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
450
487
  | `subagents:steered` | Steering message sent | `id`, `message` |
451
488
  | `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
452
489
  | `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
@@ -506,12 +543,16 @@ pi.events.emit("subagents:rpc:spawn", {
506
543
  requestId,
507
544
  type: "general-purpose",
508
545
  prompt: "Do something useful",
509
- options: { description: "My task", run_in_background: true },
546
+ options: { description: "My task" },
510
547
  });
511
548
  ```
512
549
 
550
+ RPC spawns share the same pool as `Agent`-tool spawns: they count toward `maxConcurrent`, queue behind a full pool, and deliver the standard completion notification to the session (unless the result was consumed via `get_subagent_result` first).
551
+
513
552
  `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.
514
553
 
554
+ `options.signal` (parent abort → stop the subagent) is intended for in-process callers that hold the manager directly (the manager registry or a direct reference). Cross-extension callers should pass serializable option values: `signal` is not part of the documented RPC surface and may not survive a serialized boundary. The `Agent` tool no longer forwards its tool-call signal to child runs.
555
+
515
556
  `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.
516
557
 
517
558
  ### Stop
@@ -613,14 +654,13 @@ This is useful for creating agents that inherit extension tools but should not h
613
654
 
614
655
  ```
615
656
  src/
616
- index.ts # Extension entry: tool/command registration, rendering
657
+ index.ts # Extension entry: tool/command registration, notification reports
617
658
  types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
618
659
  default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
619
660
  agent-types.ts # Unified agent registry (defaults + user), tool name resolution
620
661
  agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
621
662
  agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
622
663
  cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
623
- group-join.ts # Group join manager: batched completion notifications with timeout
624
664
  custom-agents.ts # Load user-defined agents from .pi/agents/, .agents/agents/, and global agents
625
665
  memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
626
666
  skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
@@ -0,0 +1,51 @@
1
+ # Fork differences from upstream
2
+
3
+ This extension is a fork of [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents). It tracks upstream, but it is not a mirror: some areas are deliberately not ported, and some behaviour here has no upstream counterpart. This page exists so a difference can be told from a missing port — and so the next merge has a list to check against instead of re-deriving the decisions.
4
+
5
+ When a change adds or removes something on either list, update this page in the same PR.
6
+
7
+ ## Kept here, not in upstream
8
+
9
+ Each of these is a guarantee. A port that breaks one is a regression, not a rebase.
10
+
11
+ **Background-only spawning.** The `Agent` tool always launches a background run and returns an id. There is no foreground mode, no `run_in_background` parameter, no `backgroundByDefault` setting, and no Ctrl+B handoff. Upstream keeps a foreground path and made background the default behind a setting; this fork removed the second path instead, because two spawn modes doubled every lifecycle, notification and cancellation decision.
12
+
13
+ **`get_subagent_result` shape.** The result report is this fork's: the turn counter (in the running report), the latest checkpoint, the checkpoint-history and transcript paths, and the status note. There is no `verbose` parameter — the report is always the report. The top-level tool takes only `agent_id` and cannot wait: `Agent` returns an id and the parent is notified on completion. Waiting exists only on the nested twin (`wait: true`), where a child has no notification path of its own.
14
+
15
+ **`checkpoint`.** A separate tool a subagent calls at milestones to record a progress note — what it just did, what it is doing next — which the parent sees in the result report. It is not a resume marker: resume replays the session. Upstream has no equivalent.
16
+
17
+ **Session retention model.** Every run writes a pi session file *while it runs*, so a crashed or killed run stays recoverable; retention is decided when the agent record is removed, and `persist_session: true` means "keep this file after the run settles". Upstream persists by default behind `rememberAgents` and lets `persist_session: false` keep an agent in memory. There is no `rememberAgents` here: one behaviour to reason about instead of two settings that interact.
18
+
19
+ **Parent-mirrored built-in activation.** Subagents load pi's built-in extensions (`codemode`, `tool-search`, `mcp`), but a `defaultActive: false` tool is declared in the child only when the parent session has it active or the agent names its extension with `ext:`. "As in the main session" means the main session's actual tool set, not everything that happens to be loaded.
20
+
21
+ **Nested delegation is background-only.** `allowed_subagents` gives an agent its own `Agent`/`get_subagent_result`/`steer_subagent`/`checkpoint` tools; `max_subagent_depth` caps the tree. A nested child returns an id like any other spawn and does not take a `maxConcurrent` slot. Upstream's nested spawns can run inline.
22
+
23
+ **Usage and cost surfaces.** `reportUsage`, `showCost` and `showModel`, plus the `subagents:usage` / `subagents:disposed` events and `subagents:rpc:consume` / `rpc:steer`, are ports — but the *set* of surfaces is this fork's: markdown notifications instead of an XML envelope, and a per-agent nudge instead of a grouped one, so there is no batch cost total.
24
+
25
+ **`reportUsage` and `showCost` default to on.** Upstream ships both off. A session total that omits the delegated work is wrong about what the session cost, and having to find two toggles to stop being wrong is the wrong default — especially because `/agents → Settings` writes project scope only, so turning them on in one repo follows you nowhere. Both remain settings: `reportUsage` still moves numbers a statusline may already be reading, which is the one legitimate reason to leave it off. `showModel` stays off in both, because its cost is a narrow terminal row, not a wrong number.
26
+
27
+ ## Deliberately not ported
28
+
29
+ Each of these exists upstream and is not coming here without a new decision.
30
+
31
+ **Workflows.** The `Workflow` tool, the workflow script runtime, `--subagents-workflow-file`, and every `workflowId` field in the event payloads. Codemode subsumes the use case with a real language and real tools, without a ~5k-token tool description in every turn.
32
+
33
+ **Package-shipped agents and workflows.** `package-resources.ts` and the discovery of agents declared by installed pi packages. A project's `agents/` directory and the global agent dir are the two roots here.
34
+
35
+ **`@handle` mentions.** The `@name message` grammar, the mention clone session, handle-as-identity, tombstones, and `ui/agent-mention.ts`. A mention is a prompt-typing affordance that costs a main-model call and rewrites parts of `index.ts` this fork has diverged from.
36
+
37
+ **Fullscreen conversation observer.** Upstream's viewer is pi's `ScrollView` plus markdown theme; this fork's viewer is still the custom transcript format (`[User]`/`[Assistant]`/`[Tool: name]`/`[Result]`).
38
+
39
+ **Join modes and outbox delivery.** Upstream can group concurrent completions into one notification and hold them in an outbox. Here every agent's completion is its own markdown message, delivered through pi's steering queue.
40
+
41
+ **`name:` as identity.** An agent is identified by its type. Upstream lets the *caller* name a spawn (`name:` on the `Agent` tool), making it addressable as `@name`; that goes with the mention machinery. Also not ported: per-agent colors.
42
+
43
+ **Foreground machinery.** `backgroundByDefault`, `maxConcurrentForeground`, the Ctrl+B handoff, and the agent-file `run_in_background:` writer. See "Background-only spawning" above.
44
+
45
+ **Per-spawn `model` and `thinking` overrides on `Agent`.** Here both come from the agent file (or the parent session's defaults); the tool schema carries neither parameter, so a model is never chosen per call. Cross-extension RPC and the scheduler still accept a model string, and the fuzzy matcher stays load-bearing for frontmatter pins.
46
+
47
+ ## Compatibility
48
+
49
+ - **pi peer floor: `>=1.0.4`** for `@earendil-works/pi-ai`, `pi-coding-agent` and `pi-tui`. `reportUsage` relies on pi folding `AgentToolResult.usage` into `getSessionStats()` (pi 0.81.0), so the floor is comfortably above the release that introduced it.
50
+ - **Settings are a flat list** in `/agents → Settings`, mirroring upstream's shape, and persist in `subagents.json` (global and project).
51
+ - **Tool names and calling conventions stay Claude Code-compatible** (`Agent`, `get_subagent_result`, `steer_subagent`) on purpose, even where this fork's semantics differ from upstream's.