pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
11
  import {
11
12
  formatUnresolvedMcpDirectToolSelectors,
12
13
  resolveMcpDirectToolResolution,
@@ -21,6 +22,7 @@ import {
21
22
  import { THINKING_LEVELS } from "../../shared/model-info.ts";
22
23
  import { getAgentDir } from "../../shared/utils.ts";
23
24
  import type { PermissionRules } from "./permissions.ts";
25
+ import { snapshotRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
24
26
  import {
25
27
  capabilityCeilingAgentRestrictionSources,
26
28
  intersectSubagentCapabilityCeilings,
@@ -32,18 +34,18 @@ import {
32
34
  const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
33
35
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
34
36
  path.dirname(fileURLToPath(import.meta.url)),
35
- "subagent-prompt-runtime.ts",
37
+ `subagent-prompt-runtime${path.extname(fileURLToPath(import.meta.url))}`,
36
38
  );
37
39
  const FANOUT_CHILD_EXTENSION_PATH = path.join(
38
40
  path.dirname(fileURLToPath(import.meta.url)),
39
41
  "..",
40
42
  "..",
41
43
  "extension",
42
- "fanout-child.ts",
44
+ `fanout-child${path.extname(fileURLToPath(import.meta.url))}`,
43
45
  );
44
46
  const FAST_MODE_EXTENSION_PATH = path.join(
45
47
  path.dirname(fileURLToPath(import.meta.url)),
46
- "fast-mode-extension.ts",
48
+ `fast-mode-extension${path.extname(fileURLToPath(import.meta.url))}`,
47
49
  );
48
50
  const SUBAGENT_RUNTIME_EXTENSION_PATHS = new Set([
49
51
  PROMPT_RUNTIME_EXTENSION_PATH,
@@ -60,6 +62,39 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
60
62
  "openai-codex/gpt-5.6-sol",
61
63
  ]);
62
64
  const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
65
+ const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
66
+ const REPOSITORY_INSPECTION_TOOLS = new Set(["read", "grep", "find", "ls", "bash", "powershell"]);
67
+ const REVIEW_OR_SCOUT_AGENT_PATTERN = /\b(?:reviewer|scout)\b/i;
68
+
69
+ export function isReviewOrScoutLaneAgent(agentName: string | undefined): boolean {
70
+ return typeof agentName === "string" && REVIEW_OR_SCOUT_AGENT_PATTERN.test(agentName);
71
+ }
72
+
73
+ export function missingPermittedRepositoryInspectionTools(
74
+ unavailableHostBuiltins: readonly string[],
75
+ excludeTools: readonly string[] = [],
76
+ ): string[] {
77
+ const excluded = new Set(excludeTools);
78
+ return unavailableHostBuiltins.filter((tool) => REPOSITORY_INSPECTION_TOOLS.has(tool) && !excluded.has(tool));
79
+ }
80
+
81
+ export function formatReviewLaneToolContractFailure(input: {
82
+ agentName?: string;
83
+ missingTools: readonly string[];
84
+ requestedTools?: readonly string[];
85
+ effectiveTools: readonly string[];
86
+ ceilingSources?: readonly string[];
87
+ excludeTools?: readonly string[];
88
+ }): string {
89
+ const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
90
+ return [
91
+ `${subject}: tool contract could not be satisfied; host runtime does not provide permitted required repository tools [${input.missingTools.join(", ")}].`,
92
+ `Requested tool names: ${input.requestedTools ? `[${input.requestedTools.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${input.effectiveTools.join(", ")}].`,
93
+ ...(input.ceilingSources?.length ? [`Active capability ceiling sources: [${input.ceilingSources.join(", ")}].`] : []),
94
+ ...(input.excludeTools?.length ? [`Explicit excludeTools: [${input.excludeTools.join(", ")}].`] : []),
95
+ "This is a lane infrastructure failure, not a completed review/scout result.",
96
+ ].join(" ");
97
+ }
63
98
 
64
99
  export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
65
100
  const parent = parentSessionId?.trim();
@@ -113,9 +148,9 @@ function stripThinkingSuffix(model: string): string {
113
148
  : model;
114
149
  }
115
150
 
116
- function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "modelCandidates" | "agentName">): string[] {
151
+ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "agentName">): string[] {
117
152
  if (!input.fast) return [];
118
- const candidates = (input.modelCandidates?.length ? input.modelCandidates : input.model ? [input.model] : [])
153
+ const candidates = (input.model ? [input.model] : [])
119
154
  .map(stripThinkingSuffix);
120
155
  if (candidates.length === 0) {
121
156
  throw new Error(`fast mode requires an explicit supported native OpenAI-Codex model${input.agentName ? ` for agent '${input.agentName}'` : ""}.`);
@@ -133,6 +168,7 @@ export interface ResolvePiLaunchToolPlanInput {
133
168
  allowNestedSubagents?: boolean;
134
169
  extensions?: string[];
135
170
  subagentOnlyExtensions?: string[];
171
+ requiredExtensions?: RequiredChildExtensionSnapshot;
136
172
  mcpDirectTools?: string[];
137
173
  cwd?: string;
138
174
  requireReadTool?: boolean;
@@ -145,12 +181,21 @@ export interface ResolvePiLaunchToolPlanInput {
145
181
  };
146
182
  fast?: boolean;
147
183
  model?: string;
148
- modelCandidates?: readonly string[];
149
184
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
150
185
  inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
151
186
  agentName?: string;
152
187
  permissionRules?: PermissionRules;
153
188
  runtimeSnapshotHost?: McpRuntimeSnapshotHost;
189
+ /**
190
+ * When provided, child tool plans intersect known Pi core tool slots with
191
+ * this set. Core tools the agent declares but the host does not provide are
192
+ * omitted with a non-fatal warning (tracked in `unavailableHostBuiltins`).
193
+ * Review/scout lanes fail closed when a requested, still-permitted
194
+ * repository inspection tool is among those host omissions. Intentionally
195
+ * empty or ceiling-restricted allowlists are not a minimum-tool contract.
196
+ * Non-core names remain allowed and are validated in the child's registry.
197
+ */
198
+ hostAvailableBuiltins?: readonly string[];
154
199
  }
155
200
 
156
201
  export interface PiLaunchToolPlan {
@@ -169,11 +214,14 @@ export interface PiLaunchToolPlan {
169
214
  fanoutAuthorized: boolean;
170
215
  runtimeExtensions: string[];
171
216
  configuredExtensions: string[];
217
+ requiredExtensions: RequiredChildExtensionSnapshot;
172
218
  extensionArgs: string[];
173
219
  disableAmbientExtensions: boolean;
174
220
  capabilityAudit?: SubagentCapabilityAudit;
175
221
  /** Non-fatal launch warnings; they do not change behavior. */
176
222
  warnings: string[];
223
+ /** Builtin tools the agent declared but the host runtime does not provide. */
224
+ unavailableHostBuiltins: string[];
177
225
  }
178
226
 
179
227
  function extensionIdentifier(value: string): string {
@@ -211,6 +259,7 @@ export function projectLaunchResolvedChildExtensions(
211
259
  PiLaunchToolPlan,
212
260
  | "runtimeExtensions"
213
261
  | "configuredExtensions"
262
+ | "requiredExtensions"
214
263
  | "extensionArgs"
215
264
  | "disableAmbientExtensions"
216
265
  >,
@@ -224,10 +273,12 @@ export function projectLaunchResolvedChildExtensions(
224
273
  disableAmbientExtensions: toolPlan.disableAmbientExtensions,
225
274
  runtime: runtime.ids,
226
275
  configured: configured.ids,
276
+ required: toolPlan.requiredExtensions.map(({ id }) => id),
227
277
  effective: effective.ids,
228
278
  omitted: {
229
279
  runtime: runtime.omitted,
230
280
  configured: configured.omitted,
281
+ required: 0,
231
282
  effective: effective.omitted,
232
283
  },
233
284
  };
@@ -289,6 +340,32 @@ export function resolvePermissionSystemExtension(): string | undefined {
289
340
  return undefined;
290
341
  }
291
342
 
343
+ /**
344
+ * Extract the names of builtin tools the host provides. Use this to pass
345
+ * `hostAvailableBuiltins` to `resolvePiLaunchToolPlan` so child tool plans
346
+ * intersect known core slots with what the host actually supports. Wrapped
347
+ * core slots count regardless of source; host-specific builtins count too.
348
+ *
349
+ * Returns `undefined` when builtin tool discovery fails or yields nothing,
350
+ * so callers skip the intersection (fail-safe to allowing all declared tools).
351
+ * This handles test mocks without proper tool registration and hosts whose
352
+ * getAllTools() throws before extensions load.
353
+ */
354
+ export function getHostBuiltinToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] | undefined {
355
+ try {
356
+ const builtins = pi
357
+ .getAllTools()
358
+ .filter((tool) => {
359
+ const source = (tool.sourceInfo as { source?: string } | undefined)?.source;
360
+ return source === "builtin" || PI_BUILTIN_TOOL_NAMES.has(tool.name);
361
+ })
362
+ .map((tool) => tool.name);
363
+ return builtins.length > 0 ? builtins : undefined;
364
+ } catch {
365
+ return undefined;
366
+ }
367
+ }
368
+
292
369
  export function resolvePiLaunchToolPlan(
293
370
  input: ResolvePiLaunchToolPlanInput,
294
371
  ): PiLaunchToolPlan {
@@ -296,21 +373,35 @@ export function resolvePiLaunchToolPlan(
296
373
  input.capabilityCeiling,
297
374
  input.inheritedCapabilityCeiling,
298
375
  );
376
+ const requiredExtensions = snapshotRequiredChildExtensions(input.requiredExtensions ?? []);
377
+ if (requiredExtensions.length > 0 && capabilityCeiling?.denyExtensions) {
378
+ throw new Error(`Capability ceiling from ${capabilityCeiling.sources.join(", ") || "unknown source"} denies extensions but this host requires: ${requiredExtensions.map(({ id }) => id).join(", ")}.`);
379
+ }
299
380
  const allowedToolSet =
300
381
  capabilityCeiling?.allowedTools === undefined
301
382
  ? undefined
302
383
  : new Set(capabilityCeiling.allowedTools);
384
+ const hostAvailableSet =
385
+ input.hostAvailableBuiltins === undefined
386
+ ? undefined
387
+ : new Set(input.hostAvailableBuiltins);
303
388
  const requestedBuiltinTools =
304
389
  input.tools?.filter(
305
390
  (tool) =>
306
391
  !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
307
392
  ) ?? [];
393
+ if (input.requireReadTool && hostAvailableSet && !hostAvailableSet.has("read")) {
394
+ const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
395
+ throw new Error(
396
+ `Host runtime does not provide required tool 'read'${agentLabel} for lazy skill loading.`,
397
+ );
398
+ }
308
399
  if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
309
400
  throw new Error(
310
401
  `Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
311
402
  );
312
403
  }
313
- const declaredBuiltinTools =
404
+ const ceilingFilteredBuiltinTools =
314
405
  input.tools === undefined
315
406
  ? allowedToolSet
316
407
  ? [...allowedToolSet]
@@ -322,6 +413,12 @@ export function resolvePiLaunchToolPlan(
322
413
  ? ["read", ...requestedBuiltinTools]
323
414
  : requestedBuiltinTools
324
415
  ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
416
+ const declaredBuiltinTools = hostAvailableSet
417
+ ? ceilingFilteredBuiltinTools.filter((tool) => !PI_BUILTIN_TOOL_NAMES.has(tool) || hostAvailableSet.has(tool))
418
+ : ceilingFilteredBuiltinTools;
419
+ const unavailableHostBuiltins = hostAvailableSet
420
+ ? ceilingFilteredBuiltinTools.filter((tool) => PI_BUILTIN_TOOL_NAMES.has(tool) && !hostAvailableSet.has(tool))
421
+ : [];
325
422
  const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
326
423
  const excludedToolSet = new Set(excludeTools);
327
424
  const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
@@ -330,6 +427,9 @@ export function resolvePiLaunchToolPlan(
330
427
  !excludedToolSet.has("subagent") &&
331
428
  (!allowedToolSet || allowedToolSet.has("subagent"))
332
429
  );
430
+ if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
431
+ throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
432
+ }
333
433
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
334
434
  ? []
335
435
  : (input.tools ?? []).filter(
@@ -370,8 +470,8 @@ export function resolvePiLaunchToolPlan(
370
470
  ...internalTools,
371
471
  ]),
372
472
  ];
373
- // Supervisor-coordination names stay in the --tools allowlist but are never
374
- // strict requirements: children register contact_supervisor at runtime through
473
+ // Upward contact stays in the --tools allowlist but is not a strict
474
+ // requirement: children register contact_supervisor at runtime through
375
475
  // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
376
476
  // appended intercom alongside contact_supervisor, so that exact pairing is
377
477
  // legacy plumbing, not a user demand for an external intercom provider;
@@ -392,7 +492,7 @@ export function resolvePiLaunchToolPlan(
392
492
  ? resolvePermissionSystemExtension()
393
493
  : undefined;
394
494
  if (input.fast && capabilityCeiling?.denyExtensions) throw new Error("fast mode requires a child runtime extension, but this launch denies extensions.");
395
- const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
495
+ const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, agentName: input.agentName });
396
496
  const runtimeExtensions = [
397
497
  PROMPT_RUNTIME_EXTENSION_PATH,
398
498
  ...fastModeExtensions,
@@ -419,7 +519,7 @@ export function resolvePiLaunchToolPlan(
419
519
  ...(input.extensions ?? []),
420
520
  ...(input.subagentOnlyExtensions ?? []),
421
521
  ];
422
- const extensionArgs = disableAmbientExtensions
522
+ const ordinaryExtensionArgs = disableAmbientExtensions
423
523
  ? [...new Set([...runtimeExtensions, ...configuredExtensions])]
424
524
  : [
425
525
  ...new Set([
@@ -428,6 +528,8 @@ export function resolvePiLaunchToolPlan(
428
528
  ...(input.subagentOnlyExtensions ?? []),
429
529
  ]),
430
530
  ];
531
+ // Host-required paths have final precedence and cannot be removed by agent defaults or overrides.
532
+ const extensionArgs = [...new Set([...ordinaryExtensionArgs, ...requiredExtensions.map(({ path }) => path)])];
431
533
  const requestedToolNames =
432
534
  input.tools !== undefined
433
535
  ? [
@@ -437,6 +539,32 @@ export function resolvePiLaunchToolPlan(
437
539
  ]),
438
540
  ]
439
541
  : undefined;
542
+ const missingPermittedRepositoryTools = input.tools !== undefined
543
+ ? missingPermittedRepositoryInspectionTools(unavailableHostBuiltins, excludeTools)
544
+ : [];
545
+ if (missingPermittedRepositoryTools.length > 0 && isReviewOrScoutLaneAgent(input.agentName)) {
546
+ throw new Error(formatReviewLaneToolContractFailure({
547
+ agentName: input.agentName,
548
+ missingTools: missingPermittedRepositoryTools,
549
+ requestedTools: requestedToolNames,
550
+ effectiveTools: effectiveToolAllowlist,
551
+ ceilingSources: capabilityCeiling?.sources,
552
+ excludeTools,
553
+ }));
554
+ }
555
+ // Host pruning also happens without a ceiling (and therefore without an
556
+ // audit). Use the existing non-fatal launch warnings rather than inventing
557
+ // a ceiling or treating the requested allowlist as a minimum requirement.
558
+ if (unavailableHostBuiltins.length > 0) {
559
+ const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
560
+ warnings.push(
561
+ `${subject}: host runtime tool availability omitted [${unavailableHostBuiltins.join(", ")}]. `
562
+ + `Requested tool names: ${requestedToolNames ? `[${requestedToolNames.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${effectiveToolAllowlist.join(", ")}]. `
563
+ + (capabilityCeiling ? `Active capability ceiling sources: [${capabilityCeiling.sources.join(", ") || "unknown source"}]. ` : "")
564
+ + (excludeTools.length > 0 ? `Explicit excludeTools: [${excludeTools.join(", ")}]. ` : "")
565
+ + "This is a non-fatal tool-plan diagnostic, not verification of the child's runtime tool menu.",
566
+ );
567
+ }
440
568
  const capabilityAudit = capabilityCeiling
441
569
  ? ({
442
570
  ceiling: capabilityCeiling,
@@ -474,6 +602,7 @@ export function resolvePiLaunchToolPlan(
474
602
  capabilityCeilingAgentRestrictionSources(capabilityCeiling),
475
603
  }
476
604
  : {}),
605
+ ...(unavailableHostBuiltins.length > 0 ? { unavailableHostBuiltins } : {}),
477
606
  } satisfies SubagentCapabilityAudit)
478
607
  : undefined;
479
608
  return {
@@ -492,9 +621,11 @@ export function resolvePiLaunchToolPlan(
492
621
  fanoutAuthorized,
493
622
  runtimeExtensions,
494
623
  configuredExtensions,
624
+ requiredExtensions,
495
625
  extensionArgs,
496
626
  disableAmbientExtensions,
497
627
  warnings,
628
+ unavailableHostBuiltins,
498
629
  ...(capabilityAudit ? { capabilityAudit } : {}),
499
630
  };
500
631
  }
@@ -98,10 +98,12 @@ export function validateImplementationToolContract(input: {
98
98
  const declaredMutationToolsWereRemoved = requestedMutationTools.length > 0 && !hasBuiltinMutationTool(input.tools);
99
99
  const configuredExtensionCapability = (input.configuredExtensions?.length ?? 0) > 0 && !declaredMutationToolsWereRemoved;
100
100
  if (hasMutationToolCapability(input.tools, input.mcpDirectTools) || configuredExtensionCapability) return undefined;
101
- const intent = classifyTaskMutationIntent(input.agent, input.task);
101
+ const intent = classifyTaskMutationIntent(input.acceptanceRole === "writer" ? "worker" : input.agent, input.task);
102
102
  if (intent.kind === "read-only") return undefined;
103
- const writerTaskMayMutate = isWriterRole(input.agent, input.acceptanceRole)
104
- && (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
103
+ const writerTaskMayMutate = input.acceptanceRole === "writer"
104
+ ? true
105
+ : isWriterRole(input.agent, input.acceptanceRole)
106
+ && (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
105
107
  if (intent.kind !== "implementation" && !writerTaskMayMutate && !declaredMutationToolsWereRemoved) return undefined;
106
108
  return `Agent '${input.agent}' was given an implementation task, but its tool allowlist has no mutation-capable tools. Add bash, edit, write, or another mutation-capable tool to the agent, or use a read-only task/agent.`;
107
109
  }
@@ -46,12 +46,12 @@ const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency
46
46
  const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "sessionFiles", "thinkingOverrides"]);
47
47
  const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
48
48
  const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
49
- const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
49
+ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "machine", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
50
50
  const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
51
51
  ...DYNAMIC_PARALLEL_KEYS,
52
52
  "outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
53
53
  "structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
54
- "systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
54
+ "systemPromptMode", "thinking", "requestedModel", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "requiredExtensions", "launchResolvedExtensions", "requestedCwd", "machineEnv",
55
55
  ]);
56
56
  const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
57
57
 
@@ -0,0 +1,33 @@
1
+ import type { AgentConfig } from "../../agents/agents.ts";
2
+ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
3
+ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
4
+ import { buildSkillInjection } from "../../agents/skills.ts";
5
+ import { injectOutputPathSystemPrompt } from "./single-output.ts";
6
+
7
+ export interface EffectiveSystemPromptInput {
8
+ /** Agent as handed to the child, including runtime-declared overlays such as the Intercom bridge. */
9
+ agent: AgentConfig;
10
+ resolvedSkills: Parameters<typeof buildSkillInjection>[0];
11
+ /** Directory that scopes memory and refinement lookups. */
12
+ cwd: string;
13
+ /** Omit when the caller injects the output path through another channel. */
14
+ outputPath?: string;
15
+ }
16
+
17
+ function appendSection(prompt: string, section: string): string {
18
+ return prompt ? `${prompt}\n\n${section}` : section;
19
+ }
20
+
21
+ /**
22
+ * Child system prompt in the order preflight and every execution path hash:
23
+ * base prompt, skills, memory, refinement overlay, output path. Runtime
24
+ * acceptance prose is appended later and stays outside launch identity.
25
+ */
26
+ export function buildEffectiveSystemPrompt(input: EffectiveSystemPromptInput): string {
27
+ let prompt = input.agent.systemPrompt?.trim() ?? "";
28
+ if (input.resolvedSkills.length > 0) prompt = appendSection(prompt, buildSkillInjection(input.resolvedSkills));
29
+ const memoryInjection = buildAgentMemoryInjection(input.agent, input.cwd);
30
+ if (memoryInjection) prompt = appendSection(prompt, memoryInjection);
31
+ prompt = appendAgentRefinementOverlay(prompt, { cwd: input.cwd, agentName: input.agent.name });
32
+ return injectOutputPathSystemPrompt(prompt, input.outputPath, input.agent);
33
+ }
@@ -2,7 +2,9 @@ import type {
2
2
  ExternalCliReceiptMetadata,
3
3
  ExternalCliCapabilityNarrowing,
4
4
  ExternalCliRunnerStatus,
5
+ ExternalCliMachineStatus,
5
6
  ExternalProcessStatus,
7
+ HerdrMachineReference,
6
8
  } from "../../shared/types.ts";
7
9
 
8
10
  const UNSUPPORTED = {
@@ -80,6 +82,7 @@ export function resolveExternalCliRunnerStatus(input: {
80
82
  args?: string[];
81
83
  promptDelivery?: "stdin";
82
84
  capabilities?: ExternalCliCapabilityNarrowing;
85
+ machine?: HerdrMachineReference;
83
86
  }): ExternalCliRunnerStatus {
84
87
  const codexExec = input.adapter === "codex-exec";
85
88
  const codexExecWriter = input.adapter === "codex-exec-writer";
@@ -101,6 +104,7 @@ export function resolveExternalCliRunnerStatus(input: {
101
104
  ...(claudeCodeWriter ? { safety: { access: "workspace-write" as const, authentication: "existing-cli-required" as const, permissionMode: "acceptEdits" as const, tools: "Read,Write,Edit,Glob,Grep" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
102
105
  ...(cursorAgent ? { safety: { access: "read-only" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "ask" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
103
106
  ...(cursorAgentWriter ? { safety: { access: "workspace-write" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "print" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
107
+ ...(input.machine ? { machine: input.machine } : {}),
104
108
  capabilities: {
105
109
  stop: true,
106
110
  steer: false,
@@ -124,6 +128,9 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
124
128
  ? input.args
125
129
  : undefined;
126
130
  const promptDelivery = input.promptDelivery === "stdin" ? "stdin" : undefined;
131
+ const machine = input.machine && typeof input.machine === "object" && !Array.isArray(input.machine)
132
+ ? input.machine as HerdrMachineReference
133
+ : undefined;
127
134
  const adapterId = input.adapter && typeof input.adapter === "object" && !Array.isArray(input.adapter)
128
135
  ? (input.adapter as Record<string, unknown>).id
129
136
  : undefined;
@@ -131,6 +138,7 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
131
138
  return {
132
139
  type: "external-cli",
133
140
  command: input.command,
141
+ ...(machine ? { machine } : {}),
134
142
  args: args ?? [],
135
143
  promptDelivery: "prompt-file",
136
144
  adapter: { id: "grok-build", version: 1, executionMode: "one-shot-prompt-file" },
@@ -140,7 +148,7 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
140
148
  };
141
149
  }
142
150
  const adapter = isCodeOwnedExternalCliAdapterId(adapterId) ? adapterId : undefined;
143
- return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}) });
151
+ return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}), ...(machine ? { machine } : {}) });
144
152
  }
145
153
 
146
154
  export function externalCliReceiptMetadata(input: {
@@ -149,9 +157,11 @@ export function externalCliReceiptMetadata(input: {
149
157
  outputReference?: string;
150
158
  }): ExternalCliReceiptMetadata {
151
159
  const { runner } = input;
160
+ const machine: ExternalCliMachineStatus | undefined = input.externalProcess?.machine ?? runner.machine;
152
161
  return {
153
162
  adapter: { ...runner.adapter },
154
163
  capabilities: { ...runner.capabilities },
164
+ ...(machine ? { machine: { ...machine, ...(machine.remoteGit ? { remoteGit: { ...machine.remoteGit } } : {}) } } : {}),
155
165
  ...(runner.safety ? { safety: { ...runner.safety } } : {}),
156
166
  ...(input.externalProcess ? {
157
167
  outputArtifacts: {
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
 
5
5
  const MAX_PROBE_OUTPUT_BYTES = 256 * 1024;
6
6
  const MAX_PROBE_TIMEOUT_MS = 5_000;
7
+ const MAX_REMOTE_PROBE_TIMEOUT_MS = 15_000;
7
8
  const MAX_CACHE_ENTRIES = 64;
8
9
  const MAX_AVAILABILITY_REASON_LENGTH = 256;
9
10
 
@@ -16,6 +17,8 @@ export interface ExternalCliPreflightSpec {
16
17
  evidenceArgs?: readonly string[];
17
18
  evidenceLabel?: string;
18
19
  probeTimeoutMs?: number;
20
+ /** Remote probes include SSH handshakes; values may only narrow the code-owned remote ceiling. */
21
+ remote?: boolean;
19
22
  validate?: (result: ExternalCliPreflightResult) => void;
20
23
  }
21
24
 
@@ -90,7 +93,7 @@ function narrowPositiveInteger(value: number | undefined, ceiling: number, label
90
93
  }
91
94
 
92
95
  function specKey(spec: ExternalCliPreflightSpec): string {
93
- return JSON.stringify([spec.id, spec.versionArgs, spec.helpArgs, spec.evidenceArgs, spec.evidenceLabel, spec.probeTimeoutMs]);
96
+ return JSON.stringify([spec.id, spec.versionArgs, spec.helpArgs, spec.evidenceArgs, spec.evidenceLabel, spec.probeTimeoutMs, spec.remote === true]);
94
97
  }
95
98
 
96
99
  export function preflightExternalCli(command: string, spec: ExternalCliPreflightSpec, env: NodeJS.ProcessEnv, cwd?: string): ExternalCliPreflightResult {
@@ -99,7 +102,8 @@ export function preflightExternalCli(command: string, spec: ExternalCliPreflight
99
102
  const lookupKey = JSON.stringify([binaryPath, binaryMtimeMs, specKey(spec)]);
100
103
  const cachedKey = lookup.get(lookupKey);
101
104
  const cached = cachedKey ? cache.get(cachedKey) : undefined;
102
- const probeTimeoutMs = narrowPositiveInteger(spec.probeTimeoutMs, MAX_PROBE_TIMEOUT_MS, "probeTimeoutMs");
105
+ const ceiling = spec.remote === true ? MAX_REMOTE_PROBE_TIMEOUT_MS : MAX_PROBE_TIMEOUT_MS;
106
+ const probeTimeoutMs = narrowPositiveInteger(spec.probeTimeoutMs, ceiling, "probeTimeoutMs");
103
107
  const base = cached ?? {
104
108
  binaryPath,
105
109
  binaryMtimeMs,
@@ -3,7 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { finished } from "node:stream/promises";
6
- import type { ExternalProcessStatus } from "../../shared/types.ts";
6
+ import type { ExternalProcessStatus, ProcessTreeTerminal } from "../../shared/types.ts";
7
7
  import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
8
8
  import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
9
9
  import {
@@ -141,7 +141,7 @@ function classifyInvalidation(error: string): "auth" | "permission" | "launch" {
141
141
  return "launch";
142
142
  }
143
143
 
144
- function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<unknown> {
144
+ function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<ProcessTreeTerminal> {
145
145
  if (process.platform !== "win32") return controller.terminate();
146
146
  return new Promise((resolve) => {
147
147
  const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
@@ -239,7 +239,7 @@ export function runExternalCli(input: {
239
239
  let settled = false;
240
240
  let processTree: OwnedProcessTreeController | undefined;
241
241
  let processPid: number | undefined;
242
- let termination: Promise<unknown> | undefined;
242
+ let termination: Promise<ProcessTreeTerminal> | undefined;
243
243
  const flushProgress = () => {
244
244
  if (!latestProgress) return;
245
245
  input.onParserProgress?.(latestProgress);
@@ -381,8 +381,7 @@ export function runExternalCli(input: {
381
381
  input.registerTimeout?.(undefined);
382
382
  input.registerStop?.(undefined);
383
383
  void (async () => {
384
- if (termination) await termination;
385
- else if (processTree) await processTree.finishAfterWriterClose();
384
+ const treeProof = termination ? await termination : processTree ? await processTree.finishAfterWriterClose() : undefined;
386
385
  const endedAt = Date.now();
387
386
  const externalProcess: ExternalProcessStatus = {
388
387
  ...initialProcess,
@@ -398,15 +397,18 @@ export function runExternalCli(input: {
398
397
  input.onProcess?.(externalProcess);
399
398
  const stderr = stderrTail.text().trim();
400
399
  const parserFailure = parserError?.message ?? (parserTerminal?.state === "failed" ? parserTerminal.error ?? "External CLI parser reported terminal failure." : undefined);
400
+ const treeFailure = process.platform !== "win32" && treeProof?.state === "unknown"
401
+ ? `Process-tree cleanup failed: ${treeProof.reason}.`
402
+ : undefined;
401
403
  const error = stopped
402
404
  ? input.stopMessage ?? "Subagent stopped by user."
403
405
  : timedOut
404
406
  ? input.timeoutMessage ?? "Subagent timed out."
405
- : spawnError?.message ?? parserFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
407
+ : spawnError?.message ?? parserFailure ?? treeFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
406
408
  if (error && input.preflight && !parserError) invalidateExternalCliPreflight(input.command, input.preflight, classifyInvalidation(error));
407
409
  const result: ExternalCliRunResult = {
408
410
  output: (!parserError && parserTerminal?.state === "completed" ? parserTerminal.output ?? "" : stdoutTail.text()).trim(),
409
- exitCode: timedOut || stopped || spawnError || parserFailure ? 1 : exitCode,
411
+ exitCode: timedOut || stopped || spawnError || parserFailure || treeFailure ? 1 : exitCode,
410
412
  ...(error ? { error } : {}),
411
413
  ...(timedOut ? { timedOut: true } : {}),
412
414
  ...(stopped ? { stopped: true } : {}),