pi-subagents 0.36.0 → 0.37.1

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 (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +111 -16
  3. package/agents/delegate.md +2 -0
  4. package/agents/planner.md +2 -1
  5. package/agents/worker.md +2 -0
  6. package/package.json +6 -4
  7. package/skills/pi-subagents/SKILL.md +18 -973
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  9. package/skills/pi-subagents/references/execution-controls.md +411 -0
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
  12. package/src/agents/agent-management.ts +7 -1
  13. package/src/agents/agents.ts +99 -12
  14. package/src/api/capability-ceiling.ts +17 -0
  15. package/src/api/delegation.ts +6 -1
  16. package/src/api/preflight.ts +399 -0
  17. package/src/extension/index.ts +16 -7
  18. package/src/extension/rpc.ts +29 -2
  19. package/src/extension/schemas.ts +10 -4
  20. package/src/extension/tool-description.ts +4 -2
  21. package/src/intercom/intercom-bridge.ts +5 -2
  22. package/src/runs/background/async-execution.ts +125 -14
  23. package/src/runs/background/async-job-tracker.ts +19 -12
  24. package/src/runs/background/async-resume.ts +27 -9
  25. package/src/runs/background/async-status.ts +18 -0
  26. package/src/runs/background/notify.ts +3 -0
  27. package/src/runs/background/process-terminal.ts +280 -0
  28. package/src/runs/background/result-watcher.ts +9 -3
  29. package/src/runs/background/run-status.ts +7 -1
  30. package/src/runs/background/scheduled-runs.ts +6 -1
  31. package/src/runs/background/stale-run-reconciler.ts +6 -0
  32. package/src/runs/background/subagent-runner.ts +182 -12
  33. package/src/runs/foreground/chain-execution.ts +5 -0
  34. package/src/runs/foreground/execution.ts +59 -13
  35. package/src/runs/foreground/subagent-executor.ts +142 -47
  36. package/src/runs/shared/acceptance.ts +41 -30
  37. package/src/runs/shared/capability-ceiling.ts +177 -0
  38. package/src/runs/shared/dynamic-fanout.ts +1 -1
  39. package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
  40. package/src/runs/shared/model-fallback.ts +8 -0
  41. package/src/runs/shared/nested-events.ts +8 -1
  42. package/src/runs/shared/parallel-utils.ts +5 -0
  43. package/src/runs/shared/pi-args.ts +141 -58
  44. package/src/runs/shared/session-lease.ts +25 -5
  45. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  46. package/src/runs/shared/task-intent.ts +1 -1
  47. package/src/runs/shared/tool-availability.ts +18 -2
  48. package/src/shared/launch-contract.ts +123 -0
  49. package/src/shared/types.ts +118 -8
  50. package/src/shared/utils.ts +17 -42
  51. package/src/slash/delegation-adapters.ts +6 -1
  52. package/src/slash/slash-commands.ts +1 -1
  53. package/src/tui/fleet-status.ts +62 -16
  54. package/src/tui/fleet.ts +1 -1
@@ -3,16 +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
7
  import { resolvePiPackageRoot } from "./pi-spawn.ts";
8
8
  import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
9
9
  import { TEMP_ROOT_DIR, type JsonSchemaObject, type ResolvedToolBudget } from "../../shared/types.ts";
10
10
  import { THINKING_LEVELS } from "../../shared/model-info.ts";
11
11
  import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
12
- import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
12
+ import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, MCP_DIRECT_CHILD_TOOLS_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
13
13
  import { CHILD_WATCHDOG_CONFIG_ENV, encodeChildWatchdogConfig, type ChildWatchdogConfig } from "../../watchdog/child-status.ts";
14
14
  import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
15
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";
16
17
 
17
18
  const TASK_ARG_LIMIT = 8000;
18
19
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
@@ -38,7 +39,7 @@ export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
38
39
  export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
39
40
  export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
40
41
 
41
- interface BuildPiArgsInput {
42
+ export interface BuildPiArgsInput {
42
43
  parentSessionId?: string;
43
44
  baseArgs: string[];
44
45
  task: string;
@@ -83,13 +84,15 @@ interface BuildPiArgsInput {
83
84
  allowZeroToolBudget?: boolean;
84
85
  childWatchdog?: ChildWatchdogConfig;
85
86
  waitToolEnabled?: boolean;
87
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
86
88
  }
87
89
 
88
- interface BuildPiArgsResult {
90
+ export interface BuildPiArgsResult {
89
91
  args: string[];
90
92
  env: Record<string, string | undefined>;
91
93
  tempDir?: string;
92
94
  toolDiagnosticPath?: string;
95
+ capabilityAudit?: SubagentCapabilityAudit;
93
96
  }
94
97
 
95
98
  function sanitizeSupervisorChannelSegment(value: string): string {
@@ -109,6 +112,106 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
109
112
  return `${model}:${thinking}`;
110
113
  }
111
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
+
112
215
  export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
113
216
  const args = [...input.baseArgs];
114
217
 
@@ -130,46 +233,25 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
130
233
  args.push("--model", modelArg);
131
234
  }
132
235
 
133
- const explicitToolAllowlist = input.tools !== undefined || (input.mcpDirectTools?.length ?? 0) > 0;
134
- const declaredBuiltinToolsBase = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
135
- const declaredBuiltinTools = input.requireReadTool && declaredBuiltinToolsBase.length > 0 && !declaredBuiltinToolsBase.includes("read")
136
- ? ["read", ...declaredBuiltinToolsBase]
137
- : declaredBuiltinToolsBase;
138
- const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
139
- const toolExtensionPaths: string[] = [];
140
- let requiredChildTools: string[] = [];
141
- for (const tool of input.tools ?? []) {
142
- if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
143
- toolExtensionPaths.push(tool);
144
- }
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(","));
145
250
  }
146
- if (explicitToolAllowlist) {
147
- const allowedTools = [
148
- ...declaredBuiltinTools,
149
- ...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd),
150
- ...(input.structuredOutput ? ["structured_output"] : []),
151
- ];
152
- requiredChildTools = [...new Set(allowedTools)];
153
- if (requiredChildTools.length > 0) {
154
- args.push("--tools", requiredChildTools.join(","));
155
- } else {
156
- args.push("--no-tools");
157
- }
158
- }
159
-
160
- const runtimeExtensions = fanoutAuthorized
161
- ? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
162
- : [PROMPT_RUNTIME_EXTENSION_PATH];
163
- if (input.extensions !== undefined) {
251
+ if (toolPlan.disableAmbientExtensions) {
164
252
  args.push("--no-extensions");
165
- for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
166
- args.push("--extension", extPath);
167
- }
168
- } else {
169
- for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
170
- args.push("--extension", extPath);
171
- }
172
253
  }
