@abelxiaoxing/cadence 1.1.0 → 1.1.1

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.
@@ -16,6 +16,7 @@ import {
16
16
  } from "./apply-transaction.ts";
17
17
  import { ArtifactStore } from "./artifact-store.ts";
18
18
  import {
19
+ type ApprovalBoundaryCode,
19
20
  diffWritePaths,
20
21
  isValidRelativePath,
21
22
  type StructuredVerificationContract,
@@ -26,6 +27,7 @@ import {
26
27
  type ControlCommand,
27
28
  type ControlStage,
28
29
  } from "./control-contracts.ts";
30
+ import type { GateApprovalProof } from "./delivery-compiler.ts";
29
31
  import {
30
32
  DeliveryValidationError,
31
33
  IMPLEMENT_PLAN_SCHEMA_VERSION,
@@ -172,6 +174,88 @@ interface WorkflowContextRequest {
172
174
  refs: string[];
173
175
  }
174
176
 
177
+ export const APPROVAL_AUTHORITY_CATEGORIES = [
178
+ "observable-behavior",
179
+ "architecture-policy",
180
+ "dependency",
181
+ "path-boundary",
182
+ "conflict-resource",
183
+ "verification-contract",
184
+ "agents-contract",
185
+ "irreversible-scope",
186
+ ] as const;
187
+
188
+ export type ApprovalAuthorityCategory =
189
+ (typeof APPROVAL_AUTHORITY_CATEGORIES)[number];
190
+
191
+ interface ApprovalRequirement {
192
+ category: ApprovalAuthorityCategory;
193
+ requiredGates: Array<"gate-a" | "gate-b">;
194
+ refs: string[];
195
+ }
196
+
197
+ const INTERNAL_APPROVAL_CODES = [
198
+ "boundary-review-needed",
199
+ "repair-boundary-expansion",
200
+ "task-write-set-empty",
201
+ ] as const;
202
+
203
+ type InternalApprovalCode = (typeof INTERNAL_APPROVAL_CODES)[number];
204
+ export type WorkflowApprovalCode = ApprovalBoundaryCode | InternalApprovalCode;
205
+
206
+ const APPROVAL_REQUIREMENT_BY_CODE = {
207
+ "agents-contract-insufficient": {
208
+ category: "agents-contract",
209
+ requiredGates: ["gate-b"],
210
+ },
211
+ "architecture-contract-insufficient": {
212
+ category: "architecture-policy",
213
+ requiredGates: ["gate-b"],
214
+ },
215
+ "behavior-contract-insufficient": {
216
+ category: "observable-behavior",
217
+ requiredGates: ["gate-a", "gate-b"],
218
+ },
219
+ "boundary-review-needed": {
220
+ category: "path-boundary",
221
+ requiredGates: ["gate-b"],
222
+ },
223
+ "conflict-resource-authority-insufficient": {
224
+ category: "conflict-resource",
225
+ requiredGates: ["gate-b"],
226
+ },
227
+ "irreversible-scope-insufficient": {
228
+ category: "irreversible-scope",
229
+ requiredGates: ["gate-a", "gate-b"],
230
+ },
231
+ "repair-boundary-expansion": {
232
+ category: "path-boundary",
233
+ requiredGates: ["gate-b"],
234
+ },
235
+ "task-scope-insufficient": {
236
+ category: "path-boundary",
237
+ requiredGates: ["gate-b"],
238
+ },
239
+ "task-write-set-empty": {
240
+ category: "path-boundary",
241
+ requiredGates: ["gate-b"],
242
+ },
243
+ "unapproved-dependency-change": {
244
+ category: "dependency",
245
+ requiredGates: ["gate-b"],
246
+ },
247
+ "verification-contract-insufficient": {
248
+ category: "verification-contract",
249
+ requiredGates: ["gate-b"],
250
+ },
251
+ } as const satisfies Record<
252
+ WorkflowApprovalCode,
253
+ {
254
+ category: ApprovalAuthorityCategory;
255
+ requiredGates: readonly ("gate-a" | "gate-b")[];
256
+ }
257
+ >;
258
+
175
259
  type WorkflowRetryPolicy = "artifact" | "stale" | "verification" | "checkpoint";
176
260
 
177
261
  export type WorkflowAttemptOutcome = (
@@ -261,6 +345,15 @@ export interface WorkflowDelivery {
261
345
  revision: number;
262
346
  receiptHash: string;
263
347
  plan: ImplementPlan;
348
+ approvalProofs?: {
349
+ gateA: GateApprovalProof;
350
+ gateB: GateApprovalProof;
351
+ };
352
+ }
353
+
354
+ export interface WorkflowAvailableDelivery {
355
+ deliveryRevision: number;
356
+ receiptHash: string;
264
357
  }
265
358
 
266
359
  export interface WorkflowDeliverySource {
@@ -270,6 +363,10 @@ export interface WorkflowDeliverySource {
270
363
  deliveryRevision?: number;
271
364
  receiptHash?: string;
272
365
  }): Promise<WorkflowDelivery>;
366
+ discoverLatest?(input: {
367
+ stage: ControlStage;
368
+ change: string;
369
+ }): Promise<WorkflowAvailableDelivery | undefined>;
273
370
  }
274
371
 
275
372
  export interface WorkflowChangeVerifier {
@@ -489,6 +586,27 @@ function normalizeWorkflowContextRequest(
489
586
  };
490
587
  }
491
588
 
589
+ function approvalRequirement(
590
+ code: string,
591
+ contextRequest?: WorkflowContextRequest,
592
+ ): ApprovalRequirement {
593
+ const requirement = Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code)
594
+ ? APPROVAL_REQUIREMENT_BY_CODE[
595
+ code as keyof typeof APPROVAL_REQUIREMENT_BY_CODE
596
+ ]
597
+ : undefined;
598
+ if (!requirement) throw new Error("approval-code-invalid");
599
+ return {
600
+ category: requirement.category,
601
+ requiredGates: [...requirement.requiredGates],
602
+ refs: contextRequest ? [...contextRequest.refs] : [],
603
+ };
604
+ }
605
+
606
+ function isWorkflowApprovalCode(code: string): code is WorkflowApprovalCode {
607
+ return Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code);
608
+ }
609
+
492
610
  function hash(...values: string[]): string {
493
611
  const digest = createHash("sha256");
494
612
  for (const value of values) {
@@ -846,6 +964,23 @@ function assertDelivery(
846
964
  throw new Error("delivery-invalid");
847
965
  }
848
966
  assertPlan(value.plan);
967
+ if (value.approvalProofs !== undefined) {
968
+ if (!isRecord(value.approvalProofs)) throw new Error("delivery-invalid");
969
+ for (const gate of ["gateA", "gateB"] as const) {
970
+ const proof = value.approvalProofs[gate];
971
+ if (
972
+ !isRecord(proof) ||
973
+ !Number.isSafeInteger(proof.revision) ||
974
+ (proof.revision as number) < 1 ||
975
+ typeof proof.contractHash !== "string" ||
976
+ !SHA256.test(proof.contractHash) ||
977
+ typeof proof.recordHash !== "string" ||
978
+ !SHA256.test(proof.recordHash)
979
+ ) {
980
+ throw new Error("delivery-invalid");
981
+ }
982
+ }
983
+ }
849
984
  if (stage === "abel-implement" && value.gate !== "gate-b") {
850
985
  throw new Error("delivery-gate-b-required");
851
986
  }
@@ -5187,35 +5322,69 @@ export class WorkflowEngine {
5187
5322
  };
5188
5323
  }
5189
5324
  }
