@zq-silk/yui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/cli/commandCatalog.js +28 -7
- package/dist/cli.js +50 -47
- package/dist/commands/projectCommands.js +69 -6
- package/dist/commands/taskCommands.js +639 -89
- package/dist/commands/taskContextCommand.js +78 -27
- package/dist/commands/taskNextActionCommand.js +13 -2
- package/dist/commands/taskOverviewCommand.js +21 -5
- package/dist/commands/taskUpstreamCommands.js +136 -0
- package/dist/context/runContextPack.js +184 -17
- package/dist/controller/agentRuntimeObserver.js +31 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
- package/dist/execution/candidateConvergence.js +623 -0
- package/dist/execution/executionGroup.js +255 -13
- package/dist/execution/executionHealth.js +324 -0
- package/dist/execution/resourceBroker.js +425 -0
- package/dist/executor/fileRoleLaunchPlanner.js +6 -9
- package/dist/executor/workspacePreflightClassification.js +117 -0
- package/dist/lifecycle/exactRunTerminalization.js +13 -2
- package/dist/lifecycle/taskRoleSessionReset.js +4 -2
- package/dist/repository/taskBaseFreshness.js +26 -1
- package/dist/repository/taskWorkspacePreparer.js +17 -1
- package/dist/review/reviewRound.js +27 -6
- package/dist/run/agentRun.js +2 -2
- package/dist/run/recoveryProjection.js +15 -0
- package/dist/runtime/runtimeContinuationProjection.js +7 -0
- package/dist/runtime/tmuxAdapters.js +8 -2
- package/dist/scheduler/actionability.js +169 -3
- package/dist/scheduler/activeTaskProgress.js +15 -10
- package/dist/scheduler/leaderWakeupProcessor.js +17 -1
- package/dist/scheduler/taskExecutionProjection.js +105 -8
- package/dist/scheduler/taskObservabilityProjection.js +282 -0
- package/dist/storage/migration/productionRegistry.js +14 -0
- package/dist/storage/sqliteStore.js +12 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/task/completionReadiness.js +1 -1
- package/dist/task/nextAction.js +314 -2
- package/dist/web/assets/client/components.js +116 -0
- package/dist/web/assets/client/i18n.js +66 -0
- package/dist/web/assets/client/view.js +15 -0
- package/dist/web/assets/styles/cards.js +23 -0
- package/dist/web/assets/styles/responsive.js +2 -0
- package/dist/web/webSnapshot.js +8 -2
- package/dist/workItem/workItem.js +262 -5
- package/i18n/README.zh-CN.md +42 -0
- package/package.json +1 -1
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
+
import { validateContextSnapshotRef } from "../context/contextSnapshot.js";
|
|
3
4
|
import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
4
5
|
/** The one execution abstraction used by both the one-lane and panel paths. */
|
|
5
6
|
export const EXECUTION_GROUP_SCHEMA_VERSION = 1;
|
|
6
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
|
+
];
|
|
7
26
|
export function createExecutionGroup(id, taskId, input, now) {
|
|
8
27
|
const timestamp = now.toISOString();
|
|
28
|
+
const groupId = requireIdentity(id, "ExecutionGroup id");
|
|
29
|
+
const normalizedTaskId = requireIdentity(taskId, "Task id");
|
|
9
30
|
const strategy = normalizeStrategy(input.strategy ?? { mode: "fixed", count: 1 });
|
|
10
|
-
const target = validateExecutionTarget(input.target,
|
|
31
|
+
const target = validateExecutionTarget(input.target, normalizedTaskId);
|
|
11
32
|
const laneInputs = input.lanes === undefined
|
|
12
33
|
? [{ roleName: input.roleName ?? "leader" }]
|
|
13
34
|
: input.lanes;
|
|
@@ -23,11 +44,14 @@ export function createExecutionGroup(id, taskId, input, now) {
|
|
|
23
44
|
const lanes = laneInputs.map((lane, index) => createLane(`${id}-lane-${lane.id ?? index + 1}`, id, lane, index + 1, timestamp));
|
|
24
45
|
return validateExecutionGroup({
|
|
25
46
|
schemaVersion: EXECUTION_GROUP_SCHEMA_VERSION,
|
|
26
|
-
id:
|
|
27
|
-
taskId:
|
|
47
|
+
id: groupId,
|
|
48
|
+
taskId: normalizedTaskId,
|
|
28
49
|
purpose: validatePurpose(input.purpose),
|
|
29
50
|
strategy,
|
|
30
51
|
target,
|
|
52
|
+
...(input.stage === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { stage: validateExecutionStageContext(input.stage, normalizedTaskId, groupId, strategy) }),
|
|
31
55
|
lanes,
|
|
32
56
|
createdAt: timestamp,
|
|
33
57
|
updatedAt: timestamp
|
|
@@ -85,6 +109,7 @@ export function assertExecutionGroupTransition(existing, candidate) {
|
|
|
85
109
|
|| existing.purpose !== candidate.purpose
|
|
86
110
|
|| !isDeepStrictEqual(existing.strategy, candidate.strategy)
|
|
87
111
|
|| !isDeepStrictEqual(existing.target, candidate.target)
|
|
112
|
+
|| !isDeepStrictEqual(existing.stage, candidate.stage)
|
|
88
113
|
|| existing.createdAt !== candidate.createdAt) {
|
|
89
114
|
throw new Error(`ExecutionGroup identity or target changed: ${existing.id}.`);
|
|
90
115
|
}
|
|
@@ -136,6 +161,7 @@ export function updateExecutionLane(group, laneId, patch, now) {
|
|
|
136
161
|
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
137
162
|
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
138
163
|
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
164
|
+
...(patch.directive === undefined ? {} : { directive: requireText(patch.directive, "ExecutionLane directive") }),
|
|
139
165
|
status,
|
|
140
166
|
updatedAt: timestamp,
|
|
141
167
|
...(terminal && existing.endedAt === undefined ? { endedAt: timestamp } : {})
|
|
@@ -147,9 +173,9 @@ export function updateExecutionLane(group, laneId, patch, now) {
|
|
|
147
173
|
});
|
|
148
174
|
}
|
|
149
175
|
/**
|
|
150
|
-
* Reopens a failed
|
|
151
|
-
*
|
|
152
|
-
*
|
|
176
|
+
* Reopens only a failed Lane for an explicit WorkItem retry. Completed and
|
|
177
|
+
* yielded results are immutable reusable outputs; retrying them would erase
|
|
178
|
+
* the Group's durable evidence instead of recovering unfinished work.
|
|
153
179
|
*/
|
|
154
180
|
export function restartExecutionLane(group, laneId, patch, now) {
|
|
155
181
|
validateExecutionGroup(group);
|
|
@@ -160,8 +186,8 @@ export function restartExecutionLane(group, laneId, patch, now) {
|
|
|
160
186
|
const existing = group.lanes.find((lane) => lane.id === id);
|
|
161
187
|
if (existing === undefined)
|
|
162
188
|
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
163
|
-
if (
|
|
164
|
-
throw new Error(`
|
|
189
|
+
if (existing.status !== "failed") {
|
|
190
|
+
throw new Error(`Only failed ExecutionLanes can start a new AgentRun: ${group.id}/${id}.`);
|
|
165
191
|
}
|
|
166
192
|
const timestamp = now.toISOString();
|
|
167
193
|
const { result: _result, endedAt: _endedAt, ...base } = existing;
|
|
@@ -172,6 +198,7 @@ export function restartExecutionLane(group, laneId, patch, now) {
|
|
|
172
198
|
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
173
199
|
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
174
200
|
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
201
|
+
...(patch.directive === undefined ? {} : { directive: requireText(patch.directive, "ExecutionLane directive") }),
|
|
175
202
|
status: "running",
|
|
176
203
|
updatedAt: timestamp
|
|
177
204
|
};
|
|
@@ -181,6 +208,42 @@ export function restartExecutionLane(group, laneId, patch, now) {
|
|
|
181
208
|
updatedAt: timestamp
|
|
182
209
|
});
|
|
183
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Reopen a failed execution Lane without starting its next Run when Resource
|
|
213
|
+
* Broker capacity is unavailable. The failed attempt remains in AgentRun
|
|
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 { runId: _runId, 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
|
+
}
|
|
184
247
|
/**
|
|
185
248
|
* Resets a terminal Reviewer Lane to pending without replacing its ExecutionGroup.
|
|
186
249
|
* The old AgentRun remains the attempt trail; clearing the Lane's Run/session and
|
|
@@ -225,6 +288,46 @@ export function recordExecutionLaneResult(group, laneId, result, status, now) {
|
|
|
225
288
|
updatedAt: timestamp
|
|
226
289
|
});
|
|
227
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Stop only work that has never started. Running Lanes retain their exact Run
|
|
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.runId !== 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
|
+
}
|
|
228
331
|
export function resolveExecutionGroup(group, input, now) {
|
|
229
332
|
validateExecutionGroup(group);
|
|
230
333
|
if (group.resolution !== undefined) {
|
|
@@ -250,6 +353,12 @@ export function resolveExecutionGroup(group, input, now) {
|
|
|
250
353
|
})) {
|
|
251
354
|
throw new Error(`ExecutionGroup accept selects a Lane without usable terminal output: ${group.id}.`);
|
|
252
355
|
}
|
|
356
|
+
if (input.decision === "accept" && group.stage?.resources !== undefined) {
|
|
357
|
+
const usable = group.lanes.filter(({ status }) => (status === "yielded" || 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
|
+
}
|
|
253
362
|
if (selected.some((id) => !group.lanes.some((lane) => lane.id === id))) {
|
|
254
363
|
throw new Error(`Execution resolution selects an unknown Lane: ${group.id}.`);
|
|
255
364
|
}
|
|
@@ -278,15 +387,21 @@ export function summarizeExecutionGroup(group) {
|
|
|
278
387
|
groupId: group.id,
|
|
279
388
|
purpose: group.purpose,
|
|
280
389
|
strategy: group.strategy,
|
|
390
|
+
...(group.stage === undefined ? {} : { stage: group.stage }),
|
|
281
391
|
laneCount: group.lanes.length,
|
|
282
392
|
activeLaneCount,
|
|
283
393
|
terminalLaneCount,
|
|
284
394
|
failedLaneCount: group.lanes.filter(({ status }) => status === "failed").length,
|
|
395
|
+
skippedLaneCount: group.lanes.filter(({ status }) => status === "skipped").length,
|
|
285
396
|
openHighPriorityFindingIds: openHighPriorityFindingIds(group),
|
|
286
397
|
laneSummaries: group.lanes.map((lane) => ({
|
|
287
398
|
laneId: lane.id,
|
|
288
399
|
roleName: lane.roleName,
|
|
289
400
|
ordinal: lane.ordinal,
|
|
401
|
+
...(lane.runId === undefined ? {} : { runId: lane.runId }),
|
|
402
|
+
...(lane.sessionId === undefined ? {} : { sessionId: lane.sessionId }),
|
|
403
|
+
...(lane.effective === undefined ? {} : { effective: lane.effective }),
|
|
404
|
+
...(lane.directive === undefined ? {} : { directive: lane.directive }),
|
|
290
405
|
status: lane.status,
|
|
291
406
|
...(lane.result === undefined ? {} : {
|
|
292
407
|
summary: lane.result.summary,
|
|
@@ -314,6 +429,12 @@ export function validateExecutionGroup(group) {
|
|
|
314
429
|
validatePurpose(group.purpose);
|
|
315
430
|
normalizeStrategy(group.strategy);
|
|
316
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
|
+
}
|
|
317
438
|
if (!Array.isArray(group.lanes) || group.lanes.length === 0) {
|
|
318
439
|
throw new Error("ExecutionGroup requires at least one Lane.");
|
|
319
440
|
}
|
|
@@ -349,6 +470,96 @@ export function validateExecutionGroup(group) {
|
|
|
349
470
|
requireTimestamp(group.updatedAt, "ExecutionGroup updatedAt");
|
|
350
471
|
return group;
|
|
351
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
|
+
}
|
|
352
563
|
export function validateExecutionLane(lane, group) {
|
|
353
564
|
if (lane.schemaVersion !== EXECUTION_LANE_SCHEMA_VERSION) {
|
|
354
565
|
throw new Error("ExecutionLane must use schemaVersion 1.");
|
|
@@ -361,7 +572,10 @@ export function validateExecutionLane(lane, group) {
|
|
|
361
572
|
if (group.purpose === "review" && lane.reviewRoundId === undefined) {
|
|
362
573
|
// A review lane may be pending before its ReviewRound is dispatched, but
|
|
363
574
|
// it must acquire that identity before it can run.
|
|
364
|
-
if (lane.status === "running"
|
|
575
|
+
if (lane.status === "running"
|
|
576
|
+
|| lane.status === "completed"
|
|
577
|
+
|| lane.status === "failed"
|
|
578
|
+
|| lane.status === "skipped") {
|
|
365
579
|
throw new Error(`Running Reviewer Lane requires a ReviewRound: ${lane.id}.`);
|
|
366
580
|
}
|
|
367
581
|
}
|
|
@@ -378,9 +592,19 @@ export function validateExecutionLane(lane, group) {
|
|
|
378
592
|
requireIdentity(lane.reviewRoundId, "ReviewRound id");
|
|
379
593
|
if (lane.workspace !== undefined)
|
|
380
594
|
validateLaneWorkspace(lane.workspace);
|
|
381
|
-
if (
|
|
595
|
+
if (lane.directive !== undefined)
|
|
596
|
+
requireText(lane.directive, "ExecutionLane directive");
|
|
597
|
+
if (!["pending", "running", "yielded", "completed", "failed", "skipped"].includes(lane.status)) {
|
|
382
598
|
throw new Error(`ExecutionLane status is invalid: ${String(lane.status)}.`);
|
|
383
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.runId !== undefined || lane.sessionId !== undefined) {
|
|
605
|
+
throw new Error(`Skipped ExecutionLane must never have started: ${lane.id}.`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
384
608
|
if (lane.result !== undefined)
|
|
385
609
|
validateLaneResult(lane.result);
|
|
386
610
|
requireTimestamp(lane.createdAt, "ExecutionLane createdAt");
|
|
@@ -407,6 +631,7 @@ function createLane(id, groupId, input, ordinal, timestamp) {
|
|
|
407
631
|
...(input.sessionId === undefined ? {} : { sessionId: requireIdentity(input.sessionId, "Session id") }),
|
|
408
632
|
...(input.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(input.reviewRoundId, "ReviewRound id") }),
|
|
409
633
|
...(input.workspace === undefined ? {} : { workspace: validateLaneWorkspace(input.workspace) }),
|
|
634
|
+
...(input.directive === undefined ? {} : { directive: requireText(input.directive, "ExecutionLane directive") }),
|
|
410
635
|
status: "pending",
|
|
411
636
|
createdAt: timestamp,
|
|
412
637
|
updatedAt: timestamp
|
|
@@ -549,6 +774,9 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
549
774
|
if (isTerminalLane(existing.status)) {
|
|
550
775
|
if (isDeepStrictEqual(existing, candidate))
|
|
551
776
|
return;
|
|
777
|
+
if (existing.status === "skipped") {
|
|
778
|
+
throw new Error(`Skipped ExecutionLane is immutable: ${existing.id}.`);
|
|
779
|
+
}
|
|
552
780
|
if (groupPurpose === "review"
|
|
553
781
|
&& candidate.status === "pending"
|
|
554
782
|
&& candidate.effective === undefined
|
|
@@ -559,6 +787,17 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
559
787
|
&& isDeepStrictEqual(existing.workspace, candidate.workspace)) {
|
|
560
788
|
return;
|
|
561
789
|
}
|
|
790
|
+
if (groupPurpose === "execution"
|
|
791
|
+
&& existing.status === "failed"
|
|
792
|
+
&& candidate.status === "pending"
|
|
793
|
+
&& candidate.effective !== undefined
|
|
794
|
+
&& candidate.runId === undefined
|
|
795
|
+
&& candidate.sessionId === undefined
|
|
796
|
+
&& candidate.result === undefined
|
|
797
|
+
&& candidate.endedAt === undefined
|
|
798
|
+
&& candidate.directive !== undefined) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
562
801
|
if (candidate.status !== "running"
|
|
563
802
|
|| candidate.runId === undefined
|
|
564
803
|
|| candidate.runId === existing.runId) {
|
|
@@ -569,7 +808,7 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
569
808
|
if (existing.status === "running" && candidate.status === "pending") {
|
|
570
809
|
throw new Error(`Running ExecutionLane cannot return to pending: ${existing.id}.`);
|
|
571
810
|
}
|
|
572
|
-
for (const key of ["effective", "runId", "sessionId", "workspace"]) {
|
|
811
|
+
for (const key of ["effective", "runId", "sessionId", "workspace", "directive"]) {
|
|
573
812
|
if (existing[key] !== undefined
|
|
574
813
|
&& !isDeepStrictEqual(existing[key], candidate[key])) {
|
|
575
814
|
throw new Error(`ExecutionLane ${key} changed without retry: ${existing.id}.`);
|
|
@@ -598,7 +837,10 @@ function validatePurpose(purpose) {
|
|
|
598
837
|
return purpose;
|
|
599
838
|
}
|
|
600
839
|
function isTerminalLane(status) {
|
|
601
|
-
return status === "yielded"
|
|
840
|
+
return status === "yielded"
|
|
841
|
+
|| status === "completed"
|
|
842
|
+
|| status === "failed"
|
|
843
|
+
|| status === "skipped";
|
|
602
844
|
}
|
|
603
845
|
function positiveInteger(value, label) {
|
|
604
846
|
if (!Number.isSafeInteger(value) || value < 1)
|