pi-subagents 0.35.0 → 0.36.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 +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -6,7 +6,7 @@ import type { Message } from "@earendil-works/pi-ai";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
7
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
8
8
  import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
9
- import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
+ import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths } from "../../shared/artifacts.ts";
10
10
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
11
11
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
12
12
  import {
@@ -94,10 +94,12 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
94
94
  import { writeInitialProgressFile } from "../../shared/settings.ts";
95
95
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
96
96
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
97
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
97
98
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
98
99
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
99
100
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
100
101
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
102
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
101
103
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
102
104
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
103
105
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
@@ -150,6 +152,7 @@ interface SubagentRunConfig {
150
152
 
151
153
  interface StepResult {
152
154
  agent: string;
155
+ context?: "fresh" | "fork";
153
156
  output: string;
154
157
  error?: string;
155
158
  protocolError?: ProtocolOutputLimit;
@@ -174,6 +177,10 @@ interface StepResult {
174
177
  truncated?: boolean;
175
178
  transcriptPath?: string;
176
179
  transcriptError?: string;
180
+ agentContract?: import("../../shared/types.ts").AgentContract;
181
+ execution?: import("../../shared/types.ts").ExecutionProjection;
182
+ review?: import("../../shared/types.ts").ReviewProjection;
183
+ effects?: import("../../shared/types.ts").EffectsProjection;
177
184
  structuredOutput?: unknown;
178
185
  structuredOutputPath?: string;
179
186
  structuredOutputSchemaPath?: string;
@@ -256,6 +263,15 @@ function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
256
263
  return event.type !== "message_update";
257
264
  }
258
265
 
266
+ function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
267
+ if (!args || typeof args !== "object" || Array.isArray(args)) return false;
268
+ if (toolName === "contact_supervisor") {
269
+ const reason = (args as Record<string, unknown>).reason;
270
+ return reason === "need_decision" || reason === "interview_request";
271
+ }
272
+ return toolName === "intercom" && (args as Record<string, unknown>).action === "ask";
273
+ }
274
+
259
275
  function findLatestSessionFile(sessionDir: string): string | null {
260
276
  try {
261
277
  const files = fs
@@ -951,6 +967,8 @@ async function runSingleStep(
951
967
  ctx: SingleStepContext,
952
968
  ): Promise<{
953
969
  agent: string;
970
+ context?: "fresh" | "fork";
971
+ agentContract?: import("../../shared/types.ts").AgentContract;
954
972
  output: string;
955
973
  exitCode: number | null;
956
974
  error?: string;
@@ -971,6 +989,9 @@ async function runSingleStep(
971
989
  sessionFile?: string;
972
990
  intercomTarget?: string;
973
991
  completionGuardTriggered?: boolean;
992
+ effects?: import("../../shared/types.ts").EffectsProjection;
993
+ execution?: import("../../shared/types.ts").ExecutionProjection;
994
+ review?: import("../../shared/types.ts").ReviewProjection;
974
995
  structuredOutput?: unknown;
975
996
  structuredOutputPath?: string;
976
997
  structuredOutputSchemaPath?: string;
@@ -1052,7 +1073,7 @@ async function runSingleStep(
1052
1073
  if (ctx.outputs) task = resolveOutputReferences(task, ctx.outputs);
1053
1074
  const taskForCompletionGuard = task;
1054
1075
  if (step.effectiveAcceptance) {
1055
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
1076
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract) });
1056
1077
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
1057
1078
  }
1058
1079
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1206,7 +1227,7 @@ async function runSingleStep(
1206
1227
  let structuredOutput: unknown;
1207
1228
  let structuredError: string | undefined;
1208
1229
  if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
1209
- const structured = readStructuredOutput({
1230
+ const structured = await readStructuredOutput({
1210
1231
  schema: effectiveStructuredOutput.schema,
1211
1232
  schemaPath: effectiveStructuredOutput.schemaPath,
1212
1233
  outputPath: effectiveStructuredOutput.outputPath,
@@ -1214,7 +1235,8 @@ async function runSingleStep(
1214
1235
  if (structured.error) structuredError = structured.error;
1215
1236
  else structuredOutput = structured.value;
1216
1237
  }
1217
- const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
1238
+ const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1239
+ const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1218
1240
  ? evaluateCompletionMutationGuard({
1219
1241
  agent: step.agent,
1220
1242
  task: taskForCompletionGuard,
@@ -1224,10 +1246,18 @@ async function runSingleStep(
1224
1246
  })
1225
1247
  : undefined;
1226
1248
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1227
- const completionGuardError = completionGuardTriggered
1249
+ const fileMutationEffect = completionGuard
1250
+ ? {
1251
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1252
+ expected: completionGuard.expectedMutation,
1253
+ attempted: completionGuard.attemptedMutation || run.observedMutationAttempt === true,
1254
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1255
+ }
1256
+ : undefined;
1257
+ const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1228
1258
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1229
1259
  : undefined;
1230
- const effectiveExitCode = toolAvailabilityError || completionGuardTriggered || structuredError
1260
+ const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || structuredError
1231
1261
  ? 1
1232
1262
  : hiddenError?.hasError
1233
1263
  ? (hiddenError.exitCode ?? 1)
@@ -1261,7 +1291,7 @@ async function runSingleStep(
1261
1291
  toolBudgetBlocked = Boolean(blockedMessage);
1262
1292
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1263
1293
  }
1264
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
1294
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1265
1295
  if (run.turnBudgetExceeded) break;
1266
1296
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1267
1297
  if (attempt.success || completionGuardTriggered) break;
@@ -1315,6 +1345,7 @@ async function runSingleStep(
1315
1345
  cwd: step.cwd ?? ctx.cwd,
1316
1346
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1317
1347
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1348
+ reportOptional: isAgentContractV1(step.agentContract),
1318
1349
  })
1319
1350
  : undefined;
1320
1351
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
@@ -1330,7 +1361,7 @@ async function runSingleStep(
1330
1361
  : acceptance
1331
1362
  : undefined;
1332
1363
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1333
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded;
1364
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1334
1365
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1335
1366
  const effectiveFinalError = stoppedAfterAcceptance
1336
1367
  ? ctx.stopMessage ?? "Subagent stopped by user."
@@ -1344,7 +1375,12 @@ async function runSingleStep(
1344
1375
 
1345
1376
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1346
1377
  if (ctx.artifactConfig?.includeOutput !== false) {
1347
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1378
+ fs.writeFileSync(artifactPaths.outputPath, formatOutputArtifactContent({
1379
+ output,
1380
+ error: effectiveFinalError,
1381
+ transcriptPath: transcriptWriter ? artifactPaths.transcriptPath : undefined,
1382
+ metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath,
1383
+ }), "utf-8");
1348
1384
  }
1349
1385
  if (ctx.artifactConfig?.includeMetadata !== false) {
1350
1386
  fs.writeFileSync(
@@ -1369,8 +1405,10 @@ async function runSingleStep(
1369
1405
  }
1370
1406
  }
1371
1407
 
1372
- return {
1408
+ const result = {
1373
1409
  agent: step.agent,
1410
+ context: step.context,
1411
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1374
1412
  output: outputForSummary,
1375
1413
  exitCode: effectiveFinalExitCode,
1376
1414
  error: effectiveFinalError,
@@ -1393,12 +1431,14 @@ async function runSingleStep(
1393
1431
  toolBudget,
1394
1432
  toolBudgetBlocked: toolBudgetBlocked || undefined,
1395
1433
  completionGuardTriggered: completionGuardTriggeredFinal,
1434
+ ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1396
1435
  structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1397
1436
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1398
1437
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1399
1438
  acceptance: effectiveAcceptance,
1400
1439
  watchdog: finalResult?.watchdog,
1401
1440
  };
1441
+ return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1402
1442
  }
1403
1443
 
1404
1444
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -1440,7 +1480,7 @@ function markParallelGroupSetupFailure(input: {
1440
1480
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
1441
1481
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
1442
1482
  input.statusPayload.steps[flatTaskIndex].exitCode = 1;
1443
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1483
+ input.results.push({ agent: input.group.parallel[taskIndex].agent, context: input.group.parallel[taskIndex].context, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1444
1484
  }
1445
1485
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1446
1486
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1505,19 +1545,15 @@ function prepareParallelTaskRun(
1505
1545
  };
1506
1546
  }
1507
1547
 
1508
- function appendParallelWorktreeSummary(
1509
- previousOutput: string,
1510
- worktreeSetup: WorktreeSetup | undefined,
1548
+ function captureParallelWorktreeDiffs(
1549
+ worktreeSetup: WorktreeSetup,
1511
1550
  asyncDir: string,
1512
1551
  stepIndex: number,
1513
1552
  group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
1514
- ): string {
1515
- if (!worktreeSetup) return previousOutput;
1553
+ ): { diffs: ReturnType<typeof diffWorktrees>; summary: string } {
1516
1554
  const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
1517
1555
  const diffs = diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
1518
- const diffSummary = formatWorktreeDiffSummary(diffs);
1519
- if (!diffSummary) return previousOutput;
1520
- return `${previousOutput}\n\n${diffSummary}`;
1556
+ return { diffs, summary: formatWorktreeDiffSummary(diffs) };
1521
1557
  }
1522
1558
 
1523
1559
  function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
@@ -1612,10 +1648,12 @@ async function runSubagent(
1612
1648
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1613
1649
  initialStatusSteps.push({
1614
1650
  agent: task.agent,
1651
+ ...(task.context ? { context: task.context } : {}),
1615
1652
  phase: task.phase,
1616
1653
  label: task.label,
1617
1654
  outputName: task.outputName,
1618
1655
  structured: task.structured,
1656
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
1619
1657
  status: "pending",
1620
1658
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1621
1659
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -1633,10 +1671,12 @@ async function runSubagent(
1633
1671
  parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1634
1672
  initialStatusSteps.push({
1635
1673
  agent: `expand:${step.parallel.agent}`,
1674
+ ...(step.parallel.context ? { context: step.parallel.context } : {}),
1636
1675
  phase: step.phase ?? step.parallel.phase,
1637
1676
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1638
1677
  outputName: step.collect.as,
1639
1678
  structured: Boolean(step.collect.outputSchema),
1679
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1640
1680
  status: "pending",
1641
1681
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1642
1682
  recentTools: [],
@@ -1648,10 +1688,12 @@ async function runSubagent(
1648
1688
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1649
1689
  initialStatusSteps.push({
1650
1690
  agent: step.agent,
1691
+ ...(step.context ? { context: step.context } : {}),
1651
1692
  phase: step.phase,
1652
1693
  label: step.label,
1653
1694
  outputName: step.outputName,
1654
1695
  structured: step.structured,
1696
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1655
1697
  status: "pending",
1656
1698
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1657
1699
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -1896,21 +1938,24 @@ async function runSubagent(
1896
1938
  }
1897
1939
  }
1898
1940
  };
1899
- const pausedStepResult = (agent: string): SingleStepResult => ({
1941
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1900
1942
  agent,
1943
+ context,
1901
1944
  output: "Paused after interrupt. Waiting for explicit next action.",
1902
1945
  exitCode: 0,
1903
1946
  interrupted: true,
1904
1947
  });
1905
- const timedOutStepResult = (agent: string): SingleStepResult => ({
1948
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1906
1949
  agent,
1950
+ context,
1907
1951
  output: timeoutMessage ?? "Subagent timed out.",
1908
1952
  error: timeoutMessage ?? "Subagent timed out.",
1909
1953
  exitCode: 1,
1910
1954
  timedOut: true,
1911
1955
  });
1912
- const stoppedStepResult = (agent: string): SingleStepResult => ({
1956
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1913
1957
  agent,
1958
+ context,
1914
1959
  output: stopMessage,
1915
1960
  error: stopMessage,
1916
1961
  exitCode: 1,
@@ -1980,6 +2025,7 @@ async function runSubagent(
1980
2025
  const activeLongRunningSteps = new Set<number>();
1981
2026
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1982
2027
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2028
+ const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
1983
2029
  const mutatingFailureWindowMs = 5 * 60_000;
1984
2030
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
1985
2031
  if (!controlConfig.enabled) return;
@@ -2010,6 +2056,15 @@ async function runSubagent(
2010
2056
  statusPayload.currentToolStartedAt = activeStep?.currentToolStartedAt;
2011
2057
  statusPayload.currentPath = activeStep?.currentPath;
2012
2058
  };
2059
+ const syncAggregateActivityState = (): void => {
2060
+ const nextRunState = statusPayload.steps.some((step) => step.activityState === "needs_attention")
2061
+ ? "needs_attention"
2062
+ : statusPayload.steps.some((step) => step.activityState === "active_long_running")
2063
+ ? "active_long_running"
2064
+ : undefined;
2065
+ currentActivityState = nextRunState;
2066
+ statusPayload.activityState = nextRunState;
2067
+ };
2013
2068
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2014
2069
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2015
2070
  const step = statusPayload.steps[flatIndex];
@@ -2280,15 +2335,45 @@ async function runSubagent(
2280
2335
  pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
2281
2336
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2282
2337
  syncTopLevelCurrentTool();
2338
+ if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
2339
+ const previous = step.activityState;
2340
+ step.activityState = "needs_attention";
2341
+ supervisorAttentionSteps.set(flatIndex, previous);
2342
+ currentActivityState = "needs_attention";
2343
+ statusPayload.activityState = "needs_attention";
2344
+ appendControlEvent(buildControlEvent({
2345
+ type: "needs_attention",
2346
+ from: previous,
2347
+ to: "needs_attention",
2348
+ runId: id,
2349
+ agent: step.agent,
2350
+ index: flatIndex,
2351
+ ts: now,
2352
+ message: `${step.agent} is waiting for a supervisor reply`,
2353
+ reason: "supervisor_request",
2354
+ turns: step.turnCount,
2355
+ tokens: step.tokens?.total,
2356
+ toolCount: step.toolCount,
2357
+ currentTool: step.currentTool,
2358
+ currentToolDurationMs: 0,
2359
+ currentPath: step.currentPath,
2360
+ }));
2361
+ }
2283
2362
  } else if (event.type === "tool_execution_end") {
2284
2363
  if (step.currentTool) {
2285
2364
  step.recentTools ??= [];
2286
2365
  step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
2287
2366
  }
2367
+ const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2368
+ const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2288
2369
  step.currentTool = undefined;
2289
2370
  step.currentToolArgs = undefined;
2290
2371
  step.currentToolStartedAt = undefined;
2291
2372
  step.currentPath = undefined;
2373
+ if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2374
+ step.activityState = supervisorPreviousActivity;
2375
+ syncAggregateActivityState();
2376
+ }
2292
2377
  syncTopLevelCurrentTool();
2293
2378
  } else if (event.type === "tool_result_end" && event.message) {
2294
2379
  const toolSnapshot = pendingToolResults[flatIndex];
@@ -2602,7 +2687,7 @@ async function runSubagent(
2602
2687
  if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
2603
2688
  throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
2604
2689
  }
2605
- if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2690
+ if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2606
2691
  } catch (error) {
2607
2692
  const now = Date.now();
2608
2693
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -2621,7 +2706,7 @@ async function runSubagent(
2621
2706
  statusPayload.lastUpdate = now;
2622
2707
  markDynamicGraphGroup(stepIndex, "failed", message);
2623
2708
  writeStatusPayload();
2624
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
2709
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1 });
2625
2710
  break;
2626
2711
  }
2627
2712
 
@@ -2633,6 +2718,7 @@ async function runSubagent(
2633
2718
  mode: config.mode,
2634
2719
  async: true,
2635
2720
  dynamicGroup: true,
2721
+ agentContract: step.agentContract,
2636
2722
  });
2637
2723
 
2638
2724
  if (materialized.parallel.length === 0) {
@@ -2664,13 +2750,14 @@ async function runSubagent(
2664
2750
  cwd,
2665
2751
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
2666
2752
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
2753
+ reportOptional: isAgentContractV1(step.agentContract),
2667
2754
  })
2668
2755
  : undefined;
2669
2756
  const groupStopped = stopped || stopAbortController.signal.aborted;
2670
2757
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
2671
2758
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
2672
2759
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
2673
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2760
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2674
2761
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
2675
2762
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
2676
2763
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -2686,7 +2773,7 @@ async function runSubagent(
2686
2773
  markDynamicGraphGroup(stepIndex, groupStopped ? "stopped" : "failed", errorMessage, effectiveGroupAcceptance);
2687
2774
  statusPayload.lastUpdate = Date.now();
2688
2775
  writeStatusPayload();
2689
- results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance });
2776
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance });
2690
2777
  break;
2691
2778
  }
2692
2779
  flatIndex++;
@@ -2716,6 +2803,7 @@ async function runSubagent(
2716
2803
  mode: config.mode,
2717
2804
  async: true,
2718
2805
  dynamic: step.parallel.acceptanceInput === undefined,
2806
+ agentContract: step.parallel.agentContract ?? step.agentContract,
2719
2807
  }),
2720
2808
  systemPrompt: step.parallel.namespaceOutputPath ? injectOutputPathSystemPrompt(step.parallel.systemPrompt ?? "", outputPath, step.parallel) : step.parallel.systemPrompt,
2721
2809
  outputPath,
@@ -2735,10 +2823,12 @@ async function runSubagent(
2735
2823
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2736
2824
  return {
2737
2825
  agent: task.agent,
2826
+ ...(task.context ? { context: task.context } : {}),
2738
2827
  phase: task.phase ?? step.phase,
2739
2828
  label: task.label,
2740
2829
  outputName: undefined,
2741
2830
  structured: Boolean(task.structuredOutputSchema),
2831
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
2742
2832
  status: "pending",
2743
2833
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2744
2834
  ...(transcriptPath ? { transcriptPath } : {}),
@@ -2798,9 +2888,9 @@ async function runSubagent(
2798
2888
  let aborted = false;
2799
2889
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
2800
2890
  const fi = groupStartFlatIndex + taskIdx;
2801
- if (timedOut) return timedOutStepResult(task.agent);
2802
- if (stopped) return stoppedStepResult(task.agent);
2803
- if (interrupted) return pausedStepResult(task.agent);
2891
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
2892
+ if (stopped) return stoppedStepResult(task.agent, task.context);
2893
+ if (interrupted) return pausedStepResult(task.agent, task.context);
2804
2894
  if (aborted && failFast) {
2805
2895
  const skippedAt = Date.now();
2806
2896
  statusPayload.steps[fi].status = "failed";
@@ -2811,7 +2901,7 @@ async function runSubagent(
2811
2901
  statusPayload.steps[fi].exitCode = -1;
2812
2902
  statusPayload.lastUpdate = skippedAt;
2813
2903
  writeStatusPayload();
2814
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2904
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2815
2905
  }
2816
2906
  const taskStartTime = Date.now();
2817
2907
  statusPayload.currentStep = fi;
@@ -2883,6 +2973,10 @@ async function runSubagent(
2883
2973
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2884
2974
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2885
2975
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
2976
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
2977
+ statusPayload.steps[fi].effects = singleResult.effects;
2978
+ statusPayload.steps[fi].execution = singleResult.execution;
2979
+ statusPayload.steps[fi].review = singleResult.review;
2886
2980
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2887
2981
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2888
2982
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2903,6 +2997,8 @@ async function runSubagent(
2903
2997
  for (const pr of parallelResults) {
2904
2998
  results.push({
2905
2999
  agent: pr.agent,
3000
+ context: pr.context,
3001
+ agentContract: pr.agentContract,
2906
3002
  output: pr.output,
2907
3003
  error: pr.error,
2908
3004
  protocolError: pr.protocolError,
@@ -2926,6 +3022,9 @@ async function runSubagent(
2926
3022
  artifactPaths: pr.artifactPaths,
2927
3023
  transcriptPath: pr.transcriptPath,
2928
3024
  transcriptError: pr.transcriptError,
3025
+ effects: pr.effects,
3026
+ execution: pr.execution,
3027
+ review: pr.review,
2929
3028
  structuredOutput: pr.structuredOutput,
2930
3029
  structuredOutputPath: pr.structuredOutputPath,
2931
3030
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -2935,9 +3034,20 @@ async function runSubagent(
2935
3034
  }
2936
3035
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
2937
3036
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
2938
- if (failures.length === 0) {
3037
+ const acceptanceFailures = parallelResults
3038
+ .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3039
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3040
+ if (acceptanceFailures.length > 0) {
3041
+ const message = acceptanceFailures
3042
+ .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
3043
+ .join("\n");
3044
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3045
+ statusPayload.error = message;
3046
+ markDynamicGraphGroup(stepIndex, "failed", message);
3047
+ }
3048
+ if (failures.length === 0 && acceptanceFailures.length === 0) {
2939
3049
  try {
2940
- validateDynamicCollection(step.collect.outputSchema, collection);
3050
+ await validateDynamicCollection(step.collect.outputSchema, collection);
2941
3051
  outputs[step.collect.as] = {
2942
3052
  text: JSON.stringify(collection),
2943
3053
  structured: collection,
@@ -2956,12 +3066,13 @@ async function runSubagent(
2956
3066
  cwd,
2957
3067
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
2958
3068
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3069
+ reportOptional: isAgentContractV1(step.agentContract),
2959
3070
  })
2960
3071
  : undefined;
2961
3072
  const groupStopped = stopped || stopAbortController.signal.aborted;
2962
3073
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
2963
3074
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
2964
- const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3075
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2965
3076
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2966
3077
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
2967
3078
  if (groupError) {
@@ -2981,7 +3092,7 @@ async function runSubagent(
2981
3092
  }
2982
3093
  } catch (error) {
2983
3094
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
2984
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3095
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
2985
3096
  statusPayload.error = message;
2986
3097
  markDynamicGraphGroup(stepIndex, "failed", message);
2987
3098
  }
@@ -3001,7 +3112,7 @@ async function runSubagent(
3001
3112
  ts: Date.now(),
3002
3113
  runId: id,
3003
3114
  stepIndex,
3004
- success: failures.length === 0,
3115
+ success: failures.length === 0 && acceptanceFailures.length === 0,
3005
3116
  }));
3006
3117
  if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
3007
3118
  statusPayload.lastUpdate = Date.now();
@@ -3017,6 +3128,7 @@ async function runSubagent(
3017
3128
  const groupStartFlatIndex = flatIndex;
3018
3129
  let aborted = false;
3019
3130
  let worktreeSetup: WorktreeSetup | undefined;
3131
+ let worktreeFinalized = false;
3020
3132
  if (group.worktree) {
3021
3133
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
3022
3134
  if (worktreeTaskCwdConflict) {
@@ -3085,9 +3197,9 @@ async function runSubagent(
3085
3197
  concurrency,
3086
3198
  async (task, taskIdx) => {
3087
3199
  const fi = groupStartFlatIndex + taskIdx;
3088
- if (timedOut) return timedOutStepResult(task.agent);
3089
- if (stopped) return stoppedStepResult(task.agent);
3090
- if (interrupted) return pausedStepResult(task.agent);
3200
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
3201
+ if (stopped) return stoppedStepResult(task.agent, task.context);
3202
+ if (interrupted) return pausedStepResult(task.agent, task.context);
3091
3203
  if (aborted && failFast) {
3092
3204
  const skippedAt = Date.now();
3093
3205
  statusPayload.steps[fi].status = "failed";
@@ -3102,7 +3214,7 @@ async function runSubagent(
3102
3214
  appendJsonl(eventsPath, JSON.stringify({
3103
3215
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
3104
3216
  }));
3105
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3217
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3106
3218
  }
3107
3219
 
3108
3220
  const taskStartTime = Date.now();
@@ -3192,6 +3304,10 @@ async function runSubagent(
3192
3304
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3193
3305
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3194
3306
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3307
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
3308
+ statusPayload.steps[fi].effects = singleResult.effects;
3309
+ statusPayload.steps[fi].execution = singleResult.execution;
3310
+ statusPayload.steps[fi].review = singleResult.review;
3195
3311
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
3196
3312
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
3197
3313
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -3248,6 +3364,8 @@ async function runSubagent(
3248
3364
  for (const pr of parallelResults) {
3249
3365
  results.push({
3250
3366
  agent: pr.agent,
3367
+ context: pr.context,
3368
+ agentContract: pr.agentContract,
3251
3369
  output: pr.output,
3252
3370
  error: pr.error,
3253
3371
  protocolError: pr.protocolError,
@@ -3271,6 +3389,9 @@ async function runSubagent(
3271
3389
  artifactPaths: pr.artifactPaths,
3272
3390
  transcriptPath: pr.transcriptPath,
3273
3391
  transcriptError: pr.transcriptError,
3392
+ effects: pr.effects,
3393
+ execution: pr.execution,
3394
+ review: pr.review,
3274
3395
  structuredOutput: pr.structuredOutput,
3275
3396
  structuredOutputPath: pr.structuredOutputPath,
3276
3397
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -3298,21 +3419,62 @@ async function runSubagent(
3298
3419
  attemptedModels: r.attemptedModels,
3299
3420
  })),
3300
3421
  );
3301
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
3422
+ if (worktreeSetup) {
3423
+ const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
3424
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
3425
+ worktreeFinalized = true;
3426
+ const cleanup = cleanupWorktrees(worktreeSetup);
3427
+ try {
3428
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3429
+ manifestPath: parallelHandoffPath(asyncDir),
3430
+ runId: id,
3431
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
3432
+ source: "async",
3433
+ cwd,
3434
+ stepIndex,
3435
+ flatStartIndex: groupStartFlatIndex,
3436
+ setup: worktreeSetup,
3437
+ diffs: captured.diffs,
3438
+ cleanup,
3439
+ results: parallelResults.map((result) => ({
3440
+ agent: result.agent,
3441
+ status: result.stopped ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3442
+ summary: result.output || result.error || "(no output)",
3443
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3444
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3445
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3446
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3447
+ })),
3448
+ });
3449
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
3450
+ } catch (error) {
3451
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
3452
+ }
3453
+ writeStatusPayload();
3454
+ }
3302
3455
 
3303
3456
  appendJsonl(eventsPath, JSON.stringify({
3304
3457
  type: "subagent.parallel.completed",
3305
3458
  ts: Date.now(),
3306
3459
  runId: id,
3307
3460
  stepIndex,
3308
- success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
3461
+ success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
3462
+ && parallelResults.every((result, index) => !(isAgentContractV1(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
3309
3463
  }));
3310
3464
 
3465
+ const acceptanceGateFailure = parallelResults
3466
+ .map((result, index) => ({ result, index, task: group.parallel[index] }))
3467
+ .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3468
+ if (acceptanceGateFailure) {
3469
+ statusPayload.error = acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) ?? "Parallel acceptance gate rejected the step.";
3470
+ writeStatusPayload();
3471
+ break;
3472
+ }
3311
3473
  if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
3312
3474
  break;
3313
3475
  }
3314
3476
  } finally {
3315
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3477
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
3316
3478
  }
3317
3479
  } else {
3318
3480
  const seqStep = step as SubagentStep;
@@ -3376,6 +3538,8 @@ async function runSubagent(
3376
3538
  const childStopped = singleResult.stopped === true;
3377
3539
  results.push({
3378
3540
  agent: singleResult.agent,
3541
+ context: singleResult.context,
3542
+ agentContract: singleResult.agentContract,
3379
3543
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3380
3544
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3381
3545
  protocolError: singleResult.protocolError,
@@ -3390,6 +3554,9 @@ async function runSubagent(
3390
3554
  artifactPaths: singleResult.artifactPaths,
3391
3555
  transcriptPath: singleResult.transcriptPath,
3392
3556
  transcriptError: singleResult.transcriptError,
3557
+ effects: singleResult.effects,
3558
+ execution: singleResult.execution,
3559
+ review: singleResult.review,
3393
3560
  structuredOutput: singleResult.structuredOutput,
3394
3561
  structuredOutputPath: singleResult.structuredOutputPath,
3395
3562
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
@@ -3460,6 +3627,10 @@ async function runSubagent(
3460
3627
  statusPayload.steps[flatIndex].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3461
3628
  statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
3462
3629
  statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
3630
+ statusPayload.steps[flatIndex].agentContract = singleResult.agentContract;
3631
+ statusPayload.steps[flatIndex].effects = singleResult.effects;
3632
+ statusPayload.steps[flatIndex].execution = singleResult.execution;
3633
+ statusPayload.steps[flatIndex].review = singleResult.review;
3463
3634
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
3464
3635
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
3465
3636
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -3497,6 +3668,11 @@ async function runSubagent(
3497
3668
  }
3498
3669
 
3499
3670
  flatIndex++;
3671
+ if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
3672
+ statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
3673
+ writeStatusPayload();
3674
+ break;
3675
+ }
3500
3676
  if (singleResult.exitCode !== 0) {
3501
3677
  break;
3502
3678
  }
@@ -3567,7 +3743,7 @@ async function runSubagent(
3567
3743
  clearTimeout(timeoutTimer);
3568
3744
  timeoutTimer = undefined;
3569
3745
  }
3570
- statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3746
+ statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3571
3747
  closeSteerInbox(asyncDir, statusPayload.state);
3572
3748
  disposeControlInbox();
3573
3749
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -3663,6 +3839,7 @@ async function runSubagent(
3663
3839
  ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
3664
3840
  results: results.map((r) => ({
3665
3841
  agent: r.agent,
3842
+ context: r.context,
3666
3843
  output: r.output,
3667
3844
  error: r.error,
3668
3845
  protocolError: r.protocolError,
@@ -3686,6 +3863,10 @@ async function runSubagent(
3686
3863
  truncated: r.truncated,
3687
3864
  transcriptPath: r.transcriptPath,
3688
3865
  transcriptError: r.transcriptError,
3866
+ agentContract: r.agentContract,
3867
+ execution: r.execution,
3868
+ review: r.review,
3869
+ effects: r.effects,
3689
3870
  structuredOutput: r.structuredOutput,
3690
3871
  structuredOutputPath: r.structuredOutputPath,
3691
3872
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
@@ -3694,6 +3875,7 @@ async function runSubagent(
3694
3875
  })),
3695
3876
  outputs,
3696
3877
  workflowGraph: statusPayload.workflowGraph,
3878
+ parallelHandoff: statusPayload.parallelHandoff,
3697
3879
  exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
3698
3880
  timestamp: runEndedAt,
3699
3881
  durationMs: runEndedAt - overallStartTime,