@zq-silk/yui 0.6.0 → 0.6.2
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/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 +30 -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 +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -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 +100 -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 +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- 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/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -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/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- 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 +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -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 +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { sameTaskFinalReviewContract, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
3
|
+
import { currentWorkItemCandidate } from "../workItem/workItem.js";
|
|
4
|
+
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
5
|
+
export function projectNextAction(facts) {
|
|
6
|
+
const { task } = facts;
|
|
7
|
+
if (task.status !== "active" && task.status !== "draft") {
|
|
8
|
+
return buildAction(facts, {
|
|
9
|
+
kind: "complete-task",
|
|
10
|
+
reason: `Task ${task.id} is ${task.status}; no further protocol action is available.`,
|
|
11
|
+
refs: [ref("task", task.id)],
|
|
12
|
+
preconditions: [
|
|
13
|
+
{ fact: `Task status is ${task.status}`, satisfied: true, ref: ref("task", task.id) }
|
|
14
|
+
]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const openInput = facts.openInputRequests[0];
|
|
18
|
+
if (openInput !== undefined) {
|
|
19
|
+
return buildAction(facts, {
|
|
20
|
+
kind: "resolve-input",
|
|
21
|
+
reason: `Input ${openInput.id} is open and blocks protocol convergence.`,
|
|
22
|
+
refs: [ref("input-request", openInput.id)],
|
|
23
|
+
preconditions: [
|
|
24
|
+
{ fact: "Input request is open", satisfied: true, ref: ref("input-request", openInput.id) }
|
|
25
|
+
],
|
|
26
|
+
recommendedCommand: `yui task input answer ${task.id}/${openInput.id}`
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const inconsistency = detectProtocolInconsistency(facts);
|
|
30
|
+
if (inconsistency !== null) {
|
|
31
|
+
return buildAction(facts, {
|
|
32
|
+
kind: "repair-protocol-inconsistency",
|
|
33
|
+
reason: inconsistency.reason,
|
|
34
|
+
refs: inconsistency.conflicts,
|
|
35
|
+
conflicts: inconsistency.conflicts,
|
|
36
|
+
preconditions: inconsistency.conflicts.map((entry) => ({ fact: `Conflicting record ${entry.kind} ${entry.id}`, satisfied: false, ref: entry })),
|
|
37
|
+
recommendedCommand: inconsistency.recommendedCommand
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const activeLeader = facts.activeRuns.find((run) => run.roleName === "leader");
|
|
41
|
+
if (activeLeader !== undefined) {
|
|
42
|
+
return buildAction(facts, {
|
|
43
|
+
kind: "wait-for-owned-execution",
|
|
44
|
+
reason: `Leader Run ${activeLeader.id} is active; the protocol position is being executed.`,
|
|
45
|
+
refs: [ref("agent-run", activeLeader.id)],
|
|
46
|
+
preconditions: [
|
|
47
|
+
{ fact: "Leader Run is active", satisfied: true, ref: ref("agent-run", activeLeader.id) }
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (task.status === "draft") {
|
|
52
|
+
return buildAction(facts, {
|
|
53
|
+
kind: "implement-current-work-item",
|
|
54
|
+
reason: `Task ${task.id} is still a Draft; activate it before dispatching, integrating, reviewing, or completing work.`,
|
|
55
|
+
refs: [ref("task", task.id)],
|
|
56
|
+
preconditions: [
|
|
57
|
+
{ fact: "Task is active", satisfied: false, ref: ref("task", task.id) }
|
|
58
|
+
],
|
|
59
|
+
recommendedCommand: `yui task activate ${task.id}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const candidateReady = facts.workItems
|
|
63
|
+
.find((item) => item.status === "awaiting_acceptance");
|
|
64
|
+
if (candidateReady !== undefined) {
|
|
65
|
+
const candidate = currentWorkItemCandidate(candidateReady);
|
|
66
|
+
if (candidate !== undefined && isEmptyDirectCandidate(candidate)) {
|
|
67
|
+
const candidateRef = ref("candidate", `${candidateReady.id}/${candidate.id}`);
|
|
68
|
+
return buildAction(facts, {
|
|
69
|
+
kind: "repair-protocol-inconsistency",
|
|
70
|
+
reason: `Candidate ${candidate.id} is a direct Task-main delivery with base==head (no commits); reject it and re-dispatch real work.`,
|
|
71
|
+
refs: [ref("work-item", candidateReady.id), candidateRef],
|
|
72
|
+
conflicts: [candidateRef],
|
|
73
|
+
preconditions: [
|
|
74
|
+
{ fact: "Direct Candidate contains at least one commit", satisfied: false, ref: candidateRef }
|
|
75
|
+
],
|
|
76
|
+
recommendedCommand: `yui task work reject ${task.id}/${candidateReady.id} --summary \"empty base==head candidate\"`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const activeReview = latestActiveWorkItemReview(facts.reviewRounds, candidateReady, candidate);
|
|
80
|
+
if (activeReview !== undefined) {
|
|
81
|
+
const reviewRef = ref("review-round", activeReview.id);
|
|
82
|
+
if (activeReview.status === "running") {
|
|
83
|
+
if (reviewGroupAwaitingResolution(activeReview)) {
|
|
84
|
+
return buildResolveReviewGroupAction(facts, activeReview);
|
|
85
|
+
}
|
|
86
|
+
if (activeReview.reviewerRunId === undefined) {
|
|
87
|
+
return buildAction(facts, {
|
|
88
|
+
kind: "repair-protocol-inconsistency",
|
|
89
|
+
reason: `ReviewRound ${activeReview.id} is running but has no Reviewer Run.`,
|
|
90
|
+
refs: [reviewRef],
|
|
91
|
+
conflicts: [reviewRef],
|
|
92
|
+
preconditions: [
|
|
93
|
+
{ fact: "Running ReviewRound has an exact Reviewer Run", satisfied: false, ref: reviewRef }
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const reviewRun = activeReviewRoundRun(activeReview, facts.activeRuns);
|
|
98
|
+
if (reviewRun === undefined) {
|
|
99
|
+
const runRef = ref("agent-run", activeReview.reviewerRunId);
|
|
100
|
+
return buildAction(facts, {
|
|
101
|
+
kind: "repair-protocol-inconsistency",
|
|
102
|
+
reason: `ReviewRound ${activeReview.id} references Reviewer Run ${activeReview.reviewerRunId}, but that Run is not active.`,
|
|
103
|
+
refs: [reviewRef, runRef],
|
|
104
|
+
conflicts: [reviewRef, runRef],
|
|
105
|
+
preconditions: [
|
|
106
|
+
{ fact: "Reviewer Run is active", satisfied: false, ref: runRef }
|
|
107
|
+
]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return buildAction(facts, {
|
|
111
|
+
kind: "wait-for-owned-execution",
|
|
112
|
+
reason: `Reviewer Run ${reviewRun.id} is evaluating Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"}.`,
|
|
113
|
+
refs: [reviewRef, ref("agent-run", reviewRun.id)],
|
|
114
|
+
preconditions: [
|
|
115
|
+
{ fact: "ReviewRound is running", satisfied: true, ref: reviewRef },
|
|
116
|
+
{ fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
|
|
117
|
+
]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (activeReview.reviewerRunId !== undefined) {
|
|
121
|
+
const runRef = ref("agent-run", activeReview.reviewerRunId);
|
|
122
|
+
return buildAction(facts, {
|
|
123
|
+
kind: "repair-protocol-inconsistency",
|
|
124
|
+
reason: `Pending ReviewRound ${activeReview.id} already references Reviewer Run ${activeReview.reviewerRunId}.`,
|
|
125
|
+
refs: [reviewRef, runRef],
|
|
126
|
+
conflicts: [reviewRef, runRef],
|
|
127
|
+
preconditions: [
|
|
128
|
+
{ fact: "Pending ReviewRound has no Reviewer Run", satisfied: false, ref: runRef }
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return buildAction(facts, {
|
|
133
|
+
kind: "resume-review",
|
|
134
|
+
reason: `ReviewRound ${activeReview.id} is pending and never launched; resume it before accepting or rejecting the Candidate.`,
|
|
135
|
+
refs: [reviewRef],
|
|
136
|
+
preconditions: [
|
|
137
|
+
{ fact: "ReviewRound is pending", satisfied: true, ref: reviewRef },
|
|
138
|
+
{ fact: "Reviewer Run exists", satisfied: false }
|
|
139
|
+
],
|
|
140
|
+
recommendedCommand: `yui task work review ${task.id}/${candidateReady.id}`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const refs = [
|
|
144
|
+
ref("work-item", candidateReady.id),
|
|
145
|
+
...(candidate === undefined ? [] : [ref("candidate", `${candidateReady.id}/${candidate.id}`)])
|
|
146
|
+
];
|
|
147
|
+
return buildAction(facts, {
|
|
148
|
+
kind: "accept-or-reject-candidate",
|
|
149
|
+
reason: `Work Item ${candidateReady.id} has a Candidate awaiting Leader disposition.`,
|
|
150
|
+
refs,
|
|
151
|
+
preconditions: [
|
|
152
|
+
{ fact: "Work Item is awaiting acceptance", satisfied: true, ref: refs[0] },
|
|
153
|
+
...(candidate === undefined
|
|
154
|
+
? [{ fact: "Candidate record exists", satisfied: false }]
|
|
155
|
+
: [{ fact: "Candidate record exists", satisfied: true, ref: refs[1] }])
|
|
156
|
+
],
|
|
157
|
+
recommendedCommand: `yui task work accept ${task.id}/${candidateReady.id} --summary \"<decision>\"`,
|
|
158
|
+
alternatives: [
|
|
159
|
+
{
|
|
160
|
+
kind: "reject-candidate",
|
|
161
|
+
reason: "Reject when the Candidate does not satisfy the Task objective or acceptance criteria.",
|
|
162
|
+
recommendedCommand: `yui task work reject ${task.id}/${candidateReady.id} --summary \"<reason>\"`,
|
|
163
|
+
refs
|
|
164
|
+
},
|
|
165
|
+
...(candidate?.reviewPolicy === undefined
|
|
166
|
+
|| candidate.reviewPolicy.trigger === "final"
|
|
167
|
+
? []
|
|
168
|
+
: [{
|
|
169
|
+
kind: "re-review-candidate",
|
|
170
|
+
reason: "Request another WorkItem Review when the Leader needs independent evidence before disposition.",
|
|
171
|
+
recommendedCommand: `yui task work review ${task.id}/${candidateReady.id}`,
|
|
172
|
+
refs
|
|
173
|
+
}])
|
|
174
|
+
],
|
|
175
|
+
judgmentRequired: `Leader must judge Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"} against the Task objective, acceptance criteria, and delivery risk.`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const activeWorkers = facts.activeRuns.filter((run) => run.roleName !== "leader");
|
|
179
|
+
if (activeWorkers.length > 0) {
|
|
180
|
+
return buildAction(facts, {
|
|
181
|
+
kind: "wait-for-owned-execution",
|
|
182
|
+
reason: `${activeWorkers.length} delegated Run(s) are active; wait for their delivery.`,
|
|
183
|
+
refs: activeWorkers.map((run) => ref("agent-run", run.id)),
|
|
184
|
+
preconditions: activeWorkers.map((run) => ({ fact: `Delegated Run ${run.id} is active`, satisfied: true, ref: ref("agent-run", run.id) }))
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const failedWork = facts.workItems.find((item) => item.status === "failed");
|
|
188
|
+
if (failedWork !== undefined) {
|
|
189
|
+
const failedReview = latestFailedReviewFor(facts.reviewRounds, failedWork.id);
|
|
190
|
+
if (failedReview !== undefined) {
|
|
191
|
+
return buildAction(facts, {
|
|
192
|
+
kind: "route-review-findings",
|
|
193
|
+
reason: `Work Item ${failedWork.id} failed with Review ${failedReview.id}; route its open findings into a repair wave.`,
|
|
194
|
+
refs: [ref("work-item", failedWork.id), ref("review-round", failedReview.id)],
|
|
195
|
+
preconditions: [
|
|
196
|
+
{ fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) },
|
|
197
|
+
{ fact: "Review Round is failed", satisfied: true, ref: ref("review-round", failedReview.id) }
|
|
198
|
+
],
|
|
199
|
+
recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return buildAction(facts, {
|
|
203
|
+
kind: "implement-current-work-item",
|
|
204
|
+
reason: `Work Item ${failedWork.id} failed without a Review verdict; retry implementation.`,
|
|
205
|
+
refs: [ref("work-item", failedWork.id)],
|
|
206
|
+
preconditions: [
|
|
207
|
+
{ fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) }
|
|
208
|
+
],
|
|
209
|
+
recommendedCommand: `yui task work update ${task.id}/${failedWork.id} running`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const openWork = selectOpenWorkItem(facts.workItems);
|
|
213
|
+
if (openWork?.kind === "blocked") {
|
|
214
|
+
const refs = [
|
|
215
|
+
ref("work-item", openWork.itemId),
|
|
216
|
+
ref("work-item", openWork.blockedBy)
|
|
217
|
+
];
|
|
218
|
+
return buildAction(facts, {
|
|
219
|
+
kind: "repair-protocol-inconsistency",
|
|
220
|
+
reason: `Work Item ${openWork.itemId} depends on ${openWork.blockedBy}, which is not completed or available.`,
|
|
221
|
+
refs,
|
|
222
|
+
conflicts: refs,
|
|
223
|
+
preconditions: [
|
|
224
|
+
{ fact: `Dependency ${openWork.blockedBy} is completed`, satisfied: false, ref: refs[1] }
|
|
225
|
+
]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (openWork?.kind === "ready") {
|
|
229
|
+
const item = openWork.item;
|
|
230
|
+
const refs = [ref("work-item", item.id)];
|
|
231
|
+
return buildAction(facts, {
|
|
232
|
+
kind: "implement-current-work-item",
|
|
233
|
+
reason: `Work Item ${item.id} is ${item.status}; dispatch or continue its implementation.`,
|
|
234
|
+
refs,
|
|
235
|
+
preconditions: [
|
|
236
|
+
{ fact: `Work Item is ${item.status}`, satisfied: true, ref: refs[0] }
|
|
237
|
+
],
|
|
238
|
+
recommendedCommand: `yui task work dispatch ${task.id}/${item.id}`,
|
|
239
|
+
alternatives: [
|
|
240
|
+
{
|
|
241
|
+
kind: "execute-directly",
|
|
242
|
+
reason: "Execute the bounded Work Item directly when the Leader's current context and authority are sufficient.",
|
|
243
|
+
refs
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
kind: "native-subagent",
|
|
247
|
+
reason: "Use one native implementer subagent when one bounded implementation pass benefits from parallel attention.",
|
|
248
|
+
refs
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
judgmentRequired: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (facts.workItems.length === 0) {
|
|
255
|
+
return buildAction(facts, {
|
|
256
|
+
kind: "implement-current-work-item",
|
|
257
|
+
reason: `Task ${task.id} has no Work Item; create the first unit of work.`,
|
|
258
|
+
refs: [],
|
|
259
|
+
preconditions: [
|
|
260
|
+
{ fact: "At least one Work Item exists", satisfied: false },
|
|
261
|
+
{ fact: "Task is active", satisfied: task.status === "active" }
|
|
262
|
+
],
|
|
263
|
+
recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
|
|
267
|
+
if (uncaptured !== undefined) {
|
|
268
|
+
return buildAction(facts, {
|
|
269
|
+
kind: "capture-change-set",
|
|
270
|
+
reason: `Work Item ${uncaptured.id} is completed but has no ChangeSet; capture its delivery boundary.`,
|
|
271
|
+
refs: [ref("work-item", uncaptured.id)],
|
|
272
|
+
preconditions: [
|
|
273
|
+
{ fact: "Work Item is completed", satisfied: true, ref: ref("work-item", uncaptured.id) },
|
|
274
|
+
{ fact: "ChangeSet exists for the Work Item", satisfied: false }
|
|
275
|
+
],
|
|
276
|
+
recommendedCommand: `yui task work capture ${task.id}/${uncaptured.id}`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const unintegrated = facts.changeSets
|
|
280
|
+
.find((changeSet) => !hasCommittedIntegration(facts.integrations, changeSet.id));
|
|
281
|
+
if (unintegrated !== undefined) {
|
|
282
|
+
return buildAction(facts, {
|
|
283
|
+
kind: "integrate-change-set",
|
|
284
|
+
reason: `ChangeSet ${unintegrated.id} has no committed Integration.`,
|
|
285
|
+
refs: [ref("change-set", unintegrated.id)],
|
|
286
|
+
preconditions: [
|
|
287
|
+
{ fact: "ChangeSet exists", satisfied: true, ref: ref("change-set", unintegrated.id) },
|
|
288
|
+
{ fact: "Committed Integration references the ChangeSet", satisfied: false }
|
|
289
|
+
],
|
|
290
|
+
recommendedCommand: `yui task integration start ${task.id} --project ${unintegrated.projectId} --change-set ${unintegrated.id}`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const failedFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
294
|
+
if (failedFinal !== undefined && failedFinal.status === "failed") {
|
|
295
|
+
return buildAction(facts, {
|
|
296
|
+
kind: "route-review-findings",
|
|
297
|
+
reason: `Task-final Review ${failedFinal.id} failed; route its open findings into a repair wave on one frozen head.`,
|
|
298
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
299
|
+
preconditions: [
|
|
300
|
+
{ fact: "Task-final Review is failed", satisfied: true, ref: ref("review-round", failedFinal.id) }
|
|
301
|
+
],
|
|
302
|
+
recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const activeFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
306
|
+
if (activeFinal !== undefined
|
|
307
|
+
&& (activeFinal.status === "pending" || activeFinal.status === "running")) {
|
|
308
|
+
const reviewRef = ref("review-round", activeFinal.id);
|
|
309
|
+
if (activeFinal.status === "running") {
|
|
310
|
+
if (reviewGroupAwaitingResolution(activeFinal)) {
|
|
311
|
+
return buildResolveReviewGroupAction(facts, activeFinal);
|
|
312
|
+
}
|
|
313
|
+
if (activeFinal.reviewerRunId === undefined) {
|
|
314
|
+
return buildAction(facts, {
|
|
315
|
+
kind: "repair-protocol-inconsistency",
|
|
316
|
+
reason: `Task-final ReviewRound ${activeFinal.id} is running but has no Reviewer Run.`,
|
|
317
|
+
refs: [reviewRef],
|
|
318
|
+
conflicts: [reviewRef],
|
|
319
|
+
preconditions: [
|
|
320
|
+
{ fact: "Running Task-final ReviewRound has an exact Reviewer Run", satisfied: false, ref: reviewRef }
|
|
321
|
+
]
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const reviewRun = activeReviewRoundRun(activeFinal, facts.activeRuns);
|
|
325
|
+
if (reviewRun === undefined) {
|
|
326
|
+
const runRef = ref("agent-run", activeFinal.reviewerRunId);
|
|
327
|
+
return buildAction(facts, {
|
|
328
|
+
kind: "repair-protocol-inconsistency",
|
|
329
|
+
reason: `Task-final ReviewRound ${activeFinal.id} references Reviewer Run ${activeFinal.reviewerRunId}, but that Run is not active.`,
|
|
330
|
+
refs: [reviewRef, runRef],
|
|
331
|
+
conflicts: [reviewRef, runRef],
|
|
332
|
+
preconditions: [
|
|
333
|
+
{ fact: "Reviewer Run is active", satisfied: false, ref: runRef }
|
|
334
|
+
]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return buildAction(facts, {
|
|
338
|
+
kind: "wait-for-owned-execution",
|
|
339
|
+
reason: `Reviewer Run ${reviewRun.id} is executing Task-final Review ${activeFinal.id}.`,
|
|
340
|
+
refs: [reviewRef, ref("agent-run", reviewRun.id)],
|
|
341
|
+
preconditions: [
|
|
342
|
+
{ fact: "Task-final ReviewRound is running", satisfied: true, ref: reviewRef },
|
|
343
|
+
{ fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
|
|
344
|
+
]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (activeFinal.reviewerRunId !== undefined) {
|
|
348
|
+
const runRef = ref("agent-run", activeFinal.reviewerRunId);
|
|
349
|
+
return buildAction(facts, {
|
|
350
|
+
kind: "repair-protocol-inconsistency",
|
|
351
|
+
reason: `Pending Task-final ReviewRound ${activeFinal.id} already references Reviewer Run ${activeFinal.reviewerRunId}.`,
|
|
352
|
+
refs: [reviewRef, runRef],
|
|
353
|
+
conflicts: [reviewRef, runRef],
|
|
354
|
+
preconditions: [
|
|
355
|
+
{ fact: "Pending Task-final ReviewRound has no Reviewer Run", satisfied: false, ref: runRef }
|
|
356
|
+
]
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return buildAction(facts, {
|
|
360
|
+
kind: "resume-review",
|
|
361
|
+
reason: `Task-final ReviewRound ${activeFinal.id} is pending and never launched; retry it under the same semantic Round.`,
|
|
362
|
+
refs: [reviewRef],
|
|
363
|
+
preconditions: [
|
|
364
|
+
{ fact: "Task-final ReviewRound is pending", satisfied: true, ref: reviewRef },
|
|
365
|
+
{ fact: "Reviewer Run exists", satisfied: false }
|
|
366
|
+
],
|
|
367
|
+
recommendedCommand: `yui task review retry ${task.id}/${activeFinal.id}`
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const finalReviewRequired = taskFinalReviewRequired(facts);
|
|
371
|
+
if (task.projectBindings.length > 0
|
|
372
|
+
&& finalReviewRequired
|
|
373
|
+
&& !hasValidFinalReview(facts)) {
|
|
374
|
+
const reviewerRole = taskFinalReviewRole(facts);
|
|
375
|
+
return buildAction(facts, {
|
|
376
|
+
kind: "request-final-review",
|
|
377
|
+
reason: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
|
|
378
|
+
refs: [ref("task", task.id)],
|
|
379
|
+
preconditions: [
|
|
380
|
+
{ fact: "All Work Items are terminal", satisfied: true },
|
|
381
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
382
|
+
{ fact: "Valid Task-final Review at the integrated head", satisfied: false }
|
|
383
|
+
],
|
|
384
|
+
recommendedCommand: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const finalReviewOptional = task.projectBindings.length > 0
|
|
388
|
+
&& !finalReviewRequired
|
|
389
|
+
&& !hasValidFinalReview(facts);
|
|
390
|
+
const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
|
|
391
|
+
? [{
|
|
392
|
+
kind: "request-final-review",
|
|
393
|
+
reason: "Request an independent Task-final Review when the Leader wants extra assurance before completion.",
|
|
394
|
+
recommendedCommand: `yui task review request ${task.id} --role ${facts.reviewConfig.roleName}`,
|
|
395
|
+
refs: [ref("task", task.id)]
|
|
396
|
+
}]
|
|
397
|
+
: [];
|
|
398
|
+
return buildAction(facts, {
|
|
399
|
+
kind: "complete-task",
|
|
400
|
+
reason: "The delivery chain is complete; converge the Task instead of creating successor work.",
|
|
401
|
+
refs: [ref("task", task.id)],
|
|
402
|
+
preconditions: [
|
|
403
|
+
{ fact: "All Work Items are terminal", satisfied: true },
|
|
404
|
+
...(task.projectBindings.length === 0
|
|
405
|
+
? []
|
|
406
|
+
: [
|
|
407
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
408
|
+
{
|
|
409
|
+
fact: "Valid Task-final Review at the integrated head",
|
|
410
|
+
satisfied: hasValidFinalReview(facts)
|
|
411
|
+
}
|
|
412
|
+
])
|
|
413
|
+
],
|
|
414
|
+
...(finalReviewAlternative.length === 0 ? {} : { alternatives: finalReviewAlternative }),
|
|
415
|
+
...(!finalReviewOptional
|
|
416
|
+
? {}
|
|
417
|
+
: {
|
|
418
|
+
judgmentRequired: "Leader must decide whether the integrated delivery is safe to complete directly or needs an optional Task-final Review."
|
|
419
|
+
}),
|
|
420
|
+
recommendedCommand: `yui task complete ${task.id} --summary-file -`
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Stable fingerprint of the durable delivery position. It changes exactly
|
|
425
|
+
* when a delivery record changes, so the semantic-progress budget can compare
|
|
426
|
+
* positions across Leader turns without persisting anything new.
|
|
427
|
+
*/
|
|
428
|
+
export function durableStateFingerprint(facts) {
|
|
429
|
+
const parts = [
|
|
430
|
+
`task:${facts.task.status}`,
|
|
431
|
+
...facts.workItems.map((item) => `work:${item.id}:${item.status}:${item.revision}:${item.updatedAt}`),
|
|
432
|
+
...facts.changeSets.map((changeSet) => `change-set:${changeSet.id}:${changeSet.headCommit}`),
|
|
433
|
+
...facts.integrations.map((attempt) => `integration:${attempt.id}:${attempt.status}:${attempt.updatedAt}`),
|
|
434
|
+
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`)
|
|
435
|
+
];
|
|
436
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
437
|
+
}
|
|
438
|
+
function buildAction(facts, input) {
|
|
439
|
+
const fingerprintSource = [
|
|
440
|
+
input.kind,
|
|
441
|
+
...input.refs.map((entry) => `${entry.kind}:${entry.id}`)
|
|
442
|
+
].join("|");
|
|
443
|
+
return {
|
|
444
|
+
taskId: facts.task.id,
|
|
445
|
+
kind: input.kind,
|
|
446
|
+
reason: input.reason,
|
|
447
|
+
refs: input.refs,
|
|
448
|
+
preconditions: input.preconditions,
|
|
449
|
+
...(input.recommendedCommand === undefined
|
|
450
|
+
? {}
|
|
451
|
+
: { recommendedCommand: input.recommendedCommand }),
|
|
452
|
+
...(input.alternatives === undefined || input.alternatives.length === 0
|
|
453
|
+
? {}
|
|
454
|
+
: { alternatives: input.alternatives }),
|
|
455
|
+
...(input.judgmentRequired === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: { judgmentRequired: input.judgmentRequired }),
|
|
458
|
+
...(input.conflicts === undefined ? {} : { conflicts: input.conflicts }),
|
|
459
|
+
fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function latestActiveWorkItemReview(rounds, item, candidate) {
|
|
463
|
+
if (candidate === undefined)
|
|
464
|
+
return undefined;
|
|
465
|
+
return [...rounds]
|
|
466
|
+
.reverse()
|
|
467
|
+
.find((round) => (round.workItemId === item.id
|
|
468
|
+
&& round.candidateId === candidate.id
|
|
469
|
+
&& (round.status === "pending" || round.status === "running")));
|
|
470
|
+
}
|
|
471
|
+
function activeReviewRoundRun(round, activeRuns) {
|
|
472
|
+
if (round.executionGroup !== undefined) {
|
|
473
|
+
for (const lane of round.executionGroup.lanes) {
|
|
474
|
+
if (lane.status !== "running"
|
|
475
|
+
|| lane.reviewRoundId !== round.id
|
|
476
|
+
|| lane.runId === undefined) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const run = activeRuns.find((candidate) => (candidate.id === lane.runId && candidate.roleName === lane.roleName));
|
|
480
|
+
if (run !== undefined)
|
|
481
|
+
return run;
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
if (round.reviewerRunId === undefined)
|
|
486
|
+
return undefined;
|
|
487
|
+
return activeRuns.find((run) => (run.id === round.reviewerRunId && run.roleName === round.reviewerRoleName));
|
|
488
|
+
}
|
|
489
|
+
const TERMINAL_REVIEW_LANE_STATUSES = new Set(["yielded", "completed", "failed"]);
|
|
490
|
+
function isPanelReviewRound(round) {
|
|
491
|
+
return round.executionGroup !== undefined
|
|
492
|
+
&& (round.executionGroup.lanes.length > 1
|
|
493
|
+
|| round.executionGroup.strategy.mode === "adaptive");
|
|
494
|
+
}
|
|
495
|
+
function reviewGroupAwaitingResolution(round) {
|
|
496
|
+
return round.status === "running"
|
|
497
|
+
&& round.executionGroup !== undefined
|
|
498
|
+
&& round.executionGroup.resolution === undefined
|
|
499
|
+
&& isPanelReviewRound(round)
|
|
500
|
+
&& round.executionGroup.lanes.every((lane) => TERMINAL_REVIEW_LANE_STATUSES.has(lane.status));
|
|
501
|
+
}
|
|
502
|
+
function buildResolveReviewGroupAction(facts, round) {
|
|
503
|
+
const group = round.executionGroup;
|
|
504
|
+
const refs = [
|
|
505
|
+
ref("review-round", round.id),
|
|
506
|
+
ref("execution-group", group.id)
|
|
507
|
+
];
|
|
508
|
+
const resolveCommand = (decision) => `yui task review group resolve ${facts.task.id}/${round.id}`
|
|
509
|
+
+ ` --decision ${decision} --summary \"<decision>\"`;
|
|
510
|
+
return buildAction(facts, {
|
|
511
|
+
kind: "resolve-review-group",
|
|
512
|
+
reason: `Reviewer panel ${group.id} has finished every Lane; the Leader must resolve ReviewRound ${round.id}.`,
|
|
513
|
+
refs,
|
|
514
|
+
preconditions: [
|
|
515
|
+
{ fact: "ReviewRound is running", satisfied: true, ref: refs[0] },
|
|
516
|
+
{ fact: "Every Reviewer Lane is terminal", satisfied: true, ref: refs[1] },
|
|
517
|
+
{ fact: "Leader has resolved the Review ExecutionGroup", satisfied: false, ref: refs[1] }
|
|
518
|
+
],
|
|
519
|
+
recommendedCommand: resolveCommand("<accept|reject|blocked>"),
|
|
520
|
+
alternatives: [
|
|
521
|
+
{
|
|
522
|
+
kind: "accept-review-group",
|
|
523
|
+
reason: "Accept the usable Lane outputs when their evidence satisfies the Task objective.",
|
|
524
|
+
recommendedCommand: resolveCommand("accept"),
|
|
525
|
+
refs
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
kind: "reject-review-group",
|
|
529
|
+
reason: "Reject the panel evidence when it does not support the delivery.",
|
|
530
|
+
recommendedCommand: resolveCommand("reject"),
|
|
531
|
+
refs
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
kind: "block-review-group",
|
|
535
|
+
reason: "Block the Review when a material dependency or external fact prevents a sound decision.",
|
|
536
|
+
recommendedCommand: resolveCommand("blocked"),
|
|
537
|
+
refs
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
judgmentRequired: `Leader must judge Review panel ${group.id} against the Task objective, acceptance criteria, and delivery risk.`
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function reviewRoundConflict(round, activeRuns) {
|
|
544
|
+
const reviewRef = ref("review-round", round.id);
|
|
545
|
+
if (round.status === "running") {
|
|
546
|
+
if (reviewGroupAwaitingResolution(round)) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
if (round.reviewerRunId === undefined) {
|
|
550
|
+
return {
|
|
551
|
+
reason: `ReviewRound ${round.id} is running but has no Reviewer Run.`,
|
|
552
|
+
conflicts: [reviewRef]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (activeReviewRoundRun(round, activeRuns) === undefined) {
|
|
556
|
+
const runRef = ref("agent-run", round.reviewerRunId);
|
|
557
|
+
return {
|
|
558
|
+
reason: `ReviewRound ${round.id} references Reviewer Run ${round.reviewerRunId}, but that Run is not active.`,
|
|
559
|
+
conflicts: [reviewRef, runRef]
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (round.status === "pending") {
|
|
564
|
+
const launchedRunId = round.reviewerRunId
|
|
565
|
+
?? round.executionGroup?.lanes.find((lane) => lane.runId !== undefined)?.runId;
|
|
566
|
+
if (launchedRunId !== undefined) {
|
|
567
|
+
const runRef = ref("agent-run", launchedRunId);
|
|
568
|
+
return {
|
|
569
|
+
reason: `Pending ReviewRound ${round.id} already references Reviewer Run ${launchedRunId}.`,
|
|
570
|
+
conflicts: [reviewRef, runRef]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function selectOpenWorkItem(workItems) {
|
|
577
|
+
const byId = new Map(workItems.map((item) => [item.id, item]));
|
|
578
|
+
const openItems = workItems.filter((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
|
|
579
|
+
if (openItems.length === 0)
|
|
580
|
+
return { kind: "none" };
|
|
581
|
+
const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) => byId.get(dependencyId)?.status === "completed")));
|
|
582
|
+
if (eligible !== undefined)
|
|
583
|
+
return { kind: "ready", item: eligible };
|
|
584
|
+
let current = openItems[0];
|
|
585
|
+
const visited = new Set();
|
|
586
|
+
while (current !== undefined) {
|
|
587
|
+
if (visited.has(current.id)) {
|
|
588
|
+
return { kind: "blocked", itemId: current.id, blockedBy: current.id };
|
|
589
|
+
}
|
|
590
|
+
visited.add(current.id);
|
|
591
|
+
const blockedBy = current.dependsOn
|
|
592
|
+
.find((dependencyId) => byId.get(dependencyId)?.status !== "completed");
|
|
593
|
+
if (blockedBy === undefined)
|
|
594
|
+
return { kind: "ready", item: current };
|
|
595
|
+
const dependency = byId.get(blockedBy);
|
|
596
|
+
if (dependency === undefined || !OPEN_WORK_ITEM_STATUSES.has(dependency.status)) {
|
|
597
|
+
return { kind: "blocked", itemId: current.id, blockedBy };
|
|
598
|
+
}
|
|
599
|
+
current = dependency;
|
|
600
|
+
}
|
|
601
|
+
return { kind: "none" };
|
|
602
|
+
}
|
|
603
|
+
function taskFinalReviewContract(facts) {
|
|
604
|
+
const contract = facts.workItems
|
|
605
|
+
.flatMap((item) => item.candidates)
|
|
606
|
+
.map((candidate) => candidate.taskFinalReviewContract)
|
|
607
|
+
.find((contract) => contract !== undefined);
|
|
608
|
+
return contract === undefined ? undefined : validateTaskFinalReviewContract(contract);
|
|
609
|
+
}
|
|
610
|
+
function taskFinalReviewRequired(facts) {
|
|
611
|
+
return taskFinalReviewContract(facts) !== undefined
|
|
612
|
+
|| latestTaskFinalReview(facts.reviewRounds) !== undefined
|
|
613
|
+
|| facts.reviewConfig?.trigger === "final";
|
|
614
|
+
}
|
|
615
|
+
function taskFinalReviewRole(facts) {
|
|
616
|
+
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
617
|
+
?? latestTaskFinalReview(facts.reviewRounds)?.reviewerRoleName
|
|
618
|
+
?? (facts.reviewConfig?.trigger === "final" ? facts.reviewConfig.roleName : undefined);
|
|
619
|
+
}
|
|
620
|
+
function ref(kind, id) {
|
|
621
|
+
return { kind, id };
|
|
622
|
+
}
|
|
623
|
+
function isEmptyDirectCandidate(candidate) {
|
|
624
|
+
const snapshot = candidate.taskMainSnapshot;
|
|
625
|
+
if (snapshot === undefined)
|
|
626
|
+
return false;
|
|
627
|
+
return snapshot.projects.every((project) => project.baseCommit === project.headCommit);
|
|
628
|
+
}
|
|
629
|
+
function latestFailedReviewFor(rounds, workItemId) {
|
|
630
|
+
return [...rounds]
|
|
631
|
+
.reverse()
|
|
632
|
+
.find((round) => round.workItemId === workItemId && round.status === "failed");
|
|
633
|
+
}
|
|
634
|
+
function latestTaskFinalReview(rounds) {
|
|
635
|
+
return [...rounds]
|
|
636
|
+
.reverse()
|
|
637
|
+
.find((round) => (round.scope ?? "work-item") === "task");
|
|
638
|
+
}
|
|
639
|
+
function hasCommittedIntegration(integrations, changeSetId) {
|
|
640
|
+
return integrations.some((attempt) => attempt.status === "committed" && attempt.changeSetIds.includes(changeSetId));
|
|
641
|
+
}
|
|
642
|
+
function needsChangeSetCapture(facts, item) {
|
|
643
|
+
if (item.status !== "completed")
|
|
644
|
+
return false;
|
|
645
|
+
if (facts.changeSets.some((changeSet) => changeSet.workItemId === item.id))
|
|
646
|
+
return false;
|
|
647
|
+
const candidate = item.candidates.at(-1);
|
|
648
|
+
if (candidate === undefined)
|
|
649
|
+
return false;
|
|
650
|
+
// A metadata-only direct Task-main Candidate has no WorkItem Develop
|
|
651
|
+
// workspace to capture; its boundary is the Task-main head itself.
|
|
652
|
+
if (candidate.workspace === undefined
|
|
653
|
+
&& candidate.gitSnapshot === undefined
|
|
654
|
+
&& candidate.taskMainSnapshot !== undefined) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
return candidate.workspace !== undefined || candidate.gitSnapshot !== undefined;
|
|
658
|
+
}
|
|
659
|
+
function hasValidFinalReview(facts) {
|
|
660
|
+
const final = latestTaskFinalReview(facts.reviewRounds);
|
|
661
|
+
if (final === undefined || final.status !== "completed")
|
|
662
|
+
return false;
|
|
663
|
+
const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
|
|
664
|
+
if (reviewedCommits.size === 0)
|
|
665
|
+
return false;
|
|
666
|
+
const integratedHeads = new Set(facts.integrations
|
|
667
|
+
.filter((attempt) => attempt.status === "committed")
|
|
668
|
+
.flatMap((attempt) => facts.changeSets
|
|
669
|
+
.filter((changeSet) => attempt.changeSetIds.includes(changeSet.id))
|
|
670
|
+
.map((changeSet) => changeSet.headCommit)));
|
|
671
|
+
if (integratedHeads.size === 0)
|
|
672
|
+
return false;
|
|
673
|
+
for (const head of integratedHeads) {
|
|
674
|
+
if (!reviewedCommits.has(head))
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
function detectProtocolInconsistency(facts) {
|
|
680
|
+
const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
|
|
681
|
+
const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
|
|
682
|
+
const contractedCandidates = facts.workItems
|
|
683
|
+
.flatMap((item) => item.candidates.map((candidate) => ({ item, candidate })))
|
|
684
|
+
.filter(({ candidate }) => candidate.taskFinalReviewContract !== undefined);
|
|
685
|
+
if (contractedCandidates.length > 1) {
|
|
686
|
+
const first = validateTaskFinalReviewContract(contractedCandidates[0].candidate.taskFinalReviewContract);
|
|
687
|
+
const conflict = contractedCandidates.find(({ candidate }) => (!sameTaskFinalReviewContract(first, candidate.taskFinalReviewContract)));
|
|
688
|
+
if (conflict !== undefined) {
|
|
689
|
+
const refs = contractedCandidates.map(({ item, candidate }) => ref("candidate", `${item.id}/${candidate.id}`));
|
|
690
|
+
return {
|
|
691
|
+
reason: "Task-final Review contracts conflict across Candidates; the completion gate is ambiguous.",
|
|
692
|
+
conflicts: refs
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const round of facts.reviewRounds) {
|
|
697
|
+
const reviewConflict = reviewRoundConflict(round, facts.activeRuns);
|
|
698
|
+
if (reviewConflict !== null)
|
|
699
|
+
return reviewConflict;
|
|
700
|
+
}
|
|
701
|
+
for (const attempt of facts.integrations) {
|
|
702
|
+
if (attempt.status !== "committed")
|
|
703
|
+
continue;
|
|
704
|
+
const dangling = attempt.changeSetIds
|
|
705
|
+
.filter((id) => !changeSetIds.has(id));
|
|
706
|
+
if (dangling.length > 0) {
|
|
707
|
+
return {
|
|
708
|
+
reason: `Committed Integration ${attempt.id} references missing ChangeSet(s): ${dangling.join(", ")}.`,
|
|
709
|
+
conflicts: [
|
|
710
|
+
ref("integration-attempt", attempt.id),
|
|
711
|
+
...dangling.map((id) => ref("change-set", id))
|
|
712
|
+
]
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const round of facts.reviewRounds) {
|
|
717
|
+
if (round.status !== "pending" && round.status !== "running")
|
|
718
|
+
continue;
|
|
719
|
+
const item = workItemById.get(round.workItemId);
|
|
720
|
+
if (item !== undefined && item.status === "retired") {
|
|
721
|
+
return {
|
|
722
|
+
reason: `Review ${round.id} is still ${round.status} but its Work Item ${item.id} is retired.`,
|
|
723
|
+
conflicts: [ref("review-round", round.id), ref("work-item", item.id)]
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const item of facts.workItems) {
|
|
728
|
+
if (item.status !== "awaiting_acceptance")
|
|
729
|
+
continue;
|
|
730
|
+
if (item.candidates.length === 0) {
|
|
731
|
+
return {
|
|
732
|
+
reason: `Work Item ${item.id} is awaiting acceptance but has no Candidate record.`,
|
|
733
|
+
conflicts: [ref("work-item", item.id)]
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
}
|