pi-subagents 0.65.0 → 0.66.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +17 -3
  5. package/docs/configuration.md +34 -0
  6. package/docs/extension-api.md +94 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +11 -5
  10. package/docs/workflows.md +22 -7
  11. package/package.json +4 -1
  12. package/runner-server-preload.mjs +13 -0
  13. package/skills/pi-subagents/SKILL.md +2 -1
  14. package/skills/pi-subagents/references/execution-controls.md +19 -2
  15. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  16. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  17. package/src/agents/advertised-agent-prompt.ts +63 -0
  18. package/src/agents/agent-management.ts +14 -1
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +8 -0
  21. package/src/api/preflight.ts +5 -1
  22. package/src/api/shared-types.ts +1 -1
  23. package/src/api/workflow-resources.ts +6 -0
  24. package/src/extension/config.ts +4 -2
  25. package/src/extension/index.ts +71 -4
  26. package/src/extension/public-execution.ts +0 -1
  27. package/src/extension/rpc.ts +4 -21
  28. package/src/extension/schemas.ts +9 -7
  29. package/src/extension/tool-description.ts +10 -5
  30. package/src/integrations/pi-web-session-liveness.ts +73 -0
  31. package/src/intercom/native-supervisor-channel.ts +102 -88
  32. package/src/intercom/supervisor-ui.ts +3 -2
  33. package/src/missions/workflow-state.ts +37 -16
  34. package/src/runs/background/active-async-capacity.ts +18 -18
  35. package/src/runs/background/async-execution.ts +8 -1
  36. package/src/runs/background/async-job-tracker.ts +35 -3
  37. package/src/runs/background/async-resume.ts +3 -1
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/async-status-snapshot.ts +10 -12
  40. package/src/runs/background/async-status.ts +17 -9
  41. package/src/runs/background/auto-drain.ts +40 -29
  42. package/src/runs/background/chain-root-attachment.ts +8 -0
  43. package/src/runs/background/control-channel.ts +78 -44
  44. package/src/runs/background/notify.ts +88 -12
  45. package/src/runs/background/owned-process-tree.ts +6 -6
  46. package/src/runs/background/process-terminal.ts +23 -23
  47. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  48. package/src/runs/background/run-child-session.ts +62 -33
  49. package/src/runs/background/run-status.ts +75 -5
  50. package/src/runs/background/runner-aliases.ts +46 -8
  51. package/src/runs/background/runner-child-launch.ts +86 -0
  52. package/src/runs/background/stale-run-reconciler.ts +3 -1
  53. package/src/runs/background/subagent-runner.ts +430 -208
  54. package/src/runs/background/subagent-wait.ts +3 -0
  55. package/src/runs/background/wait-completions.ts +4 -0
  56. package/src/runs/foreground/async-steering-action.ts +19 -0
  57. package/src/runs/foreground/execution.ts +116 -26
  58. package/src/runs/foreground/foreground-history.ts +3 -1
  59. package/src/runs/foreground/prompt-audit.ts +9 -5
  60. package/src/runs/foreground/subagent-executor.ts +531 -227
  61. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  62. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  63. package/src/runs/shared/acceptance.ts +16 -3
  64. package/src/runs/shared/agent-contract.ts +1 -1
  65. package/src/runs/shared/async-status-projection.ts +47 -47
  66. package/src/runs/shared/child-hooks.ts +151 -2
  67. package/src/runs/shared/child-launch.ts +18 -13
  68. package/src/runs/shared/child-session.ts +55 -24
  69. package/src/runs/shared/child-tool-plan.ts +2 -2
  70. package/src/runs/shared/completion-evidence.ts +2 -2
  71. package/src/runs/shared/completion-guard.ts +1 -0
  72. package/src/runs/shared/host-step-status.ts +11 -11
  73. package/src/runs/shared/llm-intent-arbiter.ts +30 -20
  74. package/src/runs/shared/model-exclusions.ts +2 -1
  75. package/src/runs/shared/model-fallback.ts +41 -8
  76. package/src/runs/shared/nested-events.ts +8 -8
  77. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  78. package/src/runs/shared/parallel-handoff.ts +57 -12
  79. package/src/runs/shared/parallel-utils.ts +3 -2
  80. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  81. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  82. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  83. package/src/runs/shared/run-fanout-budget.ts +8 -8
  84. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  85. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  86. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  87. package/src/runs/shared/worktree-setup-command.ts +190 -0
  88. package/src/runs/shared/worktree.ts +403 -210
  89. package/src/shared/model-response-aliases.ts +13 -0
  90. package/src/shared/types.ts +89 -60
  91. package/src/shared/utils.ts +10 -2
  92. package/src/shared/watch-strategy.ts +2 -0
  93. package/src/shared/workflow-child-permit.ts +18 -13
  94. package/src/tui/fleet-status.ts +1 -1
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +44 -15
  97. package/src/workflows/chat-progress.ts +3 -3
  98. package/src/workflows/scripted-workflow.ts +70 -16
  99. package/src/workflows/workflow-checklist.ts +15 -18
  100. package/src/workflows/workflow-child-summary.ts +57 -8
  101. package/src/workflows/workflow-preflight.ts +19 -19
  102. package/src/workflows/workflow-receipt.ts +3 -3
  103. package/src/workflows/workflow-resources.ts +96 -21
  104. package/src/workflows/workflow-settlement.ts +3 -0
