pi-subagents 0.61.0 → 0.62.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 (45) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/docs/agents.md +6 -2
  3. package/docs/configuration.md +3 -3
  4. package/docs/extension-api.md +2 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/install.mjs +0 -1
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/references/execution-controls.md +3 -4
  10. package/src/agents/agent-management.ts +19 -0
  11. package/src/agents/agent-serializer.ts +3 -0
  12. package/src/agents/agents.ts +16 -2
  13. package/src/agents/runtime-agent-registry.ts +5 -1
  14. package/src/api/preflight.ts +4 -0
  15. package/src/extension/public-execution.ts +1 -0
  16. package/src/extension/schemas.ts +6 -2
  17. package/src/extension/tool-description.ts +1 -1
  18. package/src/runs/background/active-async-capacity.ts +26 -7
  19. package/src/runs/background/async-execution.ts +47 -16
  20. package/src/runs/background/async-resume.ts +3 -2
  21. package/src/runs/background/process-terminal.ts +16 -0
  22. package/src/runs/background/scheduled-runs.ts +63 -6
  23. package/src/runs/background/steering.ts +4 -1
  24. package/src/runs/background/subagent-runner.ts +14 -7
  25. package/src/runs/background/wait-tool.ts +1 -7
  26. package/src/runs/foreground/execution.ts +14 -7
  27. package/src/runs/foreground/subagent-executor.ts +3 -2
  28. package/src/runs/shared/acceptance.ts +70 -9
  29. package/src/runs/shared/capability-ceiling.ts +1 -0
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/parallel-utils.ts +2 -6
  32. package/src/runs/shared/permissions.ts +1 -1
  33. package/src/runs/shared/pi-args.ts +24 -12
  34. package/src/runs/shared/pi-spawn.ts +69 -35
  35. package/src/runs/shared/structured-output.ts +33 -6
  36. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  37. package/src/runs/shared/task-intent.ts +17 -6
  38. package/src/runs/shared/tool-timeout.ts +1 -1
  39. package/src/shared/atomic-json.ts +3 -1
  40. package/src/shared/fork-context.ts +0 -12
  41. package/src/shared/fork-session-cwd.ts +27 -0
  42. package/src/shared/launch-contract.ts +3 -0
  43. package/src/shared/types.ts +3 -1
  44. package/src/slash/slash-commands.ts +1 -1
  45. package/src/slash/subagents-admin.ts +2 -0
