@zq-silk/yui 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli/commandCatalog.js +2 -2
  2. package/dist/cli/updatePorts.js +18 -2
  3. package/dist/cli.js +3 -6
  4. package/dist/commands/durableJobCommands.js +6 -18
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskActor.js +35 -61
  7. package/dist/commands/taskCommands.js +260 -200
  8. package/dist/commands/taskIntegrationCommands.js +33 -24
  9. package/dist/commands/taskIntegrationQueueCommands.js +16 -18
  10. package/dist/commands/taskPublicationCommands.js +0 -4
  11. package/dist/commands/taskWorkspaceCommands.js +1 -1
  12. package/dist/context/runContextPack.js +15 -0
  13. package/dist/controller/fileSchedulerStoreAdapter.js +17 -1
  14. package/dist/controller/jobClient.js +5 -8
  15. package/dist/controller/jobControl.js +59 -105
  16. package/dist/executor/workspacePreflightClassification.js +6 -5
  17. package/dist/integration/gitIntegrationService.js +3 -3
  18. package/dist/integration/integrationAttempt.js +13 -9
  19. package/dist/integration/integrationQueueService.js +7 -7
  20. package/dist/lifecycle/exactRunTerminalization.js +146 -6
  21. package/dist/milestone/milestone.js +9 -3
  22. package/dist/review/deltaRecheck.js +1 -1
  23. package/dist/review/reviewFinding.js +3 -3
  24. package/dist/review/reviewRound.js +11 -8
  25. package/dist/run/rejectedYieldAttempt.js +221 -0
  26. package/dist/runtime/tmuxAdapters.js +12 -3
  27. package/dist/scheduler/activeRoleRunDelivery.js +4 -0
  28. package/dist/storage/migration/productionRegistry.js +9 -0
  29. package/dist/storage/taskStore.js +9 -8
  30. package/package.json +1 -1
  31. package/skills/yui-leader/SKILL.md +4 -0
  32. package/skills/yui-operator/SKILL.md +19 -13
@@ -7,6 +7,7 @@ import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageErr
7
7
  import { createTaskEvent } from "../event/taskEvent.js";
8
8
  import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
9
9
  import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
10
+ import { routeRoleEvent } from "../scheduler/operatorEvent.js";
10
11
  import { readCommandText } from "./textInput.js";
11
12
  import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
12
13
  import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
@@ -18,17 +19,18 @@ import { formatTimestamp } from "../output/timePresentation.js";
18
19
  import { renderRoleDetails } from "../output/rolePresentation.js";
19
20
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
20
21
  import { cancelInputRequest } from "../input/inputRequest.js";