254
+ for (const extPath of toolPlan.extensionArgs) args.push("--extension", extPath);
173
255
 
174
256
  if (!input.inheritSkills) {
175
257
  args.push("--no-skills");
@@ -199,14 +281,15 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
199
281
  const piPackageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
200
282
  if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
201
283
  let toolDiagnosticPath: string | undefined;
202
- if (requiredChildTools.length > 0) {
284
+ if (toolPlan.requiredChildTools.length > 0) {
203
285
  if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
204
286
  toolDiagnosticPath = path.join(tempDir, "tool-diagnostic.json");
205
- env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(requiredChildTools);
287
+ env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(toolPlan.requiredChildTools);
206
288
  env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = toolDiagnosticPath;
207
289
  }
290
+ env[MCP_DIRECT_CHILD_TOOLS_ENV] = toolPlan.effectiveMcpTools.length > 0 ? JSON.stringify(toolPlan.effectiveMcpTools) : undefined;
208
291
  env[SUBAGENT_CHILD_ENV] = "1";
209
- env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
292
+ env[SUBAGENT_FANOUT_CHILD_ENV] = toolPlan.fanoutAuthorized ? "1" : "0";
210
293
  if (input.waitToolEnabled !== undefined) {
211
294
  env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
212
295
  }
@@ -227,20 +310,20 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
227
310
  ...(input.childAgentName ? { agent: input.childAgentName } : {}),
228
311
  }] : []),
229
312
  ];
230
- env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
313
+ env[SUBAGENT_PARENT_EVENT_SINK_ENV] = toolPlan.fanoutAuthorized
231
314
  ? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
