@zq-silk/yui 0.8.2 → 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 (68) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +61 -9
  3. package/dist/cli/commandCatalog.js +31 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +187 -22
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +648 -74
  9. package/dist/commands/taskCompletionGate.js +166 -2
  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/runContextPack.js +19 -4
  14. package/dist/context/sessionBootstrapManifest.js +83 -2
  15. package/dist/controller/clientRuntime.js +7 -7
  16. package/dist/controller/controller.js +16 -8
  17. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  18. package/dist/controller/handoverCandidate.js +10 -3
  19. package/dist/controller/sessionNotify.js +4 -22
  20. package/dist/executor/agentAdapter.js +2 -2
  21. package/dist/executor/agentExecutor.js +25 -5
  22. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  23. package/dist/integration/gitIntegrationService.js +50 -2
  24. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  25. package/dist/observability/executionAudit.js +47 -1
  26. package/dist/observability/faultClassification.js +6 -4
  27. package/dist/observability/orchestrationMetrics.js +196 -0
  28. package/dist/operator/operatorSessionHistory.js +36 -0
  29. package/dist/release/releaseHandover.js +7 -5
  30. package/dist/release/runtimeRelease.js +15 -0
  31. package/dist/repository/gitWorkspace.js +7 -4
  32. package/dist/repository/taskBaseFreshness.js +4 -2
  33. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  34. package/dist/review/deltaRecheck.js +3 -2
  35. package/dist/review/reviewFindingLedger.js +5 -4
  36. package/dist/review/reviewOutcomeClassifier.js +252 -54
  37. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  38. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/runIdentity.js +10 -70
  41. package/dist/runtime/agentHost.js +3 -4
  42. package/dist/runtime/codexAppServerRuntime.js +6 -0
  43. package/dist/runtime/firstProgressStopLoss.js +52 -0
  44. package/dist/runtime/launchBroker.js +10 -2
  45. package/dist/runtime/runtimeDeadlines.js +14 -0
  46. package/dist/runtime/sessionTitle.js +24 -12
  47. package/dist/runtime/structuredProviderHost.js +7 -1
  48. package/dist/runtime/tmuxAdapters.js +10 -3
  49. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  50. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  51. package/dist/scheduler/wakeReason.js +2 -0
  52. package/dist/storage/sqliteStore.js +10 -1
  53. package/dist/storage/taskStore.js +12 -1
  54. package/dist/task/completionReadiness.js +91 -19
  55. package/dist/task/deliveryGuard.js +3 -1
  56. package/dist/task/nextAction.js +146 -51
  57. package/dist/task/publicationReference.js +1 -0
  58. package/dist/task/repairWave.js +14 -1
  59. package/dist/task/task.js +10 -0
  60. package/dist/web/webSnapshot.js +7 -1
  61. package/dist/workItem/workItem.js +12 -0
  62. package/dist/workspace/workItemChangeSetManager.js +2 -1
  63. package/i18n/README.zh-CN.md +28 -8
  64. package/package.json +1 -1
  65. package/skills/yui-leader/SKILL.md +79 -32
  66. package/skills/yui-operator/SKILL.md +51 -10
  67. package/skills/yui-reviewer/SKILL.md +23 -0
  68. package/skills/yui-runtime/SKILL.md +7 -2
@@ -7,7 +7,8 @@ import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageErr
7
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
- import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
10
+ import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
11
+ import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
11
12
  import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
12
13
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
13
14
  import { defaultTableWidth, renderTable } from "../output/table.js";
@@ -34,14 +35,17 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
34
35
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
35
36
  import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
36
37
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
37
- 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";
38
39
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
40
+ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
39
41
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
40
42
  import { resolveProject } from "../repository/project.js";
41
43
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
42
- import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
44
+ import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
43
45
  import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
44
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";
45
49
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
46
50
  import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
47
51
  import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
@@ -50,7 +54,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
50
54
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
51
55
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
52
56
  import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
53
- import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
57
+ import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
54
58
  import { runGrantCommand } from "./grantCommands.js";
55
59
  import { runWorkflowCommand } from "./workflowCommands.js";
56
60
  import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
@@ -79,17 +83,17 @@ function legacyWorkItemReviewConfig(config) {
79
83
  return config?.trigger === "final" ? null : config;
80
84
  }
