@zq-silk/yui 0.8.3 → 0.8.7

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 (73) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +66 -19
  3. package/dist/cli/commandCatalog.js +43 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli/updatePorts.js +6 -0
  6. package/dist/cli.js +252 -37
  7. package/dist/commands/executionAuditCommands.js +30 -0
  8. package/dist/commands/globalRoleCommands.js +8 -4
  9. package/dist/commands/operatorCommands.js +42 -1
  10. package/dist/commands/taskCommands.js +527 -147
  11. package/dist/commands/taskCompletionGate.js +36 -24
  12. package/dist/commands/taskContextCommand.js +11 -4
  13. package/dist/commands/taskInputCommands.js +48 -10
  14. package/dist/commands/taskNextActionCommand.js +38 -3
  15. package/dist/commands/taskOverviewCommand.js +2 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  17. package/dist/context/runContextPack.js +9 -5
  18. package/dist/context/sessionBootstrapManifest.js +158 -11
  19. package/dist/context/wakeNotification.js +5 -3
  20. package/dist/controller/clientRuntime.js +15 -15
  21. package/dist/controller/controller.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
  23. package/dist/controller/handoverCandidate.js +10 -3
  24. package/dist/controller/sessionNotify.js +4 -22
  25. package/dist/executor/agentAdapter.js +2 -2
  26. package/dist/executor/agentExecutor.js +29 -9
  27. package/dist/executor/fileRoleLaunchPlanner.js +37 -45
  28. package/dist/integration/gitIntegrationService.js +50 -2
  29. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  30. package/dist/observability/executionAudit.js +47 -1
  31. package/dist/observability/faultClassification.js +6 -4
  32. package/dist/observability/orchestrationMetrics.js +196 -0
  33. package/dist/operator/operatorSessionHistory.js +36 -0
  34. package/dist/release/releaseHandover.js +7 -5
  35. package/dist/release/runtimeRelease.js +15 -0
  36. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  37. package/dist/review/deltaRecheck.js +3 -2
  38. package/dist/review/reviewFindingLedger.js +5 -4
  39. package/dist/review/reviewOutcomeClassifier.js +263 -54
  40. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  41. package/dist/review/taskFinalReviewContractRebind.js +367 -0
  42. package/dist/run/runIdentity.js +10 -70
  43. package/dist/runtime/agentHost.js +3 -4
  44. package/dist/runtime/codexAppServerRuntime.js +6 -0
  45. package/dist/runtime/exactControlPlane.js +47 -37
  46. package/dist/runtime/firstProgressStopLoss.js +54 -0
  47. package/dist/runtime/launchBroker.js +10 -2
  48. package/dist/runtime/runtimeDeadlines.js +14 -0
  49. package/dist/runtime/sessionTitle.js +24 -12
  50. package/dist/runtime/structuredProviderHost.js +7 -1
  51. package/dist/runtime/tmuxAdapters.js +10 -3
  52. package/dist/scheduler/actionability.js +4 -2
  53. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  54. package/dist/scheduler/activeTaskProgress.js +2 -1
  55. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  56. package/dist/scheduler/taskExecutionProjection.js +13 -4
  57. package/dist/scheduler/wakeReason.js +1 -0
  58. package/dist/storage/sqliteStore.js +18 -3
  59. package/dist/storage/taskStore.js +14 -3
  60. package/dist/task/completionReadiness.js +48 -19
  61. package/dist/task/deliveryGuard.js +3 -1
  62. package/dist/task/nextAction.js +153 -55
  63. package/dist/task/repairWave.js +14 -1
  64. package/dist/task/task.js +10 -0
  65. package/dist/task/taskRecordRetirement.js +72 -0
  66. package/dist/web/webSnapshot.js +7 -1
  67. package/dist/workItem/workItem.js +6 -4
  68. package/i18n/README.zh-CN.md +48 -9
  69. package/package.json +1 -1
  70. package/skills/yui-leader/SKILL.md +73 -31
  71. package/skills/yui-operator/SKILL.md +58 -10
  72. package/skills/yui-reviewer/SKILL.md +23 -0
  73. package/skills/yui-runtime/SKILL.md +6 -6
@@ -5,10 +5,11 @@ import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds,
5
5
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
6
6
  import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
7
7
  import { createTaskEvent } from "../event/taskEvent.js";
8
+ import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
8
9
  import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
9
10
  import { readCommandText } from "./textInput.js";
10
11
  import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
11
- import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
12
+ import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
12
13
  import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
13
14
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
14
15
  import { defaultTableWidth, renderTable } from "../output/table.js";
@@ -35,7 +36,7 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
35
36
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
36
37
  import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
37
38
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
38
- import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
39
+ import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskDeliveryPath, updateTaskMetadata } from "../task/task.js";
39
40
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
40
41
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
41
42
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
@@ -44,6 +45,8 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
44
45
  import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
45
46
  import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
46
47
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
48
+ import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
49
+ import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
47
50
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
48
51
  import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
49
52
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
@@ -52,7 +55,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
52
55
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
53
56
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
54
57
  import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
55
- import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
58
+ import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
56
59
  import { runGrantCommand } from "./grantCommands.js";
57
60
  import { runWorkflowCommand } from "./workflowCommands.js";
58
61
  import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
@@ -81,21 +84,17 @@ function legacyWorkItemReviewConfig(config) {
81
84
  return config?.trigger === "final" ? null : config;
82
85
  }