5190
- this.#runStore.bindDeliveryAtomically(
5191
- {
5192
- runId,
5193
- gate: delivery.gate,
5194
- revision: delivery.revision,
5195
- receiptHash: delivery.receiptHash,
5196
- operationId: `bind-${hash(runId, String(delivery.revision), delivery.receiptHash).slice(0, 40)}`,
5197
- lease,
5198
- },
5199
- (database) => {
5200
- const engineLease = database
5201
- .prepare(
5202
- `SELECT state, lease_expires_at
5325
+ const deliveryBindings = delivery.approvalProofs
5326
+ ? ([
5327
+ {
5328
+ runId,
5329
+ gate: "gate-a" as const,
5330
+ revision: delivery.revision,
5331
+ receiptHash: delivery.receiptHash,
5332
+ approvalProof: structuredClone(delivery.approvalProofs.gateA),
5333
+ operationId: `bind-a-${hash(
5334
+ runId,
5335
+ String(delivery.revision),
5336
+ delivery.approvalProofs.gateA.recordHash,
5337
+ ).slice(0, 40)}`,
5338
+ lease,
5339
+ },
5340
+ {
5341
+ runId,
5342
+ gate: "gate-b" as const,
5343
+ revision: delivery.revision,
5344
+ receiptHash: delivery.receiptHash,
5345
+ approvalProof: structuredClone(delivery.approvalProofs.gateB),
5346
+ operationId: `bind-b-${hash(
5347
+ runId,
5348
+ String(delivery.revision),
5349
+ delivery.approvalProofs.gateB.recordHash,
5350
+ ).slice(0, 40)}`,
5351
+ lease,
5352
+ },
5353
+ ] as const)
5354
+ : ([
5355
+ {
5356
+ runId,
5357
+ gate: delivery.gate,
5358
+ revision: delivery.revision,
5359
+ receiptHash: delivery.receiptHash,
5360
+ operationId: `bind-${hash(
5361
+ runId,
5362
+ String(delivery.revision),
5363
+ delivery.receiptHash,
5364
+ ).slice(0, 40)}`,
5365
+ lease,
5366
+ },
5367
+ ] as const);
5368
+ this.#runStore.bindDeliveriesAtomically(deliveryBindings, (database) => {
5369
+ const engineLease = database
5370
+ .prepare(
5371
+ `SELECT state, lease_expires_at
5203
5372
  FROM workflow_engine_operations
5204
5373
  WHERE run_id = ? AND lease_token = ?`,
5205
- )
5206
- .get(runId, lease.token) as
5207
- | { state: string; lease_expires_at: number | null }
5208
- | undefined;
5209
- if (
5210
- engineLease?.state !== "running" ||
5211
- engineLease.lease_expires_at === null ||
5212
- engineLease.lease_expires_at < this.#now()
5213
- ) {
5214
- throw new Error("lease-fenced");
5215
- }
5216
- database
5217
- .prepare(
5218
- `INSERT INTO workflow_engine_runs(
5374
+ )
5375
+ .get(runId, lease.token) as
5376
+ | { state: string; lease_expires_at: number | null }
5377
+ | undefined;
5378
+ if (
5379
+ engineLease?.state !== "running" ||
5380
+ engineLease.lease_expires_at === null ||
5381
+ engineLease.lease_expires_at < this.#now()
5382
+ ) {
5383
+ throw new Error("lease-fenced");
5384
+ }
5385
+ database
5386
+ .prepare(
5387
+ `INSERT INTO workflow_engine_runs(
5219
5388
  run_id, current_revision, delivery_diagnostics_json,
5220
5389
  next_queue_position
5221
5390
  ) VALUES (?, ?, NULL, 1)
@@ -5223,85 +5392,84 @@ export class WorkflowEngine {
5223
5392
  current_revision = excluded.current_revision,
5224
5393
  verification_json = NULL,
5225
5394
  delivery_diagnostics_json = NULL`,
5226
- )
5227
- .run(runId, delivery.revision);
5228
- if (revalidatedWorkspace) {
5229
- database
5230
- .prepare(
5231
- `UPDATE workflow_engine_runs
5395
+ )
5396
+ .run(runId, delivery.revision);
5397
+ if (revalidatedWorkspace) {
5398
+ database
5399
+ .prepare(
5400
+ `UPDATE workflow_engine_runs
5232
5401
  SET baseline_workspace_revision = ?,
5233
5402
  current_workspace_revision = ?, cleanup_state = 'retained'
5234
5403
  WHERE run_id = ?`,
5235
- )
5236
- .run(
5237
- revalidatedWorkspace.baselineRevisionId,
5238
- revalidatedWorkspace.currentWorkspaceRevisionId,
5239
- runId,
5240
- );
5241
- }
5242
- database
5243
- .prepare(
5244
- `INSERT INTO workflow_engine_deliveries(
5245
- run_id, revision, gate, receipt_hash, plan_json
5246
- ) VALUES (?, ?, ?, ?, ?)
5247
- ON CONFLICT(run_id, revision) DO NOTHING`,
5248
5404
  )
5249
5405
  .run(
5406
+ revalidatedWorkspace.baselineRevisionId,
5407
+ revalidatedWorkspace.currentWorkspaceRevisionId,
5250
5408
  runId,
5251
- delivery.revision,
5252
- delivery.gate,
5253
- delivery.receiptHash,
5254
- serializedPlan,
5255
5409
  );
5410
+ }
5411
+ database
5412
+ .prepare(
5413
+ `INSERT INTO workflow_engine_deliveries(
5414
+ run_id, revision, gate, receipt_hash, plan_json
5415
+ ) VALUES (?, ?, ?, ?, ?)
5416
+ ON CONFLICT(run_id, revision) DO NOTHING`,
5417
+ )
5418
+ .run(
5419
+ runId,
5420
+ delivery.revision,
5421
+ delivery.gate,
5422
+ delivery.receiptHash,
5423
+ serializedPlan,
5424
+ );
5256
5425
 
5257
- const admitted = new Set<string>();
5258
- delivery.plan.tasks.forEach((task, taskOrder) => {
5259
- admitted.add(task.taskId);
5260
- const taskJson = JSON.stringify(task);
5261
- const prior = priorById.get(task.taskId);
5262
- if (!prior) {
5263
- database
5264
- .prepare(
5265
- `INSERT INTO workflow_engine_tasks(
5426
+ const admitted = new Set<string>();
5427
+ delivery.plan.tasks.forEach((task, taskOrder) => {
5428
+ admitted.add(task.taskId);
5429
+ const taskJson = JSON.stringify(task);
5430
+ const prior = priorById.get(task.taskId);
5431
+ if (!prior) {
5432
+ database
5433
+ .prepare(
5434
+ `INSERT INTO workflow_engine_tasks(
5266
5435
  run_id, task_id, task_order, delivery_revision, plan_json,
5267
5436
  state, phase
5268
5437
  ) VALUES (?, ?, ?, ?, ?, 'pending', 'red')`,
5269
- )
5270
- .run(runId, task.taskId, taskOrder, delivery.revision, taskJson);
5271
- return;
5272
- }
5273
- if (compatible.has(task.taskId) && !invalidated.has(task.taskId)) {
5274
- database
5275
- .prepare(
5276
- `UPDATE workflow_engine_tasks
5277
- SET task_order = ?, plan_json = ?
5278
- WHERE run_id = ? AND task_id = ?`,
5279
- )
5280
- .run(taskOrder, taskJson, runId, task.taskId);
5281
- return;
5282
- }
5438
+ )
5439
+ .run(runId, task.taskId, taskOrder, delivery.revision, taskJson);
5440
+ return;
5441
+ }
5442
+ if (compatible.has(task.taskId) && !invalidated.has(task.taskId)) {
5283
5443
  database
5284
5444
  .prepare(
5285
5445
  `UPDATE workflow_engine_tasks
5446
+ SET task_order = ?, plan_json = ?
5447
+ WHERE run_id = ? AND task_id = ?`,
5448
+ )
5449
+ .run(taskOrder, taskJson, runId, task.taskId);
5450
+ return;
5451
+ }
5452
+ database
5453
+ .prepare(
5454
+ `UPDATE workflow_engine_tasks
5286
5455
  SET task_order = ?, delivery_revision = ?, plan_json = ?,
5287
5456
  state = 'pending', phase = 'red', pause_code = NULL,
5288
5457
  context_request_json = NULL, queue_position = NULL
5289
5458
  WHERE run_id = ? AND task_id = ?`,
5290
- )
5291
- .run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
5292
- });
5293
- for (const row of priorRows) {
5294
- if (!admitted.has(row.task_id)) {
5295
- database
5296
- .prepare(
5297
- `DELETE FROM workflow_engine_tasks
5459
+ )
5460
+ .run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
5461
+ });
5462
+ for (const row of priorRows) {
5463
+ if (!admitted.has(row.task_id)) {
5464
+ database
5465
+ .prepare(
5466
+ `DELETE FROM workflow_engine_tasks
5298
5467
  WHERE run_id = ? AND task_id = ?`,
5299
- )
5300
- .run(runId, row.task_id);
5301
- }
5468
+ )
5469
+ .run(runId, row.task_id);
5302
5470
  }
5303
- },
5304
- );
5471
+ }
5472
+ });
5305
5473
  }
5306
5474
 
5307
5475
  #deliveryFailure(
@@ -5797,16 +5965,22 @@ export class WorkflowEngine {
5797
5965
  if (typeof outcome.code !== "string" || outcome.code.length === 0) {
5798
5966
  throw new Error("workflow-attempt-outcome-invalid");
5799
5967
  }
5968
+ const approvalCodeInvalid =
5969
+ outcome.kind === "approval-needed" &&
5970
+ !isWorkflowApprovalCode(outcome.code);
5800
5971
  this.#setTask(
5801
5972
  runId,
5802
5973
  row.task_id,
5803
5974
  {
5804
- state: outcome.kind,
5975
+ state: approvalCodeInvalid ? "paused" : outcome.kind,
5805
5976
  phase,
5806
- pauseCode: outcome.code,
5807
- contextRequest: outcome.contextRequest
5808
- ? normalizeWorkflowContextRequest(outcome.contextRequest)
5809
- : null,
5977
+ pauseCode: approvalCodeInvalid
5978
+ ? "approval-code-invalid"
5979
+ : outcome.code,
5980
+ contextRequest:
5981
+ !approvalCodeInvalid && outcome.contextRequest
5982
+ ? normalizeWorkflowContextRequest(outcome.contextRequest)
5983
+ : null,
5810
5984
  queuePosition: null,
5811
5985
  },
5812
5986
  lease,
@@ -5936,13 +6110,21 @@ export class WorkflowEngine {
5936
6110
  );
5937
6111
  }
5938
6112
  }
6113
+ const approvalCodeInvalid =
6114
+ verification.kind === "approval-needed" &&
6115
+ !isWorkflowApprovalCode(verification.code);
5939
6116
  const state =
5940
- verification.kind === "approval-needed" ? "approval-needed" : "paused";
6117
+ verification.kind === "approval-needed" && !approvalCodeInvalid
6118
+ ? "approval-needed"
6119
+ : "paused";
6120
+ const code = approvalCodeInvalid
6121
+ ? "approval-code-invalid"
6122
+ : verification.code;
5941
6123
  this.#transition(
5942
6124
  runId,
5943
6125
  state,
5944
6126
  `${operationId}:change-verification-${state}`,
5945
- verification.code,
6127
+ code,
5946
6128
  lease,
5947
6129
  );
5948
6130
  return this.#statusByRun(runId);
@@ -6406,6 +6588,18 @@ export class WorkflowEngine {
6406
6588
  } catch {
6407
6589
  engineRun = undefined;
6408
6590
  }
6591
+ const contextRequest = firstPaused?.context_request_json
6592
+ ? normalizeWorkflowContextRequest(
6593
+ JSON.parse(firstPaused.context_request_json) as unknown,
6594
+ )
6595
+ : undefined;
6596
+ const pauseCode =
6597
+ projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused";
6598
+ const approval =
6599
+ projection.state === "approval-needed"
6600
+ ? approvalRequirement(pauseCode, contextRequest)
6601
+ : undefined;
6602
+ const currentRevision = projection.deliveryRevision ?? 0;
6409
6603
  return {
6410
6604
  version: 2,
6411
6605
  runId: projection.runId,
@@ -6415,21 +6609,53 @@ export class WorkflowEngine {
6415
6609
  ...(projection.deliveryRevision === undefined
6416
6610
  ? {}
6417
6611
  : { deliveryRevision: projection.deliveryRevision }),
6612
+ ...(projection.deliveryBindings.length > 0
6613
+ ? { deliveryBindings: structuredClone(projection.deliveryBindings) }
6614
+ : {}),
6418
6615
  completed: projection.state === "completed",
6419
6616
  ...(projection.terminal ? { terminal: projection.terminal } : {}),
6420
6617
  ...(projection.pauseCode || firstPaused?.pause_code
6421
6618
  ? {
6422
6619
  pause: {
6423
- code:
6424
- projection.pauseCode ??
6425
- firstPaused?.pause_code ??
6426
- "task-paused",
6620
+ code: pauseCode,
6427
6621
  },
6428
6622
  }
6429
6623
  : {}),
6430
- legalCommands: legalControlCommands(projection.state).filter(
6431
- (command) => command !== "rebind" || firstPaused !== undefined,
6432
- ),
6624
+ legalCommands:
6625
+ projection.state === "approval-needed"
6626
+ ? ["status", "discard"]
6627
+ : legalControlCommands(projection.state).filter(
6628
+ (command) => command !== "rebind" || firstPaused !== undefined,
6629
+ ),
6630
+ ...(approval && projection.change
6631
+ ? {
6632
+ approval: {
6633
+ category: approval.category,
6634
+ requiredGates: approval.requiredGates,
6635
+ refs: approval.refs,
6636
+ designRequest: `/abel-design --change ${projection.change}`,
6637
+ retainedRun: {
6638
+ runId: projection.runId,
6639
+ deliveryRevision: currentRevision,
6640
+ },
6641
+ receiptPrecondition: {
6642
+ deliveryRevision: { greaterThan: currentRevision },
6643
+ receiptHash: "matching-ready-receipt",
6644
+ },
6645
+ },
6646
+ conditionalCommands: [
6647
+ {
6648
+ command: "resume",
6649
+ stage: "abel-implement",
6650
+ change: projection.change,
6651
+ requires: {
6652
+ deliveryRevision: { greaterThan: currentRevision },
6653
+ receiptHash: "matching-ready-receipt",
6654
+ },
6655
+ },
6656
+ ],
6657
+ }
6658
+ : {}),
6433
6659
  tasks: rows.map((row) => ({
6434
6660
  taskId: row.task_id,
6435
6661
  state: row.state,
@@ -6502,6 +6728,59 @@ export class WorkflowEngine {
6502
6728
  };
6503
6729
  }
6504
6730
 
6731
+ async #withAvailableDelivery(
6732
+ outcome: Record<string, unknown>,
6733
+ ): Promise<Record<string, unknown>> {
6734
+ if (
6735
+ outcome.stage !== "abel-implement" ||
6736
+ outcome.state !== "approval-needed" ||
6737
+ typeof outcome.change !== "string" ||
6738
+ !this.#deliverySource.discoverLatest
6739
+ ) {
6740
+ return outcome;
6741
+ }
6742
+ let available: WorkflowAvailableDelivery | undefined;
6743
+ try {
6744
+ available = await this.#deliverySource.discoverLatest({
6745
+ stage: "abel-implement",
6746
+ change: outcome.change,
6747
+ });
6748
+ } catch {
6749
+ return outcome;
6750
+ }
6751
+ const currentRevision =
6752
+ typeof outcome.deliveryRevision === "number"
6753
+ ? outcome.deliveryRevision
6754
+ : 0;
6755
+ if (
6756
+ !available ||
6757
+ !Number.isSafeInteger(available.deliveryRevision) ||
6758
+ available.deliveryRevision <= currentRevision ||
6759
+ !SHA256.test(available.receiptHash)
6760
+ ) {
6761
+ return outcome;
6762
+ }
6763
+ const conditionalCommands = Array.isArray(outcome.conditionalCommands)
6764
+ ? outcome.conditionalCommands.map((entry) =>
6765
+ isRecord(entry) && entry.command === "resume"
6766
+ ? {
6767
+ ...entry,
6768
+ satisfiedBy: {
6769
+ deliveryRevision: available.deliveryRevision,
6770
+ receiptHash: available.receiptHash,
6771
+ },
6772
+ }
6773
+ : entry,
6774
+ )
6775
+ : outcome.conditionalCommands;
6776
+ return {
6777
+ ...outcome,
6778
+ legalCommands: ["status", "resume", "discard"],
6779
+ availableDelivery: structuredClone(available),
6780
+ ...(conditionalCommands === undefined ? {} : { conditionalCommands }),
6781
+ };
6782
+ }
6783
+
6505
6784
  #launchAdvance(
6506
6785
  runId: string,
6507
6786
  operationId: string,
@@ -6645,6 +6924,40 @@ export class WorkflowEngine {
6645
6924
  ) {
6646
6925
  throw new Error("provisional-design-run-conflict");
6647
6926
  }
6927
+ if (
6928
+ existingRun &&
6929
+ command.stage === "abel-design" &&
6930
+ ["completed", "discarded", "rejected"].includes(
6931
+ this.#runStore.status(existingRun).state,
6932
+ )
6933
+ ) {
6934
+ const restarted = this.#runStore.startRun({
6935
+ stage: command.stage,
6936
+ change: command.change,
6937
+ operationId: command.operationId,
6938
+ });
6939
+ existingRun = restarted.runId;
6940
+ this.#ensureEngineRun(existingRun);
6941
+ const begun = this.#beginOperation(
6942
+ existingRun,
6943
+ command.operationId,
6944
+ command.command,
6945
+ );
6946
+ if (begun) return begun;
6947
+ const lease = this.#operationLease(existingRun, command.operationId);
6948
+ this.#transition(
6949
+ existingRun,
6950
+ "paused",
6951
+ `${command.operationId}:awaiting-design-evidence`,
6952
+ "design-awaiting-evidence",
6953
+ lease,
6954
+ );
6955
+ return this.#commitOperation(
6956
+ existingRun,
6957
+ command.operationId,
6958
+ this.#statusByRun(existingRun),
6959
+ );
6960
+ }
6648
6961
  if (existingRun) {
6649
6962
  this.#ensureEngineRun(existingRun);
6650
6963
  const replay = this.#operationReplay(
@@ -6830,15 +7143,13 @@ export class WorkflowEngine {
6830
7143
  projectedRevision !== undefined && projectedRevision !== engineRevision;
6831
7144
  if (
6832
7145
  current.state === "approval-needed" &&
6833
- command.deliveryRevision === undefined &&
6834
- engineRevision !== null &&
6835
- !deliveryOutOfSync
7146
+ (command.deliveryRevision === undefined ||
7147
+ command.receiptHash === undefined ||
7148
+ command.deliveryRevision <=
7149
+ (engineRevision ?? projectedRevision ?? 0))
6836
7150
  ) {
6837
- return this.#commitOperation(
6838
- runId,
6839
- command.operationId,
6840
- this.#statusByRun(runId),
6841
- );
7151
+ this.#interruptOperation(runId, command.operationId);
7152
+ throw new Error("approval-receipt-required");
6842
7153
  }
6843
7154
  if (
6844
7155
  command.deliveryRevision !== undefined ||
@@ -6922,12 +7233,12 @@ export class WorkflowEngine {
6922
7233
  if (replay) return replay;
6923
7234
  const lease = this.#operationLease(runId, command.operationId);
6924
7235
  const state = this.#runStore.status(runId).state;
6925
- if (!["paused", "retryable", "approval-needed"].includes(state)) {
7236
+ if (!["paused", "retryable"].includes(state)) {
6926
7237
  this.#interruptOperation(runId, command.operationId);
6927
7238
  throw new Error("rebind-not-allowed");
6928
7239
  }
6929
7240
  const task = this.#tasks(runId).find((row) =>
6930
- ["paused", "retryable", "approval-needed"].includes(row.state),
7241
+ ["paused", "retryable"].includes(row.state),
6931
7242
  );
6932
7243
  if (!task) {
6933
7244
  this.#interruptOperation(runId, command.operationId);
@@ -7169,14 +7480,18 @@ export class WorkflowEngine {
7169
7480
  const command = assertControlCommand(value);
7170
7481
  switch (command.command) {
7171
7482
  case "start":
7172
- return this.#start(command, signal, onActivity);
7483
+ return this.#withAvailableDelivery(
7484
+ await this.#start(command, signal, onActivity),
7485
+ );
7173
7486
  case "status": {
7174
7487
  const runId = this.#lookupRun(command.stage, command.change);
7175
7488
  if (!runId) throw new Error("run-not-found");
7176
- return this.#statusByRun(runId);
7489
+ return this.#withAvailableDelivery(this.#statusByRun(runId));
7177
7490
  }
7178
7491
  case "resume":
7179
- return this.#resume(command, signal, onActivity);
7492
+ return this.#withAvailableDelivery(
7493
+ await this.#resume(command, signal, onActivity),
7494
+ );
7180
7495
  case "rebind":
7181
7496
  return this.#rebind(command);
7182
7497
  case "cancel":