232
315
  : "";
233
- env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
316
+ env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = toolPlan.fanoutAuthorized
234
317
  ? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
235
318
  : "";
236
- env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
319
+ env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
237
320
  ? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
238
321
  : "";
239
- env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
240
- env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
241
- env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
242
- env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
243
- 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
244
327
  ? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
245
328
  : "";
246
329
  env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
@@ -270,11 +353,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
270
353
  if (input.childIndex !== undefined) {
271
354
  env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
272
355
  }
273
- if (input.mcpDirectTools?.length) {
274
- env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
275
- } else {
276
- env.MCP_DIRECT_TOOLS = "__none__";
277
- }
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;
278
361
  if (input.structuredOutput) {
279
362
  env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
280
363
  env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
@@ -292,7 +375,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
292
375
 
293
376
  env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
294
377
 
295
- return { args, env, tempDir, toolDiagnosticPath };
378
+ return { args, env, tempDir, toolDiagnosticPath, capabilityAudit: toolPlan.capabilityAudit };
296
379
  }
297
380
 
298
381
  export const parseParentPathEnv = parseNestedPathEnv;
@@ -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
  }
@@ -7,6 +7,7 @@ import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHI
7
7
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
8
8
  import {
9
9
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
10
+ MCP_DIRECT_CHILD_TOOLS_ENV,
10
11
  REQUIRED_CHILD_TOOLS_ENV,
11
12
  writeChildToolDiagnostic,
12
13
  type ChildToolDiagnostic,
@@ -78,12 +79,24 @@ function readRequiredChildTools(): string[] | undefined {
78
79
  return required;
79
80
  }
80
81
 
82
+ function readMcpDirectChildTools(): string[] | undefined {
83
+ const encoded = process.env[MCP_DIRECT_CHILD_TOOLS_ENV]?.trim();
84
+ if (!encoded) return undefined;
85
+ try {
86
+ const tools = JSON.parse(encoded) as unknown;
87
+ if (!Array.isArray(tools) || tools.some((name) => typeof name !== "string" || !name)) return undefined;
88
+ return tools;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
81
94
  function refreshChildToolDiagnostic(pi: ExtensionAPI): ChildToolDiagnostic | undefined {
82
95
  const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
83
96
  const required = readRequiredChildTools();
84
97
  if (!filePath || !required) return undefined;
85
98
  const available = pi.getAllTools().map((tool) => tool.name);
86
- return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim());
99
+ return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim(), readMcpDirectChildTools());
87
100
  }
88
101
 
89
102
  function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
@@ -40,7 +40,7 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
40
40
  const NO_EDIT_PROHIBITION_PATTERN = /\b(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b)[^.;,:!?\n–—-])*)/gi;
41
41
 
42
42
  /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
43
- const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|source|sources|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
43
+ const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|product|source|sources|config|configs|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
44
44
 
45
45
  const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
46
46
  /\bdo not edit files?\s+outside\b/i,
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
4
  export const REQUIRED_CHILD_TOOLS_ENV = "PI_SUBAGENT_REQUIRED_TOOLS";
5
+ export const MCP_DIRECT_CHILD_TOOLS_ENV = "PI_SUBAGENT_MCP_DIRECT_TOOLS";
5
6
  export const CHILD_TOOL_DIAGNOSTIC_PATH_ENV = "PI_SUBAGENT_TOOL_DIAGNOSTIC_PATH";
6
7
 
