@zq-silk/yui 0.11.3 → 0.12.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 (86) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +36 -13
  3. package/dist/cli/commandCatalog.js +2 -4
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/taskCommands.js +154 -171
  9. package/dist/commands/taskCompletionGate.js +29 -54
  10. package/dist/commands/taskContextCommand.js +55 -11
  11. package/dist/commands/taskInputCommands.js +8 -5
  12. package/dist/commands/taskNextActionCommand.js +56 -6
  13. package/dist/commands/taskOverviewCommand.js +17 -31
  14. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  15. package/dist/commands/taskWorkspaceCommands.js +5 -2
  16. package/dist/config/configCatalog.js +2 -2
  17. package/dist/config/yuiConfig.js +3 -7
  18. package/dist/context/wakeNotification.js +20 -5
  19. package/dist/controller/agentRuntimeObserver.js +247 -51
  20. package/dist/controller/clientRuntime.js +1 -1
  21. package/dist/controller/controller.js +8 -12
  22. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  23. package/dist/controller/runtime.js +3 -3
  24. package/dist/controller/runtimeEventInbox.js +17 -7
  25. package/dist/controller/runtimeEventProcessor.js +12 -19
  26. package/dist/controller/runtimeObservationHook.js +45 -0
  27. package/dist/execution/resourceBroker.js +5 -4
  28. package/dist/executor/agentExecutor.js +4 -4
  29. package/dist/executor/effectiveLaunch.js +8 -48
  30. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  31. package/dist/executor/workspacePreflightClassification.js +23 -2
  32. package/dist/interaction/operatorPresentation.js +33 -89
  33. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  34. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  35. package/dist/observability/orchestrationMetrics.js +8 -3
  36. package/dist/profile/agentProfile.js +1 -1
  37. package/dist/release/cliHomeReleaseFence.js +123 -0
  38. package/dist/repository/taskWorkspacePreparer.js +176 -60
  39. package/dist/resources/sqliteResourceRegistry.js +1 -1
  40. package/dist/review/deltaRecheck.js +12 -51
  41. package/dist/review/reviewAcceptance.js +26 -0
  42. package/dist/review/reviewConfig.js +0 -31
  43. package/dist/review/reviewDecision.js +113 -0
  44. package/dist/review/reviewOutcomeClassifier.js +1 -1
  45. package/dist/review/reviewRound.js +1 -1
  46. package/dist/review/reviewerAvailability.js +69 -0
  47. package/dist/runtime/agentDriverObservation.js +24 -10
  48. package/dist/runtime/builtinAgentDrivers.js +4 -3
  49. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  50. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  51. package/dist/runtime/index.js +1 -0
  52. package/dist/runtime/runtimeObservation.js +13 -0
  53. package/dist/runtime/runtimeProjection.js +16 -25
  54. package/dist/runtime/sessionTokenMetrics.js +181 -0
  55. package/dist/runtime/structuredProviderHost.js +7 -1
  56. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  57. package/dist/scheduler/activeTaskProgress.js +7 -6
  58. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  59. package/dist/scheduler/operatorEvent.js +34 -0
  60. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  61. package/dist/scheduler/roleRunStall.js +0 -14
  62. package/dist/scheduler/taskExecutionProjection.js +97 -76
  63. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  64. package/dist/storage/migration/productionRegistry.js +379 -0
  65. package/dist/storage/sqliteSchema.js +18 -8
  66. package/dist/storage/sqliteStore.js +24 -20
  67. package/dist/storage/storeRpc.js +0 -1
  68. package/dist/storage/taskStore.js +15 -30
  69. package/dist/storage/upgrade/recordVersions.js +3 -2
  70. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  71. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  72. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  73. package/dist/task/completionReadiness.js +10 -6
  74. package/dist/task/nextAction.js +82 -24
  75. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  76. package/dist/web/assets/client/components.js +13 -3
  77. package/dist/web/assets/client/i18n.js +6 -2
  78. package/dist/web/webSnapshot.js +4 -0
  79. package/i18n/README.zh-CN.md +2 -2
  80. package/package.json +1 -1
  81. package/skills/yui-leader/SKILL.md +53 -16
  82. package/skills/yui-operator/SKILL.md +9 -0
  83. package/skills/yui-reviewer/SKILL.md +28 -7
  84. package/dist/context/sessionContextBudget.js +0 -71
  85. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  86. package/dist/scheduler/operatorNotification.js +0 -59
@@ -6,7 +6,7 @@ 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";
8
8
  import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
9
- import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
9
+ import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
10
10
  import { readCommandText } from "./textInput.js";
11
11
  import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
12
12
  import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
@@ -25,6 +25,8 @@ import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProject
25
25
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
26
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
+ import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
29
+ import { projectReviewerAvailability } from "../review/reviewerAvailability.js";
28
30
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
29
31
  import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
30
32
  import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
@@ -36,7 +38,7 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
36
38
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
37
39
  import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
38
40
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
39
- import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
41
+ import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
40
42
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
41
43
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
42
44
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
@@ -44,11 +46,11 @@ import { assertProjectActive, resolveProject } from "../repository/project.js";
44
46
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
45
47
  import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, planWorkItemExplorationStage, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
46
48
  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 { DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE, executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
50
+ import { resolveControllerTaskConcurrency, resolveRuntimeHealth } from "../config/yuiConfig.js";
49
51
  import { assertIndependentVerificationRoles, candidateConvergenceEvidenceSufficient, candidateConvergenceDirective, validateCandidateConvergenceResolution } from "../execution/candidateConvergence.js";
50
52
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
51
- import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
53
+ import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
52
54
  import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
53
55
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
54
56
  import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
@@ -62,7 +64,7 @@ import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCou
62
64
  import { runGrantCommand } from "./grantCommands.js";
63
65
  import { runWorkflowCommand } from "./workflowCommands.js";
64
66
  import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
65
- import { createTaskTerminalNotification } from "../scheduler/operatorNotification.js";
67
+ import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
66
68
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
67
69
  import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
68
70
  import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition, projectQueuedResourceLaneIdentities } from "../scheduler/actionability.js";
