pi-subagents 0.27.0 → 0.29.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 (33) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +16 -15
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +3 -6
  5. package/src/agents/agent-management.ts +10 -6
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agents.ts +303 -6
  8. package/src/agents/chain-serializer.ts +4 -9
  9. package/src/extension/doctor.ts +4 -3
  10. package/src/extension/fanout-child.ts +0 -1
  11. package/src/extension/index.ts +1 -4
  12. package/src/extension/schemas.ts +31 -28
  13. package/src/intercom/intercom-bridge.ts +11 -1
  14. package/src/runs/background/async-execution.ts +20 -7
  15. package/src/runs/background/run-status.ts +1 -7
  16. package/src/runs/background/subagent-runner.ts +73 -146
  17. package/src/runs/foreground/chain-execution.ts +61 -13
  18. package/src/runs/foreground/execution.ts +28 -172
  19. package/src/runs/foreground/subagent-executor.ts +25 -40
  20. package/src/runs/shared/acceptance.ts +605 -22
  21. package/src/runs/shared/completion-guard.ts +3 -26
  22. package/src/runs/shared/model-fallback.ts +38 -0
  23. package/src/runs/shared/parallel-utils.ts +6 -8
  24. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  25. package/src/shared/atomic-json.ts +68 -11
  26. package/src/shared/settings.ts +1 -0
  27. package/src/shared/types.ts +8 -32
  28. package/src/shared/utils.ts +2 -1
  29. package/src/tui/render.ts +1 -11
  30. package/src/runs/shared/acceptance-contract.ts +0 -291
  31. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  32. package/src/runs/shared/acceptance-finalization.ts +0 -161
  33. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -4,6 +4,7 @@ import { parseFrontmatter } from "./frontmatter.ts";
4
4
  import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
5
5
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
6
6
  import type { ChainStep } from "../shared/settings.ts";
7
+ import type { AgentSource } from "./agents.ts";
7
8
 
8
9
  function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
9
10
  const lines = sectionBody.split("\n");
@@ -83,7 +84,7 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
83
84
  return step;
84
85
  }
85
86
 
