@zq-silk/yui 0.8.1 → 0.8.3

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 (52) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +57 -46
  3. package/dist/cli/commandCatalog.js +57 -17
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +106 -27
  7. package/dist/commands/taskCommands.js +458 -77
  8. package/dist/commands/taskCompletionGate.js +152 -0
  9. package/dist/context/runContextPack.js +19 -4
  10. package/dist/context/sessionBootstrapManifest.js +1 -1
  11. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  12. package/dist/controller/resourceInventory.js +9 -5
  13. package/dist/controller/runtime.js +80 -7
  14. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  15. package/dist/controller/structuredProviderObservation.js +273 -0
  16. package/dist/executor/agentAdapter.js +40 -0
  17. package/dist/executor/agentExecutor.js +31 -7
  18. package/dist/executor/executorRegistry.js +11 -49
  19. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  20. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  21. package/dist/repository/gitWorkspace.js +7 -4
  22. package/dist/repository/taskBaseFreshness.js +4 -2
  23. package/dist/run/agentRun.js +4 -4
  24. package/dist/runtime/agentHost.js +767 -158
  25. package/dist/runtime/builtinAgentDrivers.js +1 -5
  26. package/dist/runtime/codexAppServerRuntime.js +67 -60
  27. package/dist/runtime/exactControlPlane.js +7 -2
  28. package/dist/runtime/index.js +6 -2
  29. package/dist/runtime/launchBroker.js +30 -8
  30. package/dist/runtime/providerAuthorityFence.js +24 -0
  31. package/dist/runtime/providerControl.js +63 -0
  32. package/dist/runtime/providerRecoveryDecision.js +55 -0
  33. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  34. package/dist/runtime/runtimeBinding.js +20 -11
  35. package/dist/runtime/structuredProviderHost.js +476 -0
  36. package/dist/runtime/tmuxAdapters.js +143 -42
  37. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  38. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  39. package/dist/scheduler/wakeReason.js +1 -0
  40. package/dist/storage/migration/productionRegistry.js +111 -0
  41. package/dist/storage/sqliteStore.js +2 -0
  42. package/dist/storage/taskStore.js +3 -1
  43. package/dist/task/completionReadiness.js +43 -0
  44. package/dist/task/nextAction.js +6 -4
  45. package/dist/task/publicationReference.js +1 -0
  46. package/dist/tmux/tmuxManager.js +1 -1
  47. package/dist/workItem/workItem.js +12 -0
  48. package/dist/workspace/workItemChangeSetManager.js +2 -1
  49. package/i18n/README.zh-CN.md +11 -8
  50. package/package.json +1 -1
  51. package/skills/yui-leader/SKILL.md +8 -3
  52. package/skills/yui-runtime/SKILL.md +7 -2
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { createRunAssignment } from "../context/runContextContract.js";
4
4
  import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeRunContextSnapshot } from "../context/runContextPack.js";
@@ -7,7 +7,9 @@ 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 } from "../executor/agentExecutor.js";
10
+ import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
11
+ import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
12
+ import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
11
13
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
12
14
  import { defaultTableWidth, renderTable } from "../output/table.js";
13
15
  import { formatTimestamp } from "../output/timePresentation.js";
@@ -16,7 +18,7 @@ import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js
16
18
  import { cancelInputRequest } from "../input/inputRequest.js";
17
19
  import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
18
20
  import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
19
- import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
21
+ import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
20
22
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
21
23
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
22
24
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
@@ -35,10 +37,11 @@ import { blockingProviderContinuations, projectProviderContinuations } from "../
35
37
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
36
38
  import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
37
39
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
40
+ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
38
41
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
39
42
  import { resolveProject } from "../repository/project.js";
40
43
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
41
- 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";
42
45
  import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
43
46
  import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
44
47
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
@@ -79,8 +82,12 @@ function legacyWorkItemReviewConfig(config) {
79
82
  }
