@zq-silk/yui 0.10.0 → 0.11.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 (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { createRunAssignment } from "../context/runContextContract.js";
4
- import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeRunContextSnapshot } from "../context/runContextPack.js";
4
+ import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeExecutionStageContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
5
5
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
6
6
  import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
7
7
  import { createTaskEvent } from "../event/taskEvent.js";
@@ -23,7 +23,7 @@ import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchAct
23
23
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
24
24
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
25
25
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
26
- import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
26
+ import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
27
27
  import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
28
28
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
29
29
  import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
@@ -34,7 +34,7 @@ import { runPublicationCommand } from "./taskPublicationCommands.js";
34
34
  import { enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
35
35
  import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
36
36
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
37
- import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
37
+ import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
38
38
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
39
39
  import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
40
40
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
@@ -42,8 +42,11 @@ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publica
42
42
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
43
43
  import { resolveProject } from "../repository/project.js";
44
44
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
45
- import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
46
- import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
45
+ import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, planWorkItemExplorationStage, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
46
+ import { addExecutionLane, createExecutionGroup, queueExecutionLaneRetry, resolveExecutionGroup, restartExecutionLane, skipPendingExecutionLanes, updateExecutionLane, WORK_ITEM_EXECUTION_MODES } from "../execution/executionGroup.js";
47
+ import { executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
48
+ import { resolveContextBudget, resolveControllerTaskConcurrency, resolveRuntimeHealth } from "../config/yuiConfig.js";
49
+ import { assertIndependentVerificationRoles, candidateConvergenceEvidenceSufficient, candidateConvergenceDirective, validateCandidateConvergenceResolution } from "../execution/candidateConvergence.js";
47
50
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
48
51
  import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
49
52
  import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
@@ -62,7 +65,7 @@ import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActo
62
65
  import { createTaskTerminalNotification } from "../scheduler/operatorNotification.js";
63
66
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
64
67
  import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
65
- import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition } from "../scheduler/actionability.js";
68
+ import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition, projectQueuedResourceLaneIdentities } from "../scheduler/actionability.js";
66
69
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
67
70
  import { buildTaskOverview, parseTaskListOptions, renderTaskOverview } from "./taskOverviewCommand.js";
68
71
  const LEADER_ROLE = "leader";
@@ -277,30 +280,76 @@ export function normalizedExecutionLaneRoles(assignee, requestedRoles, expanding
277
280
  ? [...requestedRoles]
278
281
  : [assignee, ...requestedRoles];
279
282
  }
283
+ /** A bounded same-stage retry repeats the frozen topology and Lane owners. */
284
+ export function resolvedExecutionStageRetryGroup(group) {
285
+ if (group?.stage === undefined || group.resolution === undefined)
286
+ return undefined;
287
+ if (group.resolution.decision !== "retry"
288
+ && group.resolution.decision !== "blocked")
289
+ return undefined;
290
+ return group.stage.stage === "resolve" && group.resolution.decision === "retry"
291
+ ? undefined
292
+ : group;
293
+ }
280
294
  /** Derive the complete Worker Lane plan once for both preflight and mutation. */
281
295
  export function normalizedExecutionLanePlan(input) {
282
296
  const retry = input.phase === "retry" || input.retryLaneId !== undefined;
283
- const expanding = !retry && input.status === "running" && input.existingGroup !== undefined;
297
+ const queued = !retry && input.status === "running" && input.existingGroup !== undefined
298
+ && input.requestedRoles.length === 0
299
+ ? input.existingGroup.lanes.filter(({ status, effective }) => (status === "pending" && effective !== undefined))
300
+ : [];
301
+ const resumingQueued = queued.length > 0;
302
+ const expanding = !retry
303
+ && !resumingQueued
304
+ && input.status === "running"
305
+ && input.existingGroup !== undefined;
284
306
  const roles = retry
285
307
  ? []
286
- : normalizedExecutionLaneRoles(input.assignee, input.requestedRoles, expanding);
308
+ : resumingQueued
309
+ ? queued.map(({ roleName }) => roleName)
310
+ : input.resolvedRetryGroup !== undefined && input.requestedRoles.length === 0
311
+ ? input.resolvedRetryGroup.lanes.map(({ roleName }) => roleName)
312
+ : normalizedExecutionLaneRoles(input.assignee, input.requestedRoles, expanding);
287
313
  const strategy = input.existingGroup?.strategy
288
314
  ?? input.requestedStrategy
315
+ ?? input.resolvedRetryGroup?.strategy
289
316
  ?? { mode: "fixed", count: roles.length };
290
317
  const capacity = strategy.mode === "fixed" ? strategy.count : strategy.max;
291
318
  const requestedCount = retry
292
319
  ? input.existingGroup?.lanes.length ?? 0
293
- : expanding
294
- ? input.existingGroup.lanes.length + roles.length
295
- : roles.length;
320
+ : resumingQueued
321
+ ? input.existingGroup.lanes.length
322
+ : expanding
323
+ ? input.existingGroup.lanes.length + roles.length
324
+ : roles.length;
296
325
  const laneIds = input.retryLaneId !== undefined
297
326
  ? [input.retryLaneId]
298
327
  : retry
299
328
  ? []
300
- : input.existingGroup === undefined
301
- ? roles.map((_, index) => `${input.nextGroupId}-lane-${index + 1}`)
302
- : roles.map((_, index) => `${input.existingGroup.id}-lane-${input.existingGroup.lanes.length + index + 1}`);
303
- return { expanding, roles, strategy, capacity, requestedCount, laneIds };
329
+ : resumingQueued
330
+ ? queued.map(({ id }) => id)
331
+ : input.existingGroup === undefined
332
+ ? roles.map((_, index) => `${input.nextGroupId}-lane-${index + 1}`)
333
+ : roles.map((_, index) => `${input.existingGroup.id}-lane-${input.existingGroup.lanes.length + index + 1}`);
334
+ return { expanding, resumingQueued, roles, strategy, capacity, requestedCount, laneIds };
335
+ }
336
+ function activeResourceLaneIdentities(store) {
337
+ return store.listActiveTaskIds().flatMap((taskId) => (store.listAgentRuns(taskId).flatMap((run) => {
338
+ if (run.status !== "active"
339
+ || run.executionGroupId === undefined
340
+ || run.executionLaneId === undefined)
341
+ return [];
342
+ return [{
343
+ taskId,
344
+ ...(run.workItemId === undefined ? {} : { workItemId: run.workItemId }),
345
+ executionGroupId: run.executionGroupId,
346
+ executionLaneId: run.executionLaneId,
347
+ providerId: run.effective.adapterId,
348
+ agentId: run.effective.agentId,
349
+ ...(run.effective.model === undefined ? {} : { model: run.effective.model }),
350
+ requestedAt: run.createdAt
351
+ }];
352
+ })));
304
353
  }
305
354
  function taskProjectCommand(args, store, options) {
306
355
  const [command, ...rest] = args;
@@ -2007,13 +2056,45 @@ function updateWork(args, store, options) {
2007
2056
  });
2008
2057
  }
