pi-subagents 0.64.0 → 0.65.1

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
package/docs/workflows.md CHANGED
@@ -14,11 +14,19 @@ Packaged `worker`, `oracle`, and `advisor` default to forked context when a laun
14
14
 
15
15
  Child-safety boundaries are enforced at runtime:
16
16
 
17
- - Spawned child sessions do not receive the bundled `pi-subagents` skill.
17
+ - Child sessions do not receive the bundled `pi-subagents` skill.
18
18
  - Forked child context filtering removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent `subagent` tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results.
19
19
  - By default, children do not register the `subagent` tool and receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents.
20
20
  - The explicit exception is an agent whose resolved builtin `tools` includes `subagent`; that child gets a child-safe `subagent` tool for the fanout work the parent assigned, still bounded by `maxSubagentDepth`.
21
21
 
22
+ ### Failed lane recovery and execution-mode boundaries
23
+
24
+ A failure in the subagent workflow, child launch, prompt runtime, extension loading, or child tooling setup is a lane infrastructure blocker. It is not permission to silently retry through `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external execution mode.
25
+
26
+ Stop and report the exact failure, run/status, and repository/cwd/worktree/branch/ref state. Before a same-protocol retry or asking the owner, verify the worktree is clean or capture the partial diff. Retry or fix the `subagent` path only through a clear same-protocol action. For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. `interactive_shell` remains valid when the user explicitly requests visible foreground/CLI work or the task is outside the governed subagent protocol.
27
+
28
+ Pi core may print a generic `pi -ne` extension-load hint; that out-of-repo hint is not protocol-approved fallback. Configured native model/provider fallback remains governed by its own contract and does not authorize an execution-mode switch.
29
+
22
30
  ## Prompt shortcuts
23
31
 
24
32
  The package includes reusable prompt templates for common workflows. You do not need them, but they are handy when you want the same shape every time:
@@ -369,7 +377,9 @@ Each child uses the existing worktree lifecycle: it branches from clean HEAD, jo
369
377
 
370
378
  A top-level `{ workflowScript, worktree: true }` makes isolation the default for every workflow child. An individual child can override that default with `worktree: false`. Keep one writer when parallel writes are not intentionally isolated.
371
379
 
372
- Configure the worktree base directory and setup hook in [configuration.md](configuration.md).
380
+ Use `baseRef` to branch managed worktrees from a named commit or branch instead of the default `HEAD`. For example, `{ workflowScript, worktree: true, baseRef: "refs/heads/release" }` applies the release ref to children unless a child supplies its own `baseRef`. The source checkout must still be clean, and the ref must resolve to a commit before any worktree is allocated.
381
+
382
+ Configure the worktree provider, native path layout, base directory, and setup hook in [configuration.md](configuration.md).
373
383
 
374
384
  ### Lane metadata lifecycle
375
385
 
@@ -446,7 +456,7 @@ export PI_SUBAGENT_MAX_DEPTH=1
446
456
  export PI_SUBAGENT_MAX_DEPTH=0
447
457
  ```
448
458
 
449
- `PI_SUBAGENT_DEPTH` is internal and propagated automatically. Do not set it manually.
459
+ `PI_SUBAGENT_MAX_DEPTH` applies to the top-level parent; children inherit their limit through their runtime config, and their own depth is tracked there too.
450
460
 
451
461
  ## Prompt-template integration
452
462
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.64.0",
3
+ "version": "0.65.1",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "./preflight": "./src/api/preflight.ts",
17
17
  "./control-channel": "./src/api/control-channel.ts",
18
18
  "./intercom-bridge": "./src/api/intercom-bridge.ts",
19
- "./pi-args": "./src/api/pi-args.ts",
19
+ "./child-tool-plan": "./src/api/child-tool-plan.ts",
20
20
  "./shared-types": "./src/api/shared-types.ts",
21
21
  "./project-panes": "./src/api/project-panes.ts"
22
22
  },
@@ -56,8 +56,7 @@
56
56
  "test": "npm run test:unit",
57
57
  "test:unit": "node --experimental-strip-types --import ./test/support/isolated-temp-root.mjs --test test/unit/*.test.ts",