@@ -3,12 +3,12 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
- import { existsSync, unlinkSync } from "node:fs";
6
+ import { existsSync } from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
10
10
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
11
- import { alignForkedSessionCwd } from "../../shared/fork-context.ts";
11
+ import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
12
12
  import {
13
13
  ensureArtifactsDir,
14
14
  formatOutputArtifactContent,
@@ -73,7 +73,7 @@ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledge
73
73
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
74
74
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
75
75
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
76
- import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
76
+ import { clearStructuredOutputCaptures, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
77
77
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
78
78
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
79
79
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -411,6 +411,7 @@ async function runSingleAttempt(
411
411
  inheritSkills: agent.inheritSkills,
412
412
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
413
413
  tools: agent.tools,
414
+ excludeTools: agent.excludeTools,
414
415
  allowNestedSubagents: agent.allowNestedSubagents,
415
416
  extensions: agent.extensions,
416
417
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
@@ -464,6 +465,7 @@ async function runSingleAttempt(
464
465
  cwd: options.cwd ?? runtimeCwd,
465
466
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
466
467
  structuredOutput: Boolean(options.structuredOutput),
468
+ excludeTools: agent.excludeTools,
467
469
  fast: options.fast ?? agent.fast,
468
470
  model: modelArg,
469
471
  modelCandidates: shared.modelCandidates,
@@ -520,6 +522,7 @@ async function runSingleAttempt(
520
522
  inheritSkills: agent.inheritSkills,
521
523
  skills: shared.resolvedSkillNames ?? [],
522
524
  tools: toolPlan.effectiveToolAllowlist,
525
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
523
526
  extensions: toolPlan.extensionArgs,
524
527
  mcpDirectTools: toolPlan.effectiveMcpTools,
525
528
  ...(options.outputPath ? { outputPath: options.outputPath } : {}),
@@ -551,10 +554,14 @@ async function runSingleAttempt(
551
554
  }, options.context);
552
555
  const startTime = Date.now();
553
556
  if (options.structuredOutput) {
554
- try {
555
- if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
556
- } catch {
557
- // Missing/stale structured-output files are handled after the child exits.
557
+ const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
558
+ if (cleanupError) {
559
+ cleanupTempDir(tempDir);
560
+ result.exitCode = 1;
561
+ result.error = cleanupError;
562
+ result.finalOutput = cleanupError;
563
+ result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
564
+ return result;
558
565
  }
559
566
  }
560
567
  const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
@@ -50,7 +50,7 @@ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCap
50
50
  import { isScheduledRunAction } from "../background/scheduled-runs.ts";
51
51
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
52
52
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
53
- import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
53
+ import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
54
54
  import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
55
55
  import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
56
56
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
@@ -393,6 +393,7 @@ export interface SubagentParamsLike {
393
393
  agentContract?: AgentContract;
394
394
  at?: string;
395
395
  every?: string;
396
+ sessionOnly?: boolean;
396
397
  on?: string | number;
397
398
  timezone?: string;
398
399
  overlap?: "skip";
@@ -3690,7 +3691,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3690
3691
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
3691
3692
  }
3692
3693
  const structuredRuntime = params.outputSchema
3693
- ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { captureAcceptanceReport: params.acceptance !== false })
3694
+ ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
3694
3695
  : undefined;
3695
3696
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
3696
3697
  // absolute paths pass through; relative paths resolve against the child cwd.
@@ -47,7 +47,7 @@ const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
47
47
  const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
48
48
  const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
49
49
  const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
50
- const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
50
+ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "report", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
51
51
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
52
52
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
53
53
  const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
@@ -146,11 +146,46 @@ function inferLevel(input: {
146
146
  };
147
147
  }
148
148
 
149
- export function normalizeAcceptanceInput(input: AcceptanceInput | undefined): AcceptanceConfig {
149
+ type AcceptanceInputNormalizationResult = { value: unknown; error?: string };
150
+
151
+ function normalizeAcceptanceValue(input: unknown, pathLabel = "acceptance"): AcceptanceInputNormalizationResult {
152
+ if (typeof input !== "string") return { value: input };
153
+ const trimmed = input.trim();
154
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { value: input };
155
+ let parsed: unknown;
156
+ try {
157
+ parsed = JSON.parse(trimmed);
158
+ } catch (error) {
159
+ return {
160
+ value: input,
161
+ error: `${pathLabel} JSON string must encode a valid acceptance object: ${error instanceof Error ? error.message : String(error)}`,
162
+ };
163
+ }
164
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
165
+ const kind = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
166
+ return { value: input, error: `${pathLabel} JSON string must encode an object; got ${kind}.` };
167
+ }
168
+ return { value: parsed };
169
+ }
170
+
171
+ export function normalizeAcceptanceInput(input: unknown): AcceptanceConfig {
172
+ const normalized = normalizeAcceptanceValue(input);
173
+ if (normalized.error) throw new Error(normalized.error);
174
+ input = normalized.value;
150
175
  if (input === undefined || input === "auto") return { level: "auto" };
151
176
  if (input === false) return { level: "none", reason: "disabled by deprecated false shorthand" };
152
- if (typeof input === "string") return { level: input };
153
- return { ...input };
177
+ if (typeof input === "string") return { level: input as AcceptanceConfig["level"] };
178
+ return { ...(input as AcceptanceConfig) };
179
+ }
180
+
181
+ export type ResolvedAcceptanceReportMode = "off" | "optional" | "required";
182
+
183
+ export function resolveAcceptanceReportMode(input: unknown): ResolvedAcceptanceReportMode {
184
+ const normalized = normalizeAcceptanceValue(input);
185
+ if (normalized.error) throw new Error(normalized.error);
186
+ const value = normalized.value;
187
+ const report = value && typeof value === "object" && !Array.isArray(value) ? (value as AcceptanceConfig).report : undefined;
188
+ return value === false || report === "off" ? "off" : report === "on" ? "required" : "optional";
154
189
  }
155
190
 
156
191
  type GateAcceptanceNormalizationResult =
@@ -158,7 +193,11 @@ type GateAcceptanceNormalizationResult =
158
193
  | { ok: false; error: string };
159
194
 
160
195
  export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInput | undefined): GateAcceptanceNormalizationResult {
161
- if (gate === undefined) return acceptance === undefined ? { ok: true } : { ok: true, acceptance };
196
+ if (gate === undefined) {
197
+ if (acceptance === undefined) return { ok: true };
198
+ const normalized = normalizeAcceptanceValue(acceptance);
199
+ return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
200
+ }
162
201
  if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
163
202
  if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
164
203
  return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
@@ -177,6 +216,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
177
216
  const errors: string[] = [];
178
217
  if (input === undefined) return errors;
179
218
  if (input === false) return errors;
219
+ const normalized = normalizeAcceptanceValue(input, pathLabel);
220
+ if (normalized.error) return [normalized.error];
221
+ input = normalized.value;
180
222
  if (typeof input === "string") {
181
223
  if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
182
224
  else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
@@ -197,6 +239,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
197
239
  } else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
198
240
  errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
199
241
  }
242
+ if (value.report !== undefined && value.report !== "on" && value.report !== "off") {
243
+ errors.push(`${pathLabel}.report must be on or off.`);
244
+ }
200
245
  if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
201
246
  errors.push(`${pathLabel}.reason is required when level is none.`);
202
247
  }
@@ -304,29 +349,45 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
304
349
 
305
350
  export function validateExecutionAcceptance(input: {
306
351
  acceptance?: unknown;
307
- tasks?: Array<{ acceptance?: unknown }>;
352
+ outputSchema?: unknown;
353
+ tasks?: Array<{ acceptance?: unknown; outputSchema?: unknown }>;
308
354
  chain?: Array<{
309
355
  acceptance?: unknown;
310
- parallel?: Array<{ acceptance?: unknown }> | { acceptance?: unknown };
356
+ outputSchema?: unknown;
357
+ parallel?: Array<{ acceptance?: unknown; outputSchema?: unknown }> | { acceptance?: unknown; outputSchema?: unknown };
311
358
  }>;
312
359
  }): string[] {
313
360
  const errors = validateAcceptanceInput(input.acceptance, "acceptance");
361
+ errors.push(...validateAcceptanceReportMode(input.acceptance, input.outputSchema, "acceptance"));
314
362
  for (const [index, task] of (input.tasks ?? []).entries()) {
315
363
  errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
364
+ errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `tasks[${index}].acceptance`));
316
365
  }
317
366
  for (const [stepIndex, step] of (input.chain ?? []).entries()) {
318
367
  errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
368
+ errors.push(...validateAcceptanceReportMode(step.acceptance, step.outputSchema, `chain[${stepIndex}].acceptance`));
319
369
  if (Array.isArray(step.parallel)) {
320
370
  for (const [taskIndex, task] of step.parallel.entries()) {
321
371
  errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
372
+ errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
322
373
  }
323
374
  } else if (step.parallel) {
324
375
  errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
376
+ errors.push(...validateAcceptanceReportMode(step.parallel.acceptance, step.parallel.outputSchema, `chain[${stepIndex}].parallel.acceptance`));
325
377
  }
326
378
  }
327
379
  return errors;
328
380
  }
