@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
|
@@ -27,6 +27,13 @@ export async function reconcileTaskRemoteBaselines(taskId, store, home, options
|
|
|
27
27
|
|| git.mergeRemoteIntoWorktree === undefined) {
|
|
28
28
|
throw usageError(`Task ${task.id} cannot verify remote baselines: managed Git workspace support is unavailable.`);
|
|
29
29
|
}
|
|
30
|
+
// A completed Task-final Review that exactly covers the current frozen heads
|
|
31
|
+
// is the final review evidence. Do not reconcile the remote after a clean
|
|
32
|
+
// review, as merging a moved remote would change the reviewed head and
|
|
33
|
+
// invalidate the review.
|
|
34
|
+
if (await hasCompletedTaskFinalReviewForCurrentCandidate(store, task, workspace, git)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
30
37
|
const plans = [];
|
|
31
38
|
for (const binding of task.projectBindings) {
|
|
32
39
|
const project = store.getProject(binding.projectId);
|
|
@@ -41,6 +48,13 @@ export async function reconcileTaskRemoteBaselines(taskId, store, home, options
|
|
|
41
48
|
}
|
|
42
49
|
const currentCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
|
|
43
50
|
const previousIntegration = latestCommittedIntegration(store, task.id, project.id);
|
|
51
|
+
// A completed Task-final Review for the exact current head freezes the
|
|
52
|
+
// Task baseline: the delivery has been reviewed and approved, so the
|
|
53
|
+
// completion gate must not merge remote changes that would invalidate
|
|
54
|
+
// the review evidence.
|
|
55
|
+
if (hasFrozenTaskBaseline(store, task.id, project.id, currentCommit)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
44
58
|
// Let the existing exact-head Task-final gate report local post-
|
|
45
59
|
// Integration drift before attempting any network operation. This keeps
|
|
46
60
|
// completion read-only for an already-invalid candidate and preserves its
|
|
@@ -70,6 +84,13 @@ export async function reconcileTaskRemoteBaselines(taskId, store, home, options
|
|
|
70
84
|
const reconciled = [];
|
|
71
85
|
for (const plan of plans) {
|
|
72
86
|
const attempt = store.transaction((tx) => {
|
|
87
|
+
// Re-check the frozen baseline inside the transaction: a Task-final
|
|
88
|
+
// Review may have completed during the async remote fetch, freezing
|
|
89
|
+
// the head after the initial check. Creating a new Integration here
|
|
90
|
+
// would move the reviewed head and invalidate the review evidence.
|
|
91
|
+
if (hasFrozenTaskBaseline(tx, task.id, plan.project.id, plan.currentCommit)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
73
94
|
const created = createIntegrationAttempt({
|
|
74
95
|
id: tx.nextIntegrationAttemptId(task.id),
|
|
75
96
|
taskId: task.id,
|
|
@@ -82,11 +103,25 @@ export async function reconcileTaskRemoteBaselines(taskId, store, home, options
|
|
|
82
103
|
tx.saveIntegrationAttempt(task.id, created);
|
|
83
104
|
return created;
|
|
84
105
|
});
|
|
106
|
+
if (attempt === null)
|
|
107
|
+
continue;
|
|
85
108
|
const baseline = {
|
|
86
109
|
remoteUrl: plan.project.remoteUrl,
|
|
87
110
|
branch: plan.remote.branch
|
|
88
111
|
};
|
|
89
|
-
const result = await new GitIntegrationService(home, store, git, options.now ?? (() => new Date()), options.environment).integrate(task.id, attempt.id, { remoteBaseline: baseline });
|
|
112
|
+
const result = await new GitIntegrationService(home, store, git, options.now ?? (() => new Date()), options.environment, undefined, options.jobPort).integrate(task.id, attempt.id, { remoteBaseline: baseline });
|
|
113
|
+
// rr6/f3: A moved remote with non-empty checks spawns a DurableJob. This
|
|
114
|
+
// is a pending completion outcome, not a failure: name the exact
|
|
115
|
+
// Integration and Job and the exact continuation command (mirroring the
|
|
116
|
+
// `task integration` checks-running message) so the caller can resume the
|
|
117
|
+
// same remote-reconciliation attempt instead of being blocked by the
|
|
118
|
+
// preflight's active-job/unresolved-Integration gates.
|
|
119
|
+
if (result.status === "checks-running") {
|
|
120
|
+
throw usageError(`Remote baseline reconciliation for ${task.id}/${plan.project.id} is running checks as `
|
|
121
|
+
+ `Integration ${result.attempt.id} (DurableJob ${result.job.id}); run `
|
|
122
|
+
+ `'yui task integration continue ${task.id}/${result.attempt.id}' when the job finishes, `
|
|
123
|
+
+ `then retry task complete.`);
|
|
124
|
+
}
|
|
90
125
|
if (result.status !== "committed" || result.attempt.candidateCommit === undefined) {
|
|
91
126
|
const detail = result.attempt.checks?.find(({ outcome }) => outcome === "failed")?.details
|
|
92
127
|
?? result.attempt.conflict?.summary
|
|
@@ -123,9 +158,51 @@ function requireTaskWorkspace(store, task) {
|
|
|
123
158
|
}
|
|
124
159
|
return workspace;
|
|
125
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Check whether the latest Task-final ReviewRound is completed and its
|
|
163
|
+
* immutable candidate exactly matches the current actual heads for every
|
|
164
|
+
* Project in the Task. When this holds, the reviewed head is the final
|
|
165
|
+
* head and remote reconciliation must not change it.
|
|
166
|
+
*/
|
|
167
|
+
async function hasCompletedTaskFinalReviewForCurrentCandidate(store, task, workspace, git) {
|
|
168
|
+
const rounds = store.listReviewRounds(task.id)
|
|
169
|
+
.filter((round) => (round.scope ?? "work-item") === "task")
|
|
170
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
171
|
+
const latest = rounds.at(-1);
|
|
172
|
+
if (latest === undefined || latest.status !== "completed")
|
|
173
|
+
return false;
|
|
174
|
+
if (latest.taskCandidate === undefined)
|
|
175
|
+
return false;
|
|
176
|
+
const candidateProjects = new Map(latest.taskCandidate.projects.map((entry) => [entry.projectId, entry.commit]));
|
|
177
|
+
for (const binding of task.projectBindings) {
|
|
178
|
+
const reviewedCommit = candidateProjects.get(binding.projectId);
|
|
179
|
+
if (reviewedCommit === undefined)
|
|
180
|
+
return false;
|
|
181
|
+
const entry = workspaceProjectEntry(workspace, binding.projectId);
|
|
182
|
+
if (entry === undefined || entry.access !== "write")
|
|
183
|
+
return false;
|
|
184
|
+
const actualHead = (await git.inspect(entry.path, "HEAD")).baseCommit;
|
|
185
|
+
if (actualHead !== reviewedCommit)
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
126
190
|
function latestCommittedIntegration(store, taskId, projectId) {
|
|
127
191
|
return store.listIntegrationAttempts(taskId)
|
|
128
192
|
.filter((attempt) => attempt.projectId === projectId && attempt.status === "committed")
|
|
129
193
|
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
|
|
130
194
|
.at(-1);
|
|
131
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* A Task baseline is frozen when a completed (clean) Task-final ReviewRound
|
|
198
|
+
* reviewed the exact current head for the given Project. Once frozen, the
|
|
199
|
+
* completion gate skips remote baseline reconciliation to prevent merging
|
|
200
|
+
* unreviewed remote changes into the approved delivery.
|
|
201
|
+
*/
|
|
202
|
+
function hasFrozenTaskBaseline(store, taskId, projectId, currentCommit) {
|
|
203
|
+
return store.listReviewRounds(taskId)
|
|
204
|
+
.some((round) => ((round.scope ?? "work-item") === "task"
|
|
205
|
+
&& round.status === "completed"
|
|
206
|
+
&& round.taskCandidate !== undefined
|
|
207
|
+
&& round.taskCandidate.projects.some((entry) => (entry.projectId === projectId && entry.commit === currentCommit))));
|
|
208
|
+
}
|
|
@@ -3,6 +3,7 @@ import { taskMessageAuthorLabel } from "../message/message.js";
|
|
|
3
3
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
4
4
|
import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
|
|
5
5
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
6
|
+
import { projectNextAction } from "../task/nextAction.js";
|
|
6
7
|
import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
|
|
7
8
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
8
9
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
@@ -31,6 +32,14 @@ export function runTaskContextCommand(args, store) {
|
|
|
31
32
|
throw new Error(`Task execution projection disappeared: ${task.id}.`);
|
|
32
33
|
}
|
|
33
34
|
const roleSessionSets = reader.listRoleSessionSets(task.id);
|
|
35
|
+
const agentRuns = chronological(reader.listAgentRuns(task.id));
|
|
36
|
+
const reviewRounds = chronological(reader.listReviewRounds(task.id));
|
|
37
|
+
const changeSets = chronological(reader.listChangeSets(task.id));
|
|
38
|
+
const integrations = chronological(reader.listIntegrationAttempts(task.id));
|
|
39
|
+
const nextActionFacts = reader.readNextActionFacts(task.id);
|
|
40
|
+
if (nextActionFacts === null) {
|
|
41
|
+
throw new Error(`Task next-action facts disappeared: ${task.id}.`);
|
|
42
|
+
}
|
|
34
43
|
return {
|
|
35
44
|
task,
|
|
36
45
|
execution,
|
|
@@ -44,17 +53,18 @@ export function runTaskContextCommand(args, store) {
|
|
|
44
53
|
roleSessionSets,
|
|
45
54
|
roleSessionRecoveries: roles.map((role) => (inspectTaskRoleSessionRecovery(task.id, role.name, reader))),
|
|
46
55
|
workItems,
|
|
47
|
-
agentRuns
|
|
48
|
-
reviewRounds
|
|
49
|
-
changeSets
|
|
50
|
-
integrations
|
|
56
|
+
agentRuns,
|
|
57
|
+
reviewRounds,
|
|
58
|
+
changeSets,
|
|
59
|
+
integrations,
|
|
51
60
|
messages: reader.listMessages(task.id),
|
|
52
61
|
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
53
62
|
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
54
|
-
events: reader.listEvents(task.id)
|
|
63
|
+
events: reader.listEvents(task.id),
|
|
64
|
+
nextAction: projectNextAction(nextActionFacts)
|
|
55
65
|
};
|
|
56
66
|
});
|
|
57
|
-
const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, messages, openInputRequests, resolvedInputRequests, events } = data;
|
|
67
|
+
const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
|
|
58
68
|
const timeZone = store.getConfig().timeZone;
|
|
59
69
|
const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
|
|
60
70
|
const displayedWorkItems = currentAndRecentWorkItems(workItems);
|
|
@@ -67,6 +77,23 @@ export function runTaskContextCommand(args, store) {
|
|
|
67
77
|
`Task context: ${task.id}`,
|
|
68
78
|
`Title: ${compactText(task.title)}`,
|
|
69
79
|
`Status: ${task.status}`,
|
|
80
|
+
"Next action:",
|
|
81
|
+
` ${nextAction.kind}: ${nextAction.reason}`,
|
|
82
|
+
...(nextAction.refs.length === 0
|
|
83
|
+
? []
|
|
84
|
+
: [` Refs: ${nextAction.refs.map(({ kind, id }) => `${kind} ${id}`).join(", ")}`]),
|
|
85
|
+
...(nextAction.recommendedCommand === undefined
|
|
86
|
+
? []
|
|
87
|
+
: [` Recommended: ${nextAction.recommendedCommand}`]),
|
|
88
|
+
...(nextAction.judgmentRequired === undefined
|
|
89
|
+
? []
|
|
90
|
+
: [` Judgment: ${nextAction.judgmentRequired}`]),
|
|
91
|
+
...(nextAction.alternatives === undefined || nextAction.alternatives.length === 0
|
|
92
|
+
? []
|
|
93
|
+
: [
|
|
94
|
+
" Alternatives:",
|
|
95
|
+
...nextAction.alternatives.map((alternative) => ` ${alternative.kind}: ${alternative.reason}`)
|
|
96
|
+
]),
|
|
70
97
|
...(task.description === undefined ? [] : [`Description: ${compactText(task.description)}`]),
|
|
71
98
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
72
99
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
@@ -261,7 +261,7 @@ function assertInputCancelOrigin(store, request, environment) {
|
|
|
261
261
|
}
|
|
262
262
|
return "leader";
|
|
263
263
|
}
|
|
264
|
-
function isCurrentGlobalOperator(store, environment) {
|
|
264
|
+
export function isCurrentGlobalOperator(store, environment) {
|
|
265
265
|
if (environment.YUI_SESSION_SCOPE !== "global"
|
|
266
266
|
|| environment.YUI_ROLE !== "operator"
|
|
267
267
|
|| environment.YUI_TASK_ID !== undefined)
|
|
@@ -3,23 +3,27 @@ import { resolveProject } from "../repository/project.js";
|
|
|
3
3
|
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
-
import { createIntegrationAttempt, recordResolutionDecision, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
|
|
6
|
+
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
|
|
7
7
|
import { GitIntegrationService } from "../integration/gitIntegrationService.js";
|
|
8
|
+
import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
|
|
8
9
|
import { taskActor } from "./taskActor.js";
|
|
9
10
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
11
|
+
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
10
12
|
export async function runTaskIntegrationCommand(args, store, home, options = {}) {
|
|
11
13
|
const now = options.now ?? (() => new Date());
|
|
12
14
|
const [command, ...rest] = args;
|
|
13
15
|
if (command === "start")
|
|
14
|
-
return start(rest, store, home, now, options
|
|
16
|
+
return start(rest, store, home, now, options);
|
|
15
17
|
if (command === "continue") {
|
|
16
|
-
return continueIntegration(rest, store, home, now, options
|
|
18
|
+
return continueIntegration(rest, store, home, now, options);
|
|
17
19
|
}
|
|
18
20
|
if (command === "resolve") {
|
|
19
21
|
return resolveDecision(rest, store, now, options.environment);
|
|
20
22
|
}
|
|
21
23
|
if (command === "abort")
|
|
22
|
-
return abortIntegration(rest, store, now(), options
|
|
24
|
+
return abortIntegration(rest, store, now(), options);
|
|
25
|
+
if (command === "supersede")
|
|
26
|
+
return supersedeIntegrationCommand(rest, store, now(), options.environment);
|
|
23
27
|
if (command === "cleanup") {
|
|
24
28
|
return cleanupIntegration(rest, store, home, options.environment);
|
|
25
29
|
}
|
|
@@ -27,6 +31,9 @@ export async function runTaskIntegrationCommand(args, store, home, options = {})
|
|
|
27
31
|
return list(rest, store);
|
|
28
32
|
if (command === "show")
|
|
29
33
|
return show(rest, store, options.environment);
|
|
34
|
+
if (command === "queue") {
|
|
35
|
+
return runTaskIntegrationQueueCommand(rest, store, home, options);
|
|
36
|
+
}
|
|
30
37
|
throw usageError(command === undefined
|
|
31
38
|
? "Task Integration command is required."
|
|
32
39
|
: `Unknown command: task integration ${command}`);
|
|
@@ -36,9 +43,24 @@ async function cleanupIntegration(args, store, home, environment) {
|
|
|
36
43
|
throw usageError("Task Integration cleanup usage: yui task integration cleanup <task>/<integration>.");
|
|
37
44
|
}
|
|
38
45
|
const integration = requireIntegration(store, args[0], environment);
|
|
39
|
-
if (integration.status !== "committed"
|
|
46
|
+
if (integration.status !== "committed"
|
|
47
|
+
&& integration.status !== "superseded"
|
|
48
|
+
&& integration.status !== "failed") {
|
|
40
49
|
throw usageError(`Integration is not terminal: ${integration.id}/${integration.status}.`);
|
|
41
50
|
}
|
|
51
|
+
// rr4/finding-5: An Integration Attempt with an active DurableJob cannot be
|
|
52
|
+
// cleaned up — the runner may still be using its worktree. Block on queued,
|
|
53
|
+
// running, and unacknowledged unknown-needs-attention jobs owned by it.
|
|
54
|
+
const activeIntegrationJob = store.listDurableJobs(integration.taskId).find((job) => (job.owner.kind === "integration-attempt"
|
|
55
|
+
&& job.owner.integrationAttemptId === integration.id
|
|
56
|
+
&& (job.status === "queued"
|
|
57
|
+
|| job.status === "running"
|
|
58
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined))));
|
|
59
|
+
if (activeIntegrationJob !== undefined) {
|
|
60
|
+
throw usageError(`Integration ${integration.id} has an active DurableJob: `
|
|
61
|
+
+ `${activeIntegrationJob.id}/${activeIntegrationJob.status}. `
|
|
62
|
+
+ "Cancel or acknowledge it before cleanup.");
|
|
63
|
+
}
|
|
42
64
|
const result = await new GitIntegrationService(home, store).cleanup(integration);
|
|
43
65
|
if (result === "dirty") {
|
|
44
66
|
throw usageError(`Integration worktree contains unresolved or uncommitted changes: ${integration.id}. Inspect it before cleanup.`);
|
|
@@ -50,7 +72,7 @@ async function cleanupIntegration(args, store, home, environment) {
|
|
|
50
72
|
data: { integrationId: integration.id, cleanup: result }
|
|
51
73
|
};
|
|
52
74
|
}
|
|
53
|
-
async function start(args, store, home, now,
|
|
75
|
+
async function start(args, store, home, now, options) {
|
|
54
76
|
const usage = "Task Integration start usage: yui task integration start <task> [--project <project>] --change-set <id> [--change-set <id> ...] [--target <ref>] [--check <command> ...].";
|
|
55
77
|
const parsed = parseRepeatable(args, new Set(["--change-set", "--check"]), new Set(["--project", "--target"]), usage);
|
|
56
78
|
if (parsed.positionals.length !== 1)
|
|
@@ -70,9 +92,15 @@ async function start(args, store, home, now, environment) {
|
|
|
70
92
|
throw usageError(`ChangeSet not found: ${id}.`);
|
|
71
93
|
return changeSet;
|
|
72
94
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
// Only diagnostic evidence commits (a reviewer's own commit on top of the
|
|
96
|
+
// frozen base) are barred from becoming an Integration source. A clean
|
|
97
|
+
// review attests the frozen base itself (evidenceCommit === reviewBaseCommit),
|
|
98
|
+
// which is the candidate's own head and a legitimate source.
|
|
99
|
+
const diagnosticEvidence = new Set(store.listReviewRounds(task.id)
|
|
100
|
+
.flatMap(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit
|
|
101
|
+
? [evidenceCommit]
|
|
102
|
+
: []));
|
|
103
|
+
const reviewSource = changeSets.find(({ headCommit }) => diagnosticEvidence.has(headCommit));
|
|
76
104
|
if (reviewSource !== undefined) {
|
|
77
105
|
throw usageError(`ReviewRound evidence commit cannot become an Integration source: ${reviewSource.id}.`);
|
|
78
106
|
}
|
|
@@ -101,7 +129,16 @@ async function start(args, store, home, now, environment) {
|
|
|
101
129
|
throw usageError(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
|
|
102
130
|
}
|
|
103
131
|
const expectedHead = (await new NodeGitWorkspace().inspect(project.path, targetRef)).baseCommit;
|
|
132
|
+
// The duplicate/budget preflight runs inside the same transaction as the
|
|
133
|
+
// attempt insert: on the single-writer SQLite backend the check and the
|
|
134
|
+
// record creation are atomic, so a concurrent Leader cannot sneak a
|
|
135
|
+
// duplicate Integration between the guard and the write.
|
|
104
136
|
const integration = store.transaction((tx) => {
|
|
137
|
+
const guard = runDeliveryGuardPreflight(tx, task.id, {
|
|
138
|
+
kind: "integration-start",
|
|
139
|
+
projectId: project.id,
|
|
140
|
+
changeSetIds
|
|
141
|
+
}, { environment: options.environment, budget: true });
|
|
105
142
|
const created = createIntegrationAttempt({
|
|
106
143
|
id: tx.nextIntegrationAttemptId(task.id),
|
|
107
144
|
taskId: task.id,
|
|
@@ -112,32 +149,38 @@ async function start(args, store, home, now, environment) {
|
|
|
112
149
|
checkCommands: parsed.many.get("--check") ?? []
|
|
113
150
|
}, now());
|
|
114
151
|
tx.saveIntegrationAttempt(task.id, created);
|
|
115
|
-
return created;
|
|
152
|
+
return { attempt: created, guard };
|
|
116
153
|
});
|
|
117
|
-
|
|
154
|
+
const result = await runIntegration(store, home, integration.attempt, now, options);
|
|
155
|
+
return {
|
|
156
|
+
...result,
|
|
157
|
+
output: withGuardWarnings(integration.guard, result.output)
|
|
158
|
+
};
|
|
118
159
|
}
|
|
119
|
-
async function continueIntegration(args, store, home, now,
|
|
160
|
+
async function continueIntegration(args, store, home, now, options) {
|
|
120
161
|
const usage = "Task Integration continue usage: yui task integration continue <task>/<integration>.";
|
|
121
162
|
const parsed = parseRepeatable(args, new Set(), new Set(), usage);
|
|
122
163
|
if (parsed.positionals.length !== 1)
|
|
123
164
|
throw usageError(usage);
|
|
124
|
-
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
165
|
+
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
125
166
|
requireActiveIntegrationTask(store, integration);
|
|
126
167
|
if (integration.status !== "validating"
|
|
168
|
+
&& integration.status !== "running"
|
|
127
169
|
&& (integration.status !== "blocked"
|
|
128
170
|
|| integration.resolution?.action !== "manual-resolution")) {
|
|
129
171
|
throw usageError(`Integration is not ready to continue: ${integration.id}/${integration.status}.`);
|
|
130
172
|
}
|
|
131
|
-
return runIntegration(store, home, integration, now,
|
|
173
|
+
return runIntegration(store, home, integration, now, options);
|
|
132
174
|
}
|
|
133
|
-
async function runIntegration(store, home, integration, now,
|
|
134
|
-
const result = await new GitIntegrationService(home, store, undefined, now, environment)
|
|
135
|
-
.integrate(integration.taskId, integration.id);
|
|
175
|
+
async function runIntegration(store, home, integration, now, options) {
|
|
176
|
+
const result = await new GitIntegrationService(home, store, undefined, now, options.environment, undefined, options.jobPort).integrate(integration.taskId, integration.id);
|
|
136
177
|
const output = result.status === "committed"
|
|
137
178
|
? `Integrated ${result.attempt.changeSetIds.join(", ")} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
|
|
138
179
|
: result.status === "blocked"
|
|
139
180
|
? `Integration ${result.attempt.id} requires a Leader resolution decision in ${result.workspace.path}\n`
|
|
140
|
-
:
|
|
181
|
+
: result.status === "checks-running"
|
|
182
|
+
? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
|
|
183
|
+
: `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
|
|
141
184
|
return { output, data: result };
|
|
142
185
|
}
|
|
143
186
|
function resolveDecision(args, store, now, environment) {
|
|
@@ -166,12 +209,12 @@ function resolveDecision(args, store, now, environment) {
|
|
|
166
209
|
data: { integration: resolved }
|
|
167
210
|
};
|
|
168
211
|
}
|
|
169
|
-
function abortIntegration(args, store, now,
|
|
212
|
+
async function abortIntegration(args, store, now, options) {
|
|
170
213
|
const usage = "Task Integration abort usage: yui task integration abort <task>/<integration> --reason <text>.";
|
|
171
214
|
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
172
215
|
if (parsed.positionals.length !== 1)
|
|
173
216
|
throw usageError(usage);
|
|
174
|
-
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
217
|
+
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
175
218
|
requireActiveIntegrationTask(store, integration);
|
|
176
219
|
if (integration.status !== "running" && integration.status !== "blocked") {
|
|
177
220
|
throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
|
|
@@ -179,18 +222,77 @@ function abortIntegration(args, store, now, environment) {
|
|
|
179
222
|
const reason = parsed.one.get("--reason");
|
|
180
223
|
if (reason === undefined)
|
|
181
224
|
throw usageError(usage);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
225
|
+
if (integration.jobId !== undefined && options.jobPort !== undefined) {
|
|
226
|
+
await options.jobPort.cancelJob(integration.taskId, integration.jobId);
|
|
227
|
+
}
|
|
228
|
+
return store.transaction((tx) => {
|
|
229
|
+
// Re-read inside the transaction: a concurrent `continue` can advance the
|
|
230
|
+
// Attempt to validating (and then commit the target) after the initial
|
|
231
|
+
// read. Aborting a validating or committed Attempt would leave the
|
|
232
|
+
// target advanced while the Attempt is failed.
|
|
233
|
+
const current = tx.getIntegrationAttempt(integration.taskId, integration.id);
|
|
234
|
+
if (current === null) {
|
|
235
|
+
throw usageError(`Integration Attempt not found: ${integration.taskId}/${integration.id}.`);
|
|
236
|
+
}
|
|
237
|
+
if (current.status !== "running" && current.status !== "blocked") {
|
|
238
|
+
throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
|
|
239
|
+
}
|
|
240
|
+
const aborted = updateIntegrationAttempt(current, {
|
|
241
|
+
status: "failed",
|
|
242
|
+
checks: [
|
|
243
|
+
...(current.checks ?? []),
|
|
244
|
+
{ name: "aborted", outcome: "failed", details: reason }
|
|
245
|
+
]
|
|
246
|
+
}, now);
|
|
247
|
+
tx.saveIntegrationAttempt(aborted.taskId, aborted);
|
|
248
|
+
return {
|
|
249
|
+
output: `Aborted Integration ${aborted.id}; start a new Integration Attempt to retry\n`,
|
|
250
|
+
data: { integration: aborted }
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function supersedeIntegrationCommand(args, store, now, environment) {
|
|
255
|
+
const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
|
|
256
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
257
|
+
if (parsed.positionals.length !== 1)
|
|
258
|
+
throw usageError(usage);
|
|
259
|
+
const integration = requireIntegration(store, parsed.positionals[0], environment);
|
|
260
|
+
requireActiveIntegrationTask(store, integration);
|
|
261
|
+
if (integration.status !== "committed") {
|
|
262
|
+
throw usageError(`Integration cannot be superseded from ${integration.status}: ${integration.id}.`);
|
|
263
|
+
}
|
|
264
|
+
const reason = parsed.one.get("--reason");
|
|
265
|
+
if (reason === undefined)
|
|
266
|
+
throw usageError(usage);
|
|
267
|
+
// Only the Task Leader (or Operator/user) may supersede a committed
|
|
268
|
+
// Integration: it rewrites delivery-baseline evidence and audit history.
|
|
269
|
+
taskActor(environment, integration.taskId);
|
|
270
|
+
// A queue-backed committed Attempt cannot be superseded: the queue entry
|
|
271
|
+
// would remain in its current status while its Attempt becomes "superseded",
|
|
272
|
+
// leaving contradictory terminal records that never converge. This covers
|
|
273
|
+
// the crash window where the entry is still "running" or "conflicted" after
|
|
274
|
+
// the Attempt committed.
|
|
275
|
+
const queueBacked = store.listIntegrationQueueEntries(integration.taskId)
|
|
276
|
+
.some((entry) => entry.integrationAttemptId === integration.id);
|
|
277
|
+
if (queueBacked) {
|
|
278
|
+
throw usageError(`Integration ${integration.id} is backed by a queue entry; `
|
|
279
|
+
+ "reconcile the queue entry instead of superseding its Attempt.");
|
|
280
|
+
}
|
|
281
|
+
return store.transaction((tx) => {
|
|
282
|
+
const current = tx.getIntegrationAttempt(integration.taskId, integration.id);
|
|
283
|
+
if (current === null) {
|
|
284
|
+
throw usageError(`Integration Attempt not found: ${integration.taskId}/${integration.id}.`);
|
|
285
|
+
}
|
|
286
|
+
if (current.status !== "committed") {
|
|
287
|
+
throw usageError(`Integration cannot be superseded from ${current.status}: ${current.id}.`);
|
|
288
|
+
}
|
|
289
|
+
const superseded = supersedeIntegration(current, reason, now);
|
|
290
|
+
tx.saveIntegrationAttempt(superseded.taskId, superseded);
|
|
291
|
+
return {
|
|
292
|
+
output: `Superseded Integration ${superseded.id}; the next valid committed Integration is now the delivery baseline\n`,
|
|
293
|
+
data: { integration: superseded }
|
|
294
|
+
};
|
|
295
|
+
});
|
|
194
296
|
}
|
|
195
297
|
function requireActiveIntegrationTask(store, integration) {
|
|
196
298
|
const task = store.getTask(integration.taskId);
|
|
@@ -237,6 +339,7 @@ function show(args, store, environment) {
|
|
|
237
339
|
`Target: ${integration.targetRef}`,
|
|
238
340
|
`Expected head: ${integration.expectedHead}`,
|
|
239
341
|
`Candidate: ${integration.candidateCommit ?? "-"}`,
|
|
342
|
+
`Job: ${integration.jobId ?? "-"}`,
|
|
240
343
|
`ChangeSets: ${integration.changeSetIds.join(", ")}`,
|
|
241
344
|
`Status: ${integration.status}`,
|
|
242
345
|
`Conflict: ${integration.conflict?.summary ?? "-"}`,
|
|
@@ -273,7 +376,7 @@ function requireIntegration(store, value, environment) {
|
|
|
273
376
|
}
|
|
274
377
|
return attempt;
|
|
275
378
|
}
|
|
276
|
-
function parseRepeatable(args, repeatable, singular, usage) {
|
|
379
|
+
export function parseRepeatable(args, repeatable, singular, usage) {
|
|
277
380
|
const positionals = [];
|
|
278
381
|
const many = new Map();
|
|
279
382
|
const one = new Map();
|