@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.
Files changed (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. 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, taskId);
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: requireIdentity(id, "ExecutionGroup id"),
27
- taskId: requireIdentity(taskId, "Task id"),
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/yielded lane for an explicit WorkItem retry. The lane
151
- * keeps its identity and frozen Group target, while the previous result stays
152
- * in the event/Candidate history rather than being silently overwritten.
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 (!isTerminalLane(existing.status)) {
164
- throw new Error(`ExecutionLane is not retryable: ${group.id}/${id}.`);
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" || lane.status === "completed" || lane.status === "failed") {
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 (!["pending", "running", "yielded", "completed", "failed"].includes(lane.status)) {
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" || status === "completed" || status === "failed";
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)