@zq-silk/yui 0.11.2 → 0.12.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.
- package/ARCHITECTURE.md +23 -4
- package/README.md +80 -13
- package/dist/cli/commandCatalog.js +30 -5
- package/dist/cli.js +105 -43
- package/dist/commands/configCommands.js +12 -45
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/projectCommands.js +581 -2
- package/dist/commands/taskCommands.js +159 -172
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +55 -11
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskIntegrationCommands.js +2 -1
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +24 -0
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +2 -2
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/controller.js +8 -12
- package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
- package/dist/controller/runtime.js +3 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +36 -16
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +1 -2
- package/dist/lifecycle/taskRoleSessionReset.js +5 -13
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/repository/checkoutSwap.js +61 -0
- package/dist/repository/gitWorkspace.js +36 -0
- package/dist/repository/project.js +53 -3
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtimeObservation.js +13 -0
- package/dist/runtime/runtimeProjection.js +16 -25
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/scheduler/activeRoleRunDelivery.js +4 -4
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +3 -5
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +0 -14
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +467 -0
- package/dist/storage/sqliteSchema.js +18 -8
- package/dist/storage/sqliteStore.js +61 -21
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +47 -31
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -24
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +4 -0
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +53 -16
- package/skills/yui-operator/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +28 -7
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -6,7 +6,7 @@ import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
|
6
6
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
8
|
import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
9
|
-
import {
|
|
9
|
+
import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
10
10
|
import { readCommandText } from "./textInput.js";
|
|
11
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
12
12
|
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
@@ -25,6 +25,8 @@ import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProject
|
|
|
25
25
|
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
26
26
|
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
27
27
|
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
28
|
+
import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
|
|
29
|
+
import { projectReviewerAvailability } from "../review/reviewerAvailability.js";
|
|
28
30
|
import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
|
|
29
31
|
import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
|
|
30
32
|
import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
@@ -36,19 +38,19 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
|
|
|
36
38
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
37
39
|
import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
38
40
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
39
|
-
import {
|
|
41
|
+
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
40
42
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
41
43
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
42
44
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
43
|
-
import { resolveProject } from "../repository/project.js";
|
|
45
|
+
import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
44
46
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
45
47
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, planWorkItemExplorationStage, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
46
48
|
import { addExecutionLane, createExecutionGroup, queueExecutionLaneRetry, resolveExecutionGroup, restartExecutionLane, skipPendingExecutionLanes, updateExecutionLane, WORK_ITEM_EXECUTION_MODES } from "../execution/executionGroup.js";
|
|
47
|
-
import { executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
|
|
48
|
-
import {
|
|
49
|
+
import { DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE, executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
|
|
50
|
+
import { resolveControllerTaskConcurrency, resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
49
51
|
import { assertIndependentVerificationRoles, candidateConvergenceEvidenceSufficient, candidateConvergenceDirective, validateCandidateConvergenceResolution } from "../execution/candidateConvergence.js";
|
|
50
52
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
51
|
-
import { classifyReviewRoundOutcome
|
|
53
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
52
54
|
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
|
|
53
55
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
54
56
|
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
@@ -62,7 +64,7 @@ import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCou
|
|
|
62
64
|
import { runGrantCommand } from "./grantCommands.js";
|
|
63
65
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
64
66
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
65
|
-
import {
|
|
67
|
+
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
66
68
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
67
69
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
68
70
|
import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition, projectQueuedResourceLaneIdentities } from "../scheduler/actionability.js";
|
|
@@ -77,8 +79,6 @@ export class TaskFinalReviewDispatchDriftError extends CliError {
|
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
/** Process-local command metadata; symbols never enter JSON output or durable state. */
|
|
80
|
-
export const RESUMED_PENDING_FINAL_REVIEW = Symbol("resumed-pending-final-review");
|
|
81
|
-
export const TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW = Symbol("terminalized-leader-before-final-review");
|
|
82
82
|
function taskFinalReviewDispatchDrift(error) {
|
|
83
83
|
return new TaskFinalReviewDispatchDriftError(error instanceof Error ? error.message : String(error), error instanceof CliError ? error : undefined);
|
|
84
84
|
}
|
|
@@ -235,8 +235,8 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
235
235
|
case "update": return output(updateTaskCommand(rest, store, options));
|
|
236
236
|
case "list": return listTaskCommand(rest, store);
|
|
237
237
|
case "show": return showTaskCommand(rest, store);
|
|
238
|
-
case "context": return runTaskContextCommand(rest, store);
|
|
239
|
-
case "next-action": return runTaskNextActionCommand(rest, store);
|
|
238
|
+
case "context": return runTaskContextCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
239
|
+
case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
240
240
|
case "activate": return output(activateTaskCommand(rest, store, options));
|
|
241
241
|
case "complete": return completeTaskCommand(rest, store, options);
|
|
242
242
|
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
@@ -385,6 +385,7 @@ function taskProjectCommand(args, store, options) {
|
|
|
385
385
|
const project = resolveProject(tx.listProjects(), parsed.positionals[1]);
|
|
386
386
|
if (project === null)
|
|
387
387
|
throw usageError(`Project not found: ${parsed.positionals[1]}.`);
|
|
388
|
+
assertProjectActive(project, "bind to a Task");
|
|
388
389
|
const next = addTaskProjectBinding(task, {
|
|
389
390
|
projectId: project.id,
|
|
390
391
|
directory: parsed.options.get("--directory") ?? project.name,
|
|
@@ -532,6 +533,7 @@ function parseTaskCreation(args, store) {
|
|
|
532
533
|
const project = resolveProject(store.listProjects(), reference);
|
|
533
534
|
if (project === null)
|
|
534
535
|
throw usageError(`Project not found: ${reference}.`);
|
|
536
|
+
assertProjectActive(project, "bind to a Task");
|
|
535
537
|
return project;
|
|
536
538
|
});
|
|
537
539
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
@@ -668,27 +670,34 @@ function showTaskCommand(args, store) {
|
|
|
668
670
|
}
|
|
669
671
|
function activateTaskCommand(args, store, options) {
|
|
670
672
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
671
|
-
const
|
|
673
|
+
const activation = options.taskWorkspaceActivation;
|
|
672
674
|
const result = store.transaction((tx) => {
|
|
673
675
|
const task = requireTask(tx, args[0]);
|
|
674
676
|
if (task.status === "archived")
|
|
675
677
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
678
|
+
if (task.status === "retired")
|
|
679
|
+
throw usageError(`Task is retired: ${task.id}.`);
|
|
676
680
|
if (task.status === "completed") {
|
|
677
681
|
throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
|
|
678
682
|
}
|
|
679
|
-
if (task.status === "active")
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
683
|
+
if (task.status === "active") {
|
|
684
|
+
if (activation === undefined)
|
|
685
|
+
return { task, changed: false };
|
|
686
|
+
const workspace = tx.getTaskWorkspace(task.id);
|
|
687
|
+
if (activation.taskId !== task.id
|
|
688
|
+
|| activation.task.id !== task.id
|
|
689
|
+
|| activation.task.status !== "active"
|
|
690
|
+
|| !isDeepStrictEqual(activation.task.workspaceIdentity, task.workspaceIdentity)
|
|
691
|
+
|| activation.path !== task.cwd
|
|
692
|
+
|| workspace === null
|
|
693
|
+
|| workspace.owner.type !== "task"
|
|
694
|
+
|| workspace.owner.taskId !== task.id
|
|
695
|
+
|| workspace.root !== task.cwd) {
|
|
696
|
+
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
697
|
+
}
|
|
698
|
+
return { task, changed: activation.changed };
|
|
699
|
+
}
|
|
700
|
+
throw usageError(`Task ${task.id} activation requires atomic workspace adoption through the CLI preflight.`);
|
|
692
701
|
});
|
|
693
702
|
if (result.changed) {
|
|
694
703
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
@@ -752,7 +761,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
752
761
|
// readiness via projectCompletionReadiness. The leader-Run check is
|
|
753
762
|
// actor-dependent and stays here; every other blocker is re-validated by
|
|
754
763
|
// the post-Review readiness fence below.
|
|
755
|
-
let terminalizedLeaderRun = false;
|
|
756
764
|
const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
|
|
757
765
|
if (leaderEntry !== undefined) {
|
|
758
766
|
if (actor !== "leader") {
|
|
@@ -775,15 +783,11 @@ function completeTaskCommand(args, store, options) {
|
|
|
775
783
|
if (terminal.disposition !== "applied") {
|
|
776
784
|
throw usageError(`Task Leader Run changed during completion: ${leaderEntry.run.id}/${terminal.reason}.`);
|
|
777
785
|
}
|
|
778
|
-
terminalizedLeaderRun = true;
|
|
779
786
|
}
|
|
780
787
|
// A final (Task-scoped) review is the Task delivery policy: it reviews the
|
|
781
788
|
// complete frozen Task heads, not a single WorkItem Candidate. If the
|
|
782
789
|
// review config requests a final review, create the Task ReviewRound here
|
|
783
790
|
// and return it for dispatch instead of completing the Task.
|
|
784
|
-
const pendingFinalReviewIds = new Set(tx.listReviewRounds(task.id)
|
|
785
|
-
.filter((round) => ((round.scope ?? "work-item") === "task" && round.status === "pending"))
|
|
786
|
-
.map(({ id }) => id));
|
|
787
791
|
const finalReview = prepareFinalTaskReview(tx, task, now, taskFinalContract, options);
|
|
788
792
|
if (finalReview !== null) {
|
|
789
793
|
return {
|
|
@@ -792,8 +796,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
792
796
|
runtimeCleanupTargets: [],
|
|
793
797
|
completionAdvisories: [],
|
|
794
798
|
finalReview,
|
|
795
|
-
resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
|
|
796
|
-
terminalizedLeaderRun,
|
|
797
799
|
publishedTreeAuthorization: undefined
|
|
798
800
|
};
|
|
799
801
|
}
|
|
@@ -814,9 +816,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
814
816
|
tx.saveTask(completed);
|
|
815
817
|
tx.clearPendingWakeup(task.id);
|
|
816
818
|
tx.clearLeaderFailure(task.id);
|
|
817
|
-
tx.clearOperatorNotification(task.id);
|
|
818
|
-
tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
|
|
819
|
-
enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
|
|
820
819
|
if (publishedTreeProof !== undefined) {
|
|
821
820
|
recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
|
|
822
821
|
by: actor,
|
|
@@ -830,7 +829,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
830
829
|
tree: publishedTreeProof.tree
|
|
831
830
|
}, now);
|
|
832
831
|
}
|
|
833
|
-
recordTaskEvent(tx, task.id, "task.completed", {
|
|
832
|
+
const terminalEvent = recordTaskEvent(tx, task.id, "task.completed", {
|
|
834
833
|
by: actor,
|
|
835
834
|
summary,
|
|
836
835
|
...(actualTaskCandidate === undefined
|
|
@@ -844,6 +843,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
844
843
|
? {}
|
|
845
844
|
: { cleanupAdvisories: String(readiness.advisories.length) })
|
|
846
845
|
}, now);
|
|
846
|
+
enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
|
|
847
847
|
// A terminal Task must never leave a Task-lane signal that can wake it.
|
|
848
848
|
// The durable records remain intact; only the derived mailbox work is
|
|
849
849
|
// discarded at this lifecycle boundary.
|
|
@@ -861,8 +861,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
861
861
|
runtimeCleanupTargets,
|
|
862
862
|
completionAdvisories: readiness.advisories,
|
|
863
863
|
finalReview: undefined,
|
|
864
|
-
resumedPendingFinalReview: false,
|
|
865
|
-
terminalizedLeaderRun,
|
|
866
864
|
publishedTreeAuthorization: undefined
|
|
867
865
|
};
|
|
868
866
|
});
|
|
@@ -891,9 +889,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
891
889
|
: `Final Task Review is blocked: ${result.finalReview.summary ?? result.finalReview.id}.`;
|
|
892
890
|
return output(`${status}\n`, {
|
|
893
891
|
task: result.task,
|
|
894
|
-
reviewRound: result.finalReview
|
|
895
|
-
[RESUMED_PENDING_FINAL_REVIEW]: result.resumedPendingFinalReview,
|
|
896
|
-
[TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
|
|
892
|
+
reviewRound: result.finalReview
|
|
897
893
|
});
|
|
898
894
|
}
|
|
899
895
|
const completionOutput = result.changed
|
|
@@ -933,7 +929,6 @@ function reopenTaskCommand(args, store, options) {
|
|
|
933
929
|
throw usageError(`Task is not completed: ${task.id}.`);
|
|
934
930
|
const active = reopenTask(task, now);
|
|
935
931
|
tx.saveTask(active);
|
|
936
|
-
tx.clearOperatorNotification(task.id);
|
|
937
932
|
const reopenedReason = wakeReason("task-reopened");
|
|
938
933
|
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
939
934
|
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
@@ -995,7 +990,6 @@ function archiveTaskCommand(args, store, options) {
|
|
|
995
990
|
tx.saveTask(archived);
|
|
996
991
|
tx.clearPendingWakeup(task.id);
|
|
997
992
|
tx.clearLeaderFailure(task.id);
|
|
998
|
-
tx.clearOperatorNotification(task.id);
|
|
999
993
|
for (const role of tx.listRoles(task.id)) {
|
|
1000
994
|
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
1001
995
|
}
|
|
@@ -1093,20 +1087,18 @@ function retireTaskCommand(args, store, options) {
|
|
|
1093
1087
|
}
|
|
1094
1088
|
tx.clearPendingWakeup(task.id);
|
|
1095
1089
|
tx.clearLeaderFailure(task.id);
|
|
1096
|
-
tx.clearOperatorNotification(task.id);
|
|
1097
1090
|
const retired = retireTask(task, {
|
|
1098
1091
|
by: actor,
|
|
1099
1092
|
summary,
|
|
1100
1093
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1101
1094
|
}, now);
|
|
1102
1095
|
tx.saveTask(retired);
|
|
1103
|
-
tx
|
|
1104
|
-
enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
|
|
1105
|
-
recordTaskEvent(tx, task.id, "task.retired", {
|
|
1096
|
+
const terminalEvent = recordTaskEvent(tx, task.id, "task.retired", {
|
|
1106
1097
|
by: actor,
|
|
1107
1098
|
summary,
|
|
1108
1099
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1109
1100
|
}, now);
|
|
1101
|
+
enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
|
|
1110
1102
|
return { task: retired, changed: true };
|
|
1111
1103
|
});
|
|
1112
1104
|
if (result.changed) {
|
|
@@ -1857,6 +1849,7 @@ function createWork(args, store, options) {
|
|
|
1857
1849
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
1858
1850
|
if (project === null)
|
|
1859
1851
|
throw usageError(`Task Project not found: ${reference}.`);
|
|
1852
|
+
assertProjectActive(project, "scope a Work Item");
|
|
1860
1853
|
return project.id;
|
|
1861
1854
|
});
|
|
1862
1855
|
const baseRefs = parsed.baseRefs.map(({ project, baseRef }) => {
|
|
@@ -1914,6 +1907,7 @@ function updateWorkScope(args, store, options) {
|
|
|
1914
1907
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
1915
1908
|
if (project === null)
|
|
1916
1909
|
throw usageError(`Task Project not found: ${reference}.`);
|
|
1910
|
+
assertProjectActive(project, "scope a Work Item");
|
|
1917
1911
|
return project.id;
|
|
1918
1912
|
});
|
|
1919
1913
|
const requested = new Set(requestedProjectIds);
|
|
@@ -2227,7 +2221,6 @@ function dispatchWork(args, store, options) {
|
|
|
2227
2221
|
? undefined
|
|
2228
2222
|
: (() => {
|
|
2229
2223
|
const config = tx.getConfig();
|
|
2230
|
-
const contextBudget = resolveContextBudget(config.contextBudget);
|
|
2231
2224
|
const health = resolveRuntimeHealth(config.runtimeHealth);
|
|
2232
2225
|
const maxWallClockSeconds = requestedStageMaxSeconds
|
|
2233
2226
|
?? Math.floor(health.stallWindowMs / 1_000);
|
|
@@ -2246,7 +2239,8 @@ function dispatchWork(args, store, options) {
|
|
|
2246
2239
|
? { convergence: { schemaVersion: 1 } }
|
|
2247
2240
|
: {}),
|
|
2248
2241
|
resourceBudget: {
|
|
2249
|
-
maxTokens: requestedStageMaxTokens
|
|
2242
|
+
maxTokens: requestedStageMaxTokens
|
|
2243
|
+
?? DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE * available,
|
|
2250
2244
|
maxToolCalls: requestedStageMaxToolCalls ?? 100 * available,
|
|
2251
2245
|
maxWallClockSeconds
|
|
2252
2246
|
},
|
|
@@ -3517,12 +3511,6 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3517
3511
|
}
|
|
3518
3512
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3519
3513
|
if (deltaRecheckRequested) {
|
|
3520
|
-
const reviewConfig = tx.getReviewConfig();
|
|
3521
|
-
if (reviewConfig === null || reviewConfig.deltaRecheck !== "enabled") {
|
|
3522
|
-
throw usageError("Delta-recheck is not enabled for this Project's review policy. "
|
|
3523
|
-
+ "Set `yui config workflow set review --role <role> --trigger final --delta-recheck enabled` "
|
|
3524
|
-
+ "or request a full Review.");
|
|
3525
|
-
}
|
|
3526
3514
|
if (taskFinalContract !== undefined) {
|
|
3527
3515
|
throw usageError("Delta-recheck is not supported with a Task-final review contract.");
|
|
3528
3516
|
}
|
|
@@ -3537,15 +3525,24 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3537
3525
|
}
|
|
3538
3526
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id))
|
|
3539
3527
|
.filter((entry) => (entry.scope ?? "work-item") === "task");
|
|
3528
|
+
const latestNonAcceptingDelta = deltaRecheckRequested
|
|
3529
|
+
? undefined
|
|
3530
|
+
: taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
|
|
3531
|
+
&& entry.deltaRecheck !== undefined
|
|
3532
|
+
&& deltaRecheckBlocksAcceptance(entry)
|
|
3533
|
+
&& isSameTaskReviewCandidate(entry.taskCandidate, provenance.candidate))).at(-1);
|
|
3540
3534
|
const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
|
|
3535
|
+
&& (deltaRecheckRequested
|
|
3536
|
+
? entry.deltaRecheck !== undefined
|
|
3537
|
+
: entry.deltaRecheck === undefined)
|
|
3541
3538
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(entry.taskFinalReviewContract, taskFinalContract))
|
|
3542
3539
|
&& isSameTaskReviewCandidate(entry.taskCandidate, provenance.candidate))).at(-1);
|
|
3540
|
+
if (exact?.status === "failed") {
|
|
3541
|
+
throw usageError(`Explicit Task-final ReviewRound ${exact.id} is failed for this exact candidate; resolve it before requesting again.`);
|
|
3542
|
+
}
|
|
3543
3543
|
if (exact !== undefined
|
|
3544
|
-
&& (
|
|
3545
|
-
|
|
3546
|
-
throw usageError(`Explicit Task-final ReviewRound ${exact.id} is failed for this exact candidate; resolve it before requesting again.`);
|
|
3547
|
-
}
|
|
3548
|
-
assertNoConflictingTaskReviewRound(taskRounds, exact.id);
|
|
3544
|
+
&& (exact.status === "pending" || exact.status === "running")) {
|
|
3545
|
+
assertNoConflictingTaskReviewRound(taskRounds, exact.id, reviewerRoleName);
|
|
3549
3546
|
if (requestedLaneRoles.length === 0) {
|
|
3550
3547
|
if (exact.status === "running"
|
|
3551
3548
|
&& exact.executionGroup?.resolution === undefined
|
|
@@ -3575,9 +3572,8 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3575
3572
|
if (exact.executionGroup.lanes.length + laneRoles.filter((role) => (!exact.executionGroup.lanes.some((lane) => lane.roleName === role))).length > strategy.max) {
|
|
3576
3573
|
throw usageError(`Reviewer Lane count exceeds adaptive capacity ${strategy.max}.`);
|
|
3577
3574
|
}
|
|
3578
|
-
let appended = exact.executionGroup;
|
|
3579
3575
|
for (const laneRoleName of laneRoles) {
|
|
3580
|
-
const existingLane =
|
|
3576
|
+
const existingLane = exact.executionGroup.lanes.find(({ roleName }) => (roleName === laneRoleName));
|
|
3581
3577
|
if (existingLane !== undefined) {
|
|
3582
3578
|
if (existingLane.runId === undefined)
|
|
3583
3579
|
continue;
|
|
@@ -3597,29 +3593,32 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3597
3593
|
if (taskReviewProducerCollision(provenance, laneRoleName) !== null) {
|
|
3598
3594
|
throw usageError(`Reviewer Role must be separate from the Candidate producer: ${laneRoleName}.`);
|
|
3599
3595
|
}
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3596
|
+
const availability = projectReviewerAvailability(tx, task.id, laneRoleName);
|
|
3597
|
+
if (availability.kind === "busy")
|
|
3598
|
+
return availability;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
let appended = exact.executionGroup;
|
|
3602
|
+
for (const laneRoleName of laneRoles) {
|
|
3603
|
+
if (appended.lanes.some(({ roleName }) => roleName === laneRoleName))
|
|
3604
|
+
continue;
|
|
3605
|
+
let laneRole = tx.getRole(task.id, laneRoleName);
|
|
3606
|
+
if (laneRole === null) {
|
|
3607
|
+
laneRole = createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName);
|
|
3608
|
+
tx.saveRole(task.id, laneRole);
|
|
3610
3609
|
}
|
|
3610
|
+
appended = addExecutionLane(appended, {
|
|
3611
|
+
roleName: laneRole.name,
|
|
3612
|
+
reviewRoundId: exact.id
|
|
3613
|
+
}, now);
|
|
3611
3614
|
}
|
|
3612
3615
|
const updated = updateReviewExecutionGroup(exact, appended);
|
|
3613
3616
|
tx.saveReviewRound(task.id, updated);
|
|
3614
3617
|
return updated;
|
|
3615
3618
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
if (reviewer === null) {
|
|
3620
|
-
reviewer = createTaskRole(tx, task, reviewerRoleName, undefined, now, reviewerRoleName);
|
|
3621
|
-
tx.saveRole(task.id, reviewer);
|
|
3622
|
-
}
|
|
3619
|
+
const primaryAvailability = projectReviewerAvailability(tx, task.id, reviewerRoleName);
|
|
3620
|
+
if (primaryAvailability.kind === "busy")
|
|
3621
|
+
return primaryAvailability;
|
|
3623
3622
|
const laneRoles = requestedLaneRoles.length === 0
|
|
3624
3623
|
? [reviewerRoleName]
|
|
3625
3624
|
: requestedLaneRoles[0] === reviewerRoleName
|
|
@@ -3638,22 +3637,24 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3638
3637
|
if (tx.getGlobalRole(laneRoleName) === null && tx.getRole(task.id, laneRoleName) === null) {
|
|
3639
3638
|
throw usageError(`Global Role not found: ${laneRoleName}.`);
|
|
3640
3639
|
}
|
|
3641
|
-
|
|
3642
|
-
if (
|
|
3643
|
-
|
|
3644
|
-
|
|
3640
|
+
const availability = projectReviewerAvailability(tx, task.id, laneRoleName);
|
|
3641
|
+
if (availability.kind === "busy")
|
|
3642
|
+
return availability;
|
|
3643
|
+
if (taskReviewProducerCollision(provenance, laneRoleName) !== null) {
|
|
3644
|
+
throw usageError(`Reviewer Role must be separate from the Candidate producer: ${laneRoleName}.`);
|
|
3645
3645
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3646
|
+
}
|
|
3647
|
+
for (const laneRoleName of laneRoles) {
|
|
3648
|
+
if (tx.getRole(task.id, laneRoleName) === null) {
|
|
3649
|
+
tx.saveRole(task.id, createTaskRole(tx, task, laneRoleName, undefined, now, laneRoleName));
|
|
3648
3650
|
}
|
|
3649
|
-
assertTaskReviewRequestLane(tx, task.id, laneRole.name);
|
|
3650
3651
|
}
|
|
3651
3652
|
let deltaRecord;
|
|
3652
3653
|
if (deltaRecheckRequested) {
|
|
3653
3654
|
if (requestedLaneRoles.length > 0 || requestedStrategy !== undefined) {
|
|
3654
3655
|
throw usageError("Delta-recheck supports only the default single Reviewer Lane.");
|
|
3655
3656
|
}
|
|
3656
|
-
deltaRecord = validateDeltaRecheckRequest(tx, task.id,
|
|
3657
|
+
deltaRecord = validateDeltaRecheckRequest(tx, task.id, provenance.candidate, options.deltaRecheckPreflight);
|
|
3657
3658
|
}
|
|
3658
3659
|
let created = deltaRecord === undefined
|
|
3659
3660
|
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
|
|
@@ -3671,15 +3672,14 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3671
3672
|
tx.saveReviewRound(task.id, created);
|
|
3672
3673
|
// Issue 07: when a full Review is created after a non-accepting delta
|
|
3673
3674
|
// disposition, record the escalation lineage on the delta Round.
|
|
3674
|
-
if (
|
|
3675
|
-
&& deltaRecheckBlocksAcceptance(exact)
|
|
3675
|
+
if (latestNonAcceptingDelta !== undefined
|
|
3676
3676
|
&& created.deltaRecheck === undefined
|
|
3677
|
-
&&
|
|
3678
|
-
&&
|
|
3677
|
+
&& latestNonAcceptingDelta.deltaRecheck !== undefined
|
|
3678
|
+
&& latestNonAcceptingDelta.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
3679
3679
|
tx.saveReviewRound(task.id, {
|
|
3680
|
-
...
|
|
3680
|
+
...latestNonAcceptingDelta,
|
|
3681
3681
|
deltaRecheck: {
|
|
3682
|
-
...
|
|
3682
|
+
...latestNonAcceptingDelta.deltaRecheck,
|
|
3683
3683
|
escalatedToReviewRoundId: created.id
|
|
3684
3684
|
}
|
|
3685
3685
|
});
|
|
@@ -3699,11 +3699,20 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3699
3699
|
}, now);
|
|
3700
3700
|
return created;
|
|
3701
3701
|
});
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
?
|
|
3705
|
-
|
|
3706
|
-
|
|
3702
|
+
if ("kind" in round && round.kind === "busy") {
|
|
3703
|
+
return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
|
|
3704
|
+
+ `${round.activeRunId === undefined ? "" : `; Run ${round.activeRunId}`}); `
|
|
3705
|
+
+ `${round.activeReviewRoundId === undefined
|
|
3706
|
+
? ""
|
|
3707
|
+
: `active ReviewRound ${round.activeReviewRoundId}; `}`
|
|
3708
|
+
+ `retry after ${round.retryAfterSeconds}s or choose another Reviewer.\n`, { reviewRequest: round });
|
|
3709
|
+
}
|
|
3710
|
+
const requestedRound = round;
|
|
3711
|
+
return output(requestedRound.status === "pending"
|
|
3712
|
+
? requestedRound.deltaRecheck === undefined
|
|
3713
|
+
? `Task-final Review requested as ${requestedRound.id}\n`
|
|
3714
|
+
: `Task-final delta-recheck requested as ${requestedRound.id} (rechecks ${requestedRound.deltaRecheck.previousReviewRoundId})\n`
|
|
3715
|
+
: `Task-final Review is already ${requestedRound.status}: ${requestedRound.id}\n`, { reviewRound: requestedRound });
|
|
3707
3716
|
}
|
|
3708
3717
|
const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
3709
3718
|
/**
|
|
@@ -3786,12 +3795,12 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3786
3795
|
if (sourceIndex < 0) {
|
|
3787
3796
|
throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
|
|
3788
3797
|
}
|
|
3789
|
-
const laterRound = taskRounds.slice(sourceIndex + 1).
|
|
3798
|
+
const laterRound = taskRounds.slice(sourceIndex + 1).find((entry) => (entry.reviewerRoleName === source.reviewerRoleName));
|
|
3790
3799
|
if (laterRound !== undefined) {
|
|
3791
3800
|
throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
|
|
3792
3801
|
+ `${laterRound.id}/${laterRound.status}.`);
|
|
3793
3802
|
}
|
|
3794
|
-
assertNoConflictingTaskReviewRound(taskRounds, source.id);
|
|
3803
|
+
assertNoConflictingTaskReviewRound(taskRounds, source.id, source.reviewerRoleName);
|
|
3795
3804
|
assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
|
|
3796
3805
|
let reviewer = tx.getRole(task.id, source.reviewerRoleName);
|
|
3797
3806
|
if (reviewer === null) {
|
|
@@ -3849,22 +3858,17 @@ export function classifyForceFreshReviewRecovery(store, round) {
|
|
|
3849
3858
|
* Review or an equivalent-and-accepted delta) so a delta never extends a
|
|
3850
3859
|
* non-accepting disposition.
|
|
3851
3860
|
*/
|
|
3852
|
-
function validateDeltaRecheckRequest(store, taskId,
|
|
3861
|
+
function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
|
|
3853
3862
|
if (preflight === undefined) {
|
|
3854
3863
|
throw usageError("Delta-recheck assessment is missing; the CLI preflight did not run. "
|
|
3855
3864
|
+ "Request a full Review or retry with a current CLI.");
|
|
3856
3865
|
}
|
|
3857
3866
|
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
3858
3867
|
if (previous === null
|
|
3859
|
-
|| (previous
|
|
3860
|
-
|
|
3861
|
-
throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
|
|
3868
|
+
|| !isAcceptedTaskReviewBaseline(store, previous)) {
|
|
3869
|
+
throw usageError(`Delta-recheck previous ReviewRound is not an accepted Task-final baseline: `
|
|
3862
3870
|
+ `${preflight.record.previousReviewRoundId}.`);
|
|
3863
3871
|
}
|
|
3864
|
-
if (previous.reviewerRoleName !== reviewerRoleName) {
|
|
3865
|
-
throw usageError(`Delta-recheck Reviewer Role must match the previous acceptance: `
|
|
3866
|
-
+ `${previous.reviewerRoleName}.`);
|
|
3867
|
-
}
|
|
3868
3872
|
if (previous.reviewBaseCommit !== preflight.record.previousBaseCommit) {
|
|
3869
3873
|
throw usageError("Delta-recheck previous base commit does not match the recorded acceptance.");
|
|
3870
3874
|
}
|
|
@@ -3872,7 +3876,7 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
|
|
|
3872
3876
|
if (previous.deltaRecheck !== undefined
|
|
3873
3877
|
&& previous.deltaRecheck.disposition !== "equivalent-and-accepted") {
|
|
3874
3878
|
throw usageError(`Delta-recheck cannot extend ${previous.id}: its disposition is `
|
|
3875
|
-
+ `${previous.deltaRecheck.disposition}.
|
|
3879
|
+
+ `${previous.deltaRecheck.disposition}. The Leader must choose the next Review action.`);
|
|
3876
3880
|
}
|
|
3877
3881
|
if (candidate.projects[0].commit === preflight.record.previousBaseCommit) {
|
|
3878
3882
|
throw usageError("Delta-recheck candidate head is unchanged; the previous acceptance already covers it.");
|
|
@@ -3982,12 +3986,12 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3982
3986
|
}
|
|
3983
3987
|
// Issue 06: infra retries reuse the same semantic Round ID. Any later
|
|
3984
3988
|
// Round supersedes this one; an active Round for the same Reviewer blocks.
|
|
3985
|
-
const conflictingLater = reviewerRounds.slice(roundIndex + 1).
|
|
3989
|
+
const conflictingLater = reviewerRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === round.reviewerRoleName));
|
|
3986
3990
|
if (conflictingLater !== undefined) {
|
|
3987
3991
|
throw usageError(`A newer conflicting final ReviewRound already exists after ${round.id}: `
|
|
3988
3992
|
+ `${conflictingLater.id}/${conflictingLater.status}.`);
|
|
3989
3993
|
}
|
|
3990
|
-
assertNoConflictingTaskReviewRound(reviewerRounds, round.id);
|
|
3994
|
+
assertNoConflictingTaskReviewRound(reviewerRounds, round.id, round.reviewerRoleName);
|
|
3991
3995
|
const activeRound = reviewerRounds.find((entry) => (entry.id !== round.id
|
|
3992
3996
|
&& entry.reviewerRoleName === round.reviewerRoleName
|
|
3993
3997
|
&& (entry.status === "pending" || entry.status === "running")));
|
|
@@ -4258,7 +4262,9 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4258
4262
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4259
4263
|
throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
4260
4264
|
}
|
|
4261
|
-
const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review"
|
|
4265
|
+
const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review"
|
|
4266
|
+
&& entry.roleName === round.reviewerRoleName
|
|
4267
|
+
&& entry.status === "active"));
|
|
4262
4268
|
const activeRoleRun = tx.getActiveAgentRun(task.id, round.reviewerRoleName);
|
|
4263
4269
|
if (activeReviewerRun !== undefined || activeRoleRun !== null) {
|
|
4264
4270
|
const active = activeReviewerRun ?? activeRoleRun;
|
|
@@ -4270,7 +4276,7 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4270
4276
|
if (roundIndex < 0) {
|
|
4271
4277
|
throw dataError(`Final ReviewRound is not in Task history: ${round.id}.`);
|
|
4272
4278
|
}
|
|
4273
|
-
const laterRound = taskRounds.slice(roundIndex + 1).
|
|
4279
|
+
const laterRound = taskRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === round.reviewerRoleName));
|
|
4274
4280
|
if (laterRound !== undefined) {
|
|
4275
4281
|
throw usageError(`A later final ReviewRound already exists: ${laterRound.id}/${laterRound.status}.`);
|
|
4276
4282
|
}
|
|
@@ -4747,54 +4753,50 @@ function isSameTaskReviewCandidate(left, right) {
|
|
|
4747
4753
|
return left !== undefined && isDeepStrictEqual(left, right);
|
|
4748
4754
|
}
|
|
4749
4755
|
/**
|
|
4750
|
-
*
|
|
4751
|
-
*
|
|
4752
|
-
* Round is a conflicting lane regardless of its Reviewer Role.
|
|
4756
|
+
* One Reviewer Role has one live Task-final Review lane. Different Reviewer
|
|
4757
|
+
* Roles own independent Session/Workspace slots and may review concurrently.
|
|
4753
4758
|
*/
|
|
4754
|
-
function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = []) {
|
|
4759
|
+
function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = [], reviewerRoleName) {
|
|
4755
4760
|
const reusable = new Set(typeof reusableRoundIds === "string" ? [reusableRoundIds] : reusableRoundIds);
|
|
4756
4761
|
const conflicting = rounds.find((entry) => (!reusable.has(entry.id)
|
|
4757
4762
|
&& (entry.scope ?? "work-item") === "task"
|
|
4758
|
-
&& (entry.status === "pending" || entry.status === "running")
|
|
4763
|
+
&& (entry.status === "pending" || entry.status === "running")
|
|
4764
|
+
&& (reviewerRoleName === undefined || entry.reviewerRoleName === reviewerRoleName)));
|
|
4759
4765
|
if (conflicting !== undefined) {
|
|
4760
4766
|
throw usageError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4761
4767
|
}
|
|
4762
4768
|
}
|
|
4763
4769
|
/**
|
|
4764
|
-
* Queues a Task-scoped final ReviewRound
|
|
4765
|
-
*
|
|
4766
|
-
*
|
|
4770
|
+
* Queues a Task-scoped final ReviewRound only after the Reviewer lane passes
|
|
4771
|
+
* its mechanical preflight. Busy or unavailable preparation never becomes a
|
|
4772
|
+
* failed semantic ReviewRound.
|
|
4767
4773
|
*/
|
|
4768
4774
|
function queueTaskReviewRound(store, task, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
|
|
4769
|
-
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id));
|
|
4770
4775
|
const provenance = taskReviewProvenance(store, task, options, taskCandidate);
|
|
4771
4776
|
if (!isSameTaskReviewCandidate(provenance.candidate, taskCandidate)) {
|
|
4772
4777
|
throw usageError(`Task-final ReviewRound frozen Task heads changed before queueing.`);
|
|
4773
4778
|
}
|
|
4774
|
-
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
|
|
4775
|
-
store.saveReviewRound(task.id, pending);
|
|
4776
4779
|
const producerCollision = taskReviewProducerCollision(provenance, config.roleName);
|
|
4777
4780
|
if (producerCollision !== null) {
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
+
throw usageError(producerCollision);
|
|
4782
|
+
}
|
|
4783
|
+
const availability = projectReviewerAvailability(store, task.id, config.roleName);
|
|
4784
|
+
if (availability.kind === "busy") {
|
|
4785
|
+
throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
|
|
4786
|
+
+ `${availability.activeRunId === undefined ? "" : `; Run ${availability.activeRunId}`}); `
|
|
4787
|
+
+ `retry after ${availability.retryAfterSeconds}s.`);
|
|
4781
4788
|
}
|
|
4782
4789
|
let reviewer = store.getRole(task.id, config.roleName);
|
|
4783
4790
|
if (reviewer === null) {
|
|
4784
4791
|
const globalRole = store.getGlobalRole(config.roleName);
|
|
4785
4792
|
if (globalRole === null) {
|
|
4786
|
-
|
|
4787
|
-
store.saveReviewRound(task.id, failed);
|
|
4788
|
-
return failed;
|
|
4793
|
+
throw usageError(`Global Role not found: ${config.roleName}.`);
|
|
4789
4794
|
}
|
|
4790
4795
|
reviewer = createTaskRole(store, task, config.roleName, undefined, now, config.roleName);
|
|
4791
4796
|
store.saveRole(task.id, reviewer);
|
|
4792
4797
|
}
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
store.saveReviewRound(task.id, failed);
|
|
4796
|
-
return failed;
|
|
4797
|
-
}
|
|
4798
|
+
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
|
|
4799
|
+
store.saveReviewRound(task.id, pending);
|
|
4798
4800
|
return pending;
|
|
4799
4801
|
}
|
|
4800
4802
|
/**
|
|
@@ -4829,33 +4831,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4829
4831
|
// review evidence. Do not create duplicate rounds on repeated completion
|
|
4830
4832
|
// attempts. A failed round remains a blocker until the Leader changes the
|
|
4831
4833
|
// candidate or otherwise resolves the failed evidence explicitly.
|
|
4832
|
-
//
|
|
4833
|
-
//
|
|
4834
|
-
//
|
|
4835
|
-
|
|
4836
|
-
&& latest.deltaRecheck !== undefined
|
|
4837
|
-
&& latest.deltaRecheck.disposition === "requires-full-review") {
|
|
4838
|
-
// Fall through to queue a full Review for the same candidate.
|
|
4839
|
-
const escalated = queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4840
|
-
// Record the escalation lineage on the delta Round so the full Review
|
|
4841
|
-
// is traceable from the non-accepting delta disposition.
|
|
4842
|
-
if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
4843
|
-
store.saveReviewRound(task.id, {
|
|
4844
|
-
...latest,
|
|
4845
|
-
deltaRecheck: {
|
|
4846
|
-
...latest.deltaRecheck,
|
|
4847
|
-
escalatedToReviewRoundId: escalated.id
|
|
4848
|
-
}
|
|
4849
|
-
});
|
|
4850
|
-
}
|
|
4851
|
-
return escalated;
|
|
4852
|
-
}
|
|
4853
|
-
else {
|
|
4854
|
-
return latest.status === "completed"
|
|
4855
|
-
&& classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
|
|
4856
|
-
? null
|
|
4857
|
-
: latest;
|
|
4858
|
-
}
|
|
4834
|
+
// A non-accepting delta is durable evidence for the Leader, not an
|
|
4835
|
+
// instruction for Core to manufacture a full Review. Completion remains
|
|
4836
|
+
// blocked until the Leader explicitly chooses and obtains valid evidence.
|
|
4837
|
+
return isAcceptedTaskReviewBaseline(store, latest) ? null : latest;
|
|
4859
4838
|
}
|
|
4860
4839
|
return queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4861
4840
|
}
|
|
@@ -4876,7 +4855,7 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4876
4855
|
if (!isSameTaskReviewCandidate(round.taskCandidate, taskCandidate)) {
|
|
4877
4856
|
throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
4878
4857
|
}
|
|
4879
|
-
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id);
|
|
4858
|
+
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id, round.reviewerRoleName);
|
|
4880
4859
|
const reviewer = store.getRole(task.id, round.reviewerRoleName);
|
|
4881
4860
|
if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
|
|
4882
4861
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
@@ -5000,12 +4979,12 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5000
4979
|
if (roundIndex < 0) {
|
|
5001
4980
|
throw dataError(`Final ReviewRound is not in Task history: ${round.id}.`);
|
|
5002
4981
|
}
|
|
5003
|
-
const laterRound = taskRounds.slice(roundIndex + 1).
|
|
4982
|
+
const laterRound = taskRounds.slice(roundIndex + 1).find((entry) => (entry.reviewerRoleName === retryReviewerRoleName));
|
|
5004
4983
|
if (laterRound !== undefined && !isSameTaskReviewCandidate(laterRound.taskCandidate, round.taskCandidate)) {
|
|
5005
4984
|
throw usageError(`A newer final Task candidate already has ReviewRound ${laterRound.id}.`);
|
|
5006
4985
|
}
|
|
5007
4986
|
const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === retryReviewerRoleName)));
|
|
5008
|
-
assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id);
|
|
4987
|
+
assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id, retryReviewerRoleName);
|
|
5009
4988
|
const activeRound = allReviewerRounds.find((entry) => (entry.id !== round.id
|
|
5010
4989
|
&& (entry.status === "pending" || entry.status === "running")));
|
|
5011
4990
|
if (activeRound !== undefined) {
|
|
@@ -5531,7 +5510,6 @@ function yieldRun(args, store, options) {
|
|
|
5531
5510
|
kind: "yield"
|
|
5532
5511
|
}, now);
|
|
5533
5512
|
}
|
|
5534
|
-
clearMatchingLeaderStallAttention(tx, task.id, active.id);
|
|
5535
5513
|
if (active.purpose === "review") {
|
|
5536
5514
|
const round = active.reviewRoundId === undefined
|
|
5537
5515
|
? null
|
|
@@ -5810,7 +5788,6 @@ function checkpointRun(args, store, options) {
|
|
|
5810
5788
|
kind: "checkpoint"
|
|
5811
5789
|
}, now);
|
|
5812
5790
|
}
|
|
5813
|
-
clearMatchingLeaderStallAttention(tx, task.id, run.id);
|
|
5814
5791
|
return progress;
|
|
5815
5792
|
});
|
|
5816
5793
|
return `Checkpoint recorded for ${parsed.positionals[0]} (${event.id}).\n`;
|
|
@@ -5953,9 +5930,13 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5953
5930
|
throw usageError(message);
|
|
5954
5931
|
}
|
|
5955
5932
|
if (taskScope) {
|
|
5933
|
+
const requestedReviewers = new Set(round.executionGroup?.lanes.map(({ roleName }) => roleName)
|
|
5934
|
+
?? [round.reviewerRoleName]);
|
|
5956
5935
|
const conflicting = tx.listReviewRounds(task.id).find((entry) => (entry.id !== round.id
|
|
5957
5936
|
&& (entry.scope ?? "work-item") === "task"
|
|
5958
5937
|
&& (entry.status === "pending" || entry.status === "running")
|
|
5938
|
+
&& (entry.executionGroup?.lanes.some(({ roleName }) => requestedReviewers.has(roleName))
|
|
5939
|
+
?? requestedReviewers.has(entry.reviewerRoleName))
|
|
5959
5940
|
&& !(entry.status === "running"
|
|
5960
5941
|
&& entry.reviewerRunId !== undefined
|
|
5961
5942
|
&& tx.getAgentRun(task.id, entry.reviewerRunId)?.status === "failed")));
|
|
@@ -5999,8 +5980,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5999
5980
|
let deltaContext = "";
|
|
6000
5981
|
if (taskScope && round.deltaRecheck !== undefined) {
|
|
6001
5982
|
const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
|
|
6002
|
-
if (previousRound === null || !
|
|
6003
|
-
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck
|
|
5983
|
+
if (previousRound === null || !isAcceptedTaskReviewBaseline(tx, previousRound)) {
|
|
5984
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck accepted baseline is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
|
|
6004
5985
|
}
|
|
6005
5986
|
const diffByProject = options.deltaRecheckDiff;
|
|
6006
5987
|
if (diffByProject === undefined) {
|
|
@@ -6044,9 +6025,9 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
6044
6025
|
"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.",
|
|
6045
6026
|
"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.",
|
|
6046
6027
|
...(round.scope === "task" && round.deltaRecheck === undefined
|
|
6047
|
-
? ["This is
|
|
6028
|
+
? ["This is a full Task Review: inspect every bound Project at the frozen Task heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
|
|
6048
6029
|
: []),
|
|
6049
|
-
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound
|
|
6030
|
+
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this stable Reviewer workspace at the exact ReviewRound snapshot.",
|
|
6050
6031
|
"Do not push, integrate, mutate Task state, touch the Candidate or Worker workspace, another Task/workspace, a stable checkout, or the real Yui control-plane home.",
|
|
6051
6032
|
"Use the exact --summary-file - body to report complete findings, evidence, checks actually run, uncertainty, and recommended next actions in clear Markdown or JSON. Yui preserves the full report; no fixed wording or field list is required. If you include evidenceCommit, it must match the managed Review workspace.",
|
|
6052
6033
|
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. Review yield completes only this Round and creates no Candidate or ChangeSet.",
|
|
@@ -6244,7 +6225,13 @@ export function failPendingReviewRound(taskId, reviewRoundId, summary, store, op
|
|
|
6244
6225
|
return round;
|
|
6245
6226
|
const terminal = finishReviewRound(round, "failed", summary, now);
|
|
6246
6227
|
tx.saveReviewRound(taskId, terminal);
|
|
6228
|
+
const event = recordTaskEventRecord(tx, taskId, "review.failed-to-start", {
|
|
6229
|
+
reviewRoundId: terminal.id,
|
|
6230
|
+
reviewerRoleName: terminal.reviewerRoleName,
|
|
6231
|
+
reason: summary
|
|
6232
|
+
}, now);
|
|
6247
6233
|
enqueueWork(tx, leaderMailbox(taskId), "review-failed", now, [
|
|
6234
|
+
eventRef(taskId, event.id),
|
|
6248
6235
|
...(round.workItemId === undefined ? [] : [workItemRef(taskId, round.workItemId)])
|
|
6249
6236
|
]);
|
|
6250
6237
|
return terminal;
|
|
@@ -6330,7 +6317,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
|
6330
6317
|
return message;
|
|
6331
6318
|
}
|
|
6332
6319
|
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
6333
|
-
recordTaskEventRecord(store, taskId, type, payload, now);
|
|
6320
|
+
return recordTaskEventRecord(store, taskId, type, payload, now);
|
|
6334
6321
|
}
|
|
6335
6322
|
function leaderActionEventPayload(store, taskId, options) {
|
|
6336
6323
|
const runId = taskLeaderActionRunId(store, taskId, options.environment, options.yuiHome);
|