83
86
  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.`);
87
+ return storedTaskFinalReviewContractResolution(store, taskId)?.effective;
88
+ }
89
+ function storedTaskFinalReviewContractResolution(store, taskId) {
90
+ try {
91
+ return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId), store.listEvents(taskId));
92
+ }
93
+ catch (error) {
94
+ throw dataError(error instanceof Error
95
+ ? error.message
96
+ : `Task ${taskId} contains conflicting final-review contracts.`);
97
97
  }
98
- return first;
99
98
  }
100
99
  /**
101
100
  * Resolve one exact Task-local contract before the caller performs any write.
@@ -367,12 +366,14 @@ function taskProjectCommand(args, store, options) {
367
366
  return output(`Added Project to ${updated.id}\n`, { task: updated });
368
367
  }
369
368
  function updateTaskCommand(args, store, options) {
370
- const optionNames = new Set(["--title", "--description", "--priority", "--tags", "--due-at"]);
369
+ const optionNames = new Set([
370
+ "--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
371
+ ]);
371
372
  const flagOptions = new Set([
372
373
  "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
373
374
  "--require-integration"
374
375
  ]);
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].";
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].";
376
377
  const parsed = parseTail(args, optionNames, usage, flagOptions);
377
378
  exactPositionals(parsed.positionals, 1, usage);
378
379
  if (parsed.options.size === 0)
@@ -396,19 +397,42 @@ function updateTaskCommand(args, store, options) {
396
397
  const tags = parsed.options.has("--tags")
397
398
  ? parseTaskTags(requiredOption(parsed.options, "--tags"))
398
399
  : 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");
399
408
  const now = clock(options);
400
409
  const result = store.transaction((tx) => {
401
410
  const current = requireTask(tx, parsed.positionals[0]);
402
411
  if (current.status === "archived")
403
412
  throw usageError(`Task is archived: ${current.id}.`);
404
- if (parsed.options.has("--require-integration") && current.status === "completed") {
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") {
405
421
  throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
406
422
  }
423
+ if (enableIntegration && current.requireIntegration !== true) {
424
+ assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
425
+ }
407
426
  if (parsed.options.size === 1
408
- && parsed.options.has("--require-integration")
427
+ && enableIntegration
409
428
  && current.requireIntegration === true) {
410
429
  return { task: current, integrationState: "already-enabled" };
411
430
  }
431
+ if (parsed.options.size === 1
432
+ && requestedDelivery === "direct"
433
+ && taskDeliveryPath(current) === "direct") {
434
+ return { task: current, integrationState: "already-direct" };
435
+ }
412
436
  const updated = updateTaskMetadata(current, {
413
437
  ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
414
438
  ...(parsed.options.has("--description")
@@ -423,31 +447,93 @@ function updateTaskCommand(args, store, options) {
423
447
  ...(dueAt === undefined
424
448
  ? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
425
449
  : { dueAt }),
426
- ...(parsed.options.has("--require-integration") ? { requireIntegration: true } : {})
450
+ ...(enableIntegration ? { requireIntegration: true } : {})
427
451
  }, now);
428
452
  tx.saveTask(updated);
429
453
  recordTaskEvent(tx, updated.id, "task.updated", {
430
454
  status: updated.status,
431
- ...(parsed.options.has("--require-integration")
432
- ? { completionEvidence: "integration-required" }
433
- : {})
455
+ ...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
456
+ ? {}
457
+ : {
458
+ completionEvidence: enableIntegration
459
+ ? "integration-required"
460
+ : "direct",
461
+ deliveryPath: taskDeliveryPath(updated)
462
+ })
434
463
  }, now);
435
464
  enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
436
465
  return {
437
466
  task: updated,
438
- integrationState: parsed.options.has("--require-integration")
467
+ integrationState: enableIntegration
439
468
  ? "enabled"
440
- : "unchanged"
469
+ : requestedDelivery === "direct"
470
+ ? "direct"
471
+ : "unchanged"
441
472
  };
442
473
  });
443
- if (result.integrationState !== "already-enabled") {
474
+ if (result.integrationState !== "already-enabled"
475
+ && result.integrationState !== "already-direct") {
444
476
  notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
445
477
  }
446
478
  return result.integrationState === "enabled"
447
- ? `Updated task ${result.task.id}\nCompletion evidence enabled: WorkItem, ChangeSet, and committed Integration required\n`
479
+ ? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
448
480
  : result.integrationState === "already-enabled"
449
- ? `Task ${result.task.id} completion evidence is already enabled\n`
450
- : `Updated task ${result.task.id}\n`;
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
+ }
451
537
  }
452
538
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
453
539
  export function runTaskOutputCommand(args, store, options = {}) {
@@ -488,13 +574,20 @@ function createTaskCommand(args, store, options) {
488
574
  notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
489
575
  return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
490
576
  + `Assigned role: ${created.leader.name}\n`
577
+ + `Delivery: ${taskDeliveryPath(created.task)}\n`
491
578
  + (created.task.requireIntegration
492
579
  ? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
493
- : "Completion: delivery integration not required\n"), { task: created.task, leader: created.leader });
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"), {
583
+ task: created.task,
584
+ leader: created.leader,
585
+ deliveryPath: taskDeliveryPath(created.task)
586
+ });
494
587
  }
495
588
  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"]));
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"]));
498
591
  exactPositionals(parsed.positionals, 1, usage);
499
592
  const projectReferences = parsed.multiOptions.get("--project") ?? [];
500
593
  const baseOptions = parsed.multiOptions.get("--base") ?? [];
@@ -510,6 +603,16 @@ function parseTaskCreation(args, store) {
510
603
  if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
511
604
  throw usageError("A Task cannot bind the same Project more than once.");
512
605
  }
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
+ }
513
616
  const bases = new Map();
514
617
  for (const option of baseOptions) {
515
618
  const separator = option.indexOf("=");
@@ -540,7 +643,8 @@ function parseTaskCreation(args, store) {
540
643
  baseRef: bases.get(project.id) ?? project.developmentBranch
541
644
  })),
542
645
  defaultProjectIds,
543
- requireIntegration: parsed.options.has("--require-integration")
646
+ requireIntegration: requestedDelivery === "integrated"
647
+ || parsed.options.has("--require-integration")
544
648
  };
545
649
  }
546
650
  function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
@@ -550,6 +654,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
550
654
  store.saveRole(task.id, leader);
551
655
  recordTaskEvent(store, task.id, "task.created", {
552
656
  status: task.status,
657
+ deliveryPath: taskDeliveryPath(task),
553
658
  ...(defaultProjectIds.length === 0
554
659
  ? {}
555
660
  : { defaultProjectIds: defaultProjectIds.join(",") })
@@ -598,11 +703,16 @@ function showTaskCommand(args, store) {
598
703
  `Task: ${task.id}`,
599
704
  `Title: ${task.title}`,
600
705
  `Status: ${task.status}`,
706
+ `Delivery: ${taskDeliveryPath(task)}`,
601
707
  ...(task.description === undefined ? [] : [`Description: ${task.description}`]),
602
708
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
603
709
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
604
710
  ...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
605
- `Completion evidence: ${task.requireIntegration === true ? "required" : "not required"}`,
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"}`,
606
716
  ...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
