@zq-silk/yui 0.5.3 → 0.6.1
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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +35 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +610 -201
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +24 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +85 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +60 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +413 -61
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/resourceInventoryRpc.js +85 -0
- package/dist/controller/resourceInventoryWorker.js +50 -0
- package/dist/controller/runtime.js +238 -22
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +549 -42
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/boundedRpc.js +475 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/setup/setupCommand.js +27 -4
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +769 -1
- package/dist/storage/persistenceWorker.js +194 -0
- package/dist/storage/sqliteSchema.js +705 -0
- package/dist/storage/sqliteStore.js +1695 -0
- package/dist/storage/storageVersions.js +9 -2
- package/dist/storage/storeRpc.js +298 -0
- package/dist/storage/taskStore.js +982 -21
- package/dist/storage/upgrade/homeClassification.js +157 -12
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +343 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +2 -1
|
@@ -15,7 +15,11 @@ import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRo
|
|
|
15
15
|
import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
|
|
16
16
|
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
|
|
17
17
|
import { createAgentRun } from "../run/agentRun.js";
|
|
18
|
-
import {
|
|
18
|
+
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
19
|
+
import { providerRetryConfig } from "../run/providerRetryConfig.js";
|
|
20
|
+
import { createReviewRound, createTaskReviewRound, attachReviewExecutionGroup, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
21
|
+
import { blockingOpenFindings, buildTaskFinalReviewFindingContext, completionGateBlocked, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview, reviewFindingLedgerWriteFailed } from "../review/reviewFindingLedger.js";
|
|
22
|
+
import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
|
|
19
23
|
import { markYuiRunInput, retagYuiRunInput } from "../run/runIdentity.js";
|
|
20
24
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
21
25
|
import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
@@ -26,6 +30,7 @@ import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runt
|
|
|
26
30
|
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
27
31
|
import { formatAgentRunReceiptId, resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
28
32
|
import { resolveProject } from "../repository/project.js";
|
|
33
|
+
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
29
34
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
30
35
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
31
36
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
@@ -34,10 +39,17 @@ import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOpt
|
|
|
34
39
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
35
40
|
import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
36
41
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
42
|
+
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
43
|
+
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
37
44
|
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
38
45
|
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
46
|
+
import { runGrantCommand } from "./grantCommands.js";
|
|
47
|
+
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
39
48
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
40
49
|
import { createTaskTerminalNotification } from "../scheduler/operatorNotification.js";
|
|
50
|
+
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
51
|
+
import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition } from "../scheduler/actionability.js";
|
|
52
|
+
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
41
53
|
import { buildTaskOverview, parseTaskListOptions, renderTaskOverview } from "./taskOverviewCommand.js";
|
|
42
54
|
const LEADER_ROLE = "leader";
|
|
43
55
|
/** Exact Task-final identity or evidence changed between queueing and dispatch. */
|
|
@@ -140,6 +152,12 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
140
152
|
if (incompleteWork !== undefined) {
|
|
141
153
|
throw usageError(`Task ${task.id} has unaccepted work: ${incompleteWork.id}/${incompleteWork.status}.`);
|
|
142
154
|
}
|
|
155
|
+
const activeJob = store.listDurableJobs(task.id).find((job) => (job.status === "queued"
|
|
156
|
+
|| job.status === "running"
|
|
157
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
158
|
+
if (activeJob !== undefined) {
|
|
159
|
+
throw usageError(`Task ${task.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
160
|
+
}
|
|
143
161
|
if (task.requireIntegration) {
|
|
144
162
|
if (store.listWorkItems(task.id).length === 0) {
|
|
145
163
|
throw usageError(`Task ${task.id} requires at least one WorkItem before completion.`);
|
|
@@ -157,6 +175,11 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
157
175
|
if (unresolvedIntegration !== undefined) {
|
|
158
176
|
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
159
177
|
}
|
|
178
|
+
const unsettledQueueEntry = store.listIntegrationQueueEntries(task.id).find((entry) => (entry.status !== "committed" && entry.status !== "superseded"));
|
|
179
|
+
if (unsettledQueueEntry !== undefined) {
|
|
180
|
+
throw usageError(`Task ${task.id} has an unsettled integration queue entry: `
|
|
181
|
+
+ `${unsettledQueueEntry.id}/${unsettledQueueEntry.status}.`);
|
|
182
|
+
}
|
|
160
183
|
const isolatedWorkspace = store.listManagedWorkspaces(task.id)
|
|
161
184
|
.find(({ owner }) => owner.type === "work-item");
|
|
162
185
|
if (isolatedWorkspace?.owner.type === "work-item") {
|
|
@@ -204,6 +227,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
204
227
|
case "list": return listTaskCommand(rest, store);
|
|
205
228
|
case "show": return showTaskCommand(rest, store);
|
|
206
229
|
case "context": return runTaskContextCommand(rest, store);
|
|
230
|
+
case "next-action": return runTaskNextActionCommand(rest, store);
|
|
207
231
|
case "activate": return output(activateTaskCommand(rest, store, options));
|
|
208
232
|
case "complete": return completeTaskCommand(rest, store, options);
|
|
209
233
|
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
@@ -211,8 +235,11 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
211
235
|
case "retire": return retireTaskCommand(rest, store, options);
|
|
212
236
|
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
213
237
|
case "message": return output(taskMessageCommand(rest, store, options));
|
|
238
|
+
case "wake": return output(taskWakeCommand(rest, store, options));
|
|
214
239
|
case "project": return taskProjectCommand(rest, store, options);
|
|
215
240
|
case "input": return runTaskInputCommand(rest, store, options);
|
|
241
|
+
case "grant": return runGrantCommand(rest, store, options);
|
|
242
|
+
case "workflow": return runWorkflowCommand(rest, store, options);
|
|
216
243
|
case "role": return taskRoleCommand(rest, store, options);
|
|
217
244
|
case "work": return taskWorkCommand(rest, store, options);
|
|
218
245
|
case "review": return taskReviewCommand(rest, store, options);
|
|
@@ -691,6 +718,22 @@ function completeTaskCommand(args, store, options) {
|
|
|
691
718
|
terminalizedLeaderRun
|
|
692
719
|
};
|
|
693
720
|
}
|
|
721
|
+
// Issue 06: under `review.findingLedger=enforce`, Task completion fails
|
|
722
|
+
// closed on undispositioned open P1/P2 findings. Run this after final-review
|
|
723
|
+
// preparation so `fixed-pending-review` findings can still request the one
|
|
724
|
+
// re-review for a changed frozen head; once no Review is needed, the gate
|
|
725
|
+
// blocks final completion.
|
|
726
|
+
if (completionGateBlocked(tx, task.id)) {
|
|
727
|
+
const blocking = blockingOpenFindings(tx, task.id);
|
|
728
|
+
if (reviewFindingLedgerWriteFailed(tx, task.id)) {
|
|
729
|
+
throw usageError(`Task ${task.id} cannot complete: the Review finding ledger was unavailable `
|
|
730
|
+
+ "while reconciling a semantic Review. Recover the ledger and reconcile the Round "
|
|
731
|
+
+ "before completing the Task.");
|
|
732
|
+
}
|
|
733
|
+
throw usageError(`Task ${task.id} has ${blocking.length} undispositioned open P1/P2 finding(s): `
|
|
734
|
+
+ `${blocking.map(({ id }) => id).join(", ")}. `
|
|
735
|
+
+ "Disposition each finding (yui task review finding dispose) before completing the Task.");
|
|
736
|
+
}
|
|
694
737
|
const completed = completeTask(task, now, { by: actor, summary });
|
|
695
738
|
tx.saveTask(completed);
|
|
696
739
|
tx.clearPendingWakeup(task.id);
|
|
@@ -745,29 +788,45 @@ function completeTaskCommand(args, store, options) {
|
|
|
745
788
|
function reopenTaskCommand(args, store, options) {
|
|
746
789
|
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
747
790
|
const now = clock(options);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
791
|
+
// Reopen changes the Task status that archiveLegacyTaskRefs reads before
|
|
792
|
+
// deleting the legacy ref. Take the same per-Project maintenance fence so a
|
|
793
|
+
// reopen cannot commit between archive's status read and its ref deletion:
|
|
794
|
+
// the two operations are mutually exclusive per Project.
|
|
795
|
+
const existing = store.getTask(args[0]);
|
|
796
|
+
const projectIds = existing === null
|
|
797
|
+
? []
|
|
798
|
+
: existing.projectBindings.map(({ projectId }) => projectId);
|
|
799
|
+
const release = options.yuiHome === undefined || projectIds.length === 0
|
|
800
|
+
? () => { }
|
|
801
|
+
: acquireProjectMaintenanceLocks(options.yuiHome, projectIds);
|
|
802
|
+
try {
|
|
803
|
+
const result = store.transaction((tx) => {
|
|
804
|
+
const task = requireTask(tx, args[0]);
|
|
805
|
+
if (task.status === "active")
|
|
806
|
+
return { task, changed: false };
|
|
807
|
+
if (task.status === "archived")
|
|
808
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
809
|
+
if (task.status !== "completed")
|
|
810
|
+
throw usageError(`Task is not completed: ${task.id}.`);
|
|
811
|
+
const active = reopenTask(task, now);
|
|
812
|
+
tx.saveTask(active);
|
|
813
|
+
tx.clearOperatorNotification(task.id);
|
|
814
|
+
enqueueWork(tx, leaderMailbox(task.id), "task-reopened", now, [taskRef(task.id)]);
|
|
815
|
+
enqueueWork(tx, taskMailbox(task.id), "task-reopened", now, [taskRef(task.id)]);
|
|
816
|
+
recordTaskEvent(tx, task.id, "task.reopened", { status: active.status }, now);
|
|
817
|
+
return { task: active, changed: true };
|
|
818
|
+
});
|
|
819
|
+
if (result.changed) {
|
|
820
|
+
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
821
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
822
|
+
}
|
|
823
|
+
return result.changed
|
|
824
|
+
? `Reopened task ${result.task.id}\n`
|
|
825
|
+
: `Task ${result.task.id} is already active\n`;
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
release();
|
|
767
829
|
}
|
|
768
|
-
return result.changed
|
|
769
|
-
? `Reopened task ${result.task.id}\n`
|
|
770
|
-
: `Task ${result.task.id} is already active\n`;
|
|
771
830
|
}
|
|
772
831
|
function archiveTaskCommand(args, store, options) {
|
|
773
832
|
const request = validateTaskArchiveRequest(args, store, options);
|
|
@@ -781,9 +840,6 @@ function archiveTaskCommand(args, store, options) {
|
|
|
781
840
|
&& task.status !== "retired") {
|
|
782
841
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
783
842
|
}
|
|
784
|
-
if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
|
|
785
|
-
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
786
|
-
}
|
|
787
843
|
assertNoOpenInputRequests(tx, task.id, "archiving the Task");
|
|
788
844
|
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
789
845
|
|| integration.status === "blocked"
|
|
@@ -791,6 +847,15 @@ function archiveTaskCommand(args, store, options) {
|
|
|
791
847
|
if (unresolvedIntegration !== undefined) {
|
|
792
848
|
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
793
849
|
}
|
|
850
|
+
const activeArchiveJob = tx.listDurableJobs(task.id).find((job) => (job.status === "queued"
|
|
851
|
+
|| job.status === "running"
|
|
852
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
853
|
+
if (activeArchiveJob !== undefined) {
|
|
854
|
+
throw usageError(`Task ${task.id} has an active DurableJob: ${activeArchiveJob.id}/${activeArchiveJob.status}.`);
|
|
855
|
+
}
|
|
856
|
+
if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
|
|
857
|
+
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
858
|
+
}
|
|
794
859
|
const activeRole = tx.listRoles(task.id)
|
|
795
860
|
.find((role) => tx.getActiveAgentRun(task.id, role.name) !== null);
|
|
796
861
|
if (activeRole !== undefined) {
|
|
@@ -851,6 +916,12 @@ function retireTaskCommand(args, store, options) {
|
|
|
851
916
|
if (unresolvedIntegration !== undefined) {
|
|
852
917
|
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
853
918
|
}
|
|
919
|
+
const activeRetireJob = tx.listDurableJobs(task.id).find((job) => (job.status === "queued"
|
|
920
|
+
|| job.status === "running"
|
|
921
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
922
|
+
if (activeRetireJob !== undefined) {
|
|
923
|
+
throw usageError(`Task ${task.id} has an active DurableJob: ${activeRetireJob.id}/${activeRetireJob.status}.`);
|
|
924
|
+
}
|
|
854
925
|
assertTaskRetirementProof(tx, task, options.taskRetirementProof);
|
|
855
926
|
for (const run of tx.listAgentRuns(task.id).filter(({ status: runStatus }) => (runStatus === "active"))) {
|
|
856
927
|
const terminal = terminalizeExactTaskRun(tx, {
|
|
@@ -982,11 +1053,22 @@ function reconcileTaskCommand(args, store, options) {
|
|
|
982
1053
|
function taskMessageCommand(args, store, options) {
|
|
983
1054
|
const [command, ...rest] = args;
|
|
984
1055
|
if (command === "send") {
|
|
985
|
-
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->).";
|
|
986
|
-
const parsed = parseTail(rest, new Set(["--body-file"]), usage);
|
|
1056
|
+
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1057
|
+
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy"]), usage);
|
|
987
1058
|
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
988
1059
|
throw usageError(usage);
|
|
989
1060
|
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
1061
|
+
const wakePolicyRaw = parsed.options.get("--wake-policy");
|
|
1062
|
+
let wakePolicy;
|
|
1063
|
+
if (wakePolicyRaw === undefined) {
|
|
1064
|
+
wakePolicy = undefined;
|
|
1065
|
+
}
|
|
1066
|
+
else if (wakePolicyRaw === "leader" || wakePolicyRaw === "none") {
|
|
1067
|
+
wakePolicy = wakePolicyRaw;
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
|
|
1071
|
+
}
|
|
990
1072
|
const now = clock(options);
|
|
991
1073
|
const result = store.transaction((tx) => {
|
|
992
1074
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
@@ -995,9 +1077,14 @@ function taskMessageCommand(args, store, options) {
|
|
|
995
1077
|
const message = actor === "leader"
|
|
996
1078
|
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: LEADER_ROLE }, now)
|
|
997
1079
|
: actor === "operator"
|
|
998
|
-
? appendMessage(tx, task.id, body, "operator", { type: "operator" }, now)
|
|
999
|
-
: appendMessage(tx, task.id, body, "user", { type: "user" }, now);
|
|
1000
|
-
|
|
1080
|
+
? appendMessage(tx, task.id, body, "operator", { type: "operator" }, now, { wakePolicy })
|
|
1081
|
+
: appendMessage(tx, task.id, body, "user", { type: "user" }, now, { wakePolicy });
|
|
1082
|
+
// Issue 05: only `wakePolicy=leader` (the default for backward
|
|
1083
|
+
// compatibility) enqueues Leader work. `wakePolicy=none` persists the
|
|
1084
|
+
// message as context without waking the Leader.
|
|
1085
|
+
if (task.status === "active"
|
|
1086
|
+
&& actor !== "leader"
|
|
1087
|
+
&& wakePolicy !== "none") {
|
|
1001
1088
|
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)]);
|
|
1002
1089
|
}
|
|
1003
1090
|
return { task, message, actor };
|
|
@@ -1032,6 +1119,30 @@ function taskMessageCommand(args, store, options) {
|
|
|
1032
1119
|
? "Task message command is required."
|
|
1033
1120
|
: `Unknown command: task message ${command}`);
|
|
1034
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Issue 05: force-wake escape hatch. Bypasses the actionability digest and
|
|
1124
|
+
* enqueues exactly one Leader wakeup with an auditable reason. The reason is
|
|
1125
|
+
* truncated to keep the event payload compact.
|
|
1126
|
+
*/
|
|
1127
|
+
function taskWakeCommand(args, store, options) {
|
|
1128
|
+
const usage = "Task wake usage: yui task wake <id> --force --reason <text>.";
|
|
1129
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage, new Set(["--force"]));
|
|
1130
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1131
|
+
if (!parsed.options.has("--force")) {
|
|
1132
|
+
throw usageError("--force is required to wake a Task.", usage);
|
|
1133
|
+
}
|
|
1134
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1135
|
+
const now = clock(options);
|
|
1136
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
1137
|
+
assertTaskOpen(task);
|
|
1138
|
+
const wakeReason = `force-wake:${truncateEventNote(reason)}`;
|
|
1139
|
+
store.transaction((tx) => {
|
|
1140
|
+
queueLeaderWakeup(tx, task.id, wakeReason, now);
|
|
1141
|
+
recordTaskEvent(tx, task.id, "task.wake-forced", { reason: wakeReason }, now);
|
|
1142
|
+
});
|
|
1143
|
+
notifyMailbox(options.runtime, leaderMailbox(task.id), task.id);
|
|
1144
|
+
return `Woke ${task.id} (${wakeReason})\n`;
|
|
1145
|
+
}
|
|
1035
1146
|
function taskRoleCommand(args, store, options) {
|
|
1036
1147
|
const [command, ...rest] = args;
|
|
1037
1148
|
if (command === "add")
|
|
@@ -1448,6 +1559,15 @@ function createWork(args, store, options) {
|
|
|
1448
1559
|
if (new Set(baseRefs.map(({ projectId }) => projectId)).size !== baseRefs.length) {
|
|
1449
1560
|
throw usageError("Each Work Item Project may specify at most one base ref.");
|
|
1450
1561
|
}
|
|
1562
|
+
const guard = runDeliveryGuardPreflight(tx, task.id, {
|
|
1563
|
+
kind: "create-work-item",
|
|
1564
|
+
scope: {
|
|
1565
|
+
title: parsed.positionals[1],
|
|
1566
|
+
objective: parsed.objective ?? parsed.positionals[1],
|
|
1567
|
+
acceptance: parsed.acceptance,
|
|
1568
|
+
writeProjectIds
|
|
1569
|
+
}
|
|
1570
|
+
}, { environment: options.environment, budget: true });
|
|
1451
1571
|
const created = createWorkItem(tx.nextWorkItemId(task.id), task.id, {
|
|
1452
1572
|
title: parsed.positionals[1],
|
|
1453
1573
|
objective: parsed.objective ?? parsed.positionals[1],
|
|
@@ -1459,10 +1579,10 @@ function createWork(args, store, options) {
|
|
|
1459
1579
|
}, now);
|
|
1460
1580
|
tx.saveWorkItem(task.id, created);
|
|
1461
1581
|
enqueueWork(tx, taskMailbox(task.id), "work-created", now, [workItemRef(task.id, created.id)]);
|
|
1462
|
-
return created;
|
|
1582
|
+
return { item: created, guard };
|
|
1463
1583
|
});
|
|
1464
|
-
notifyMailbox(options.runtime, taskMailbox(item.taskId), item.taskId);
|
|
1465
|
-
return output(`Created work item ${item.id} for ${item.taskId}\n
|
|
1584
|
+
notifyMailbox(options.runtime, taskMailbox(item.item.taskId), item.item.taskId);
|
|
1585
|
+
return output(withGuardWarnings(item.guard, `Created work item ${item.item.id} for ${item.item.taskId}\n`), { workItem: item.item });
|
|
1466
1586
|
}
|
|
1467
1587
|
function updateWorkScope(args, store, options) {
|
|
1468
1588
|
const usage = "Task work scope usage: yui task work scope <task>/<work> [--project <project> ...].";
|
|
@@ -1819,6 +1939,24 @@ function dispatchWork(args, store, options) {
|
|
|
1819
1939
|
|| prepared.owner.executionLaneId !== lane.id) {
|
|
1820
1940
|
throw usageError(`Execution Lane workspace identity does not match dispatch: ${runningGroup.id}/${lane.id}.`);
|
|
1821
1941
|
}
|
|
1942
|
+
if (options.laneDispatchProjectPaths !== undefined) {
|
|
1943
|
+
// The prepared workspaces were created under a held maintenance
|
|
1944
|
+
// fence that this transaction still holds. Re-prove the Task
|
|
1945
|
+
// binding set and exact Project paths match the preparation
|
|
1946
|
+
// snapshot: a migrate in the prepare/adopt gap fails closed here
|
|
1947
|
+
// rather than stranding a Lane on the external checkout.
|
|
1948
|
+
const currentIds = task.projectBindings.map(({ projectId }) => projectId).sort();
|
|
1949
|
+
const proofIds = [...options.laneDispatchProjectPaths.keys()].sort();
|
|
1950
|
+
if (currentIds.length !== proofIds.length
|
|
1951
|
+
|| currentIds.some((projectId, index) => projectId !== proofIds[index])) {
|
|
1952
|
+
throw new Error(`Task Project bindings changed during Lane dispatch: ${task.id}.`);
|
|
1953
|
+
}
|
|
1954
|
+
for (const [projectId, preparedPath] of options.laneDispatchProjectPaths) {
|
|
1955
|
+
if (requireProject(tx, projectId).path !== preparedPath) {
|
|
1956
|
+
throw new Error(`Project path changed during Lane dispatch: ${projectId}.`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1822
1960
|
if (tx.getManagedWorkspace(prepared.owner) === null)
|
|
1823
1961
|
tx.saveManagedWorkspace(prepared);
|
|
1824
1962
|
}
|
|
@@ -2018,10 +2156,16 @@ function acceptWork(args, store, options) {
|
|
|
2018
2156
|
if (options.workItemIntegrationProof?.workspace.owner.type === "review-round") {
|
|
2019
2157
|
throw usageError("A ReviewRound-owned workspace cannot be used for WorkItem acceptance.");
|
|
2020
2158
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2159
|
+
// Only diagnostic evidence commits (a reviewer's own commit on top of the
|
|
2160
|
+
// frozen base) are barred from WorkItem acceptance. A clean review
|
|
2161
|
+
// attests the frozen base itself (evidenceCommit === reviewBaseCommit),
|
|
2162
|
+
// which is the candidate's own head.
|
|
2163
|
+
const diagnosticEvidence = new Set(tx.listReviewRounds(item.taskId)
|
|
2164
|
+
.flatMap(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit
|
|
2165
|
+
? [evidenceCommit]
|
|
2166
|
+
: []));
|
|
2167
|
+
if (options.workItemIntegrationProof?.projects.some(({ headCommit }) => diagnosticEvidence.has(headCommit))) {
|
|
2168
|
+
throw usageError("A ReviewRound diagnostic evidence commit cannot be used for WorkItem acceptance.");
|
|
2025
2169
|
}
|
|
2026
2170
|
const candidate = requireWorkItemCandidate(item);
|
|
2027
2171
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
@@ -2175,6 +2319,19 @@ function retireWork(args, store, options) {
|
|
|
2175
2319
|
throw usageError("A Work Item cannot replace itself.");
|
|
2176
2320
|
}
|
|
2177
2321
|
}
|
|
2322
|
+
// rr4/finding-5: A Work Item with an active DurableJob cannot be retired —
|
|
2323
|
+
// the runner may still be using its workspace. Block on queued, running,
|
|
2324
|
+
// and unacknowledged unknown-needs-attention jobs owned by this Work Item.
|
|
2325
|
+
const activeWorkItemJob = tx.listDurableJobs(task.id).find((job) => (job.owner.kind === "work-item"
|
|
2326
|
+
&& job.owner.workItemId === item.id
|
|
2327
|
+
&& (job.status === "queued"
|
|
2328
|
+
|| job.status === "running"
|
|
2329
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined))));
|
|
2330
|
+
if (activeWorkItemJob !== undefined) {
|
|
2331
|
+
throw usageError(`Work Item ${item.id} has an active DurableJob: `
|
|
2332
|
+
+ `${activeWorkItemJob.id}/${activeWorkItemJob.status}. `
|
|
2333
|
+
+ "Cancel or acknowledge it before retiring.");
|
|
2334
|
+
}
|
|
2178
2335
|
for (const run of tx.listAgentRuns(task.id).filter((candidate) => (candidate.status === "active" && candidate.workItemId === item.id))) {
|
|
2179
2336
|
const terminal = terminalizeExactTaskRun(tx, {
|
|
2180
2337
|
taskId: task.id,
|
|
@@ -2313,6 +2470,8 @@ function taskReviewCommand(args, store, options) {
|
|
|
2313
2470
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2314
2471
|
if (command === "group")
|
|
2315
2472
|
return resolveReviewExecutionGroup(rest, store, options);
|
|
2473
|
+
if (command === "finding")
|
|
2474
|
+
return reviewFindingCommand(rest, store, options);
|
|
2316
2475
|
throw usageError(command === undefined
|
|
2317
2476
|
? "Task review command is required."
|
|
2318
2477
|
: `Unknown command: task review ${command}`);
|
|
@@ -2355,29 +2514,32 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2355
2514
|
: selectedLaneIds === undefined ? {} : { selectedLaneIds })
|
|
2356
2515
|
}, now);
|
|
2357
2516
|
const withGroup = updateReviewExecutionGroup(round, resolved);
|
|
2358
|
-
const
|
|
2359
|
-
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2517
|
+
const selectedLanes = resolved.lanes
|
|
2518
|
+
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false);
|
|
2519
|
+
const laneReports = selectedLanes
|
|
2360
2520
|
.map((lane) => lane.result?.report ?? lane.result?.summary ?? "")
|
|
2361
2521
|
.filter((report) => report.length > 0);
|
|
2362
|
-
const checks =
|
|
2363
|
-
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2522
|
+
const checks = selectedLanes
|
|
2364
2523
|
.flatMap((lane) => lane.result?.checks ?? [])
|
|
2365
2524
|
.map(({ name, outcome, details }) => ({
|
|
2366
2525
|
name,
|
|
2367
2526
|
outcome,
|
|
2368
2527
|
...(details === undefined ? {} : { details })
|
|
2369
2528
|
}));
|
|
2370
|
-
const findings =
|
|
2371
|
-
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2529
|
+
const findings = selectedLanes
|
|
2372
2530
|
.flatMap((lane) => lane.result?.findings ?? []);
|
|
2373
|
-
const evidence =
|
|
2374
|
-
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2531
|
+
const evidence = selectedLanes
|
|
2375
2532
|
.flatMap((lane) => lane.result?.evidence ?? []);
|
|
2376
|
-
const evidenceCommits = [...new Set(
|
|
2377
|
-
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2533
|
+
const evidenceCommits = [...new Set(selectedLanes
|
|
2378
2534
|
.map((lane) => lane.result?.evidenceCommit)
|
|
2379
2535
|
.filter((commit) => commit !== undefined))];
|
|
2380
|
-
|
|
2536
|
+
// A Round attests a single tree only when EVERY selected Lane attests it.
|
|
2537
|
+
// A dirty Lane (no evidenceCommit) ran checks on an uncommitted tree, so its
|
|
2538
|
+
// checks cannot be covered by another Lane's base attestation.
|
|
2539
|
+
const allLanesAttest = selectedLanes.every((lane) => lane.result?.evidenceCommit !== undefined);
|
|
2540
|
+
const evidenceCommit = allLanesAttest && evidenceCommits.length === 1
|
|
2541
|
+
? evidenceCommits[0]
|
|
2542
|
+
: undefined;
|
|
2381
2543
|
const terminal = finishReviewRound(withGroup, decision === "accept" ? "completed" : "failed", summary, now, {
|
|
2382
2544
|
report: [
|
|
2383
2545
|
laneReports.join("\n\n") || summary,
|
|
@@ -2393,6 +2555,11 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2393
2555
|
...(evidenceCommit === undefined ? {} : { evidenceCommit })
|
|
2394
2556
|
});
|
|
2395
2557
|
tx.saveReviewRound(task.id, terminal);
|
|
2558
|
+
// Issue 06: a panel-resolved completed Round feeds the finding ledger;
|
|
2559
|
+
// a rejected Round is an execution-attempt failure and is skipped.
|
|
2560
|
+
if (terminal.status === "completed") {
|
|
2561
|
+
reconcileReviewFindingsAfterReview(tx, task.id, terminal.id, now);
|
|
2562
|
+
}
|
|
2396
2563
|
enqueueWork(tx, leaderMailbox(task.id), "review-group-resolved", now, [
|
|
2397
2564
|
workItemRef(task.id, round.workItemId)
|
|
2398
2565
|
]);
|
|
@@ -2400,6 +2567,166 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2400
2567
|
});
|
|
2401
2568
|
return output(`Resolved Review ExecutionGroup ${result.executionGroup?.id ?? "unknown"} as ${decision}; ReviewRound ${result.id} is ${result.status}.\n`, { reviewRound: result });
|
|
2402
2569
|
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
|
|
2572
|
+
* Findings are extracted automatically from completed Rounds; these commands
|
|
2573
|
+
* let the Leader inspect the ledger, disposition each finding, and plan the
|
|
2574
|
+
* parallel repair wave.
|
|
2575
|
+
*/
|
|
2576
|
+
function reviewFindingCommand(args, store, options) {
|
|
2577
|
+
const [command, ...rest] = args;
|
|
2578
|
+
if (command === "list")
|
|
2579
|
+
return listReviewFindings(rest, store, options);
|
|
2580
|
+
if (command === "dispose")
|
|
2581
|
+
return disposeReviewFindingCommand(rest, store, options);
|
|
2582
|
+
if (command === "repair-wave")
|
|
2583
|
+
return planReviewRepairWave(rest, store, options);
|
|
2584
|
+
if (command === "extract")
|
|
2585
|
+
return extractReviewFindingsCommand(rest, store, options);
|
|
2586
|
+
throw usageError(command === undefined
|
|
2587
|
+
? "Task review finding command is required."
|
|
2588
|
+
: `Unknown command: task review finding ${command}`);
|
|
2589
|
+
}
|
|
2590
|
+
function listReviewFindings(args, store, options) {
|
|
2591
|
+
const usage = "Task review finding list usage: yui task review finding list <task>.";
|
|
2592
|
+
exactPositionals(args, 1, usage);
|
|
2593
|
+
const task = requireTask(store, args[0]);
|
|
2594
|
+
const findings = store.listReviewFindings(task.id);
|
|
2595
|
+
if (findings.length === 0) {
|
|
2596
|
+
return output(`No review findings recorded for ${task.id}.\n`);
|
|
2597
|
+
}
|
|
2598
|
+
const lines = findings.map((finding) => {
|
|
2599
|
+
const repair = finding.repair === undefined
|
|
2600
|
+
? ""
|
|
2601
|
+
: `; repair: ${finding.repair.workItemId ?? "?"}${finding.repair.commit === undefined ? "" : `@${finding.repair.commit.slice(0, 12)}`}`;
|
|
2602
|
+
const merge = finding.mergeRequired === true ? " [merge-required]" : "";
|
|
2603
|
+
return `${finding.id} [${finding.severity}/${finding.disposition}] ${finding.title}`
|
|
2604
|
+
+ ` (invariant: ${finding.invariant}; first: ${finding.firstReviewRoundId}; last: ${finding.lastReviewRoundId})${repair}${merge}`;
|
|
2605
|
+
});
|
|
2606
|
+
return output(`Review findings for ${task.id}:\n${lines.join("\n")}\n`);
|
|
2607
|
+
}
|
|
2608
|
+
function disposeReviewFindingCommand(args, store, options) {
|
|
2609
|
+
const usage = "Task review finding dispose usage: yui task review finding dispose <task>/<finding> "
|
|
2610
|
+
+ "--disposition <fixed-pending-review|verified-fixed|accepted-risk|not-actionable|superseded> "
|
|
2611
|
+
+ "[--work-item <id>] [--commit <sha>] [--verification <text>] [--note <text>] [--superseded-by <stable-key>].";
|
|
2612
|
+
const parsed = parseTail(args, new Set(["--disposition", "--work-item", "--commit", "--verification", "--note", "--superseded-by"]), usage);
|
|
2613
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
2614
|
+
const disposition = requiredOption(parsed.options, "--disposition");
|
|
2615
|
+
if (!LEADER_FINDING_DISPOSITIONS.includes(disposition)) {
|
|
2616
|
+
throw usageError(`Review finding disposition is invalid: ${disposition}.`);
|
|
2617
|
+
}
|
|
2618
|
+
const now = clock(options);
|
|
2619
|
+
const reference = resolveTaskRecordReference(parsed.positionals[0], {
|
|
2620
|
+
kind: "reviewFinding",
|
|
2621
|
+
label: "Review finding"
|
|
2622
|
+
});
|
|
2623
|
+
const result = store.transaction((tx) => {
|
|
2624
|
+
const task = requireTask(tx, reference.taskId);
|
|
2625
|
+
if (task.status !== "active")
|
|
2626
|
+
throw usageError(inactiveTaskMessage(task, "dispositioning a review finding"));
|
|
2627
|
+
if (taskActor(options, task.id) !== "leader") {
|
|
2628
|
+
throw usageError("Only the Task Leader may disposition a review finding.");
|
|
2629
|
+
}
|
|
2630
|
+
const command = {
|
|
2631
|
+
disposition,
|
|
2632
|
+
by: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader",
|
|
2633
|
+
...(parsed.options.get("--note") === undefined ? {} : { note: parsed.options.get("--note") }),
|
|
2634
|
+
...(parsed.options.get("--work-item") === undefined ? {} : { workItemId: parsed.options.get("--work-item") }),
|
|
2635
|
+
...(parsed.options.get("--commit") === undefined ? {} : { commit: parsed.options.get("--commit") }),
|
|
2636
|
+
...(parsed.options.get("--verification") === undefined ? {} : { verification: parsed.options.get("--verification") }),
|
|
2637
|
+
...(parsed.options.get("--superseded-by") === undefined ? {} : { supersededBy: parsed.options.get("--superseded-by") }),
|
|
2638
|
+
now
|
|
2639
|
+
};
|
|
2640
|
+
return dispositionReviewFinding(tx, task.id, reference.localId, command);
|
|
2641
|
+
});
|
|
2642
|
+
return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
|
|
2643
|
+
}
|
|
2644
|
+
function planReviewRepairWave(args, store, options) {
|
|
2645
|
+
const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--create].";
|
|
2646
|
+
const parsed = parseTail(args, new Set(), usage, new Set(["--create"]));
|
|
2647
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
2648
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
2649
|
+
const groups = planRepairGroups(store, task.id);
|
|
2650
|
+
if (groups.length === 0) {
|
|
2651
|
+
return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
|
|
2652
|
+
}
|
|
2653
|
+
if (parsed.options.has("--create")) {
|
|
2654
|
+
const now = clock(options);
|
|
2655
|
+
const created = store.transaction((tx) => {
|
|
2656
|
+
const currentTask = requireTask(tx, task.id);
|
|
2657
|
+
if (currentTask.status !== "active") {
|
|
2658
|
+
throw usageError(inactiveTaskMessage(currentTask, "creating a review repair wave"));
|
|
2659
|
+
}
|
|
2660
|
+
if (taskActor(options, currentTask.id) !== "leader") {
|
|
2661
|
+
throw usageError("Only the Task Leader may create a review repair wave.");
|
|
2662
|
+
}
|
|
2663
|
+
const openItems = tx.listWorkItems(currentTask.id)
|
|
2664
|
+
.filter((item) => item.status === "pending" || item.status === "running");
|
|
2665
|
+
return groups.map((group) => {
|
|
2666
|
+
const findingMarkers = group.findingIds.map((id) => `review-finding:${id}`);
|
|
2667
|
+
const existing = openItems.find((item) => isDeepStrictEqual([...item.acceptance].sort(), [...findingMarkers].sort()));
|
|
2668
|
+
if (existing !== undefined)
|
|
2669
|
+
return { group, item: existing, changed: false };
|
|
2670
|
+
const item = createWorkItem(tx.nextWorkItemId(currentTask.id), currentTask.id, {
|
|
2671
|
+
title: `Repair review findings ${group.findingIds.join(", ")}`,
|
|
2672
|
+
objective: [
|
|
2673
|
+
"Repair the following Task-final Review findings as one overlapping group:",
|
|
2674
|
+
...group.findings.map((finding) => (`- ${finding.id} [${finding.severity}] ${finding.title} `
|
|
2675
|
+
+ `(invariant: ${finding.invariant}; evidence: ${finding.evidence.join(" | ") || "see ReviewRound"})`)),
|
|
2676
|
+
"Integrate this repair with the rest of the Task before requesting another Task-final Review."
|
|
2677
|
+
].join("\n"),
|
|
2678
|
+
acceptance: findingMarkers,
|
|
2679
|
+
writeProjectIds: reviewRepairProjectIds(currentTask, group.affectedPaths)
|
|
2680
|
+
}, now);
|
|
2681
|
+
tx.saveWorkItem(currentTask.id, item);
|
|
2682
|
+
enqueueWork(tx, taskMailbox(currentTask.id), "work-created", now, [
|
|
2683
|
+
workItemRef(currentTask.id, item.id)
|
|
2684
|
+
]);
|
|
2685
|
+
return { group, item, changed: true };
|
|
2686
|
+
});
|
|
2687
|
+
});
|
|
2688
|
+
const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
|
|
2689
|
+
+ `(${group.findingIds.join(", ")})`));
|
|
2690
|
+
return output(`Review repair wave for ${task.id} (${groups.length} group(s)):\n${lines.join("\n")}\n`, { groups: created });
|
|
2691
|
+
}
|
|
2692
|
+
const lines = groups.map((group, index) => {
|
|
2693
|
+
const findings = group.findings
|
|
2694
|
+
.map((finding) => `${finding.id} [${finding.severity}] ${finding.title}`)
|
|
2695
|
+
.join("; ");
|
|
2696
|
+
return `wave ${index + 1}: ${findings}`
|
|
2697
|
+
+ ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
|
|
2698
|
+
});
|
|
2699
|
+
return output(`Repair wave for ${task.id} (${groups.length} group(s), run disjoint groups in parallel):\n${lines.join("\n")}\n`);
|
|
2700
|
+
}
|
|
2701
|
+
function reviewRepairProjectIds(task, affectedPaths) {
|
|
2702
|
+
const bindings = task.projectBindings;
|
|
2703
|
+
const matched = bindings.filter((binding) => affectedPaths.some((path) => pathWithinProject(path, binding.directory)));
|
|
2704
|
+
const projectIds = (matched.length > 0 ? matched : bindings).map(({ projectId }) => projectId);
|
|
2705
|
+
return [...new Set(projectIds)];
|
|
2706
|
+
}
|
|
2707
|
+
function pathWithinProject(path, directory) {
|
|
2708
|
+
const normalizedPath = path.replace(/^\.\//u, "").replace(/^\/+/u, "");
|
|
2709
|
+
const normalizedDirectory = directory.replace(/^\.\//u, "").replace(/^\/+|\/+$/gu, "");
|
|
2710
|
+
if (normalizedDirectory.length === 0 || normalizedDirectory === ".")
|
|
2711
|
+
return true;
|
|
2712
|
+
return normalizedPath === normalizedDirectory
|
|
2713
|
+
|| normalizedPath.startsWith(`${normalizedDirectory}/`);
|
|
2714
|
+
}
|
|
2715
|
+
function extractReviewFindingsCommand(args, store, options) {
|
|
2716
|
+
const usage = "Task review finding extract usage: yui task review finding extract <task>/<review-round>.";
|
|
2717
|
+
exactPositionals(args, 1, usage);
|
|
2718
|
+
const reference = resolveTaskRecordReference(args[0], {
|
|
2719
|
+
kind: "reviewRound",
|
|
2720
|
+
label: "ReviewRound"
|
|
2721
|
+
});
|
|
2722
|
+
const now = clock(options);
|
|
2723
|
+
const result = store.transaction((tx) => reconcileReviewFindings(tx, reference.taskId, reference.localId, now));
|
|
2724
|
+
if (result.skipped) {
|
|
2725
|
+
return output(`ReviewRound ${result.roundId} produced no findings: ${result.reason ?? "skipped"}\n`);
|
|
2726
|
+
}
|
|
2727
|
+
return output(`Reconciled ${result.created.length + result.updated.length + result.conflicts.length} finding(s) from ${result.roundId}: `
|
|
2728
|
+
+ `${result.created.length} created, ${result.updated.length} updated, ${result.conflicts.length} conflict(s).\n`);
|
|
2729
|
+
}
|
|
2403
2730
|
function requestTaskReviewRound(args, store, options) {
|
|
2404
2731
|
const usage = "Task review request usage: yui task review request <task> --role <global-role> [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...].";
|
|
2405
2732
|
const parsed = parseMultiValueTail(args, new Set(["--role", "--strategy"]), new Set(["--lane-role"]), usage);
|
|
@@ -2633,9 +2960,12 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
2633
2960
|
throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
|
|
2634
2961
|
}
|
|
2635
2962
|
if (round.reviewerRunId !== undefined) {
|
|
2963
|
+
if (round.status === "completed") {
|
|
2964
|
+
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
2965
|
+
}
|
|
2636
2966
|
throw usageError(`ReviewRound ${round.id} has Reviewer Run ${round.reviewerRunId}; use task run retry instead.`);
|
|
2637
2967
|
}
|
|
2638
|
-
if (round.status !== "failed") {
|
|
2968
|
+
if (round.status !== "failed" && round.status !== "pending") {
|
|
2639
2969
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
2640
2970
|
}
|
|
2641
2971
|
if (round.taskCandidate === undefined) {
|
|
@@ -2667,25 +2997,15 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
2667
2997
|
if (roundIndex < 0) {
|
|
2668
2998
|
throw dataError(`Final ReviewRound is not in Task history: ${round.id}.`);
|
|
2669
2999
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
&& entry.requestedBy === "leader"
|
|
2674
|
-
&& entry.workItemId === round.workItemId
|
|
2675
|
-
&& entry.candidateId === round.candidateId
|
|
2676
|
-
&& entry.reviewerRoleName === round.reviewerRoleName
|
|
2677
|
-
&& entry.reviewBaseCommit === round.reviewBaseCommit
|
|
2678
|
-
&& sameTaskFinalReviewContract(entry.taskFinalReviewContract, taskFinalContract)
|
|
2679
|
-
&& isSameTaskReviewCandidate(entry.taskCandidate, round.taskCandidate)
|
|
2680
|
-
&& entry.status !== "failed")).at(-1);
|
|
2681
|
-
const conflictingLater = laterRounds.find((entry) => entry.id !== existingRetry?.id);
|
|
3000
|
+
// Issue 06: infra retries reuse the same semantic Round ID. Any later
|
|
3001
|
+
// Round supersedes this one; an active Round for the same Reviewer blocks.
|
|
3002
|
+
const conflictingLater = reviewerRounds.slice(roundIndex + 1).at(-1);
|
|
2682
3003
|
if (conflictingLater !== undefined) {
|
|
2683
3004
|
throw usageError(`A newer conflicting final ReviewRound already exists after ${round.id}: `
|
|
2684
3005
|
+ `${conflictingLater.id}/${conflictingLater.status}.`);
|
|
2685
3006
|
}
|
|
2686
|
-
assertNoConflictingTaskReviewRound(reviewerRounds,
|
|
3007
|
+
assertNoConflictingTaskReviewRound(reviewerRounds, round.id);
|
|
2687
3008
|
const activeRound = reviewerRounds.find((entry) => (entry.id !== round.id
|
|
2688
|
-
&& entry.id !== existingRetry?.id
|
|
2689
3009
|
&& entry.reviewerRoleName === round.reviewerRoleName
|
|
2690
3010
|
&& (entry.status === "pending" || entry.status === "running")));
|
|
2691
3011
|
if (activeRound !== undefined) {
|
|
@@ -2711,62 +3031,27 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
2711
3031
|
|| mailbox?.pending !== null && mailbox?.pending !== undefined);
|
|
2712
3032
|
const activeReviewerRuns = tx.listAgentRuns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
|
|
2713
3033
|
const activePointer = tx.getActiveAgentRun(task.id, reviewer.name);
|
|
2714
|
-
// An exact already-created retry is the idempotent result. The identity
|
|
2715
|
-
// fences are intentionally as strict as the failed-Run retry path: pending
|
|
2716
|
-
// is reusable only while every Reviewer lane is idle; running is reusable
|
|
2717
|
-
// only with its exact active Run/mailbox; completed is a no-write result.
|
|
2718
|
-
if (existingRetry !== undefined) {
|
|
2719
|
-
if (existingRetry.status === "pending") {
|
|
2720
|
-
if (activePointer !== null || activeReviewerRuns.length > 0
|
|
2721
|
-
|| hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
2722
|
-
throw usageError(`Reviewer has unrelated active execution: ${reviewer.name}.`);
|
|
2723
|
-
}
|
|
2724
|
-
return { round: existingRetry, created: false };
|
|
2725
|
-
}
|
|
2726
|
-
if (existingRetry.status === "running") {
|
|
2727
|
-
const reviewerRunId = existingRetry.reviewerRunId;
|
|
2728
|
-
const activeMatches = reviewerRunId !== undefined
|
|
2729
|
-
&& activePointer !== null
|
|
2730
|
-
&& activePointer.id === reviewerRunId
|
|
2731
|
-
&& activePointer.status === "active"
|
|
2732
|
-
&& activeReviewerRuns.length === 1
|
|
2733
|
-
&& activeReviewerRuns[0].id === reviewerRunId;
|
|
2734
|
-
const processingMatches = reviewerRunId !== undefined
|
|
2735
|
-
&& reviewerMailbox?.processing?.executionRef !== undefined
|
|
2736
|
-
&& isDeepStrictEqual(reviewerMailbox.processing.executionRef, runRef(task.id, reviewerRunId))
|
|
2737
|
-
&& reviewerMailbox.pending === null;
|
|
2738
|
-
const pendingMatches = reviewerRunId !== undefined
|
|
2739
|
-
&& reviewerMailbox?.pending?.requestCount === 1
|
|
2740
|
-
&& reviewerMailbox.pending.refs.some((ref) => (isDeepStrictEqual(ref, runRef(task.id, reviewerRunId))));
|
|
2741
|
-
if (!activeMatches || (!processingMatches && !pendingMatches) || hasMailboxWork(runtimeMailbox)) {
|
|
2742
|
-
throw usageError(`Existing retry ${existingRetry.id} is running without its exact active Reviewer execution.`);
|
|
2743
|
-
}
|
|
2744
|
-
return { round: existingRetry, created: false };
|
|
2745
|
-
}
|
|
2746
|
-
if (existingRetry.status === "completed") {
|
|
2747
|
-
if (activePointer !== null || activeReviewerRuns.length > 0
|
|
2748
|
-
|| hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
2749
|
-
throw usageError(`Reviewer has unrelated active execution: ${reviewer.name}.`);
|
|
2750
|
-
}
|
|
2751
|
-
return { round: existingRetry, created: false };
|
|
2752
|
-
}
|
|
2753
|
-
throw usageError(`Existing retry ${existingRetry.id} is not reusable from ${existingRetry.status}.`);
|
|
2754
|
-
}
|
|
2755
3034
|
if (activePointer !== null || activeReviewerRuns.length > 0) {
|
|
2756
3035
|
throw usageError(`Reviewer Role already has an active run: ${reviewer.name}.`);
|
|
2757
3036
|
}
|
|
2758
3037
|
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
2759
3038
|
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
2760
3039
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
3040
|
+
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
3041
|
+
if (round.status === "pending") {
|
|
3042
|
+
return { round, created: false };
|
|
3043
|
+
}
|
|
3044
|
+
// Issue 06: infra retry resets the same semantic Round to pending instead
|
|
3045
|
+
// of manufacturing a new Round, so Round count and finding identity stay
|
|
3046
|
+
// stable across execution-attempt failures.
|
|
3047
|
+
const resetRound = retryTaskReviewRound(round);
|
|
3048
|
+
tx.saveReviewRound(task.id, resetRound);
|
|
2763
3049
|
recordTaskEvent(tx, task.id, "review.task-final-retried", {
|
|
2764
|
-
|
|
2765
|
-
nextReviewRoundId: nextRound.id,
|
|
3050
|
+
reviewRoundId: round.id,
|
|
2766
3051
|
workItemId: round.workItemId,
|
|
2767
3052
|
candidateId: round.candidateId
|
|
2768
3053
|
}, now);
|
|
2769
|
-
return { round:
|
|
3054
|
+
return { round: resetRound, created: true };
|
|
2770
3055
|
});
|
|
2771
3056
|
return output(result.created
|
|
2772
3057
|
? `Task-final Review retry requested as ${result.round.id}\n`
|
|
@@ -2784,6 +3069,8 @@ function taskRunCommand(args, store, options) {
|
|
|
2784
3069
|
return output(recoverRun(rest, store, options));
|
|
2785
3070
|
if (command === "yield")
|
|
2786
3071
|
return yieldRun(rest, store, options);
|
|
3072
|
+
if (command === "yield-status")
|
|
3073
|
+
return yieldRunStatus(rest, store, options);
|
|
2787
3074
|
if (command === "checkpoint")
|
|
2788
3075
|
return output(checkpointRun(rest, store, options));
|
|
2789
3076
|
throw usageError(command === undefined
|
|
@@ -3436,11 +3723,9 @@ function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
|
|
|
3436
3723
|
}
|
|
3437
3724
|
/**
|
|
3438
3725
|
* Leader-only retry of an exact failed Task-final review Run. The old failed
|
|
3439
|
-
* Run
|
|
3440
|
-
*
|
|
3441
|
-
*
|
|
3442
|
-
* prior retry already produced one). Every identity and frozen-head fence is
|
|
3443
|
-
* checked inside one transaction so a partial fail-old-without-new state can
|
|
3726
|
+
* Run remains the attempt trail, while the semantic ReviewRound is reset to
|
|
3727
|
+
* pending under its existing identity. Every identity and frozen-head fence is
|
|
3728
|
+
* checked inside one transaction so a partial fail-old-without-reset state can
|
|
3444
3729
|
* never be committed.
|
|
3445
3730
|
*/
|
|
3446
3731
|
function retryFailedReviewRun(previous, store, options, now) {
|
|
@@ -3470,13 +3755,12 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3470
3755
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3471
3756
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3472
3757
|
}
|
|
3473
|
-
if (round.status !== "failed"
|
|
3758
|
+
if (round.status !== "failed"
|
|
3759
|
+
&& round.status !== "running"
|
|
3760
|
+
&& round.status !== "pending"
|
|
3761
|
+
&& round.status !== "completed") {
|
|
3474
3762
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
3475
3763
|
}
|
|
3476
|
-
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3477
|
-
if (validation.disposition !== "applied" || validation.round === null) {
|
|
3478
|
-
throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
3479
|
-
}
|
|
3480
3764
|
const currentTaskCandidate = actualTaskReviewCandidateForMutation(tx, task, options);
|
|
3481
3765
|
if (!isSameTaskReviewCandidate(currentTaskCandidate, round.taskCandidate)) {
|
|
3482
3766
|
throw usageError(`Task-final ReviewRound ${round.id} no longer matches the latest committed Integration heads.`);
|
|
@@ -3499,27 +3783,9 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3499
3783
|
if (laterRound !== undefined && !isSameTaskReviewCandidate(laterRound.taskCandidate, round.taskCandidate)) {
|
|
3500
3784
|
throw usageError(`A newer final Task candidate already has ReviewRound ${laterRound.id}.`);
|
|
3501
3785
|
}
|
|
3502
|
-
const reviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.workItemId === item.id
|
|
3503
|
-
&& entry.candidateId === candidate.id
|
|
3504
|
-
&& entry.reviewerRoleName === round.reviewerRoleName)));
|
|
3505
3786
|
const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === round.reviewerRoleName)));
|
|
3506
|
-
|
|
3507
|
-
&& entry.status !== "failed"
|
|
3508
|
-
&& entry.requestedBy === "leader"
|
|
3509
|
-
&& (entry.scope ?? "work-item") === "task"
|
|
3510
|
-
&& entry.candidateId === candidate.id
|
|
3511
|
-
&& entry.workItemId === item.id
|
|
3512
|
-
&& entry.reviewerRoleName === round.reviewerRoleName
|
|
3513
|
-
&& entry.reviewBaseCommit === round.reviewBaseCommit
|
|
3514
|
-
&& sameTaskFinalReviewContract(entry.taskFinalReviewContract, taskFinalContract)
|
|
3515
|
-
&& isSameTaskReviewCandidate(entry.taskCandidate, round.taskCandidate))).at(-1);
|
|
3516
|
-
if (round.status === "running"
|
|
3517
|
-
&& tx.getActiveAgentRun(task.id, round.reviewerRoleName) !== null) {
|
|
3518
|
-
throw usageError(`${task.id}/${round.reviewerRoleName} already has an active run.`);
|
|
3519
|
-
}
|
|
3520
|
-
assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), [round.id, ...(existingRetry === undefined ? [] : [existingRetry.id])]);
|
|
3787
|
+
assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id);
|
|
3521
3788
|
const activeRound = allReviewerRounds.find((entry) => (entry.id !== round.id
|
|
3522
|
-
&& entry.id !== existingRetry?.id
|
|
3523
3789
|
&& (entry.status === "pending" || entry.status === "running")));
|
|
3524
3790
|
if (activeRound !== undefined) {
|
|
3525
3791
|
throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
|
|
@@ -3536,39 +3802,21 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3536
3802
|
const reviewer = requireRole(tx, task.id, round.reviewerRoleName);
|
|
3537
3803
|
const activePointer = tx.getActiveAgentRun(task.id, reviewer.name);
|
|
3538
3804
|
const activeReviewerRuns = tx.listAgentRuns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
throw usageError(`${task.id}/${reviewer.name} already has an active run.`);
|
|
3542
|
-
}
|
|
3543
|
-
// A completed identical retry is terminal evidence and is a no-write
|
|
3544
|
-
// idempotent read only when no unrelated Reviewer state is live. Keep the
|
|
3545
|
-
// same fail-closed fences as pending/running retries.
|
|
3546
|
-
if (existingRetry?.status === "completed") {
|
|
3547
|
-
if (activePointer !== null || activeReviewerRuns.length > 0) {
|
|
3548
|
-
throw usageError(`Reviewer Role already has an active run: ${round.reviewerRoleName}.`);
|
|
3549
|
-
}
|
|
3550
|
-
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3551
|
-
throw usageError(`Reviewer has unrelated mailbox work: ${round.reviewerRoleName}.`);
|
|
3552
|
-
}
|
|
3553
|
-
return { round: existingRetry, previousRun: run };
|
|
3554
|
-
}
|
|
3555
|
-
if (existingRetry?.status === "pending") {
|
|
3556
|
-
// A pending retry is reusable only while every Reviewer lane is idle.
|
|
3557
|
-
// In particular, never return it to the CLI while another Round/Run or
|
|
3558
|
-
// mailbox batch could cause the CLI to dispatch or fail it.
|
|
3805
|
+
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
3806
|
+
if (round.status === "completed") {
|
|
3559
3807
|
if (activePointer !== null || activeReviewerRuns.length > 0) {
|
|
3560
3808
|
throw usageError(`Reviewer Role already has an active run: ${reviewer.name}.`);
|
|
3561
3809
|
}
|
|
3562
|
-
if (hasMailboxWork(reviewerMailbox)) {
|
|
3563
|
-
throw usageError(`Reviewer
|
|
3564
|
-
}
|
|
3565
|
-
if (hasMailboxWork(runtimeMailbox)) {
|
|
3566
|
-
throw usageError(`Reviewer runtime lifecycle is pending: ${reviewer.name}.`);
|
|
3810
|
+
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3811
|
+
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
3567
3812
|
}
|
|
3568
|
-
return { round
|
|
3813
|
+
return { round, previousRun: run, created: false };
|
|
3569
3814
|
}
|
|
3570
|
-
|
|
3571
|
-
|
|
3815
|
+
// Issue 06: a running same Round is reusable only with its exact active
|
|
3816
|
+
// Run and mailbox execution. A stranded Run (no active pointer) falls
|
|
3817
|
+
// through and resets the Round after the identity fences below.
|
|
3818
|
+
if (round.status === "running") {
|
|
3819
|
+
const reviewerRunId = round.reviewerRunId;
|
|
3572
3820
|
const activeMatches = reviewerRunId !== undefined
|
|
3573
3821
|
&& activePointer !== null
|
|
3574
3822
|
&& activePointer.id === reviewerRunId
|
|
@@ -3583,12 +3831,24 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3583
3831
|
&& reviewerMailbox?.processing === null
|
|
3584
3832
|
&& reviewerMailbox.pending?.requestCount === 1
|
|
3585
3833
|
&& reviewerMailbox.pending.refs.some((ref) => (isDeepStrictEqual(ref, runRef(task.id, reviewerRunId))));
|
|
3586
|
-
if (
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3834
|
+
if (activePointer !== null
|
|
3835
|
+
&& (!activeMatches
|
|
3836
|
+
|| (!processingMatches && !pendingMatches)
|
|
3837
|
+
|| hasMailboxWork(runtimeMailbox))) {
|
|
3838
|
+
throw usageError(`Existing running ReviewRound ${round.id} lacks its exact active Reviewer execution.`);
|
|
3590
3839
|
}
|
|
3591
|
-
|
|
3840
|
+
if (activeMatches)
|
|
3841
|
+
return { round, previousRun: run, created: false };
|
|
3842
|
+
}
|
|
3843
|
+
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
3844
|
+
if (round.status === "pending") {
|
|
3845
|
+
if (activePointer !== null || activeReviewerRuns.length > 0) {
|
|
3846
|
+
throw usageError(`Reviewer Role already has an active run: ${reviewer.name}.`);
|
|
3847
|
+
}
|
|
3848
|
+
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3849
|
+
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
3850
|
+
}
|
|
3851
|
+
return { round, previousRun: run, created: false };
|
|
3592
3852
|
}
|
|
3593
3853
|
if (activePointer !== null || activeReviewerRuns.length > 0) {
|
|
3594
3854
|
throw usageError(`Reviewer Role already has an active run: ${reviewer.name}.`);
|
|
@@ -3599,6 +3859,10 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3599
3859
|
if (runtimeMailbox?.pending !== null && runtimeMailbox?.pending !== undefined) {
|
|
3600
3860
|
throw usageError(`Reviewer runtime lifecycle has pending work: ${reviewer.name}.`);
|
|
3601
3861
|
}
|
|
3862
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3863
|
+
if (validation.disposition !== "applied" || validation.round === null) {
|
|
3864
|
+
throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
3865
|
+
}
|
|
3602
3866
|
// A stranded pre-delivery Run can leave its exact pending or processing
|
|
3603
3867
|
// dispatch behind; settle only that exact reference while holding the
|
|
3604
3868
|
// same aggregate lock. Any merged or unrelated batch fails closed.
|
|
@@ -3622,27 +3886,31 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3622
3886
|
}
|
|
3623
3887
|
// Terminalize the old stranded Round only after every identity and mailbox
|
|
3624
3888
|
// fence has passed. The outer transaction rolls back if Round creation fails.
|
|
3889
|
+
let roundToReset = round;
|
|
3625
3890
|
if (round.status !== "failed") {
|
|
3626
3891
|
const summary = round.summary
|
|
3627
3892
|
?? run.summary
|
|
3628
3893
|
?? `Review Run ${run.id} failed before delivery.`;
|
|
3629
|
-
|
|
3894
|
+
roundToReset = finishReviewRound(round, "failed", summary, now, {
|
|
3630
3895
|
report: round.report ?? summary,
|
|
3631
3896
|
checks: round.checks ?? [],
|
|
3632
3897
|
...(round.evidenceCommit === undefined ? {} : { evidenceCommit: round.evidenceCommit })
|
|
3633
|
-
})
|
|
3898
|
+
});
|
|
3899
|
+
tx.saveReviewRound(task.id, roundToReset);
|
|
3634
3900
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3901
|
+
// Issue 06: infra retry resets the same semantic Round to pending instead
|
|
3902
|
+
// of manufacturing a new Round, so Round count and finding identity stay
|
|
3903
|
+
// stable across execution-attempt failures.
|
|
3904
|
+
const resetRound = retryTaskReviewRound(roundToReset);
|
|
3905
|
+
tx.saveReviewRound(task.id, resetRound);
|
|
3637
3906
|
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
3638
3907
|
runId: run.id,
|
|
3639
3908
|
reviewRoundId: round.id,
|
|
3640
|
-
nextReviewRoundId: nextRound.id,
|
|
3641
3909
|
candidateId: candidate.id
|
|
3642
3910
|
}, now);
|
|
3643
|
-
return { round:
|
|
3911
|
+
return { round: resetRound, previousRun: run, created: true };
|
|
3644
3912
|
});
|
|
3645
|
-
return output(result.
|
|
3913
|
+
return output(result.created
|
|
3646
3914
|
? `Review retry requested as ${result.round.id}\n`
|
|
3647
3915
|
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
3648
3916
|
}
|
|
@@ -3722,12 +3990,111 @@ function recoverRun(args, store, options) {
|
|
|
3722
3990
|
: "";
|
|
3723
3991
|
return `Recorded exact ${result.action} recovery for ${result.run?.id ?? "unknown Run"}.${followup}\n`;
|
|
3724
3992
|
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Issue 04: builds the terminal yield outcome from the command inputs. The
|
|
3995
|
+
* same construction feeds both the first commit and the idempotent replay, so
|
|
3996
|
+
* a resend always hashes to the same digest.
|
|
3997
|
+
*/
|
|
3998
|
+
function buildYieldOutcome(run, inputSummary, options) {
|
|
3999
|
+
let yieldedReport;
|
|
4000
|
+
if (run.purpose === "review"
|
|
4001
|
+
|| (run.purpose === "execution" && run.executionGroupId !== undefined)) {
|
|
4002
|
+
yieldedReport = parseReviewYieldReport(inputSummary);
|
|
4003
|
+
}
|
|
4004
|
+
const summary = yieldedReport?.summary ?? inputSummary;
|
|
4005
|
+
if (yieldedReport === undefined)
|
|
4006
|
+
return { summary };
|
|
4007
|
+
return {
|
|
4008
|
+
summary,
|
|
4009
|
+
reviewResult: {
|
|
4010
|
+
report: yieldedReport.report,
|
|
4011
|
+
checks: yieldedReport.checks,
|
|
4012
|
+
...(yieldedReport.findings === undefined ? {} : { findings: yieldedReport.findings }),
|
|
4013
|
+
...(yieldedReport.evidence === undefined ? {} : { evidence: yieldedReport.evidence }),
|
|
4014
|
+
...(run.purpose === "review"
|
|
4015
|
+
&& options.reviewWorkspaceResult?.evidenceCommit === undefined
|
|
4016
|
+
? {}
|
|
4017
|
+
: run.purpose === "review"
|
|
4018
|
+
? { evidenceCommit: options.reviewWorkspaceResult.evidenceCommit }
|
|
4019
|
+
: yieldedReport.evidenceCommit === undefined
|
|
4020
|
+
? {}
|
|
4021
|
+
: { evidenceCommit: yieldedReport.evidenceCommit }),
|
|
4022
|
+
...(options.executionLaneGitSnapshot === undefined
|
|
4023
|
+
|| options.executionLaneGitSnapshot === null
|
|
4024
|
+
? {}
|
|
4025
|
+
: { gitSnapshot: options.executionLaneGitSnapshot })
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Issue 04: replays an already-committed yield. Returns the committed receipt
|
|
4031
|
+
* for the same outcome, fails closed for a different outcome, or returns
|
|
4032
|
+
* `null` to keep the legacy "already terminal" behavior.
|
|
4033
|
+
*/
|
|
4034
|
+
function replayYieldReceipt(run, inputSummary, options) {
|
|
4035
|
+
const config = providerRetryConfig(options.environment ?? process.env);
|
|
4036
|
+
if (!config.yieldReceiptReplay)
|
|
4037
|
+
return null;
|
|
4038
|
+
if (run.yieldReceipt === undefined)
|
|
4039
|
+
return null;
|
|
4040
|
+
const outcome = buildYieldOutcome(run, inputSummary, options);
|
|
4041
|
+
const match = matchYieldReceipt(run.yieldReceipt, {
|
|
4042
|
+
status: "yielded",
|
|
4043
|
+
summary: outcome.summary,
|
|
4044
|
+
...(outcome.reviewResult === undefined ? {} : { reviewResult: outcome.reviewResult })
|
|
4045
|
+
});
|
|
4046
|
+
if (match === null)
|
|
4047
|
+
return null;
|
|
4048
|
+
if (match.kind === "digest-mismatch") {
|
|
4049
|
+
throw usageError(`Run ${run.id} is already terminal with a different yield outcome. `
|
|
4050
|
+
+ `Existing receipt: ${match.existing.receiptId} (request ${match.existing.requestId}).`);
|
|
4051
|
+
}
|
|
4052
|
+
return output(`Run ${run.id} yield already committed.\n`
|
|
4053
|
+
+ `Receipt: ${match.receipt.receiptId}\n`
|
|
4054
|
+
+ `Request: ${match.receipt.requestId}\n`, { receipt: match.receipt });
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Issue 04: `yui task run yield-status <task>/<run>` — returns the committed
|
|
4058
|
+
* yield receipt for a terminal Run, or the current status for an active Run.
|
|
4059
|
+
*/
|
|
4060
|
+
function yieldRunStatus(args, store, options) {
|
|
4061
|
+
const usage = "Task run yield-status usage: yui task run yield-status <task>/<run>.";
|
|
4062
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
4063
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
4064
|
+
const run = requireRun(store, parsed.positionals[0], options);
|
|
4065
|
+
if (run.status === "active") {
|
|
4066
|
+
return output(`Run ${run.id} is active; no yield receipt yet.\n`, {
|
|
4067
|
+
runId: run.id,
|
|
4068
|
+
status: run.status
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
if (run.yieldReceipt === undefined) {
|
|
4072
|
+
return output(`Run ${run.id} is ${run.status}; no yield receipt recorded.\n`, {
|
|
4073
|
+
runId: run.id,
|
|
4074
|
+
status: run.status
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
return output(`Run ${run.id} yield receipt:\n`
|
|
4078
|
+
+ `Receipt: ${run.yieldReceipt.receiptId}\n`
|
|
4079
|
+
+ `Request: ${run.yieldReceipt.requestId}\n`
|
|
4080
|
+
+ `Committed: ${run.yieldReceipt.committedAt}\n`, { receipt: run.yieldReceipt });
|
|
4081
|
+
}
|
|
3725
4082
|
function yieldRun(args, store, options) {
|
|
3726
4083
|
const usage = "Task run yield usage: yui task run yield <task>/<run> (--summary <text>|--summary-file <path|->).";
|
|
3727
4084
|
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
|
|
3728
4085
|
exactPositionals(parsed.positionals, 1, usage);
|
|
3729
4086
|
const inputSummary = readCommandText(parsed.options.get("--summary"), parsed.options.get("--summary-file"), "--summary", usage);
|
|
3730
4087
|
const now = clock(options);
|
|
4088
|
+
// Issue 04: an already-terminal Run may be a lost-response resend. Match
|
|
4089
|
+
// the presented outcome against the committed receipt before opening the
|
|
4090
|
+
// transaction; the receipt is immutable once committed.
|
|
4091
|
+
const existing = requireRun(store, parsed.positionals[0], options);
|
|
4092
|
+
if (existing.status !== "active") {
|
|
4093
|
+
const replayed = replayYieldReceipt(existing, inputSummary, options);
|
|
4094
|
+
if (replayed !== null)
|
|
4095
|
+
return replayed;
|
|
4096
|
+
throw usageError(`Run ${existing.id} is already terminal: ${existing.status}.`);
|
|
4097
|
+
}
|
|
3731
4098
|
const yielded = store.transaction((tx) => {
|
|
3732
4099
|
const active = requireRun(tx, parsed.positionals[0], options);
|
|
3733
4100
|
if (active.status !== "active") {
|
|
@@ -3775,7 +4142,8 @@ function yieldRun(args, store, options) {
|
|
|
3775
4142
|
throw usageError(`Reported Review evidence commit does not match the managed workspace: ${active.id}.`);
|
|
3776
4143
|
}
|
|
3777
4144
|
}
|
|
3778
|
-
const
|
|
4145
|
+
const yieldOutcome = buildYieldOutcome(active, inputSummary, options);
|
|
4146
|
+
const summary = yieldOutcome.summary;
|
|
3779
4147
|
const terminalization = terminalizeExactTaskRun(tx, {
|
|
3780
4148
|
taskId: task.id,
|
|
3781
4149
|
roleName: role.name,
|
|
@@ -3789,28 +4157,9 @@ function yieldRun(args, store, options) {
|
|
|
3789
4157
|
? {}
|
|
3790
4158
|
: { launchId: options.environment.YUI_LAUNCH_ID }),
|
|
3791
4159
|
outcome: { status: "yielded", summary },
|
|
3792
|
-
...(
|
|
4160
|
+
...(yieldOutcome.reviewResult === undefined
|
|
3793
4161
|
? {}
|
|
3794
|
-
: {
|
|
3795
|
-
reviewResult: {
|
|
3796
|
-
report: yieldedReport.report,
|
|
3797
|
-
checks: yieldedReport.checks,
|
|
3798
|
-
...(yieldedReport.findings === undefined ? {} : { findings: yieldedReport.findings }),
|
|
3799
|
-
...(yieldedReport.evidence === undefined ? {} : { evidence: yieldedReport.evidence }),
|
|
3800
|
-
...(active.purpose === "review"
|
|
3801
|
-
&& options.reviewWorkspaceResult?.evidenceCommit === undefined
|
|
3802
|
-
? {}
|
|
3803
|
-
: active.purpose === "review"
|
|
3804
|
-
? { evidenceCommit: options.reviewWorkspaceResult.evidenceCommit }
|
|
3805
|
-
: yieldedReport.evidenceCommit === undefined
|
|
3806
|
-
? {}
|
|
3807
|
-
: { evidenceCommit: yieldedReport.evidenceCommit }),
|
|
3808
|
-
...(options.executionLaneGitSnapshot === undefined
|
|
3809
|
-
|| options.executionLaneGitSnapshot === null
|
|
3810
|
-
? {}
|
|
3811
|
-
: { gitSnapshot: options.executionLaneGitSnapshot })
|
|
3812
|
-
}
|
|
3813
|
-
})
|
|
4162
|
+
: { reviewResult: yieldOutcome.reviewResult })
|
|
3814
4163
|
}, now);
|
|
3815
4164
|
if (terminalization.disposition !== "applied" || terminalization.run === null) {
|
|
3816
4165
|
throw usageError(`Run ${active.id} no longer matches its exact execution fence: `
|
|
@@ -3978,6 +4327,21 @@ function yieldRun(args, store, options) {
|
|
|
3978
4327
|
...(terminal.workItemId === undefined ? [] : [workItemRef(task.id, terminal.workItemId)])
|
|
3979
4328
|
]);
|
|
3980
4329
|
}
|
|
4330
|
+
// Issue 05: record the Leader's terminal receipt so the Scheduler can
|
|
4331
|
+
// suppress no-change `task-orphaned` wakes. The disposition and digest are
|
|
4332
|
+
// machine-derived from the post-yield projection; the Leader does not
|
|
4333
|
+
// need to cooperate for the admission check to work.
|
|
4334
|
+
if (role.name === LEADER_ROLE) {
|
|
4335
|
+
const receipt = leaderYieldReceipt(tx, task, terminal, now);
|
|
4336
|
+
if (receipt !== null) {
|
|
4337
|
+
tx.saveAgentRun(receipt);
|
|
4338
|
+
return {
|
|
4339
|
+
run: receipt,
|
|
4340
|
+
reviewDispatch,
|
|
4341
|
+
notifyLeader: leaderHandoff !== null
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
3981
4345
|
return {
|
|
3982
4346
|
run: terminal,
|
|
3983
4347
|
reviewDispatch,
|
|
@@ -3998,6 +4362,47 @@ function yieldRun(args, store, options) {
|
|
|
3998
4362
|
: { reviewRound: yielded.reviewDispatch.round })
|
|
3999
4363
|
});
|
|
4000
4364
|
}
|
|
4365
|
+
/**
|
|
4366
|
+
* Issue 05: compute the Leader Run terminal receipt (disposition + observed
|
|
4367
|
+
* actionability digest) from the post-yield projection. Returns the updated
|
|
4368
|
+
* Run, or null when the receipt cannot be computed (the caller keeps the
|
|
4369
|
+
* unmodified terminal Run in that case; the Scheduler fails open).
|
|
4370
|
+
*/
|
|
4371
|
+
function leaderYieldReceipt(tx, task, terminal, now) {
|
|
4372
|
+
try {
|
|
4373
|
+
const projection = buildTaskExecutionProjection(tx, task.id, task);
|
|
4374
|
+
if (projection === null)
|
|
4375
|
+
return null;
|
|
4376
|
+
const disposition = deriveLeaderRunDisposition(projection.status, task.status);
|
|
4377
|
+
const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id));
|
|
4378
|
+
const waitReason = disposition === "waiting" || disposition === "blocked"
|
|
4379
|
+
? leaderWaitReason(projection)
|
|
4380
|
+
: undefined;
|
|
4381
|
+
return {
|
|
4382
|
+
...terminal,
|
|
4383
|
+
disposition,
|
|
4384
|
+
observedActionabilityDigest: digest,
|
|
4385
|
+
...(waitReason === undefined ? {} : { waitReason }),
|
|
4386
|
+
updatedAt: now.toISOString()
|
|
4387
|
+
};
|
|
4388
|
+
}
|
|
4389
|
+
catch {
|
|
4390
|
+
return null;
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function leaderWaitReason(projection) {
|
|
4394
|
+
const blocker = projection.blockers[0];
|
|
4395
|
+
if (blocker !== undefined) {
|
|
4396
|
+
return { kind: blocker.kind, ref: blocker.id };
|
|
4397
|
+
}
|
|
4398
|
+
if (projection.status === "waiting-on-agents") {
|
|
4399
|
+
return { kind: "delegated-work" };
|
|
4400
|
+
}
|
|
4401
|
+
if (projection.status === "waiting-user") {
|
|
4402
|
+
return { kind: "input" };
|
|
4403
|
+
}
|
|
4404
|
+
return undefined;
|
|
4405
|
+
}
|
|
4001
4406
|
/**
|
|
4002
4407
|
* Records a structured progress checkpoint for an active Run. This is a durable
|
|
4003
4408
|
* run fact, not a Task Message: it advances the Run's durable-progress clock so
|
|
@@ -4222,6 +4627,9 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4222
4627
|
.map(({ projectId, commit }) => `${projectId}@${commit}`)
|
|
4223
4628
|
.join(", ")
|
|
4224
4629
|
: `candidate@${round.reviewBaseCommit}`;
|
|
4630
|
+
const findingContext = taskScope
|
|
4631
|
+
? buildTaskFinalReviewFindingContext(tx, taskId, round.taskCandidate).context
|
|
4632
|
+
: "";
|
|
4225
4633
|
const scopeLabel = taskScope ? "Task-final" : "WorkItem";
|
|
4226
4634
|
const projectPolicyPointers = task.projectBindings
|
|
4227
4635
|
.map(({ projectId }) => (`yui project show ${projectId}; yui project knowledge list ${projectId}`))
|
|
@@ -4238,6 +4646,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4238
4646
|
`Review workspace source: exact workspace attached to this Reviewer Lane`,
|
|
4239
4647
|
`Candidate summary: ${candidate.summary}`,
|
|
4240
4648
|
`Acceptance criteria: ${item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
|
|
4649
|
+
...(taskScope ? [findingContext] : []),
|
|
4241
4650
|
"Start from the user's core outcome and the WorkItem intent. The candidate summary is a pointer, not proof: inspect the complete relevant change, callers, and proportionate checks.",
|
|
4242
4651
|
"Keep Yui Core lifecycle safety, generic Reviewer behavior, Project Policy/Knowledge, and the Task Contract separate. Follow Project Policy pointers from the dispatch context for project-specific checks.",
|
|
4243
4652
|
...(round.scope === "task"
|