pi-subagents 0.60.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 (76) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/docs/agents.md +7 -3
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +13 -4
  8. package/docs/workflows.md +14 -13
  9. package/install.mjs +1 -1
  10. package/package.json +1 -1
  11. package/skills/pi-subagents/SKILL.md +6 -4
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +41 -11
  14. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  15. package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  16. package/skills/pi-subagents/references/review-and-validation.md +1 -1
  17. package/src/agents/agent-management.ts +121 -63
  18. package/src/agents/agent-serializer.ts +3 -0
  19. package/src/agents/agents.ts +543 -223
  20. package/src/agents/runtime-agent-registry.ts +5 -1
  21. package/src/api/background-work.ts +7 -2
  22. package/src/api/external-runs.ts +67 -4
  23. package/src/api/preflight.ts +17 -8
  24. package/src/api/shared-types.ts +1 -0
  25. package/src/extension/index.ts +7 -4
  26. package/src/extension/public-execution.ts +48 -4
  27. package/src/extension/rpc.ts +62 -4
  28. package/src/extension/schemas.ts +17 -9
  29. package/src/extension/tool-description.ts +15 -19
  30. package/src/runs/background/active-async-capacity.ts +26 -7
  31. package/src/runs/background/async-execution.ts +98 -51
  32. package/src/runs/background/async-job-tracker.ts +62 -3
  33. package/src/runs/background/async-resume.ts +6 -3
  34. package/src/runs/background/async-status.ts +59 -9
  35. package/src/runs/background/auto-drain.ts +1 -1
  36. package/src/runs/background/fleet-view.ts +1 -1
  37. package/src/runs/background/process-terminal.ts +16 -0
  38. package/src/runs/background/result-watcher.ts +1 -1
  39. package/src/runs/background/resume-guidance.ts +1 -1
  40. package/src/runs/background/run-status.ts +2 -2
  41. package/src/runs/background/scheduled-runs.ts +63 -6
  42. package/src/runs/background/steering.ts +4 -1
  43. package/src/runs/background/subagent-runner.ts +15 -9
  44. package/src/runs/background/subagent-wait.ts +20 -21
  45. package/src/runs/background/wait-completions.ts +1 -1
  46. package/src/runs/background/wait-tool.ts +18 -18
  47. package/src/runs/foreground/execution.ts +75 -9
  48. package/src/runs/foreground/subagent-executor.ts +181 -60
  49. package/src/runs/shared/acceptance.ts +113 -27
  50. package/src/runs/shared/capability-ceiling.ts +1 -0
  51. package/src/runs/shared/dynamic-fanout.ts +1 -1
  52. package/src/runs/shared/host-step-status.ts +1 -0
  53. package/src/runs/shared/model-fallback.ts +61 -17
  54. package/src/runs/shared/parallel-utils.ts +2 -6
  55. package/src/runs/shared/permissions.ts +1 -1
  56. package/src/runs/shared/pi-args.ts +24 -12
  57. package/src/runs/shared/pi-spawn.ts +69 -35
  58. package/src/runs/shared/structured-output.ts +33 -6
  59. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  60. package/src/runs/shared/task-intent.ts +17 -6
  61. package/src/runs/shared/tool-timeout.ts +1 -1
  62. package/src/runs/shared/workflow-graph.ts +3 -2
  63. package/src/shared/atomic-json.ts +3 -1
  64. package/src/shared/fork-context.ts +0 -12
  65. package/src/shared/fork-session-cwd.ts +27 -0
  66. package/src/shared/launch-contract.ts +3 -0
  67. package/src/shared/types.ts +43 -4
  68. package/src/shared/workflow-child-permit.ts +91 -0
  69. package/src/slash/prompt-template-bridge.ts +37 -1
  70. package/src/slash/slash-commands.ts +18 -26
  71. package/src/slash/subagents-admin.ts +2 -0
  72. package/src/tui/render.ts +95 -52
  73. package/src/workflows/scripted-workflow.ts +153 -4
  74. package/src/workflows/workflow-child-summary.ts +1 -1
  75. package/src/workflows/workflow-receipt.ts +41 -4
  76. package/src/workflows/workflow-resources.ts +150 -0
@@ -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,22 @@ 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;
41
44
 
42
45
  /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
43
46
  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;
47
+ const SCOPED_PROHIBITION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|under|within|outside|except|other\s+than|besides)\b/i;
48
+ 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;
49
+
50
+ function hasScopedProhibitionContinuation(text: string): boolean {
51
+ return SCOPED_PROHIBITION_CONTINUATION.test(text) && !OUTPUT_INSTRUCTION_CONTINUATION.test(text);
52
+ }
44
53
 
