@zq-silk/yui 0.12.4 → 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.
@@ -30,7 +30,7 @@ import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../rev
30
30
  import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
31
31
  import { projectReviewerAvailability } from "../review/reviewerAvailability.js";
32
32
  import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
33
- import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
33
+ import { TASK_CONTROL_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
34
34
  import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
35
35
  import { createDecision, supersedeDecision } from "../decision/decision.js";
36
36
  import { createMilestone } from "../milestone/milestone.js";
@@ -42,7 +42,6 @@ import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlo
42
42
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
43
43
  import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
44
44
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
45
- import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
46
45
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
47
46
  import { assertProjectActive, resolveProject } from "../repository/project.js";
48
47
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
@@ -203,9 +202,6 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
203
202
  .filter((entry) => entry.run !== null);
204
203
  const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
205
204
  if (leaderEntry !== undefined) {
206
- if (actor !== "leader") {
207
- throw usageError(`Task ${task.id} has an active Leader run.`);
208
- }
209
205
  if (leaderEntry.run.workItemId !== undefined) {
210
206
  throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
211
207
  }
@@ -381,9 +377,7 @@ function taskProjectCommand(args, store, options) {
381
377
  const updated = store.transaction((tx) => {
382
378
  const task = requireTask(tx, parsed.positionals[0]);
383
379
  assertTaskOpen(task);
384
- if (task.status === "active" && taskActor(tx, options, task.id) !== "leader") {
385
- throw usageError("Only the Task Leader may add a Project to an active Task.");
386
- }
380
+ taskActor(tx, options, task.id);
387
381
  const project = resolveProject(tx.listProjects(), parsed.positionals[1]);
388
382
  if (project === null)
389
383
  throw usageError(`Project not found: ${parsed.positionals[1]}.`);
@@ -723,8 +717,7 @@ function completeTaskCommand(args, store, options) {
723
717
  changed: false,
724
718
  runtimeCleanupTargets: [],
725
719
  completionAdvisories: [],
726
- finalReview: undefined,
727
- publishedTreeAuthorization: undefined
720
+ finalReview: undefined
728
721
  };
729
722
  }
730
723
  const taskFinalContract = preflight.taskFinalReviewContract;
@@ -734,28 +727,6 @@ function completeTaskCommand(args, store, options) {
734
727
  const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
735
728
  ? undefined
736
729
  : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
737
- const requiresContractHandoff = publishedTreeProof !== undefined
738
- && taskFinalContract !== undefined;
739
- if (requiresContractHandoff && actor !== "leader") {
740
- const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
741
- const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
742
- enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
743
- return {
744
- task,
745
- changed: false,
746
- runtimeCleanupTargets: [],
747
- completionAdvisories: [],
748
- finalReview: undefined,
749
- publishedTreeAuthorization: {
750
- event,
751
- proof: publishedTreeProof,
752
- created: existing === undefined
753
- }
754
- };
755
- }
756
- if (publishedTreeProof !== undefined && actor === "leader") {
757
- requirePublishedTreeAuthorization(tx, publishedTreeProof);
758
- }
759
730
  const roles = tx.listRoles(task.id);
760
731
  const activeRuns = roles
761
732
  .map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
@@ -766,9 +737,6 @@ function completeTaskCommand(args, store, options) {
766
737
  // the post-Review readiness fence below.
767
738
  const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
768
739
  if (leaderEntry !== undefined) {
769
- if (actor !== "leader") {
770
- throw usageError(`Task ${task.id} has an active Leader run.`);
771
- }
772
740
  if (leaderEntry.run.workItemId !== undefined) {
773
741
  throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
774
742
  }
@@ -799,7 +767,6 @@ function completeTaskCommand(args, store, options) {
799
767
  runtimeCleanupTargets: [],
800
768
  completionAdvisories: [],
801
769
  finalReview,
802
- publishedTreeAuthorization: undefined
803
770
  };
804
771
  }
805
772
  // Issue 06: re-validate the full completion readiness inside the
@@ -863,21 +830,9 @@ function completeTaskCommand(args, store, options) {
863
830
  changed: true,
864
831
  runtimeCleanupTargets,
865
832
  completionAdvisories: readiness.advisories,
866
- finalReview: undefined,
867
- publishedTreeAuthorization: undefined
833
+ finalReview: undefined
868
834
  };
869
835
  });
870
- if (result.publishedTreeAuthorization !== undefined) {
871
- notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
872
- const authorization = result.publishedTreeAuthorization;
873
- return output(authorization.created
874
- ? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
875
- : `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
876
- task: result.task,
877
- authorizationEvent: authorization.event,
878
- publishedTreeProof: authorization.proof
879
- });
880
- }
881
836
  if (result.changed) {
882
837
  for (const target of result.runtimeCleanupTargets) {
883
838
  // Cleanup owns an independent lifecycle lane. Never fall back to the
@@ -1147,10 +1102,7 @@ export function parseTaskArchiveArguments(args) {
1147
1102
  export function validateTaskArchiveRequest(args, store, options = {}) {
1148
1103
  const request = parseTaskArchiveArguments(args);
1149
1104
  const task = requireTask(store, request.taskId);
1150
- const actor = taskActor(store, options, task.id);
1151
- if (actor === "leader") {
1152
- throw usageError("Only the global Operator may archive a Task from a managed Session.");
1153
- }
1105
+ taskActor(store, options, task.id);
1154
1106
  if (task.status !== "archived"
1155
1107
  && task.status !== "completed"
1156
1108
  && task.status !== "retired") {
@@ -1954,9 +1906,7 @@ function updateWorkScope(args, store, options) {
1954
1906
  const updated = store.transaction((tx) => {
1955
1907
  const item = requireWorkItem(tx, parsed.positionals[0], options);
1956
1908
  const task = requireTask(tx, item.taskId);
1957
- if (taskActor(tx, options, task.id) !== "leader") {
1958
- throw usageError("Only the Task Leader may change a Work Item Project scope.");
1959
- }
1909
+ taskActor(tx, options, task.id);
1960
1910
  if (tx.getActiveAgentRun(task.id, item.assignee ?? "") !== null) {
1961
1911
  throw usageError(`Stop the active Work Item Run before changing scope: ${item.id}.`);
1962
1912
  }
@@ -2004,9 +1954,7 @@ function updateWork(args, store, options) {
2004
1954
  const task = requireTask(tx, current.taskId);
2005
1955
  assertTaskOpen(task);
2006
1956
  if (current.assignee === undefined) {
2007
- if (taskActor(tx, options, task.id) !== "leader") {
2008
- throw usageError(`Only the Task Leader may update unassigned Work Item execution: ${current.id}.`);
2009
- }
1957
+ taskActor(tx, options, task.id);
2010
1958
  if (status === "running") {
2011
1959
  assertWorkItemDependenciesCompleted(tx, current);
2012
1960
  }
@@ -2199,9 +2147,8 @@ function dispatchWork(args, store, options) {
2199
2147
  + `The Task Leader must run "yui task work update ${item.id} running", `
2200
2148
  + "then execute it directly or create native subagents in the Leader Session.");
2201
2149
  }
2202
- if (expanding && taskActor(tx, options, task.id) !== "leader") {
2203
- throw usageError("Only the Task Leader may expand a running ExecutionGroup.");
2204
- }
2150
+ if (expanding)
2151
+ taskActor(tx, options, task.id);
2205
2152
  assertWorkItemDependenciesCompleted(tx, item);
2206
2153
  const workspace = tx.getWorkItemWorkspace(task.id, item.id);
2207
2154
  if (workspace?.owner.type === "work-item"
@@ -2581,9 +2528,7 @@ function resolveWorkExecutionGroup(args, store, options) {
2581
2528
  const task = requireTask(tx, item.taskId);
2582
2529
  if (task.status !== "active")
2583
2530
  throw usageError(inactiveTaskMessage(task, "resolving an ExecutionGroup"));
2584
- if (taskActor(tx, options, task.id) !== "leader") {
2585
- throw usageError("Only the Task Leader may resolve an ExecutionGroup.");
2586
- }
2531
+ taskActor(tx, options, task.id);
2587
2532
  let group = currentWorkItemExecutionGroup(item);
2588
2533
  if (group === undefined
2589
2534
  || (group.stage === undefined
@@ -2815,9 +2760,7 @@ function acceptWork(args, store, options) {
2815
2760
  if (task.status !== "active") {
2816
2761
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2817
2762
  }
2818
- if (taskActor(tx, options, task.id) !== "leader") {
2819
- throw usageError("Only the Task Leader may accept a Work Item.");
2820
- }
2763
+ const actor = taskActor(tx, options, task.id);
2821
2764
  if (item.status !== "awaiting_acceptance") {
2822
2765
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
2823
2766
  }
@@ -2876,9 +2819,9 @@ function acceptWork(args, store, options) {
2876
2819
  ...(candidate.source.type === "run"
2877
2820
  ? { runId: candidate.source.runId }
2878
2821
  : { workItemRevision: String(candidate.workItemRevision) }),
2879
- acceptedBy: "leader",
2822
+ acceptedBy: actor,
2880
2823
  summary,
2881
- ...leaderActionEventPayload(tx, item.taskId, options)
2824
+ ...(actor === "leader" ? leaderActionEventPayload(tx, item.taskId, options) : {})
2882
2825
  }, now);
2883
2826
  return completed;
2884
2827
  });
@@ -2938,9 +2881,7 @@ function rejectWork(args, store, options) {
2938
2881
  if (task.status !== "active") {
2939
2882
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
2940
2883
  }
2941
- if (taskActor(tx, options, task.id) !== "leader") {
2942
- throw usageError("Only the Task Leader may reject a Work Item.");
2943
- }
2884
+ const actor = taskActor(tx, options, task.id);
2944
2885
  if (item.status !== "awaiting_acceptance") {
2945
2886
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
2946
2887
  }
@@ -2954,7 +2895,7 @@ function rejectWork(args, store, options) {
2954
2895
  recordTaskEvent(tx, item.taskId, "work.rejected", {
2955
2896
  workItemId: item.id,
2956
2897
  candidateId: candidate.id,
2957
- rejectedBy: "leader",
2898
+ rejectedBy: actor,
2958
2899
  summary
2959
2900
  }, now);
2960
2901
  return failed;
@@ -3097,9 +3038,7 @@ function reviewWork(args, store, options) {
3097
3038
  if (task.status !== "active") {
3098
3039
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
3099
3040
  }
3100
- if (taskActor(tx, options, task.id) !== "leader") {
3101
- throw usageError("Only the Task Leader may request a Work Item review.");
3102
- }
3041
+ const requestedBy = taskActor(tx, options, task.id);
3103
3042
  if (item.status !== "awaiting_acceptance") {
3104
3043
  throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
3105
3044
  }
@@ -3126,7 +3065,7 @@ function reviewWork(args, store, options) {
3126
3065
  }
3127
3066
  throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
3128
3067
  }
3129
- const queued = queueReviewRound(tx, item, config, "leader", now);
3068
+ const queued = queueReviewRound(tx, item, config, requestedBy, now);
3130
3069
  return { ...queued, resumed: false };
3131
3070
  });
3132
3071
  if (result.run !== null) {
@@ -3140,7 +3079,7 @@ function reviewWork(args, store, options) {
3140
3079
  : output(`Review requested as ${result.round.id}\n`, { reviewRound: result.round });
3141
3080
  }
3142
3081
  /**
3143
- * Leader-controlled recovery for a failed Task-final ReviewRound that never
3082
+ * Task-control recovery for a failed Task-final ReviewRound that never
3144
3083
  * created a Reviewer Run. This is deliberately separate from `task run retry`:
3145
3084
  * that command requires an exact failed Agent Run and remains the only retry
3146
3085
  * path for a failed provider execution. Here the old terminal Round is an
@@ -3290,9 +3229,7 @@ function resolveReviewExecutionGroup(args, store, options) {
3290
3229
  const task = requireTask(tx, round.taskId);
3291
3230
  if (task.status !== "active")
3292
3231
  throw usageError(inactiveTaskMessage(task, "resolving a Review ExecutionGroup"));
3293
- if (taskActor(tx, options, task.id) !== "leader") {
3294
- throw usageError("Only the Task Leader may resolve a Reviewer ExecutionGroup.");
3295
- }
3232
+ taskActor(tx, options, task.id);
3296
3233
  const group = round.executionGroup;
3297
3234
  if (group === undefined
3298
3235
  || (group.lanes.length < 2 && group.strategy.mode !== "adaptive")) {
@@ -3411,7 +3348,7 @@ function disposeReviewFindingCommand(args, store, options) {
3411
3348
  const parsed = parseTail(args, new Set(["--disposition", "--work-item", "--commit", "--verification", "--note", "--superseded-by"]), usage);
3412
3349
  exactPositionals(parsed.positionals, 1, usage);
3413
3350
  const disposition = requiredOption(parsed.options, "--disposition");
3414
- if (!LEADER_FINDING_DISPOSITIONS.includes(disposition)) {
3351
+ if (!TASK_CONTROL_FINDING_DISPOSITIONS.includes(disposition)) {
3415
3352
  throw usageError(`Review finding disposition is invalid: ${disposition}.`);
3416
3353
  }
3417
3354
  const now = clock(options);
@@ -3423,12 +3360,12 @@ function disposeReviewFindingCommand(args, store, options) {
3423
3360
  const task = requireTask(tx, reference.taskId);
3424
3361
  if (task.status !== "active")
3425
3362
  throw usageError(inactiveTaskMessage(task, "dispositioning a review finding"));
3426
- if (taskActor(tx, options, task.id) !== "leader") {
3427
- throw usageError("Only the Task Leader may disposition a review finding.");
3428
- }
3363
+ const actor = taskActor(tx, options, task.id);
3429
3364
  const command = {
3430
3365
  disposition,
3431
- 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,
3432
3369
  ...(parsed.options.get("--note") === undefined ? {} : { note: parsed.options.get("--note") }),
3433
3370
  ...(parsed.options.get("--work-item") === undefined ? {} : { workItemId: parsed.options.get("--work-item") }),
3434
3371
  ...(parsed.options.get("--commit") === undefined ? {} : { commit: parsed.options.get("--commit") }),
@@ -3460,9 +3397,7 @@ function planReviewRepairWave(args, store, options) {
3460
3397
  if (currentTask.status !== "active") {
3461
3398
  throw usageError(inactiveTaskMessage(currentTask, "creating a review repair wave"));
3462
3399
  }
3463
- if (taskActor(tx, options, currentTask.id) !== "leader") {
3464
- throw usageError("Only the Task Leader may create a review repair wave.");
3465
- }
3400
+ taskActor(tx, options, currentTask.id);
3466
3401
  const openItems = tx.listWorkItems(currentTask.id)
3467
3402
  .filter((item) => item.status === "pending" || item.status === "running");
3468
3403
  return groups.map((group) => {
@@ -3561,9 +3496,7 @@ function requestTaskReviewRound(args, store, options) {
3561
3496
  const task = requireTask(tx, parsed.positionals[0]);
3562
3497
  if (task.status !== "active")
3563
3498
  throw usageError(`Task is not active: ${task.id}.`);
3564
- if (taskActor(tx, options, task.id) !== "leader") {
3565
- throw usageError("Only the Task Leader may request a Task-final Review.");
3566
- }
3499
+ const requestedBy = taskActor(tx, options, task.id);
3567
3500
  if (task.projectBindings.length === 0) {
3568
3501
  throw usageError(`Task ${task.id} has no bound Projects for a Task-final Review.`);
3569
3502
  }
@@ -3715,8 +3648,8 @@ function requestTaskReviewRound(args, store, options) {
3715
3648
  deltaRecord = validateDeltaRecheckRequest(tx, task.id, provenance.candidate, options.deltaRecheckPreflight);
3716
3649
  }
3717
3650
  let created = deltaRecord === undefined
3718
- ? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
3719
- : 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);
3720
3653
  let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3721
3654
  purpose: "review",
3722
3655
  target: executionTargetForReviewRound(task, created),
@@ -3792,9 +3725,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3792
3725
  const task = requireTask(tx, reference.taskId);
3793
3726
  if (task.status !== "active")
3794
3727
  throw usageError(`Task is not active: ${task.id}.`);
3795
- if (taskActor(tx, options, task.id) !== "leader") {
3796
- throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
3797
- }
3728
+ const requestedBy = taskActor(tx, options, task.id);
3798
3729
  if ((source.scope ?? "work-item") !== "task") {
3799
3730
  throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
3800
3731
  }
@@ -3868,7 +3799,7 @@ function forceFreshTaskReviewRound(args, store, options) {
3868
3799
  reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
3869
3800
  tx.saveRole(task.id, reviewer);
3870
3801
  }
3871
- 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);
3872
3803
  const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
3873
3804
  purpose: "review",
3874
3805
  target: executionTargetForReviewRound(task, created),
@@ -3884,7 +3815,8 @@ function forceFreshTaskReviewRound(args, store, options) {
3884
3815
  reviewerRoleName: created.reviewerRoleName,
3885
3816
  taskCandidate: JSON.stringify(created.taskCandidate),
3886
3817
  reason: "source-round-terminal-without-semantic-review",
3887
- leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
3818
+ requestedBy,
3819
+ ...(requestedBy === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
3888
3820
  }, now);
3889
3821
  return { round: created, source, created: true };
3890
3822
  });
@@ -4004,9 +3936,7 @@ function retryFailedTaskReviewRound(args, store, options) {
4004
3936
  const task = requireTask(tx, reference.taskId);
4005
3937
  if (task.status !== "active")
4006
3938
  throw usageError(`Task is not active: ${task.id}.`);
4007
- if (taskActor(tx, options, task.id) !== "leader") {
4008
- throw usageError("Only the Task Leader may retry a failed Task-final ReviewRound.");
4009
- }
3939
+ const requestedBy = taskActor(tx, options, task.id);
4010
3940
  if ((round.scope ?? "work-item") !== "task") {
4011
3941
  throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
4012
3942
  }
@@ -4088,7 +4018,7 @@ function retryFailedTaskReviewRound(args, store, options) {
4088
4018
  // Issue 06: infra retry resets the same semantic Round to pending instead
4089
4019
  // of manufacturing a new Round, so Round count and finding identity stay
4090
4020
  // stable across execution-attempt failures.
4091
- const resetRound = retryTaskReviewRound(round);
4021
+ const resetRound = retryTaskReviewRound(round, requestedBy);
4092
4022
  tx.saveReviewRound(task.id, resetRound);
4093
4023
  recordTaskEvent(tx, task.id, "review.task-final-retried", {
4094
4024
  reviewRoundId: round.id
@@ -4350,9 +4280,7 @@ function settleStaleFinalReviewRun(args, store, options) {
4350
4280
  const task = requireTask(tx, run.taskId);
4351
4281
  if (task.status !== "active")
4352
4282
  throw usageError(`Task is not active: ${task.id}.`);
4353
- if (taskActor(tx, options, task.id) !== "leader") {
4354
- throw usageError("Only the Task Leader may settle a stale final review Run.");
4355
- }
4283
+ const actor = taskActor(tx, options, task.id);
4356
4284
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
4357
4285
  if (round === null) {
4358
4286
  throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
@@ -4461,7 +4389,9 @@ function settleStaleFinalReviewRun(args, store, options) {
4461
4389
  runId: run.id,
4462
4390
  reviewRoundId: round.id,
4463
4391
  previousTaskCandidate: JSON.stringify(round.taskCandidate),
4464
- currentTaskCandidate: JSON.stringify(currentTaskCandidate)
4392
+ currentTaskCandidate: JSON.stringify(currentTaskCandidate),
4393
+ settledBy: actor,
4394
+ ...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
4465
4395
  }, now);
4466
4396
  return { run, round: terminal, changed: true };
4467
4397
  });
@@ -5012,7 +4942,7 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
5012
4942
  }
5013
4943
  }
5014
4944
  /**
5015
- * 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
5016
4946
  * Run remains the attempt trail, while the semantic ReviewRound is reset to
5017
4947
  * pending under its existing identity. Every identity and frozen-head fence is
5018
4948
  * checked inside one transaction so a partial fail-old-without-reset state can
@@ -5039,9 +4969,7 @@ function retryFailedReviewRun(previous, store, options, now) {
5039
4969
  throw usageError(`Review Run ${run.id} still owns an unsettled Provider continuation; `
5040
4970
  + "reconcile it before retrying the Lane.");
5041
4971
  }
5042
- if (taskActor(tx, options, task.id) !== "leader") {
5043
- throw usageError("Only the Task Leader may retry a failed final review Run.");
5044
- }
4972
+ const requestedBy = taskActor(tx, options, task.id);
5045
4973
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
5046
4974
  if (round === null) {
5047
4975
  throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
@@ -5221,7 +5149,7 @@ function retryFailedReviewRun(previous, store, options, now) {
5221
5149
  // Issue 06: infra retry resets the same semantic Round to pending instead
5222
5150
  // of manufacturing a new Round, so Round count and finding identity stay
5223
5151
  // stable across execution-attempt failures.
5224
- const resetRound = retryTaskReviewRound(roundToReset, run.executionLaneId);
5152
+ const resetRound = retryTaskReviewRound(roundToReset, requestedBy, run.executionLaneId);
5225
5153
  tx.saveReviewRound(task.id, resetRound);
5226
5154
  recordTaskEvent(tx, task.id, "run.review-retried", {
5227
5155
  runId: run.id,
@@ -5266,9 +5194,7 @@ function recoverRun(args, store, options) {
5266
5194
  const task = requireTask(tx, active.taskId);
5267
5195
  if (task.status !== "active")
5268
5196
  throw usageError(inactiveTaskMessage(task, "recovering a run"));
5269
- if (taskActor(tx, options, task.id) !== "leader") {
5270
- throw usageError("Only the Task Leader may control exact Agent Run recovery.");
5271
- }
5197
+ taskActor(tx, options, task.id);
5272
5198
  const roleName = parsed.options.get("--role") ?? active.roleName;
5273
5199
  if (roleName !== active.roleName) {
5274
5200
  throw usageError(`Recovery Role does not match Run ${active.id}: ${roleName}.`);
@@ -6559,52 +6485,6 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
6559
6485
  store.saveEvent(taskId, event);
6560
6486
  return event;
6561
6487
  }
6562
- function publishedTreeAuthorizationPayload(actor, proof) {
6563
- return {
6564
- by: actor,
6565
- projectId: proof.projectId,
6566
- publicationId: proof.publicationId,
6567
- ...(proof.reviewRoundId === undefined
6568
- ? { reviewAnchor: publishedTreeReviewAnchor(proof) }
6569
- : { reviewRoundId: proof.reviewRoundId }),
6570
- localCommit: proof.localCommit,
6571
- remoteCommit: proof.remoteCommit,
6572
- tree: proof.tree
6573
- };
6574
- }
6575
- function matchingPublishedTreeAuthorization(store, proof) {
6576
- const events = store.listEvents(proof.taskId);
6577
- for (let index = events.length - 1; index >= 0; index -= 1) {
6578
- const event = events[index];
6579
- if (event.type === "task.completed" || event.type === "task.reopened")
6580
- return undefined;
6581
- if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
6582
- && (event.payload.by === "user" || event.payload.by === "operator")
6583
- && event.payload.projectId === proof.projectId
6584
- && event.payload.publicationId === proof.publicationId
6585
- && (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
6586
- === publishedTreeReviewAnchor(proof)
6587
- && event.payload.localCommit === proof.localCommit
6588
- && event.payload.remoteCommit === proof.remoteCommit
6589
- && event.payload.tree === proof.tree) {
6590
- return event;
6591
- }
6592
- }
6593
- return undefined;
6594
- }
6595
- function requirePublishedTreeAuthorization(store, proof) {
6596
- const authorization = matchingPublishedTreeAuthorization(store, proof);
6597
- if (authorization === undefined) {
6598
- throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
6599
- + `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
6600
- ? "the Leader-owned Task-main head"
6601
- : `Task-final Review ${proof.reviewRoundId}`}.`);
6602
- }
6603
- return authorization;
6604
- }
6605
- function publishedTreeReviewAnchor(proof) {
6606
- return proof.reviewRoundId ?? "direct";
6607
- }
6608
6488
  /** Keeps a free-text run-fact note bounded so an event payload stays compact. */
6609
6489
  function truncateEventNote(note) {
6610
6490
  const normalized = note.trim();
@@ -7083,10 +6963,8 @@ function taskMilestoneCommand(args, store, options) {
7083
6963
  const result = store.transaction((tx) => {
7084
6964
  const task = requireTask(tx, parsed.positionals[0]);
7085
6965
  assertTaskOpen(task);
7086
- if (taskActor(tx, options, task.id) !== "leader") {
7087
- throw usageError("Only the Task Leader can add a Milestone.");
7088
- }
7089
- 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);
7090
6968
  tx.saveMilestone(task.id, milestone);
7091
6969
  recordTaskEvent(tx, task.id, "milestone.added", {
7092
6970
  milestoneId: milestone.id,
@@ -183,7 +183,7 @@ async function runIntegration(store, home, integration, now, options) {
183
183
  const output = result.status === "committed"
184
184
  ? `Integrated ${result.attempt.changeSetIds.join(", ")} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
185
185
  : result.status === "blocked"
186
- ? `Integration ${result.attempt.id} requires a Leader resolution decision in ${result.workspace.path}\n`
186
+ ? `Integration ${result.attempt.id} requires a Task Agent resolution decision in ${result.workspace.path}\n`
187
187
  : result.status === "checks-running"
188
188
  ? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
189
189
  : `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
@@ -201,20 +201,18 @@ function resolveDecision(args, store, now, environment, home) {
201
201
  const resolved = store.transaction((tx) => {
202
202
  const integration = requireIntegration(tx, parsed.positionals[0], environment);
203
203
  const task = requireActiveIntegrationTask(tx, integration);
204
- if (taskLocalActor(tx, environment, task.id, home) !== "leader") {
205
- throw usageError("Only the Task Leader can resolve an Integration conflict.");
206
- }
204
+ const actor = taskLocalActor(tx, environment, task.id, home);
207
205
  const updated = recordResolutionDecision(integration, {
208
206
  action: selectedOption,
209
207
  rationale
210
- }, now());
208
+ }, actor, now());
211
209
  tx.saveIntegrationAttempt(updated.taskId, updated);
212
210
  return updated;
213
211
  });
214
212
  return {
215
213
  output: selectedOption === "manual-resolution"
216
- ? `Leader selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
217
- : `Leader rejected Integration ${resolved.id}\n`,
214
+ ? `Selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
215
+ : `Rejected Integration ${resolved.id}\n`,
218
216
  data: { integration: resolved }
219
217
  };
220
218
  }
@@ -275,8 +273,8 @@ function supersedeIntegrationCommand(args, store, now, environment, home) {
275
273
  const reason = parsed.one.get("--reason");
276
274
  if (reason === undefined)
277
275
  throw usageError(usage);
278
- // Only the Task Leader (or Operator/user) may supersede a committed
279
- // Integration: it rewrites delivery-baseline evidence and audit history.
276
+ // Superseding a committed Integration rewrites delivery-baseline evidence
277
+ // and audit history, so it remains an explicit Task-control decision.
280
278
  taskLocalActor(store, environment, integration.taskId, home);
281
279
  // A queue-backed committed Attempt cannot be superseded: the queue entry
282
280
  // would remain in its current status while its Attempt becomes "superseded",
@@ -37,7 +37,7 @@ async function enqueue(args, store, options, home) {
37
37
  if (parsed.positionals.length !== 1)
38
38
  throw usageError(usage);
39
39
  const task = requireActiveTask(store, parsed.positionals[0]);
40
- requireLeader(store, options.environment, task.id, home);
40
+ requireTaskControlActor(store, options.environment, task.id, home);
41
41
  const projectRef = parsed.one.get("--project");
42
42
  const changeSetId = parsed.one.get("--change-set");
43
43
  if (projectRef === undefined || changeSetId === undefined)
@@ -120,7 +120,7 @@ async function process(args, store, home, options) {
120
120
  if (parsed.positionals.length !== 1)
121
121
  throw usageError(usage);
122
122
  const task = requireActiveTask(store, parsed.positionals[0]);
123
- requireLeader(store, options.environment, task.id, home);
123
+ requireTaskControlActor(store, options.environment, task.id, home);
124
124
  const limitValue = parsed.one.get("--limit");
125
125
  const limit = limitValue === undefined ? undefined : Number.parseInt(limitValue, 10);
126
126
  if (limitValue !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
@@ -154,11 +154,11 @@ function supersede(args, store, options, home) {
154
154
  if (parsed.positionals.length !== 1)
155
155
  throw usageError(usage);
156
156
  const entry = requireQueueEntry(store, parsed.positionals[0], options.environment);
157
- requireLeader(store, options.environment, entry.taskId, home);
157
+ const actor = requireTaskControlActor(store, options.environment, entry.taskId, home);
158
158
  const reason = parsed.one.get("--reason");
159
159
  if (reason === undefined)
160
160
  throw usageError(usage);
161
- const superseded = supersedeIntegrationQueueEntry(store, entry.taskId, entry.id, reason, options.now ?? (() => new Date()));
161
+ const superseded = supersedeIntegrationQueueEntry(store, entry.taskId, entry.id, reason, actor, options.now ?? (() => new Date()));
162
162
  return {
163
163
  output: `Superseded ${superseded.id}: ${reason}\n`,
164
164
  data: { entry: superseded }
@@ -169,8 +169,8 @@ function requeue(args, store, options, home) {
169
169
  if (args.length !== 1)
170
170
  throw usageError(usage);
171
171
  const entry = requireQueueEntry(store, args[0], options.environment);
172
- requireLeader(store, options.environment, entry.taskId, home);
173
- const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, options.now ?? (() => new Date()));
172
+ const actor = requireTaskControlActor(store, options.environment, entry.taskId, home);
173
+ const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, actor, options.now ?? (() => new Date()));
174
174
  return {
175
175
  output: `Requeued ${waiting.id}; it will be processed again on the next queue run\n`,
176
176
  data: { entry: waiting }
@@ -181,7 +181,7 @@ async function reconcile(args, store, options, home) {
181
181
  if (args.length !== 1)
182
182
  throw usageError(usage);
183
183
  const entry = requireQueueEntry(store, args[0], options.environment);
184
- requireLeader(store, options.environment, entry.taskId, home);
184
+ requireTaskControlActor(store, options.environment, entry.taskId, home);
185
185
  const committed = await reconcileIntegrationQueueEntry(store, entry.taskId, entry.id, new NodeGitWorkspace(), options.now ?? (() => new Date()));
186
186
  return {
187
187
  output: `Reconciled ${committed.id} as committed -> ${committed.targetAfter ?? "-"}\n`,
@@ -221,8 +221,6 @@ function requireQueueEntry(store, value, environment) {
221
221
  }
222
222
  return entry;
223
223
  }
224
- function requireLeader(store, environment, taskId, home) {
225
- if (taskLocalActor(store, environment, taskId, home) !== "leader") {
226
- throw usageError("Only the Task Leader can change the integration queue.");
227
- }
224
+ function requireTaskControlActor(store, environment, taskId, home) {
225
+ return taskLocalActor(store, environment, taskId, home);
228
226
  }
@@ -34,10 +34,6 @@ function addPublication(args, store, options) {
34
34
  throw usageError(`Project is not bound to Task ${task.id}: ${projectReference}.`, usage);
35
35
  }
36
36
  const actor = taskActor(options.environment, task.id);
37
- if (actor === "leader") {
38
- throw usageError("A managed Task Leader cannot record external publication evidence; "
39
- + "use a user or global Operator session.", usage);
40
- }
41
37
  const state = enumOption(parsed.options, "--state", STATES, usage) ?? "open";
42
38
  const verification = parseVerification(parsed, usage);
43
39
  const input = {
@@ -135,7 +135,7 @@ function replaceTaskCommand(args, store, options) {
135
135
  }
136
136
  const now = options.now?.() ?? new Date();
137
137
  const milestone = createMilestone(store.nextMilestoneId(replacement.id), replacement.id, `Replaces ${old.id}`, `Replacement Task for ${old.id} (${old.status}); the original Task, its refs, `
138
- + "and its evidence are preserved and were not rewritten.", now);
138
+ + "and its evidence are preserved and were not rewritten.", "user", now);
139
139
  store.saveMilestone(replacement.id, milestone);
140
140
  return {
141
141
  output: `${created.output}\nRecorded replacement of ${old.id} on ${replacement.id} `
@@ -68,16 +68,13 @@ function parseJobResult(value) {
68
68
  * list/show paths do not use this port and stay lean.
69
69
  */
70
70
  export function createControllerIntegrationJobPort(home, clientOptions = {}) {
71
- const { store, ...controllerOptions } = clientOptions;
72
71
  return {
73
72
  async startCheckJob(input) {
74
73
  const owner = {
75
74
  kind: "integration-attempt",
76
75
  integrationAttemptId: input.integrationId
77
76
  };
78
- // rr8/rr12: Bind the Integration-issued job to the caller's managed
79
- // identity. A user-scope caller carries a verified Leader assertion.
80
- const caller = resolveJobCaller(controllerOptions.environment, input.taskId, store);
77
+ const caller = resolveJobCaller(clientOptions.environment, input.taskId);
81
78
  const { job } = await startDurableJob(home, {
82
79
  taskId: input.taskId,
83
80
  owner,
@@ -87,16 +84,16 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
87
84
  env: input.env,
88
85
  steps: input.steps,
89
86
  caller
90
- }, controllerOptions);
87
+ }, clientOptions);
91
88
  return job;
92
89
  },
93
90
  async getJob(taskId, jobId) {
94
- return getDurableJob(home, taskId, jobId, controllerOptions);
91
+ return getDurableJob(home, taskId, jobId, clientOptions);
95
92
  },
96
93
  async cancelJob(taskId, jobId) {
97
94
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
98
- const caller = resolveJobCaller(controllerOptions.environment, taskId, store);
99
- await cancelDurableJob(home, taskId, jobId, caller, controllerOptions);
95
+ const caller = resolveJobCaller(clientOptions.environment, taskId);
96
+ await cancelDurableJob(home, taskId, jobId, caller, clientOptions);
100
97
  }
101
98
  };
102
99
  }