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
@@ -42,6 +42,7 @@ import {
42
42
  getFinalOutput,
43
43
  findLatestSessionFile,
44
44
  detectSubagentError,
45
+ hasEmptyTerminalAssistantResponse,
45
46
  extractToolArgsPreview,
46
47
  extractTextFromContent,
47
48
  } from "../../shared/utils.ts";
@@ -51,7 +52,8 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
51
52
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
52
53
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
53
54
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
54
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
55
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
56
+ import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
55
57
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
56
58
  import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
57
59
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
@@ -77,6 +79,7 @@ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-co
77
79
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
78
80
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
79
81
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
82
+ import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
80
83
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
81
84
  import {
82
85
  acceptChildWatchdogEvent,
@@ -125,6 +128,7 @@ function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; re
125
128
  function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
126
129
  return {
127
130
  status: "pending",
131
+ evidenceStatus: "pending",
128
132
  explicit: acceptance.explicit,
129
133
  effectiveAcceptance: acceptance,
130
134
  inferredReason: acceptance.inferredReason,
@@ -194,6 +198,7 @@ async function runSingleAttempt(
194
198
  sessionEnabled: boolean;
195
199
  systemPrompt: string;
196
200
  resolvedSkillNames?: string[];
201
+ modelCandidates?: string[];
197
202
  skillsWarning?: string;
198
203
  jsonlPath?: string;
199
204
  artifactPaths?: ArtifactPaths;
@@ -215,7 +220,7 @@ async function runSingleAttempt(
215
220
  childIndex: options.index ?? 0,
216
221
  })
217
222
  : undefined;
218
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath } = buildPiArgs({
223
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
219
224
  baseArgs: ["--mode", "json", "-p"],
220
225
  task,
221
226
  sessionEnabled: shared.sessionEnabled,
@@ -249,12 +254,44 @@ async function runSingleAttempt(
249
254
  allowZeroToolBudget: options.allowZeroToolBudget,
250
255
  childWatchdog,
251
256
  waitToolEnabled: options.waitToolEnabled,
257
+ capabilityCeiling: options.capabilityCeiling,
252
258
  });
253
259
 
260
+ const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
261
+ const toolPlan = resolvePiLaunchToolPlan({
262
+ tools: agent.tools,
263
+ extensions: agent.extensions,
264
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
265
+ mcpDirectTools: agent.mcpDirectTools,
266
+ cwd: options.cwd ?? runtimeCwd,
267
+ requireReadTool: Boolean(shared.resolvedSkillNames?.length),
268
+ structuredOutput: Boolean(options.structuredOutput),
269
+ capabilityCeiling: options.capabilityCeiling,
270
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
271
+ });
272
+ const launchContractDigest = launchBindingDigest({
273
+ definitionDigest: agentDefinitionDigest(agent),
274
+ task: shared.originalTask ?? task,
275
+ ...(modelArg ? { model: modelArg } : {}),
276
+ modelCandidates: shared.modelCandidates,
277
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
278
+ systemPrompt: effectiveSystemPrompt,
279
+ systemPromptMode: agent.systemPromptMode,
280
+ inheritProjectContext: agent.inheritProjectContext,
281
+ inheritSkills: agent.inheritSkills,
282
+ skills: shared.resolvedSkillNames ?? [],
283
+ tools: toolPlan.effectiveToolAllowlist,
284
+ extensions: toolPlan.extensionArgs,
285
+ mcpDirectTools: toolPlan.effectiveMcpTools,
286
+ ...(options.outputPath ? { outputPath: options.outputPath } : {}),
287
+ outputMode: options.outputMode ?? "inline",
288
+ ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
289
+ });
254
290
  const result: SingleResult = withRunContext({
255
291
  agent: agent.name,
256
292
  task: shared.originalTask ?? task,
257
293
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
294
+ launchContractDigest,
258
295
  exitCode: 0,
259
296
  messages: [],
260
297
  usage: emptyUsage(),
@@ -266,6 +303,8 @@ async function runSingleAttempt(
266
303
  skillsWarning: shared.skillsWarning,
267
304
  ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
268
305
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
306
+ ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
307
+ ...(capabilityAudit ? { capabilityAudit } : {}),
269
308
  }, options.context);
270
309
  const startTime = Date.now();
271
310
  if (options.structuredOutput) {
@@ -1041,22 +1080,21 @@ async function runSingleAttempt(
1041
1080
  result.exitCode = 1;
1042
1081
  }
1043
1082
  if (result.exitCode === 0 && !result.error) {
1044
- const errInfo = detectSubagentError(result.messages ?? []);
1045
- if (errInfo.hasError) {
1046
- result.exitCode = errInfo.exitCode ?? 1;
1047
- result.error = errInfo.details
1048
- ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
1049
- : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
1050
- }
1051
- }
1052
- if (result.exitCode === 0 && !result.error) {
1053
- const finalText = getFinalOutput(result.messages ?? []);
1083
+ const messages = result.messages ?? [];
1084
+ const finalText = getFinalOutput(messages);
1054
1085
  const missingStructuredOutput = options.structuredOutput
1055
1086
  ? !existsSync(options.structuredOutput.outputPath)
1056
1087
  : false;
1057
- if (!finalText?.trim() && (!options.structuredOutput || missingStructuredOutput)) {
1088
+ const errInfo = detectSubagentError(messages);
1089
+ const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
1090
+ if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
1058
1091
  result.exitCode = 1;
1059
1092
  result.error = "Subagent produced no output (possible model cold-start or empty response).";
1093
+ } else if (errInfo.hasError) {
1094
+ result.exitCode = errInfo.exitCode ?? 1;
1095
+ result.error = errInfo.details
1096
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
1097
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
1060
1098
  }
1061
1099
  }
1062
1100
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
@@ -1194,6 +1232,10 @@ export async function runSync(
1194
1232
  task: string,
1195
1233
  options: RunSyncOptions,
1196
1234
  ): Promise<SingleResult> {
1235
+ options = {
1236
+ ...options,
1237
+ capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
1238
+ };
1197
1239
  const agent = agents.find((a) => a.name === agentName);
1198
1240
  if (!agent) {
1199
1241
  return withRunContext({
@@ -1317,8 +1359,11 @@ export async function runSync(
1317
1359
  toolCount: target.progressSummary?.toolCount,
1318
1360
  error: target.error,
1319
1361
  agentContract: target.agentContract,
1362
+ launchContractDigest: target.launchContractDigest,
1320
1363
  execution: target.execution,
1321
1364
  acceptance: target.acceptance,
1365
+ capabilityCeiling: target.capabilityCeiling,
1366
+ capabilityAudit: target.capabilityAudit,
1322
1367
  review: target.review,
1323
1368
  effects: target.effects,
1324
1369
  ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
@@ -1383,6 +1428,7 @@ export async function runSync(
1383
1428
  artifactPaths: artifactPathsResult,
1384
1429
  transcriptWriter,
1385
1430
  attemptNotes,
1431
+ modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1386
1432
  outputSnapshot,
1387
1433
  originalTask: task,
1388
1434
  });
@@ -47,6 +47,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
47
47
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
48
48
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
49
49
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
50
+ import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
50
51
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
51
52
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
52
53
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
@@ -240,6 +241,7 @@ interface ExecutionContextData {
240
241
  contextPolicy: AgentDefaultContextPolicy;
241
242
  modelScope?: ModelScopeConfig;
242
243
  parentSessionId: string | null;
244
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
243
245
  }
244
246
 
245
247
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -410,6 +412,8 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
410
412
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
411
413
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
412
414
  ...(result.acceptance ? { acceptance: result.acceptance } : {}),
415
+ ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
416
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
413
417
  };
414
418
  const recovered = previous?.children[index];
415
419
  return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
@@ -473,6 +477,8 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
473
477
  ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
474
478
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
475
479
  ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
480
+ ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
481
+ ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
476
482
  };
477
483
  trimRememberedForegroundRuns(state);
478
484
  const output = getSingleResultOutput(input.result).trim();
@@ -501,7 +507,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
501
507
  });
502
508
  }
503
509
 
504
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
510
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
505
511
  const requested = (params.id ?? params.runId)?.trim();
506
512
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
507
513
  const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
@@ -520,7 +526,16 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
520
526
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
521
527
  const sessionFile = path.resolve(child.sessionFile);
522
528
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
523
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile };
529
+ return {
530
+ runId: run.runId,
531
+ mode: run.mode,
532
+ state: "complete",
533
+ agent: child.agent,
534
+ index,
535
+ cwd: run.cwd,
536
+ sessionFile,
537
+ ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
538
+ };
524
539
  }
525
540
 
526
541
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -534,6 +549,7 @@ type NestedResumeSourceTarget = {
534
549
  index: number;
535
550
  cwd?: string;
536
551
  sessionFile: string;
552
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
537
553
  };
538
554
  type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
539
555
 
@@ -886,6 +902,7 @@ function appendStepToAsyncChain(input: {
886
902
  contextForAgent: contextPolicy.contextForAgent,
887
903
  asyncDir: resolved.location.asyncDir,
888
904
  validateOutputBindings: false,
905
+ capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
889
906
  });
890
907
  if ("error" in built) {
891
908
  return {
@@ -990,6 +1007,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
990
1007
  index: 0,
991
1008
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
992
1009
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1010
+ ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
993
1011
  };
994
1012
  }
995
1013
 
@@ -1283,6 +1301,7 @@ async function resumeAsyncRun(input: {
1283
1301
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1284
1302
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1285
1303
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1304
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1286
1305
  });
1287
1306
  if (result.isError) return result;
1288
1307
  const attachedId = result.details.asyncId ?? runId;
@@ -1352,6 +1371,7 @@ async function resumeAsyncRun(input: {
1352
1371
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1353
1372
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1354
1373
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1374
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1355
1375
  });
1356
1376
  if (result.isError) return result;
1357
1377
 
@@ -2092,6 +2112,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2092
2112
  turnBudget: data.turnBudget,
2093
2113
  toolBudget: data.toolBudget,
2094
2114
  configToolBudget: data.configToolBudget,
2115
+ capabilityCeiling: data.capabilityCeiling,
2095
2116
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2096
2117
  });