81
85
  function storedTaskFinalReviewContract(store, taskId) {
82
- const contracts = store.listWorkItems(taskId)
83
- .flatMap(({ candidates }) => candidates)
84
- .flatMap(({ taskFinalReviewContract }) => (taskFinalReviewContract === undefined ? [] : [taskFinalReviewContract]));
85
- const first = contracts[0];
86
- if (first === undefined)
87
- return undefined;
88
- validateTaskFinalReviewContract(first);
89
- if (contracts.some((contract) => !sameTaskFinalReviewContract(first, contract))) {
90
- 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.`);
91
96
  }
92
- return first;
93
97
  }
94
98
  /**
95
99
  * Resolve one exact Task-local contract before the caller performs any write.
@@ -97,7 +101,7 @@ function storedTaskFinalReviewContract(store, taskId) {
97
101
  * acceptance, and completion mutation must present the same verified
98
102
  * capability; shared review-config drift is intentionally irrelevant.
99
103
  */
100
- function taskFinalReviewContractForMutation(store, taskId, options) {
104
+ function taskFinalReviewContractForMutation(store, taskId, options, authorization = {}) {
101
105
  const supplied = options.taskFinalReviewContract;
102
106
  if (supplied !== undefined) {
103
107
  validateTaskFinalReviewContract(supplied);
@@ -113,6 +117,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
113
117
  if (stored === undefined)
114
118
  return supplied;
115
119
  if (supplied === undefined) {
120
+ if (authorization.allowStoredWithoutSupplied === true)
121
+ return stored;
116
122
  throw usageError(`Task final-review contract is missing for ${taskId}.`);
117
123
  }
118
124
  if (!sameTaskFinalReviewContract(stored, supplied)) {
@@ -121,8 +127,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
121
127
  return stored;
122
128
  }
123
129
  export function parseTaskCompletionRequest(args, summaryOverride) {
124
- const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote].";
125
- const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage, new Set(["--refresh-remote"]));
130
+ const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
131
+ const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), usage, new Set(["--refresh-remote"]));
126
132
  exactPositionals(parsed.positionals, 1, usage);
127
133
  const inlineSummary = parsed.options.get("--summary");
128
134
  const summaryFile = parsed.options.get("--summary-file");
@@ -130,7 +136,14 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
130
136
  throw usageError(`Specify exactly one of --summary or --summary-file.`, usage);
131
137
  }
132
138
  const summary = summaryOverride ?? readCommandText(inlineSummary, summaryFile, "--summary", usage);
133
- return { taskId: parsed.positionals[0], summary };
139
+ const acceptedPublishedTreePublicationId = parsed.options.get("--accept-published-tree");
140
+ return {
141
+ taskId: parsed.positionals[0],
142
+ summary,
143
+ ...(acceptedPublishedTreePublicationId === undefined
144
+ ? {}
145
+ : { acceptedPublishedTreePublicationId })
146
+ };
134
147
  }
135
148
  /**
136
149
  * Check every read-only completion blocker before a caller resolves remote
@@ -144,7 +157,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
144
157
  * terminalization precondition before attempting completion. All blockers
145
158
  * are reported in one error instead of one per attempt.
146
159
  */
147
- export function preflightTaskCompletion(taskId, store, options = {}) {
160
+ export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
148
161
  const task = requireTask(store, taskId);
149
162
  const actor = taskActor(options, task.id);
150
163
  if (task.status === "completed") {
@@ -157,7 +170,15 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
157
170
  // Resolve and authenticate the durable Task-local gate before any remote
158
171
  // fetch or Integration write. All checks below mirror the transactional
159
172
  // completion path, which remains the final CAS fence after reconciliation.
160
- const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
173
+ // A human/global Operator cannot present the exact managed Leader contract.
174
+ // For the explicit published-tree path only, let that caller authenticate
175
+ // the stored contract far enough to persist an exact authorization fact.
176
+ // The same command must return before any contract-governed completion
177
+ // mutation; the exact Leader later consumes the authorization with the real
178
+ // contract capability.
179
+ const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
180
+ && actor !== "leader";
181
+ const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
161
182
  const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
162
183
  && (round.status === "pending" || round.status === "running")));
163
184
  // Issue 06: one shared readiness projection enumerates every blocker.
@@ -344,12 +365,14 @@ function taskProjectCommand(args, store, options) {
344
365
  return output(`Added Project to ${updated.id}\n`, { task: updated });
345
366
  }
346
367
  function updateTaskCommand(args, store, options) {
347
- const optionNames = new Set(["--title", "--description", "--priority", "--tags", "--due-at"]);
368
+ const optionNames = new Set([
369
+ "--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
370
+ ]);
348
371
  const flagOptions = new Set([
349
372
  "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
350
373
  "--require-integration"
351
374
  ]);
352
- 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].";
353
376
  const parsed = parseTail(args, optionNames, usage, flagOptions);
354
377
  exactPositionals(parsed.positionals, 1, usage);
355
378
  if (parsed.options.size === 0)
@@ -373,19 +396,42 @@ function updateTaskCommand(args, store, options) {
373
396
  const tags = parsed.options.has("--tags")
374
397
  ? parseTaskTags(requiredOption(parsed.options, "--tags"))
375
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");
376
407
  const now = clock(options);
377
408
  const result = store.transaction((tx) => {
378
409
  const current = requireTask(tx, parsed.positionals[0]);
379
410
  if (current.status === "archived")
380
411
  throw usageError(`Task is archived: ${current.id}.`);
381
- 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") {
382
420
  throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
383
421
  }
422
+ if (enableIntegration && current.requireIntegration !== true) {
423
+ assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
424
+ }
384
425
  if (parsed.options.size === 1
385
- && parsed.options.has("--require-integration")
426
+ && enableIntegration
386
427
  && current.requireIntegration === true) {
387
428
  return { task: current, integrationState: "already-enabled" };
388
429
  }
430
+ if (parsed.options.size === 1
431
+ && requestedDelivery === "direct"
432
+ && taskDeliveryPath(current) === "direct") {
433
+ return { task: current, integrationState: "already-direct" };
434
+ }
389
435
  const updated = updateTaskMetadata(current, {
390
436
  ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
391
437
  ...(parsed.options.has("--description")
@@ -400,31 +446,93 @@ function updateTaskCommand(args, store, options) {
400
446
  ...(dueAt === undefined
401
447
  ? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
402
448
  : { dueAt }),
403
- ...(parsed.options.has("--require-integration") ? { requireIntegration: true } : {})
449
+ ...(enableIntegration ? { requireIntegration: true } : {})
404
450
  }, now);
405
451
  tx.saveTask(updated);
406
452
  recordTaskEvent(tx, updated.id, "task.updated", {
407
453
  status: updated.status,
408
- ...(parsed.options.has("--require-integration")
409
- ? { completionEvidence: "integration-required" }
410
- : {})
454
+ ...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
455
+ ? {}
456
+ : {
457
+ completionEvidence: enableIntegration
458
+ ? "integration-required"
459
+ : "direct",
460
+ deliveryPath: taskDeliveryPath(updated)
461
+ })
411
462
  }, now);
412
463
  enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
413
464
  return {
414
465
  task: updated,
415
- integrationState: parsed.options.has("--require-integration")
466
+ integrationState: enableIntegration
416
467
  ? "enabled"
417
- : "unchanged"
468
+ : requestedDelivery === "direct"
469
+ ? "direct"
470
+ : "unchanged"
418
471
  };
419
472
  });
420
- if (result.integrationState !== "already-enabled") {
473
+ if (result.integrationState !== "already-enabled"
474
+ && result.integrationState !== "already-direct") {
421
475
  notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
422
476
  }
423
477
  return result.integrationState === "enabled"
424
- ? `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`
425
479
  : result.integrationState === "already-enabled"
426
- ? `Task ${result.task.id} completion evidence is already enabled\n`
427
- : `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
+ }
428
536
  }
