pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -3,14 +3,17 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
6
- import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
6
+ import { resolveMcpDirectToolSelections, type ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-allowlist.ts";
7
+ import { resolvePiPackageRoot } from "./pi-spawn.ts";
7
8
  import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
8
9
  import { TEMP_ROOT_DIR, type JsonSchemaObject, type ResolvedToolBudget } from "../../shared/types.ts";
9
10
  import { THINKING_LEVELS } from "../../shared/model-info.ts";
10
- import { TOOL_BUDGET_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
11
- import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
11
+ import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
12
+ import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, MCP_DIRECT_CHILD_TOOLS_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
12
13
  import { CHILD_WATCHDOG_CONFIG_ENV, encodeChildWatchdogConfig, type ChildWatchdogConfig } from "../../watchdog/child-status.ts";
13
14
  import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
15
+ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
16
+ import { SUBAGENT_CAPABILITY_CEILING_ENV, decodeSubagentCapabilityCeiling, encodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "./capability-ceiling.ts";
14
17
 
15
18
  const TASK_ARG_LIMIT = 8000;
16
19
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
@@ -36,7 +39,7 @@ export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
36
39
  export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
37
40
  export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
38
41
 
39
- interface BuildPiArgsInput {
42
+ export interface BuildPiArgsInput {
40
43
  parentSessionId?: string;
41
44
  baseArgs: string[];
42
45
  task: string;
@@ -78,15 +81,18 @@ interface BuildPiArgsInput {
78
81
  outputPath: string;
79
82
  };
80
83
  toolBudget?: ResolvedToolBudget;
84
+ allowZeroToolBudget?: boolean;
81
85
  childWatchdog?: ChildWatchdogConfig;
82
86
  waitToolEnabled?: boolean;
87
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
83
88
  }
84
89
 
85
- interface BuildPiArgsResult {
90
+ export interface BuildPiArgsResult {
86
91
  args: string[];
87
92
  env: Record<string, string | undefined>;
88
93
  tempDir?: string;
89
94
  toolDiagnosticPath?: string;
95
+ capabilityAudit?: SubagentCapabilityAudit;
90
96
  }
91
97
 
92
98
  function sanitizeSupervisorChannelSegment(value: string): string {
@@ -106,6 +112,106 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
106
112
  return `${model}:${thinking}`;
107
113
  }
108
114
 
115
+ export interface ResolvePiLaunchToolPlanInput {
116
+ tools?: string[];
117
+ extensions?: string[];
118
+ subagentOnlyExtensions?: string[];
119
+ mcpDirectTools?: string[];
120
+ cwd?: string;
121
+ requireReadTool?: boolean;
122
+ structuredOutput?: boolean;
123
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
124
+ inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
125
+ }
126
+
127
+ export interface PiLaunchToolPlan {
128
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
129
+ requestedBuiltinTools: string[];
130
+ declaredBuiltinTools: string[];
131
+ toolExtensionPaths: string[];
132
+ resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
133
+ effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
134
+ effectiveMcpTools: string[];
135
+ explicitToolAllowlist: boolean;
136
+ internalTools: string[];
137
+ effectiveToolAllowlist: string[];
138
+ requiredChildTools: string[];
139
+ fanoutAuthorized: boolean;
140
+ runtimeExtensions: string[];
141
+ configuredExtensions: string[];
142
+ extensionArgs: string[];
143
+ disableAmbientExtensions: boolean;
144
+ capabilityAudit?: SubagentCapabilityAudit;
145
+ }
146
+
147
+ export function resolvePiLaunchToolPlan(input: ResolvePiLaunchToolPlanInput): PiLaunchToolPlan {
148
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(input.capabilityCeiling, input.inheritedCapabilityCeiling);
149
+ const allowedToolSet = capabilityCeiling?.allowedTools === undefined ? undefined : new Set(capabilityCeiling.allowedTools);
150
+ const requestedBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
151
+ if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
152
+ throw new Error(`Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`);
153
+ }
154
+ const declaredBuiltinTools = input.tools === undefined
155
+ ? (allowedToolSet ? [...allowedToolSet] : [])
156
+ : (input.requireReadTool && requestedBuiltinTools.length > 0 && !requestedBuiltinTools.includes("read") && !allowedToolSet
157
+ ? ["read", ...requestedBuiltinTools]
158
+ : requestedBuiltinTools).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
159
+ const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
160
+ const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions ? [] : (input.tools ?? []).filter((tool) => !requestedBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")));
161
+ const resolvedMcpSelections = capabilityCeiling?.denyExtensions ? [] : resolveMcpDirectToolSelections(input.mcpDirectTools, input.cwd);
162
+ const effectiveMcpSelections = resolvedMcpSelections.filter((selection) => !allowedToolSet || allowedToolSet.has(selection.name));
163
+ const effectiveMcpTools = effectiveMcpSelections.map((selection) => selection.name);
164
+ const explicitToolAllowlist = input.tools !== undefined || (input.mcpDirectTools?.length ?? 0) > 0 || allowedToolSet !== undefined;
165
+ const internalTools = input.structuredOutput ? ["structured_output"] : [];
166
+ const effectiveToolAllowlist = [...new Set([...declaredBuiltinTools, ...effectiveMcpTools, ...internalTools])];
167
+ const requiredChildTools = explicitToolAllowlist ? [...new Set([
168
+ ...(input.tools !== undefined ? declaredBuiltinTools : []),
169
+ ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
170
+ ...internalTools,
171
+ ])] : [];
172
+ const runtimeExtensions = fanoutAuthorized
173
+ ? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
174
+ : [PROMPT_RUNTIME_EXTENSION_PATH];
175
+ const disableAmbientExtensions = capabilityCeiling?.denyExtensions === true || input.extensions !== undefined;
176
+ const configuredExtensions = capabilityCeiling?.denyExtensions ? [] : [...toolExtensionPaths, ...(input.extensions ?? []), ...(input.subagentOnlyExtensions ?? [])];
177
+ const extensionArgs = disableAmbientExtensions
178
+ ? [...new Set([...runtimeExtensions, ...configuredExtensions])]
179
+ : [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])];
180
+ const requestedToolNames = input.tools !== undefined
181
+ ? [...new Set([...requestedBuiltinTools, ...resolvedMcpSelections.map((selection) => selection.name)])]
182
+ : undefined;
183
+ const capabilityAudit = capabilityCeiling ? {
184
+ ceiling: capabilityCeiling,
185
+ ...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
186
+ effectiveTools: effectiveToolAllowlist,
187
+ removedTools: requestedToolNames?.filter((tool) => !effectiveToolAllowlist.includes(tool)) ?? [],
188
+ internalTools,
189
+ extensionsDenied: capabilityCeiling.denyExtensions,
190
+ removedExtensionCount: capabilityCeiling.denyExtensions ? (input.extensions?.length ?? 0) + (input.subagentOnlyExtensions?.length ?? 0) + ((input.tools ?? []).filter((tool) => tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")).length) : 0,
191
+ requestedMcpToolCount: input.mcpDirectTools?.length ?? 0,
192
+ effectiveMcpTools,
193
+ } satisfies SubagentCapabilityAudit : undefined;
194
+ return {
195
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
196
+ requestedBuiltinTools,
197
+ declaredBuiltinTools,
198
+ toolExtensionPaths,
199
+ resolvedMcpSelections,
200
+ effectiveMcpSelections,
201
+ effectiveMcpTools,
202
+ explicitToolAllowlist,
203
+ internalTools,
204
+ effectiveToolAllowlist,
205
+ requiredChildTools,
206
+ fanoutAuthorized,
207
+ runtimeExtensions,
208
+ configuredExtensions,
209
+ extensionArgs,
210
+ disableAmbientExtensions,
211
+ ...(capabilityAudit ? { capabilityAudit } : {}),
212
+ };
213
+ }
214
+
109
215
  export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
110
216
  const args = [...input.baseArgs];
111
217
 
@@ -127,43 +233,25 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
127
233
  args.push("--model", modelArg);
128
234
  }
129
235
 
130
- const declaredBuiltinToolsBase = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
131
- const declaredBuiltinTools = input.requireReadTool && input.tools?.length && !declaredBuiltinToolsBase.includes("read")
132
- ? ["read", ...declaredBuiltinToolsBase]
133
- : declaredBuiltinToolsBase;
134
- const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
135
- const toolExtensionPaths: string[] = [];
136
- let requiredChildTools: string[] = [];
137
- if (input.tools?.length) {
138
- const allowedTools = [...declaredBuiltinTools];
139
- for (const tool of input.tools) {
140
- if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
141
- toolExtensionPaths.push(tool);
142
- }
143
- }
144
- if (allowedTools.length > 0) {
145
- if (input.mcpDirectTools?.length) {
146
- allowedTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd));
147
- }
148
- if (input.structuredOutput) allowedTools.push("structured_output");
149
- requiredChildTools = [...new Set(allowedTools)];
150
- args.push("--tools", requiredChildTools.join(","));
151
- }
236
+ const toolPlan = resolvePiLaunchToolPlan({
237
+ tools: input.tools,
238
+ extensions: input.extensions,
239
+ subagentOnlyExtensions: input.subagentOnlyExtensions,
240
+ mcpDirectTools: input.mcpDirectTools,
241
+ cwd: input.cwd,
242
+ requireReadTool: input.requireReadTool,
243
+ structuredOutput: input.structuredOutput,
244
+ capabilityCeiling: input.capabilityCeiling,
245
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
246
+ });
247
+ if (toolPlan.explicitToolAllowlist) {
248
+ args.push(toolPlan.effectiveToolAllowlist.length > 0 ? "--tools" : "--no-tools");
249
+ if (toolPlan.effectiveToolAllowlist.length > 0) args.push(toolPlan.effectiveToolAllowlist.join(","));
152
250
  }
153
-
154
- const runtimeExtensions = fanoutAuthorized
155
- ? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
156
- : [PROMPT_RUNTIME_EXTENSION_PATH];
157
- if (input.extensions !== undefined) {
251
+ if (toolPlan.disableAmbientExtensions) {
158
252
  args.push("--no-extensions");
159
- for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
160
- args.push("--extension", extPath);
161
- }
162
- } else {
163
- for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
164
- args.push("--extension", extPath);
165
- }
166
253
  }
254
+ for (const extPath of toolPlan.extensionArgs) args.push("--extension", extPath);
167
255
 
168
256
  if (!input.inheritSkills) {
169
257
  args.push("--no-skills");
@@ -190,15 +278,18 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
190
278
  }
191
279
 
192
280
  const env: Record<string, string | undefined> = {};
281
+ const piPackageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
282
+ if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
193
283
  let toolDiagnosticPath: string | undefined;
194
- if (requiredChildTools.length > 0) {
284
+ if (toolPlan.requiredChildTools.length > 0) {
195
285
  if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
196
286
  toolDiagnosticPath = path.join(tempDir, "tool-diagnostic.json");
197
- env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(requiredChildTools);
287
+ env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(toolPlan.requiredChildTools);
198
288
  env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = toolDiagnosticPath;
199
289
  }
290
+ env[MCP_DIRECT_CHILD_TOOLS_ENV] = toolPlan.effectiveMcpTools.length > 0 ? JSON.stringify(toolPlan.effectiveMcpTools) : undefined;
200
291
  env[SUBAGENT_CHILD_ENV] = "1";
201
- env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
292
+ env[SUBAGENT_FANOUT_CHILD_ENV] = toolPlan.fanoutAuthorized ? "1" : "0";
202
293
  if (input.waitToolEnabled !== undefined) {
203
294
  env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
204
295
  }
@@ -219,20 +310,20 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
219
310
  ...(input.childAgentName ? { agent: input.childAgentName } : {}),
220
311
  }] : []),