21
- import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
22
+ import { recoverExactAgentRun, retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
22
23
  import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
23
24
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
24
25
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
25
26
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
27
+ import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
26
28
  import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
27
29
  import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
28
30
  import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
29
31
  import { projectReviewerAvailability } from "../review/reviewerAvailability.js";
30
32
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
31
- import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
33
+ import { TASK_CONTROL_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
32
34
  import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
33
35
  import { createDecision, supersedeDecision } from "../decision/decision.js";
34
36
  import { createMilestone } from "../milestone/milestone.js";
@@ -40,7 +42,6 @@ import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlo
40
42
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
41
43
  import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
42
44
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
43
- import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
44
45
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
45
46
  import { assertProjectActive, resolveProject } from "../repository/project.js";
46
47
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
@@ -63,7 +64,7 @@ import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleAc
63
64
  import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
64
65
  import { runGrantCommand } from "./grantCommands.js";
65
66
  import { runWorkflowCommand } from "./workflowCommands.js";
66
- import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
67
+ import { taskLocalActor as resolveTaskLocalActor, taskLeaderActionRunId } from "./taskActor.js";
67
68
  import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
68
69
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
69
70
  import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
@@ -164,7 +165,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
164
165
  */
165
166
  export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
166
167
  const task = requireTask(store, taskId);
167
- const actor = taskActor(options, task.id);
168
+ const actor = taskActor(store, options, task.id);
168
169
  if (task.status === "completed") {
169
170
  return { task, actor, completed: true, activeTaskReview: false };
170
171
  }
@@ -201,9 +202,6 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
201
202
  .filter((entry) => entry.run !== null);
202
203
  const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
203
204
  if (leaderEntry !== undefined) {
204
- if (actor !== "leader") {
205
- throw usageError(`Task ${task.id} has an active Leader run.`);
206
- }
207
205
  if (leaderEntry.run.workItemId !== undefined) {
208
206
  throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
209
207
  }
@@ -379,9 +377,7 @@ function taskProjectCommand(args, store, options) {
379
377
  const updated = store.transaction((tx) => {
380
378
  const task = requireTask(tx, parsed.positionals[0]);
381
379
  assertTaskOpen(task);
382
- if (task.status === "active" && taskActor(options, task.id) !== "leader") {
383
- throw usageError("Only the Task Leader may add a Project to an active Task.");
384
- }
380
+ taskActor(tx, options, task.id);
385
381
  const project = resolveProject(tx.listProjects(), parsed.positionals[1]);
386
382
  if (project === null)
387
383
  throw usageError(`Project not found: ${parsed.positionals[1]}.`);
@@ -444,6 +440,7 @@ function updateTaskCommand(args, store, options) {
444
440
  const current = requireTask(tx, parsed.positionals[0]);
445
441
  if (current.status === "archived")
446
442
  throw usageError(`Task is archived: ${current.id}.`);
443
+ taskActor(tx, options, current.id);
447
444
  const updated = updateTaskMetadata(current, {
448
445
  ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
449
446
  ...(parsed.options.has("--type")
@@ -720,8 +717,7 @@ function completeTaskCommand(args, store, options) {
720
717
  changed: false,
721
718
  runtimeCleanupTargets: [],
722
719
  completionAdvisories: [],
723
- finalReview: undefined,
724
- publishedTreeAuthorization: undefined
720
+ finalReview: undefined
725
721
  };
726
722
  }
727
723
  const taskFinalContract = preflight.taskFinalReviewContract;
@@ -731,28 +727,6 @@ function completeTaskCommand(args, store, options) {
731
727
  const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
732
728
  ? undefined
733
729
  : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
734
- const requiresContractHandoff = publishedTreeProof !== undefined
735
- && taskFinalContract !== undefined;
736
- if (requiresContractHandoff && actor !== "leader") {
737
- const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
738
- const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
739
- enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
740
- return {
741
- task,
742
- changed: false,
743
- runtimeCleanupTargets: [],
744
- completionAdvisories: [],
745
- finalReview: undefined,
746
- publishedTreeAuthorization: {
747
- event,
748
- proof: publishedTreeProof,
749
- created: existing === undefined
750
- }
751
- };
752
- }
753
- if (publishedTreeProof !== undefined && actor === "leader") {
754
- requirePublishedTreeAuthorization(tx, publishedTreeProof);
755
- }
756
730
  const roles = tx.listRoles(task.id);
757
731
  const activeRuns = roles
758
732
  .map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
@@ -763,9 +737,6 @@ function completeTaskCommand(args, store, options) {
763
737
  // the post-Review readiness fence below.
764
738
  const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
765
739
  if (leaderEntry !== undefined) {
766
- if (actor !== "leader") {
767
- throw usageError(`Task ${task.id} has an active Leader run.`);
768
- }
769
740
  if (leaderEntry.run.workItemId !== undefined) {
770
741
  throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
771
742
  }
@@ -796,7 +767,6 @@ function completeTaskCommand(args, store, options) {
796
767
  runtimeCleanupTargets: [],
797
768
  completionAdvisories: [],
798
769
  finalReview,
799
- publishedTreeAuthorization: undefined
800
770
  };
801
771
  }
802
772
  // Issue 06: re-validate the full completion readiness inside the
@@ -860,21 +830,9 @@ function completeTaskCommand(args, store, options) {
860
830
  changed: true,
861
831
  runtimeCleanupTargets,
862
832
  completionAdvisories: readiness.advisories,
863
- finalReview: undefined,
864
- publishedTreeAuthorization: undefined
833
+ finalReview: undefined
865
834
  };
866
835
  });
867
- if (result.publishedTreeAuthorization !== undefined) {
868
- notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
869
- const authorization = result.publishedTreeAuthorization;
870
- return output(authorization.created
871
- ? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
872
- : `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
873
- task: result.task,
874
- authorizationEvent: authorization.event,
875
- publishedTreeProof: authorization.proof
876
- });
877
- }
878
836
  if (result.changed) {
879
837
  for (const target of result.runtimeCleanupTargets) {
880
838
  // Cleanup owns an independent lifecycle lane. Never fall back to the
@@ -952,7 +910,7 @@ function archiveTaskCommand(args, store, options) {
952
910
  const now = clock(options);
953
911
  const result = store.transaction((tx) => {
954
912
  const task = requireTask(tx, request.taskId);
955
- const actor = taskActor(options, task.id);
913
+ const actor = taskActor(tx, options, task.id);
956
914
  if (task.status === "archived")
957
915
  return { task, changed: false };
958
916
  if (task.status !== "completed"
@@ -1016,7 +974,7 @@ function retireTaskCommand(args, store, options) {
1016
974
  const now = clock(options);
1017
975
  const result = store.transaction((tx) => {
1018
976
  const task = requireTask(tx, taskId);
1019
- const actor = taskActor(options, task.id);
977
+ const actor = taskActor(tx, options, task.id);
1020
978
  if (task.status === "retired") {
1021
979
  const same = task.retirementSummary === summary
1022
980
  && task.replacementTaskId === replacementTaskId;
@@ -1144,10 +1102,7 @@ export function parseTaskArchiveArguments(args) {
1144
1102
  export function validateTaskArchiveRequest(args, store, options = {}) {
1145
1103
  const request = parseTaskArchiveArguments(args);
1146
1104
  const task = requireTask(store, request.taskId);
1147
- const actor = taskActor(options, task.id);
1148
- if (actor === "leader") {
1149
- throw usageError("Only the global Operator may archive a Task from a managed Session.");
1150
- }
1105
+ taskActor(store, options, task.id);
1151
1106
  if (task.status !== "archived"
1152
1107
  && task.status !== "completed"
1153
1108
  && task.status !== "retired") {
@@ -1201,7 +1156,7 @@ function taskMessageCommand(args, store, options) {
1201
1156
  const result = store.transaction((tx) => {
1202
1157
  const task = requireTask(tx, parsed.positionals[0]);
1203
1158
  assertTaskOpen(task);
1204
- const actor = taskActor(options, task.id);
1159
+ const actor = taskActor(tx, options, task.id);
1205
1160
  const message = actor === "leader"
1206
1161
  ? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: LEADER_ROLE }, now)
1207
1162
  : actor === "operator"
@@ -1296,10 +1251,7 @@ function retireMessage(args, store, options) {
1296
1251
  const result = store.transaction((tx) => {
1297
1252
  const task = requireTask(tx, reference.taskId);
1298
1253
  assertTaskOpen(task);
1299
- const actor = taskActor(options, task.id);
1300
- if (actor === "leader") {
1301
- throw usageError("Only the user or global Operator may retire a Task Message.");
1302
- }
1254
+ const actor = taskActor(tx, options, task.id);
1303
1255
  const message = tx.listMessages(task.id).find(({ id }) => id === reference.localId);
1304
1256
  if (message === undefined) {
1305
1257
  throw dataError(`Task Message not found: ${task.id}/${reference.localId}.`);
@@ -1405,7 +1357,7 @@ function switchTaskRoleSession(args, store, options) {
1405
1357
  throw usageError(inactiveTaskMessage(task, "switching a Provider Conversation"), usage);
1406
1358
  }
1407
1359
  const role = requireRole(tx, task.id, parsed.positionals[1]);
1408
- const requestedBy = taskActor(options, task.id);
1360
+ const requestedBy = taskActor(tx, options, task.id);
1409
1361
  const leaderRunId = requestedBy === "leader"
1410
1362
  ? taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome)
1411
1363
  : undefined;
@@ -1476,6 +1428,7 @@ function addTaskRole(args, store, options) {
1476
1428
  const result = store.transaction((tx) => {
1477
1429
  const task = requireTask(tx, taskId);
1478
1430
  assertTaskOpen(task);
1431
+ taskActor(tx, options, task.id);
1479
1432
  assertRoleRuntimeMutationAllowed(tx, {
1480
1433
  scope: "task",
1481
1434
  taskId: task.id,
@@ -1594,6 +1547,7 @@ function updateTaskRole(args, store, options) {
1594
1547
  const updated = store.transaction((tx) => {
1595
1548
  const task = requireTask(tx, taskId);
1596
1549
  assertTaskOpen(task);
1550
+ taskActor(tx, options, task.id);
1597
1551
  const role = requireRole(tx, task.id, roleName);
1598
1552
  const changesLaunchContext = hasRoleLaunchContextOptions(parsed) || parsed.has("--profile");
1599
1553
  const changesAgentConfig = hasAgentConfigOptions(parsed);
@@ -1640,6 +1594,7 @@ function removeTaskRole(args, store, options) {
1640
1594
  const removed = store.transaction((tx) => {
1641
1595
  const task = requireTask(tx, args[0]);
1642
1596
  assertTaskOpen(task);
1597
+ taskActor(tx, options, task.id);
1643
1598
  const role = requireRole(tx, task.id, args[1]);
1644
1599
  if (role.name === LEADER_ROLE)
1645
1600
  throw usageError("The Task Leader role cannot be removed.");
@@ -1669,6 +1624,7 @@ function bindTaskRole(args, store, options) {
1669
1624
  const result = store.transaction((tx) => {
1670
1625
  const task = requireTask(tx, args[0]);
1671
1626
  assertTaskOpen(task);
1627
+ taskActor(tx, options, task.id);
1672
1628
  const role = requireRole(tx, task.id, args[1]);
1673
1629
  assertRoleRuntimeMutationAllowed(tx, {
1674
1630
  scope: "task",
@@ -1722,6 +1678,8 @@ function unbindTaskRole(args, store, options) {
1722
1678
  const now = clock(options);
1723
1679
  const result = store.transaction((tx) => {
1724
1680
  const task = requireTask(tx, args[0]);
1681
+ assertTaskOpen(task);
1682
+ taskActor(tx, options, task.id);
1725
1683
  const role = requireRole(tx, task.id, args[1]);
1726
1684
  try {
1727
1685
  const unbound = unbindRoleAgent(role, tx.getTaskRoleSessionSet(task.id, role.name), args[2], now);
@@ -1772,6 +1730,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
1772
1730
  if (task.status !== "active") {
1773
1731
  throw usageError(inactiveTaskMessage(task, `${action} Provider authority`));
1774
1732
  }
1733
+ taskActor(tx, options, task.id);
1775
1734
  const role = requireRole(tx, task.id, args[1]);
1776
1735
  const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1777
1736
  const session = sessions?.sessions[role.activeAgentId];
@@ -1887,6 +1846,7 @@ function createWork(args, store, options) {
1887
1846
  const item = store.transaction((tx) => {
1888
1847
  const task = requireTask(tx, parsed.positionals[0]);
1889
1848
  assertTaskOpen(task);
1849
+ taskActor(tx, options, task.id);
1890
1850
  for (const dependencyId of parsed.after) {
1891
1851
  const dependency = tx.getWorkItem(task.id, dependencyId);
1892
1852
  if (dependency === null)
@@ -1946,9 +1906,7 @@ function updateWorkScope(args, store, options) {
1946
1906
  const updated = store.transaction((tx) => {
1947
1907
  const item = requireWorkItem(tx, parsed.positionals[0], options);
1948
1908
  const task = requireTask(tx, item.taskId);
1949
- if (taskActor(options, task.id) !== "leader") {
1950
- throw usageError("Only the Task Leader may change a Work Item Project scope.");
1951
- }
1909
+ taskActor(tx, options, task.id);
1952
1910
  if (tx.getActiveAgentRun(task.id, item.assignee ?? "") !== null) {
1953
1911
  throw usageError(`Stop the active Work Item Run before changing scope: ${item.id}.`);
1954
1912
  }
@@ -1996,9 +1954,7 @@ function updateWork(args, store, options) {
1996
1954
  const task = requireTask(tx, current.taskId);
1997
1955
  assertTaskOpen(task);
1998
1956
  if (current.assignee === undefined) {
1999
- if (taskActor(options, task.id) !== "leader") {
2000
- throw usageError(`Only the Task Leader may update unassigned Work Item execution: ${current.id}.`);
2001
- }
1957
+ taskActor(tx, options, task.id);
2002
1958
  if (status === "running") {
2003
1959
  assertWorkItemDependenciesCompleted(tx, current);
2004
1960
  }
@@ -2143,6 +2099,7 @@ function dispatchWork(args, store, options) {
2143
2099
  if (task.status !== "active") {
2144
2100
  throw usageError(inactiveTaskMessage(task, "dispatch"));
2145
2101
  }
2102
+ taskActor(tx, options, task.id);
2146
2103
  const currentGroup = currentWorkItemExecutionGroup(item);
2147
2104
  const existingGroup = currentGroup?.resolution === undefined
2148
2105
  ? currentGroup
@@ -2190,9 +2147,8 @@ function dispatchWork(args, store, options) {
2190
2147
  + `The Task Leader must run "yui task work update ${item.id} running", `
2191
2148
  + "then execute it directly or create native subagents in the Leader Session.");
2192
2149
  }
2193
- if (expanding && taskActor(options, task.id) !== "leader") {
2194
- throw usageError("Only the Task Leader may expand a running ExecutionGroup.");
2195
- }
2150
+ if (expanding)
2151
+ taskActor(tx, options, task.id);
2196
2152
  assertWorkItemDependenciesCompleted(tx, item);
2197
2153
  const workspace = tx.getWorkItemWorkspace(task.id, item.id);
2198
2154
  if (workspace?.owner.type === "work-item"
@@ -2572,9 +2528,7 @@ function resolveWorkExecutionGroup(args, store, options) {
2572
2528
  const task = requireTask(tx, item.taskId);
2573
2529
  if (task.status !== "active")
2574
2530
  throw usageError(inactiveTaskMessage(task, "resolving an ExecutionGroup"));
2575
- if (taskActor(options, task.id) !== "leader") {
2576
- throw usageError("Only the Task Leader may resolve an ExecutionGroup.");
2577
- }
2531
+ taskActor(tx, options, task.id);
2578
2532
  let group = currentWorkItemExecutionGroup(item);
2579
2533
  if (group === undefined
2580
2534
  || (group.stage === undefined
@@ -2806,9 +2760,7 @@ function acceptWork(args, store, options) {
2806
2760
  if (task.status !== "active") {
2807
2761
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2808
2762
  }
2809
- if (taskActor(options, task.id) !== "leader") {
2810
- throw usageError("Only the Task Leader may accept a Work Item.");
2811
- }
2763
+ const actor = taskActor(tx, options, task.id);
2812
2764
  if (item.status !== "awaiting_acceptance") {
2813
2765
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
2814
2766
  }
@@ -2867,9 +2819,9 @@ function acceptWork(args, store, options) {
2867
2819
  ...(candidate.source.type === "run"
2868
2820
  ? { runId: candidate.source.runId }
2869
2821
  : { workItemRevision: String(candidate.workItemRevision) }),
2870
- acceptedBy: "leader",
2822
+ acceptedBy: actor,
2871
2823
  summary,
2872
- ...leaderActionEventPayload(tx, item.taskId, options)
2824
+ ...(actor === "leader" ? leaderActionEventPayload(tx, item.taskId, options) : {})
2873
2825
  }, now);
2874
2826
  return completed;
2875
2827
  });
@@ -2929,9 +2881,7 @@ function rejectWork(args, store, options) {
2929
2881
  if (task.status !== "active") {
2930
2882
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2931
2883
  }
2932
- if (taskActor(options, task.id) !== "leader") {
2933
- throw usageError("Only the Task Leader may reject a Work Item.");
2934
- }
2884
+ const actor = taskActor(tx, options, task.id);
2935
2885
  if (item.status !== "awaiting_acceptance") {
2936
2886
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
2937
2887
  }
@@ -2945,7 +2895,7 @@ function rejectWork(args, store, options) {
2945
2895
  recordTaskEvent(tx, item.taskId, "work.rejected", {
2946
2896
  workItemId: item.id,
2947
2897
  candidateId: candidate.id,
2948
- rejectedBy: "leader",
2898
+ rejectedBy: actor,
2949
2899
  summary
2950
2900
  }, now);
2951
2901
  return failed;
@@ -2966,7 +2916,7 @@ function retireWork(args, store, options) {
2966
2916
  if (task.status !== "active") {
2967
2917
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2968
2918
  }
2969
- const actor = taskActor(options, task.id);
2919
+ const actor = taskActor(tx, options, task.id);
2970
2920
  if (replacementWorkItemId !== undefined) {
2971
2921
  const replacement = requireWorkItem(tx, replacementWorkItemId, options);
2972
2922
  if (replacement.taskId !== task.id) {
@@ -3088,9 +3038,7 @@ function reviewWork(args, store, options) {
3088
3038
  if (task.status !== "active") {
3089
3039
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
3090
3040
  }
3091
- if (taskActor(options, task.id) !== "leader") {
3092
- throw usageError("Only the Task Leader may request a Work Item review.");
3093
- }
3041
+ const requestedBy = taskActor(tx, options, task.id);
3094
3042
  if (item.status !== "awaiting_acceptance") {
3095
3043
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
3096
3044
  }
@@ -3117,7 +3065,7 @@ function reviewWork(args, store, options) {
3117
3065
  }
3118
3066
  throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
3119
3067
  }
3120
- const queued = queueReviewRound(tx, item, config, "leader", now);
3068
+ const queued = queueReviewRound(tx, item, config, requestedBy, now);
3121
3069
  return { ...queued, resumed: false };
3122
3070
  });
3123
3071
  if (result.run !== null) {
@@ -3131,7 +3079,7 @@ function reviewWork(args, store, options) {
3131
3079
  : output(`Review requested as ${result.round.id}\n`, { reviewRound: result.round });
3132
3080
  }
3133
3081
  /**
3134
- * Leader-controlled recovery for a failed Task-final ReviewRound that never
3082
+ * Task-control recovery for a failed Task-final ReviewRound that never
3135
3083
  * created a Reviewer Run. This is deliberately separate from `task run retry`:
3136
3084
  * that command requires an exact failed Agent Run and remains the only retry
3137
3085
  * path for a failed provider execution. Here the old terminal Round is an
@@ -3281,9 +3229,7 @@ function resolveReviewExecutionGroup(args, store, options) {
3281
3229
  const task = requireTask(tx, round.taskId);
3282
3230
  if (task.status !== "active")
3283
3231
  throw usageError(inactiveTaskMessage(task, "resolving a Review ExecutionGroup"));
3284
- if (taskActor(options, task.id) !== "leader") {
3285
- throw usageError("Only the Task Leader may resolve a Reviewer ExecutionGroup.");
3286
- }
3232
+ taskActor(tx, options, task.id);
3287
3233
  const group = round.executionGroup;
3288
3234
  if (group === undefined
3289
3235
  || (group.lanes.length < 2 && group.strategy.mode !== "adaptive")) {
@@ -3402,7 +3348,7 @@ function disposeReviewFindingCommand(args, store, options) {
3402
3348
  const parsed = parseTail(args, new Set(["--disposition", "--work-item", "--commit", "--verification", "--note", "--superseded-by"]), usage);
3403
3349
  exactPositionals(parsed.positionals, 1, usage);
3404
3350
  const disposition = requiredOption(parsed.options, "--disposition");
3405
- if (!LEADER_FINDING_DISPOSITIONS.includes(disposition)) {
3351
+ if (!TASK_CONTROL_FINDING_DISPOSITIONS.includes(disposition)) {
3406
3352
  throw usageError(`Review finding disposition is invalid: ${disposition}.`);
3407
3353
  }
3408
3354
  const now = clock(options);
@@ -3414,12 +3360,12 @@ function disposeReviewFindingCommand(args, store, options) {
3414
3360
  const task = requireTask(tx, reference.taskId);
3415
3361
  if (task.status !== "active")
3416
3362
  throw usageError(inactiveTaskMessage(task, "dispositioning a review finding"));
3417
- if (taskActor(options, task.id) !== "leader") {
3418
- throw usageError("Only the Task Leader may disposition a review finding.");
3419
- }
3363
+ const actor = taskActor(tx, options, task.id);
3420
3364
  const command = {
3421
3365
  disposition,
3422
- by: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader",
3366
+ by: actor === "leader"
3367
+ ? taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? actor
3368
+ : actor,
3423
3369
  ...(parsed.options.get("--note") === undefined ? {} : { note: parsed.options.get("--note") }),
3424
3370
  ...(parsed.options.get("--work-item") === undefined ? {} : { workItemId: parsed.options.get("--work-item") }),
3425
3371
  ...(parsed.options.get("--commit") === undefined ? {} : { commit: parsed.options.get("--commit") }),
@@ -3451,9 +3397,7 @@ function planReviewRepairWave(args, store, options) {
3451
3397
  if (currentTask.status !== "active") {
3452
3398
  throw usageError(inactiveTaskMessage(currentTask, "creating a review repair wave"));
3453
3399
  }
3454
- if (taskActor(options, currentTask.id) !== "leader") {
3455
- throw usageError("Only the Task Leader may create a review repair wave.");
3456
- }
3400
+ taskActor(tx, options, currentTask.id);
3457
3401
  const openItems = tx.listWorkItems(currentTask.id)
3458
3402
  .filter((item) => item.status === "pending" || item.status === "running");
3459
3403
  return groups.map((group) => {
@@ -3552,9 +3496,7 @@ function requestTaskReviewRound(args, store, options) {
3552
3496
  const task = requireTask(tx, parsed.positionals[0]);
3553
3497
  if (task.status !== "active")
3554
3498
  throw usageError(`Task is not active: ${task.id}.`);
3555
- if (taskActor(options, task.id) !== "leader") {
3556
- throw usageError("Only the Task Leader may request a Task-final Review.");
3557
- }
3499
+ const requestedBy = taskActor(tx, options, task.id);
3558
3500
  if (task.projectBindings.length === 0) {
3559
3501
  throw usageError(`Task ${task.id} has no bound Projects for a Task-final Review.`);
3560
3502
  }
@@ -3706,8 +3648,8 @@ function requestTaskReviewRound(args, store, options) {
3706
3648
  deltaRecord = validateDeltaRecheckRequest(tx, task.id, provenance.candidate, options.deltaRecheckPreflight);
3707
3649
  }
3708
3650
  let created = deltaRecord === undefined
3709
- ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
3710
- : createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
3651
+ ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, requestedBy, provenance.candidate, now, taskFinalContract)
3652
+ : createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, requestedBy, provenance.candidate, deltaRecord, now, taskFinalContract);
3711
3653
  let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3712
3654
  purpose: "review",
3713
3655
  target: executionTargetForReviewRound(task, created),
@@ -3783,9 +3725,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3783
3725
  const task = requireTask(tx, reference.taskId);
3784
3726
  if (task.status !== "active")
3785
3727
  throw usageError(`Task is not active: ${task.id}.`);
3786
- if (taskActor(options, task.id) !== "leader") {
3787
- throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
3788
- }
3728
+ const requestedBy = taskActor(tx, options, task.id);
3789
3729
  if ((source.scope ?? "work-item") !== "task") {
3790
3730
  throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
3791
3731
  }
@@ -3859,7 +3799,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3859
3799
  reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
3860
3800
  tx.saveRole(task.id, reviewer);
3861
3801
  }
3862
- let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
3802
+ let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.reviewerRoleName, requestedBy, source.taskCandidate, now, taskFinalContract);
3863
3803
  const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3864
3804
  purpose: "review",
3865
3805
  target: executionTargetForReviewRound(task, created),
@@ -3875,7 +3815,8 @@ function forceFreshTaskReviewRound(args, store, options) {
3875
3815
  reviewerRoleName: created.reviewerRoleName,
3876
3816
  taskCandidate: JSON.stringify(created.taskCandidate),
3877
3817
  reason: "source-round-terminal-without-semantic-review",
3878
- leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3818
+ requestedBy,
3819
+ ...(requestedBy === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
3879
3820
  }, now);
3880
3821
  return { round: created, source, created: true };
3881
3822
  });
@@ -3995,9 +3936,7 @@ function retryFailedTaskReviewRound(args, store, options) {
3995
3936
  const task = requireTask(tx, reference.taskId);
3996
3937
  if (task.status !== "active")
3997
3938
  throw usageError(`Task is not active: ${task.id}.`);
3998
- if (taskActor(options, task.id) !== "leader") {
3999
- throw usageError("Only the Task Leader may retry a failed Task-final ReviewRound.");
4000
- }
3939
+ const requestedBy = taskActor(tx, options, task.id);
4001
3940
  if ((round.scope ?? "work-item") !== "task") {
4002
3941
  throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
4003
3942
  }
@@ -4079,7 +4018,7 @@ function retryFailedTaskReviewRound(args, store, options) {
4079
4018
  // Issue 06: infra retry resets the same semantic Round to pending instead
4080
4019
  // of manufacturing a new Round, so Round count and finding identity stay
4081
4020
  // stable across execution-attempt failures.
4082
- const resetRound = retryTaskReviewRound(round);
4021
+ const resetRound = retryTaskReviewRound(round, requestedBy);
4083
4022
  tx.saveReviewRound(task.id, resetRound);
4084
4023
  recordTaskEvent(tx, task.id, "review.task-final-retried", {
4085
4024
  reviewRoundId: round.id
@@ -4117,8 +4056,16 @@ function taskRunCommand(args, store, options) {
4117
4056
  : `Unknown command: task run ${command}`);
4118
4057
  }
4119
4058
  function retireRun(args, store, options) {
4120
- const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text>.";
4121
- const parsed = parseTail(args, new Set(["--reason"]), usage);
4059
+ const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
4060
+ const parsed = parseTail(args, new Set([
4061
+ "--reason",
4062
+ "--expected-progress-at",
4063
+ "--progress-at",
4064
+ "--agent-id",
4065
+ "--adapter-id",
4066
+ "--native-session-id",
4067
+ "--launch-id"
4068
+ ]), usage);
4122
4069
  exactPositionals(parsed.positionals, 1, usage);
4123
4070
  const reason = requiredOption(parsed.options, "--reason");
4124
4071
  const reference = taskRecordReference(parsed.positionals[0], "agentRun", "Agent Run reference", options);
@@ -4126,10 +4073,7 @@ function retireRun(args, store, options) {
4126
4073
  const result = store.transaction((tx) => {
4127
4074
  const task = requireTask(tx, reference.taskId);
4128
4075
  assertTaskOpen(task);
4129
- const actor = taskActor(options, task.id);
4130
- if (actor === "leader") {
4131
- throw usageError("Only the user or global Operator may retire an Agent Run.");
4132
- }
4076
+ const actor = taskActor(tx, options, task.id);
4133
4077
  let run = tx.getAgentRun(task.id, reference.localId);
4134
4078
  if (run === null)
4135
4079
  throw dataError(`Agent Run not found: ${task.id}/${reference.localId}.`);
@@ -4138,19 +4082,66 @@ function retireRun(args, store, options) {
4138
4082
  return { task, run, changed: false };
4139
4083
  }
4140
4084
  if (run.status === "active") {
4141
- const terminal = terminalizeExactTaskRun(tx, {
4085
+ if (actor === "leader"
4086
+ && taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) === run.id) {
4087
+ throw usageError("A Task Leader cannot retire its own current authority Run.", usage);
4088
+ }
4089
+ const expectedProgressAt = requiredOption(parsed.options, parsed.options.has("--expected-progress-at")
4090
+ ? "--expected-progress-at"
4091
+ : "--progress-at");
4092
+ if (parsed.options.has("--expected-progress-at") && parsed.options.has("--progress-at")) {
4093
+ throw usageError("--expected-progress-at and --progress-at are mutually exclusive.", usage);
4094
+ }
4095
+ const agentId = requiredOption(parsed.options, "--agent-id");
4096
+ const adapterId = requiredOption(parsed.options, "--adapter-id");
4097
+ const nativeSessionId = parsed.options.get("--native-session-id");
4098
+ const launchId = parsed.options.get("--launch-id");
4099
+ const sessions = tx.getTaskRoleSessionSet(task.id, run.roleName);
4100
+ const session = sessions?.sessions[run.effective.agentId];
4101
+ if (session?.nativeSessionId !== undefined && nativeSessionId === undefined) {
4102
+ throw usageError("--native-session-id is required for this active Run.", usage);
4103
+ }
4104
+ if (session?.nativeSessionId === undefined && launchId === undefined) {
4105
+ throw usageError("--launch-id is required for an opaque active Run.", usage);
4106
+ }
4107
+ const terminal = retireExactActiveAgentRun(tx, {
4142
4108
  taskId: task.id,
4143
4109
  roleName: run.roleName,
4144
- agentId: run.effective.agentId,
4145
4110
  runId: run.id,
4146
- receiptId: agentRunDeliveryReceiptId(run),
4147
- outcome: { status: "failed", summary: `Agent Run retired: ${reason}` }
4111
+ agentId,
4112
+ adapterId,
4113
+ ...(nativeSessionId === undefined ? {} : { nativeSessionId }),
4114
+ ...(launchId === undefined ? {} : { launchId }),
4115
+ expectedProgressAt,
4116
+ reason: `Agent Run retired: ${reason}`
4148
4117
  }, now);
4149
4118
  if (terminal.disposition !== "applied" || terminal.run === null) {
4150
- throw usageError(`Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
4119
+ throw usageError(terminal.disposition === "blocked"
4120
+ ? `Agent Run retirement is blocked: ${run.id}/${terminal.reason ?? "unsafe"}.`
4121
+ : `Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
4151
4122
  }
4152
4123
  run = terminal.run;
4153
4124
  }
4125
+ recordTaskEvent(tx, task.id, "run.retired", {
4126
+ runId: run.id,
4127
+ reason,
4128
+ ...(parsed.options.get("--expected-progress-at") === undefined
4129
+ && parsed.options.get("--progress-at") === undefined
4130
+ ? {}
4131
+ : {
4132
+ expectedProgressAt: parsed.options.get("--expected-progress-at")
4133
+ ?? parsed.options.get("--progress-at")
4134
+ }),
4135
+ ...(parsed.options.get("--native-session-id") === undefined
4136
+ ? {}
4137
+ : { nativeSessionId: parsed.options.get("--native-session-id") }),
4138
+ ...(parsed.options.get("--launch-id") === undefined
4139
+ ? {}
4140
+ : { launchId: parsed.options.get("--launch-id") }),
4141
+ ...(actor === "leader"
4142
+ ? leaderActionEventPayload(tx, task.id, options)
4143
+ : { retiredBy: actor })
4144
+ }, now);
4154
4145
  tx.saveEvent(task.id, createTaskRecordRetirement({
4155
4146
  eventId: tx.nextEventId(task.id),
4156
4147
  taskId: task.id,
@@ -4289,9 +4280,7 @@ function settleStaleFinalReviewRun(args, store, options) {
4289
4280
  const task = requireTask(tx, run.taskId);
4290
4281
  if (task.status !== "active")
4291
4282
  throw usageError(`Task is not active: ${task.id}.`);
4292
- if (taskActor(options, task.id) !== "leader") {
4293
- throw usageError("Only the Task Leader may settle a stale final review Run.");
4294
- }
4283
+ const actor = taskActor(tx, options, task.id);
4295
4284
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
4296
4285
  if (round === null) {
4297
4286
  throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
@@ -4400,7 +4389,9 @@ function settleStaleFinalReviewRun(args, store, options) {
4400
4389
  runId: run.id,
4401
4390
  reviewRoundId: round.id,
4402
4391
  previousTaskCandidate: JSON.stringify(round.taskCandidate),
4403
- currentTaskCandidate: JSON.stringify(currentTaskCandidate)
4392
+ currentTaskCandidate: JSON.stringify(currentTaskCandidate),
4393
+ settledBy: actor,
4394
+ ...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
4404
4395
  }, now);
4405
4396
  return { run, round: terminal, changed: true };
4406
4397
  });
@@ -4951,7 +4942,7 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
4951
4942
  }
4952
4943
  }
4953
4944
  /**
4954
- * Leader-only retry of an exact failed Task-final review Run. The old failed
4945
+ * Task-control retry of an exact failed Task-final review Run. The old failed
4955
4946
  * Run remains the attempt trail, while the semantic ReviewRound is reset to
4956
4947
  * pending under its existing identity. Every identity and frozen-head fence is
4957
4948
  * checked inside one transaction so a partial fail-old-without-reset state can
@@ -4978,9 +4969,7 @@ function retryFailedReviewRun(previous, store, options, now) {
4978
4969
  throw usageError(`Review Run ${run.id} still owns an unsettled Provider continuation; `
4979
4970
  + "reconcile it before retrying the Lane.");
4980
4971
  }
4981
- if (taskActor(options, task.id) !== "leader") {
4982
- throw usageError("Only the Task Leader may retry a failed final review Run.");
4983
- }
4972
+ const requestedBy = taskActor(tx, options, task.id);
4984
4973
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
4985
4974
  if (round === null) {
4986
4975
  throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
@@ -5160,7 +5149,7 @@ function retryFailedReviewRun(previous, store, options, now) {
5160
5149
  // Issue 06: infra retry resets the same semantic Round to pending instead
5161
5150
  // of manufacturing a new Round, so Round count and finding identity stay
5162
5151
  // stable across execution-attempt failures.
5163
- const resetRound = retryTaskReviewRound(roundToReset, run.executionLaneId);
5152
+ const resetRound = retryTaskReviewRound(roundToReset, requestedBy, run.executionLaneId);
5164
5153
  tx.saveReviewRound(task.id, resetRound);
5165
5154
  recordTaskEvent(tx, task.id, "run.review-retried", {
5166
5155
  runId: run.id,
@@ -5205,9 +5194,7 @@ function recoverRun(args, store, options) {
5205
5194
  const task = requireTask(tx, active.taskId);
5206
5195
  if (task.status !== "active")
5207
5196
  throw usageError(inactiveTaskMessage(task, "recovering a run"));
5208
- if (taskActor(options, task.id) !== "leader") {
5209
- throw usageError("Only the Task Leader may control exact Agent Run recovery.");
5210
- }
5197
+ taskActor(tx, options, task.id);
5211
5198
  const roleName = parsed.options.get("--role") ?? active.roleName;
5212
5199
  if (roleName !== active.roleName) {
5213
5200
  throw usageError(`Recovery Role does not match Run ${active.id}: ${roleName}.`);
@@ -5308,18 +5295,27 @@ function showRun(args, store, options) {
5308
5295
  const retirement = tx.listEvents(run.taskId)
5309
5296
  .map(taskRecordRetirement)
5310
5297
  .find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
5311
- return { run, recovery: projectRunRecovery(facts), retirement };
5298
+ const allRejectedYieldAttempts = tx.listEvents(run.taskId)
5299
+ .map(rejectedYieldAttemptFromTaskEvent)
5300
+ .filter((attempt) => (attempt !== null && attempt.runId === run.id));
5301
+ return {
5302
+ run,
5303
+ recovery: projectRunRecovery(facts),
5304
+ retirement,
5305
+ rejectedYieldAttemptCount: allRejectedYieldAttempts.length,
5306
+ rejectedYieldAttempts: allRejectedYieldAttempts.slice(-16)
5307
+ };
5312
5308
  });
5313
5309
  if (asJson) {
5314
5310
  return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
5315
5311
  }
5316
5312
  return {
5317
5313
  kind: "output",
5318
- output: renderRunShow(data.run, data.recovery, data.retirement),
5314
+ output: renderRunShow(data.run, data.recovery, data.retirement, data.rejectedYieldAttemptCount, data.rejectedYieldAttempts),
5319
5315
  data
5320
5316
  };
5321
5317
  }
5322
- function renderRunShow(run, recovery, retirement) {
5318
+ function renderRunShow(run, recovery, retirement, rejectedYieldAttemptCount, rejectedYieldAttempts) {
5323
5319
  const lines = [
5324
5320
  `Run: ${run.id}`,
5325
5321
  `Task: ${run.taskId}`,
@@ -5339,7 +5335,19 @@ function renderRunShow(run, recovery, retirement) {
5339
5335
  : [`Provider accepted (durable): ${run.deliveredAt}`]),
5340
5336
  ...(run.summary === undefined || run.summary.trim().length === 0
5341
5337
  ? []
5342
- : [`Summary: ${run.summary}`])
5338
+ : [`Summary: ${run.summary}`]),
5339
+ ...(rejectedYieldAttemptCount === 0
5340
+ ? []
5341
+ : [
5342
+ `Rejected yield attempts: ${rejectedYieldAttempts.length} shown of ${rejectedYieldAttemptCount} (unaccepted diagnostic evidence only)`,
5343
+ ...rejectedYieldAttempts.slice(-5).flatMap((attempt) => [
5344
+ ` ${attempt.attemptedAt}: ${attempt.rejectionReason} (${attempt.attemptDigest.slice(0, 16)})`,
5345
+ ` Reviewer output: ${truncateRunShowText(attempt.summary)}`,
5346
+ ` Checks: ${attempt.checks.length === 0
5347
+ ? "none"
5348
+ : attempt.checks.map(({ name, outcome }) => `${name}:${outcome}`).join(", ")}`
5349
+ ])
5350
+ ])
5343
5351
  ];
5344
5352
  if (recovery.canonicalProgressAt !== null) {
5345
5353
  lines.push(`Canonical recovery fence (Yui durable CAS): ${recovery.canonicalProgressAt}`, ...(recovery.canonicalProgressEvidence === undefined
@@ -5370,6 +5378,10 @@ function renderRunShow(run, recovery, retirement) {
5370
5378
  }
5371
5379
  return `${lines.join("\n")}\n`;
5372
5380
  }
5381
+ function truncateRunShowText(value) {
5382
+ const normalized = value.replaceAll(/\s+/gu, " ").trim();
5383
+ return normalized.length <= 400 ? normalized : `${normalized.slice(0, 399)}…`;
5384
+ }
5373
5385
  /**
5374
5386
  * Issue 04: builds the terminal yield outcome from the command inputs. The
5375
5387
  * same construction feeds both the first commit and the idempotent replay, so
@@ -5462,6 +5474,70 @@ function yieldRunStatus(args, store, options) {
5462
5474
  + `Request: ${run.yieldReceipt.requestId}\n`
5463
5475
  + `Committed: ${run.yieldReceipt.committedAt}\n`, { receipt: run.yieldReceipt });
5464
5476
  }
5477
+ function recordRejectedReviewYield(store, input) {
5478
+ if (input.run.purpose !== "review" || input.run.reviewRoundId === undefined) {
5479
+ throw new Error(`Only a Review Run can record rejected yield evidence: ${input.run.id}.`);
5480
+ }
5481
+ const receiptId = agentRunDeliveryReceiptId(input.run);
5482
+ const sessions = store.getTaskRoleSessionSet(input.run.taskId, input.run.roleName);
5483
+ const durableSession = sessions?.sessions[input.run.effective.agentId];
5484
+ const attempt = createRejectedYieldAttempt({
5485
+ taskId: input.run.taskId,
5486
+ runId: input.run.id,
5487
+ roleName: input.run.roleName,
5488
+ reviewRoundId: input.run.reviewRoundId,
5489
+ receiptId,
5490
+ rejectionReason: input.rejectionReason,
5491
+ summary: input.outcome.summary,
5492
+ ...(input.outcome.reviewResult === undefined
5493
+ ? {}
5494
+ : { reviewResult: input.outcome.reviewResult }),
5495
+ ...(input.nativeSessionId === undefined
5496
+ ? {}
5497
+ : { nativeSessionId: input.nativeSessionId }),
5498
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
5499
+ ...(durableSession?.nativeSessionId === undefined
5500
+ ? {}
5501
+ : { durableNativeSessionId: durableSession.nativeSessionId }),
5502
+ ...(durableSession?.launchId === undefined
5503
+ ? {}
5504
+ : { durableLaunchId: durableSession.launchId }),
5505
+ ...(sessions?.inFlight?.runId === undefined
5506
+ ? {}
5507
+ : { inFlightRunId: sessions.inFlight.runId }),
5508
+ ...(sessions?.inFlight?.receiptId === undefined
5509
+ ? {}
5510
+ : { inFlightReceiptId: sessions.inFlight.receiptId }),
5511
+ ...(input.run.assignment.contextSnapshotRef === undefined
5512
+ ? {}
5513
+ : { contextSnapshot: input.run.assignment.contextSnapshotRef }),
5514
+ activeRun: input.activeRun === null
5515
+ ? null
5516
+ : {
5517
+ id: input.activeRun.id,
5518
+ receiptId: agentRunDeliveryReceiptId(input.activeRun),
5519
+ ...(input.activeRun.assignment.contextSnapshotRef === undefined
5520
+ ? {}
5521
+ : { contextSnapshot: input.activeRun.assignment.contextSnapshotRef })
5522
+ },
5523
+ attemptedAt: input.now
5524
+ });
5525
+ const existing = store.listEvents(input.run.taskId).find((event) => (event.type === RUN_YIELD_REJECTED_EVENT
5526
+ && event.payload.runId === input.run.id
5527
+ && event.payload.receiptId === receiptId
5528
+ && event.payload.attemptDigest === attempt.attemptDigest));
5529
+ if (existing !== undefined) {
5530
+ const persisted = rejectedYieldAttemptFromTaskEvent(existing);
5531
+ if (persisted === null) {
5532
+ throw new Error(`Rejected yield attempt is malformed: ${existing.id}.`);
5533
+ }
5534
+ return { attempt: persisted, event: existing, created: false };
5535
+ }
5536
+ const event = createTaskEvent(store.nextEventId(input.run.taskId), input.run.taskId, RUN_YIELD_REJECTED_EVENT, rejectedYieldAttemptEventPayload(attempt), input.now);
5537
+ store.saveEvent(input.run.taskId, event);
5538
+ routeRoleEvent(store, event, input.run.roleName, "run-yield-rejected", input.now);
5539
+ return { attempt, event, created: true };
5540
+ }
5465
5541
  function yieldRun(args, store, options) {
5466
5542
  const usage = "Task run yield usage: yui task run yield <task>/<run> (--summary <text>|--summary-file <path|->).";
5467
5543
  const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
@@ -5491,8 +5567,6 @@ function yieldRun(args, store, options) {
5491
5567
  throw usageError(inactiveTaskMessage(task, "yielding a run"));
5492
5568
  const role = requireRole(tx, task.id, active.roleName);
5493
5569
  const pointer = activeRunPointer(tx, active);
5494
- if (pointer?.id !== active.id)
5495
- throw usageError(`Run is not active for ${task.id}/${role.name}: ${active.id}.`);
5496
5570
  const taskFinalContract = active.purpose === "execution"
5497
5571
  && active.workItemId !== undefined
5498
5572
  ? taskFinalReviewContractForMutation(tx, task.id, options)
@@ -5545,6 +5619,27 @@ function yieldRun(args, store, options) {
5545
5619
  : { reviewResult: yieldOutcome.reviewResult })
5546
5620
  }, now);
5547
5621
  if (terminalization.disposition !== "applied" || terminalization.run === null) {
5622
+ if (active.purpose === "review" && active.reviewRoundId !== undefined) {
5623
+ const rejected = recordRejectedReviewYield(tx, {
5624
+ run: active,
5625
+ activeRun: pointer,
5626
+ rejectionReason: terminalization.reason ?? "obsolete",
5627
+ outcome: yieldOutcome,
5628
+ ...(options.environment?.YUI_NATIVE_SESSION_ID === undefined
5629
+ ? {}
5630
+ : { nativeSessionId: options.environment.YUI_NATIVE_SESSION_ID }),
5631
+ ...(options.environment?.YUI_LAUNCH_ID === undefined
5632
+ ? {}
5633
+ : { launchId: options.environment.YUI_LAUNCH_ID }),
5634
+ now
5635
+ });
5636
+ return {
5637
+ kind: "rejected",
5638
+ run: active,
5639
+ rejected,
5640
+ notifyLeader: rejected.created
5641
+ };
5642
+ }
5548
5643
  throw usageError(`Run ${active.id} no longer matches its exact execution fence: `
5549
5644
  + `${terminalization.reason ?? "obsolete"}.`);
5550
5645
  }
@@ -5727,6 +5822,7 @@ function yieldRun(args, store, options) {
5727
5822
  if (receipt !== null) {
5728
5823
  tx.saveAgentRun(receipt);
5729
5824
  return {
5825
+ kind: "yielded",
5730
5826
  run: receipt,
5731
5827
  reviewDispatch,
5732
5828
  notifyLeader: leaderHandoff !== null
@@ -5734,11 +5830,21 @@ function yieldRun(args, store, options) {
5734
5830
  }
5735
5831
  }
5736
5832
  return {
5833
+ kind: "yielded",
5737
5834
  run: terminal,
5738
5835
  reviewDispatch,
5739
5836
  notifyLeader: leaderHandoff !== null
5740
5837
  };
5741
5838
  });
5839
+ if (yielded.kind === "rejected") {
5840
+ if (yielded.notifyLeader) {
5841
+ notifyMailbox(options.runtime, leaderMailbox(yielded.run.taskId), yielded.run.taskId);
5842
+ }
5843
+ throw usageError(`Run ${yielded.run.id} no longer matches its exact execution fence: `
5844
+ + `${yielded.rejected.attempt.rejectionReason}. `
5845
+ + `Rejected Reviewer output was recorded as ${yielded.rejected.event.id} `
5846
+ + "(unaccepted diagnostic evidence only).");
5847
+ }
5742
5848
  if (yielded.notifyLeader) {
5743
5849
  notifyMailbox(options.runtime, leaderMailbox(yielded.run.taskId), yielded.run.taskId);
5744
5850
  }
@@ -6267,6 +6373,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6267
6373
  export function failPendingReviewRound(taskId, reviewRoundId, summary, store, options = {}) {
6268
6374
  const now = clock(options);
6269
6375
  const failed = store.transaction((tx) => {
6376
+ const task = requireTask(tx, taskId);
6377
+ taskActor(tx, options, task.id);
6270
6378
  const round = tx.getReviewRound(taskId, reviewRoundId);
6271
6379
  if (round === null)
6272
6380
  throw usageError(`ReviewRound not found: ${taskId}/${reviewRoundId}.`);
@@ -6377,52 +6485,6 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
6377
6485
  store.saveEvent(taskId, event);
6378
6486
  return event;
6379
6487
  }
6380
- function publishedTreeAuthorizationPayload(actor, proof) {
6381
- return {
6382
- by: actor,
6383
- projectId: proof.projectId,
6384
- publicationId: proof.publicationId,
6385
- ...(proof.reviewRoundId === undefined
6386
- ? { reviewAnchor: publishedTreeReviewAnchor(proof) }
6387
- : { reviewRoundId: proof.reviewRoundId }),
6388
- localCommit: proof.localCommit,
6389
- remoteCommit: proof.remoteCommit,
6390
- tree: proof.tree
6391
- };
6392
- }
6393
- function matchingPublishedTreeAuthorization(store, proof) {
6394
- const events = store.listEvents(proof.taskId);
6395
- for (let index = events.length - 1; index >= 0; index -= 1) {
6396
- const event = events[index];
6397
- if (event.type === "task.completed" || event.type === "task.reopened")
6398
- return undefined;
6399
- if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
6400
- && (event.payload.by === "user" || event.payload.by === "operator")
6401
- && event.payload.projectId === proof.projectId
6402
- && event.payload.publicationId === proof.publicationId
6403
- && (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
6404
- === publishedTreeReviewAnchor(proof)
6405
- && event.payload.localCommit === proof.localCommit
6406
- && event.payload.remoteCommit === proof.remoteCommit
6407
- && event.payload.tree === proof.tree) {
6408
- return event;
6409
- }
6410
- }
6411
- return undefined;
6412
- }
6413
- function requirePublishedTreeAuthorization(store, proof) {
6414
- const authorization = matchingPublishedTreeAuthorization(store, proof);
6415
- if (authorization === undefined) {
6416
- throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
6417
- + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
6418
- ? "the Leader-owned Task-main head"
6419
- : `Task-final Review ${proof.reviewRoundId}`}.`);
6420
- }
6421
- return authorization;
6422
- }
6423
- function publishedTreeReviewAnchor(proof) {
6424
- return proof.reviewRoundId ?? "direct";
6425
- }
6426
6488
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
6427
6489
  function truncateEventNote(note) {
6428
6490
  const normalized = note.trim();
@@ -6565,8 +6627,8 @@ function assertTaskOpen(task) {
6565
6627
  if (task.status === "retired")
6566
6628
  throw usageError(`Task is retired: ${task.id}.`);
6567
6629
  }
6568
- function taskActor(options, taskId) {
6569
- return resolveTaskActor(options.environment, taskId);
6630
+ function taskActor(store, options, taskId) {
6631
+ return resolveTaskLocalActor(store, options.environment, taskId, options.yuiHome);
6570
6632
  }
6571
6633
  function taskRoleDispatchMode(store, taskId, roleName, sessions, agentId, effective) {
6572
6634
  return roleSessionDispatchModeWithConversationSwitch(sessions, store.listEvents(taskId), store.getWorkMailbox({ kind: "role", taskId, roleName }), roleName, agentId, effective);
@@ -6741,7 +6803,7 @@ function taskBriefCommand(args, store, options) {
6741
6803
  const task = requireTask(tx, parsed.positionals[0]);
6742
6804
  assertTaskOpen(task);
6743
6805
  const existing = tx.getTaskBrief(task.id);
6744
- const updatedBy = taskActor(options, task.id);
6806
+ const updatedBy = taskActor(tx, options, task.id);
6745
6807
  const brief = existing === null
6746
6808
  ? createTaskBrief({
6747
6809
  objective: requiredText(parsed.options.get("--objective"), "--objective"),
@@ -6792,7 +6854,7 @@ function taskDecisionCommand(args, store, options) {
6792
6854
  const result = store.transaction((tx) => {
6793
6855
  const task = requireTask(tx, parsed.positionals[0]);
6794
6856
  assertTaskOpen(task);
6795
- const actor = taskActor(options, task.id);
6857
+ const actor = taskActor(tx, options, task.id);
6796
6858
  const decision = createDecision(tx.nextDecisionId(task.id), task.id, title, rationale, now);
6797
6859
  tx.saveDecision(task.id, decision);
6798
6860
  recordTaskEvent(tx, task.id, "decision.recorded", {
@@ -6863,7 +6925,7 @@ function taskDecisionCommand(args, store, options) {
6863
6925
  const result = store.transaction((tx) => {
6864
6926
  const task = requireTask(tx, parsed.positionals[0]);
6865
6927
  assertTaskOpen(task);
6866
- const actor = taskActor(options, task.id);
6928
+ const actor = taskActor(tx, options, task.id);
6867
6929
  const existing = tx.getDecision(task.id, parsed.positionals[1]);
6868
6930
  if (existing === null)
6869
6931
  throw dataError(`Decision not found: ${parsed.positionals[1]}.`);
@@ -6901,10 +6963,8 @@ function taskMilestoneCommand(args, store, options) {
6901
6963
  const result = store.transaction((tx) => {
6902
6964
  const task = requireTask(tx, parsed.positionals[0]);
6903
6965
  assertTaskOpen(task);
6904
- if (taskActor(options, task.id) !== "leader") {
6905
- throw usageError("Only the Task Leader can add a Milestone.");
6906
- }
6907
- const milestone = createMilestone(tx.nextMilestoneId(task.id), task.id, title, summary, now);
6966
+ const actor = taskActor(tx, options, task.id);
6967
+ const milestone = createMilestone(tx.nextMilestoneId(task.id), task.id, title, summary, actor, now);
6908
6968
  tx.saveMilestone(task.id, milestone);
6909
6969
  recordTaskEvent(tx, task.id, "milestone.added", {
6910
6970
  milestoneId: milestone.id,