58
58
  "test:integration": "node --experimental-strip-types --import ./test/support/register-loader.mjs --test test/integration/*.test.ts",
59
- "test:e2e": "node --experimental-strip-types --import ./test/support/register-loader.mjs --test test/e2e/*.test.ts",
60
- "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e"
59
+ "test:all": "npm run test:unit && npm run test:integration"
61
60
  },
62
61
  "pi": {
63
62
  "extensions": [
@@ -91,9 +90,11 @@
91
90
  }
92
91
  },
93
92
  "dependencies": {
93
+ "@earendil-works/pi-server": "0.85.0",
94
94
  "acorn": "8.18.0",
95
95
  "jiti": "2.7.0",
96
96
  "typebox": "1.1.38",
97
+ "undici": "8.10.0",
97
98
  "yaml": "2.8.3"
98
99
  },
99
100
  "devDependencies": {
@@ -0,0 +1,13 @@
1
+ // Only loaded when the parent supplies Pi 0.85.0's missing server exports.
2
+ import { registerHooks } from "node:module";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ const aliases = JSON.parse(process.env.JITI_ALIAS);
6
+ registerHooks({
7
+ resolve(specifier, context, nextResolve) {
8
+ if (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix") {
9
+ return nextResolve(pathToFileURL(aliases[specifier]).href, context);
10
+ }
11
+ return nextResolve(specifier, context);
12
+ },
13
+ });
@@ -91,7 +91,8 @@ review.
91
91
  - Exact model names are deployment policy. Put them in user/project settings or profiles, not package guidance.
92
92
  - Give every child a compact meta-prompt checklist: objective; repo/cwd/ref; authority/edit boundary; relevant files/contracts and constraints; success/acceptance criteria; validation; expected output/report; and stop/ask conditions. See `references/prompting-and-roles.md`.
93
93
  - For mutation work, use an isolated lane/worktree when isolation, overlap, or concurrent juggling matters; keep one writer per cwd/worktree. See `references/multi-lane-orchestration.md` for lane mechanics.
94
- - Keep long/high-output validation out of chat: prefer `interactive_shell` dispatch/background monitors, bounded logs, or subagent-owned reports; return a concise summary plus report path unless same-turn output is required. See `references/execution-controls.md`.
94
+ - Keep long/high-output validation out of chat: prefer `interactive_shell` dispatch/background monitors, bounded logs, or subagent-owned reports; return a concise summary plus report path unless same-turn output is required. Do not use `interactive_shell` as an implicit fallback for a failed `subagent` lane; see `references/execution-controls.md`.
95
+ - Treat subagent workflow, child launch, prompt runtime, extension load, and child tooling setup failures as lane infrastructure blockers. Stop, report the exact failure and run/worktree state, verify a clean worktree or capture a partial diff, and use only a clear same-protocol retry or an owner-approved execution-mode fallback.
95
96
  - For cross-codebase work, record the repo, explicit `cwd`, authority boundary, and expected output before launch.
96
97
  - Make parallel prompts distinct by source seam, evidence, and decision. Do not clone prompts with only item numbers swapped.
97
98
  - Prefer fresh-context review/validation fanout, then synthesize and apply fixes in the parent.
@@ -21,7 +21,7 @@ This file is a detailed reference loaded from `skills/pi-subagents/SKILL.md`.
21
21
  become second decision-makers.
22
22
  - **Respect the fixed authority policy.** `authorityPolicy` is a small `auto` / `confirm` / `forbid` map for supported operational actions. Worktree discard, destructive cleanup, and spawn-budget grants default to confirmation; stop, steer, and schedule creation remain automatic. Use `worktree.discard` with the durable `handoffPath`; confirm-required actions refuse safely without an interactive UI and retained paths include manual Git recovery commands.
23
23
 
24
- Runtime config can change orchestration behavior. `intercomBridge.resultDelivery: false` disables only external acknowledged grouped-result delivery when native parent notifications own completion; supervisor asks/progress stay active, and enabled transport failures are still reported. `asyncByDefault` and `forceTopLevelAsync` affect whether launches detach; `waitTool` can make direct `bg_wait()` calls return immediately while headless auto-drain remains active, and its effective value is propagated to child runtimes; `globalConcurrencyLimit` bounds concurrent fanout, while a positive `maxSubagentSpawnsPerSession` optionally caps cumulative launches (`0` or unset is unlimited). Status and doctor report the budget; static work preflights declared capacity; only the settled root interactive parent can use `grant-spawn-budget` after native confirmation, with total grants bounded by the original cap. Compaction does not reset usage or grants; `singleRunOutputBaseDir` and `worktreeBaseDir` route outputs and worktrees; `completionBatch` groups async notifications. `artifactDir` is `session` (default), `project`, or `temp` and chooses where subagent artifacts are stored. Set `asyncWidget: false` to hide the above-editor background-run widget when a companion footer or dashboard owns that space (fleet inspector remains available). Per-run `artifacts: false` disables artifact capture for that launch. Async status and result artifacts include `lifecycleArtifactVersion` and fields such as `workflowGraph`, `steps`, `results`, `totalTokens`, `totalCost`, `turnCount`, `toolCount`, and nested `children`. Child protocol failures expose a structured `protocolError`; `protocol_output_limit` means a child emitted a JSONL line above the 16 MiB live-parser cap. Prefer these artifacts and `status` views over scraping terminal output.
24
+ Runtime config can change orchestration behavior. `intercomBridge.resultDelivery: false` disables only external acknowledged grouped-result delivery when native parent notifications own completion; supervisor asks/progress stay active, and enabled transport failures are still reported. `asyncByDefault` and `forceTopLevelAsync` affect whether launches detach; `waitTool` can make direct `bg_wait()` calls return immediately while headless auto-drain remains active, and its effective value is propagated to child runtimes; `globalConcurrencyLimit` bounds concurrent fanout, while a positive `maxSubagentSpawnsPerSession` optionally caps cumulative launches (`0` or unset is unlimited). Status and doctor report the budget; static work preflights declared capacity; only the settled root interactive parent can use `grant-spawn-budget` after native confirmation, with total grants bounded by the original cap. Compaction does not reset usage or grants; `singleRunOutputBaseDir` and `worktreeBaseDir` route outputs and worktrees; `completionBatch` groups async notifications. `artifactDir` is `session` (default), `project`, or `temp` and chooses where subagent artifacts are stored. Set `asyncWidget: false` to hide the above-editor background-run widget when a companion footer or dashboard owns that space (fleet inspector remains available). Per-run `artifacts: false` disables artifact capture for that launch. Async status and result artifacts include `lifecycleArtifactVersion` and fields such as `workflowGraph`, `steps`, `results`, `totalTokens`, `totalCost`, `turnCount`, `toolCount`, and nested `children`. Prefer these artifacts and `status` views over scraping terminal output.
25
25
 
26
26
  ### Keep report artifacts out of the repository root
27
27
 
@@ -236,8 +236,8 @@ Resume behavior:
236
236
  - Multi-child async runs require `index` unless only one running child is selectable.
237
237
  - Completed foreground single, parallel, and chain runs can also be revived by `index` while their run metadata remains in extension state.
238
238
  - Nested runs can be resumed by nested id when a live route or persisted nested session metadata is available.
239
- - Revive starts a new child process from the old session context; it does not restart the same OS process.
240
- - Direct revival holds an exclusive cross-process lease on the canonical child session file until the new child finishes. Concurrent attempts fail before Pi starts and identify the owning revived run; stale ownership is reclaimed only when the recorded process is demonstrably gone or reused.
239
+ - Revive starts a new child session from the old session context; it does not resume the original child session.
240
+ - Direct revival holds an exclusive cross-process lease on the canonical child session file until the new child finishes. Concurrent attempts fail before the child session starts and identify the owning revived run; stale ownership is reclaimed only when the recorded process is demonstrably gone or reused.
241
241
  - If the chosen child has no persisted `.jsonl` session file, resume fails and reports that directly.
242
242
 
243
243
  Use diagnostics when setup or child startup looks wrong:
@@ -246,9 +246,13 @@ Use diagnostics when setup or child startup looks wrong:
246
246
  subagent({ action: "doctor" })
247
247
  ```
248
248
 
249
+ ### Failed lane recovery and execution-mode fallback
250
+
251
+ A failure in the subagent workflow, child launch, prompt runtime, extension loading, or child tooling setup is a lane infrastructure blocker, not permission to silently change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state. Retry or fix the `subagent` path only through a clear same-protocol retry; before retrying or asking the owner, verify the worktree is clean or capture the partial diff. For backlog lanes and other subagent-governed workflows, switching to `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external mode requires explicit owner approval. Pi core may print a generic `pi -ne` extension-load hint; that hint is outside this package and is not protocol-approved fallback. This execution-mode boundary does not prohibit configured native model/provider fallback.
252
+
249
253
  ### External terminal work
250
254
 
251
- Use native `subagent` runs for unattended implementation, review, and gate work that needs managed isolation, durable artifacts, and process controls. Use `interactive_shell` for visible terminal work, alternate CLIs, trust prompts, and recovery.
255
+ Use native `subagent` runs for unattended implementation, review, and gate work that needs managed isolation, durable artifacts, and process controls. Use `interactive_shell` for visible terminal work, alternate CLIs, trust prompts, or recovery only when the user explicitly requests that mode or the task is outside the governed subagent protocol; it is not an implicit replacement for a failed `subagent` lane.
252
256
 
253
257
  A cooperating terminal runtime can register read-only external records through `pi-subagents/external-runs`. Records include the source, session, state, optional report path, and completion reason. They are observations only: pi-subagents does not start, stop, steer, or otherwise own the foreign process. Run unattended raw terminal agents in an explicit isolated cwd or worktree; do not use a live project checkout as disposable review space.
254
258
 
@@ -46,6 +46,8 @@ After a writer produces a candidate, run the required fresh-context, read-only r
46
46
 
47
47
  Use stable lane-qualified artifact paths for reports and review output. A handoff states the lane status, repository and worktree, changed files, validation, open decisions, next action, and artifact or receipt paths. Copy only the final evidence to memory, a mission record, or a PR/comment, then remove scratch files from the active worktree before closing the lane.
48
48
 
49
+ For backlog lanes and other subagent-governed workflows, a setup/runtime/tooling failure remains an infrastructure blocker. Preserve the exact failure, run/status, and repository/cwd/worktree/branch/ref state; verify a clean worktree or capture any partial diff before a same-protocol retry or asking the owner. Do not start another writer or switch to an external, foreground, or CLI execution mode without explicit owner approval.
50
+
49
51
  Keep a worktree until its handoff is durable, no run owns it, and no later gate needs it. Clean up only inside the recorded authority boundary. If a run stops or needs attention, preserve its worktree and artifacts, record the last known state and recovery owner, then resume that run or create one replacement lane from the handoff. Do not start another writer while worktree ownership is uncertain.
50
52
 
51
53
  Before completion, inspect the board. Every lane must be terminal or blocked with a named next action. Confirm one writer per repo/cwd or worktree, required validation, required fresh read-only review, and a durable handoff. The parent reports outcomes, evidence, residual risks, and the next decision.
@@ -32,6 +32,7 @@ import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/m
32
32
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
33
33
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
34
34
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, resolveExternalCliRunnerStatus, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
35
+ import { resolveExternalCliBinaryAvailability, type ExternalCliBinaryAvailability } from "../runs/shared/external-cli-preflight.ts";
35
36
  import type { AcceptanceInput, AgentCapabilitiesSnapshot, AgentCapabilityRow, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
36
37
  import { getProjectConfigDir } from "../shared/utils.ts";
37
38
  import { previewDisplayText } from "../shared/display-text.ts";
@@ -704,17 +705,34 @@ function externalJobProviderSuffix(provider: string, names: Set<string> | undefi
704
705
  return names.has(provider) ? "✓" : "missing";
705
706
  }
706
707
 
707
- function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
708
+ type ExternalCliAvailabilityByCommand = ReadonlyMap<string, ExternalCliBinaryAvailability>;
709
+
710
+ function externalCliAvailabilityForAgents(agents: readonly AgentConfig[]): ExternalCliAvailabilityByCommand {
711
+ const availability = new Map<string, ExternalCliBinaryAvailability>();
712
+ for (const agent of agents) {
713
+ const runner = agent.runner;
714
+ if (runner?.type === "external-cli" && !availability.has(runner.command)) {
715
+ availability.set(runner.command, resolveExternalCliBinaryAvailability(runner.command, process.env));
716
+ }
717
+ }
718
+ return availability;
719
+ }
720
+
721
+ function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string | undefined {
708
722
  if (agent.runner?.type === "external-job") return `external-job:${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
709
- if (agent.runner?.type === "external-cli") return "external-cli";
723
+ if (agent.runner?.type === "external-cli") {
724
+ const availability = externalCliAvailability?.get(agent.runner.command);
725
+ if (!availability) return "external-cli";
726
+ return `external-cli:${agent.runner.command} ${availability.available ? "✓" : "missing"}`;
727
+ }
710
728
  return undefined;
711
729
  }
712
730
 
713
- function agentListMetadata(agent: AgentConfig, providerNames: Set<string> | undefined): string {
731
+ function agentListMetadata(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string {
714
732
  const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
715
733
  return [
716
734
  source,
717
- runnerListBadge(agent, providerNames),
735
+ runnerListBadge(agent, providerNames, externalCliAvailability),
718
736
  agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
719
737
  agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
720
738
  ].filter((part): part is string => Boolean(part)).join(", ");
@@ -724,7 +742,7 @@ function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | un
724
742
  return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): ${agent.description}`;
725
743
  }
726
744
 
727
- function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
745
+ function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string {
728
746
  const declaredTools = [
729
747
  ...(agent.tools ?? []),
730
748
  ...(agent.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
@@ -742,7 +760,7 @@ function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<stri
742
760
  if (agent.modelProvider && !agent.model.includes("/")) model = `${agent.modelProvider}/${agent.model}`;
743
761
  }
744
762
  const thinking = agent.thinking === false ? "off" : agent.thinking ?? "default";
745
- return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): Description: ${previewDisplayText(agent.description, 240)}; Tools: ${tools}; Model: ${model}; Thinking: ${thinking}`;
763
+ return `- ${agent.name} (${agentListMetadata(agent, providerNames, externalCliAvailability)}): Description: ${previewDisplayText(agent.description, 240)}; Tools: ${tools}; Model: ${model}; Thinking: ${thinking}`;
746
764
  }
747
765
 
748
766
  const EXTERNAL_JOB_CAPABILITIES = { stop: false, steer: false, resume: false, structuredOutput: false, toolEvents: false } as const;
@@ -752,10 +770,19 @@ function listOrEmpty<T>(values: T[] | undefined): T[] {
752
770
  return values ?? [];
753
771
  }
754
772
 
755
- function agentCapabilityRunner(agent: AgentConfig, providerNames: Set<string> | undefined): AgentCapabilityRow["runner"] {
773
+ function agentCapabilityRunner(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability: ExternalCliAvailabilityByCommand): AgentCapabilityRow["runner"] {
756
774
  const runner = agent.runner;
757
775
  if (!runner || runner.type === "pi") return PI_AGENT_RUNNER;
758
- if (runner.type === "external-cli") return { type: "external-cli", adapter: runner.adapter, capabilities: resolveExternalCliRunnerStatus(runner).capabilities };
776
+ if (runner.type === "external-cli") {
777
+ const availability = externalCliAvailability.get(runner.command)!;
778
+ return {
779
+ type: "external-cli",
780
+ adapter: runner.adapter,
781
+ command: runner.command,
782
+ ...availability,
783
+ capabilities: resolveExternalCliRunnerStatus(runner).capabilities,
784
+ };
785
+ }
759
786
  return { type: "external-job", provider: runner.provider, available: providerNames?.has(runner.provider), capabilities: EXTERNAL_JOB_CAPABILITIES };
760
787
  }
761
788
 
@@ -769,7 +796,7 @@ function agentCapabilityTools(agent: AgentConfig): AgentCapabilityRow["tools"] {
769
796
  };
770
797
  }
771
798
 
772
- function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean; providerNames?: Set<string>; restrictionSources?: string[] }): AgentCapabilityRow {
799
+ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictionSources?: string[] }): AgentCapabilityRow {
773
800
  return {
774
801
  name: agent.name,
775
802
  description: previewDisplayText(agent.description, 1000),
@@ -777,7 +804,7 @@ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean;
777
804
  executable: options.executable,
778
805
  restrictionSources: options.executable ? undefined : options.restrictionSources ?? [],
779
806
  aliases: agent.aliases ? [...agent.aliases] : undefined,
780
- runner: agentCapabilityRunner(agent, options.providerNames),
807
+ runner: agentCapabilityRunner(agent, options.providerNames, options.externalCliAvailability),
781
808
  tools: agentCapabilityTools(agent),
782
809
  model: presentDetails({ value: agent.model, fallbackModels: agent.fallbackModels, thinking: agent.thinking }),
783
810
  execution: presentDetails({ defaultAsync: agent.defaultAsync, timeoutMs: agent.defaultTimeoutMs }),
@@ -786,11 +813,11 @@ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean;
786
813
  };
787
814
  }
788
815
 
789
- function agentCapabilitiesSnapshot(input: { agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): AgentCapabilitiesSnapshot {
816
+ function agentCapabilitiesSnapshot(input: { agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictedSources?: string[] }): AgentCapabilitiesSnapshot {
790
817
  return {
791
818
  agents: [
792
- ...input.agents.map((agent) => agentCapabilityRow(agent, { executable: true, providerNames: input.providerNames })),
793
- ...input.restrictedAgents.map((agent) => agentCapabilityRow(agent, { executable: false, providerNames: input.providerNames, restrictionSources: input.restrictedSources })),
819
+ ...input.agents.map((agent) => agentCapabilityRow(agent, { executable: true, providerNames: input.providerNames, externalCliAvailability: input.externalCliAvailability })),
820
+ ...input.restrictedAgents.map((agent) => agentCapabilityRow(agent, { executable: false, providerNames: input.providerNames, externalCliAvailability: input.externalCliAvailability, restrictionSources: input.restrictedSources })),
794
821
  ],
795
822
  restrictedCount: input.restrictedAgents.length,
796
823
  ...(input.restrictedSources?.length ? { capabilityCeilingSources: [...input.restrictedSources] } : {}),
@@ -824,13 +851,14 @@ function appendAgentDiagnosticLines(lines: string[], diagnostics: AgentDiscovery
824
851
  );
825
852
  }
826
853
 
827
- function agentCapabilityDetails(input: { capabilityMode: boolean; agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): Partial<Details> | undefined {
854
+ function agentCapabilityDetails(input: { capabilityMode: boolean; agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictedSources?: string[] }): Partial<Details> | undefined {
828
855
  if (!input.capabilityMode) return undefined;
829
856
  return {
830
857
  agentCapabilities: jsonDetails(agentCapabilitiesSnapshot({
831
858
  agents: input.agents,
832
859
  restrictedAgents: input.restrictedAgents,
833
860
  providerNames: input.providerNames,
861
+ externalCliAvailability: input.externalCliAvailability,
834
862
  restrictedSources: input.restrictedSources,
835
863
  })),
836
864
  };
@@ -936,7 +964,10 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
936
964
  const providerStatus = registeredExternalJobProviderStatus();
937
965
  const providerNameSet = providerNames(providerStatus);
938
966
  const capabilityMode = params.capabilities === true;
939
- const formatLine = capabilityMode ? formatAgentCapabilitiesLine : formatAgentListLine;
967
+ const externalCliAvailability = capabilityMode ? externalCliAvailabilityForAgents([...agents, ...restrictedAgents]) : undefined;
968
+ const formatLine = capabilityMode
969
+ ? (agent: AgentConfig, names: Set<string> | undefined) => formatAgentCapabilitiesLine(agent, names, externalCliAvailability)
970
+ : formatAgentListLine;
940
971
  const lines = [
941
972
  capabilityMode ? "Executable agents (capabilities):" : "Executable agents:",
942
973
  ...formatAgentListSections(agents, providerNameSet, formatLine),
@@ -950,6 +981,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
950
981
  agents,
951
982
  restrictedAgents,
952
983
  providerNames: providerNameSet,
984
+ externalCliAvailability: externalCliAvailability ?? new Map(),
953
985
  restrictedSources,
954
986
  }));
955
987
  }
@@ -1,5 +1,4 @@
1
1
  export {
2
- SUBAGENT_CAPABILITY_CEILING_ENV,
3
2
  SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY,
4
3
  SUBAGENT_CAPABILITY_CEILING_VERSION,
5
4
  decodeSubagentCapabilityCeiling,
@@ -2,4 +2,4 @@ export {
2
2
  resolvePiLaunchToolPlan,
3
3
  type PiLaunchToolPlan,
4
4
  type ResolvePiLaunchToolPlanInput,
5
- } from "../runs/shared/pi-args.ts";
5
+ } from "../runs/shared/child-tool-plan.ts";
@@ -7,11 +7,11 @@ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } f
7
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
8
8
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
9
9
  import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
10
- import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
10
+ import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
11
11
  import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
12
12
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
13
13
  import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
14
+ import { assertThinkingWithinCeiling, intersectThinkingCeilings, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
15
15
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
16
16
  import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
17
17
  import { resolvePermissionRules } from "../runs/shared/permissions.ts";
@@ -73,6 +73,7 @@ export interface SubagentLaunchContractInput {
73
73
  /** Current parent leaf required before an implicit `defaultContext: fork` stays `fork`. */
74
74
  parentLeafId?: string | null;
75
75
  sessionRoot?: string;
76
+ /** Caller directory used as a root keyed by the child run id ("preflight" placeholder when runId is omitted). */
76
77
  sessionDir?: string;
77
78
  runId?: string;
78
79
  /** Root run id supplied by a host when projecting nested async lifecycle paths. */
@@ -330,7 +331,6 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
330
331
  discovered.maxThinking,
331
332
  input.thinkingCeiling,
332
333
  input.inheritedThinkingCeiling,
333
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
334
334
  );
335
335
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
336
336
  const modelCandidates = externalRunner
@@ -381,7 +381,10 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
381
381
  const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir) : undefined;
382
382
  const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
383
383
  const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
384
- const sessionRoot = input.sessionDir ? path.resolve(input.sessionDir) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
384
+ // An explicit sessionDir is a root keyed by the child run id, matching the
385
+ // sibling sessionRoot derivation; hosts omitting runId get the documented
386
+ // deterministic "preflight" placeholder.
387
+ const sessionRoot = input.sessionDir ? path.join(path.resolve(input.sessionDir), runId) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
385
388
  const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined;
386
389
  const lifecycleAsyncDir = input.nestedRootRunId
387
390
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId)
@@ -10,6 +10,7 @@ import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultT
10
10
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
11
  import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
12
12
  import { normalizeWorktreeBranchPrefix } from "../runs/shared/worktree.ts";
13
+ import { validateModelResponseAliases } from "../shared/model-response-aliases.ts";
13
14
 
14
15
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
15
16
  const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
@@ -177,6 +178,7 @@ function validateConfig(config: Record<string, unknown>): void {
177
178
  validateArtifactConfig(config.artifactConfig);
178
179
  validateCapacityConfig(config.capacity);
179
180
  validateModelExclusionsConfig(config.modelExclusions);
181
+ validateModelResponseAliases(config.modelResponseAliases);
180
182
  validateMainWindowRendererConfig(config.mainWindowRenderer);
181
183
  validateOrcaProgressTabsConfig(config.orcaProgressTabs);
182
184
  }
@@ -240,12 +242,12 @@ export function loadConfig(): ExtensionConfig {
240
242
  return readConfigForUpdate(configPath);
241
243
  } catch (error) {
242
244
  if (error instanceof PrunedForkConfigError) throw error;
243
- // An explicitly requested worktree provider/prefix must not be silently
245
+ // Explicit route identity and worktree policies must not be silently
244
246
  // discarded and replaced by the built-in defaults after validation fails.
245
247
  try {
246
248
  const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
247
249
  if (raw && typeof raw === "object" && !Array.isArray(raw)
248
- && (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix"))) throw error;
250
+ && (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases"))) throw error;
249
251
  } catch (readError) {
250
252
  if (readError === error) throw error;
251
253
  }
@@ -4,7 +4,7 @@ import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
4
4
  import { isAsyncAvailable } from "../runs/background/async-execution.ts";
5
5
  import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
6
6
  import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
7
- import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
7
+
8
8
  import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
9
9
  import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
10
10
  import {
@@ -178,14 +178,6 @@ function formatSpawnBudgetSection(input: DoctorReportInput): string[] {
178
178
  }
179
179
 
180
180
  function formatRunFanoutSection(input: DoctorReportInput): string[] {
181
- try {
182
- const inherited = decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
183
- if (inherited) {
184
- return [`- usage: ${formatRunFanoutBudget(getRunFanoutBudgetSnapshot(inherited)).replace(/^Run fan-out: /, "")}`, `- root run: ${inherited.rootRunId}`, "- reset boundary: cumulative claims are never released; a new top-level run creates a new budget"];
185
- }
186
- } catch (error) {
187
- return [`- inherited budget: invalid — ${errorText(error)}`];
188
- }
189
181
  const configured = resolveMaxSubagentSpawnsPerRun(input.config.maxSubagentSpawnsPerRun);
190
182
  const source = normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN) !== undefined
191
183
  ? "environment"
@@ -214,7 +206,7 @@ function formatPermissionSystemSection(): string[] {
214
206
  if (trimmed) {
215
207
  lines.push(`- parent session: set (${trimmed})`);
216
208
  } else {
217
- lines.push("- parent session: not set — ask forwarding from subprocess children will not reach a parent UI");
209
+ lines.push("- parent session: not set — ask forwarding from background children will not reach a parent UI");
218
210
  }
219
211
  const isChild = process.env["PI_SUBAGENT_CHILD"] === "1";
220
212
  lines.push(`- subagent process: ${isChild ? "yes (PI_SUBAGENT_CHILD=1)" : "no"}`);
@@ -6,8 +6,8 @@ import { discoverAgents } from "../agents/agents.ts";
6
6
  import { getArtifactsDir } from "../shared/artifacts.ts";
7
7
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
8
  import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
9
- import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
10
- import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
9
+ import type { ChildRuntimeConfig } from "../runs/shared/child-runtime-config.ts";
10
+ import { readNestedControlRequests, resolveInheritedNestedRoute, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
13
  import { createSubagentParamsSchema } from "./schemas.ts";
@@ -51,12 +51,8 @@ function createChildSafeState(): SubagentState {
51
51
  };
52
52
  }
53
53
 
54
- function resolveNestedControlRoute(): NestedRoute | undefined {
55
- try {
56
- return resolveNestedRouteFromEnv();
57
- } catch {
58
- return undefined;
59
- }
54
+ function resolveNestedControlRoute(config: ChildRuntimeConfig): NestedRoute | undefined {
55
+ return config.nestedRoute ? resolveInheritedNestedRoute(config.nestedRoute) : undefined;
60
56
  }
61
57
 
62
58
  function nestedControlRouteKey(route: NestedRoute): string {
@@ -145,8 +141,9 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState,
145
141
  return () => clearInterval(timer);
146
142
  }
147
143
 
148
- export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
149
- if (process.env[SUBAGENT_CHILD_ENV] !== "1" || process.env[SUBAGENT_FANOUT_CHILD_ENV] !== "1") return;
144
+ /** Register the child-side `subagent` tool for fanout-authorized children. */
145
+ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, childConfig: ChildRuntimeConfig): void {
146
+ if (!childConfig.fanoutChild) return;
150
147
 
151
148
  const globalStore = globalThis as Record<string, unknown>;
152
149
  const registeredKey = "__piSubagentFanoutChildRegisteredApis";
@@ -172,6 +169,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
172
169
  expandTilde,
173
170
  discoverAgents,
174
171
  allowMutatingManagementActions: false,
172
+ childRuntime: childConfig,
175
173
  });
176
174
 
177
175
  const params = createSubagentParamsSchema();
@@ -190,7 +188,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
190
188
  };
191
189
 
192
190
  pi.registerTool(tool);
193
- const route = resolveNestedControlRoute();
191
+ const route = resolveNestedControlRoute(childConfig);
194
192
  if (!route) return;
195
193
  const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
196
194
  const listenerCleanups = globalStore[listenerCleanupKey] instanceof Map