pi-subagents 0.63.0 → 0.65.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,1062 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import {
7
- encodeNestedPathEnv,
8
- parseNestedPathEnv,
9
- type NestedPathEntry,
10
- } from "./nested-path.ts";
11
- import {
12
- formatUnresolvedMcpDirectToolSelectors,
13
- resolveMcpDirectToolResolution,
14
- type McpConfig,
15
- type McpRuntimeSnapshotHost,
16
- type ResolvedMcpDirectToolSelection,
17
- } from "./mcp-direct-tool-allowlist.ts";
18
- import { resolvePiPackageRoot } from "./pi-spawn.ts";
19
- import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
20
- import { RUNTIME_EXTENSION_ACK_PATH_ENV } from "./runtime-acknowledged-extensions.ts";
21
- import {
22
- STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV,
23
- STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV,
24
- STRUCTURED_OUTPUT_CAPTURE_ENV,
25
- STRUCTURED_OUTPUT_SCHEMA_ENV,
26
- type StructuredOutputRuntime,
27
- } from "./structured-output.ts";
28
- import {
29
- TEMP_ROOT_DIR,
30
- type JsonSchemaObject,
31
- type LaunchResolvedChildExtensionsV1,
32
- type ResolvedToolBudget,
33
- type RunFanoutBudgetDescriptor,
34
- } from "../../shared/types.ts";
35
- import { THINKING_LEVELS } from "../../shared/model-info.ts";
36
- import { decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
37
- import { encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDGET_ENV } from "./run-fanout-budget.ts";
38
- import {
39
- TOOL_BUDGET_ENV,
40
- TOOL_BUDGET_ZERO_AUTH_ENV,
41
- encodeToolBudgetEnv,
42
- } from "./tool-budget.ts";
43
- import {
44
- CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
45
- MCP_DIRECT_CHILD_TOOLS_ENV,
46
- REQUIRED_CHILD_TOOLS_ENV,
47
- } from "./tool-availability.ts";
48
- import {
49
- CHILD_WATCHDOG_CONFIG_ENV,
50
- encodeChildWatchdogConfig,
51
- type ChildWatchdogConfig,
52
- } from "../../watchdog/child-status.ts";
53
- import { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
54
- import {
55
- PI_CODING_AGENT_PACKAGE_ROOT_ENV,
56
- getAgentDir,
57
- } from "../../shared/utils.ts";
58
- import {
59
- encodePermissionRules,
60
- PERMISSION_AUDIT_PATH_ENV,
61
- PERMISSION_POLICY_ENV,
62
- type PermissionRules,
63
- } from "./permissions.ts";
64
- import {
65
- SUBAGENT_CAPABILITY_CEILING_ENV,
66
- capabilityCeilingAgentRestrictionSources,
67
- decodeSubagentCapabilityCeiling,
68
- encodeSubagentCapabilityCeiling,
69
- intersectSubagentCapabilityCeilings,
70
- isAgentAllowedByCapabilityCeiling,
71
- type ResolvedSubagentCapabilityCeiling,
72
- type SubagentCapabilityAudit,
73
- } from "./capability-ceiling.ts";
74
-
75
- const TASK_ARG_LIMIT = 8000;
76
-
77
- /**
78
- * Env override for how the task text reaches the child process. Endpoint
79
- * protection (EDR) pre-execution command-line scanning may deny exec of
80
- * children whose argv embeds a long natural-language task, which surfaces
81
- * as an immediate zero-activity SIGKILL. File delivery keeps the task out
82
- * of argv entirely.
83
- */
84
- export const SUBAGENT_TASK_DELIVERY_ENV = "PI_SUBAGENT_TASK_DELIVERY";
85
-
86
- export type SubagentTaskDelivery = "auto" | "file";
87
-
88
- export function resolveSubagentTaskDelivery(
89
- env: NodeJS.ProcessEnv = process.env,
90
- ): SubagentTaskDelivery {
91
- return env[SUBAGENT_TASK_DELIVERY_ENV]?.trim().toLowerCase() === "file"
92
- ? "file"
93
- : "auto";
94
- }
95
-
96
- export function shouldDeliverTaskViaFile(
97
- task: string,
98
- delivery: SubagentTaskDelivery,
99
- platform: NodeJS.Platform = process.platform,
100
- ): boolean {
101
- return delivery === "file" || platform === "darwin" || task.length > TASK_ARG_LIMIT;
102
- }
103
- const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
104
- const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
105
- path.dirname(fileURLToPath(import.meta.url)),
106
- "subagent-prompt-runtime.ts",
107
- );
108
- const FANOUT_CHILD_EXTENSION_PATH = path.join(
109
- path.dirname(fileURLToPath(import.meta.url)),
110
- "..",
111
- "..",
112
- "extension",
113
- "fanout-child.ts",
114
- );
115
- const FAST_MODE_EXTENSION_PATH = path.join(
116
- path.dirname(fileURLToPath(import.meta.url)),
117
- "fast-mode-extension.ts",
118
- );
119
- const FAST_MODE_ALLOWED_MODELS = new Set([
120
- "openai-codex/gpt-5.6-luna",
121
- "openai-codex/gpt-5.6-sol",
122
- ]);
123
- const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
124
- export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
125
- export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
126
- "PI_SUBAGENT_ORCHESTRATOR_TARGET";
127
- export const SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV =
128
- "PI_SUBAGENT_ORCHESTRATOR_SESSION_ID";
129
- export const SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV =
130
- "PI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
131
- export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
132
- export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
133
- export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
134
- export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD";
135
- export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK";
136
- export const SUBAGENT_PARENT_CONTROL_INBOX_ENV =
137
- "PI_SUBAGENT_PARENT_CONTROL_INBOX";
138
- export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID";
139
- export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID";
140
- export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
141
- export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
142
- export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
143
- export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
144
- "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
145
- export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
146
- export const SUBAGENT_FORK_CACHE_KEY_ENV = "PI_SUBAGENT_FORK_CACHE_KEY";
147
- export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
148
- export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
149
- export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
150
- export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
151
- export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
152
- export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
153
-
154
- export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
155
- const parent = parentSessionId?.trim();
156
- if (!parent) return undefined;
157
- const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
158
- return `pi-fork:${digest}`;
159
- }
160
-
161
- export interface BuildPiArgsInput {
162
- parentSessionId?: string;
163
- forkCacheKey?: string;
164
- baseArgs: string[];
165
- task: string;
166
- sessionEnabled: boolean;
167
- sessionDir?: string;
168
- sessionFile?: string;
169
- model?: string;
170
- thinking?: string | false;
171
- systemPromptMode?: "append" | "replace";
172
- inheritProjectContext: boolean;
173
- inheritGlobalContext: boolean;
174
- inheritSkills: boolean;
175
- requireReadTool?: boolean;
176
- tools?: string[];
177
- excludeTools?: string[];
178
- extensions?: string[];
179
- subagentOnlyExtensions?: string[];
180
- systemPrompt?: string | null;
181
- mcpDirectTools?: string[];
182
- mcpConfig?: McpConfig;
183
- runtimeServerNames?: string[];
184
- cwd?: string;
185
- promptFileStem?: string;
186
- intercomSessionName?: string;
187
- /** Human-readable display name for the child session (agent + task excerpt,
188
- * derived by the caller). Passed to the child as PI_SUBAGENT_SESSION_NAME;
189
- * the prompt runtime applies it via pi.setSessionName unless an intercom
190
- * target takes precedence. */
191
- sessionName?: string;
192
- orchestratorIntercomTarget?: string;
193
- runId?: string;
194
- childAgentName?: string;
195
- childIndex?: number;
196
- parentEventSink?: string;
197
- parentControlInbox?: string;
198
- parentRootRunId?: string;
199
- parentRunId?: string;
200
- parentChildIndex?: number;
201
- parentDepth?: number;
202
- parentPath?: NestedPathEntry[];
203
- parentCapabilityToken?: string;
204
- runFanoutBudget?: RunFanoutBudgetDescriptor;
205
- steerInboxDir?: string;
206
- steerCapabilityPath?: string;
207
- steerAckDir?: string;
208
- structuredOutput?: StructuredOutputRuntime;
209
- fast?: boolean;
210
- modelCandidates?: readonly string[];
211
- toolBudget?: ResolvedToolBudget;
212
- allowZeroToolBudget?: boolean;
213
- permissionRules?: PermissionRules;
214
- permissionAuditPath?: string;
215
- childWatchdog?: ChildWatchdogConfig;
216
- /**
217
- * Per-launch override of the task delivery mode. Startup-retry paths set
218
- * this to "file" after an unexplained zero-activity SIGKILL so the retry
219
- * keeps the task text out of the child's argv.
220
- */
221
- taskDelivery?: SubagentTaskDelivery;
222
- waitToolEnabled?: boolean;
223
- waitToolDefaultTimeoutMs?: number;
224
- allowNestedSubagents?: boolean;
225
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
226
- thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
227
- extensionBindings?: ExtensionBindings;
228
- runtimeSnapshotHost?: McpRuntimeSnapshotHost;
229
- }
230
-
231
- export interface BuildPiArgsResult {
232
- args: string[];
233
- env: Record<string, string | undefined>;
234
- warnings: string[];
235
- tempDir?: string;
236
- toolDiagnosticPath?: string;
237
- runtimeAcknowledgedExtensionsPath?: string;
238
- capabilityAudit?: SubagentCapabilityAudit;
239
- }
240
-
241
- function sanitizeSupervisorChannelSegment(value: string): string {
242
- return (
243
- value
244
- .trim()
245
- .replace(/[^A-Za-z0-9._-]+/g, "-")
246
- .replace(/^-+|-+$/g, "") || "unknown"
247
- );
248
- }
249
-
250
- function supervisorChannelDir(
251
- runId: string,
252
- agent: string,
253
- childIndex: number,
254
- ): string {
255
- return path.join(
256
- TEMP_ROOT_DIR,
257
- "supervisor-channels",
258
- `${sanitizeSupervisorChannelSegment(runId)}-${sanitizeSupervisorChannelSegment(agent)}-${childIndex}`,
259
- );
260
- }
261
-
262
- export function applyThinkingSuffix(
263
- model: string | undefined,
264
- thinking: string | false | undefined,
265
- replaceExisting = false,
266
- ): string | undefined {
267
- if (!model || !thinking) return model;
268
- const colonIdx = model.lastIndexOf(":");
269
- if (
270
- colonIdx !== -1 &&
271
- THINKING_LEVELS.some((level) => level === model.substring(colonIdx + 1))
272
- ) {
273
- return replaceExisting ? `${model.slice(0, colonIdx)}:${thinking}` : model;
274
- }
275
- return `${model}:${thinking}`;
276
- }
277
-
278
- function stripThinkingSuffix(model: string): string {
279
- const colonIdx = model.lastIndexOf(":");
280
- if (colonIdx === -1) return model;
281
- return THINKING_LEVELS.some((level) => level === model.substring(colonIdx + 1))
282
- ? model.slice(0, colonIdx)
283
- : model;
284
- }
285
-
286
- function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "modelCandidates" | "agentName">): string[] {
287
- if (!input.fast) return [];
288
- const candidates = (input.modelCandidates?.length ? input.modelCandidates : input.model ? [input.model] : [])
289
- .map(stripThinkingSuffix);
290
- if (candidates.length === 0) {
291
- throw new Error(`fast mode requires an explicit supported native OpenAI-Codex model${input.agentName ? ` for agent '${input.agentName}'` : ""}.`);
292
- }
293
- const unsupported = candidates.filter((model) => !FAST_MODE_ALLOWED_MODELS.has(model));
294
- if (unsupported.length > 0) {
295
- throw new Error(`fast mode supports only ${[...FAST_MODE_ALLOWED_MODELS].join(", ")}; unsupported model${unsupported.length === 1 ? "" : "s"}: ${unsupported.join(", ")}.`);
296
- }
297
- return [FAST_MODE_EXTENSION_PATH];
298
- }
299
-
300
- export interface ResolvePiLaunchToolPlanInput {
301
- tools?: string[];
302
- excludeTools?: string[];
303
- allowNestedSubagents?: boolean;
304
- extensions?: string[];
305
- subagentOnlyExtensions?: string[];
306
- mcpDirectTools?: string[];
307
- mcpConfig?: McpConfig;
308
- runtimeServerNames?: string[];
309
- cwd?: string;
310
- requireReadTool?: boolean;
311
- structuredOutput?:
312
- | boolean
313
- | {
314
- schema: JsonSchemaObject;
315
- schemaPath: string;
316
- outputPath: string;
317
- };
318
- fast?: boolean;
319
- model?: string;
320
- modelCandidates?: readonly string[];
321
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
322
- inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
323
- agentName?: string;
324
- permissionRules?: PermissionRules;
325
- runtimeSnapshotHost?: McpRuntimeSnapshotHost;
326
- }
327
-
328
- export interface PiLaunchToolPlan {
329
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
330
- requestedBuiltinTools: string[];
331
- declaredBuiltinTools: string[];
332
- excludeTools: string[];
333
- toolExtensionPaths: string[];
334
- resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
335
- effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
336
- effectiveMcpTools: string[];
337
- explicitToolAllowlist: boolean;
338
- internalTools: string[];
339
- effectiveToolAllowlist: string[];
340
- requiredChildTools: string[];
341
- fanoutAuthorized: boolean;
342
- runtimeExtensions: string[];
343
- configuredExtensions: string[];
344
- extensionArgs: string[];
345
- disableAmbientExtensions: boolean;
346
- capabilityAudit?: SubagentCapabilityAudit;
347
- /** Non-fatal launch warnings; they do not change behavior. */
348
- warnings: string[];
349
- mcpConfig?: McpConfig;
350
- runtimeServerNames?: string[];
351
- }
352
-
353
- function extensionIdentifier(value: string): string {
354
- return `sha256:${createHash("sha256").update(path.normalize(value.trim())).digest("hex").slice(0, 16)}`;
355
- }
356
-
357
- function boundedExtensionIdentifiers(values: string[]): {
358
- ids: string[];
359
- omitted: number;
360
- } {
361
- const ids = [...new Set(values.map(extensionIdentifier))];
362
- return {
363
- ids: ids.slice(0, MAX_LAUNCH_RESOLVED_EXTENSION_IDS),
364
- omitted: Math.max(0, ids.length - MAX_LAUNCH_RESOLVED_EXTENSION_IDS),
365
- };
366
- }
367
-
368
- function hasPermissionRules(rules: PermissionRules | undefined): boolean {
369
- return rules !== undefined && Object.keys(rules).length > 0;
370
- }
371
-
372
- function filterRuntimeMcpConfig(
373
- config: McpConfig,
374
- runtimeServerNames: readonly string[],
375
- effectiveRuntimeServerNames: readonly string[],
376
- ): McpConfig {
377
- const runtimeNames = new Set(runtimeServerNames);
378
- const effectiveNames = new Set(effectiveRuntimeServerNames);
379
- return {
380
- ...config,
381
- mcpServers: Object.fromEntries(
382
- Object.entries(config.mcpServers).filter(([name]) => !runtimeNames.has(name) || effectiveNames.has(name)),
383
- ),
384
- };
385
- }
386
-
387
- export function projectLaunchResolvedChildExtensions(
388
- toolPlan: Pick<
389
- PiLaunchToolPlan,
390
- | "runtimeExtensions"
391
- | "configuredExtensions"
392
- | "extensionArgs"
393
- | "disableAmbientExtensions"
394
- >,
395
- ): LaunchResolvedChildExtensionsV1 {
396
- const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
397
- const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
398
- const effective = boundedExtensionIdentifiers(toolPlan.extensionArgs);
399
- return {
400
- version: 1,
401
- source: "launch-resolved",
402
- disableAmbientExtensions: toolPlan.disableAmbientExtensions,
403
- runtime: runtime.ids,
404
- configured: configured.ids,
405
- effective: effective.ids,
406
- omitted: {
407
- runtime: runtime.omitted,
408
- configured: configured.omitted,
409
- effective: effective.omitted,
410
- },
411
- };
412
- }
413
-
414
- /**
415
- * Resolve the permission-system extension entry point when installed.
416
- * Returns the absolute path to the extension's main module, or undefined
417
- * when the package is not installed. Callers can check `autoInject` config
418
- * to decide whether to include it in child processes.
419
- */
420
- export function resolvePermissionSystemExtension(): string | undefined {
421
- const agentDir = getAgentDir();
422
- const candidates = [
423
- // npm-scoped package (most common)
424
- path.join(
425
- agentDir,
426
- "npm",
427
- "node_modules",
428
- "@gotgenes",
429
- "pi-permission-system",
430
- ),
431
- // direct extension directory (some layouts)
432
- path.join(agentDir, "extensions", "pi-permission-system"),
433
- ];
434
- const errors: Error[] = [];
435
- for (const extDir of candidates) {
436
- if (!fs.existsSync(extDir)) continue;
437
- const pkgPath = path.join(extDir, "package.json");
438
- if (!fs.existsSync(pkgPath)) {
439
- errors.push(new Error(`Permission-system package manifest is missing at ${pkgPath}.`));
440
- continue;
441
- }
442
- try {
443
- let pkg: { pi?: { extensions?: string[] } };
444
- const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
445
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
446
- throw new Error("manifest root must be an object");
447
- }
448
- pkg = parsed as typeof pkg;
449
- const extensions = pkg.pi?.extensions;
450
- const entry = Array.isArray(extensions) ? extensions[0] : undefined;
451
- if (typeof entry !== "string" || !entry.trim()) {
452
- throw new Error(
453
- `Permission-system package manifest at ${pkgPath} must declare pi.extensions[0] as a non-empty string.`,
454
- );
455
- }
456
- const resolved = path.resolve(extDir, entry);
457
- if (fs.existsSync(resolved)) return resolved;
458
- throw new Error(
459
- `Permission-system extension entry ${JSON.stringify(entry)} in ${pkgPath} does not exist at ${resolved}.`,
460
- );
461
- } catch (error) {
462
- const message = error instanceof Error ? error.message : String(error);
463
- errors.push(message.startsWith("Permission-system") ? new Error(message) : new Error(`Cannot read permission-system package manifest at ${pkgPath}: ${message}`));
464
- }
465
- }
466
- if (errors.length > 0) throw errors[0]!;
467
- return undefined;
468
- }
469
-
470
- export function resolvePiLaunchToolPlan(
471
- input: ResolvePiLaunchToolPlanInput,
472
- ): PiLaunchToolPlan {
473
- const capabilityCeiling = intersectSubagentCapabilityCeilings(
474
- input.capabilityCeiling,
475
- input.inheritedCapabilityCeiling,
476
- );
477
- const allowedToolSet =
478
- capabilityCeiling?.allowedTools === undefined
479
- ? undefined
480
- : new Set(capabilityCeiling.allowedTools);
481
- const requestedBuiltinTools =
482
- input.tools?.filter(
483
- (tool) =>
484
- !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
485
- ) ?? [];
486
- if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
487
- throw new Error(
488
- `Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
489
- );
490
- }
491
- const declaredBuiltinTools =
492
- input.tools === undefined
493
- ? allowedToolSet
494
- ? [...allowedToolSet]
495
- : []
496
- : (input.requireReadTool &&
497
- requestedBuiltinTools.length > 0 &&
498
- !requestedBuiltinTools.includes("read") &&
499
- !allowedToolSet
500
- ? ["read", ...requestedBuiltinTools]
501
- : requestedBuiltinTools
502
- ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
503
- const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
504
- const excludedToolSet = new Set(excludeTools);
505
- const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
506
- const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
507
- input.allowNestedSubagents === true &&
508
- !excludedToolSet.has("subagent") &&
509
- (!allowedToolSet || allowedToolSet.has("subagent"))
510
- );
511
- const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
512
- ? []
513
- : (input.tools ?? []).filter(
514
- (tool) =>
515
- !requestedBuiltinTools.includes(tool) &&
516
- (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
517
- );
518
- const mcpResolution = capabilityCeiling?.denyExtensions
519
- ? { selections: [], unresolvedSelectors: [] }
520
- : resolveMcpDirectToolResolution(input.mcpDirectTools, input.cwd, input.runtimeSnapshotHost, input.mcpConfig);
521
- if (mcpResolution.unresolvedSelectors.length > 0) {
522
- throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
523
- }
524
- const resolvedMcpSelections = mcpResolution.selections;
525
- const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
526
- const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
527
- const effectiveMcpSelections = resolvedMcpSelections.filter(
528
- (selection) =>
529
- !allowedToolSet ||
530
- allowedToolSet.has(selection.name) ||
531
- isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
532
- ).filter((selection) => !excludedToolSet.has(selection.name));
533
- const effectiveMcpTools = effectiveMcpSelections.map(
534
- (selection) => selection.name,
535
- );
536
- const runtimeServerNames = mcpResolution.runtimeServerNames ?? input.runtimeServerNames ?? [];
537
- const effectiveRuntimeServerNames = runtimeServerNames.filter((serverName) =>
538
- effectiveMcpSelections.some((selection) => selection.selector.startsWith(`${serverName}/`)),
539
- );
540
- const effectiveMcpConfig = mcpResolution.mcpConfig ?? input.mcpConfig;
541
- const filteredMcpConfig = effectiveMcpConfig
542
- ? filterRuntimeMcpConfig(effectiveMcpConfig, runtimeServerNames, effectiveRuntimeServerNames)
543
- : undefined;
544
- const explicitToolAllowlist =
545
- input.tools !== undefined ||
546
- (input.mcpDirectTools?.length ?? 0) > 0 ||
547
- allowedToolSet !== undefined;
548
- const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
549
- const effectiveToolAllowlist = [
550
- ...new Set([
551
- ...effectiveDeclaredBuiltinTools,
552
- ...effectiveMcpTools,
553
- ...internalTools,
554
- ]),
555
- ];
556
- // Supervisor-coordination names stay in the --tools allowlist but are never
557
- // strict requirements: children register contact_supervisor at runtime through
558
- // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
559
- // appended intercom alongside contact_supervisor, so that exact pairing is
560
- // legacy plumbing, not a user demand for an external intercom provider;
561
- // a lone intercom entry stays strictly required (#1207).
562
- const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
563
- const requiredChildTools = explicitToolAllowlist
564
- ? [
565
- ...new Set([
566
- ...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
567
- ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
568
- ...internalTools,
569
- ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
570
- ]
571
- : [];
572
- const permSystemExt = capabilityCeiling?.denyExtensions
573
- ? undefined
574
- : hasPermissionRules(input.permissionRules)
575
- ? resolvePermissionSystemExtension()
576
- : undefined;
577
- if (input.fast && capabilityCeiling?.denyExtensions) throw new Error("fast mode requires a child runtime extension, but this launch denies extensions.");
578
- const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
579
- const runtimeExtensions = [
580
- PROMPT_RUNTIME_EXTENSION_PATH,
581
- ...fastModeExtensions,
582
- ...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
583
- ...(permSystemExt ? [permSystemExt] : []),
584
- ];
585
- const disableAmbientExtensions =
586
- capabilityCeiling?.denyExtensions === true ||
587
- input.extensions !== undefined;
588
- const warnings: string[] = [];
589
- // An explicit empty list disables ambient extensions, including model providers.
590
- if (capabilityCeiling?.denyExtensions !== true && Array.isArray(input.extensions) && input.extensions.length === 0) {
591
- const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
592
- warnings.push(
593
- `extensions: [] override${agentLabel} disables ALL ambient extensions for this child (not just "adds nothing"), `
594
- + "including any model-provider extension needed to resolve a provider-qualified model. "
595
- + "List the extensions this child actually needs instead of an empty array.",
596
- );
597
- }
598
- const configuredExtensions = capabilityCeiling?.denyExtensions
599
- ? []
600
- : [
601
- ...toolExtensionPaths,
602
- ...(input.extensions ?? []),
603
- ...(input.subagentOnlyExtensions ?? []),
604
- ];
605
- const extensionArgs = disableAmbientExtensions
606
- ? [...new Set([...runtimeExtensions, ...configuredExtensions])]
607
- : [
608
- ...new Set([
609
- ...runtimeExtensions,
610
- ...toolExtensionPaths,
611
- ...(input.subagentOnlyExtensions ?? []),
612
- ]),
613
- ];
614
- const requestedToolNames =
615
- input.tools !== undefined
616
- ? [
617
- ...new Set([
618
- ...requestedBuiltinTools,
619
- ...resolvedMcpSelections.map((selection) => selection.name),
620
- ]),
621
- ]
622
- : undefined;
623
- const capabilityAudit = capabilityCeiling
624
- ? ({
625
- ceiling: capabilityCeiling,
626
- ...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
627
- effectiveTools: effectiveToolAllowlist,
628
- ...(excludeTools.length > 0 ? { excludeTools } : {}),
629
- removedTools:
630
- requestedToolNames?.filter(
631
- (tool) => !effectiveToolAllowlist.includes(tool),
632
- ) ?? [],
633
- internalTools,
634
- extensionsDenied: capabilityCeiling.denyExtensions,
635
- removedExtensionCount: capabilityCeiling.denyExtensions
636
- ? (input.extensions?.length ?? 0) +
637
- (input.subagentOnlyExtensions?.length ?? 0) +
638
- (input.tools ?? []).filter(
639
- (tool) =>
640
- tool.includes("/") ||
641
- tool.endsWith(".ts") ||
642
- tool.endsWith(".js"),
643
- ).length
644
- : 0,
645
- requestedMcpToolCount: input.mcpDirectTools?.length ?? 0,
646
- effectiveMcpTools,
647
- agentAllowed:
648
- input.agentName === undefined
649
- ? true
650
- : isAgentAllowedByCapabilityCeiling(
651
- input.agentName,
652
- capabilityCeiling,
653
- ),
654
- ...(capabilityCeilingAgentRestrictionSources(capabilityCeiling)
655
- ? {
656
- agentRestrictionSources:
657
- capabilityCeilingAgentRestrictionSources(capabilityCeiling),
658
- }
659
- : {}),
660
- } satisfies SubagentCapabilityAudit)
661
- : undefined;
662
- return {
663
- ...(capabilityCeiling ? { capabilityCeiling } : {}),
664
- requestedBuiltinTools,
665
- declaredBuiltinTools,
666
- excludeTools,
667
- toolExtensionPaths,
668
- resolvedMcpSelections,
669
- effectiveMcpSelections,
670
- effectiveMcpTools,
671
- explicitToolAllowlist,
672
- internalTools,
673
- effectiveToolAllowlist,
674
- requiredChildTools,
675
- fanoutAuthorized,
676
- runtimeExtensions,
677
- configuredExtensions,
678
- extensionArgs,
679
- disableAmbientExtensions,
680
- warnings,
681
- ...(effectiveRuntimeServerNames.length > 0 && filteredMcpConfig
682
- ? { mcpConfig: filteredMcpConfig, runtimeServerNames: effectiveRuntimeServerNames }
683
- : {}),
684
- ...(capabilityAudit ? { capabilityAudit } : {}),
685
- };
686
- }
687
-
688
- // Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
689
- function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
690
- const counts = new Map<string, number>();
691
- for (const selection of selections) {
692
- const legacyName = legacyUnderscoreMcpToolName(selection);
693
- if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
694
- }
695
- return counts;
696
- }
697
-
698
- function isLegacyUnderscoreMcpToolAllowed(
699
- selection: ResolvedMcpDirectToolSelection,
700
- allowedToolSet: ReadonlySet<string>,
701
- resolvedMcpNames: ReadonlySet<string>,
702
- legacyMcpNameCounts: ReadonlyMap<string, number>,
703
- ): boolean {
704
- const legacyName = legacyUnderscoreMcpToolName(selection);
705
- return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
706
- }
707
-
708
- function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
709
- const slash = selection.selector.indexOf("/");
710
- if (slash < 1) return selection.name;
711
- const toolName = selection.selector.slice(slash + 1);
712
- const suffix = `_${toolName}`;
713
- if (!selection.name.endsWith(suffix)) return selection.name;
714
- const prefix = selection.name.slice(0, -suffix.length);
715
- return `${prefix.replace(/-/g, "_")}${suffix}`;
716
- }
717
-
718
- /** Escape XML-significant characters in a string for safe attribute interpolation. */
719
- function escapeXmlAttr(value: string): string {
720
- return value
721
- .replace(/&/g, "&amp;")
722
- .replace(/"/g, "&quot;")
723
- .replace(/</g, "&lt;")
724
- .replace(/>/g, "&gt;");
725
- }
726
-
727
- export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
728
- const args = [...input.baseArgs];
729
-
730
- if (input.sessionFile) {
731
- fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
732
- args.push("--session", input.sessionFile);
733
- } else {
734
- if (!input.sessionEnabled) {
735
- args.push("--no-session");
736
- }
737
- if (input.sessionDir) {
738
- fs.mkdirSync(input.sessionDir, { recursive: true });
739
- args.push("--session-dir", input.sessionDir);
740
- }
741
- }
742
-
743
- const modelArg = applyThinkingSuffix(input.model, input.thinking);
744
- if (modelArg) {
745
- args.push("--model", modelArg);
746
- }
747
-
748
- const toolPlan = resolvePiLaunchToolPlan({
749
- tools: input.tools,
750
- excludeTools: input.excludeTools,
751
- allowNestedSubagents: input.allowNestedSubagents,
752
- extensions: input.extensions,
753
- subagentOnlyExtensions: input.subagentOnlyExtensions,
754
- mcpDirectTools: input.mcpDirectTools,
755
- mcpConfig: input.mcpConfig,
756
- runtimeServerNames: input.runtimeServerNames,
757
- cwd: input.cwd,
758
- requireReadTool: input.requireReadTool,
759
- structuredOutput: input.structuredOutput,
760
- fast: input.fast,
761
- model: modelArg,
762
- modelCandidates: input.modelCandidates,
763
- capabilityCeiling: input.capabilityCeiling,
764
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(
765
- process.env[SUBAGENT_CAPABILITY_CEILING_ENV],
766
- ),
767
- agentName: input.childAgentName,
768
- permissionRules: input.permissionRules,
769
- runtimeSnapshotHost: input.runtimeSnapshotHost,
770
- });
771
- if (toolPlan.explicitToolAllowlist) {
772
- args.push(
773
- toolPlan.effectiveToolAllowlist.length > 0 ? "--tools" : "--no-tools",
774
- );
775
- if (toolPlan.effectiveToolAllowlist.length > 0)
776
- args.push(toolPlan.effectiveToolAllowlist.join(","));
777
- } else if (toolPlan.excludeTools.length > 0) {
778
- args.push("--exclude-tools", toolPlan.excludeTools.join(","));
779
- }
780
- if (toolPlan.disableAmbientExtensions) {
781
- args.push("--no-extensions");
782
- }
783
- for (const extPath of toolPlan.extensionArgs)
784
- args.push("--extension", extPath);
785
- let tempDir: string | undefined;
786
- if (toolPlan.mcpConfig && toolPlan.runtimeServerNames?.length) {
787
- if (!tempDir)
788
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
789
- const mcpConfigPath = path.join(tempDir, "mcp-config.json");
790
- fs.writeFileSync(mcpConfigPath, JSON.stringify(toolPlan.mcpConfig, null, 2), { mode: 0o600 });
791
- args.push("--mcp-config", mcpConfigPath);
792
- }
793
-
794
- if (!input.inheritProjectContext) {
795
- args.push("--no-context-files");
796
- }
797
- if (!input.inheritSkills) {
798
- args.push("--no-skills");
799
- }
800
-
801
- if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
802
- if (!tempDir)
803
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
804
- const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
805
- const promptPath = path.join(tempDir, `${stem}.md`);
806
- // Inject <active_agent> tag so @gotgenes/pi-permission-system can
807
- // resolve per-agent policy inside the child session.
808
- const taggedPrompt = input.childAgentName
809
- ? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
810
- : input.systemPrompt;
811
- fs.writeFileSync(promptPath, taggedPrompt, { mode: 0o600 });
812
- args.push(
813
- input.systemPromptMode === "replace"
814
- ? "--system-prompt"
815
- : "--append-system-prompt",
816
- promptPath,
817
- );
818
- }
819
-
820
- if (
821
- shouldDeliverTaskViaFile(
822
- input.task,
823
- input.taskDelivery ?? resolveSubagentTaskDelivery(),
824
- )
825
- ) {
826
- if (!tempDir) {
827
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
828
- }
829
- const taskFilePath = path.join(tempDir, "task.md");
830
- fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 });
831
- args.push(`@${taskFilePath}`);
832
- } else {
833
- args.push(`Task: ${input.task}`);
834
- }
835
-
836
- const env: Record<string, string | undefined> = {};
837
- env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
838
- const piPackageRoot =
839
- process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
840
- if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
841
- if (!tempDir)
842
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
843
- const runtimeAcknowledgedExtensionsPath = path.join(
844
- tempDir,
845
- "runtime-acknowledged-extensions.json",
846
- );
847
- env[RUNTIME_EXTENSION_ACK_PATH_ENV] = runtimeAcknowledgedExtensionsPath;
848
- let toolDiagnosticPath: string | undefined;
849
- // Child launch environments are merged over process.env. Explicitly clear
850
- // parent-scoped diagnostics so a nested zero-tool child cannot validate
851
- // against, or overwrite, its parent's required-tool report.
852
- env[REQUIRED_CHILD_TOOLS_ENV] = undefined;
853
- env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = undefined;
854
- if (toolPlan.requiredChildTools.length > 0) {
855
- if (!tempDir)
856
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
857
- toolDiagnosticPath = path.join(tempDir, "tool-diagnostic.json");
858
- env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(toolPlan.requiredChildTools);
859
- env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = toolDiagnosticPath;
860
- }
861
- env[MCP_DIRECT_CHILD_TOOLS_ENV] =
862
- toolPlan.effectiveMcpTools.length > 0
863
- ? JSON.stringify(toolPlan.effectiveMcpTools)
864
- : undefined;
865
- env[SUBAGENT_CHILD_ENV] = "1";
866
- env[SUBAGENT_FANOUT_CHILD_ENV] = toolPlan.fanoutAuthorized ? "1" : "0";
867
- if (input.waitToolEnabled !== undefined) {
868
- env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
869
- }
870
- if (input.waitToolDefaultTimeoutMs !== undefined) {
871
- env[WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV] = String(input.waitToolDefaultTimeoutMs);
872
- }
873
- const inheritedNestedRoute = Boolean(
874
- process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] &&
875
- process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] &&
876
- process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV],
877
- );
878
- const parentRunId =
879
- input.parentRunId ??
880
- input.runId ??
881
- (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ??
882
- process.env[SUBAGENT_PARENT_RUN_ID_ENV] ??
883
- "";
884
- const parentChildIndex =
885
- input.parentChildIndex !== undefined
886
- ? String(input.parentChildIndex)
887
- : input.childIndex !== undefined
888
- ? String(input.childIndex)
889
- : (process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? "");
890
- const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]);
891
- const parentDepth =
892
- input.parentDepth ??
893
- (inheritedNestedRoute && Number.isFinite(inheritedDepth)
894
- ? inheritedDepth + 1
895
- : 1);
896
- const parentPath = input.parentPath ?? [
897
- ...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]),
898
- ...(parentRunId
899
- ? [
900
- {
901
- runId: parentRunId,
902
- ...(parentChildIndex && /^\d+$/.test(parentChildIndex)
903
- ? { stepIndex: Number(parentChildIndex) }
904
- : {}),
905
- ...(input.childAgentName ? { agent: input.childAgentName } : {}),
906
- },
907
- ]
908
- : []),
909
- ];
910
- env[SUBAGENT_PARENT_EVENT_SINK_ENV] = toolPlan.fanoutAuthorized
911
- ? (input.parentEventSink ??
912
- process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ??
913
- "")
914
- : "";
915
- env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = toolPlan.fanoutAuthorized
916
- ? (input.parentControlInbox ??
917
- process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ??
918
- "")
919
- : "";
920
- env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
921
- ? (input.parentRootRunId ??
922
- process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ??
923
- input.runId ??
924
- "")
925
- : "";
926
- env[SUBAGENT_PARENT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
927
- ? parentRunId
928
- : "";
929
- env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = toolPlan.fanoutAuthorized
930
- ? parentChildIndex
931
- : "";
932
- env[SUBAGENT_PARENT_DEPTH_ENV] = toolPlan.fanoutAuthorized
933
- ? String(parentDepth)
934
- : "";
935
- env[SUBAGENT_PARENT_PATH_ENV] = toolPlan.fanoutAuthorized
936
- ? encodeNestedPathEnv(parentPath)
937
- : "";
938
- env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = toolPlan.fanoutAuthorized
939
- ? (input.parentCapabilityToken ??
940
- process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ??
941
- "")
942
- : "";
943
- env[RUN_FANOUT_BUDGET_ENV] = toolPlan.fanoutAuthorized
944
- ? (input.runFanoutBudget ? encodeRunFanoutBudgetDescriptor(input.runFanoutBudget) : process.env[RUN_FANOUT_BUDGET_ENV])
945
- : undefined;
946
- env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext
947
- ? "1"
948
- : "0";
949
- env[SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV] = input.inheritGlobalContext ? "1" : "0";
950
- env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
951
- env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
952
- env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
953
- if (input.intercomSessionName) {
954
- env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
955
- }
956
- if (input.sessionName?.trim()) {
957
- env.PI_SUBAGENT_SESSION_NAME = input.sessionName.trim();
958
- }
959
- if (input.orchestratorIntercomTarget) {
960
- env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
961
- }
962
- if (input.parentSessionId) {
963
- env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV] = input.parentSessionId;
964
- }
965
- const encodedPermissionRules = encodePermissionRules(input.permissionRules);
966
- if (
967
- input.orchestratorIntercomTarget &&
968
- input.parentSessionId &&
969
- input.runId &&
970
- input.childAgentName
971
- ) {
972
- const childIndex = input.childIndex ?? 0;
973
- const channelDir = supervisorChannelDir(
974
- input.runId,
975
- input.childAgentName,
976
- childIndex,
977
- );
978
- fs.mkdirSync(path.join(channelDir, "requests"), { recursive: true });
979
- fs.mkdirSync(path.join(channelDir, "replies"), { recursive: true });
980
- env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV] = channelDir;
981
- }
982
- if (encodedPermissionRules)
983
- env[PERMISSION_AUDIT_PATH_ENV] =
984
- input.permissionAuditPath ?? path.join(tempDir, "permission-audit.jsonl");
985
- if (input.runId) {
986
- env[SUBAGENT_RUN_ID_ENV] = input.runId;
987
- }
988
- if (input.childAgentName) {
989
- env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
990
- }
991
- if (input.childIndex !== undefined) {
992
- env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
993
- }
994
- if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length)
995
- env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
996
- else if (
997
- toolPlan.capabilityCeiling &&
998
- toolPlan.effectiveMcpSelections.length &&
999
- !toolPlan.capabilityCeiling.denyExtensions
1000
- )
1001
- env.MCP_DIRECT_TOOLS = toolPlan.effectiveMcpSelections
1002
- .map((selection) => selection.selector)
1003
- .join(",");
1004
- else env.MCP_DIRECT_TOOLS = "__none__";
1005
- const encodedCapabilityCeiling = encodeSubagentCapabilityCeiling(
1006
- toolPlan.capabilityCeiling,
1007
- );
1008
- const thinkingCeiling = intersectThinkingCeilings(
1009
- input.thinkingCeiling,
1010
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1011
- );
1012
- if (thinkingCeiling) env[SUBAGENT_THINKING_CEILING_ENV] = thinkingCeiling;
1013
- if (encodedCapabilityCeiling)
1014
- env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
1015
- if (encodedPermissionRules)
1016
- env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
1017
- env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
1018
- env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
1019
- if (input.structuredOutput) {
1020
- env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
1021
- env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
1022
- if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
1023
- if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
1024
- }
1025
- if (input.steerInboxDir) {
1026
- env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
1027
- }
1028
- if (input.steerCapabilityPath)
1029
- env[SUBAGENT_STEER_CAPABILITY_ENV] = input.steerCapabilityPath;
1030
- if (input.steerAckDir) env[SUBAGENT_STEER_ACK_DIR_ENV] = input.steerAckDir;
1031
- const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
1032
- if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
1033
- env[TOOL_BUDGET_ZERO_AUTH_ENV] = input.allowZeroToolBudget ? "1" : undefined;
1034
- const encodedChildWatchdog = encodeChildWatchdogConfig(input.childWatchdog);
1035
- if (encodedChildWatchdog)
1036
- env[CHILD_WATCHDOG_CONFIG_ENV] = encodedChildWatchdog;
1037
-
1038
- env[SUBAGENT_PARENT_SESSION_ENV] =
1039
- input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
1040
- env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
1041
-
1042
- return {
1043
- args,
1044
- env,
1045
- warnings: toolPlan.warnings,
1046
- tempDir,
1047
- toolDiagnosticPath,
1048
- runtimeAcknowledgedExtensionsPath,
1049
- capabilityAudit: toolPlan.capabilityAudit,
1050
- };
1051
- }
1052
-
1053
- export const parseParentPathEnv = parseNestedPathEnv;
1054
-
1055
- export function cleanupTempDir(tempDir: string | null | undefined): void {
1056
- if (!tempDir) return;
1057
- try {
1058
- fs.rmSync(tempDir, { recursive: true, force: true });
1059
- } catch {
1060
- // Temp cleanup is best effort.
1061
- }
1062
- }