pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
package/docs/watchdog.md CHANGED
@@ -7,10 +7,11 @@ The watchdog is an opt-in second model that reviews what the agent just did and
7
7
  | Timing | Trigger | Gate | Delivery |
8
8
  |---|---|---|---|
9
9
  | Boundary review | `agent_end` of every main or child turn | Repo changed | Steered into the transcript; the agent gets one continuation, then that turn is reviewed again |
10
+ | Main activity review | `agent_end`, with `clarification: true` | New delivered orchestration evidence; at most one additional review per user prompt | Same warning/clarification path, even without local edits |
10
11
  | Cadence review | Every `cadence.everyNTools` tool results, minimum 5 | Opt-in | Steered after the current tool, before the next step |
11
12
  | LSP pre-pass | Before boundary review | Changed TypeScript/JavaScript files | Diagnostics become watchdog findings without a model call |
12
13
 
13
- Boundary reviews coalesce a turn's edits into one final-state review. Unchanged or reverted diffs are skipped, as are `.pi/subagents/` and `tmp/` artifacts. In orchestrated runs, each writing child reviews its own worktree and the parent reviews the aggregate diff after child changes land. There is no timer or "every turn regardless of edits" mode; the closest is a low cadence such as `everyNTools: 5`. Cadence monitoring is inspired by [Scopey](https://github.com/ArchAstro/scopey).
14
+ Boundary reviews coalesce a turn's edits into one final-state review. Unchanged or reverted diffs are skipped unless the main-only activity opt-in below admits new evidence; `.pi/subagents/` and `tmp/` artifacts remain excluded. In orchestrated runs, each writing child reviews its own worktree and the parent reviews the aggregate diff after child changes land. There are no idle timer reviews. Cadence monitoring is inspired by [Scopey](https://github.com/ArchAstro/scopey).
14
15
 
15
16
  Children get the same boundary, cadence, and LSP behavior. Child cadence resolves from `children.overrides.<agent>.cadence`, then `children.cadence`, then top-level `cadence`:
16
17
 
@@ -67,7 +68,7 @@ Child watchdog findings are lifted into the parent in three ways:
67
68
  ## What the reviewer is given
68
69
 
69
70
  - **Turn delta** with changed repo paths. Over-long input keeps the first 6,000 characters and the tail.
70
- - **Current scope** (`scope.enabled`, default on): bounded real user prompts, with newer prompts superseding older ones.
71
+ - **Current scope** (`scope.enabled`, default on): bounded real user prompts. Side questions are additive; only explicit changes supersede older requirements.
71
72
  - **`watchdog_diff`** when inside git: diff since the session-start commit, including later commits, plus untracked paths to inspect with `read`; accepts `path` and `stat:true`.
72
73
  - **`WATCHDOG.md`** standing instructions, read fresh on every review: `<project>/.pi/WATCHDOG.md` first, then `~/.pi/agent/WATCHDOG.md`, capped at 8,000 characters. Set `guidance.watchdogMd: false` to ignore them.
73
74
  - **LSP diagnostics** from `typescript-language-server`, auto-detected in `node_modules/.bin` or `PATH`; it is never installed and never run over the whole workspace. Errors become blockers, warnings concerns, and info/hints stay in status.
@@ -87,7 +88,9 @@ One model setting serves both boundary and cadence reviews per endpoint. Use a s
87
88
  /subagents-watchdog on
88
89
  ```
89
90
 
90
- The recommendation is Opus 4.8 or GPT 5.5 at thinking high, whichever your main session is not using and is authenticated. Saving a model does not enable the watchdog; use `on` separately.
91
+ When a main watchdog model is configured (including a session override), recommendations keep that model and its effective thinking level rather than judging its strength or independence. An unavailable or unauthenticated configured model is reported, not replaced. Without a configured main model, the recommendation remains Opus 4.8 or GPT 5.5 at thinking high, whichever your main session is not using and is authenticated.
92
+
93
+ `session model recommended` changes only this session. `model recommended` explicitly saves the recommendation to **user settings**, affecting other projects without overrides; it does not change project settings. Project and session overrides still take precedence. Use an explicit model to replace a configured choice. Saving a model does not enable the watchdog; use `on` separately.
91
94
 
92
95
  ```json
