@zq-silk/yui 0.2.0 → 0.4.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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
+
import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
4
|
+
/** The one execution abstraction used by both the one-lane and panel paths. */
|
|
5
|
+
export const EXECUTION_GROUP_SCHEMA_VERSION = 1;
|
|
6
|
+
export const EXECUTION_LANE_SCHEMA_VERSION = 1;
|
|
7
|
+
export function createExecutionGroup(id, taskId, input, now) {
|
|
8
|
+
const timestamp = now.toISOString();
|
|
9
|
+
const strategy = normalizeStrategy(input.strategy ?? { mode: "fixed", count: 1 });
|
|
10
|
+
const target = validateExecutionTarget(input.target, taskId);
|
|
11
|
+
const laneInputs = input.lanes === undefined
|
|
12
|
+
? [{ roleName: input.roleName ?? "leader" }]
|
|
13
|
+
: input.lanes;
|
|
14
|
+
if (laneInputs.length === 0) {
|
|
15
|
+
throw new Error("An ExecutionGroup requires at least one Lane.");
|
|
16
|
+
}
|
|
17
|
+
if (strategy.mode === "fixed" && laneInputs.length > strategy.count) {
|
|
18
|
+
throw new Error("ExecutionGroup Lane count exceeds its fixed strategy.");
|
|
19
|
+
}
|
|
20
|
+
if (strategy.mode === "adaptive" && laneInputs.length > strategy.max) {
|
|
21
|
+
throw new Error("ExecutionGroup Lane count exceeds its adaptive maximum.");
|
|
22
|
+
}
|
|
23
|
+
const lanes = laneInputs.map((lane, index) => createLane(`${id}-lane-${lane.id ?? index + 1}`, id, lane, index + 1, timestamp));
|
|
24
|
+
return validateExecutionGroup({
|
|
25
|
+
schemaVersion: EXECUTION_GROUP_SCHEMA_VERSION,
|
|
26
|
+
id: requireIdentity(id, "ExecutionGroup id"),
|
|
27
|
+
taskId: requireIdentity(taskId, "Task id"),
|
|
28
|
+
purpose: validatePurpose(input.purpose),
|
|
29
|
+
strategy,
|
|
30
|
+
target,
|
|
31
|
+
lanes,
|
|
32
|
+
createdAt: timestamp,
|
|
33
|
+
updatedAt: timestamp
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function addExecutionLane(group, input, now) {
|
|
37
|
+
validateExecutionGroup(group);
|
|
38
|
+
if (group.resolution !== undefined) {
|
|
39
|
+
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
40
|
+
}
|
|
41
|
+
if (!isDeepStrictEqual(group.target, validateExecutionTarget(group.target, group.taskId))) {
|
|
42
|
+
throw new Error(`ExecutionGroup target is not frozen: ${group.id}.`);
|
|
43
|
+
}
|
|
44
|
+
const nextCount = group.lanes.length + 1;
|
|
45
|
+
if (group.strategy.mode === "fixed" && nextCount > group.strategy.count) {
|
|
46
|
+
throw new Error(`Fixed ExecutionGroup cannot add Lane ${nextCount}: ${group.id}.`);
|
|
47
|
+
}
|
|
48
|
+
if (group.strategy.mode === "adaptive" && nextCount > group.strategy.max) {
|
|
49
|
+
throw new Error(`Adaptive ExecutionGroup reached its maximum: ${group.id}.`);
|
|
50
|
+
}
|
|
51
|
+
const timestamp = now.toISOString();
|
|
52
|
+
const lane = createLane(`${group.id}-lane-${input.id ?? nextCount}`, group.id, input, input.ordinal ?? nextCount, timestamp);
|
|
53
|
+
if (group.lanes.some((existing) => existing.id === lane.id)) {
|
|
54
|
+
throw new Error(`ExecutionGroup Lane already exists: ${lane.id}.`);
|
|
55
|
+
}
|
|
56
|
+
return validateExecutionGroup({
|
|
57
|
+
...group,
|
|
58
|
+
lanes: [...group.lanes, lane],
|
|
59
|
+
updatedAt: timestamp
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Alias used by scheduler-facing callers. */
|
|
63
|
+
export const appendExecutionLane = addExecutionLane;
|
|
64
|
+
export function assertExecutionTargetUnchanged(group, target) {
|
|
65
|
+
validateExecutionGroup(group);
|
|
66
|
+
const next = validateExecutionTarget(target, group.taskId);
|
|
67
|
+
if (!isDeepStrictEqual(group.target, next)) {
|
|
68
|
+
throw new Error(`ExecutionGroup target changed; create a new Group: ${group.id}.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Enforce the only durable Group/Lane evolution accepted by both domain
|
|
73
|
+
* helpers and storage. Identity, target, prior results, and a final Leader
|
|
74
|
+
* resolution never move backwards. A terminal Lane may only reopen as an
|
|
75
|
+
* explicit retry with a fresh Run identity.
|
|
76
|
+
*/
|
|
77
|
+
export function assertExecutionGroupTransition(existing, candidate) {
|
|
78
|
+
validateExecutionGroup(existing);
|
|
79
|
+
validateExecutionGroup(candidate);
|
|
80
|
+
if (isDeepStrictEqual(existing, candidate))
|
|
81
|
+
return;
|
|
82
|
+
if (existing.id !== candidate.id
|
|
83
|
+
|| existing.taskId !== candidate.taskId
|
|
84
|
+
|| existing.purpose !== candidate.purpose
|
|
85
|
+
|| !isDeepStrictEqual(existing.strategy, candidate.strategy)
|
|
86
|
+
|| !isDeepStrictEqual(existing.target, candidate.target)
|
|
87
|
+
|| existing.createdAt !== candidate.createdAt) {
|
|
88
|
+
throw new Error(`ExecutionGroup identity or target changed: ${existing.id}.`);
|
|
89
|
+
}
|
|
90
|
+
if (Date.parse(candidate.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
91
|
+
throw new Error(`ExecutionGroup time moved backwards: ${existing.id}.`);
|
|
92
|
+
}
|
|
93
|
+
if (existing.resolution !== undefined) {
|
|
94
|
+
throw new Error(`Resolved ExecutionGroup is immutable: ${existing.id}.`);
|
|
95
|
+
}
|
|
96
|
+
if (candidate.lanes.length < existing.lanes.length) {
|
|
97
|
+
throw new Error(`ExecutionGroup cannot remove Lanes: ${existing.id}.`);
|
|
98
|
+
}
|
|
99
|
+
for (const [index, lane] of existing.lanes.entries()) {
|
|
100
|
+
const next = candidate.lanes[index];
|
|
101
|
+
if (next === undefined) {
|
|
102
|
+
throw new Error(`ExecutionGroup cannot remove Lane: ${lane.id}.`);
|
|
103
|
+
}
|
|
104
|
+
assertExecutionLaneTransition(lane, next, existing.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function isExecutionGroupTransition(existing, candidate) {
|
|
108
|
+
try {
|
|
109
|
+
assertExecutionGroupTransition(existing, candidate);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function updateExecutionLane(group, laneId, patch, now) {
|
|
117
|
+
validateExecutionGroup(group);
|
|
118
|
+
if (group.resolution !== undefined) {
|
|
119
|
+
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
120
|
+
}
|
|
121
|
+
const id = requireIdentity(laneId, "ExecutionLane id");
|
|
122
|
+
const existing = group.lanes.find((lane) => lane.id === id);
|
|
123
|
+
if (existing === undefined)
|
|
124
|
+
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
125
|
+
if (isTerminalLane(existing.status) && patch.status !== existing.status) {
|
|
126
|
+
throw new Error(`Terminal ExecutionLane cannot be updated; create a retry Lane: ${group.id}/${id}.`);
|
|
127
|
+
}
|
|
128
|
+
const status = patch.status ?? existing.status;
|
|
129
|
+
const timestamp = now.toISOString();
|
|
130
|
+
const terminal = status === "completed" || status === "failed" || status === "yielded";
|
|
131
|
+
const next = {
|
|
132
|
+
...existing,
|
|
133
|
+
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
134
|
+
...(patch.runId === undefined ? {} : { runId: requireIdentity(patch.runId, "Agent Run id") }),
|
|
135
|
+
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
136
|
+
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
137
|
+
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
138
|
+
status,
|
|
139
|
+
updatedAt: timestamp,
|
|
140
|
+
...(terminal && existing.endedAt === undefined ? { endedAt: timestamp } : {})
|
|
141
|
+
};
|
|
142
|
+
return validateExecutionGroup({
|
|
143
|
+
...group,
|
|
144
|
+
lanes: group.lanes.map((lane) => lane.id === id ? next : lane),
|
|
145
|
+
updatedAt: timestamp
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reopens a failed/yielded lane for an explicit WorkItem retry. The lane
|
|
150
|
+
* keeps its identity and frozen Group target, while the previous result stays
|
|
151
|
+
* in the event/Candidate history rather than being silently overwritten.
|
|
152
|
+
*/
|
|
153
|
+
export function restartExecutionLane(group, laneId, patch, now) {
|
|
154
|
+
validateExecutionGroup(group);
|
|
155
|
+
if (group.resolution !== undefined) {
|
|
156
|
+
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
157
|
+
}
|
|
158
|
+
const id = requireIdentity(laneId, "ExecutionLane id");
|
|
159
|
+
const existing = group.lanes.find((lane) => lane.id === id);
|
|
160
|
+
if (existing === undefined)
|
|
161
|
+
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
162
|
+
if (!isTerminalLane(existing.status)) {
|
|
163
|
+
throw new Error(`ExecutionLane is not retryable: ${group.id}/${id}.`);
|
|
164
|
+
}
|
|
165
|
+
const timestamp = now.toISOString();
|
|
166
|
+
const { result: _result, endedAt: _endedAt, ...base } = existing;
|
|
167
|
+
const next = {
|
|
168
|
+
...base,
|
|
169
|
+
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
170
|
+
...(patch.runId === undefined ? {} : { runId: requireIdentity(patch.runId, "Agent Run id") }),
|
|
171
|
+
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
172
|
+
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
173
|
+
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
174
|
+
status: "running",
|
|
175
|
+
updatedAt: timestamp
|
|
176
|
+
};
|
|
177
|
+
return validateExecutionGroup({
|
|
178
|
+
...group,
|
|
179
|
+
lanes: group.lanes.map((lane) => lane.id === id ? next : lane),
|
|
180
|
+
updatedAt: timestamp
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export function recordExecutionLaneResult(group, laneId, result, status, now) {
|
|
184
|
+
const checked = validateLaneResult(result);
|
|
185
|
+
validateExecutionGroup(group);
|
|
186
|
+
const existing = group.lanes.find((lane) => lane.id === laneId);
|
|
187
|
+
if (existing === undefined)
|
|
188
|
+
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
189
|
+
if (isTerminalLane(existing.status)) {
|
|
190
|
+
throw new Error(`ExecutionLane is already terminal: ${group.id}/${laneId}.`);
|
|
191
|
+
}
|
|
192
|
+
const timestamp = now.toISOString();
|
|
193
|
+
return validateExecutionGroup({
|
|
194
|
+
...group,
|
|
195
|
+
lanes: group.lanes.map((lane) => lane.id === laneId
|
|
196
|
+
? { ...lane, status, result: checked, updatedAt: timestamp, endedAt: timestamp }
|
|
197
|
+
: lane),
|
|
198
|
+
updatedAt: timestamp
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function resolveExecutionGroup(group, input, now) {
|
|
202
|
+
validateExecutionGroup(group);
|
|
203
|
+
if (group.resolution !== undefined) {
|
|
204
|
+
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
205
|
+
}
|
|
206
|
+
if (group.lanes.some((lane) => !isTerminalLane(lane.status))) {
|
|
207
|
+
throw new Error(`ExecutionGroup has active Lanes: ${group.id}.`);
|
|
208
|
+
}
|
|
209
|
+
const summary = requireText(input.summary, "Execution resolution summary");
|
|
210
|
+
const defaultSelectedLaneIds = input.decision === "accept"
|
|
211
|
+
? group.lanes
|
|
212
|
+
.filter((lane) => lane.status === "yielded" || lane.status === "completed")
|
|
213
|
+
.map(({ id }) => id)
|
|
214
|
+
: group.lanes.map(({ id }) => id);
|
|
215
|
+
const selected = [...new Set((input.selectedLaneIds ?? defaultSelectedLaneIds)
|
|
216
|
+
.map((id) => requireIdentity(id, "Selected ExecutionLane id")))];
|
|
217
|
+
if (input.decision === "accept" && selected.length === 0) {
|
|
218
|
+
throw new Error(`ExecutionGroup has no usable terminal Lane outputs: ${group.id}.`);
|
|
219
|
+
}
|
|
220
|
+
if (input.decision === "accept" && selected.some((id) => {
|
|
221
|
+
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
222
|
+
return lane?.status !== "yielded" && lane?.status !== "completed";
|
|
223
|
+
})) {
|
|
224
|
+
throw new Error(`ExecutionGroup accept selects a Lane without usable terminal output: ${group.id}.`);
|
|
225
|
+
}
|
|
226
|
+
if (selected.some((id) => !group.lanes.some((lane) => lane.id === id))) {
|
|
227
|
+
throw new Error(`Execution resolution selects an unknown Lane: ${group.id}.`);
|
|
228
|
+
}
|
|
229
|
+
const openHigh = openHighPriorityFindingIds(group);
|
|
230
|
+
if (input.decision === "accept" && openHigh.length > 0) {
|
|
231
|
+
throw new Error(`ExecutionGroup cannot be accepted with open high-priority findings: ${openHigh.join(", ")}.`);
|
|
232
|
+
}
|
|
233
|
+
const timestamp = now.toISOString();
|
|
234
|
+
return validateExecutionGroup({
|
|
235
|
+
...group,
|
|
236
|
+
resolution: {
|
|
237
|
+
decision: input.decision,
|
|
238
|
+
summary,
|
|
239
|
+
selectedLaneIds: selected,
|
|
240
|
+
unresolvedFindingIds: openHigh,
|
|
241
|
+
decidedAt: timestamp
|
|
242
|
+
},
|
|
243
|
+
updatedAt: timestamp
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export function summarizeExecutionGroup(group) {
|
|
247
|
+
validateExecutionGroup(group);
|
|
248
|
+
const activeLaneCount = group.lanes.filter(({ status }) => (status === "pending" || status === "running")).length;
|
|
249
|
+
const terminalLaneCount = group.lanes.length - activeLaneCount;
|
|
250
|
+
return {
|
|
251
|
+
groupId: group.id,
|
|
252
|
+
purpose: group.purpose,
|
|
253
|
+
strategy: group.strategy,
|
|
254
|
+
laneCount: group.lanes.length,
|
|
255
|
+
activeLaneCount,
|
|
256
|
+
terminalLaneCount,
|
|
257
|
+
failedLaneCount: group.lanes.filter(({ status }) => status === "failed").length,
|
|
258
|
+
openHighPriorityFindingIds: openHighPriorityFindingIds(group),
|
|
259
|
+
laneSummaries: group.lanes.map((lane) => ({
|
|
260
|
+
laneId: lane.id,
|
|
261
|
+
roleName: lane.roleName,
|
|
262
|
+
ordinal: lane.ordinal,
|
|
263
|
+
status: lane.status,
|
|
264
|
+
...(lane.result === undefined ? {} : {
|
|
265
|
+
summary: lane.result.summary,
|
|
266
|
+
...(lane.result.report === undefined ? {} : { report: lane.result.report }),
|
|
267
|
+
...(lane.result.checks === undefined ? {} : { checks: lane.result.checks }),
|
|
268
|
+
...(lane.result.findings === undefined ? {} : { findings: lane.result.findings }),
|
|
269
|
+
...(lane.result.evidence === undefined ? {} : { evidence: lane.result.evidence }),
|
|
270
|
+
...(lane.result.evidenceCommit === undefined ? {} : { evidenceCommit: lane.result.evidenceCommit }),
|
|
271
|
+
...(lane.result.gitSnapshot === undefined ? {} : { gitSnapshot: lane.result.gitSnapshot })
|
|
272
|
+
}),
|
|
273
|
+
...(group.resolution === undefined
|
|
274
|
+
|| !group.resolution.selectedLaneIds.includes(lane.id)
|
|
275
|
+
? {}
|
|
276
|
+
: { decision: group.resolution.decision })
|
|
277
|
+
})),
|
|
278
|
+
...(group.resolution === undefined ? {} : { resolution: group.resolution })
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function validateExecutionGroup(group) {
|
|
282
|
+
if (group.schemaVersion !== EXECUTION_GROUP_SCHEMA_VERSION) {
|
|
283
|
+
throw new Error("ExecutionGroup must use schemaVersion 1.");
|
|
284
|
+
}
|
|
285
|
+
const taskId = requireIdentity(group.taskId, "Task id");
|
|
286
|
+
requireIdentity(group.id, "ExecutionGroup id");
|
|
287
|
+
validatePurpose(group.purpose);
|
|
288
|
+
normalizeStrategy(group.strategy);
|
|
289
|
+
validateExecutionTarget(group.target, taskId);
|
|
290
|
+
if (!Array.isArray(group.lanes) || group.lanes.length === 0) {
|
|
291
|
+
throw new Error("ExecutionGroup requires at least one Lane.");
|
|
292
|
+
}
|
|
293
|
+
const ids = new Set();
|
|
294
|
+
const ordinals = new Set();
|
|
295
|
+
const roots = new Set();
|
|
296
|
+
for (const lane of group.lanes) {
|
|
297
|
+
validateExecutionLane(lane, group);
|
|
298
|
+
if (ids.has(lane.id))
|
|
299
|
+
throw new Error(`ExecutionLane is duplicated: ${lane.id}.`);
|
|
300
|
+
if (ordinals.has(lane.ordinal))
|
|
301
|
+
throw new Error(`ExecutionLane ordinal is duplicated: ${lane.ordinal}.`);
|
|
302
|
+
ids.add(lane.id);
|
|
303
|
+
ordinals.add(lane.ordinal);
|
|
304
|
+
if (lane.workspace !== undefined) {
|
|
305
|
+
if (roots.has(lane.workspace.root) && lane.workspace.writableProjectIds.length > 0) {
|
|
306
|
+
throw new Error(`Writable ExecutionLane workspace is shared: ${lane.workspace.root}.`);
|
|
307
|
+
}
|
|
308
|
+
if (lane.workspace.writableProjectIds.length > 0)
|
|
309
|
+
roots.add(lane.workspace.root);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const expectedMax = group.strategy.mode === "fixed" ? group.strategy.count : group.strategy.max;
|
|
313
|
+
if (group.lanes.length > expectedMax)
|
|
314
|
+
throw new Error("ExecutionGroup has too many Lanes.");
|
|
315
|
+
if (group.resolution !== undefined) {
|
|
316
|
+
if (group.lanes.some(({ status }) => !isTerminalLane(status))) {
|
|
317
|
+
throw new Error(`Resolved ExecutionGroup has active Lanes: ${group.id}.`);
|
|
318
|
+
}
|
|
319
|
+
validateResolution(group.resolution, group);
|
|
320
|
+
}
|
|
321
|
+
requireTimestamp(group.createdAt, "ExecutionGroup createdAt");
|
|
322
|
+
requireTimestamp(group.updatedAt, "ExecutionGroup updatedAt");
|
|
323
|
+
return group;
|
|
324
|
+
}
|
|
325
|
+
export function validateExecutionLane(lane, group) {
|
|
326
|
+
if (lane.schemaVersion !== EXECUTION_LANE_SCHEMA_VERSION) {
|
|
327
|
+
throw new Error("ExecutionLane must use schemaVersion 1.");
|
|
328
|
+
}
|
|
329
|
+
requireIdentity(lane.id, "ExecutionLane id");
|
|
330
|
+
requireIdentity(lane.groupId, "ExecutionGroup id");
|
|
331
|
+
if (group !== undefined) {
|
|
332
|
+
if (lane.groupId !== group.id)
|
|
333
|
+
throw new Error(`ExecutionLane belongs to another Group: ${lane.id}.`);
|
|
334
|
+
if (group.purpose === "review" && lane.reviewRoundId === undefined) {
|
|
335
|
+
// A review lane may be pending before its ReviewRound is dispatched, but
|
|
336
|
+
// it must acquire that identity before it can run.
|
|
337
|
+
if (lane.status === "running" || lane.status === "completed" || lane.status === "failed") {
|
|
338
|
+
throw new Error(`Running Reviewer Lane requires a ReviewRound: ${lane.id}.`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
positiveInteger(lane.ordinal, "ExecutionLane ordinal");
|
|
343
|
+
requireIdentity(lane.roleName, "ExecutionLane Role");
|
|
344
|
+
if (lane.effective !== undefined)
|
|
345
|
+
validateEffectiveLaunchSnapshot(lane.effective);
|
|
346
|
+
if (lane.runId !== undefined)
|
|
347
|
+
requireIdentity(lane.runId, "Agent Run id");
|
|
348
|
+
if (lane.sessionId !== undefined)
|
|
349
|
+
requireIdentity(lane.sessionId, "Session id");
|
|
350
|
+
if (lane.reviewRoundId !== undefined)
|
|
351
|
+
requireIdentity(lane.reviewRoundId, "ReviewRound id");
|
|
352
|
+
if (lane.workspace !== undefined)
|
|
353
|
+
validateLaneWorkspace(lane.workspace);
|
|
354
|
+
if (!["pending", "running", "yielded", "completed", "failed"].includes(lane.status)) {
|
|
355
|
+
throw new Error(`ExecutionLane status is invalid: ${String(lane.status)}.`);
|
|
356
|
+
}
|
|
357
|
+
if (lane.result !== undefined)
|
|
358
|
+
validateLaneResult(lane.result);
|
|
359
|
+
requireTimestamp(lane.createdAt, "ExecutionLane createdAt");
|
|
360
|
+
requireTimestamp(lane.updatedAt, "ExecutionLane updatedAt");
|
|
361
|
+
if (isTerminalLane(lane.status)) {
|
|
362
|
+
requireTimestamp(lane.endedAt ?? "", "ExecutionLane endedAt");
|
|
363
|
+
if (lane.result === undefined)
|
|
364
|
+
throw new Error(`Terminal ExecutionLane requires a result: ${lane.id}.`);
|
|
365
|
+
}
|
|
366
|
+
else if (lane.endedAt !== undefined || lane.result !== undefined) {
|
|
367
|
+
throw new Error(`Active ExecutionLane cannot have terminal output: ${lane.id}.`);
|
|
368
|
+
}
|
|
369
|
+
return lane;
|
|
370
|
+
}
|
|
371
|
+
function createLane(id, groupId, input, ordinal, timestamp) {
|
|
372
|
+
return validateExecutionLane({
|
|
373
|
+
schemaVersion: EXECUTION_LANE_SCHEMA_VERSION,
|
|
374
|
+
id: requireIdentity(id, "ExecutionLane id"),
|
|
375
|
+
groupId: requireIdentity(groupId, "ExecutionGroup id"),
|
|
376
|
+
ordinal: positiveInteger(input.ordinal ?? ordinal, "ExecutionLane ordinal"),
|
|
377
|
+
roleName: requireIdentity(input.roleName, "ExecutionLane Role"),
|
|
378
|
+
...(input.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(input.effective) }),
|
|
379
|
+
...(input.runId === undefined ? {} : { runId: requireIdentity(input.runId, "Agent Run id") }),
|
|
380
|
+
...(input.sessionId === undefined ? {} : { sessionId: requireIdentity(input.sessionId, "Session id") }),
|
|
381
|
+
...(input.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(input.reviewRoundId, "ReviewRound id") }),
|
|
382
|
+
...(input.workspace === undefined ? {} : { workspace: validateLaneWorkspace(input.workspace) }),
|
|
383
|
+
status: "pending",
|
|
384
|
+
createdAt: timestamp,
|
|
385
|
+
updatedAt: timestamp
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
export function validateExecutionTarget(target, taskId) {
|
|
389
|
+
if (target === null || typeof target !== "object" || target.schemaVersion !== 1) {
|
|
390
|
+
throw new Error("ExecutionTarget must use schemaVersion 1.");
|
|
391
|
+
}
|
|
392
|
+
if (target.taskId !== taskId)
|
|
393
|
+
throw new Error("ExecutionTarget belongs to another Task.");
|
|
394
|
+
if (target.kind !== "work-item" && target.kind !== "task-final-review") {
|
|
395
|
+
throw new Error(`ExecutionTarget kind is invalid: ${String(target.kind)}.`);
|
|
396
|
+
}
|
|
397
|
+
positiveInteger(target.revision, "ExecutionTarget revision");
|
|
398
|
+
if (!Array.isArray(target.projects))
|
|
399
|
+
throw new Error("ExecutionTarget Projects are invalid.");
|
|
400
|
+
const projects = new Set();
|
|
401
|
+
for (const project of target.projects) {
|
|
402
|
+
const id = requireIdentity(project.projectId, "ExecutionTarget Project");
|
|
403
|
+
if (projects.has(id))
|
|
404
|
+
throw new Error(`ExecutionTarget Project is duplicated: ${id}.`);
|
|
405
|
+
projects.add(id);
|
|
406
|
+
requireCommit(project.commit, "ExecutionTarget commit");
|
|
407
|
+
}
|
|
408
|
+
if (target.kind === "work-item" && target.workItemId === undefined) {
|
|
409
|
+
throw new Error("WorkItem ExecutionTarget requires a Work Item id.");
|
|
410
|
+
}
|
|
411
|
+
if (target.kind === "task-final-review" && target.candidateId === undefined) {
|
|
412
|
+
throw new Error("Task-final ExecutionTarget requires a Candidate id.");
|
|
413
|
+
}
|
|
414
|
+
if (target.workItemId !== undefined)
|
|
415
|
+
requireIdentity(target.workItemId, "Work Item id");
|
|
416
|
+
if (target.candidateId !== undefined)
|
|
417
|
+
requireIdentity(target.candidateId, "Candidate id");
|
|
418
|
+
if (target.contractDigest !== undefined)
|
|
419
|
+
requireText(target.contractDigest, "Execution contract digest");
|
|
420
|
+
requireText(target.fingerprint, "ExecutionTarget fingerprint");
|
|
421
|
+
return target;
|
|
422
|
+
}
|
|
423
|
+
function validateLaneWorkspace(workspace) {
|
|
424
|
+
requireText(workspace.root, "ExecutionLane workspace root");
|
|
425
|
+
if (!Array.isArray(workspace.writableProjectIds))
|
|
426
|
+
throw new Error("ExecutionLane workspace Projects are invalid.");
|
|
427
|
+
const ids = workspace.writableProjectIds.map((id) => requireIdentity(id, "ExecutionLane writable Project"));
|
|
428
|
+
if (new Set(ids).size !== ids.length)
|
|
429
|
+
throw new Error("ExecutionLane writable Projects are duplicated.");
|
|
430
|
+
return workspace;
|
|
431
|
+
}
|
|
432
|
+
function validateLaneResult(result) {
|
|
433
|
+
requireText(result.summary, "ExecutionLane summary");
|
|
434
|
+
if (result.report !== undefined)
|
|
435
|
+
requireText(result.report, "ExecutionLane report");
|
|
436
|
+
if (result.checks !== undefined) {
|
|
437
|
+
for (const check of result.checks) {
|
|
438
|
+
requireText(check.name, "Execution check name");
|
|
439
|
+
if (!["passed", "failed", "skipped"].includes(check.outcome))
|
|
440
|
+
throw new Error("Execution check outcome is invalid.");
|
|
441
|
+
if (check.details !== undefined)
|
|
442
|
+
requireText(check.details, "Execution check details");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (result.findings !== undefined) {
|
|
446
|
+
const ids = new Set();
|
|
447
|
+
for (const finding of result.findings) {
|
|
448
|
+
const id = requireIdentity(finding.id, "Execution finding id");
|
|
449
|
+
if (ids.has(id))
|
|
450
|
+
throw new Error(`Execution finding is duplicated: ${id}.`);
|
|
451
|
+
ids.add(id);
|
|
452
|
+
if (!["low", "medium", "high", "critical"].includes(finding.severity))
|
|
453
|
+
throw new Error("Execution finding severity is invalid.");
|
|
454
|
+
requireText(finding.summary, "Execution finding summary");
|
|
455
|
+
if (finding.status !== "open" && finding.status !== "resolved")
|
|
456
|
+
throw new Error("Execution finding status is invalid.");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (result.evidence !== undefined)
|
|
460
|
+
result.evidence.forEach((value) => requireText(value, "Execution evidence"));
|
|
461
|
+
if (result.evidenceCommit !== undefined) {
|
|
462
|
+
requireCommit(result.evidenceCommit, "Execution evidence commit");
|
|
463
|
+
}
|
|
464
|
+
if (result.gitSnapshot !== undefined)
|
|
465
|
+
validateExecutionLaneGitSnapshot(result.gitSnapshot);
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
function validateExecutionLaneGitSnapshot(snapshot) {
|
|
469
|
+
if (snapshot === null || typeof snapshot !== "object" || snapshot.schemaVersion !== 1) {
|
|
470
|
+
throw new Error("Execution Lane Git snapshot must use schemaVersion 1.");
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(snapshot.projects) || snapshot.projects.length === 0) {
|
|
473
|
+
throw new Error("Execution Lane Git snapshot requires Projects.");
|
|
474
|
+
}
|
|
475
|
+
const ids = new Set();
|
|
476
|
+
for (const project of snapshot.projects) {
|
|
477
|
+
const projectId = requireIdentity(project.projectId, "Execution Lane snapshot Project");
|
|
478
|
+
if (ids.has(projectId))
|
|
479
|
+
throw new Error(`Execution Lane snapshot Project is duplicated: ${projectId}.`);
|
|
480
|
+
ids.add(projectId);
|
|
481
|
+
requireCommit(project.headCommit, "Execution Lane snapshot head commit");
|
|
482
|
+
requireText(project.branch, "Execution Lane snapshot branch");
|
|
483
|
+
}
|
|
484
|
+
return snapshot;
|
|
485
|
+
}
|
|
486
|
+
function validateResolution(resolution, group) {
|
|
487
|
+
if (!["accept", "reject", "retry", "blocked"].includes(resolution.decision))
|
|
488
|
+
throw new Error("Execution resolution decision is invalid.");
|
|
489
|
+
requireText(resolution.summary, "Execution resolution summary");
|
|
490
|
+
const laneIds = new Set(group.lanes.map(({ id }) => id));
|
|
491
|
+
if (new Set(resolution.selectedLaneIds).size !== resolution.selectedLaneIds.length
|
|
492
|
+
|| resolution.selectedLaneIds.some((id) => !laneIds.has(id))) {
|
|
493
|
+
throw new Error("Execution resolution Lane selection is invalid.");
|
|
494
|
+
}
|
|
495
|
+
if (resolution.decision === "accept"
|
|
496
|
+
&& (resolution.selectedLaneIds.length === 0
|
|
497
|
+
|| resolution.selectedLaneIds.some((id) => {
|
|
498
|
+
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
499
|
+
return lane?.status !== "yielded" && lane?.status !== "completed";
|
|
500
|
+
}))) {
|
|
501
|
+
throw new Error("Execution accept resolution must select usable Lane output.");
|
|
502
|
+
}
|
|
503
|
+
requireTimestamp(resolution.decidedAt, "Execution resolution time");
|
|
504
|
+
const findings = openHighPriorityFindingIds(group);
|
|
505
|
+
if (!isDeepStrictEqual([...resolution.unresolvedFindingIds].sort(), [...findings].sort())) {
|
|
506
|
+
throw new Error("Execution resolution findings do not match the unresolved high-priority findings.");
|
|
507
|
+
}
|
|
508
|
+
if (resolution.decision === "accept" && findings.length > 0) {
|
|
509
|
+
throw new Error("Execution accept resolution cannot retain high-priority findings.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function assertExecutionLaneTransition(existing, candidate, groupId) {
|
|
513
|
+
if (existing.id !== candidate.id
|
|
514
|
+
|| existing.groupId !== candidate.groupId
|
|
515
|
+
|| existing.groupId !== groupId
|
|
516
|
+
|| existing.ordinal !== candidate.ordinal
|
|
517
|
+
|| existing.roleName !== candidate.roleName
|
|
518
|
+
|| existing.createdAt !== candidate.createdAt
|
|
519
|
+
|| existing.reviewRoundId !== candidate.reviewRoundId) {
|
|
520
|
+
throw new Error(`ExecutionLane identity changed: ${existing.id}.`);
|
|
521
|
+
}
|
|
522
|
+
if (Date.parse(candidate.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
523
|
+
throw new Error(`ExecutionLane time moved backwards: ${existing.id}.`);
|
|
524
|
+
}
|
|
525
|
+
if (isTerminalLane(existing.status)) {
|
|
526
|
+
if (isDeepStrictEqual(existing, candidate))
|
|
527
|
+
return;
|
|
528
|
+
if (candidate.status !== "running"
|
|
529
|
+
|| candidate.runId === undefined
|
|
530
|
+
|| candidate.runId === existing.runId) {
|
|
531
|
+
throw new Error(`Terminal ExecutionLane is immutable without a fresh retry Run: ${existing.id}.`);
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (existing.status === "running" && candidate.status === "pending") {
|
|
536
|
+
throw new Error(`Running ExecutionLane cannot return to pending: ${existing.id}.`);
|
|
537
|
+
}
|
|
538
|
+
for (const key of ["effective", "runId", "sessionId", "workspace"]) {
|
|
539
|
+
if (existing[key] !== undefined
|
|
540
|
+
&& !isDeepStrictEqual(existing[key], candidate[key])) {
|
|
541
|
+
throw new Error(`ExecutionLane ${key} changed without retry: ${existing.id}.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function openHighPriorityFindingIds(group) {
|
|
546
|
+
return group.lanes.flatMap((lane) => (lane.result?.findings ?? [])
|
|
547
|
+
.filter((finding) => finding.status === "open" && (finding.severity === "high" || finding.severity === "critical"))
|
|
548
|
+
.map(({ id }) => id));
|
|
549
|
+
}
|
|
550
|
+
function normalizeStrategy(strategy) {
|
|
551
|
+
if (strategy.mode === "fixed") {
|
|
552
|
+
positiveInteger(strategy.count, "Fixed ExecutionGroup Lane count");
|
|
553
|
+
return { mode: "fixed", count: strategy.count };
|
|
554
|
+
}
|
|
555
|
+
if (strategy.mode === "adaptive") {
|
|
556
|
+
positiveInteger(strategy.max, "Adaptive ExecutionGroup Lane maximum");
|
|
557
|
+
return { mode: "adaptive", max: strategy.max };
|
|
558
|
+
}
|
|
559
|
+
throw new Error(`Execution strategy is invalid: ${String(strategy.mode)}.`);
|
|
560
|
+
}
|
|
561
|
+
function validatePurpose(purpose) {
|
|
562
|
+
if (purpose !== "execution" && purpose !== "review")
|
|
563
|
+
throw new Error(`Execution purpose is invalid: ${String(purpose)}.`);
|
|
564
|
+
return purpose;
|
|
565
|
+
}
|
|
566
|
+
function isTerminalLane(status) {
|
|
567
|
+
return status === "yielded" || status === "completed" || status === "failed";
|
|
568
|
+
}
|
|
569
|
+
function positiveInteger(value, label) {
|
|
570
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
571
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
function requireCommit(value, label) {
|
|
575
|
+
const commit = requireText(value, label).toLowerCase();
|
|
576
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit))
|
|
577
|
+
throw new Error(`${label} is invalid.`);
|
|
578
|
+
return commit;
|
|
579
|
+
}
|