pi-subagents 0.36.0 → 0.37.1

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 (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +111 -16
  3. package/agents/delegate.md +2 -0
  4. package/agents/planner.md +2 -1
  5. package/agents/worker.md +2 -0
  6. package/package.json +6 -4
  7. package/skills/pi-subagents/SKILL.md +18 -973
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  9. package/skills/pi-subagents/references/execution-controls.md +411 -0
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
  12. package/src/agents/agent-management.ts +7 -1
  13. package/src/agents/agents.ts +99 -12
  14. package/src/api/capability-ceiling.ts +17 -0
  15. package/src/api/delegation.ts +6 -1
  16. package/src/api/preflight.ts +399 -0
  17. package/src/extension/index.ts +16 -7
  18. package/src/extension/rpc.ts +29 -2
  19. package/src/extension/schemas.ts +10 -4
  20. package/src/extension/tool-description.ts +4 -2
  21. package/src/intercom/intercom-bridge.ts +5 -2
  22. package/src/runs/background/async-execution.ts +125 -14
  23. package/src/runs/background/async-job-tracker.ts +19 -12
  24. package/src/runs/background/async-resume.ts +27 -9
  25. package/src/runs/background/async-status.ts +18 -0
  26. package/src/runs/background/notify.ts +3 -0
  27. package/src/runs/background/process-terminal.ts +280 -0
  28. package/src/runs/background/result-watcher.ts +9 -3
  29. package/src/runs/background/run-status.ts +7 -1
  30. package/src/runs/background/scheduled-runs.ts +6 -1
  31. package/src/runs/background/stale-run-reconciler.ts +6 -0
  32. package/src/runs/background/subagent-runner.ts +182 -12
  33. package/src/runs/foreground/chain-execution.ts +5 -0
  34. package/src/runs/foreground/execution.ts +59 -13
  35. package/src/runs/foreground/subagent-executor.ts +142 -47
  36. package/src/runs/shared/acceptance.ts +41 -30
  37. package/src/runs/shared/capability-ceiling.ts +177 -0
  38. package/src/runs/shared/dynamic-fanout.ts +1 -1
  39. package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
  40. package/src/runs/shared/model-fallback.ts +8 -0
  41. package/src/runs/shared/nested-events.ts +8 -1
  42. package/src/runs/shared/parallel-utils.ts +5 -0
  43. package/src/runs/shared/pi-args.ts +141 -58
  44. package/src/runs/shared/session-lease.ts +25 -5
  45. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  46. package/src/runs/shared/task-intent.ts +1 -1
  47. package/src/runs/shared/tool-availability.ts +18 -2
  48. package/src/shared/launch-contract.ts +123 -0
  49. package/src/shared/types.ts +118 -8
  50. package/src/shared/utils.ts +17 -42
  51. package/src/slash/delegation-adapters.ts +6 -1
  52. package/src/slash/slash-commands.ts +1 -1
  53. package/src/tui/fleet-status.ts +62 -16
  54. package/src/tui/fleet.ts +1 -1
@@ -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";
@@ -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,6 +93,7 @@ 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";
@@ -103,6 +106,7 @@ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHan
103
106
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
104
107
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
105
108
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
109
+ import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
106
110
  import {
107
111
  CHILD_WATCHDOG_CONFIG_ENV,
108
112
  acceptChildWatchdogEvent,
@@ -146,13 +150,19 @@ interface SubagentRunConfig {
146
150
  turnBudget?: ResolvedTurnBudget;
147
151
  toolBudget?: ResolvedToolBudget;
148
152
  revivalLease?: SessionLeaseRequest;
153
+ revivalLeaseToken?: string;
149
154
  /** Global cap on simultaneously-running subagent tasks within this run. */
150
155
  globalConcurrencyLimit?: number;
156
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
157
+ launchContractDigest?: string;
158
+ runnerProcessInstanceId?: string;
151
159
  }
152
160
 
153
161
  interface StepResult {
154
162
  agent: string;
155
163
  context?: "fresh" | "fork";
164
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
165
+ capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
156
166
  output: string;
157
167
  error?: string;
158
168
  protocolError?: ProtocolOutputLimit;
@@ -178,6 +188,7 @@ interface StepResult {
178
188
  transcriptPath?: string;
179
189
  transcriptError?: string;
180
190
  agentContract?: import("../../shared/types.ts").AgentContract;
191
+ launchContractDigest?: string;
181
192
  execution?: import("../../shared/types.ts").ExecutionProjection;
182
193
  review?: import("../../shared/types.ts").ReviewProjection;
183
194
  effects?: import("../../shared/types.ts").EffectsProjection;
@@ -186,6 +197,8 @@ interface StepResult {
186
197
  structuredOutputSchemaPath?: string;
187
198
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
188
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;
189
202
  }
190
203
 
191
204
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -396,6 +409,9 @@ interface RunPiStreamingResult {
396
409
  toolBudgetBlocked?: boolean;
397
410
  observedMutationAttempt?: boolean;
398
411
  watchdog?: ChildWatchdogStateSnapshot;
412
+ processInstanceId: string;
413
+ processCloseObservedAt?: number;
414
+ processSignal?: string | null;
399
415
  }
400
416
 
401
417
  function runPiStreaming(
@@ -418,6 +434,7 @@ function runPiStreaming(
418
434
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
419
435
  ): Promise<RunPiStreamingResult> {
420
436
  return new Promise((resolve) => {
437
+ const processInstanceId = randomUUID();
421
438
  onWriterProcess?.({ state: "spawning" });
422
439
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
423
440
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
@@ -755,6 +772,7 @@ function runPiStreaming(
755
772
  });
756
773
  child.on("close", (exitCode, signal) => {
757
774
  settled = true;
775
+ const processCloseObservedAt = Date.now();
758
776
  try {
759
777
  onWriterProcess?.({ state: "none" });
760
778
  } catch {
@@ -790,6 +808,9 @@ function runPiStreaming(
790
808
  wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
791
809
  observedMutationAttempt,
792
810
  watchdog: childWatchdogState,
811
+ processInstanceId,
812
+ processCloseObservedAt,
813
+ processSignal: signal,
793
814
  });
794
815
  });
795
816
 
@@ -812,7 +833,7 @@ function runPiStreaming(
812
833
  const stderr = stderrTail.text();
813
834
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
814
835
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
815
- 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 });
816
837
  });
817
838
  });
818
839
  }
@@ -955,6 +976,7 @@ interface SingleStepContext {
955
976
  childIntercomTarget?: string;
956
977
  orchestratorIntercomTarget?: string;
957
978
  nestedRoute?: NestedRouteInfo;
979
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
958
980
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
959
981
  onChildEvent?: (event: ChildEvent) => void;
960
982
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
@@ -969,6 +991,7 @@ async function runSingleStep(
969
991
  agent: string;
970
992
  context?: "fresh" | "fork";
971
993
  agentContract?: import("../../shared/types.ts").AgentContract;
994
+ launchContractDigest?: string;
972
995
  output: string;
973
996
  exitCode: number | null;
974
997
  error?: string;
@@ -996,6 +1019,7 @@ async function runSingleStep(
996
1019
  structuredOutputPath?: string;
997
1020
  structuredOutputSchemaPath?: string;
998
1021
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
1022
+ writerAttemptCount?: number;
999
1023
  }> {
1000
1024
  if (step.importAsyncRoot) {
1001
1025
  let importTimedOut = false;
@@ -1107,7 +1131,10 @@ async function runSingleStep(
1107
1131
  ? [step.model]
1108
1132
  : [undefined];
1109
1133
  const attemptedModels: string[] = [];
1134
+ let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1110
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;
1111
1138
  const attemptNotes: string[] = [];
1112
1139
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1113
1140
  let finalResult: RunPiStreamingResult | undefined;
@@ -1116,6 +1143,7 @@ async function runSingleStep(
1116
1143
  let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1117
1144
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1118
1145
  let toolBudgetBlocked = false;
1146
+ let actualLaunchContractDigest = step.launchContractDigest;
1119
1147
 
1120
1148
  for (let index = 0; index < candidates.length; index++) {
1121
1149
  if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
@@ -1138,7 +1166,7 @@ async function runSingleStep(
1138
1166
  childIndex: ctx.flatIndex,
1139
1167
  })
1140
1168
  : undefined;
1141
- const { args, env, tempDir, toolDiagnosticPath } = buildPiArgs({
1169
+ const { args, env, tempDir, toolDiagnosticPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
1142
1170
  parentSessionId: step.parentSessionId,
1143
1171
  baseArgs: ["--mode", "json", "-p"],
1144
1172
  task,
@@ -1155,6 +1183,7 @@ async function runSingleStep(
1155
1183
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1156
1184
  systemPromptMode: step.systemPromptMode,
1157
1185
  mcpDirectTools: step.mcpDirectTools,
1186
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1158
1187
  cwd: step.cwd ?? ctx.cwd,
1159
1188
  promptFileStem: step.agent,
1160
1189
  intercomSessionName: ctx.childIntercomTarget,
@@ -1174,6 +1203,39 @@ async function runSingleStep(
1174
1203
  childWatchdog,
1175
1204
  waitToolEnabled: step.waitToolEnabled,
1176
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;
1177
1239
  const run = await runPiStreaming(
1178
1240
  args,
1179
1241
  step.cwd ?? ctx.cwd,
@@ -1193,6 +1255,16 @@ async function runSingleStep(
1193
1255
  ctx.registerTurnBudgetAbort,
1194
1256
  ctx.onWriterProcess,
1195
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
+ }
1196
1268
  if (run.turnBudget) turnBudget = run.turnBudget;
1197
1269
  else if (ctx.turnBudget) {
1198
1270
  const assistantMessages = run.messages.filter((message) => message.role === "assistant");
@@ -1221,7 +1293,12 @@ async function runSingleStep(
1221
1293
  const missingStructuredOutput = effectiveStructuredOutput
1222
1294
  ? !fs.existsSync(effectiveStructuredOutput.outputPath)
1223
1295
  : false;
1224
- 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))
1225
1302
  ? "Subagent produced no output (possible model cold-start or empty response)."
1226
1303
  : undefined;
1227
1304
  let structuredOutput: unknown;
@@ -1257,23 +1334,22 @@ async function runSingleStep(
1257
1334
  const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1258
1335
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1259
1336
  : undefined;
1260
- const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || structuredError
1337
+ const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || structuredError || emptyOutputError
1261
1338
  ? 1
1262
1339
  : hiddenError?.hasError
1263
1340
  ? (hiddenError.exitCode ?? 1)
1264
- : emptyOutputError
1341
+ : run.error && run.exitCode === 0
1265
1342
  ? 1
1266
- : run.error && run.exitCode === 0
1267
- ? 1
1268
- : run.exitCode;
1343
+ : run.exitCode;
1269
1344
  const error = toolAvailabilityError
1270
1345
  ?? completionGuardError
1271
1346
  ?? structuredError
1347
+ ?? emptyOutputError
1272
1348
  ?? (hiddenError?.hasError
1273
1349
  ? hiddenError.details
1274
1350
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1275
1351
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1276
- : 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));
1277
1353
  const attempt: ModelAttempt = {
1278
1354
  model: candidate ?? run.model ?? step.model ?? "default",
1279
1355
  success: effectiveExitCode === 0 && !error,
@@ -1395,6 +1471,8 @@ async function runSingleStep(
1395
1471
  modelAttempts,
1396
1472
  error: effectiveFinalError,
1397
1473
  acceptance: effectiveAcceptance,
1474
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1475
+ launchContractDigest: actualLaunchContractDigest,
1398
1476
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1399
1477
  transcriptError: transcriptWriter?.getError(),
1400
1478
  skills: step.skills,
@@ -1409,6 +1487,7 @@ async function runSingleStep(
1409
1487
  agent: step.agent,
1410
1488
  context: step.context,
1411
1489
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1490
+ launchContractDigest: actualLaunchContractDigest,
1412
1491
  output: outputForSummary,
1413
1492
  exitCode: effectiveFinalExitCode,
1414
1493
  error: effectiveFinalError,
@@ -1437,6 +1516,9 @@ async function runSingleStep(
1437
1516
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1438
1517
  acceptance: effectiveAcceptance,
1439
1518
  watchdog: finalResult?.watchdog,
1519
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1520
+ writerProcesses,
1521
+ writerAttemptCount,
1440
1522
  };
1441
1523
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1442
1524
  }
@@ -1445,6 +1527,19 @@ type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1445
1527
  exitCode?: number | null;
1446
1528
  };
1447
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
+
1448
1543
  type RunnerStatusPayload = Omit<AsyncStatus, "steps" | "parallelGroups" | "pid" | "cwd" | "currentStep" | "chainStepCount" | "lastUpdate"> & {
1449
1544
  pid: number;
1450
1545
  cwd: string;
@@ -1654,6 +1749,8 @@ async function runSubagent(
1654
1749
  outputName: task.outputName,
1655
1750
  structured: task.structured,
1656
1751
  ...(task.agentContract ? { agentContract: task.agentContract } : {}),
1752
+ ...(task.launchContractDigest ? { launchContractDigest: task.launchContractDigest } : {}),
1753
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
1657
1754
  status: "pending",
1658
1755
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1659
1756
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -1677,6 +1774,7 @@ async function runSubagent(
1677
1774
  outputName: step.collect.as,
1678
1775
  structured: Boolean(step.collect.outputSchema),
1679
1776
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1777
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1680
1778
  status: "pending",
1681
1779
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1682
1780
  recentTools: [],
@@ -1694,6 +1792,8 @@ async function runSubagent(
1694
1792
  outputName: step.outputName,
1695
1793
  structured: step.structured,
1696
1794
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1795
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
1796
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1697
1797
  status: "pending",
1698
1798
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1699
1799
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -1711,6 +1811,11 @@ async function runSubagent(
1711
1811
  const sessionEnabled = Boolean(config.sessionDir)
1712
1812
  || shareEnabled
1713
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
+ }
1714
1819
  const statusPayload: RunnerStatusPayload = {
1715
1820
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1716
1821
  runId: id,
@@ -1732,6 +1837,9 @@ async function runSubagent(
1732
1837
  chainStepCount: steps.length,
1733
1838
  parallelGroups,
1734
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 } } : {}),
1735
1843
  steps: initialStatusSteps,
1736
1844
  artifactsDir,
1737
1845
  sessionDir: config.sessionDir,
@@ -2829,6 +2937,7 @@ async function runSubagent(
2829
2937
  outputName: undefined,
2830
2938
  structured: Boolean(task.structuredOutputSchema),
2831
2939
  ...(task.agentContract ? { agentContract: task.agentContract } : {}),
2940
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
2832
2941
  status: "pending",
2833
2942
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2834
2943
  ...(transcriptPath ? { transcriptPath } : {}),
@@ -2932,6 +3041,7 @@ async function runSubagent(
2932
3041
  childIntercomTarget: config.childIntercomTargets?.[fi],
2933
3042
  orchestratorIntercomTarget: config.controlIntercomTarget,
2934
3043
  nestedRoute: config.nestedRoute,
3044
+ capabilityCeiling: config.capabilityCeiling,
2935
3045
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2936
3046
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2937
3047
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -2974,6 +3084,7 @@ async function runSubagent(
2974
3084
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2975
3085
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
2976
3086
  statusPayload.steps[fi].agentContract = singleResult.agentContract;
3087
+ statusPayload.steps[fi].launchContractDigest = singleResult.launchContractDigest;
2977
3088
  statusPayload.steps[fi].effects = singleResult.effects;
2978
3089
  statusPayload.steps[fi].execution = singleResult.execution;
2979
3090
  statusPayload.steps[fi].review = singleResult.review;
@@ -2982,8 +3093,13 @@ async function runSubagent(
2982
3093
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2983
3094
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
2984
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;
2985
3100
  statusPayload.lastUpdate = taskEndTime;
2986
3101
  writeStatusPayload();
3102
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
2987
3103
  appendJsonl(eventsPath, JSON.stringify({
2988
3104
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2989
3105
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
@@ -2999,6 +3115,7 @@ async function runSubagent(
2999
3115
  agent: pr.agent,
3000
3116
  context: pr.context,
3001
3117
  agentContract: pr.agentContract,
3118
+ launchContractDigest: pr.launchContractDigest,
3002
3119
  output: pr.output,
3003
3120
  error: pr.error,
3004
3121
  protocolError: pr.protocolError,
@@ -3030,6 +3147,8 @@ async function runSubagent(
3030
3147
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
3031
3148
  acceptance: pr.acceptance,
3032
3149
  watchdog: pr.watchdog,
3150
+ capabilityCeiling: pr.capabilityCeiling,
3151
+ capabilityAudit: pr.capabilityAudit,
3033
3152
  });
3034
3153
  }
3035
3154
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
@@ -3257,6 +3376,7 @@ async function runSubagent(
3257
3376
  childIntercomTarget: config.childIntercomTargets?.[fi],
3258
3377
  orchestratorIntercomTarget: config.controlIntercomTarget,
3259
3378
  nestedRoute: config.nestedRoute,
3379
+ capabilityCeiling: config.capabilityCeiling,
3260
3380
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3261
3381
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3262
3382
  registerStop: (stop) => registerStepStop(fi, stop),
@@ -3313,8 +3433,13 @@ async function runSubagent(
3313
3433
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3314
3434
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
3315
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;
3316
3440
  statusPayload.lastUpdate = taskEndTime;
3317
3441
  writeStatusPayload();
3442
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
3318
3443
 
3319
3444
  appendJsonl(eventsPath, JSON.stringify({
3320
3445
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -3366,6 +3491,7 @@ async function runSubagent(
3366
3491
  agent: pr.agent,
3367
3492
  context: pr.context,
3368
3493
  agentContract: pr.agentContract,
3494
+ launchContractDigest: pr.launchContractDigest,
3369
3495
  output: pr.output,
3370
3496
  error: pr.error,
3371
3497
  protocolError: pr.protocolError,
@@ -3516,6 +3642,7 @@ async function runSubagent(
3516
3642
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
3517
3643
  orchestratorIntercomTarget: config.controlIntercomTarget,
3518
3644
  nestedRoute: config.nestedRoute,
3645
+ capabilityCeiling: config.capabilityCeiling,
3519
3646
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
3520
3647
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
3521
3648
  registerStop: (stop) => registerStepStop(flatIndex, stop),
@@ -3540,6 +3667,7 @@ async function runSubagent(
3540
3667
  agent: singleResult.agent,
3541
3668
  context: singleResult.context,
3542
3669
  agentContract: singleResult.agentContract,
3670
+ launchContractDigest: singleResult.launchContractDigest,
3543
3671
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3544
3672
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3545
3673
  protocolError: singleResult.protocolError,
@@ -3562,6 +3690,8 @@ async function runSubagent(
3562
3690
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
3563
3691
  acceptance: singleResult.acceptance,
3564
3692
  watchdog: singleResult.watchdog,
3693
+ capabilityCeiling: singleResult.capabilityCeiling,
3694
+ capabilityAudit: singleResult.capabilityAudit,
3565
3695
  interrupted: singleResult.interrupted,
3566
3696
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
3567
3697
  stopped: stopped || childStopped ? true : undefined,
@@ -3636,12 +3766,17 @@ async function runSubagent(
3636
3766
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3637
3767
  statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
3638
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;
3639
3773
  if (stepTokens) {
3640
3774
  statusPayload.steps[flatIndex].tokens = stepTokens;
3641
3775
  statusPayload.totalTokens = { ...previousCumulativeTokens };
3642
3776
  }
3643
3777
  statusPayload.lastUpdate = stepEndTime;
3644
3778
  writeStatusPayload();
3779
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, flatIndex, seqStep.agent, singleResult);
3645
3780
 
3646
3781
  appendJsonl(eventsPath, JSON.stringify({
3647
3782
  type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -3864,6 +3999,7 @@ async function runSubagent(
3864
3999
  transcriptPath: r.transcriptPath,
3865
4000
  transcriptError: r.transcriptError,
3866
4001
  agentContract: r.agentContract,
4002
+ launchContractDigest: r.launchContractDigest,
3867
4003
  execution: r.execution,
3868
4004
  review: r.review,
3869
4005
  effects: r.effects,
@@ -3872,10 +4008,14 @@ async function runSubagent(
3872
4008
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
3873
4009
  acceptance: r.acceptance,
3874
4010
  watchdog: r.watchdog,
4011
+ capabilityCeiling: r.capabilityCeiling,
4012
+ capabilityAudit: r.capabilityAudit,
3875
4013
  })),
3876
4014
  outputs,
3877
4015
  workflowGraph: statusPayload.workflowGraph,
3878
4016
  parallelHandoff: statusPayload.parallelHandoff,
4017
+ capabilityCeiling: statusPayload.capabilityCeiling,
4018
+ capabilityAudit: statusPayload.capabilityAudit,
3879
4019
  exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
3880
4020
  timestamp: runEndedAt,
3881
4021
  durationMs: runEndedAt - overallStartTime,
@@ -3885,6 +4025,7 @@ async function runSubagent(
3885
4025
  artifactsDir,
3886
4026
  cwd,
3887
4027
  asyncDir,
4028
+ launchContractDigest: config.launchContractDigest,
3888
4029
  sessionId: config.sessionId,
3889
4030
  sessionFile: effectiveSessionFile,
3890
4031
  intercomTarget: config.controlIntercomTarget,
@@ -3897,6 +4038,28 @@ async function runSubagent(
3897
4038
  } catch (err) {
3898
4039
  console.error(`Failed to write result file ${resultPath}:`, err);
3899
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
+ }
3900
4063
  }
3901
4064
 
3902
4065
  async function waitForStartupControl(
@@ -3940,6 +4103,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
3940
4103
  try {
3941
4104
  if (config.revivalLease) {
3942
4105
  lease = acquireSessionLease(config.revivalLease);
4106
+ config.revivalLeaseToken = lease.owner.token;
3943
4107
  writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
3944
4108
  await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
3945
4109
  writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
@@ -3966,11 +4130,17 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
3966
4130
  } finally {
3967
4131
  process.off("exit", releaseOnExit);
3968
4132
  if (lease) {
4133
+ let acknowledged = false;
3969
4134
  try {
3970
- lease.release();
4135
+ acknowledged = lease.release();
3971
4136
  } catch (error) {
3972
4137
  console.error("Failed to release session revival lease:", error);
3973
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
+ }
3974
4144
  }
3975
4145
  }
3976
4146
  }
@@ -79,6 +79,7 @@ import { isAgentContractV1 } from "../shared/agent-contract.ts";
79
79
  import type { ChainOutputMap } from "../../shared/types.ts";
80
80
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
81
81
  import type { ContextMode } from "../shared/context-mode.ts";
82
+ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
82
83
 
83
84
  interface ChainExecutionDetailsInput {
84
85
  results: SingleResult[];
@@ -148,6 +149,7 @@ interface ParallelChainRunInput {
148
149
  toolBudget?: ResolvedToolBudget;
149
150
  configToolBudget?: ToolBudgetConfig;
150
151
  globalSemaphore?: Semaphore;
152
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
151
153
  dynamic?: boolean;
152
154
  }
153
155
 
@@ -342,6 +344,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
342
344
  const agentContract = task.agentContract ?? input.step.agentContract ?? input.agentContract;
343
345
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
344
346
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
347
+ capabilityCeiling: input.capabilityCeiling,
345
348
  context: input.contextForAgent?.(task.agent),
346
349
  cwd: taskCwd,
347
350
  signal: input.signal,
@@ -471,6 +474,7 @@ interface ChainExecutionParams {
471
474
  configToolBudget?: ToolBudgetConfig;
472
475
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
473
476
  globalConcurrencyLimit?: number;
477
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
474
478
  }
475
479
 
476
480
  interface ChainExecutionResult {
@@ -1248,6 +1252,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1248
1252
  });
1249
1253
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1250
1254
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1255
+ capabilityCeiling: params.capabilityCeiling,
1251
1256
  context: params.contextForAgent?.(seqStep.agent),
1252
1257
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1253
1258
  signal,