@zq-silk/yui 0.8.3 → 0.8.6

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 (60) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +46 -16
  3. package/dist/cli/commandCatalog.js +23 -11
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +154 -16
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +386 -138
  9. package/dist/commands/taskCompletionGate.js +36 -24
  10. package/dist/commands/taskContextCommand.js +6 -1
  11. package/dist/commands/taskInputCommands.js +48 -10
  12. package/dist/commands/taskNextActionCommand.js +36 -3
  13. package/dist/context/sessionBootstrapManifest.js +82 -1
  14. package/dist/controller/clientRuntime.js +7 -7
  15. package/dist/controller/controller.js +16 -8
  16. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  17. package/dist/controller/handoverCandidate.js +10 -3
  18. package/dist/controller/sessionNotify.js +4 -22
  19. package/dist/executor/agentAdapter.js +2 -2
  20. package/dist/executor/agentExecutor.js +25 -5
  21. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  22. package/dist/integration/gitIntegrationService.js +50 -2
  23. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  24. package/dist/observability/executionAudit.js +47 -1
  25. package/dist/observability/faultClassification.js +6 -4
  26. package/dist/observability/orchestrationMetrics.js +196 -0
  27. package/dist/operator/operatorSessionHistory.js +36 -0
  28. package/dist/release/releaseHandover.js +7 -5
  29. package/dist/release/runtimeRelease.js +15 -0
  30. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  31. package/dist/review/deltaRecheck.js +3 -2
  32. package/dist/review/reviewFindingLedger.js +5 -4
  33. package/dist/review/reviewOutcomeClassifier.js +252 -54
  34. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  35. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  36. package/dist/run/runIdentity.js +10 -70
  37. package/dist/runtime/agentHost.js +3 -4
  38. package/dist/runtime/codexAppServerRuntime.js +6 -0
  39. package/dist/runtime/firstProgressStopLoss.js +52 -0
  40. package/dist/runtime/launchBroker.js +10 -2
  41. package/dist/runtime/runtimeDeadlines.js +14 -0
  42. package/dist/runtime/sessionTitle.js +24 -12
  43. package/dist/runtime/structuredProviderHost.js +7 -1
  44. package/dist/runtime/tmuxAdapters.js +10 -3
  45. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  46. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  47. package/dist/scheduler/wakeReason.js +1 -0
  48. package/dist/storage/sqliteStore.js +8 -1
  49. package/dist/storage/taskStore.js +10 -1
  50. package/dist/task/completionReadiness.js +48 -19
  51. package/dist/task/deliveryGuard.js +3 -1
  52. package/dist/task/nextAction.js +145 -52
  53. package/dist/task/repairWave.js +14 -1
  54. package/dist/task/task.js +10 -0
  55. package/dist/web/webSnapshot.js +7 -1
  56. package/i18n/README.zh-CN.md +28 -8
  57. package/package.json +1 -1
  58. package/skills/yui-leader/SKILL.md +73 -31
  59. package/skills/yui-operator/SKILL.md +51 -10
  60. package/skills/yui-reviewer/SKILL.md +23 -0
@@ -8,7 +8,7 @@ import { createTaskEvent } from "../event/taskEvent.js";
8
8
  import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
9
9
  import { readCommandText } from "./textInput.js";
10
10
  import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
11
- import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
11
+ import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
12
12
  import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
13
13
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
14
14
  import { defaultTableWidth, renderTable } from "../output/table.js";
@@ -35,7 +35,7 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
35
35
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
36
36
  import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
37
37
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
38
- import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
38
+ import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskDeliveryPath, updateTaskMetadata } from "../task/task.js";
39
39
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
40
40
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
41
41
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
@@ -44,6 +44,8 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
44
44
  import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
45
45
  import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
46
46
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
47
+ import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
48
+ import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
47
49
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
48
50
  import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
49
51
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
@@ -52,7 +54,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
52
54
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
53
55
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
54
56
  import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
55
- import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
57
+ import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
56
58
  import { runGrantCommand } from "./grantCommands.js";
57
59
  import { runWorkflowCommand } from "./workflowCommands.js";
58
60
  import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
@@ -81,21 +83,17 @@ function legacyWorkItemReviewConfig(config) {
81
83
  return config?.trigger === "final" ? null : config;
82
84
  }
83
85
  function storedTaskFinalReviewContract(store, taskId) {
84
- const contracts = store.listWorkItems(taskId)
85
- .flatMap((item) => {
86
- const candidate = governingWorkItemCandidate(item);
87
- return candidate?.taskFinalReviewContract === undefined
88
- ? []
89
- : [candidate.taskFinalReviewContract];
90
- });
91
- const first = contracts[0];
92
- if (first === undefined)
93
- return undefined;
94
- validateTaskFinalReviewContract(first);
95
- if (contracts.some((contract) => !sameTaskFinalReviewContract(first, contract))) {
96
- throw dataError(`Task ${taskId} contains conflicting final-review contracts.`);
86
+ return storedTaskFinalReviewContractResolution(store, taskId)?.effective;
87
+ }
88
+ function storedTaskFinalReviewContractResolution(store, taskId) {
89
+ try {
90
+ return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId), store.listEvents(taskId));
91
+ }
92
+ catch (error) {
93
+ throw dataError(error instanceof Error
94
+ ? error.message
95
+ : `Task ${taskId} contains conflicting final-review contracts.`);
97
96
  }
98
- return first;
99
97
  }