45
54
  const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
46
55
  /\bdo not edit files?\s+outside\b/i,
@@ -135,11 +144,13 @@ function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis
135
144
  || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
136
145
  let blanket = present;
137
146
  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) => {
147
+ strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), (match, object: string, offset: number, source: string) => {
139
148
  present = true;
140
- if (GENERIC_PROHIBITION_OBJECT.test(object)) blanket = true;
149
+ if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
141
150
  return " ";
142
151
  });
152
+ // Restore boundaries after stripping a prohibition from serialized prompts.
153
+ strippedText = strippedText.replace(/\\(?:r\\n|n)/g, "\n");
143
154
  return { present, blanket, strippedText };
144
155
  }
145
156
 
@@ -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", "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;
@@ -15,7 +15,8 @@ export interface WorkflowGraphBuildInput {
15
15
 
16
16
  /** Return displayable workflow stages while hiding structural parallel groups and host monitors. */
17
17
  export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
18
- if (!graph?.nodes?.length) return [];
18
+ const nodes = graph?.nodes;
19
+ if (!nodes?.length) return [];
19
20
  const stages: WorkflowGraphNode[] = [];
20
21
  const visit = (node: WorkflowGraphNode): void => {
21
22
  if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
@@ -24,7 +25,7 @@ export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined
24
25
  }
25
26
  if (node.kind !== "host-step") stages.push(node);
26
27
  };
27
- for (const node of graph.nodes) visit(node);
28
+ for (const node of nodes) visit(node);
28
29
  return stages;
29
30
  }
30
31
 
@@ -15,6 +15,7 @@ type AtomicJsonWriterOptions = {
15
15
  mode?: number;
16
16
  retryRenameErrors?: boolean;
17
17
  retryDirectoryErrors?: boolean;
18
+ ignoreCleanupErrorAfterSuccess?: boolean;
18
19
  retryDelaysMs?: readonly number[];
19
20
  wait?: (delayMs: number) => void;
20
21
  };
@@ -46,6 +47,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
46
47
  const mode = options.mode;
47
48
  const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
48
49
  const retryDirectoryErrors = options.retryDirectoryErrors ?? retryRenameErrors;
50
+ const ignoreCleanupErrorAfterSuccess = options.ignoreCleanupErrorAfterSuccess ?? false;
49
51
  const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
50
52
  const renameRetryDelaysMs = retryRenameErrors ? retryDelaysMs : [];
51
53
  const directoryRetryDelaysMs = retryDirectoryErrors ? retryDelaysMs : [];
@@ -71,7 +73,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
71
73
  } catch (cleanupError) {
72
74
  // Preserve the write/rename failure: cleanup is best effort and must
73
75
  // not hide the error callers need to classify or report.
74
- if (writeError === undefined) throw cleanupError;
76
+ if (writeError === undefined && !ignoreCleanupErrorAfterSuccess) throw cleanupError;
75
77
  }
76
78
  }
77
79
  };
@@ -174,18 +174,6 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
174
174
  });
175
175
  }
176
176
 
