@zq-silk/yui 0.8.9 → 0.10.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 (51) hide show
  1. package/ARCHITECTURE.md +48 -46
  2. package/README.md +72 -42
  3. package/dist/cli/commandCatalog.js +16 -8
  4. package/dist/cli.js +27 -32
  5. package/dist/commands/executionAuditCommands.js +2 -2
  6. package/dist/commands/globalRoleCommands.js +0 -12
  7. package/dist/commands/sessionCommands.js +116 -0
  8. package/dist/commands/taskBaseCommands.js +1 -11
  9. package/dist/commands/taskCommands.js +123 -340
  10. package/dist/commands/taskCompletionGate.js +15 -12
  11. package/dist/commands/taskContextCommand.js +18 -11
  12. package/dist/commands/taskNextActionCommand.js +4 -5
  13. package/dist/commands/taskWorkspaceCommands.js +2 -2
  14. package/dist/controller/clientRuntime.js +65 -0
  15. package/dist/controller/fileSchedulerStoreAdapter.js +2 -49
  16. package/dist/doctor/doctor.js +16 -12
  17. package/dist/execution/executionGroup.js +0 -3
  18. package/dist/executor/agentAdapter.js +39 -42
  19. package/dist/executor/agentExecutor.js +4 -2
  20. package/dist/executor/codexConfigConflict.js +40 -16
  21. package/dist/executor/effectiveLaunch.js +33 -3
  22. package/dist/executor/fileRoleLaunchPlanner.js +15 -24
  23. package/dist/integration/deliveryObligation.js +72 -0
  24. package/dist/integration/gitIntegrationService.js +1 -1
  25. package/dist/lifecycle/exactRunTerminalization.js +12 -8
  26. package/dist/observability/orchestrationMetrics.js +12 -26
  27. package/dist/profile/agentProfile.js +1 -1
  28. package/dist/repository/taskBaseFreshness.js +5 -11
  29. package/dist/repository/taskWorkspaceCoordinator.js +2 -0
  30. package/dist/repository/taskWorkspacePreparer.js +173 -26
  31. package/dist/review/reviewRound.js +41 -24
  32. package/dist/role/role.js +0 -9
  33. package/dist/runtime/{firstProgressStopLoss.js → firstProgressAdvisory.js} +7 -21
  34. package/dist/scheduler/leaderWakeupProcessor.js +0 -25
  35. package/dist/setup/setupCommand.js +0 -5
  36. package/dist/storage/migration/productionRegistry.js +138 -0
  37. package/dist/storage/sqliteStore.js +2 -1
  38. package/dist/storage/taskStore.js +27 -27
  39. package/dist/storage/upgrade/upgradeOrchestrator.js +35 -7
  40. package/dist/task/completionReadiness.js +35 -25
  41. package/dist/task/nextAction.js +70 -78
  42. package/dist/task/task.js +12 -19
  43. package/dist/web/assets/client/components.js +3 -1
  44. package/dist/web/assets/client/i18n.js +6 -0
  45. package/dist/web/webSnapshot.js +0 -3
  46. package/i18n/README.zh-CN.md +33 -24
  47. package/package.json +1 -1
  48. package/skills/yui-leader/SKILL.md +55 -52
  49. package/skills/yui-operator/SKILL.md +31 -40
  50. package/skills/yui-reviewer/SKILL.md +18 -12
  51. package/skills/yui-worker/SKILL.md +5 -3
@@ -36,13 +36,13 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
36
36
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
37
37
  import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
38
38
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
39
- import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskDeliveryPath, updateTaskMetadata } from "../task/task.js";
39
+ import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
40
40
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
41
41
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
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, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
45
+ import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
46
46
  import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
47
47
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
48
48
  import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