100
98
  /**
101
99
  * Resolve one exact Task-local contract before the caller performs any write.
@@ -367,12 +365,14 @@ function taskProjectCommand(args, store, options) {
367
365
  return output(`Added Project to ${updated.id}\n`, { task: updated });
368
366
  }
369
367
  function updateTaskCommand(args, store, options) {
370
- const optionNames = new Set(["--title", "--description", "--priority", "--tags", "--due-at"]);
368
+ const optionNames = new Set([
369
+ "--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
370
+ ]);
371
371
  const flagOptions = new Set([
372
372
  "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
373
373
  "--require-integration"
374
374
  ]);
375
- 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] [--require-integration].";
375
+ 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].";
376
376
  const parsed = parseTail(args, optionNames, usage, flagOptions);
377
377
  exactPositionals(parsed.positionals, 1, usage);
378
378
  if (parsed.options.size === 0)
@@ -396,19 +396,42 @@ function updateTaskCommand(args, store, options) {
396
396
  const tags = parsed.options.has("--tags")
397
397
  ? parseTaskTags(requiredOption(parsed.options, "--tags"))
398
398
  : undefined;
399
+ const requestedDelivery = parsed.options.has("--delivery")
400
+ ? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
401
+ : undefined;
402
+ if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
403
+ throw usageError("--delivery direct conflicts with --require-integration.", usage);
404
+ }
405
+ const enableIntegration = requestedDelivery === "integrated"
406
+ || parsed.options.has("--require-integration");
399
407
  const now = clock(options);
400
408
  const result = store.transaction((tx) => {
401
409
  const current = requireTask(tx, parsed.positionals[0]);
402
410
  if (current.status === "archived")
403
411
  throw usageError(`Task is archived: ${current.id}.`);
404
- if (parsed.options.has("--require-integration") && current.status === "completed") {
412
+ if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
413
+ && current.projectBindings.length === 0) {
414
+ throw usageError(`Task ${current.id} has no Project; delivery selection is not applicable.`);
415
+ }
416
+ if (requestedDelivery === "direct" && current.requireIntegration === true) {
417
+ throw usageError(`Task ${current.id} already uses integrated delivery and cannot be downgraded to direct.`);
418
+ }
419
+ if (enableIntegration && current.status === "completed") {
405
420
  throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
406
421
  }
422
+ if (enableIntegration && current.requireIntegration !== true) {
423
+ assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
424
+ }
407
425
  if (parsed.options.size === 1
408
- && parsed.options.has("--require-integration")
426
+ && enableIntegration
409
427
  && current.requireIntegration === true) {
410
428
  return { task: current, integrationState: "already-enabled" };
411
429
  }
430
+ if (parsed.options.size === 1
431
+ && requestedDelivery === "direct"
432
+ && taskDeliveryPath(current) === "direct") {
433
+ return { task: current, integrationState: "already-direct" };
434
+ }
412
435
  const updated = updateTaskMetadata(current, {
413
436
  ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
414
437
  ...(parsed.options.has("--description")
@@ -423,31 +446,93 @@ function updateTaskCommand(args, store, options) {
423
446
  ...(dueAt === undefined
424
447
  ? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
425
448
  : { dueAt }),
426
- ...(parsed.options.has("--require-integration") ? { requireIntegration: true } : {})
449
+ ...(enableIntegration ? { requireIntegration: true } : {})
427
450
  }, now);
428
451
  tx.saveTask(updated);
429
452
  recordTaskEvent(tx, updated.id, "task.updated", {
430
453
  status: updated.status,
431
- ...(parsed.options.has("--require-integration")
432
- ? { completionEvidence: "integration-required" }
433
- : {})
454
+ ...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
455
+ ? {}
456
+ : {
457
+ completionEvidence: enableIntegration
458
+ ? "integration-required"
459
+ : "direct",
460
+ deliveryPath: taskDeliveryPath(updated)
461
+ })
434
462
  }, now);
435
463
  enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
436
464
  return {
437
465
  task: updated,
438
- integrationState: parsed.options.has("--require-integration")
466
+ integrationState: enableIntegration
439
467
  ? "enabled"
440
- : "unchanged"
468
+ : requestedDelivery === "direct"
469
+ ? "direct"
470
+ : "unchanged"
441
471
  };
442
472
  });
443
- if (result.integrationState !== "already-enabled") {
473
+ if (result.integrationState !== "already-enabled"
474
+ && result.integrationState !== "already-direct") {
444
475
  notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
445
476
  }
446
477
  return result.integrationState === "enabled"
447
- ? `Updated task ${result.task.id}\nCompletion evidence enabled: WorkItem, ChangeSet, and committed Integration required\n`
478
+ ? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
448
479
  : result.integrationState === "already-enabled"
449
- ? `Task ${result.task.id} completion evidence is already enabled\n`
450
- : `Updated task ${result.task.id}\n`;
480
+ ? `Task ${result.task.id} already uses integrated delivery\n`
481
+ : result.integrationState === "already-direct"
482
+ ? `Task ${result.task.id} already uses direct delivery\n`
483
+ : result.integrationState === "direct"
484
+ ? `Updated task ${result.task.id}\nDelivery: direct\n`
485
+ : `Updated task ${result.task.id}\n`;
486
+ }
487
+ function assertTaskDeliveryPromotionEligible(store, task, snapshot) {
488
+ const evidence = [
489
+ ...store.listWorkItems(task.id).map(({ id }) => `WorkItem ${id}`),
490
+ ...store.listChangeSets(task.id).map(({ id }) => `ChangeSet ${id}`),
491
+ ...store.listIntegrationAttempts(task.id).map(({ id }) => `IntegrationAttempt ${id}`),
492
+ ...store.listReviewRounds(task.id).map(({ id }) => `ReviewRound ${id}`)
493
+ ];
494
+ if (evidence.length > 0) {
495
+ throw usageError(`Task ${task.id} cannot promote to integrated delivery after delivery evidence exists: `
496
+ + `${evidence.join(", ")}. Create an integrated replacement Task or keep the current direct contract.`);
497
+ }
498
+ if (task.status !== "draft" && task.status !== "active") {
499
+ throw usageError(`Task ${task.id} must be Draft or Active to promote delivery; current status is ${task.status}.`);
500
+ }
501
+ const workspace = store.getTaskWorkspace(task.id);
502
+ if (task.status === "draft" && workspace === null)
503
+ return;
504
+ if (snapshot === undefined) {
505
+ throw usageError(`Task ${task.id} delivery promotion requires a CLI-verified clean Task-main snapshot.`);
506
+ }
507
+ if (workspace === null
508
+ || workspace.owner.type !== "task"
509
+ || workspace.owner.taskId !== task.id) {
510
+ throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
511
+ }
512
+ const snapshotIds = snapshot.schemaVersion === 1 && Array.isArray(snapshot.projects)
513
+ ? snapshot.projects.map(({ projectId }) => projectId)
514
+ : [];
515
+ if (snapshotIds.length !== task.projectBindings.length
516
+ || new Set(snapshotIds).size !== snapshotIds.length) {
517
+ throw usageError(`Task-main promotion snapshot does not match bound Projects: ${task.id}.`);
518
+ }
519
+ for (const binding of task.projectBindings) {
520
+ const project = snapshot.projects.find(({ projectId }) => projectId === binding.projectId);
521
+ const entry = workspace.entries.find(({ projectId }) => projectId === binding.projectId);
522
+ if (project === undefined
523
+ || entry === undefined
524
+ || entry.access !== "write"
525
+ || project.directory !== entry.directory
526
+ || project.branch !== entry.branch
527
+ || project.baseCommit !== entry.baseCommit) {
528
+ throw usageError(`Task-main promotion snapshot changed before mutation: ${task.id}/${binding.projectId}.`);
529
+ }
530
+ if (project.headCommit !== project.baseCommit) {
531
+ throw usageError(`Task ${task.id} main already advanced for Project ${binding.projectId}; `
532
+ + "cannot promote without losing ChangeSet provenance. Create an integrated replacement "
533
+ + "Task or keep the current direct contract.");
534
+ }
535
+ }
451
536
  }
452
537
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
453
538
  export function runTaskOutputCommand(args, store, options = {}) {
@@ -488,13 +573,20 @@ function createTaskCommand(args, store, options) {
488
573
  notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
489
574
  return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
490
575
  + `Assigned role: ${created.leader.name}\n`
576
+ + `Delivery: ${taskDeliveryPath(created.task)}\n`
491
577
  + (created.task.requireIntegration
492
578
  ? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
493
- : "Completion: delivery integration not required\n"), { task: created.task, leader: created.leader });
579
+ : created.task.projectBindings.length > 0
580
+ ? "Completion: clean committed Task main required; no WorkItem, ChangeSet, IntegrationAttempt, or managed ReviewRound required\n"
581
+ : "Completion: no Project delivery evidence required\n"), {
582
+ task: created.task,
583
+ leader: created.leader,
584
+ deliveryPath: taskDeliveryPath(created.task)
585
+ });
494
586
  }
495
587
  function parseTaskCreation(args, store) {
496
- const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--require-integration].";
497
- const parsed = parseMultiValueTail(args, new Set(), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
588
+ const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--delivery <direct|integrated>] [--require-integration].";
589
+ const parsed = parseMultiValueTail(args, new Set(["--delivery"]), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
498
590
  exactPositionals(parsed.positionals, 1, usage);
499
591
  const projectReferences = parsed.multiOptions.get("--project") ?? [];
500
592
  const baseOptions = parsed.multiOptions.get("--base") ?? [];
@@ -510,6 +602,16 @@ function parseTaskCreation(args, store) {
510
602
  if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
511
603
  throw usageError("A Task cannot bind the same Project more than once.");
512
604
  }
605
+ const requestedDelivery = parsed.options.has("--delivery")
606
+ ? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
607
+ : undefined;
608
+ if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
609
+ && projects.length === 0) {
610
+ throw usageError("Delivery selection requires at least one --project.", usage);
611
+ }
612
+ if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
613
+ throw usageError("--delivery direct conflicts with --require-integration.", usage);
614
+ }
513
615
  const bases = new Map();
514
616
  for (const option of baseOptions) {
515
617
  const separator = option.indexOf("=");
@@ -540,7 +642,8 @@ function parseTaskCreation(args, store) {
540
642
  baseRef: bases.get(project.id) ?? project.developmentBranch
541
643
  })),
542
644
  defaultProjectIds,
543
- requireIntegration: parsed.options.has("--require-integration")
645
+ requireIntegration: requestedDelivery === "integrated"
646
+ || parsed.options.has("--require-integration")
544
647
  };
545
648
  }
546
649
  function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
@@ -550,6 +653,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
550
653
  store.saveRole(task.id, leader);
551
654
  recordTaskEvent(store, task.id, "task.created", {
552
655
  status: task.status,
656
+ deliveryPath: taskDeliveryPath(task),
553
657
  ...(defaultProjectIds.length === 0
554
658
  ? {}
555
659
  : { defaultProjectIds: defaultProjectIds.join(",") })
@@ -598,11 +702,16 @@ function showTaskCommand(args, store) {
598
702
  `Task: ${task.id}`,
599
703
  `Title: ${task.title}`,
600
704
  `Status: ${task.status}`,
705
+ `Delivery: ${taskDeliveryPath(task)}`,
601
706
  ...(task.description === undefined ? [] : [`Description: ${task.description}`]),
602
707
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
603
708
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
604
709
  ...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
605
- `Completion evidence: ${task.requireIntegration === true ? "required" : "not required"}`,
710
+ `Completion evidence: ${task.requireIntegration === true
711
+ ? "WorkItem, ChangeSet, and committed Integration required"
712
+ : task.projectBindings.length > 0
713
+ ? "clean committed Task main required"
714
+ : "no Project evidence required"}`,
606
715
  ...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
607
716
  ...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
608
717
  ...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
@@ -631,7 +740,12 @@ function showTaskCommand(args, store) {
631
740
  `Created: ${presentTime(task.createdAt, timeZone)}`,
632
741
  `Updated: ${presentTime(task.updatedAt, timeZone)}`
633
742
  ].join("\n").concat("\n");
634
- return output(rendered, { task, counts, hasBrief: brief !== null });
743
+ return output(rendered, {
744
+ task,
745
+ deliveryPath: taskDeliveryPath(task),
746
+ counts,
747
+ hasBrief: brief !== null
748
+ });
635
749
  }
636
750
  function activateTaskCommand(args, store, options) {
637
751
  exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
@@ -677,14 +791,18 @@ function completeTaskCommand(args, store, options) {
677
791
  task,
678
792
  changed: false,
679
793
  runtimeCleanupTargets: [],
794
+ completionAdvisories: [],
680
795
  finalReview: undefined,
681
796
  publishedTreeAuthorization: undefined
682
797
  };
683
798
  }
684
799
  const taskFinalContract = preflight.taskFinalReviewContract;
800
+ const actualTaskCandidate = task.projectBindings.length === 0
801
+ ? undefined
802
+ : actualTaskReviewCandidateForMutation(tx, task, options);
685
803
  const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
686
804
  ? undefined
687
- : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskReviewCandidateForMutation(tx, task, options));
805
+ : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
688
806
  const requiresContractHandoff = publishedTreeProof !== undefined
689
807
  && taskFinalContract !== undefined;
690
808
  if (requiresContractHandoff && actor !== "leader") {
@@ -695,6 +813,7 @@ function completeTaskCommand(args, store, options) {
695
813
  task,
696
814
  changed: false,
697
815
  runtimeCleanupTargets: [],
816
+ completionAdvisories: [],
698
817
  finalReview: undefined,
699
818
  publishedTreeAuthorization: {
700
819
  event,
@@ -752,6 +871,7 @@ function completeTaskCommand(args, store, options) {
752
871
  task,
753
872
  changed: false,
754
873
  runtimeCleanupTargets: [],
874
+ completionAdvisories: [],
755
875
  finalReview,
756
876
  resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
757
877
  terminalizedLeaderRun,
@@ -783,13 +903,29 @@ function completeTaskCommand(args, store, options) {
783
903
  by: actor,
784
904
  projectId: publishedTreeProof.projectId,
785
905
  publicationId: publishedTreeProof.publicationId,
786
- reviewRoundId: publishedTreeProof.reviewRoundId,
906
+ ...(publishedTreeProof.reviewRoundId === undefined
907
+ ? {}
908
+ : { reviewRoundId: publishedTreeProof.reviewRoundId }),
787
909
  localCommit: publishedTreeProof.localCommit,
788
910
  remoteCommit: publishedTreeProof.remoteCommit,
789
911
  tree: publishedTreeProof.tree
790
912
  }, now);
791
913
  }
792
- recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
914
+ recordTaskEvent(tx, task.id, "task.completed", {
915
+ by: actor,
916
+ summary,
917
+ deliveryPath: taskDeliveryPath(task),
918
+ ...(actualTaskCandidate === undefined
919
+ ? {}
920
+ : {
921
+ projectHeads: actualTaskCandidate.projects
922
+ .map(({ projectId, commit }) => `${projectId}@${commit}`)
923
+ .join(",")
924
+ }),
925
+ ...(readiness.advisories.length === 0
926
+ ? {}
927
+ : { cleanupAdvisories: String(readiness.advisories.length) })
928
+ }, now);
793
929
  // A terminal Task must never leave a Task-lane signal that can wake it.
794
930
  // The durable records remain intact; only the derived mailbox work is
795
931
  // discarded at this lifecycle boundary.
@@ -805,6 +941,7 @@ function completeTaskCommand(args, store, options) {
805
941
  task: completed,
806
942
  changed: true,
807
943
  runtimeCleanupTargets,
944
+ completionAdvisories: readiness.advisories,
808
945
  finalReview: undefined,
809
946
  resumedPendingFinalReview: false,
810
947
  terminalizedLeaderRun,
@@ -841,9 +978,17 @@ function completeTaskCommand(args, store, options) {
841
978
  [TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
842
979
  });
843
980
  }
844
- return output(result.changed
981
+ const completionOutput = result.changed
845
982
  ? `Completed task ${result.task.id}\n`
846
- : `Task ${result.task.id} is already completed\n`);
983
+ : `Task ${result.task.id} is already completed\n`;
984
+ const advisoryOutput = result.completionAdvisories.length === 0
985
+ ? ""
986
+ : `Cleanup advisories (non-blocking; settle before archive):\n`
987
+ + result.completionAdvisories.map((advisory) => (`- ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.fix}`)).join("\n").concat("\n");
988
+ return output(completionOutput + advisoryOutput, {
989
+ task: result.task,
990
+ completionAdvisories: result.completionAdvisories
991
+ });
847
992
  }
848
993
  function reopenTaskCommand(args, store, options) {
849
994
  exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
@@ -2678,6 +2823,8 @@ function reviewWork(args, store, options) {
2678
2823
  */