607
717
  ...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
608
718
  ...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
@@ -631,7 +741,12 @@ function showTaskCommand(args, store) {
631
741
  `Created: ${presentTime(task.createdAt, timeZone)}`,
632
742
  `Updated: ${presentTime(task.updatedAt, timeZone)}`
633
743
  ].join("\n").concat("\n");
634
- return output(rendered, { task, counts, hasBrief: brief !== null });
744
+ return output(rendered, {
745
+ task,
746
+ deliveryPath: taskDeliveryPath(task),
747
+ counts,
748
+ hasBrief: brief !== null
749
+ });
635
750
  }
636
751
  function activateTaskCommand(args, store, options) {
637
752
  exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
@@ -677,14 +792,18 @@ function completeTaskCommand(args, store, options) {
677
792
  task,
678
793
  changed: false,
679
794
  runtimeCleanupTargets: [],
795
+ completionAdvisories: [],
680
796
  finalReview: undefined,
681
797
  publishedTreeAuthorization: undefined
682
798
  };
683
799
  }
684
800
  const taskFinalContract = preflight.taskFinalReviewContract;
801
+ const actualTaskCandidate = task.projectBindings.length === 0
802
+ ? undefined
803
+ : actualTaskReviewCandidateForMutation(tx, task, options);
685
804
  const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
686
805
  ? undefined
687
- : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskReviewCandidateForMutation(tx, task, options));
806
+ : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
688
807
  const requiresContractHandoff = publishedTreeProof !== undefined
689
808
  && taskFinalContract !== undefined;
690
809
  if (requiresContractHandoff && actor !== "leader") {
@@ -695,6 +814,7 @@ function completeTaskCommand(args, store, options) {
695
814
  task,
696
815
  changed: false,
697
816
  runtimeCleanupTargets: [],
817
+ completionAdvisories: [],
698
818
  finalReview: undefined,
699
819
  publishedTreeAuthorization: {
700
820
  event,
@@ -752,6 +872,7 @@ function completeTaskCommand(args, store, options) {
752
872
  task,
753
873
  changed: false,
754
874
  runtimeCleanupTargets: [],
875
+ completionAdvisories: [],
755
876
  finalReview,
756
877
  resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
757
878
  terminalizedLeaderRun,
@@ -783,13 +904,29 @@ function completeTaskCommand(args, store, options) {
783
904
  by: actor,
784
905
  projectId: publishedTreeProof.projectId,
785
906
  publicationId: publishedTreeProof.publicationId,
786
- reviewRoundId: publishedTreeProof.reviewRoundId,
907
+ ...(publishedTreeProof.reviewRoundId === undefined
908
+ ? {}
909
+ : { reviewRoundId: publishedTreeProof.reviewRoundId }),
787
910
  localCommit: publishedTreeProof.localCommit,
788
911
  remoteCommit: publishedTreeProof.remoteCommit,
789
912
  tree: publishedTreeProof.tree
790
913
  }, now);
791
914
  }
792
- recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
915
+ recordTaskEvent(tx, task.id, "task.completed", {
916
+ by: actor,
917
+ summary,
918
+ deliveryPath: taskDeliveryPath(task),
919
+ ...(actualTaskCandidate === undefined
920
+ ? {}
921
+ : {
922
+ projectHeads: actualTaskCandidate.projects
923
+ .map(({ projectId, commit }) => `${projectId}@${commit}`)
924
+ .join(",")
925
+ }),
926
+ ...(readiness.advisories.length === 0
927
+ ? {}
928
+ : { cleanupAdvisories: String(readiness.advisories.length) })
929
+ }, now);
793
930
  // A terminal Task must never leave a Task-lane signal that can wake it.
794
931
  // The durable records remain intact; only the derived mailbox work is
795
932
  // discarded at this lifecycle boundary.
@@ -805,6 +942,7 @@ function completeTaskCommand(args, store, options) {
805
942
  task: completed,
806
943
  changed: true,
807
944
  runtimeCleanupTargets,
945
+ completionAdvisories: readiness.advisories,
808
946
  finalReview: undefined,
809
947
  resumedPendingFinalReview: false,
810
948
  terminalizedLeaderRun,
@@ -841,9 +979,17 @@ function completeTaskCommand(args, store, options) {
841
979
  [TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
842
980
  });
843
981
  }
844
- return output(result.changed
982
+ const completionOutput = result.changed
845
983
  ? `Completed task ${result.task.id}\n`
846
- : `Task ${result.task.id} is already completed\n`);
984
+ : `Task ${result.task.id} is already completed\n`;
985
+ const advisoryOutput = result.completionAdvisories.length === 0
986
+ ? ""
987
+ : `Cleanup advisories (non-blocking; settle before archive):\n`
988
+ + result.completionAdvisories.map((advisory) => (`- ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.fix}`)).join("\n").concat("\n");
989
+ return output(completionOutput + advisoryOutput, {
990
+ task: result.task,
991
+ completionAdvisories: result.completionAdvisories
992
+ });
847
993
  }
848
994
  function reopenTaskCommand(args, store, options) {
849
995
  exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
@@ -1203,23 +1349,79 @@ function taskMessageCommand(args, store, options) {
1203
1349
  }
1204
1350
  if (messages.length === 0)
1205
1351
  return "No messages found.\n";
1352
+ const retirements = new Map(store.listEvents(task.id).flatMap((event) => {
1353
+ const retirement = taskRecordRetirement(event);
1354
+ return retirement?.recordKind === "message"
1355
+ ? [[retirement.recordId, retirement]]
1356
+ : [];
1357
+ }));
1206
1358
  const timeZone = store.getConfig().timeZone;
1207
1359
  return `${renderTable(`Task messages: ${task.id}`, [
1208
1360
  { header: "Message", minWidth: 7, maxWidth: 18 },
1361
+ { header: "Status", minWidth: 6, maxWidth: 9 },
1209
1362
  { header: "Author", minWidth: 6, maxWidth: 18 },
1210
1363
  { header: "Created", minWidth: 10, maxWidth: 28 },
1211
1364
  { header: "Body", minWidth: 8, maxWidth: 72 }
1212
1365
  ], messages.map((message) => [
1213
1366
  message.id,
1367
+ retirements.has(message.id) ? "retired" : "active",
1214
1368
  taskMessageAuthorLabel(message.author),
1215
1369
  presentTime(message.createdAt, timeZone),
1216
1370
  message.body
1217
1371
  ]), defaultTableWidth())}\n`;
1218
1372
  }
1373
+ if (command === "retire") {
1374
+ return retireMessage(rest, store, options);
1375
+ }
1219
1376
  throw usageError(command === undefined
1220
1377
  ? "Task message command is required."
1221
1378
  : `Unknown command: task message ${command}`);
1222
1379
  }