429
537
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
430
538
  export function runTaskOutputCommand(args, store, options = {}) {
@@ -465,13 +573,20 @@ function createTaskCommand(args, store, options) {
465
573
  notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
466
574
  return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
467
575
  + `Assigned role: ${created.leader.name}\n`
576
+ + `Delivery: ${taskDeliveryPath(created.task)}\n`
468
577
  + (created.task.requireIntegration
469
578
  ? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
470
- : "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
+ });
471
586
  }
472
587
  function parseTaskCreation(args, store) {
473
- const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--require-integration].";
474
- 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"]));
475
590
  exactPositionals(parsed.positionals, 1, usage);
476
591
  const projectReferences = parsed.multiOptions.get("--project") ?? [];
477
592
  const baseOptions = parsed.multiOptions.get("--base") ?? [];
@@ -487,6 +602,16 @@ function parseTaskCreation(args, store) {
487
602
  if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
488
603
  throw usageError("A Task cannot bind the same Project more than once.");
489
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
+ }
490
615
  const bases = new Map();
491
616
  for (const option of baseOptions) {
492
617
  const separator = option.indexOf("=");
@@ -517,7 +642,8 @@ function parseTaskCreation(args, store) {
517
642
  baseRef: bases.get(project.id) ?? project.developmentBranch
518
643
  })),
519
644
  defaultProjectIds,
520
- requireIntegration: parsed.options.has("--require-integration")
645
+ requireIntegration: requestedDelivery === "integrated"
646
+ || parsed.options.has("--require-integration")
521
647
  };
522
648
  }
523
649
  function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
@@ -527,6 +653,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
527
653
  store.saveRole(task.id, leader);