@@ -77,8 +79,6 @@ export class TaskFinalReviewDispatchDriftError extends CliError {
77
79
  }
78
80
  }
79
81
  /** Process-local command metadata; symbols never enter JSON output or durable state. */
80
- export const RESUMED_PENDING_FINAL_REVIEW = Symbol("resumed-pending-final-review");
81
- export const TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW = Symbol("terminalized-leader-before-final-review");
82
82
  function taskFinalReviewDispatchDrift(error) {
83
83
  return new TaskFinalReviewDispatchDriftError(error instanceof Error ? error.message : String(error), error instanceof CliError ? error : undefined);
84
84
  }
@@ -235,8 +235,8 @@ export function runTaskCommand(args, store, options = {}) {
235
235
  case "update": return output(updateTaskCommand(rest, store, options));
236
236
  case "list": return listTaskCommand(rest, store);
237
237
  case "show": return showTaskCommand(rest, store);
238
- case "context": return runTaskContextCommand(rest, store);
239
- case "next-action": return runTaskNextActionCommand(rest, store);
238
+ case "context": return runTaskContextCommand(rest, store, options.actualTaskReviewCandidate ?? null);
239
+ case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
240
240
  case "activate": return output(activateTaskCommand(rest, store, options));
241
241
  case "complete": return completeTaskCommand(rest, store, options);
242
242
  case "reopen": return output(reopenTaskCommand(rest, store, options));
@@ -670,27 +670,34 @@ function showTaskCommand(args, store) {
670
670
  }
671
671
  function activateTaskCommand(args, store, options) {
672
672
  exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
673
- const now = clock(options);
673
+ const activation = options.taskWorkspaceActivation;
674
674
  const result = store.transaction((tx) => {
675
675
  const task = requireTask(tx, args[0]);
676
676
  if (task.status === "archived")
677
677
  throw usageError(`Task is archived: ${task.id}.`);
678
+ if (task.status === "retired")
679
+ throw usageError(`Task is retired: ${task.id}.`);
678
680
  if (task.status === "completed") {
679
681
  throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
680
682
  }
681
- if (task.status === "active")
682
- return { task, changed: false };
683
- const active = activateTask(task, now);
684
- tx.saveTask(active);
685
- // Project-backed Tasks keep this durable wake pending until the
686
- // Controller has prepared and recorded the Task workspace.
687
- enqueueWork(tx, leaderMailbox(task.id), "task-created", now, [taskRef(task.id)]);
688
- enqueueWork(tx, taskMailbox(task.id), "task-activated", now, [taskRef(task.id)]);
689
- recordTaskEvent(tx, task.id, "task.activated", {
690
- fromStatus: task.status,
691
- status: active.status
692
- }, now);
693
- return { task: active, changed: true };
683
+ if (task.status === "active") {
684
+ if (activation === undefined)
685
+ return { task, changed: false };
686
+ const workspace = tx.getTaskWorkspace(task.id);
687
+ if (activation.taskId !== task.id
688
+ || activation.task.id !== task.id
689
+ || activation.task.status !== "active"
690
+ || !isDeepStrictEqual(activation.task.workspaceIdentity, task.workspaceIdentity)
691
+ || activation.path !== task.cwd
692
+ || workspace === null
693
+ || workspace.owner.type !== "task"
694
+ || workspace.owner.taskId !== task.id
695
+ || workspace.root !== task.cwd) {
696
+ throw usageError(`Task workspace activation proof does not match ${task.id}.`);
697
+ }
698
+ return { task, changed: activation.changed };
699
+ }
700
+ throw usageError(`Task ${task.id} activation requires atomic workspace adoption through the CLI preflight.`);
694
701
  });
695
702
  if (result.changed) {
696
703
  notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
@@ -754,7 +761,6 @@ function completeTaskCommand(args, store, options) {
754
761
  // readiness via projectCompletionReadiness. The leader-Run check is
755
762
  // actor-dependent and stays here; every other blocker is re-validated by
756
763
  // the post-Review readiness fence below.
757
- let terminalizedLeaderRun = false;
758
764
  const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
759
765
  if (leaderEntry !== undefined) {
760
766
  if (actor !== "leader") {
@@ -777,15 +783,11 @@ function completeTaskCommand(args, store, options) {
777
783
  if (terminal.disposition !== "applied") {
778
784
  throw usageError(`Task Leader Run changed during completion: ${leaderEntry.run.id}/${terminal.reason}.`);
779
785
  }
780
- terminalizedLeaderRun = true;
781
786
  }
782
787
  // A final (Task-scoped) review is the Task delivery policy: it reviews the
783
788
  // complete frozen Task heads, not a single WorkItem Candidate. If the
784
789
  // review config requests a final review, create the Task ReviewRound here
785
790
  // and return it for dispatch instead of completing the Task.
786
- const pendingFinalReviewIds = new Set(tx.listReviewRounds(task.id)
787
- .filter((round) => ((round.scope ?? "work-item") === "task" && round.status === "pending"))
788
- .map(({ id }) => id));
789
791
  const finalReview = prepareFinalTaskReview(tx, task, now, taskFinalContract, options);
790
792
  if (finalReview !== null) {
791
793
  return {
@@ -794,8 +796,6 @@ function completeTaskCommand(args, store, options) {
794
796
  runtimeCleanupTargets: [],
795
797
  completionAdvisories: [],
796
798
  finalReview,
797
- resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
798
- terminalizedLeaderRun,
799
799
  publishedTreeAuthorization: undefined
800
800
  };
801
801
  }
@@ -816,9 +816,6 @@ function completeTaskCommand(args, store, options) {
816
816
  tx.saveTask(completed);
817
817
  tx.clearPendingWakeup(task.id);
818
818
  tx.clearLeaderFailure(task.id);
819
- tx.clearOperatorNotification(task.id);
820
- tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
821
- enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
822
819
  if (publishedTreeProof !== undefined) {
823
820
  recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
824
821
  by: actor,
@@ -832,7 +829,7 @@ function completeTaskCommand(args, store, options) {
832
829
  tree: publishedTreeProof.tree
833
830
  }, now);
834
831
  }
835
- recordTaskEvent(tx, task.id, "task.completed", {
832
+ const terminalEvent = recordTaskEvent(tx, task.id, "task.completed", {
836
833
  by: actor,
837
834
  summary,
838
835
  ...(actualTaskCandidate === undefined
@@ -846,6 +843,7 @@ function completeTaskCommand(args, store, options) {
846
843
  ? {}
847
844
  : { cleanupAdvisories: String(readiness.advisories.length) })
848
845
  }, now);
846
+ enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
849
847
  // A terminal Task must never leave a Task-lane signal that can wake it.
850
848
  // The durable records remain intact; only the derived mailbox work is
851
849
  // discarded at this lifecycle boundary.
@@ -863,8 +861,6 @@ function completeTaskCommand(args, store, options) {
863
861
  runtimeCleanupTargets,
864
862
  completionAdvisories: readiness.advisories,
865
863
  finalReview: undefined,
866
- resumedPendingFinalReview: false,
867
- terminalizedLeaderRun,
868
864
  publishedTreeAuthorization: undefined
869
865
  };
870
866
  });
@@ -893,9 +889,7 @@ function completeTaskCommand(args, store, options) {
893
889
  : `Final Task Review is blocked: ${result.finalReview.summary ?? result.finalReview.id}.`;
894
890
  return output(`${status}\n`, {
895
891
  task: result.task,
896
- reviewRound: result.finalReview,
897
- [RESUMED_PENDING_FINAL_REVIEW]: result.resumedPendingFinalReview,
898
- [TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
892
+ reviewRound: result.finalReview
899
893
  });
900
894
  }
901
895
  const completionOutput = result.changed
@@ -935,7 +929,6 @@ function reopenTaskCommand(args, store, options) {
935
929
  throw usageError(`Task is not completed: ${task.id}.`);
936
930
  const active = reopenTask(task, now);
937
931
  tx.saveTask(active);
938
- tx.clearOperatorNotification(task.id);
939
932
  const reopenedReason = wakeReason("task-reopened");
940
933
  enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
941
934
  enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
@@ -997,7 +990,6 @@ function archiveTaskCommand(args, store, options) {
997
990
  tx.saveTask(archived);
998
991
  tx.clearPendingWakeup(task.id);
999
992
  tx.clearLeaderFailure(task.id);
1000
- tx.clearOperatorNotification(task.id);
1001
993
  for (const role of tx.listRoles(task.id)) {
1002
994
  tx.removeWorkMailbox(roleMailbox(task.id, role.name));
1003
995
  }
@@ -1095,20 +1087,18 @@ function retireTaskCommand(args, store, options) {
1095
1087
  }
1096
1088
  tx.clearPendingWakeup(task.id);
1097
1089
  tx.clearLeaderFailure(task.id);
1098
- tx.clearOperatorNotification(task.id);
1099
1090
  const retired = retireTask(task, {
1100
1091
  by: actor,
1101
1092
  summary,
1102
1093
  ...(replacementTaskId === undefined ? {} : { replacementTaskId })
1103
1094
  }, now);
1104
1095
  tx.saveTask(retired);
1105
- tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "retired", actor, summary, now));
1106
- enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
1107
- recordTaskEvent(tx, task.id, "task.retired", {
1096
+ const terminalEvent = recordTaskEvent(tx, task.id, "task.retired", {
1108
1097
  by: actor,
1109
1098
  summary,
1110
1099
  ...(replacementTaskId === undefined ? {} : { replacementTaskId })
1111
1100
  }, now);
1101
+ enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
1112
1102
  return { task: retired, changed: true };
1113
1103
  });
1114
1104
  if (result.changed) {
@@ -2231,7 +2221,6 @@ function dispatchWork(args, store, options) {
2231
2221
  ? undefined
2232
2222
  : (() => {
2233
2223
  const config = tx.getConfig();
2234
- const contextBudget = resolveContextBudget(config.contextBudget);
2235
2224
  const health = resolveRuntimeHealth(config.runtimeHealth);
2236
2225
  const maxWallClockSeconds = requestedStageMaxSeconds
2237
2226
  ?? Math.floor(health.stallWindowMs / 1_000);
@@ -2250,7 +2239,8 @@ function dispatchWork(args, store, options) {
2250
2239
  ? { convergence: { schemaVersion: 1 } }
2251
2240
  : {}),
2252
2241
  resourceBudget: {
2253
- maxTokens: requestedStageMaxTokens ?? contextBudget.hardTokens * available,
2242
+ maxTokens: requestedStageMaxTokens
2243
+ ?? DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE * available,
2254
2244
  maxToolCalls: requestedStageMaxToolCalls ?? 100 * available,
2255
2245
  maxWallClockSeconds
2256
2246
  },
@@ -3521,12 +3511,6 @@ function requestTaskReviewRound(args, store, options) {
3521
3511
  }
3522
3512
  const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
3523
3513
  if (deltaRecheckRequested) {
3524
- const reviewConfig = tx.getReviewConfig();
3525
- if (reviewConfig === null || reviewConfig.deltaRecheck !== "enabled") {
3526
- throw usageError("Delta-recheck is not enabled for this Project's review policy. "
3527
- + "Set `yui config workflow set review --role <role> --trigger final --delta-recheck enabled` "
3528
- + "or request a full Review.");
3529
- }
3530
3514
  if (taskFinalContract !== undefined) {
3531
3515
  throw usageError("Delta-recheck is not supported with a Task-final review contract.");
3532
3516
  }
@@ -3541,15 +3525,24 @@ function requestTaskReviewRound(args, store, options) {
3541
3525
  }
3542
3526
  const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id))
3543
3527
  .filter((entry) => (entry.scope ?? "work-item") === "task");
3528
+ const latestNonAcceptingDelta = deltaRecheckRequested
3529
+ ? undefined
3530
+ : taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
3531
+ && entry.deltaRecheck !== undefined
3532
+ && deltaRecheckBlocksAcceptance(entry)
3533
+ && isSameTaskReviewCandidate(entry.taskCandidate, provenance.candidate))).at(-1);
3544
3534
  const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
3535
+ && (deltaRecheckRequested
3536
+ ? entry.deltaRecheck !== undefined
3537
+ : entry.deltaRecheck === undefined)
3545
3538
  && (taskFinalContract === undefined || sameTaskFinalReviewContract(entry.taskFinalReviewContract, taskFinalContract))
3546
3539
  && isSameTaskReviewCandidate(entry.taskCandidate, provenance.candidate))).at(-1);
3540
+ if (exact?.status === "failed") {
3541
+ throw usageError(`Explicit Task-final ReviewRound ${exact.id} is failed for this exact candidate; resolve it before requesting again.`);
3542
+ }
3547
3543
  if (exact !== undefined
3548
- && (deltaRecheckRequested || !deltaRecheckBlocksAcceptance(exact))) {
3549
- if (exact.status === "failed") {
3550
- throw usageError(`Explicit Task-final ReviewRound ${exact.id} is failed for this exact candidate; resolve it before requesting again.`);
3551
- }
3552
- assertNoConflictingTaskReviewRound(taskRounds, exact.id);
3544
+ && (exact.status === "pending" || exact.status === "running")) {
3545
+ assertNoConflictingTaskReviewRound(taskRounds, exact.id, reviewerRoleName);
3553
3546
  if (requestedLaneRoles.length === 0) {
3554
3547
  if (exact.status === "running"
3555
3548
  && exact.executionGroup?.resolution === undefined
@@ -3579,9 +3572,8 @@ function requestTaskReviewRound(args, store, options) {
3579
3572
  if (exact.executionGroup.lanes.length + laneRoles.filter((role) => (!exact.executionGroup.lanes.some((lane) => lane.roleName === role))).length > strategy.max) {
3580
3573
  throw usageError(`Reviewer Lane count exceeds adaptive capacity ${strategy.max}.`);
3581
3574
  }
3582
- let appended = exact.executionGroup;
3583
3575
  for (const laneRoleName of laneRoles) {
3584
- const existingLane = appended.lanes.find(({ roleName }) => roleName === laneRoleName);
3576
+ const existingLane = exact.executionGroup.lanes.find(({ roleName }) => (roleName === laneRoleName));
3585
3577
  if (existingLane !== undefined) {
3586
3578
  if (existingLane.runId === undefined)
3587
3579
  continue;
@@ -3601,29 +3593,32 @@ function requestTaskReviewRound(args, store, options) {
3601
3593
  if (taskReviewProducerCollision(provenance, laneRoleName) !== null) {
3602
3594
  throw usageError(`Reviewer Role must be separate from the Candidate producer: ${laneRoleName}.`);
3603
3595
  }
3604
- assertTaskReviewRequestLane(tx, task.id, laneRoleName);
3605
- let laneRole = tx.getRole(task.id, laneRoleName);
3606
- if (laneRole === null) {
3607
- laneRole = createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName);
3608
- tx.saveRole(task.id, laneRole);
3609
- }
3610
- appended = addExecutionLane(appended, {
3611
- roleName: laneRole.name,
3612
- reviewRoundId: exact.id
3613
- }, now);
3596
+ const availability = projectReviewerAvailability(tx, task.id, laneRoleName);
3597
+ if (availability.kind === "busy")
3598
+ return availability;
3599
+ }
3600
+ }
3601
+ let appended = exact.executionGroup;
3602
+ for (const laneRoleName of laneRoles) {
3603
+ if (appended.lanes.some(({ roleName }) => roleName === laneRoleName))
3604
+ continue;
3605
+ let laneRole = tx.getRole(task.id, laneRoleName);
3606
+ if (laneRole === null) {
3607
+ laneRole = createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName);
3608
+ tx.saveRole(task.id, laneRole);
3614
3609
  }
3610
+ appended = addExecutionLane(appended, {
3611
+ roleName: laneRole.name,
3612
+ reviewRoundId: exact.id
3613
+ }, now);
3615
3614
  }
3616
3615
  const updated = updateReviewExecutionGroup(exact, appended);
3617
3616
  tx.saveReviewRound(task.id, updated);
3618
3617
  return updated;
3619
3618
  }
3620
- assertNoConflictingTaskReviewRound(taskRounds);
3621
- assertTaskReviewRequestLane(tx, task.id, reviewerRoleName);
3622
- let reviewer = tx.getRole(task.id, reviewerRoleName);
3623
- if (reviewer === null) {
3624
- reviewer = createTaskRole(tx, task, reviewerRoleName, undefined, now, reviewerRoleName);
3625
- tx.saveRole(task.id, reviewer);
3626
- }
3619
+ const primaryAvailability = projectReviewerAvailability(tx, task.id, reviewerRoleName);
3620
+ if (primaryAvailability.kind === "busy")
3621
+ return primaryAvailability;
3627
3622
  const laneRoles = requestedLaneRoles.length === 0
3628
3623
  ? [reviewerRoleName]
3629
3624
  : requestedLaneRoles[0] === reviewerRoleName
@@ -3642,22 +3637,24 @@ function requestTaskReviewRound(args, store, options) {
3642
3637
  if (tx.getGlobalRole(laneRoleName) === null && tx.getRole(task.id, laneRoleName) === null) {
3643
3638
  throw usageError(`Global Role not found: ${laneRoleName}.`);
3644
3639
  }
3645
- let laneRole = tx.getRole(task.id, laneRoleName);
3646
- if (laneRole === null) {
3647
- laneRole = createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName);
3648
- tx.saveRole(task.id, laneRole);
3640
+ const availability = projectReviewerAvailability(tx, task.id, laneRoleName);
3641
+ if (availability.kind === "busy")
3642
+ return availability;
3643
+ if (taskReviewProducerCollision(provenance, laneRoleName) !== null) {
3644
+ throw usageError(`Reviewer Role must be separate from the Candidate producer: ${laneRoleName}.`);
3649
3645
  }
3650
- if (taskReviewProducerCollision(provenance, laneRole.name) !== null) {
3651
- throw usageError(`Reviewer Role must be separate from the Candidate producer: ${laneRole.name}.`);
3646
+ }
3647
+ for (const laneRoleName of laneRoles) {
3648
+ if (tx.getRole(task.id, laneRoleName) === null) {
3649
+ tx.saveRole(task.id, createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName));
3652
3650
  }
3653
- assertTaskReviewRequestLane(tx, task.id, laneRole.name);
3654
3651
  }
3655
3652
  let deltaRecord;
3656
3653
  if (deltaRecheckRequested) {
3657
3654
  if (requestedLaneRoles.length > 0 || requestedStrategy !== undefined) {
3658
3655
  throw usageError("Delta-recheck supports only the default single Reviewer Lane.");
3659
3656
  }
3660
- deltaRecord = validateDeltaRecheckRequest(tx, task.id, reviewerRoleName, provenance.candidate, options.deltaRecheckPreflight);
3657
+ deltaRecord = validateDeltaRecheckRequest(tx, task.id, provenance.candidate, options.deltaRecheckPreflight);
3661
3658
  }
3662
3659
  let created = deltaRecord === undefined
3663
3660
  ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
@@ -3675,15 +3672,14 @@ function requestTaskReviewRound(args, store, options) {
3675
3672
  tx.saveReviewRound(task.id, created);
3676
3673
  // Issue 07: when a full Review is created after a non-accepting delta
3677
3674
  // disposition, record the escalation lineage on the delta Round.
3678
- if (exact !== undefined
3679
- && deltaRecheckBlocksAcceptance(exact)
3675
+ if (latestNonAcceptingDelta !== undefined
3680
3676
  && created.deltaRecheck === undefined
3681
- && exact.deltaRecheck !== undefined
3682
- && exact.deltaRecheck.escalatedToReviewRoundId === undefined) {
3677
+ && latestNonAcceptingDelta.deltaRecheck !== undefined
3678
+ && latestNonAcceptingDelta.deltaRecheck.escalatedToReviewRoundId === undefined) {
3683
3679
  tx.saveReviewRound(task.id, {
3684
- ...exact,
3680
+ ...latestNonAcceptingDelta,
3685
3681
  deltaRecheck: {
3686
- ...exact.deltaRecheck,
3682
+ ...latestNonAcceptingDelta.deltaRecheck,
3687
3683
  escalatedToReviewRoundId: created.id
3688
3684
  }
3689
3685
  });
@@ -3703,11 +3699,20 @@ function requestTaskReviewRound(args, store, options) {
3703
3699
  }, now);
3704
3700
  return created;
3705
3701
  });
3706
- return output(round.status === "pending"
3707
- ? round.deltaRecheck === undefined
3708
- ? `Task-final Review requested as ${round.id}\n`
3709
- : `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
3710
- : `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
3702
+ if ("kind" in round && round.kind === "busy") {
3703
+ return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
3704
+ + `${round.activeRunId === undefined ? "" : `; Run ${round.activeRunId}`}); `
3705
+ + `${round.activeReviewRoundId === undefined
3706
+ ? ""
3707
+ : `active ReviewRound ${round.activeReviewRoundId}; `}`
3708
+ + `retry after ${round.retryAfterSeconds}s or choose another Reviewer.\n`, { reviewRequest: round });
3709
+ }
3710
+ const requestedRound = round;
3711
+ return output(requestedRound.status === "pending"
3712
+ ? requestedRound.deltaRecheck === undefined
3713
+ ? `Task-final Review requested as ${requestedRound.id}\n`
3714
+ : `Task-final delta-recheck requested as ${requestedRound.id} (rechecks ${requestedRound.deltaRecheck.previousReviewRoundId})\n`
3715
+ : `Task-final Review is already ${requestedRound.status}: ${requestedRound.id}\n`, { reviewRound: requestedRound });
3711
3716
  }
3712
3717
  const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
3713
3718
  /**
@@ -3790,12 +3795,12 @@ function forceFreshTaskReviewRound(args, store, options) {
3790
3795
  if (sourceIndex < 0) {
3791
3796
  throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
3792
3797
  }
3793
- const laterRound = taskRounds.slice(sourceIndex + 1).at(-1);
3798
+ const laterRound = taskRounds.slice(sourceIndex + 1).find((entry) => (entry.reviewerRoleName === source.reviewerRoleName));
3794
3799
  if (laterRound !== undefined) {
3795
3800
  throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
3796
3801
  + `${laterRound.id}/${laterRound.status}.`);
3797
3802
  }
3798
- assertNoConflictingTaskReviewRound(taskRounds, source.id);
3803
+ assertNoConflictingTaskReviewRound(taskRounds, source.id, source.reviewerRoleName);
3799
3804
  assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
3800
3805
  let reviewer = tx.getRole(task.id, source.reviewerRoleName);
3801
3806
  if (reviewer === null) {
@@ -3853,22 +3858,17 @@ export function classifyForceFreshReviewRecovery(store, round) {
3853
3858
  * Review or an equivalent-and-accepted delta) so a delta never extends a
3854
3859
  * non-accepting disposition.
3855
3860
  */
3856
- function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate, preflight) {
3861
+ function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
3857
3862
  if (preflight === undefined) {
3858
3863
  throw usageError("Delta-recheck assessment is missing; the CLI preflight did not run. "
3859
3864
  + "Request a full Review or retry with a current CLI.");
3860
3865
  }
3861
3866
  const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
3862
3867
  if (previous === null
3863
- || (previous.scope ?? "work-item") !== "task"
3864
- || !isSemanticReviewRound(previous, store)) {
3865
- throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
3868
+ || !isAcceptedTaskReviewBaseline(store, previous)) {
3869
+ throw usageError(`Delta-recheck previous ReviewRound is not an accepted Task-final baseline: `
3866
3870
  + `${preflight.record.previousReviewRoundId}.`);
3867
3871
  }
3868
- if (previous.reviewerRoleName !== reviewerRoleName) {
3869
- throw usageError(`Delta-recheck Reviewer Role must match the previous acceptance: `
3870
- + `${previous.reviewerRoleName}.`);
3871
- }
3872
3872
  if (previous.reviewBaseCommit !== preflight.record.previousBaseCommit) {
3873
3873
  throw usageError("Delta-recheck previous base commit does not match the recorded acceptance.");
3874
3874
  }
@@ -3876,7 +3876,7 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
3876
3876
  if (previous.deltaRecheck !== undefined
3877
3877
  && previous.deltaRecheck.disposition !== "equivalent-and-accepted") {
3878
3878
  throw usageError(`Delta-recheck cannot extend ${previous.id}: its disposition is `
3879
- + `${previous.deltaRecheck.disposition}. Resolve it with a full Review first.`);
3879
+ + `${previous.deltaRecheck.disposition}. The Leader must choose the next Review action.`);
3880
3880
  }
3881
3881
  if (candidate.projects[0].commit === preflight.record.previousBaseCommit) {
3882
3882
  throw usageError("Delta-recheck candidate head is unchanged; the previous acceptance already covers it.");
@@ -3986,12 +3986,12 @@ function retryFailedTaskReviewRound(args, store, options) {
3986
3986
  }
3987
3987
  // Issue 06: infra retries reuse the same semantic Round ID. Any later
3988
3988
  // Round supersedes this one; an active Round for the same Reviewer blocks.
3989
- const conflictingLater = reviewerRounds.slice(roundIndex + 1).at(-1);
3989
+ const conflictingLater = reviewerRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === round.reviewerRoleName));
3990
3990
  if (conflictingLater !== undefined) {
3991
3991
  throw usageError(`A newer conflicting final ReviewRound already exists after ${round.id}: `
3992
3992
  + `${conflictingLater.id}/${conflictingLater.status}.`);
3993
3993
  }
3994
- assertNoConflictingTaskReviewRound(reviewerRounds, round.id);
3994
+ assertNoConflictingTaskReviewRound(reviewerRounds, round.id, round.reviewerRoleName);
3995
3995
  const activeRound = reviewerRounds.find((entry) => (entry.id !== round.id
3996
3996
  && entry.reviewerRoleName === round.reviewerRoleName
3997
3997
  && (entry.status === "pending" || entry.status === "running")));
@@ -4262,7 +4262,9 @@ function settleStaleFinalReviewRun(args, store, options) {
4262
4262
  if (validation.disposition !== "applied" || validation.round === null) {
4263
4263
  throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
4264
4264
  }
4265
- const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review" && entry.status === "active"));
4265
+ const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review"
4266
+ && entry.roleName === round.reviewerRoleName
4267
+ && entry.status === "active"));
4266
4268
  const activeRoleRun = tx.getActiveAgentRun(task.id, round.reviewerRoleName);
4267
4269
  if (activeReviewerRun !== undefined || activeRoleRun !== null) {
4268
4270
  const active = activeReviewerRun ?? activeRoleRun;
@@ -4274,7 +4276,7 @@ function settleStaleFinalReviewRun(args, store, options) {
4274
4276
  if (roundIndex < 0) {
4275
4277
  throw dataError(`Final ReviewRound is not in Task history: ${round.id}.`);
4276
4278
  }
4277
- const laterRound = taskRounds.slice(roundIndex + 1).at(-1);
4279
+ const laterRound = taskRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === round.reviewerRoleName));
4278
4280
  if (laterRound !== undefined) {
4279
4281
  throw usageError(`A later final ReviewRound already exists: ${laterRound.id}/${laterRound.status}.`);
4280
4282
  }
@@ -4751,54 +4753,50 @@ function isSameTaskReviewCandidate(left, right) {
4751
4753
  return left !== undefined && isDeepStrictEqual(left, right);
4752
4754
  }
4753
4755
  /**
4754
- * A Task can have at most one live Task-final ReviewRound. An exact existing
4755
- * Round may be reused by an idempotent caller; every other pending/running
4756
- * Round is a conflicting lane regardless of its Reviewer Role.
4756
+ * One Reviewer Role has one live Task-final Review lane. Different Reviewer
4757
+ * Roles own independent Session/Workspace slots and may review concurrently.
4757
4758
  */
4758
- function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = []) {
4759
+ function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = [], reviewerRoleName) {
4759
4760
  const reusable = new Set(typeof reusableRoundIds === "string" ? [reusableRoundIds] : reusableRoundIds);
4760
4761
  const conflicting = rounds.find((entry) => (!reusable.has(entry.id)
4761
4762
  && (entry.scope ?? "work-item") === "task"
4762
- && (entry.status === "pending" || entry.status === "running")));
4763
+ && (entry.status === "pending" || entry.status === "running")
4764
+ && (reviewerRoleName === undefined || entry.reviewerRoleName === reviewerRoleName)));
4763
4765
  if (conflicting !== undefined) {
4764
4766
  throw usageError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
4765
4767
  }
4766
4768
  }
4767
4769
  /**
4768
- * Queues a Task-scoped final ReviewRound. The reviewer Role must be separate
4769
- * from the Candidate producer. Returns the round (pending or failed) so the
4770
- * caller can surface it to the CLI for workspace preparation and dispatch.
4770
+ * Queues a Task-scoped final ReviewRound only after the Reviewer lane passes
4771
+ * its mechanical preflight. Busy or unavailable preparation never becomes a
4772
+ * failed semantic ReviewRound.
4771
4773
  */
4772
4774
  function queueTaskReviewRound(store, task, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
4773
- assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id));
4774
4775
  const provenance = taskReviewProvenance(store, task, options, taskCandidate);
4775
4776
  if (!isSameTaskReviewCandidate(provenance.candidate, taskCandidate)) {
4776
4777
  throw usageError(`Task-final ReviewRound frozen Task heads changed before queueing.`);
4777
4778
  }
4778
- const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
4779
- store.saveReviewRound(task.id, pending);
4780
4779
  const producerCollision = taskReviewProducerCollision(provenance, config.roleName);
4781
4780
  if (producerCollision !== null) {
4782
- const failed = finishReviewRound(pending, "failed", producerCollision, now);
4783
- store.saveReviewRound(task.id, failed);
4784
- return failed;
4781
+ throw usageError(producerCollision);
4782
+ }
4783
+ const availability = projectReviewerAvailability(store, task.id, config.roleName);
4784
+ if (availability.kind === "busy") {
4785
+ throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
4786
+ + `${availability.activeRunId === undefined ? "" : `; Run ${availability.activeRunId}`}); `
4787
+ + `retry after ${availability.retryAfterSeconds}s.`);
4785
4788
  }
4786
4789
  let reviewer = store.getRole(task.id, config.roleName);
4787
4790
  if (reviewer === null) {
4788
4791
  const globalRole = store.getGlobalRole(config.roleName);
4789
4792
  if (globalRole === null) {
4790
- const failed = finishReviewRound(pending, "failed", `Global Role not found: ${config.roleName}.`, now);
4791
- store.saveReviewRound(task.id, failed);
4792
- return failed;
4793
+ throw usageError(`Global Role not found: ${config.roleName}.`);
4793
4794
  }
4794
4795
  reviewer = createTaskRole(store, task, config.roleName, undefined, now, config.roleName);
4795
4796
  store.saveRole(task.id, reviewer);
4796
4797
  }
4797
- if (store.getActiveAgentRun(task.id, reviewer.name) !== null) {
4798
- const failed = finishReviewRound(pending, "failed", `Reviewer Role already has an active run: ${reviewer.name}.`, now);
4799
- store.saveReviewRound(task.id, failed);
4800
- return failed;
4801
- }
4798
+ const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
4799
+ store.saveReviewRound(task.id, pending);
4802
4800
  return pending;
4803
4801
  }
