pi-subagents 0.71.0 → 0.72.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 (127) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +1 -1
  3. package/docs/configuration.md +3 -1
  4. package/docs/extension-api.md +64 -2
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +8 -6
  8. package/docs/workflows.md +2 -0
  9. package/package.json +10 -3
  10. package/prompts/council.md +6 -3
  11. package/skills/pi-subagents/references/execution-controls.md +1 -1
  12. package/src/agents/agent-management.d.ts +2 -1
  13. package/src/agents/agent-management.d.ts.map +1 -1
  14. package/src/agents/agent-management.js +32 -29
  15. package/src/agents/agent-management.js.map +1 -1
  16. package/src/agents/agents.d.ts +7 -3
  17. package/src/agents/agents.d.ts.map +1 -1
  18. package/src/agents/agents.js +16 -15
  19. package/src/agents/agents.js.map +1 -1
  20. package/src/agents/global-npm-root.d.ts +7 -0
  21. package/src/agents/global-npm-root.d.ts.map +1 -0
  22. package/src/agents/global-npm-root.js +58 -0
  23. package/src/agents/global-npm-root.js.map +1 -0
  24. package/src/api/inspectors.d.ts +21 -0
  25. package/src/api/inspectors.d.ts.map +1 -0
  26. package/src/api/inspectors.js +12 -0
  27. package/src/api/inspectors.js.map +1 -0
  28. package/src/extension/fanout-child.d.ts.map +1 -1
  29. package/src/extension/fanout-child.js +9 -0
  30. package/src/extension/fanout-child.js.map +1 -1
  31. package/src/extension/index.d.ts.map +1 -1
  32. package/src/extension/index.js +74 -31
  33. package/src/extension/index.js.map +1 -1
  34. package/src/extension/schemas.js +5 -5
  35. package/src/extension/schemas.js.map +1 -1
  36. package/src/extension/subagent-guide.d.ts +1 -1
  37. package/src/extension/subagent-guide.d.ts.map +1 -1
  38. package/src/extension/subagent-guide.js +11 -4
  39. package/src/extension/subagent-guide.js.map +1 -1
  40. package/src/extension/tool-activation.d.ts +1 -1
  41. package/src/extension/tool-activation.d.ts.map +1 -1
  42. package/src/extension/tool-activation.js +16 -6
  43. package/src/extension/tool-activation.js.map +1 -1
  44. package/src/inspectors/plugins.d.ts +7 -2
  45. package/src/inspectors/plugins.d.ts.map +1 -1
  46. package/src/inspectors/plugins.js +78 -3
  47. package/src/inspectors/plugins.js.map +1 -1
  48. package/src/intercom/native-supervisor-channel.d.ts.map +1 -1
  49. package/src/intercom/native-supervisor-channel.js +3 -1
  50. package/src/intercom/native-supervisor-channel.js.map +1 -1
  51. package/src/runs/background/async-execution.d.ts.map +1 -1
  52. package/src/runs/background/async-execution.js +2 -0
  53. package/src/runs/background/async-execution.js.map +1 -1
  54. package/src/runs/background/async-job-tracker.d.ts.map +1 -1
  55. package/src/runs/background/async-job-tracker.js +1 -20
  56. package/src/runs/background/async-job-tracker.js.map +1 -1
  57. package/src/runs/background/binary-bootstrap.d.ts.map +1 -1
  58. package/src/runs/background/binary-bootstrap.js +6 -0
  59. package/src/runs/background/binary-bootstrap.js.map +1 -1
  60. package/src/runs/background/notify.d.ts.map +1 -1
  61. package/src/runs/background/notify.js +2 -0
  62. package/src/runs/background/notify.js.map +1 -1
  63. package/src/runs/background/pid-namespace.d.ts +3 -0
  64. package/src/runs/background/pid-namespace.d.ts.map +1 -0
  65. package/src/runs/background/pid-namespace.js +20 -0
  66. package/src/runs/background/pid-namespace.js.map +1 -0
  67. package/src/runs/background/run-child-session.d.ts +1 -0
  68. package/src/runs/background/run-child-session.d.ts.map +1 -1
  69. package/src/runs/background/run-child-session.js +2 -0
  70. package/src/runs/background/run-child-session.js.map +1 -1
  71. package/src/runs/background/stale-run-reconciler.d.ts +1 -0
  72. package/src/runs/background/stale-run-reconciler.d.ts.map +1 -1
  73. package/src/runs/background/stale-run-reconciler.js +9 -2
  74. package/src/runs/background/stale-run-reconciler.js.map +1 -1
  75. package/src/runs/background/subagent-runner.d.ts.map +1 -1
  76. package/src/runs/background/subagent-runner.js +5 -2
  77. package/src/runs/background/subagent-runner.js.map +1 -1
  78. package/src/runs/foreground/prompt-audit.d.ts.map +1 -1
  79. package/src/runs/foreground/prompt-audit.js +2 -6
  80. package/src/runs/foreground/prompt-audit.js.map +1 -1
  81. package/src/runs/foreground/subagent-executor.d.ts +2 -1
  82. package/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  83. package/src/runs/foreground/subagent-executor.js +120 -9
  84. package/src/runs/foreground/subagent-executor.js.map +1 -1
  85. package/src/runs/shared/child-session.d.ts +1 -0
  86. package/src/runs/shared/child-session.d.ts.map +1 -1
  87. package/src/runs/shared/child-session.js +1 -0
  88. package/src/runs/shared/child-session.js.map +1 -1
  89. package/src/runs/shared/child-tool-plan.d.ts.map +1 -1
  90. package/src/runs/shared/child-tool-plan.js +4 -6
  91. package/src/runs/shared/child-tool-plan.js.map +1 -1
  92. package/src/runs/shared/external-job-bridge.d.ts +7 -0
  93. package/src/runs/shared/external-job-bridge.d.ts.map +1 -1
  94. package/src/runs/shared/external-job-bridge.js +63 -0
  95. package/src/runs/shared/external-job-bridge.js.map +1 -1
  96. package/src/runs/shared/model-resolution.d.ts.map +1 -1
  97. package/src/runs/shared/model-resolution.js +7 -1
  98. package/src/runs/shared/model-resolution.js.map +1 -1
  99. package/src/runs/shared/tool-availability.js +1 -1
  100. package/src/runs/shared/tool-availability.js.map +1 -1
  101. package/src/runs/shared/worktree.d.ts.map +1 -1
  102. package/src/runs/shared/worktree.js +9 -3
  103. package/src/runs/shared/worktree.js.map +1 -1
  104. package/src/shared/types.d.ts +2 -0
  105. package/src/shared/types.d.ts.map +1 -1
  106. package/src/shared/types.js.map +1 -1
  107. package/src/slash/slash-commands.d.ts.map +1 -1
  108. package/src/slash/slash-commands.js +3 -3
  109. package/src/slash/slash-commands.js.map +1 -1
  110. package/src/slash/subagent-cost.d.ts +2 -1
  111. package/src/slash/subagent-cost.d.ts.map +1 -1
  112. package/src/slash/subagent-cost.js +40 -4
  113. package/src/slash/subagent-cost.js.map +1 -1
  114. package/src/tui/fleet-status.d.ts +2 -0
  115. package/src/tui/fleet-status.d.ts.map +1 -1
  116. package/src/tui/fleet-status.js +25 -2
  117. package/src/tui/fleet-status.js.map +1 -1
  118. package/src/tui/fleet.d.ts +1 -1
  119. package/src/tui/fleet.d.ts.map +1 -1
  120. package/src/tui/fleet.js +1 -1
  121. package/src/tui/fleet.js.map +1 -1
  122. package/src/watchdog/permission-arbiter.d.ts.map +1 -1
  123. package/src/watchdog/permission-arbiter.js +1 -5
  124. package/src/watchdog/permission-arbiter.js.map +1 -1
  125. package/src/watchdog/review.d.ts.map +1 -1
  126. package/src/watchdog/review.js +1 -5
  127. package/src/watchdog/review.js.map +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.72.0] - 2026-09-26