528
654
  recordTaskEvent(store, task.id, "task.created", {
529
655
  status: task.status,
656
+ deliveryPath: taskDeliveryPath(task),
530
657
  ...(defaultProjectIds.length === 0
531
658
  ? {}
532
659
  : { defaultProjectIds: defaultProjectIds.join(",") })
@@ -575,11 +702,16 @@ function showTaskCommand(args, store) {
575
702
  `Task: ${task.id}`,
576
703
  `Title: ${task.title}`,
577
704
  `Status: ${task.status}`,
705
+ `Delivery: ${taskDeliveryPath(task)}`,
578
706
  ...(task.description === undefined ? [] : [`Description: ${task.description}`]),
579
707
  ...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
580
708
  ...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
581
709
  ...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
582
- `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"}`,
583
715
  ...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
584
716
  ...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
585
717
  ...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
@@ -608,7 +740,12 @@ function showTaskCommand(args, store) {
608
740
  `Created: ${presentTime(task.createdAt, timeZone)}`,
609
741
  `Updated: ${presentTime(task.updatedAt, timeZone)}`
610
742
  ].join("\n").concat("\n");
611
- 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
+ });
612
749
  }
613
750
  function activateTaskCommand(args, store, options) {
614
751
  exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
@@ -647,17 +784,47 @@ function completeTaskCommand(args, store, options) {
647
784
  const summary = request.summary;
648
785
  const now = clock(options);
649
786
  const result = store.transaction((tx) => {
650
- const preflight = preflightTaskCompletion(request.taskId, tx, options);
787
+ const preflight = preflightTaskCompletion(request.taskId, tx, options, request);
651
788
  const { task, actor } = preflight;
652
789
  if (preflight.completed) {
653
790
  return {
654
791
  task,
655
792
  changed: false,
656
793
  runtimeCleanupTargets: [],
657
- finalReview: undefined
794
+ completionAdvisories: [],
795
+ finalReview: undefined,
796
+ publishedTreeAuthorization: undefined
658
797
  };
659
798
  }
660
799
  const taskFinalContract = preflight.taskFinalReviewContract;
800
+ const actualTaskCandidate = task.projectBindings.length === 0
801
+ ? undefined
802
+ : actualTaskReviewCandidateForMutation(tx, task, options);
803
+ const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
804
+ ? undefined
805
+ : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
806
+ const requiresContractHandoff = publishedTreeProof !== undefined
807
+ && taskFinalContract !== undefined;
808
+ if (requiresContractHandoff && actor !== "leader") {
809
+ const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
810
+ const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
811
+ enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
812
+ return {
813
+ task,
814
+ changed: false,
815
+ runtimeCleanupTargets: [],
816
+ completionAdvisories: [],
817
+ finalReview: undefined,
818
+ publishedTreeAuthorization: {
819
+ event,
820
+ proof: publishedTreeProof,
821
+ created: existing === undefined
822
+ }
823
+ };
824
+ }
825
+ if (publishedTreeProof !== undefined && actor === "leader") {
826
+ requirePublishedTreeAuthorization(tx, publishedTreeProof);
827
+ }
661
828
  const roles = tx.listRoles(task.id);
662
829
  const activeRuns = roles
663
830
  .map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
@@ -704,9 +871,11 @@ function completeTaskCommand(args, store, options) {
704
871
  task,
705
872
  changed: false,
706
873
  runtimeCleanupTargets: [],
874
+ completionAdvisories: [],
707
875
  finalReview,
708
876
  resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
709
- terminalizedLeaderRun
877
+ terminalizedLeaderRun,
878
+ publishedTreeAuthorization: undefined
710
879
  };
711
880
  }
712
881
  // Issue 06: re-validate the full completion readiness inside the
@@ -729,7 +898,34 @@ function completeTaskCommand(args, store, options) {
729
898
  tx.clearOperatorNotification(task.id);
730
899
  tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
731
900
  enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
732
- recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
901
+ if (publishedTreeProof !== undefined) {
902
+ recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
903
+ by: actor,
904
+ projectId: publishedTreeProof.projectId,
905
+ publicationId: publishedTreeProof.publicationId,
906
+ ...(publishedTreeProof.reviewRoundId === undefined
907
+ ? {}
908
+ : { reviewRoundId: publishedTreeProof.reviewRoundId }),
909
+ localCommit: publishedTreeProof.localCommit,
910
+ remoteCommit: publishedTreeProof.remoteCommit,
911
+ tree: publishedTreeProof.tree
912
+ }, now);
913
+ }
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);
733
929
  // A terminal Task must never leave a Task-lane signal that can wake it.
734
930
  // The durable records remain intact; only the derived mailbox work is
735
931
  // discarded at this lifecycle boundary.
@@ -745,11 +941,24 @@ function completeTaskCommand(args, store, options) {
745
941
  task: completed,
746
942
  changed: true,
747
943
  runtimeCleanupTargets,
944
+ completionAdvisories: readiness.advisories,
748
945
  finalReview: undefined,
749
946
  resumedPendingFinalReview: false,
750
- terminalizedLeaderRun
947
+ terminalizedLeaderRun,
948
+ publishedTreeAuthorization: undefined
751
949
  };
752
950
  });