177
- /** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
178
- export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
179
- const entries = readSessionEntries(sessionFile);
180
- const header = entries[0];
181
- if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
182
- const resolvedCwd = path.resolve(cwd);
183
- const effectiveCwd = fs.realpathSync.native(resolvedCwd);
184
- if (header.cwd === effectiveCwd) return;
185
- header.cwd = effectiveCwd;
186
- fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
187
- }
188
-
189
177
  export function createForkContextResolver(
190
178
  sessionManager: ForkableSessionManager,
191
179
  requestedContext: unknown,
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ interface SessionEntry {
5
+ type?: string;
6
+ cwd?: string;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ /** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
11
+ export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
12
+ const lines = fs.readFileSync(sessionFile, "utf-8").split("\n").filter((line) => line.trim().length > 0);
13
+ const entries = lines.map((line, index) => {
14
+ try {
15
+ return JSON.parse(line) as SessionEntry;
16
+ } catch (error) {
17
+ const cause = error instanceof Error ? error : new Error(String(error));
18
+ throw new Error(`Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`, { cause });
19
+ }
20
+ });
21
+ const header = entries[0];
22
+ if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
23
+ const effectiveCwd = fs.realpathSync.native(path.resolve(cwd));
24
+ if (header.cwd === effectiveCwd) return;
25
+ header.cwd = effectiveCwd;
26
+ fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
27
+ }
@@ -51,6 +51,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
51
51
  fast: agent.fast,
52
52
  thinking: agent.thinking,
53
53
  tools: agent.tools,
54
+ excludeTools: agent.excludeTools,
54
55
  allowNestedSubagents: agent.allowNestedSubagents,
55
56
  mcpDirectTools: agent.mcpDirectTools,
56
57
  extensions: agent.extensions,
@@ -93,6 +94,7 @@ export interface LaunchBindingInput {
93
94
  inheritSkills: boolean;
94
95
  skills?: string[];
95
96
  tools?: string[];
97
+ excludeTools?: string[];
96
98
  extensions?: string[];
97
99
  subagentOnlyExtensions?: string[];
98
100
  mcpDirectTools?: string[];
@@ -120,6 +122,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
120
122
  inheritSkills: input.inheritSkills,
121
123
  skills: input.skills,
122
124
  tools: input.tools,
125
+ excludeTools: input.excludeTools,
123
126
  extensions: input.extensions,
124
127
  subagentOnlyExtensions: input.subagentOnlyExtensions,
125
128
  mcpDirectTools: input.mcpDirectTools,
@@ -143,6 +143,20 @@ export interface WorkflowRecoveryAction {
143
143
  taskRequired: true;
144
144
  }
145
145
 
146
+ /**
147
+ * Bounded, host-generated identity for a resolved pi-subagents workflow resource.
148
+ * Permission/policy extensions can use it to distinguish resolved content from
149
+ * raw workflow scripts. This audit projection never grants execution authority.
150
+ */
151
+ export interface WorkflowResourceProvenanceV1 {
152
+ kind: "workflow";
153
+ name: string;
154
+ version: number;
155
+ invocation: "named";
156
+ expansion: "resolved";
157
+ id: string;
158
+ }
159
+
146
160
  /**
147
161
  * Bounded, launch-declared workflow lane metadata. This is display and
148
162
  * triage information only; capability ceilings, authorization, and cleanup
@@ -189,6 +203,7 @@ export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
189
203
  requestedContext?: "fresh" | "fork";
190
204
  resolvedContext?: "fresh" | "fork" | "mixed";
191
205
  outputReference?: string;
206
+ acceptanceRecovery?: AcceptanceRecoveryMetadata;
192
207
  externalAdapter?: ExternalCliReceiptMetadata;
193
208
  continuation: { runIds: string[] };
194
209
  };
@@ -199,6 +214,7 @@ export interface WorkflowReceipt {
199
214
  state: WorkflowReceiptState;
200
215
  createdAt: number;
201
216
  entries: Record<string, WorkflowReceiptEntry>;
217
+ resource?: WorkflowResourceProvenanceV1;
202
218
  hostSteps?: HostStepNodeV1[];
203
219
  workflowChildren?: WorkflowChildSummaryV1;
204
220
  workflowResolution?: WorkflowTerminalResolution;
@@ -330,7 +346,7 @@ export interface CompletionBatchConfig {
330
346
 
331
347
  export interface WaitToolConfigObject {
332
348
  enabled?: boolean;
333
- /** Default blocking window for subagent_wait calls that omit timeoutMs. */
349
+ /** Default blocking window for bg_wait calls that omit timeoutMs. */
334
350
  defaultTimeoutMs?: number;
335
351
  }
336
352
 