2097
2118
  }
@@ -2133,6 +2154,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2133
2154
  turnBudget: data.turnBudget,
2134
2155
  toolBudget: data.toolBudget,
2135
2156
  configToolBudget: data.configToolBudget,
2157
+ capabilityCeiling: data.capabilityCeiling,
2136
2158
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2137
2159
  });
2138
2160
  }
@@ -2190,6 +2212,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2190
2212
  turnBudget: data.turnBudget,
2191
2213
  toolBudget: data.toolBudget,
2192
2214
  configToolBudget: data.configToolBudget,
2215
+ capabilityCeiling: data.capabilityCeiling,
2193
2216
  });
2194
2217
  }
2195
2218
 
@@ -2265,6 +2288,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2265
2288
  toolBudget: data.toolBudget,
2266
2289
  configToolBudget: data.configToolBudget,
2267
2290
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2291
+ capabilityCeiling: data.capabilityCeiling,
2268
2292
  });
2269
2293
 
2270
2294
  if (chainResult.requestedAsync) {
@@ -2321,6 +2345,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2321
2345
  turnBudget: data.turnBudget,
2322
2346
  toolBudget: data.toolBudget,
2323
2347
  configToolBudget: data.configToolBudget,
2348
+ capabilityCeiling: data.capabilityCeiling,
2324
2349
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2325
2350
  });
