@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.
- package/README.md +11 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +69 -13
- package/prompts/abel-diagnose.md +2 -0
- package/prompts/abel-implement.md +19 -8
- package/skills/abel-workflow/SKILL.md +37 -3
- package/src/contracts.ts +18 -2
- package/src/control-contracts.ts +2 -15
- package/src/delivery-compiler.ts +47 -17
- package/src/design-control.ts +846 -0
- package/src/design-journal.ts +1569 -0
- package/src/index.ts +580 -197
- package/src/run-state.ts +39 -2
- package/src/run-store.ts +81 -9
- package/src/subagent-activity.ts +1 -1
- package/src/verification-capability.ts +43 -15
- package/src/workflow-engine.ts +428 -113
package/src/workflow-engine.ts
CHANGED
|
@@ -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
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
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
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
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
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
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
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
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
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
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
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
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
|
-
|
|
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:
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
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"
|
|
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
|
-
|
|
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:
|
|
6431
|
-
|
|
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
|
-
|
|
6835
|
-
|
|
7146
|
+
(command.deliveryRevision === undefined ||
|
|
7147
|
+
command.receiptHash === undefined ||
|
|
7148
|
+
command.deliveryRevision <=
|
|
7149
|
+
(engineRevision ?? projectedRevision ?? 0))
|
|
6836
7150
|
) {
|
|
6837
|
-
|
|
6838
|
-
|
|
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"
|
|
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"
|
|
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.#
|
|
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.#
|
|
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":
|