951
+ if (result.publishedTreeAuthorization !== undefined) {
952
+ notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
953
+ const authorization = result.publishedTreeAuthorization;
954
+ return output(authorization.created
955
+ ? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
956
+ : `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
957
+ task: result.task,
958
+ authorizationEvent: authorization.event,
959
+ publishedTreeProof: authorization.proof
960
+ });
961
+ }
753
962
  if (result.changed) {
754
963
  for (const target of result.runtimeCleanupTargets) {
755
964
  // Cleanup owns an independent lifecycle lane. Never fall back to the
@@ -769,9 +978,17 @@ function completeTaskCommand(args, store, options) {
769
978
  [TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
770
979
  });
771
980
  }
772
- return output(result.changed
981
+ const completionOutput = result.changed
773
982
  ? `Completed task ${result.task.id}\n`
774
- : `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
+ });
775
992
  }
776
993
  function reopenTaskCommand(args, store, options) {
777
994
  exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
@@ -2606,8 +2823,12 @@ function reviewWork(args, store, options) {
2606
2823
  */
2607
2824
  function taskReviewCommand(args, store, options) {
2608
2825
  const [command, ...rest] = args;
2826
+ if (command === "rebind")
2827
+ return rebindTaskFinalReviewContract(rest, store, options);
2609
2828
  if (command === "request")
2610
2829
  return requestTaskReviewRound(rest, store, options);
2830
+ if (command === "force-fresh")
2831
+ return forceFreshTaskReviewRound(rest, store, options);
2611
2832
  if (command === "retry")
2612
2833
  return retryFailedTaskReviewRound(rest, store, options);
2613
2834
  if (command === "group")
@@ -2618,6 +2839,115 @@ function taskReviewCommand(args, store, options) {
2618
2839
  ? "Task review command is required."
2619
2840
  : `Unknown command: task review ${command}`);
2620
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
+ }
2621
2951
  function resolveReviewExecutionGroup(args, store, options) {
2622
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> ...].";
2623
2953
  if (args[0] !== "resolve")
@@ -2712,8 +3042,8 @@ function resolveReviewExecutionGroup(args, store, options) {
2712
3042
  /**
2713
3043
  * Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
2714
3044
  * Findings are extracted automatically from completed Rounds; these commands
2715
- * let the Leader inspect the ledger, disposition each finding, and plan the
2716
- * 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.
2717
3047
  */
2718
3048
  function reviewFindingCommand(args, store, options) {
2719
3049
  const [command, ...rest] = args;
@@ -2784,11 +3114,15 @@ function disposeReviewFindingCommand(args, store, options) {
2784
3114
  return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
2785
3115
  }
2786
3116
  function planReviewRepairWave(args, store, options) {
2787
- const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--create].";
2788
- 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"]));
2789
3119
  exactPositionals(parsed.positionals, 1, usage);
2790
3120
  const task = requireTask(store, parsed.positionals[0]);
2791
- 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);
2792
3126
  if (groups.length === 0) {
2793
3127
  return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
2794
3128
  }
@@ -2829,7 +3163,7 @@ function planReviewRepairWave(args, store, options) {
2829
3163
  });
2830
3164
  const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
2831
3165
  + `(${group.findingIds.join(", ")})`));
2832
- 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 });
2833
3167
  }
2834
3168
  const lines = groups.map((group, index) => {
2835
3169
  const findings = group.findings
@@ -2838,7 +3172,24 @@ function planReviewRepairWave(args, store, options) {
2838
3172
  return `wave ${index + 1}: ${findings}`
2839
3173
  + ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
2840
3174
  });
2841
- 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
+ }];
2842
3193
  }
