pi-subagents 0.61.0 → 0.63.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 (63) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/docs/agents.md +11 -6
  3. package/docs/configuration.md +31 -5
  4. package/docs/extension-api.md +2 -2
  5. package/docs/models.md +5 -5
  6. package/docs/observability.md +5 -2
  7. package/docs/tool-reference.md +3 -3
  8. package/install.mjs +0 -1
  9. package/package.json +1 -1
  10. package/skills/pi-subagents/references/execution-controls.md +3 -4
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  13. package/src/agents/agent-management.ts +41 -4
  14. package/src/agents/agent-serializer.ts +3 -0
  15. package/src/agents/agents.ts +120 -124
  16. package/src/agents/runtime-agent-registry.ts +5 -1
  17. package/src/api/preflight.ts +4 -0
  18. package/src/api/shared-types.ts +3 -0
  19. package/src/extension/config.ts +20 -0
  20. package/src/extension/public-execution.ts +1 -0
  21. package/src/extension/schemas.ts +6 -2
  22. package/src/extension/tool-description.ts +1 -1
  23. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  24. package/src/runs/background/active-async-capacity.ts +26 -8
  25. package/src/runs/background/async-execution.ts +100 -23
  26. package/src/runs/background/async-resume.ts +6 -2
  27. package/src/runs/background/async-status.ts +18 -2
  28. package/src/runs/background/notify.ts +13 -1
  29. package/src/runs/background/process-terminal.ts +16 -0
  30. package/src/runs/background/run-status.ts +22 -2
  31. package/src/runs/background/scheduled-runs.ts +63 -6
  32. package/src/runs/background/steering.ts +4 -1
  33. package/src/runs/background/subagent-runner.ts +44 -9
  34. package/src/runs/background/wait-completions.ts +13 -0
  35. package/src/runs/background/wait-tool.ts +1 -7
  36. package/src/runs/foreground/execution.ts +14 -7
  37. package/src/runs/foreground/subagent-executor.ts +38 -5
  38. package/src/runs/shared/acceptance.ts +85 -18
  39. package/src/runs/shared/capability-ceiling.ts +1 -0
  40. package/src/runs/shared/dynamic-fanout.ts +1 -1
  41. package/src/runs/shared/lane-metadata.ts +24 -3
  42. package/src/runs/shared/parallel-handoff.ts +4 -0
  43. package/src/runs/shared/parallel-utils.ts +2 -6
  44. package/src/runs/shared/permissions.ts +1 -1
  45. package/src/runs/shared/pi-args.ts +32 -14
  46. package/src/runs/shared/pi-spawn.ts +69 -35
  47. package/src/runs/shared/structured-output.ts +33 -6
  48. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  49. package/src/runs/shared/task-intent.ts +21 -7
  50. package/src/runs/shared/tool-timeout.ts +1 -1
  51. package/src/runs/shared/worktree.ts +467 -63
  52. package/src/shared/atomic-json.ts +3 -1
  53. package/src/shared/fork-context.ts +0 -12
  54. package/src/shared/fork-session-cwd.ts +27 -0
  55. package/src/shared/launch-contract.ts +3 -0
  56. package/src/shared/types.ts +32 -1
  57. package/src/shared/utils.ts +18 -7
  58. package/src/slash/slash-commands.ts +1 -1
  59. package/src/slash/subagents-admin.ts +26 -12
  60. package/src/tui/fleet-status.ts +61 -2
  61. package/src/tui/fleet.ts +12 -7
  62. package/src/tui/render.ts +222 -14
  63. package/src/workflows/workflow-checklist.ts +441 -0
@@ -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,
@@ -91,11 +93,12 @@ export function resolveSubagentTaskDelivery(
91
93
  : "auto";
92
94
  }
93
95
 