2679
2824
  function taskReviewCommand(args, store, options) {
2680
2825
  const [command, ...rest] = args;
2826
+ if (command === "rebind")
2827
+ return rebindTaskFinalReviewContract(rest, store, options);
2681
2828
  if (command === "request")
2682
2829
  return requestTaskReviewRound(rest, store, options);
2683
2830
  if (command === "force-fresh")
@@ -2692,6 +2839,115 @@ function taskReviewCommand(args, store, options) {
2692
2839
  ? "Task review command is required."
2693
2840
  : `Unknown command: task review ${command}`);
2694
2841
  }
2842
+ export function parseTaskFinalReviewContractRebindRequest(args) {
2843
+ const usage = "Task review rebind usage: yui task review rebind <task> "
2844
+ + "--from-control <digest> --to-control <digest> "
2845
+ + "--from-release <release-id> --to-release <release-id>.";
2846
+ const parsed = parseTail(args, new Set(["--from-control", "--to-control", "--from-release", "--to-release"]), usage);
2847
+ exactPositionals(parsed.positionals, 1, usage);
2848
+ return {
2849
+ taskId: parsed.positionals[0],
2850
+ fromControlPlaneDigest: requiredOption(parsed.options, "--from-control"),
2851
+ toControlPlaneDigest: requiredOption(parsed.options, "--to-control"),
2852
+ fromReleaseId: requiredOption(parsed.options, "--from-release"),
2853
+ toReleaseId: requiredOption(parsed.options, "--to-release")
2854
+ };
2855
+ }
2856
+ function rebindTaskFinalReviewContract(args, store, options) {
2857
+ const request = parseTaskFinalReviewContractRebindRequest(args);
2858
+ const environment = options.environment ?? {};
2859
+ const usage = "Task review rebind usage: yui task review rebind <task> "
2860
+ + "--from-control <digest> --to-control <digest> "
2861
+ + "--from-release <release-id> --to-release <release-id>.";
2862
+ if (!isCurrentGlobalOperator(store, environment)) {
2863
+ throw usageError("Task-final Review contract rebind requires the authenticated global Operator session.", usage);
2864
+ }
2865
+ const proof = options.taskFinalReviewRebindProof;
2866
+ if (proof === undefined
2867
+ || proof.schemaVersion !== 1
2868
+ || proof.taskId !== request.taskId
2869
+ || proof.fromControlPlaneDigest !== request.fromControlPlaneDigest
2870
+ || proof.toControlPlaneDigest !== request.toControlPlaneDigest
2871
+ || proof.fromRelease.releaseId !== request.fromReleaseId
2872
+ || proof.toRelease.releaseId !== request.toReleaseId) {
2873
+ throw usageError("Task-final Review contract rebind proof is missing or does not match the explicit request.", usage);
2874
+ }
2875
+ const now = clock(options);
2876
+ const result = store.transaction((tx) => {
2877
+ const task = requireTask(tx, request.taskId);
2878
+ if (task.status !== "active") {
2879
+ throw usageError(`Task is not active: ${task.id}.`);
2880
+ }
2881
+ if (task.projectBindings.length === 0) {
2882
+ throw usageError(`Task final-review contract requires a Project-backed Task: ${task.id}.`);
2883
+ }
2884
+ if (!isCurrentGlobalOperator(tx, environment)) {
2885
+ throw usageError("Task-final Review contract rebind Operator identity drifted before commit.");
2886
+ }
2887
+ const resolution = storedTaskFinalReviewContractResolution(tx, task.id);
2888
+ if (resolution === undefined) {
2889
+ throw usageError(`Task final-review contract is missing for ${task.id}.`);
2890
+ }
2891
+ const exactExisting = resolution.rebinds.at(-1);
2892
+ if (resolution.effective.controlPlaneDigest === request.toControlPlaneDigest) {
2893
+ if (exactExisting !== undefined
2894
+ && exactExisting.fromContract.controlPlaneDigest === request.fromControlPlaneDigest
2895
+ && exactExisting.toContract.controlPlaneDigest === request.toControlPlaneDigest
2896
+ && exactExisting.fromRelease.releaseId === request.fromReleaseId
2897
+ && exactExisting.toRelease.releaseId === request.toReleaseId
2898
+ && exactExisting.handoverId === proof.handoverId) {
2899
+ return { rebind: exactExisting, changed: false };
2900
+ }
2901
+ throw usageError(`Task final-review contract already targets ${request.toControlPlaneDigest} without the requested proof tuple.`);
2902
+ }
2903
+ if (resolution.effective.controlPlaneDigest !== request.fromControlPlaneDigest) {
2904
+ throw usageError(`Task final-review contract source control-plane digest drifted for ${task.id}.`);
2905
+ }
2906
+ const activeRound = tx.listReviewRounds(task.id).find((round) => ((round.scope ?? "work-item") === "task"
2907
+ && (round.status === "pending" || round.status === "running")));
2908
+ if (activeRound !== undefined) {
2909
+ throw usageError(`Task final-review contract cannot rebind while ReviewRound ${activeRound.id} is ${activeRound.status}.`);
2910
+ }
2911
+ const activeLeaderRun = tx.getActiveAgentRun(task.id, "leader");
2912
+ if (activeLeaderRun !== null) {
2913
+ throw usageError(`Task final-review contract cannot rebind while Leader Run ${activeLeaderRun.id} is active.`);
2914
+ }
2915
+ const leaderSessions = tx.getTaskRoleSessionSet(task.id, "leader");
2916
+ if (leaderSessions !== null) {
2917
+ if (leaderSessions.inFlight !== null) {
2918
+ throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
2919
+ }
2920
+ const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
2921
+ if (liveLeaderSession !== undefined) {
2922
+ throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
2923
+ }
2924
+ if (Object.keys(leaderSessions.sessions).length > 0
2925
+ || leaderSessions.providerBinding !== null) {
2926
+ tx.saveTaskRoleSessionSet(retireTaskRoleSessionsForWorkspace(leaderSessions, now));
2927
+ }
2928
+ }
2929
+ const rebind = createTaskFinalReviewContractRebind({
2930
+ taskId: task.id,
2931
+ reviewerRoleName: resolution.effective.reviewerRoleName,
2932
+ fromContract: resolution.effective,
2933
+ toControlPlaneDigest: proof.toControlPlaneDigest,
2934
+ fromRelease: proof.fromRelease,
2935
+ toRelease: proof.toRelease,
2936
+ handoverId: proof.handoverId,
2937
+ authorizedBy: `operator:${environment.YUI_AGENT_ID ?? "unknown"}`
2938
+ });
2939
+ const event = recordTaskEventRecord(tx, task.id, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, taskFinalReviewContractRebindPayload(rebind), now);
2940
+ enqueueWork(tx, leaderMailbox(task.id), wakeReason("review-contract-rebound", event.id), now, [eventRef(task.id, event.id)]);
2941
+ return { rebind, event, changed: true };
2942
+ });
2943
+ if (result.changed) {
2944
+ notifyMailbox(options.runtime, leaderMailbox(request.taskId), request.taskId);
2945
+ }
2946
+ return output(result.changed
2947
+ ? `Rebound Task-final Review contract for ${request.taskId} from `
2948
+ + `${request.fromControlPlaneDigest} to ${request.toControlPlaneDigest}.\n`
2949
+ : `Task-final Review contract rebind is already recorded for ${request.taskId}.\n`, result);
2950
+ }
2695
2951
  function resolveReviewExecutionGroup(args, store, options) {
2696
2952
  const usage = "Task review group resolve usage: yui task review group resolve <task>/<review-round> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...].";
2697
2953
  if (args[0] !== "resolve")
@@ -2786,8 +3042,8 @@ function resolveReviewExecutionGroup(args, store, options) {
2786
3042
  /**
2787
3043
  * Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
2788
3044
  * Findings are extracted automatically from completed Rounds; these commands
2789
- * let the Leader inspect the ledger, disposition each finding, and plan the
2790
- * parallel repair wave.
3045
+ * let the Leader inspect the ledger, disposition each finding, and plan one
3046
+ * convergent repair unit by default. Parallel fan-out is explicit.
2791
3047
  */
2792
3048
  function reviewFindingCommand(args, store, options) {
2793
3049
  const [command, ...rest] = args;
@@ -2858,11 +3114,15 @@ function disposeReviewFindingCommand(args, store, options) {
2858
3114
  return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
2859
3115
  }
2860
3116
  function planReviewRepairWave(args, store, options) {
2861
- const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--create].";
2862
- const parsed = parseTail(args, new Set(), usage, new Set(["--create"]));
3117
+ const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--strategy <consolidated|parallel>] [--create].";
3118
+ const parsed = parseTail(args, new Set(["--strategy"]), usage, new Set(["--create"]));
2863
3119
  exactPositionals(parsed.positionals, 1, usage);
2864
3120
  const task = requireTask(store, parsed.positionals[0]);
2865
- const groups = planRepairGroups(store, task.id);
3121
+ const strategy = parsed.options.get("--strategy") ?? "consolidated";
3122
+ if (strategy !== "consolidated" && strategy !== "parallel") {
3123
+ throw usageError(`Review repair strategy is invalid: ${strategy}.`, usage);
3124
+ }
3125
+ const groups = repairGroupsForStrategy(planRepairGroups(store, task.id), strategy);
2866
3126
  if (groups.length === 0) {
2867
3127
  return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
2868
3128
  }
@@ -2903,7 +3163,7 @@ function planReviewRepairWave(args, store, options) {
2903
3163
  });
2904
3164
  const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
2905
3165
  + `(${group.findingIds.join(", ")})`));