7
8
  export interface ChildToolDiagnostic {
@@ -9,6 +10,7 @@ export interface ChildToolDiagnostic {
9
10
  required: string[];
10
11
  available: string[];
11
12
  missing: string[];
13
+ missingMcpDirectTools?: string[];
12
14
  }
13
15
 
14
16
  const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
@@ -18,6 +20,7 @@ export function writeChildToolDiagnostic(
18
20
  required: string[],
19
21
  available: string[],
20
22
  agent?: string,
23
+ mcpDirectTools?: string[],
21
24
  ): ChildToolDiagnostic | undefined {
22
25
  const availableNames = new Set([...available, ...PI_CORE_CHILD_TOOLS]);
23
26
  const missing = required.filter((name) => !availableNames.has(name));
@@ -26,7 +29,16 @@ export function writeChildToolDiagnostic(
26
29
  return undefined;
27
30
  }
28
31
 
29
- const diagnostic: ChildToolDiagnostic = { agent, required, available, missing };
32
+ const missingMcpDirectTools = mcpDirectTools?.length
33
+ ? missing.filter((name) => mcpDirectTools.includes(name))
34
+ : [];
35
+ const diagnostic: ChildToolDiagnostic = {
36
+ agent,
37
+ required,
38
+ available,
39
+ missing,
40
+ ...(missingMcpDirectTools.length > 0 ? { missingMcpDirectTools } : {}),
41
+ };
30
42
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
31
43
  fs.writeFileSync(filePath, JSON.stringify(diagnostic), { mode: 0o600 });
32
44
  return diagnostic;
@@ -36,7 +48,7 @@ export function readChildToolDiagnostic(filePath: string | undefined): ChildTool
36
48
  if (!filePath || !fs.existsSync(filePath)) return undefined;
37
49
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Partial<ChildToolDiagnostic>;
38
50
  const stringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
39
- if (!stringArray(parsed.required) || !stringArray(parsed.available) || !stringArray(parsed.missing) || (parsed.agent !== undefined && typeof parsed.agent !== "string")) {
51
+ if (!stringArray(parsed.required) || !stringArray(parsed.available) || !stringArray(parsed.missing) || (parsed.agent !== undefined && typeof parsed.agent !== "string") || (parsed.missingMcpDirectTools !== undefined && !stringArray(parsed.missingMcpDirectTools))) {
40
52
  throw new Error(`Malformed child tool diagnostic at '${filePath}'.`);
41
53
  }
42
54
  return {
@@ -44,6 +56,7 @@ export function readChildToolDiagnostic(filePath: string | undefined): ChildTool
44
56
  required: parsed.required,
45
57
  available: parsed.available,
46
58
  missing: parsed.missing,
59
+ ...(parsed.missingMcpDirectTools ? { missingMcpDirectTools: parsed.missingMcpDirectTools } : {}),
47
60
  };
48
61
  }
49
62
 
@@ -52,6 +65,9 @@ export function formatChildToolDiagnostic(diagnostic: ChildToolDiagnostic): stri
52
65
  return [
53
66
  `${subject} requested unavailable child tools: ${diagnostic.missing.join(", ")}.`,
54
67
  "The `tools` field is a strict allowlist; it does not load extension code.",
68
+ ...(diagnostic.missingMcpDirectTools?.length
69
+ ? [`Resolved MCP direct tools missing from the child registry: ${diagnostic.missingMcpDirectTools.join(", ")}. This indicates a host/pi-mcp-adapter registration problem, not a tool-call failure.`]
70
+ : []),
55
71
  "For extension tools, add the provider path to `subagentOnlyExtensions` (child-only), `extensions`, or as a path-like entry in `tools`, while keeping each registered tool name in `tools`.",
56
72
  "For MCP tools, verify the MCP adapter configuration and selected tool names. For builtin tools, verify the name against the installed Pi version.",
57
73
  ].join("\n");
@@ -0,0 +1,123 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import type { AgentConfig } from "../agents/agents.ts";
4
+
5
+ export const AGENT_DEFINITION_PROJECTION_VERSION = 1 as const;
6
+ export const LAUNCH_BINDING_PROJECTION_VERSION = 1 as const;
7
+
8
+ function stableJson(value: unknown): string {
9
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
10
+ if (value && typeof value === "object") {
11
+ return `{${Object.entries(value as Record<string, unknown>)
12
+ .filter(([, entry]) => entry !== undefined)
13
+ .sort(([a], [b]) => a.localeCompare(b))
14
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
15
+ .join(",")}}`;
16
+ }
17
+ return JSON.stringify(value);
18
+ }
19
+
20
+ function sha256(value: unknown): string {
21
+ return createHash("sha256").update(stableJson(value)).digest("hex");
22
+ }
23
+
24
+ function fileDigest(filePath: string): string | undefined {
25
+ try {
26
+ return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ /** Public-safe, deterministic evidence for the parsed launch-affecting agent definition. */
33
+ export function projectAgentDefinition(agent: AgentConfig): Record<string, unknown> {
34
+ return {
35
+ version: AGENT_DEFINITION_PROJECTION_VERSION,
36
+ name: agent.name,
37
+ localName: agent.localName,
38
+ packageName: agent.packageName,
39
+ filePath: agent.filePath,
40
+ fileContentDigest: fileDigest(agent.filePath),
41
+ systemPrompt: agent.systemPrompt,
42
+ systemPromptMode: agent.systemPromptMode,
43
+ inheritProjectContext: agent.inheritProjectContext,
44
+ inheritSkills: agent.inheritSkills,
45
+ model: agent.model,
46
+ fallbackModels: agent.fallbackModels,
47
+ thinking: agent.thinking,
48
+ tools: agent.tools,
49
+ mcpDirectTools: agent.mcpDirectTools,
50
+ extensions: agent.extensions,
51
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
52
+ skills: agent.skills,
53
+ skillPath: agent.skillPath,
54
+ output: agent.output,
55
+ defaultReads: agent.defaultReads,
56
+ defaultProgress: agent.defaultProgress,
57
+ defaultContext: agent.defaultContext,
58
+ defaultAsync: agent.defaultAsync,
59
+ defaultTimeoutMs: agent.defaultTimeoutMs,
60
+ defaultTurnBudget: agent.defaultTurnBudget,
61
+ defaultAcceptance: agent.defaultAcceptance,
62
+ acceptanceRole: agent.acceptanceRole,
63
+ interactive: agent.interactive,
64
+ maxSubagentDepth: agent.maxSubagentDepth,
65
+ completionGuard: agent.completionGuard,
66
+ toolBudget: agent.toolBudget,
67
+ memory: agent.memory,
68
+ };
69
+ }
70
+
71
+ export function agentDefinitionDigest(agent: AgentConfig): string {
72
+ return sha256(projectAgentDefinition(agent));
73
+ }
74
+
75
+ export interface LaunchBindingInput {
76
+ definitionDigest: string;
77
+ /** Caller task; runtime acceptance/output task annotations are explicitly outside the preflight-known subset. */
78
+ task?: string;
79
+ model?: string;
80
+ modelCandidates?: string[];
81
+ thinking?: string;
82
+ systemPrompt?: string | null;
83
+ systemPromptMode?: AgentConfig["systemPromptMode"];
84
+ inheritProjectContext: boolean;
85
+ inheritSkills: boolean;
86
+ skills?: string[];
87
+ tools?: string[];
88
+ extensions?: string[];
89
+ subagentOnlyExtensions?: string[];
90
+ mcpDirectTools?: string[];
91
+ outputPath?: string;
92
+ outputMode?: string;
93
+ structuredOutputSchema?: unknown;
94
+ }
95
+
96
+ /** Canonical projection of the resolved inputs handed to the child. */
97
+ export function projectLaunchBinding(input: LaunchBindingInput): Record<string, unknown> {
98
+ return {
99
+ version: LAUNCH_BINDING_PROJECTION_VERSION,
100
+ definitionDigest: input.definitionDigest,
101
+ taskDigest: input.task === undefined ? undefined : sha256(input.task),
102
+ // The ordered candidate set already contains each attempted model; keeping only
103
+ // this set makes retries correlate to the same preflight binding.
104
+ modelCandidates: input.modelCandidates,
105
+ thinking: input.thinking,
106
+ systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : sha256(input.systemPrompt),
107
+ systemPromptMode: input.systemPromptMode,
108
+ inheritProjectContext: input.inheritProjectContext,
109
+ inheritSkills: input.inheritSkills,
110
+ skills: input.skills,
111
+ tools: input.tools,
112
+ extensions: input.extensions,
113
+ subagentOnlyExtensions: input.subagentOnlyExtensions,
114
+ mcpDirectTools: input.mcpDirectTools,
115
+ outputPath: input.outputPath,
116
+ outputMode: input.outputMode,
117
+ structuredOutputSchema: input.structuredOutputSchema,
118
+ };
119
+ }
120
+
121
+ export function launchBindingDigest(input: LaunchBindingInput): string {
122
+ return sha256(projectLaunchBinding(input));
123
+ }