@@ -102,7 +102,7 @@ function storedTaskFinalReviewContractResolution(store, taskId) {
102
102
  * acceptance, and completion mutation must present the same verified
103
103
  * capability; shared review-config drift is intentionally irrelevant.
104
104
  */
105
- function taskFinalReviewContractForMutation(store, taskId, options, authorization = {}) {
105
+ function taskFinalReviewContractForMutation(store, taskId, options) {
106
106
  const supplied = options.taskFinalReviewContract;
107
107
  if (supplied !== undefined) {
108
108
  validateTaskFinalReviewContract(supplied);
@@ -117,11 +117,12 @@ function taskFinalReviewContractForMutation(store, taskId, options, authorizatio
117
117
  }
118
118
  if (stored === undefined)
119
119
  return supplied;
120
- if (supplied === undefined) {
121
- if (authorization.allowStoredWithoutSupplied === true)
122
- return stored;
123
- throw usageError(`Task final-review contract is missing for ${taskId}.`);
124
- }
120
+ // Historical exact contracts now express only a durable Reviewer policy.
121
+ // The persisted contract remains the audit authority, but ordinary
122
+ // protocol/storage-compatible CLIs may continue the Task without presenting
123
+ // a release-bound capability on every mutation.
124
+ if (supplied === undefined)
125
+ return stored;
125
126
  if (!sameTaskFinalReviewContract(stored, supplied)) {
126
127
  throw usageError(`Task final-review contract control-plane digest mismatch for ${taskId}.`);
127
128
  }
@@ -168,18 +169,10 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
168
169
  throw usageError(`Task is archived: ${task.id}.`);
169
170
  if (task.status !== "active")
170
171
  throw usageError(`Task is not active: ${task.id}.`);
171
- // Resolve and authenticate the durable Task-local gate before any remote
172
- // fetch or Integration write. All checks below mirror the transactional
173
- // completion path, which remains the final CAS fence after reconciliation.
174
- // A human/global Operator cannot present the exact managed Leader contract.
175
- // For the explicit published-tree path only, let that caller authenticate
176
- // the stored contract far enough to persist an exact authorization fact.
177
- // The same command must return before any contract-governed completion
178
- // mutation; the exact Leader later consumes the authorization with the real
179
- // contract capability.
180
- const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
181
- && actor !== "leader";
182
- const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
172
+ // Resolve the durable Task-local Reviewer policy before any remote fetch or
173
+ // Integration write. Historical control-plane identity is audit evidence,
174
+ // not a capability that every compatible CLI must reproduce.
175
+ const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
183
176
  const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
184
177
  && (round.status === "pending" || round.status === "running")));
185
178
  // Issue 06: one shared readiness projection enumerates every blocker.
@@ -367,13 +360,12 @@ function taskProjectCommand(args, store, options) {
367
360
  }
368
361
  function updateTaskCommand(args, store, options) {
369
362
  const optionNames = new Set([
370
- "--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
363
+ "--title", "--type", "--description", "--priority", "--tags", "--due-at"
371
364
  ]);
372
365
  const flagOptions = new Set([
373
- "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
374
- "--require-integration"
366
+ "--clear-type", "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
375
367
  ]);
376
- const usage = "Task update usage: yui task update <id> [--title <text>] [--description <text>|--clear-description] [--priority <low|medium|high|urgent>|--clear-priority] [--tags <comma-separated>|--clear-tags] [--due-at <RFC3339>|--clear-due-at] [--delivery <direct|integrated>] [--require-integration].";
368
+ const usage = "Task update usage: yui task update <id> [--title <text>] [--type <project-defined-type>|--clear-type] [--description <text>|--clear-description] [--priority <low|medium|high|urgent>|--clear-priority] [--tags <comma-separated>|--clear-tags] [--due-at <RFC3339>|--clear-due-at].";
377
369
  const parsed = parseTail(args, optionNames, usage, flagOptions);
378
370
  exactPositionals(parsed.positionals, 1, usage);
379
371
  if (parsed.options.size === 0)
@@ -397,44 +389,16 @@ function updateTaskCommand(args, store, options) {
397
389
  const tags = parsed.options.has("--tags")
398
390
  ? parseTaskTags(requiredOption(parsed.options, "--tags"))
399
391
  : undefined;
400
- const requestedDelivery = parsed.options.has("--delivery")
401
- ? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
402
- : undefined;
403
- if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
404
- throw usageError("--delivery direct conflicts with --require-integration.", usage);
405
- }
406
- const enableIntegration = requestedDelivery === "integrated"
407
- || parsed.options.has("--require-integration");
408
392
  const now = clock(options);
409
393
  const result = store.transaction((tx) => {
410
394
  const current = requireTask(tx, parsed.positionals[0]);
411
395
  if (current.status === "archived")
412
396
  throw usageError(`Task is archived: ${current.id}.`);
413
- if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
414
- && current.projectBindings.length === 0) {
415
- throw usageError(`Task ${current.id} has no Project; delivery selection is not applicable.`);
416
- }
417
- if (requestedDelivery === "direct" && current.requireIntegration === true) {
418
- throw usageError(`Task ${current.id} already uses integrated delivery and cannot be downgraded to direct.`);
419
- }
420
- if (enableIntegration && current.status === "completed") {
421
- throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
422
- }
423
- if (enableIntegration && current.requireIntegration !== true) {
424
- assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
425
- }
426
- if (parsed.options.size === 1
427
- && enableIntegration
428
- && current.requireIntegration === true) {
429
- return { task: current, integrationState: "already-enabled" };
430
- }
431
- if (parsed.options.size === 1
432
- && requestedDelivery === "direct"
433
- && taskDeliveryPath(current) === "direct") {
434
- return { task: current, integrationState: "already-direct" };
435
- }
436
397
  const updated = updateTaskMetadata(current, {
437
398
  ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
399
+ ...(parsed.options.has("--type")
400
+ ? { type: requiredOption(parsed.options, "--type") }
401
+ : parsed.options.has("--clear-type") ? { type: null } : {}),
438
402
  ...(parsed.options.has("--description")
439
403
  ? { description: requiredOption(parsed.options, "--description") }
440
404
  : parsed.options.has("--clear-description") ? { description: null } : {}),
@@ -446,94 +410,18 @@ function updateTaskCommand(args, store, options) {
446
410
  : { tags }),
447
411
  ...(dueAt === undefined
448
412
  ? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
449
- : { dueAt }),
450
- ...(enableIntegration ? { requireIntegration: true } : {})
413
+ : { dueAt })
451
414
  }, now);
452
415
  tx.saveTask(updated);
453
416
  recordTaskEvent(tx, updated.id, "task.updated", {
454
417
  status: updated.status,
455
- ...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
456
- ? {}
457
- : {
458
- completionEvidence: enableIntegration
459
- ? "integration-required"
460
- : "direct",
461
- deliveryPath: taskDeliveryPath(updated)
462
- })
418
+ ...(updated.type === undefined ? {} : { taskType: updated.type })
463
419
  }, now);
464
420
  enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
465
- return {
466
- task: updated,
467
- integrationState: enableIntegration
468
- ? "enabled"
469
- : requestedDelivery === "direct"
470
- ? "direct"
471
- : "unchanged"
472
- };
421
+ return updated;
473
422
  });
474
- if (result.integrationState !== "already-enabled"
475
- && result.integrationState !== "already-direct") {
476
- notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
477
- }
478
- return result.integrationState === "enabled"
479
- ? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
480
- : result.integrationState === "already-enabled"
481
- ? `Task ${result.task.id} already uses integrated delivery\n`
482
- : result.integrationState === "already-direct"
483
- ? `Task ${result.task.id} already uses direct delivery\n`
484
- : result.integrationState === "direct"
485
- ? `Updated task ${result.task.id}\nDelivery: direct\n`
486
- : `Updated task ${result.task.id}\n`;
487
- }
488
- function assertTaskDeliveryPromotionEligible(store, task, snapshot) {
489
- const evidence = [
490
- ...store.listWorkItems(task.id).map(({ id }) => `WorkItem ${id}`),
491
- ...store.listChangeSets(task.id).map(({ id }) => `ChangeSet ${id}`),
492
- ...store.listIntegrationAttempts(task.id).map(({ id }) => `IntegrationAttempt ${id}`),
493
- ...store.listReviewRounds(task.id).map(({ id }) => `ReviewRound ${id}`)
494
- ];
495
- if (evidence.length > 0) {
496
- throw usageError(`Task ${task.id} cannot promote to integrated delivery after delivery evidence exists: `
497
- + `${evidence.join(", ")}. Create an integrated replacement Task or keep the current direct contract.`);
498
- }
499
- if (task.status !== "draft" && task.status !== "active") {
500
- throw usageError(`Task ${task.id} must be Draft or Active to promote delivery; current status is ${task.status}.`);
501
- }
502
- const workspace = store.getTaskWorkspace(task.id);
503
- if (task.status === "draft" && workspace === null)
504
- return;
505
- if (snapshot === undefined) {
506
- throw usageError(`Task ${task.id} delivery promotion requires a CLI-verified clean Task-main snapshot.`);
507
- }
508
- if (workspace === null
509
- || workspace.owner.type !== "task"
510
- || workspace.owner.taskId !== task.id) {
511
- throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
512
- }
513
- const snapshotIds = snapshot.schemaVersion === 1 && Array.isArray(snapshot.projects)
514
- ? snapshot.projects.map(({ projectId }) => projectId)
515
- : [];
516
- if (snapshotIds.length !== task.projectBindings.length
517
- || new Set(snapshotIds).size !== snapshotIds.length) {
518
- throw usageError(`Task-main promotion snapshot does not match bound Projects: ${task.id}.`);
519
- }
520
- for (const binding of task.projectBindings) {
521
- const project = snapshot.projects.find(({ projectId }) => projectId === binding.projectId);
522
- const entry = workspace.entries.find(({ projectId }) => projectId === binding.projectId);
523
- if (project === undefined
524
- || entry === undefined
525
- || entry.access !== "write"
526
- || project.directory !== entry.directory
527
- || project.branch !== entry.branch
528
- || project.baseCommit !== entry.baseCommit) {
529
- throw usageError(`Task-main promotion snapshot changed before mutation: ${task.id}/${binding.projectId}.`);
530
- }
531
- if (project.headCommit !== project.baseCommit) {
532
- throw usageError(`Task ${task.id} main already advanced for Project ${binding.projectId}; `
533
- + "cannot promote without losing ChangeSet provenance. Create an integrated replacement "
534
- + "Task or keep the current direct contract.");
535
- }
536
- }
423
+ notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
424
+ return `Updated task ${result.id}\n`;
537
425
  }