2843
3194
  function reviewRepairProjectIds(task, affectedPaths) {
2844
3195
  const bindings = task.projectBindings;
@@ -3078,6 +3429,156 @@ function requestTaskReviewRound(args, store, options) {
3078
3429
  : `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
3079
3430
  : `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
3080
3431
  }
3432
+ const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
3433
+ /**
3434
+ * Creates a distinct full Task-final ReviewRound only when the exact previous
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.
3438
+ */
3439
+ function forceFreshTaskReviewRound(args, store, options) {
3440
+ const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
3441
+ exactPositionals(args, 1, usage);
3442
+ const reference = taskRecordReference(args[0], "reviewRound", "ReviewRound reference", options);
3443
+ const now = clock(options);
3444
+ const result = store.transaction((tx) => {
3445
+ const source = tx.getReviewRound(reference.taskId, reference.localId);
3446
+ if (source === null) {
3447
+ throw dataError(`ReviewRound not found: ${reference.taskId}/${reference.localId}.`);
3448
+ }
3449
+ const task = requireTask(tx, reference.taskId);
3450
+ if (task.status !== "active")
3451
+ throw usageError(`Task is not active: ${task.id}.`);
3452
+ if (taskActor(options, task.id) !== "leader") {
3453
+ throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
3454
+ }
3455
+ if ((source.scope ?? "work-item") !== "task") {
3456
+ throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
3457
+ }
3458
+ const replacementEvents = tx.listEvents(task.id).filter((event) => (event.type === TASK_FINAL_FORCE_FRESH_EVENT
3459
+ && event.payload.sourceReviewRoundId === source.id));
3460
+ if (replacementEvents.length > 1) {
3461
+ throw dataError(`ReviewRound ${source.id} has duplicate force-fresh audit events.`);
3462
+ }
3463
+ const replacementEvent = replacementEvents[0];
3464
+ if (replacementEvent !== undefined) {
3465
+ const replacementId = replacementEvent.payload.reviewRoundId;
3466
+ const replacement = replacementId === undefined
3467
+ ? null
3468
+ : tx.getReviewRound(task.id, replacementId);
3469
+ if (replacement === null
3470
+ || replacement.id === source.id
3471
+ || (replacement.scope ?? "work-item") !== "task"
3472
+ || replacement.workItemId !== source.workItemId
3473
+ || replacement.candidateId !== source.candidateId
3474
+ || replacement.reviewerRoleName !== source.reviewerRoleName
3475
+ || replacement.deltaRecheck !== undefined
3476
+ || !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
3477
+ || !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
3478
+ || replacementEvent.payload.workItemId !== replacement.workItemId
3479
+ || replacementEvent.payload.candidateId !== replacement.candidateId
3480
+ || replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
3481
+ || replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
3482
+ throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
3483
+ }
3484
+ return { round: replacement, source, created: false };
3485
+ }
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}`);
3489
+ }
3490
+ if (source.taskCandidate === undefined) {
3491
+ throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
3492
+ }
3493
+ const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
3494
+ if (!sameTaskFinalReviewContract(source.taskFinalReviewContract, taskFinalContract)) {
3495
+ throw usageError(`Task final-review contract does not match ReviewRound ${source.id}.`);
3496
+ }
3497
+ if (source.executionGroup !== undefined
3498
+ && (source.executionGroup.strategy.mode !== "fixed"
3499
+ || source.executionGroup.strategy.count !== 1
3500
+ || source.executionGroup.lanes.length !== 1
3501
+ || source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
3502
+ throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
3503
+ }
3504
+ const item = tx.getWorkItem(task.id, source.workItemId);
3505
+ const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
3506
+ if (item === null || item === undefined || candidate === undefined) {
3507
+ throw dataError(`Final Review anchor Candidate is no longer available: `
3508
+ + `${source.workItemId}/${source.candidateId}.`);
3509
+ }
3510
+ const provenance = taskReviewProvenance(tx, task, options);
3511
+ if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
3512
+ throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
3513
+ }
3514
+ const producerCollision = taskReviewProducerCollision(provenance, source.reviewerRoleName);
3515
+ if (producerCollision !== null)
3516
+ throw usageError(producerCollision);
3517
+ const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
3518
+ .filter((entry) => (entry.scope ?? "work-item") === "task"));
3519
+ const sourceIndex = taskRounds.findIndex(({ id }) => id === source.id);
3520
+ if (sourceIndex < 0) {
3521
+ throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
3522
+ }
3523
+ const laterRound = taskRounds.slice(sourceIndex + 1).at(-1);
3524
+ if (laterRound !== undefined) {
3525
+ throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
3526
+ + `${laterRound.id}/${laterRound.status}.`);
3527
+ }
3528
+ assertNoConflictingTaskReviewRound(taskRounds, source.id);
3529
+ assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
3530
+ let reviewer = tx.getRole(task.id, source.reviewerRoleName);
3531
+ if (reviewer === null) {
3532
+ if (tx.getGlobalRole(source.reviewerRoleName) === null) {
3533
+ throw usageError(`Global Role not found: ${source.reviewerRoleName}.`);
3534
+ }
3535
+ reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
3536
+ tx.saveRole(task.id, reviewer);
3537
+ }
3538
+ let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.workItemId, source.candidateId, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
3539
+ const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3540
+ purpose: "review",
3541
+ target: executionTargetForReviewRound(task, created, item, candidate),
3542
+ strategy: { mode: "fixed", count: 1 },
3543
+ lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
3544
+ }, now);
3545
+ created = { ...created, executionGroup: group };
3546
+ tx.saveReviewRound(task.id, created);
3547
+ recordTaskEvent(tx, task.id, TASK_FINAL_FORCE_FRESH_EVENT, {
3548
+ sourceReviewRoundId: source.id,
3549
+ ...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
3550
+ reviewRoundId: created.id,
3551
+ workItemId: created.workItemId,
3552
+ candidateId: created.candidateId,
3553
+ reviewerRoleName: created.reviewerRoleName,
3554
+ taskCandidate: JSON.stringify(created.taskCandidate),
3555
+ reason: "source-round-terminal-without-semantic-review",
3556
+ leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3557
+ }, now);
3558
+ return { round: created, source, created: true };
3559
+ });
3560
+ return output(result.created
3561
+ ? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
3562
+ : `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
3563
+ }
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
+ };
3581
+ }
3081
3582
  /**
3082
3583
  * Issue 07: re-validates the CLI-computed delta preflight inside the store
3083
3584
  * transaction. The previous Round must be a completed acceptance (a full
@@ -3092,8 +3593,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
3092
3593
  const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
3093
3594
  if (previous === null
3094
3595
  || (previous.scope ?? "work-item") !== "task"
3095
- || previous.status !== "completed") {
3096
- 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: `
3097
3598
  + `${preflight.record.previousReviewRoundId}.`);
3098
3599
  }
3099
3600
  if (previous.reviewerRoleName !== reviewerRoleName) {
@@ -3307,13 +3808,16 @@ function taskRunCommand(args, store, options) {
3307
3808
  function runContextCommand(args, store, options) {
3308
3809
  const [first, ...rest] = args;
3309
3810
  if (first === "expand") {
3310
- if (rest.length < 2 || rest.length > 4 || (rest.length > 2
3311
- && !(rest.length === 4 && rest[2] === "--mode" && rest[3] === "full"))) {
3312
- throw usageError("Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--mode full].");
3811
+ const usage = "Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
3812
+ const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
3813
+ exactPositionals(parsed.positionals, 2, usage);
3814
+ const mode = parsed.options.get("--mode");
3815
+ if (mode !== undefined && mode !== "full") {
3816
+ throw usageError("Run Context expansion mode must be full.", usage);
3313
3817
  }
3314
- const { taskId, runId } = parseRunContextReference(rest[0]);
3818
+ const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
3315
3819
  authorizeRunContext(store, taskId, runId, options.environment);
3316
- const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, rest[1]));
3820
+ const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
3317
3821
  return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
3318
3822
  }
3319
3823
  if (first === "delta") {
@@ -3706,7 +4210,7 @@ function retryRun(args, store, options) {
3706
4210
  }
3707
4211
  function actualTaskReviewCandidateForMutation(store, task, options) {
3708
4212
  if (options.actualTaskReviewCandidate === undefined) {
3709
- 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}.`);
3710
4214
  }
3711
4215
  let actual;
3712
4216
  try {
@@ -3922,8 +4426,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
3922
4426
  // Any Task-final ReviewRound is durable completion evidence/obligation.
3923
4427
  // Once one exists, later changes to the mutable global review config cannot
3924
4428
  // weaken the requirement or change its reviewer. Before the first such
3925
- // Round, the current global `final` config is still the supported way to
3926
- // establish the policy and queue that initial Round.
4429
+ // Round, the current global `final` config establishes the initial Round
4430
+ // only for integrated delivery.
3927
4431
  const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
3928
4432
  .filter((round) => ((round.scope ?? "work-item") === "task"
3929
4433
  && (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
@@ -3934,7 +4438,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
3934
4438
  }
3935
4439
  else if (establishedRound === undefined) {
3936
4440
  const globalConfig = store.getReviewConfig();
3937
- 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;
3938
4450
  }
3939
4451
  else {
3940
4452
  config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
@@ -3979,7 +4491,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
3979
4491
  return escalated;
3980
4492
  }
3981
4493
  else {
3982
- return latest.status === "completed" ? null : latest;
4494
+ return latest.status === "completed"
4495
+ && classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
4496
+ ? null
4497
+ : latest;
3983
4498
  }
3984
4499
  }
3985
4500
  const anchor = taskFinalContract === undefined
@@ -4051,15 +4566,20 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
4051
4566
  }
4052
4567
  }
4053
4568
  function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
4054
- const item = [...store.listWorkItems(task.id)]
4055
- .filter(({ candidates }) => candidates.some(({ taskFinalReviewContract: candidateContract }) => (sameTaskFinalReviewContract(candidateContract, taskFinalContract))))
4056
- .sort((left, right) => (left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id)))
4569
+ const anchor = store.listWorkItems(task.id)
4570
+ .flatMap((item) => {
4571
+ const candidate = governingWorkItemCandidate(item);
4572
+ return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
4573
+ ? [{ item, candidate }]
4574
+ : [];
4575
+ })
4576
+ .sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
4577
+ || left.item.id.localeCompare(right.item.id)))
4057
4578
  .at(-1);
4058
- const candidate = item?.candidates.filter(({ taskFinalReviewContract: candidateContract }) => (sameTaskFinalReviewContract(candidateContract, taskFinalContract))).at(-1);
4059
- if (item === undefined || candidate === undefined) {
4579
+ if (anchor === undefined) {
4060
4580
  throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
4061
4581
  }
4062
- return { item, candidate };
4582
+ return anchor;
4063
4583
  }
4064
4584
  /**
4065
4585
  * Leader-only retry of an exact failed Task-final review Run. The old failed
@@ -5095,8 +5615,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
5095
5615
  let deltaContext = "";
5096
5616
  if (taskScope && round.deltaRecheck !== undefined) {
5097
5617
  const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
5098
- if (previousRound === null || previousRound.status !== "completed") {
5099
- 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}.`);
5100
5620
  }
5101
5621
  const diffByProject = options.deltaRecheckDiff;
5102
5622
  if (diffByProject === undefined) {
@@ -5379,6 +5899,52 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
5379
5899
  store.saveEvent(taskId, event);
5380
5900
  return event;
5381
5901
  }
5902
+ function publishedTreeAuthorizationPayload(actor, proof) {
5903
+ return {
5904
+ by: actor,
5905
+ projectId: proof.projectId,
5906
+ publicationId: proof.publicationId,
5907
+ ...(proof.reviewRoundId === undefined
5908
+ ? { reviewAnchor: publishedTreeReviewAnchor(proof) }
5909
+ : { reviewRoundId: proof.reviewRoundId }),
5910
+ localCommit: proof.localCommit,
5911
+ remoteCommit: proof.remoteCommit,
5912
+ tree: proof.tree
5913
+ };
5914
+ }
5915
+ function matchingPublishedTreeAuthorization(store, proof) {
5916
+ const events = store.listEvents(proof.taskId);
5917
+ for (let index = events.length - 1; index >= 0; index -= 1) {
5918
+ const event = events[index];
5919
+ if (event.type === "task.completed" || event.type === "task.reopened")
5920
+ return undefined;
5921
+ if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
5922
+ && (event.payload.by === "user" || event.payload.by === "operator")
5923
+ && event.payload.projectId === proof.projectId
5924
+ && event.payload.publicationId === proof.publicationId
5925
+ && (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
5926
+ === publishedTreeReviewAnchor(proof)
5927
+ && event.payload.localCommit === proof.localCommit
5928
+ && event.payload.remoteCommit === proof.remoteCommit
5929
+ && event.payload.tree === proof.tree) {
5930
+ return event;
5931
+ }
5932
+ }
5933
+ return undefined;
5934
+ }
5935
+ function requirePublishedTreeAuthorization(store, proof) {
5936
+ const authorization = matchingPublishedTreeAuthorization(store, proof);
5937
+ if (authorization === undefined) {
5938
+ throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
5939
+ + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
5940
+ ? "the direct Task-main head"
5941
+ : `Task-final Review ${proof.reviewRoundId}`}.`);
5942
+ }
5943
+ return authorization;
5944
+ }
5945
+ function publishedTreeReviewAnchor(proof) {
5946
+ return proof.reviewRoundId ?? "direct";
5947
+ }
5382
5948
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
5383
5949
  function truncateEventNote(note) {
5384
5950
  const normalized = note.trim();
@@ -5639,6 +6205,11 @@ function parseTaskPriority(value) {
5639
6205
  return value;
5640
6206
  throw usageError(`Invalid Task priority: ${value}.`);
5641
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
+ }
5642
6213
  function parseTaskTags(value) {
5643
6214
  const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
5644
6215
  if (tags.length === 0)
@@ -6327,6 +6898,9 @@ function workItemRef(taskId, id) {
6327
6898
  function messageRef(taskId, id) {
6328
6899
  return { type: "message", taskId, id };
6329
6900
  }
6901
+ function eventRef(taskId, id) {
6902
+ return { type: "event", taskId, id };
6903
+ }
6330
6904
  function notifyMailbox(runtime, target, compatibilityTaskId) {
6331
6905
  if (runtime?.notifyMailboxChanged !== undefined) {
6332
6906
  runtime.notifyMailboxChanged(target);