2009
2058
  function dispatchWork(args, store, options) {
2010
- const usage = "Task work dispatch usage: yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...].";
2011
- const parsed = parseMultiValueTail(args, new Set(["--input", "--strategy"]), new Set(["--lane-role"]), usage);
2059
+ const usage = "Task work dispatch usage: yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...] [--mode <single|parallel-diverse|ensemble-replicated|adversarial|adaptive-exploration>] [--max-rounds <count>] [--stage-max-attempts <count>] [--stage-max-tokens <count>] [--stage-max-tool-calls <count>] [--stage-max-seconds <count>] [--stage-quorum <count>] [--stage-straggler-seconds <count>] [--stage-min-marginal-value <0-100>].";
2060
+ const parsed = parseMultiValueTail(args, new Set([
2061
+ "--input",
2062
+ "--strategy",
2063
+ "--mode",
2064
+ "--max-rounds",
2065
+ "--stage-max-attempts",
2066
+ "--stage-max-tokens",
2067
+ "--stage-max-tool-calls",
2068
+ "--stage-max-seconds",
2069
+ "--stage-quorum",
2070
+ "--stage-straggler-seconds",
2071
+ "--stage-min-marginal-value"
2072
+ ]), new Set(["--lane-role"]), usage);
2012
2073
  exactPositionals(parsed.positionals, 1, usage);
2013
2074
  const requestedStrategy = parseExecutionStrategy(parsed.options.get("--strategy"), usage);
2014
2075
  const requestedLaneRoles = parsed.multiOptions.get("--lane-role") ?? [];
2076
+ const requestedMode = parseWorkItemExecutionMode(parsed.options.get("--mode"), usage);
2077
+ const requestedMaxRounds = parseOptionalPositiveInteger(parsed.options.get("--max-rounds"), "--max-rounds", usage);
2078
+ const requestedStageMaxAttempts = parseOptionalPositiveInteger(parsed.options.get("--stage-max-attempts"), "--stage-max-attempts", usage);
2079
+ const requestedStageMaxTokens = parseOptionalPositiveInteger(parsed.options.get("--stage-max-tokens"), "--stage-max-tokens", usage);
2080
+ const requestedStageMaxToolCalls = parseOptionalPositiveInteger(parsed.options.get("--stage-max-tool-calls"), "--stage-max-tool-calls", usage);
2081
+ const requestedStageMaxSeconds = parseOptionalPositiveInteger(parsed.options.get("--stage-max-seconds"), "--stage-max-seconds", usage);
2082
+ const requestedStageQuorum = parseOptionalPositiveInteger(parsed.options.get("--stage-quorum"), "--stage-quorum", usage);
2083
+ const requestedStageStragglerSeconds = parseOptionalPositiveInteger(parsed.options.get("--stage-straggler-seconds"), "--stage-straggler-seconds", usage);
2084
+ const requestedStageMinMarginalValue = parseOptionalPercentage(parsed.options.get("--stage-min-marginal-value"), "--stage-min-marginal-value", usage);
2085
+ const requestedResourceOptions = [
2086
+ requestedStageMaxTokens,
2087
+ requestedStageMaxToolCalls,
2088
+ requestedStageMaxSeconds,
2089
+ requestedStageQuorum,
2090
+ requestedStageStragglerSeconds,
2091
+ requestedStageMinMarginalValue
2092
+ ];
2093
+ const explorationMode = requestedMode === undefined || requestedMode === "single"
2094
+ ? undefined
2095
+ : requestedMode;
2015
2096
  const now = clock(options);
2016
- const runs = store.transaction((tx) => {
2097
+ const dispatch = store.transaction((tx) => {
2017
2098
  const item = requireWorkItem(tx, parsed.positionals[0], options);
2018
2099
  const task = requireTask(tx, item.taskId);
2019
2100
  if (task.status !== "active") {
@@ -2023,10 +2104,44 @@ function dispatchWork(args, store, options) {
2023
2104
  const existingGroup = currentGroup?.resolution === undefined
2024
2105
  ? currentGroup
2025
2106
  : undefined;
2107
+ if (item.status === "failed" && existingGroup !== undefined) {
2108
+ throw usageError(`Work Item ${item.id} retains unresolved ExecutionGroup ${existingGroup.id}; `
2109
+ + "retry the exact failed AgentRun so completed Lane results remain reusable.");
2110
+ }
2026
2111
  const expanding = item.status === "running" && existingGroup !== undefined;
2027
- if (!expanding && item.status !== "pending" && item.status !== "failed") {
2112
+ const continuingExploration = currentGroup?.stage !== undefined
2113
+ && currentGroup.resolution !== undefined
2114
+ && (item.status === "running" || item.status === "failed");
2115
+ const resolvedRetryGroup = resolvedExecutionStageRetryGroup(currentGroup);
2116
+ const startingExploration = item.executionGroups.length === 0
2117
+ && explorationMode !== undefined;
2118
+ if (!expanding
2119
+ && !continuingExploration
2120
+ && item.status !== "pending"
2121
+ && item.status !== "failed") {
2028
2122
  throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
2029
2123
  }
2124
+ if (requestedMode === "single" && currentGroup?.stage !== undefined) {
2125
+ throw usageError("An exploration WorkItem cannot return to single mode.");
2126
+ }
2127
+ if (explorationMode !== undefined
2128
+ && item.executionGroups.length > 0
2129
+ && currentGroup?.stage === undefined) {
2130
+ throw usageError("A WorkItem cannot enter exploration after single-mode ExecutionGroups.");
2131
+ }
2132
+ if (!startingExploration
2133
+ && !continuingExploration
2134
+ && (requestedMaxRounds !== undefined
2135
+ || requestedStageMaxAttempts !== undefined
2136
+ || requestedResourceOptions.some((value) => value !== undefined))) {
2137
+ throw usageError("Exploration limits require an explicit non-single --mode.");
2138
+ }
2139
+ if (existingGroup !== undefined && (requestedMode !== undefined
2140
+ || requestedMaxRounds !== undefined
2141
+ || requestedStageMaxAttempts !== undefined
2142
+ || requestedResourceOptions.some((value) => value !== undefined))) {
2143
+ throw usageError(`Execution stage configuration is frozen: ${existingGroup.id}.`);
2144
+ }
2030
2145
  if (item.assignee === undefined) {
2031
2146
  throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
2032
2147
  + `The Task Leader must run "yui task work update ${item.id} running", `
@@ -2066,6 +2181,7 @@ function dispatchWork(args, store, options) {
2066
2181
  requestedRoles: requestedLaneRoles,
2067
2182
  requestedStrategy,
2068
2183
  existingGroup,
2184
+ resolvedRetryGroup,
2069
2185
  status: item.status,
2070
2186
  nextGroupId: `execution-group-${tx.peekNextAgentRunId(task.id)}`,
2071
2187
  retryLaneId: undefined,
@@ -2088,7 +2204,10 @@ function dispatchWork(args, store, options) {
2088
2204
  if (requestedCount > available) {
2089
2205
  throw usageError(`ExecutionGroup Lane count ${requestedCount} exceeds its ${strategy.mode} capacity ${available}.`);
2090
2206
  }
2091
- if (strategy.mode === "fixed" && !expanding && laneRoles.length !== strategy.count) {
2207
+ if (strategy.mode === "fixed"
2208
+ && !lanePlan.expanding
2209
+ && !lanePlan.resumingQueued
2210
+ && laneRoles.length !== strategy.count) {
2092
2211
  throw usageError(`fixed:${strategy.count} requires exactly ${strategy.count} --lane-role values.`);
2093
2212
  }
2094
2213
  const roles = laneRoles.map((name) => requireRole(tx, task.id, name));
@@ -2103,36 +2222,121 @@ function dispatchWork(args, store, options) {
2103
2222
  });
2104
2223
  let group = existingGroup;
2105
2224
  if (group === undefined) {
2106
- group = createExecutionGroup(`execution-group-${plans[0].runId}`, task.id, {
2225
+ const groupId = `execution-group-${plans[0].runId}`;
2226
+ const stage = !startingExploration && !continuingExploration
2227
+ ? undefined
2228
+ : (() => {
2229
+ const config = tx.getConfig();
2230
+ const contextBudget = resolveContextBudget(config.contextBudget);
2231
+ const health = resolveRuntimeHealth(config.runtimeHealth);
2232
+ const maxWallClockSeconds = requestedStageMaxSeconds
2233
+ ?? Math.floor(health.stallWindowMs / 1_000);
2234
+ const quorum = requestedStageQuorum
2235
+ ?? (strategy.mode === "fixed" ? strategy.count : 1);
2236
+ if (quorum > available) {
2237
+ throw usageError(`Execution stage quorum ${quorum} exceeds Lane capacity ${available}.`);
2238
+ }
2239
+ return planWorkItemExplorationStage(item, {
2240
+ ...(explorationMode === undefined ? {} : { mode: explorationMode }),
2241
+ ...(requestedMaxRounds === undefined ? {} : { maxRounds: requestedMaxRounds }),
2242
+ ...(requestedStageMaxAttempts === undefined
2243
+ ? {}
2244
+ : { maxAttempts: requestedStageMaxAttempts }),
2245
+ ...(startingExploration
2246
+ ? { convergence: { schemaVersion: 1 } }
2247
+ : {}),
2248
+ resourceBudget: {
2249
+ maxTokens: requestedStageMaxTokens ?? contextBudget.hardTokens * available,
2250
+ maxToolCalls: requestedStageMaxToolCalls ?? 100 * available,
2251
+ maxWallClockSeconds
2252
+ },
2253
+ resources: {
2254
+ schemaVersion: 1,
2255
+ quorum,
2256
+ deadlineAt: new Date(now.getTime() + maxWallClockSeconds * 1_000).toISOString(),
2257
+ stragglerAfterSeconds: requestedStageStragglerSeconds
2258
+ ?? Math.floor(health.quietAfterMs / 1_000),
2259
+ minimumMarginalValuePercent: requestedStageMinMarginalValue ?? 10
2260
+ },
2261
+ strategy,
2262
+ contextSnapshotRef: contextSnapshotRef(freezeExecutionStageContextSnapshot(tx, {
2263
+ taskId: task.id,
2264
+ workItemId: item.id,
2265
+ executionGroupId: groupId
2266
+ }, now))
2267
+ });
2268
+ })();
2269
+ group = createExecutionGroup(groupId, task.id, {
2107
2270
  purpose: "execution",
2108
2271
  target: executionTargetForWorkItem(task.id, item.id, item.revision, item, workspace),
2109
2272
  strategy,
2110
- lanes: laneRoles.map((roleName) => ({ roleName }))
2273
+ lanes: laneRoles.map((roleName) => ({ roleName })),
2274
+ ...(stage === undefined ? {} : { stage })
2111
2275
  }, now);
2112
2276
  }
2113
- else if (expanding) {
2277
+ else if (lanePlan.expanding) {
2114
2278
  for (const roleName of laneRoles) {
2115
2279
  group = addExecutionLane(group, { roleName }, now);
2116
2280
  }
2117
2281
  }
2118
- const lanes = expanding
2119
- ? group.lanes.slice(-plans.length)
2120
- : group.lanes.slice(0, plans.length);
2282
+ if (group.stage !== undefined) {
2283
+ assertIndependentVerificationRoles(item, group.stage, laneRoles);
2284
+ }
2285
+ const lanes = lanePlan.resumingQueued
2286
+ ? lanePlan.laneIds.map((laneId) => group.lanes.find(({ id }) => id === laneId))
2287
+ : lanePlan.expanding
2288
+ ? group.lanes.slice(-plans.length)
2289
+ : group.lanes.slice(0, plans.length);
2121
2290
  if (lanes.length !== plans.length)
2122
2291
  throw new Error(`ExecutionGroup Lane planning drift: ${group.id}.`);
2123
2292
  const createdRuns = [];
2293
+ const queuedAdmissions = [];
2124
2294
  let runningGroup = group;
2295
+ const config = tx.getConfig();
2296
+ const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
2297
+ const brokerPolicy = resolveResourceBrokerPolicy({
2298
+ maxActiveLanes: controllerConcurrency,
2299
+ maxActiveLanesPerProvider: controllerConcurrency,
2300
+ maxQueuedLanesPerGroup: Math.max(controllerConcurrency, available)
2301
+ });
2302
+ const activeResources = activeResourceLaneIdentities(tx);
2303
+ const queuedResources = projectQueuedResourceLaneIdentities(tx, now);
2304
+ const stageResources = group.stage === undefined
2305
+ ? undefined
2306
+ : projectExecutionStageResources({
2307
+ group,
2308
+ stageGroups: [...item.executionGroups, group],
2309
+ usage: observedExecutionResourceUsage({
2310
+ group,
2311
+ stageGroups: [...item.executionGroups, group],
2312
+ runs: tx.listAgentRuns(task.id),
2313
+ events: tx.listEvents(task.id)
2314
+ }),
2315
+ now
2316
+ });
2317
+ const stageSpendClosed = stageResources !== undefined
2318
+ && executionStageSpendClosed(stageResources);
2125
2319
  for (let index = 0; index < plans.length; index += 1) {
2126
2320
  const plan = plans[index];
2127
2321
  const lane = lanes[index];
2128
2322
  const laneIsolated = plans.length > 1 || group.lanes.length > 1 || group.strategy.mode === "adaptive";
2323
+ const storedLaneWorkspace = laneIsolated
2324
+ ? tx.getManagedWorkspace({
2325
+ type: "execution-lane",
2326
+ taskId: task.id,
2327
+ executionGroupId: group.id,
2328
+ executionLaneId: lane.id,
2329
+ purpose: "execution",
2330
+ workItemId: item.id
2331
+ }) ?? undefined
2332
+ : undefined;
2129
2333
  const laneManagedWorkspace = laneIsolated
2130
- ? options.executionLaneWorkspaces?.get(lane.id)
2334
+ ? options.executionLaneWorkspaces?.get(lane.id) ?? storedLaneWorkspace
2131
2335
  : runWorkspace;
2132
2336
  if (laneIsolated && laneManagedWorkspace === undefined && options.yuiHome !== undefined) {
2133
2337
  throw usageError(`Execution Lane workspace preflight is missing: ${group.id}/${lane.id}.`);
2134
2338
  }
2135
- const effective = resolveEffectiveLaunch({
2339
+ const effective = lane.effective ?? resolveEffectiveLaunch({
2136
2340
  role: plan.role,
2137
2341
  purpose: "execution",
2138
2342
  ...(laneManagedWorkspace === undefined ? {} : { workspace: laneManagedWorkspace }),
@@ -2146,19 +2350,56 @@ function dispatchWork(args, store, options) {
2146
2350
  root: laneManagedWorkspace.root,
2147
2351
  writableProjectIds: [...item.writeProjectIds]
2148
2352
  };
2353
+ const request = {
2354
+ taskId: task.id,
2355
+ workItemId: item.id,
2356
+ executionGroupId: group.id,
2357
+ executionLaneId: lane.id,
2358
+ providerId: effective.adapterId,
2359
+ agentId: effective.agentId,
2360
+ ...(effective.model === undefined ? {} : { model: effective.model }),
2361
+ requestedAt: lane.effective === undefined ? now.toISOString() : lane.updatedAt
2362
+ };
2363
+ const admission = planResourceAdmissions({
2364
+ policy: brokerPolicy,
2365
+ active: activeResources,
2366
+ queued: queuedResources.filter(({ taskId, executionGroupId, executionLaneId }) => !(taskId === request.taskId
2367
+ && executionGroupId === request.executionGroupId
2368
+ && executionLaneId === request.executionLaneId)),
2369
+ requests: [request]
2370
+ })[0];
2371
+ const frozenDirective = lane.directive ?? rawInput;
2372
+ if (stageSpendClosed || admission.decision !== "admitted") {
2373
+ if (lane.effective === undefined || lane.directive === undefined) {
2374
+ runningGroup = updateExecutionLane(runningGroup, lane.id, {
2375
+ effective,
2376
+ workspace: laneWorkspace,
2377
+ directive: frozenDirective
2378
+ }, now);
2379
+ }
2380
+ queuedAdmissions.push(stageSpendClosed
2381
+ ? `${lane.id}: stage budget/deadline reached`
2382
+ : `${lane.id}: ${admission.reason}`);
2383
+ if (!queuedResources.some(({ taskId, executionGroupId, executionLaneId }) => (taskId === request.taskId
2384
+ && executionGroupId === request.executionGroupId
2385
+ && executionLaneId === request.executionLaneId)))
2386
+ queuedResources.push(request);
2387
+ continue;
2388
+ }
2389
+ activeResources.push(request);
2149
2390
  const runningLane = lane.status === "failed"
2150
- || lane.status === "yielded"
2151
- || lane.status === "completed"
2152
2391
  ? restartExecutionLane(runningGroup, lane.id, {
2153
2392
  runId: plan.runId,
2154
2393
  effective,
2155
- workspace: laneWorkspace
2394
+ workspace: laneWorkspace,
2395
+ directive: frozenDirective
2156
2396
  }, now)
2157
2397
  : updateExecutionLane(runningGroup, lane.id, {
2158
2398
  status: "running",
2159
2399
  runId: plan.runId,
2160
2400
  effective,
2161
- workspace: laneWorkspace
2401
+ workspace: laneWorkspace,
2402
+ directive: frozenDirective
2162
2403
  }, now);
2163
2404
  runningGroup = runningLane;
2164
2405
  const assignment = createRunAssignment({
@@ -2172,7 +2413,16 @@ function dispatchWork(args, store, options) {
2172
2413
  executionGroupId: runningGroup.id,
2173
2414
  executionLaneId: lane.id
2174
2415
  },
2175
- directive: `${rawInput}\nFrozen target: ${group.target.fingerprint}.`,
2416
+ directive: `${frozenDirective}\nFrozen target: ${group.target.fingerprint}.`
2417
+ + (group.stage === undefined
2418
+ ? ""
2419
+ : `\nExploration stage: ${group.stage.stage}; round=${group.stage.round}; attempt=${group.stage.stageAttempt}.`
2420
+ + (group.stage.resources === undefined
2421
+ ? ""
2422
+ : `\nResource budget: tokens=${group.stage.budget.maxTokens}; tools=${group.stage.budget.maxToolCalls}; wall=${group.stage.budget.maxWallClockSeconds}s; quorum=${group.stage.resources.quorum}; deadline=${group.stage.resources.deadlineAt}; straggler=${group.stage.resources.stragglerAfterSeconds}s.`)
2423
+ + (group.stage.convergence === undefined
2424
+ ? ""
2425
+ : `\n${candidateConvergenceDirective(group.stage)}`)),
2176
2426
  deltaRefIds: []
2177
2427
  });
2178
2428
  createdRuns.push(createAgentRun(plan.runId, task.id, plan.role.name, dispatchMode, assignment, now, {
@@ -2237,10 +2487,10 @@ function dispatchWork(args, store, options) {
2237
2487
  roleName: unboundRun.roleName,
2238
2488
  purpose: "execution",
2239
2489
  workItemId: item.id
2240
- }, now);
2490
+ }, now, "controller", runningGroup.stage?.contextSnapshotRef);
2241
2491
  const runWithLineage = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
2242
2492
  createdRuns[index] = runWithLineage;
2243
- const role = plans[index].role;
2493
+ const role = requireRole(tx, task.id, unboundRun.roleName);
2244
2494
  tx.saveAgentRun(runWithLineage);
2245
2495
  tx.saveActiveAgentRun(runWithLineage);
2246
2496
  tx.saveRole(task.id, updateRoleStatus(role, "running", now));
@@ -2250,22 +2500,29 @@ function dispatchWork(args, store, options) {
2250
2500
  ]);
2251
2501
  recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(runWithLineage), now);
2252
2502
  }
2253
- return createdRuns;
2503
+ return { runs: createdRuns, queuedAdmissions, group: runningGroup };
2254
2504
  });
2255
- for (const run of runs)
2505
+ for (const run of dispatch.runs) {
2256
2506
  notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
2257
- const first = runs[0];
2258
- return `Dispatch queued for ${first.taskId}/${first.roleName} (${first.id})${runs.length > 1 ? `; ${runs.length} Lanes` : ""}\n`;
2507
+ }
2508
+ const backpressure = dispatch.queuedAdmissions.length === 0
2509
+ ? ""
2510
+ : `; ${dispatch.queuedAdmissions.length} Lane(s) retained pending by Resource Broker`;
2511
+ const first = dispatch.runs[0];
2512
+ return first === undefined
2513
+ ? `No Lane admitted; ${dispatch.queuedAdmissions.join("; ")}\n`
2514
+ : `Dispatch queued for ${first.taskId}/${first.roleName} (${first.id})${dispatch.runs.length > 1 ? `; ${dispatch.runs.length} Lanes` : ""}${backpressure}\n`;
2259
2515
  }
2260
2516
  function resolveWorkExecutionGroup(args, store, options) {
2261
- const usage = "Task work group resolve usage: yui task work group resolve <task>/<work> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...].";
2517
+ const usage = "Task work group resolve usage: yui task work group resolve <task>/<work> --decision <accept|reject|retry|blocked> --summary <text> [--lane <lane-id> ...] [--early-stop <marginal-value-percent>].";
2262
2518
  if (args[0] !== "resolve")
2263
2519
  throw usageError(usage);
2264
- const parsed = parseMultiValueTail(args.slice(1), new Set(["--decision", "--summary"]), new Set(["--lane"]), usage);
2520
+ const parsed = parseMultiValueTail(args.slice(1), new Set(["--decision", "--summary", "--early-stop"]), new Set(["--lane"]), usage);
2265
2521
  exactPositionals(parsed.positionals, 1, usage);
2266
2522
  const decision = parseExecutionResolutionDecision(parsed.options.get("--decision"), usage);
2267
2523
  const summary = requiredOption(parsed.options, "--summary");
2268
2524
  const selectedLaneIds = parsed.multiOptions.get("--lane");
2525
+ const earlyStopMarginalValue = parseOptionalPercentage(parsed.options.get("--early-stop"), "--early-stop", usage);
2269
2526
  const now = clock(options);
2270
2527
  const result = store.transaction((tx) => {
2271
2528
  const item = requireWorkItem(tx, parsed.positionals[0], options);
@@ -2275,19 +2532,73 @@ function resolveWorkExecutionGroup(args, store, options) {
2275
2532
  if (taskActor(options, task.id) !== "leader") {
2276
2533
  throw usageError("Only the Task Leader may resolve an ExecutionGroup.");
2277
2534
  }
2278
- const group = currentWorkItemExecutionGroup(item);
2535
+ let group = currentWorkItemExecutionGroup(item);
2279
2536
  if (group === undefined
2280
- || (group.lanes.length < 2 && group.strategy.mode !== "adaptive")) {
2537
+ || (group.stage === undefined
2538
+ && group.lanes.length < 2
2539
+ && group.strategy.mode !== "adaptive")) {
2281
2540
  throw usageError(`Work Item ${item.id} has no resolvable ExecutionGroup.`);
2282
2541
  }
2283
2542
  if (item.status !== "running") {
2284
2543
  throw usageError(`Work Item ${item.id} cannot resolve from ${item.status}.`);
2285
2544
  }
2545
+ if (decision !== "accept" && group.stage?.resources !== undefined) {
2546
+ const activeLaneIds = group.lanes
2547
+ .filter(({ status }) => status === "running")
2548
+ .map(({ id }) => id);
2549
+ if (activeLaneIds.length > 0) {
2550
+ throw usageError(`Active Lanes must settle before ${decision}; Resource Broker will not kill them: ${activeLaneIds.join(", ")}.`);
2551
+ }
2552
+ group = skipPendingExecutionLanes(group, group.lanes.filter(({ status }) => status === "pending").map(({ id }) => id), `Leader resolved the stage as ${decision} before queued Lanes started.`, now);
2553
+ }
2286
2554
  const acceptedLaneIds = decision === "accept"
2287
2555
  ? selectedLaneIds ?? group.lanes
2288
2556
  .filter((lane) => lane.status === "yielded" || lane.status === "completed")
2289
2557
  .map(({ id }) => id)
2290
2558
  : undefined;
2559
+ if (earlyStopMarginalValue !== undefined) {
2560
+ if (decision !== "accept" || acceptedLaneIds === undefined) {
2561
+ throw usageError("--early-stop requires --decision accept.");
2562
+ }
2563
+ if (group.stage?.resources === undefined) {
2564
+ throw usageError("--early-stop requires a T6 resource-scheduled exploration stage.");
2565
+ }
2566
+ const resources = projectExecutionStageResources({
2567
+ group,
2568
+ stageGroups: item.executionGroups,
2569
+ usage: observedExecutionResourceUsage({
2570
+ group,
2571
+ stageGroups: item.executionGroups,
2572
+ runs: tx.listAgentRuns(task.id),
2573
+ events: tx.listEvents(task.id)
2574
+ }),
2575
+ now
2576
+ });
2577
+ const routing = routeExecutionStage({
2578
+ group,
2579
+ resources,
2580
+ evidenceSufficient: candidateConvergenceEvidenceSufficient(item, group, acceptedLaneIds),
2581
+ disagreement: "unknown",
2582
+ marginalValuePercent: earlyStopMarginalValue
2583
+ });
2584
+ if (!routing.earlyTerminationAllowed) {
2585
+ throw usageError("Early termination is forbidden until quorum and acceptance-level evidence are sufficient.");
2586
+ }
2587
+ if (routing.action !== "resolve" && routing.retainActiveLaneIds.length === 0) {
2588
+ throw usageError(routing.reason);
2589
+ }
2590
+ group = skipPendingExecutionLanes(group, routing.cancelPendingLaneIds, `Stopped after quorum and sufficient evidence; observed marginal value ${earlyStopMarginalValue}%.`, now);
2591
+ if (routing.retainActiveLaneIds.length > 0) {
2592
+ const waiting = updateWorkItemExecutionGroup(item, group, now);
2593
+ tx.saveWorkItem(task.id, waiting);
2594
+ return {
2595
+ item: waiting,
2596
+ group,
2597
+ reviewDispatch: null,
2598
+ waitingForStragglers: true
2599
+ };
2600
+ }
2601
+ }
2291
2602
  const resolutionSummary = acceptedLaneIds === undefined
2292
2603
  ? summary
2293
2604
  : aggregateExecutionLaneSummary(summary, group, acceptedLaneIds);
@@ -2298,6 +2609,7 @@ function resolveWorkExecutionGroup(args, store, options) {
2298
2609
  ? { selectedLaneIds: acceptedLaneIds }
2299
2610
  : selectedLaneIds === undefined ? {} : { selectedLaneIds })
2300
2611
  }, now);
2612
+ validateCandidateConvergenceResolution(item, resolved);
2301
2613
  const groupedItem = updateWorkItemExecutionGroup(item, resolved, now);
2302
2614
  tx.saveWorkItem(task.id, groupedItem);
2303
2615
  if (decision !== "accept") {
@@ -2306,7 +2618,23 @@ function resolveWorkExecutionGroup(args, store, options) {
2306
2618
  enqueueWork(tx, leaderMailbox(task.id), "work-group-resolved", now, [
2307
2619
  workItemRef(task.id, item.id)
2308
2620
  ]);
2309
- return { item: failed, group: resolved, reviewDispatch: null };
2621
+ return {
2622
+ item: failed,
2623
+ group: resolved,
2624
+ reviewDispatch: null,
2625
+ waitingForStragglers: false
2626
+ };
2627
+ }
2628
+ if (resolved.stage !== undefined && resolved.stage.stage !== "resolve") {
2629
+ enqueueWork(tx, leaderMailbox(task.id), "work-group-resolved", now, [
2630
+ workItemRef(task.id, item.id)
2631
+ ]);
2632
+ return {
2633
+ item: groupedItem,
2634
+ group: resolved,
2635
+ reviewDispatch: null,
2636
+ waitingForStragglers: false
2637
+ };
2310
2638
  }
2311
2639
  const eligible = resolved.lanes
2312
2640
  .filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
@@ -2346,8 +2674,16 @@ function resolveWorkExecutionGroup(args, store, options) {
2346
2674
  runRef(task.id, eligible.id),
2347
2675
  workItemRef(task.id, item.id)
2348
2676
  ]);
2349
- return { item: candidate, group: resolved, reviewDispatch };
2677
+ return {
2678
+ item: candidate,
2679
+ group: resolved,
2680
+ reviewDispatch,
2681
+ waitingForStragglers: false
2682
+ };
2350
2683
  });
2684
+ if (result.waitingForStragglers) {
2685
+ return output(`Stopped pending spend for ${result.group.id}; active stragglers are retained until they settle: ${result.group.lanes.filter(({ status }) => status === "running").map(({ id }) => id).join(", ")}\n`, { workItem: result.item, executionGroup: result.group });
2686
+ }
2351
2687
  if (result.reviewDispatch?.run !== null && result.reviewDispatch?.run !== undefined) {
2352
2688
  notifyReviewMailbox(options, options.runtime, roleMailbox(result.reviewDispatch.run.taskId, result.reviewDispatch.run.roleName), result.reviewDispatch.run.taskId);
2353
2689
  }
@@ -2728,7 +3064,12 @@ function reviewWork(args, store, options) {
2728
3064
  && round.candidateId === candidate.id
2729
3065
  && (round.status === "pending" || round.status === "running")))).at(-1);
2730
3066
  if (activeRound !== undefined) {
2731
- if (activeRound.status === "pending" && activeRound.reviewerRunId === undefined) {
3067
+ const resourceQueued = activeRound.executionGroup !== undefined
3068
+ && activeRound.executionGroup.resolution === undefined
3069
+ && activeRound.executionGroup.lanes.some((lane) => (lane.status === "pending"
3070
+ && lane.runId === undefined));
3071
+ if ((activeRound.status === "pending" && activeRound.reviewerRunId === undefined)
3072
+ || (activeRound.status === "running" && resourceQueued)) {
2732
3073
  return { round: activeRound, run: null, resumed: true };
2733
3074
  }
2734
3075
  throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
@@ -2740,7 +3081,7 @@ function reviewWork(args, store, options) {
2740
3081
  notifyReviewMailbox(options, options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
2741
3082
  }
2742
3083
  if (result.resumed) {
2743
- return output(`Review request ${result.round.id} is pending; resuming dispatch.\n`, { reviewRound: result.round });
3084
+ return output(`Review request ${result.round.id} has pending Lanes; resuming dispatch.\n`, { reviewRound: result.round });
2744
3085
  }
2745
3086
  return result.round.status === "failed"
2746
3087
  ? output(`Review could not start for ${result.round.workItemId}: ${result.round.summary}\n`, { reviewRound: result.round })
@@ -3206,6 +3547,10 @@ function requestTaskReviewRound(args, store, options) {
3206
3547
  }
3207
3548
  assertNoConflictingTaskReviewRound(taskRounds, exact.id);
3208
3549
  if (requestedLaneRoles.length === 0) {
3550
+ if (exact.status === "running"
3551
+ && exact.executionGroup?.resolution === undefined
3552
+ && exact.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.runId === undefined)))
3553
+ return exact;
3209
3554
  assertTaskReviewRequestLane(tx, task.id, reviewerRoleName, exact);
3210
3555
  return exact;
3211
3556
  }
@@ -4022,6 +4367,15 @@ function retryRun(args, store, options) {
4022
4367
  const task = requireTask(tx, previous.taskId);
4023
4368
  if (task.status !== "active")
4024
4369
  throw usageError(`Task is not active: ${task.id}.`);
4370
+ if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
4371
+ taskId: previous.taskId,
4372
+ roleName: previous.roleName,
4373
+ runId: previous.id,
4374
+ agentId: previous.effective.agentId
4375
+ })) {
4376
+ throw usageError(`Run ${previous.id} still owns an unsettled Provider continuation; `
4377
+ + "reconcile it before retrying the Lane.");
4378
+ }
4025
4379
  const role = requireRole(tx, task.id, previous.roleName);
4026
4380
  if (tx.getActiveAgentRun(task.id, role.name) !== null) {
4027
4381
  throw usageError(`${task.id}/${role.name} already has an active run.`);
@@ -4041,10 +4395,21 @@ function retryRun(args, store, options) {
4041
4395
  const currentRetryGroup = retryItem === null
4042
4396
  ? undefined
4043
4397
  : currentWorkItemExecutionGroup(retryItem);
4398
+ const retriesExecutionLane = previous.executionGroupId !== undefined;
4399
+ const exactCurrentLane = !retriesExecutionLane || (retryItem !== null
4400
+ && currentRetryGroup !== undefined
4401
+ && currentRetryGroup.id === previous.executionGroupId
4402
+ && currentRetryGroup.resolution === undefined
4403
+ && retryLaneBefore !== undefined
4404
+ && retryLaneBefore.id === previous.executionLaneId
4405
+ && retryLaneBefore.status === "failed"
4406
+ && retryLaneBefore.runId === previous.id);
4407
+ if (!exactCurrentLane) {
4408
+ throw usageError(`Run ${previous.id} no longer owns the current failed Execution Lane.`);
4409
+ }
4044
4410
  const groupedRunningRetry = retryItem?.status === "running"
4045
- && currentRetryGroup?.id === previous.executionGroupId
4046
- && (currentRetryGroup?.lanes.length ?? 0) > 1
4047
- && retryLaneBefore?.status === "failed";
4411
+ && retriesExecutionLane
4412
+ && exactCurrentLane;
4048
4413
  if (retryItem !== null && retryItem.status !== "failed" && !groupedRunningRetry) {
4049
4414
  throw usageError(`Work Item ${retryItem.id} is not retryable from ${retryItem.status}.`);
4050
4415
  }
@@ -4053,7 +4418,6 @@ function retryRun(args, store, options) {
4053
4418
  ? tx.getTaskWorkspace(task.id)
4054
4419
  : tx.getWorkItemWorkspace(task.id, retryItem.id))
4055
4420
  ?? undefined;
4056
- const runId = tx.nextAgentRunId(task.id);
4057
4421
  const retryGroup = retryItem === null || previous.executionGroupId === undefined
4058
4422
  ? undefined
4059
4423
  : workItemExecutionGroupById(retryItem, previous.executionGroupId);
@@ -4077,20 +4441,81 @@ function retryRun(args, store, options) {
4077
4441
  ...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
4078
4442
  ...(retryItem === null ? {} : { workItemWriteProjectIds: retryItem.writeProjectIds })
4079
4443
  });
4444
+ const laneWorkspace = retryManagedWorkspace === undefined
4445
+ ? undefined
4446
+ : {
4447
+ root: retryManagedWorkspace.root,
4448
+ writableProjectIds: [...(retryItem?.writeProjectIds ?? [])]
4449
+ };
4450
+ const retryDirective = retryLane?.directive
4451
+ ?? previous.assignment.directive
4452
+ ?? retryItem?.objective
4453
+ ?? `Retry Run ${previous.id}.`;
4454
+ let queuedReason;
4455
+ if (retryGroup !== undefined && retryLane !== undefined && retryItem !== null) {
4456
+ const config = tx.getConfig();
4457
+ const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
4458
+ const capacity = retryGroup.strategy.mode === "fixed"
4459
+ ? retryGroup.strategy.count
4460
+ : retryGroup.strategy.max;
4461
+ const policy = resolveResourceBrokerPolicy({
4462
+ maxActiveLanes: controllerConcurrency,
4463
+ maxActiveLanesPerProvider: controllerConcurrency,
4464
+ maxQueuedLanesPerGroup: Math.max(controllerConcurrency, capacity)
4465
+ });
4466
+ if (retryGroup.stage !== undefined) {
4467
+ const stageGroups = retryItem.executionGroups;
4468
+ const resources = projectExecutionStageResources({
4469
+ group: retryGroup,
4470
+ stageGroups,
4471
+ usage: observedExecutionResourceUsage({
4472
+ group: retryGroup,
4473
+ stageGroups,
4474
+ runs: tx.listAgentRuns(task.id),
4475
+ events: tx.listEvents(task.id)
4476
+ }),
4477
+ now
4478
+ });
4479
+ if (executionStageSpendClosed(resources)) {
4480
+ throw usageError(`Execution stage budget/deadline is exhausted; resolve Group ${retryGroup.id} instead of retrying.`);
4481
+ }
4482
+ }
4483
+ const admission = planResourceAdmissions({
4484
+ policy,
4485
+ active: activeResourceLaneIdentities(tx),
4486
+ queued: projectQueuedResourceLaneIdentities(tx, now),
4487
+ requests: [{
4488
+ taskId: task.id,
4489
+ workItemId: retryItem.id,
4490
+ executionGroupId: retryGroup.id,
4491
+ executionLaneId: retryLane.id,
4492
+ providerId: effective.adapterId,
4493
+ agentId: effective.agentId,
4494
+ ...(effective.model === undefined ? {} : { model: effective.model }),
4495
+ requestedAt: now.toISOString()
4496
+ }]
4497
+ })[0];
4498
+ if (admission.decision === "blocked") {
4499
+ throw usageError(`Execution Lane retry queue is full: ${admission.reason}.`);
4500
+ }
4501
+ if (admission.decision === "queued")
4502
+ queuedReason = admission.reason;
4503
+ }
4504
+ const runId = queuedReason === undefined ? tx.nextAgentRunId(task.id) : undefined;
4080
4505
  const runningGroup = retryGroup === undefined || retryLane === undefined
4081
4506
  ? undefined
4082
- : restartExecutionLane(retryGroup, retryLane.id, {
4083
- runId,
4084
- effective,
4085
- ...(retryManagedWorkspace === undefined
4086
- ? {}
4087
- : {
4088
- workspace: {
4089
- root: retryManagedWorkspace.root,
4090
- writableProjectIds: [...(retryItem?.writeProjectIds ?? [])]
4091
- }
4092
- })
4093
- }, now);
4507
+ : queuedReason === undefined
4508
+ ? restartExecutionLane(retryGroup, retryLane.id, {
4509
+ runId: runId,
4510
+ effective,
4511
+ ...(laneWorkspace === undefined ? {} : { workspace: laneWorkspace }),
4512
+ directive: retryDirective
4513
+ }, now)
4514
+ : queueExecutionLaneRetry(retryGroup, retryLane.id, {
4515
+ effective,
4516
+ ...(laneWorkspace === undefined ? {} : { workspace: laneWorkspace }),
4517
+ directive: retryDirective
4518
+ }, now);
4094
4519
  // Restart the bound lane and reopen the failed WorkItem as two ordered
4095
4520
  // single-step record revisions, matching the dispatch path. Folding both
4096
4521
  // transforms into one save would move the WorkItem two revisions at once,
@@ -4111,14 +4536,23 @@ function retryRun(args, store, options) {
4111
4536
  tx.saveWorkItem(task.id, retriedItemWithGroup);
4112
4537
  }
4113
4538
  }
4539
+ if (queuedReason !== undefined) {
4540
+ return {
4541
+ kind: "queued",
4542
+ taskId: task.id,
4543
+ roleName: role.name,
4544
+ laneId: retryLane.id,
4545
+ reason: queuedReason
4546
+ };
4547
+ }
4114
4548
  const retrySnapshot = freezeRunContextSnapshot(tx, {
4115
4549
  taskId: task.id,
4116
4550
  roleName: role.name,
4117
4551
  purpose: "execution",
4118
4552
  ...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
4119
- }, now);
4553
+ }, now, "controller", retryGroup?.stage?.contextSnapshotRef);
4120
4554
  const assignment = createRunAssignment({
4121
- runId,
4555
+ runId: runId,
4122
4556
  roleName: role.name,
4123
4557
  purpose: previous.assignment.purpose,
4124
4558
  action: previous.workItemId === undefined ? previous.assignment.action : "repair-work-item",
@@ -4164,6 +4598,9 @@ function retryRun(args, store, options) {
4164
4598
  }, now);
4165
4599
  return { kind: "run", run: created };
4166
4600
  });
4601
+ if (retried.kind === "queued") {
4602
+ return output(`Retry retained pending for ${retried.taskId}/${retried.laneId}: ${retried.reason}\n`);
4603
+ }
4167
4604
  notifyMailbox(options.runtime, roleMailbox(retried.run.taskId, retried.run.roleName), retried.run.taskId);
4168
4605
  return output(`Retry queued as ${retried.run.id} for ${retried.run.taskId}/${retried.run.roleName}\n`);
4169
4606
  }
@@ -4504,6 +4941,15 @@ function retryFailedReviewRun(previous, store, options, now) {
4504
4941
  const task = requireTask(tx, run.taskId);
4505
4942
  if (task.status !== "active")
4506
4943
  throw usageError(`Task is not active: ${task.id}.`);
4944
+ if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
4945
+ taskId: run.taskId,
4946
+ roleName: run.roleName,
4947
+ runId: run.id,
4948
+ agentId: run.effective.agentId
4949
+ })) {
4950
+ throw usageError(`Review Run ${run.id} still owns an unsettled Provider continuation; `
4951
+ + "reconcile it before retrying the Lane.");
4952
+ }
4507
4953
  if (taskActor(options, task.id) !== "leader") {
4508
4954
  throw usageError("Only the Task Leader may retry a failed final review Run.");
4509
4955
  }
@@ -4515,6 +4961,25 @@ function retryFailedReviewRun(previous, store, options, now) {
4515
4961
  throw usageError(`Review Run ${run.id} is not a Task-final review; request a new WorkItem review `
4516
4962
  + "for a new Candidate.");
4517
4963
  }
4964
+ const retryLane = run.executionLaneId === undefined
4965
+ ? undefined
4966
+ : round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
4967
+ if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
4968
+ || retryLane === undefined
4969
+ || retryLane.runId !== run.id
4970
+ || retryLane.roleName !== run.roleName)) {
4971
+ throw usageError(`Review Run ${run.id} no longer owns its exact Review Lane attempt.`);
4972
+ }
4973
+ const panelGroup = round.executionGroup !== undefined
4974
+ && (round.executionGroup.lanes.length > 1
4975
+ || round.executionGroup.strategy.mode === "adaptive");
4976
+ const runningPanelLaneRetry = round.status === "running"
4977
+ && panelGroup
4978
+ && retryLane?.status === "failed";
4979
+ if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
4980
+ throw usageError(`Review Run ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
4981
+ }
4982
+ const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
4518
4983
  const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
4519
4984
  if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
4520
4985
  throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
@@ -4539,23 +5004,23 @@ function retryFailedReviewRun(previous, store, options, now) {
4539
5004
  if (laterRound !== undefined && !isSameTaskReviewCandidate(laterRound.taskCandidate, round.taskCandidate)) {
4540
5005
  throw usageError(`A newer final Task candidate already has ReviewRound ${laterRound.id}.`);
4541
5006
  }
4542
- const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === round.reviewerRoleName)));
5007
+ const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === retryReviewerRoleName)));
4543
5008
  assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id);
4544
5009
  const activeRound = allReviewerRounds.find((entry) => (entry.id !== round.id
4545
5010
  && (entry.status === "pending" || entry.status === "running")));
4546
5011
  if (activeRound !== undefined) {
4547
5012
  throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
4548
5013
  }
4549
- const reviewerMailboxTarget = roleMailbox(task.id, round.reviewerRoleName);
5014
+ const reviewerMailboxTarget = roleMailbox(task.id, retryReviewerRoleName);
4550
5015
  const reviewerMailbox = tx.getWorkMailbox(reviewerMailboxTarget);
4551
5016
  const reviewerPending = reviewerMailbox === null ? null : nextPendingBatch(reviewerMailbox);
4552
5017
  const runtimeMailbox = tx.getWorkMailbox(runtimeLifecycleTarget({
4553
5018
  scope: "task",
4554
5019
  taskId: task.id,
4555
- roleName: round.reviewerRoleName
5020
+ roleName: retryReviewerRoleName
4556
5021
  }));
4557
5022
  const hasMailboxWork = (mailbox) => (mailbox !== null && workMailboxHasWork(mailbox));
4558
- const reviewer = requireRole(tx, task.id, round.reviewerRoleName);
5023
+ const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
4559
5024
  const activePointer = tx.getActiveAgentRun(task.id, reviewer.name);
4560
5025
  const activeReviewerRuns = tx.listAgentRuns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
4561
5026
  // Issue 06: a completed same-Round retry is a no-write idempotent result.
@@ -4571,7 +5036,7 @@ function retryFailedReviewRun(previous, store, options, now) {
4571
5036
  // Issue 06: a running same Round is reusable only with its exact active
4572
5037
  // Run and mailbox execution. A stranded Run (no active pointer) falls
4573
5038
  // through and resets the Round after the identity fences below.
4574
- if (round.status === "running") {
5039
+ if (round.status === "running" && !runningPanelLaneRetry) {
4575
5040
  const reviewerRunId = round.reviewerRunId;
4576
5041
  const activeMatches = reviewerRunId !== undefined
4577
5042
  && activePointer !== null
@@ -4640,6 +5105,16 @@ function retryFailedReviewRun(previous, store, options, now) {
4640
5105
  }
4641
5106
  settleExactWorkExecution(tx, reviewerMailboxTarget, exactOldRunRef);
4642
5107
  }
5108
+ if (runningPanelLaneRetry) {
5109
+ const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id, now);
5110
+ tx.saveReviewRound(task.id, resetRound);
5111
+ recordTaskEvent(tx, task.id, "run.review-retried", {
5112
+ runId: run.id,
5113
+ reviewRoundId: round.id,
5114
+ executionLaneId: retryLane.id
5115
+ }, now);
5116
+ return { round: resetRound, previousRun: run, created: true };
5117
+ }
4643
5118
  // Terminalize the old stranded Round only after every identity and mailbox
4644
5119
  // fence has passed. The outer transaction rolls back if Round creation fails.
4645
5120
  let roundToReset = round;
@@ -4657,7 +5132,7 @@ function retryFailedReviewRun(previous, store, options, now) {
4657
5132
  // Issue 06: infra retry resets the same semantic Round to pending instead
4658
5133
  // of manufacturing a new Round, so Round count and finding identity stay
4659
5134
  // stable across execution-attempt failures.
4660
- const resetRound = retryTaskReviewRound(roundToReset);
5135
+ const resetRound = retryTaskReviewRound(roundToReset, run.executionLaneId);
4661
5136
  tx.saveReviewRound(task.id, resetRound);
4662
5137
  recordTaskEvent(tx, task.id, "run.review-retried", {
4663
5138
  runId: run.id,
@@ -5121,12 +5596,13 @@ function yieldRun(args, store, options) {
5121
5596
  throw dataError(`Work item not found for run ${active.id}: ${active.workItemId}.`);
5122
5597
  const currentGroup = currentWorkItemExecutionGroup(item);
5123
5598
  const multiLaneGroup = currentGroup !== undefined
5124
- && (currentGroup.lanes.length > 1
5599
+ && (currentGroup.stage !== undefined
5600
+ || currentGroup.lanes.length > 1
5125
5601
  || currentGroup.strategy.mode === "adaptive")
5126
5602
  && currentGroup.resolution === undefined;
5127
5603
  if (multiLaneGroup) {
5128
5604
  executionGroupPending = true;
5129
- executionGroupReady = currentGroup.lanes.every(({ status }) => (["yielded", "completed", "failed"].includes(status)));
5605
+ executionGroupReady = currentGroup.lanes.every(({ status }) => (["yielded", "completed", "failed", "skipped"].includes(status)));
5130
5606
  if (executionGroupReady) {
5131
5607
  recordTaskEvent(tx, task.id, "execution-group-ready", {
5132
5608
  workItemId: item.id,
@@ -5262,7 +5738,7 @@ function leaderYieldReceipt(tx, task, terminal, now) {
5262
5738
  if (projection === null)
5263
5739
  return null;
5264
5740
  const disposition = deriveLeaderRunDisposition(projection.status, task.status);
5265
- const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id));
5741
+ const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id, now));
5266
5742
  const waitReason = disposition === "waiting" || disposition === "blocked"
5267
5743
  ? leaderWaitReason(projection)
5268
5744
  : undefined;
@@ -5465,13 +5941,6 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5465
5941
  }
5466
5942
  throw roleNotFound(round.reviewerRoleName);
5467
5943
  }
5468
- if (tx.getActiveAgentRun(taskId, reviewer.name) !== null) {
5469
- const message = `Reviewer Role already has an active run: ${reviewer.name}.`;
5470
- if ((round.scope ?? "work-item") === "task") {
5471
- throw new TaskFinalReviewDispatchDriftError(message);
5472
- }
5473
- throw usageError(message);
5474
- }
5475
5944
  const storedWorkspace = tx.getReviewRoundWorkspace(taskId, round.id);
5476
5945
  if (storedWorkspace === null
5477
5946
  || !isDeepStrictEqual(storedWorkspace, round.workspace)
@@ -5603,17 +6072,35 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5603
6072
  return laneReviewer;
5604
6073
  });
5605
6074
  const createdRuns = [];
6075
+ const config = tx.getConfig();
6076
+ const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
6077
+ const capacity = runningGroup.strategy.mode === "fixed"
6078
+ ? runningGroup.strategy.count
6079
+ : runningGroup.strategy.max;
6080
+ const brokerPolicy = resolveResourceBrokerPolicy({
6081
+ maxActiveLanes: controllerConcurrency,
6082
+ maxActiveLanesPerProvider: controllerConcurrency,
6083
+ maxQueuedLanesPerGroup: Math.max(controllerConcurrency, capacity)
6084
+ });
6085
+ const activeResources = activeResourceLaneIdentities(tx);
6086
+ const queuedResources = projectQueuedResourceLaneIdentities(tx, now);
6087
+ const reviewBaseline = dispatchLanes.length === 0
6088
+ ? undefined
6089
+ : freezeReviewStageContextSnapshot(tx, {
6090
+ taskId,
6091
+ reviewRoundId: round.id,
6092
+ executionGroupId: runningGroup.id
6093
+ }, now);
5606
6094
  for (let index = 0; index < reviewers.length; index += 1) {
5607
6095
  const lane = dispatchLanes[index];
5608
6096
  const laneReviewer = reviewers[index];
5609
- const runId = tx.nextAgentRunId(taskId);
5610
6097
  const laneManagedWorkspace = runningGroup.lanes.length > 1 || runningGroup.strategy.mode === "adaptive"
5611
6098
  ? options.executionLaneWorkspaces?.get(lane.id)
5612
6099
  : round.workspace;
5613
6100
  if (laneManagedWorkspace === undefined && options.yuiHome !== undefined) {
5614
6101
  throw usageError(`Review Lane workspace preflight is missing: ${runningGroup.id}/${lane.id}.`);
5615
6102
  }
5616
- const effective = resolveEffectiveLaunch({
6103
+ const effective = lane.effective ?? resolveEffectiveLaunch({
5617
6104
  role: laneReviewer,
5618
6105
  purpose: "review",
5619
6106
  workspace: laneManagedWorkspace ?? round.workspace,
@@ -5629,6 +6116,40 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5629
6116
  .filter(({ access }) => access === "write")
5630
6117
  .map(({ projectId }) => projectId)
5631
6118
  };
6119
+ const request = {
6120
+ taskId,
6121
+ ...(item === undefined ? {} : { workItemId: item.id }),
6122
+ executionGroupId: runningGroup.id,
6123
+ executionLaneId: lane.id,
6124
+ providerId: effective.adapterId,
6125
+ agentId: effective.agentId,
6126
+ ...(effective.model === undefined ? {} : { model: effective.model }),
6127
+ requestedAt: lane.effective === undefined ? now.toISOString() : lane.updatedAt
6128
+ };
6129
+ const admission = planResourceAdmissions({
6130
+ policy: brokerPolicy,
6131
+ active: activeResources,
6132
+ queued: queuedResources.filter(({ taskId: queuedTaskId, executionGroupId, executionLaneId }) => !(queuedTaskId === request.taskId
6133
+ && executionGroupId === request.executionGroupId
6134
+ && executionLaneId === request.executionLaneId)),
6135
+ requests: [request]
6136
+ })[0];
6137
+ if (admission.decision !== "admitted") {
6138
+ if (lane.effective === undefined) {
6139
+ runningGroup = updateExecutionLane(runningGroup, lane.id, {
6140
+ reviewRoundId: round.id,
6141
+ effective,
6142
+ workspace: laneWorkspace
6143
+ }, now);
6144
+ }
6145
+ if (!queuedResources.some(({ taskId: queuedTaskId, executionGroupId, executionLaneId }) => (queuedTaskId === request.taskId
6146
+ && executionGroupId === request.executionGroupId
6147
+ && executionLaneId === request.executionLaneId)))
6148
+ queuedResources.push(request);
6149
+ continue;
6150
+ }
6151
+ activeResources.push(request);
6152
+ const runId = tx.nextAgentRunId(taskId);
5632
6153
  const assignment = createRunAssignment({
5633
6154
  runId,
5634
6155
  roleName: laneReviewer.name,
@@ -5664,7 +6185,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5664
6185
  const roundWithGroup = round.executionGroup === undefined
5665
6186
  ? attachReviewExecutionGroup(round, runningGroup)
5666
6187
  : updateReviewExecutionGroup(round, runningGroup);
5667
- const persistedRound = round.status === "pending"
6188
+ const persistedRound = round.status === "pending" && createdRuns.length > 0
5668
6189
  ? startReviewRound(roundWithGroup, createdRuns[0].id)
5669
6190
  : roundWithGroup;
5670
6191
  // Save the aggregate before adopting prepared Lane workspaces. The
@@ -5691,10 +6212,12 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5691
6212
  purpose: "review",
5692
6213
  ...(item === undefined ? {} : { workItemId: item.id }),
5693
6214
  reviewRoundId: round.id
5694
- }, now);
6215
+ }, now, "controller", reviewBaseline === undefined
6216
+ ? undefined
6217
+ : contextSnapshotRef(reviewBaseline));
5695
6218
  const created = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
5696
6219
  createdRuns[index] = created;
5697
- const laneReviewer = reviewers[index];
6220
+ const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
5698
6221
  tx.saveAgentRun(created);
5699
6222
  tx.saveActiveAgentRun(created);
5700
6223
  tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
@@ -5709,7 +6232,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5709
6232
  for (const run of runs) {
5710
6233
  notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
5711
6234
  }
5712
- return runs[0];
6235
+ return runs[0] ?? null;
5713
6236
  }
5714
6237
  export function failPendingReviewRound(taskId, reviewRoundId, summary, store, options = {}) {
5715
6238
  const now = clock(options);
@@ -6730,6 +7253,32 @@ function parseExecutionStrategy(value, usage) {
6730
7253
  }
6731
7254
  throw usageError(`Invalid execution strategy: ${value}. Use fixed:<count> or adaptive:<max>.`, usage);
6732
7255
  }
7256
+ function parseWorkItemExecutionMode(value, usage) {
7257
+ if (value === undefined)
7258
+ return undefined;
7259
+ const normalized = value.trim().toLowerCase();
7260
+ if (WORK_ITEM_EXECUTION_MODES.includes(normalized))
7261
+ return normalized;
7262
+ throw usageError(`Invalid WorkItem execution mode: ${value}. Use ${WORK_ITEM_EXECUTION_MODES.join(", ")}.`, usage);
7263
+ }
7264
+ function parseOptionalPositiveInteger(value, label, usage) {
7265
+ if (value === undefined)
7266
+ return undefined;
7267
+ const parsed = Number(value);
7268
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
7269
+ throw usageError(`${label} must be a positive integer.`, usage);
7270
+ }
7271
+ return parsed;
7272
+ }
7273
+ function parseOptionalPercentage(value, label, usage) {
7274
+ if (value === undefined)
7275
+ return undefined;
7276
+ const parsed = Number(value);
7277
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 100) {
7278
+ throw usageError(`${label} must be an integer from 0 to 100.`, usage);
7279
+ }
7280
+ return parsed;
7281
+ }
6733
7282
  function sameExecutionStrategy(left, right) {
6734
7283
  return left.mode === right.mode
6735
7284
  && (left.mode === "fixed"
@@ -6737,9 +7286,10 @@ function sameExecutionStrategy(left, right) {
6737
7286
  : right.mode === "adaptive" && left.max === right.max);
6738
7287
  }
6739
7288
  function parseExecutionResolutionDecision(value, usage) {
6740
- if (value === "accept" || value === "reject" || value === "blocked")
7289
+ if (value === "accept" || value === "reject" || value === "retry" || value === "blocked") {
6741
7290
  return value;
6742
- throw usageError("--decision must be accept, reject, or blocked.", usage);
7291
+ }
7292
+ throw usageError("--decision must be accept, reject, retry, or blocked.", usage);
6743
7293
  }
6744
7294
  function trimmed(value) {
6745
7295
  const normalized = value?.trim();