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
@@ -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", "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;
@@ -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,
@@ -780,6 +780,7 @@ export interface SteeringRecoveryDescriptor {
780
780
  thinking?: string;
781
781
  thinkingCeiling?: ThinkingLevel;
782
782
  tools?: string[];
783
+ excludeTools?: string[];
783
784
  allowNestedSubagents?: boolean;
784
785
  extensions?: string[];
785
786
  subagentOnlyExtensions?: string[];
@@ -994,6 +995,7 @@ export interface AcceptanceReviewGate {
994
995
 
995
996
  export interface AcceptanceConfig {
996
997
  level?: AcceptanceLevel;
998
+ report?: "on" | "off";
997
999
  criteria?: Array<string | AcceptanceGate>;
998
1000
  evidence?: AcceptanceEvidenceKind[];
999
1001
  verify?: AcceptanceVerifyCommand[];
@@ -1342,7 +1344,7 @@ export interface AgentCapabilityRow {
1342
1344
  restrictionSources?: string[];
1343
1345
  aliases?: string[];
1344
1346
  runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
1345
- tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1347
+ tools: { ambient: boolean; names: string[]; excludeTools?: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1346
1348
  model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
1347
1349
  execution?: { defaultAsync?: boolean; timeoutMs?: number };
1348
1350
  output?: { path?: string; mode?: OutputMode };
@@ -417,7 +417,7 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
417
417
  }
418
418
  if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
419
419
  const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
420
- if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait" && message.toolName !== "subagent_wait")) return undefined;
420
+ if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait")) return undefined;
421
421
  return isSubagentDetails(message.details) ? message.details : undefined;
422
422
  }
423
423
 
@@ -97,6 +97,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
97
97
  systemPrompt: agent.systemPrompt,
98
98
  ...(agent.skills !== undefined ? { skills: [...agent.skills] } : {}),
99
99
  ...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
100
+ ...(agent.excludeTools !== undefined ? { excludeTools: [...agent.excludeTools] } : {}),
100
101
  ...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
101
102
  ...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
102
103
  ...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
@@ -188,6 +189,7 @@ function metadataFor(agent: AgentConfig): string {
188
189
  if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
189
190
  if (agent.thinking !== undefined) lines.push(`Thinking: ${agent.thinking === false ? "off" : agent.thinking}`);
190
191
  if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
192
+ if (agent.excludeTools?.length) lines.push(`Excluded tools: ${agent.excludeTools.join(", ")}`);
191
193
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
192
194
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
193
195
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);