2326
2351
  }
@@ -2363,6 +2388,7 @@ interface ForegroundParallelRunInput {
2363
2388
  state: SubagentState;
2364
2389
  intercomEvents: IntercomEventBus;
2365
2390
  parentSessionId: string | null;
2391
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
2366
2392
  signal: AbortSignal;
2367
2393
  runId: string;
2368
2394
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2605,6 +2631,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2605
2631
  outputMode: behavior?.outputMode,
2606
2632
  maxSubagentDepth: input.maxSubagentDepths[index],
2607
2633
  waitToolEnabled: input.waitToolEnabled,
2634
+ capabilityCeiling: input.capabilityCeiling,
2608
2635
  controlConfig: input.controlConfig,
2609
2636
  onControlEvent: input.onControlEvent,
2610
2637
  onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
@@ -2915,6 +2942,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2915
2942
  state: deps.state,
2916
2943
  intercomEvents: deps.pi.events,
2917
2944
  parentSessionId: data.parentSessionId,
2945
+ capabilityCeiling: data.capabilityCeiling,
2918
2946
  signal,
2919
2947
  runId,
2920
2948
  sessionDirForIndex,
@@ -3274,6 +3302,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3274
3302
  turnBudget: data.turnBudget,
3275
3303
  enforceHardTurnLimit: params.enforceHardTurnLimit,
3276
3304
  toolBudget: effectiveToolBudget.toolBudget,
3305
+ capabilityCeiling: data.capabilityCeiling,
3277
3306
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3278
3307
  });