6
+
7
+ ### Highlights
8
+
9
+ - Pi starts faster with pi-subagents installed: startup no longer waits on global agent discovery, and the executor and Fleet load the first time you use them.
10
+ - The package no longer bundles its own copy of TypeBox, so installs are smaller and Pi stops warning about the manifest.
11
+ - Models from complete provider extensions now work everywhere pi-subagents calls a model, including the watchdog, permission checks, and Prompt Audit.
12
+ - Other extensions can plug their own inspectors into Fleet.
13
+ - Async runs are more dependable: child sessions can start and resume their own external jobs, `/subagent-cost` counts every async launch, and live runs are no longer marked failed when containers share a temp directory.
14
+
15
+ ### Added
16
+
17
+ - Extensions can register their own inspector providers with the `pi-subagents/inspectors` helper or the `pi-subagents:inspector-register:v1` event. Fleet uses them for inspector actions, while the built-in inspectors and runner controls keep working as before. Thanks to [@ninjapenguin](https://github.com/ninjapenguin) for [#2482](https://github.com/nicobailon/pi-subagents/pull/2482).
18
+
19
+ ### Changed
20
+
21
+ - Pi starts faster with pi-subagents enabled. The foreground executor and the Fleet view now load on first use, which cuts the extension's own startup modules from 264 to 199. Tools, commands, and prompts are registered exactly as before. Thanks to [@h4yfans](https://github.com/h4yfans) for the measurements in [#2480](https://github.com/nicobailon/pi-subagents/issues/2480).
22
+ - TypeBox is now provided by Pi instead of bundled with the package. Pi no longer warns about the manifest, and the extension, its children, and background runners all use Pi's copy. Thanks to [@felipemm](https://github.com/felipemm) for [#2454](https://github.com/nicobailon/pi-subagents/issues/2454) and [#2455](https://github.com/nicobailon/pi-subagents/pull/2455).
23
+
24
+ ### Fixed
25
+
26
+ - The watchdog, permission checks, and Prompt Audit now reach models from complete provider extensions (`pi.registerProvider(provider)`). They used an older lookup that only knew config-style providers, so these calls failed even though ordinary children could use the same model. Thanks to [@chem](https://github.com/chem) for [#2496](https://github.com/nicobailon/pi-subagents/issues/2496).
27
+ - Model names such as `openrouter/auto-beta` now resolve when the provider's own model id already starts with the provider name. They used to fail with `Unknown subagent model`. Thanks to [@schmlblk](https://github.com/schmlblk) for [#2487](https://github.com/nicobailon/pi-subagents/issues/2487).
28
+ - `fast: true` accepts any `openai-codex/*` model instead of only two named models, so newer Codex models no longer fail before launch. Other providers are still rejected. Thanks to [@jtabke](https://github.com/jtabke) for [#2452](https://github.com/nicobailon/pi-subagents/issues/2452).
29
+ - Async status shows a child's real context limit once its session starts, including a limit raised by an extension. Thanks to [@johnhenaot](https://github.com/johnhenaot) for [#2448](https://github.com/nicobailon/pi-subagents/pull/2448).
30
+ - Session startup no longer freezes while pi-subagents looks for globally installed agents. The first agent prompt and `subagents_enable` still wait until discovery finishes. Thanks to [@trading-bl](https://github.com/trading-bl) for [#2474](https://github.com/nicobailon/pi-subagents/issues/2474).
31
+ - `subagents_enable` ignores extra arguments instead of rejecting the call. DeepSeek V4.1 Flash sends `subagents_enable({ action: "enable" })`, which used to fail every time. Thanks to [@crusaderky](https://github.com/crusaderky) for [#2483](https://github.com/nicobailon/pi-subagents/issues/2483).
32
+ - An older `pi-ai` installed next to the package no longer turns off on-demand tool loading, so `subagent` stays hidden behind `subagents_enable` until it is needed. Thanks to [@abdwhb-png](https://github.com/abdwhb-png) for [#2471](https://github.com/nicobailon/pi-subagents/pull/2471).
33
+ - `/council` works when Pi runs with `--no-skills`. It now loads its instructions through `subagent({ action: "guide", topic: "council" })`. Thanks to [@felipemm](https://github.com/felipemm) for [#2467](https://github.com/nicobailon/pi-subagents/issues/2467).
34
+ - External-job runs that a child session launches now start instead of staying queued. Thanks to [@juanpprieto](https://github.com/juanpprieto) for [#2449](https://github.com/nicobailon/pi-subagents/issues/2449).
35
+ - A child session can follow up its own external-job runs with `resume`, and a repeated resume no longer sends the follow-up twice. Thanks to [@juanpprieto](https://github.com/juanpprieto) for [#2465](https://github.com/nicobailon/pi-subagents/issues/2465).
36
+ - A background run that is still working is no longer reported failed when Pi processes in different containers share one `PI_SUBAGENTS_TEMP_ROOT`. Pi could not see the runner's process from another container and treated it as gone. Such runs now fall back to the usual 24-hour stale check. Thanks to [@zeezooz](https://github.com/zeezooz) for [#2494](https://github.com/nicobailon/pi-subagents/issues/2494) and [#2495](https://github.com/nicobailon/pi-subagents/issues/2495).
37
+ - `/subagent-cost` includes async single, chain, and parallel launches, which it used to report as having no child usage. Thanks to [@zeezooz](https://github.com/zeezooz) for [#2484](https://github.com/nicobailon/pi-subagents/issues/2484).
38
+ - Awaited workflow children send the `subagent:async-complete` event without an extra child notification. Thanks to [@mmarabel](https://github.com/mmarabel) for [#2456](https://github.com/nicobailon/pi-subagents/issues/2456).
39
+ - Worktree labels stay within 256 bytes when shortened in the middle of a multi-byte character, so async status stays readable. Thanks to [@chenhaoxiang](https://github.com/chenhaoxiang) for [#2446](https://github.com/nicobailon/pi-subagents/pull/2446).
40
+ - A launch blocked by the session's capability ceiling now fails before any fork work starts. It used to branch the parent session or prepare the fork summary first. Thanks to [@antonioc-cl](https://github.com/antonioc-cl) for [#2481](https://github.com/nicobailon/pi-subagents/pull/2481).
41
+ - The `subagent` schema and guides describe preflight, timeout aliases, budget limits, and child extension bindings accurately, and an empty `usageBudget` is now rejected. Thanks to [@amchen2310](https://github.com/amchen2310) for [#2473](https://github.com/nicobailon/pi-subagents/issues/2473).
42
+ - The async workflow widget no longer flickers while the inline Fleet roster is open. Thanks to [@gustavo-neiva](https://github.com/gustavo-neiva) for [#2468](https://github.com/nicobailon/pi-subagents/pull/2468).
43
+ - `subagent_supervisor` `pending` shows each request's question, so you can answer a request after missing its notice ([#2460](https://github.com/nicobailon/pi-subagents/issues/2460)).
44
+
5
45
  ## [0.71.0] - 2026-09-23
6
46
 
7
47
  ### Highlights
package/README.md CHANGED
@@ -110,7 +110,7 @@ For bounded orchestration, `maxSubagentSpawnsPerRun` limits cumulative logical c
110
110
 
111
111
  or ask: "Check whether subagents and intercom are set up correctly."
112
112
 
113
- For installed-version help, use `/subagents-guide [topic]` or `subagent({ action: "guide", topic: "workflows" })`. The default topic is `overview`; available topics are `overview`, `workflows`, `agents`, `missions`, `observability`, `tool-reference`, `configuration`, `models`, `watchdog`, and `extension-api`.
113
+ For installed-version help, use `/subagents-guide [topic]` or `subagent({ action: "guide", topic: "workflows" })`. The default topic is `overview`; available topics are `overview`, `workflows`, `agents`, `missions`, `observability`, `tool-reference`, `configuration`, `models`, `watchdog`, `extension-api`, and `council`.
114
114
 
115
115
  ## Documentation
116
116
 
@@ -82,6 +82,8 @@ On Pi 0.86.1 or newer, a fresh unrestricted parent starts with `subagents_enable
82
82
 
83
83
  The recorded native `subagent` selection is restored on resume, reload, and tree navigation, so an activated session stays activated and a cold session stays cold. Older history without tool-selection records keeps eager `subagent` availability. If Pi's allowlist or exclusions remove the loader, the extension does not hide `subagent`; if they remove `subagent`, the loader reports it unavailable. Hosts older than the verified dynamic-tool baseline keep eager behavior and log one compatibility warning.
84
84
 
85
+ Some providers fix the tool list for a whole prompt, for example bridges that hand Pi's tools to another agent SDK. There `subagent` appears only on the next user prompt, not the next model request. Start Pi with `--exclude-tools subagents_enable` to keep `subagent` active from the start.
86
+
85
87
  ## `toolDescriptionMode`
86
88
 
87
89
  ```json
@@ -406,7 +408,7 @@ Routes relative `output` paths for single-agent `/run` calls under this director
406
408
  { "maxSubagentDepth": 1 }
407
409
  ```
408
410
 
409
- Controls nested delegation when no stricter limit is inherited from the launching child's runtime config. Per-agent `maxSubagentDepth` can tighten the limit for that agent's child runs, but cannot relax an inherited stricter limit. This applies even to children that explicitly declare `tools: subagent` or `allowNestedSubagents: true`; at the cap, execution fanout is blocked instead of silently hiding nested work.
411
+ Controls nested delegation when no stricter limit is inherited from the launching child's runtime config. The default is 2 when neither the runtime limit, `PI_SUBAGENT_MAX_DEPTH`, nor this setting supplies a limit. Per-agent `maxSubagentDepth` can tighten the limit for that agent's child runs, but cannot relax an inherited stricter limit. This applies even to children that explicitly declare `tools: subagent` or `allowNestedSubagents: true`; at the cap, execution fanout is blocked instead of silently hiding nested work.
410
412
 
411
413
  ## `PI_SUBAGENT_PI_BINARY`
412
414
 
@@ -437,7 +437,7 @@ The provider returns handles with `providerJobId`, `state`, optional `handleUrl`
437
437
 
438
438
  `followUp(input)` is optional. When it is present, a completed external-job run can be continued with `subagent({ action: "resume", id: "<run>", message: "..." })`. Pi sends the completed parent provider job id plus a stable `requestId` and `requestDigest`. The provider must continue that parent conversation or fail closed. It must not open a fresh thread when the parent conversation is missing.
439
439
 
440
- The async runner process does not import provider internals. It writes operation requests into its async run directory. The parent Pi process services those requests against the registered provider and writes operation responses. If the provider is not registered, the bridge fails closed with an actionable error. If a run is recovered after provider job metadata exists, the runner calls `reattach` and `result`; it does not call `start` or `follow-up` again.
440
+ The async runner process does not import provider internals. It writes operation requests into its async run directory. The Pi process of the session that launched the run services those requests against the registered provider and writes operation responses. A child session services the external-job runs it launches. Register the provider in that child too, for example through the child agent's `extensions`. If the provider is not registered, the bridge fails closed with an actionable error. If a run is recovered after provider job metadata exists, the runner calls `reattach` and `result`; it does not call `start` or `follow-up` again.
441
441
 
442
442
  ## Inspect integration
443
443
 
@@ -450,7 +450,69 @@ subagent({ action: "inspector.status", id: "<run-id>", index: 0 })
450
450
  subagent({ action: "inspector.close", id: "<run-id>", index: 0 })
451
451
  ```
452
452
 
453
- `inspector.command` returns a standalone runner command without contacting a host or writing a binding. `inspector.open` selects an available bundled inspector plugin. `status` and `close` select the plugin that owns the run binding and report clearly when that plugin does not support the requested lifecycle action. Without an available plugin, `open` fails closed with an actionable message; ordinary launches remain headless. Closing an inspector never stops the run.
453
+ `inspector.command` returns a standalone runner command without contacting a host or writing a binding. `inspector.open` selects an available built-in or externally registered inspector plugin. `status` and `close` select the plugin that owns the run binding and report clearly when that plugin does not support the requested lifecycle action. Without an available plugin, `open` fails closed with an actionable message; ordinary launches remain headless. Closing an inspector never stops the run.
454
+
455
+ ### Register an external inspector
456
+
457
+ A loaded Pi extension can add an inspector through the synchronous
458
+ `pi-subagents:inspector-register:v1` event. This lets terminal integrations use
459
+ Fleet's existing Enter/H actions without modifying pi-subagents. Registration
460
+ adds no runner, tool, or configuration option.
461
+
462
+ ```ts
463
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
464
+ import type { InspectorRegistration, InspectorRegistrationRequest } from "pi-subagents/inspectors";
465
+ import { myInspector } from "./my-inspector.ts"; // Your InspectorPlugin implementation.
466
+
467
+ export default function (pi: ExtensionAPI) {
468
+ let registration: InspectorRegistration | undefined;
469
+ pi.on("session_start", () => {
470
+ registration?.dispose();
471
+ const request: InspectorRegistrationRequest = { version: 1, plugin: myInspector };
472
+ pi.events.emit("pi-subagents:inspector-register:v1", request);
473
+ if (!request.result) throw new Error("pi-subagents inspector registration is unavailable.");
474
+ if (!request.result.ok) throw request.result.error;
475
+ registration = request.result.registration;
476
+ });
477
+ pi.on("session_shutdown", () => registration?.dispose());
478
+ }
479
+ ```
480
+
481
+ `pi-subagents/inspectors` exports the event name as `INSPECTOR_REGISTER_EVENT`,
482
+ the request and registration types, and the existing `InspectorPlugin`,
483
+ `InspectorContext`, `InspectorLaunch`, `InspectorParams`, and `InspectorTarget`
484
+ types. When pi-subagents is a resolvable dependency, `registerInspector(pi, plugin)`
485
+ emits the same request and returns the registration or throws. Independently
486
+ installed extensions can use the event directly; type-only imports need only a
487
+ development dependency and create no runtime module dependency.
488
+
489
+ An `InspectorPlugin` supplies:
490
+
491
+ - `name`: 1–128 letters, digits, dots, underscores, or hyphens, starting with a letter or digit.
492
+ - `available(context)`: whether this terminal host can open an inspector; may be async.
493
+ - `open(context, launch, params)`: open the supplied inspector command and return an `AgentToolResult<Details>`, like the built-in plugins. `launch.executable` and `launch.argv` carry the existing runner, target, trusted session roots, and steer/stop permission flags. Preserve these arguments and respect `params.focus`.
494
+ - `owns(context)`: synchronously check whether the provider owns an inspector binding for this target.
495
+ - Optional `status(context)` and `close(context)`: inspect or close that binding, returning the same result type. Closing an inspector must not stop the subagent.
496
+
497
+ The existing dispatcher tries Herdr, then Ghostty, then external providers in
498
+ registration order. Names are case-sensitive; duplicate names and the built-in
499
+ names `herdr` and `ghostty` are rejected. A selected provider's failure is not
500
+ retried through another provider. Status/close use the first provider whose
501
+ `owns` returns true; unavailable lifecycle methods remain explicit errors.
502
+ Providers own their pane bindings and must verify ownership before closing one.
503
+
504
+ Registrations belong to the pi-subagents runtimes listening on the Pi event bus,
505
+ not the consumer module; they survive runtime replacement, are isolated from
506
+ child runtimes and other Pi instances, and are cleared when the last owner shuts
507
+ down or reloads. Disposal is idempotent, removes callbacks without closing panes,
508
+ and takes effect even while Fleet is open because it resolves providers per
509
+ action. Providers receive the current action context, so do not capture a stale
510
+ session context; dispose before re-registering on session changes. Unsupported
511
+ versions and malformed plugins return `{ ok: false, error }`, and the first owner
512
+ to fill `result` handles the request.
513
+
514
+ This is a trusted-code integration, not a sandbox. Registration does not change
515
+ run resolution, child-safe restrictions, authority policy, or supervisor routing.
454
516
 
455
517
  ### Herdr inspector plugin
456
518
 
package/docs/models.md CHANGED
@@ -88,7 +88,7 @@ For a persistent role override:
88
88
 
89
89
  Set `fast: true` on a run, in agent frontmatter, or in `subagents.agentOverrides.<name>.fast` to request the OpenAI priority service tier for supported native OpenAI-Codex children. This can use a higher quota tier or cost more. It is off by default.
90
90
 
91
- Fast mode fails before launch unless the resolved model is on the allowlist. The current allowlist is `openai-codex/gpt-5.6-luna` and `openai-codex/gpt-5.6-sol`. External runners, Anthropic models, and other providers do not use fast mode.
91
+ Fast mode fails before launch unless the resolved model is a native `openai-codex/*` model. External runners, Anthropic models, and other providers do not use fast mode.
92
92
 
93
93
  ## Recommended model tiering (optional)
94
94
 
@@ -300,7 +300,7 @@ Session files are stored under a per-run session directory. With `context: "fork
300
300
 
301
301
  ## Completion notifications
302
302
 
303
- Async completions belong only to the originating session. The result watcher emits `subagent:async-complete`, and the extension consumes that event to record completion state.
303
+ Async completions belong only to the originating session. The result watcher emits `subagent:async-complete` for detached runs. Awaited workflow children emit it when they settle, with `awaitedByWorkflow: true`, `parentWorkflowRunId`, and `triggerTurn: false`. They do not send a separate child notification or turn.
304
304
 
305
305
  Successful sibling completions are held briefly and delivered as a quiet grouped completion when they finish within a short window (see `completionBatch` in [configuration.md](configuration.md)), avoiding unread markers on inactive tabs. Failed and paused completions remain visible and fire immediately.
306
306
 
@@ -95,7 +95,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
95
95
  | `agent` | string | - | One direct child or agent-management target. Workflow child agents are set inside `runs.run` or `runs.all`. |
96
96
  | `task` | string | agent default | Direct child's task; requires `agent`, excludes `action` and workflow inputs. `agent` may also select a management target. |
97
97
  | `action` | string | - | Offline workflow `validate`, agent management (including `guide`, `children.list`, and `refine`/`refine.show`/`refine.rollback`), lane evidence (`lane.status`, `lane.recordMerge`, `lane.recordSupersession`), mission (`mission.create/list/show/update/resolve-decision/attach-run/close`), Inspect actions (`inspector.command/open/status/close`), Herdr project pane (`project.open/status/close`), status/control, plan-only `worktree.cleanup`, schedule, watchdog, or doctor action. |
98
- | `topic` | `overview \| workflows \| agents \| missions \| observability \| tool-reference \| configuration \| models \| watchdog \| extension-api` | `overview` | Packaged guide topic for `action: "guide"`. |
98
+ | `topic` | `overview \| workflows \| agents \| missions \| observability \| tool-reference \| configuration \| models \| watchdog \| extension-api \| council` | `overview` | Packaged guide topic for `action: "guide"`. |
99
99
  | `config` | object/string | - | Agent config for management create/update. |
100
100
  | `context` | `fresh \| fork \| profile` | global or per-agent default, else `fresh` | Explicit `fresh` or `fork` overrides every workflow child. `profile` requires the selected agent's declared `defaultContext` and ignores config `defaultSubagentContext`; missing agent defaults fail. When omitted, [`defaultSubagentContext`](configuration.md#defaultsubagentcontext) wins over each agent's `defaultContext`; implicit fork falls back to fresh without a persisted parent session and leaf. Explicit fork is strict. Packaged `worker` defaults to `fresh`; packaged `oracle` and `advisor` default to `fork`. |
101
101
  | `model` | string | agent default | Call `{action:"models"}` first and copy an exact `provider/id`; bare ids resolve only if unique, and agent names are not model ids. A suffix such as `provider/id:high` (`off/minimal/low/medium/high/xhigh/max`) overrides agent thinking. The `thinking` field is only for `watchdog.configure`, ignored on dispatch. |
@@ -117,11 +117,13 @@ The complete plain-JSON inventory is validated before the first launch (maximum
117
117
  | `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. |
118
118
  | `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. |
119
119
  | `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. |
120
- | `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. |
120
+ | `timeoutMs` / `maxRuntimeMs` | number | config `timeoutMs`, else 30 min foreground / single-agent async | Optional run-level max runtime in milliseconds. Both aliases may be supplied only when their values agree. 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. |
121
121
  | `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. |
122
122
  | `checkpointBeforeDeadlineMs` | number | none | Async single-agent runs only. The runner requests that the child "checkpoint and stop" this many milliseconds before the run deadline (finish the current tool call, report changed files, build/test state, remaining work, commit/PR state; start no new work). This best-effort steer uses the normal steering lifecycle at the next tool boundary, so the receipt is visible in status and events; the ordinary deadline kill still applies. Precedence: call value → config `checkpointBeforeDeadlineMs`. Disarmed when the deadline leaves under one second before the checkpoint. |
123
- | `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. |
124
- | `usageBudget` | object | none | Optional root-only reported-usage budget `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }`. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made. |
123
+ | `toolBudget` | object | none | Optional child tool-call budget `{ soft?, hard, block? }`: `hard` is a positive integer and `soft`, if set, is a positive integer no greater than `hard`. At `soft` the child is nudged to finalize. After `hard`, configured tools are blocked; `block` defaults to `read`, `grep`, `find`, and `ls`, and accepts either a nonempty array of tool names or `"*"` for every tool call. Final assistant text is never blocked. |
124
+ | `usageBudget` | object | none | Optional root-only reported-usage budget `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }`. Include at least one metric; each `hard` must be positive and each optional `soft` positive and no greater than its `hard`. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made. |
125
+ | `extensionBindings` | object | none | Child-only plain JSON keyed by namespaces like `package.name/1` (positive version): at most 16 namespaces, nesting depth 16, 256 total object properties, and 16 KiB of canonical UTF-8 JSON. Native Pi launches only; external runners and retained resumes reject it. |
126
+ | `preflight` | object | none | Advisory lane hints for `workflowScript` or `workflowScriptPath` only; not accepted with named workflows or direct children. See [bounded workflows](workflows.md#opt-in-bounded-workflows). |
125
127
  | `cwd` | string | runtime cwd | Override working directory. With `machine`, the directory on that machine. |
126
128
  | `machine` | string | - | Herdr saved machine (label or profile id) for external-cli agents; see [agents.md](agents.md#running-external-cli-agents-on-a-herdr-saved-machine). |
127
129
  | `maxOutput` | object | none | Final output truncation limits `{ bytes?, lines? }`. Only applied when set; there is no default cap on the inline path, so use `outputMode: "file-only"` for large outputs. |
@@ -206,7 +208,7 @@ For a simple implementation challenge outside a workflow script, send the challe
206
208
 
207
209
  ### Guide
208
210
 
209
- `{ action: "guide" }` reads the packaged `README.md` from the installed version. Pass `topic` to read its packaged `docs/<topic>.md` file instead. Valid topics are `overview`, `workflows`, `agents`, `missions`, `observability`, `tool-reference`, `configuration`, `models`, `watchdog`, and `extension-api`. Unknown topics list the valid values and do not change files. Use `/subagents-guide [topic]` for the slash equivalent.
211
+ `{ action: "guide" }` reads the packaged `README.md` from the installed version. Pass `topic` to read its packaged `docs/<topic>.md` file instead; see the `topic` parameter table for valid values. The `council` topic returns the packaged `skills/council-mode/SKILL.md` with the references it asks for, so `/council` works when Pi runs with `--no-skills`. Unknown topics list the valid values and do not change files. Use `/subagents-guide [topic]` for the slash equivalent.
210
212
 
211
213
  Agent definitions are not loaded into context by default. Management actions let the LLM discover, inspect, create, update, and delete agents at runtime. An unknown action returns safe next steps (`status` and `list`) and may suggest a close non-destructive action. Destructive actions are only named for a near-complete one-character typo, and suggestions never execute an action.
212
214
 
@@ -351,7 +353,7 @@ subagent({ action: "doctor" })
351
353
 
352
354
  - Multi-child async runs and remembered foreground single, parallel, or chain runs can be revived by passing `index` to choose the child.
353
355
  - Nested runs can be resumed by nested id when their live route or persisted nested session metadata is available.
354
- - Completed external-job runs can use the same `resume` action as a provider follow-up when the registered provider exposes `followUp(input)`. Running external-job parents fail closed with guidance to wait for completion. Unsupported providers fail with an update/reload message.
356
+ - Completed external-job runs, including nested runs, can use the same `resume` action as a provider follow-up when the registered provider exposes `followUp(input)`. Running external-job parents fail closed with guidance to wait for completion. Unsupported providers fail with an update/reload message.
355
357
  - Revive starts a new child session from the old session context; it does not resume the live session, and it requires the chosen child to have a persisted `.jsonl` session file.
356
358
  - Direct revival takes an exclusive cross-process lease on the canonical session file until the new child finishes. A concurrent attempt fails before Pi is spawned and identifies the owning revived run; dead-owner leases are reclaimed only when staleness can be proved.
357
359
 
package/docs/workflows.md CHANGED
@@ -118,6 +118,8 @@ subagent({
118
118
 
119
119
  These controls are opt-in. Avoid tight hard budgets for mutation-capable workers unless the workflow has an explicit checkpoint and handoff path.
120
120
 
121
+ The optional `preflight` lane hints apply only to raw `workflowScript` or `workflowScriptPath` requests, not named workflows or direct child calls. They are display-only: mismatched coverage warns but never changes execution authority.
122
+
121
123
  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.
122
124
 
123
125
  ```js
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "pi-subagents",
4
- "version": "0.71.0",
4
+ "version": "0.72.0",
5
5
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
6
6
  "author": "Nico Bailon",
7
7
  "license": "MIT",
@@ -19,7 +19,6 @@
19
19
  "dependencies": {
20
20
  "acorn": "8.18.0",
21
21
  "jiti": "2.7.0",
22
- "typebox": "1.1.38",
23
22
  "undici": "8.10.0",
24
23
  "yaml": "2.8.3"
25
24
  },
@@ -27,7 +26,8 @@
27
26
  "@earendil-works/pi-agent-core": "*",
28
27
  "@earendil-works/pi-ai": ">=0.86.1",
29
28
  "@earendil-works/pi-coding-agent": "*",
30
- "@earendil-works/pi-tui": "*"
29
+ "@earendil-works/pi-tui": "*",
30
+ "typebox": "*"
31
31
  },
32
32
  "peerDependenciesMeta": {
33
33
  "@earendil-works/pi-agent-core": {
@@ -41,6 +41,9 @@
41
41
  },
42
42
  "@earendil-works/pi-tui": {
43
43
  "optional": true
44
+ },
45
+ "typebox": {
46
+ "optional": true
44
47
  }
45
48
  },
46
49
  "keywords": [
@@ -74,6 +77,10 @@
74
77
  "types": "./src/api/agents.d.ts",
75
78
  "default": "./src/api/agents.js"
76
79
  },
80
+ "./inspectors": {
81
+ "types": "./src/api/inspectors.d.ts",
82
+ "default": "./src/api/inspectors.js"
83
+ },
77
84
  "./delegation": {
78
85
  "types": "./src/api/delegation.d.ts",
79
86
  "default": "./src/api/delegation.js"
@@ -8,8 +8,11 @@ session, are the supervisor. You select the roster, curate cross-advisor packets
8
8
  decide which feedback is valid, and write the final memo. Advisors do not talk
9
9
  directly or see peer transcripts by default. This is not free-form agent chat.
10
10
 
11
- Before you orchestrate, read `skills/council-mode/SKILL.md` and
12
- `skills/pi-subagents/references/execution-controls.md`.
11
+ Before you orchestrate, load the council protocol with
12
+ `subagent({ action: "guide", topic: "council" })`. If only `subagents_enable`
13
+ is available, call it first; `subagent` is available on the next request. The
14
+ guide returns the packaged council-mode skill and the references it asks you to
15
+ read, so it also works when Pi runs with `--no-skills`.
13
16
 
14
17
  Parse the invocation yourself. The flags below are conventions, not runtime
15
18
  options. Record a brief with the question, scope, non-goals, evidence targets,
@@ -48,7 +51,7 @@ path if the run is not resumable.
48
51
  ## Run the protocol
49
52
 
50
53
  Use the canonical workflow, structured advisor contracts, aggregate pass receipts,
51
- and memo requirements in `skills/council-mode/SKILL.md`. Keep the parent as the
54
+ and memo requirements from the council guide. Keep the parent as the
52
55
  only synthesizer and decision maker. Do not introduce a chair advisor, peer chat,
53
56
  or transcript sharing.
54
57
 
@@ -32,7 +32,7 @@ An agent may set `runner.type: external-job` with a non-empty `provider` and opt
32
32
 
33
33
  External job profiles are async-only. The provider owns the remote job and Pi owns the async run record. Status persists provider name, provider job id, prompt digest, provider options, handle/conversation URLs when supplied, result artifact path, last known state, and provider failure code/message. Recovery uses existing provider job metadata to call `reattach` and `result`; it refuses to redispatch a prompt when the persisted provider job does not match the prompt digest.
34
34
 
35
- External job profiles do not support foreground/clarify, steer/resume, Pi models/tools/extensions/skills, tool budgets, structured output, native child permissions, or Pi child sessions. Capacity conflicts fail closed and include the blocking provider job id when the provider supplies it.
35
+ External job profiles do not support foreground/clarify, live steer or native session resume, Pi models/tools/extensions/skills, tool budgets, structured output, native child permissions, or Pi child sessions. Completed external-job runs can use `action: "resume"` for provider follow-up when the registered provider exposes `followUp(input)`; running jobs must finish first, and providers without follow-up support fail closed with an update/reload message. Capacity conflicts fail closed and include the blocking provider job id when the provider supplies it.
36
36
 
37
37
  ### Single agent
38
38
 
@@ -1,6 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { type AgentConfig } from "./agents.ts";
3
+ import { type AgentConfig, discoverAgentsAll } from "./agents.ts";
4
4
  import type { Details, ExtensionConfig } from "../shared/types.ts";
5
5
  import { type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
6
6
  type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & {
@@ -9,6 +9,7 @@ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & {
9
9
  currentSessionId?: string;
10
10
  runtimeAgentOwner?: RuntimeAgentOwner;
11
11
  onAgentsChanged?: () => void;
12
+ discoverAgentsAll?: typeof discoverAgentsAll;
12
13
  };
13
14
  interface ManagementParams {
14
15
  action?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-management.d.ts","sourceRoot":"","sources":["../../../src/agents/agent-management.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EACN,KAAK,WAAW,EAiBhB,MAAM,aAAa,CAAC;AAcrB,OAAO,KAAK,EAAkE,OAAO,EAAE,eAAe,EAAoB,MAAM,oBAAoB,CAAC;AAIrJ,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAKzF,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,eAAe,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAE3O,UAAU,gBAAgB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAoMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CA8EnE;AAWD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAsD7G;AA8jBD,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAuCrG;AAmID,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAiDvG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CA+DvG;AAoJD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAcjI"}
1
+ {"version":3,"file":"agent-management.d.ts","sourceRoot":"","sources":["../../../src/agents/agent-management.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EACN,KAAK,WAAW,EAQhB,iBAAiB,EASjB,MAAM,aAAa,CAAC;AAcrB,OAAO,KAAK,EAAkE,OAAO,EAAE,eAAe,EAAoB,MAAM,oBAAoB,CAAC;AAIrJ,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAKzF,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,eAAe,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAA;CAAE,CAAC;AAMzR,UAAU,gBAAgB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAoMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CA8EnE;AAWD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAsD7G;AA+jBD,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAuCrG;AAmID,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAiDvG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CA+DvG;AAoJD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAAC,OAAO,CAAC,CAcjI"}
@@ -17,6 +17,9 @@ import { previewDisplayText } from "../shared/display-text.js";
17
17
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.js";
18
18
  import { mergeRuntimeAgents } from "./runtime-agent-registry.js";
19
19
  import { listExternalJobProviders } from "../api/external-job-provider.js";
20
+ function discoverCatalog(ctx, cwd = ctx.cwd, provider) {
21
+ return (ctx.discoverAgentsAll ?? discoverAgentsAll)(cwd, provider);
22
+ }
20
23
  function result(text, isError = false, details) {
21
24
  return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [], ...details } };
22
25
  }
@@ -97,8 +100,8 @@ function availableAgentNamesFromDiscovery(d, runtimeAgentOwner, preferredModelPr
97
100
  const agents = runtimeAgentOwner ? effectiveAgentsForScope("both", d, runtimeAgentOwner, preferredModelProvider) : allAgents(d);
98
101
  return [...new Set(agents.map((agent) => agent.name))].sort((a, b) => a.localeCompare(b));
99
102
  }
100
- function availableAgentNames(cwd) {
101
- return availableAgentNamesFromDiscovery(discoverAgentsAll(cwd));
103
+ function availableAgentNames(ctx, cwd = ctx.cwd) {
104
+ return availableAgentNamesFromDiscovery(discoverCatalog(ctx, cwd));
102
105
  }
103
106
  function findAgentsInDiscovery(name, d, scope = "both", runtimeAgentOwner, preferredModelProvider) {
104
107
  const raw = name.trim();
@@ -114,8 +117,8 @@ function findAgentsInDiscovery(name, d, scope = "both", runtimeAgentOwner, prefe
114
117
  || (sanitized !== raw && Boolean(resolveAgentName(sanitized, [agent]).agent)))
115
118
  .sort((a, b) => a.source.localeCompare(b.source));
116
119
  }
117
- function findAgents(name, cwd, scope = "both") {
118
- return findAgentsInDiscovery(name, discoverAgentsAll(cwd), scope);
120
+ function findAgents(name, ctx, scope = "both") {
121
+ return findAgentsInDiscovery(name, discoverCatalog(ctx), scope);
119
122
  }
120
123
  function diagnosticsForScope(diagnostics, scope) {
121
124
  if (scope === "both")
@@ -142,8 +145,8 @@ function resolveEffectiveAgent(d, name) {
142
145
  const matches = candidates.filter((agent) => agent.name === resolved.agent.name);
143
146
  return { agent: matches.reduce((best, agent) => (AGENT_SOURCE_PRECEDENCE[agent.source] > AGENT_SOURCE_PRECEDENCE[best.source] ? agent : best)) };
144
147
  }
145
- function nameExistsInScope(cwd, scope, name, excludePath) {
146
- const d = discoverAgentsAll(cwd);
148
+ function nameExistsInScope(ctx, scope, name, excludePath) {
149
+ const d = discoverCatalog(ctx);
147
150
  for (const a of scope === "user" ? d.user : d.project) {
148
151
  if (a.name === name && a.filePath !== excludePath)
149
152
  return true;
@@ -664,7 +667,7 @@ function applyAgentConfig(target, cfg) {
664
667
  }
665
668
  return undefined;
666
669
  }
667
- function resolveTarget(name, matches, cwd, scopeHint) {
670
+ function resolveTarget(name, matches, cwd, ctx, scopeHint) {
668
671
  const distinctNames = [...new Set(matches.map((match) => match.name))];
669
672
  if (distinctNames.length > 1)
670
673
  return result(`Ambiguous agent alias or name '${name}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
@@ -672,7 +675,7 @@ function resolveTarget(name, matches, cwd, scopeHint) {
672
675
  if (mutable.length === 0) {
673
676
  if (matches.length > 0)
674
677
  return result(`Agent '${name}' is read-only and cannot be modified. Create a same-named agent in user or project scope to override it.`, true);
675
- return result(`Agent '${name}' not found. Available: ${availableAgentNames(cwd).join(", ") || "none"}.`, true);
678
+ return result(`Agent '${name}' not found. Available: ${availableAgentNames(ctx, cwd).join(", ") || "none"}.`, true);
676
679
  }
677
680
  if (mutable.length === 1)
678
681
  return mutable[0];
@@ -688,8 +691,8 @@ function resolveTarget(name, matches, cwd, scopeHint) {
688
691
  return result(`Multiple agents named '${name}' found in scope '${scope}': ${scoped.map((match) => match.filePath).join(", ")}`, true);
689
692
  return scoped[0];
690
693
  }
691
- function renamePath(currentPath, newName, scope, cwd) {
692
- if (nameExistsInScope(cwd, scope, newName, currentPath))
694
+ function renamePath(currentPath, newName, scope, ctx) {
695
+ if (nameExistsInScope(ctx, scope, newName, currentPath))
693
696
  return { error: `Name '${newName}' already exists in ${scope} scope.` };
694
697
  const filePath = path.join(path.dirname(currentPath), `${newName}.md`);
695
698
  if (fs.existsSync(filePath) && filePath !== currentPath)
@@ -1017,7 +1020,7 @@ function formatAgentDetail(agent) {
1017
1020
  }
1018
1021
  export function handleList(params, ctx) {
1019
1022
  const scope = normalizeListScope(params.agentScope) ?? "both";
1020
- const d = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
1023
+ const d = discoverCatalog(ctx, ctx.cwd, ctx.model?.provider);
1021
1024
  let scopedAgents = effectiveAgentsForScope(scope, d, ctx.runtimeAgentOwner, ctx.model?.provider);
1022
1025
  scopedAgents = scopedAgents
1023
1026
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -1074,7 +1077,7 @@ function handleModels(params, ctx) {
1074
1077
  const scope = normalizeListScope(params.agentScope);
1075
1078
  if (!scope)
1076
1079
  return result("agentScope must be 'user', 'project', or 'both' for models.", true);
1077
- const discovered = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
1080
+ const discovered = discoverCatalog(ctx, ctx.cwd, ctx.model?.provider);
1078
1081
  const effectiveAgents = effectiveAgentsForScope(scope, discovered, ctx.runtimeAgentOwner, ctx.model?.provider)
1079
1082
  .sort((a, b) => a.name.localeCompare(b.name));
1080
1083
  const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -1174,7 +1177,7 @@ function handleGet(params, ctx) {
1174
1177
  const scope = normalizeListScope(params.agentScope);
1175
1178
  if (!scope)
1176
1179
  return result("agentScope must be 'user', 'project', or 'both' for get.", true);
1177
- const discovered = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
1180
+ const discovered = discoverCatalog(ctx, ctx.cwd, ctx.model?.provider);
1178
1181
  const matches = findAgentsInDiscovery(params.agent, discovered, scope);
1179
1182
  const diagnostics = diagnosticsForScope(discovered.agentDiagnostics, scope);
1180
1183
  const rawName = params.agent.trim();
@@ -1215,11 +1218,11 @@ export function handleCreate(params, ctx) {
1215
1218
  const scope = scopeRaw;
1216
1219
  if (hasKey(cfg, "steps"))
1217
1220
  return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
1218
- const d = discoverAgentsAll(ctx.cwd);
1221
+ const d = discoverCatalog(ctx);
1219
1222
  const projectConfigDir = getProjectConfigDir(ctx.cwd);
1220
1223
  const targetDir = scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
1221
1224
  fs.mkdirSync(targetDir, { recursive: true });
1222
- if (nameExistsInScope(ctx.cwd, scope, runtimeName))
1225
+ if (nameExistsInScope(ctx, scope, runtimeName))
1223
1226
  return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
1224
1227
  const targetPath = path.join(targetDir, `${runtimeName}.md`);
1225
1228
  if (fs.existsSync(targetPath))
@@ -1269,7 +1272,7 @@ export function handleUpdate(params, ctx) {
1269
1272
  return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
1270
1273
  const warnings = [];
1271
1274
  const scopeHint = asDisambiguationScope(params.agentScope);
1272
- const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1275
+ const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx, scopeHint ?? "both"), ctx.cwd, ctx, scopeHint);
1273
1276
  if ("content" in targetOrError)
1274
1277
  return targetOrError;
1275
1278
  const target = targetOrError;
@@ -1327,7 +1330,7 @@ export function handleUpdate(params, ctx) {
1327
1330
  warnings.push(sw);
1328
1331
  }
1329
1332
  if (updated.name !== oldName) {
1330
- const renamed = renamePath(target.filePath, updated.name, target.source, ctx.cwd);
1333
+ const renamed = renamePath(target.filePath, updated.name, target.source, ctx);
1331
1334
  if (renamed.error)
1332
1335
  return result(renamed.error, true);
1333
1336
  updated.filePath = renamed.filePath;
@@ -1343,7 +1346,7 @@ function handleDelete(params, ctx) {
1343
1346
  if (!params.agent)
1344
1347
  return result("Specify 'agent' for delete.", true);
1345
1348
  const scopeHint = asDisambiguationScope(params.agentScope);
1346
- const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1349
+ const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx, scopeHint ?? "both"), ctx.cwd, ctx, scopeHint);
1347
1350
  if ("content" in targetOrError)
1348
1351
  return targetOrError;
1349
1352
  const target = targetOrError;
@@ -1360,17 +1363,17 @@ function handleEject(params, ctx) {
1360
1363
  if (parsedScope.error)
1361
1364
  return parsedScope.error;
1362
1365
  const scope = parsedScope.scope;
1363
- const d = discoverAgentsAll(ctx.cwd);
1366
+ const d = discoverCatalog(ctx);
1364
1367
  const source = [...d.package, ...d.builtin].find((a) => a.name === raw || a.name === sanitized);
1365
1368
  if (!source) {
1366
- return result(`Agent '${raw}' not found or is not a bundled/package agent. eject copies a builtin or package agent to ${scope} scope so it can be customized. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1369
+ return result(`Agent '${raw}' not found or is not a bundled/package agent. eject copies a builtin or package agent to ${scope} scope so it can be customized. Available: ${availableAgentNames(ctx).join(", ") || "none"}.`, true);
1367
1370
  }
1368
1371
  const runtimeName = source.name;
1369
1372
  const existingCustom = (scope === "user" ? d.user : d.project).find((a) => a.name === runtimeName);
1370
1373
  if (existingCustom) {
1371
1374
  return result(`Agent '${runtimeName}' is already a custom ${scope} agent at ${existingCustom.filePath}. Edit it with { action: "update", agent: "${runtimeName}" } or delete it first.`, true);
1372
1375
  }
1373
- if (nameExistsInScope(ctx.cwd, scope, runtimeName)) {
1376
+ if (nameExistsInScope(ctx, scope, runtimeName)) {
1374
1377
  return result(`An agent named '${runtimeName}' already exists in ${scope} scope. Remove or rename it first.`, true);
1375
1378
  }
1376
1379
  const projectConfigDir = getProjectConfigDir(ctx.cwd);
@@ -1400,7 +1403,7 @@ function handleDisable(params, ctx) {
1400
1403
  if (parsedScope.error)
1401
1404
  return parsedScope.error;
1402
1405
  const scope = parsedScope.scope;
1403
- const d = discoverAgentsAll(ctx.cwd);
1406
+ const d = discoverCatalog(ctx);
1404
1407
  if (scope === "project" && d.projectSettingsPath === null) {
1405
1408
  return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1406
1409
  }
@@ -1408,11 +1411,11 @@ function handleDisable(params, ctx) {
1408
1411
  if (effective.error)
1409
1412
  return result(effective.error, true);
1410
1413
  if (!effective.agent) {
1411
- return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1414
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx).join(", ") || "none"}.`, true);
1412
1415
  }
1413
1416
  const runtimeName = effective.agent.name;
1414
1417
  const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
1415
- const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1418
+ const after = resolveEffectiveAgent(discoverCatalog(ctx), raw).agent;
1416
1419
  if (after?.disabled === true) {
1417
1420
  ctx.onAgentsChanged?.();
1418
1421
  return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
@@ -1427,7 +1430,7 @@ function handleEnable(params, ctx) {
1427
1430
  if (parsedScope.error)
1428
1431
  return parsedScope.error;
1429
1432
  const scope = parsedScope.scope;
1430
- const d = discoverAgentsAll(ctx.cwd);
1433
+ const d = discoverCatalog(ctx);
1431
1434
  if (scope === "project" && d.projectSettingsPath === null) {
1432
1435
  return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1433
1436
  }
@@ -1435,11 +1438,11 @@ function handleEnable(params, ctx) {
1435
1438
  if (effective.error)
1436
1439
  return result(effective.error, true);
1437
1440
  if (!effective.agent) {
1438
- return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1441
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx).join(", ") || "none"}.`, true);
1439
1442
  }
1440
1443
  const runtimeName = effective.agent.name;
1441
1444
  const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
1442
- const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
1445
+ const after = resolveEffectiveAgent(discoverCatalog(ctx), raw).agent;
1443
1446
  if (after && after.disabled !== true) {
1444
1447
  if (removed)
1445
1448
  ctx.onAgentsChanged?.();
@@ -1461,7 +1464,7 @@ function handleReset(params, ctx) {
1461
1464
  if (parsedScope.error)
1462
1465
  return parsedScope.error;
1463
1466
  const scope = parsedScope.scope;
1464
- const d = discoverAgentsAll(ctx.cwd);
1467
+ const d = discoverCatalog(ctx);
1465
1468
  if (scope === "project" && d.projectSettingsPath === null) {
1466
1469
  return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
1467
1470
  }
@@ -1471,7 +1474,7 @@ function handleReset(params, ctx) {
1471
1474
  if (custom) {
1472
1475
  return result(`Agent '${raw}' has no bundled default to reset to. Use { action: "delete", agent: "${custom.name}" } to remove the custom ${custom.source} agent.`, true);
1473
1476
  }
1474
- return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1477
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx).join(", ") || "none"}.`, true);
1475
1478
  }
1476
1479
  const runtimeName = bundled.name;
1477
1480
  const custom = (scope === "user" ? d.user : d.project).find((a) => a.name === raw || a.name === sanitized);