93
96
  {
@@ -105,11 +108,39 @@ The recommendation is Opus 4.8 or GPT 5.5 at thinking high, whichever your main
105
108
 
106
109
  Omit `main.model` to inherit the session model and thinking level. A `main.model` without a thinking suffix or `main.thinking` runs with thinking off, so prefer `:high` for the strong pairing.
107
110
 
111
+ Set `fallbackModels` in JSON settings on `main`, `children`, or `children.overrides.<agent>` to opt into an ordered fallback chain, for example `"fallbackModels": ["openai-codex/gpt-5.5:high"]`. Child overrides win over `children.fallbackModels`; neither inherits the main watchdog's chain. Arrays replace across user → project → session settings, and `[]` clears an inherited chain. Status shows configured chains.
112
+
113
+ Unavailable configured candidates are skipped and resolved duplicates are tried once. Each attempt uses a fresh reviewer with its own model auth, provider stream, and thinking; an inherited primary keeps the actual session model/thinking, while fallbacks use explicit-model thinking rules. Fallback follows normal subagent provider-failure semantics (including rate limits, quota, auth, unavailability, and provider timeouts), **only before any tool work**, including read-only inspection. Clean/normal completion, length limits, findings, clarification, cancellation, and the overall watchdog deadline never trigger fallback. All attempts share the original deadline; exhaustion remains a failed review. With no fallback chain, existing single-model behavior is unchanged.
114
+
108
115
  Agents can call `subagent({ action: "watchdog.recommend-model" })` and `subagent({ action: "watchdog.configure", model: "recommended", scope: "session" | "user" | "project" })`. They should use `scope: "session"` unless you ask for a lasting default.
109
116
 
117
+ ## Optional main-session clarification
118
+
119
+ Use `watchdog_warn` directly for evidence-backed reminders of forgotten authorized work; a question is not a prerequisite. Distinguish forgotten work from dependencies still pending or explicit holds. Use clarification when task status or intent is genuinely unclear. The orchestrator remains owner of its task/lane board.
120
+
121
+ With this opt-in, completed `turn_end` events retain a recent actual-activity tail: paired calls/results for `subagent` dispatch (no action), `subagent` actions `status`, `resume`, `interrupt`, `steer`, `stop`, `bg_wait`, and `subagent_supervisor` actions `pending`, `list`, `reply`. Pairing requires the same tool name and exact tool-call ID; raw results, unrelated tool names and watchdog management actions do not qualify. Each activity entry is bounded to 3,000 characters, with a 6,000-character recent tail retained across ordinary new prompts and skipped edit boundaries. Session replacement, compaction, shutdown or disabling clears it. This is observed text, not an inferred task board.
122
+
123
+ New unreviewed activity permits at most one additional boundary review per user prompt even with no local edit. Side questions keep earlier authorized task evidence available; they do not themselves trigger a model call. Activity gathered after that prompt's extra review remains available for the next prompt. Warning continuations cannot supply fresh triggering activity. No polling, task scheduling, cross-worktree scans or idle calls are added.
124
+
125
+ **Visibility limit:** external task/gate completions are visible when returned through those parent tool results. Standalone native completion notifications, arbitrary custom messages, direct shell/CI output, and events not delivered to the parent through these contracts are not ingested by this activity tail. Existing scope retains at most eight prompts (2,000 characters each); new streaming user input cancels an active review but is not added to scope unless `before_agent_start` fires. Reminders depend on retained evidence and model judgment, not an exhaustive view of running work.
126
+
127
+ Set `subagents.watchdog.clarification: true` in Pi settings alongside `enabled: true`. It defaults to `false` and applies **only to the main watchdog**, not child watchdogs or child permission arbitration.
128
+
129
+ ```json
130
+ { "subagents": { "watchdog": { "enabled": true, "clarification": true } } }
131
+ ```
132
+
133
+ At an eligible activity or repo-edit boundary, the reviewer may use `watchdog_ask` for one focused question when missing orchestrator context prevents a concrete judgment. It cannot ask during cadence reviews, after an accepted warning, or during stalemate. There is at most one question per real user prompt.
134
+
135
+ The tool **yields and ends that review**. A visible question with concrete evidence steers the main session into Pi's native automatic continuation after the boundary hook returns. The orchestrator handles the context as needed and continues; no answer, receipt, deadline or follow-up review is required or tracked. Questions are **not approval, permission, or warnings**.
136
+
137
+ Asking consumes the current review evidence, so an unchanged Git-backed boundary does not immediately review it again. Later reviews use the normal edit, cadence and bounded activity triggers, with the same read-only tools, warning thresholds, budgets and stalemate protections. Non-Git observed edits can also prompt a question: there is no cross-answer evidence guarantee to verify. Disabling watchdog or clarification, new user input, model changes and session lifecycle resets cancel applicable active reviews and suppress stale results; already delivered questions remain ordinary transcript messages.
138
+
139
+ Reviews retain the existing `agentEndTimeoutMs`. Questions and evidence are capped at 1,000 and 2,000 characters; scope, activity and delta share the existing 24,000-character input limit. Enabled cost adds at most one activity boundary review and one question-triggered continuation per prompt, not a dedicated answer review. Disabled execution does not collect activity or add polling, model calls or reviewer prompt/tool content. Child warning messaging and permission decisions remain unchanged.
140
+
110
141
  ## Child watchdogs
111
142
 
112
- Opt in under `subagents.watchdog.children`. `model` and `thinking` set the default child watchdog; `overrides.<agent>` can set `model`, `thinking`, `enabled`, or `cadence` per role.
143
+ Opt in under `subagents.watchdog.children`. `model`, `fallbackModels`, and `thinking` set the default child watchdog; `overrides.<agent>` can set `model`, `fallbackModels`, `thinking`, `enabled`, or `cadence` per role.
113
144
 
114
145
  ## Launch rules
115
146
 
package/docs/workflows.md CHANGED
@@ -45,6 +45,10 @@ Add `autofix` to `/parallel-review` or `/parallel-cleanup` to apply only the syn
45
45
 
46
46
  Use direct `{ agent, task }` for one bounded child. Use `workflowScript` when the parent needs a stable keyed child, sequence, fanout, steering, retry, or aggregation. For ordinary parallel fanout, use `await runs.all([{ key, agent, task }, ...])`. It resolves to an ordered array, not a key map, so use indexes, destructuring, or `.map(...)`, not `results.<key>`. Do not read `.output` from unawaited `runs.run` launches. Store a `runs.run` promise only when the script later observes it with `await`, `Promise.race`, or `Promise.all`, such as steering a live child before awaiting its result. Scripts are ordinary JavaScript statement bodies. Use an explicit `return` for a useful result:
47
47
 
48
+ For multi-step or parallel work, make exactly one top-level `subagent` workflow call with `async:true` and launch children only inside it. Read this guide for recipes rather than constructing a second top-level orchestration. Available sandbox helpers include `runs.run`, `runs.all`, `runs.lanes`, `runs.steer`, `runs.status`, `runs.ref`/`runs.refs`, `emit`, `console`, standard JavaScript, and mission `state` when enabled. No filesystem, shell, arbitrary Pi tools, or host globals are available; named resources alone may grant `runs.host` authority.
49
+
50
+ Workflow-level child controls default onto each `runs.run`/`runs.all` launch; explicit child fields override them. See [retained children](tool-reference.md#retained-children) for follow-up challenges, [output binding](tool-reference.md#output-mode-details) for durable artifacts, and [schedules](missions.md#schedules) for delayed/recurring scripts.
51
+
48
52
  Child results cross into the script as plain JSON data. Non-JSON host metadata is omitted, so use returned fields such as `runId`, `ok`, `output`, and `structuredOutput` for workflow control.
49
53
 
50
54
  Validate a script without launching children:
@@ -96,17 +100,18 @@ subagent({
96
100
  - `toolBudget` becomes the default for each child unless that child supplies a narrower value.
97
101
  - `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.
98
102
  - 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.
103
+ - 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 }`.
99
104
 
100
105
  These controls are opt-in. Avoid tight hard budgets for mutation-capable workers unless the workflow has an explicit checkpoint and handoff path.
101
106
 
102
- 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.
107
+ 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.
103
108
 
104
109
  ```js
105
110
  subagent({ workflowScript: `
106
- const scan = await runs.run("scan", { agent: "scout", task: "Scan the codebase" });
111
+ const scan = await runs.run("scan", { label: "Map codebase behavior", agent: "scout", task: "Scan the codebase" });
107
112
  const reviews = await runs.all([
108
- { key: "correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
109
- { key: "tests", agent: "reviewer", task: "Review tests: " + scan.output }
113
+ { key: "correctness", label: "Review codebase correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
114
+ { key: "tests", label: "Review test coverage", agent: "reviewer", task: "Review tests: " + scan.output }
110
115
  ]);
111
116
  return reviews.map(result => result.output);
112
117
  ` });
@@ -117,7 +122,7 @@ Keep helper functions portable across Node and Bun. Use top-level `await`, plain
117
122
  ```js
118
123
  subagent({ workflowScript: `
119
124
  function scan() {
120
- return runs.run("scan", { agent: "scout", task: "Scan the codebase" });
125
+ return runs.run("scan", { label: "Map codebase behavior", agent: "scout", task: "Scan the codebase" });
121
126
  }
122
127
  const result = await scan();
123
128
  return result.output;
@@ -128,8 +133,8 @@ Chaining is still supported. The supported form is scripted chaining: await one
128
133
 
129
134
  ```js
130
135
  subagent({ workflowScript: `
131
- const plan = await runs.run("plan", { agent: "scout", task: "Plan the migration" });
132
- const patch = await runs.run("patch", { agent: "worker", task: "Implement this plan:\n" + plan.output });
136
+ const plan = await runs.run("plan", { label: "Plan migration behavior", agent: "scout", task: "Plan the migration" });
137
+ const patch = await runs.run("patch", { label: "Implement migration behavior", agent: "worker", task: "Implement this plan:\n" + plan.output });
133
138
  return patch.output;
134
139
  ` });
135
140
  ```
@@ -144,16 +149,16 @@ subagent({ workflowScript: `
144
149
  {
145
150
  key: "api",
146
151
  stages: [
147
- { key: "writer", agent: "worker", task: "Implement the API change" },
148
- { key: "challenge", resume: "previous", task: "Challenge the API implementation" },
149
- { key: "review", agent: "reviewer", task: "Review the API lane" }
152
+ { key: "writer", label: "Implement API behavior", agent: "worker", task: "Implement the API change" },
153
+ { key: "challenge", label: "Challenge API behavior", resume: "previous", task: "Challenge the API implementation" },
154
+ { key: "review", label: "Review API behavior", agent: "reviewer", task: "Review the API lane" }
150
155
  ]
151
156
  },
152
157
  {
153
158
  key: "ui",
154
159
  stages: [
155
- { key: "writer", agent: "worker", task: "Implement the UI change" },
156
- { key: "review", agent: "reviewer", task: "Review the UI lane" }
160
+ { key: "writer", label: "Implement UI behavior", agent: "worker", task: "Implement the UI change" },
161
+ { key: "review", label: "Review UI behavior", agent: "reviewer", task: "Review the UI lane" }
157
162
  ]
158
163
  }
159
164
  ]);
@@ -202,7 +207,7 @@ subagent({ workflowScript: `
202
207
  ` });
203
208
  ```
204
209
 
205
- The receipt state is `queued`, `delivered`, `missed`, or `failed`. `delivered` means the child Pi session accepted the input. It does not mean the model followed it. `missed` means the keyed child became terminal or had no live route before delivery. This first slice uses the existing foreground and async steering transports but does not start steering recovery. Workflow traces include one steering attempt entry and one receipt entry.
210
+ The receipt state is `queued`, `delivered`, `missed`, or `failed`. For an async child, `delivered` means it consumed the correlated user input; for a foreground child, it means the in-process Pi transport accepted the input. It does not mean the model followed it. `missed` means the keyed child became terminal or had no live route before delivery. This first slice uses the existing foreground and async steering transports but does not start steering recovery. Workflow traces include one steering attempt entry and one receipt entry.
206
211
 
207
212
  Always await or return a `runs.steer` promise. The workflow waits for an observed steering side effect to settle before it exits and rejects fire-and-forget calls. Use ordinary `Promise.race` when the first child or steering receipt should advance the script. There is no callback API or child inbox access.
208
213
 
@@ -355,6 +360,8 @@ known, or for explicit emergency hotfix lanes.
355
360
 
356
361
  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.
357
362
 
363
+ 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.
364
+
358
365
  The legacy `/chain`, `/parallel`, and `/run-chain` commands are not registered.
359
366
 
360
367
  ## Direct commands
@@ -377,10 +384,14 @@ Each child uses the existing worktree lifecycle: it branches from clean HEAD, jo
377
384
 
378
385
  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.
379
386
 
380
- Use `baseRef` to branch managed worktrees from a named commit or branch instead of the default `HEAD`. For example, `{ workflowScript, worktree: true, baseRef: "refs/heads/release" }` applies the release ref to children unless a child supplies its own `baseRef`. The source checkout must still be clean, and the ref must resolve to a commit before any worktree is allocated.
387
+ Before a materialized `runs.run` or `runs.all` group dispatches fresh children, isolated sources must be Git repositories with clean working trees (excluding `.pi/subagents/` runtime state). A rejected group dispatches no children and spends no fan-out slots or child output claims; key-level failure traces can remain. Checks are shared only within that group, are cancellable, and run again at allocation because sources can change. Retained resumes keep their stored contracts. Select the correct cwd or arrange an operator-approved commit/stash; isolation is never dropped automatically.
388
+
389
+ 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.
381
390
 
382
391
  Configure the worktree provider, native path layout, base directory, and setup hook in [configuration.md](configuration.md).
383
392
 
393
+ 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).
394
+
384
395
  ### Lane metadata lifecycle
385
396
 
386
397
  Workflow children may declare a bounded `lane` object (`version`, `key`, optional
@@ -402,11 +413,13 @@ Older runs without lane metadata remain readable and retain their existing
402
413
  handoff/cleanup behavior. Missing lane, receipt, or handoff metadata is
403
414
  unknown—not eligible for destructive cleanup.
404
415
 
405
- For managed worktree launches, the runner writes the pending handoff and the
406
- display-only status path/branch from the deterministic setup plan before the
407
- first `git worktree add`. If setup then fails or is interrupted, that pending
408
- ownership record remains preserved evidence; cleanup still rechecks the actual
409
- worktree state before any removal.
416
+ Managed setup records actual allocation attempts in the handoff; only validated
417
+ allocations become cleanup tasks and display-only status paths/branches. On
418
+ cancellation or failure with unknown settlement, it retains actual/attempted
419
+ ownership evidence and artifacts for manual reconciliation, blocking further
420
+ unsafe setup and cleanup in that process. An allocator interrupted before
421
+ reporting its path may leave branch-only diagnostics, never an invented path.
422
+ Inspect the handoff before reconciliation; cleanup still requires fresh checks.
410
423
 
411
424
  ## Supervisor coordination (child asks parent)
412
425
 
@@ -432,6 +445,14 @@ Children should not ask for clarification when the only conflict is review-only/
432
445
 
433
446
  The parent replies with `subagent_supervisor({ action: "reply", replyTo, message })` or checks pending requests with `subagent_supervisor({ action: "pending" })`. Supervisor messages are scoped to the exact Pi session id that spawned the child. A second Pi session in the same repository does not receive those requests.
434
447
 
448
+ A nested coordinator needs both directions of coordination. If its agent declares an explicit `tools` allowlist, include `subagent_supervisor` to answer its own children, alongside `subagent` for delegation and `contact_supervisor` for asking its parent:
449
+
450
+ ```yaml
451
+ tools: read, subagent, contact_supervisor, subagent_supervisor
452
+ ```
453
+
454
+ For A → B → C, C's request belongs to B, not A. B can escalate a separate question to A with `contact_supervisor`, then answer C using C's original `replyTo` request id. A's reply to B does not resolve C's request, and steering is not a substitute for replying. Only fanout-authorized children get the downward supervisor provider; explicit tool exclusions and capability ceilings still apply, and ordinary leaves do not gain delegation or reply tools. Requesting `subagent_supervisor` without fanout authorization fails at launch with an actionable error. A coordinator that excludes the reply tool does not start downward supervision or receive prompts to use it. Explicitly selected native coordination tools survive host-builtin filtering because their providers are child runtime hooks, not host builtins.
455
+
435
456
  Child-side routine completion handoffs are not expected. If a child appears stalled, needs-attention notices show up in the parent session with useful next actions, such as checking `subagent({ action: "status" })`, interrupting the run, or nudging the child.
436
457
 
437
458
  If a `workflowScript` child detaches through `contact_supervisor`, the enclosing async workflow stays `paused` until that child exits. Then the extension reconciles it to `complete` or `failed`. Wait on the child until that happens.
@@ -1,11 +1,11 @@
1
1
  import { createJiti } from "jiti";
2
2
 
3
3
  const jiti = createJiti(import.meta.url);
4
- const { runInspector } = await jiti.import("./src/inspectors/herdr/inspector-runner.ts");
4
+ const { runInspector } = await jiti.import("./src/inspectors/inspector-runner.ts");
5
5
 
6
6
  try {
7
7
  runInspector();
8
8
  } catch (cause) {
9
- process.stderr.write(`Herdr inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
9
+ process.stderr.write(`Inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
10
10
  process.exitCode = 1;
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.65.1",
3
+ "version": "0.67.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",
@@ -28,7 +28,7 @@ Choose or adapt angles when the work calls for it:
28
28
 
29
29
  Prefer three strong reviewers over many vague reviewers.
30
30
 
31
- Give every reviewer a specific task prompt naming its angle. Ask reviewers to return concise, evidence-backed findings with file/line references and suggested fixes. Filter on evidence, not severity: a finding must be concrete, current, caused or made reachable by the target diff, and supported by source proof, a test or repro, or a contract contradiction. Label findings P0/P1/P2. P0 blocks merge. P1 should be fixed before release. P2 is report-only. End each review with `Merge verdict: BLOCK`, `Merge verdict: OK`, or `Merge verdict: OK with notes`. If nothing qualifies, ask the reviewer to say exactly `No issues found.` The response should be review feedback, not a context summary. Reviewers must not edit files unless I explicitly ask for a writer pass.
31
+ Give every reviewer a specific task prompt naming its angle. Ask reviewers to return concise, evidence-backed findings with file/line references and suggested fixes. Filter on evidence, not severity: a finding must be concrete and current within the named review target, and supported by source proof, a test or repro, or a contract contradiction. For a diff review, require that the issue is caused or made reachable by that diff. Label findings P0/P1/P2. P0 blocks merge. P1 should be fixed before release. P2 is report-only. End each review with `Merge verdict: BLOCK`, `Merge verdict: OK`, or `Merge verdict: OK with notes`. If nothing qualifies, ask the reviewer to say exactly `No issues found.` The response should be review feedback, not a context summary. Reviewers must not edit files unless I explicitly ask for a writer pass.
32
32
 
33
33
  Do not default first-pass reviews to `blockers only`. That phrase is valid only for final pre-merge re-checks after P1/P2 findings are already inventoried, or for explicit emergency hotfix lanes where non-blocking findings are intentionally deferred.
34
34
 
@@ -1,11 +1,16 @@
1
- // Only loaded when the parent supplies Pi 0.85.0's missing server exports.
2
1
  import { registerHooks } from "node:module";
3
2
  import { pathToFileURL } from "node:url";
4
3
 
5
- const aliases = JSON.parse(process.env.JITI_ALIAS);
4
+ const aliases = JSON.parse(process.env.JITI_ALIAS ?? "{}");
5
+ const redirected = new Set([
6
+ "@earendil-works/pi-server",
7
+ "@earendil-works/pi-server/unix",
8
+ "@earendil-works/pi-tui",
9
+ ]);
10
+
6
11
  registerHooks({
7
12
  resolve(specifier, context, nextResolve) {
8
- if (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix") {
13
+ if (redirected.has(specifier) && aliases[specifier]) {
9
14
  return nextResolve(pathToFileURL(aliases[specifier]).href, context);
10
15
  }
11
16
  return nextResolve(specifier, context);
@@ -50,6 +50,18 @@ use ordinary `runs.run(...)` / `runs.all(...)`. See the [canonical staged-lane
50
50
  example](../../docs/workflows.md#parallel-sequential-lanes). Keep assignments
51
51
  bounded, but do not add stages or ceremony just to satisfy this skill.
52
52
 
53
+ When composing `runs.run(...)`, `runs.all(...)`, or `runs.lanes(...)`, always
54
+ supply a short verb + behavior display `label` derived from the task, unless
55
+ the user supplied an explicit label; preserve that label. Keep the stable
56
+ machine `key` independent (for example, `issue2011-writer` with
57
+ `label: "Fix workflow steering"`). For `runs.lanes`, put labels on stage
58
+ items, not lane objects. Use stage-appropriate labels for reviews and retained-child
59
+ follow-ups too (for example, `Review workflow steering`). Generate labels in
60
+ the orchestrator while composing the launch—no extra model call, runtime
61
+ generator, or schema change. Native direct `{ agent, task }` calls have no
62
+ top-level `label` parameter; do not invent one or wrap a tiny single task in
63
+ a workflow just to label it.
64
+
53
65
  Use async/background by default. Set `async:false` only when the parent must
54
66
  block. Final reviews, validation gates, oracle checks, and publication checks
55
67
  stay async.
@@ -71,6 +83,8 @@ that runner explicitly supports the option.
71
83
 
72
84
  ## Read the reference for the branch
73
85
 
86
+ For exact API fields and worked examples, call `subagent({action:"guide",topic:"tool-reference"})` or `topic:"workflows"`. The compact tool definition is not the recipe catalog; use `topic:"missions"` for mission updates and schedules.
87
+
74
88
  | Branch | Read |
75
89
  | --- | --- |
76
90
  | Delegate or choose roles, prompts, models, or slash commands | `references/prompting-and-roles.md` |
@@ -83,10 +83,10 @@ lanes, or a fanout that the parent will consume together.
83
83
  ```js
84
84
  subagent({
85
85
  workflowScript: `
86
- const scan = await runs.run("scan", { agent: "scout", task: "Map the target" });
86
+ const scan = await runs.run("scan", { label: "Map target behavior", agent: "scout", task: "Map the target" });
87
87
  const reviews = await runs.all([
88
- { key: "correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
89
- { key: "tests", agent: "reviewer", task: "Review tests: " + scan.output }
88
+ { key: "correctness", label: "Review target correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
89
+ { key: "tests", label: "Review target test coverage", agent: "reviewer", task: "Review tests: " + scan.output }
90
90
  ]);
91
91
  return reviews.map(result => result.output);
92
92
  `
@@ -109,6 +109,7 @@ Terminal async workflows also persist `workflow-receipt.json` beside `status.jso
109
109
 
110
110
  ```js
111
111
  return runs.run("cross-oracle", {
112
+ label: "Challenge proposed direction",
112
113
  resume: { workflowRunId: "<pass-1-workflow-id>", key: "advisor-oracle", latest: true },
113
114
  task: "Review the focused challenge packet."
114
115
  });
@@ -120,7 +121,8 @@ Keyed resume reads that one exact receipt and revalidates the retained run at la
120
121
 
121
122
  For a broad plan with a known set of narrow, visible stages per lane, use
122
123
  `runs.lanes(...)` inside a `workflowScript`; it is a nested helper, not a
123
- top-level `subagent` mode. Give each lane and stage a stable key. The first
124
+ top-level `subagent` mode. Give each lane and stage a stable key; give stage
125
+ items a short verb + behavior `label`, preserving explicit user labels. The first
124
126
  stage from every lane is launched together, then later stages sequence per lane.
125
127
  `resume: "previous"` requires the retained predecessor, and a failed or blocked
126
128
  stage blocks only that lane. The returned board exposes lane/stage results for
@@ -258,7 +260,7 @@ A cooperating terminal runtime can register read-only external records through `
258
260
 
259
261
  ### Scheduled subagent runs
260
262
 
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.
263
+ 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>/`.
262
264
 
263
265
  ```typescript
264
266
  // One-shot reviewer
@@ -327,6 +329,19 @@ subagent({ action: "steer", id: "abc123", message: "Focus on the failing test."
327
329
 
328
330
  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.
329
331
 
332
+ Steering supports three delivery modes via the `mode` parameter (`steer` is the default):
333
+
334
+ - `mode: "steer"` — interrupt the child at the next safe point of its current turn and deliver the message.
335
+ - `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.
336
+ - `mode: "auto"` — same next-safe-point delivery path as `steer`, but without the automatic pause-and-revive recovery after a missed acknowledgment.
337
+
338
+ ```typescript
339
+ subagent({ action: "steer", id: "abc123", mode: "follow_up", message: "After this step, also validate the config file." })
340
+ subagent({ action: "steer", id: "abc123", mode: "auto", message: "Switch to the failing test now." })
341
+ ```
342
+
343
+ For async runs, `delivered` records that the child consumed the correlated user input; foreground `delivered` records in-process transport acceptance. Neither is proof of model compliance. A live foreground follow-up acknowledgment reports `queued`, meaning Pi accepted it into its follow-up queue, not that it was delivered. The foreground transport does not provide a later correlated queued-to-delivered receipt.
344
+
330
345
  ## Watchdog
331
346
 
332
347
  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`
@@ -63,7 +63,7 @@ Council advisors are read-only. User or project `council-*` profiles choose allo
63
63
 
64
64
  ### Parallel review technique
65
65
 
66
- Use this when the user wants adversarial review of a diff, plan, issue, file, or implemented work. Launch fresh-context `reviewer` agents with distinct angles generated from the actual target. Common angles are correctness/regressions, tests/validation, and simplicity/maintainability; adapt for TypeScript, UI, security, docs, or large structural changes. Reviewers should inspect files and diffs directly, return concise evidence-backed findings with file/line references, and avoid edits unless the user explicitly asks for a writer pass. Filter on evidence, not severity: report only concrete current issues caused or made reachable by the target diff, with source proof, a test or repro, or a contract contradiction. Label findings P0/P1/P2 and end with `Merge verdict: BLOCK`, `Merge verdict: OK`, or `Merge verdict: OK with notes`. Use `blockers only` only for final pre-merge re-checks after P1/P2 findings are already captured, or for explicit emergency hotfix lanes where non-blocking findings are intentionally deferred. For targeted follow-up, ask only whether the named finding was resolved, whether the fix introduced a new defect in the fix blast radius, and whether prior P1/P2 notes still stand. For bot or PR-comment triage, classify each comment as VALID, STALE, INVALID, or OUT-OF-POLICY against current HEAD, then assign P0/P1/P2 only to VALID comments. The parent synthesizes fixes worth doing now, optional improvements, and feedback to ignore/defer before applying anything.
66
+ Use this when the user wants adversarial review of a diff, plan, issue, file, or implemented work. Launch fresh-context `reviewer` agents with distinct angles generated from the actual target. Common angles are correctness/regressions, tests/validation, and simplicity/maintainability; adapt for TypeScript, UI, security, docs, or large structural changes. Reviewers should inspect files and diffs directly, return concise evidence-backed findings with file/line references, and avoid edits unless the user explicitly asks for a writer pass. Filter on evidence, not severity: report concrete current issues within the named review target, with source proof, a test or repro, or a contract contradiction. For a diff review, require that the issue is caused or made reachable by that diff. Label findings P0/P1/P2 and end with `Merge verdict: BLOCK`, `Merge verdict: OK`, or `Merge verdict: OK with notes`. Use `blockers only` only for final pre-merge re-checks after P1/P2 findings are already captured, or for explicit emergency hotfix lanes where non-blocking findings are intentionally deferred. For targeted follow-up, ask only whether the named finding was resolved, whether the fix introduced a new defect in the fix blast radius, and whether prior P1/P2 notes still stand. For bot or PR-comment triage, classify each comment as VALID, STALE, INVALID, or OUT-OF-POLICY against current HEAD, then assign P0/P1/P2 only to VALID comments. The parent synthesizes fixes worth doing now, optional improvements, and feedback to ignore/defer before applying anything.
67
67
 
68
68
  ### Proactive skill-specialist technique
69
69
 
@@ -283,7 +283,7 @@ Keep the parent/orchestrator on the ordinary strong default model because omissi
283
283
 
284
284
  Examples are illustrative, not requirements. Map these tiers to concrete models in user/project settings or a profile. A non-OpenAI setup should choose comparable available models by capability.
285
285
 
286
- Use `fallbackModels` when a tier has provider quota or availability risk. Prefer fresh context for cross-provider children when inherited provider-specific reasoning blocks would force thinking off.
286
+ Use `fallbackModels` when a tier has provider quota or availability risk. Forked children keep their requested thinking level even when provider-specific reasoning blocks are stripped from the inherited transcript.
287
287
 
288
288
  If a provider rejects model IDs with thinking suffixes, use
289
289
  `subagents.disableThinking: true` in user or project settings to clear bundled
@@ -0,0 +1,94 @@
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
+ export function appendAdvertisedAgentPrompt(systemPrompt: string[], advertisedPrompt: string | undefined): string[];
62
+ export function appendAdvertisedAgentPrompt(systemPrompt: undefined, advertisedPrompt: string | undefined): string | undefined;
63
+ export function appendAdvertisedAgentPrompt(
64
+ systemPrompt: string | string[] | undefined,
65
+ advertisedPrompt: string | undefined,
66
+ ): string | string[] | undefined;
67
+ export function appendAdvertisedAgentPrompt(
68
+ systemPrompt: string | string[] | undefined,
69
+ advertisedPrompt: string | undefined,
70
+ ): string | string[] | undefined {
71
+ if (Array.isArray(systemPrompt)) {
72
+ let changed = false;
73
+ const cleaned = systemPrompt
74
+ .map((part) => {
75
+ if (typeof part !== "string") return part;
76
+ const stripped = part.replace(ADVERTISED_AGENTS_BLOCK, "");
77
+ if (stripped !== part) changed = true;
78
+ return stripped;
79
+ })
80
+ .filter((b) => typeof b === "string" && b.length > 0);
81
+
82
+ if (advertisedPrompt) {
83
+ return [...cleaned, advertisedPrompt];
84
+ }
85
+ return changed ? cleaned : systemPrompt;
86
+ }
87
+
88
+ if (typeof systemPrompt === "string") {
89
+ const base = systemPrompt.replace(ADVERTISED_AGENTS_BLOCK, "");
90
+ return advertisedPrompt ? (base.trim() ? `${base.trimEnd()}\n\n${advertisedPrompt}` : advertisedPrompt) : base;
91
+ }
92
+
93
+ return advertisedPrompt;
94
+ }
@@ -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