@zq-silk/yui 0.14.0 → 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 +258 -125
- package/dist/agent/argumentPolicy.js +2 -1
- 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/sessionBootstrapManifest.js +18 -1
- 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 +51 -83
- 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 +165 -59
- 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 +27 -6
- 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,854 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
-
import { validateContextSnapshotRef } from "../context/contextSnapshot.js";
|
|
4
|
-
import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
5
|
-
/** The one execution abstraction used by both the one-lane and panel paths. */
|
|
6
|
-
export const EXECUTION_GROUP_SCHEMA_VERSION = 1;
|
|
7
|
-
export const EXECUTION_LANE_SCHEMA_VERSION = 1;
|
|
8
|
-
export const WORK_ITEM_EXPLORATION_MODES = [
|
|
9
|
-
"parallel-diverse",
|
|
10
|
-
"ensemble-replicated",
|
|
11
|
-
"adversarial",
|
|
12
|
-
"adaptive-exploration"
|
|
13
|
-
];
|
|
14
|
-
export const WORK_ITEM_EXECUTION_MODES = [
|
|
15
|
-
"single",
|
|
16
|
-
...WORK_ITEM_EXPLORATION_MODES
|
|
17
|
-
];
|
|
18
|
-
export const WORK_ITEM_EXPLORATION_STAGES = [
|
|
19
|
-
"plan",
|
|
20
|
-
"generate",
|
|
21
|
-
"compare",
|
|
22
|
-
"synthesize",
|
|
23
|
-
"verify",
|
|
24
|
-
"resolve"
|
|
25
|
-
];
|
|
26
|
-
export function createExecutionGroup(id, taskId, input, now) {
|
|
27
|
-
const timestamp = now.toISOString();
|
|
28
|
-
const groupId = requireIdentity(id, "ExecutionGroup id");
|
|
29
|
-
const normalizedTaskId = requireIdentity(taskId, "Task id");
|
|
30
|
-
const strategy = normalizeStrategy(input.strategy ?? { mode: "fixed", count: 1 });
|
|
31
|
-
const target = validateExecutionTarget(input.target, normalizedTaskId);
|
|
32
|
-
const laneInputs = input.lanes === undefined
|
|
33
|
-
? [{ roleName: input.roleName ?? "leader" }]
|
|
34
|
-
: input.lanes;
|
|
35
|
-
if (laneInputs.length === 0) {
|
|
36
|
-
throw new Error("An ExecutionGroup requires at least one Lane.");
|
|
37
|
-
}
|
|
38
|
-
if (strategy.mode === "fixed" && laneInputs.length > strategy.count) {
|
|
39
|
-
throw new Error("ExecutionGroup Lane count exceeds its fixed strategy.");
|
|
40
|
-
}
|
|
41
|
-
if (strategy.mode === "adaptive" && laneInputs.length > strategy.max) {
|
|
42
|
-
throw new Error("ExecutionGroup Lane count exceeds its adaptive maximum.");
|
|
43
|
-
}
|
|
44
|
-
const lanes = laneInputs.map((lane, index) => createLane(`${id}-lane-${lane.id ?? index + 1}`, id, lane, index + 1, timestamp));
|
|
45
|
-
return validateExecutionGroup({
|
|
46
|
-
schemaVersion: EXECUTION_GROUP_SCHEMA_VERSION,
|
|
47
|
-
id: groupId,
|
|
48
|
-
taskId: normalizedTaskId,
|
|
49
|
-
purpose: validatePurpose(input.purpose),
|
|
50
|
-
strategy,
|
|
51
|
-
target,
|
|
52
|
-
...(input.stage === undefined
|
|
53
|
-
? {}
|
|
54
|
-
: { stage: validateExecutionStageContext(input.stage, normalizedTaskId, groupId, strategy) }),
|
|
55
|
-
lanes,
|
|
56
|
-
createdAt: timestamp,
|
|
57
|
-
updatedAt: timestamp
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
export function addExecutionLane(group, input, now) {
|
|
61
|
-
validateExecutionGroup(group);
|
|
62
|
-
if (group.resolution !== undefined) {
|
|
63
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
64
|
-
}
|
|
65
|
-
if (!isDeepStrictEqual(group.target, validateExecutionTarget(group.target, group.taskId))) {
|
|
66
|
-
throw new Error(`ExecutionGroup target is not frozen: ${group.id}.`);
|
|
67
|
-
}
|
|
68
|
-
const nextCount = group.lanes.length + 1;
|
|
69
|
-
if (group.strategy.mode === "fixed" && nextCount > group.strategy.count) {
|
|
70
|
-
throw new Error(`Fixed ExecutionGroup cannot add Lane ${nextCount}: ${group.id}.`);
|
|
71
|
-
}
|
|
72
|
-
if (group.strategy.mode === "adaptive" && nextCount > group.strategy.max) {
|
|
73
|
-
throw new Error(`Adaptive ExecutionGroup reached its maximum: ${group.id}.`);
|
|
74
|
-
}
|
|
75
|
-
const timestamp = now.toISOString();
|
|
76
|
-
const lane = createLane(`${group.id}-lane-${input.id ?? nextCount}`, group.id, input, input.ordinal ?? nextCount, timestamp);
|
|
77
|
-
if (group.lanes.some((existing) => existing.id === lane.id)) {
|
|
78
|
-
throw new Error(`ExecutionGroup Lane already exists: ${lane.id}.`);
|
|
79
|
-
}
|
|
80
|
-
return validateExecutionGroup({
|
|
81
|
-
...group,
|
|
82
|
-
lanes: [...group.lanes, lane],
|
|
83
|
-
updatedAt: timestamp
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
/** Alias used by scheduler-facing callers. */
|
|
87
|
-
export const appendExecutionLane = addExecutionLane;
|
|
88
|
-
export function assertExecutionTargetUnchanged(group, target) {
|
|
89
|
-
validateExecutionGroup(group);
|
|
90
|
-
const next = validateExecutionTarget(target, group.taskId);
|
|
91
|
-
if (!isDeepStrictEqual(group.target, next)) {
|
|
92
|
-
throw new Error(`ExecutionGroup target changed; create a new Group: ${group.id}.`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Enforce the only durable Group/Lane evolution accepted by both domain
|
|
97
|
-
* helpers and storage. Identity, target, prior results, and a final Leader
|
|
98
|
-
* resolution never move backwards. A terminal Lane may only reopen as an
|
|
99
|
-
* explicit retry with a fresh Turn identity, or reset to pending for a
|
|
100
|
-
* Task-final Review execution retry.
|
|
101
|
-
*/
|
|
102
|
-
export function assertExecutionGroupTransition(existing, candidate) {
|
|
103
|
-
validateExecutionGroup(existing);
|
|
104
|
-
validateExecutionGroup(candidate);
|
|
105
|
-
if (isDeepStrictEqual(existing, candidate))
|
|
106
|
-
return;
|
|
107
|
-
if (existing.id !== candidate.id
|
|
108
|
-
|| existing.taskId !== candidate.taskId
|
|
109
|
-
|| existing.purpose !== candidate.purpose
|
|
110
|
-
|| !isDeepStrictEqual(existing.strategy, candidate.strategy)
|
|
111
|
-
|| !isDeepStrictEqual(existing.target, candidate.target)
|
|
112
|
-
|| !isDeepStrictEqual(existing.stage, candidate.stage)
|
|
113
|
-
|| existing.createdAt !== candidate.createdAt) {
|
|
114
|
-
throw new Error(`ExecutionGroup identity or target changed: ${existing.id}.`);
|
|
115
|
-
}
|
|
116
|
-
if (Date.parse(candidate.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
117
|
-
throw new Error(`ExecutionGroup time moved backwards: ${existing.id}.`);
|
|
118
|
-
}
|
|
119
|
-
if (existing.resolution !== undefined) {
|
|
120
|
-
throw new Error(`Resolved ExecutionGroup is immutable: ${existing.id}.`);
|
|
121
|
-
}
|
|
122
|
-
if (candidate.lanes.length < existing.lanes.length) {
|
|
123
|
-
throw new Error(`ExecutionGroup cannot remove Lanes: ${existing.id}.`);
|
|
124
|
-
}
|
|
125
|
-
for (const [index, lane] of existing.lanes.entries()) {
|
|
126
|
-
const next = candidate.lanes[index];
|
|
127
|
-
if (next === undefined) {
|
|
128
|
-
throw new Error(`ExecutionGroup cannot remove Lane: ${lane.id}.`);
|
|
129
|
-
}
|
|
130
|
-
assertExecutionLaneTransition(lane, next, existing.id, existing.purpose);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
export function isExecutionGroupTransition(existing, candidate) {
|
|
134
|
-
try {
|
|
135
|
-
assertExecutionGroupTransition(existing, candidate);
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
export function updateExecutionLane(group, laneId, patch, now) {
|
|
143
|
-
validateExecutionGroup(group);
|
|
144
|
-
if (group.resolution !== undefined) {
|
|
145
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
146
|
-
}
|
|
147
|
-
const id = requireIdentity(laneId, "ExecutionLane id");
|
|
148
|
-
const existing = group.lanes.find((lane) => lane.id === id);
|
|
149
|
-
if (existing === undefined)
|
|
150
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
151
|
-
if (isTerminalLane(existing.status) && patch.status !== existing.status) {
|
|
152
|
-
throw new Error(`Terminal ExecutionLane cannot be updated; create a retry Lane: ${group.id}/${id}.`);
|
|
153
|
-
}
|
|
154
|
-
const status = patch.status ?? existing.status;
|
|
155
|
-
const timestamp = now.toISOString();
|
|
156
|
-
const terminal = status === "completed" || status === "failed";
|
|
157
|
-
const next = {
|
|
158
|
-
...existing,
|
|
159
|
-
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
160
|
-
...(patch.turnId === undefined ? {} : { turnId: requireIdentity(patch.turnId, "Turn id") }),
|
|
161
|
-
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
162
|
-
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
163
|
-
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
164
|
-
...(patch.directive === undefined ? {} : { directive: requireText(patch.directive, "ExecutionLane directive") }),
|
|
165
|
-
status,
|
|
166
|
-
updatedAt: timestamp,
|
|
167
|
-
...(terminal && existing.endedAt === undefined ? { endedAt: timestamp } : {})
|
|
168
|
-
};
|
|
169
|
-
return validateExecutionGroup({
|
|
170
|
-
...group,
|
|
171
|
-
lanes: group.lanes.map((lane) => lane.id === id ? next : lane),
|
|
172
|
-
updatedAt: timestamp
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Reopens only a failed Lane for an explicit WorkItem retry. Completed
|
|
177
|
-
* results are immutable reusable outputs; retrying them would erase
|
|
178
|
-
* the Group's durable evidence instead of recovering unfinished work.
|
|
179
|
-
*/
|
|
180
|
-
export function restartExecutionLane(group, laneId, patch, now) {
|
|
181
|
-
validateExecutionGroup(group);
|
|
182
|
-
if (group.resolution !== undefined) {
|
|
183
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
184
|
-
}
|
|
185
|
-
const id = requireIdentity(laneId, "ExecutionLane id");
|
|
186
|
-
const existing = group.lanes.find((lane) => lane.id === id);
|
|
187
|
-
if (existing === undefined)
|
|
188
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
189
|
-
if (existing.status !== "failed") {
|
|
190
|
-
throw new Error(`Only failed ExecutionLanes can start a new Turn: ${group.id}/${id}.`);
|
|
191
|
-
}
|
|
192
|
-
const timestamp = now.toISOString();
|
|
193
|
-
const { result: _result, endedAt: _endedAt, ...base } = existing;
|
|
194
|
-
const next = {
|
|
195
|
-
...base,
|
|
196
|
-
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
197
|
-
...(patch.turnId === undefined ? {} : { turnId: requireIdentity(patch.turnId, "Turn id") }),
|
|
198
|
-
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
199
|
-
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
200
|
-
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
201
|
-
...(patch.directive === undefined ? {} : { directive: requireText(patch.directive, "ExecutionLane directive") }),
|
|
202
|
-
status: "running",
|
|
203
|
-
updatedAt: timestamp
|
|
204
|
-
};
|
|
205
|
-
return validateExecutionGroup({
|
|
206
|
-
...group,
|
|
207
|
-
lanes: group.lanes.map((lane) => lane.id === id ? next : lane),
|
|
208
|
-
updatedAt: timestamp
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Reopen a failed execution Lane without starting its next Turn when Resource
|
|
213
|
-
* Broker capacity is unavailable. The failed attempt remains in Turn
|
|
214
|
-
* history; the Lane freezes the next launch input until ordinary dispatch can
|
|
215
|
-
* admit it.
|
|
216
|
-
*/
|
|
217
|
-
export function queueExecutionLaneRetry(group, laneId, patch, now) {
|
|
218
|
-
validateExecutionGroup(group);
|
|
219
|
-
if (group.purpose !== "execution") {
|
|
220
|
-
throw new Error(`Only WorkItem ExecutionLanes can queue a retry: ${group.id}/${laneId}.`);
|
|
221
|
-
}
|
|
222
|
-
if (group.resolution !== undefined) {
|
|
223
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
224
|
-
}
|
|
225
|
-
const existing = group.lanes.find((lane) => lane.id === laneId);
|
|
226
|
-
if (existing === undefined)
|
|
227
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
228
|
-
if (existing.status !== "failed") {
|
|
229
|
-
throw new Error(`Only a failed ExecutionLane can queue a retry: ${group.id}/${laneId}.`);
|
|
230
|
-
}
|
|
231
|
-
const timestamp = now.toISOString();
|
|
232
|
-
const { turnId: _turnId, sessionId: _sessionId, result: _result, endedAt: _endedAt, ...base } = existing;
|
|
233
|
-
const next = {
|
|
234
|
-
...base,
|
|
235
|
-
effective: validateEffectiveLaunchSnapshot(patch.effective),
|
|
236
|
-
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
237
|
-
directive: requireText(patch.directive, "ExecutionLane directive"),
|
|
238
|
-
status: "pending",
|
|
239
|
-
updatedAt: timestamp
|
|
240
|
-
};
|
|
241
|
-
return validateExecutionGroup({
|
|
242
|
-
...group,
|
|
243
|
-
lanes: group.lanes.map((lane) => lane.id === laneId ? next : lane),
|
|
244
|
-
updatedAt: timestamp
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Resets a terminal Reviewer Lane to pending without replacing its ExecutionGroup.
|
|
249
|
-
* The old Turn remains the attempt trail; clearing the Lane's Turn/Session and
|
|
250
|
-
* result lets the same semantic ReviewRound be dispatched again.
|
|
251
|
-
*/
|
|
252
|
-
export function resetReviewExecutionLane(group, laneId, now) {
|
|
253
|
-
validateExecutionGroup(group);
|
|
254
|
-
if (group.purpose !== "review") {
|
|
255
|
-
throw new Error(`Only Review ExecutionLanes can reset to pending: ${group.id}/${laneId}.`);
|
|
256
|
-
}
|
|
257
|
-
if (group.resolution !== undefined) {
|
|
258
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
259
|
-
}
|
|
260
|
-
const existing = group.lanes.find((lane) => lane.id === laneId);
|
|
261
|
-
if (existing === undefined)
|
|
262
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
263
|
-
if (!isTerminalLane(existing.status))
|
|
264
|
-
return existing;
|
|
265
|
-
const timestamp = now.toISOString();
|
|
266
|
-
const { effective: _effective, turnId: _turnId, sessionId: _sessionId, result: _result, endedAt: _endedAt, ...base } = existing;
|
|
267
|
-
return validateExecutionLane({
|
|
268
|
-
...base,
|
|
269
|
-
status: "pending",
|
|
270
|
-
updatedAt: timestamp
|
|
271
|
-
}, group);
|
|
272
|
-
}
|
|
273
|
-
export function recordExecutionLaneResult(group, laneId, result, status, now) {
|
|
274
|
-
const checked = validateLaneResult(result);
|
|
275
|
-
validateExecutionGroup(group);
|
|
276
|
-
const existing = group.lanes.find((lane) => lane.id === laneId);
|
|
277
|
-
if (existing === undefined)
|
|
278
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
279
|
-
if (isTerminalLane(existing.status)) {
|
|
280
|
-
throw new Error(`ExecutionLane is already terminal: ${group.id}/${laneId}.`);
|
|
281
|
-
}
|
|
282
|
-
const timestamp = now.toISOString();
|
|
283
|
-
return validateExecutionGroup({
|
|
284
|
-
...group,
|
|
285
|
-
lanes: group.lanes.map((lane) => lane.id === laneId
|
|
286
|
-
? { ...lane, status, result: checked, updatedAt: timestamp, endedAt: timestamp }
|
|
287
|
-
: lane),
|
|
288
|
-
updatedAt: timestamp
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Stop only work that has never started. Running Lanes retain their exact Turn
|
|
293
|
-
* and Session; the Resource Broker never kills a straggler merely to save
|
|
294
|
-
* budget. Skipped Lanes are terminal but never usable Candidate inputs.
|
|
295
|
-
*/
|
|
296
|
-
export function skipPendingExecutionLanes(group, laneIds, summary, now) {
|
|
297
|
-
validateExecutionGroup(group);
|
|
298
|
-
if (group.purpose !== "execution") {
|
|
299
|
-
throw new Error("Only WorkItem ExecutionLanes can be skipped by resource policy.");
|
|
300
|
-
}
|
|
301
|
-
if (group.resolution !== undefined) {
|
|
302
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
303
|
-
}
|
|
304
|
-
const selected = new Set(laneIds.map((id) => requireIdentity(id, "ExecutionLane id")));
|
|
305
|
-
if (selected.size === 0)
|
|
306
|
-
return group;
|
|
307
|
-
const reason = requireText(summary, "ExecutionLane skip summary");
|
|
308
|
-
for (const laneId of selected) {
|
|
309
|
-
const lane = group.lanes.find(({ id }) => id === laneId);
|
|
310
|
-
if (lane === undefined)
|
|
311
|
-
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
312
|
-
if (lane.status !== "pending" || lane.turnId !== undefined) {
|
|
313
|
-
throw new Error(`Only an unstarted pending ExecutionLane can be skipped: ${group.id}/${laneId}.`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const timestamp = now.toISOString();
|
|
317
|
-
return validateExecutionGroup({
|
|
318
|
-
...group,
|
|
319
|
-
lanes: group.lanes.map((lane) => selected.has(lane.id)
|
|
320
|
-
? {
|
|
321
|
-
...lane,
|
|
322
|
-
status: "skipped",
|
|
323
|
-
result: { summary: reason },
|
|
324
|
-
updatedAt: timestamp,
|
|
325
|
-
endedAt: timestamp
|
|
326
|
-
}
|
|
327
|
-
: lane),
|
|
328
|
-
updatedAt: timestamp
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
export function resolveExecutionGroup(group, input, now) {
|
|
332
|
-
validateExecutionGroup(group);
|
|
333
|
-
if (group.resolution !== undefined) {
|
|
334
|
-
throw new Error(`ExecutionGroup is already resolved: ${group.id}.`);
|
|
335
|
-
}
|
|
336
|
-
if (group.lanes.some((lane) => !isTerminalLane(lane.status))) {
|
|
337
|
-
throw new Error(`ExecutionGroup has active Lanes: ${group.id}.`);
|
|
338
|
-
}
|
|
339
|
-
const summary = requireText(input.summary, "Execution resolution summary");
|
|
340
|
-
const defaultSelectedLaneIds = input.decision === "accept"
|
|
341
|
-
? group.lanes
|
|
342
|
-
.filter((lane) => lane.status === "completed")
|
|
343
|
-
.map(({ id }) => id)
|
|
344
|
-
: group.lanes.map(({ id }) => id);
|
|
345
|
-
const selected = [...new Set((input.selectedLaneIds ?? defaultSelectedLaneIds)
|
|
346
|
-
.map((id) => requireIdentity(id, "Selected ExecutionLane id")))];
|
|
347
|
-
if (input.decision === "accept" && selected.length === 0) {
|
|
348
|
-
throw new Error(`ExecutionGroup has no usable terminal Lane outputs: ${group.id}.`);
|
|
349
|
-
}
|
|
350
|
-
if (input.decision === "accept" && selected.some((id) => {
|
|
351
|
-
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
352
|
-
return lane?.status !== "completed";
|
|
353
|
-
})) {
|
|
354
|
-
throw new Error(`ExecutionGroup accept selects a Lane without usable terminal output: ${group.id}.`);
|
|
355
|
-
}
|
|
356
|
-
if (input.decision === "accept" && group.stage?.resources !== undefined) {
|
|
357
|
-
const usable = group.lanes.filter(({ status }) => status === "completed").length;
|
|
358
|
-
if (usable < group.stage.resources.quorum) {
|
|
359
|
-
throw new Error(`ExecutionGroup quorum is not met: ${usable}/${group.stage.resources.quorum} usable Lanes.`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (selected.some((id) => !group.lanes.some((lane) => lane.id === id))) {
|
|
363
|
-
throw new Error(`Execution resolution selects an unknown Lane: ${group.id}.`);
|
|
364
|
-
}
|
|
365
|
-
const openHigh = openHighPriorityFindingIds(group);
|
|
366
|
-
if (input.decision === "accept" && openHigh.length > 0) {
|
|
367
|
-
throw new Error(`ExecutionGroup cannot be accepted with open high-priority findings: ${openHigh.join(", ")}.`);
|
|
368
|
-
}
|
|
369
|
-
const timestamp = now.toISOString();
|
|
370
|
-
return validateExecutionGroup({
|
|
371
|
-
...group,
|
|
372
|
-
resolution: {
|
|
373
|
-
decision: input.decision,
|
|
374
|
-
summary,
|
|
375
|
-
selectedLaneIds: selected,
|
|
376
|
-
unresolvedFindingIds: openHigh,
|
|
377
|
-
decidedAt: timestamp
|
|
378
|
-
},
|
|
379
|
-
updatedAt: timestamp
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
export function summarizeExecutionGroup(group) {
|
|
383
|
-
validateExecutionGroup(group);
|
|
384
|
-
const activeLaneCount = group.lanes.filter(({ status }) => (status === "pending" || status === "running")).length;
|
|
385
|
-
const terminalLaneCount = group.lanes.length - activeLaneCount;
|
|
386
|
-
return {
|
|
387
|
-
groupId: group.id,
|
|
388
|
-
purpose: group.purpose,
|
|
389
|
-
strategy: group.strategy,
|
|
390
|
-
...(group.stage === undefined ? {} : { stage: group.stage }),
|
|
391
|
-
laneCount: group.lanes.length,
|
|
392
|
-
activeLaneCount,
|
|
393
|
-
terminalLaneCount,
|
|
394
|
-
failedLaneCount: group.lanes.filter(({ status }) => status === "failed").length,
|
|
395
|
-
skippedLaneCount: group.lanes.filter(({ status }) => status === "skipped").length,
|
|
396
|
-
openHighPriorityFindingIds: openHighPriorityFindingIds(group),
|
|
397
|
-
laneSummaries: group.lanes.map((lane) => ({
|
|
398
|
-
laneId: lane.id,
|
|
399
|
-
roleName: lane.roleName,
|
|
400
|
-
ordinal: lane.ordinal,
|
|
401
|
-
...(lane.turnId === undefined ? {} : { turnId: lane.turnId }),
|
|
402
|
-
...(lane.sessionId === undefined ? {} : { sessionId: lane.sessionId }),
|
|
403
|
-
...(lane.effective === undefined ? {} : { effective: lane.effective }),
|
|
404
|
-
...(lane.directive === undefined ? {} : { directive: lane.directive }),
|
|
405
|
-
status: lane.status,
|
|
406
|
-
...(lane.result === undefined ? {} : {
|
|
407
|
-
summary: lane.result.summary,
|
|
408
|
-
...(lane.result.report === undefined ? {} : { report: lane.result.report }),
|
|
409
|
-
...(lane.result.checks === undefined ? {} : { checks: lane.result.checks }),
|
|
410
|
-
...(lane.result.findings === undefined ? {} : { findings: lane.result.findings }),
|
|
411
|
-
...(lane.result.evidence === undefined ? {} : { evidence: lane.result.evidence }),
|
|
412
|
-
...(lane.result.evidenceCommit === undefined ? {} : { evidenceCommit: lane.result.evidenceCommit }),
|
|
413
|
-
...(lane.result.gitSnapshot === undefined ? {} : { gitSnapshot: lane.result.gitSnapshot })
|
|
414
|
-
}),
|
|
415
|
-
...(group.resolution === undefined
|
|
416
|
-
|| !group.resolution.selectedLaneIds.includes(lane.id)
|
|
417
|
-
? {}
|
|
418
|
-
: { decision: group.resolution.decision })
|
|
419
|
-
})),
|
|
420
|
-
...(group.resolution === undefined ? {} : { resolution: group.resolution })
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
export function validateExecutionGroup(group) {
|
|
424
|
-
if (group.schemaVersion !== EXECUTION_GROUP_SCHEMA_VERSION) {
|
|
425
|
-
throw new Error("ExecutionGroup must use schemaVersion 1.");
|
|
426
|
-
}
|
|
427
|
-
const taskId = requireIdentity(group.taskId, "Task id");
|
|
428
|
-
requireIdentity(group.id, "ExecutionGroup id");
|
|
429
|
-
validatePurpose(group.purpose);
|
|
430
|
-
normalizeStrategy(group.strategy);
|
|
431
|
-
validateExecutionTarget(group.target, taskId);
|
|
432
|
-
if (group.stage !== undefined) {
|
|
433
|
-
if (group.purpose !== "execution" || group.target.kind !== "work-item") {
|
|
434
|
-
throw new Error("Only a WorkItem ExecutionGroup can carry exploration stage context.");
|
|
435
|
-
}
|
|
436
|
-
validateExecutionStageContext(group.stage, taskId, group.id, group.strategy);
|
|
437
|
-
}
|
|
438
|
-
if (!Array.isArray(group.lanes) || group.lanes.length === 0) {
|
|
439
|
-
throw new Error("ExecutionGroup requires at least one Lane.");
|
|
440
|
-
}
|
|
441
|
-
const ids = new Set();
|
|
442
|
-
const ordinals = new Set();
|
|
443
|
-
const roots = new Set();
|
|
444
|
-
for (const lane of group.lanes) {
|
|
445
|
-
validateExecutionLane(lane, group);
|
|
446
|
-
if (ids.has(lane.id))
|
|
447
|
-
throw new Error(`ExecutionLane is duplicated: ${lane.id}.`);
|
|
448
|
-
if (ordinals.has(lane.ordinal))
|
|
449
|
-
throw new Error(`ExecutionLane ordinal is duplicated: ${lane.ordinal}.`);
|
|
450
|
-
ids.add(lane.id);
|
|
451
|
-
ordinals.add(lane.ordinal);
|
|
452
|
-
if (lane.workspace !== undefined) {
|
|
453
|
-
if (roots.has(lane.workspace.root) && lane.workspace.writableProjectIds.length > 0) {
|
|
454
|
-
throw new Error(`Writable ExecutionLane workspace is shared: ${lane.workspace.root}.`);
|
|
455
|
-
}
|
|
456
|
-
if (lane.workspace.writableProjectIds.length > 0)
|
|
457
|
-
roots.add(lane.workspace.root);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const expectedMax = group.strategy.mode === "fixed" ? group.strategy.count : group.strategy.max;
|
|
461
|
-
if (group.lanes.length > expectedMax)
|
|
462
|
-
throw new Error("ExecutionGroup has too many Lanes.");
|
|
463
|
-
if (group.resolution !== undefined) {
|
|
464
|
-
if (group.lanes.some(({ status }) => !isTerminalLane(status))) {
|
|
465
|
-
throw new Error(`Resolved ExecutionGroup has active Lanes: ${group.id}.`);
|
|
466
|
-
}
|
|
467
|
-
validateResolution(group.resolution, group);
|
|
468
|
-
}
|
|
469
|
-
requireTimestamp(group.createdAt, "ExecutionGroup createdAt");
|
|
470
|
-
requireTimestamp(group.updatedAt, "ExecutionGroup updatedAt");
|
|
471
|
-
return group;
|
|
472
|
-
}
|
|
473
|
-
export function validateExecutionStageContext(stage, taskId, executionGroupId, strategy) {
|
|
474
|
-
if (stage === null || typeof stage !== "object" || stage.schemaVersion !== 1) {
|
|
475
|
-
throw new Error("Execution stage context must use schemaVersion 1.");
|
|
476
|
-
}
|
|
477
|
-
if (!WORK_ITEM_EXPLORATION_MODES.includes(stage.mode)) {
|
|
478
|
-
throw new Error("Execution stage mode must be an exploration mode.");
|
|
479
|
-
}
|
|
480
|
-
if (!WORK_ITEM_EXPLORATION_STAGES.includes(stage.stage)) {
|
|
481
|
-
throw new Error("Execution stage is invalid.");
|
|
482
|
-
}
|
|
483
|
-
requirePositiveInteger(stage.round, "Execution stage round");
|
|
484
|
-
requirePositiveInteger(stage.stageAttempt, "Execution stage attempt");
|
|
485
|
-
requirePositiveInteger(stage.maxRounds, "Execution stage max rounds");
|
|
486
|
-
if (stage.round > stage.maxRounds) {
|
|
487
|
-
throw new Error("Execution stage round exceeds maxRounds.");
|
|
488
|
-
}
|
|
489
|
-
if (stage.budget === null || typeof stage.budget !== "object") {
|
|
490
|
-
throw new Error("Execution stage budget is required.");
|
|
491
|
-
}
|
|
492
|
-
requirePositiveInteger(stage.budget.maxLanes, "Execution stage max Lanes");
|
|
493
|
-
requirePositiveInteger(stage.budget.maxAttempts, "Execution stage max attempts");
|
|
494
|
-
if (stage.budget.maxTokens !== undefined) {
|
|
495
|
-
requirePositiveInteger(stage.budget.maxTokens, "Execution stage max tokens");
|
|
496
|
-
}
|
|
497
|
-
if (stage.budget.maxToolCalls !== undefined) {
|
|
498
|
-
requirePositiveInteger(stage.budget.maxToolCalls, "Execution stage max tool calls");
|
|
499
|
-
}
|
|
500
|
-
if (stage.budget.maxWallClockSeconds !== undefined) {
|
|
501
|
-
requirePositiveInteger(stage.budget.maxWallClockSeconds, "Execution stage max wall-clock seconds");
|
|
502
|
-
}
|
|
503
|
-
if (stage.stageAttempt > stage.budget.maxAttempts) {
|
|
504
|
-
throw new Error("Execution stage attempt exceeds its budget.");
|
|
505
|
-
}
|
|
506
|
-
const capacity = strategy.mode === "fixed" ? strategy.count : strategy.max;
|
|
507
|
-
if (stage.budget.maxLanes !== capacity) {
|
|
508
|
-
throw new Error("Execution stage Lane budget must match its Group strategy capacity.");
|
|
509
|
-
}
|
|
510
|
-
if (stage.resources !== undefined) {
|
|
511
|
-
if (stage.resources === null
|
|
512
|
-
|| typeof stage.resources !== "object"
|
|
513
|
-
|| stage.resources.schemaVersion !== 1) {
|
|
514
|
-
throw new Error("Execution stage resource policy must use schemaVersion 1.");
|
|
515
|
-
}
|
|
516
|
-
if (stage.budget.maxTokens === undefined
|
|
517
|
-
|| stage.budget.maxToolCalls === undefined
|
|
518
|
-
|| stage.budget.maxWallClockSeconds === undefined) {
|
|
519
|
-
throw new Error("A resource-scheduled Execution stage requires token, tool-call and wall-clock budgets.");
|
|
520
|
-
}
|
|
521
|
-
requirePositiveInteger(stage.resources.quorum, "Execution stage quorum");
|
|
522
|
-
if (stage.resources.quorum > stage.budget.maxLanes) {
|
|
523
|
-
throw new Error("Execution stage quorum exceeds its Lane budget.");
|
|
524
|
-
}
|
|
525
|
-
requireTimestamp(stage.resources.deadlineAt, "Execution stage deadline");
|
|
526
|
-
requirePositiveInteger(stage.resources.stragglerAfterSeconds, "Execution stage straggler threshold");
|
|
527
|
-
if (!Number.isSafeInteger(stage.resources.minimumMarginalValuePercent)
|
|
528
|
-
|| stage.resources.minimumMarginalValuePercent < 0
|
|
529
|
-
|| stage.resources.minimumMarginalValuePercent > 100) {
|
|
530
|
-
throw new Error("Execution stage minimum marginal value must be an integer from 0 to 100.");
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const snapshot = validateContextSnapshotRef(stage.contextSnapshotRef);
|
|
534
|
-
if (snapshot.taskId !== taskId
|
|
535
|
-
|| snapshot.scope !== "stage"
|
|
536
|
-
|| snapshot.scopeRef !== executionGroupId) {
|
|
537
|
-
throw new Error("Execution stage ContextSnapshot does not match its Group.");
|
|
538
|
-
}
|
|
539
|
-
if (!Array.isArray(stage.parentResults)) {
|
|
540
|
-
throw new Error("Execution stage parentResults are invalid.");
|
|
541
|
-
}
|
|
542
|
-
const parents = new Set();
|
|
543
|
-
for (const parent of stage.parentResults) {
|
|
544
|
-
const groupId = requireIdentity(parent.executionGroupId, "Parent ExecutionGroup id");
|
|
545
|
-
const laneId = requireIdentity(parent.executionLaneId, "Parent ExecutionLane id");
|
|
546
|
-
if (!/^[0-9a-f]{64}$/u.test(parent.resultDigest)) {
|
|
547
|
-
throw new Error("Parent ExecutionLane result digest must be SHA-256 hex.");
|
|
548
|
-
}
|
|
549
|
-
const key = `${groupId}\0${laneId}`;
|
|
550
|
-
if (parents.has(key)) {
|
|
551
|
-
throw new Error(`Execution stage parent result is duplicated: ${groupId}/${laneId}.`);
|
|
552
|
-
}
|
|
553
|
-
parents.add(key);
|
|
554
|
-
}
|
|
555
|
-
if (stage.convergence !== undefined
|
|
556
|
-
&& (stage.convergence === null
|
|
557
|
-
|| typeof stage.convergence !== "object"
|
|
558
|
-
|| stage.convergence.schemaVersion !== 1)) {
|
|
559
|
-
throw new Error("Candidate convergence policy must use schemaVersion 1.");
|
|
560
|
-
}
|
|
561
|
-
return stage;
|
|
562
|
-
}
|
|
563
|
-
export function validateExecutionLane(lane, group) {
|
|
564
|
-
if (lane.schemaVersion !== EXECUTION_LANE_SCHEMA_VERSION) {
|
|
565
|
-
throw new Error("ExecutionLane must use schemaVersion 1.");
|
|
566
|
-
}
|
|
567
|
-
requireIdentity(lane.id, "ExecutionLane id");
|
|
568
|
-
requireIdentity(lane.groupId, "ExecutionGroup id");
|
|
569
|
-
if (group !== undefined) {
|
|
570
|
-
if (lane.groupId !== group.id)
|
|
571
|
-
throw new Error(`ExecutionLane belongs to another Group: ${lane.id}.`);
|
|
572
|
-
if (group.purpose === "review" && lane.reviewRoundId === undefined) {
|
|
573
|
-
// A review lane may be pending before its ReviewRound is dispatched, but
|
|
574
|
-
// it must acquire that identity before it can turn.
|
|
575
|
-
if (lane.status === "running"
|
|
576
|
-
|| lane.status === "completed"
|
|
577
|
-
|| lane.status === "failed"
|
|
578
|
-
|| lane.status === "skipped") {
|
|
579
|
-
throw new Error(`Running Reviewer Lane requires a ReviewRound: ${lane.id}.`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
positiveInteger(lane.ordinal, "ExecutionLane ordinal");
|
|
584
|
-
requireIdentity(lane.roleName, "ExecutionLane Role");
|
|
585
|
-
if (lane.effective !== undefined)
|
|
586
|
-
validateEffectiveLaunchSnapshot(lane.effective);
|
|
587
|
-
if (lane.turnId !== undefined)
|
|
588
|
-
requireIdentity(lane.turnId, "Turn id");
|
|
589
|
-
if (lane.sessionId !== undefined)
|
|
590
|
-
requireIdentity(lane.sessionId, "Session id");
|
|
591
|
-
if (lane.reviewRoundId !== undefined)
|
|
592
|
-
requireIdentity(lane.reviewRoundId, "ReviewRound id");
|
|
593
|
-
if (lane.workspace !== undefined)
|
|
594
|
-
validateLaneWorkspace(lane.workspace);
|
|
595
|
-
if (lane.directive !== undefined)
|
|
596
|
-
requireText(lane.directive, "ExecutionLane directive");
|
|
597
|
-
if (!["pending", "running", "completed", "failed", "skipped"].includes(lane.status)) {
|
|
598
|
-
throw new Error(`ExecutionLane status is invalid: ${String(lane.status)}.`);
|
|
599
|
-
}
|
|
600
|
-
if (lane.status === "skipped") {
|
|
601
|
-
if (group !== undefined && group.purpose !== "execution") {
|
|
602
|
-
throw new Error(`Only WorkItem ExecutionLanes can be skipped: ${lane.id}.`);
|
|
603
|
-
}
|
|
604
|
-
if (lane.turnId !== undefined || lane.sessionId !== undefined) {
|
|
605
|
-
throw new Error(`Skipped ExecutionLane must never have started: ${lane.id}.`);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (lane.result !== undefined)
|
|
609
|
-
validateLaneResult(lane.result);
|
|
610
|
-
requireTimestamp(lane.createdAt, "ExecutionLane createdAt");
|
|
611
|
-
requireTimestamp(lane.updatedAt, "ExecutionLane updatedAt");
|
|
612
|
-
if (isTerminalLane(lane.status)) {
|
|
613
|
-
requireTimestamp(lane.endedAt ?? "", "ExecutionLane endedAt");
|
|
614
|
-
if (lane.result === undefined)
|
|
615
|
-
throw new Error(`Terminal ExecutionLane requires a result: ${lane.id}.`);
|
|
616
|
-
}
|
|
617
|
-
else if (lane.endedAt !== undefined || lane.result !== undefined) {
|
|
618
|
-
throw new Error(`Active ExecutionLane cannot have terminal output: ${lane.id}.`);
|
|
619
|
-
}
|
|
620
|
-
return lane;
|
|
621
|
-
}
|
|
622
|
-
function createLane(id, groupId, input, ordinal, timestamp) {
|
|
623
|
-
return validateExecutionLane({
|
|
624
|
-
schemaVersion: EXECUTION_LANE_SCHEMA_VERSION,
|
|
625
|
-
id: requireIdentity(id, "ExecutionLane id"),
|
|
626
|
-
groupId: requireIdentity(groupId, "ExecutionGroup id"),
|
|
627
|
-
ordinal: positiveInteger(input.ordinal ?? ordinal, "ExecutionLane ordinal"),
|
|
628
|
-
roleName: requireIdentity(input.roleName, "ExecutionLane Role"),
|
|
629
|
-
...(input.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(input.effective) }),
|
|
630
|
-
...(input.turnId === undefined ? {} : { turnId: requireIdentity(input.turnId, "Turn id") }),
|
|
631
|
-
...(input.sessionId === undefined ? {} : { sessionId: requireIdentity(input.sessionId, "Session id") }),
|
|
632
|
-
...(input.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(input.reviewRoundId, "ReviewRound id") }),
|
|
633
|
-
...(input.workspace === undefined ? {} : { workspace: validateLaneWorkspace(input.workspace) }),
|
|
634
|
-
...(input.directive === undefined ? {} : { directive: requireText(input.directive, "ExecutionLane directive") }),
|
|
635
|
-
status: "pending",
|
|
636
|
-
createdAt: timestamp,
|
|
637
|
-
updatedAt: timestamp
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
export function validateExecutionTarget(target, taskId) {
|
|
641
|
-
if (target === null || typeof target !== "object" || target.schemaVersion !== 1) {
|
|
642
|
-
throw new Error("ExecutionTarget must use schemaVersion 1.");
|
|
643
|
-
}
|
|
644
|
-
if (target.taskId !== taskId)
|
|
645
|
-
throw new Error("ExecutionTarget belongs to another Task.");
|
|
646
|
-
if (target.kind !== "work-item" && target.kind !== "task-final-review") {
|
|
647
|
-
throw new Error(`ExecutionTarget kind is invalid: ${String(target.kind)}.`);
|
|
648
|
-
}
|
|
649
|
-
positiveInteger(target.revision, "ExecutionTarget revision");
|
|
650
|
-
if (!Array.isArray(target.projects))
|
|
651
|
-
throw new Error("ExecutionTarget Projects are invalid.");
|
|
652
|
-
const projects = new Set();
|
|
653
|
-
for (const project of target.projects) {
|
|
654
|
-
const id = requireIdentity(project.projectId, "ExecutionTarget Project");
|
|
655
|
-
if (projects.has(id))
|
|
656
|
-
throw new Error(`ExecutionTarget Project is duplicated: ${id}.`);
|
|
657
|
-
projects.add(id);
|
|
658
|
-
requireCommit(project.commit, "ExecutionTarget commit");
|
|
659
|
-
}
|
|
660
|
-
if (target.kind === "work-item" && target.workItemId === undefined) {
|
|
661
|
-
throw new Error("WorkItem ExecutionTarget requires a Work Item id.");
|
|
662
|
-
}
|
|
663
|
-
if (target.workItemId !== undefined)
|
|
664
|
-
requireIdentity(target.workItemId, "Work Item id");
|
|
665
|
-
if (target.candidateId !== undefined)
|
|
666
|
-
requireIdentity(target.candidateId, "Candidate id");
|
|
667
|
-
if (target.contractDigest !== undefined)
|
|
668
|
-
requireText(target.contractDigest, "Execution contract digest");
|
|
669
|
-
requireText(target.fingerprint, "ExecutionTarget fingerprint");
|
|
670
|
-
return target;
|
|
671
|
-
}
|
|
672
|
-
function validateLaneWorkspace(workspace) {
|
|
673
|
-
requireText(workspace.root, "ExecutionLane workspace root");
|
|
674
|
-
if (!Array.isArray(workspace.writableProjectIds))
|
|
675
|
-
throw new Error("ExecutionLane workspace Projects are invalid.");
|
|
676
|
-
const ids = workspace.writableProjectIds.map((id) => requireIdentity(id, "ExecutionLane writable Project"));
|
|
677
|
-
if (new Set(ids).size !== ids.length)
|
|
678
|
-
throw new Error("ExecutionLane writable Projects are duplicated.");
|
|
679
|
-
return workspace;
|
|
680
|
-
}
|
|
681
|
-
function validateLaneResult(result) {
|
|
682
|
-
requireText(result.summary, "ExecutionLane summary");
|
|
683
|
-
if (result.report !== undefined)
|
|
684
|
-
requireText(result.report, "ExecutionLane report");
|
|
685
|
-
if (result.checks !== undefined) {
|
|
686
|
-
for (const check of result.checks) {
|
|
687
|
-
requireText(check.name, "Execution check name");
|
|
688
|
-
if (!["passed", "failed", "skipped"].includes(check.outcome))
|
|
689
|
-
throw new Error("Execution check outcome is invalid.");
|
|
690
|
-
if (check.details !== undefined)
|
|
691
|
-
requireText(check.details, "Execution check details");
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
if (result.findings !== undefined) {
|
|
695
|
-
const ids = new Set();
|
|
696
|
-
for (const finding of result.findings) {
|
|
697
|
-
const id = requireIdentity(finding.id, "Execution finding id");
|
|
698
|
-
if (ids.has(id))
|
|
699
|
-
throw new Error(`Execution finding is duplicated: ${id}.`);
|
|
700
|
-
ids.add(id);
|
|
701
|
-
if (!["low", "medium", "high", "critical"].includes(finding.severity))
|
|
702
|
-
throw new Error("Execution finding severity is invalid.");
|
|
703
|
-
requireText(finding.summary, "Execution finding summary");
|
|
704
|
-
if (finding.status !== "open" && finding.status !== "resolved")
|
|
705
|
-
throw new Error("Execution finding status is invalid.");
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (result.evidence !== undefined)
|
|
709
|
-
result.evidence.forEach((value) => requireText(value, "Execution evidence"));
|
|
710
|
-
if (result.evidenceCommit !== undefined) {
|
|
711
|
-
requireCommit(result.evidenceCommit, "Execution evidence commit");
|
|
712
|
-
}
|
|
713
|
-
if (result.gitSnapshot !== undefined)
|
|
714
|
-
validateExecutionLaneGitSnapshot(result.gitSnapshot);
|
|
715
|
-
return result;
|
|
716
|
-
}
|
|
717
|
-
function validateExecutionLaneGitSnapshot(snapshot) {
|
|
718
|
-
if (snapshot === null || typeof snapshot !== "object" || snapshot.schemaVersion !== 1) {
|
|
719
|
-
throw new Error("Execution Lane Git snapshot must use schemaVersion 1.");
|
|
720
|
-
}
|
|
721
|
-
if (!Array.isArray(snapshot.projects) || snapshot.projects.length === 0) {
|
|
722
|
-
throw new Error("Execution Lane Git snapshot requires Projects.");
|
|
723
|
-
}
|
|
724
|
-
const ids = new Set();
|
|
725
|
-
for (const project of snapshot.projects) {
|
|
726
|
-
const projectId = requireIdentity(project.projectId, "Execution Lane snapshot Project");
|
|
727
|
-
if (ids.has(projectId))
|
|
728
|
-
throw new Error(`Execution Lane snapshot Project is duplicated: ${projectId}.`);
|
|
729
|
-
ids.add(projectId);
|
|
730
|
-
requireCommit(project.headCommit, "Execution Lane snapshot head commit");
|
|
731
|
-
requireText(project.branch, "Execution Lane snapshot branch");
|
|
732
|
-
}
|
|
733
|
-
return snapshot;
|
|
734
|
-
}
|
|
735
|
-
function validateResolution(resolution, group) {
|
|
736
|
-
if (!["accept", "reject", "retry", "blocked"].includes(resolution.decision))
|
|
737
|
-
throw new Error("Execution resolution decision is invalid.");
|
|
738
|
-
requireText(resolution.summary, "Execution resolution summary");
|
|
739
|
-
const laneIds = new Set(group.lanes.map(({ id }) => id));
|
|
740
|
-
if (new Set(resolution.selectedLaneIds).size !== resolution.selectedLaneIds.length
|
|
741
|
-
|| resolution.selectedLaneIds.some((id) => !laneIds.has(id))) {
|
|
742
|
-
throw new Error("Execution resolution Lane selection is invalid.");
|
|
743
|
-
}
|
|
744
|
-
if (resolution.decision === "accept"
|
|
745
|
-
&& (resolution.selectedLaneIds.length === 0
|
|
746
|
-
|| resolution.selectedLaneIds.some((id) => {
|
|
747
|
-
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
748
|
-
return lane?.status !== "completed";
|
|
749
|
-
}))) {
|
|
750
|
-
throw new Error("Execution accept resolution must select usable Lane output.");
|
|
751
|
-
}
|
|
752
|
-
requireTimestamp(resolution.decidedAt, "Execution resolution time");
|
|
753
|
-
const findings = openHighPriorityFindingIds(group);
|
|
754
|
-
if (!isDeepStrictEqual([...resolution.unresolvedFindingIds].sort(), [...findings].sort())) {
|
|
755
|
-
throw new Error("Execution resolution findings do not match the unresolved high-priority findings.");
|
|
756
|
-
}
|
|
757
|
-
if (resolution.decision === "accept" && findings.length > 0) {
|
|
758
|
-
throw new Error("Execution accept resolution cannot retain high-priority findings.");
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpose) {
|
|
762
|
-
if (existing.id !== candidate.id
|
|
763
|
-
|| existing.groupId !== candidate.groupId
|
|
764
|
-
|| existing.groupId !== groupId
|
|
765
|
-
|| existing.ordinal !== candidate.ordinal
|
|
766
|
-
|| existing.roleName !== candidate.roleName
|
|
767
|
-
|| existing.createdAt !== candidate.createdAt
|
|
768
|
-
|| existing.reviewRoundId !== candidate.reviewRoundId) {
|
|
769
|
-
throw new Error(`ExecutionLane identity changed: ${existing.id}.`);
|
|
770
|
-
}
|
|
771
|
-
if (Date.parse(candidate.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
772
|
-
throw new Error(`ExecutionLane time moved backwards: ${existing.id}.`);
|
|
773
|
-
}
|
|
774
|
-
if (isTerminalLane(existing.status)) {
|
|
775
|
-
if (isDeepStrictEqual(existing, candidate))
|
|
776
|
-
return;
|
|
777
|
-
if (existing.status === "skipped") {
|
|
778
|
-
throw new Error(`Skipped ExecutionLane is immutable: ${existing.id}.`);
|
|
779
|
-
}
|
|
780
|
-
if (groupPurpose === "review"
|
|
781
|
-
&& candidate.status === "pending"
|
|
782
|
-
&& candidate.effective === undefined
|
|
783
|
-
&& candidate.turnId === undefined
|
|
784
|
-
&& candidate.sessionId === undefined
|
|
785
|
-
&& candidate.result === undefined
|
|
786
|
-
&& candidate.endedAt === undefined
|
|
787
|
-
&& isDeepStrictEqual(existing.workspace, candidate.workspace)) {
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (groupPurpose === "execution"
|
|
791
|
-
&& existing.status === "failed"
|
|
792
|
-
&& candidate.status === "pending"
|
|
793
|
-
&& candidate.effective !== undefined
|
|
794
|
-
&& candidate.turnId === undefined
|
|
795
|
-
&& candidate.sessionId === undefined
|
|
796
|
-
&& candidate.result === undefined
|
|
797
|
-
&& candidate.endedAt === undefined
|
|
798
|
-
&& candidate.directive !== undefined) {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
if (candidate.status !== "running"
|
|
802
|
-
|| candidate.turnId === undefined
|
|
803
|
-
|| candidate.turnId === existing.turnId) {
|
|
804
|
-
throw new Error(`Terminal ExecutionLane is immutable without a fresh retry Turn: ${existing.id}.`);
|
|
805
|
-
}
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
if (existing.status === "running" && candidate.status === "pending") {
|
|
809
|
-
throw new Error(`Running ExecutionLane cannot return to pending: ${existing.id}.`);
|
|
810
|
-
}
|
|
811
|
-
for (const key of ["effective", "turnId", "sessionId", "workspace", "directive"]) {
|
|
812
|
-
if (existing[key] !== undefined
|
|
813
|
-
&& !isDeepStrictEqual(existing[key], candidate[key])) {
|
|
814
|
-
throw new Error(`ExecutionLane ${key} changed without retry: ${existing.id}.`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
function openHighPriorityFindingIds(group) {
|
|
819
|
-
return group.lanes.flatMap((lane) => (lane.result?.findings ?? [])
|
|
820
|
-
.filter((finding) => finding.status === "open" && (finding.severity === "high" || finding.severity === "critical"))
|
|
821
|
-
.map(({ id }) => id));
|
|
822
|
-
}
|
|
823
|
-
function normalizeStrategy(strategy) {
|
|
824
|
-
if (strategy.mode === "fixed") {
|
|
825
|
-
positiveInteger(strategy.count, "Fixed ExecutionGroup Lane count");
|
|
826
|
-
return { mode: "fixed", count: strategy.count };
|
|
827
|
-
}
|
|
828
|
-
if (strategy.mode === "adaptive") {
|
|
829
|
-
positiveInteger(strategy.max, "Adaptive ExecutionGroup Lane maximum");
|
|
830
|
-
return { mode: "adaptive", max: strategy.max };
|
|
831
|
-
}
|
|
832
|
-
throw new Error(`Execution strategy is invalid: ${String(strategy.mode)}.`);
|
|
833
|
-
}
|
|
834
|
-
function validatePurpose(purpose) {
|
|
835
|
-
if (purpose !== "execution" && purpose !== "review")
|
|
836
|
-
throw new Error(`Execution purpose is invalid: ${String(purpose)}.`);
|
|
837
|
-
return purpose;
|
|
838
|
-
}
|
|
839
|
-
function isTerminalLane(status) {
|
|
840
|
-
return status === "completed"
|
|
841
|
-
|| status === "failed"
|
|
842
|
-
|| status === "skipped";
|
|
843
|
-
}
|
|
844
|
-
function positiveInteger(value, label) {
|
|
845
|
-
if (!Number.isSafeInteger(value) || value < 1)
|
|
846
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
847
|
-
return value;
|
|
848
|
-
}
|
|
849
|
-
function requireCommit(value, label) {
|
|
850
|
-
const commit = requireText(value, label).toLowerCase();
|
|
851
|
-
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit))
|
|
852
|
-
throw new Error(`${label} is invalid.`);
|
|
853
|
-
return commit;
|
|
854
|
-
}
|