@abelxiaoxing/cadence 1.1.0 → 1.2.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 +19 -3
- package/agents/implementation-worker.md +17 -11
- package/config/routes.example.json +0 -2
- package/package.json +2 -2
- package/prompts/abel-design.md +77 -17
- package/prompts/abel-diagnose.md +2 -0
- package/prompts/abel-implement.md +23 -11
- package/skills/abel-workflow/SKILL.md +42 -5
- package/src/agent-registry.ts +0 -1
- package/src/candidate-patch.ts +250 -0
- package/src/child-session.ts +27 -24
- package/src/contracts.ts +19 -10
- package/src/control-contracts.ts +15 -62
- package/src/delivery-compiler.ts +47 -17
- package/src/design-control.ts +1053 -0
- package/src/design-journal.ts +1616 -0
- package/src/index.ts +626 -196
- package/src/packet-runtime.ts +6 -6
- package/src/route-policy.ts +45 -17
- package/src/run-state.ts +43 -4
- package/src/run-store.ts +103 -15
- package/src/subagent-activity.ts +1 -1
- package/src/submit-tool.ts +149 -49
- package/src/verification-capability.ts +43 -15
- package/src/worker-broker.ts +0 -1
- package/src/workflow-engine.ts +416 -250
- package/agents/contract-reviewer.md +0 -11
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
|
}
|
|
@@ -2698,13 +2833,14 @@ class DurableWorkflowComposition
|
|
|
2698
2833
|
const phase = request.task.phases[request.phase];
|
|
2699
2834
|
if (!phase) throw new Error("workflow-task-phase-invalid");
|
|
2700
2835
|
const allPhases = Object.values(request.task.phases);
|
|
2836
|
+
const approvedWritePaths = [
|
|
2837
|
+
...new Set(allPhases.flatMap((boundary) => boundary.write)),
|
|
2838
|
+
].sort();
|
|
2839
|
+
const approvedDeletePaths = [
|
|
2840
|
+
...new Set(allPhases.flatMap((boundary) => boundary.delete)),
|
|
2841
|
+
].sort();
|
|
2701
2842
|
const approvedPaths = [
|
|
2702
|
-
...new Set(
|
|
2703
|
-
allPhases.flatMap((boundary) => [
|
|
2704
|
-
...boundary.write,
|
|
2705
|
-
...boundary.delete,
|
|
2706
|
-
]),
|
|
2707
|
-
),
|
|
2843
|
+
...new Set([...approvedWritePaths, ...approvedDeletePaths]),
|
|
2708
2844
|
].sort();
|
|
2709
2845
|
const boundPaths = [
|
|
2710
2846
|
...new Set(
|
|
@@ -2780,7 +2916,13 @@ class DurableWorkflowComposition
|
|
|
2780
2916
|
task: structuredClone(request.task),
|
|
2781
2917
|
workspaceRoot: proposalRoot,
|
|
2782
2918
|
ledgerProjection,
|
|
2783
|
-
candidateArtifact: {
|
|
2919
|
+
candidateArtifact: {
|
|
2920
|
+
ledger,
|
|
2921
|
+
identity,
|
|
2922
|
+
workspaceRoot: proposalRoot,
|
|
2923
|
+
writePaths: approvedWritePaths,
|
|
2924
|
+
deletePaths: approvedDeletePaths,
|
|
2925
|
+
},
|
|
2784
2926
|
route: attempt.route,
|
|
2785
2927
|
repair,
|
|
2786
2928
|
signal: attempt.signal,
|
|
@@ -3549,7 +3691,13 @@ class DurableWorkflowComposition
|
|
|
3549
3691
|
task: structuredClone(input.task),
|
|
3550
3692
|
workspaceRoot: proposalRoot,
|
|
3551
3693
|
ledgerProjection,
|
|
3552
|
-
candidateArtifact: {
|
|
3694
|
+
candidateArtifact: {
|
|
3695
|
+
ledger,
|
|
3696
|
+
identity,
|
|
3697
|
+
workspaceRoot: proposalRoot,
|
|
3698
|
+
writePaths: phase.write,
|
|
3699
|
+
deletePaths: phase.delete,
|
|
3700
|
+
},
|
|
3553
3701
|
route: attempt.route,
|
|
3554
3702
|
signal: attempt.signal,
|
|
3555
3703
|
onHeaders: attempt.onHeaders,
|
|
@@ -4676,21 +4824,6 @@ export class WorkflowEngine {
|
|
|
4676
4824
|
)?.run_id;
|
|
4677
4825
|
}
|
|
4678
4826
|
|
|
4679
|
-
#lookupProvisionalRun(provisionalKey: string): string | undefined {
|
|
4680
|
-
return (
|
|
4681
|
-
this.#database
|
|
4682
|
-
.prepare(
|
|
4683
|
-
`SELECT run_id FROM runs
|
|
4684
|
-
WHERE root_hash = ? AND stage = 'abel-design'
|
|
4685
|
-
AND lookup_key = ?`,
|
|
4686
|
-
)
|
|
4687
|
-
.get(
|
|
4688
|
-
this.#stateRoot.consumerRootHash,
|
|
4689
|
-
`provisional:${provisionalKey}`,
|
|
4690
|
-
) as { run_id: string } | undefined
|
|
4691
|
-
)?.run_id;
|
|
4692
|
-
}
|
|
4693
|
-
|
|
4694
4827
|
#ensureEngineRun(runId: string): void {
|
|
4695
4828
|
this.#database
|
|
4696
4829
|
.prepare(
|
|
@@ -5187,35 +5320,69 @@ export class WorkflowEngine {
|
|
|
5187
5320
|
};
|
|
5188
5321
|
}
|
|
5189
5322
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5323
|
+
const deliveryBindings = delivery.approvalProofs
|
|
5324
|
+
? ([
|
|
5325
|
+
{
|
|
5326
|
+
runId,
|
|
5327
|
+
gate: "gate-a" as const,
|
|
5328
|
+
revision: delivery.revision,
|
|
5329
|
+
receiptHash: delivery.receiptHash,
|
|
5330
|
+
approvalProof: structuredClone(delivery.approvalProofs.gateA),
|
|
5331
|
+
operationId: `bind-a-${hash(
|
|
5332
|
+
runId,
|
|
5333
|
+
String(delivery.revision),
|
|
5334
|
+
delivery.approvalProofs.gateA.recordHash,
|
|
5335
|
+
).slice(0, 40)}`,
|
|
5336
|
+
lease,
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
runId,
|
|
5340
|
+
gate: "gate-b" as const,
|
|
5341
|
+
revision: delivery.revision,
|
|
5342
|
+
receiptHash: delivery.receiptHash,
|
|
5343
|
+
approvalProof: structuredClone(delivery.approvalProofs.gateB),
|
|
5344
|
+
operationId: `bind-b-${hash(
|
|
5345
|
+
runId,
|
|
5346
|
+
String(delivery.revision),
|
|
5347
|
+
delivery.approvalProofs.gateB.recordHash,
|
|
5348
|
+
).slice(0, 40)}`,
|
|
5349
|
+
lease,
|
|
5350
|
+
},
|
|
5351
|
+
] as const)
|
|
5352
|
+
: ([
|
|
5353
|
+
{
|
|
5354
|
+
runId,
|
|
5355
|
+
gate: delivery.gate,
|
|
5356
|
+
revision: delivery.revision,
|
|
5357
|
+
receiptHash: delivery.receiptHash,
|
|
5358
|
+
operationId: `bind-${hash(
|
|
5359
|
+
runId,
|
|
5360
|
+
String(delivery.revision),
|
|
5361
|
+
delivery.receiptHash,
|
|
5362
|
+
).slice(0, 40)}`,
|
|
5363
|
+
lease,
|
|
5364
|
+
},
|
|
5365
|
+
] as const);
|
|
5366
|
+
this.#runStore.bindDeliveriesAtomically(deliveryBindings, (database) => {
|
|
5367
|
+
const engineLease = database
|
|
5368
|
+
.prepare(
|
|
5369
|
+
`SELECT state, lease_expires_at
|
|
5203
5370
|
FROM workflow_engine_operations
|
|
5204
5371
|
WHERE run_id = ? AND lease_token = ?`,
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5372
|
+
)
|
|
5373
|
+
.get(runId, lease.token) as
|
|
5374
|
+
| { state: string; lease_expires_at: number | null }
|
|
5375
|
+
| undefined;
|
|
5376
|
+
if (
|
|
5377
|
+
engineLease?.state !== "running" ||
|
|
5378
|
+
engineLease.lease_expires_at === null ||
|
|
5379
|
+
engineLease.lease_expires_at < this.#now()
|
|
5380
|
+
) {
|
|
5381
|
+
throw new Error("lease-fenced");
|
|
5382
|
+
}
|
|
5383
|
+
database
|
|
5384
|
+
.prepare(
|
|
5385
|
+
`INSERT INTO workflow_engine_runs(
|
|
5219
5386
|
run_id, current_revision, delivery_diagnostics_json,
|
|
5220
5387
|
next_queue_position
|
|
5221
5388
|
) VALUES (?, ?, NULL, 1)
|
|
@@ -5223,85 +5390,84 @@ export class WorkflowEngine {
|
|
|
5223
5390
|
current_revision = excluded.current_revision,
|
|
5224
5391
|
verification_json = NULL,
|
|
5225
5392
|
delivery_diagnostics_json = NULL`,
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5393
|
+
)
|
|
5394
|
+
.run(runId, delivery.revision);
|
|
5395
|
+
if (revalidatedWorkspace) {
|
|
5396
|
+
database
|
|
5397
|
+
.prepare(
|
|
5398
|
+
`UPDATE workflow_engine_runs
|
|
5232
5399
|
SET baseline_workspace_revision = ?,
|
|
5233
5400
|
current_workspace_revision = ?, cleanup_state = 'retained'
|
|
5234
5401
|
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
5402
|
)
|
|
5249
5403
|
.run(
|
|
5404
|
+
revalidatedWorkspace.baselineRevisionId,
|
|
5405
|
+
revalidatedWorkspace.currentWorkspaceRevisionId,
|
|
5250
5406
|
runId,
|
|
5251
|
-
delivery.revision,
|
|
5252
|
-
delivery.gate,
|
|
5253
|
-
delivery.receiptHash,
|
|
5254
|
-
serializedPlan,
|
|
5255
5407
|
);
|
|
5408
|
+
}
|
|
5409
|
+
database
|
|
5410
|
+
.prepare(
|
|
5411
|
+
`INSERT INTO workflow_engine_deliveries(
|
|
5412
|
+
run_id, revision, gate, receipt_hash, plan_json
|
|
5413
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
5414
|
+
ON CONFLICT(run_id, revision) DO NOTHING`,
|
|
5415
|
+
)
|
|
5416
|
+
.run(
|
|
5417
|
+
runId,
|
|
5418
|
+
delivery.revision,
|
|
5419
|
+
delivery.gate,
|
|
5420
|
+
delivery.receiptHash,
|
|
5421
|
+
serializedPlan,
|
|
5422
|
+
);
|
|
5256
5423
|
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5424
|
+
const admitted = new Set<string>();
|
|
5425
|
+
delivery.plan.tasks.forEach((task, taskOrder) => {
|
|
5426
|
+
admitted.add(task.taskId);
|
|
5427
|
+
const taskJson = JSON.stringify(task);
|
|
5428
|
+
const prior = priorById.get(task.taskId);
|
|
5429
|
+
if (!prior) {
|
|
5430
|
+
database
|
|
5431
|
+
.prepare(
|
|
5432
|
+
`INSERT INTO workflow_engine_tasks(
|
|
5266
5433
|
run_id, task_id, task_order, delivery_revision, plan_json,
|
|
5267
5434
|
state, phase
|
|
5268
5435
|
) 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
|
-
}
|
|
5436
|
+
)
|
|
5437
|
+
.run(runId, task.taskId, taskOrder, delivery.revision, taskJson);
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
5440
|
+
if (compatible.has(task.taskId) && !invalidated.has(task.taskId)) {
|
|
5283
5441
|
database
|
|
5284
5442
|
.prepare(
|
|
5285
5443
|
`UPDATE workflow_engine_tasks
|
|
5444
|
+
SET task_order = ?, plan_json = ?
|
|
5445
|
+
WHERE run_id = ? AND task_id = ?`,
|
|
5446
|
+
)
|
|
5447
|
+
.run(taskOrder, taskJson, runId, task.taskId);
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
database
|
|
5451
|
+
.prepare(
|
|
5452
|
+
`UPDATE workflow_engine_tasks
|
|
5286
5453
|
SET task_order = ?, delivery_revision = ?, plan_json = ?,
|
|
5287
5454
|
state = 'pending', phase = 'red', pause_code = NULL,
|
|
5288
5455
|
context_request_json = NULL, queue_position = NULL
|
|
5289
5456
|
WHERE run_id = ? AND task_id = ?`,
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5457
|
+
)
|
|
5458
|
+
.run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
|
|
5459
|
+
});
|
|
5460
|
+
for (const row of priorRows) {
|
|
5461
|
+
if (!admitted.has(row.task_id)) {
|
|
5462
|
+
database
|
|
5463
|
+
.prepare(
|
|
5464
|
+
`DELETE FROM workflow_engine_tasks
|
|
5298
5465
|
WHERE run_id = ? AND task_id = ?`,
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
}
|
|
5466
|
+
)
|
|
5467
|
+
.run(runId, row.task_id);
|
|
5302
5468
|
}
|
|
5303
|
-
}
|
|
5304
|
-
);
|
|
5469
|
+
}
|
|
5470
|
+
});
|
|
5305
5471
|
}
|
|
5306
5472
|
|
|
5307
5473
|
#deliveryFailure(
|
|
@@ -5797,16 +5963,22 @@ export class WorkflowEngine {
|
|
|
5797
5963
|
if (typeof outcome.code !== "string" || outcome.code.length === 0) {
|
|
5798
5964
|
throw new Error("workflow-attempt-outcome-invalid");
|
|
5799
5965
|
}
|
|
5966
|
+
const approvalCodeInvalid =
|
|
5967
|
+
outcome.kind === "approval-needed" &&
|
|
5968
|
+
!isWorkflowApprovalCode(outcome.code);
|
|
5800
5969
|
this.#setTask(
|
|
5801
5970
|
runId,
|
|
5802
5971
|
row.task_id,
|
|
5803
5972
|
{
|
|
5804
|
-
state: outcome.kind,
|
|
5973
|
+
state: approvalCodeInvalid ? "paused" : outcome.kind,
|
|
5805
5974
|
phase,
|
|
5806
|
-
pauseCode:
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5975
|
+
pauseCode: approvalCodeInvalid
|
|
5976
|
+
? "approval-code-invalid"
|
|
5977
|
+
: outcome.code,
|
|
5978
|
+
contextRequest:
|
|
5979
|
+
!approvalCodeInvalid && outcome.contextRequest
|
|
5980
|
+
? normalizeWorkflowContextRequest(outcome.contextRequest)
|
|
5981
|
+
: null,
|
|
5810
5982
|
queuePosition: null,
|
|
5811
5983
|
},
|
|
5812
5984
|
lease,
|
|
@@ -5936,13 +6108,21 @@ export class WorkflowEngine {
|
|
|
5936
6108
|
);
|
|
5937
6109
|
}
|
|
5938
6110
|
}
|
|
6111
|
+
const approvalCodeInvalid =
|
|
6112
|
+
verification.kind === "approval-needed" &&
|
|
6113
|
+
!isWorkflowApprovalCode(verification.code);
|
|
5939
6114
|
const state =
|
|
5940
|
-
verification.kind === "approval-needed"
|
|
6115
|
+
verification.kind === "approval-needed" && !approvalCodeInvalid
|
|
6116
|
+
? "approval-needed"
|
|
6117
|
+
: "paused";
|
|
6118
|
+
const code = approvalCodeInvalid
|
|
6119
|
+
? "approval-code-invalid"
|
|
6120
|
+
: verification.code;
|
|
5941
6121
|
this.#transition(
|
|
5942
6122
|
runId,
|
|
5943
6123
|
state,
|
|
5944
6124
|
`${operationId}:change-verification-${state}`,
|
|
5945
|
-
|
|
6125
|
+
code,
|
|
5946
6126
|
lease,
|
|
5947
6127
|
);
|
|
5948
6128
|
return this.#statusByRun(runId);
|
|
@@ -6406,6 +6586,18 @@ export class WorkflowEngine {
|
|
|
6406
6586
|
} catch {
|
|
6407
6587
|
engineRun = undefined;
|
|
6408
6588
|
}
|
|
6589
|
+
const contextRequest = firstPaused?.context_request_json
|
|
6590
|
+
? normalizeWorkflowContextRequest(
|
|
6591
|
+
JSON.parse(firstPaused.context_request_json) as unknown,
|
|
6592
|
+
)
|
|
6593
|
+
: undefined;
|
|
6594
|
+
const pauseCode =
|
|
6595
|
+
projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused";
|
|
6596
|
+
const approval =
|
|
6597
|
+
projection.state === "approval-needed"
|
|
6598
|
+
? approvalRequirement(pauseCode, contextRequest)
|
|
6599
|
+
: undefined;
|
|
6600
|
+
const currentRevision = projection.deliveryRevision ?? 0;
|
|
6409
6601
|
return {
|
|
6410
6602
|
version: 2,
|
|
6411
6603
|
runId: projection.runId,
|
|
@@ -6415,21 +6607,53 @@ export class WorkflowEngine {
|
|
|
6415
6607
|
...(projection.deliveryRevision === undefined
|
|
6416
6608
|
? {}
|
|
6417
6609
|
: { deliveryRevision: projection.deliveryRevision }),
|
|
6610
|
+
...(projection.deliveryBindings.length > 0
|
|
6611
|
+
? { deliveryBindings: structuredClone(projection.deliveryBindings) }
|
|
6612
|
+
: {}),
|
|
6418
6613
|
completed: projection.state === "completed",
|
|
6419
6614
|
...(projection.terminal ? { terminal: projection.terminal } : {}),
|
|
6420
6615
|
...(projection.pauseCode || firstPaused?.pause_code
|
|
6421
6616
|
? {
|
|
6422
6617
|
pause: {
|
|
6423
|
-
code:
|
|
6424
|
-
projection.pauseCode ??
|
|
6425
|
-
firstPaused?.pause_code ??
|
|
6426
|
-
"task-paused",
|
|
6618
|
+
code: pauseCode,
|
|
6427
6619
|
},
|
|
6428
6620
|
}
|
|
6429
6621
|
: {}),
|
|
6430
|
-
legalCommands:
|
|
6431
|
-
|
|
6432
|
-
|
|
6622
|
+
legalCommands:
|
|
6623
|
+
projection.state === "approval-needed"
|
|
6624
|
+
? ["status", "discard"]
|
|
6625
|
+
: legalControlCommands(projection.state).filter(
|
|
6626
|
+
(command) => command !== "rebind" || firstPaused !== undefined,
|
|
6627
|
+
),
|
|
6628
|
+
...(approval && projection.change
|
|
6629
|
+
? {
|
|
6630
|
+
approval: {
|
|
6631
|
+
category: approval.category,
|
|
6632
|
+
requiredGates: approval.requiredGates,
|
|
6633
|
+
refs: approval.refs,
|
|
6634
|
+
designRequest: `/abel-design --change ${projection.change}`,
|
|
6635
|
+
retainedRun: {
|
|
6636
|
+
runId: projection.runId,
|
|
6637
|
+
deliveryRevision: currentRevision,
|
|
6638
|
+
},
|
|
6639
|
+
receiptPrecondition: {
|
|
6640
|
+
deliveryRevision: { greaterThan: currentRevision },
|
|
6641
|
+
receiptHash: "matching-ready-receipt",
|
|
6642
|
+
},
|
|
6643
|
+
},
|
|
6644
|
+
conditionalCommands: [
|
|
6645
|
+
{
|
|
6646
|
+
command: "resume",
|
|
6647
|
+
stage: "abel-implement",
|
|
6648
|
+
change: projection.change,
|
|
6649
|
+
requires: {
|
|
6650
|
+
deliveryRevision: { greaterThan: currentRevision },
|
|
6651
|
+
receiptHash: "matching-ready-receipt",
|
|
6652
|
+
},
|
|
6653
|
+
},
|
|
6654
|
+
],
|
|
6655
|
+
}
|
|
6656
|
+
: {}),
|
|
6433
6657
|
tasks: rows.map((row) => ({
|
|
6434
6658
|
taskId: row.task_id,
|
|
6435
6659
|
state: row.state,
|
|
@@ -6502,6 +6726,59 @@ export class WorkflowEngine {
|
|
|
6502
6726
|
};
|
|
6503
6727
|
}
|
|
6504
6728
|
|
|
6729
|
+
async #withAvailableDelivery(
|
|
6730
|
+
outcome: Record<string, unknown>,
|
|
6731
|
+
): Promise<Record<string, unknown>> {
|
|
6732
|
+
if (
|
|
6733
|
+
outcome.stage !== "abel-implement" ||
|
|
6734
|
+
outcome.state !== "approval-needed" ||
|
|
6735
|
+
typeof outcome.change !== "string" ||
|
|
6736
|
+
!this.#deliverySource.discoverLatest
|
|
6737
|
+
) {
|
|
6738
|
+
return outcome;
|
|
6739
|
+
}
|
|
6740
|
+
let available: WorkflowAvailableDelivery | undefined;
|
|
6741
|
+
try {
|
|
6742
|
+
available = await this.#deliverySource.discoverLatest({
|
|
6743
|
+
stage: "abel-implement",
|
|
6744
|
+
change: outcome.change,
|
|
6745
|
+
});
|
|
6746
|
+
} catch {
|
|
6747
|
+
return outcome;
|
|
6748
|
+
}
|
|
6749
|
+
const currentRevision =
|
|
6750
|
+
typeof outcome.deliveryRevision === "number"
|
|
6751
|
+
? outcome.deliveryRevision
|
|
6752
|
+
: 0;
|
|
6753
|
+
if (
|
|
6754
|
+
!available ||
|
|
6755
|
+
!Number.isSafeInteger(available.deliveryRevision) ||
|
|
6756
|
+
available.deliveryRevision <= currentRevision ||
|
|
6757
|
+
!SHA256.test(available.receiptHash)
|
|
6758
|
+
) {
|
|
6759
|
+
return outcome;
|
|
6760
|
+
}
|
|
6761
|
+
const conditionalCommands = Array.isArray(outcome.conditionalCommands)
|
|
6762
|
+
? outcome.conditionalCommands.map((entry) =>
|
|
6763
|
+
isRecord(entry) && entry.command === "resume"
|
|
6764
|
+
? {
|
|
6765
|
+
...entry,
|
|
6766
|
+
satisfiedBy: {
|
|
6767
|
+
deliveryRevision: available.deliveryRevision,
|
|
6768
|
+
receiptHash: available.receiptHash,
|
|
6769
|
+
},
|
|
6770
|
+
}
|
|
6771
|
+
: entry,
|
|
6772
|
+
)
|
|
6773
|
+
: outcome.conditionalCommands;
|
|
6774
|
+
return {
|
|
6775
|
+
...outcome,
|
|
6776
|
+
legalCommands: ["status", "resume", "discard"],
|
|
6777
|
+
availableDelivery: structuredClone(available),
|
|
6778
|
+
...(conditionalCommands === undefined ? {} : { conditionalCommands }),
|
|
6779
|
+
};
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6505
6782
|
#launchAdvance(
|
|
6506
6783
|
runId: string,
|
|
6507
6784
|
operationId: string,
|
|
@@ -6567,84 +6844,7 @@ export class WorkflowEngine {
|
|
|
6567
6844
|
signal?: AbortSignal,
|
|
6568
6845
|
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
6569
6846
|
) {
|
|
6570
|
-
|
|
6571
|
-
const existingRun = this.#lookupProvisionalRun(command.provisionalKey);
|
|
6572
|
-
if (existingRun) {
|
|
6573
|
-
this.#ensureEngineRun(existingRun);
|
|
6574
|
-
const replay = this.#operationReplay(
|
|
6575
|
-
existingRun,
|
|
6576
|
-
command.operationId,
|
|
6577
|
-
command.command,
|
|
6578
|
-
);
|
|
6579
|
-
if (replay) return replay;
|
|
6580
|
-
const restarted = this.#runStore.startRun({
|
|
6581
|
-
stage: "abel-design",
|
|
6582
|
-
provisionalKey: command.provisionalKey,
|
|
6583
|
-
operationId: command.operationId,
|
|
6584
|
-
});
|
|
6585
|
-
const begun = this.#beginOperation(
|
|
6586
|
-
existingRun,
|
|
6587
|
-
command.operationId,
|
|
6588
|
-
command.command,
|
|
6589
|
-
);
|
|
6590
|
-
if (begun) return begun;
|
|
6591
|
-
if (restarted.state === "created") {
|
|
6592
|
-
const lease = this.#operationLease(existingRun, command.operationId);
|
|
6593
|
-
this.#transition(
|
|
6594
|
-
existingRun,
|
|
6595
|
-
"paused",
|
|
6596
|
-
`${command.operationId}:awaiting-gate-a`,
|
|
6597
|
-
"design-awaiting-gate-a",
|
|
6598
|
-
lease,
|
|
6599
|
-
);
|
|
6600
|
-
}
|
|
6601
|
-
return this.#commitOperation(
|
|
6602
|
-
existingRun,
|
|
6603
|
-
command.operationId,
|
|
6604
|
-
this.#statusByRun(existingRun),
|
|
6605
|
-
);
|
|
6606
|
-
}
|
|
6607
|
-
const created = this.#runStore.startRun({
|
|
6608
|
-
stage: "abel-design",
|
|
6609
|
-
provisionalKey: command.provisionalKey,
|
|
6610
|
-
operationId: command.operationId,
|
|
6611
|
-
});
|
|
6612
|
-
this.#ensureEngineRun(created.runId);
|
|
6613
|
-
const begun = this.#beginOperation(
|
|
6614
|
-
created.runId,
|
|
6615
|
-
command.operationId,
|
|
6616
|
-
command.command,
|
|
6617
|
-
);
|
|
6618
|
-
if (begun) return begun;
|
|
6619
|
-
const lease = this.#operationLease(created.runId, command.operationId);
|
|
6620
|
-
this.#transition(
|
|
6621
|
-
created.runId,
|
|
6622
|
-
"paused",
|
|
6623
|
-
`${command.operationId}:awaiting-gate-a`,
|
|
6624
|
-
"design-awaiting-gate-a",
|
|
6625
|
-
lease,
|
|
6626
|
-
);
|
|
6627
|
-
return this.#commitOperation(
|
|
6628
|
-
created.runId,
|
|
6629
|
-
command.operationId,
|
|
6630
|
-
this.#statusByRun(created.runId),
|
|
6631
|
-
);
|
|
6632
|
-
}
|
|
6633
|
-
let existingRun = this.#lookupRun(command.stage, command.change);
|
|
6634
|
-
const requestedProvisionalRun =
|
|
6635
|
-
command.stage === "abel-design" && command.provisionalKey
|
|
6636
|
-
? this.#lookupProvisionalRun(command.provisionalKey)
|
|
6637
|
-
: undefined;
|
|
6638
|
-
if (command.provisionalKey && !requestedProvisionalRun) {
|
|
6639
|
-
throw new Error("provisional-design-run-not-found");
|
|
6640
|
-
}
|
|
6641
|
-
if (
|
|
6642
|
-
existingRun &&
|
|
6643
|
-
requestedProvisionalRun &&
|
|
6644
|
-
existingRun !== requestedProvisionalRun
|
|
6645
|
-
) {
|
|
6646
|
-
throw new Error("provisional-design-run-conflict");
|
|
6647
|
-
}
|
|
6847
|
+
const existingRun = this.#lookupRun(command.stage, command.change);
|
|
6648
6848
|
if (existingRun) {
|
|
6649
6849
|
this.#ensureEngineRun(existingRun);
|
|
6650
6850
|
const replay = this.#operationReplay(
|
|
@@ -6673,17 +6873,6 @@ export class WorkflowEngine {
|
|
|
6673
6873
|
}
|
|
6674
6874
|
}
|
|
6675
6875
|
|
|
6676
|
-
if (requestedProvisionalRun) {
|
|
6677
|
-
this.#runStore.bindChange({
|
|
6678
|
-
runId: requestedProvisionalRun,
|
|
6679
|
-
change: command.change,
|
|
6680
|
-
operationId: `bind-change-${hash(
|
|
6681
|
-
requestedProvisionalRun,
|
|
6682
|
-
command.change,
|
|
6683
|
-
).slice(0, 40)}`,
|
|
6684
|
-
});
|
|
6685
|
-
existingRun = requestedProvisionalRun;
|
|
6686
|
-
}
|
|
6687
6876
|
const runId =
|
|
6688
6877
|
existingRun ??
|
|
6689
6878
|
this.#runStore.startRun({
|
|
@@ -6699,20 +6888,6 @@ export class WorkflowEngine {
|
|
|
6699
6888
|
);
|
|
6700
6889
|
if (begun) return begun;
|
|
6701
6890
|
const lease = this.#operationLease(runId, command.operationId);
|
|
6702
|
-
if (command.stage === "abel-design") {
|
|
6703
|
-
this.#transition(
|
|
6704
|
-
runId,
|
|
6705
|
-
"paused",
|
|
6706
|
-
`${command.operationId}:awaiting-design-evidence`,
|
|
6707
|
-
"design-awaiting-evidence",
|
|
6708
|
-
lease,
|
|
6709
|
-
);
|
|
6710
|
-
return this.#commitOperation(
|
|
6711
|
-
runId,
|
|
6712
|
-
command.operationId,
|
|
6713
|
-
this.#statusByRun(runId),
|
|
6714
|
-
);
|
|
6715
|
-
}
|
|
6716
6891
|
this.#transition(
|
|
6717
6892
|
runId,
|
|
6718
6893
|
"validating-delivery",
|
|
@@ -6774,17 +6949,6 @@ export class WorkflowEngine {
|
|
|
6774
6949
|
if (replay) return replay;
|
|
6775
6950
|
const lease = this.#operationLease(runId, command.operationId);
|
|
6776
6951
|
const current = this.#runStore.status(runId);
|
|
6777
|
-
if (current.stage === "abel-design") {
|
|
6778
|
-
if (current.state !== "paused") {
|
|
6779
|
-
this.#interruptOperation(runId, command.operationId);
|
|
6780
|
-
throw new Error("resume-not-allowed");
|
|
6781
|
-
}
|
|
6782
|
-
return this.#commitOperation(
|
|
6783
|
-
runId,
|
|
6784
|
-
command.operationId,
|
|
6785
|
-
this.#statusByRun(runId),
|
|
6786
|
-
);
|
|
6787
|
-
}
|
|
6788
6952
|
if (current.state === "recovering") {
|
|
6789
6953
|
emitWorkflowActivity(onActivity, {
|
|
6790
6954
|
state: "recovering",
|
|
@@ -6830,15 +6994,13 @@ export class WorkflowEngine {
|
|
|
6830
6994
|
projectedRevision !== undefined && projectedRevision !== engineRevision;
|
|
6831
6995
|
if (
|
|
6832
6996
|
current.state === "approval-needed" &&
|
|
6833
|
-
command.deliveryRevision === undefined
|
|
6834
|
-
|
|
6835
|
-
|
|
6997
|
+
(command.deliveryRevision === undefined ||
|
|
6998
|
+
command.receiptHash === undefined ||
|
|
6999
|
+
command.deliveryRevision <=
|
|
7000
|
+
(engineRevision ?? projectedRevision ?? 0))
|
|
6836
7001
|
) {
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
command.operationId,
|
|
6840
|
-
this.#statusByRun(runId),
|
|
6841
|
-
);
|
|
7002
|
+
this.#interruptOperation(runId, command.operationId);
|
|
7003
|
+
throw new Error("approval-receipt-required");
|
|
6842
7004
|
}
|
|
6843
7005
|
if (
|
|
6844
7006
|
command.deliveryRevision !== undefined ||
|
|
@@ -6922,12 +7084,12 @@ export class WorkflowEngine {
|
|
|
6922
7084
|
if (replay) return replay;
|
|
6923
7085
|
const lease = this.#operationLease(runId, command.operationId);
|
|
6924
7086
|
const state = this.#runStore.status(runId).state;
|
|
6925
|
-
if (!["paused", "retryable"
|
|
7087
|
+
if (!["paused", "retryable"].includes(state)) {
|
|
6926
7088
|
this.#interruptOperation(runId, command.operationId);
|
|
6927
7089
|
throw new Error("rebind-not-allowed");
|
|
6928
7090
|
}
|
|
6929
7091
|
const task = this.#tasks(runId).find((row) =>
|
|
6930
|
-
["paused", "retryable"
|
|
7092
|
+
["paused", "retryable"].includes(row.state),
|
|
6931
7093
|
);
|
|
6932
7094
|
if (!task) {
|
|
6933
7095
|
this.#interruptOperation(runId, command.operationId);
|
|
@@ -7169,14 +7331,18 @@ export class WorkflowEngine {
|
|
|
7169
7331
|
const command = assertControlCommand(value);
|
|
7170
7332
|
switch (command.command) {
|
|
7171
7333
|
case "start":
|
|
7172
|
-
return this.#
|
|
7334
|
+
return this.#withAvailableDelivery(
|
|
7335
|
+
await this.#start(command, signal, onActivity),
|
|
7336
|
+
);
|
|
7173
7337
|
case "status": {
|
|
7174
7338
|
const runId = this.#lookupRun(command.stage, command.change);
|
|
7175
7339
|
if (!runId) throw new Error("run-not-found");
|
|
7176
|
-
return this.#statusByRun(runId);
|
|
7340
|
+
return this.#withAvailableDelivery(this.#statusByRun(runId));
|
|
7177
7341
|
}
|
|
7178
7342
|
case "resume":
|
|
7179
|
-
return this.#
|
|
7343
|
+
return this.#withAvailableDelivery(
|
|
7344
|
+
await this.#resume(command, signal, onActivity),
|
|
7345
|
+
);
|
|
7180
7346
|
case "rebind":
|
|
7181
7347
|
return this.#rebind(command);
|
|
7182
7348
|
case "cancel":
|