94
- function shouldDeliverTaskViaFile(
96
+ export function shouldDeliverTaskViaFile(
95
97
  task: string,
96
98
  delivery: SubagentTaskDelivery,
99
+ platform: NodeJS.Platform = process.platform,
97
100
  ): boolean {
98
- return delivery === "file" || task.length > TASK_ARG_LIMIT;
101
+ return delivery === "file" || platform === "darwin" || task.length > TASK_ARG_LIMIT;
99
102
  }
100
103
  const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
101
104
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
@@ -171,6 +174,7 @@ export interface BuildPiArgsInput {
171
174
  inheritSkills: boolean;
172
175
  requireReadTool?: boolean;
173
176
  tools?: string[];
177
+ excludeTools?: string[];
174
178
  extensions?: string[];
175
179
  subagentOnlyExtensions?: string[];
176
180
  systemPrompt?: string | null;
@@ -201,12 +205,7 @@ export interface BuildPiArgsInput {
201
205
  steerInboxDir?: string;
202
206
  steerCapabilityPath?: string;
203
207
  steerAckDir?: string;
204
- structuredOutput?: {
205
- schema: JsonSchemaObject;
206
- schemaPath: string;
207
- outputPath: string;
208
- acceptanceReportPath?: string;
209
- };
208
+ structuredOutput?: StructuredOutputRuntime;
210
209
  fast?: boolean;
211
210
  modelCandidates?: readonly string[];
212
211
  toolBudget?: ResolvedToolBudget;
@@ -300,6 +299,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
300
299
 
301
300
  export interface ResolvePiLaunchToolPlanInput {
302
301
  tools?: string[];
302
+ excludeTools?: string[];
303
303
  allowNestedSubagents?: boolean;
304
304
  extensions?: string[];
305
305
  subagentOnlyExtensions?: string[];
@@ -329,6 +329,7 @@ export interface PiLaunchToolPlan {
329
329
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
330
330
  requestedBuiltinTools: string[];
331
331
  declaredBuiltinTools: string[];
332
+ excludeTools: string[];
332
333
  toolExtensionPaths: string[];
333
334
  resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
334
335
  effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
@@ -499,8 +500,12 @@ export function resolvePiLaunchToolPlan(
499
500
  ? ["read", ...requestedBuiltinTools]
500
501
  : requestedBuiltinTools
501
502
  ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
502
- const fanoutAuthorized = declaredBuiltinTools.includes("subagent") || (
503
+ const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
504
+ const excludedToolSet = new Set(excludeTools);
505
+ const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
506
+ const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
503
507
  input.allowNestedSubagents === true &&
508
+ !excludedToolSet.has("subagent") &&
504
509
  (!allowedToolSet || allowedToolSet.has("subagent"))
505
510
  );
506
511
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
@@ -524,7 +529,7 @@ export function resolvePiLaunchToolPlan(
524
529
  !allowedToolSet ||
525
530
  allowedToolSet.has(selection.name) ||
526
531
  isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
527
- );
532
+ ).filter((selection) => !excludedToolSet.has(selection.name));
528
533
  const effectiveMcpTools = effectiveMcpSelections.map(
529
534
  (selection) => selection.name,
530
535
  );
@@ -540,10 +545,10 @@ export function resolvePiLaunchToolPlan(
540
545
  input.tools !== undefined ||
541
546
  (input.mcpDirectTools?.length ?? 0) > 0 ||
542
547
  allowedToolSet !== undefined;
543
- const internalTools = input.structuredOutput ? ["structured_output"] : [];
548
+ const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
544
549
  const effectiveToolAllowlist = [
545
550
  ...new Set([
546
- ...declaredBuiltinTools,
551
+ ...effectiveDeclaredBuiltinTools,
547
552
  ...effectiveMcpTools,
548
553
  ...internalTools,
549
554
  ]),
@@ -554,11 +559,11 @@ export function resolvePiLaunchToolPlan(
554
559
  // appended intercom alongside contact_supervisor, so that exact pairing is
555
560
  // legacy plumbing, not a user demand for an external intercom provider;
556
561
  // a lone intercom entry stays strictly required (#1207).
557
- const legacySupervisorPairing = declaredBuiltinTools.includes("contact_supervisor");
562
+ const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
558
563
  const requiredChildTools = explicitToolAllowlist
559
564
  ? [
560
565
  ...new Set([
561
- ...(input.tools !== undefined ? declaredBuiltinTools : []),
566
+ ...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
562
567
  ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
563
568
  ...internalTools,
564
569
  ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
@@ -620,6 +625,7 @@ export function resolvePiLaunchToolPlan(
620
625
  ceiling: capabilityCeiling,
621
626
  ...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
622
627
  effectiveTools: effectiveToolAllowlist,
628
+ ...(excludeTools.length > 0 ? { excludeTools } : {}),
623
629
  removedTools:
624
630
  requestedToolNames?.filter(
625
631
  (tool) => !effectiveToolAllowlist.includes(tool),
@@ -657,6 +663,7 @@ export function resolvePiLaunchToolPlan(
657
663
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
658
664
  requestedBuiltinTools,
659
665
  declaredBuiltinTools,
666
+ excludeTools,
660
667
  toolExtensionPaths,
661
668
  resolvedMcpSelections,
662
669
  effectiveMcpSelections,
@@ -740,6 +747,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
740
747
 
741
748
  const toolPlan = resolvePiLaunchToolPlan({
742
749
  tools: input.tools,
750
+ excludeTools: input.excludeTools,
743
751
  allowNestedSubagents: input.allowNestedSubagents,
744
752
  extensions: input.extensions,
745
753
  subagentOnlyExtensions: input.subagentOnlyExtensions,
@@ -766,6 +774,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
766
774
  );
767
775
  if (toolPlan.effectiveToolAllowlist.length > 0)
768
776
  args.push(toolPlan.effectiveToolAllowlist.join(","));
777
+ } else if (toolPlan.excludeTools.length > 0) {
778
+ args.push("--exclude-tools", toolPlan.excludeTools.join(","));
769
779
  }
770
780
  if (toolPlan.disableAmbientExtensions) {
771
781
  args.push("--no-extensions");
@@ -836,6 +846,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
836
846
  );
837
847
  env[RUNTIME_EXTENSION_ACK_PATH_ENV] = runtimeAcknowledgedExtensionsPath;
838
848
  let toolDiagnosticPath: string | undefined;
849
+ // Child launch environments are merged over process.env. Explicitly clear
850
+ // parent-scoped diagnostics so a nested zero-tool child cannot validate
851
+ // against, or overwrite, its parent's required-tool report.
852
+ env[REQUIRED_CHILD_TOOLS_ENV] = undefined;
853
+ env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = undefined;
839
854
  if (toolPlan.requiredChildTools.length > 0) {
840
855
  if (!tempDir)
841
856
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
@@ -999,10 +1014,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
999
1014
  env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
1000
1015
  if (encodedPermissionRules)
1001
1016
  env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
1017
+ env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
1018
+ env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
1002
1019
  if (input.structuredOutput) {
1003
1020
  env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
1004
1021
  env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
1005
1022
  if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
1023
+ if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
1006
1024
  }
1007
1025
  if (input.steerInboxDir) {
1008
1026
  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
  }
@@ -5,17 +5,21 @@ import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
7
7
  import type { JsonSchemaObject } from "../../shared/types.ts";
8
+ import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
8
9
 
9
10
  export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
10
11
  export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
11
12
  export const STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE";
13
+ export const STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED";
12
14
  export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
15
+ export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
13
16
 
14
17
  export interface StructuredOutputRuntime {
15
18
  schema: JsonSchemaObject;
16
19
  schemaPath: string;
17
20
  outputPath: string;
18
21
  acceptanceReportPath?: string;
22
+ acceptanceReportRequired?: boolean;
19
23
  }
20
24
 
21
25
  const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
@@ -61,14 +65,14 @@ function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inh
61
65
  return rewritten;
62
66
  }
63
67
 
64
- export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?: boolean } = {}): JsonSchemaObject {
68
+ export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?: "optional" | "required" } = {}): JsonSchemaObject {
65
69
  return {
66
70
  type: "object",
67
71
  properties: {
68
72
  value: rewriteLocalJsonPointerRefs(schema, "#/properties/value"),
69
73
  ...(options.acceptanceReport ? { acceptanceReport: { type: "object" } } : {}),
70
74
  },
71
- required: ["value"],
75
+ required: ["value", ...(options.acceptanceReport === "required" ? ["acceptanceReport"] : [])],
72
76
  additionalProperties: false,
73
77
  };
74
78
  }
@@ -129,7 +133,7 @@ export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"):
129
133
  }
130
134
  }
131
135
 
132
- export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: { captureAcceptanceReport?: boolean } = {}): StructuredOutputRuntime {
136
+ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: { acceptanceReport?: ResolvedAcceptanceReportMode } = {}): StructuredOutputRuntime {
133
137
  assertJsonSchemaObject(schema);
134
138
  const rootDir = baseDir ?? os.tmpdir();
135
139
  fs.mkdirSync(rootDir, { recursive: true });
@@ -137,7 +141,14 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
137
141
  const schemaPath = path.join(dir, "schema.json");
138
142
  const outputPath = path.join(dir, "output.json");
139
143
  fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 });
140
- return { schema, schemaPath, outputPath, ...(options.captureAcceptanceReport ? { acceptanceReportPath: path.join(dir, "acceptance-report.json") } : {}) };
144
+ return {
145
+ schema,
146
+ schemaPath,
147
+ outputPath,
148
+ ...(options.acceptanceReport && options.acceptanceReport !== "off"
149
+ ? { acceptanceReportPath: path.join(dir, "acceptance-report.json"), acceptanceReportRequired: options.acceptanceReport === "required" }
150
+ : {}),
151
+ };
141
152
  }
142
153
 
143
154
  export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
@@ -178,14 +189,30 @@ export async function readStructuredOutput(runtime: StructuredOutputRuntime): Pr
178
189
  }
179
190
 
180
191
  export function readStructuredOutputAcceptanceReport(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
181
- if (!runtime.acceptanceReportPath || !fs.existsSync(runtime.acceptanceReportPath)) return {};
192
+ if (!runtime.acceptanceReportPath) return {};
193
+ if (!fs.existsSync(runtime.acceptanceReportPath)) {
194
+ return runtime.acceptanceReportRequired ? { error: MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR } : {};
195
+ }
182
196
  try {
183
- return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) };
197
+ return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) as unknown };
184
198
  } catch (error) {
185
199
  return { error: `Failed to read structured output acceptance report: ${error instanceof Error ? error.message : String(error)}` };
186
200
  }
187
201
  }
188
202
 
203
+ export function clearStructuredOutputCaptures(runtime: StructuredOutputRuntime): string | undefined {
204
+ let cleanupError: string | undefined;
205
+ for (const filePath of [runtime.outputPath, runtime.acceptanceReportPath]) {
206
+ if (!filePath) continue;
207
+ try {
208
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
209
+ } catch (error) {
210
+ cleanupError ??= `Failed to clear stale structured output capture ${filePath}: ${error instanceof Error ? error.message : String(error)}`;
211
+ }
212
+ }
213
+ return cleanupError;
214
+ }
215
+
189
216
  export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void {
190
217
  if (!runtime) return;
191
218
  try {
@@ -8,7 +8,8 @@ import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, P
8
8
  import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
9
  import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
10
  import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
- import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
11
+ import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
+ import { validateAcceptanceReport } from "./acceptance.ts";
12
13
  import {
13
14
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
14
15
  formatChildToolDiagnostic,
@@ -729,10 +730,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
729
730
  });
730
731
  const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
731
732
  const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
733
+ const structuredAcceptanceReportRequired = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] === "1";
732
734
  const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
733
735
  if (structuredOutputPath && structuredSchemaPath) {
734
736
  const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
735
- const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: Boolean(structuredAcceptanceReportPath) });
737
+ const acceptanceReportMode = structuredAcceptanceReportPath
738
+ ? structuredAcceptanceReportRequired ? "required" as const : "optional" as const
739
+ : undefined;
740
+ const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: acceptanceReportMode });
736
741
  const registerTool = pi.registerTool as unknown as (tool: {
737
742
  name: string;
738
743
  label: string;
@@ -750,11 +755,23 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
750
755
  if (validation.status === "invalid") {
751
756
  throw new Error(`Structured output validation failed: ${validation.message}`);
752
757
  }
758
+ if (structuredAcceptanceReportRequired && params.acceptanceReport === undefined) {
759
+ throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
760
+ }
761
+ if (structuredAcceptanceReportRequired && params.acceptanceReport !== undefined) {
762
+ const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
763
+ if (!acceptanceValidation.report) {
764
+ throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
765
+ }
766
+ }
753
767
  fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
754
- fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
755
768
  if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
769
+ fs.mkdirSync(path.dirname(structuredAcceptanceReportPath), { recursive: true });
756
770
  fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
771
+ } else if (structuredAcceptanceReportPath && fs.existsSync(structuredAcceptanceReportPath)) {
772
+ fs.unlinkSync(structuredAcceptanceReportPath);
757
773
  }
774
+ fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
758
775
  return {
759
776
  content: [{ type: "text", text: "Structured output captured." }],
760
777
  details: { path: structuredOutputPath },
@@ -34,13 +34,23 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
34
34
  /\bmake\s+(?:the\s+)?code\s+changes\b/i,
35
35
  ];
36
36
 
37
- // The prohibition's object ends at punctuation or at a coordinating word
38
- // (but/and/then), so a follow-on clause like "but implement the fix" stays in
39
- // the text for write-intent testing instead of being swallowed as the object.
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;
37
+ // The prohibition's object ends at punctuation, a serialized line separator,
38
+ // or a coordinating word (but/and/then), so a follow-on clause like "but
39
+ // implement the fix" stays in the text for write-intent testing instead of
40
+ // being swallowed as the object.
41
+ // Accept serialized line separators too: workflow prompts can carry literal
42
+ // `\\n`/`\\r\\n` between clauses instead of decoded newlines.
43
+ const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
44
+ const COORDINATED_NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+((?=(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b)(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*?\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b(?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
41
45
 
42
46
  /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
43
47
  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;
48
+ const SCOPED_PROHIBITION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|under|within|outside|except|other\s+than|besides)\b/i;
49
+ const OUTPUT_INSTRUCTION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|within)\s+(?:(?:(?:your|the)\s+)(?:final\s+)?|final\s+)(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?)(?:\s*\/\s*(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?))*\b(?![\\/])/i;
50
+
51
+ function hasScopedProhibitionContinuation(text: string): boolean {
52
+ return SCOPED_PROHIBITION_CONTINUATION.test(text) && !OUTPUT_INSTRUCTION_CONTINUATION.test(text);
53
+ }
44
54
 
45
55
  const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
46
56
  /\bdo not edit files?\s+outside\b/i,
@@ -135,11 +145,15 @@ function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis
135
145
  || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
136
146
  let blanket = present;
137
147
  let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
138
- strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), (_match, object: string) => {
148
+ const stripNoEditProhibition = (match: string, object: string, offset: number, source: string): string => {
139
149
  present = true;
140
- if (GENERIC_PROHIBITION_OBJECT.test(object)) blanket = true;
150
+ if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
141
151
  return " ";
142
- });
152
+ };
153
+ strippedText = strippedText.replace(new RegExp(COORDINATED_NO_EDIT_PROHIBITION_PATTERN.source, COORDINATED_NO_EDIT_PROHIBITION_PATTERN.flags), stripNoEditProhibition);
154
+ strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), stripNoEditProhibition);
155
+ // Restore boundaries after stripping a prohibition from serialized prompts.
156
+ strippedText = strippedText.replace(/\\(?:r\\n|n)/g, "\n");
143
157
  return { present, blanket, strippedText };
144
158
  }
145
159
 
@@ -16,7 +16,7 @@ export const DEFAULT_FAST_TOOL_TIMEOUT_TOOLS = new Set([
16
16
  ]);
17
17
 
18
18
  /** Tools whose normal job can be to wait for a person or another run. */
19
- export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "subagent_wait"]);
19
+ export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait"]);
20
20
 
21
21
  // Backward-compatible export name for existing callers/tests.
22
22
  export const TOOL_TIMEOUT_ALLOWLIST = TOOL_TIMEOUT_EXEMPT_TOOLS;