@@ -6,7 +6,7 @@ Parameters and actions for the `subagent` tool. These are what the LLM passes wh
6
6
 
7
7
  Chaining is code-driven through `workflowScript`. Use `await runs.run(...)` for sequential steps and `await runs.all([{ key, agent, task }, ...])` for ordinary parallel fanout. `runs.all` resolves to an ordered array, not a key map, so use indexes, destructuring, or `.map(...)`, not `results.<key>`. Do not read `.output` from an unawaited `runs.run` launch. Stored `runs.run` promises are only for the advanced rolling fanout pattern under [Workflow steering](#workflow-steering), where every promise is later observed with direct `await`, `Promise.race`, or `Promise.all`. Legacy top-level `chain`, `tasks`, and `parallel` inputs are not supported. Helper functions must be plain functions or explicit Promise chains. Nested `async function` helpers, async arrows, and async methods are rejected so child-launch tracking stays portable across Node and Bun. For permission-sensitive host calls, use an extension-owned named resource such as `{ workflow: "run-ci", args: { command: "npm test" } }`; raw public `workflowScript`/`workflowScriptPath` inputs have unknown resource provenance and cannot call `runs.host`. A resolved resource may internally use `runs.host(key, { kind: "command", command, timeoutMs, output?, role?, provider? })` within its authority ceiling; there is no per-step `cwd`, and commands and relative output paths use the workflow `cwd`. Set `cwd` on the outer `subagent({...})` request instead, or put a trusted directory change in the command (for example, `cd /path/to/worktree && npm test`).
8
8
 
9
- Use `{ action: "validate", workflowScript }` to check statically decidable syntax and structure without launching children. It returns `{ ok, errors }` and fails the tool call when `ok` is false. Dynamic keys and values remain valid because runtime-only cases are not guessed.
9
+ Use `{ action: "validate", workflowScript }` to check statically decidable syntax and structure without launching children. It returns `{ ok, errors }` and fails the tool call when `ok` is false. Literal child `baseRef` values are checked against the runtime ref policy. Dynamic keys and values remain subject to runtime checks; static validation does not guess them.
10
10
 
11
11
  Use `workflowScriptPath` instead of `workflowScript` to load the same JavaScript statement body from a file. The two fields are mutually exclusive. Relative paths resolve against the request `cwd`, and absolute paths pass through. The host reads the file before validation, scheduling, or sandbox execution. The workflow sandbox still has no filesystem access. Missing, unreadable, and empty files fail as file input errors.
12
12
 
@@ -108,7 +108,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
108
108
  | `async` | boolean | default-on | Background execution. Workflows default to background. `async:false` blocks the parent until completion and runs the child as a session inside the parent Pi process; such foreground children never load the parent's ambient extensions, so agents that need MCP tools (`mcpDirectTools`, or MCP tools from an ambient adapter such as pi-mcp-adapter) or models from a provider extension must run as background children, which load them inside the detached runner process. |
109
109
  | `chatProgress` | `auto \| off \| live-card` | `auto` | WorkflowScript chat projection. `auto` renders a live in-chat card only for watched foreground workflows in the same Git repository, including managed worktrees; it is off otherwise. Explicit `live-card` requires `async:false` and the same Git repository. Async workflows have no inline live card, so omit `chatProgress` or use `auto`/`off`; use `async:false` only when the parent must block. |
110
110
  | `isolation` | `none \| worktree` | - | Workflow child isolation. `none` runs in the shared cwd and does not need Git. `worktree` requires a managed Git worktree. Do not combine it with a contradictory `worktree` value. |
111
- | `baseRef` | string | `HEAD` | Git ref used as the base commit for managed worktrees. It must be a safe Git ref that resolves to a commit; source-checkout cleanliness is still checked before allocation. For workflowScript, set it on the outer request as a default or on an individual `runs.run`/`runs.all` child to override it. |
111
+ | `baseRef` | string | `HEAD` | `HEAD` or a supported named ref such as `refs/heads/release`, `refs/tags/v1`, or `origin/main`. Full 40/64-character commit IDs and revision expressions such as `HEAD~1` are unsupported. The ref must resolve to a commit at worktree allocation; omitted values default to `HEAD` resolved at that time. Source-checkout cleanliness is still checked. For workflowScript, set it on the outer request as a default or on an individual `runs.run`/`runs.all` child to override it. |
112
112
  | `timeoutMs` / `maxRuntimeMs` | number | config `timeoutMs`, else 30 min foreground / single-agent async | Optional run-level max runtime in milliseconds. When omitted, the global [`timeoutMs`](configuration.md#timeoutms) config provides the default; absent that, foreground and plain single-agent async runs fall back to 30 minutes, while composite async runs (chains, parallel tasks, workflows) stay unbounded at the top level. Expiration of this run-level deadline is terminal and does not trigger `fallbackModels`. |
113
113
  | `toolTimeoutMs` | number | fast-tool default | Optional positive hard per-tool-call deadline in milliseconds. Precedence: call value → agent frontmatter → config → `PI_SUBAGENT_TOOL_TIMEOUT_MS`. The timer starts on `tool_execution_start`, clears on the matching `tool_execution_end`, and terminates the run with `timedOut: true` if the tool remains open. When omitted, known-fast built-in tools get a five-minute default; long-running tools get attention notices but no hard default. It never extends the run deadline; `contact_supervisor`, `intercom`, and `bg_wait` are exempt. |
114
114
  | `toolBudget` | object | none | Optional child tool-call budget `{ soft?, hard, block? }`. At `soft` the child is nudged to finalize. After `hard`, configured tools are blocked; `block` defaults to `read`, `grep`, `find`, and `ls`, while `"*"` blocks every tool call. Final assistant text is never blocked. |
@@ -120,7 +120,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
120
120
  | `share` | boolean | false | Upload session export to GitHub Gist. |
121
121
  | `sessionDir` | string | derived | Override session log directory. |
122
122
  | `acceptance` | string/object/false | inferred | Configure evidence gates. See [Acceptance gates](#acceptance-gates). |
123
- | `gate` | string | - | One host-run verification command, shorthand for `acceptance: { level: "verified", verify: [{ id: "gate", command }] }`. Also valid on individual `runs.run`/`runs.all` items. Cannot be combined with `acceptance`, and is rejected with retained `resume`. |
123
+ | `gate` | string | - | One host-run verification command, shorthand for `acceptance: { level: "verified", verify: [{ id: "gate", command }] }`. Also valid on individual `runs.run`/`runs.all` items. Rejects `acceptance` except `false` (treated as omitted), and rejects retained `resume`. |
124
124
 
125
125
  ### Budget guidance for writers
126
126
 
@@ -298,6 +298,12 @@ The manifest stores one of these fail-closed eligibility states: `active` (an ow
298
298
 
299
299
  ## Status and control actions
300
300
 
301
+ ### Failed lane recovery and execution-mode boundaries
302
+
303
+ 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. 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.
304
+
305
+ For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. Do not silently switch to `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external mode. `interactive_shell` remains valid when the user explicitly requests visible foreground/CLI work or the task is outside the governed subagent protocol. 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.
306
+
301
307
  ```ts
302
308
  subagent({ action: "status" })
303
309
  subagent({ action: "status", view: "fleet" })
@@ -352,7 +358,7 @@ subagent({ action: "doctor" })
352
358
 
353
359
  `steer` waits up to three seconds for a correlated child-Pi input acceptance and returns a request id with `delivered`, `scheduled`, `pending`, `partial`, `recovered`, or `failed` plus per-child states. The receipt also has `deliveryStatus: "delivered" | "queued"`. Delivery means Pi accepted the user message, not model compliance. A pending indexed child returns `scheduled`.
354
360
 
355
- The optional `mode` is `steer` by default and keeps the current interrupt behavior. `follow_up` waits for the next turn boundary. `auto` queues during an active turn and delivers immediately between turns. The bounded FIFO holds 20 messages and returns a clear error when full. Terminal details report queued messages that the run did not deliver. A `follow_up` sent to a completed retained workflow child becomes the first brief for its next `resume`; it does not revive the child by itself.
361
+ The optional `mode` is `steer` by default and keeps the current interrupt behavior. `follow_up` waits for the next turn boundary. `auto` uses the same native steer delivery path as `steer`, without automatic pause-and-revive recovery after a missed acknowledgment. The retained revival-brief queue holds 20 messages and returns a clear error when full; this is not a live follow-up queue bound. Terminal details report queued messages without recorded delivery. A live follow-up acknowledgment reports queue acceptance, not delivery, and has no later correlated queued-to-delivered receipt. A `follow_up` sent to a completed retained workflow child becomes the first brief for its next `resume`; it does not revive the child by itself.
356
362
 
357
363
  Only a top-level single run may interrupt after the acknowledgment deadline and recover after a further 15-second pause/revival bound; durable multi-child and nested runs never auto-interrupt. Recovery launches a replacement only after the source is confirmed paused, a valid persisted session exists, and deadline, turn, and tool budgets remain. It preserves the original child contract and remaining limits; otherwise the source stays paused with an explicit failure. Late acceptance is recorded but cannot cancel committed recovery.
358
364
 
@@ -385,7 +391,7 @@ When one host-run command is the entire verification contract, use the `gate` sh
385
391
  { workflowScript: `return runs.run("impl", { agent: "worker", task: "Implement the fix", gate: "npm test" })` }
386
392
  ```
387
393
 
388
- `gate` normalizes to verified acceptance with that single command, so the runtime executes it on the host and records the result as evidence. Verification results are memoized per tracked workspace state and effective environment, so an unchanged tree does not rerun the same command. Use explicit `acceptance.verify` when you need multiple commands, timeouts, or custom criteria. `gate` cannot be combined with `acceptance` and is rejected on retained `resume` items. With `worktree: true`, the gate runs inside the child's managed worktree.
394
+ `gate` normalizes to verified acceptance with that single command, so the runtime executes it on the host and records the result as evidence. Verification results are memoized per tracked workspace state and effective environment, so an unchanged tree does not rerun the same command. Use explicit `acceptance.verify` when you need multiple commands, timeouts, or custom criteria. `gate` rejects `acceptance` except `false` (treated as omitted), and rejects retained `resume` items. With `worktree: true`, the gate runs inside the child's managed worktree.
389
395
 
390
396
  ### Levels and inference
391
397
 
package/docs/workflows.md CHANGED
@@ -19,6 +19,14 @@ Child-safety boundaries are enforced at runtime:
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:
@@ -88,10 +96,11 @@ subagent({
88
96
  - `toolBudget` becomes the default for each child unless that child supplies a narrower value.
89
97
  - `usageBudget` accounts for reported usage across completed workflow children. Once exhausted, it rejects later child launches but does not stop children that are already running.
90
98
  - Budget and timeout stops return a structured `terminalOutcome` with `state: "partial"` and reason `budget_exhausted` or `timeout`. Workflow receipts keep settled child evidence for recovery.
99
+ - After an async workflow receipt is successfully published, `workflowReceiptPath` exposes its exact path in wait completion details, completion notifications, and exact status/debug details. Text responses also identify the receipt. Pending runs and failed receipt publications omit the reference; older status records are not backfilled. The reference records publication, not a guarantee against later retention cleanup. Raw result files retain `workflowReceipt: { path, receipt }`.
91
100
 
92
101
  These controls are opt-in. Avoid tight hard budgets for mutation-capable workers unless the workflow has an explicit checkpoint and handoff path.
93
102
 
94
- The result is `{ ok, errors }`. Invalid scripts return a tool error and include line and column data when available. Validation checks syntax, portable nested-async rules, literal `runs.run` and `runs.all` keys, duplicate literal keys in one `runs.all` group, direct keyed access to a known `runs.all` result, and statically clear non-JSON boundary values. Dynamic keys and other runtime-only values are accepted without a warning. Validation does not discover agents, launch children, or create run artifacts.
103
+ The result is `{ ok, errors }`. Invalid scripts return a tool error and include line and column data when available. Validation checks syntax, portable nested-async rules, literal `runs.run` and `runs.all` keys and child `baseRef` values, duplicate literal keys in one `runs.all` group, direct keyed access to a known `runs.all` result, and statically clear non-JSON boundary values. Dynamic keys and other runtime-only values are accepted without a warning. Validation does not discover agents, launch children, or create run artifacts.
95
104
 
96
105
  ```js
97
106
  subagent({ workflowScript: `
@@ -347,6 +356,8 @@ known, or for explicit emergency hotfix lanes.
347
356
 
348
357
  For watched same-repo workflows, pass `async:false` only when the parent must block until completion. That blocking mode also shows the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Blocking workflows default to a 30-minute timeout; async workflows have no default timeout. See the [tool reference](tool-reference.md) for the full parameter list.
349
358
 
359
+ Synchronous workflows publish trace and `emit(...)` updates through the tool update callback regardless of `chatProgress`, including RPC/headless and cross-repository runs. These updates include `details.workflow` and `details.workflowChildren`; `chatProgress: "off"` disables the live card, not transport progress. Running foreground child rows additionally expose bounded `activity` (current tool, timing, and counters), plus resolved model/thinking when available, keyed by `childId`. Activity-only updates coalesce over 100 ms; lifecycle updates remain immediate. Activity clears when children settle, and is not persisted for async workflows. Tool names are limited to 256 UTF-8 bytes and each activity object is below 2 KiB (including JSON escaping); arguments and transcripts are not forwarded.
360
+
350
361
  The legacy `/chain`, `/parallel`, and `/run-chain` commands are not registered.
351
362
 
352
363
  ## Direct commands
@@ -369,10 +380,12 @@ Each child uses the existing worktree lifecycle: it branches from clean HEAD, jo
369
380
 
370
381
  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
382
 
372
- 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.
383
+ Use `baseRef` to branch managed worktrees from `HEAD` or a supported named ref such as `refs/heads/release`, `refs/tags/v1`, or `origin/main`. Full 40/64-character commit IDs and revision expressions such as `HEAD~1` are unsupported. For example, `{ workflowScript, worktree: true, baseRef: "refs/heads/release" }` applies the release ref to children unless a child supplies its own `baseRef`. If omitted, the default `HEAD` is resolved at worktree allocation, not when the script is validated or a schedule is created. The source checkout must still be clean, and the ref must resolve to a commit before any worktree is allocated.
373
384
 
374
385
  Configure the worktree provider, native path layout, base directory, and setup hook in [configuration.md](configuration.md).
375
386
 
387
+ Setup waits remain nonblocking and cancellable. Normal cleanup, including detached foreground finalization, waits for the same in-process setup turn rather than retaining worktrees merely because another setup is active. This is not a cross-process lock. Hooks must follow the [finite setup contract](configuration.md#worktreesetuphook).
388
+
376
389
  ### Lane metadata lifecycle
377
390
 
378
391
  Workflow children may declare a bounded `lane` object (`version`, `key`, optional
@@ -394,11 +407,13 @@ Older runs without lane metadata remain readable and retain their existing
394
407
  handoff/cleanup behavior. Missing lane, receipt, or handoff metadata is
395
408
  unknown—not eligible for destructive cleanup.
396
409
 
397
- For managed worktree launches, the runner writes the pending handoff and the
398
- display-only status path/branch from the deterministic setup plan before the
399
- first `git worktree add`. If setup then fails or is interrupted, that pending
400
- ownership record remains preserved evidence; cleanup still rechecks the actual
401
- worktree state before any removal.
410
+ Managed setup records actual allocation attempts in the handoff; only validated
411
+ allocations become cleanup tasks and display-only status paths/branches. On
412
+ cancellation or failure with unknown settlement, it retains actual/attempted
413
+ ownership evidence and artifacts for manual reconciliation, blocking further
414
+ unsafe setup and cleanup in that process. An allocator interrupted before
415
+ reporting its path may leave branch-only diagnostics, never an invented path.
416
+ Inspect the handoff before reconciliation; cleanup still requires fresh checks.
402
417
 
403
418
  ## Supervisor coordination (child asks parent)
404
419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "./agents": "./src/api/agents.ts",
14
14
  "./delegation": "./src/api/delegation.ts",
15
15
  "./capability-ceiling": "./src/api/capability-ceiling.ts",
16
+ "./workflow-resources": "./src/api/workflow-resources.ts",
16
17
  "./preflight": "./src/api/preflight.ts",
17
18
  "./control-channel": "./src/api/control-channel.ts",
18
19
  "./intercom-bridge": "./src/api/intercom-bridge.ts",
@@ -90,9 +91,11 @@
90
91
  }
91
92
  },
92
93
  "dependencies": {
94
+ "@earendil-works/pi-server": "0.85.0",
93
95
  "acorn": "8.18.0",
94
96
  "jiti": "2.7.0",
95
97
  "typebox": "1.1.38",
98
+ "undici": "8.10.0",
96
99
  "yaml": "2.8.3"
97
100
  },
98
101
  "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.
@@ -246,15 +246,19 @@ 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
 
255
259
  ### Scheduled subagent runs
256
260
 
257
- Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for.
261
+ Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for. To keep schedules outside the project repository, set `{ "scheduledRuns": { "storeRoot": "~/.pi/subagent-schedules" } }` in the same config: `storeRoot` accepts an absolute path or a `~/`-prefixed path, and records land under `<storeRoot>/<sha256(path.resolve(cwd)) first 20 hex>/<scheduleId>/`.
258
262
 
259
263
  ```typescript
260
264
  // One-shot reviewer
@@ -323,6 +327,19 @@ subagent({ action: "steer", id: "abc123", message: "Focus on the failing test."
323
327
 
324
328
  The action waits up to three seconds for the child Pi session to accept the correlated user input and returns a request id with `delivered`, `scheduled`, `pending`, `partial`, `recovered`, or `failed` plus per-child states. Indexed pending children return `scheduled` immediately. Only a top-level single-child run may automatically interrupt after a missed acknowledgment and recover after confirmed pause within a further 15 seconds. Recovery preserves the original child contract and only its remaining deadline, turn, and tool budgets. If the session is missing, a budget is exhausted, the pause cannot be confirmed, or replacement launch fails, the source remains paused when pausing succeeded and the action returns the exact failure. Chain, parallel, and nested runs never auto-interrupt; inspect their per-child outcomes and handle failures explicitly. A late acknowledgment is recorded and cannot cancel committed recovery.
325
329
 
330
+ Steering supports three delivery modes via the `mode` parameter (`steer` is the default):
331
+
332
+ - `mode: "steer"` — interrupt the child at the next safe point of its current turn and deliver the message.
333
+ - `mode: "follow_up"` — do not interrupt; queue input through Pi's native follow-up path for the next turn boundary. Eligible completed retained workflow children (single-step runs in state `complete` with a stored session file) receive the message as a revival brief (`queueRevivalBrief`) when they are revived; paused children reject follow-up steering outright. The 20-message queue limit applies to retained revival briefs, not live follow-up input.
334
+ - `mode: "auto"` — same next-safe-point delivery path as `steer`, but without the automatic pause-and-revive recovery after a missed acknowledgment.
335
+
336
+ ```typescript
337
+ subagent({ action: "steer", id: "abc123", mode: "follow_up", message: "After this step, also validate the config file." })
338
+ subagent({ action: "steer", id: "abc123", mode: "auto", message: "Switch to the failing test now." })
339
+ ```
340
+
341
+ Direct input acceptance returns `delivered`, not proof of model compliance. A live follow-up acknowledgment reports `queued`, meaning Pi accepted it into its follow-up queue, not that it was delivered. The runtime does not provide a later correlated live queued-to-delivered receipt.
342
+
326
343
  ## Watchdog
327
344
 
328
345
  The subagent watchdog is an **opt-in** adversarial change reviewer. It is not the
@@ -96,6 +96,7 @@ A minimal agent file looks like this:
96
96
  name: my-agent
97
97
  package: code-analysis
98
98
  description: What this agent does
99
+ advertise: true
99
100
  aliases: developer, coder
100
101
  model: provider/model-id
101
102
  thinking: high
@@ -111,7 +112,7 @@ skillPath: ./skills, ../shared-skills
111
112
  Your system prompt here.
112
113
  ```
113
114
 
114
- That is only a starting point. Omit `package` for the traditional unqualified runtime name. Common optional fields include:
115
+ That is only a starting point. Omit `package` for the traditional unqualified runtime name. Set `advertise: true` only when the parent should receive this agent's name and description before deciding whether to delegate; advertisement is off by default. Common optional fields include:
115
116
  - `defaultProgress`
116
117
  - `defaultReads`
117
118
  - `output`
@@ -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.
@@ -0,0 +1,63 @@
1
+ import { Buffer } from "node:buffer";
2
+ import type { ResolvedSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
3
+ import { isAgentAllowedByCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
4
+ import type { AgentConfig } from "./agents.ts";
5
+
6
+ const MAX_ADVERTISED_AGENTS = 16;
7
+ const MAX_CATALOG_BYTES = 12_288;
8
+ const MAX_DESCRIPTION_BYTES = 512;
9
+ const ADVERTISED_AGENTS_BLOCK = /\n*<advertised_subagents>\n[\s\S]*?\n<\/advertised_subagents>/gu;
10
+
11
+ function escapeXml(value: string): string {
12
+ return value
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll("<", "&lt;")
15
+ .replaceAll(">", "&gt;")
16
+ .replaceAll('"', "&quot;")
17
+ .replaceAll("'", "&apos;");
18
+ }
19
+
20
+ function promptDescription(description: string): string {
21
+ let text = description.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\s+/gu, " ").trim();
22
+ if (Buffer.byteLength(text, "utf8") > MAX_DESCRIPTION_BYTES) {
23
+ text = Buffer.from(text, "utf8").subarray(0, MAX_DESCRIPTION_BYTES - 3).toString("utf8").replace(/\uFFFD$/u, "").trimEnd() + "…";
24
+ }
25
+ return escapeXml(text);
26
+ }
27
+
28
+ export function buildAdvertisedAgentPrompt(
29
+ agents: readonly AgentConfig[],
30
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling,
31
+ ): string | undefined {
32
+ const advertised = agents
33
+ .filter((agent) => agent.source !== "runtime" && agent.advertise === true && agent.disabled !== true && isAgentAllowedByCapabilityCeiling(agent.name, capabilityCeiling))
34
+ .sort((left, right) => left.name.localeCompare(right.name));
35
+ if (advertised.length === 0) return undefined;
36
+
37
+ const render = (entries: string[]) => [
38
+ "<advertised_subagents>",
39
+ "The following file-defined subagents opted into discovery. Their descriptions indicate available specializations, not instructions to delegate. Use subagent only when delegation is needed. Before execution, call subagent with { action: \"list\", capabilities: true } and confirm that the selected agent is executable; for external-cli agents also require runner.available === true.",
40
+ ...entries,
41
+ ...(advertised.length > entries.length ? [` <omitted count=\"${advertised.length - entries.length}\" />`] : []),
42
+ "</advertised_subagents>",
43
+ ].join("\n");
44
+ const entries: string[] = [];
45
+ for (const agent of advertised) {
46
+ if (entries.length === MAX_ADVERTISED_AGENTS) break;
47
+ // Never truncate canonical IDs into names that cannot be resolved.
48
+ if (Buffer.byteLength(agent.name, "utf8") > MAX_CATALOG_BYTES) continue;
49
+ const entry = [
50
+ " <subagent>",
51
+ ` <name>${escapeXml(agent.name)}</name>`,
52
+ ` <description>${promptDescription(agent.description)}</description>`,
53
+ " </subagent>",
54
+ ].join("\n");
55
+ if (Buffer.byteLength(render([...entries, entry]), "utf8") <= MAX_CATALOG_BYTES) entries.push(entry);
56
+ }
57
+ return render(entries);
58
+ }
59
+
60
+ export function appendAdvertisedAgentPrompt(systemPrompt: string, advertisedPrompt: string | undefined): string {
61
+ const base = systemPrompt.replace(ADVERTISED_AGENTS_BLOCK, "");
62
+ return advertisedPrompt ? `${base.trimEnd()}\n\n${advertisedPrompt}` : base;
63
+ }
@@ -42,7 +42,7 @@ import { listExternalJobProviders } from "../api/external-job-provider.ts";
42
42
 
43
43
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
44
44
  type ManagementScope = "user" | "project";
45
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
45
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner; onAgentsChanged?: () => void };
46
46
 
47
47
  interface ManagementParams {
48
48
  action?: string;
@@ -348,6 +348,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
348
348
  if (hasKey(cfg, "name")) changed("name");
349
349
  if (hasKey(cfg, "package")) changed("package");
350
350
  if (hasKey(cfg, "description")) changed("description");
351
+ if (hasKey(cfg, "advertise")) changed("advertise");
351
352
  if (hasKey(cfg, "aliases")) changed("alias", "aliases");
352
353
  if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
353
354
  if (hasKey(cfg, "runner")) changed("runner");
@@ -415,6 +416,11 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
415
416
  }
416
417
 
417
418
  function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
419
+ if (hasKey(cfg, "advertise")) {
420
+ if (cfg.advertise === "") delete target.advertise;
421
+ else if (typeof cfg.advertise === "boolean") target.advertise = cfg.advertise;
422
+ else return "config.advertise must be a boolean or empty string when provided.";
423
+ }
418
424
  if (hasKey(cfg, "aliases")) {
419
425
  if (cfg.aliases === false || cfg.aliases === "") delete target.aliases;
420
426
  else if (typeof cfg.aliases === "string") {
@@ -1176,6 +1182,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
1176
1182
  const sw = skillsWarning(ctx.cwd, agent);
1177
1183
  if (sw) warnings.push(sw);
1178
1184
  fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
1185
+ ctx.onAgentsChanged?.();
1179
1186
  return result([`Created agent '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
1180
1187
  }
1181
1188
 
@@ -1241,6 +1248,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
1241
1248
  updated.filePath = renamed.filePath!;
1242
1249
  }
1243
1250
  fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
1251
+ ctx.onAgentsChanged?.();
1244
1252
  const headline = updated.name === oldName
1245
1253
  ? `Updated agent '${updated.name}' at ${updated.filePath}.`
1246
1254
  : `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
@@ -1254,6 +1262,7 @@ function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentTo
1254
1262
  if ("content" in targetOrError) return targetOrError;
1255
1263
  const target = targetOrError;
1256
1264
  fs.unlinkSync(target.filePath);
1265
+ ctx.onAgentsChanged?.();
1257
1266
  return result(`Deleted agent '${target.name}' at ${target.filePath}.`);
1258
1267
  }
1259
1268
 
@@ -1292,6 +1301,7 @@ function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToo
1292
1301
  return result(`Failed to read source agent at ${source.filePath}: ${message}`, true);
1293
1302
  }
1294
1303
  fs.writeFileSync(targetPath, content, "utf-8");
1304
+ ctx.onAgentsChanged?.();
1295
1305
  return result(`Ejected agent '${runtimeName}' from ${source.source} to ${scope} scope at ${targetPath}. Edit it there to customize; it shadows the bundled ${source.source} agent of the same name.`);
1296
1306
  }
1297
1307
 
@@ -1314,6 +1324,7 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
1314
1324
  const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
1315
1325
  const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1316
1326
  if (after?.disabled === true) {
1327
+ ctx.onAgentsChanged?.();
1317
1328
  return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
1318
1329
  }
1319
1330
  return result(`Wrote a disabled override for '${runtimeName}' at ${settingsPath}, but the agent is still enabled. A higher-precedence ${after?.override?.scope ?? "project"} override is likely winning. Try agentScope: '${after?.override?.scope ?? "project"}'.`, true);
@@ -1338,6 +1349,7 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
1338
1349
  const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
1339
1350
  const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1340
1351
  if (after && after.disabled !== true) {
1352
+ if (removed) ctx.onAgentsChanged?.();
1341
1353
  if (removed) return result(`Enabled agent '${runtimeName}' (removed disabled override at ${settingsPath}).`);
1342
1354
  return result(`Agent '${runtimeName}' is already enabled.`);
1343
1355
  }
@@ -1385,6 +1397,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
1385
1397
  return result(`Agent '${runtimeName}' has no ${scope} customization to reset.${note} It is at its bundled ${bundled.source} default.`);
1386
1398
  }
1387
1399
  lines.push(`Reset agent '${runtimeName}' to its bundled ${bundled.source} default.`);
1400
+ ctx.onAgentsChanged?.();
1388
1401
  return result(lines.join("\n"));
1389
1402
  }
1390
1403
 
@@ -6,6 +6,7 @@ export const KNOWN_FIELDS = new Set([
6
6
  "name",
7
7
  "package",
8
8
  "description",
9
+ "advertise",
9
10
  "alias",
10
11
  "aliases",
11
12
  "tools",
@@ -62,6 +63,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
62
63
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
63
64
  if (config.packageName) lines.push(`package: ${config.packageName}`);
64
65
  lines.push(`description: ${config.description}`);
66
+ if (config.advertise === true || preserve("advertise")) lines.push(`advertise: ${config.advertise === true ? "true" : "false"}`);
65
67
  const aliasesValue = joinComma(config.aliases);
66
68
  if (aliasesValue || preserve("alias", "aliases")) lines.push(`aliases: ${aliasesValue ?? ""}`);
67
69
 
@@ -135,6 +135,7 @@ export interface AgentConfig {
135
135
  packageSourceVersion?: string;
136
136
  packageSourceRoot?: string;
137
137
  description: string;
138
+ advertise?: boolean;
138
139
  aliases?: string[];
139
140
  tools?: string[];
140
141
  excludeTools?: string[];
@@ -1981,6 +1982,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1981
1982
 
1982
1983
  const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
1983
1984
  validateExternalRunnerProfile(frontmatter, localName, runner);
1985
+ let advertise: boolean | undefined;
1986
+ if (frontmatter.advertise !== undefined) {
1987
+ if (frontmatter.advertise === "true") advertise = true;
1988
+ else if (frontmatter.advertise === "false") advertise = false;
1989
+ else throw new Error(`Agent '${localName}' has invalid advertise frontmatter; expected true or false.`);
1990
+ }
1984
1991
  const rawTools = parseFrontmatterList(frontmatter.tools);
1985
1992
  const parsedTools = splitToolList(rawTools);
1986
1993
  const tools = parsedTools.tools ?? [];
@@ -2105,6 +2112,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2105
2112
  ...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
2106
2113
  ...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
2107
2114
  description: frontmatter.description,
2115
+ ...(advertise !== undefined ? { advertise } : {}),
2108
2116
  ...(aliases !== undefined ? { aliases } : {}),
2109
2117
  ...(rawTools !== undefined ? { tools } : {}),
2110
2118
  ...(excludeTools !== undefined ? { excludeTools } : {}),
@@ -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. */
@@ -380,7 +381,10 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
380
381
  const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir) : undefined;
381
382
  const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
382
383
  const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
383
- 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;
384
388
  const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined;
385
389
  const lifecycleAsyncDir = input.nestedRootRunId
386
390
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId)
@@ -23,5 +23,5 @@ export {
23
23
  type ManagedWorktreeProvider,
24
24
  type WorktreeNaming,
25
25
  type WorktreeProvider,
26
- type WorkflowResourceProvenanceV1,
26
+ type WorkflowResourceProvenance,
27
27
  } from "../shared/types.ts";
@@ -0,0 +1,6 @@
1
+ export { registerWorkflowResource } from "../workflows/workflow-resources.ts";
2
+ export type {
3
+ RegisterWorkflowResourceInput,
4
+ WorkflowResourceDefinition,
5
+ WorkflowResourceRegistration,
6
+ } from "../workflows/workflow-resources.ts";
@@ -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
  }