221
312
  ];
222
- env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
313
+ env[SUBAGENT_PARENT_EVENT_SINK_ENV] = toolPlan.fanoutAuthorized
223
314
  ? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
224
315
  : "";
225
- env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
316
+ env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = toolPlan.fanoutAuthorized
226
317
  ? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
227
318
  : "";
228
- env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
319
+ env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
229
320
  ? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
230
321
  : "";
231
- env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
232
- env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
233
- env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
234
- env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
235
- env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized
322
+ env[SUBAGENT_PARENT_RUN_ID_ENV] = toolPlan.fanoutAuthorized ? parentRunId : "";
323
+ env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = toolPlan.fanoutAuthorized ? parentChildIndex : "";
324
+ env[SUBAGENT_PARENT_DEPTH_ENV] = toolPlan.fanoutAuthorized ? String(parentDepth) : "";
325
+ env[SUBAGENT_PARENT_PATH_ENV] = toolPlan.fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
326
+ env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = toolPlan.fanoutAuthorized
236
327
  ? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
237
328
  : "";
238
329
  env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
@@ -262,11 +353,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
262
353
  if (input.childIndex !== undefined) {
263
354
  env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
264
355
  }
265
- if (input.mcpDirectTools?.length) {
266
- env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
267
- } else {
268
- env.MCP_DIRECT_TOOLS = "__none__";
269
- }
356
+ if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length) env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
357
+ else if (toolPlan.capabilityCeiling && toolPlan.effectiveMcpSelections.length && !toolPlan.capabilityCeiling.denyExtensions) env.MCP_DIRECT_TOOLS = toolPlan.effectiveMcpSelections.map((selection) => selection.selector).join(",");
358
+ else env.MCP_DIRECT_TOOLS = "__none__";
359
+ const encodedCapabilityCeiling = encodeSubagentCapabilityCeiling(toolPlan.capabilityCeiling);
360
+ if (encodedCapabilityCeiling) env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
270
361
  if (input.structuredOutput) {
271
362
  env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
272
363
  env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
@@ -278,12 +369,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
278
369
  if (input.steerAckDir) env[SUBAGENT_STEER_ACK_DIR_ENV] = input.steerAckDir;
279
370
  const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
280
371
  if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
372
+ env[TOOL_BUDGET_ZERO_AUTH_ENV] = input.allowZeroToolBudget ? "1" : undefined;
281
373
  const encodedChildWatchdog = encodeChildWatchdogConfig(input.childWatchdog);
282
374
  if (encodedChildWatchdog) env[CHILD_WATCHDOG_CONFIG_ENV] = encodedChildWatchdog;
283
375
 
284
376
  env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
285
377
 
286
- return { args, env, tempDir, toolDiagnosticPath };
378
+ return { args, env, tempDir, toolDiagnosticPath, capabilityAudit: toolPlan.capabilityAudit };
287
379
  }