329
381
 
382
+ function validateAcceptanceReportMode(acceptance: unknown, outputSchema: unknown, pathLabel: string): string[] {
383
+ const normalized = normalizeAcceptanceValue(acceptance, pathLabel);
384
+ if (normalized.error) return [];
385
+ acceptance = normalized.value;
386
+ if (!acceptance || typeof acceptance !== "object" || Array.isArray(acceptance)) return [];
387
+ if (!("report" in acceptance)) return [];
388
+ return outputSchema === undefined ? [`${pathLabel}.report requires outputSchema.`] : [];
389
+ }
390
+
330
391
  function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
331
392
  return (criteria ?? []).map((criterion, index): ResolvedAcceptanceGate => {
332
393
  if (typeof criterion === "string") {
@@ -851,7 +912,7 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
851
912
  }
852
913
  }
853
914
 
854
- function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
915
+ export function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
855
916
  const normalized = normalizeAcceptanceReportValue(value, pathLabel);
856
917
  value = normalized.value;
857
918
  pathLabel = normalized.pathLabel;
@@ -1310,7 +1371,7 @@ export async function evaluateAcceptance(input: {
1310
1371
 
1311
1372
  const parsed: AcceptanceReportParseResult = input.reportError
1312
1373
  ? { error: input.reportError }
1313
- : input.report
1374
+ : input.report !== undefined
1314
1375
  ? (() => {
1315
1376
  const validation = validateAcceptanceReport(input.report);
1316
1377
  return validation.report
@@ -22,6 +22,7 @@ export interface SubagentCapabilityAudit {
22
22
  requestedTools?: string[];
23
23
  effectiveTools: string[];
24
24
  removedTools: string[];
25
+ excludeTools?: string[];
25
26
  internalTools: string[];
26
27
  extensionsDenied: boolean;
27
28
  removedExtensionCount: number;
@@ -50,7 +50,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
50
50
  const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
51
51
  ...DYNAMIC_PARALLEL_KEYS,
52
52
  "outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
53
- "structuredOutput", "structuredOutputSchema", "tools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
53
+ "structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
54
54
  "systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
55
55
  ]);
56
56
  const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
@@ -42,6 +42,7 @@ export interface RunnerSubagentStep {
42
42
  skipPrimaryModelVerification?: boolean;
43
43
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
44
44
  tools?: string[];
45
+ excludeTools?: string[];
45
46
  allowNestedSubagents?: boolean;
46
47
  extensions?: string[];
47
48
  subagentOnlyExtensions?: string[];
@@ -68,12 +69,7 @@ export interface RunnerSubagentStep {
68
69
  toolTimeoutMs?: number;
69
70
  waitToolEnabled?: boolean;
70
71
  waitToolDefaultTimeoutMs?: number;
71
- structuredOutput?: {
72
- schema: import("../../shared/types.ts").JsonSchemaObject;
73
- schemaPath: string;
74
- outputPath: string;
75
- acceptanceReportPath?: string;
76
- };
72
+ structuredOutput?: import("./structured-output.ts").StructuredOutputRuntime;
77
73
  structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
78
74
  agentContract?: import("../../shared/types.ts").AgentContract;
79
75
  definitionDigest?: string;
@@ -7,7 +7,7 @@ export interface PermissionConfig { rules?: PermissionRules }
7
7
 
8
8
  export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
9
9
  export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
10
- const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "subagent_wait", "structured_output"]);
10
+ const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "structured_output"]);
11
11
  const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
12
12
  const MAX_POLICY_BYTES = 16 * 1024;
13
13
  const MAX_PREVIEW_BYTES = 2048;
@@ -20,8 +20,10 @@ import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type Exten
20
20
  import { RUNTIME_EXTENSION_ACK_PATH_ENV } from "./runtime-acknowledged-extensions.ts";
21
21
  import {
22
22
  STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV,
23
+ STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV,
23
24
  STRUCTURED_OUTPUT_CAPTURE_ENV,
24
25
  STRUCTURED_OUTPUT_SCHEMA_ENV,
26
+ type StructuredOutputRuntime,
25
27
  } from "./structured-output.ts";
26
28
  import {
27
29
  TEMP_ROOT_DIR,
@@ -171,6 +173,7 @@ export interface BuildPiArgsInput {
171
173
  inheritSkills: boolean;
172
174
  requireReadTool?: boolean;
173
175
  tools?: string[];
176
+ excludeTools?: string[];
174
177
  extensions?: string[];
175
178
  subagentOnlyExtensions?: string[];
176
179
  systemPrompt?: string | null;
@@ -201,12 +204,7 @@ export interface BuildPiArgsInput {
201
204
  steerInboxDir?: string;
202
205
  steerCapabilityPath?: string;
203
206
  steerAckDir?: string;
204
- structuredOutput?: {
205
- schema: JsonSchemaObject;
206
- schemaPath: string;
207
- outputPath: string;
208
- acceptanceReportPath?: string;
209
- };
207
+ structuredOutput?: StructuredOutputRuntime;
210
208
  fast?: boolean;
211
209
  modelCandidates?: readonly string[];
212
210
  toolBudget?: ResolvedToolBudget;
@@ -300,6 +298,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
300
298
 
301
299
  export interface ResolvePiLaunchToolPlanInput {
302
300
  tools?: string[];
301
+ excludeTools?: string[];
303
302
  allowNestedSubagents?: boolean;
304
303
  extensions?: string[];
305
304
  subagentOnlyExtensions?: string[];
@@ -329,6 +328,7 @@ export interface PiLaunchToolPlan {
329
328
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
330
329
  requestedBuiltinTools: string[];
331
330
  declaredBuiltinTools: string[];
331
+ excludeTools: string[];
332
332
  toolExtensionPaths: string[];
333
333
  resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
334
334
  effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
@@ -499,8 +499,12 @@ export function resolvePiLaunchToolPlan(
499
499
  ? ["read", ...requestedBuiltinTools]
500
500
  : requestedBuiltinTools
501
501
  ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
502
- const fanoutAuthorized = declaredBuiltinTools.includes("subagent") || (
502
+ const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
503
+ const excludedToolSet = new Set(excludeTools);
504
+ const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
505
+ const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
503
506
  input.allowNestedSubagents === true &&
507
+ !excludedToolSet.has("subagent") &&
504
508
  (!allowedToolSet || allowedToolSet.has("subagent"))
505
509
  );
506
510
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
@@ -524,7 +528,7 @@ export function resolvePiLaunchToolPlan(
524
528
  !allowedToolSet ||
525
529
  allowedToolSet.has(selection.name) ||
526
530
  isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
527
- );
531
+ ).filter((selection) => !excludedToolSet.has(selection.name));
528
532
  const effectiveMcpTools = effectiveMcpSelections.map(
529
533
  (selection) => selection.name,
530
534
  );
@@ -540,10 +544,10 @@ export function resolvePiLaunchToolPlan(
540
544
  input.tools !== undefined ||
541
545
  (input.mcpDirectTools?.length ?? 0) > 0 ||
542
546
  allowedToolSet !== undefined;
543
- const internalTools = input.structuredOutput ? ["structured_output"] : [];
547
+ const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
544
548
  const effectiveToolAllowlist = [
545
549
  ...new Set([
546
- ...declaredBuiltinTools,
550
+ ...effectiveDeclaredBuiltinTools,
547
551
  ...effectiveMcpTools,
548
552
  ...internalTools,
549
553
  ]),
@@ -554,11 +558,11 @@ export function resolvePiLaunchToolPlan(
554
558
  // appended intercom alongside contact_supervisor, so that exact pairing is
555
559
  // legacy plumbing, not a user demand for an external intercom provider;
556
560
  // a lone intercom entry stays strictly required (#1207).
557
- const legacySupervisorPairing = declaredBuiltinTools.includes("contact_supervisor");
561
+ const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
558
562
  const requiredChildTools = explicitToolAllowlist
559
563
  ? [
560
564
  ...new Set([
561
- ...(input.tools !== undefined ? declaredBuiltinTools : []),
565
+ ...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
562
566
  ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
563
567
  ...internalTools,
564
568
  ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
@@ -620,6 +624,7 @@ export function resolvePiLaunchToolPlan(
620
624
  ceiling: capabilityCeiling,
621
625
  ...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
622
626
  effectiveTools: effectiveToolAllowlist,
627
+ ...(excludeTools.length > 0 ? { excludeTools } : {}),
623
628
  removedTools:
624
629
  requestedToolNames?.filter(
625
630
  (tool) => !effectiveToolAllowlist.includes(tool),
@@ -657,6 +662,7 @@ export function resolvePiLaunchToolPlan(
657
662
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
658
663
  requestedBuiltinTools,
659
664
  declaredBuiltinTools,
665
+ excludeTools,
660
666
  toolExtensionPaths,
661
667
  resolvedMcpSelections,
662
668
  effectiveMcpSelections,
@@ -740,6 +746,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
740
746
 
741
747
  const toolPlan = resolvePiLaunchToolPlan({
742
748
  tools: input.tools,
749
+ excludeTools: input.excludeTools,
743
750
  allowNestedSubagents: input.allowNestedSubagents,
744
751
  extensions: input.extensions,
745
752
  subagentOnlyExtensions: input.subagentOnlyExtensions,
@@ -766,6 +773,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
766
773
  );
767
774
  if (toolPlan.effectiveToolAllowlist.length > 0)
768
775
  args.push(toolPlan.effectiveToolAllowlist.join(","));
776
+ } else if (toolPlan.excludeTools.length > 0) {
777
+ args.push("--exclude-tools", toolPlan.excludeTools.join(","));
769
778
  }
770
779
  if (toolPlan.disableAmbientExtensions) {
771
780
  args.push("--no-extensions");
@@ -999,10 +1008,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
999
1008
  env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
1000
1009
  if (encodedPermissionRules)
1001
1010
  env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
1011
+ env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
1012
+ env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
1002
1013
  if (input.structuredOutput) {
1003
1014
  env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
1004
1015
  env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
1005
1016
  if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
1017
+ if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
1006
1018
  }
1007
1019
  if (input.steerInboxDir) {
1008
1020
  env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
4
5
 
5
6
  export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
6
7
  export const PI_SUBAGENT_PI_BINARY_ENV = "PI_SUBAGENT_PI_BINARY";
@@ -23,9 +24,13 @@ export function findPiPackageRootFromEntry(
23
24
  }
24
25
 
25
26
  export function resolveInstalledPiPackageRoot(): string | undefined {
26
- return findPiPackageRootFromEntry(
27
- fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)),
28
- );
27
+ try {
28
+ return findPiPackageRootFromEntry(
29
+ fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)),
30
+ );
31
+ } catch {
32
+ return undefined;
33
+ }
29
34
  }
30
35
 
31
36
  export function resolvePiPackageRoot(): string | undefined {
@@ -58,12 +63,21 @@ interface PiSpawnCommand {
58
63
  args: string[];
59
64
  }
60
65
 
66
+ interface PiPackageJson {
67
+ name?: unknown;
68
+ bin?: string | Record<string, string>;
69
+ }
70
+
71
+ function isNodeScriptPath(filePath: string): boolean {
72
+ return /\.(?:mjs|cjs|js)$/i.test(filePath);
73
+ }
74
+
61
75
  function isRunnableNodeScript(
62
76
  filePath: string,
63
77
  existsSync: (filePath: string) => boolean,
64
78
  ): boolean {
65
79
  if (!existsSync(filePath)) return false;
66
- return /\.(?:mjs|cjs|js)$/i.test(filePath);
80
+ return isNodeScriptPath(filePath);
67
81
  }
68
82
 
69
83
  function normalizePath(filePath: string): string {
@@ -75,6 +89,23 @@ function isStandalonePiExecutable(execPath: string): boolean {
75
89
  return /^pi(?:\.exe)?$/i.test(executableName ?? "");
76
90
  }
77
91
 
92
+ function resolvePiCliScriptFromPackageJson(
93
+ packageJsonPath: string,
94
+ readFileSync: (filePath: string, encoding: "utf-8") => string,
95
+ existsSync: (filePath: string) => boolean,
96
+ ): string | undefined {
97
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PiPackageJson;
98
+ if (packageJson.name !== PI_CODING_AGENT_PACKAGE) return undefined;
99
+ const binField = packageJson.bin;
100
+ const binPath =
101
+ typeof binField === "string"
102
+ ? binField
103
+ : (binField?.pi ?? Object.values(binField ?? {})[0]);
104
+ if (!binPath) return undefined;
105
+ const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
106
+ return isRunnableNodeScript(candidate, existsSync) ? candidate : undefined;
107
+ }
108
+
78
109
  export function resolvePiCliScript(
79
110
  deps: PiSpawnDeps = {},
80
111
  ): string | undefined {
@@ -84,6 +115,7 @@ export function resolvePiCliScript(
84
115
  deps.readFileSync ??
85
116
  ((filePath, encoding) => fs.readFileSync(filePath, encoding));
86
117
  const argv1 = deps.argv1 ?? process.argv[1];
118
+ const env = deps.env ?? process.env;
87
119
 
88
120
  if (argv1) {
89
121
  const argvPath = normalizePath(argv1);
@@ -99,38 +131,28 @@ export function resolvePiCliScript(
99
131
  }
100
132
  }
101
133
 
102
- try {
103
- const resolvePackageJson =
104
- deps.resolvePackageJson ??
105
- (() => {
106
- const root = deps.piPackageRoot ?? resolvePiPackageRoot();
107
- if (root) return path.join(root, "package.json");
108
- const packageRoot = deps.resolvePackageEntry
109
- ? findPiPackageRootFromEntry(deps.resolvePackageEntry())
110
- : resolveInstalledPiPackageRoot();
111
- if (!packageRoot)
112
- throw new Error(
113
- `Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`,
114
- );
115
- return path.join(packageRoot, "package.json");
116
- });
117
- const packageJsonPath = resolvePackageJson();
118
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
119
- bin?: string | Record<string, string>;
120
- };
121
- const binField = packageJson.bin;
122
- const binPath =
123
- typeof binField === "string"
124
- ? binField
125
- : (binField?.pi ?? Object.values(binField ?? {})[0]);
126
- if (!binPath) return undefined;
127
- const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
128
- if (isRunnableNodeScript(candidate, existsSync)) {
129
- return candidate;
134
+ const packageJsonCandidates: Array<() => string | undefined> = [];
135
+ if (deps.resolvePackageJson) packageJsonCandidates.push(deps.resolvePackageJson);
136
+ for (const root of [deps.piPackageRoot, env[PI_CODING_AGENT_PACKAGE_ROOT_ENV], resolvePiPackageRoot()]) {
137
+ const trimmed = root?.trim();
138
+ if (trimmed) packageJsonCandidates.push(() => path.join(trimmed, "package.json"));
139
+ }
140
+ packageJsonCandidates.push(() => {
141
+ const packageRoot = deps.resolvePackageEntry
142
+ ? findPiPackageRootFromEntry(deps.resolvePackageEntry())
143
+ : resolveInstalledPiPackageRoot();
144
+ return packageRoot ? path.join(packageRoot, "package.json") : undefined;
145
+ });
146
+
147
+ for (const candidatePackageJson of packageJsonCandidates) {
148
+ try {
149
+ const packageJsonPath = candidatePackageJson();
150
+ if (!packageJsonPath) continue;
151
+ const candidate = resolvePiCliScriptFromPackageJson(packageJsonPath, readFileSync, existsSync);
152
+ if (candidate) return candidate;
153
+ } catch {
154
+ // Keep resolving; callers decide whether a PATH fallback is safe.
130
155
  }
131
- } catch {
132
- // Verified CLI resolution is optional; falling back to `pi` lets PATH handle execution.
133
- return undefined;
134
156
  }
135
157
 
136
158
  return undefined;
@@ -140,9 +162,16 @@ export function getPiSpawnCommand(
140
162
  args: string[],
141
163
  deps: PiSpawnDeps = {},
142
164
  ): PiSpawnCommand {
165
+ const platform = deps.platform ?? process.platform;
143
166
  const env = deps.env ?? process.env;
144
167
  const piBinary = env[PI_SUBAGENT_PI_BINARY_ENV]?.trim();
145
168
  if (piBinary) {
169
+ if (platform === "win32" && isNodeScriptPath(piBinary)) {
170
+ return {
171
+ command: deps.execPath ?? process.execPath,
172
+ args: [piBinary, ...args],
173
+ };
174
+ }
146
175
  return { command: piBinary, args };
147
176
  }
148
177
 
@@ -158,6 +187,11 @@ export function getPiSpawnCommand(
158
187
  args: [piCliPath, ...args],
159
188
  };
160
189
  }
190
+ if (platform === "win32") {
191
+ throw new Error(
192
+ `Could not resolve the Pi CLI on Windows. Set ${PI_SUBAGENT_PI_BINARY_ENV} or ensure ${PI_CODING_AGENT_PACKAGE} is installed.`,
193
+ );
194
+ }
161
195
 
162
196
  return { command: "pi", args };
163
197
  }