80
83
  function storedTaskFinalReviewContract(store, taskId) {
81
84
  const contracts = store.listWorkItems(taskId)
82
- .flatMap(({ candidates }) => candidates)
83
- .flatMap(({ taskFinalReviewContract }) => (taskFinalReviewContract === undefined ? [] : [taskFinalReviewContract]));
85
+ .flatMap((item) => {
86
+ const candidate = governingWorkItemCandidate(item);
87
+ return candidate?.taskFinalReviewContract === undefined
88
+ ? []
89
+ : [candidate.taskFinalReviewContract];
90
+ });
84
91
  const first = contracts[0];
85
92
  if (first === undefined)
86
93
  return undefined;
@@ -96,7 +103,7 @@ function storedTaskFinalReviewContract(store, taskId) {
96
103
  * acceptance, and completion mutation must present the same verified
97
104
  * capability; shared review-config drift is intentionally irrelevant.
98
105
  */
99
- function taskFinalReviewContractForMutation(store, taskId, options) {
106
+ function taskFinalReviewContractForMutation(store, taskId, options, authorization = {}) {
100
107
  const supplied = options.taskFinalReviewContract;
101
108
  if (supplied !== undefined) {
102
109
  validateTaskFinalReviewContract(supplied);
@@ -112,6 +119,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
112
119
  if (stored === undefined)
113
120
  return supplied;
114
121
  if (supplied === undefined) {
122
+ if (authorization.allowStoredWithoutSupplied === true)
123
+ return stored;
115
124
  throw usageError(`Task final-review contract is missing for ${taskId}.`);
116
125
  }
117
126
  if (!sameTaskFinalReviewContract(stored, supplied)) {
@@ -120,8 +129,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
120
129
  return stored;
121
130
  }
122
131
  export function parseTaskCompletionRequest(args, summaryOverride) {
123
- const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote].";
124
- const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage, new Set(["--refresh-remote"]));
132
+ const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
133
+ const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), usage, new Set(["--refresh-remote"]));
125
134
  exactPositionals(parsed.positionals, 1, usage);
126
135
  const inlineSummary = parsed.options.get("--summary");
127
136
  const summaryFile = parsed.options.get("--summary-file");
@@ -129,7 +138,14 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
129
138
  throw usageError(`Specify exactly one of --summary or --summary-file.`, usage);
130
139
  }
131
140
  const summary = summaryOverride ?? readCommandText(inlineSummary, summaryFile, "--summary", usage);
132
- return { taskId: parsed.positionals[0], summary };
141
+ const acceptedPublishedTreePublicationId = parsed.options.get("--accept-published-tree");
142
+ return {
143
+ taskId: parsed.positionals[0],
144
+ summary,
145
+ ...(acceptedPublishedTreePublicationId === undefined
146
+ ? {}
147
+ : { acceptedPublishedTreePublicationId })
148
+ };
133
149
  }
134
150
  /**
135
151
  * Check every read-only completion blocker before a caller resolves remote
@@ -143,7 +159,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
143
159
  * terminalization precondition before attempting completion. All blockers
144
160
  * are reported in one error instead of one per attempt.
145
161
  */
146
- export function preflightTaskCompletion(taskId, store, options = {}) {
162
+ export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
147
163
  const task = requireTask(store, taskId);
148
164
  const actor = taskActor(options, task.id);
149
165
  if (task.status === "completed") {
@@ -156,7 +172,15 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
156
172
  // Resolve and authenticate the durable Task-local gate before any remote
157
173
  // fetch or Integration write. All checks below mirror the transactional
158
174
  // completion path, which remains the final CAS fence after reconciliation.
159
- const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
175
+ // A human/global Operator cannot present the exact managed Leader contract.
176
+ // For the explicit published-tree path only, let that caller authenticate
177
+ // the stored contract far enough to persist an exact authorization fact.
178
+ // The same command must return before any contract-governed completion
179
+ // mutation; the exact Leader later consumes the authorization with the real
180
+ // contract capability.
181
+ const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
182
+ && actor !== "leader";
183
+ const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
160
184
  const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
161
185
  && (round.status === "pending" || round.status === "running")));
162
186
  // Issue 06: one shared readiness projection enumerates every blocker.