538
426
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
539
427
  export function runTaskOutputCommand(args, store, options = {}) {
@@ -569,25 +457,22 @@ function createTaskCommand(args, store, options) {
569
457
  const now = clock(options);
570
458
  const created = store.transaction((tx) => createTaskAggregate(tx, parsed.title, {
571
459
  projectBindings: parsed.projectBindings,
572
- ...(parsed.requireIntegration ? { requireIntegration: true } : {})
460
+ ...(parsed.type === undefined ? {} : { type: parsed.type })
573
461
  }, now, parsed.defaultProjectIds));
574
462
  notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
575
463
  return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
576
464
  + `Assigned role: ${created.leader.name}\n`
577
- + `Delivery: ${taskDeliveryPath(created.task)}\n`
578
- + (created.task.requireIntegration
579
- ? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
580
- : created.task.projectBindings.length > 0
581
- ? "Completion: clean committed Task main required; no WorkItem, ChangeSet, IntegrationAttempt, or managed ReviewRound required\n"
582
- : "Completion: no Project delivery evidence required\n"), {
465
+ + `Type: ${created.task.type ?? "unspecified"}\n`
466
+ + (created.task.projectBindings.length > 0
467
+ ? "Execution: Leader decides whether independent WorkItems are warranted\n"
468
+ : "Execution: no Project delivery evidence required\n"), {
583
469
  task: created.task,
584
- leader: created.leader,
585
- deliveryPath: taskDeliveryPath(created.task)
470
+ leader: created.leader
586
471
  });
587
472
  }
588
473
  function parseTaskCreation(args, store) {
589
- const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--delivery <direct|integrated>] [--require-integration].";
590
- const parsed = parseMultiValueTail(args, new Set(["--delivery"]), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
474
+ const usage = "Task create usage: yui task create <title> [--type <project-defined-type>] [--project <project> ...] [--base <project>=<ref> ...].";
475
+ const parsed = parseMultiValueTail(args, new Set(["--type"]), new Set(["--project", "--base"]), usage);
591
476
  exactPositionals(parsed.positionals, 1, usage);
592
477
  const projectReferences = parsed.multiOptions.get("--project") ?? [];
593
478
  const baseOptions = parsed.multiOptions.get("--base") ?? [];
@@ -603,16 +488,6 @@ function parseTaskCreation(args, store) {
603
488
  if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
604
489
  throw usageError("A Task cannot bind the same Project more than once.");
605
490
  }
606
- const requestedDelivery = parsed.options.has("--delivery")
607
- ? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
608
- : undefined;
609
- if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
610
- && projects.length === 0) {
611
- throw usageError("Delivery selection requires at least one --project.", usage);
612
- }
613
- if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
614
- throw usageError("--delivery direct conflicts with --require-integration.", usage);
615
- }
616
491
  const bases = new Map();