4804
4802
  /**
@@ -4833,33 +4831,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4833
4831
  // review evidence. Do not create duplicate rounds on repeated completion
4834
4832
  // attempts. A failed round remains a blocker until the Leader changes the
4835
4833
  // candidate or otherwise resolves the failed evidence explicitly.
4836
- // Issue 07: a completed delta-recheck that did not accept the head is not
4837
- // final evidence. A `requires-full-review` disposition escalates to a new
4838
- // full Review; a `finding` disposition stays a blocker for the Leader.
4839
- if (latest.status === "completed"
4840
- && latest.deltaRecheck !== undefined
4841
- && latest.deltaRecheck.disposition === "requires-full-review") {
4842
- // Fall through to queue a full Review for the same candidate.
4843
- const escalated = queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4844
- // Record the escalation lineage on the delta Round so the full Review
4845
- // is traceable from the non-accepting delta disposition.
4846
- if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
4847
- store.saveReviewRound(task.id, {
4848
- ...latest,
4849
- deltaRecheck: {
4850
- ...latest.deltaRecheck,
4851
- escalatedToReviewRoundId: escalated.id
4852
- }
4853
- });
4854
- }
4855
- return escalated;
4856
- }
4857
- else {
4858
- return latest.status === "completed"
4859
- && classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
4860
- ? null
4861
- : latest;
4862
- }
4834
+ // A non-accepting delta is durable evidence for the Leader, not an
4835
+ // instruction for Core to manufacture a full Review. Completion remains
4836
+ // blocked until the Leader explicitly chooses and obtains valid evidence.
4837
+ return isAcceptedTaskReviewBaseline(store, latest) ? null : latest;
4863
4838
  }
4864
4839
  return queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4865
4840
  }
@@ -4880,7 +4855,7 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
4880
4855
  if (!isSameTaskReviewCandidate(round.taskCandidate, taskCandidate)) {
4881
4856
  throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
4882
4857
  }
4883
- assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id);
4858
+ assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id, round.reviewerRoleName);
4884
4859
  const reviewer = store.getRole(task.id, round.reviewerRoleName);
4885
4860
  if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
4886
4861
  throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
@@ -5004,12 +4979,12 @@ function retryFailedReviewRun(previous, store, options, now) {
5004
4979
  if (roundIndex < 0) {
5005
4980
  throw dataError(`Final ReviewRound is not in Task history: ${round.id}.`);
5006
4981
  }
5007
- const laterRound = taskRounds.slice(roundIndex + 1).at(-1);
4982
+ const laterRound = taskRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === retryReviewerRoleName));
5008
4983
  if (laterRound !== undefined && !isSameTaskReviewCandidate(laterRound.taskCandidate, round.taskCandidate)) {
5009
4984
  throw usageError(`A newer final Task candidate already has ReviewRound ${laterRound.id}.`);
5010
4985
  }
5011
4986
  const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === retryReviewerRoleName)));
5012
- assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id);
4987
+ assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id, retryReviewerRoleName);
5013
4988
  const activeRound = allReviewerRounds.find((entry) => (entry.id !== round.id
5014
4989
  && (entry.status === "pending" || entry.status === "running")));
5015
4990
  if (activeRound !== undefined) {
@@ -5535,7 +5510,6 @@ function yieldRun(args, store, options) {
5535
5510
  kind: "yield"
5536
5511
  }, now);
5537
5512
  }
5538
- clearMatchingLeaderStallAttention(tx, task.id, active.id);
5539
5513
  if (active.purpose === "review") {
5540
5514
  const round = active.reviewRoundId === undefined
5541
5515
  ? null
@@ -5814,7 +5788,6 @@ function checkpointRun(args, store, options) {
5814
5788
  kind: "checkpoint"
5815
5789
  }, now);
5816
5790
  }
5817
- clearMatchingLeaderStallAttention(tx, task.id, run.id);
5818
5791
  return progress;
5819
5792
  });
5820
5793
  return `Checkpoint recorded for ${parsed.positionals[0]} (${event.id}).\n`;
@@ -5957,9 +5930,13 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5957
5930
  throw usageError(message);
5958
5931
  }
5959
5932
  if (taskScope) {
5933
+ const requestedReviewers = new Set(round.executionGroup?.lanes.map(({ roleName }) => roleName)
5934
+ ?? [round.reviewerRoleName]);
5960
5935
  const conflicting = tx.listReviewRounds(task.id).find((entry) => (entry.id !== round.id
5961
5936
  && (entry.scope ?? "work-item") === "task"
5962
5937
  && (entry.status === "pending" || entry.status === "running")
5938
+ && (entry.executionGroup?.lanes.some(({ roleName }) => requestedReviewers.has(roleName))
5939
+ ?? requestedReviewers.has(entry.reviewerRoleName))
5963
5940
  && !(entry.status === "running"
5964
5941
  && entry.reviewerRunId !== undefined
5965
5942
  && tx.getAgentRun(task.id, entry.reviewerRunId)?.status === "failed")));
@@ -6003,8 +5980,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6003
5980
  let deltaContext = "";
6004
5981
  if (taskScope && round.deltaRecheck !== undefined) {
6005
5982
  const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
6006
- if (previousRound === null || !isSemanticReviewRound(previousRound, tx)) {
6007
- throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous semantic ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
5983
+ if (previousRound === null || !isAcceptedTaskReviewBaseline(tx, previousRound)) {
5984
+ throw new TaskFinalReviewDispatchDriftError(`Delta-recheck accepted baseline is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
6008
5985
  }
6009
5986
  const diffByProject = options.deltaRecheckDiff;
6010
5987
  if (diffByProject === undefined) {
@@ -6048,9 +6025,9 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6048
6025
  "Start from the user's core outcome and the WorkItem intent. The candidate summary is a pointer, not proof: inspect the complete relevant change, callers, and proportionate checks.",
6049
6026
  "Keep Yui Core lifecycle safety, generic Reviewer behavior, Project Policy/Knowledge, and the Task Contract separate. Follow Project Policy pointers from the dispatch context for project-specific checks.",
6050
6027
  ...(round.scope === "task" && round.deltaRecheck === undefined
6051
- ? ["This is the one final Task Review: inspect every bound Project at the frozen Task heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
6028
+ ? ["This is a full Task Review: inspect every bound Project at the frozen Task heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
6052
6029
  : []),
6053
- "You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound-owned workspace.",
6030
+ "You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this stable Reviewer workspace at the exact ReviewRound snapshot.",
6054
6031
  "Do not push, integrate, mutate Task state, touch the Candidate or Worker workspace, another Task/workspace, a stable checkout, or the real Yui control-plane home.",
6055
6032
  "Use the exact --summary-file - body to report complete findings, evidence, checks actually run, uncertainty, and recommended next actions in clear Markdown or JSON. Yui preserves the full report; no fixed wording or field list is required. If you include evidenceCommit, it must match the managed Review workspace.",
6056
6033
  "Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. Review yield completes only this Round and creates no Candidate or ChangeSet.",
@@ -6248,7 +6225,13 @@ export function failPendingReviewRound(taskId, reviewRoundId, summary, store, op
6248
6225
  return round;
6249
6226
  const terminal = finishReviewRound(round, "failed", summary, now);
6250
6227
  tx.saveReviewRound(taskId, terminal);
6228
+ const event = recordTaskEventRecord(tx, taskId, "review.failed-to-start", {
6229
+ reviewRoundId: terminal.id,
6230
+ reviewerRoleName: terminal.reviewerRoleName,
6231
+ reason: summary
6232
+ }, now);
6251
6233
  enqueueWork(tx, leaderMailbox(taskId), "review-failed", now, [
6234
+ eventRef(taskId, event.id),
6252
6235
  ...(round.workItemId === undefined ? [] : [workItemRef(taskId, round.workItemId)])
6253
6236
  ]);
6254
6237
  return terminal;
@@ -6334,7 +6317,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
6334
6317
  return message;
6335
6318
  }
6336
6319
  function recordTaskEvent(store, taskId, type, payload, now) {
6337
- recordTaskEventRecord(store, taskId, type, payload, now);
6320
+ return recordTaskEventRecord(store, taskId, type, payload, now);
6338
6321
  }
6339
6322
  function leaderActionEventPayload(store, taskId, options) {
6340
6323
  const runId = taskLeaderActionRunId(store, taskId, options.environment, options.yuiHome);