86
- export function parseChain(content: string, source: "user" | "project", filePath: string): ChainConfig {
87
+ export function parseChain(content: string, source: AgentSource, filePath: string): ChainConfig {
87
88
  const { frontmatter, body } = parseFrontmatter(content);
88
89
  if (!frontmatter.name || !frontmatter.description) {
89
90
  throw new Error("Chain frontmatter must include name and description");
@@ -124,7 +125,7 @@ export function parseChain(content: string, source: "user" | "project", filePath
124
125
  };
125
126
  }
126
127
 
127
- export function parseJsonChain(content: string, source: "user" | "project", filePath: string): ChainConfig {
128
+ export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
128
129
  let parsed: unknown;
129
130
  try {
130
131
  parsed = JSON.parse(content);
@@ -151,17 +152,11 @@ export function parseJsonChain(content: string, source: "user" | "project", file
151
152
  throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
152
153
  }
153
154
  const stepRecord = step as Record<string, unknown>;
154
- const parallel = stepRecord.parallel;
155
- if (Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
156
- throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
157
- }
158
- if (parallel && typeof parallel === "object" && !Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
159
- throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on dynamic fanout groups; set acceptance on the dynamic template.`);
160
- }
161
155
  const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
162
156
  if (acceptanceErrors.length > 0) {
163
157
  throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
164
158
  }
159
+ const parallel = stepRecord.parallel;
165
160
  if (Array.isArray(parallel)) {
166
161
  for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
167
162
  const task = parallel[taskIndex];
@@ -81,7 +81,7 @@ function formatExistingDirectory(label: string, dirPath: string): string {
81
81
  }
82
82
 
83
83
  function formatSourceCounts(counts: Record<AgentSource, number>): string {
84
- return `builtin ${counts.builtin}, user ${counts.user}, project ${counts.project}`;
84
+ return `builtin ${counts.builtin}, package ${counts.package}, user ${counts.user}, project ${counts.project}`;
85
85
  }
86
86
 
87
87
  function formatSkillSourceCounts(skills: Array<{ source: SkillSource }>): string {
@@ -132,15 +132,16 @@ function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
132
132
  const discovered = deps.discoverAgentsAll(input.cwd);
133
133
  const agentCounts = {
134
134
  builtin: discovered.builtin.length,
135
+ package: discovered.package?.length ?? 0,
135
136
  user: discovered.user.length,
136
137
  project: discovered.project.length,
137
138
  };
138
139
  const chainCounts = discovered.chains.reduce<Record<AgentSource, number>>((counts, chain) => {
139
140
  counts[chain.source] += 1;
140
141
  return counts;
141
- }, { builtin: 0, user: 0, project: 0 });
142
+ }, { builtin: 0, package: 0, user: 0, project: 0 });
142
143
  return [
143
- `- agents: total ${agentCounts.builtin + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
144
+ `- agents: total ${agentCounts.builtin + agentCounts.package + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
144
145
  `- chains: total ${discovered.chains.length} (${formatSourceCounts(chainCounts)})`,
145
146
  ].join("\n");
146
147
  }),
@@ -156,7 +156,6 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
156
156
  label: "Subagent",
157
157
  description: [
158
158
  "Delegate to subagents from child-safe fanout mode.",
159
- "For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
160
159
  "Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
161
160
  "Agent config mutation actions create, update, and delete are blocked in this mode.",
162
161
  ].join("\n"),
@@ -33,8 +33,7 @@ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
33
33
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
34
34
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
35
35
  import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
36
- import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
37
- import registerFanoutChildSubagentExtension from "./fanout-child.ts";
36
+ import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
38
37
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
39
38
  import { loadConfig } from "./config.ts";
40
39
  import {
@@ -209,7 +208,6 @@ class SubagentControlNoticeComponent implements Component {
209
208
 
210
209
  export default function registerSubagentExtension(pi: ExtensionAPI): void {
211
210
  if (process.env[SUBAGENT_CHILD_ENV] === "1") {
212
- if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
213
211
  return;
214
212
  }
215
213
  const globalStore = globalThis as Record<string, unknown>;
@@ -395,7 +393,6 @@ EXECUTION (use exactly ONE mode):
395
393
  • CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
396
394
  • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
397
395
  • Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
398
- • Goal-style requests: when the user says “/goal”, “goal”, “active goal”, “work until evidence says done”, or “verify against a goal”, model that as explicit acceptance. Use acceptance.criteria for the target, acceptance.evidence/verify for proof, acceptance.stopRules for constraints, and acceptance.maxFinalizationTurns for the bounded loop.
399
396
 
400
397
  CHAIN TEMPLATE VARIABLES (use in task strings):
401
398
  • {task} - The original task/request from the user
@@ -78,39 +78,42 @@ const AcceptanceReviewGateSchema = Type.Object({
78
78
  }, { additionalProperties: false });
79
79
 
80
80
  const AcceptanceOverride = Type.Unsafe({
81
- type: "object",
82
- properties: {
83
- criteria: {
84
- type: "array",
85
- items: {
86
- anyOf: [
87
- { type: "string" },
88
- AcceptanceGateSchema,
89
- ],
81
+ anyOf: [
82
+ { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
83
+ { const: false },
84
+ {
85
+ type: "object",
86
+ properties: {
87
+ level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
88
+ criteria: {
89
+ type: "array",
90
+ items: {
91
+ anyOf: [
92
+ { type: "string" },
93
+ AcceptanceGateSchema,
94
+ ],
95
+ },
96
+ },
97
+ evidence: { type: "array", items: AcceptanceEvidenceKind },
98
+ verify: { type: "array", items: AcceptanceVerifyCommandSchema },
99
+ review: {
100
+ anyOf: [
101
+ { const: false },
102
+ AcceptanceReviewGateSchema,
103
+ ],
104
+ },
105
+ stopRules: { type: "array", items: { type: "string" } },
106
+ reason: { type: "string" },
90
107
  },
108
+ additionalProperties: false,
91
109
  },
92
- evidence: { type: "array", items: AcceptanceEvidenceKind },
93
- verify: { type: "array", items: AcceptanceVerifyCommandSchema },
94
- review: AcceptanceReviewGateSchema,
95
- stopRules: { type: "array", items: { type: "string" } },
96
- maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
97
- },
98
- additionalProperties: false,
99
- allOf: [{
100
- anyOf: [
101
- { required: ["criteria"] },
102
- { required: ["evidence"] },
103
- { required: ["verify"] },
104
- { required: ["review"] },
105
- { required: ["stopRules"] },
106
- ],
107
- }],
108
- description: "Optional acceptance contract. Use this for goal-style requests such as /goal, goal, active goal, or work until evidence says done: criteria define the target, evidence/verify define proof, stopRules define constraints, and maxFinalizationTurns defines the bounded loop. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
110
+ ],
111
+ description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
109
112
  });
110
113
 
111
114
  const TaskItem = Type.Object({
112
- agent: Type.String(),
113
- task: Type.String(),
115
+ agent: Type.String(),
116
+ task: Type.String(),
114
117
  cwd: Type.Optional(Type.String()),
115
118
  count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
116
119
  output: Type.Optional(OutputOverride),
@@ -17,6 +17,16 @@ function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
17
17
  return path.join(agentDir, "extensions", PI_INTERCOM_PACKAGE_NAME);
18
18
  }
19
19
 
20
+ export const INTERCOM_EXTENSION_DIR_ENV = "PI_INTERCOM_EXTENSION_DIR";
21
+
22
+ // Launcher-provided override for the pi-intercom package directory. Lets a hermetic
23
+ // wrapper point the subagent intercom bridge at a read-only install (e.g. a Nix-store
24
+ // path) instead of seeding the package into the writable agent dir.
25
+ function envIntercomExtensionDir(): string | undefined {
26
+ const dir = process.env[INTERCOM_EXTENSION_DIR_ENV]?.trim();
27
+ return dir ? dir : undefined;
28
+ }
29
+
20
30
  function defaultIntercomConfigPath(agentDir = defaultAgentDir()): string {
21
31
  return path.join(agentDir, "intercom", "config.json");
22
32
  }
@@ -224,7 +234,7 @@ function configuredPiIntercomPackageDir(input: ResolveIntercomBridgeInput, agent
224
234
  }
225
235
 
226
236
  function resolveIntercomExtensionDir(input: ResolveIntercomBridgeInput, agentDir: string): string {
227
- const legacyDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir(agentDir));
237
+ const legacyDir = path.resolve(input.extensionDir ?? envIntercomExtensionDir() ?? defaultIntercomExtensionDir(agentDir));
228
238
  if (fs.existsSync(legacyDir)) return legacyDir;
229
239
  return configuredPiIntercomPackageDir(input, agentDir) ?? legacyDir;
230
240
  }
@@ -17,7 +17,7 @@ import type { RunnerStep } from "../shared/parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
19
19
  import { resolveChildCwd } from "../../shared/utils.ts";
20
- import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
20
+ import { buildModelCandidates, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
21
21
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
22
22
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
23
23
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -95,6 +95,7 @@ interface AsyncExecutionContext {
95
95
  cwd: string;
96
96
  currentSessionId: string;
97
97
  currentModelProvider?: string;
98
+ currentModel?: ParentModel;
98
99
  }
99
100
 
100
101
  interface AsyncChainParams {
@@ -342,7 +343,8 @@ export function executeAsyncChain(
342
343
  taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
343
344
  const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
344
345
 
345
- const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
346
+ const requestedModel = behavior.model ?? a.model;
347
+ const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
346
348
  const model = applyThinkingSuffix(primaryModel, a.thinking);
347
349
  return {
348
350
  agent: s.agent,
@@ -354,7 +356,7 @@ export function executeAsyncChain(
354
356
  cwd: stepCwd,
355
357
  model,
356
358
  thinking: resolveEffectiveThinking(model, a.thinking),
357
- modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
359
+ modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
358
360
  applyThinkingSuffix(candidate, a.thinking),
359
361
  ),
360
362
  tools: a.tools,
@@ -436,6 +438,14 @@ export function executeAsyncChain(
436
438
  failFast: s.failFast,
437
439
  phase: s.phase,
438
440
  label: s.label,
441
+ effectiveAcceptance: resolveEffectiveAcceptance({
442
+ explicit: s.acceptance,
443
+ agentName: s.parallel.agent,
444
+ task: s.parallel.task,
445
+ mode: resultMode,
446
+ async: true,
447
+ dynamicGroup: true,
448
+ }),
439
449
  };
440
450
  }
441
451
  return buildSeqStep(s as SequentialStep, nextSessionFile());
@@ -657,10 +667,13 @@ export function executeAsyncSingle(
657
667
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
658
668
  if (validationError) return formatAsyncStartError("single", validationError);
659
669
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
660
- const model = applyThinkingSuffix(
661
- resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
662
- agentConfig.thinking,
670
+ const primaryModel = resolveSubagentModelOverride(
671
+ params.modelOverride ?? agentConfig.model,
672
+ ctx.currentModel,
673
+ availableModels,
674
+ ctx.currentModelProvider,
663
675
  );
676
+ const model = applyThinkingSuffix(primaryModel, agentConfig.thinking);
664
677
  let spawnResult: { pid?: number; error?: string } = {};
665
678
  try {
666
679
  spawnResult = spawnRunner(
@@ -673,7 +686,7 @@ export function executeAsyncSingle(
673
686
  cwd: runnerCwd,
674
687
  model,
675
688
  thinking: resolveEffectiveThinking(model, agentConfig.thinking),
676
- modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
689
+ modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
677
690
  applyThinkingSuffix(candidate, agentConfig.thinking),
678
691
  ),
679
692
  tools: agentConfig.tools,
@@ -49,11 +49,6 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
49
49
  return "Resume: unavailable; no child session file was persisted.";
50
50
  }
51
51
 
52
- function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
53
- if (!finalization) return "";
54
- return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
55
- }
56
-
57
52
  function stepLineLabel(status: AsyncStatus, index: number): string {
58
53
  const steps = status.steps ?? [];
59
54
  if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
@@ -222,8 +217,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
222
217
  const modelThinking = formatModelThinking(step.model, step.thinking);
223
218
  const modelText = modelThinking ? ` (${modelThinking})` : "";
224
219
  const errorText = step.error ? `, error: ${step.error}` : "";
225
- const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
226
- const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
220
+ const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
227
221
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
228
222
  const phase = step.phase ? `[${step.phase}] ` : "";
229
223
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
@@ -8,8 +8,6 @@ import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
8
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
9
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
10
  import {
11
- type AcceptanceFinalizationTurn,
12
- type AcceptanceLedger,
13
11
  type ActivityState,
14
12
  type ArtifactConfig,
15
13
  type ArtifactPaths,
@@ -20,7 +18,6 @@ import {
20
18
  type NestedRouteInfo,
21
19
  type ResolvedControlConfig,
22
20
  type SubagentRunMode,
23
- type TokenUsage,
24
21
  type Usage,
25
22
  type WorkflowGraphSnapshot,
26
23
  DEFAULT_MAX_OUTPUT,
@@ -54,7 +51,7 @@ import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested
54
51
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
55
52
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
56
53
  import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
57
- import { evaluateCompletionMutationGuard, resolveCompletionPolicy } from "../shared/completion-guard.ts";
54
+ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
58
55
  import {
59
56
  createMutatingFailureState,
60
57
  didMutatingToolFail,
@@ -67,6 +64,7 @@ import {
67
64
  summarizeRecentMutatingFailures,
68
65
  } from "../shared/long-running-guard.ts";
69
66
  import { parseSessionTokens } from "../../shared/session-tokens.ts";
67
+ import type { TokenUsage } from "../../shared/types.ts";
70
68
  import {
71
69
  cleanupWorktrees,
72
70
  createWorktrees,
@@ -79,19 +77,7 @@ import {
79
77
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
80
78
  import { writeInitialProgressFile } from "../../shared/settings.ts";
81
79
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
82
- import {
83
- acceptanceFailureMessage,
84
- acceptanceSelfReviewConfig,
85
- attachFinalizationToLedger,
86
- buildFinalizationProcessFailureLedger,
87
- createFinalizationProcessFailureTurn,
88
- createFinalizationTurn,
89
- evaluateAcceptance,
90
- formatAcceptanceFinalizationPrompt,
91
- formatAcceptancePrompt,
92
- shouldRunAcceptanceFinalization,
93
- stripAcceptanceReport,
94
- } from "../shared/acceptance.ts";
80
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
95
81
 
96
82
  interface SubagentRunConfig {
97
83
  id: string;
@@ -139,7 +125,7 @@ interface StepResult {
139
125
  structuredOutput?: unknown;
140
126
  structuredOutputPath?: string;
141
127
  structuredOutputSchemaPath?: string;
142
- acceptance?: AcceptanceLedger;
128
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
143
129
  }
144
130
 
145
131
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -613,7 +599,7 @@ async function runSingleStep(
613
599
  structuredOutput?: unknown;
614
600
  structuredOutputPath?: string;
615
601
  structuredOutputSchemaPath?: string;
616
- acceptance?: AcceptanceLedger;
602
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
617
603
  }> {
618
604
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
619
605
  ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
@@ -716,15 +702,7 @@ async function runSingleStep(
716
702
  if (structured.error) structuredError = structured.error;
717
703
  else structuredOutput = structured.value;
718
704
  }
719
- const completionPolicy = resolveCompletionPolicy({
720
- agent: step.agent,
721
- task: taskForCompletionGuard,
722
- completionGuardEnabled: step.completionGuard !== false,
723
- usesAcceptanceContract: step.effectiveAcceptance?.explicit === true,
724
- tools: step.tools,
725
- mcpDirectTools: step.mcpDirectTools,
726
- });
727
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && completionPolicy === "mutation-guard"
705
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
728
706
  ? evaluateCompletionMutationGuard({
729
707
  agent: step.agent,
730
708
  task: taskForCompletionGuard,
@@ -737,7 +715,7 @@ async function runSingleStep(
737
715
  const completionGuardError = completionGuardTriggered
738
716
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
739
717
  : undefined;
740
- const effectiveExitCode = completionGuardError
718
+ const effectiveExitCode = completionGuardTriggered
741
719
  ? 1
742
720
  : structuredError
743
721
  ? 1
@@ -765,7 +743,7 @@ async function runSingleStep(
765
743
  completionGuardTriggeredFinal = completionGuardTriggered;
766
744
  finalOutputSnapshot = outputSnapshot;
767
745
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
768
- if (attempt.success || completionGuardError) break;
746
+ if (attempt.success || completionGuardTriggered) break;
769
747
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
770
748
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
771
749
  }
@@ -778,12 +756,12 @@ async function runSingleStep(
778
756
  const output = resolvedOutput.fullOutput;
779
757
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
780
758
  let outputForSummary = output;
781
- if (attemptNotes.length > 0) {
782
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
783
- }
759
+ if (attemptNotes.length > 0) {
760
+ outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
761
+ }
784
762
  const outputForAcceptance = rawOutput;
785
- const finalizedOutput = finalizeSingleOutput({
786
- fullOutput: outputForSummary,
763
+ const finalizedOutput = finalizeSingleOutput({
764
+ fullOutput: outputForSummary,
787
765
  outputPath: step.outputPath,
788
766
  outputMode: step.outputMode,
789
767
  exitCode: finalResult?.exitCode ?? 1,
@@ -792,115 +770,13 @@ async function runSingleStep(
792
770
  saveError: resolvedOutput.saveError,
793
771
  });
794
772
  outputForSummary = finalizedOutput.displayOutput;
795
- const acceptanceForInitialReport = step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance)
796
- ? acceptanceSelfReviewConfig(step.effectiveAcceptance)
797
- : step.effectiveAcceptance;
798
- let acceptance = acceptanceForInitialReport
799
- ? await evaluateAcceptance({
800
- acceptance: acceptanceForInitialReport,
801
- output: outputForAcceptance,
802
- cwd: step.cwd ?? ctx.cwd,
803
- })
773
+ const acceptance = step.effectiveAcceptance
774
+ ? await evaluateAcceptance({
775
+ acceptance: step.effectiveAcceptance,
776
+ output: outputForAcceptance,
777
+ cwd: step.cwd ?? ctx.cwd,
778
+ })
804
779
  : undefined;
805
- if (acceptance && step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance) && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted) {
806
- const sessionFile = step.sessionFile ?? (sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined);
807
- const maxTurns = step.effectiveAcceptance.finalization.maxTurns;
808
- const turns: AcceptanceFinalizationTurn[] = [];
809
- if (!sessionFile) {
810
- const message = "Acceptance finalization requires a session file for same-session continuation.";
811
- turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
812
- acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
813
- } else {
814
- const selfReviewAcceptance = acceptanceSelfReviewConfig(step.effectiveAcceptance);
815
- let previousFailure = acceptanceFailureMessage(acceptance);
816
- let authoritativeLedger = acceptance;
817
- for (let turn = 1; turn <= maxTurns; turn++) {
818
- const prompt = formatAcceptanceFinalizationPrompt({
819
- acceptance: step.effectiveAcceptance,
820
- initialOutput: outputForAcceptance,
821
- initialLedger: acceptance,
822
- turn,
823
- maxTurns,
824
- ...(previousFailure ? { previousFailure } : {}),
825
- });
826
- const { args, env, tempDir } = buildPiArgs({
827
- baseArgs: ["--mode", "json", "-p"],
828
- task: prompt,
829
- sessionEnabled: true,
830
- sessionFile,
831
- model: finalResult?.model ?? step.model,
832
- thinking: step.thinking,
833
- inheritProjectContext: step.inheritProjectContext,
834
- inheritSkills: step.inheritSkills,
835
- tools: step.tools,
836
- extensions: step.extensions,
837
- systemPrompt: step.systemPrompt,
838
- systemPromptMode: step.systemPromptMode,
839
- mcpDirectTools: step.mcpDirectTools,
840
- cwd: step.cwd ?? ctx.cwd,
841
- promptFileStem: `${step.agent}-acceptance-finalization`,
842
- intercomSessionName: ctx.childIntercomTarget,
843
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
844
- runId: ctx.id,
845
- childAgentName: step.agent,
846
- childIndex: ctx.flatIndex,
847
- parentEventSink: ctx.nestedRoute?.eventSink,
848
- parentControlInbox: ctx.nestedRoute?.controlInbox,
849
- parentRootRunId: ctx.nestedRoute?.rootRunId,
850
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
851
- });
852
- ctx.onAttemptStart?.({ model: finalResult?.model ?? step.model, thinking: resolveEffectiveThinking(finalResult?.model ?? step.model, step.thinking) });
853
- const finalizationRun = await runPiStreaming(
854
- args,
855
- step.cwd ?? ctx.cwd,
856
- `${ctx.outputFile}.finalization-${turn}.log`,
857
- env,
858
- ctx.piPackageRoot,
859
- ctx.piArgv1,
860
- step.maxSubagentDepth,
861
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
862
- ctx.registerInterrupt,
863
- ctx.onChildEvent,
864
- );
865
- cleanupTempDir(tempDir);
866
- modelAttempts.push({
867
- model: finalResult?.model ?? finalizationRun.model ?? step.model ?? "default",
868
- success: finalizationRun.exitCode === 0 && !finalizationRun.error,
869
- exitCode: finalizationRun.exitCode,
870
- error: finalizationRun.error,
871
- usage: finalizationRun.usage,
872
- });
873
- const finalizationOutput = finalizationRun.finalOutput;
874
- if (finalizationRun.exitCode !== 0 || finalizationRun.error || finalizationRun.interrupted) {
875
- const message = finalizationRun.error ?? "Acceptance finalization turn did not complete successfully.";
876
- turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: finalizationOutput, message }));
877
- acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
878
- break;
879
- }
880
- const selfReviewLedger = await evaluateAcceptance({
881
- acceptance: selfReviewAcceptance,
882
- output: finalizationOutput,
883
- cwd: step.cwd ?? ctx.cwd,
884
- });
885
- authoritativeLedger = selfReviewLedger;
886
- turns.push(createFinalizationTurn({ turn, prompt, rawOutput: finalizationOutput, ledger: selfReviewLedger }));
887
- const failure = acceptanceFailureMessage(selfReviewLedger);
888
- if (!failure) {
889
- authoritativeLedger = step.effectiveAcceptance === selfReviewAcceptance
890
- ? selfReviewLedger
891
- : await evaluateAcceptance({
892
- acceptance: step.effectiveAcceptance,
893
- output: finalizationOutput,
894
- cwd: step.cwd ?? ctx.cwd,
895
- });
896
- acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "completed", maxTurns });
897
- break;
898
- }
899
- previousFailure = failure;
900
- if (turn === maxTurns) acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "failed", maxTurns });
901
- }
902
- }
903
- }
904
780
  const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
905
781
  const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
906
782
  const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
@@ -1236,7 +1112,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1236
1112
  writeAtomicJson(statusPath, statusPayload);
1237
1113
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1238
1114
  };
1239
- const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: AcceptanceLedger): void => {
1115
+ const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1240
1116
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1241
1117
  if (!groupNode) return;
1242
1118
  groupNode.status = status;
@@ -1578,9 +1454,36 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1578
1454
  placeholder.durationMs = 0;
1579
1455
  }
1580
1456
  previousOutput = "Dynamic fanout produced 0 results.";
1457
+ const groupAcceptance = step.effectiveAcceptance?.explicit
1458
+ ? await evaluateAcceptance({
1459
+ acceptance: step.effectiveAcceptance,
1460
+ output: "",
1461
+ report: aggregateAcceptanceReport({
1462
+ results: [],
1463
+ notes: "Dynamic fanout produced 0 results.",
1464
+ }),
1465
+ cwd,
1466
+ })
1467
+ : undefined;
1468
+ if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1469
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1470
+ if (groupAcceptanceFailure) {
1471
+ statusPayload.state = "failed";
1472
+ statusPayload.error = groupAcceptanceFailure;
1473
+ if (placeholder) {
1474
+ placeholder.status = "failed";
1475
+ placeholder.error = groupAcceptanceFailure;
1476
+ placeholder.exitCode = 1;
1477
+ }
1478
+ markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1479
+ statusPayload.lastUpdate = now;
1480
+ writeStatusPayload();
1481
+ results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
1482
+ break;
1483
+ }
1581
1484
  flatIndex++;
1582
1485
  statusPayload.lastUpdate = now;
1583
- markDynamicGraphGroup(stepIndex, "completed");
1486
+ markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
1584
1487
  writeStatusPayload();
1585
1488
  continue;
1586
1489
  }
@@ -1756,7 +1659,31 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1756
1659
  stepIndex,
1757
1660
  };
1758
1661
  statusPayload.outputs = outputs;
1759
- markDynamicGraphGroup(stepIndex, "completed");
1662
+ const groupAcceptance = step.effectiveAcceptance
1663
+ ? await evaluateAcceptance({
1664
+ acceptance: step.effectiveAcceptance,
1665
+ output: "",
1666
+ report: aggregateAcceptanceReport({
1667
+ results: parallelResults,
1668
+ notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1669
+ }),
1670
+ cwd,
1671
+ })
1672
+ : undefined;
1673
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1674
+ markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1675
+ if (groupAcceptanceFailure) {
1676
+ results.push({
1677
+ agent: step.parallel.agent,
1678
+ output: groupAcceptanceFailure,
1679
+ error: groupAcceptanceFailure,
1680
+ success: false,
1681
+ exitCode: 1,
1682
+ structuredOutput: collection,
1683
+ acceptance: groupAcceptance,
1684
+ });
1685
+ statusPayload.error = groupAcceptanceFailure;
1686
+ }
1760
1687
  } catch (error) {
1761
1688
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1762
1689
  results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });