pi-subagents 0.36.0 → 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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +93 -8
  3. package/agents/delegate.md +2 -0
  4. package/agents/worker.md +2 -0
  5. package/package.json +4 -2
  6. package/skills/pi-subagents/SKILL.md +20 -4
  7. package/src/agents/agent-management.ts +7 -1
  8. package/src/agents/agents.ts +99 -12
  9. package/src/api/capability-ceiling.ts +17 -0
  10. package/src/api/delegation.ts +3 -1
  11. package/src/api/preflight.ts +399 -0
  12. package/src/extension/index.ts +2 -0
  13. package/src/extension/rpc.ts +4 -0
  14. package/src/extension/schemas.ts +8 -2
  15. package/src/extension/tool-description.ts +2 -0
  16. package/src/runs/background/async-execution.ts +125 -14
  17. package/src/runs/background/async-resume.ts +24 -7
  18. package/src/runs/background/async-status.ts +18 -0
  19. package/src/runs/background/process-terminal.ts +280 -0
  20. package/src/runs/background/run-status.ts +7 -1
  21. package/src/runs/background/scheduled-runs.ts +6 -1
  22. package/src/runs/background/stale-run-reconciler.ts +6 -0
  23. package/src/runs/background/subagent-runner.ts +182 -12
  24. package/src/runs/foreground/chain-execution.ts +5 -0
  25. package/src/runs/foreground/execution.ts +59 -13
  26. package/src/runs/foreground/subagent-executor.ts +32 -2
  27. package/src/runs/shared/acceptance.ts +41 -30
  28. package/src/runs/shared/capability-ceiling.ts +177 -0
  29. package/src/runs/shared/dynamic-fanout.ts +1 -1
  30. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  31. package/src/runs/shared/nested-events.ts +8 -1
  32. package/src/runs/shared/parallel-utils.ts +5 -0
  33. package/src/runs/shared/pi-args.ts +141 -58
  34. package/src/runs/shared/session-lease.ts +25 -5
  35. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  36. package/src/runs/shared/tool-availability.ts +18 -2
  37. package/src/shared/launch-contract.ts +123 -0
  38. package/src/shared/types.ts +104 -7
  39. package/src/shared/utils.ts +17 -42
  40. package/src/slash/delegation-adapters.ts +1 -1
  41. package/src/slash/slash-commands.ts +1 -1
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
6
7
  import * as fs from "node:fs";
7
8
  import * as os from "node:os";
8
9
  import * as path from "node:path";
@@ -11,7 +12,7 @@ import { createRequire } from "node:module";
11
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
13
  import type { AgentConfig } from "../../agents/agents.ts";
13
14
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
- import { applyThinkingSuffix } from "../shared/pi-args.ts";
15
+ import { applyThinkingSuffix, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
15
16
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
16
17
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
17
18
  import type { RunnerStep } from "../shared/parallel-utils.ts";
@@ -49,11 +50,15 @@ import {
49
50
  getAsyncConfigPath,
50
51
  resolveChildMaxSubagentDepth,
51
52
  } from "../../shared/types.ts";
52
- import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
53
- import { initialTurnBudgetState } from "../shared/turn-budget.ts";
53
+ import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
54
+ import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
54
55
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
55
56
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
56
57
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
58
+ import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
59
+ import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
60
+ import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
61
+ import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
57
62
 
58
63
  const require = createRequire(import.meta.url);
59
64
  const piPackageRoot = resolvePiPackageRoot();
@@ -157,6 +162,7 @@ interface AsyncChainParams {
157
162
  configToolBudget?: ResolvedToolBudget;
158
163
  /** Global cap on simultaneously-running subagent tasks within the async run. */
159
164
  globalConcurrencyLimit?: number;
165
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
160
166
  }
161
167
 
162
168
  interface AsyncSingleParams {
@@ -200,6 +206,7 @@ interface AsyncSingleParams {
200
206
  turnBudget?: ResolvedTurnBudget;
201
207
  toolBudget?: ResolvedToolBudget;
202
208
  configToolBudget?: ResolvedToolBudget;
209
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
203
210
  }
204
211
 
205
212
  interface AsyncExecutionResult {
@@ -232,6 +239,7 @@ export interface AsyncRunnerStepBuildParams {
232
239
  validateOutputBindings?: boolean;
233
240
  toolBudget?: ResolvedToolBudget;
234
241
  configToolBudget?: ResolvedToolBudget;
242
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
235
243
  }
236
244
 
237
245
  export type AsyncRunnerStepBuildResult =
@@ -397,7 +405,7 @@ function terminateRunnerBeforeProceed(pid: number): void {
397
405
  }
398
406
  }
399
407
 
400
- function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number; error?: string } {
408
+ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): { pid?: number; error?: string } {
401
409
  if (!jitiCliPath) {
402
410
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
403
411
  }
@@ -413,12 +421,14 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
413
421
 
414
422
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
415
423
  const cfgPath = getAsyncConfigPath(suffix);
416
- fs.writeFileSync(cfgPath, JSON.stringify(cfg));
424
+ const runnerProcessInstanceId = randomUUID();
425
+ const launchConfig = { ...cfg, runnerProcessInstanceId };
426
+ fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
417
427
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
418
428
  const nodeCommand = resolveAsyncRunnerNodeCommand();
419
- const startupPath = typeof (cfg as { revivalLease?: unknown; asyncDir?: unknown }).revivalLease === "object"
420
- && typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
421
- ? path.join((cfg as { asyncDir: string }).asyncDir, "runner-startup.json")
429
+ const startupPath = typeof (launchConfig as { revivalLease?: unknown; asyncDir?: unknown }).revivalLease === "object"
430
+ && typeof (launchConfig as { asyncDir?: unknown }).asyncDir === "string"
431
+ ? path.join((launchConfig as { asyncDir: string }).asyncDir, "runner-startup.json")
422
432
  : undefined;
423
433
  const startupAckPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-ack.json") : undefined;
424
434
  const startupProceedPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-proceed.json") : undefined;
@@ -426,7 +436,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
426
436
  if (startupAckPath) fs.rmSync(startupAckPath, { force: true });
427
437
  if (startupProceedPath) fs.rmSync(startupProceedPath, { force: true });
428
438
 
429
- const logPaths = resolveAsyncRunnerLogPaths(cfg);
439
+ const logPaths = resolveAsyncRunnerLogPaths(launchConfig);
430
440
  let stdoutFd: number | undefined;
431
441
  let stderrFd: number | undefined;
432
442
  try {
@@ -450,6 +460,56 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
450
460
  proc.on("error", (error) => {
451
461
  console.error(`[pi-subagents] async spawn failed: ${error.message}`);
452
462
  });
463
+ proc.once("close", (exitCode, signal) => {
464
+ const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
465
+ const asyncDir = launch.asyncDir;
466
+ const runId = launch.id;
467
+ if (typeof asyncDir !== "string" || typeof runId !== "string") return;
468
+ finalizeProcessTerminal(asyncDir, runId, {
469
+ processInstanceId: runnerProcessInstanceId,
470
+ closeObservedAt: Date.now(),
471
+ exitCode,
472
+ signal,
473
+ });
474
+ const persisted = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId });
475
+ if (!persisted) return;
476
+ if (launch.nestedRoute && launch.nestedSelf) {
477
+ try {
478
+ let status: import("../../shared/types.ts").AsyncStatus;
479
+ try {
480
+ status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as import("../../shared/types.ts").AsyncStatus;
481
+ status.processTerminal = persisted;
482
+ } catch {
483
+ status = {
484
+ runId,
485
+ mode: "single",
486
+ state: persisted.state === "observed" ? "complete" : "failed",
487
+ startedAt: persisted.observedAt ?? Date.now(),
488
+ lastUpdate: Date.now(),
489
+ processTerminal: persisted,
490
+ };
491
+ }
492
+ writeNestedEvent(launch.nestedRoute, {
493
+ type: "subagent.nested.completed",
494
+ ts: Date.now(),
495
+ parentRunId: launch.nestedSelf.parentRunId,
496
+ parentStepIndex: launch.nestedSelf.parentStepIndex,
497
+ child: nestedSummaryFromAsyncStatus(status, asyncDir, {
498
+ id: runId,
499
+ parentRunId: launch.nestedSelf.parentRunId,
500
+ parentStepIndex: launch.nestedSelf.parentStepIndex,
501
+ depth: launch.nestedSelf.depth,
502
+ path: launch.nestedSelf.path,
503
+ mode: status.mode,
504
+ ts: Date.now(),
505
+ }),
506
+ });
507
+ } catch (error) {
508
+ console.error("Failed to emit final nested process-terminal status:", error);
509
+ }
510
+ }
511
+ onProcessTerminal?.(persisted);
512
+ });
453
513
  if (typeof proc.pid !== "number") {
454
514
  return { error: `async runner did not produce a pid for cwd: ${cwd}` };
455
515
  }
@@ -637,6 +697,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
637
697
  const agentContract = s.agentContract ?? params.agentContract;
638
698
  return {
639
699
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
700
+ ...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
640
701
  agent: s.agent,
641
702
  task,
642
703
  ...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
@@ -834,6 +895,7 @@ export function executeAsyncChain(
834
895
  nestedRoute,
835
896
  } = params;
836
897
  const resultMode = params.resultMode ?? "chain";
898
+ const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
837
899
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
838
900
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
839
901
  const asyncDir = inheritedNestedRoute
@@ -873,6 +935,7 @@ export function executeAsyncChain(
873
935
  asyncDir,
874
936
  toolBudget: params.toolBudget,
875
937
  configToolBudget: params.configToolBudget,
938
+ capabilityCeiling,
876
939
  });
877
940
  if ("error" in built) {
878
941
  try {
@@ -917,6 +980,7 @@ export function executeAsyncChain(
917
980
  sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
918
981
  asyncDir,
919
982
  sessionId: ctx.currentSessionId,
983
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
920
984
  piPackageRoot,
921
985
  piArgv1: process.argv[1],
922
986
  worktreeSetupHook,
@@ -943,6 +1007,7 @@ export function executeAsyncChain(
943
1007
  },
944
1008
  id,
945
1009
  runnerCwd,
1010
+ (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
946
1011
  );
947
1012
  } catch (error) {
948
1013
  const message = error instanceof Error ? error.message : String(error);
@@ -1014,6 +1079,7 @@ export function executeAsyncChain(
1014
1079
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1015
1080
  startedAt: now,
1016
1081
  lastUpdate: now,
1082
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1017
1083
  },
1018
1084
  });
1019
1085
  } catch (error) {
@@ -1040,6 +1106,7 @@ export function executeAsyncChain(
1040
1106
  asyncDir,
1041
1107
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1042
1108
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1109
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1043
1110
  nestedRoute,
1044
1111
  });
1045
1112
  }
@@ -1052,7 +1119,7 @@ export function executeAsyncChain(
1052
1119
 
1053
1120
  return {
1054
1121
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`, ctx.interactive === true) }],
1055
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1122
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1056
1123
  };
1057
1124
  }
1058
1125
 
@@ -1084,6 +1151,7 @@ export function executeAsyncSingle(
1084
1151
  nestedRoute,
1085
1152
  } = params;
1086
1153
  const task = params.task ?? "";
1154
+ const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
1087
1155
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1088
1156
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1089
1157
  const availableModels = params.availableModels;
@@ -1149,6 +1217,39 @@ export function executeAsyncSingle(
1149
1217
  const structuredOutput = params.structuredOutputSchema
1150
1218
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1151
1219
  : undefined;
1220
+ const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1221
+ applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1222
+ );
1223
+ const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1224
+ const toolPlan = resolvePiLaunchToolPlan({
1225
+ tools: agentConfig.tools,
1226
+ extensions: agentConfig.extensions,
1227
+ subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1228
+ mcpDirectTools: agentConfig.mcpDirectTools,
1229
+ cwd: runnerCwd,
1230
+ requireReadTool: Boolean(resolvedSkills.length),
1231
+ structuredOutput: Boolean(params.structuredOutputSchema),
1232
+ capabilityCeiling,
1233
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1234
+ });
1235
+ const launchContractDigest = launchBindingDigest({
1236
+ definitionDigest: agentDefinitionDigest(agentConfig),
1237
+ task,
1238
+ ...(model ? { model } : {}),
1239
+ modelCandidates,
1240
+ ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1241
+ systemPrompt: effectiveSystemPrompt,
1242
+ systemPromptMode: agentConfig.systemPromptMode,
1243
+ inheritProjectContext: agentConfig.inheritProjectContext,
1244
+ inheritSkills: agentConfig.inheritSkills,
1245
+ skills: resolvedSkills.map((skill) => skill.name),
1246
+ tools: toolPlan.effectiveToolAllowlist,
1247
+ extensions: toolPlan.extensionArgs,
1248
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1249
+ ...(outputPath ? { outputPath } : {}),
1250
+ outputMode,
1251
+ ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1252
+ });
1152
1253
  const resolvedAcceptance = resolveEffectiveAcceptance({
1153
1254
  explicit: params.acceptance,
1154
1255
  agentName: agent,
@@ -1160,6 +1261,7 @@ export function executeAsyncSingle(
1160
1261
  });
1161
1262
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1162
1263
  version: 1,
1264
+ launchContractDigest,
1163
1265
  sourceRunId: id,
1164
1266
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1165
1267
  agent,
@@ -1195,6 +1297,7 @@ export function executeAsyncSingle(
1195
1297
  ...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
1196
1298
  ...(artifactsDir ? { artifactsDir } : {}),
1197
1299
  artifactConfig,
1300
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1198
1301
  };
1199
1302
  try {
1200
1303
  writePrivateAtomicJson(path.join(asyncDir, "recovery-descriptor.json"), recoveryDescriptor);
@@ -1209,15 +1312,14 @@ export function executeAsyncSingle(
1209
1312
  steps: [
1210
1313
  {
1211
1314
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
1315
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1212
1316
  agent,
1213
1317
  task: taskWithOutputInstruction,
1214
1318
  ...(params.context ? { context: params.context } : {}),
1215
1319
  cwd: runnerCwd,
1216
1320
  model,
1217
1321
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1218
- modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
1219
- applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
1220
- ),
1322
+ modelCandidates,
1221
1323
  tools: agentConfig.tools,
1222
1324
  extensions: agentConfig.extensions,
1223
1325
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1234,6 +1336,9 @@ export function executeAsyncSingle(
1234
1336
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1235
1337
  waitToolEnabled: params.waitToolEnabled,
1236
1338
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1339
+ definitionDigest: agentDefinitionDigest(agentConfig),
1340
+ launchBindingTask: task,
1341
+ launchContractDigest,
1237
1342
  effectiveAcceptance: resolvedAcceptance,
1238
1343
  ...(structuredOutput ? { structuredOutput } : {}),
1239
1344
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
@@ -1250,6 +1355,7 @@ export function executeAsyncSingle(
1250
1355
  sessionDir: resolvedSessionDir,
1251
1356
  asyncDir,
1252
1357
  sessionId: ctx.currentSessionId,
1358
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1253
1359
  piPackageRoot,
1254
1360
  piArgv1: process.argv[1],
1255
1361
  worktreeSetupHook,
@@ -1263,6 +1369,7 @@ export function executeAsyncSingle(
1263
1369
  controlIntercomTarget,
1264
1370
  childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
1265
1371
  resultMode: "single",
1372
+ launchContractDigest,
1266
1373
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1267
1374
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1268
1375
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1274,6 +1381,7 @@ export function executeAsyncSingle(
1274
1381
  },
1275
1382
  id,
1276
1383
  runnerCwd,
1384
+ (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1277
1385
  );
1278
1386
  } catch (error) {
1279
1387
  const message = error instanceof Error ? error.message : String(error);
@@ -1314,6 +1422,7 @@ export function executeAsyncSingle(
1314
1422
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1315
1423
  startedAt: now,
1316
1424
  lastUpdate: now,
1425
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1317
1426
  },
1318
1427
  });
1319
1428
  } catch (error) {
@@ -1331,14 +1440,16 @@ export function executeAsyncSingle(
1331
1440
  goal: (params.goal ?? task).slice(0, 120),
1332
1441
  cwd: runnerCwd,
1333
1442
  asyncDir,
1443
+ launchContractDigest,
1334
1444
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1335
1445
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1446
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
1336
1447
  nestedRoute,
1337
1448
  });
1338
1449
  }
1339
1450
 
1340
1451
  return {
1341
1452
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
1342
- details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1453
+ details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1343
1454
  };
1344
1455
  }
@@ -4,6 +4,7 @@ import { ASYNC_DIR, RESULTS_DIR, type AcceptanceInput, type AsyncStatus, type St
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
5
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
6
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
7
+ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
7
8
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
8
9
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
9
10
 
@@ -38,6 +39,8 @@ export type AsyncResumeTarget = {
38
39
  model?: string;
39
40
  thinking?: string;
40
41
  recoveryDescriptor?: SteeringRecoveryDescriptor;
42
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
43
+ launchContractDigest?: string;
41
44
  };
42
45
 
43
46
  interface AsyncResultFile {
@@ -52,7 +55,9 @@ interface AsyncResultFile {
52
55
  sessionFile?: string;
53
56
  model?: string;
54
57
  thinking?: string;
55
- results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string }>;
58
+ launchContractDigest?: string;
59
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
60
+ results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
56
61
  }
57
62
 
58
63
  export interface AsyncRunLocation {
@@ -92,9 +97,11 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
92
97
  const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
93
98
  const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
94
99
  const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
100
+ const launchContractDigest = validateOptionalString(child, "launchContractDigest", resultPath, `results[${index}].launchContractDigest`);
101
+ const capabilityCeiling = child.capabilityCeiling === undefined ? undefined : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
95
102
  const success = child.success;
96
103
  if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
97
- return { agent, sessionFile, intercomTarget, model, thinking, ...(typeof success === "boolean" ? { success } : {}) };
104
+ return { agent, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
98
105
  });
99
106
  }
100
107
  const success = data.success;
@@ -110,6 +117,8 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
110
117
  sessionFile: validateOptionalString(data, "sessionFile", resultPath),
111
118
  model: validateOptionalString(data, "model", resultPath),
112
119
  thinking: validateOptionalString(data, "thinking", resultPath),
120
+ launchContractDigest: validateOptionalString(data, "launchContractDigest", resultPath),
121
+ ...(data.capabilityCeiling === undefined ? {} : { capabilityCeiling: parseSubagentCapabilityCeiling(data.capabilityCeiling, `async result file '${resultPath}' capabilityCeiling`) }),
113
122
  ...(typeof success === "boolean" ? { success } : {}),
114
123
  ...(results ? { results } : {}),
115
124
  };
@@ -237,6 +246,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
237
246
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
238
247
  if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
239
248
  if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
249
+ if (status.capabilityCeiling !== undefined) status.capabilityCeiling = parseSubagentCapabilityCeiling(status.capabilityCeiling, `async status '${source}' capabilityCeiling`);
240
250
  if (status.steps !== undefined) {
241
251
  if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
242
252
  status.steps.forEach((step, index) => {
@@ -246,6 +256,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
246
256
  if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
247
257
  if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
248
258
  if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
259
+ if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
249
260
  });
250
261
  }
251
262
  }
@@ -254,10 +265,6 @@ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): Ac
254
265
  if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
255
266
  const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
256
267
  if (explicit === false) return undefined;
257
- if (publicAcceptance.level === "reviewed") {
258
- publicAcceptance.level = "verified";
259
- delete publicAcceptance.review;
260
- }
261
268
  value = publicAcceptance;
262
269
  }
263
270
  const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
@@ -281,7 +288,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
281
288
  "version", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
282
289
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
283
290
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
284
- "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share",
291
+ "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
285
292
  ]);
286
293
  for (const field of Object.keys(parsed)) {
287
294
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -291,6 +298,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
291
298
  if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
292
299
  }
293
300
  if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
301
+ if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
294
302
  if (parsed.agentContract !== undefined) {
295
303
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
296
304
  const contract = parsed.agentContract as Record<string, unknown>;
@@ -407,6 +415,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
407
415
  if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
408
416
  const selectedStep = statusSteps[requestedIndex];
409
417
  if (selectedStep?.status === "running") {
418
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selectedStep.capabilityCeiling);
410
419
  return {
411
420
  kind: "live",
412
421
  runId,
@@ -418,6 +427,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
418
427
  sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
419
428
  model: selectedStep.model,
420
429
  thinking: selectedStep.thinking,
430
+ launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest,
431
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
421
432
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
422
433
  };
423
434
  }
@@ -431,6 +442,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
431
442
  if (!selected) {
432
443
  throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
433
444
  }
445
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selected.step.capabilityCeiling);
434
446
  return {
435
447
  kind: "live",
436
448
  runId,
@@ -442,6 +454,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
442
454
  sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
443
455
  model: selected.step.model,
444
456
  thinking: selected.step.thinking,
457
+ launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest,
458
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
445
459
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
446
460
  };
447
461
  }
@@ -463,6 +477,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
463
477
  const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
464
478
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
465
479
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
480
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
466
481
 
467
482
  return {
468
483
  kind: "revive",
@@ -475,6 +490,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
475
490
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
476
491
  ...(stepModel ? { model: stepModel } : {}),
477
492
  ...(stepThinking ? { thinking: stepThinking } : {}),
493
+ launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest,
494
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
478
495
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
479
496
  };
480
497
  }
@@ -3,12 +3,14 @@ import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
5
  import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
6
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
6
7
  import { readStatus } from "../../shared/utils.ts";
7
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
8
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
9
10
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
10
11
  import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
11
12
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
13
+ import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
12
14
 
13
15
  interface AsyncRunStepSummary {
14
16
  index: number;
@@ -50,6 +52,9 @@ interface AsyncRunStepSummary {
50
52
  execution?: AsyncJobStep["execution"];
51
53
  review?: AsyncJobStep["review"];
52
54
  effects?: AsyncJobStep["effects"];
55
+ processTerminal?: AsyncJobStep["processTerminal"];
56
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
57
+ capabilityAudit?: SubagentCapabilityAudit;
53
58
  children?: NestedRunSummary[];
54
59
  }
55
60
 
@@ -92,6 +97,9 @@ export interface AsyncRunSummary {
92
97
  sessionFile?: string;
93
98
  nestedChildren?: NestedRunSummary[];
94
99
  nestedWarnings?: string[];
100
+ processTerminal?: AsyncStatus["processTerminal"];
101
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
102
+ capabilityAudit?: SubagentCapabilityAudit;
95
103
  }
96
104
 
97
105
  interface AsyncRunListOptions {
@@ -194,6 +202,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
194
202
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
195
203
  }
196
204
  const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
205
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
206
+ ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
197
207
  const steps = status.steps ?? [];
198
208
  const chainStepCount = status.chainStepCount ?? steps.length;
199
209
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
@@ -247,9 +257,13 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
247
257
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
248
258
  ...(step.acceptance ? { acceptance: step.acceptance } : {}),
249
259
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
260
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
250
261
  ...(step.execution ? { execution: step.execution } : {}),
251
262
  ...(step.review ? { review: step.review } : {}),
252
263
  ...(step.effects ? { effects: step.effects } : {}),
264
+ ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
265
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
266
+ ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
253
267
  ...(step.children?.length ? { children: step.children } : {}),
254
268
  };
255
269
  });
@@ -288,6 +302,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
288
302
  steps: summarizedSteps,
289
303
  ...(nestedChildren.length ? { nestedChildren } : {}),
290
304
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
305
+ ...(processTerminal ? { processTerminal } : {}),
306
+ ...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
307
+ ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
308
+ ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
291
309
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
292
310
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
293
311
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),