2906
- return output(`Review repair wave for ${task.id} (${groups.length} group(s)):\n${lines.join("\n")}\n`, { groups: created });
3166
+ return output(`Review repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`, { strategy, groups: created });
2907
3167
  }
2908
3168
  const lines = groups.map((group, index) => {
2909
3169
  const findings = group.findings
@@ -2912,7 +3172,24 @@ function planReviewRepairWave(args, store, options) {
2912
3172
  return `wave ${index + 1}: ${findings}`
2913
3173
  + ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
2914
3174
  });
2915
- return output(`Repair wave for ${task.id} (${groups.length} group(s), run disjoint groups in parallel):\n${lines.join("\n")}\n`);
3175
+ return output(`Repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`
3176
+ + (strategy === "consolidated"
3177
+ ? "Default: keep all findings in one WorkItem; use --strategy parallel only for proven independent ownership.\n"
3178
+ : "Parallel strategy explicitly selected; each disjoint group may become one WorkItem.\n"));
3179
+ }
3180
+ function repairGroupsForStrategy(groups, strategy) {
3181
+ if (strategy === "parallel" || groups.length <= 1)
3182
+ return groups;
3183
+ const findings = groups.flatMap(({ findings }) => findings)
3184
+ .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
3185
+ return [{
3186
+ groupKey: findings.map(({ id }) => id).join("+"),
3187
+ findings,
3188
+ findingIds: findings.map(({ id }) => id),
3189
+ affectedPaths: [...new Set(groups.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
3190
+ affectedSymbols: [...new Set(groups.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
3191
+ invariants: [...new Set(groups.flatMap(({ invariants }) => invariants))].sort()
3192
+ }];
2916
3193
  }
2917
3194
  function reviewRepairProjectIds(task, affectedPaths) {
2918
3195
  const bindings = task.projectBindings;
@@ -3155,9 +3432,9 @@ function requestTaskReviewRound(args, store, options) {
3155
3432
  const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
3156
3433
  /**
3157
3434
  * Creates a distinct full Task-final ReviewRound only when the exact previous
3158
- * Round failed without producing semantic review evidence. The source Round,
3159
- * Run, findings, workspace, and terminal report remain immutable history; the
3160
- * linking Event is both the audit record and the idempotence key.
3435
+ * terminal Round durably proves that no semantic review was produced. The
3436
+ * source Round, Run, findings, workspace, and terminal report remain immutable
3437
+ * history; the linking Event is both the audit record and the idempotence key.
3161
3438
  */
3162
3439
  function forceFreshTaskReviewRound(args, store, options) {
3163
3440
  const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
@@ -3206,9 +3483,9 @@ function forceFreshTaskReviewRound(args, store, options) {
3206
3483
  }
3207
3484
  return { round: replacement, source, created: false };
3208
3485
  }
3209
- const semanticBlocker = forceFreshSemanticBlocker(tx, source);
3210
- if (semanticBlocker !== null) {
3211
- throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${semanticBlocker}`);
3486
+ const recovery = classifyForceFreshReviewRecovery(tx, source);
3487
+ if (recovery.kind === "semantic-or-ambiguous") {
3488
+ throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${recovery.reason}`);
3212
3489
  }
3213
3490
  if (source.taskCandidate === undefined) {
3214
3491
  throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
@@ -3275,7 +3552,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3275
3552
  candidateId: created.candidateId,
3276
3553
  reviewerRoleName: created.reviewerRoleName,
3277
3554
  taskCandidate: JSON.stringify(created.taskCandidate),
3278
- reason: "source-round-failed-without-semantic-review",
3555
+ reason: "source-round-terminal-without-semantic-review",
3279
3556
  leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3280
3557
  }, now);
3281
3558
  return { round: created, source, created: true };
@@ -3284,76 +3561,23 @@ function forceFreshTaskReviewRound(args, store, options) {
3284
3561
  ? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
3285
3562
  : `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
3286
3563
  }
3287
- /** Returns the exact semantic evidence that makes a failed Round ineligible. */
3288
- function forceFreshSemanticBlocker(store, round) {
3289
- if (round.status !== "failed")
3290
- return `source status is ${round.status}, not failed.`;
3291
- if ((round.checks ?? []).length > 0)
3292
- return "the Round records review checks.";
3293
- if (round.evidenceCommit !== undefined)
3294
- return "the Round records a review evidence commit.";
3295
- if (round.report !== round.summary) {
3296
- return "the Round stores a report distinct from its failure summary.";
3297
- }
3298
- if (runtimeFailureSummaryHasReviewerOutput(round.report ?? "")) {
3299
- return "the Round stores non-empty Reviewer output in its runtime failure summary.";
3300
- }
3301
- if (round.deltaRecheck?.disposition !== undefined
3302
- || round.deltaRecheck?.reasoning !== undefined) {
3303
- return "the Round records a semantic delta-recheck disposition.";
3304
- }
3305
- const semanticLane = round.executionGroup?.lanes.find((lane) => (lane.status === "yielded"
3306
- || lane.status === "completed"
3307
- || lane.result?.report !== undefined
3308
- || (lane.result?.checks ?? []).length > 0
3309
- || (lane.result?.findings ?? []).length > 0
3310
- || (lane.result?.evidence ?? []).length > 0
3311
- || lane.result?.evidenceCommit !== undefined));
3312
- if (semanticLane !== undefined) {
3313
- return `Reviewer Lane ${semanticLane.id} delivered semantic evidence.`;
3314
- }
3315
- const yieldedRun = store.listAgentRuns(round.taskId).find((run) => (run.purpose === "review"
3316
- && run.reviewRoundId === round.id
3317
- && run.status === "yielded"));
3318
- if (yieldedRun !== undefined)
3319
- return `Reviewer Run ${yieldedRun.id} yielded a report.`;
3320
- const finding = store.listReviewFindings(round.taskId).find((entry) => (entry.firstReviewRoundId === round.id || entry.lastReviewRoundId === round.id));
3321
- if (finding !== undefined)
3322
- return `Review finding ${finding.id} references the Round.`;
3323
- const semanticEvent = store.listEvents(round.taskId).find((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
3324
- if (semanticEvent !== undefined)
3325
- return `Review completion Event ${semanticEvent.id} exists.`;
3326
- if (looksLikeStructuredReviewReport(round.report ?? "")) {
3327
- return "the Round stores a structured reviewer report.";
3328
- }
3329
- return null;
3330
- }
3331
- function runtimeFailureSummaryHasReviewerOutput(summary) {
3332
- const match = /(?:^|\n)last_assistant_message:[ \t]*([\s\S]*)$/u.exec(summary);
3333
- const output = match?.[1];
3334
- return output !== undefined && output.trim().length > 0;
3335
- }
3336
- function looksLikeStructuredReviewReport(report) {
3337
- let parsed;
3338
- try {
3339
- parsed = JSON.parse(report);
3340
- }
3341
- catch {
3342
- return false;
3343
- }
3344
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
3345
- return false;
3346
- const record = parsed;
3347
- return [
3348
- "summary",
3349
- "report",
3350
- "checks",
3351
- "findings",
3352
- "evidence",
3353
- "evidenceCommit",
3354
- "deltaDisposition",
3355
- "deltaReasoning"
3356
- ].some((key) => Object.hasOwn(record, key));
3564
+ /**
3565
+ * Conservatively classifies an immutable Task-final Review as replaceable.
3566
+ * A failed Round keeps the existing no-semantic-evidence behavior. A completed
3567
+ * Round needs stronger, mutually corroborating evidence: an explicit internal
3568
+ * context/workspace failure, its exact yielded Run and receipt, matching Lane
3569
+ * output, and the mechanically emitted empty completion Event.
3570
+ * This is command eligibility only; it never rewrites the source outcome or
3571
+ * changes the global semantic classifier used by the finding ledger.
3572
+ */
3573
+ export function classifyForceFreshReviewRecovery(store, round) {
3574
+ const classification = classifyReviewRoundOutcome(round, store);
3575
+ return classification?.kind === "non-semantic"
3576
+ ? { kind: "non-semantic-terminal", reason: classification.reason }
3577
+ : {
3578
+ kind: "semantic-or-ambiguous",
3579
+ reason: classification?.reason ?? `source status is ${round.status}, not terminal.`
3580
+ };
3357
3581
  }
3358
3582
  /**
3359
3583
  * Issue 07: re-validates the CLI-computed delta preflight inside the store
@@ -3369,8 +3593,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
3369
3593
  const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
3370
3594
  if (previous === null
3371
3595
  || (previous.scope ?? "work-item") !== "task"
3372
- || previous.status !== "completed") {
3373
- throw usageError(`Delta-recheck previous ReviewRound is not a completed Task-final Review: `
3596
+ || !isSemanticReviewRound(previous, store)) {
3597
+ throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
3374
3598
  + `${preflight.record.previousReviewRoundId}.`);
3375
3599
  }
3376
3600
  if (previous.reviewerRoleName !== reviewerRoleName) {
@@ -3986,7 +4210,7 @@ function retryRun(args, store, options) {
3986
4210
  }
3987
4211
  function actualTaskReviewCandidateForMutation(store, task, options) {
3988
4212
  if (options.actualTaskReviewCandidate === undefined) {
3989
- throw usageError(`Actual Task Project heads were not verified for final Review: ${task.id}.`);
4213
+ throw usageError(`Actual Task Project heads were not verified for delivery: ${task.id}.`);
3990
4214
  }
3991
4215
  let actual;
3992
4216
  try {
@@ -4202,8 +4426,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4202
4426
  // Any Task-final ReviewRound is durable completion evidence/obligation.
4203
4427
  // Once one exists, later changes to the mutable global review config cannot
4204
4428
  // weaken the requirement or change its reviewer. Before the first such
4205
- // Round, the current global `final` config is still the supported way to
4206
- // establish the policy and queue that initial Round.
4429
+ // Round, the current global `final` config establishes the initial Round
4430
+ // only for integrated delivery.
4207
4431
  const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
4208
4432
  .filter((round) => ((round.scope ?? "work-item") === "task"
4209
4433
  && (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
@@ -4214,7 +4438,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4214
4438
  }
4215
4439
  else if (establishedRound === undefined) {
4216
4440
  const globalConfig = store.getReviewConfig();
4217
- config = globalConfig?.trigger === "final" ? globalConfig : null;
4441
+ // Direct delivery is an explicit low-overhead contract. Mutable global
4442
+ // policy must not create a managed Round during direct completion; risk
4443
+ // that warrants one promotes the Task to integrated delivery. Any already-
4444
+ // established Task Round or immutable contract remains authoritative
4445
+ // through the branches above.
4446
+ config = taskDeliveryPath(task) === "integrated"
4447
+ && globalConfig?.trigger === "final"
4448
+ ? globalConfig
4449
+ : null;
4218
4450
  }
4219
4451
  else {
4220
4452
  config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
@@ -4259,7 +4491,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4259
4491
  return escalated;
4260
4492
  }
4261
4493
  else {
4262
- return latest.status === "completed" ? null : latest;
4494
+ return latest.status === "completed"
4495
+ && classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
4496
+ ? null
4497
+ : latest;
4263
4498
  }
4264
4499
  }
4265
4500
  const anchor = taskFinalContract === undefined
@@ -5380,8 +5615,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5380
5615
  let deltaContext = "";
5381
5616
  if (taskScope && round.deltaRecheck !== undefined) {
5382
5617
  const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
5383
- if (previousRound === null || previousRound.status !== "completed") {
5384
- throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
5618
+ if (previousRound === null || !isSemanticReviewRound(previousRound, tx)) {
5619
+ throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous semantic ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
5385
5620
  }
5386
5621
  const diffByProject = options.deltaRecheckDiff;
5387
5622
  if (diffByProject === undefined) {
@@ -5669,7 +5904,9 @@ function publishedTreeAuthorizationPayload(actor, proof) {
5669
5904
  by: actor,
5670
5905
  projectId: proof.projectId,
5671
5906
  publicationId: proof.publicationId,
5672
- reviewRoundId: proof.reviewRoundId,
5907
+ ...(proof.reviewRoundId === undefined
5908
+ ? { reviewAnchor: publishedTreeReviewAnchor(proof) }
5909
+ : { reviewRoundId: proof.reviewRoundId }),
5673
5910
  localCommit: proof.localCommit,
5674
5911
  remoteCommit: proof.remoteCommit,
5675
5912
  tree: proof.tree
@@ -5685,7 +5922,8 @@ function matchingPublishedTreeAuthorization(store, proof) {
5685
5922
  && (event.payload.by === "user" || event.payload.by === "operator")
5686
5923
  && event.payload.projectId === proof.projectId
5687
5924
  && event.payload.publicationId === proof.publicationId
5688
- && event.payload.reviewRoundId === proof.reviewRoundId
5925
+ && (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
5926
+ === publishedTreeReviewAnchor(proof)
5689
5927
  && event.payload.localCommit === proof.localCommit
5690
5928
  && event.payload.remoteCommit === proof.remoteCommit
5691
5929
  && event.payload.tree === proof.tree) {
@@ -5698,10 +5936,15 @@ function requirePublishedTreeAuthorization(store, proof) {
5698
5936
  const authorization = matchingPublishedTreeAuthorization(store, proof);
5699
5937
  if (authorization === undefined) {
5700
5938
  throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
5701
- + `${proof.taskId}/${proof.publicationId} at Task-final Review ${proof.reviewRoundId}.`);
5939
+ + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
5940
+ ? "the direct Task-main head"
5941
+ : `Task-final Review ${proof.reviewRoundId}`}.`);
5702
5942
  }
5703
5943
  return authorization;
5704
5944
  }
5945
+ function publishedTreeReviewAnchor(proof) {
5946
+ return proof.reviewRoundId ?? "direct";
5947
+ }
5705
5948
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
5706
5949
  function truncateEventNote(note) {
5707
5950
  const normalized = note.trim();
@@ -5962,6 +6205,11 @@ function parseTaskPriority(value) {
5962
6205
  return value;
5963
6206
  throw usageError(`Invalid Task priority: ${value}.`);
5964
6207
  }
6208
+ function parseTaskDelivery(value) {
6209
+ if (value === "direct" || value === "integrated")
6210
+ return value;
6211
+ throw usageError(`Task delivery is invalid: ${value}. Use direct or integrated.`);
6212
+ }
5965
6213
  function parseTaskTags(value) {
5966
6214
  const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
5967
6215
  if (tags.length === 0)