@@ -240,7 +264,6 @@ export function runTaskCommand(args, store, options = {}) {
240
264
  case "milestone": return taskMilestoneCommand(rest, store, options);
241
265
  case "event": return taskEventCommand(rest, store);
242
266
  case "continuation": return taskContinuationCommand(rest, store);
243
- case "enter": return enterTaskRoleAlias(rest, store, options);
244
267
  default:
245
268
  throw usageError(command === undefined
246
269
  ? "Task command is required."
@@ -429,8 +452,8 @@ function updateTaskCommand(args, store, options) {
429
452
  /** Compatibility helper for call sites that cannot yet handle foreground enter. */
430
453
  export function runTaskOutputCommand(args, store, options = {}) {
431
454
  const execution = runTaskCommand(args, store, options);
432
- if (execution.kind === "enter") {
433
- throw runtimeError("Task role enter requires foreground tmux handoff by the CLI.");
455
+ if (execution.kind !== "output") {
456
+ throw runtimeError("Task Role foreground runtime control requires the CLI.");
434
457
  }
435
458
  return execution.output;
436
459
  }
@@ -647,17 +670,42 @@ function completeTaskCommand(args, store, options) {
647
670
  const summary = request.summary;
648
671
  const now = clock(options);
649
672
  const result = store.transaction((tx) => {
650
- const preflight = preflightTaskCompletion(request.taskId, tx, options);
673
+ const preflight = preflightTaskCompletion(request.taskId, tx, options, request);
651
674
  const { task, actor } = preflight;
652
675
  if (preflight.completed) {
653
676
  return {
654
677
  task,
655
678
  changed: false,
656
679
  runtimeCleanupTargets: [],
657
- finalReview: undefined
680
+ finalReview: undefined,
681
+ publishedTreeAuthorization: undefined
658
682
  };
659
683
  }
660
684
  const taskFinalContract = preflight.taskFinalReviewContract;
685
+ const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
686
+ ? undefined
687
+ : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskReviewCandidateForMutation(tx, task, options));
688
+ const requiresContractHandoff = publishedTreeProof !== undefined
689
+ && taskFinalContract !== undefined;
690
+ if (requiresContractHandoff && actor !== "leader") {
691
+ const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
692
+ const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
693
+ enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
694
+ return {
695
+ task,
696
+ changed: false,
697
+ runtimeCleanupTargets: [],
698
+ finalReview: undefined,
699
+ publishedTreeAuthorization: {
700
+ event,
701
+ proof: publishedTreeProof,
702
+ created: existing === undefined
703
+ }
704
+ };
705
+ }
706
+ if (publishedTreeProof !== undefined && actor === "leader") {
707
+ requirePublishedTreeAuthorization(tx, publishedTreeProof);
708
+ }
661
709
  const roles = tx.listRoles(task.id);
662
710
  const activeRuns = roles
663
711
  .map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
@@ -706,7 +754,8 @@ function completeTaskCommand(args, store, options) {
706
754
  runtimeCleanupTargets: [],
707
755
  finalReview,
708
756
  resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
709
- terminalizedLeaderRun
757
+ terminalizedLeaderRun,
758
+ publishedTreeAuthorization: undefined
710
759
  };
711
760
  }
712
761
  // Issue 06: re-validate the full completion readiness inside the
@@ -729,6 +778,17 @@ function completeTaskCommand(args, store, options) {
729
778
  tx.clearOperatorNotification(task.id);
730
779
  tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
731
780
  enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
781
+ if (publishedTreeProof !== undefined) {
782
+ recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
783
+ by: actor,
784
+ projectId: publishedTreeProof.projectId,
785
+ publicationId: publishedTreeProof.publicationId,
786
+ reviewRoundId: publishedTreeProof.reviewRoundId,
787
+ localCommit: publishedTreeProof.localCommit,
788
+ remoteCommit: publishedTreeProof.remoteCommit,
789
+ tree: publishedTreeProof.tree
790
+ }, now);
791
+ }
732
792
  recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
733
793
  // A terminal Task must never leave a Task-lane signal that can wake it.
734
794
  // The durable records remain intact; only the derived mailbox work is
@@ -747,9 +807,21 @@ function completeTaskCommand(args, store, options) {
747
807
  runtimeCleanupTargets,
748
808
  finalReview: undefined,
749
809
  resumedPendingFinalReview: false,
750
- terminalizedLeaderRun
810
+ terminalizedLeaderRun,
811
+ publishedTreeAuthorization: undefined
751
812
  };
752
813
  });
814
+ if (result.publishedTreeAuthorization !== undefined) {
815
+ notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
816
+ const authorization = result.publishedTreeAuthorization;
817
+ return output(authorization.created
818
+ ? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
819
+ : `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
820
+ task: result.task,
821
+ authorizationEvent: authorization.event,
822
+ publishedTreeProof: authorization.proof
823
+ });
824
+ }
753
825
  if (result.changed) {
754
826
  for (const target of result.runtimeCleanupTargets) {
755
827
  // Cleanup owns an independent lifecycle lane. Never fall back to the
@@ -1192,8 +1264,12 @@ function taskRoleCommand(args, store, options) {
1192
1264
  return output(unbindTaskRole(rest, store, options));
1193
1265
  if (command === "reset")
1194
1266
  return resetTaskRole(rest, store, options);
1195
- if (command === "enter")
1196
- return enterTaskRole(rest, store, options);
1267
+ if (command === "view")
1268
+ return viewTaskRole(rest, store);
1269
+ if (command === "takeover")
1270
+ return transferTaskRoleAuthority(rest, store, options, "takeover");
1271
+ if (command === "release")
1272
+ return transferTaskRoleAuthority(rest, store, options, "release");
1197
1273
  throw usageError(command === undefined
1198
1274
  ? "Task role command is required."
1199
1275
  : `Unknown command: task role ${command}`);
@@ -1506,60 +1582,111 @@ function unbindTaskRole(args, store, options) {
1506
1582
  });
1507
1583
  return `Unbound Agent ${args[2]} from ${result.taskId}/${result.name}\n`;
1508
1584
  }
1509
- function enterTaskRole(args, store, _options) {
1510
- const usage = "Task role enter usage: yui task role enter <task> <role> "
1511
- + "[--read-only | --read-write].";
1512
- const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1513
- exactPositionals(parsed.positionals, 2, usage);
1514
- if (parsed.options.has("--read-only") && parsed.options.has("--read-write")) {
1515
- throw usageError("--read-only and --read-write are mutually exclusive.", usage);
1516
- }
1517
- const task = requireTask(store, parsed.positionals[0]);
1585
+ function viewTaskRole(args, store) {
1586
+ const usage = "Task role view usage: yui task role view <task> <role>.";
1587
+ exactPositionals(args, 2, usage);
1588
+ const task = requireTask(store, args[0]);
1518
1589
  if (task.status !== "active") {
1519
- throw usageError(inactiveTaskMessage(task, "entering a role session"));
1590
+ throw usageError(inactiveTaskMessage(task, "viewing a role session"));
1520
1591
  }
1521
- const role = requireRole(store, task.id, parsed.positionals[1]);
1522
- const access = parsed.options.has("--read-write") ? "read-write" : "read-only";
1523
- if (access === "read-write") {
1524
- assertTaskRoleWritableAttachAvailable(store, task.id, role.name);
1592
+ const role = requireRole(store, task.id, args[1]);
1593
+ const session = store.getRoleSession(task.id, role.name);
1594
+ if (session === null || session.status === "stopped" || session.status === "broken") {
1595
+ throw usageError(`Task Role has no live Provider view: ${task.id}/${role.name}.`);
1525
1596
  }
1526
1597
  return {
1527
- kind: "enter",
1598
+ kind: "view",
1528
1599
  taskId: task.id,
1529
1600
  roleName: role.name,
1530
- access,
1531
- output: `Attaching to ${role.name} for ${task.id} (${access})\n`
1601
+ access: "read-only",
1602
+ output: `Viewing ${role.name} for ${task.id} (read-only)\n`
1532
1603
  };
1533
1604
  }
1534
- /** Re-run after the tmux writer lease exists to close attach/launch races. */
1535
- export function assertTaskRoleWritableAttachAvailable(store, taskId, roleName, options = {}) {
1536
- const role = requireRole(store, taskId, roleName);
1537
- if (store.getActiveAgentRun(taskId, roleName) !== null) {
1538
- throw usageError(`Role has an active managed Run; writable attach is unavailable: ${taskId}/${roleName}.`);
1539
- }
1540
- const session = store.getRoleSession(taskId, roleName);
1541
- if (activeRoleAgentBinding(role).adapterId === "claude"
1542
- && ((session !== null
1543
- && session.status !== "stopped"
1544
- && session.status !== "broken")
1545
- || options.isManagedProcessRunning?.() === true)) {
1546
- throw usageError(`A managed Claude process is still running; writable attach is unavailable: ${taskId}/${roleName}.`);
1547
- }
1548
- }
1549
- function enterTaskRoleAlias(args, store, options) {
1550
- const usage = "Task enter usage: yui task enter <task> [role] "
1551
- + "[--read-only | --read-write].";
1552
- const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1553
- if (parsed.positionals.length < 1 || parsed.positionals.length > 2
1554
- || parsed.positionals.some((value) => value.trim().length === 0)) {
1555
- throw usageError(usage);
1605
+ function transferTaskRoleAuthority(args, store, options, action) {
1606
+ const usage = `Task role ${action} usage: yui task role ${action} <task> <role>.`;
1607
+ exactPositionals(args, 2, usage);
1608
+ const now = clock(options);
1609
+ try {
1610
+ return store.transaction((tx) => {
1611
+ const task = requireTask(tx, args[0]);
1612
+ if (task.status !== "active") {
1613
+ throw usageError(inactiveTaskMessage(task, `${action} Provider authority`));
1614
+ }
1615
+ const role = requireRole(tx, task.id, args[1]);
1616
+ const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1617
+ const session = sessions?.sessions[role.activeAgentId];
1618
+ const binding = sessions?.providerBinding;
1619
+ if (sessions === null || sessions === undefined || session === undefined
1620
+ || binding === null || binding === undefined
1621
+ || session.launchId === undefined
1622
+ || session.status === "stopped" || session.status === "broken") {
1623
+ throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
1624
+ }
1625
+ const activation = currentProviderActivation(binding);
1626
+ if (activation === null) {
1627
+ throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
1628
+ }
1629
+ if (action === "takeover") {
1630
+ const activeRun = tx.getActiveAgentRun(task.id, role.name);
1631
+ if (sessions.inFlight === null || activeRun?.id !== sessions.inFlight.runId) {
1632
+ throw new Error(`Task Role has no active managed Run for takeover: ${task.id}/${role.name}.`);
1633
+ }
1634
+ }
1635
+ if (action === "takeover"
1636
+ && binding.authority.owner !== "controller"
1637
+ && binding.authority.owner !== "human") {
1638
+ throw new Error(`Provider authority is not Controller-owned: ${task.id}/${role.name}.`);
1639
+ }
1640
+ if (action === "release"
1641
+ && binding.authority.owner !== "human"
1642
+ && binding.authority.owner !== "controller") {
1643
+ throw new Error(`Provider authority is not human-owned: ${task.id}/${role.name}.`);
1644
+ }
1645
+ const desiredOwner = action === "takeover" ? "human" : "controller";
1646
+ const unchanged = binding.authority.owner === desiredOwner;
1647
+ const updatedBinding = unchanged
1648
+ ? binding
1649
+ : transferProviderAuthority(binding, {
1650
+ expectedEpoch: binding.authority.epoch,
1651
+ expectedOwner: binding.authority.owner,
1652
+ owner: desiredOwner,
1653
+ holderId: action === "takeover" ? `human:${randomUUID()}` : activation.activationId,
1654
+ changedAt: now.toISOString()
1655
+ });
1656
+ const authority = updatedBinding.authority;
1657
+ if (authority.owner !== "controller" && authority.owner !== "human") {
1658
+ throw new Error("Provider authority transfer did not produce a writer.");
1659
+ }
1660
+ if (!unchanged) {
1661
+ tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updatedBinding, now));
1662
+ recordTaskEvent(tx, task.id, "runtime.provider-authority-transferred", {
1663
+ role: role.name,
1664
+ owner: authority.owner,
1665
+ holderId: authority.holderId,
1666
+ epoch: String(authority.epoch)
1667
+ }, now);
1668
+ }
1669
+ return {
1670
+ kind: "authority",
1671
+ action,
1672
+ taskId: task.id,
1673
+ roleName: role.name,
1674
+ launchId: session.launchId,
1675
+ nativeSessionId: session.nativeSessionId,
1676
+ authority: {
1677
+ epoch: authority.epoch,
1678
+ owner: authority.owner,
1679
+ holderId: authority.holderId
1680
+ },
1681
+ output: action === "takeover"
1682
+ ? `Human authority ${unchanged ? "replayed" : "acquired"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
1683
+ : `Controller authority ${unchanged ? "replayed" : "restored"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
1684
+ };
1685
+ });
1686
+ }
1687
+ catch (error) {
1688
+ throw usageError(messageOf(error), usage);
1556
1689
  }
1557
- return enterTaskRole([
1558
- parsed.positionals[0],
1559
- parsed.positionals[1] ?? LEADER_ROLE,
1560
- ...(parsed.options.has("--read-only") ? ["--read-only"] : []),
1561
- ...(parsed.options.has("--read-write") ? ["--read-write"] : [])
1562
- ], store, options);
1563
1690
  }
1564
1691
  function taskWorkCommand(args, store, options) {
1565
1692
  const [command, ...rest] = args;
@@ -2553,6 +2680,8 @@ function taskReviewCommand(args, store, options) {
2553
2680
  const [command, ...rest] = args;
2554
2681
  if (command === "request")
2555
2682
  return requestTaskReviewRound(rest, store, options);
2683
+ if (command === "force-fresh")
2684
+ return forceFreshTaskReviewRound(rest, store, options);
2556
2685
  if (command === "retry")
2557
2686
  return retryFailedTaskReviewRound(rest, store, options);
2558
2687
  if (command === "group")
@@ -3023,6 +3152,209 @@ function requestTaskReviewRound(args, store, options) {
3023
3152
  : `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
3024
3153
  : `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
3025
3154
  }
3155
+ const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
3156
+ /**
3157
+ * 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.
3161
+ */
3162
+ function forceFreshTaskReviewRound(args, store, options) {
3163
+ const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
3164
+ exactPositionals(args, 1, usage);
3165
+ const reference = taskRecordReference(args[0], "reviewRound", "ReviewRound reference", options);
3166
+ const now = clock(options);
3167
+ const result = store.transaction((tx) => {
3168
+ const source = tx.getReviewRound(reference.taskId, reference.localId);
3169
+ if (source === null) {
3170
+ throw dataError(`ReviewRound not found: ${reference.taskId}/${reference.localId}.`);
3171
+ }
3172
+ const task = requireTask(tx, reference.taskId);
3173
+ if (task.status !== "active")
3174
+ throw usageError(`Task is not active: ${task.id}.`);
3175
+ if (taskActor(options, task.id) !== "leader") {
3176
+ throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
3177
+ }
3178
+ if ((source.scope ?? "work-item") !== "task") {
3179
+ throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
3180
+ }
3181
+ const replacementEvents = tx.listEvents(task.id).filter((event) => (event.type === TASK_FINAL_FORCE_FRESH_EVENT
3182
+ && event.payload.sourceReviewRoundId === source.id));
3183
+ if (replacementEvents.length > 1) {
3184
+ throw dataError(`ReviewRound ${source.id} has duplicate force-fresh audit events.`);
3185
+ }
3186
+ const replacementEvent = replacementEvents[0];
3187
+ if (replacementEvent !== undefined) {
3188
+ const replacementId = replacementEvent.payload.reviewRoundId;
3189
+ const replacement = replacementId === undefined
3190
+ ? null
3191
+ : tx.getReviewRound(task.id, replacementId);
3192
+ if (replacement === null
3193
+ || replacement.id === source.id
3194
+ || (replacement.scope ?? "work-item") !== "task"
3195
+ || replacement.workItemId !== source.workItemId
3196
+ || replacement.candidateId !== source.candidateId
3197
+ || replacement.reviewerRoleName !== source.reviewerRoleName
3198
+ || replacement.deltaRecheck !== undefined
3199
+ || !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
3200
+ || !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
3201
+ || replacementEvent.payload.workItemId !== replacement.workItemId
3202
+ || replacementEvent.payload.candidateId !== replacement.candidateId
3203
+ || replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
3204
+ || replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
3205
+ throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
3206
+ }
3207
+ return { round: replacement, source, created: false };
3208
+ }
3209
+ const semanticBlocker = forceFreshSemanticBlocker(tx, source);
3210
+ if (semanticBlocker !== null) {
3211
+ throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${semanticBlocker}`);
3212
+ }
3213
+ if (source.taskCandidate === undefined) {
3214
+ throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
3215
+ }
3216
+ const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
3217
+ if (!sameTaskFinalReviewContract(source.taskFinalReviewContract, taskFinalContract)) {
3218
+ throw usageError(`Task final-review contract does not match ReviewRound ${source.id}.`);
3219
+ }
3220
+ if (source.executionGroup !== undefined
3221
+ && (source.executionGroup.strategy.mode !== "fixed"
3222
+ || source.executionGroup.strategy.count !== 1
3223
+ || source.executionGroup.lanes.length !== 1
3224
+ || source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
3225
+ throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
3226
+ }
3227
+ const item = tx.getWorkItem(task.id, source.workItemId);
3228
+ const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
3229
+ if (item === null || item === undefined || candidate === undefined) {
3230
+ throw dataError(`Final Review anchor Candidate is no longer available: `
3231
+ + `${source.workItemId}/${source.candidateId}.`);
3232
+ }
3233
+ const provenance = taskReviewProvenance(tx, task, options);
3234
+ if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
3235
+ throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
3236
+ }
3237
+ const producerCollision = taskReviewProducerCollision(provenance, source.reviewerRoleName);
3238
+ if (producerCollision !== null)
3239
+ throw usageError(producerCollision);
3240
+ const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
3241
+ .filter((entry) => (entry.scope ?? "work-item") === "task"));
3242
+ const sourceIndex = taskRounds.findIndex(({ id }) => id === source.id);
3243
+ if (sourceIndex < 0) {
3244
+ throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
3245
+ }
3246
+ const laterRound = taskRounds.slice(sourceIndex + 1).at(-1);
3247
+ if (laterRound !== undefined) {
3248
+ throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
3249
+ + `${laterRound.id}/${laterRound.status}.`);
3250
+ }
3251
+ assertNoConflictingTaskReviewRound(taskRounds, source.id);
3252
+ assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
3253
+ let reviewer = tx.getRole(task.id, source.reviewerRoleName);
3254
+ if (reviewer === null) {
3255
+ if (tx.getGlobalRole(source.reviewerRoleName) === null) {
3256
+ throw usageError(`Global Role not found: ${source.reviewerRoleName}.`);
3257
+ }
3258
+ reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
3259
+ tx.saveRole(task.id, reviewer);
3260
+ }
3261
+ let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.workItemId, source.candidateId, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
3262
+ const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3263
+ purpose: "review",
3264
+ target: executionTargetForReviewRound(task, created, item, candidate),
3265
+ strategy: { mode: "fixed", count: 1 },
3266
+ lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
3267
+ }, now);
3268
+ created = { ...created, executionGroup: group };
3269
+ tx.saveReviewRound(task.id, created);
3270
+ recordTaskEvent(tx, task.id, TASK_FINAL_FORCE_FRESH_EVENT, {
3271
+ sourceReviewRoundId: source.id,
3272
+ ...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
3273
+ reviewRoundId: created.id,
3274
+ workItemId: created.workItemId,
3275
+ candidateId: created.candidateId,
3276
+ reviewerRoleName: created.reviewerRoleName,
3277
+ taskCandidate: JSON.stringify(created.taskCandidate),
3278
+ reason: "source-round-failed-without-semantic-review",
3279
+ leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3280
+ }, now);
3281
+ return { round: created, source, created: true };
3282
+ });
3283
+ return output(result.created
3284
+ ? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
3285
+ : `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
3286
+ }
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));
3357
+ }
3026
3358
  /**
3027
3359
  * Issue 07: re-validates the CLI-computed delta preflight inside the store
3028
3360
  * transaction. The previous Round must be a completed acceptance (a full
@@ -3252,13 +3584,16 @@ function taskRunCommand(args, store, options) {
3252
3584
  function runContextCommand(args, store, options) {
3253
3585
  const [first, ...rest] = args;
3254
3586
  if (first === "expand") {
3255
- if (rest.length < 2 || rest.length > 4 || (rest.length > 2
3256
- && !(rest.length === 4 && rest[2] === "--mode" && rest[3] === "full"))) {
3257
- throw usageError("Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--mode full].");
3587
+ const usage = "Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
3588
+ const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
3589
+ exactPositionals(parsed.positionals, 2, usage);
3590
+ const mode = parsed.options.get("--mode");
3591
+ if (mode !== undefined && mode !== "full") {
3592
+ throw usageError("Run Context expansion mode must be full.", usage);
3258
3593
  }
3259
- const { taskId, runId } = parseRunContextReference(rest[0]);
3594
+ const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
3260
3595
  authorizeRunContext(store, taskId, runId, options.environment);
3261
- const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, rest[1]));
3596
+ const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
3262
3597
  return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
3263
3598
  }
3264
3599
  if (first === "delta") {
@@ -3996,15 +4331,20 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
3996
4331
  }
3997
4332
  }
3998
4333
  function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
3999
- const item = [...store.listWorkItems(task.id)]
4000
- .filter(({ candidates }) => candidates.some(({ taskFinalReviewContract: candidateContract }) => (sameTaskFinalReviewContract(candidateContract, taskFinalContract))))
4001
- .sort((left, right) => (left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id)))
4334
+ const anchor = store.listWorkItems(task.id)
4335
+ .flatMap((item) => {
4336
+ const candidate = governingWorkItemCandidate(item);
4337
+ return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
4338
+ ? [{ item, candidate }]
4339
+ : [];
4340
+ })
4341
+ .sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
4342
+ || left.item.id.localeCompare(right.item.id)))
4002
4343
  .at(-1);
4003
- const candidate = item?.candidates.filter(({ taskFinalReviewContract: candidateContract }) => (sameTaskFinalReviewContract(candidateContract, taskFinalContract))).at(-1);
4004
- if (item === undefined || candidate === undefined) {
4344
+ if (anchor === undefined) {
4005
4345
  throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
4006
4346
  }
4007
- return { item, candidate };
4347
+ return anchor;
4008
4348
  }
4009
4349
  /**
4010
4350
  * Leader-only retry of an exact failed Task-final review Run. The old failed
@@ -5324,6 +5664,44 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
5324
5664
  store.saveEvent(taskId, event);
5325
5665
  return event;
5326
5666
  }
5667
+ function publishedTreeAuthorizationPayload(actor, proof) {
5668
+ return {
5669
+ by: actor,
5670
+ projectId: proof.projectId,
5671
+ publicationId: proof.publicationId,
5672
+ reviewRoundId: proof.reviewRoundId,
5673
+ localCommit: proof.localCommit,
5674
+ remoteCommit: proof.remoteCommit,
5675
+ tree: proof.tree
5676
+ };
5677
+ }
5678
+ function matchingPublishedTreeAuthorization(store, proof) {
5679
+ const events = store.listEvents(proof.taskId);
5680
+ for (let index = events.length - 1; index >= 0; index -= 1) {
5681
+ const event = events[index];
5682
+ if (event.type === "task.completed" || event.type === "task.reopened")
5683
+ return undefined;
5684
+ if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
5685
+ && (event.payload.by === "user" || event.payload.by === "operator")
5686
+ && event.payload.projectId === proof.projectId
5687
+ && event.payload.publicationId === proof.publicationId
5688
+ && event.payload.reviewRoundId === proof.reviewRoundId
5689
+ && event.payload.localCommit === proof.localCommit
5690
+ && event.payload.remoteCommit === proof.remoteCommit
5691
+ && event.payload.tree === proof.tree) {
5692
+ return event;
5693
+ }
5694
+ }
5695
+ return undefined;
5696
+ }
5697
+ function requirePublishedTreeAuthorization(store, proof) {
5698
+ const authorization = matchingPublishedTreeAuthorization(store, proof);
5699
+ if (authorization === undefined) {
5700
+ throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
5701
+ + `${proof.taskId}/${proof.publicationId} at Task-final Review ${proof.reviewRoundId}.`);
5702
+ }
5703
+ return authorization;
5704
+ }
5327
5705
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
5328
5706
  function truncateEventNote(note) {
5329
5707
  const normalized = note.trim();
@@ -6272,6 +6650,9 @@ function workItemRef(taskId, id) {
6272
6650
  function messageRef(taskId, id) {
6273
6651
  return { type: "message", taskId, id };
6274
6652
  }
6653
+ function eventRef(taskId, id) {
6654
+ return { type: "event", taskId, id };
6655
+ }
6275
6656
  function notifyMailbox(runtime, target, compatibilityTaskId) {
6276
6657
  if (runtime?.notifyMailboxChanged !== undefined) {
6277
6658
  runtime.notifyMailboxChanged(target);