1380
+ function retireMessage(args, store, options) {
1381
+ const usage = "Task message retire usage: yui task message retire <task>/<message> --reason <text>.";
1382
+ const parsed = parseTail(args, new Set(["--reason"]), usage);
1383
+ exactPositionals(parsed.positionals, 1, usage);
1384
+ const reason = requiredOption(parsed.options, "--reason");
1385
+ const reference = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
1386
+ const now = clock(options);
1387
+ const result = store.transaction((tx) => {
1388
+ const task = requireTask(tx, reference.taskId);
1389
+ assertTaskOpen(task);
1390
+ const actor = taskActor(options, task.id);
1391
+ if (actor === "leader") {
1392
+ throw usageError("Only the user or global Operator may retire a Task Message.");
1393
+ }
1394
+ const message = tx.listMessages(task.id).find(({ id }) => id === reference.localId);
1395
+ if (message === undefined) {
1396
+ throw dataError(`Task Message not found: ${task.id}/${reference.localId}.`);
1397
+ }
1398
+ const events = tx.listEvents(task.id);
1399
+ if (isTaskRecordRetired(events, "message", message.id)) {
1400
+ return { task, message, changed: false };
1401
+ }
1402
+ // Remove an isolated pending wake for this exact directive. A merged batch
1403
+ // is retained because its other signals remain actionable; context and
1404
+ // actionability projections still filter the retired message below.
1405
+ try {
1406
+ settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
1407
+ }
1408
+ catch {
1409
+ // Merged pending work cannot be split without losing unrelated signals.
1410
+ }
1411
+ tx.saveEvent(task.id, createTaskRecordRetirement({
1412
+ eventId: tx.nextEventId(task.id),
1413
+ taskId: task.id,
1414
+ recordKind: "message",
1415
+ recordId: message.id,
1416
+ reason,
1417
+ retiredBy: actor
1418
+ }, now));
1419
+ return { task, message, changed: true };
1420
+ });
1421
+ if (result.changed)
1422
+ options.runtime?.notifyStateChanged(result.task.id);
1423
+ return `Retired Task Message ${result.task.id}/${result.message.id}\n`;
1424
+ }
1223
1425
  /**
1224
1426
  * Issue 05: force-wake escape hatch. Bypasses the actionability digest and
1225
1427
  * enqueues exactly one Leader wakeup with an auditable reason. The reason is
@@ -2514,9 +2716,7 @@ function retireWork(args, store, options) {
2514
2716
  if (task.status !== "active") {
2515
2717
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2516
2718
  }
2517
- if (taskActor(options, task.id) !== "leader") {
2518
- throw usageError("Only the Task Leader may retire a Work Item.");
2519
- }
2719
+ const actor = taskActor(options, task.id);
2520
2720
  if (replacementWorkItemId !== undefined) {
2521
2721
  const replacement = requireWorkItem(tx, replacementWorkItemId, options);
2522
2722
  if (replacement.taskId !== task.id) {
@@ -2556,7 +2756,7 @@ function retireWork(args, store, options) {
2556
2756
  }
2557
2757
  }
2558
2758
  const next = retireWorkItem(item, {
2559
- by: "leader",
2759
+ by: actor,
2560
2760
  summary,
2561
2761
  ...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
2562
2762
  }, now);
@@ -2568,8 +2768,16 @@ function retireWork(args, store, options) {
2568
2768
  ...(replacementWorkItemId === undefined
2569
2769
  ? {}
2570
2770
  : { replacementWorkItemId }),
2571
- ...leaderActionEventPayload(tx, task.id, options)
2771
+ ...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : { retiredBy: actor })
2572
2772
  }, now);
2773
+ tx.saveEvent(task.id, createTaskRecordRetirement({
2774
+ eventId: tx.nextEventId(task.id),
2775
+ taskId: task.id,
2776
+ recordKind: "work-item",
2777
+ recordId: next.id,
2778
+ reason: summary,
2779
+ retiredBy: actor
2780
+ }, now));
2573
2781
  }
2574
2782
  return next;
2575
2783
  });
@@ -2678,6 +2886,8 @@ function reviewWork(args, store, options) {
2678
2886
  */
2679
2887
  function taskReviewCommand(args, store, options) {
2680
2888
  const [command, ...rest] = args;
2889
+ if (command === "rebind")
2890
+ return rebindTaskFinalReviewContract(rest, store, options);
2681
2891
  if (command === "request")
2682
2892
  return requestTaskReviewRound(rest, store, options);
2683
2893
  if (command === "force-fresh")
@@ -2692,6 +2902,115 @@ function taskReviewCommand(args, store, options) {
2692
2902
  ? "Task review command is required."
2693
2903
  : `Unknown command: task review ${command}`);
2694
2904
  }