288
380
 
289
381
  export const parseParentPathEnv = parseNestedPathEnv;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { getAgentDir } from "../../shared/utils.ts";
@@ -5,6 +6,7 @@ import { getAgentDir } from "../../shared/utils.ts";
5
6
  export interface RunEntry {
6
7
  agent: string;
7
8
  task: string;
9
+ taskHash?: string;
8
10
  ts: number;
9
11
  status: "ok" | "error";
10
12
  duration: number;
@@ -13,24 +15,103 @@ export interface RunEntry {
13
15
 
14
16
  const ROTATE_READ_THRESHOLD = 1200;
15
17
  const ROTATE_KEEP = 1000;
18
+ const PRIVATE_DIR_MODE = 0o700;
19
+ const PRIVATE_FILE_MODE = 0o600;
20
+ const REDACTED_TASK = "[redacted]";
16
21
 
17
22
  function getHistoryPath(): string {
18
23
  return path.join(getAgentDir(), "run-history.jsonl");
19
24
  }
20
25
 
26
+ function hashTask(task: string): string {
27
+ return createHash("sha256").update(task).digest("hex");
28
+ }
29
+
30
+ function hardenHistoryStorage(historyPath: string): void {
31
+ const historyDir = path.dirname(historyPath);
32
+ fs.mkdirSync(historyDir, { recursive: true, mode: PRIVATE_DIR_MODE });
33
+ try { fs.chmodSync(historyDir, PRIVATE_DIR_MODE); } catch {}
34
+ try { if (fs.existsSync(historyPath)) fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
35
+ }
36
+
37
+ function sanitizeHistoryLine(line: string): string | undefined {
38
+ let value: unknown;
39
+ try {
40
+ value = JSON.parse(line);
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ if (!value || typeof value !== "object") return undefined;
45
+
46
+ const record = value as Record<string, unknown>;
47
+ const task = typeof record.task === "string" ? record.task : "";
48
+ const taskHash = typeof record.taskHash === "string" && record.taskHash
49
+ ? record.taskHash
50
+ : task && task !== REDACTED_TASK
51
+ ? hashTask(task)
52
+ : undefined;
53
+
54
+ return JSON.stringify({
55
+ ...record,
56
+ task: REDACTED_TASK,
57
+ ...(taskHash ? { taskHash } : {}),
58
+ });
59
+ }
60
+
61
+ function sanitizeHistoryLines(raw: string): { lines: string[]; changed: boolean } {
62
+ const lines: string[] = [];
63
+ let changed = false;
64
+ for (const line of raw.split("\n")) {
65
+ const trimmed = line.trim();
66
+ if (!trimmed) continue;
67
+ const sanitized = sanitizeHistoryLine(trimmed);
68
+ if (!sanitized) {
69
+ changed = true;
70
+ continue;
71
+ }
72
+ if (sanitized !== trimmed) changed = true;
73
+ lines.push(sanitized);
74
+ }
75
+ return { lines, changed };
76
+ }
77
+
78
+ function writePrivateHistory(historyPath: string, lines: string[]): void {
79
+ fs.writeFileSync(historyPath, lines.length ? `${lines.join("\n")}\n` : "", { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
80
+ try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
81
+ }
82
+
83
+ function sanitizeHistoryFile(historyPath: string): void {
84
+ if (!fs.existsSync(historyPath)) return;
85
+ const raw = fs.readFileSync(historyPath, "utf-8");
86
+ const { lines, changed } = sanitizeHistoryLines(raw);
87
+ if (changed) writePrivateHistory(historyPath, lines);
88
+ }
89
+
90
+ function appendPrivateHistoryLine(historyPath: string, line: string): void {
91
+ const fd = fs.openSync(historyPath, fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, PRIVATE_FILE_MODE);
92
+ try {
93
+ fs.writeSync(fd, `${line}\n`);
94
+ } finally {
95
+ fs.closeSync(fd);
96
+ }
97
+ try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
98
+ }
99
+
21
100
  export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
22
101
  try {
23
102
  const entry: RunEntry = {
24
103
  agent,
25
- task: task.slice(0, 200),
104
+ task: REDACTED_TASK,
105
+ taskHash: hashTask(task),
26
106
  ts: Math.floor(Date.now() / 1000),
27
107
  status: exitCode === 0 ? "ok" : "error",
28
108
  duration: durationMs,
29
109
  ...(exitCode !== 0 ? { exit: exitCode } : {}),
30
110
  };
31
111
  const historyPath = getHistoryPath();
32
- fs.mkdirSync(path.dirname(historyPath), { recursive: true });
33
- fs.appendFileSync(historyPath, `${JSON.stringify(entry)}\n`);
112
+ hardenHistoryStorage(historyPath);
113
+ try { sanitizeHistoryFile(historyPath); } catch {}
114
+ appendPrivateHistoryLine(historyPath, JSON.stringify(entry));
34
115
  } catch {
35
116
  // Best-effort — never crash the execution flow for history recording
36
117
  }
@@ -38,6 +119,7 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
38
119
 
39
120
  export function loadRunsForAgent(agent: string): RunEntry[] {
40
121
  const historyPath = getHistoryPath();
122
+ try { hardenHistoryStorage(historyPath); } catch {}
41
123
  if (!fs.existsSync(historyPath)) return [];
42
124
  let raw: string;
43
125
  try {
@@ -46,11 +128,14 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
46
128
  return [];
47
129
  }
48
130
 
49
- let lines = raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
131
+ let { lines, changed } = sanitizeHistoryLines(raw);
50
132
 
51
133
  if (lines.length > ROTATE_READ_THRESHOLD) {
52
134
  lines = lines.slice(-ROTATE_KEEP);
53
- try { fs.writeFileSync(historyPath, `${lines.join("\n")}\n`, "utf-8"); } catch {}
135
+ changed = true;
136
+ }
137
+ if (changed) {
138
+ try { writePrivateHistory(historyPath, lines); } catch {}
54
139
  }
55
140
 
56
141
  return lines
@@ -37,9 +37,14 @@ export interface SessionLeaseHandle {
37
37
  leaseDir: string;
38
38
  owner: SessionLeaseOwner;
39
39
  updateWriter(writer: { state: "none" | "spawning" } | { state: "running"; pid: number }): void;
40
- release(): void;
40
+ release(): boolean;
41
41
  }
42
42
 
43
+ export type SessionLeaseState =
44
+ | { state: "free"; canonicalSessionFile: string; canonicalSessionId: string }
45
+ | { state: "owned"; canonicalSessionFile: string; canonicalSessionId: string; owner: SessionLeaseOwner }
46
+ | { state: "unreadable"; canonicalSessionFile: string; canonicalSessionId: string };
47
+
43
48
  interface SessionLeaseOptions {
44
49
  rootDir?: string;
45
50
  now?: () => number;
@@ -98,11 +103,25 @@ export function canonicalSessionFilePath(sessionFile: string): string {
98
103
  return fs.realpathSync.native(path.resolve(sessionFile));
99
104
  }
100
105
 
101
- export function sessionLeaseDir(sessionFile: string, rootDir = SESSION_LEASES_DIR): string {
106
+ export function canonicalSessionId(sessionFile: string): string {
102
107
  const canonical = canonicalSessionFilePath(sessionFile);
103
108
  const key = process.platform === "win32" ? canonical.toLowerCase() : canonical;
104
- const digest = createHash("sha256").update(key).digest("hex");
105
- return path.join(rootDir, digest);
109
+ return createHash("sha256").update(key).digest("hex");
110
+ }
111
+
112
+ export function sessionLeaseDir(sessionFile: string, rootDir = SESSION_LEASES_DIR): string {
113
+ return path.join(rootDir, canonicalSessionId(sessionFile));
114
+ }
115
+
116
+ export function inspectSessionLease(sessionFile: string, rootDir = SESSION_LEASES_DIR): SessionLeaseState {
117
+ const canonicalSessionFile = canonicalSessionFilePath(sessionFile);
118
+ const canonicalSessionIdValue = canonicalSessionId(canonicalSessionFile);
119
+ const leaseDir = path.join(rootDir, canonicalSessionIdValue);
120
+ if (!fs.existsSync(leaseDir)) return { state: "free", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
121
+ const owner = readLeaseOwner(leaseDir);
122
+ return owner
123
+ ? { state: "owned", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue, owner }
124
+ : { state: "unreadable", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
106
125
  }
107
126
 
108
127
  function parseOwner(value: unknown): SessionLeaseOwner | undefined {
@@ -251,8 +270,9 @@ export function acquireSessionLease(request: SessionLeaseRequest, options: Sessi
251
270
  },
252
271
  release() {
253
272
  const currentOwner = readLeaseOwner(leaseDir);
254
- if (!currentOwner || currentOwner.token !== owner.token) return;
273
+ if (!currentOwner || currentOwner.token !== owner.token) return false;
255
274
  fs.rmSync(leaseDir, { recursive: true, force: true });
275
+ return !fs.existsSync(leaseDir);
256
276
  },
257
277
  };
258
278
  }
@@ -1,11 +1,14 @@
1
1
  import * as fs from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
- import { Compile } from "typebox/compile";
5
+ import { pathToFileURL } from "node:url";
6
+ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
5
7
  import type { JsonSchemaObject } from "../../shared/types.ts";
6
8
 
7
9
  export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
8
10
  export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
11
+ export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
9
12
 
10
13
  export interface StructuredOutputRuntime {
11
14
  schema: JsonSchemaObject;
@@ -13,11 +16,108 @@ export interface StructuredOutputRuntime {
13
16
  outputPath: string;
14
17
  }
15
18
 
19
+ const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
20
+ const SCHEMA_SINGLE_KEYWORDS = ["additionalItems", "additionalProperties", "contains", "not", "propertyNames", "if", "then", "else", "unevaluatedItems", "unevaluatedProperties", "contentSchema"] as const;
21
+ const SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"] as const;
22
+
23
+ function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inheritsWrapperResource = true): unknown {
24
+ if (typeof schema === "boolean" || !schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
25
+ const source = schema as Record<string, unknown>;
26
+ const rewritten: Record<string, unknown> = { ...source };
27
+ const sharesWrapperResource = inheritsWrapperResource && typeof source.$id !== "string";
28
+ if (sharesWrapperResource) {
29
+ for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"] as const) {
30
+ const ref = source[keyword];
31
+ if (ref === "#") rewritten[keyword] = pointerPrefix;
32
+ else if (typeof ref === "string" && ref.startsWith("#/")) rewritten[keyword] = `${pointerPrefix}${ref.slice(1)}`;
33
+ }
34
+ }
35
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
36
+ const entries = source[keyword];
37
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
38
+ rewritten[keyword] = Object.fromEntries(Object.entries(entries).map(([name, nested]) => [
39
+ name,
40
+ rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
41
+ ]));
42
+ }
43
+ const items = source.items;
44
+ if (Array.isArray(items)) rewritten.items = items.map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
45
+ else if (items !== undefined) rewritten.items = rewriteLocalJsonPointerRefs(items, pointerPrefix, sharesWrapperResource);
46
+ for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
47
+ if (source[keyword] !== undefined) rewritten[keyword] = rewriteLocalJsonPointerRefs(source[keyword], pointerPrefix, sharesWrapperResource);
48
+ }
49
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
50
+ if (Array.isArray(source[keyword])) rewritten[keyword] = source[keyword].map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
51
+ }
52
+ const dependencies = source.dependencies;
53
+ if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
54
+ rewritten.dependencies = Object.fromEntries(Object.entries(dependencies).map(([name, nested]) => [
55
+ name,
56
+ Array.isArray(nested) ? nested : rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
57
+ ]));
58
+ }
59
+ return rewritten;
60
+ }
61
+
62
+ export function createStructuredOutputToolParameters(schema: JsonSchemaObject): JsonSchemaObject {
63
+ return {
64
+ type: "object",
65
+ properties: { value: rewriteLocalJsonPointerRefs(schema, "#/properties/value") },
66
+ required: ["value"],
67
+ additionalProperties: false,
68
+ };
69
+ }
70
+
16
71
  interface CompiledJsonSchema {
17
72
  Check(value: unknown): boolean;
18
73
  Errors(value: unknown): Iterable<{ instancePath?: string; message?: string }>;
19
74
  }
20
75
 
76
+ type CompileJsonSchema = (schema: unknown) => CompiledJsonSchema;
77
+
78
+ let cachedCompile: Promise<CompileJsonSchema> | undefined;
79
+
80
+ export async function resolveCompileFromPackageRoot(packageRoot: string): Promise<CompileJsonSchema | undefined> {
81
+ const requireFromRoot = createRequire(path.join(packageRoot, "package.json"));
82
+ const resolved = requireFromRoot.resolve("typebox/compile");
83
+ const mod = (await import(pathToFileURL(resolved).href)) as { Compile?: unknown };
84
+ return typeof mod.Compile === "function" ? (mod.Compile as CompileJsonSchema) : undefined;
85
+ }
86
+
87
+ async function importCompile(): Promise<CompileJsonSchema> {
88
+ const failures: string[] = [];
89
+ try {
90
+ const mod = (await import("typebox/compile")) as { Compile?: unknown };
91
+ if (typeof mod.Compile === "function") return mod.Compile as CompileJsonSchema;
92
+ failures.push("typebox/compile did not export a Compile function");
93
+ } catch (error) {
94
+ failures.push(`direct import failed: ${error instanceof Error ? error.message : String(error)}`);
95
+ }
96
+ const packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
97
+ if (packageRoot) {
98
+ try {
99
+ const compile = await resolveCompileFromPackageRoot(packageRoot);
100
+ if (compile) return compile;
101
+ failures.push("Pi package root typebox/compile did not export a Compile function");
102
+ } catch (error) {
103
+ failures.push(`Pi package root import failed: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ } else {
106
+ failures.push(`${PI_CODING_AGENT_PACKAGE_ROOT_ENV} is not set`);
107
+ }
108
+ throw new Error(`Cannot load typebox/compile for structured output validation (${failures.join("; ")})`);
109
+ }
110
+
111
+ function loadCompile(): Promise<CompileJsonSchema> {
112
+ if (!cachedCompile) {
113
+ cachedCompile = importCompile().catch((error) => {
114
+ cachedCompile = undefined;
115
+ throw error;
116
+ });
117
+ }
118
+ return cachedCompile;
119
+ }
120
+
21
121
  export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
22
122
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
23
123
  throw new Error(`${label} must be a JSON Schema object.`);
@@ -35,10 +135,11 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
35
135
  return { schema, schemaPath, outputPath };
36
136
  }
37
137
 
38
- export function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): { status: "valid" } | { status: "invalid"; message: string } {
138
+ export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
139
+ const compile = await loadCompile();
39
140
  let validator: CompiledJsonSchema;
40
141
  try {
41
- validator = (Compile as (schema: unknown) => CompiledJsonSchema)(schema);
142
+ validator = compile(schema);
42
143
  } catch (error) {
43
144
  return { status: "invalid", message: `invalid outputSchema: ${error instanceof Error ? error.message : String(error)}` };
44
145
  }
@@ -52,9 +153,9 @@ export function validateStructuredOutputValue(schema: JsonSchemaObject, value: u
52
153
  return { status: "invalid", message: errors.join("; ") || "schema validation failed" };
53
154
  }
54
155
 
55
- export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
156
+ export async function readStructuredOutput(runtime: StructuredOutputRuntime): Promise<{ value?: unknown; error?: string }> {
56
157
  if (!fs.existsSync(runtime.outputPath)) {
57
- return { error: "Missing structured_output call; this step has outputSchema and must finish by calling structured_output." };
158
+ return { error: MISSING_STRUCTURED_OUTPUT_CALL_ERROR };
58
159
  }
59
160
  let value: unknown;
60
161
  try {
@@ -62,8 +163,12 @@ export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?
62
163
  } catch (error) {
63
164
  return { error: `Failed to read structured output: ${error instanceof Error ? error.message : String(error)}` };
64
165
  }
65
- const validation = validateStructuredOutputValue(runtime.schema, value);
66
- if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` };
166
+ try {
167
+ const validation = await validateStructuredOutputValue(runtime.schema, value);
168
+ if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` };
169
+ } catch (error) {
170
+ return { error: `Failed to validate structured output: ${error instanceof Error ? error.message : String(error)}` };
171
+ }
67
172
  return { value };
68
173
  }
69
174