3279
3308
  } finally {
@@ -3991,6 +4020,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3991
4020
  contextPolicy,
3992
4021
  modelScope,
3993
4022
  parentSessionId: deps.state.currentSessionId,
4023
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
3994
4024
  };
3995
4025
 
3996
4026
  const foregroundDescription = effectiveParams.task?.trim()
@@ -29,10 +29,9 @@ const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
29
29
  attested: 1,
30
30
  checked: 2,
31
31
  verified: 3,
32
- reviewed: 4,
33
32
  };
34
33
 
35
- const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified", "reviewed"]);
34
+ const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
36
35
  const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
37
36
  "changed-files",
38
37
  "tests-added",
@@ -48,7 +47,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
48
47
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
49
48
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
50
49
  const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
51
- const EXPLICIT_REVIEWED_UNAVAILABLE = "cannot be requested explicitly because this run cannot supply an independent reviewer result; use checked/verified and orchestrate the reviewer separately, or omit acceptance for read-only review tasks.";
50
+ const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
52
51
 
53
52
  function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
54
53
  return level ?? "auto";
@@ -67,7 +66,6 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
67
66
  case "checked":
68
67
  return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
69
68
  case "verified":
70
- case "reviewed":
71
69
  return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
72
70
  }
73
71
  }
@@ -110,10 +108,10 @@ function inferLevel(input: {
110
108
  reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
111
109
  if (input.dynamic || input.dynamicGroup) reasons.push("dynamic fanout context");
112
110
  return {
113
- level: "reviewed",
111
+ level: "checked",
114
112
  reasons,
115
113
  criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
116
- evidence: requiredEvidenceForLevel("reviewed"),
114
+ evidence: requiredEvidenceForLevel("checked"),
117
115
  review: { agent: "reviewer", required: true },
118
116
  };
119
117
  }
@@ -160,9 +158,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
160
158
  if (input === undefined) return errors;
161
159
  if (input === false) return errors;
162
160
  if (typeof input === "string") {
163
- if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
161
+ if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
162
+ else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
164
163
  else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
165
- else if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
166
164
  return errors;
167
165
  }
168
166
  if (!input || typeof input !== "object" || Array.isArray(input)) {
@@ -173,13 +171,14 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
173
171
  for (const key of Object.keys(value)) {
174
172
  if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
175
173
  }
176
- if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
177
- errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified, reviewed.`);
174
+ if (value.level === "reviewed") {
175
+ errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
176
+ } else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
177
+ errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
178
178
  }
179
179
  if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
180
180
  errors.push(`${pathLabel}.reason is required when level is none.`);
181
181
  }
182
- if (value.level === "reviewed") errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
183
182
  if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
184
183
  if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
185
184
  if (Array.isArray(value.criteria)) {
@@ -362,10 +361,7 @@ export function resolveEffectiveAcceptance(input: {
362
361
  (explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
363
362
  evidence,
364
363
  );
365
- let review = explicit.review !== undefined ? explicit.review : inferred.review;
366
- if (level === "reviewed" && input.explicit !== undefined && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
367
- review = { ...review, required: false };
368
- }
364
+ const review = explicit.review !== undefined ? explicit.review : inferred.review;
369
365
  return {
370
366
  level,
371
367
  explicit: input.explicit !== undefined,
@@ -1061,8 +1057,10 @@ export async function evaluateAcceptance(input: {
1061
1057
  reportOptional?: boolean;
1062
1058
  }): Promise<AcceptanceLedger> {
1063
1059
  const acceptance = input.acceptance;
1060
+ const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
1064
1061
  const ledger: AcceptanceLedger = {
1065
- status: acceptance.level === "none" ? "not-required" : "claimed",
1062
+ status: initialStatus,
1063
+ evidenceStatus: initialStatus,
1066
1064
  explicit: acceptance.explicit,
1067
1065
  effectiveAcceptance: acceptance,
1068
1066
  inferredReason: acceptance.inferredReason,
@@ -1084,11 +1082,13 @@ export async function evaluateAcceptance(input: {
1084
1082
  if (parsed.report) {
1085
1083
  ledger.childReport = parsed.report;
1086
1084
  ledger.status = "attested";
1085
+ ledger.evidenceStatus = "attested";
1087
1086
  } else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
1088
1087
  ledger.childReportParseError = parsed.error;
1089
1088
  ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
1090
1089
  if (!input.reportOptional) {
1091
1090
  ledger.status = "rejected";
1091
+ ledger.evidenceStatus = "rejected";
1092
1092
  return ledger;
1093
1093
  }
1094
1094
  } else {
@@ -1103,6 +1103,7 @@ export async function evaluateAcceptance(input: {
1103
1103
  ];
1104
1104
  if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1105
1105
  ledger.status = "checked";
1106
+ ledger.evidenceStatus = "checked";
1106
1107
  }
1107
1108
  }
1108
1109
 
@@ -1110,6 +1111,7 @@ export async function evaluateAcceptance(input: {
1110
1111
  if (acceptance.level === "verified" && acceptance.verify.length === 0) {
1111
1112
  ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
1112
1113
  ledger.status = "rejected";
1114
+ ledger.evidenceStatus = "rejected";
1113
1115
  return ledger;
1114
1116
  }
1115
1117
  ledger.verifyRuns = [];
@@ -1119,34 +1121,42 @@ export async function evaluateAcceptance(input: {
1119
1121
  }
1120
1122
  if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
1121
1123
  ledger.status = "rejected";
1124
+ ledger.evidenceStatus = "rejected";
1122
1125
  return ledger;
1123
1126
  }
1124
- if (!ledger.runtimeChecks.some((check) => check.status === "failed")) ledger.status = "verified";
1127
+ if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
1128
+ ledger.status = "verified";
1129
+ ledger.evidenceStatus = "verified";
1130
+ }
1125
1131
  }
1126
1132
 
1127
1133
  if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
1128
1134
  ledger.status = "rejected";
1135
+ ledger.evidenceStatus = "rejected";
1129
1136
  return ledger;
1130
1137
  }
1131
- if (ledger.status === "claimed" && acceptance.level !== "reviewed") {
1138
+ if (ledger.status === "claimed") {
1132
1139
  ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
1140
+ ledger.evidenceStatus = ledger.status;
1133
1141
  }
1134
1142
 
1135
- if (acceptance.level === "reviewed") {
1136
- if (input.reviewResult) {
1143
+ if (acceptance.review && acceptance.review !== false) {
1144
+ if (input.reviewResult?.status === "reviewed") {
1137
1145
  ledger.reviewResult = input.reviewResult;
1138
- ledger.status = input.reviewResult.status === "no-blockers" ? "reviewed" : "rejected";
1139
- } else {
1140
- const optionalReview = acceptance.review && acceptance.review !== false && acceptance.review.required === false;
1141
- ledger.reviewResult = {
1142
- status: "needs-parent-decision",
1146
+ ledger.status = "reviewed";
1147
+ } else if (input.reviewResult?.status === "blockers") {
1148
+ ledger.reviewResult = input.reviewResult;
1149
+ ledger.status = "rejected";
1150
+ } else if (acceptance.review.required !== false) {
1151
+ ledger.reviewResult = input.reviewResult ?? {
1152
+ status: "review-required",
1143
1153
  findings: [{
1144
- severity: acceptance.explicit && !optionalReview ? "blocker" : "non-blocking",
1145
- issue: "Reviewed acceptance requires an independent reviewer result.",
1154
+ severity: "non-blocking",
1155
+ issue: "Independent review has not been supplied.",
1146
1156
  rationale: "The run cannot be marked reviewed from child evidence alone.",
1147
1157
  }],
1148
1158
  };
1149
- if (acceptance.review === false || (acceptance.explicit && !optionalReview)) ledger.status = "rejected";
1159
+ ledger.status = "review-required";
1150
1160
  }
1151
1161
  }
1152
1162
 
@@ -1154,8 +1164,10 @@ export async function evaluateAcceptance(input: {
1154
1164
  }
1155
1165
 
1156
1166
  export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
1167
+ const status = acceptance.level === "none" ? "not-required" : "rejected";
1157
1168
  return {
1158
- status: acceptance.level === "none" ? "not-required" : "rejected",
1169
+ status,
1170
+ evidenceStatus: status,
1159
1171
  explicit: acceptance.explicit,
1160
1172
  effectiveAcceptance: acceptance,
1161
1173
  inferredReason: acceptance.inferredReason,
@@ -1173,7 +1185,6 @@ export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | und
1173
1185
  if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
1174
1186
  const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
1175
1187
  if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
1176
- if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
1177
1188
  if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
1178
1189
  return "Acceptance rejected.";
1179
1190
  }