2905
+ export function parseTaskFinalReviewContractRebindRequest(args) {
2906
+ const usage = "Task review rebind usage: yui task review rebind <task> "
2907
+ + "--from-control <digest> --to-control <digest> "
2908
+ + "--from-release <release-id> --to-release <release-id>.";
2909
+ const parsed = parseTail(args, new Set(["--from-control", "--to-control", "--from-release", "--to-release"]), usage);
2910
+ exactPositionals(parsed.positionals, 1, usage);
2911
+ return {
2912
+ taskId: parsed.positionals[0],
2913
+ fromControlPlaneDigest: requiredOption(parsed.options, "--from-control"),
2914
+ toControlPlaneDigest: requiredOption(parsed.options, "--to-control"),
2915
+ fromReleaseId: requiredOption(parsed.options, "--from-release"),
2916
+ toReleaseId: requiredOption(parsed.options, "--to-release")
2917
+ };
2918
+ }
2919
+ function rebindTaskFinalReviewContract(args, store, options) {
2920
+ const request = parseTaskFinalReviewContractRebindRequest(args);
2921
+ const environment = options.environment ?? {};
2922
+ const usage = "Task review rebind usage: yui task review rebind <task> "
2923
+ + "--from-control <digest> --to-control <digest> "
2924
+ + "--from-release <release-id> --to-release <release-id>.";
2925
+ if (!isCurrentGlobalOperator(store, environment)) {
2926
+ throw usageError("Task-final Review contract rebind requires the authenticated global Operator session.", usage);
2927
+ }
2928
+ const proof = options.taskFinalReviewRebindProof;
2929
+ if (proof === undefined
2930
+ || proof.schemaVersion !== 1
2931
+ || proof.taskId !== request.taskId
2932
+ || proof.fromControlPlaneDigest !== request.fromControlPlaneDigest
2933
+ || proof.toControlPlaneDigest !== request.toControlPlaneDigest
2934
+ || proof.fromRelease.releaseId !== request.fromReleaseId
2935
+ || proof.toRelease.releaseId !== request.toReleaseId) {
2936
+ throw usageError("Task-final Review contract rebind proof is missing or does not match the explicit request.", usage);
2937
+ }
2938
+ const now = clock(options);
2939
+ const result = store.transaction((tx) => {
2940
+ const task = requireTask(tx, request.taskId);
2941
+ if (task.status !== "active") {
2942
+ throw usageError(`Task is not active: ${task.id}.`);
2943
+ }
2944
+ if (task.projectBindings.length === 0) {
2945
+ throw usageError(`Task final-review contract requires a Project-backed Task: ${task.id}.`);
2946
+ }
2947
+ if (!isCurrentGlobalOperator(tx, environment)) {
2948
+ throw usageError("Task-final Review contract rebind Operator identity drifted before commit.");
2949
+ }
2950
+ const resolution = storedTaskFinalReviewContractResolution(tx, task.id);
2951
+ if (resolution === undefined) {
2952
+ throw usageError(`Task final-review contract is missing for ${task.id}.`);
2953
+ }
2954
+ const exactExisting = resolution.rebinds.at(-1);
2955
+ if (resolution.effective.controlPlaneDigest === request.toControlPlaneDigest) {
2956
+ if (exactExisting !== undefined
2957
+ && exactExisting.fromContract.controlPlaneDigest === request.fromControlPlaneDigest
2958
+ && exactExisting.toContract.controlPlaneDigest === request.toControlPlaneDigest
2959
+ && exactExisting.fromRelease.releaseId === request.fromReleaseId
2960
+ && exactExisting.toRelease.releaseId === request.toReleaseId
2961
+ && exactExisting.handoverId === proof.handoverId) {
2962
+ return { rebind: exactExisting, changed: false };
2963
+ }
2964
+ throw usageError(`Task final-review contract already targets ${request.toControlPlaneDigest} without the requested proof tuple.`);
2965
+ }
2966
+ if (resolution.effective.controlPlaneDigest !== request.fromControlPlaneDigest) {
2967
+ throw usageError(`Task final-review contract source control-plane digest drifted for ${task.id}.`);
2968
+ }
2969
+ const activeRound = tx.listReviewRounds(task.id).find((round) => ((round.scope ?? "work-item") === "task"
2970
+ && (round.status === "pending" || round.status === "running")));
2971
+ if (activeRound !== undefined) {
2972
+ throw usageError(`Task final-review contract cannot rebind while ReviewRound ${activeRound.id} is ${activeRound.status}.`);
2973
+ }
2974
+ const activeLeaderRun = tx.getActiveAgentRun(task.id, "leader");
2975
+ if (activeLeaderRun !== null) {
2976
+ throw usageError(`Task final-review contract cannot rebind while Leader Run ${activeLeaderRun.id} is active.`);
2977
+ }
2978
+ const leaderSessions = tx.getTaskRoleSessionSet(task.id, "leader");
2979
+ if (leaderSessions !== null) {
2980
+ if (leaderSessions.inFlight !== null) {
2981
+ throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
2982
+ }
2983
+ const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
2984
+ if (liveLeaderSession !== undefined) {
2985
+ throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
2986
+ }
2987
+ if (Object.keys(leaderSessions.sessions).length > 0
2988
+ || leaderSessions.providerBinding !== null) {
2989
+ tx.saveTaskRoleSessionSet(retireTaskRoleSessionsForWorkspace(leaderSessions, now));
2990
+ }
2991
+ }
2992
+ const rebind = createTaskFinalReviewContractRebind({
2993
+ taskId: task.id,
2994
+ reviewerRoleName: resolution.effective.reviewerRoleName,
2995
+ fromContract: resolution.effective,
2996
+ toControlPlaneDigest: proof.toControlPlaneDigest,
2997
+ fromRelease: proof.fromRelease,
2998
+ toRelease: proof.toRelease,
2999
+ handoverId: proof.handoverId,
3000
+ authorizedBy: `operator:${environment.YUI_AGENT_ID ?? "unknown"}`
3001
+ });
3002
+ const event = recordTaskEventRecord(tx, task.id, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, taskFinalReviewContractRebindPayload(rebind), now);
3003
+ enqueueWork(tx, leaderMailbox(task.id), wakeReason("review-contract-rebound", event.id), now, [eventRef(task.id, event.id)]);
3004
+ return { rebind, event, changed: true };
3005
+ });
3006
+ if (result.changed) {
3007
+ notifyMailbox(options.runtime, leaderMailbox(request.taskId), request.taskId);
3008
+ }
3009
+ return output(result.changed
3010
+ ? `Rebound Task-final Review contract for ${request.taskId} from `
3011
+ + `${request.fromControlPlaneDigest} to ${request.toControlPlaneDigest}.\n`
3012
+ : `Task-final Review contract rebind is already recorded for ${request.taskId}.\n`, result);
3013
+ }
2695
3014
  function resolveReviewExecutionGroup(args, store, options) {
2696
3015
  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
3016
  if (args[0] !== "resolve")
@@ -2786,8 +3105,8 @@ function resolveReviewExecutionGroup(args, store, options) {
2786
3105
  /**
2787
3106
  * Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
2788
3107
  * 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.
3108
+ * let the Leader inspect the ledger, disposition each finding, and plan one
3109
+ * convergent repair unit by default. Parallel fan-out is explicit.
2791
3110
  */
2792
3111
  function reviewFindingCommand(args, store, options) {
2793
3112
  const [command, ...rest] = args;
@@ -2858,11 +3177,15 @@ function disposeReviewFindingCommand(args, store, options) {
2858
3177
  return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
2859
3178
  }
2860
3179
  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"]));
3180
+ const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--strategy <consolidated|parallel>] [--create].";
3181
+ const parsed = parseTail(args, new Set(["--strategy"]), usage, new Set(["--create"]));
2863
3182
  exactPositionals(parsed.positionals, 1, usage);
2864
3183
  const task = requireTask(store, parsed.positionals[0]);
2865
- const groups = planRepairGroups(store, task.id);
3184
+ const strategy = parsed.options.get("--strategy") ?? "consolidated";
3185
+ if (strategy !== "consolidated" && strategy !== "parallel") {
3186
+ throw usageError(`Review repair strategy is invalid: ${strategy}.`, usage);
3187
+ }
3188
+ const groups = repairGroupsForStrategy(planRepairGroups(store, task.id), strategy);
2866
3189
  if (groups.length === 0) {
2867
3190
  return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
2868
3191
  }
@@ -2903,7 +3226,7 @@ function planReviewRepairWave(args, store, options) {
2903
3226
  });
2904
3227
  const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
2905
3228
  + `(${group.findingIds.join(", ")})`));