@@ -758,11 +774,13 @@ export interface SteeringRecoveryDescriptor {
758
774
  model?: string;
759
775
  modelProvider?: string;
760
776
  modelOverrideFromParent?: boolean;
777
+ modelOrigin?: "explicit" | "inherited" | "configured";
761
778
  fallbackModels?: string[];
762
779
  fast?: boolean;
763
780
  thinking?: string;
764
781
  thinkingCeiling?: ThinkingLevel;
765
782
  tools?: string[];
783
+ excludeTools?: string[];
766
784
  allowNestedSubagents?: boolean;
767
785
  extensions?: string[];
768
786
  subagentOnlyExtensions?: string[];
@@ -977,6 +995,7 @@ export interface AcceptanceReviewGate {
977
995
 
978
996
  export interface AcceptanceConfig {
979
997
  level?: AcceptanceLevel;
998
+ report?: "on" | "off";
980
999
  criteria?: Array<string | AcceptanceGate>;
981
1000
  evidence?: AcceptanceEvidenceKind[];
982
1001
  verify?: AcceptanceVerifyCommand[];
@@ -1086,6 +1105,18 @@ export type AcceptanceLedgerStatus =
1086
1105
  | "reviewed"
1087
1106
  | "accepted";
1088
1107
 
1108
+ /**
1109
+ * Durable child evidence that can be handed to a read-only review after the
1110
+ * acceptance envelope itself was rejected. This never upgrades acceptance;
1111
+ * the enclosing ledger remains rejected and the child remains unsuccessful.
1112
+ */
1113
+ export interface AcceptanceRecoveryMetadata {
1114
+ status: "available-for-review";
1115
+ reason: "acceptance-metadata-rejected";
1116
+ reportPath: string;
1117
+ reportHash: string;
1118
+ }
1119
+
1089
1120
  export interface AcceptanceLedger {
1090
1121
  status: AcceptanceLedgerStatus;
1091
1122
  evidenceStatus: AcceptanceEvidenceStatus;
@@ -1095,6 +1126,7 @@ export interface AcceptanceLedger {
1095
1126
  criteria: ResolvedAcceptanceGate[];
1096
1127
  childReport?: AcceptanceReport;
1097
1128
  childReportParseError?: string;
1129
+ recovery?: AcceptanceRecoveryMetadata;
1098
1130
  runtimeChecks: AcceptanceRuntimeCheck[];
1099
1131
  verifyRuns: AcceptanceVerifyResult[];
1100
1132
  reviewResult?: AcceptanceReviewResult;
@@ -1283,7 +1315,7 @@ export interface WaitCompletionChild {
1283
1315
  }
1284
1316
 
1285
1317
  /**
1286
- * Terminal completion observed for a run a subagent_wait call covered. Carries run
1318
+ * Terminal completion observed for a run a bg_wait call covered. Carries run
1287
1319
  * identity and the artifact trail; output text stays in the tool result content.
1288
1320
  */
1289
1321
  export interface WaitCompletion {
@@ -1312,7 +1344,7 @@ export interface AgentCapabilityRow {
1312
1344
  restrictionSources?: string[];
1313
1345
  aliases?: string[];
1314
1346
  runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
1315
- tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1347
+ tools: { ambient: boolean; names: string[]; excludeTools?: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1316
1348
  model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
1317
1349
  execution?: { defaultAsync?: boolean; timeoutMs?: number };
1318
1350
  output?: { path?: string; mode?: OutputMode };
@@ -1329,7 +1361,7 @@ export interface Details {
1329
1361
  results: SingleResult[];
1330
1362
  workflowChildren?: WorkflowChildSummaryV1;
1331
1363
  /**
1332
- * Terminal completion payloads for runs this subagent_wait call observed
1364
+ * Terminal completion payloads for runs this bg_wait call observed
1333
1365
  * finishing. Async completions travel as result files that are consumed and
1334
1366
  * deleted after text delivery, so without this field their run and artifact
1335
1367
  * identity never reaches tool_result details.
@@ -1401,6 +1433,7 @@ export interface Details {
1401
1433
  mission?: MissionRecord;
1402
1434
  workflow?: {
1403
1435
  value?: unknown;
1436
+ resource?: WorkflowResourceProvenanceV1;
1404
1437
  preflightWarnings?: string[];
1405
1438
  trace: Array<{
1406
1439
  operation: "run" | "status" | "steer" | "host";
@@ -2117,6 +2150,8 @@ export interface ActiveAsyncCapacitySnapshot {
2117
2150
  export interface SubagentState {
2118
2151
  baseCwd: string;
2119
2152
  currentSessionId: string | null;
2153
+ /** Session for which active status projections were restored successfully. */
2154
+ statusProjectionSessionId?: string | null;
2120
2155
  /** Reload-stable identity for this parent Pi process/window. */
2121
2156
  completionOwnerId?: string;
2122
2157
  /** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
@@ -2283,6 +2318,8 @@ export interface RunSyncOptions {
2283
2318
  allowIntercomDetach?: boolean;
2284
2319
  intercomEvents?: IntercomEventBus;
2285
2320
  onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
2321
+ /** Internal structured-delegation transport optimization: skip unchanged live snapshots. */
2322
+ suppressUnchangedDelegationUpdates?: boolean;
2286
2323
  onControlEvent?: (event: ControlEvent) => void;
2287
2324
  /** Exposes a non-terminating detach callback while the child is active. */
2288
2325
  onDetachReady?: (detach: (reason?: string) => boolean) => void;
@@ -2318,6 +2355,8 @@ export interface RunSyncOptions {
2318
2355
  fast?: boolean;
2319
2356
  /** The override came from the running parent session, not configuration. */
2320
2357
  modelOverrideFromParent?: boolean;
2358
+ /** How the launch model was selected: explicit per-call, configured agent primary, or inherited parent. */
2359
+ modelOrigin?: "explicit" | "inherited" | "configured";
2321
2360
  /** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
2322
2361
  llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
2323
2362
  /** Override the agent's default thinking level for this run */