@zq-silk/yui 0.14.1 → 0.14.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/ARCHITECTURE.md +64 -35
- package/README.md +254 -124
- package/dist/agent/managedRuntimeEnvironment.js +5 -8
- package/dist/cli/commandCatalog.js +74 -109
- package/dist/cli/helpRenderer.js +1 -2
- package/dist/cli/interactionCandidates.js +10 -0
- package/dist/cli/interactionPolicy.js +76 -37
- package/dist/cli/roleWizard.js +5 -6
- package/dist/cli/updateOrchestrator.js +10 -8
- package/dist/cli/updatePorts.js +17 -13
- package/dist/cli/upgradeCommand.js +2 -5
- package/dist/cli.js +268 -231
- package/dist/commands/agentCommands.js +13 -0
- package/dist/commands/configCommands.js +7 -19
- package/dist/commands/configOverview.js +3 -2
- package/dist/commands/durableJobCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +4 -9
- package/dist/commands/globalRoleCommands.js +17 -0
- package/dist/commands/profileCommands.js +200 -30
- package/dist/commands/sessionCommands.js +1 -1
- package/dist/commands/taskActor.js +29 -127
- package/dist/commands/taskChangeSetCommands.js +1 -3
- package/dist/commands/taskCommands.js +1771 -2107
- package/dist/commands/taskCompletionGate.js +27 -103
- package/dist/commands/taskContextCommand.js +47 -80
- package/dist/commands/taskExecutionCommands.js +9 -27
- package/dist/commands/taskInputCommands.js +10 -10
- package/dist/commands/taskIntegrationCommands.js +69 -61
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -29
- package/dist/commands/taskOverviewCommand.js +25 -3
- package/dist/commands/taskPublicationCommands.js +151 -85
- package/dist/commands/taskPublicationVerifyCommand.js +181 -0
- package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
- package/dist/commands/taskRoleRuntimeStatus.js +5 -6
- package/dist/commands/taskUpstreamCommands.js +79 -95
- package/dist/config/configCatalog.js +1 -1
- package/dist/context/contextSnapshot.js +17 -2
- package/dist/context/sourceTurnContext.js +30 -0
- package/dist/context/turnContextPack.js +107 -48
- package/dist/context/turnInputContract.js +11 -4
- package/dist/context/wakeNotification.js +84 -22
- package/dist/controller/agentRuntimeObserver.js +9 -12
- package/dist/controller/clientRuntime.js +13 -13
- package/dist/controller/controller.js +11 -11
- package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
- package/dist/controller/jobControl.js +11 -10
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +2 -2
- package/dist/controller/runtime.js +18 -61
- package/dist/controller/runtimeEventInbox.js +57 -34
- package/dist/controller/runtimeEventProcessor.js +36 -96
- package/dist/controller/runtimeHookTurnFence.js +33 -57
- package/dist/controller/runtimeLaunchCoordinator.js +44 -40
- package/dist/controller/runtimeObservationHook.js +18 -12
- package/dist/controller/sessionNotify.js +43 -28
- package/dist/controller/sessionOwnerReconciliation.js +21 -21
- package/dist/controller/structuredProviderObservation.js +13 -16
- package/dist/coordination/workMailboxQueue.js +132 -0
- package/dist/core/controllerServer.js +1 -0
- package/dist/domain/agentResultTransport.js +60 -0
- package/dist/execution/executionHealth.js +111 -118
- package/dist/execution/reviewMainTurn.js +161 -0
- package/dist/execution/workItemExecution.js +454 -0
- package/dist/execution/workItemExecutionProjection.js +309 -0
- package/dist/execution/workItemMainTurn.js +164 -0
- package/dist/executor/agentExecutor.js +19 -19
- package/dist/executor/executorRegistry.js +7 -28
- package/dist/executor/fileRoleLaunchPlanner.js +16 -82
- package/dist/external/githubPublicationVerifier.js +114 -0
- package/dist/external/gitlabPublicationVerifier.js +158 -0
- package/dist/external/pinnedCommandRunner.js +73 -0
- package/dist/integration/changeSetManifest.js +0 -3
- package/dist/integration/deliveryObligation.js +43 -6
- package/dist/integration/gitIntegrationService.js +284 -101
- package/dist/integration/integrationAttempt.js +64 -14
- package/dist/integration/integrationQueueEntry.js +5 -35
- package/dist/integration/integrationQueueService.js +65 -194
- package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
- package/dist/lifecycle/exactTurnTerminalization.js +100 -97
- package/dist/message/message.js +16 -0
- package/dist/observability/executionAudit.js +4 -25
- package/dist/observability/faultClassification.js +58 -70
- package/dist/observability/orchestrationMetrics.js +8 -50
- package/dist/profile/agentProfile.js +60 -15
- package/dist/profile/agentProfileRuntime.js +100 -0
- package/dist/release/releaseWorkflowPorts.js +5 -68
- package/dist/repository/executionLaneGitSnapshot.js +80 -0
- package/dist/repository/gitWorkspace.js +9 -67
- package/dist/repository/taskBaseFreshness.js +3 -1
- package/dist/repository/taskWorkspaceCoordinator.js +32 -4
- package/dist/repository/taskWorkspacePreparer.js +252 -411
- package/dist/resources/liveReferences.js +6 -55
- package/dist/resources/resourceDiscovery.js +1 -30
- package/dist/resources/resourceRegistrar.js +0 -23
- package/dist/review/deltaRecheck.js +8 -65
- package/dist/review/reviewAcceptance.js +22 -19
- package/dist/review/reviewConfig.js +0 -10
- package/dist/review/reviewDecision.js +2 -2
- package/dist/review/reviewRound.js +147 -273
- package/dist/review/reviewerAvailability.js +13 -23
- package/dist/role/role.js +3 -0
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +51 -51
- package/dist/runtime/builtinAgentDrivers.js +9 -2
- package/dist/runtime/codexAppServerRuntime.js +23 -5
- package/dist/runtime/exactControlPlane.js +2 -267
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +11 -11
- package/dist/runtime/lifecycleReservation.js +2 -2
- package/dist/runtime/managedCaller.js +111 -0
- package/dist/runtime/ports.js +16 -3
- package/dist/runtime/preallocatedNativeSession.js +2 -2
- package/dist/runtime/processExitObservation.js +3 -2
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/runtimeBinding.js +1 -1
- package/dist/runtime/runtimeObservation.js +33 -16
- package/dist/runtime/runtimeSessionCandidate.js +2 -2
- package/dist/runtime/runtimeStopReceipt.js +11 -11
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/sessionOwnerIdentity.js +9 -9
- package/dist/runtime/sessionOwnerRegistry.js +13 -13
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTerminationGuard.js +9 -9
- package/dist/runtime/sessionTokenMetrics.js +5 -7
- package/dist/runtime/structuredProviderHost.js +5 -5
- package/dist/runtime/taskRuntimeIsolation.js +24 -24
- package/dist/runtime/tmuxAdapters.js +32 -30
- package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
- package/dist/scheduler/leaderWakeupProcessor.js +78 -55
- package/dist/scheduler/roleTurnLiveness.js +6 -6
- package/dist/scheduler/roleTurnStall.js +8 -9
- package/dist/scheduler/taskExecutionProjection.js +19 -41
- package/dist/scheduler/taskObservabilityProjection.js +75 -122
- package/dist/storage/sqliteSchema.js +1 -21
- package/dist/storage/sqliteStore.js +62 -47
- package/dist/storage/storageSchema.js +1 -8
- package/dist/storage/storageVersions.js +7 -4
- package/dist/storage/taskStore.js +8 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
- package/dist/task/completionReadiness.js +17 -98
- package/dist/task/deliveryGuard.js +9 -11
- package/dist/task/draftPlan.js +142 -0
- package/dist/task/nextAction.js +179 -477
- package/dist/task/publicationVerification.js +1 -0
- package/dist/task/remoteDelivery.js +233 -0
- package/dist/task/task.js +108 -4
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/turn/turn.js +109 -16
- package/dist/verification/verificationGateService.js +0 -26
- package/dist/web/assets/client/components.js +99 -121
- package/dist/web/assets/client/i18n.js +136 -58
- package/dist/web/assets/client/view.js +44 -0
- package/dist/web/assets/styles/cards.js +3 -15
- package/dist/web/assets/styles/widgets.js +4 -0
- package/dist/web/webSnapshot.js +29 -14
- package/dist/workItem/dependencyGate.js +34 -0
- package/dist/workItem/workItem.js +99 -266
- package/dist/workspace/workItemChangeSetManager.js +15 -59
- package/docs/task-local-identity.md +1 -1
- package/i18n/README.zh-CN.md +164 -58
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +272 -782
- package/skills/yui-operator/SKILL.md +190 -365
- package/skills/yui-reviewer/SKILL.md +40 -33
- package/skills/yui-runtime/SKILL.md +22 -0
- package/skills/yui-worker/SKILL.md +19 -6
- package/dist/execution/candidateConvergence.js +0 -623
- package/dist/execution/executionGroup.js +0 -854
- package/dist/execution/resourceBroker.js +0 -426
- package/dist/executor/launchPlan.js +0 -16
- package/dist/review/reviewFinding.js +0 -240
- package/dist/review/reviewFindingLedger.js +0 -550
- package/dist/review/reviewOutcomeClassifier.js +0 -255
- package/dist/scheduler/resourceQueueProjection.js +0 -71
- package/dist/task/repairWave.js +0 -150
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
2
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { normalizeCheckResult } from "./checkResult.js";
|
|
4
4
|
export function createIntegrationAttempt(input, now) {
|
|
5
5
|
const timestamp = now.toISOString();
|
|
6
6
|
return validateIntegrationAttempt({
|
|
7
|
-
schemaVersion:
|
|
7
|
+
schemaVersion: 6,
|
|
8
8
|
id: input.id,
|
|
9
9
|
taskId: input.taskId,
|
|
10
10
|
projectId: input.projectId,
|
|
11
11
|
targetRef: input.targetRef,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
source: input.source,
|
|
13
|
+
beforeCommit: input.beforeCommit,
|
|
14
14
|
checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
|
|
15
15
|
status: "running",
|
|
16
16
|
createdAt: timestamp,
|
|
@@ -123,8 +123,8 @@ export function supersedeIntegration(attempt, reason, now) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
export function validateIntegrationAttempt(attempt) {
|
|
126
|
-
if (attempt.schemaVersion !==
|
|
127
|
-
throw new Error("IntegrationAttempt must use schemaVersion
|
|
126
|
+
if (attempt.schemaVersion !== 6) {
|
|
127
|
+
throw new Error("IntegrationAttempt must use schemaVersion 6.");
|
|
128
128
|
}
|
|
129
129
|
validateTaskRecordReference({
|
|
130
130
|
taskId: attempt.taskId,
|
|
@@ -132,18 +132,29 @@ export function validateIntegrationAttempt(attempt) {
|
|
|
132
132
|
}, "integrationAttempt");
|
|
133
133
|
requireIdentity(attempt.projectId, "Project id");
|
|
134
134
|
requireText(attempt.targetRef, "Integration target ref");
|
|
135
|
-
requireCommit(attempt.
|
|
136
|
-
|
|
137
|
-
for (const changeSetId of attempt.changeSetIds) {
|
|
138
|
-
validateTaskRecordReference({ taskId: attempt.taskId, localId: changeSetId }, "changeSet");
|
|
139
|
-
}
|
|
135
|
+
requireCommit(attempt.beforeCommit, "Integration before commit");
|
|
136
|
+
validateIntegrationSource(attempt.taskId, attempt.source);
|
|
140
137
|
normalizedUniqueText(attempt.checkCommands, "Integration check command");
|
|
141
|
-
if (attempt.changeSetIds.length === 0) {
|
|
142
|
-
throw new Error("IntegrationAttempt requires at least one ChangeSet.");
|
|
143
|
-
}
|
|
144
138
|
if (attempt.candidateCommit !== undefined) {
|
|
145
139
|
requireCommit(attempt.candidateCommit, "Integration candidate commit");
|
|
146
140
|
}
|
|
141
|
+
if (attempt.afterCommit !== undefined) {
|
|
142
|
+
requireCommit(attempt.afterCommit, "Integration after commit");
|
|
143
|
+
}
|
|
144
|
+
if (attempt.summary !== undefined)
|
|
145
|
+
requireText(attempt.summary, "Integration summary");
|
|
146
|
+
if (attempt.status === "committed") {
|
|
147
|
+
requireCommit(attempt.afterCommit ?? "", "Committed Integration after commit");
|
|
148
|
+
requireText(attempt.summary ?? "", "Committed Integration summary");
|
|
149
|
+
if (attempt.candidateCommit !== attempt.afterCommit) {
|
|
150
|
+
throw new Error("Committed Integration candidate and afterCommit must match.");
|
|
151
|
+
}
|
|
152
|
+
if (attempt.source.kind === "work-item"
|
|
153
|
+
&& attempt.source.strategy === "manual"
|
|
154
|
+
&& attempt.resolution?.action !== "manual-resolution") {
|
|
155
|
+
throw new Error("Committed manual Integration requires a Task-control resolution decision.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
147
158
|
if (attempt.jobId !== undefined) {
|
|
148
159
|
validateTaskRecordReference({ taskId: attempt.taskId, localId: attempt.jobId }, "durableJob");
|
|
149
160
|
}
|
|
@@ -179,6 +190,45 @@ export function validateIntegrationAttempt(attempt) {
|
|
|
179
190
|
}
|
|
180
191
|
return attempt;
|
|
181
192
|
}
|
|
193
|
+
function validateIntegrationSource(taskId, source) {
|
|
194
|
+
if (typeof source !== "object" || source === null) {
|
|
195
|
+
throw new Error("Integration source is required.");
|
|
196
|
+
}
|
|
197
|
+
if (source.kind === "work-item") {
|
|
198
|
+
validateTaskRecordReference({ taskId, localId: source.workItemId }, "workItem");
|
|
199
|
+
requireCommit(source.startCommit, "WorkItem Integration start commit");
|
|
200
|
+
requireCommit(source.resultCommit, "WorkItem Integration result commit");
|
|
201
|
+
if (!["ff", "cherry-pick", "merge", "manual"].includes(source.strategy)) {
|
|
202
|
+
throw new Error(`WorkItem Integration strategy is invalid: ${String(source.strategy)}.`);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (source.kind === "upstream") {
|
|
207
|
+
requireText(source.branch, "Upstream branch");
|
|
208
|
+
requireCommit(source.remoteCommit, "Upstream remote commit");
|
|
209
|
+
requireCommit(source.taskBaseCommit, "Upstream Task base commit");
|
|
210
|
+
const strategy = source.strategy;
|
|
211
|
+
if (strategy !== "rebase") {
|
|
212
|
+
throw new Error(`Upstream Integration strategy is invalid: ${String(strategy)}.`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (source.kind === "historical-change-sets") {
|
|
217
|
+
if (!Array.isArray(source.changeSetIds) || source.changeSetIds.length === 0) {
|
|
218
|
+
throw new Error("Historical Integration source requires ChangeSet ids.");
|
|
219
|
+
}
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
for (const changeSetId of source.changeSetIds) {
|
|
222
|
+
validateTaskRecordReference({ taskId, localId: changeSetId }, "changeSet");
|
|
223
|
+
if (seen.has(changeSetId)) {
|
|
224
|
+
throw new Error(`Historical Integration ChangeSet is duplicated: ${changeSetId}.`);
|
|
225
|
+
}
|
|
226
|
+
seen.add(changeSetId);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
throw new Error("Integration source is invalid.");
|
|
231
|
+
}
|
|
182
232
|
function normalizeConflictReport(report) {
|
|
183
233
|
const affectedPaths = normalizedUniqueText(report.affectedPaths, "Conflict path");
|
|
184
234
|
return {
|
|
@@ -47,7 +47,7 @@ export function createConvergedIntegrationQueueEntry(input, now) {
|
|
|
47
47
|
}
|
|
48
48
|
export function markIntegrationQueueRunning(entry, targetBefore, now) {
|
|
49
49
|
validateIntegrationQueueEntry(entry);
|
|
50
|
-
if (!["queued", "conflicted"
|
|
50
|
+
if (!["queued", "conflicted"].includes(entry.status)) {
|
|
51
51
|
throw new Error(`Integration queue entry cannot run from ${entry.status}: ${entry.id}.`);
|
|
52
52
|
}
|
|
53
53
|
const { conflictSummary: _previousConflict, ...rest } = entry;
|
|
@@ -95,36 +95,10 @@ export function markIntegrationQueueCommitted(entry, targetAfter, now) {
|
|
|
95
95
|
endedAt: timestamp
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Records that this entry's reusable evidence was checked against an exact
|
|
100
|
-
* target head. The evidence only covers `evidenceTargetHead`; a target
|
|
101
|
-
* advance invalidates it unless the delta is provably empty. Path
|
|
102
|
-
* disjointness alone never justifies this transition: a gate may read any
|
|
103
|
-
* file, so a non-empty target increment cannot be proven irrelevant.
|
|
104
|
-
*/
|
|
105
|
-
export function markIntegrationQueueValidated(entry, now, evidenceTargetHead) {
|
|
106
|
-
validateIntegrationQueueEntry(entry);
|
|
107
|
-
if (entry.status !== "queued") {
|
|
108
|
-
throw new Error(`Integration queue entry cannot validate from ${entry.status}: ${entry.id}.`);
|
|
109
|
-
}
|
|
110
|
-
if (entry.evidenceRefs.length === 0) {
|
|
111
|
-
throw new Error(`Integration queue entry has no reusable evidence: ${entry.id}.`);
|
|
112
|
-
}
|
|
113
|
-
return validateIntegrationQueueEntry({
|
|
114
|
-
...entry,
|
|
115
|
-
status: "validated",
|
|
116
|
-
...(evidenceTargetHead === undefined ? {} : { evidenceTargetHead }),
|
|
117
|
-
updatedAt: now.toISOString()
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* A validated entry whose ChangeSet became affected by a later target advance
|
|
122
|
-
* loses its evidence coverage and runs its checks again; a conflicted entry is
|
|
123
|
-
* manually retried after its underlying issue was fixed.
|
|
124
|
-
*/
|
|
98
|
+
/** A conflicted entry is manually retried after its underlying issue was fixed. */
|
|
125
99
|
export function markIntegrationQueueRequeued(entry, now) {
|
|
126
100
|
validateIntegrationQueueEntry(entry);
|
|
127
|
-
if (entry.status !== "
|
|
101
|
+
if (entry.status !== "conflicted") {
|
|
128
102
|
throw new Error(`Integration queue entry cannot requeue from ${entry.status}: ${entry.id}.`);
|
|
129
103
|
}
|
|
130
104
|
return validateIntegrationQueueEntry({
|
|
@@ -135,7 +109,7 @@ export function markIntegrationQueueRequeued(entry, now) {
|
|
|
135
109
|
}
|
|
136
110
|
export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
|
|
137
111
|
validateIntegrationQueueEntry(entry);
|
|
138
|
-
if (
|
|
112
|
+
if (entry.status !== "queued") {
|
|
139
113
|
throw new Error(`Integration queue entry cannot recompute overlap from ${entry.status}: ${entry.id}.`);
|
|
140
114
|
}
|
|
141
115
|
const previous = new Set(entry.affectedPaths ?? []);
|
|
@@ -148,7 +122,7 @@ export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
|
|
|
148
122
|
}
|
|
149
123
|
export function markIntegrationQueueSuperseded(entry, reason, now) {
|
|
150
124
|
validateIntegrationQueueEntry(entry);
|
|
151
|
-
if (!["queued", "conflicted"
|
|
125
|
+
if (!["queued", "conflicted"].includes(entry.status)) {
|
|
152
126
|
throw new Error(`Integration queue entry cannot be superseded from ${entry.status}: ${entry.id}.`);
|
|
153
127
|
}
|
|
154
128
|
const timestamp = now.toISOString();
|
|
@@ -174,7 +148,6 @@ export function validateIntegrationQueueEntry(entry) {
|
|
|
174
148
|
"queued",
|
|
175
149
|
"running",
|
|
176
150
|
"conflicted",
|
|
177
|
-
"validated",
|
|
178
151
|
"committed",
|
|
179
152
|
"superseded"
|
|
180
153
|
].includes(entry.status)) {
|
|
@@ -184,9 +157,6 @@ export function validateIntegrationQueueEntry(entry) {
|
|
|
184
157
|
requireCommit(entry.targetBefore, "Integration queue targetBefore");
|
|
185
158
|
if (entry.targetAfter !== undefined)
|
|
186
159
|
requireCommit(entry.targetAfter, "Integration queue targetAfter");
|
|
187
|
-
if (entry.evidenceTargetHead !== undefined) {
|
|
188
|
-
requireCommit(entry.evidenceTargetHead, "Integration queue evidenceTargetHead");
|
|
189
|
-
}
|
|
190
160
|
if (entry.integrationAttemptId !== undefined) {
|
|
191
161
|
normalizedUniqueIdentities([entry.integrationAttemptId], "Integration attempt id");
|
|
192
162
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
2
2
|
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
3
3
|
import { createIntegrationAttempt, recordResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
4
|
-
import { createConvergedIntegrationQueueEntry, createIntegrationQueueEntry, markIntegrationQueueBlocked, markIntegrationQueueCommitted, markIntegrationQueueRequeued, markIntegrationQueueRunning, markIntegrationQueueSuperseded,
|
|
4
|
+
import { createConvergedIntegrationQueueEntry, createIntegrationQueueEntry, markIntegrationQueueBlocked, markIntegrationQueueCommitted, markIntegrationQueueRequeued, markIntegrationQueueRunning, markIntegrationQueueSuperseded, recordIntegrationQueueAffectedPaths, recordIntegrationQueueAttempt } from "./integrationQueueEntry.js";
|
|
5
5
|
import { GitIntegrationService } from "./gitIntegrationService.js";
|
|
6
|
-
import { gateArtifactCoversCheckCommands } from "../verification/verificationGateService.js";
|
|
7
6
|
/**
|
|
8
7
|
* Producer WorkItem statuses that close the WorkItem lifecycle. A ChangeSet
|
|
9
8
|
* captured from a WorkItem that reached one of these without ever producing a
|
|
@@ -197,13 +196,14 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
197
196
|
throw new Error(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
|
|
198
197
|
}
|
|
199
198
|
const canonicalTargetRef = canonicalizeTargetRef(targetRef);
|
|
200
|
-
const
|
|
201
|
-
|
|
199
|
+
const repositoryPath = taskMainRepositoryPath(input.store, task.id, project.id);
|
|
200
|
+
const targetHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
|
|
201
|
+
let equivalent = await findEquivalentCommit(git, repositoryPath, changeSet.headCommit, targetHead, changeSet.baseCommit);
|
|
202
202
|
if (equivalent === null) {
|
|
203
203
|
// The identical change may already have landed through another commit
|
|
204
204
|
// while the target also moved on with unrelated work: the whole-tree
|
|
205
205
|
// check misses it, but the touched paths already agree.
|
|
206
|
-
equivalent = await findContainedChangeSet(git,
|
|
206
|
+
equivalent = await findContainedChangeSet(git, repositoryPath, changeSet, targetHead);
|
|
207
207
|
}
|
|
208
208
|
return input.store.transactionAsync(async (tx) => {
|
|
209
209
|
const duplicate = findActiveQueueDuplicate(tx, task.id, input.projectId, input.changeSetId);
|
|
@@ -252,7 +252,7 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
252
252
|
// proof if it moved.
|
|
253
253
|
let effectiveTargetHead = targetHead;
|
|
254
254
|
if (equivalent !== null) {
|
|
255
|
-
effectiveTargetHead = (await git.inspect(
|
|
255
|
+
effectiveTargetHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
|
|
256
256
|
if (effectiveTargetHead !== targetHead) {
|
|
257
257
|
equivalent = null;
|
|
258
258
|
}
|
|
@@ -261,7 +261,7 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
261
261
|
// advance the target during the call, so the returned head may
|
|
262
262
|
// already be stale. Verify with a second read; if it moved, the
|
|
263
263
|
// convergence proof is stale and the entry must queue.
|
|
264
|
-
const verifiedHead = (await git.inspect(
|
|
264
|
+
const verifiedHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
|
|
265
265
|
if (verifiedHead !== effectiveTargetHead) {
|
|
266
266
|
equivalent = null;
|
|
267
267
|
effectiveTargetHead = verifiedHead;
|
|
@@ -269,25 +269,14 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
if (equivalent === null || requestedChecks.length > 0) {
|
|
272
|
-
|
|
272
|
+
const entry = createIntegrationQueueEntry({
|
|
273
273
|
id,
|
|
274
274
|
taskId: task.id,
|
|
275
275
|
projectId: input.projectId,
|
|
276
276
|
changeSetId: input.changeSetId,
|
|
277
277
|
targetRef: canonicalTargetRef,
|
|
278
|
-
checkCommands: requestedChecks
|
|
279
|
-
evidenceRefs: changeSet.manifest.evidenceRefs
|
|
278
|
+
checkCommands: requestedChecks
|
|
280
279
|
}, now());
|
|
281
|
-
// Durable positive exact-SHA evidence on an unchanged target, at the lane
|
|
282
|
-
// head, validates the entry so processing skips its checks. The process-
|
|
283
|
-
// path evidence fence still invalidates the binding if the target moves
|
|
284
|
-
// before this entry runs, so a non-empty advance never rebinds old
|
|
285
|
-
// evidence. A converged ChangeSet with explicit gates queues normally:
|
|
286
|
-
// the no-op apply still runs the caller's checks against the current
|
|
287
|
-
// target instead of waiving them.
|
|
288
|
-
if (await canReuseEvidenceAtHead(tx, task.id, tx.listIntegrationQueueEntries(task.id), entry, changeSet, effectiveTargetHead)) {
|
|
289
|
-
entry = markIntegrationQueueValidated(entry, now(), effectiveTargetHead);
|
|
290
|
-
}
|
|
291
280
|
tx.saveIntegrationQueueEntry(task.id, entry);
|
|
292
281
|
return { entry, outcome: "queued" };
|
|
293
282
|
}
|
|
@@ -314,10 +303,21 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
314
303
|
taskId: task.id,
|
|
315
304
|
projectId: input.projectId,
|
|
316
305
|
targetRef: canonicalTargetRef,
|
|
317
|
-
|
|
318
|
-
|
|
306
|
+
beforeCommit: effectiveTargetHead,
|
|
307
|
+
source: {
|
|
308
|
+
kind: "work-item",
|
|
309
|
+
workItemId: changeSet.workItemId,
|
|
310
|
+
startCommit: changeSet.baseCommit,
|
|
311
|
+
resultCommit: changeSet.headCommit,
|
|
312
|
+
strategy: "cherry-pick"
|
|
313
|
+
},
|
|
319
314
|
checkCommands: []
|
|
320
|
-
}, now()), {
|
|
315
|
+
}, now()), {
|
|
316
|
+
status: "committed",
|
|
317
|
+
candidateCommit: effectiveTargetHead,
|
|
318
|
+
afterCommit: effectiveTargetHead,
|
|
319
|
+
summary: `WorkItem ${changeSet.workItemId} result was already represented; Task head was unchanged.`
|
|
320
|
+
}, now());
|
|
321
321
|
const convergedEntry = createConvergedIntegrationQueueEntry({
|
|
322
322
|
id,
|
|
323
323
|
taskId: task.id,
|
|
@@ -331,7 +331,7 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
331
331
|
integrationAttemptId: attempt.id
|
|
332
332
|
}, now());
|
|
333
333
|
try {
|
|
334
|
-
await git.assertRefAt(
|
|
334
|
+
await git.assertRefAt(repositoryPath, exactBranchRef(canonicalTargetRef), effectiveTargetHead);
|
|
335
335
|
}
|
|
336
336
|
catch {
|
|
337
337
|
const queuedEntry = createIntegrationQueueEntry({
|
|
@@ -340,8 +340,7 @@ export async function enqueueIntegrationQueueEntry(input) {
|
|
|
340
340
|
projectId: input.projectId,
|
|
341
341
|
changeSetId: input.changeSetId,
|
|
342
342
|
targetRef: canonicalTargetRef,
|
|
343
|
-
checkCommands: requestedChecks
|
|
344
|
-
evidenceRefs: changeSet.manifest.evidenceRefs
|
|
343
|
+
checkCommands: requestedChecks
|
|
345
344
|
}, now());
|
|
346
345
|
tx.saveIntegrationQueueEntry(task.id, queuedEntry);
|
|
347
346
|
return { entry: queuedEntry, outcome: "queued" };
|
|
@@ -358,82 +357,10 @@ function findActiveQueueDuplicate(store, taskId, projectId, changeSetId) {
|
|
|
358
357
|
&& entry.status !== "superseded");
|
|
359
358
|
}
|
|
360
359
|
/**
|
|
361
|
-
*
|
|
362
|
-
* its ChangeSet carries durable positive exact-SHA evidence, the target is
|
|
363
|
-
* unchanged since the ChangeSet was based (so integrating fast-forwards to
|
|
364
|
-
* the reviewed head), no earlier lane entry will advance the target first,
|
|
365
|
-
* and the evidence covers every requested check command. Entries behind a
|
|
366
|
-
* lane mate stay `queued`: the mate's commit moves the target, so the
|
|
367
|
-
* evidence would not cover their integration anyway. A review check that
|
|
368
|
-
* passed for command X never waives a different gate Y.
|
|
369
|
-
*/
|
|
370
|
-
async function canReuseEvidenceAtHead(store, taskId, entries, entry, changeSet, targetHead) {
|
|
371
|
-
if (entry.evidenceRefs.length === 0)
|
|
372
|
-
return false;
|
|
373
|
-
if (targetHead !== changeSet.baseCommit)
|
|
374
|
-
return false;
|
|
375
|
-
const lane = queueLaneKey(entry);
|
|
376
|
-
if (entries.some((existing) => queueLaneKey(existing) === lane
|
|
377
|
-
&& existing.status !== "committed"
|
|
378
|
-
&& existing.status !== "superseded"))
|
|
379
|
-
return false;
|
|
380
|
-
return evidenceCoversCheckCommands(store, taskId, entry.evidenceRefs, entry.checkCommands, changeSet.headCommit, entry.projectId);
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Resolve each reusable evidence reference to its ReviewRound and collect the
|
|
384
|
-
* names of checks that passed. A requested gate command is covered only when
|
|
385
|
-
* a resolved round recorded that exact command as passed. A review that ran
|
|
386
|
-
* `true` (or any other unrelated check) never waives a different gate.
|
|
387
|
-
*
|
|
388
|
-
* A check is reusable only when it ran on the frozen candidate tree. A
|
|
389
|
-
* ReviewRound must attest to this by recording its `evidenceCommit` as the
|
|
390
|
-
* exact `reviewBaseCommit`. A missing `evidenceCommit` is ambiguous: the
|
|
391
|
-
* reviewer may have run checks with uncommitted dirty diagnostics, so the
|
|
392
|
-
* absence cannot be treated as frozen-tree proof. A round whose
|
|
393
|
-
* `evidenceCommit` differs from its `reviewBaseCommit` ran its checks on a
|
|
394
|
-
* diagnostic tree (the reviewer's own commit with review-only changes); those
|
|
395
|
-
* checks proved a different tree and cannot waive the candidate gate.
|
|
396
|
-
*/
|
|
397
|
-
async function evidenceCoversCheckCommands(store, taskId, evidenceRefs, checkCommands, candidateCommit, projectId) {
|
|
398
|
-
if (checkCommands.length === 0)
|
|
399
|
-
return true;
|
|
400
|
-
const covered = new Set();
|
|
401
|
-
for (const ref of evidenceRefs) {
|
|
402
|
-
// Issue 08: a `gate-artifact:` ref covers the exact commands its L2
|
|
403
|
-
// artifact passed on the exact candidate commit. The store resolves the
|
|
404
|
-
// artifact through its persistence backend (SQLite or file).
|
|
405
|
-
if (ref.startsWith("gate-artifact:")) {
|
|
406
|
-
if (await gateArtifactCoversCheckCommands(store, projectId, ref, checkCommands, candidateCommit)) {
|
|
407
|
-
for (const command of checkCommands)
|
|
408
|
-
covered.add(command);
|
|
409
|
-
}
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
const roundId = parseReviewRoundRef(ref);
|
|
413
|
-
if (roundId === undefined)
|
|
414
|
-
continue;
|
|
415
|
-
const round = store.getReviewRound(taskId, roundId);
|
|
416
|
-
if (round === null || round.status !== "completed")
|
|
417
|
-
continue;
|
|
418
|
-
if (round.evidenceCommit !== round.reviewBaseCommit)
|
|
419
|
-
continue;
|
|
420
|
-
for (const check of round.checks ?? []) {
|
|
421
|
-
if (check.outcome === "passed")
|
|
422
|
-
covered.add(check.name);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return checkCommands.every((cmd) => covered.has(cmd));
|
|
426
|
-
}
|
|
427
|
-
function parseReviewRoundRef(ref) {
|
|
428
|
-
const prefix = "review-round:";
|
|
429
|
-
return ref.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Process queued items in id order. Each item gets a fresh IntegrationAttempt
|
|
360
|
+
* Process queued items in id order. Each item gets a fresh IntegrationAttempt
|
|
433
361
|
* on the exact current target; conflicts and gate failures map the item to
|
|
434
362
|
* `conflicted` without touching the others. After every commit the remaining
|
|
435
|
-
* items recompute overlap
|
|
436
|
-
* its evidence coverage and runs its checks again.
|
|
363
|
+
* items recompute overlap diagnostics.
|
|
437
364
|
*
|
|
438
365
|
* A lane (one Project/targetRef pair) integrates one item at a time: while an
|
|
439
366
|
* item is `running` — claimed by this or another processor — no other item of
|
|
@@ -467,22 +394,8 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
|
|
|
467
394
|
const project = store.getProject(candidate.projectId);
|
|
468
395
|
if (project === null)
|
|
469
396
|
throw new Error(`Project not found: ${candidate.projectId}.`);
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
// exact target head it was validated against. An out-of-band target
|
|
473
|
-
// advance (another Task) that touches the entry's paths, or whose impact
|
|
474
|
-
// cannot be proven, invalidates the evidence: requeue and run its checks.
|
|
475
|
-
if (candidate.status === "validated"
|
|
476
|
-
&& await evidenceTargetAdvanced(git, project.path, candidate, targetBefore, store, task.id)) {
|
|
477
|
-
store.transaction((tx) => {
|
|
478
|
-
assertTaskActive(tx, task.id);
|
|
479
|
-
const current = tx.getIntegrationQueueEntry(task.id, candidate.id);
|
|
480
|
-
if (current !== null && current.status === "validated") {
|
|
481
|
-
tx.saveIntegrationQueueEntry(task.id, markIntegrationQueueRequeued(current, now()));
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
397
|
+
const repositoryPath = taskMainRepositoryPath(store, task.id, project.id);
|
|
398
|
+
const targetBefore = (await git.inspect(repositoryPath, exactBranchRef(candidate.targetRef))).baseCommit;
|
|
486
399
|
// CAS claim: re-read the entry inside the write transaction. Another
|
|
487
400
|
// store instance may have taken it since selection; if the status changed,
|
|
488
401
|
// do not create an Attempt and re-select instead. The running barrier is
|
|
@@ -490,8 +403,7 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
|
|
|
490
403
|
// between selection and claim serializes this entry behind it.
|
|
491
404
|
const claimed = store.transaction((tx) => {
|
|
492
405
|
const current = tx.getIntegrationQueueEntry(task.id, candidate.id);
|
|
493
|
-
if (current === null
|
|
494
|
-
|| (current.status !== "queued" && current.status !== "validated")) {
|
|
406
|
+
if (current === null || current.status !== "queued") {
|
|
495
407
|
return { skipped: true };
|
|
496
408
|
}
|
|
497
409
|
if (laneBlockedByRunningEntry(tx.listIntegrationQueueEntries(task.id), current)) {
|
|
@@ -506,8 +418,8 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
|
|
|
506
418
|
// entered retry (or whose latest Candidate no longer matches) must not
|
|
507
419
|
// advance the target. Mark it conflicted so the Leader can supersede
|
|
508
420
|
// or re-enqueue a fresh ChangeSet.
|
|
421
|
+
const changeSet = tx.getChangeSet(task.id, current.changeSetId);
|
|
509
422
|
try {
|
|
510
|
-
const changeSet = tx.getChangeSet(task.id, current.changeSetId);
|
|
511
423
|
if (changeSet === null) {
|
|
512
424
|
throw new Error(`ChangeSet not found: ${current.changeSetId}.`);
|
|
513
425
|
}
|
|
@@ -519,14 +431,23 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
|
|
|
519
431
|
tx.saveIntegrationQueueEntry(task.id, markIntegrationQueueBlocked(running, error instanceof Error ? error.message : String(error), now()));
|
|
520
432
|
return { skipped: true };
|
|
521
433
|
}
|
|
434
|
+
if (changeSet === null) {
|
|
435
|
+
throw new Error(`ChangeSet not found after validation: ${current.changeSetId}.`);
|
|
436
|
+
}
|
|
522
437
|
const attempt = createIntegrationAttempt({
|
|
523
438
|
id: tx.nextIntegrationAttemptId(task.id),
|
|
524
439
|
taskId: task.id,
|
|
525
440
|
projectId: current.projectId,
|
|
526
441
|
targetRef: current.targetRef,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
442
|
+
beforeCommit: targetBefore,
|
|
443
|
+
source: {
|
|
444
|
+
kind: "work-item",
|
|
445
|
+
workItemId: changeSet.workItemId,
|
|
446
|
+
startCommit: changeSet.baseCommit,
|
|
447
|
+
resultCommit: changeSet.headCommit,
|
|
448
|
+
strategy: "cherry-pick"
|
|
449
|
+
},
|
|
450
|
+
checkCommands: current.checkCommands
|
|
530
451
|
}, now());
|
|
531
452
|
tx.saveIntegrationAttempt(task.id, attempt);
|
|
532
453
|
const entry = recordIntegrationQueueAttempt(markIntegrationQueueRunning(current, targetBefore, now()), attempt.id, now());
|
|
@@ -553,10 +474,7 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
|
|
|
553
474
|
return entry;
|
|
554
475
|
});
|
|
555
476
|
if (result.status === "committed") {
|
|
556
|
-
|
|
557
|
-
if (project !== null) {
|
|
558
|
-
await recomputeAffectedPaths(store, task.id, settled, project.path, git, now);
|
|
559
|
-
}
|
|
477
|
+
await recomputeAffectedPaths(store, task.id, settled, taskMainRepositoryPath(store, task.id, settled.projectId), git, now);
|
|
560
478
|
}
|
|
561
479
|
processed.push({ entry: settled, attempt: result.attempt, result });
|
|
562
480
|
}
|
|
@@ -588,7 +506,7 @@ function queueLaneKey(entry) {
|
|
|
588
506
|
return `${entry.projectId} ${canonicalizeTargetRef(entry.targetRef)}`;
|
|
589
507
|
}
|
|
590
508
|
/**
|
|
591
|
-
* The first entry a processor may claim: the earliest queued
|
|
509
|
+
* The first entry a processor may claim: the earliest queued entry
|
|
592
510
|
* whose lane has no running entry anywhere in it. A running entry serializes
|
|
593
511
|
* its whole lane — predecessors and successors alike — so a requeued
|
|
594
512
|
* predecessor cannot slip past a successor that is already running.
|
|
@@ -605,7 +523,7 @@ function firstClaimableQueueEntry(entries, projectId) {
|
|
|
605
523
|
for (const entry of entries) {
|
|
606
524
|
if (projectId !== undefined && entry.projectId !== projectId)
|
|
607
525
|
continue;
|
|
608
|
-
if (
|
|
526
|
+
if (entry.status === "queued"
|
|
609
527
|
&& !blockedLanes.has(queueLaneKey(entry))) {
|
|
610
528
|
return entry;
|
|
611
529
|
}
|
|
@@ -624,50 +542,6 @@ function laneBlockedByRunningEntry(entries, candidate) {
|
|
|
624
542
|
&& entry.status === "running"
|
|
625
543
|
&& queueLaneKey(entry) === lane);
|
|
626
544
|
}
|
|
627
|
-
/**
|
|
628
|
-
* Whether a validated entry's reusable evidence no longer covers the current
|
|
629
|
-
* target. The evidence only proves the gate as of `entry.evidenceTargetHead`;
|
|
630
|
-
* an out-of-band target advance (another Task) on the entry's own paths, or any
|
|
631
|
-
* advance whose impact on the entry's gate cannot be proven, invalidates it.
|
|
632
|
-
* Returns false (evidence still covers the target) only when proven unaffected.
|
|
633
|
-
*/
|
|
634
|
-
async function evidenceTargetAdvanced(git, repositoryPath, entry, currentTargetHead, store, taskId) {
|
|
635
|
-
const boundary = entry.evidenceTargetHead;
|
|
636
|
-
if (boundary === undefined)
|
|
637
|
-
return true;
|
|
638
|
-
if (boundary === currentTargetHead)
|
|
639
|
-
return false;
|
|
640
|
-
// The target advanced out of band since the evidence boundary. Compute the
|
|
641
|
-
// real path delta and re-run the gate unless the entry is provably unaffected.
|
|
642
|
-
if (git.changedFilesBetween === undefined)
|
|
643
|
-
return true;
|
|
644
|
-
let delta;
|
|
645
|
-
try {
|
|
646
|
-
delta = await git.changedFilesBetween({
|
|
647
|
-
repositoryPath,
|
|
648
|
-
fromCommit: boundary,
|
|
649
|
-
toCommit: currentTargetHead
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
return true;
|
|
654
|
-
}
|
|
655
|
-
const changeSet = store.getChangeSet(taskId, entry.changeSetId);
|
|
656
|
-
if (changeSet === null)
|
|
657
|
-
return true;
|
|
658
|
-
const ownPaths = new Set([
|
|
659
|
-
...changeSet.changedPaths,
|
|
660
|
-
...changeSet.manifest.deletedPaths
|
|
661
|
-
]);
|
|
662
|
-
if (delta.some((path) => ownPaths.has(path)))
|
|
663
|
-
return true;
|
|
664
|
-
// The delta does not touch the entry's own paths, but the entry's gate may
|
|
665
|
-
// read any file — including the exact target SHA the evidence is bound to.
|
|
666
|
-
// A changed target SHA invalidates exact-SHA evidence regardless of tree
|
|
667
|
-
// identity, so any entry with checks must re-run its gate. An entry
|
|
668
|
-
// without checks has nothing to re-run and may commit directly.
|
|
669
|
-
return entry.checkCommands.length > 0;
|
|
670
|
-
}
|
|
671
545
|
/**
|
|
672
546
|
* A conflicted entry whose manual-resolution Attempt committed converges. The
|
|
673
547
|
* settle happens in one transaction; the downstream affected-path recompute
|
|
@@ -693,10 +567,7 @@ export async function reconcileIntegrationQueueEntry(store, taskId, entryId, git
|
|
|
693
567
|
tx.saveIntegrationQueueEntry(taskId, settled);
|
|
694
568
|
return settled;
|
|
695
569
|
});
|
|
696
|
-
|
|
697
|
-
if (project !== null) {
|
|
698
|
-
await recomputeAffectedPaths(store, taskId, committed, project.path, git, now);
|
|
699
|
-
}
|
|
570
|
+
await recomputeAffectedPaths(store, taskId, committed, taskMainRepositoryPath(store, taskId, committed.projectId), git, now);
|
|
700
571
|
return committed;
|
|
701
572
|
}
|
|
702
573
|
/**
|
|
@@ -786,10 +657,7 @@ async function reconcileTerminalAttempts(store, taskId, git, now) {
|
|
|
786
657
|
// Crash window: the settle marked this entry committed but died before
|
|
787
658
|
// recomputing downstream affectedPaths. Replay that update idempotently
|
|
788
659
|
// so waiting entries regain their overlap evidence.
|
|
789
|
-
|
|
790
|
-
if (project !== null) {
|
|
791
|
-
await recomputeAffectedPaths(store, taskId, entry, project.path, git, now);
|
|
792
|
-
}
|
|
660
|
+
await recomputeAffectedPaths(store, taskId, entry, taskMainRepositoryPath(store, taskId, entry.projectId), git, now);
|
|
793
661
|
continue;
|
|
794
662
|
}
|
|
795
663
|
if (entry.status !== "conflicted" && entry.status !== "running")
|
|
@@ -800,10 +668,7 @@ async function reconcileTerminalAttempts(store, taskId, git, now) {
|
|
|
800
668
|
if (attempt.status === "committed") {
|
|
801
669
|
const committed = markIntegrationQueueCommitted(entry, committedTargetAfter(attempt), now());
|
|
802
670
|
store.saveIntegrationQueueEntry(taskId, committed);
|
|
803
|
-
|
|
804
|
-
if (project !== null) {
|
|
805
|
-
await recomputeAffectedPaths(store, taskId, committed, project.path, git, now);
|
|
806
|
-
}
|
|
671
|
+
await recomputeAffectedPaths(store, taskId, committed, taskMainRepositoryPath(store, taskId, committed.projectId), git, now);
|
|
807
672
|
continue;
|
|
808
673
|
}
|
|
809
674
|
// Crash window: the Attempt persisted a terminal failed/blocked outcome
|
|
@@ -832,7 +697,7 @@ async function recomputeAffectedPaths(store, taskId, committed, projectPath, git
|
|
|
832
697
|
continue;
|
|
833
698
|
if (canonicalizeTargetRef(entry.targetRef) !== canonicalizeTargetRef(committed.targetRef))
|
|
834
699
|
continue;
|
|
835
|
-
if (entry.status !== "queued"
|
|
700
|
+
if (entry.status !== "queued")
|
|
836
701
|
continue;
|
|
837
702
|
const changeSet = store.getChangeSet(taskId, entry.changeSetId);
|
|
838
703
|
if (changeSet === null)
|
|
@@ -853,21 +718,16 @@ async function recomputeAffectedPaths(store, taskId, committed, projectPath, git
|
|
|
853
718
|
const current = tx.getIntegrationQueueEntry(taskId, entry.id);
|
|
854
719
|
if (current === null)
|
|
855
720
|
return;
|
|
856
|
-
if (current.status !== "queued"
|
|
721
|
+
if (current.status !== "queued")
|
|
857
722
|
return;
|
|
858
|
-
|
|
859
|
-
if (updated.status === "validated" && (updated.affectedPaths?.length ?? 0) > 0) {
|
|
860
|
-
// The target advanced onto this entry's own paths: its evidence no
|
|
861
|
-
// longer covers the target, so the gate must run again.
|
|
862
|
-
updated = markIntegrationQueueRequeued(updated, now());
|
|
863
|
-
}
|
|
723
|
+
const updated = recordIntegrationQueueAffectedPaths(current, affected, now());
|
|
864
724
|
// A queued entry keeps its place and runs its checks against the exact
|
|
865
725
|
// current target at process time. Disjoint changedPaths must never
|
|
866
726
|
// rebind its evidence to the new target head: a gate may read any file,
|
|
867
727
|
// so a non-empty target increment cannot be proven irrelevant from path
|
|
868
728
|
// metadata alone.
|
|
869
|
-
if (updated.
|
|
870
|
-
|
|
729
|
+
if ((updated.affectedPaths ?? []).join("\n")
|
|
730
|
+
!== (current.affectedPaths ?? []).join("\n")) {
|
|
871
731
|
tx.saveIntegrationQueueEntry(taskId, updated);
|
|
872
732
|
}
|
|
873
733
|
});
|
|
@@ -937,3 +797,14 @@ function taskMainBranch(store, taskId, projectId) {
|
|
|
937
797
|
return undefined;
|
|
938
798
|
return workspaceProjectEntry(mainWorkspace, projectId)?.branch;
|
|
939
799
|
}
|
|
800
|
+
function taskMainRepositoryPath(store, taskId, projectId) {
|
|
801
|
+
const mainWorkspace = store.getTaskWorkspace(taskId);
|
|
802
|
+
if (mainWorkspace === null || mainWorkspace.owner.type !== "task") {
|
|
803
|
+
throw new Error(`Task has no authoritative main workspace: ${taskId}.`);
|
|
804
|
+
}
|
|
805
|
+
const entry = workspaceProjectEntry(mainWorkspace, projectId);
|
|
806
|
+
if (entry === undefined) {
|
|
807
|
+
throw new Error(`Task main workspace has no Project: ${taskId}/${projectId}.`);
|
|
808
|
+
}
|
|
809
|
+
return entry.path;
|
|
810
|
+
}
|