617
492
  for (const option of baseOptions) {
618
493
  const separator = option.indexOf("=");
@@ -637,14 +512,15 @@ function parseTaskCreation(args, store) {
637
512
  .map(({ id }) => id);
638
513
  return {
639
514
  title: parsed.positionals[0],
515
+ ...(parsed.options.has("--type")
516
+ ? { type: requiredOption(parsed.options, "--type") }
517
+ : {}),
640
518
  projectBindings: projects.map((project) => ({
641
519
  projectId: project.id,
642
520
  directory: project.name,
643
521
  baseRef: bases.get(project.id) ?? project.developmentBranch
644
522
  })),
645
- defaultProjectIds,
646
- requireIntegration: requestedDelivery === "integrated"
647
- || parsed.options.has("--require-integration")
523
+ defaultProjectIds
648
524
  };
649
525
  }
650
526
  function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
@@ -654,7 +530,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
654
530
  store.saveRole(task.id, leader);
655
531
  recordTaskEvent(store, task.id, "task.created", {
656
532
  status: task.status,
657
- deliveryPath: taskDeliveryPath(task),
533
+ ...(task.type === undefined ? {} : { taskType: task.type }),
658
534
  ...(defaultProjectIds.length === 0
659
535
  ? {}
660
536
  : { defaultProjectIds: defaultProjectIds.join(",") })
@@ -703,16 +579,14 @@ function showTaskCommand(args, store) {
703
579
  `Task: ${task.id}`,
704
580
  `Title: ${task.title}`,
705
581
  `Status: ${task.status}`,
706
- `Delivery: ${taskDeliveryPath(task)}`,
582
+ `Type: ${task.type ?? "unspecified"}`,
707
583
  ...(task.description === undefined ? [] : [`Description: ${task.description}`]),
708
584
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
709
585
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
710
586
  ...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
711
- `Completion evidence: ${task.requireIntegration === true
712
- ? "WorkItem, ChangeSet, and committed Integration required"
713
- : task.projectBindings.length > 0
714
- ? "clean committed Task main required"
715
- : "no Project evidence required"}`,
587
+ `Execution topology: ${task.projectBindings.length > 0
588
+ ? "Leader-owned; WorkItems are optional independent delivery units"
589
+ : "no Project delivery evidence required"}`,
716
590
  ...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
717
591
  ...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
718
592
  ...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
@@ -741,12 +615,7 @@ function showTaskCommand(args, store) {
741
615
  `Created: ${presentTime(task.createdAt, timeZone)}`,
742
616
  `Updated: ${presentTime(task.updatedAt, timeZone)}`
743
617
  ].join("\n").concat("\n");
744
- return output(rendered, {
745
- task,
746
- deliveryPath: taskDeliveryPath(task),
747
- counts,
748
- hasBrief: brief !== null
749
- });
618
+ return output(rendered, { task, counts, hasBrief: brief !== null });
750
619
  }
751
620
  function activateTaskCommand(args, store, options) {
752
621
  exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
@@ -860,7 +729,7 @@ function completeTaskCommand(args, store, options) {
860
729
  terminalizedLeaderRun = true;
861
730
  }
862
731
  // A final (Task-scoped) review is the Task delivery policy: it reviews the
863
- // complete integrated Task heads, not a single WorkItem Candidate. If the
732
+ // complete frozen Task heads, not a single WorkItem Candidate. If the
864
733
  // review config requests a final review, create the Task ReviewRound here
865
734
  // and return it for dispatch instead of completing the Task.
866
735
  const pendingFinalReviewIds = new Set(tx.listReviewRounds(task.id)
@@ -915,7 +784,6 @@ function completeTaskCommand(args, store, options) {
915
784
  recordTaskEvent(tx, task.id, "task.completed", {
916
785
  by: actor,
917
786
  summary,
918
- deliveryPath: taskDeliveryPath(task),
919
787
  ...(actualTaskCandidate === undefined
920
788
  ? {}
921
789
  : {
@@ -2518,14 +2386,16 @@ function executionTargetForWorkItem(taskId, workItemId, revision, item, workspac
2518
2386
  }
2519
2387
  function executionTargetForReviewRound(task, round, item, candidate) {
2520
2388
  const taskScope = (round.scope ?? "work-item") === "task";
2389
+ if (!taskScope && (item === undefined || candidate === undefined)) {
2390
+ throw dataError(`WorkItem ReviewRound target is missing its Candidate: ${round.id}.`);
2391
+ }
2521
2392
  const projects = taskScope
2522
2393
  ? round.taskCandidate?.projects ?? []
2523
2394
  : candidate.gitSnapshot?.projects ?? [];
2524
2395
  const fingerprint = JSON.stringify({
2525
2396
  taskId: task.id,
2526
2397
  reviewRoundId: round.id,
2527
- workItemId: item.id,
2528
- candidateId: candidate.id,
2398
+ ...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
2529
2399
  scope: taskScope ? "task" : "work-item",
2530
2400
  projects,
2531
2401
  contractDigest: round.taskFinalReviewContract?.digest
@@ -2534,9 +2404,10 @@ function executionTargetForReviewRound(task, round, item, candidate) {
2534
2404
  schemaVersion: 1,
2535
2405
  kind: taskScope ? "task-final-review" : "work-item",
2536
2406
  taskId: task.id,
2537
- workItemId: item.id,
2538
- candidateId: candidate.id,
2539
- revision: candidate.workItemRevision,
2407
+ ...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
2408
+ // A Task-final ReviewRound is itself the immutable semantic target. Runs
2409
+ // retry that same revision; a changed frozen Task creates a new Round.
2410
+ revision: taskScope ? 1 : candidate.workItemRevision,
2540
2411
  projects,
2541
2412
  ...(round.taskFinalReviewContract === undefined
2542
2413
  ? {}
@@ -3096,7 +2967,7 @@ function resolveReviewExecutionGroup(args, store, options) {
3096
2967
  reconcileReviewFindingsAfterReview(tx, task.id, terminal.id, now);
3097
2968
  }
3098
2969
  enqueueWork(tx, leaderMailbox(task.id), "review-group-resolved", now, [
3099
- workItemRef(task.id, round.workItemId)
2970
+ ...(round.workItemId === undefined ? [] : [workItemRef(task.id, round.workItemId)])
3100
2971
  ]);
3101
2972
  return terminal;
3102
2973
  });
@@ -3323,7 +3194,6 @@ function requestTaskReviewRound(args, store, options) {
3323
3194
  if (producerCollision !== null) {
3324
3195
  throw usageError(producerCollision);
3325
3196
  }
3326
- const anchor = latestTaskReviewAnchor(tx, task);
3327
3197
  const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id))
3328
3198
  .filter((entry) => (entry.scope ?? "work-item") === "task");
3329
3199
  const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
@@ -3441,11 +3311,11 @@ function requestTaskReviewRound(args, store, options) {
3441
3311
  deltaRecord = validateDeltaRecheckRequest(tx, task.id, reviewerRoleName, provenance.candidate, options.deltaRecheckPreflight);
3442
3312
  }
3443
3313
  let created = deltaRecord === undefined
3444
- ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, anchor.item.id, anchor.candidate.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
3445
- : createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, anchor.item.id, anchor.candidate.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
3314
+ ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
3315
+ : createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
3446
3316
  let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3447
3317
  purpose: "review",
3448
- target: executionTargetForReviewRound(task, created, anchor.item, anchor.candidate),
3318
+ target: executionTargetForReviewRound(task, created),
3449
3319
  strategy,
3450
3320
  lanes: laneRoles.map((roleName) => ({ roleName, reviewRoundId: created.id }))
3451
3321
  }, now);
@@ -3471,8 +3341,6 @@ function requestTaskReviewRound(args, store, options) {
3471
3341
  }
3472
3342
  recordTaskEvent(tx, task.id, "review.task-final-requested", {
3473
3343
  reviewRoundId: created.id,
3474
- workItemId: created.workItemId,
3475
- candidateId: created.candidateId,
3476
3344
  reviewerRoleName: created.reviewerRoleName,
3477
3345
  requestedBy: created.requestedBy,
3478
3346
  taskCandidate: JSON.stringify(created.taskCandidate),
@@ -3532,14 +3400,10 @@ function forceFreshTaskReviewRound(args, store, options) {
3532
3400
  if (replacement === null
3533
3401
  || replacement.id === source.id
3534
3402
  || (replacement.scope ?? "work-item") !== "task"
3535
- || replacement.workItemId !== source.workItemId
3536
- || replacement.candidateId !== source.candidateId
3537
3403
  || replacement.reviewerRoleName !== source.reviewerRoleName
3538
3404
  || replacement.deltaRecheck !== undefined
3539
3405
  || !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
3540
3406
  || !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
3541
- || replacementEvent.payload.workItemId !== replacement.workItemId
3542
- || replacementEvent.payload.candidateId !== replacement.candidateId
3543
3407
  || replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
3544
3408
  || replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
3545
3409
  throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
@@ -3564,12 +3428,6 @@ function forceFreshTaskReviewRound(args, store, options) {
3564
3428
  || source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
3565
3429
  throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
3566
3430
  }
3567
- const item = tx.getWorkItem(task.id, source.workItemId);
3568
- const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
3569
- if (item === null || item === undefined || candidate === undefined) {
3570
- throw dataError(`Final Review anchor Candidate is no longer available: `
3571
- + `${source.workItemId}/${source.candidateId}.`);
3572
- }
3573
3431
  const provenance = taskReviewProvenance(tx, task, options);
3574
3432
  if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
3575
3433
  throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
@@ -3598,10 +3456,10 @@ function forceFreshTaskReviewRound(args, store, options) {
3598
3456
  reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
3599
3457
  tx.saveRole(task.id, reviewer);
3600
3458
  }
3601
- let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.workItemId, source.candidateId, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
3459
+ let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
3602
3460
  const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3603
3461
  purpose: "review",
3604
- target: executionTargetForReviewRound(task, created, item, candidate),
3462
+ target: executionTargetForReviewRound(task, created),
3605
3463
  strategy: { mode: "fixed", count: 1 },
3606
3464
  lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
3607
3465
  }, now);
@@ -3611,8 +3469,6 @@ function forceFreshTaskReviewRound(args, store, options) {
3611
3469
  sourceReviewRoundId: source.id,
3612
3470
  ...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
3613
3471
  reviewRoundId: created.id,
3614
- workItemId: created.workItemId,
3615
- candidateId: created.candidateId,
3616
3472
  reviewerRoleName: created.reviewerRoleName,
3617
3473
  taskCandidate: JSON.stringify(created.taskCandidate),
3618
3474
  reason: "source-round-terminal-without-semantic-review",
@@ -3763,15 +3619,9 @@ function retryFailedTaskReviewRound(args, store, options) {
3763
3619
  if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
3764
3620
  throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
3765
3621
  }
3766
- // Re-read the exact current integrated heads and all ChangeSet-linked
3767
- // WorkItem producer roles before touching any record. A moved head,
3768
- // missing anchor, malformed provenance, or reviewer collision fails closed
3769
- // with the old failed Round byte-for-byte unchanged.
3770
- const item = tx.getWorkItem(task.id, round.workItemId);
3771
- if (item === null || item.candidates.every(({ id }) => id !== round.candidateId)) {
3772
- throw dataError(`Final Review anchor Candidate is no longer available: `
3773
- + `${round.workItemId}/${round.candidateId}.`);
3774
- }
3622
+ // Re-read the exact current Task heads and producer roles before touching
3623
+ // any record. A moved head or reviewer collision fails closed with the old
3624
+ // failed Round byte-for-byte unchanged.
3775
3625
  const provenance = taskReviewProvenance(tx, task, options);
3776
3626
  if (!isSameTaskReviewCandidate(round.taskCandidate, provenance.candidate)) {
3777
3627
  throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
@@ -3834,9 +3684,7 @@ function retryFailedTaskReviewRound(args, store, options) {
3834
3684
  const resetRound = retryTaskReviewRound(round);
3835
3685
  tx.saveReviewRound(task.id, resetRound);
3836
3686
  recordTaskEvent(tx, task.id, "review.task-final-retried", {
3837
- reviewRoundId: round.id,
3838
- workItemId: round.workItemId,
3839
- candidateId: round.candidateId
3687
+ reviewRoundId: round.id
3840
3688
  }, now);
3841
3689
  return { round: resetRound, created: true };
3842
3690
  });
@@ -4026,7 +3874,7 @@ function listRuns(args, store, options) {
4026
3874
  * on. This is deliberately narrower than retry: it cannot manufacture a
4027
3875
  * review or fail an arbitrary Round, and every identity/mailbox fence is
4028
3876
  * checked before the old Round changes. The next normal Task completion then
4029
- * creates one fresh Round over the newer integrated heads.
3877
+ * creates one fresh Round over the newer frozen Task heads.
4030
3878
  */
4031
3879
  function settleStaleFinalReviewRun(args, store, options) {
4032
3880
  exactPositionals(args, 1, "Task run settle usage: yui task run settle <task>/<run>.");
@@ -4065,14 +3913,6 @@ function settleStaleFinalReviewRun(args, store, options) {
4065
3913
  if (validation.disposition !== "applied" || validation.round === null) {
4066
3914
  throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
4067
3915
  }
4068
- const item = tx.getWorkItem(task.id, round.workItemId);
4069
- if (item === null) {
4070
- throw dataError(`Work item not found for run ${run.id}: ${round.workItemId}.`);
4071
- }
4072
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
4073
- if (candidate === undefined) {
4074
- throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
4075
- }
4076
3916
  const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review" && entry.status === "active"));
4077
3917
  const activeRoleRun = tx.getActiveAgentRun(task.id, round.reviewerRoleName);
4078
3918
  if (activeReviewerRun !== undefined || activeRoleRun !== null) {
@@ -4159,7 +3999,6 @@ function settleStaleFinalReviewRun(args, store, options) {
4159
3999
  recordTaskEvent(tx, task.id, "run.review-stale-settled", {
4160
4000
  runId: run.id,
4161
4001
  reviewRoundId: round.id,
4162
- candidateId: candidate.id,
4163
4002
  previousTaskCandidate: JSON.stringify(round.taskCandidate),
4164
4003
  currentTaskCandidate: JSON.stringify(currentTaskCandidate)
4165
4004
  }, now);
@@ -4364,7 +4203,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
4364
4203
  .sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
4365
4204
  return workItemIds.length === 0
4366
4205
  ? null
4367
- : `Reviewer Role must be separate from every integrated Candidate producer: `
4206
+ : `Reviewer Role must be separate from every WorkItem Candidate producer: `
4368
4207
  + `${reviewerRoleName} (${workItemIds.join(", ")}).`;
4369
4208
  }
4370
4209
  /**
@@ -4381,7 +4220,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
4381
4220
  function taskReviewProvenance(store, task, options, expected) {
4382
4221
  const candidate = actualTaskReviewCandidateForMutation(store, task, options);
4383
4222
  if (expected !== undefined && !isSameTaskReviewCandidate(candidate, expected)) {
4384
- throw usageError(`Task-final ReviewRound frozen integrated heads changed for its Project set.`);
4223
+ throw usageError(`Task-final ReviewRound frozen Task heads changed for its Project set.`);
4385
4224
  }
4386
4225
  const producerRoles = new Set();
4387
4226
  const producerWorkItemIds = new Map();
@@ -4484,32 +4323,18 @@ function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = []) {
4484
4323
  throw usageError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
4485
4324
  }
4486
4325
  }
4487
- function latestTaskReviewAnchor(store, task) {
4488
- const item = [...store.listWorkItems(task.id)]
4489
- .filter(({ candidates }) => candidates.length > 0)
4490
- .sort((left, right) => (left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id)))
4491
- .at(-1);
4492
- if (item === undefined) {
4493
- throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
4494
- }
4495
- const candidate = item.candidates.at(-1);
4496
- if (candidate === undefined) {
4497
- throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
4498
- }
4499
- return { item, candidate };
4500
- }
4501
4326
  /**
4502
4327
  * Queues a Task-scoped final ReviewRound. The reviewer Role must be separate
4503
4328
  * from the Candidate producer. Returns the round (pending or failed) so the
4504
4329
  * caller can surface it to the CLI for workspace preparation and dispatch.
4505
4330
  */
4506
- function queueTaskReviewRound(store, task, item, candidateId, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
4331
+ function queueTaskReviewRound(store, task, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
4507
4332
  assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id));
4508
4333
  const provenance = taskReviewProvenance(store, task, options, taskCandidate);
4509
4334
  if (!isSameTaskReviewCandidate(provenance.candidate, taskCandidate)) {
4510
- throw usageError(`Task-final ReviewRound integrated heads changed before queueing.`);
4335
+ throw usageError(`Task-final ReviewRound frozen Task heads changed before queueing.`);
4511
4336
  }
4512
- const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, item.id, candidateId, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
4337
+ const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
4513
4338
  store.saveReviewRound(task.id, pending);
4514
4339
  const producerCollision = taskReviewProducerCollision(provenance, config.roleName);
4515
4340
  if (producerCollision !== null) {
@@ -4543,36 +4368,17 @@ function queueTaskReviewRound(store, task, item, candidateId, config, taskCandid
4543
4368
  * (or a failed round is surfaced as a blocker).
4544
4369
  */
4545
4370
  function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4546
- // Any Task-final ReviewRound is durable completion evidence/obligation.
4547
- // Once one exists, later changes to the mutable global review config cannot
4548
- // weaken the requirement or change its reviewer. Before the first such
4549
- // Round, the current global `final` config establishes the initial Round
4550
- // only for integrated delivery.
4371
+ // A Leader-requested ReviewRound is evidence, not policy. Only an explicit
4372
+ // immutable Task contract creates a durable completion obligation; optional
4373
+ // historical Rounds never cause completion to manufacture another Round for
4374
+ // a later head.
4375
+ if (taskFinalContract === undefined || task.projectBindings.length === 0)
4376
+ return null;
4551
4377
  const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
4552
4378
  .filter((round) => ((round.scope ?? "work-item") === "task"
4553
- && (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
4379
+ && sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)));
4554
4380
  const establishedRound = taskRounds.at(-1);
4555
- let config;
4556
- if (taskFinalContract !== undefined) {
4557
- config = taskFinalReviewConfig(taskFinalContract);
4558
- }
4559
- else if (establishedRound === undefined) {
4560
- const globalConfig = store.getReviewConfig();
4561
- // Direct delivery is an explicit low-overhead contract. Mutable global
4562
- // policy must not create a managed Round during direct completion; risk
4563
- // that warrants one promotes the Task to integrated delivery. Any already-
4564
- // established Task Round or immutable contract remains authoritative
4565
- // through the branches above.
4566
- config = taskDeliveryPath(task) === "integrated"
4567
- && globalConfig?.trigger === "final"
4568
- ? globalConfig
4569
- : null;
4570
- }
4571
- else {
4572
- config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
4573
- }
4574
- if (config === null || task.projectBindings.length === 0)
4575
- return null;
4381
+ const config = taskFinalReviewConfig(taskFinalContract);
4576
4382
  const taskCandidate = taskReviewProvenance(store, task, options).candidate;
4577
4383
  const latest = establishedRound;
4578
4384
  if (latest?.status === "running") {
@@ -4593,10 +4399,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4593
4399
  && latest.deltaRecheck !== undefined
4594
4400
  && latest.deltaRecheck.disposition === "requires-full-review") {
4595
4401
  // Fall through to queue a full Review for the same candidate.
4596
- const anchor = taskFinalContract === undefined
4597
- ? latestTaskReviewAnchor(store, task)
4598
- : latestTaskReviewContractAnchor(store, task, taskFinalContract);
4599
- const escalated = queueTaskReviewRound(store, task, anchor.item, anchor.candidate.id, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4402
+ const escalated = queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4600
4403
  // Record the escalation lineage on the delta Round so the full Review
4601
4404
  // is traceable from the non-accepting delta disposition.
4602
4405
  if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
@@ -4617,10 +4420,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4617
4420
  : latest;
4618
4421
  }
4619
4422
  }
4620
- const anchor = taskFinalContract === undefined
4621
- ? latestTaskReviewAnchor(store, task)
4622
- : latestTaskReviewContractAnchor(store, task, taskFinalContract);
4623
- return queueTaskReviewRound(store, task, anchor.item, anchor.candidate.id, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4423
+ return queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
4624
4424
  }
4625
4425
  function resumablePendingFinalTaskReview(store, task, round, config, taskCandidate, taskFinalContract, options) {
4626
4426
  if (taskFinalContract === undefined || round.taskFinalReviewContract === undefined) {
@@ -4685,22 +4485,6 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
4685
4485
  }
4686
4486
  }
4687
4487
  }
4688
- function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
4689
- const anchor = store.listWorkItems(task.id)
4690
- .flatMap((item) => {
4691
- const candidate = governingWorkItemCandidate(item);
4692
- return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
4693
- ? [{ item, candidate }]
4694
- : [];
4695
- })
4696
- .sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
4697
- || left.item.id.localeCompare(right.item.id)))
4698
- .at(-1);
4699
- if (anchor === undefined) {
4700
- throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
4701
- }
4702
- return anchor;
4703
- }
4704
4488
  /**
4705
4489
  * Leader-only retry of an exact failed Task-final review Run. The old failed
4706
4490
  * Run remains the attempt trail, while the semantic ReviewRound is reset to
@@ -4743,15 +4527,7 @@ function retryFailedReviewRun(previous, store, options, now) {
4743
4527
  }
4744
4528
  const currentTaskCandidate = actualTaskReviewCandidateForMutation(tx, task, options);
4745
4529
  if (!isSameTaskReviewCandidate(currentTaskCandidate, round.taskCandidate)) {
4746
- throw usageError(`Task-final ReviewRound ${round.id} no longer matches the latest committed Integration heads.`);
4747
- }
4748
- const item = tx.getWorkItem(task.id, round.workItemId);
4749
- if (item === null) {
4750
- throw dataError(`Work item not found for run ${run.id}: ${round.workItemId}.`);
4751
- }
4752
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
4753
- if (candidate === undefined) {
4754
- throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
4530
+ throw usageError(`Task-final ReviewRound ${round.id} no longer matches the frozen Task heads.`);
4755
4531
  }
4756
4532
  const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
4757
4533
  .filter((entry) => (entry.scope ?? "work-item") === "task"));
@@ -4885,8 +4661,7 @@ function retryFailedReviewRun(previous, store, options, now) {
4885
4661
  tx.saveReviewRound(task.id, resetRound);
4886
4662
  recordTaskEvent(tx, task.id, "run.review-retried", {
4887
4663
  runId: run.id,
4888
- reviewRoundId: round.id,
4889
- candidateId: candidate.id
4664
+ reviewRoundId: round.id
4890
4665
  }, now);
4891
4666
  return { round: resetRound, previousRun: run, created: true };
4892
4667
  });
@@ -5300,8 +5075,8 @@ function yieldRun(args, store, options) {
5300
5075
  const panelGroup = round.executionGroup;
5301
5076
  recordTaskEvent(tx, task.id, "review-group-ready", {
5302
5077
  reviewRoundId: round.id,
5303
- workItemId: round.workItemId,
5304
- candidateId: round.candidateId,
5078
+ ...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
5079
+ ...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
5305
5080
  executionGroupId: panelGroup.id,
5306
5081
  terminalLanes: String(panelGroup.lanes
5307
5082
  .filter(({ status }) => ["yielded", "completed", "failed"].includes(status)).length),
@@ -5315,8 +5090,8 @@ function yieldRun(args, store, options) {
5315
5090
  }
5316
5091
  recordTaskEvent(tx, task.id, "review.completed", {
5317
5092
  reviewRoundId: round.id,
5318
- workItemId: round.workItemId,
5319
- candidateId: round.candidateId,
5093
+ ...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
5094
+ ...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
5320
5095
  reviewBaseCommit: round.reviewBaseCommit,
5321
5096
  evidenceCommit: round.evidenceCommit ?? "none",
5322
5097
  checks: round.checks?.map(({ name, outcome }) => `${name}:${outcome}`)
@@ -5614,18 +5389,12 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5614
5389
  }
5615
5390
  throw usageError(`ReviewRound workspace is not ready: ${round.id}.`);
5616
5391
  }
5617
- const item = tx.getWorkItem(taskId, round.workItemId);
5618
- if (item === null)
5619
- throw dataError(`ReviewRound Work Item not found: ${round.workItemId}.`);
5620
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
5621
- if (candidate === undefined) {
5622
- throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
5623
- }
5624
5392
  const taskScope = (round.scope ?? "work-item") === "task";
5393
+ let item;
5394
+ let candidate;
5625
5395
  if (taskScope) {
5626
- // A Task-scoped final ReviewRound freezes the integrated Task heads in
5627
- // its taskCandidate. The WorkItem Candidate snapshot is irrelevant: the
5628
- // authoritative review base is the first Project's committed head.
5396
+ // A Task-scoped final ReviewRound is anchored directly to its frozen
5397
+ // Task candidate. It deliberately has no synthetic WorkItem Candidate.
5629
5398
  if (round.taskCandidate === undefined) {
5630
5399
  throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound ${round.id} has no frozen Task candidate.`);
5631
5400
  }
@@ -5633,8 +5402,21 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5633
5402
  throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound base does not match its frozen Task candidate: ${round.id}.`);
5634
5403
  }
5635
5404
  }
5636
- else if (candidate.gitSnapshot?.reviewBaseCommit !== round.reviewBaseCommit) {
5637
- throw usageError(`ReviewRound Candidate snapshot changed: ${round.id}.`);
5405
+ else {
5406
+ if (round.workItemId === undefined || round.candidateId === undefined) {
5407
+ throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
5408
+ }
5409
+ item = tx.getWorkItem(taskId, round.workItemId) ?? undefined;
5410
+ if (item === undefined) {
5411
+ throw dataError(`ReviewRound Work Item not found: ${round.workItemId}.`);
5412
+ }
5413
+ candidate = item.candidates.find(({ id }) => id === round.candidateId);
5414
+ if (candidate === undefined) {
5415
+ throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
5416
+ }
5417
+ if (candidate.gitSnapshot?.reviewBaseCommit !== round.reviewBaseCommit) {
5418
+ throw usageError(`ReviewRound Candidate snapshot changed: ${round.id}.`);
5419
+ }
5638
5420
  }
5639
5421
  const task = requireTask(tx, taskId);
5640
5422
  if ((round.scope ?? "work-item") === "task") {
@@ -5671,7 +5453,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5671
5453
  && frozenCommits.get(project.projectId) !== project.commit;
5672
5454
  });
5673
5455
  throw new TaskFinalReviewDispatchDriftError(committedIntegrationMoved
5674
- ? "Task-final ReviewRound frozen integrated heads changed for its Project set."
5456
+ ? "Task-final ReviewRound frozen Task heads changed for its Project set."
5675
5457
  : `Final ReviewRound ${round.id} freezes a candidate that is no longer `
5676
5458
  + "the current Task candidate.");
5677
5459
  }
@@ -5732,9 +5514,11 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5732
5514
  throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound frozen Project heads changed: ${round.id}.`);
5733
5515
  }
5734
5516
  }
5735
- const candidateLabel = candidate.source.type === "run"
5736
- ? `candidate Run ${candidate.source.runId}`
5737
- : `revision ${candidate.workItemRevision}`;
5517
+ const candidateLabel = taskScope
5518
+ ? "frozen Task candidate"
5519
+ : candidate.source.type === "run"
5520
+ ? `candidate Run ${candidate.source.runId}`
5521
+ : `revision ${candidate.workItemRevision}`;
5738
5522
  const frozenHeads = taskScope
5739
5523
  ? round.taskCandidate.projects
5740
5524
  .map(({ projectId, commit }) => `${projectId}@${commit}`)
@@ -5772,22 +5556,26 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5772
5556
  .map(({ projectId }) => (`yui project show ${projectId}; yui project knowledge list ${projectId}`))
5773
5557
  .join(" | ");
5774
5558
  const rawInput = [
5775
- `Review ${scopeLabel} WorkItem ${item.id} ${candidateLabel}.`,
5559
+ taskScope
5560
+ ? `Review the ${scopeLabel} ${candidateLabel}.`
5561
+ : `Review ${scopeLabel} WorkItem ${item.id} ${candidateLabel}.`,
5776
5562
  `ReviewRound: ${round.id}`,
5777
5563
  `Review scope: ${taskScope ? "task" : "work-item"}`,
5778
5564
  `Review base commit: ${round.reviewBaseCommit}`,
5779
5565
  ...(taskScope
5780
- ? [`Frozen integrated Task heads: ${frozenHeads}`]
5566
+ ? [`Frozen Task heads: ${frozenHeads}`]
5781
5567
  : [`Candidate snapshot base: ${round.reviewBaseCommit}`]),
5782
5568
  `Project Policy pointers: ${projectPolicyPointers || "none"}`,
5783
5569
  `Review workspace source: exact workspace attached to this Reviewer Lane`,
5784
- `Candidate summary: ${candidate.summary}`,
5785
- `Acceptance criteria: ${item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
5570
+ `Candidate summary: ${taskScope ? task.title : candidate.summary}`,
5571
+ `Acceptance criteria: ${taskScope
5572
+ ? "Task objective, maintained decisions, and Project Policy"
5573
+ : item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
5786
5574
  ...(taskScope ? [deltaContext !== "" ? deltaContext : findingContext] : []),
5787
5575
  "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.",
5788
5576
  "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.",
5789
5577
  ...(round.scope === "task" && round.deltaRecheck === undefined
5790
- ? ["This is the one final Task Review: inspect every bound Project at the frozen integrated heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
5578
+ ? ["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."]
5791
5579
  : []),
5792
5580
  "You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound-owned workspace.",
5793
5581
  "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.",
@@ -5848,7 +5636,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5848
5636
  action: "review-round",
5849
5637
  subject: {
5850
5638
  taskId,
5851
- workItemId: item.id,
5639
+ ...(item === undefined ? {} : { workItemId: item.id }),
5852
5640
  reviewRoundId: round.id,
5853
5641
  executionGroupId: runningGroup.id,
5854
5642
  executionLaneId: lane.id
@@ -5864,7 +5652,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5864
5652
  workspace: laneWorkspace
5865
5653
  }, now);
5866
5654
  createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), assignment, now, {
5867
- workItemId: item.id,
5655
+ ...(item === undefined ? {} : { workItemId: item.id }),
5868
5656
  purpose: "review",
5869
5657
  reviewRoundId: round.id,
5870
5658
  executionGroupId: runningGroup.id,
@@ -5901,7 +5689,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5901
5689
  taskId,
5902
5690
  roleName: unboundRun.roleName,
5903
5691
  purpose: "review",
5904
- workItemId: item.id,
5692
+ ...(item === undefined ? {} : { workItemId: item.id }),
5905
5693
  reviewRoundId: round.id
5906
5694
  }, now);
5907
5695
  const created = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
@@ -5912,7 +5700,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5912
5700
  tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
5913
5701
  enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
5914
5702
  runRef(taskId, created.id),
5915
- workItemRef(taskId, item.id)
5703
+ ...(item === undefined ? [] : [workItemRef(taskId, item.id)])
5916
5704
  ]);
5917
5705
  recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
5918
5706
  }
@@ -5934,7 +5722,7 @@ export function failPendingReviewRound(taskId, reviewRoundId, summary, store, op
5934
5722
  const terminal = finishReviewRound(round, "failed", summary, now);
5935
5723
  tx.saveReviewRound(taskId, terminal);
5936
5724
  enqueueWork(tx, leaderMailbox(taskId), "review-failed", now, [
5937
- workItemRef(taskId, round.workItemId)
5725
+ ...(round.workItemId === undefined ? [] : [workItemRef(taskId, round.workItemId)])
5938
5726
  ]);
5939
5727
  return terminal;
5940
5728
  });
@@ -6068,7 +5856,7 @@ function requirePublishedTreeAuthorization(store, proof) {
6068
5856
  if (authorization === undefined) {
6069
5857
  throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
6070
5858
  + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
6071
- ? "the direct Task-main head"
5859
+ ? "the Leader-owned Task-main head"
6072
5860
  : `Task-final Review ${proof.reviewRoundId}`}.`);
6073
5861
  }
6074
5862
  return authorization;
@@ -6337,11 +6125,6 @@ function parseTaskPriority(value) {
6337
6125
  return value;
6338
6126
  throw usageError(`Invalid Task priority: ${value}.`);
6339
6127
  }
6340
- function parseTaskDelivery(value) {
6341
- if (value === "direct" || value === "integrated")
6342
- return value;
6343
- throw usageError(`Task delivery is invalid: ${value}. Use direct or integrated.`);
6344
- }
6345
6128
  function parseTaskTags(value) {
6346
6129
  const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
6347
6130
  if (tags.length === 0)