pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
@@ -6,7 +7,7 @@ import type { Message } from "@earendil-works/pi-ai";
6
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
8
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
8
9
  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";
10
+ import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths } from "../../shared/artifacts.ts";
10
11
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
11
12
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
12
13
  import {
@@ -57,16 +58,17 @@ import {
57
58
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
58
59
  Semaphore,
59
60
  } from "../shared/parallel-utils.ts";
60
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
61
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
61
62
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
62
63
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
63
64
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
64
65
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
65
66
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
66
67
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
68
+ import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
67
69
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
68
70
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
69
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
71
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
70
72
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
71
73
  import {
72
74
  createMutatingFailureState,
@@ -91,16 +93,20 @@ import {
91
93
  type WorktreeSetup,
92
94
  } from "../shared/worktree.ts";
93
95
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
96
+ import { launchBindingDigest } from "../../shared/launch-contract.ts";
94
97
  import { writeInitialProgressFile } from "../../shared/settings.ts";
95
98
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
96
99
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
100
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
97
101
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
98
102
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
99
103
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
100
104
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
105
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
101
106
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
102
107
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
103
108
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
109
+ import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
104
110
  import {
105
111
  CHILD_WATCHDOG_CONFIG_ENV,
106
112
  acceptChildWatchdogEvent,
@@ -144,12 +150,19 @@ interface SubagentRunConfig {
144
150
  turnBudget?: ResolvedTurnBudget;
145
151
  toolBudget?: ResolvedToolBudget;
146
152
  revivalLease?: SessionLeaseRequest;
153
+ revivalLeaseToken?: string;
147
154
  /** Global cap on simultaneously-running subagent tasks within this run. */
148
155
  globalConcurrencyLimit?: number;
156
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
157
+ launchContractDigest?: string;
158
+ runnerProcessInstanceId?: string;
149
159
  }
150
160
 
151
161
  interface StepResult {
152
162
  agent: string;
163
+ context?: "fresh" | "fork";
164
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
165
+ capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
153
166
  output: string;
154
167
  error?: string;
155
168
  protocolError?: ProtocolOutputLimit;
@@ -174,11 +187,18 @@ interface StepResult {
174
187
  truncated?: boolean;
175
188
  transcriptPath?: string;
176
189
  transcriptError?: string;
190
+ agentContract?: import("../../shared/types.ts").AgentContract;
191
+ launchContractDigest?: string;
192
+ execution?: import("../../shared/types.ts").ExecutionProjection;
193
+ review?: import("../../shared/types.ts").ReviewProjection;
194
+ effects?: import("../../shared/types.ts").EffectsProjection;
177
195
  structuredOutput?: unknown;
178
196
  structuredOutputPath?: string;
179
197
  structuredOutputSchemaPath?: string;
180
198
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
181
199
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
200
+ writerProcesses?: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>;
201
+ writerAttemptCount?: number;
182
202
  }
183
203
 
184
204
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -256,6 +276,15 @@ function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
256
276
  return event.type !== "message_update";
257
277
  }
258
278
 
279
+ function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
280
+ if (!args || typeof args !== "object" || Array.isArray(args)) return false;
281
+ if (toolName === "contact_supervisor") {
282
+ const reason = (args as Record<string, unknown>).reason;
283
+ return reason === "need_decision" || reason === "interview_request";
284
+ }
285
+ return toolName === "intercom" && (args as Record<string, unknown>).action === "ask";
286
+ }
287
+
259
288
  function findLatestSessionFile(sessionDir: string): string | null {
260
289
  try {
261
290
  const files = fs
@@ -380,6 +409,9 @@ interface RunPiStreamingResult {
380
409
  toolBudgetBlocked?: boolean;
381
410
  observedMutationAttempt?: boolean;
382
411
  watchdog?: ChildWatchdogStateSnapshot;
412
+ processInstanceId: string;
413
+ processCloseObservedAt?: number;
414
+ processSignal?: string | null;
383
415
  }
384
416
 
385
417
  function runPiStreaming(
@@ -402,6 +434,7 @@ function runPiStreaming(
402
434
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
403
435
  ): Promise<RunPiStreamingResult> {
404
436
  return new Promise((resolve) => {
437
+ const processInstanceId = randomUUID();
405
438
  onWriterProcess?.({ state: "spawning" });
406
439
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
407
440
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
@@ -739,6 +772,7 @@ function runPiStreaming(
739
772
  });
740
773
  child.on("close", (exitCode, signal) => {
741
774
  settled = true;
775
+ const processCloseObservedAt = Date.now();
742
776
  try {
743
777
  onWriterProcess?.({ state: "none" });
744
778
  } catch {
@@ -774,6 +808,9 @@ function runPiStreaming(
774
808
  wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
775
809
  observedMutationAttempt,
776
810
  watchdog: childWatchdogState,
811
+ processInstanceId,
812
+ processCloseObservedAt,
813
+ processSignal: signal,
777
814
  });
778
815
  });
779
816
 
@@ -796,7 +833,7 @@ function runPiStreaming(
796
833
  const stderr = stderrTail.text();
797
834
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
798
835
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
799
- resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState });
836
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
800
837
  });
801
838
  });
802
839
  }
@@ -939,6 +976,7 @@ interface SingleStepContext {
939
976
  childIntercomTarget?: string;
940
977
  orchestratorIntercomTarget?: string;
941
978
  nestedRoute?: NestedRouteInfo;
979
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
942
980
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
943
981
  onChildEvent?: (event: ChildEvent) => void;
944
982
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
@@ -951,6 +989,9 @@ async function runSingleStep(
951
989
  ctx: SingleStepContext,
952
990
  ): Promise<{
953
991
  agent: string;
992
+ context?: "fresh" | "fork";
993
+ agentContract?: import("../../shared/types.ts").AgentContract;
994
+ launchContractDigest?: string;
954
995
  output: string;
955
996
  exitCode: number | null;
956
997
  error?: string;
@@ -971,10 +1012,14 @@ async function runSingleStep(
971
1012
  sessionFile?: string;
972
1013
  intercomTarget?: string;
973
1014
  completionGuardTriggered?: boolean;
1015
+ effects?: import("../../shared/types.ts").EffectsProjection;
1016
+ execution?: import("../../shared/types.ts").ExecutionProjection;
1017
+ review?: import("../../shared/types.ts").ReviewProjection;
974
1018
  structuredOutput?: unknown;
975
1019
  structuredOutputPath?: string;
976
1020
  structuredOutputSchemaPath?: string;
977
1021
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
1022
+ writerAttemptCount?: number;
978
1023
  }> {
979
1024
  if (step.importAsyncRoot) {
980
1025
  let importTimedOut = false;
@@ -1052,7 +1097,7 @@ async function runSingleStep(
1052
1097
  if (ctx.outputs) task = resolveOutputReferences(task, ctx.outputs);
1053
1098
  const taskForCompletionGuard = task;
1054
1099
  if (step.effectiveAcceptance) {
1055
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
1100
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract) });
1056
1101
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
1057
1102
  }
1058
1103
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1086,7 +1131,10 @@ async function runSingleStep(
1086
1131
  ? [step.model]
1087
1132
  : [undefined];
1088
1133
  const attemptedModels: string[] = [];
1134
+ let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1089
1135
  const modelAttempts: ModelAttempt[] = [];
1136
+ const writerProcesses: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }> = [];
1137
+ let writerAttemptCount = 0;
1090
1138
  const attemptNotes: string[] = [];
1091
1139
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1092
1140
  let finalResult: RunPiStreamingResult | undefined;
@@ -1095,6 +1143,7 @@ async function runSingleStep(
1095
1143
  let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1096
1144
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1097
1145
  let toolBudgetBlocked = false;
1146
+ let actualLaunchContractDigest = step.launchContractDigest;
1098
1147
 
1099
1148
  for (let index = 0; index < candidates.length; index++) {
1100
1149
  if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
@@ -1117,7 +1166,7 @@ async function runSingleStep(
1117
1166
  childIndex: ctx.flatIndex,
1118
1167
  })
1119
1168
  : undefined;
1120
- const { args, env, tempDir, toolDiagnosticPath } = buildPiArgs({
1169
+ const { args, env, tempDir, toolDiagnosticPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
1121
1170
  parentSessionId: step.parentSessionId,
1122
1171
  baseArgs: ["--mode", "json", "-p"],
1123
1172
  task,
@@ -1134,6 +1183,7 @@ async function runSingleStep(
1134
1183
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1135
1184
  systemPromptMode: step.systemPromptMode,
1136
1185
  mcpDirectTools: step.mcpDirectTools,
1186
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1137
1187
  cwd: step.cwd ?? ctx.cwd,
1138
1188
  promptFileStem: step.agent,
1139
1189
  intercomSessionName: ctx.childIntercomTarget,
@@ -1153,6 +1203,39 @@ async function runSingleStep(
1153
1203
  childWatchdog,
1154
1204
  waitToolEnabled: step.waitToolEnabled,
1155
1205
  });
1206
+ if (step.definitionDigest) {
1207
+ const toolPlan = resolvePiLaunchToolPlan({
1208
+ tools: step.tools,
1209
+ extensions: step.extensions,
1210
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
1211
+ mcpDirectTools: step.mcpDirectTools,
1212
+ cwd: step.cwd ?? ctx.cwd,
1213
+ requireReadTool: Boolean(step.skills?.length),
1214
+ structuredOutput: Boolean(effectiveStructuredOutput),
1215
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1216
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1217
+ });
1218
+ actualLaunchContractDigest = launchBindingDigest({
1219
+ definitionDigest: step.definitionDigest,
1220
+ task: step.launchBindingTask ?? task,
1221
+ ...(candidate ? { model: candidate } : {}),
1222
+ modelCandidates: candidates,
1223
+ ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1224
+ systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1225
+ systemPromptMode: step.systemPromptMode,
1226
+ inheritProjectContext: step.inheritProjectContext,
1227
+ inheritSkills: step.inheritSkills,
1228
+ skills: step.skills,
1229
+ tools: toolPlan.effectiveToolAllowlist,
1230
+ extensions: toolPlan.extensionArgs,
1231
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1232
+ ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1233
+ ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1234
+ ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1235
+ });
1236
+ }
1237
+ capabilityAudit = attemptCapabilityAudit;
1238
+ writerAttemptCount += 1;
1156
1239
  const run = await runPiStreaming(
1157
1240
  args,
1158
1241
  step.cwd ?? ctx.cwd,
@@ -1172,6 +1255,16 @@ async function runSingleStep(
1172
1255
  ctx.registerTurnBudgetAbort,
1173
1256
  ctx.onWriterProcess,
1174
1257
  );
1258
+ if (run.processCloseObservedAt !== undefined) {
1259
+ writerProcesses.push({
1260
+ processInstanceId: run.processInstanceId,
1261
+ kind: "pi-writer",
1262
+ attempt: index,
1263
+ closeObservedAt: run.processCloseObservedAt,
1264
+ exitCode: run.exitCode,
1265
+ signal: run.processSignal ?? null,
1266
+ });
1267
+ }
1175
1268
  if (run.turnBudget) turnBudget = run.turnBudget;
1176
1269
  else if (ctx.turnBudget) {
1177
1270
  const assistantMessages = run.messages.filter((message) => message.role === "assistant");
@@ -1200,13 +1293,18 @@ async function runSingleStep(
1200
1293
  const missingStructuredOutput = effectiveStructuredOutput
1201
1294
  ? !fs.existsSync(effectiveStructuredOutput.outputPath)
1202
1295
  : false;
1203
- const emptyOutputError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
1296
+ const emptyOutputError = run.exitCode === 0
1297
+ && !run.error
1298
+ && !toolAvailabilityError
1299
+ && !run.finalOutput.trim()
1300
+ && (!effectiveStructuredOutput || missingStructuredOutput)
1301
+ && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1204
1302
  ? "Subagent produced no output (possible model cold-start or empty response)."
1205
1303
  : undefined;
1206
1304
  let structuredOutput: unknown;
1207
1305
  let structuredError: string | undefined;
1208
1306
  if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
1209
- const structured = readStructuredOutput({
1307
+ const structured = await readStructuredOutput({
1210
1308
  schema: effectiveStructuredOutput.schema,
1211
1309
  schemaPath: effectiveStructuredOutput.schemaPath,
1212
1310
  outputPath: effectiveStructuredOutput.outputPath,
@@ -1214,7 +1312,8 @@ async function runSingleStep(
1214
1312
  if (structured.error) structuredError = structured.error;
1215
1313
  else structuredOutput = structured.value;
1216
1314
  }
1217
- const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
1315
+ const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1316
+ const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1218
1317
  ? evaluateCompletionMutationGuard({
1219
1318
  agent: step.agent,
1220
1319
  task: taskForCompletionGuard,
@@ -1224,26 +1323,33 @@ async function runSingleStep(
1224
1323
  })
1225
1324
  : undefined;
1226
1325
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1227
- const completionGuardError = completionGuardTriggered
1326
+ const fileMutationEffect = completionGuard
1327
+ ? {
1328
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1329
+ expected: completionGuard.expectedMutation,
1330
+ attempted: completionGuard.attemptedMutation || run.observedMutationAttempt === true,
1331
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1332
+ }
1333
+ : undefined;
1334
+ const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1228
1335
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1229
1336
  : undefined;
1230
- const effectiveExitCode = toolAvailabilityError || completionGuardTriggered || structuredError
1337
+ const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || structuredError || emptyOutputError
1231
1338
  ? 1
1232
1339
  : hiddenError?.hasError
1233
1340
  ? (hiddenError.exitCode ?? 1)
1234
- : emptyOutputError
1341
+ : run.error && run.exitCode === 0
1235
1342
  ? 1
1236
- : run.error && run.exitCode === 0
1237
- ? 1
1238
- : run.exitCode;
1343
+ : run.exitCode;
1239
1344
  const error = toolAvailabilityError
1240
1345
  ?? completionGuardError
1241
1346
  ?? structuredError
1347
+ ?? emptyOutputError
1242
1348
  ?? (hiddenError?.hasError
1243
1349
  ? hiddenError.details
1244
1350
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1245
1351
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1246
- : emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
1352
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1247
1353
  const attempt: ModelAttempt = {
1248
1354
  model: candidate ?? run.model ?? step.model ?? "default",
1249
1355
  success: effectiveExitCode === 0 && !error,
@@ -1261,7 +1367,7 @@ async function runSingleStep(
1261
1367
  toolBudgetBlocked = Boolean(blockedMessage);
1262
1368
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1263
1369
  }
1264
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
1370
+ 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
1371
  if (run.turnBudgetExceeded) break;
1266
1372
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1267
1373
  if (attempt.success || completionGuardTriggered) break;
@@ -1315,6 +1421,7 @@ async function runSingleStep(
1315
1421
  cwd: step.cwd ?? ctx.cwd,
1316
1422
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1317
1423
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1424
+ reportOptional: isAgentContractV1(step.agentContract),
1318
1425
  })
1319
1426
  : undefined;
1320
1427
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
@@ -1330,7 +1437,7 @@ async function runSingleStep(
1330
1437
  : acceptance
1331
1438
  : undefined;
1332
1439
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1333
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded;
1440
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1334
1441
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1335
1442
  const effectiveFinalError = stoppedAfterAcceptance
1336
1443
  ? ctx.stopMessage ?? "Subagent stopped by user."
@@ -1344,7 +1451,12 @@ async function runSingleStep(
1344
1451
 
1345
1452
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1346
1453
  if (ctx.artifactConfig?.includeOutput !== false) {
1347
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1454
+ fs.writeFileSync(artifactPaths.outputPath, formatOutputArtifactContent({
1455
+ output,
1456
+ error: effectiveFinalError,
1457
+ transcriptPath: transcriptWriter ? artifactPaths.transcriptPath : undefined,
1458
+ metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath,
1459
+ }), "utf-8");
1348
1460
  }
1349
1461
  if (ctx.artifactConfig?.includeMetadata !== false) {
1350
1462
  fs.writeFileSync(
@@ -1359,6 +1471,8 @@ async function runSingleStep(
1359
1471
  modelAttempts,
1360
1472
  error: effectiveFinalError,
1361
1473
  acceptance: effectiveAcceptance,
1474
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1475
+ launchContractDigest: actualLaunchContractDigest,
1362
1476
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1363
1477
  transcriptError: transcriptWriter?.getError(),
1364
1478
  skills: step.skills,
@@ -1369,8 +1483,11 @@ async function runSingleStep(
1369
1483
  }
1370
1484
  }
1371
1485
 
1372
- return {
1486
+ const result = {
1373
1487
  agent: step.agent,
1488
+ context: step.context,
1489
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1490
+ launchContractDigest: actualLaunchContractDigest,
1374
1491
  output: outputForSummary,
1375
1492
  exitCode: effectiveFinalExitCode,
1376
1493
  error: effectiveFinalError,
@@ -1393,18 +1510,36 @@ async function runSingleStep(
1393
1510
  toolBudget,
1394
1511
  toolBudgetBlocked: toolBudgetBlocked || undefined,
1395
1512
  completionGuardTriggered: completionGuardTriggeredFinal,
1513
+ ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1396
1514
  structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1397
1515
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1398
1516
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1399
1517
  acceptance: effectiveAcceptance,
1400
1518
  watchdog: finalResult?.watchdog,
1519
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1520
+ writerProcesses,
1521
+ writerAttemptCount,
1401
1522
  };
1523
+ return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1402
1524
  }
1403
1525
 
1404
1526
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1405
1527
  exitCode?: number | null;
1406
1528
  };
1407
1529
 
1530
+ function appendCapabilityCeilingAppliedEvent(eventsPath: string, runId: string, stepIndex: number, agent: string, result: StepResult): void {
1531
+ if (!result.capabilityCeiling) return;
1532
+ appendJsonl(eventsPath, JSON.stringify({
1533
+ type: "subagent.capability-ceiling.applied",
1534
+ ts: Date.now(),
1535
+ runId,
1536
+ stepIndex,
1537
+ agent,
1538
+ capabilityCeiling: result.capabilityCeiling,
1539
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
1540
+ }));
1541
+ }
1542
+
1408
1543
  type RunnerStatusPayload = Omit<AsyncStatus, "steps" | "parallelGroups" | "pid" | "cwd" | "currentStep" | "chainStepCount" | "lastUpdate"> & {
1409
1544
  pid: number;
1410
1545
  cwd: string;
@@ -1440,7 +1575,7 @@ function markParallelGroupSetupFailure(input: {
1440
1575
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
1441
1576
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
1442
1577
  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 });
1578
+ 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
1579
  }
1445
1580
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1446
1581
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1505,19 +1640,15 @@ function prepareParallelTaskRun(
1505
1640
  };
1506
1641
  }
1507
1642
 
1508
- function appendParallelWorktreeSummary(
1509
- previousOutput: string,
1510
- worktreeSetup: WorktreeSetup | undefined,
1643
+ function captureParallelWorktreeDiffs(
1644
+ worktreeSetup: WorktreeSetup,
1511
1645
  asyncDir: string,
1512
1646
  stepIndex: number,
1513
1647
  group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
1514
- ): string {
1515
- if (!worktreeSetup) return previousOutput;
1648
+ ): { diffs: ReturnType<typeof diffWorktrees>; summary: string } {
1516
1649
  const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
1517
1650
  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}`;
1651
+ return { diffs, summary: formatWorktreeDiffSummary(diffs) };
1521
1652
  }
1522
1653
 
1523
1654
  function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
@@ -1612,10 +1743,14 @@ async function runSubagent(
1612
1743
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1613
1744
  initialStatusSteps.push({
1614
1745
  agent: task.agent,
1746
+ ...(task.context ? { context: task.context } : {}),
1615
1747
  phase: task.phase,
1616
1748
  label: task.label,
1617
1749
  outputName: task.outputName,
1618
1750
  structured: task.structured,
1751
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
1752
+ ...(task.launchContractDigest ? { launchContractDigest: task.launchContractDigest } : {}),
1753
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
1619
1754
  status: "pending",
1620
1755
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1621
1756
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -1633,10 +1768,13 @@ async function runSubagent(
1633
1768
  parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1634
1769
  initialStatusSteps.push({
1635
1770
  agent: `expand:${step.parallel.agent}`,
1771
+ ...(step.parallel.context ? { context: step.parallel.context } : {}),
1636
1772
  phase: step.phase ?? step.parallel.phase,
1637
1773
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1638
1774
  outputName: step.collect.as,
1639
1775
  structured: Boolean(step.collect.outputSchema),
1776
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1777
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1640
1778
  status: "pending",
1641
1779
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1642
1780
  recentTools: [],
@@ -1648,10 +1786,14 @@ async function runSubagent(
1648
1786
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1649
1787
  initialStatusSteps.push({
1650
1788
  agent: step.agent,
1789
+ ...(step.context ? { context: step.context } : {}),
1651
1790
  phase: step.phase,
1652
1791
  label: step.label,
1653
1792
  outputName: step.outputName,
1654
1793
  structured: step.structured,
1794
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1795
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
1796
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1655
1797
  status: "pending",
1656
1798
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1657
1799
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -1669,6 +1811,11 @@ async function runSubagent(
1669
1811
  const sessionEnabled = Boolean(config.sessionDir)
1670
1812
  || shareEnabled
1671
1813
  || flatSteps.some((step) => Boolean(step.sessionFile));
1814
+ if (config.runnerProcessInstanceId) {
1815
+ for (const step of initialStatusSteps) {
1816
+ step.processTerminal = { version: 1, state: "pending", runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId };
1817
+ }
1818
+ }
1672
1819
  const statusPayload: RunnerStatusPayload = {
1673
1820
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1674
1821
  runId: id,
@@ -1690,6 +1837,9 @@ async function runSubagent(
1690
1837
  chainStepCount: steps.length,
1691
1838
  parallelGroups,
1692
1839
  workflowGraph: config.workflowGraph,
1840
+ ...(config.launchContractDigest ? { launchContractDigest: config.launchContractDigest } : {}),
1841
+ ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
1842
+ ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
1693
1843
  steps: initialStatusSteps,
1694
1844
  artifactsDir,
1695
1845
  sessionDir: config.sessionDir,
@@ -1896,21 +2046,24 @@ async function runSubagent(
1896
2046
  }
1897
2047
  }
1898
2048
  };
1899
- const pausedStepResult = (agent: string): SingleStepResult => ({
2049
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1900
2050
  agent,
2051
+ context,
1901
2052
  output: "Paused after interrupt. Waiting for explicit next action.",
1902
2053
  exitCode: 0,
1903
2054
  interrupted: true,
1904
2055
  });
1905
- const timedOutStepResult = (agent: string): SingleStepResult => ({
2056
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1906
2057
  agent,
2058
+ context,
1907
2059
  output: timeoutMessage ?? "Subagent timed out.",
1908
2060
  error: timeoutMessage ?? "Subagent timed out.",
1909
2061
  exitCode: 1,
1910
2062
  timedOut: true,
1911
2063
  });
1912
- const stoppedStepResult = (agent: string): SingleStepResult => ({
2064
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1913
2065
  agent,
2066
+ context,
1914
2067
  output: stopMessage,
1915
2068
  error: stopMessage,
1916
2069
  exitCode: 1,
@@ -1980,6 +2133,7 @@ async function runSubagent(
1980
2133
  const activeLongRunningSteps = new Set<number>();
1981
2134
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1982
2135
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2136
+ const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
1983
2137
  const mutatingFailureWindowMs = 5 * 60_000;
1984
2138
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
1985
2139
  if (!controlConfig.enabled) return;
@@ -2010,6 +2164,15 @@ async function runSubagent(
2010
2164
  statusPayload.currentToolStartedAt = activeStep?.currentToolStartedAt;
2011
2165
  statusPayload.currentPath = activeStep?.currentPath;
2012
2166
  };
2167
+ const syncAggregateActivityState = (): void => {
2168
+ const nextRunState = statusPayload.steps.some((step) => step.activityState === "needs_attention")
2169
+ ? "needs_attention"
2170
+ : statusPayload.steps.some((step) => step.activityState === "active_long_running")
2171
+ ? "active_long_running"
2172
+ : undefined;
2173
+ currentActivityState = nextRunState;
2174
+ statusPayload.activityState = nextRunState;
2175
+ };
2013
2176
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2014
2177
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2015
2178
  const step = statusPayload.steps[flatIndex];
@@ -2280,15 +2443,45 @@ async function runSubagent(
2280
2443
  pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
2281
2444
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2282
2445
  syncTopLevelCurrentTool();
2446
+ if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
2447
+ const previous = step.activityState;
2448
+ step.activityState = "needs_attention";
2449
+ supervisorAttentionSteps.set(flatIndex, previous);
2450
+ currentActivityState = "needs_attention";
2451
+ statusPayload.activityState = "needs_attention";
2452
+ appendControlEvent(buildControlEvent({
2453
+ type: "needs_attention",
2454
+ from: previous,
2455
+ to: "needs_attention",
2456
+ runId: id,
2457
+ agent: step.agent,
2458
+ index: flatIndex,
2459
+ ts: now,
2460
+ message: `${step.agent} is waiting for a supervisor reply`,
2461
+ reason: "supervisor_request",
2462
+ turns: step.turnCount,
2463
+ tokens: step.tokens?.total,
2464
+ toolCount: step.toolCount,
2465
+ currentTool: step.currentTool,
2466
+ currentToolDurationMs: 0,
2467
+ currentPath: step.currentPath,
2468
+ }));
2469
+ }
2283
2470
  } else if (event.type === "tool_execution_end") {
2284
2471
  if (step.currentTool) {
2285
2472
  step.recentTools ??= [];
2286
2473
  step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
2287
2474
  }
2475
+ const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2476
+ const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2288
2477
  step.currentTool = undefined;
2289
2478
  step.currentToolArgs = undefined;
2290
2479
  step.currentToolStartedAt = undefined;
2291
2480
  step.currentPath = undefined;
2481
+ if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2482
+ step.activityState = supervisorPreviousActivity;
2483
+ syncAggregateActivityState();
2484
+ }
2292
2485
  syncTopLevelCurrentTool();
2293
2486
  } else if (event.type === "tool_result_end" && event.message) {
2294
2487
  const toolSnapshot = pendingToolResults[flatIndex];
@@ -2602,7 +2795,7 @@ async function runSubagent(
2602
2795
  if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
2603
2796
  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
2797
  }
2605
- if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2798
+ if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2606
2799
  } catch (error) {
2607
2800
  const now = Date.now();
2608
2801
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -2621,7 +2814,7 @@ async function runSubagent(
2621
2814
  statusPayload.lastUpdate = now;
2622
2815
  markDynamicGraphGroup(stepIndex, "failed", message);
2623
2816
  writeStatusPayload();
2624
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
2817
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1 });
2625
2818
  break;
2626
2819
  }
2627
2820
 
@@ -2633,6 +2826,7 @@ async function runSubagent(
2633
2826
  mode: config.mode,
2634
2827
  async: true,
2635
2828
  dynamicGroup: true,
2829
+ agentContract: step.agentContract,
2636
2830
  });
2637
2831
 
2638
2832
  if (materialized.parallel.length === 0) {
@@ -2664,13 +2858,14 @@ async function runSubagent(
2664
2858
  cwd,
2665
2859
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
2666
2860
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
2861
+ reportOptional: isAgentContractV1(step.agentContract),
2667
2862
  })
2668
2863
  : undefined;
2669
2864
  const groupStopped = stopped || stopAbortController.signal.aborted;
2670
2865
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
2671
2866
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
2672
2867
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
2673
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2868
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2674
2869
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
2675
2870
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
2676
2871
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -2686,7 +2881,7 @@ async function runSubagent(
2686
2881
  markDynamicGraphGroup(stepIndex, groupStopped ? "stopped" : "failed", errorMessage, effectiveGroupAcceptance);
2687
2882
  statusPayload.lastUpdate = Date.now();
2688
2883
  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 });
2884
+ 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
2885
  break;
2691
2886
  }
2692
2887
  flatIndex++;
@@ -2716,6 +2911,7 @@ async function runSubagent(
2716
2911
  mode: config.mode,
2717
2912
  async: true,
2718
2913
  dynamic: step.parallel.acceptanceInput === undefined,
2914
+ agentContract: step.parallel.agentContract ?? step.agentContract,
2719
2915
  }),
2720
2916
  systemPrompt: step.parallel.namespaceOutputPath ? injectOutputPathSystemPrompt(step.parallel.systemPrompt ?? "", outputPath, step.parallel) : step.parallel.systemPrompt,
2721
2917
  outputPath,
@@ -2735,10 +2931,13 @@ async function runSubagent(
2735
2931
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2736
2932
  return {
2737
2933
  agent: task.agent,
2934
+ ...(task.context ? { context: task.context } : {}),
2738
2935
  phase: task.phase ?? step.phase,
2739
2936
  label: task.label,
2740
2937
  outputName: undefined,
2741
2938
  structured: Boolean(task.structuredOutputSchema),
2939
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
2940
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
2742
2941
  status: "pending",
2743
2942
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2744
2943
  ...(transcriptPath ? { transcriptPath } : {}),
@@ -2798,9 +2997,9 @@ async function runSubagent(
2798
2997
  let aborted = false;
2799
2998
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
2800
2999
  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);
3000
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
3001
+ if (stopped) return stoppedStepResult(task.agent, task.context);
3002
+ if (interrupted) return pausedStepResult(task.agent, task.context);
2804
3003
  if (aborted && failFast) {
2805
3004
  const skippedAt = Date.now();
2806
3005
  statusPayload.steps[fi].status = "failed";
@@ -2811,7 +3010,7 @@ async function runSubagent(
2811
3010
  statusPayload.steps[fi].exitCode = -1;
2812
3011
  statusPayload.lastUpdate = skippedAt;
2813
3012
  writeStatusPayload();
2814
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3013
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2815
3014
  }
2816
3015
  const taskStartTime = Date.now();
2817
3016
  statusPayload.currentStep = fi;
@@ -2842,6 +3041,7 @@ async function runSubagent(
2842
3041
  childIntercomTarget: config.childIntercomTargets?.[fi],
2843
3042
  orchestratorIntercomTarget: config.controlIntercomTarget,
2844
3043
  nestedRoute: config.nestedRoute,
3044
+ capabilityCeiling: config.capabilityCeiling,
2845
3045
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2846
3046
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2847
3047
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -2883,13 +3083,23 @@ async function runSubagent(
2883
3083
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2884
3084
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2885
3085
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3086
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
3087
+ statusPayload.steps[fi].launchContractDigest = singleResult.launchContractDigest;
3088
+ statusPayload.steps[fi].effects = singleResult.effects;
3089
+ statusPayload.steps[fi].execution = singleResult.execution;
3090
+ statusPayload.steps[fi].review = singleResult.review;
2886
3091
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2887
3092
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2888
3093
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2889
3094
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
2890
3095
  statusPayload.steps[fi].watchdog = singleResult.watchdog;
3096
+ statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3097
+ statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3098
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3099
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
2891
3100
  statusPayload.lastUpdate = taskEndTime;
2892
3101
  writeStatusPayload();
3102
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
2893
3103
  appendJsonl(eventsPath, JSON.stringify({
2894
3104
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2895
3105
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
@@ -2903,6 +3113,9 @@ async function runSubagent(
2903
3113
  for (const pr of parallelResults) {
2904
3114
  results.push({
2905
3115
  agent: pr.agent,
3116
+ context: pr.context,
3117
+ agentContract: pr.agentContract,
3118
+ launchContractDigest: pr.launchContractDigest,
2906
3119
  output: pr.output,
2907
3120
  error: pr.error,
2908
3121
  protocolError: pr.protocolError,
@@ -2926,18 +3139,34 @@ async function runSubagent(
2926
3139
  artifactPaths: pr.artifactPaths,
2927
3140
  transcriptPath: pr.transcriptPath,
2928
3141
  transcriptError: pr.transcriptError,
3142
+ effects: pr.effects,
3143
+ execution: pr.execution,
3144
+ review: pr.review,
2929
3145
  structuredOutput: pr.structuredOutput,
2930
3146
  structuredOutputPath: pr.structuredOutputPath,
2931
3147
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2932
3148
  acceptance: pr.acceptance,
2933
3149
  watchdog: pr.watchdog,
3150
+ capabilityCeiling: pr.capabilityCeiling,
3151
+ capabilityAudit: pr.capabilityAudit,
2934
3152
  });
2935
3153
  }
2936
3154
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
2937
3155
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
2938
- if (failures.length === 0) {
3156
+ const acceptanceFailures = parallelResults
3157
+ .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3158
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3159
+ if (acceptanceFailures.length > 0) {
3160
+ const message = acceptanceFailures
3161
+ .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
3162
+ .join("\n");
3163
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3164
+ statusPayload.error = message;
3165
+ markDynamicGraphGroup(stepIndex, "failed", message);
3166
+ }
3167
+ if (failures.length === 0 && acceptanceFailures.length === 0) {
2939
3168
  try {
2940
- validateDynamicCollection(step.collect.outputSchema, collection);
3169
+ await validateDynamicCollection(step.collect.outputSchema, collection);
2941
3170
  outputs[step.collect.as] = {
2942
3171
  text: JSON.stringify(collection),
2943
3172
  structured: collection,
@@ -2956,12 +3185,13 @@ async function runSubagent(
2956
3185
  cwd,
2957
3186
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
2958
3187
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3188
+ reportOptional: isAgentContractV1(step.agentContract),
2959
3189
  })
2960
3190
  : undefined;
2961
3191
  const groupStopped = stopped || stopAbortController.signal.aborted;
2962
3192
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
2963
3193
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
2964
- const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3194
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2965
3195
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2966
3196
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
2967
3197
  if (groupError) {
@@ -2981,7 +3211,7 @@ async function runSubagent(
2981
3211
  }
2982
3212
  } catch (error) {
2983
3213
  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 });
3214
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
2985
3215
  statusPayload.error = message;
2986
3216
  markDynamicGraphGroup(stepIndex, "failed", message);
2987
3217
  }
@@ -3001,7 +3231,7 @@ async function runSubagent(
3001
3231
  ts: Date.now(),
3002
3232
  runId: id,
3003
3233
  stepIndex,
3004
- success: failures.length === 0,
3234
+ success: failures.length === 0 && acceptanceFailures.length === 0,
3005
3235
  }));
3006
3236
  if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
3007
3237
  statusPayload.lastUpdate = Date.now();
@@ -3017,6 +3247,7 @@ async function runSubagent(
3017
3247
  const groupStartFlatIndex = flatIndex;
3018
3248
  let aborted = false;
3019
3249
  let worktreeSetup: WorktreeSetup | undefined;
3250
+ let worktreeFinalized = false;
3020
3251
  if (group.worktree) {
3021
3252
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
3022
3253
  if (worktreeTaskCwdConflict) {
@@ -3085,9 +3316,9 @@ async function runSubagent(
3085
3316
  concurrency,
3086
3317
  async (task, taskIdx) => {
3087
3318
  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);
3319
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
3320
+ if (stopped) return stoppedStepResult(task.agent, task.context);
3321
+ if (interrupted) return pausedStepResult(task.agent, task.context);
3091
3322
  if (aborted && failFast) {
3092
3323
  const skippedAt = Date.now();
3093
3324
  statusPayload.steps[fi].status = "failed";
@@ -3102,7 +3333,7 @@ async function runSubagent(
3102
3333
  appendJsonl(eventsPath, JSON.stringify({
3103
3334
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
3104
3335
  }));
3105
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3336
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3106
3337
  }
3107
3338
 
3108
3339
  const taskStartTime = Date.now();
@@ -3145,6 +3376,7 @@ async function runSubagent(
3145
3376
  childIntercomTarget: config.childIntercomTargets?.[fi],
3146
3377
  orchestratorIntercomTarget: config.controlIntercomTarget,
3147
3378
  nestedRoute: config.nestedRoute,
3379
+ capabilityCeiling: config.capabilityCeiling,
3148
3380
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3149
3381
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3150
3382
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -3192,13 +3424,22 @@ async function runSubagent(
3192
3424
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3193
3425
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3194
3426
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3427
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
3428
+ statusPayload.steps[fi].effects = singleResult.effects;
3429
+ statusPayload.steps[fi].execution = singleResult.execution;
3430
+ statusPayload.steps[fi].review = singleResult.review;
3195
3431
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
3196
3432
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
3197
3433
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3198
3434
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
3199
3435
  statusPayload.steps[fi].watchdog = singleResult.watchdog;
3436
+ statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3437
+ statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3438
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3439
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
3200
3440
  statusPayload.lastUpdate = taskEndTime;
3201
3441
  writeStatusPayload();
3442
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
3202
3443
 
3203
3444
  appendJsonl(eventsPath, JSON.stringify({
3204
3445
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -3248,6 +3489,9 @@ async function runSubagent(
3248
3489
  for (const pr of parallelResults) {
3249
3490
  results.push({
3250
3491
  agent: pr.agent,
3492
+ context: pr.context,
3493
+ agentContract: pr.agentContract,
3494
+ launchContractDigest: pr.launchContractDigest,
3251
3495
  output: pr.output,
3252
3496
  error: pr.error,
3253
3497
  protocolError: pr.protocolError,
@@ -3271,6 +3515,9 @@ async function runSubagent(
3271
3515
  artifactPaths: pr.artifactPaths,
3272
3516
  transcriptPath: pr.transcriptPath,
3273
3517
  transcriptError: pr.transcriptError,
3518
+ effects: pr.effects,
3519
+ execution: pr.execution,
3520
+ review: pr.review,
3274
3521
  structuredOutput: pr.structuredOutput,
3275
3522
  structuredOutputPath: pr.structuredOutputPath,
3276
3523
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -3298,21 +3545,62 @@ async function runSubagent(
3298
3545
  attemptedModels: r.attemptedModels,
3299
3546
  })),
3300
3547
  );
3301
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
3548
+ if (worktreeSetup) {
3549
+ const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
3550
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
3551
+ worktreeFinalized = true;
3552
+ const cleanup = cleanupWorktrees(worktreeSetup);
3553
+ try {
3554
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3555
+ manifestPath: parallelHandoffPath(asyncDir),
3556
+ runId: id,
3557
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
3558
+ source: "async",
3559
+ cwd,
3560
+ stepIndex,
3561
+ flatStartIndex: groupStartFlatIndex,
3562
+ setup: worktreeSetup,
3563
+ diffs: captured.diffs,
3564
+ cleanup,
3565
+ results: parallelResults.map((result) => ({
3566
+ agent: result.agent,
3567
+ status: result.stopped ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3568
+ summary: result.output || result.error || "(no output)",
3569
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3570
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3571
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3572
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3573
+ })),
3574
+ });
3575
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
3576
+ } catch (error) {
3577
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
3578
+ }
3579
+ writeStatusPayload();
3580
+ }
3302
3581
 
3303
3582
  appendJsonl(eventsPath, JSON.stringify({
3304
3583
  type: "subagent.parallel.completed",
3305
3584
  ts: Date.now(),
3306
3585
  runId: id,
3307
3586
  stepIndex,
3308
- success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
3587
+ success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
3588
+ && parallelResults.every((result, index) => !(isAgentContractV1(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
3309
3589
  }));
3310
3590
 
3591
+ const acceptanceGateFailure = parallelResults
3592
+ .map((result, index) => ({ result, index, task: group.parallel[index] }))
3593
+ .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3594
+ if (acceptanceGateFailure) {
3595
+ statusPayload.error = acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) ?? "Parallel acceptance gate rejected the step.";
3596
+ writeStatusPayload();
3597
+ break;
3598
+ }
3311
3599
  if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
3312
3600
  break;
3313
3601
  }
3314
3602
  } finally {
3315
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3603
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
3316
3604
  }
3317
3605
  } else {
3318
3606
  const seqStep = step as SubagentStep;
@@ -3354,6 +3642,7 @@ async function runSubagent(
3354
3642
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
3355
3643
  orchestratorIntercomTarget: config.controlIntercomTarget,
3356
3644
  nestedRoute: config.nestedRoute,
3645
+ capabilityCeiling: config.capabilityCeiling,
3357
3646
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
3358
3647
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
3359
3648
  registerStop: (stop) => registerStepStop(flatIndex, stop),
@@ -3376,6 +3665,9 @@ async function runSubagent(
3376
3665
  const childStopped = singleResult.stopped === true;
3377
3666
  results.push({
3378
3667
  agent: singleResult.agent,
3668
+ context: singleResult.context,
3669
+ agentContract: singleResult.agentContract,
3670
+ launchContractDigest: singleResult.launchContractDigest,
3379
3671
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3380
3672
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3381
3673
  protocolError: singleResult.protocolError,
@@ -3390,11 +3682,16 @@ async function runSubagent(
3390
3682
  artifactPaths: singleResult.artifactPaths,
3391
3683
  transcriptPath: singleResult.transcriptPath,
3392
3684
  transcriptError: singleResult.transcriptError,
3685
+ effects: singleResult.effects,
3686
+ execution: singleResult.execution,
3687
+ review: singleResult.review,
3393
3688
  structuredOutput: singleResult.structuredOutput,
3394
3689
  structuredOutputPath: singleResult.structuredOutputPath,
3395
3690
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
3396
3691
  acceptance: singleResult.acceptance,
3397
3692
  watchdog: singleResult.watchdog,
3693
+ capabilityCeiling: singleResult.capabilityCeiling,
3694
+ capabilityAudit: singleResult.capabilityAudit,
3398
3695
  interrupted: singleResult.interrupted,
3399
3696
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
3400
3697
  stopped: stopped || childStopped ? true : undefined,
@@ -3460,17 +3757,26 @@ async function runSubagent(
3460
3757
  statusPayload.steps[flatIndex].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3461
3758
  statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
3462
3759
  statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
3760
+ statusPayload.steps[flatIndex].agentContract = singleResult.agentContract;
3761
+ statusPayload.steps[flatIndex].effects = singleResult.effects;
3762
+ statusPayload.steps[flatIndex].execution = singleResult.execution;
3763
+ statusPayload.steps[flatIndex].review = singleResult.review;
3463
3764
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
3464
3765
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
3465
3766
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3466
3767
  statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
3467
3768
  statusPayload.steps[flatIndex].watchdog = singleResult.watchdog;
3769
+ statusPayload.steps[flatIndex].capabilityCeiling = singleResult.capabilityCeiling;
3770
+ statusPayload.steps[flatIndex].capabilityAudit = singleResult.capabilityAudit;
3771
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3772
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
3468
3773
  if (stepTokens) {
3469
3774
  statusPayload.steps[flatIndex].tokens = stepTokens;
3470
3775
  statusPayload.totalTokens = { ...previousCumulativeTokens };
3471
3776
  }
3472
3777
  statusPayload.lastUpdate = stepEndTime;
3473
3778
  writeStatusPayload();
3779
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, flatIndex, seqStep.agent, singleResult);
3474
3780
 
3475
3781
  appendJsonl(eventsPath, JSON.stringify({
3476
3782
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -3497,6 +3803,11 @@ async function runSubagent(
3497
3803
  }
3498
3804
 
3499
3805
  flatIndex++;
3806
+ if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
3807
+ statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
3808
+ writeStatusPayload();
3809
+ break;
3810
+ }
3500
3811
  if (singleResult.exitCode !== 0) {
3501
3812
  break;
3502
3813
  }
@@ -3567,7 +3878,7 @@ async function runSubagent(
3567
3878
  clearTimeout(timeoutTimer);
3568
3879
  timeoutTimer = undefined;
3569
3880
  }
3570
- statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3881
+ statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3571
3882
  closeSteerInbox(asyncDir, statusPayload.state);
3572
3883
  disposeControlInbox();
3573
3884
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -3663,6 +3974,7 @@ async function runSubagent(
3663
3974
  ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
3664
3975
  results: results.map((r) => ({
3665
3976
  agent: r.agent,
3977
+ context: r.context,
3666
3978
  output: r.output,
3667
3979
  error: r.error,
3668
3980
  protocolError: r.protocolError,
@@ -3686,14 +3998,24 @@ async function runSubagent(
3686
3998
  truncated: r.truncated,
3687
3999
  transcriptPath: r.transcriptPath,
3688
4000
  transcriptError: r.transcriptError,
4001
+ agentContract: r.agentContract,
4002
+ launchContractDigest: r.launchContractDigest,
4003
+ execution: r.execution,
4004
+ review: r.review,
4005
+ effects: r.effects,
3689
4006
  structuredOutput: r.structuredOutput,
3690
4007
  structuredOutputPath: r.structuredOutputPath,
3691
4008
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
3692
4009
  acceptance: r.acceptance,
3693
4010
  watchdog: r.watchdog,
4011
+ capabilityCeiling: r.capabilityCeiling,
4012
+ capabilityAudit: r.capabilityAudit,
3694
4013
  })),
3695
4014
  outputs,
3696
4015
  workflowGraph: statusPayload.workflowGraph,
4016
+ parallelHandoff: statusPayload.parallelHandoff,
4017
+ capabilityCeiling: statusPayload.capabilityCeiling,
4018
+ capabilityAudit: statusPayload.capabilityAudit,
3697
4019
  exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
3698
4020
  timestamp: runEndedAt,
3699
4021
  durationMs: runEndedAt - overallStartTime,
@@ -3703,6 +4025,7 @@ async function runSubagent(
3703
4025
  artifactsDir,
3704
4026
  cwd,
3705
4027
  asyncDir,
4028
+ launchContractDigest: config.launchContractDigest,
3706
4029
  sessionId: config.sessionId,
3707
4030
  sessionFile: effectiveSessionFile,
3708
4031
  intercomTarget: config.controlIntercomTarget,
@@ -3715,6 +4038,28 @@ async function runSubagent(
3715
4038
  } catch (err) {
3716
4039
  console.error(`Failed to write result file ${resultPath}:`, err);
3717
4040
  }
4041
+ if (config.runnerProcessInstanceId) {
4042
+ const writers: Record<string, Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>> = {};
4043
+ const expectedWriters: Record<string, number> = {};
4044
+ for (const [index, result] of results.entries()) {
4045
+ writers[String(index)] = result.writerProcesses ?? [];
4046
+ expectedWriters[String(index)] = result.writerAttemptCount ?? 0;
4047
+ }
4048
+ const candidate: ProcessTerminalCandidate = {
4049
+ version: 1,
4050
+ runId: id,
4051
+ runnerProcessInstanceId: config.runnerProcessInstanceId,
4052
+ writers,
4053
+ expectedWriters,
4054
+ ...(config.revivalLease?.sessionFile ? { sessionFile: config.revivalLease.sessionFile } : {}),
4055
+ ...(config.revivalLeaseToken ? { revivalLeaseToken: config.revivalLeaseToken } : {}),
4056
+ };
4057
+ try {
4058
+ writeProcessTerminalCandidate(asyncDir, candidate);
4059
+ } catch (error) {
4060
+ console.error(`Failed to write process-terminal candidate for '${id}':`, error);
4061
+ }
4062
+ }
3718
4063
  }
3719
4064
 
3720
4065
  async function waitForStartupControl(
@@ -3758,6 +4103,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
3758
4103
  try {
3759
4104
  if (config.revivalLease) {
3760
4105
  lease = acquireSessionLease(config.revivalLease);
4106
+ config.revivalLeaseToken = lease.owner.token;
3761
4107
  writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
3762
4108
  await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
3763
4109
  writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
@@ -3784,11 +4130,17 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
3784
4130
  } finally {
3785
4131
  process.off("exit", releaseOnExit);
3786
4132
  if (lease) {
4133
+ let acknowledged = false;
3787
4134
  try {
3788
- lease.release();
4135
+ acknowledged = lease.release();
3789
4136
  } catch (error) {
3790
4137
  console.error("Failed to release session revival lease:", error);
3791
4138
  }
4139
+ try {
4140
+ markProcessTerminalCandidateLeaseRelease(config.asyncDir, lease.owner.token, acknowledged);
4141
+ } catch (error) {
4142
+ console.error("Failed to record session revival lease release:", error);
4143
+ }
3792
4144
  }
3793
4145
  }
3794
4146
  }