2906
- return output(`Review repair wave for ${task.id} (${groups.length} group(s)):\n${lines.join("\n")}\n`, { groups: created });
3229
+ return output(`Review repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`, { strategy, groups: created });
2907
3230
  }
2908
3231
  const lines = groups.map((group, index) => {
2909
3232
  const findings = group.findings
@@ -2912,7 +3235,24 @@ function planReviewRepairWave(args, store, options) {
2912
3235
  return `wave ${index + 1}: ${findings}`
2913
3236
  + ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
2914
3237
  });
2915
- return output(`Repair wave for ${task.id} (${groups.length} group(s), run disjoint groups in parallel):\n${lines.join("\n")}\n`);
3238
+ return output(`Repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`
3239
+ + (strategy === "consolidated"
3240
+ ? "Default: keep all findings in one WorkItem; use --strategy parallel only for proven independent ownership.\n"
3241
+ : "Parallel strategy explicitly selected; each disjoint group may become one WorkItem.\n"));
3242
+ }
3243
+ function repairGroupsForStrategy(groups, strategy) {
3244
+ if (strategy === "parallel" || groups.length <= 1)
3245
+ return groups;
3246
+ const findings = groups.flatMap(({ findings }) => findings)
3247
+ .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
3248
+ return [{
3249
+ groupKey: findings.map(({ id }) => id).join("+"),
3250
+ findings,
3251
+ findingIds: findings.map(({ id }) => id),
3252
+ affectedPaths: [...new Set(groups.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
3253
+ affectedSymbols: [...new Set(groups.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
3254
+ invariants: [...new Set(groups.flatMap(({ invariants }) => invariants))].sort()
3255
+ }];
2916
3256
  }
2917
3257
  function reviewRepairProjectIds(task, affectedPaths) {
2918
3258
  const bindings = task.projectBindings;
@@ -3155,9 +3495,9 @@ function requestTaskReviewRound(args, store, options) {
3155
3495
  const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
3156
3496
  /**
3157
3497
  * 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.
3498
+ * terminal Round durably proves that no semantic review was produced. The
3499
+ * source Round, Run, findings, workspace, and terminal report remain immutable
3500
+ * history; the linking Event is both the audit record and the idempotence key.
3161
3501
  */
3162
3502
  function forceFreshTaskReviewRound(args, store, options) {
3163
3503
  const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
@@ -3206,9 +3546,9 @@ function forceFreshTaskReviewRound(args, store, options) {
3206
3546
  }
3207
3547
  return { round: replacement, source, created: false };
3208
3548
  }
3209
- const semanticBlocker = forceFreshSemanticBlocker(tx, source);
3210
- if (semanticBlocker !== null) {
3211
- throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${semanticBlocker}`);
3549
+ const recovery = classifyForceFreshReviewRecovery(tx, source);
3550
+ if (recovery.kind === "semantic-or-ambiguous") {
3551
+ throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${recovery.reason}`);
3212
3552
  }
3213
3553
  if (source.taskCandidate === undefined) {
3214
3554
  throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
@@ -3275,7 +3615,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3275
3615
  candidateId: created.candidateId,
3276
3616
  reviewerRoleName: created.reviewerRoleName,
3277
3617
  taskCandidate: JSON.stringify(created.taskCandidate),
3278
- reason: "source-round-failed-without-semantic-review",
3618
+ reason: "source-round-terminal-without-semantic-review",
3279
3619
  leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3280
3620
  }, now);
3281
3621
  return { round: created, source, created: true };
@@ -3284,76 +3624,23 @@ function forceFreshTaskReviewRound(args, store, options) {
3284
3624
  ? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
3285
3625
  : `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
3286
3626
  }
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));
3627
+ /**
3628
+ * Conservatively classifies an immutable Task-final Review as replaceable.
3629
+ * A failed Round keeps the existing no-semantic-evidence behavior. A completed
3630
+ * Round needs stronger, mutually corroborating evidence: an explicit internal
3631
+ * context/workspace failure, its exact yielded Run and receipt, matching Lane
3632
+ * output, and the mechanically emitted empty completion Event.
3633
+ * This is command eligibility only; it never rewrites the source outcome or
3634
+ * changes the global semantic classifier used by the finding ledger.
3635
+ */
3636
+ export function classifyForceFreshReviewRecovery(store, round) {
3637
+ const classification = classifyReviewRoundOutcome(round, store);
3638
+ return classification?.kind === "non-semantic"
3639
+ ? { kind: "non-semantic-terminal", reason: classification.reason }
3640
+ : {
3641
+ kind: "semantic-or-ambiguous",
3642
+ reason: classification?.reason ?? `source status is ${round.status}, not terminal.`
3643
+ };
3357
3644
  }
3358
3645
  /**
3359
3646
  * Issue 07: re-validates the CLI-computed delta preflight inside the store
@@ -3369,8 +3656,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
3369
3656
  const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
3370
3657
  if (previous === null
3371
3658
  || (previous.scope ?? "work-item") !== "task"
3372
- || previous.status !== "completed") {
3373
- throw usageError(`Delta-recheck previous ReviewRound is not a completed Task-final Review: `
3659
+ || !isSemanticReviewRound(previous, store)) {
3660
+ throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
3374
3661
  + `${preflight.record.previousReviewRoundId}.`);
3375
3662
  }
3376
3663
  if (previous.reviewerRoleName !== reviewerRoleName) {
@@ -3577,10 +3864,64 @@ function taskRunCommand(args, store, options) {
3577
3864
  return yieldRunStatus(rest, store, options);
3578
3865
  if (command === "checkpoint")
3579
3866
  return output(checkpointRun(rest, store, options));
3867
+ if (command === "retire")
3868
+ return retireRun(rest, store, options);
3580
3869
  throw usageError(command === undefined
3581
3870
  ? "Task run command is required."
3582
3871
  : `Unknown command: task run ${command}`);
3583
3872
  }
3873
+ function retireRun(args, store, options) {
3874
+ const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text>.";
3875
+ const parsed = parseTail(args, new Set(["--reason"]), usage);
3876
+ exactPositionals(parsed.positionals, 1, usage);
3877
+ const reason = requiredOption(parsed.options, "--reason");
3878
+ const reference = taskRecordReference(parsed.positionals[0], "agentRun", "Agent Run reference", options);
3879
+ const now = clock(options);
3880
+ const result = store.transaction((tx) => {
3881
+ const task = requireTask(tx, reference.taskId);
3882
+ assertTaskOpen(task);
3883
+ const actor = taskActor(options, task.id);
3884
+ if (actor === "leader") {
3885
+ throw usageError("Only the user or global Operator may retire an Agent Run.");
3886
+ }
3887
+ let run = tx.getAgentRun(task.id, reference.localId);
3888
+ if (run === null)
3889
+ throw dataError(`Agent Run not found: ${task.id}/${reference.localId}.`);
3890
+ const events = tx.listEvents(task.id);
3891
+ if (isTaskRecordRetired(events, "agent-run", run.id)) {
3892
+ return { task, run, changed: false };
3893
+ }
3894
+ if (run.status === "active") {
3895
+ const terminal = terminalizeExactTaskRun(tx, {
3896
+ taskId: task.id,
3897
+ roleName: run.roleName,
3898
+ agentId: run.effective.agentId,
3899
+ runId: run.id,
3900
+ receiptId: agentRunDeliveryReceiptId(run),
3901
+ outcome: { status: "failed", summary: `Agent Run retired: ${reason}` }
3902
+ }, now);
3903
+ if (terminal.disposition !== "applied" || terminal.run === null) {
3904
+ throw usageError(`Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
3905
+ }
3906
+ run = terminal.run;
3907
+ }
3908
+ tx.saveEvent(task.id, createTaskRecordRetirement({
3909
+ eventId: tx.nextEventId(task.id),
3910
+ taskId: task.id,
3911
+ recordKind: "agent-run",
3912
+ recordId: run.id,
3913
+ reason,
3914
+ retiredBy: actor
3915
+ }, now));
3916
+ return { task, run, changed: true };
3917
+ });
3918
+ if (result.changed)
3919
+ options.runtime?.notifyStateChanged(result.task.id);
3920
+ return output(`Retired Agent Run ${result.task.id}/${result.run.id}\n`, {
3921
+ agentRun: result.run,
3922
+ retired: true
3923
+ });
3924
+ }
3584
3925
  function runContextCommand(args, store, options) {
3585
3926
  const [first, ...rest] = args;
3586
3927
  if (first === "expand") {
@@ -3654,6 +3995,7 @@ function listRuns(args, store, options) {
3654
3995
  const runs = store.listAgentRuns(item.taskId).filter((run) => run.workItemId === item.id);
3655
3996
  if (runs.length === 0)
3656
3997
  return "No runs found.\n";
3998
+ const events = store.listEvents(item.taskId);
3657
3999
  return `${renderTable(`Runs: ${item.id}`, [
3658
4000
  { header: "Run", minWidth: 6, maxWidth: 20 },
3659
4001
  { header: "Role", minWidth: 4, maxWidth: 22 },
@@ -3663,6 +4005,7 @@ function listRuns(args, store, options) {
3663
4005
  { header: "Profile", minWidth: 7, maxWidth: 8 },
3664
4006
  { header: "Permission", minWidth: 8, maxWidth: 16 },
3665
4007
  { header: "Status", minWidth: 6, maxWidth: 12 },
4008
+ { header: "History", minWidth: 7, maxWidth: 9 },
3666
4009
  { header: "Summary", minWidth: 8, maxWidth: 58 }
3667
4010
  ], runs.map((run) => [
3668
4011
  run.id,
@@ -3673,6 +4016,7 @@ function listRuns(args, store, options) {
3673
4016
  run.effective.profileAccess,
3674
4017
  run.effective.permission.strategy,
3675
4018
  run.status,
4019
+ isTaskRecordRetired(events, "agent-run", run.id) ? "retired" : "active",
3676
4020
  run.summary ?? "-"
3677
4021
  ]), defaultTableWidth())}\n`;
3678
4022
  }
@@ -3986,7 +4330,7 @@ function retryRun(args, store, options) {
3986
4330
  }
3987
4331
  function actualTaskReviewCandidateForMutation(store, task, options) {
3988
4332
  if (options.actualTaskReviewCandidate === undefined) {
3989
- throw usageError(`Actual Task Project heads were not verified for final Review: ${task.id}.`);
4333
+ throw usageError(`Actual Task Project heads were not verified for delivery: ${task.id}.`);
3990
4334
  }
3991
4335
  let actual;
3992
4336
  try {
@@ -4202,8 +4546,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4202
4546
  // Any Task-final ReviewRound is durable completion evidence/obligation.
4203
4547
  // Once one exists, later changes to the mutable global review config cannot
4204
4548
  // 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.
4549
+ // Round, the current global `final` config establishes the initial Round
4550
+ // only for integrated delivery.
4207
4551
  const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
4208
4552
  .filter((round) => ((round.scope ?? "work-item") === "task"
4209
4553
  && (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
@@ -4214,7 +4558,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4214
4558
  }
4215
4559
  else if (establishedRound === undefined) {
4216
4560
  const globalConfig = store.getReviewConfig();
4217
- config = globalConfig?.trigger === "final" ? globalConfig : null;
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;
4218
4570
  }
4219
4571
  else {
4220
4572
  config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
@@ -4259,7 +4611,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
4259
4611
  return escalated;
4260
4612
  }
4261
4613
  else {
4262
- return latest.status === "completed" ? null : latest;
4614
+ return latest.status === "completed"
4615
+ && classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
4616
+ ? null
4617
+ : latest;
4263
4618
  }
4264
4619
  }
4265
4620
  const anchor = taskFinalContract === undefined
@@ -4672,14 +5027,21 @@ function showRun(args, store, options) {
4672
5027
  const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
4673
5028
  if (facts === null)
4674
5029
  throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
4675
- return { run, recovery: projectRunRecovery(facts) };
5030
+ const retirement = tx.listEvents(run.taskId)
5031
+ .map(taskRecordRetirement)
5032
+ .find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
5033
+ return { run, recovery: projectRunRecovery(facts), retirement };
4676
5034
  });
4677
5035
  if (asJson) {
4678
5036
  return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
4679
5037
  }
4680
- return { kind: "output", output: renderRunShow(data.run, data.recovery), data };
5038
+ return {
5039
+ kind: "output",
5040
+ output: renderRunShow(data.run, data.recovery, data.retirement),
5041
+ data
5042
+ };
4681
5043
  }
4682
- function renderRunShow(run, recovery) {
5044
+ function renderRunShow(run, recovery, retirement) {
4683
5045
  const lines = [
4684
5046
  `Run: ${run.id}`,
4685
5047
  `Task: ${run.taskId}`,
@@ -4687,6 +5049,10 @@ function renderRunShow(run, recovery) {
4687
5049
  `Purpose: ${run.purpose}`,
4688
5050
  `Mode: ${run.mode}`,
4689
5051
  `Status: ${run.status}`,
5052
+ ...(retirement === null ? [] : [
5053
+ `History: retired by ${retirement.retiredBy}`,
5054
+ `Retirement reason: ${retirement.reason}`
5055
+ ]),
4690
5056
  `Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
4691
5057
  `Created: ${run.createdAt}`,
4692
5058
  ...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
@@ -5380,8 +5746,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5380
5746
  let deltaContext = "";
5381
5747
  if (taskScope && round.deltaRecheck !== undefined) {
5382
5748
  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}.`);
5749
+ if (previousRound === null || !isSemanticReviewRound(previousRound, tx)) {
5750
+ throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous semantic ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
5385
5751
  }
5386
5752
  const diffByProject = options.deltaRecheckDiff;
5387
5753
  if (diffByProject === undefined) {
@@ -5669,7 +6035,9 @@ function publishedTreeAuthorizationPayload(actor, proof) {
5669
6035
  by: actor,
5670
6036
  projectId: proof.projectId,
5671
6037
  publicationId: proof.publicationId,
5672
- reviewRoundId: proof.reviewRoundId,
6038
+ ...(proof.reviewRoundId === undefined
6039
+ ? { reviewAnchor: publishedTreeReviewAnchor(proof) }
6040
+ : { reviewRoundId: proof.reviewRoundId }),
5673
6041
  localCommit: proof.localCommit,
5674
6042
  remoteCommit: proof.remoteCommit,
5675
6043
  tree: proof.tree
@@ -5685,7 +6053,8 @@ function matchingPublishedTreeAuthorization(store, proof) {
5685
6053
  && (event.payload.by === "user" || event.payload.by === "operator")
5686
6054
  && event.payload.projectId === proof.projectId
5687
6055
  && event.payload.publicationId === proof.publicationId
5688
- && event.payload.reviewRoundId === proof.reviewRoundId
6056
+ && (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
6057
+ === publishedTreeReviewAnchor(proof)
5689
6058
  && event.payload.localCommit === proof.localCommit
5690
6059
  && event.payload.remoteCommit === proof.remoteCommit
5691
6060
  && event.payload.tree === proof.tree) {
@@ -5698,10 +6067,15 @@ function requirePublishedTreeAuthorization(store, proof) {
5698
6067
  const authorization = matchingPublishedTreeAuthorization(store, proof);
5699
6068
  if (authorization === undefined) {
5700
6069
  throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
5701
- + `${proof.taskId}/${proof.publicationId} at Task-final Review ${proof.reviewRoundId}.`);
6070
+ + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
6071
+ ? "the direct Task-main head"
6072
+ : `Task-final Review ${proof.reviewRoundId}`}.`);
5702
6073
  }
5703
6074
  return authorization;
5704
6075
  }
6076
+ function publishedTreeReviewAnchor(proof) {
6077
+ return proof.reviewRoundId ?? "direct";
6078
+ }
5705
6079
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
5706
6080
  function truncateEventNote(note) {
5707
6081
  const normalized = note.trim();
@@ -5822,7 +6196,8 @@ function taskRecordReference(value, kind, label, options) {
5822
6196
  function assertWorkItemDependenciesCompleted(store, item) {
5823
6197
  for (const dependencyId of item.dependsOn) {
5824
6198
  const dependency = store.getWorkItem(item.taskId, dependencyId);
5825
- if (dependency === null || dependency.status !== "completed") {
6199
+ if (dependency === null
6200
+ || (dependency.status !== "completed" && dependency.status !== "retired")) {
5826
6201
  throw usageError(`Work Item dependency is not completed: ${dependencyId}.`);
5827
6202
  }
5828
6203
  }
@@ -5962,6 +6337,11 @@ function parseTaskPriority(value) {
5962
6337
  return value;
5963
6338
  throw usageError(`Invalid Task priority: ${value}.`);
5964
6339
  }
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
+ }
5965
6345
  function parseTaskTags(value) {
5966
6346
  const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
5967
6347
  if (tags.length === 0)