@abelxiaoxing/cadence 1.3.0 → 1.4.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 (45) hide show
  1. package/README.md +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. package/skills/abel-workflow/SKILL.md +0 -205
@@ -0,0 +1,4385 @@
1
+ import { compareCanonicalStrings } from "./canonical.ts";
2
+ import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
3
+ import { ROUTE_HEALTH_SCHEMA } from "./storage-schema.ts";
4
+ import { hasVerificationLifecycle } from "./workflow-policy.ts";
5
+
6
+ interface DurableResourceInput {
7
+ runId: string;
8
+ deliveryRevision: number;
9
+ plan?: ImplementPlan;
10
+ baselineRevisionId?: string;
11
+ currentWorkspaceRevisionId?: string;
12
+ }
13
+
14
+ import { spawn } from "node:child_process";
15
+ import { createHash, randomUUID } from "node:crypto";
16
+ import {
17
+ lstatSync,
18
+ mkdirSync,
19
+ mkdtempSync,
20
+ readFileSync,
21
+ rmSync,
22
+ } from "node:fs";
23
+ import path from "node:path";
24
+ import { DatabaseSync } from "node:sqlite";
25
+ import {
26
+ ApplyTransaction,
27
+ verifyCumulativeRevision,
28
+ } from "./apply-transaction.ts";
29
+ import { ArtifactStore } from "./artifact-store.ts";
30
+ import {
31
+ diffWritePaths,
32
+ type StructuredVerificationContract,
33
+ } from "./contracts.ts";
34
+ import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
35
+ import type { RoutePolicy, WorkerRoutePolicy } from "./route-policy.ts";
36
+ import { observeSafePath } from "./safe-path.ts";
37
+ import type { ResolvedStateRoot } from "./state-root.ts";
38
+ import type { CandidateArtifactSubmission } from "./submit-tool.ts";
39
+ import {
40
+ type BeginCandidateInput,
41
+ TASK_LEDGER_LIMITS,
42
+ TaskLedger,
43
+ type VerifiedTaskEvent,
44
+ } from "./task-ledger.ts";
45
+ import { type RouteHealthStore, RunWorkerBroker } from "./worker-broker.ts";
46
+ import {
47
+ type DependencyContractEntry,
48
+ DependencyManifestError,
49
+ type DurableCandidateProposal,
50
+ type DurableVerificationScope,
51
+ decideRecoveryAction,
52
+ deliveryBoundPaths,
53
+ deliveryTrackingPath,
54
+ dependencyContract,
55
+ emitWorkflowActivity,
56
+ expectedClassification,
57
+ hash,
58
+ hasUnapprovedDependencyChange,
59
+ IDENTIFIER,
60
+ implementationRouteRequirements,
61
+ isRecord,
62
+ LOCKFILES,
63
+ type RedArtifactCorrection,
64
+ SHA256,
65
+ semanticRouteFailure,
66
+ type WorkflowApplication,
67
+ type WorkflowApplicationContext,
68
+ type WorkflowAttemptOutcome,
69
+ type WorkflowChangeVerifier,
70
+ type WorkflowContextRequest,
71
+ type WorkflowDeliverySource,
72
+ type WorkflowRecoveryFeedback,
73
+ type WorkflowRouteFacts,
74
+ type WorkflowRunLifecycle,
75
+ type WorkflowWorker,
76
+ workspaceEntryEqual,
77
+ } from "./workflow-policy.ts";
78
+ import { WorkflowEngine } from "./workflow-state-machine.ts";
79
+ import { type WorkspaceRevision, WorkspaceStore } from "./workspace-store.ts";
80
+ export type DurablePhaseVerificationResult =
81
+ | {
82
+ ok: true;
83
+ exitCode: number;
84
+ classification: "expected-red" | "expected-green" | "expected-refactor";
85
+ diagnostic: { kind: "assertion" | "compiler"; id: string };
86
+ }
87
+ | {
88
+ ok: false;
89
+ kind: "paused" | "retryable" | "approval-needed";
90
+ code: string;
91
+ };
92
+
93
+ export type DurableChangeVerificationResult =
94
+ | {
95
+ ok: true;
96
+ exitCode: 0;
97
+ classification: string;
98
+ failureIdentities?: string[];
99
+ }
100
+ | {
101
+ ok: false;
102
+ kind:
103
+ | "artifact"
104
+ | "environment"
105
+ | "verification"
106
+ | "verification-adapter"
107
+ | "approval-boundary"
108
+ | "cancelled";
109
+ code: string;
110
+ failureIdentities?: string[];
111
+ };
112
+
113
+ interface DurableVerificationObservation {
114
+ status: "passed" | "failed";
115
+ verificationId: string;
116
+ failureIdentities: string[];
117
+ code?: string;
118
+ }
119
+
120
+ interface DurableVerificationBaseline {
121
+ revisionId: string;
122
+ targetContracts: Array<{
123
+ taskId: string;
124
+ verificationId: string;
125
+ expectedFailure: string;
126
+ }>;
127
+ affected: Array<{
128
+ taskId: string;
129
+ observation: DurableVerificationObservation;
130
+ }>;
131
+ fullSuite: DurableVerificationObservation;
132
+ }
133
+
134
+ type DurableObservedVerification =
135
+ | { ok: true; observation: DurableVerificationObservation }
136
+ | {
137
+ ok: false;
138
+ outcome:
139
+ | { kind: "paused"; code: string }
140
+ | { kind: "approval-needed"; code: string }
141
+ | { kind: "operation-cancelled"; code: "cancelled" };
142
+ };
143
+
144
+ type DurableBaselineResult =
145
+ | { ok: true; baseline: DurableVerificationBaseline }
146
+ | Extract<DurableObservedVerification, { ok: false }>;
147
+
148
+ type DurableAffectedResult =
149
+ | { kind: "verified"; attribution: "none" | "pre-existing" }
150
+ | {
151
+ kind: "repairable";
152
+ attribution: "introduced";
153
+ failureIdentities: string[];
154
+ }
155
+ | Extract<DurableObservedVerification, { ok: false }>["outcome"];
156
+
157
+ export interface DurableWorkflowEngineOptions {
158
+ consumerRoot: string;
159
+ stateRoot: ResolvedStateRoot;
160
+ deliverySource: WorkflowDeliverySource;
161
+ routePolicy: RoutePolicy;
162
+ proposeCandidate(input: {
163
+ runId: string;
164
+ operationId: string;
165
+ deliveryRevision: number;
166
+ taskId: string;
167
+ phase: "red" | "green" | "refactor";
168
+ task: PlanTaskDraft;
169
+ workspaceRoot: string;
170
+ ledgerProjection: unknown;
171
+ candidateArtifact: CandidateArtifactSubmission;
172
+ route: WorkerRoutePolicy;
173
+ repair?: {
174
+ attempt: number;
175
+ attribution: "introduced";
176
+ failureIdentities: string[];
177
+ };
178
+ artifactCorrection?: RedArtifactCorrection;
179
+ contextRequest?: WorkflowContextRequest;
180
+ contextReadPaths?: string[];
181
+ recoveryFeedback?: WorkflowRecoveryFeedback;
182
+ signal: AbortSignal;
183
+ onHeaders(): void;
184
+ onProgress(): void;
185
+ }): Promise<DurableCandidateProposal>;
186
+ verifyPhase(input: {
187
+ executionWritePaths?: readonly string[];
188
+ runId: string;
189
+ deliveryRevision: number;
190
+ taskId: string;
191
+ phase: "red" | "green" | "refactor";
192
+ root: string;
193
+ verification: StructuredVerificationContract;
194
+ signal: AbortSignal;
195
+ }): Promise<DurablePhaseVerificationResult>;
196
+ verifyChange(input: {
197
+ runId: string;
198
+ deliveryRevision: number;
199
+ root: string;
200
+ plan: ImplementPlan;
201
+ scope?: DurableVerificationScope;
202
+ verification?: StructuredVerificationContract;
203
+ taskId?: string;
204
+ signal: AbortSignal;
205
+ }): Promise<DurableChangeVerificationResult>;
206
+ now?: () => number;
207
+ leaseTtlMs?: number;
208
+ workHardLimit?: number;
209
+ verificationPolicy?: string;
210
+ verificationEnvironment?(
211
+ plan: ImplementPlan,
212
+ signal: AbortSignal,
213
+ ): Promise<string>;
214
+ }
215
+
216
+ interface DurableRunResources {
217
+ runId: string;
218
+ root: string;
219
+ plan: ImplementPlan;
220
+ deliveryRevision: number;
221
+ artifacts: ArtifactStore;
222
+ workspaces: WorkspaceStore;
223
+ ledgers: Map<number, TaskLedger>;
224
+ transactions: ApplyTransaction;
225
+ baselineRevisionId: string;
226
+ currentRevisionId: string;
227
+ mergeTail: Promise<void>;
228
+ verificationFact?: object;
229
+ baselinePromises: Map<number, Promise<DurableBaselineResult>>;
230
+ environmentIdentity?: string;
231
+ closed: boolean;
232
+ }
233
+
234
+ interface DurablePhaseFact {
235
+ kind: "phase-verified" | "repair-verified";
236
+ phase: "red" | "green" | "refactor";
237
+ isolatedRevisionId: string;
238
+ outputFacts: Array<
239
+ | { path: string; kind: "absent" }
240
+ | { path: string; kind: "file"; hash: string; bytes: number }
241
+ >;
242
+ }
243
+
244
+ interface DurableCandidateCommit {
245
+ artifactHash: string;
246
+ isolatedRevisionId: string;
247
+ exitCode: number;
248
+ classification: "expected-red" | "expected-green" | "expected-refactor";
249
+ diagnostic: { kind: "assertion" | "compiler"; id: string };
250
+ outputFacts: DurablePhaseFact["outputFacts"];
251
+ routeId: string;
252
+ routeFingerprint: string;
253
+ }
254
+
255
+ type DurableRepairResult =
256
+ | { kind: "repair-committed"; commit: DurableCandidateCommit }
257
+ | Exclude<WorkflowAttemptOutcome, { kind: "phase-committed" }>;
258
+
259
+ type DurableRouteHealth = Parameters<RouteHealthStore["set"]>[1];
260
+
261
+ class DurableRouteHealthStore implements RouteHealthStore {
262
+ readonly #databasePath: string;
263
+ #database?: DatabaseSync;
264
+ #closed = false;
265
+
266
+ constructor(databasePath: string) {
267
+ this.#databasePath = databasePath;
268
+ }
269
+
270
+ #open(): DatabaseSync {
271
+ if (this.#closed) throw new Error("route-health-store-closed");
272
+ if (this.#database) return this.#database;
273
+ const database = new DatabaseSync(this.#databasePath);
274
+ try {
275
+ configureSqlite(database);
276
+ ensureSqliteSchema(database, ROUTE_HEALTH_SCHEMA);
277
+ } catch (error) {
278
+ database.close();
279
+ throw error;
280
+ }
281
+
282
+ this.#database = database;
283
+ return database;
284
+ }
285
+
286
+ get(fingerprint: string): DurableRouteHealth | undefined {
287
+ if (!SHA256.test(fingerprint)) return undefined;
288
+ const row = this.#open()
289
+ .prepare(
290
+ `SELECT state, next_half_open_at, projection_json
291
+ FROM route_health WHERE route_fingerprint = ?`,
292
+ )
293
+ .get(fingerprint) as
294
+ | {
295
+ state: string;
296
+ next_half_open_at: number | null;
297
+ projection_json: string;
298
+ }
299
+ | undefined;
300
+ if (!row || !["healthy", "open", "half-open"].includes(row.state)) {
301
+ return undefined;
302
+ }
303
+ let projection: unknown;
304
+ try {
305
+ projection = JSON.parse(row.projection_json);
306
+ } catch {
307
+ return undefined;
308
+ }
309
+ if (!isRecord(projection) || projection.state !== row.state) {
310
+ return undefined;
311
+ }
312
+ const retryAt = row.next_half_open_at ?? undefined;
313
+ const lastCode =
314
+ typeof projection.lastCode === "string" &&
315
+ projection.lastCode.length > 0 &&
316
+ projection.lastCode.length <= 128
317
+ ? projection.lastCode
318
+ : undefined;
319
+ const probeExpiresAt =
320
+ Number.isSafeInteger(projection.probeExpiresAt) &&
321
+ (projection.probeExpiresAt as number) >= 0
322
+ ? (projection.probeExpiresAt as number)
323
+ : undefined;
324
+ return {
325
+ state: row.state as DurableRouteHealth["state"],
326
+ ...(retryAt === undefined ? {} : { retryAt }),
327
+ ...(projection.probeInFlight === true ? { probeInFlight: true } : {}),
328
+ ...(probeExpiresAt === undefined ? {} : { probeExpiresAt }),
329
+ ...(lastCode === undefined ? {} : { lastCode }),
330
+ };
331
+ }
332
+
333
+ set(fingerprint: string, health: DurableRouteHealth): void {
334
+ if (
335
+ !SHA256.test(fingerprint) ||
336
+ !["healthy", "open", "half-open"].includes(health.state) ||
337
+ (health.retryAt !== undefined &&
338
+ (!Number.isSafeInteger(health.retryAt) || health.retryAt < 0)) ||
339
+ (health.probeExpiresAt !== undefined &&
340
+ (!Number.isSafeInteger(health.probeExpiresAt) ||
341
+ health.probeExpiresAt < 0))
342
+ ) {
343
+ throw new Error("route-health-fact-invalid");
344
+ }
345
+ const projection = {
346
+ state: health.state,
347
+ ...(health.probeInFlight ? { probeInFlight: true } : {}),
348
+ ...(health.probeExpiresAt === undefined
349
+ ? {}
350
+ : { probeExpiresAt: health.probeExpiresAt }),
351
+ ...(health.lastCode ? { lastCode: health.lastCode.slice(0, 128) } : {}),
352
+ };
353
+ this.#open()
354
+ .prepare(
355
+ `INSERT INTO route_health(
356
+ route_fingerprint, state, next_half_open_at, projection_json
357
+ ) VALUES (?, ?, ?, ?)
358
+ ON CONFLICT(route_fingerprint) DO UPDATE SET
359
+ state = excluded.state,
360
+ next_half_open_at = excluded.next_half_open_at,
361
+ projection_json = excluded.projection_json`,
362
+ )
363
+ .run(
364
+ fingerprint,
365
+ health.state,
366
+ health.retryAt ?? null,
367
+ JSON.stringify(projection),
368
+ );
369
+ }
370
+
371
+ close(): void {
372
+ if (this.#closed) return;
373
+ this.#database?.close();
374
+ this.#closed = true;
375
+ }
376
+ }
377
+
378
+ function ensurePrivateRunDirectory(directory: string): void {
379
+ const existing = lstatSync(directory, { throwIfNoEntry: false });
380
+ if (existing) {
381
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
382
+ throw new Error("workflow-run-data-unsafe");
383
+ }
384
+ return;
385
+ }
386
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
387
+ const created = lstatSync(directory);
388
+ if (created.isSymbolicLink() || !created.isDirectory()) {
389
+ throw new Error("workflow-run-data-unsafe");
390
+ }
391
+ }
392
+
393
+ function sha256Bytes(bytes: Uint8Array): string {
394
+ return createHash("sha256").update(bytes).digest("hex");
395
+ }
396
+
397
+ function verificationIdentities(
398
+ result: DurableChangeVerificationResult,
399
+ verification: StructuredVerificationContract,
400
+ scope: DurableVerificationScope,
401
+ ): string[] {
402
+ // Keep the complete comparison set; only Worker/status summaries are bounded.
403
+ const supplied = result.failureIdentities;
404
+ if (
405
+ Array.isArray(supplied) &&
406
+ supplied.every(
407
+ (identity) => typeof identity === "string" && SHA256.test(identity),
408
+ )
409
+ ) {
410
+ return [...new Set(supplied)].sort();
411
+ }
412
+ if (result.ok) return [];
413
+ return [hash("verification-failure", scope, verification.id, result.code)];
414
+ }
415
+
416
+ function verificationBaselineFact(
417
+ baseline: DurableVerificationBaseline,
418
+ artifacts: ArtifactStore,
419
+ ) {
420
+ // Complete private evidence must not inherit the bounded model projection limits.
421
+ const artifact = artifacts.put(Buffer.from(JSON.stringify(baseline)));
422
+ return {
423
+ revisionId: baseline.revisionId,
424
+ baselineArtifactHash: artifact.hash,
425
+ };
426
+ }
427
+
428
+ function parseVerificationBaseline(
429
+ value: unknown,
430
+ artifacts: ArtifactStore,
431
+ ): DurableVerificationBaseline {
432
+ if (isRecord(value) && typeof value.baselineArtifactHash === "string") {
433
+ const revisionId = value.revisionId;
434
+ value = JSON.parse(
435
+ Buffer.from(artifacts.read(value.baselineArtifactHash)).toString("utf8"),
436
+ );
437
+ if (!isRecord(value) || value.revisionId !== revisionId)
438
+ throw new Error("workflow-verification-baseline-conflict");
439
+ }
440
+ if (
441
+ !isRecord(value) ||
442
+ typeof value.revisionId !== "string" ||
443
+ !SHA256.test(value.revisionId) ||
444
+ !Array.isArray(value.targetContracts) ||
445
+ !Array.isArray(value.affected) ||
446
+ !isRecord(value.fullSuite)
447
+ ) {
448
+ throw new Error("workflow-verification-baseline-invalid");
449
+ }
450
+ const baseline = structuredClone(
451
+ value,
452
+ ) as unknown as DurableVerificationBaseline;
453
+ const validObservation = (observation: DurableVerificationObservation) =>
454
+ (observation.status === "passed" || observation.status === "failed") &&
455
+ typeof observation.verificationId === "string" &&
456
+ IDENTIFIER.test(observation.verificationId) &&
457
+ Array.isArray(observation.failureIdentities) &&
458
+ observation.failureIdentities.every((identity) => SHA256.test(identity)) &&
459
+ (observation.code === undefined || typeof observation.code === "string");
460
+ if (
461
+ !baseline.targetContracts.every(
462
+ (entry) =>
463
+ isRecord(entry) &&
464
+ typeof entry.taskId === "string" &&
465
+ typeof entry.verificationId === "string" &&
466
+ typeof entry.expectedFailure === "string",
467
+ ) ||
468
+ !baseline.affected.every(
469
+ (entry) =>
470
+ isRecord(entry) &&
471
+ typeof entry.taskId === "string" &&
472
+ isRecord(entry.observation) &&
473
+ validObservation(entry.observation),
474
+ ) ||
475
+ !validObservation(baseline.fullSuite)
476
+ ) {
477
+ throw new Error("workflow-verification-baseline-invalid");
478
+ }
479
+ return baseline;
480
+ }
481
+
482
+ function runGitApply(
483
+ root: string,
484
+ diff: Uint8Array,
485
+ checkOnly: boolean,
486
+ signal: AbortSignal,
487
+ ): Promise<"ok" | "failed" | "cancelled"> {
488
+ if (signal.aborted) return Promise.resolve("cancelled");
489
+ return new Promise((resolve) => {
490
+ let settled = false;
491
+ const settle = (value: "ok" | "failed" | "cancelled") => {
492
+ if (settled) return;
493
+ settled = true;
494
+ signal.removeEventListener("abort", abort);
495
+ resolve(value);
496
+ };
497
+ const child = spawn(
498
+ "git",
499
+ [
500
+ "apply",
501
+ ...(checkOnly ? ["--check"] : []),
502
+ "--recount",
503
+ "--whitespace=nowarn",
504
+ "-",
505
+ ],
506
+ { cwd: root, shell: false, stdio: ["pipe", "ignore", "ignore"] },
507
+ );
508
+ const abort = () => {
509
+ child.kill("SIGTERM");
510
+ settle("cancelled");
511
+ };
512
+ signal.addEventListener("abort", abort, { once: true });
513
+ child.once("error", () => settle(signal.aborted ? "cancelled" : "failed"));
514
+ child.once("close", (code) =>
515
+ settle(signal.aborted ? "cancelled" : code === 0 ? "ok" : "failed"),
516
+ );
517
+ child.stdin.on("error", () => undefined);
518
+ child.stdin.end(Buffer.from(diff));
519
+ });
520
+ }
521
+
522
+ function revisionChanges(
523
+ root: string,
524
+ paths: readonly string[],
525
+ ): Record<
526
+ string,
527
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
528
+ > {
529
+ const changes: Record<
530
+ string,
531
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
532
+ > = {};
533
+ for (const relative of [...new Set(paths)].sort()) {
534
+ const observation = observeSafePath(root, relative);
535
+ if (observation.kind === "absent") {
536
+ changes[relative] = { kind: "absent" };
537
+ continue;
538
+ }
539
+ if (observation.kind !== "file") {
540
+ throw new Error("workflow-candidate-path-unsafe");
541
+ }
542
+ const target = path.join(root, ...relative.split("/"));
543
+ const stat = lstatSync(target);
544
+ changes[relative] = {
545
+ kind: "file",
546
+ bytes: readFileSync(target),
547
+ mode: stat.mode & 0o777,
548
+ };
549
+ }
550
+ return changes;
551
+ }
552
+
553
+ class DurableWorkflowComposition
554
+ implements
555
+ WorkflowWorker,
556
+ WorkflowChangeVerifier,
557
+ WorkflowApplication,
558
+ WorkflowRunLifecycle
559
+ {
560
+ readonly #options: DurableWorkflowEngineOptions;
561
+ readonly #routeHealth: DurableRouteHealthStore;
562
+ #broker: RunWorkerBroker;
563
+ readonly #runs = new Map<string, DurableRunResources>();
564
+
565
+ constructor(options: DurableWorkflowEngineOptions) {
566
+ this.#options = options;
567
+ this.#routeHealth = new DurableRouteHealthStore(
568
+ options.stateRoot.databasePath,
569
+ );
570
+ this.#broker = new RunWorkerBroker(options.routePolicy, {
571
+ ...(options.now ? { now: options.now } : {}),
572
+ healthStore: this.#routeHealth,
573
+ });
574
+ }
575
+
576
+ updateRoutePolicy(policy: RoutePolicy): void {
577
+ this.#broker.updatePolicy(policy);
578
+ }
579
+
580
+ async #refreshEnvironment(
581
+ resources: DurableRunResources,
582
+ signal: AbortSignal,
583
+ ): Promise<void> {
584
+ if (!this.#options.verificationEnvironment) return;
585
+ const identity = await this.#options.verificationEnvironment(
586
+ resources.plan,
587
+ signal,
588
+ );
589
+ if (identity !== resources.environmentIdentity) {
590
+ resources.environmentIdentity = identity;
591
+ resources.baselinePromises.clear();
592
+ resources.verificationFact = undefined;
593
+ }
594
+ }
595
+
596
+ async #environmentCurrent(
597
+ runId: string,
598
+ signal: AbortSignal,
599
+ expected?: string,
600
+ ): Promise<boolean> {
601
+ if (!this.#options.verificationEnvironment) return true;
602
+ const resources = this.#runs.get(runId);
603
+ if (!resources) return false;
604
+ try {
605
+ const identity = await this.#options.verificationEnvironment(
606
+ resources.plan,
607
+ signal,
608
+ );
609
+ resources.environmentIdentity ??= identity;
610
+ return identity === (expected ?? resources.environmentIdentity);
611
+ } catch {
612
+ signal.throwIfAborted();
613
+ return false;
614
+ }
615
+ }
616
+
617
+ async #verifyPhase(
618
+ input: Parameters<DurableWorkflowEngineOptions["verifyPhase"]>[0],
619
+ ): Promise<DurablePhaseVerificationResult> {
620
+ if (await this.#environmentCurrent(input.runId, input.signal)) {
621
+ const identity = this.#runs.get(input.runId)?.environmentIdentity;
622
+ const result = await this.#options.verifyPhase(input);
623
+ if (await this.#environmentCurrent(input.runId, input.signal, identity))
624
+ return result;
625
+ }
626
+ return {
627
+ ok: false,
628
+ kind: "paused",
629
+ code: "verification-environment-changed",
630
+ };
631
+ }
632
+
633
+ async #verifyChange(
634
+ input: Parameters<DurableWorkflowEngineOptions["verifyChange"]>[0],
635
+ ): Promise<DurableChangeVerificationResult> {
636
+ if (await this.#environmentCurrent(input.runId, input.signal)) {
637
+ const identity = this.#runs.get(input.runId)?.environmentIdentity;
638
+ const result = await this.#options.verifyChange(input);
639
+ if (await this.#environmentCurrent(input.runId, input.signal, identity))
640
+ return result;
641
+ }
642
+ return {
643
+ ok: false,
644
+ kind: "environment",
645
+ code: "verification-environment-changed",
646
+ };
647
+ }
648
+
649
+ routePolicyStatus(): Record<string, unknown> {
650
+ return this.#broker.status();
651
+ }
652
+
653
+ #recordCorrection(
654
+ ledger: TaskLedger,
655
+ input: {
656
+ runId: string;
657
+ operationId: string;
658
+ taskId: string;
659
+ phase: "red" | "green" | "refactor";
660
+ },
661
+ identity: BeginCandidateInput,
662
+ correction: {
663
+ category: "artifact" | "stale" | "verification" | "checkpoint";
664
+ code: string;
665
+ stage: string;
666
+ },
667
+ ): void {
668
+ ledger.commitVerifiedEvent({
669
+ runId: input.runId,
670
+ taskId: input.taskId,
671
+ eventId: `correction-${hash(
672
+ input.operationId,
673
+ identity.candidateId,
674
+ correction.category,
675
+ correction.code,
676
+ correction.stage,
677
+ ).slice(0, 40)}`,
678
+ kind: "artifact-correction",
679
+ phase: input.phase,
680
+ correctionCategory: correction.category,
681
+ safeFailure: { code: correction.code, stage: correction.stage },
682
+ candidateId: identity.candidateId,
683
+ attemptId: identity.attemptId,
684
+ routeId: identity.routeId,
685
+ routeFingerprint: identity.routeFingerprint,
686
+ });
687
+ }
688
+
689
+ #runRoot(runId: string): string {
690
+ if (!IDENTIFIER.test(runId)) throw new Error("workflow-run-id-invalid");
691
+ const dataRoot = path.join(this.#options.stateRoot.rootDir, "run-data");
692
+ const root = path.join(dataRoot, runId);
693
+ const relative = path.relative(dataRoot, root);
694
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
695
+ throw new Error("workflow-run-data-unsafe");
696
+ }
697
+ return root;
698
+ }
699
+
700
+ #captureBaseline(
701
+ workspaces: WorkspaceStore,
702
+ plan: ImplementPlan,
703
+ signal: AbortSignal,
704
+ ) {
705
+ const candidates = new Set<string>();
706
+ const absent = new Set<string>();
707
+ for (const relative of deliveryBoundPaths(plan)) {
708
+ const observation = observeSafePath(this.#options.consumerRoot, relative);
709
+ if (observation.kind === "file") candidates.add(relative);
710
+ else if (observation.kind === "absent") absent.add(relative);
711
+ else throw new Error("workflow-baseline-path-unsafe");
712
+ }
713
+ return workspaces.captureBaselineAsync(
714
+ {
715
+ consumerRoot: this.#options.consumerRoot,
716
+ approvedUntracked: [...candidates].sort(),
717
+ absent: [...absent].sort(),
718
+ },
719
+ signal,
720
+ );
721
+ }
722
+
723
+ async #verifyPostApply(
724
+ resources: DurableRunResources,
725
+ root: string,
726
+ signal: AbortSignal,
727
+ transactionId: string,
728
+ ): Promise<{ ok: true } | { ok: false; code: string }> {
729
+ if (this.#options.verificationEnvironment) {
730
+ const fact = this.#ledger(
731
+ resources,
732
+ resources.deliveryRevision,
733
+ ).durableFact(`apply-environment-${hash(transactionId).slice(0, 40)}`);
734
+ if (!isRecord(fact) || typeof fact.identity !== "string")
735
+ return { ok: false, code: "verification-environment-changed" };
736
+ resources.environmentIdentity = fact.identity;
737
+ if (
738
+ !(await this.#environmentCurrent(
739
+ resources.runId,
740
+ signal,
741
+ fact.identity,
742
+ ))
743
+ )
744
+ return { ok: false, code: "verification-environment-changed" };
745
+ }
746
+ if (
747
+ resources.plan.outputs.some(
748
+ (output) => observeSafePath(root, output.path).kind !== "file",
749
+ )
750
+ ) {
751
+ return { ok: false, code: "producer-output-unavailable" };
752
+ }
753
+ const lifecycle = hasVerificationLifecycle(resources.plan);
754
+ const result = await this.#verifyChange({
755
+ runId: resources.runId,
756
+ deliveryRevision: resources.deliveryRevision,
757
+ root,
758
+ plan: structuredClone(resources.plan),
759
+ ...(lifecycle
760
+ ? {
761
+ scope: "post-apply" as const,
762
+ verification: structuredClone(
763
+ resources.plan.verification.change.postApply,
764
+ ),
765
+ }
766
+ : {}),
767
+ signal,
768
+ });
769
+ return result.ok &&
770
+ result.exitCode === 0 &&
771
+ result.classification.length > 0
772
+ ? { ok: true }
773
+ : {
774
+ ok: false,
775
+ code: result.ok ? "post-apply-verification-invalid" : result.code,
776
+ };
777
+ }
778
+
779
+ readonly #initializing = new Map<string, Promise<DurableRunResources>>();
780
+
781
+ async #prepareResources(
782
+ input: DurableResourceInput,
783
+ signal: AbortSignal,
784
+ ): Promise<DurableRunResources> {
785
+ signal.throwIfAborted();
786
+ const pending = this.#initializing.get(input.runId);
787
+ if (pending) return pending;
788
+ if (
789
+ this.#runs.has(input.runId) ||
790
+ (input.baselineRevisionId && input.currentWorkspaceRevisionId)
791
+ )
792
+ return this.#resources(input);
793
+ const initialize = async () => {
794
+ if (
795
+ !input.plan ||
796
+ input.baselineRevisionId ||
797
+ input.currentWorkspaceRevisionId
798
+ )
799
+ throw new Error("workflow-workspace-revision-unavailable");
800
+ const root = this.#runRoot(input.runId);
801
+ ensurePrivateRunDirectory(root);
802
+ const artifacts = new ArtifactStore(path.join(root, "artifacts"));
803
+ const workspaces = new WorkspaceStore(
804
+ path.join(root, "workspaces"),
805
+ artifacts,
806
+ );
807
+ const baseline = await this.#captureBaseline(
808
+ workspaces,
809
+ input.plan,
810
+ signal,
811
+ );
812
+ signal.throwIfAborted();
813
+ return this.#resources({
814
+ ...input,
815
+ baselineRevisionId: baseline.revisionId,
816
+ currentWorkspaceRevisionId: baseline.revisionId,
817
+ });
818
+ };
819
+ const promise = initialize();
820
+ this.#initializing.set(input.runId, promise);
821
+ try {
822
+ return await promise;
823
+ } finally {
824
+ this.#initializing.delete(input.runId);
825
+ }
826
+ }
827
+
828
+ #resources(input: {
829
+ runId: string;
830
+ deliveryRevision: number;
831
+ plan?: ImplementPlan;
832
+ baselineRevisionId?: string;
833
+ currentWorkspaceRevisionId?: string;
834
+ }): DurableRunResources {
835
+ const existing = this.#runs.get(input.runId);
836
+ if (existing) {
837
+ if (
838
+ input.baselineRevisionId &&
839
+ existing.baselineRevisionId !== input.baselineRevisionId
840
+ ) {
841
+ throw new Error("workflow-baseline-revision-conflict");
842
+ }
843
+ if (input.plan) {
844
+ existing.plan = structuredClone(input.plan);
845
+ existing.deliveryRevision = input.deliveryRevision;
846
+ }
847
+ return existing;
848
+ }
849
+ if (!input.plan) {
850
+ throw new Error("workflow-plan-unavailable");
851
+ }
852
+ const root = this.#runRoot(input.runId);
853
+ ensurePrivateRunDirectory(root);
854
+ const artifacts = new ArtifactStore(path.join(root, "artifacts"));
855
+ const workspaces = new WorkspaceStore(
856
+ path.join(root, "workspaces"),
857
+ artifacts,
858
+ );
859
+ const baselineRevisionId = input.baselineRevisionId;
860
+ const currentRevisionId = input.currentWorkspaceRevisionId;
861
+ if (!baselineRevisionId || !currentRevisionId)
862
+ throw new Error("workflow-workspace-revision-unavailable");
863
+ workspaces.getRevision(baselineRevisionId);
864
+ workspaces.getRevision(currentRevisionId);
865
+ let resources!: DurableRunResources;
866
+ const transactions = new ApplyTransaction({
867
+ root: path.join(root, "transactions"),
868
+ artifacts,
869
+ workspaces,
870
+ hooks: {
871
+ postApply: ({ transactionId, root: verificationRoot, signal }) =>
872
+ this.#verifyPostApply(
873
+ resources,
874
+ verificationRoot,
875
+ signal,
876
+ transactionId,
877
+ ),
878
+ },
879
+ });
880
+ resources = {
881
+ runId: input.runId,
882
+ root,
883
+ plan: structuredClone(input.plan),
884
+ deliveryRevision: input.deliveryRevision,
885
+ artifacts,
886
+ workspaces,
887
+ ledgers: new Map(),
888
+ transactions,
889
+ baselineRevisionId,
890
+ currentRevisionId,
891
+ mergeTail: Promise.resolve(),
892
+ baselinePromises: new Map(),
893
+ closed: false,
894
+ };
895
+ this.#runs.set(input.runId, resources);
896
+ return resources;
897
+ }
898
+
899
+ #closeResources(resources: DurableRunResources): void {
900
+ if (resources.closed) return;
901
+ for (const ledger of resources.ledgers.values()) ledger.close();
902
+ resources.ledgers.clear();
903
+ resources.transactions.close();
904
+ resources.closed = true;
905
+ }
906
+
907
+ #ledger(
908
+ resources: DurableRunResources,
909
+ deliveryRevision: number,
910
+ ): TaskLedger {
911
+ const existing = resources.ledgers.get(deliveryRevision);
912
+ if (existing) return existing;
913
+ const ledger = new TaskLedger({
914
+ root: path.join(
915
+ resources.root,
916
+ "ledgers",
917
+ `revision-${deliveryRevision}`,
918
+ ),
919
+ artifacts: resources.artifacts,
920
+ });
921
+ resources.ledgers.set(deliveryRevision, ledger);
922
+ return ledger;
923
+ }
924
+
925
+ async #observeVerification(input: {
926
+ resources: DurableRunResources;
927
+ revisionId: string;
928
+ scope: DurableVerificationScope;
929
+ verification: StructuredVerificationContract;
930
+ signal: AbortSignal;
931
+ taskId?: string;
932
+ }): Promise<DurableObservedVerification> {
933
+ if (input.signal.aborted) {
934
+ return {
935
+ ok: false,
936
+ outcome: { kind: "operation-cancelled", code: "cancelled" },
937
+ };
938
+ }
939
+ const root = mkdtempSync(path.join(input.resources.root, "verification-"));
940
+ try {
941
+ await input.resources.workspaces.materializeAsync(
942
+ input.revisionId,
943
+ root,
944
+ input.signal,
945
+ );
946
+ const result = await this.#verifyChange({
947
+ runId: input.resources.runId,
948
+ deliveryRevision: input.resources.deliveryRevision,
949
+ root,
950
+ plan: structuredClone(input.resources.plan),
951
+ scope: input.scope,
952
+ verification: structuredClone(input.verification),
953
+ ...(input.taskId ? { taskId: input.taskId } : {}),
954
+ signal: input.signal,
955
+ });
956
+ if (result.ok) {
957
+ return {
958
+ ok: true,
959
+ observation: {
960
+ status: "passed",
961
+ verificationId: input.verification.id,
962
+ failureIdentities: verificationIdentities(
963
+ result,
964
+ input.verification,
965
+ input.scope,
966
+ ),
967
+ },
968
+ };
969
+ }
970
+ if (result.kind === "verification") {
971
+ return {
972
+ ok: true,
973
+ observation: {
974
+ status: "failed",
975
+ verificationId: input.verification.id,
976
+ failureIdentities: verificationIdentities(
977
+ result,
978
+ input.verification,
979
+ input.scope,
980
+ ),
981
+ code: result.code,
982
+ },
983
+ };
984
+ }
985
+ if (result.kind === "approval-boundary") {
986
+ return {
987
+ ok: false,
988
+ outcome: { kind: "approval-needed", code: result.code },
989
+ };
990
+ }
991
+ if (result.kind === "cancelled" || input.signal.aborted) {
992
+ return {
993
+ ok: false,
994
+ outcome: { kind: "operation-cancelled", code: "cancelled" },
995
+ };
996
+ }
997
+ return {
998
+ ok: false,
999
+ outcome: { kind: "paused", code: result.code },
1000
+ };
1001
+ } finally {
1002
+ rmSync(root, { recursive: true, force: true });
1003
+ }
1004
+ }
1005
+
1006
+ async #captureVerificationBaseline(input: {
1007
+ resources: DurableRunResources;
1008
+ ledger: TaskLedger;
1009
+ signal: AbortSignal;
1010
+ }): Promise<DurableBaselineResult> {
1011
+ const environmentIdentity = input.resources.environmentIdentity;
1012
+ const stored = input.ledger.durableFact(
1013
+ this.#baselineFactKey(input.resources),
1014
+ );
1015
+ if (stored !== undefined) {
1016
+ const baseline = parseVerificationBaseline(
1017
+ stored,
1018
+ input.resources.artifacts,
1019
+ );
1020
+ if (baseline.revisionId !== input.resources.baselineRevisionId) {
1021
+ throw new Error("workflow-verification-baseline-conflict");
1022
+ }
1023
+ return { ok: true, baseline };
1024
+ }
1025
+ const plan = input.resources.plan;
1026
+ const affected: DurableVerificationBaseline["affected"] = [];
1027
+ for (const task of plan.tasks) {
1028
+ const observed = await this.#observeVerification({
1029
+ resources: input.resources,
1030
+ revisionId: input.resources.baselineRevisionId,
1031
+ scope: "baseline-task-affected",
1032
+ verification: task.affectedVerification,
1033
+ taskId: task.taskId,
1034
+ signal: input.signal,
1035
+ });
1036
+ if (!observed.ok) return observed;
1037
+ affected.push({
1038
+ taskId: task.taskId,
1039
+ observation: observed.observation,
1040
+ });
1041
+ }
1042
+ const fullSuite = await this.#observeVerification({
1043
+ resources: input.resources,
1044
+ revisionId: input.resources.baselineRevisionId,
1045
+ scope: "baseline-full-suite",
1046
+ verification: plan.verification.baseline.fullSuite,
1047
+ signal: input.signal,
1048
+ });
1049
+ if (!fullSuite.ok) return fullSuite;
1050
+ const baseline: DurableVerificationBaseline = {
1051
+ revisionId: input.resources.baselineRevisionId,
1052
+ targetContracts: plan.tasks
1053
+ .filter(
1054
+ (task) =>
1055
+ !task.verificationMode || task.verificationMode === "behavior",
1056
+ )
1057
+ .map((task) => ({
1058
+ taskId: task.taskId,
1059
+ verificationId: task.phases.red.verification.id,
1060
+ expectedFailure:
1061
+ "expectedFailure" in task.phases.red.verification
1062
+ ? (task.phases.red.verification.expectedFailure ??
1063
+ task.phases.red.verification.id)
1064
+ : task.phases.red.verification.id,
1065
+ })),
1066
+ affected,
1067
+ fullSuite: fullSuite.observation,
1068
+ };
1069
+ if (environmentIdentity !== input.resources.environmentIdentity)
1070
+ return {
1071
+ ok: false,
1072
+ outcome: { kind: "paused", code: "verification-environment-changed" },
1073
+ };
1074
+ input.ledger.putDurableFact(
1075
+ this.#baselineFactKey(input.resources),
1076
+ verificationBaselineFact(baseline, input.resources.artifacts),
1077
+ );
1078
+ return { ok: true, baseline };
1079
+ }
1080
+
1081
+ async #ensureVerificationBaseline(input: {
1082
+ resources: DurableRunResources;
1083
+ ledger: TaskLedger;
1084
+ signal: AbortSignal;
1085
+ }): Promise<DurableBaselineResult> {
1086
+ try {
1087
+ await this.#refreshEnvironment(input.resources, input.signal);
1088
+ } catch {
1089
+ return {
1090
+ ok: false,
1091
+ outcome: {
1092
+ kind: "paused",
1093
+ code: "verification-environment-unavailable",
1094
+ },
1095
+ };
1096
+ }
1097
+ const existing = input.resources.baselinePromises.get(
1098
+ input.resources.deliveryRevision,
1099
+ );
1100
+ if (existing) return existing;
1101
+ const pending = this.#captureVerificationBaseline(input).then((result) => {
1102
+ if (!result.ok) {
1103
+ input.resources.baselinePromises.delete(
1104
+ input.resources.deliveryRevision,
1105
+ );
1106
+ }
1107
+ return result;
1108
+ });
1109
+ input.resources.baselinePromises.set(
1110
+ input.resources.deliveryRevision,
1111
+ pending,
1112
+ );
1113
+ return pending;
1114
+ }
1115
+
1116
+ async #verifyTaskAffected(input: {
1117
+ resources: DurableRunResources;
1118
+ baseline: DurableVerificationBaseline;
1119
+ task: PlanTaskDraft;
1120
+ revisionId: string;
1121
+ signal: AbortSignal;
1122
+ }): Promise<DurableAffectedResult> {
1123
+ const baseline = input.baseline.affected.find(
1124
+ (entry) => entry.taskId === input.task.taskId,
1125
+ );
1126
+ if (!baseline) throw new Error("workflow-task-baseline-unavailable");
1127
+ const current = await this.#observeVerification({
1128
+ resources: input.resources,
1129
+ revisionId: input.revisionId,
1130
+ scope: "task-affected",
1131
+ verification: input.task.affectedVerification,
1132
+ taskId: input.task.taskId,
1133
+ signal: input.signal,
1134
+ });
1135
+ if (!current.ok) return current.outcome;
1136
+ if (current.observation.status === "passed") {
1137
+ return { kind: "verified", attribution: "none" };
1138
+ }
1139
+ const prior = new Set(baseline.observation.failureIdentities);
1140
+ const introduced = current.observation.failureIdentities.filter(
1141
+ (identity) => !prior.has(identity),
1142
+ );
1143
+ if (introduced.length === 0) {
1144
+ return { kind: "verified", attribution: "pre-existing" };
1145
+ }
1146
+ return {
1147
+ kind: "repairable",
1148
+ attribution: "introduced",
1149
+ failureIdentities: introduced.slice(0, 256),
1150
+ };
1151
+ }
1152
+
1153
+ #completeTracking(
1154
+ resources: DurableRunResources,
1155
+ taskId: string,
1156
+ ): WorkspaceRevision {
1157
+ const current = resources.workspaces.getRevision(
1158
+ resources.currentRevisionId,
1159
+ );
1160
+ const trackingPath = deliveryTrackingPath(resources.plan);
1161
+ if (!trackingPath) return current;
1162
+ const entry = current.entries[trackingPath];
1163
+ if (!entry || entry.kind === "absent") return current;
1164
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(
1165
+ resources.artifacts.read(entry.hash),
1166
+ );
1167
+ const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1168
+ const taskIdentity = new RegExp(
1169
+ `(?:^|[^A-Za-z0-9._:-])${escaped}(?![A-Za-z0-9._:-])`,
1170
+ "u",
1171
+ );
1172
+ let matches = 0;
1173
+ let changed = false;
1174
+ const updated = text
1175
+ .split(/(?<=\n)/u)
1176
+ .map((segment) => {
1177
+ const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
1178
+ if (!/^\s*-\s+\[[ xX]\]/u.test(line) || !taskIdentity.test(line)) {
1179
+ return segment;
1180
+ }
1181
+ matches += 1;
1182
+ if (/^\s*-\s+\[[xX]\]/u.test(line)) return segment;
1183
+ changed = true;
1184
+ return segment.replace(/^(\s*-\s+)\[ \]/u, "$1[x]");
1185
+ })
1186
+ .join("");
1187
+ if (matches !== 1) throw new Error("workflow-tracking-task-mismatch");
1188
+ if (!changed) return current;
1189
+ const next = resources.workspaces.createRevision({
1190
+ parentRevisionId: current.revisionId,
1191
+ changes: {
1192
+ [trackingPath]: {
1193
+ kind: "file",
1194
+ bytes: new TextEncoder().encode(updated),
1195
+ mode: entry.mode,
1196
+ },
1197
+ },
1198
+ });
1199
+ resources.currentRevisionId = next.revisionId;
1200
+ return next;
1201
+ }
1202
+
1203
+ #agentsManagedParts(
1204
+ text: string,
1205
+ ): { prefix: string; suffix: string } | undefined {
1206
+ const startMarker = "<!-- ABEL:AGENTS-INDEX:START -->";
1207
+ const endMarker = "<!-- ABEL:AGENTS-INDEX:END -->";
1208
+ const lines = text.split("\n");
1209
+ const starts: Array<{ line: number; start: number }> = [];
1210
+ const ends: Array<{ line: number; end: number }> = [];
1211
+ let offset = 0;
1212
+ for (const [line, value] of lines.entries()) {
1213
+ if (value === startMarker) starts.push({ line, start: offset });
1214
+ if (value === endMarker) {
1215
+ ends.push({ line, end: offset + value.length });
1216
+ }
1217
+ offset += value.length + 1;
1218
+ }
1219
+ if (
1220
+ starts.length !== 1 ||
1221
+ ends.length !== 1 ||
1222
+ starts[0].line >= ends[0].line
1223
+ ) {
1224
+ return undefined;
1225
+ }
1226
+ return {
1227
+ prefix: text.slice(0, starts[0].start),
1228
+ suffix: text.slice(ends[0].end),
1229
+ };
1230
+ }
1231
+
1232
+ #applyAgentsCheckpoint(
1233
+ resources: DurableRunResources,
1234
+ ): { ok: true; revision: WorkspaceRevision } | { ok: false; code: string } {
1235
+ const checkpoint = resources.plan.verification.agentsCheckpoint;
1236
+ const current = resources.workspaces.getRevision(
1237
+ resources.currentRevisionId,
1238
+ );
1239
+ if (!checkpoint.required) return { ok: true, revision: current };
1240
+ const changes: Record<
1241
+ string,
1242
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1243
+ > = {};
1244
+ for (const operation of checkpoint.operations) {
1245
+ const entry = current.entries[operation.target];
1246
+ let before: string | null;
1247
+ if (!entry || entry.kind === "absent") {
1248
+ before = null;
1249
+ } else {
1250
+ try {
1251
+ before = new TextDecoder("utf-8", { fatal: true }).decode(
1252
+ resources.artifacts.read(entry.hash),
1253
+ );
1254
+ } catch {
1255
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
1256
+ }
1257
+ }
1258
+ if (operation.impact === "create-index") {
1259
+ if (operation.managedBlock === null) {
1260
+ throw new Error("workflow-agents-checkpoint-invalid");
1261
+ }
1262
+ const desired = `${operation.managedBlock}\n`;
1263
+ if (before !== null && before !== desired) {
1264
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
1265
+ }
1266
+ if (before === null) {
1267
+ changes[operation.target] = {
1268
+ kind: "file",
1269
+ bytes: new TextEncoder().encode(desired),
1270
+ mode: 0o644,
1271
+ };
1272
+ }
1273
+ continue;
1274
+ }
1275
+ if (operation.impact === "remove-index") {
1276
+ if (operation.managedBlock !== null) {
1277
+ throw new Error("workflow-agents-checkpoint-invalid");
1278
+ }
1279
+ if (before === null) continue;
1280
+ const parts = this.#agentsManagedParts(before);
1281
+ if (!parts) {
1282
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
1283
+ }
1284
+ const after = `${parts.prefix}${parts.suffix}`;
1285
+ changes[operation.target] =
1286
+ after.trim().length === 0
1287
+ ? { kind: "absent" }
1288
+ : {
1289
+ kind: "file",
1290
+ bytes: new TextEncoder().encode(after),
1291
+ mode: entry?.kind === "file" ? entry.mode : 0o644,
1292
+ };
1293
+ continue;
1294
+ }
1295
+ if (operation.managedBlock === null || before === null) {
1296
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
1297
+ }
1298
+ const parts = this.#agentsManagedParts(before);
1299
+ if (!parts) {
1300
+ return { ok: false, code: "agents-checkpoint-contract-stale" };
1301
+ }
1302
+ const after = `${parts.prefix}${operation.managedBlock}${parts.suffix}`;
1303
+ if (after !== before) {
1304
+ changes[operation.target] = {
1305
+ kind: "file",
1306
+ bytes: new TextEncoder().encode(after),
1307
+ mode: entry?.kind === "file" ? entry.mode : 0o644,
1308
+ };
1309
+ }
1310
+ }
1311
+ if (Object.keys(changes).length === 0) {
1312
+ return { ok: true, revision: current };
1313
+ }
1314
+ return {
1315
+ ok: true,
1316
+ revision: resources.workspaces.createRevision({
1317
+ parentRevisionId: current.revisionId,
1318
+ changes,
1319
+ }),
1320
+ };
1321
+ }
1322
+
1323
+ #openTask(
1324
+ resources: DurableRunResources,
1325
+ ledger: TaskLedger,
1326
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1327
+ ): void {
1328
+ const revision = resources.workspaces.getRevision(
1329
+ resources.baselineRevisionId,
1330
+ );
1331
+ const contextRefs = [...new Set(input.task.phases.red.read)]
1332
+ .sort()
1333
+ .flatMap((relative) => {
1334
+ const entry = revision.entries[relative];
1335
+ return entry?.kind === "file"
1336
+ ? [{ kind: "path" as const, path: relative, hash: entry.hash }]
1337
+ : [];
1338
+ });
1339
+ ledger.openTask({
1340
+ runId: input.runId,
1341
+ deliveryRevision: input.deliveryRevision,
1342
+ taskId: input.taskId,
1343
+ boundaryHash: hash("durable-task-boundary", JSON.stringify(input.task)),
1344
+ objective: input.task.objective,
1345
+ contextRefs,
1346
+ initialPhase:
1347
+ input.task.verificationMode &&
1348
+ input.task.verificationMode !== "behavior"
1349
+ ? "green"
1350
+ : "red",
1351
+ });
1352
+ }
1353
+
1354
+ #executionWritePaths(resources: DurableRunResources): string[] {
1355
+ return [
1356
+ ...new Set(
1357
+ resources.plan.tasks.flatMap((task) =>
1358
+ Object.values(task.phases).flatMap((phase) => [
1359
+ ...phase.write,
1360
+ ...phase.delete,
1361
+ ]),
1362
+ ),
1363
+ ),
1364
+ ];
1365
+ }
1366
+
1367
+ #baselineFactKey(resources: DurableRunResources): string {
1368
+ return this.#options.verificationPolicy || resources.environmentIdentity
1369
+ ? `verification-baseline-${hash(this.#options.verificationPolicy ?? "legacy", resources.environmentIdentity ?? "legacy").slice(0, 24)}`
1370
+ : "verification-baseline";
1371
+ }
1372
+
1373
+ #policyFactKey(
1374
+ resources: DurableRunResources,
1375
+ event: Record<string, unknown>,
1376
+ ): string {
1377
+ return `verified-policy-${hash(this.#options.verificationPolicy ?? "legacy", resources.environmentIdentity ?? "legacy", JSON.stringify({ kind: event.kind, phase: event.phase, commandId: event.commandId, artifactHash: event.artifactHash, isolatedRevisionId: event.isolatedRevisionId, exitCode: event.exitCode, actualClassification: event.actualClassification })).slice(0, 40)}`;
1378
+ }
1379
+
1380
+ async #revalidatePhasePolicy(
1381
+ resources: DurableRunResources,
1382
+ ledger: TaskLedger,
1383
+ task: PlanTaskDraft,
1384
+ signal: AbortSignal,
1385
+ ): Promise<WorkflowAttemptOutcome | undefined> {
1386
+ if (
1387
+ !this.#options.verificationPolicy &&
1388
+ !this.#options.verificationEnvironment
1389
+ )
1390
+ return undefined;
1391
+ const projection = ledger.projection({
1392
+ runId: resources.runId,
1393
+ taskId: task.taskId,
1394
+ nextPhase: "green",
1395
+ }) as { history: Array<Record<string, unknown>> };
1396
+ for (const event of projection.history.filter(
1397
+ (event) =>
1398
+ event.kind === "phase-verified" || event.kind === "repair-verified",
1399
+ )) {
1400
+ const key = this.#policyFactKey(resources, event);
1401
+ if (ledger.durableFact(key) !== undefined) continue;
1402
+ const phase = event.phase as "red" | "green" | "refactor";
1403
+ const boundary = task.phases[phase];
1404
+ const repair = event.kind === "repair-verified";
1405
+ const verification = repair
1406
+ ? task.repairVerification
1407
+ : boundary?.verification;
1408
+ const expected = repair
1409
+ ? "expected-green"
1410
+ : expectedClassification(phase);
1411
+ if (
1412
+ !boundary ||
1413
+ !verification ||
1414
+ typeof event.isolatedRevisionId !== "string"
1415
+ )
1416
+ throw new Error("workflow-ledger-phase-invalid");
1417
+ const root = mkdtempSync(
1418
+ path.join(resources.root, "policy-verification-"),
1419
+ );
1420
+ try {
1421
+ await resources.workspaces.materializeAsync(
1422
+ event.isolatedRevisionId,
1423
+ root,
1424
+ signal,
1425
+ );
1426
+ const result = await this.#verifyPhase({
1427
+ executionWritePaths: this.#executionWritePaths(resources),
1428
+ runId: resources.runId,
1429
+ deliveryRevision: resources.deliveryRevision,
1430
+ taskId: task.taskId,
1431
+ phase,
1432
+ root,
1433
+ verification,
1434
+ signal,
1435
+ });
1436
+ if (!result.ok)
1437
+ return {
1438
+ kind: "paused",
1439
+ code:
1440
+ result.kind === "paused"
1441
+ ? result.code
1442
+ : "verification-policy-rejected",
1443
+ };
1444
+ if (
1445
+ result.classification !== expected ||
1446
+ (expected === "expected-red"
1447
+ ? result.exitCode === 0
1448
+ : result.exitCode !== 0)
1449
+ )
1450
+ return { kind: "paused", code: "verification-policy-rejected" };
1451
+ ledger.putDurableFact(key, {
1452
+ policy: this.#options.verificationPolicy ?? "legacy",
1453
+ });
1454
+ } finally {
1455
+ rmSync(root, { recursive: true, force: true });
1456
+ }
1457
+ }
1458
+ return undefined;
1459
+ }
1460
+
1461
+ #pendingCandidate(
1462
+ ledger: TaskLedger,
1463
+ key: string,
1464
+ baseRevisionId: string,
1465
+ ):
1466
+ | {
1467
+ identity: BeginCandidateInput;
1468
+ proposal: Extract<
1469
+ DurableCandidateProposal,
1470
+ { kind: "sealed-candidate" }
1471
+ >;
1472
+ }
1473
+ | undefined {
1474
+ const value = ledger.durableFact(key);
1475
+ if (
1476
+ !isRecord(value) ||
1477
+ !isRecord(value.identity) ||
1478
+ !isRecord(value.proposal) ||
1479
+ value.identity.isolatedRevisionId !== baseRevisionId
1480
+ )
1481
+ return undefined;
1482
+ return value as unknown as {
1483
+ identity: BeginCandidateInput;
1484
+ proposal: Extract<DurableCandidateProposal, { kind: "sealed-candidate" }>;
1485
+ };
1486
+ }
1487
+
1488
+ #savePendingCandidate(ledger: TaskLedger, key: string, value: unknown): void {
1489
+ const previous = ledger.durableFact(key);
1490
+ if (previous === undefined) ledger.putDurableFact(key, value);
1491
+ else ledger.replaceDurableFact(key, previous, value);
1492
+ }
1493
+
1494
+ #replayedPhase(
1495
+ resources: DurableRunResources,
1496
+ ledger: TaskLedger,
1497
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1498
+ ): WorkflowAttemptOutcome | undefined {
1499
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
1500
+ return undefined;
1501
+ }
1502
+ const projection = ledger.projection({
1503
+ runId: input.runId,
1504
+ taskId: input.taskId,
1505
+ nextPhase: input.phase,
1506
+ }) as {
1507
+ history: Array<Record<string, unknown>>;
1508
+ };
1509
+ const event = projection.history.find(
1510
+ (candidate) =>
1511
+ candidate.kind === "phase-verified" && candidate.phase === input.phase,
1512
+ );
1513
+ if (!event) return undefined;
1514
+ if (
1515
+ typeof event.artifactHash !== "string" ||
1516
+ !SHA256.test(event.artifactHash) ||
1517
+ typeof event.isolatedRevisionId !== "string" ||
1518
+ !SHA256.test(event.isolatedRevisionId) ||
1519
+ typeof event.exitCode !== "number" ||
1520
+ event.actualClassification !== expectedClassification(input.phase)
1521
+ ) {
1522
+ throw new Error("workflow-ledger-phase-invalid");
1523
+ }
1524
+ let isolatedRevisionId = event.isolatedRevisionId;
1525
+ const finalPhase = input.task.phases.refactor ? "refactor" : "green";
1526
+ if (hasVerificationLifecycle(input.plan) && input.phase === finalPhase) {
1527
+ const fact = ledger.durableFact(
1528
+ `task-final-${hash(input.taskId).slice(0, 40)}`,
1529
+ );
1530
+ if (
1531
+ !isRecord(fact) ||
1532
+ fact.taskId !== input.taskId ||
1533
+ fact.phase !== input.phase ||
1534
+ typeof fact.isolatedRevisionId !== "string" ||
1535
+ !SHA256.test(fact.isolatedRevisionId)
1536
+ ) {
1537
+ throw new Error("workflow-task-completion-fact-invalid");
1538
+ }
1539
+ isolatedRevisionId = fact.isolatedRevisionId;
1540
+ }
1541
+ resources.workspaces.getRevision(isolatedRevisionId);
1542
+ if (
1543
+ this.#revisionDescendsFrom(
1544
+ resources,
1545
+ resources.currentRevisionId,
1546
+ isolatedRevisionId,
1547
+ )
1548
+ ) {
1549
+ isolatedRevisionId = resources.currentRevisionId;
1550
+ } else if (
1551
+ !this.#revisionDescendsFrom(
1552
+ resources,
1553
+ isolatedRevisionId,
1554
+ resources.currentRevisionId,
1555
+ )
1556
+ ) {
1557
+ throw new Error("workspace-revision-stale");
1558
+ }
1559
+ resources.currentRevisionId = isolatedRevisionId;
1560
+ return {
1561
+ kind: "phase-committed",
1562
+ artifactHash: event.artifactHash,
1563
+ isolatedRevisionId,
1564
+ exitCode: event.exitCode,
1565
+ classification: expectedClassification(input.phase),
1566
+ baselineRevisionId: resources.baselineRevisionId,
1567
+ };
1568
+ }
1569
+
1570
+ #expandedBaseline(
1571
+ resources: DurableRunResources,
1572
+ plan: ImplementPlan,
1573
+ ): WorkspaceRevision {
1574
+ const baseline = resources.workspaces.getRevision(
1575
+ resources.baselineRevisionId,
1576
+ );
1577
+ const candidates = new Map<string, boolean>(
1578
+ deliveryBoundPaths(plan).map((relative) => [relative, false]),
1579
+ );
1580
+ for (const task of plan.tasks) {
1581
+ for (const phase of Object.values(task.phases)) {
1582
+ for (const relative of phase.read) candidates.set(relative, false);
1583
+ for (const relative of [...phase.write, ...phase.delete]) {
1584
+ candidates.set(relative, true);
1585
+ }
1586
+ }
1587
+ }
1588
+ for (const output of plan.outputs) candidates.set(output.path, true);
1589
+ for (const task of plan.tasks) {
1590
+ if (task.agents.impact === "none" || !task.agents.target) continue;
1591
+ candidates.set(task.agents.target, task.agents.impact === "create-index");
1592
+ }
1593
+ const trackingPath = deliveryTrackingPath(plan);
1594
+ if (
1595
+ trackingPath &&
1596
+ (Object.hasOwn(baseline.entries, trackingPath) ||
1597
+ observeSafePath(this.#options.consumerRoot, trackingPath).kind ===
1598
+ "file")
1599
+ ) {
1600
+ candidates.set(trackingPath, false);
1601
+ }
1602
+ const changes: Record<
1603
+ string,
1604
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1605
+ > = {};
1606
+ for (const [relative, mayBeAbsent] of [...candidates].sort(
1607
+ ([left], [right]) => compareCanonicalStrings(left, right),
1608
+ )) {
1609
+ if (relative === ".") continue;
1610
+ const observation = observeSafePath(this.#options.consumerRoot, relative);
1611
+ if (observation.kind === "absent") {
1612
+ if (baseline.entries[relative]?.kind === "absent") continue;
1613
+ if (!mayBeAbsent)
1614
+ throw new Error("workflow-baseline-input-unavailable");
1615
+ changes[relative] = { kind: "absent" };
1616
+ continue;
1617
+ }
1618
+ if (observation.kind !== "file") {
1619
+ throw new Error("workflow-baseline-path-unsafe");
1620
+ }
1621
+ const target = path.join(
1622
+ this.#options.consumerRoot,
1623
+ ...relative.split("/"),
1624
+ );
1625
+ const stat = lstatSync(target);
1626
+ const bytes = readFileSync(target);
1627
+ const mode = stat.mode & 0o777;
1628
+ const prior = baseline.entries[relative];
1629
+ if (
1630
+ prior?.kind === "file" &&
1631
+ prior.hash === sha256Bytes(bytes) &&
1632
+ prior.bytes === bytes.byteLength &&
1633
+ prior.mode === mode
1634
+ ) {
1635
+ continue;
1636
+ }
1637
+ changes[relative] = {
1638
+ kind: "file",
1639
+ bytes,
1640
+ mode,
1641
+ };
1642
+ }
1643
+ return Object.keys(changes).length === 0
1644
+ ? baseline
1645
+ : resources.workspaces.createRevision({
1646
+ parentRevisionId: baseline.revisionId,
1647
+ changes,
1648
+ });
1649
+ }
1650
+
1651
+ #orderedTasks(plan: ImplementPlan): PlanTaskDraft[] {
1652
+ const remaining = new Map(plan.tasks.map((task) => [task.taskId, task]));
1653
+ const ordered: PlanTaskDraft[] = [];
1654
+ const admitted = new Set<string>();
1655
+ while (remaining.size > 0) {
1656
+ let progressed = false;
1657
+ for (const task of plan.tasks) {
1658
+ if (
1659
+ !remaining.has(task.taskId) ||
1660
+ task.dependsOn.some((dependency) => !admitted.has(dependency))
1661
+ ) {
1662
+ continue;
1663
+ }
1664
+ remaining.delete(task.taskId);
1665
+ admitted.add(task.taskId);
1666
+ ordered.push(task);
1667
+ progressed = true;
1668
+ }
1669
+ if (!progressed) throw new Error("workflow-plan-dependency-cycle");
1670
+ }
1671
+ return ordered;
1672
+ }
1673
+
1674
+ #phaseFacts(
1675
+ resources: DurableRunResources,
1676
+ task: {
1677
+ taskId: string;
1678
+ deliveryRevision: number;
1679
+ phase: "red" | "green" | "refactor";
1680
+ },
1681
+ ): DurablePhaseFact[] {
1682
+ let projection: unknown;
1683
+ try {
1684
+ projection = this.#ledger(resources, task.deliveryRevision).projection({
1685
+ runId: resources.runId,
1686
+ taskId: task.taskId,
1687
+ nextPhase: task.phase,
1688
+ });
1689
+ } catch (error) {
1690
+ if (
1691
+ error instanceof Error &&
1692
+ error.message === "ledger-task-unavailable"
1693
+ ) {
1694
+ return [];
1695
+ }
1696
+ throw error;
1697
+ }
1698
+ if (!isRecord(projection) || !Array.isArray(projection.history)) {
1699
+ throw new Error("workflow-ledger-projection-invalid");
1700
+ }
1701
+ return projection.history.flatMap((event): DurablePhaseFact[] => {
1702
+ if (
1703
+ !isRecord(event) ||
1704
+ (event.kind !== "phase-verified" && event.kind !== "repair-verified")
1705
+ ) {
1706
+ return [];
1707
+ }
1708
+ if (
1709
+ !["red", "green", "refactor"].includes(String(event.phase)) ||
1710
+ typeof event.isolatedRevisionId !== "string" ||
1711
+ !SHA256.test(event.isolatedRevisionId) ||
1712
+ !Array.isArray(event.outputFacts)
1713
+ ) {
1714
+ throw new Error("workflow-ledger-phase-invalid");
1715
+ }
1716
+ const outputFacts = event.outputFacts.map((fact) => {
1717
+ if (
1718
+ !isRecord(fact) ||
1719
+ typeof fact.path !== "string" ||
1720
+ (fact.kind !== "absent" && fact.kind !== "file")
1721
+ ) {
1722
+ throw new Error("workflow-ledger-phase-invalid");
1723
+ }
1724
+ if (fact.kind === "absent") {
1725
+ return { path: fact.path, kind: "absent" as const };
1726
+ }
1727
+ if (
1728
+ typeof fact.hash !== "string" ||
1729
+ !SHA256.test(fact.hash) ||
1730
+ !Number.isSafeInteger(fact.bytes) ||
1731
+ (fact.bytes as number) < 0
1732
+ ) {
1733
+ throw new Error("workflow-ledger-phase-invalid");
1734
+ }
1735
+ return {
1736
+ path: fact.path,
1737
+ kind: "file" as const,
1738
+ hash: fact.hash,
1739
+ bytes: fact.bytes as number,
1740
+ };
1741
+ });
1742
+ return [
1743
+ {
1744
+ kind: event.kind,
1745
+ phase: event.phase as DurablePhaseFact["phase"],
1746
+ isolatedRevisionId: event.isolatedRevisionId,
1747
+ outputFacts,
1748
+ },
1749
+ ];
1750
+ });
1751
+ }
1752
+
1753
+ revalidateDelivery(
1754
+ input: Parameters<NonNullable<WorkflowWorker["revalidateDelivery"]>>[0],
1755
+ ): ReturnType<NonNullable<WorkflowWorker["revalidateDelivery"]>> {
1756
+ const resources = this.#resources({
1757
+ runId: input.runId,
1758
+ deliveryRevision: input.deliveryRevision,
1759
+ plan: input.plan,
1760
+ baselineRevisionId: input.baselineRevisionId,
1761
+ currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
1762
+ });
1763
+ const expandedBaseline = this.#expandedBaseline(resources, input.plan);
1764
+ const rows = new Map(input.tasks.map((task) => [task.taskId, task]));
1765
+ const facts = new Map(
1766
+ input.tasks.map((task) => [
1767
+ task.taskId,
1768
+ this.#phaseFacts(resources, task),
1769
+ ]),
1770
+ );
1771
+ const invalidated = new Set(input.invalidatedTaskIds);
1772
+ const ordered = this.#orderedTasks(input.plan);
1773
+ let rebuilt = expandedBaseline;
1774
+ for (;;) {
1775
+ let grew = false;
1776
+ rebuilt = expandedBaseline;
1777
+ for (const task of ordered) {
1778
+ const row = rows.get(task.taskId);
1779
+ if (!row || invalidated.has(task.taskId)) continue;
1780
+ if (task.dependsOn.some((dependency) => invalidated.has(dependency))) {
1781
+ invalidated.add(task.taskId);
1782
+ grew = true;
1783
+ continue;
1784
+ }
1785
+ for (const fact of facts.get(task.taskId) ?? []) {
1786
+ const phase = task.phases[fact.phase];
1787
+ if (!phase) {
1788
+ invalidated.add(task.taskId);
1789
+ grew = true;
1790
+ break;
1791
+ }
1792
+ const original = resources.workspaces.getRevision(
1793
+ fact.isolatedRevisionId,
1794
+ );
1795
+ if (!original.parentRevisionId) {
1796
+ throw new Error("workflow-ledger-revision-base-invalid");
1797
+ }
1798
+ const originalBase = resources.workspaces.getRevision(
1799
+ original.parentRevisionId,
1800
+ );
1801
+ const current = resources.workspaces.getRevision(rebuilt.revisionId);
1802
+ const boundaries =
1803
+ fact.kind === "repair-verified"
1804
+ ? Object.values(task.phases)
1805
+ : [phase];
1806
+ const boundPaths = [
1807
+ ...new Set(
1808
+ boundaries.flatMap((boundary) => [
1809
+ ...(row.contextReadPaths ?? []),
1810
+ ...boundary.read,
1811
+ ...boundary.write,
1812
+ ...boundary.delete,
1813
+ ...boundary.verificationInputs.flatMap((binding) =>
1814
+ binding.kind === "workspace" ? [binding.path] : [],
1815
+ ),
1816
+ ]),
1817
+ ),
1818
+ ];
1819
+ if (
1820
+ boundPaths.some(
1821
+ (relative) =>
1822
+ !workspaceEntryEqual(
1823
+ originalBase.entries[relative],
1824
+ current.entries[relative],
1825
+ ),
1826
+ )
1827
+ ) {
1828
+ invalidated.add(task.taskId);
1829
+ grew = true;
1830
+ break;
1831
+ }
1832
+ const changes: Record<
1833
+ string,
1834
+ | { kind: "absent" }
1835
+ | { kind: "file"; bytes: Uint8Array; mode: number }
1836
+ > = {};
1837
+ for (const output of fact.outputFacts) {
1838
+ const entry = original.entries[output.path];
1839
+ if (
1840
+ output.kind === "absent"
1841
+ ? entry?.kind !== "absent"
1842
+ : entry?.kind !== "file" ||
1843
+ entry.hash !== output.hash ||
1844
+ entry.bytes !== output.bytes
1845
+ ) {
1846
+ throw new Error("workflow-ledger-output-integrity-invalid");
1847
+ }
1848
+ changes[output.path] =
1849
+ entry.kind === "absent"
1850
+ ? { kind: "absent" }
1851
+ : {
1852
+ kind: "file",
1853
+ bytes: resources.artifacts.read(entry.hash),
1854
+ mode: entry.mode,
1855
+ };
1856
+ }
1857
+ rebuilt = resources.workspaces.createRevision({
1858
+ parentRevisionId: rebuilt.revisionId,
1859
+ changes,
1860
+ });
1861
+ }
1862
+ }
1863
+ for (const output of input.plan.outputs) {
1864
+ if (invalidated.has(output.producer.taskId)) continue;
1865
+ const produced = (facts.get(output.producer.taskId) ?? []).some(
1866
+ (fact) => fact.phase === output.producer.phase,
1867
+ );
1868
+ if (
1869
+ produced &&
1870
+ rebuilt.entries[output.path]?.kind !== "file" &&
1871
+ !invalidated.has(output.producer.taskId)
1872
+ ) {
1873
+ invalidated.add(output.producer.taskId);
1874
+ grew = true;
1875
+ }
1876
+ }
1877
+ if (!grew) break;
1878
+ }
1879
+ if (hasVerificationLifecycle(input.plan)) {
1880
+ const retainedRevisions = new Set(
1881
+ input.tasks
1882
+ .filter((task) => !invalidated.has(task.taskId))
1883
+ .map((task) => task.deliveryRevision),
1884
+ );
1885
+ for (const deliveryRevision of retainedRevisions) {
1886
+ const ledger = this.#ledger(resources, deliveryRevision);
1887
+ const stored = ledger.durableFact(this.#baselineFactKey(resources));
1888
+ resources.baselinePromises.delete(deliveryRevision);
1889
+ if (stored === undefined) continue;
1890
+ const baseline = parseVerificationBaseline(stored, resources.artifacts);
1891
+ if (baseline.revisionId === expandedBaseline.revisionId) continue;
1892
+ if (baseline.revisionId !== input.baselineRevisionId) {
1893
+ throw new Error("workflow-verification-baseline-conflict");
1894
+ }
1895
+ ledger.replaceDurableFact(
1896
+ this.#baselineFactKey(resources),
1897
+ stored,
1898
+ verificationBaselineFact(
1899
+ {
1900
+ ...baseline,
1901
+ revisionId: expandedBaseline.revisionId,
1902
+ },
1903
+ resources.artifacts,
1904
+ ),
1905
+ );
1906
+ }
1907
+ }
1908
+ resources.baselineRevisionId = expandedBaseline.revisionId;
1909
+ resources.currentRevisionId = rebuilt.revisionId;
1910
+ if (hasVerificationLifecycle(input.plan)) {
1911
+ for (const task of input.tasks) {
1912
+ if (task.state === "verified" && !invalidated.has(task.taskId)) {
1913
+ this.#completeTracking(resources, task.taskId);
1914
+ }
1915
+ }
1916
+ }
1917
+ return {
1918
+ invalidatedTaskIds: [...invalidated].sort(),
1919
+ baselineRevisionId: resources.baselineRevisionId,
1920
+ currentWorkspaceRevisionId: resources.currentRevisionId,
1921
+ };
1922
+ }
1923
+
1924
+ async #mergeCandidate(
1925
+ resources: DurableRunResources,
1926
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1927
+ baseRevisionId: string,
1928
+ candidateRevisionId: string,
1929
+ boundPaths?: string[],
1930
+ ) {
1931
+ let release!: () => void;
1932
+ const prior = resources.mergeTail;
1933
+ resources.mergeTail = new Promise<void>((resolve) => {
1934
+ release = resolve;
1935
+ });
1936
+ await prior;
1937
+ try {
1938
+ const phase = input.task.phases[input.phase];
1939
+ if (!phase) throw new Error("workflow-task-phase-invalid");
1940
+ const merged = resources.workspaces.mergeRevision({
1941
+ baseRevisionId,
1942
+ currentRevisionId: resources.currentRevisionId,
1943
+ candidateRevisionId,
1944
+ boundPaths: [
1945
+ ...new Set([
1946
+ ...(boundPaths ?? [...phase.read, ...phase.write, ...phase.delete]),
1947
+ ...(input.contextReadPaths ?? []),
1948
+ ]),
1949
+ ],
1950
+ });
1951
+ resources.currentRevisionId = merged.revisionId;
1952
+ return merged;
1953
+ } finally {
1954
+ release();
1955
+ }
1956
+ }
1957
+
1958
+ async #rollbackWorkspacePaths(input: {
1959
+ resources: DurableRunResources;
1960
+ baseRevisionId: string;
1961
+ rejectedRevisionId: string;
1962
+ paths: readonly string[];
1963
+ }): Promise<void> {
1964
+ const { resources } = input;
1965
+ let release!: () => void;
1966
+ const prior = resources.mergeTail;
1967
+ resources.mergeTail = new Promise<void>((resolve) => {
1968
+ release = resolve;
1969
+ });
1970
+ await prior;
1971
+ try {
1972
+ const base = resources.workspaces.getRevision(input.baseRevisionId);
1973
+ const rejected = resources.workspaces.getRevision(
1974
+ input.rejectedRevisionId,
1975
+ );
1976
+ const current = resources.workspaces.getRevision(
1977
+ resources.currentRevisionId,
1978
+ );
1979
+ const changes: Record<
1980
+ string,
1981
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1982
+ > = {};
1983
+ for (const relative of [...new Set(input.paths)].sort()) {
1984
+ const before = base.entries[relative];
1985
+ const rejectedEntry = rejected.entries[relative];
1986
+ if (workspaceEntryEqual(before, rejectedEntry)) continue;
1987
+ const latest = current.entries[relative];
1988
+ if (workspaceEntryEqual(latest, before)) continue;
1989
+ if (!workspaceEntryEqual(latest, rejectedEntry)) {
1990
+ throw new Error("workspace-revision-stale");
1991
+ }
1992
+ changes[relative] =
1993
+ before?.kind === "file"
1994
+ ? {
1995
+ kind: "file",
1996
+ bytes: resources.artifacts.read(before.hash),
1997
+ mode: before.mode,
1998
+ }
1999
+ : { kind: "absent" };
2000
+ }
2001
+ if (Object.keys(changes).length === 0) return;
2002
+ const rolledBack = resources.workspaces.createRevision({
2003
+ parentRevisionId: current.revisionId,
2004
+ changes,
2005
+ });
2006
+ resources.currentRevisionId = rolledBack.revisionId;
2007
+ } finally {
2008
+ release();
2009
+ }
2010
+ }
2011
+
2012
+ async #runRepairCandidate(input: {
2013
+ resources: DurableRunResources;
2014
+ ledger: TaskLedger;
2015
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
2016
+ repairAttempt: number;
2017
+ failureIdentities: string[];
2018
+ }): Promise<DurableRepairResult> {
2019
+ const { resources, ledger } = input;
2020
+ const request = input.attempt;
2021
+ if (request.signal.aborted) {
2022
+ return { kind: "operation-cancelled", code: "cancelled" };
2023
+ }
2024
+ const phase = request.task.phases[request.phase];
2025
+ if (!phase) throw new Error("workflow-task-phase-invalid");
2026
+ const allPhases = Object.values(request.task.phases);
2027
+ const approvedWritePaths = [
2028
+ ...new Set(allPhases.flatMap((boundary) => boundary.write)),
2029
+ ].sort();
2030
+ const approvedDeletePaths = [
2031
+ ...new Set(allPhases.flatMap((boundary) => boundary.delete)),
2032
+ ].sort();
2033
+ const approvedPaths = [
2034
+ ...new Set([...approvedWritePaths, ...approvedDeletePaths]),
2035
+ ].sort();
2036
+ const boundPaths = [
2037
+ ...new Set(
2038
+ allPhases.flatMap((boundary) => [
2039
+ ...boundary.read,
2040
+ ...boundary.write,
2041
+ ...boundary.delete,
2042
+ ]),
2043
+ ),
2044
+ ].sort();
2045
+ if (approvedPaths.length === 0) {
2046
+ return { kind: "approval-needed", code: "task-write-set-empty" };
2047
+ }
2048
+ const baseRevisionId = resources.currentRevisionId;
2049
+ const pendingKey = `pending-${hash(request.taskId, request.phase, request.task.repairVerification.id).slice(0, 40)}`;
2050
+ const pending = this.#pendingCandidate(ledger, pendingKey, baseRevisionId);
2051
+ const proposalRoot = mkdtempSync(path.join(resources.root, "repair-"));
2052
+ try {
2053
+ await resources.workspaces.materializeAsync(
2054
+ baseRevisionId,
2055
+ proposalRoot,
2056
+ request.signal,
2057
+ );
2058
+ if (request.routeId && !pending) {
2059
+ const rebound = this.#broker.resumeBinding({
2060
+ runId: request.runId,
2061
+ taskId: request.taskId,
2062
+ role: "implementation-worker",
2063
+ routeId: request.routeId,
2064
+ ...(request.routeFingerprint
2065
+ ? { expectedFingerprint: request.routeFingerprint }
2066
+ : {}),
2067
+ requirements: implementationRouteRequirements({
2068
+ task: request.task,
2069
+ ...(request.artifactCorrection
2070
+ ? { artifactCorrection: request.artifactCorrection }
2071
+ : {}),
2072
+ repair: { failureIdentities: input.failureIdentities },
2073
+ }),
2074
+ });
2075
+ if (!rebound.ok) return { kind: "paused", code: rebound.code };
2076
+ }
2077
+ const ledgerProjection = ledger.projection({
2078
+ runId: request.runId,
2079
+ taskId: request.taskId,
2080
+ nextPhase: request.phase,
2081
+ });
2082
+ const repair = {
2083
+ attempt: input.repairAttempt,
2084
+ attribution: "introduced" as const,
2085
+ failureIdentities: [...input.failureIdentities],
2086
+ };
2087
+ let identity: BeginCandidateInput | undefined = pending?.identity;
2088
+ const routed = pending
2089
+ ? {
2090
+ ok: true as const,
2091
+ value: pending.proposal,
2092
+ routeId: pending.identity.routeId,
2093
+ routeFingerprint: pending.identity.routeFingerprint,
2094
+ }
2095
+ : await this.#broker.run({
2096
+ runId: request.runId,
2097
+ taskId: request.taskId,
2098
+ operationId: `${request.operationId}:${request.taskId}:repair:${input.repairAttempt}`,
2099
+ role: "implementation-worker",
2100
+ requirements: implementationRouteRequirements({
2101
+ task: request.task,
2102
+ ...(request.artifactCorrection
2103
+ ? { artifactCorrection: request.artifactCorrection }
2104
+ : {}),
2105
+ repair: { failureIdentities: input.failureIdentities },
2106
+ }),
2107
+ signal: request.signal,
2108
+ ...(request.onActivity ? { onActivity: request.onActivity } : {}),
2109
+ classifyResult: semanticRouteFailure,
2110
+ execute: async (attempt) => {
2111
+ identity = {
2112
+ candidateId: `candidate-${randomUUID()}`,
2113
+ runId: request.runId,
2114
+ deliveryRevision: request.deliveryRevision,
2115
+ taskId: request.taskId,
2116
+ phase: request.phase,
2117
+ attemptId: `repair-${hash(
2118
+ request.operationId,
2119
+ request.taskId,
2120
+ String(input.repairAttempt),
2121
+ attempt.route.fingerprint,
2122
+ ).slice(0, 40)}`,
2123
+ approvedPaths,
2124
+ isolatedRevisionId: baseRevisionId,
2125
+ verificationId: request.task.repairVerification.id,
2126
+ routeId: attempt.route.id,
2127
+ routeFingerprint: attempt.route.fingerprint,
2128
+ };
2129
+ if (request.reserveCandidate && !request.reserveCandidate())
2130
+ return {
2131
+ kind: "paused" as const,
2132
+ code:
2133
+ request.additionalAttempt || request.verificationOnly
2134
+ ? "repair-attempts-exhausted"
2135
+ : "change-work-budget-exhausted",
2136
+ };
2137
+ return this.#options.proposeCandidate({
2138
+ runId: request.runId,
2139
+ operationId: request.operationId,
2140
+ deliveryRevision: request.deliveryRevision,
2141
+ taskId: request.taskId,
2142
+ phase: request.phase,
2143
+ task: structuredClone(request.task),
2144
+ contextReadPaths: request.contextReadPaths,
2145
+ workspaceRoot: proposalRoot,
2146
+ ledgerProjection,
2147
+ candidateArtifact: {
2148
+ ledger,
2149
+ identity,
2150
+ workspaceRoot: proposalRoot,
2151
+ writePaths: approvedWritePaths,
2152
+ deletePaths: approvedDeletePaths,
2153
+ },
2154
+ route: attempt.route,
2155
+ repair,
2156
+ ...(request.artifactCorrection
2157
+ ? { artifactCorrection: request.artifactCorrection }
2158
+ : {}),
2159
+ ...(request.contextRequest
2160
+ ? { contextRequest: request.contextRequest }
2161
+ : {}),
2162
+ ...(request.recoveryFeedback
2163
+ ? { recoveryFeedback: request.recoveryFeedback }
2164
+ : {}),
2165
+ signal: attempt.signal,
2166
+ onHeaders: attempt.onHeaders,
2167
+ onProgress: attempt.onProgress,
2168
+ });
2169
+ },
2170
+ });
2171
+ if (!routed.ok) {
2172
+ return routed.state === "cancelled" || request.signal.aborted
2173
+ ? { kind: "operation-cancelled", code: "cancelled" }
2174
+ : { kind: "paused", code: routed.code };
2175
+ }
2176
+ if (!identity || identity.routeId !== routed.routeId) {
2177
+ throw new Error("candidate-route-identity-invalid");
2178
+ }
2179
+ const correctionIdentity = identity;
2180
+ const withRoute = <T extends object>(outcome: T) => ({
2181
+ ...outcome,
2182
+ routeId: correctionIdentity.routeId,
2183
+ routeFingerprint: correctionIdentity.routeFingerprint,
2184
+ });
2185
+ let uncommittedRevisionId: string | undefined;
2186
+ const reject = async <T extends object>(
2187
+ outcome: T,
2188
+ correction: {
2189
+ category: "artifact" | "stale" | "verification" | "checkpoint";
2190
+ code: string;
2191
+ stage: string;
2192
+ },
2193
+ rollback = false,
2194
+ ): Promise<T> => {
2195
+ if (rollback && uncommittedRevisionId) {
2196
+ await this.#rollbackWorkspacePaths({
2197
+ resources,
2198
+ baseRevisionId,
2199
+ rejectedRevisionId: uncommittedRevisionId,
2200
+ paths: approvedPaths,
2201
+ });
2202
+ }
2203
+ this.#recordCorrection(ledger, request, correctionIdentity, correction);
2204
+ return withRoute({ ...outcome, retryPolicy: correction.category });
2205
+ };
2206
+ const proposal = routed.value;
2207
+ if (
2208
+ proposal.kind !== "candidate" &&
2209
+ proposal.kind !== "sealed-candidate"
2210
+ ) {
2211
+ return proposal.kind === "operation-cancelled" ||
2212
+ proposal.kind === "paused" ||
2213
+ proposal.kind === "approval-needed"
2214
+ ? withRoute(proposal)
2215
+ : reject(proposal, {
2216
+ category: "artifact",
2217
+ code: proposal.code,
2218
+ stage: "candidate-submit",
2219
+ });
2220
+ }
2221
+ emitWorkflowActivity(request.onActivity, { state: "validating" });
2222
+ ledger.beginCandidate(identity);
2223
+ let bytes: Buffer;
2224
+ if (proposal.kind === "sealed-candidate") {
2225
+ if (
2226
+ proposal.candidateId !== identity.candidateId ||
2227
+ !SHA256.test(proposal.artifactHash) ||
2228
+ !Number.isSafeInteger(proposal.bytes) ||
2229
+ proposal.bytes < 1 ||
2230
+ !Array.isArray(proposal.paths)
2231
+ ) {
2232
+ return reject(
2233
+ { kind: "retryable", code: "candidate-diff-invalid" },
2234
+ {
2235
+ category: "artifact",
2236
+ code: "candidate-diff-invalid",
2237
+ stage: "candidate-seal",
2238
+ },
2239
+ );
2240
+ }
2241
+ bytes = Buffer.from(ledger.readSealedCandidate(identity.candidateId));
2242
+ if (
2243
+ bytes.length !== proposal.bytes ||
2244
+ sha256Bytes(bytes) !== proposal.artifactHash
2245
+ ) {
2246
+ throw new Error("candidate-seal-integrity-invalid");
2247
+ }
2248
+ } else {
2249
+ if (
2250
+ !(proposal.bytes instanceof Uint8Array) ||
2251
+ proposal.bytes.length === 0
2252
+ ) {
2253
+ return reject(
2254
+ { kind: "retryable", code: "candidate-diff-invalid" },
2255
+ {
2256
+ category: "artifact",
2257
+ code: "candidate-diff-invalid",
2258
+ stage: "candidate-submit",
2259
+ },
2260
+ );
2261
+ }
2262
+ bytes = Buffer.from(proposal.bytes);
2263
+ }
2264
+ let candidatePaths: string[];
2265
+ try {
2266
+ candidatePaths = diffWritePaths(
2267
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
2268
+ ).paths;
2269
+ } catch {
2270
+ return reject(
2271
+ { kind: "retryable", code: "candidate-diff-invalid" },
2272
+ {
2273
+ category: "artifact",
2274
+ code: "candidate-diff-invalid",
2275
+ stage: "candidate-diff",
2276
+ },
2277
+ );
2278
+ }
2279
+ if (
2280
+ candidatePaths.some((relative) => !approvedPaths.includes(relative))
2281
+ ) {
2282
+ return reject(
2283
+ {
2284
+ kind: "approval-needed",
2285
+ code: "repair-boundary-expansion",
2286
+ },
2287
+ {
2288
+ category: "artifact",
2289
+ code: "repair-boundary-expansion",
2290
+ stage: "candidate-boundary",
2291
+ },
2292
+ );
2293
+ }
2294
+ if (proposal.kind === "sealed-candidate") {
2295
+ if (
2296
+ JSON.stringify([...proposal.paths].sort()) !==
2297
+ JSON.stringify([...candidatePaths].sort())
2298
+ ) {
2299
+ return reject(
2300
+ { kind: "retryable", code: "candidate-diff-invalid" },
2301
+ {
2302
+ category: "artifact",
2303
+ code: "candidate-diff-invalid",
2304
+ stage: "candidate-seal",
2305
+ },
2306
+ );
2307
+ }
2308
+ }
2309
+ const dependencySensitive = candidatePaths.some(
2310
+ (relative) => relative === "package.json" || LOCKFILES.has(relative),
2311
+ );
2312
+ let dependenciesBefore: Map<string, DependencyContractEntry> | undefined;
2313
+ if (dependencySensitive) {
2314
+ try {
2315
+ dependenciesBefore = dependencyContract(proposalRoot);
2316
+ } catch (error) {
2317
+ if (!(error instanceof DependencyManifestError)) throw error;
2318
+ return reject(
2319
+ { kind: "retryable", code: error.message },
2320
+ {
2321
+ category: "artifact",
2322
+ code: error.message,
2323
+ stage: "candidate-dependency",
2324
+ },
2325
+ );
2326
+ }
2327
+ }
2328
+
2329
+ const checked = await runGitApply(
2330
+ proposalRoot,
2331
+ bytes,
2332
+ true,
2333
+ request.signal,
2334
+ );
2335
+ if (checked !== "ok") {
2336
+ return checked === "cancelled"
2337
+ ? withRoute({ kind: "operation-cancelled", code: "cancelled" })
2338
+ : reject(
2339
+ { kind: "retryable", code: "candidate-diff-invalid" },
2340
+ {
2341
+ category: "artifact",
2342
+ code: "candidate-diff-invalid",
2343
+ stage: "candidate-check",
2344
+ },
2345
+ );
2346
+ }
2347
+ const applied = await runGitApply(
2348
+ proposalRoot,
2349
+ bytes,
2350
+ false,
2351
+ request.signal,
2352
+ );
2353
+ if (applied !== "ok") {
2354
+ return applied === "cancelled"
2355
+ ? withRoute({ kind: "operation-cancelled", code: "cancelled" })
2356
+ : reject(
2357
+ { kind: "retryable", code: "candidate-diff-invalid" },
2358
+ {
2359
+ category: "artifact",
2360
+ code: "candidate-diff-invalid",
2361
+ stage: "candidate-apply",
2362
+ },
2363
+ );
2364
+ }
2365
+ if (dependenciesBefore) {
2366
+ let dependenciesAfter: Map<string, DependencyContractEntry>;
2367
+ try {
2368
+ dependenciesAfter = dependencyContract(proposalRoot);
2369
+ } catch (error) {
2370
+ if (!(error instanceof DependencyManifestError)) throw error;
2371
+ return reject(
2372
+ { kind: "retryable", code: error.message },
2373
+ {
2374
+ category: "artifact",
2375
+ code: error.message,
2376
+ stage: "candidate-dependency",
2377
+ },
2378
+ );
2379
+ }
2380
+ if (
2381
+ hasUnapprovedDependencyChange(
2382
+ dependenciesBefore,
2383
+ dependenciesAfter,
2384
+ request.task.approvedDependencies,
2385
+ candidatePaths.some((relative) => LOCKFILES.has(relative)),
2386
+ )
2387
+ ) {
2388
+ return reject(
2389
+ { kind: "approval-needed", code: "unapproved-dependency-change" },
2390
+ {
2391
+ category: "artifact",
2392
+ code: "unapproved-dependency-change",
2393
+ stage: "candidate-dependency",
2394
+ },
2395
+ );
2396
+ }
2397
+ }
2398
+
2399
+ let artifactHash: string;
2400
+ if (proposal.kind === "sealed-candidate") {
2401
+ artifactHash = proposal.artifactHash;
2402
+ } else {
2403
+ let sequence = 0;
2404
+ for (
2405
+ let offset = 0;
2406
+ offset < bytes.length;
2407
+ offset += TASK_LEDGER_LIMITS.maxSegmentBytes
2408
+ ) {
2409
+ const segment = bytes.subarray(
2410
+ offset,
2411
+ Math.min(offset + TASK_LEDGER_LIMITS.maxSegmentBytes, bytes.length),
2412
+ );
2413
+ const accepted = ledger.appendCandidateSegment({
2414
+ ...identity,
2415
+ sequence,
2416
+ bytes: segment,
2417
+ segmentHash: sha256Bytes(segment),
2418
+ });
2419
+ if (!accepted.ok) {
2420
+ return reject(
2421
+ { kind: "paused", code: accepted.code },
2422
+ {
2423
+ category: "artifact",
2424
+ code: accepted.code,
2425
+ stage: "candidate-segment",
2426
+ },
2427
+ );
2428
+ }
2429
+ sequence += 1;
2430
+ }
2431
+ const sealed = ledger.sealCandidate({
2432
+ ...identity,
2433
+ segmentCount: sequence,
2434
+ totalBytes: bytes.length,
2435
+ candidateHash: sha256Bytes(bytes),
2436
+ });
2437
+ if (!sealed.ok) {
2438
+ return reject(
2439
+ { kind: "retryable", code: sealed.code },
2440
+ {
2441
+ category: "artifact",
2442
+ code: sealed.code,
2443
+ stage: "candidate-seal",
2444
+ },
2445
+ );
2446
+ }
2447
+ artifactHash = sealed.artifactHash;
2448
+ }
2449
+ const candidate = resources.workspaces.createRevision({
2450
+ parentRevisionId: baseRevisionId,
2451
+ changes: revisionChanges(proposalRoot, approvedPaths),
2452
+ });
2453
+ const requiredOutputs = request.plan.outputs.filter(
2454
+ (output) =>
2455
+ output.producer.taskId === request.taskId &&
2456
+ (!request.artifactCorrection || output.producer.phase !== "refactor"),
2457
+ );
2458
+ if (
2459
+ requiredOutputs.some(
2460
+ (output) => candidate.entries[output.path]?.kind !== "file",
2461
+ )
2462
+ ) {
2463
+ return reject(
2464
+ { kind: "retryable", code: "producer-output-unavailable" },
2465
+ {
2466
+ category: "artifact",
2467
+ code: "producer-output-unavailable",
2468
+ stage: "candidate-output",
2469
+ },
2470
+ );
2471
+ }
2472
+ emitWorkflowActivity(request.onActivity, { state: "verifying" });
2473
+ this.#savePendingCandidate(ledger, pendingKey, {
2474
+ identity,
2475
+ proposal: {
2476
+ kind: "sealed-candidate",
2477
+ candidateId: identity.candidateId,
2478
+ artifactHash,
2479
+ bytes: bytes.length,
2480
+ paths: candidatePaths,
2481
+ },
2482
+ });
2483
+ const verified = await this.#verifyPhase({
2484
+ executionWritePaths: this.#executionWritePaths(resources),
2485
+ runId: request.runId,
2486
+ deliveryRevision: request.deliveryRevision,
2487
+ taskId: request.taskId,
2488
+ phase: request.phase,
2489
+ root: proposalRoot,
2490
+ verification: structuredClone(request.task.repairVerification),
2491
+ signal: request.signal,
2492
+ });
2493
+ if (!verified.ok) {
2494
+ if (verified.kind === "paused") return verified;
2495
+ this.#savePendingCandidate(ledger, pendingKey, null);
2496
+ return reject(verified, {
2497
+ category: "verification",
2498
+ code: verified.code,
2499
+ stage: "phase-verification",
2500
+ });
2501
+ }
2502
+ if (
2503
+ verified.classification !== "expected-green" ||
2504
+ verified.exitCode !== 0
2505
+ ) {
2506
+ return reject(
2507
+ { kind: "retryable", code: "verification-rejected" },
2508
+ {
2509
+ category: "verification",
2510
+ code: "verification-rejected",
2511
+ stage: "phase-verification",
2512
+ },
2513
+ );
2514
+ }
2515
+ let merged: ReturnType<WorkspaceStore["mergeRevision"]>;
2516
+ try {
2517
+ merged = await this.#mergeCandidate(
2518
+ resources,
2519
+ request,
2520
+ baseRevisionId,
2521
+ candidate.revisionId,
2522
+ boundPaths,
2523
+ );
2524
+ uncommittedRevisionId = merged.revisionId;
2525
+ } catch (error) {
2526
+ if (
2527
+ error instanceof Error &&
2528
+ error.message === "workspace-revision-stale"
2529
+ ) {
2530
+ return reject(
2531
+ { kind: "retryable", code: "workspace-revision-stale" },
2532
+ {
2533
+ category: "stale",
2534
+ code: "workspace-revision-stale",
2535
+ stage: "candidate-merge",
2536
+ },
2537
+ );
2538
+ }
2539
+ throw error;
2540
+ }
2541
+ if (
2542
+ requiredOutputs.some(
2543
+ (output) => merged.entries[output.path]?.kind !== "file",
2544
+ )
2545
+ ) {
2546
+ return reject(
2547
+ { kind: "retryable", code: "producer-output-unavailable" },
2548
+ {
2549
+ category: "artifact",
2550
+ code: "producer-output-unavailable",
2551
+ stage: "candidate-output",
2552
+ },
2553
+ true,
2554
+ );
2555
+ }
2556
+ const outputFacts = approvedPaths.map((relative) => {
2557
+ const entry = merged.entries[relative];
2558
+ return entry?.kind === "file"
2559
+ ? {
2560
+ path: relative,
2561
+ kind: "file" as const,
2562
+ hash: entry.hash,
2563
+ bytes: entry.bytes,
2564
+ }
2565
+ : { path: relative, kind: "absent" as const };
2566
+ });
2567
+ return {
2568
+ kind: "repair-committed",
2569
+ commit: {
2570
+ artifactHash,
2571
+ isolatedRevisionId: merged.revisionId,
2572
+ exitCode: verified.exitCode,
2573
+ classification: verified.classification,
2574
+ diagnostic: verified.diagnostic,
2575
+ outputFacts,
2576
+ routeId: identity.routeId,
2577
+ routeFingerprint: identity.routeFingerprint,
2578
+ },
2579
+ };
2580
+ } finally {
2581
+ rmSync(proposalRoot, { recursive: true, force: true });
2582
+ }
2583
+ }
2584
+
2585
+ #revisionDescendsFrom(
2586
+ resources: DurableRunResources,
2587
+ descendantRevisionId: string,
2588
+ ancestorRevisionId: string,
2589
+ ): boolean {
2590
+ let current = resources.workspaces.getRevision(descendantRevisionId);
2591
+ const visited = new Set<string>();
2592
+ for (;;) {
2593
+ if (current.revisionId === ancestorRevisionId) return true;
2594
+ if (
2595
+ current.parentRevisionId === null ||
2596
+ visited.has(current.revisionId)
2597
+ ) {
2598
+ return false;
2599
+ }
2600
+ visited.add(current.revisionId);
2601
+ current = resources.workspaces.getRevision(current.parentRevisionId);
2602
+ }
2603
+ }
2604
+
2605
+ #verifiedPhaseReplay(
2606
+ ledger: TaskLedger,
2607
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
2608
+ ): { artifactHash: string; exitCode: number; isolatedRevisionId: string } {
2609
+ const projection = ledger.projection({
2610
+ runId: input.runId,
2611
+ taskId: input.taskId,
2612
+ nextPhase: input.phase,
2613
+ }) as { history: Array<Record<string, unknown>> };
2614
+ const event = projection.history.find(
2615
+ (candidate) =>
2616
+ candidate.kind === "phase-verified" && candidate.phase === input.phase,
2617
+ );
2618
+ if (
2619
+ !event ||
2620
+ typeof event.artifactHash !== "string" ||
2621
+ !SHA256.test(event.artifactHash) ||
2622
+ typeof event.isolatedRevisionId !== "string" ||
2623
+ !SHA256.test(event.isolatedRevisionId) ||
2624
+ typeof event.exitCode !== "number" ||
2625
+ event.actualClassification !== expectedClassification(input.phase)
2626
+ ) {
2627
+ throw new Error("workflow-ledger-phase-invalid");
2628
+ }
2629
+ return {
2630
+ artifactHash: event.artifactHash,
2631
+ exitCode: event.exitCode,
2632
+ isolatedRevisionId: event.isolatedRevisionId,
2633
+ };
2634
+ }
2635
+
2636
+ async #runRedArtifactCorrection(input: {
2637
+ resources: DurableRunResources;
2638
+ ledger: TaskLedger;
2639
+ baseline?: DurableVerificationBaseline;
2640
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
2641
+ }): Promise<WorkflowAttemptOutcome> {
2642
+ const { resources, ledger } = input;
2643
+ const request = input.attempt;
2644
+ if (
2645
+ request.phase !== "green" ||
2646
+ request.artifactCorrection?.code !== "red-artifact-constraint"
2647
+ ) {
2648
+ throw new Error("workflow-red-artifact-correction-invalid");
2649
+ }
2650
+ const priorRed = this.#verifiedPhaseReplay(ledger, {
2651
+ ...request,
2652
+ phase: "red",
2653
+ });
2654
+ if (!hasVerificationLifecycle(request.plan)) {
2655
+ return { kind: "paused", code: "red-artifact-constraint" };
2656
+ }
2657
+ const stableRevisionId = resources.currentRevisionId;
2658
+ const correctionPaths = [
2659
+ ...new Set(
2660
+ Object.values(request.task.phases).flatMap((phase) => [
2661
+ ...phase.write,
2662
+ ...phase.delete,
2663
+ ]),
2664
+ ),
2665
+ ];
2666
+ const rejectCorrection = async <T extends WorkflowAttemptOutcome>(
2667
+ outcome: T,
2668
+ ): Promise<T> => {
2669
+ if (resources.currentRevisionId !== stableRevisionId) {
2670
+ await this.#rollbackWorkspacePaths({
2671
+ resources,
2672
+ baseRevisionId: stableRevisionId,
2673
+ rejectedRevisionId: resources.currentRevisionId,
2674
+ paths: correctionPaths,
2675
+ });
2676
+ }
2677
+ return outcome;
2678
+ };
2679
+ const failureIdentities = [
2680
+ hash(
2681
+ "red-artifact-constraint",
2682
+ request.taskId,
2683
+ request.task.phases.red.verification.id,
2684
+ ),
2685
+ ];
2686
+ const correctionAttemptUsed = request.artifactCorrection.attempt;
2687
+ const correctionAction = {
2688
+ kind: "red-correction" as const,
2689
+ attempt:
2690
+ request.additionalAttempt || request.verificationOnly
2691
+ ? 1
2692
+ : correctionAttemptUsed,
2693
+ };
2694
+ const correctionDecision =
2695
+ request.recoveryDecision?.(correctionAction) ??
2696
+ decideRecoveryAction(request.plan, correctionAction, request);
2697
+ if (!correctionDecision.allowed)
2698
+ return { kind: "paused", code: correctionDecision.code };
2699
+ const repaired = await this.#runRepairCandidate({
2700
+ resources,
2701
+ ledger,
2702
+ attempt: request,
2703
+ repairAttempt: correctionAttemptUsed,
2704
+ failureIdentities,
2705
+ });
2706
+ if (repaired.kind !== "repair-committed") {
2707
+ return rejectCorrection(repaired);
2708
+ }
2709
+
2710
+ const correctedRevision = resources.workspaces.getRevision(
2711
+ repaired.commit.isolatedRevisionId,
2712
+ );
2713
+ const acceptedRedRevision = resources.workspaces.getRevision(
2714
+ priorRed.isolatedRevisionId,
2715
+ );
2716
+ if (!acceptedRedRevision.parentRevisionId) {
2717
+ throw new Error("workflow-ledger-revision-base-invalid");
2718
+ }
2719
+ const correctedRedChanges: Record<
2720
+ string,
2721
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
2722
+ > = {};
2723
+ for (const relative of [
2724
+ ...new Set([
2725
+ ...request.task.phases.red.write,
2726
+ ...request.task.phases.red.delete,
2727
+ ]),
2728
+ ]) {
2729
+ const entry = correctedRevision.entries[relative];
2730
+ correctedRedChanges[relative] =
2731
+ entry?.kind === "file"
2732
+ ? {
2733
+ kind: "file",
2734
+ bytes: resources.artifacts.read(entry.hash),
2735
+ mode: entry.mode,
2736
+ }
2737
+ : { kind: "absent" };
2738
+ }
2739
+ const correctedRedRevision = resources.workspaces.createRevision({
2740
+ parentRevisionId: acceptedRedRevision.parentRevisionId,
2741
+ changes: correctedRedChanges,
2742
+ });
2743
+ const verifyCorrectedContract = async (input: {
2744
+ phase: "red" | "green";
2745
+ revisionId: string;
2746
+ verification: StructuredVerificationContract;
2747
+ }): Promise<DurablePhaseVerificationResult> => {
2748
+ const root = mkdtempSync(path.join(resources.root, "correction-check-"));
2749
+ try {
2750
+ await resources.workspaces.materializeAsync(
2751
+ input.revisionId,
2752
+ root,
2753
+ request.signal,
2754
+ );
2755
+ return await this.#verifyPhase({
2756
+ executionWritePaths: this.#executionWritePaths(resources),
2757
+ runId: request.runId,
2758
+ deliveryRevision: request.deliveryRevision,
2759
+ taskId: request.taskId,
2760
+ phase: input.phase,
2761
+ root,
2762
+ verification: structuredClone(input.verification),
2763
+ signal: request.signal,
2764
+ });
2765
+ } finally {
2766
+ rmSync(root, { recursive: true, force: true });
2767
+ }
2768
+ };
2769
+ emitWorkflowActivity(request.onActivity, { state: "verifying" });
2770
+ const redVerification = await verifyCorrectedContract({
2771
+ phase: "red",
2772
+ revisionId: correctedRedRevision.revisionId,
2773
+ verification: request.task.phases.red.verification,
2774
+ });
2775
+ if (!redVerification.ok) {
2776
+ return rejectCorrection({
2777
+ ...redVerification,
2778
+ ...(redVerification.kind === "retryable"
2779
+ ? { retryPolicy: "verification" as const }
2780
+ : {}),
2781
+ });
2782
+ }
2783
+ if (
2784
+ redVerification.classification !== "expected-red" ||
2785
+ redVerification.exitCode === 0
2786
+ ) {
2787
+ return rejectCorrection({
2788
+ kind: "retryable",
2789
+ code: "verification-rejected",
2790
+ retryPolicy: "verification",
2791
+ });
2792
+ }
2793
+ const greenVerification = await verifyCorrectedContract({
2794
+ phase: "green",
2795
+ revisionId: repaired.commit.isolatedRevisionId,
2796
+ verification: request.task.phases.green.verification,
2797
+ });
2798
+ if (!greenVerification.ok) {
2799
+ return rejectCorrection({
2800
+ ...greenVerification,
2801
+ ...(greenVerification.kind === "retryable"
2802
+ ? { retryPolicy: "verification" as const }
2803
+ : {}),
2804
+ });
2805
+ }
2806
+ if (
2807
+ greenVerification.classification !== "expected-green" ||
2808
+ greenVerification.exitCode !== 0
2809
+ ) {
2810
+ return rejectCorrection({
2811
+ kind: "retryable",
2812
+ code: "verification-rejected",
2813
+ retryPolicy: "verification",
2814
+ });
2815
+ }
2816
+
2817
+ const repairEvent: VerifiedTaskEvent = {
2818
+ runId: request.runId,
2819
+ taskId: request.taskId,
2820
+ eventId: `red-artifact-correction-${hash(
2821
+ request.operationId,
2822
+ request.taskId,
2823
+ repaired.commit.artifactHash,
2824
+ ).slice(0, 40)}`,
2825
+ kind: "repair-verified",
2826
+ phase: request.phase,
2827
+ attempt: correctionAttemptUsed,
2828
+ commandId: request.task.repairVerification.id,
2829
+ exitCode: 0,
2830
+ diagnostic: repaired.commit.diagnostic,
2831
+ artifactHash: repaired.commit.artifactHash,
2832
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
2833
+ outputFacts: repaired.commit.outputFacts,
2834
+ attribution: "introduced",
2835
+ failureIdentities: [
2836
+ hash(
2837
+ "red-artifact-constraint",
2838
+ request.taskId,
2839
+ request.task.phases.red.verification.id,
2840
+ ),
2841
+ ],
2842
+ routeId: repaired.commit.routeId,
2843
+ routeFingerprint: repaired.commit.routeFingerprint,
2844
+ };
2845
+ const greenPaths = new Set([
2846
+ ...request.task.phases.green.write,
2847
+ ...request.task.phases.green.delete,
2848
+ ]);
2849
+ const greenEvent: VerifiedTaskEvent = {
2850
+ runId: request.runId,
2851
+ taskId: request.taskId,
2852
+ eventId: `green-corrected-${hash(
2853
+ request.operationId,
2854
+ request.taskId,
2855
+ repaired.commit.artifactHash,
2856
+ ).slice(0, 40)}`,
2857
+ kind: "phase-verified",
2858
+ phase: "green",
2859
+ commandId: request.task.phases.green.verification.id,
2860
+ exitCode: greenVerification.exitCode,
2861
+ expectedClassification: "expected-green",
2862
+ actualClassification: greenVerification.classification,
2863
+ diagnostic: greenVerification.diagnostic,
2864
+ artifactHash: repaired.commit.artifactHash,
2865
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
2866
+ outputFacts: repaired.commit.outputFacts.filter((fact) =>
2867
+ greenPaths.has(fact.path),
2868
+ ),
2869
+ routeId: repaired.commit.routeId,
2870
+ routeFingerprint: repaired.commit.routeFingerprint,
2871
+ };
2872
+ const events: VerifiedTaskEvent[] = [repairEvent, greenEvent];
2873
+ if (input.baseline) {
2874
+ const affected = await this.#verifyTaskAffected({
2875
+ resources,
2876
+ baseline: input.baseline,
2877
+ task: request.task,
2878
+ revisionId: repaired.commit.isolatedRevisionId,
2879
+ signal: request.signal,
2880
+ });
2881
+ if (affected.kind === "repairable") {
2882
+ return rejectCorrection({
2883
+ kind: "retryable",
2884
+ code: "red-artifact-constraint",
2885
+ retryPolicy: "artifact",
2886
+ });
2887
+ }
2888
+ if (affected.kind !== "verified") return rejectCorrection(affected);
2889
+ }
2890
+ if (request.task.phases.refactor) {
2891
+ ledger.commitVerifiedEvents(events);
2892
+ return {
2893
+ kind: "phase-committed",
2894
+ artifactHash: repaired.commit.artifactHash,
2895
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
2896
+ exitCode: greenVerification.exitCode,
2897
+ classification: greenVerification.classification,
2898
+ routeId: repaired.commit.routeId,
2899
+ routeFingerprint: repaired.commit.routeFingerprint,
2900
+ };
2901
+ }
2902
+ const completedRevision = this.#completeTracking(resources, request.taskId);
2903
+ ledger.commitTaskCompletion({
2904
+ events,
2905
+ factKey: `task-final-${hash(request.taskId).slice(0, 40)}`,
2906
+ fact: {
2907
+ taskId: request.taskId,
2908
+ phase: request.phase,
2909
+ isolatedRevisionId: completedRevision.revisionId,
2910
+ attribution: "introduced",
2911
+ repairAttempts: correctionAttemptUsed,
2912
+ },
2913
+ });
2914
+ return {
2915
+ kind: "phase-committed",
2916
+ artifactHash: repaired.commit.artifactHash,
2917
+ isolatedRevisionId: completedRevision.revisionId,
2918
+ exitCode: greenVerification.exitCode,
2919
+ classification: greenVerification.classification,
2920
+ routeId: repaired.commit.routeId,
2921
+ routeFingerprint: repaired.commit.routeFingerprint,
2922
+ };
2923
+ }
2924
+
2925
+ async #runCumulativeRepair(input: {
2926
+ resources: DurableRunResources;
2927
+ ledger: TaskLedger;
2928
+ baseline: DurableVerificationBaseline;
2929
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
2930
+ }): Promise<WorkflowAttemptOutcome> {
2931
+ const { resources, ledger } = input;
2932
+ const request = input.attempt;
2933
+ const finalPhase = request.task.phases.refactor ? "refactor" : "green";
2934
+ const requestedFailures = request.repair?.failureIdentities;
2935
+ if (
2936
+ request.phase !== finalPhase ||
2937
+ request.repair?.attribution !== "introduced" ||
2938
+ !Array.isArray(requestedFailures) ||
2939
+ requestedFailures.length === 0 ||
2940
+ requestedFailures.length > 256 ||
2941
+ requestedFailures.some(
2942
+ (identity) => typeof identity !== "string" || !SHA256.test(identity),
2943
+ )
2944
+ ) {
2945
+ throw new Error("workflow-repair-context-invalid");
2946
+ }
2947
+ this.#verifiedPhaseReplay(ledger, request);
2948
+ const projection = ledger.projection({
2949
+ runId: request.runId,
2950
+ taskId: request.taskId,
2951
+ nextPhase: request.phase,
2952
+ }) as { history: Array<Record<string, unknown>> };
2953
+ const priorRepairs = projection.history.filter(
2954
+ (event) =>
2955
+ event.kind === "repair-verified" && event.phase === request.phase,
2956
+ );
2957
+ let stableRevisionId = resources.currentRevisionId;
2958
+ let affected: DurableAffectedResult = {
2959
+ kind: "repairable",
2960
+ attribution: "introduced",
2961
+ failureIdentities: [...new Set(requestedFailures)].sort(),
2962
+ };
2963
+ const replayedRepair = priorRepairs.at(-1);
2964
+ if (replayedRepair) {
2965
+ if (
2966
+ typeof replayedRepair.artifactHash !== "string" ||
2967
+ !SHA256.test(replayedRepair.artifactHash) ||
2968
+ typeof replayedRepair.isolatedRevisionId !== "string" ||
2969
+ !SHA256.test(replayedRepair.isolatedRevisionId)
2970
+ ) {
2971
+ throw new Error("workflow-ledger-phase-invalid");
2972
+ }
2973
+ if (
2974
+ this.#revisionDescendsFrom(
2975
+ resources,
2976
+ replayedRepair.isolatedRevisionId,
2977
+ stableRevisionId,
2978
+ )
2979
+ ) {
2980
+ resources.currentRevisionId = replayedRepair.isolatedRevisionId;
2981
+ stableRevisionId = replayedRepair.isolatedRevisionId;
2982
+ const replayedAffected = await this.#verifyTaskAffected({
2983
+ resources,
2984
+ baseline: input.baseline,
2985
+ task: request.task,
2986
+ revisionId: stableRevisionId,
2987
+ signal: request.signal,
2988
+ });
2989
+ if (replayedAffected.kind === "verified") {
2990
+ return {
2991
+ kind: "phase-committed",
2992
+ artifactHash: replayedRepair.artifactHash,
2993
+ isolatedRevisionId: stableRevisionId,
2994
+ exitCode: 0,
2995
+ classification: expectedClassification(request.phase),
2996
+ };
2997
+ }
2998
+ if (replayedAffected.kind !== "repairable") return replayedAffected;
2999
+ affected = replayedAffected;
3000
+ }
3001
+ }
3002
+
3003
+ const repairEvents: VerifiedTaskEvent[] = [];
3004
+ let lastCommit: DurableCandidateCommit | undefined;
3005
+ let repairAttempt = 0;
3006
+ let uncommittedRevisionId: string | undefined;
3007
+ const rollbackPaths = [
3008
+ ...new Set(
3009
+ Object.values(request.task.phases).flatMap((phase) => [
3010
+ ...phase.write,
3011
+ ...phase.delete,
3012
+ ]),
3013
+ ),
3014
+ ];
3015
+ const discardUncommitted = async <T extends WorkflowAttemptOutcome>(
3016
+ outcome: T,
3017
+ ): Promise<T> => {
3018
+ if (uncommittedRevisionId) {
3019
+ await this.#rollbackWorkspacePaths({
3020
+ resources,
3021
+ baseRevisionId: stableRevisionId,
3022
+ rejectedRevisionId: uncommittedRevisionId,
3023
+ paths: rollbackPaths,
3024
+ });
3025
+ }
3026
+ return outcome;
3027
+ };
3028
+ while (affected.kind === "repairable") {
3029
+ if (
3030
+ !(
3031
+ request.recoveryDecision?.({
3032
+ kind: "cumulative-repair",
3033
+ attempt: repairAttempt + 1,
3034
+ }) ??
3035
+ decideRecoveryAction(
3036
+ request.plan,
3037
+ { kind: "cumulative-repair", attempt: repairAttempt + 1 },
3038
+ request,
3039
+ )
3040
+ ).allowed
3041
+ ) {
3042
+ return discardUncommitted({
3043
+ kind: "paused",
3044
+ code: "repair-attempts-exhausted",
3045
+ });
3046
+ }
3047
+ repairAttempt += 1;
3048
+ const failureIdentities = [...affected.failureIdentities];
3049
+ const repair = await this.#runRepairCandidate({
3050
+ resources,
3051
+ ledger,
3052
+ attempt: request,
3053
+ repairAttempt,
3054
+ failureIdentities,
3055
+ });
3056
+ if (repair.kind !== "repair-committed") {
3057
+ return discardUncommitted(repair);
3058
+ }
3059
+ lastCommit = repair.commit;
3060
+ uncommittedRevisionId = repair.commit.isolatedRevisionId;
3061
+ repairEvents.push({
3062
+ runId: request.runId,
3063
+ taskId: request.taskId,
3064
+ eventId: `cumulative-repair-${hash(
3065
+ stableRevisionId,
3066
+ request.taskId,
3067
+ String(repairAttempt),
3068
+ ).slice(0, 40)}`,
3069
+ kind: "repair-verified",
3070
+ phase: request.phase,
3071
+ attempt: repairAttempt,
3072
+ commandId: request.task.repairVerification.id,
3073
+ exitCode: 0,
3074
+ diagnostic: repair.commit.diagnostic,
3075
+ artifactHash: repair.commit.artifactHash,
3076
+ isolatedRevisionId: repair.commit.isolatedRevisionId,
3077
+ outputFacts: repair.commit.outputFacts,
3078
+ attribution: "introduced",
3079
+ failureIdentities,
3080
+ routeId: repair.commit.routeId,
3081
+ routeFingerprint: repair.commit.routeFingerprint,
3082
+ });
3083
+ affected = await this.#verifyTaskAffected({
3084
+ resources,
3085
+ baseline: input.baseline,
3086
+ task: request.task,
3087
+ revisionId: repair.commit.isolatedRevisionId,
3088
+ signal: request.signal,
3089
+ });
3090
+ }
3091
+ if (affected.kind !== "verified") return discardUncommitted(affected);
3092
+ if (!lastCommit || repairEvents.length === 0) {
3093
+ throw new Error("workflow-repair-commit-unavailable");
3094
+ }
3095
+ ledger.commitVerifiedEvents(repairEvents);
3096
+ return {
3097
+ kind: "phase-committed",
3098
+ artifactHash: lastCommit.artifactHash,
3099
+ isolatedRevisionId: lastCommit.isolatedRevisionId,
3100
+ exitCode: 0,
3101
+ classification: expectedClassification(request.phase),
3102
+ routeId: lastCommit.routeId,
3103
+ routeFingerprint: lastCommit.routeFingerprint,
3104
+ };
3105
+ }
3106
+
3107
+ async hasPendingVerification(
3108
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
3109
+ ): Promise<boolean> {
3110
+ if (!input.baselineRevisionId || !input.currentWorkspaceRevisionId)
3111
+ return false;
3112
+ const resources = await this.#prepareResources(input, input.signal);
3113
+ const ledger = this.#ledger(resources, input.deliveryRevision);
3114
+ const verificationIds = [
3115
+ input.task.phases[input.phase]?.verification.id,
3116
+ input.task.repairVerification?.id,
3117
+ ];
3118
+ return verificationIds.some(
3119
+ (id) =>
3120
+ id &&
3121
+ this.#pendingCandidate(
3122
+ ledger,
3123
+ `pending-${hash(input.taskId, input.phase, id).slice(0, 40)}`,
3124
+ resources.currentRevisionId,
3125
+ ) !== undefined,
3126
+ );
3127
+ }
3128
+
3129
+ isContextReadAvailable(
3130
+ input: Parameters<NonNullable<WorkflowWorker["isContextReadAvailable"]>>[0],
3131
+ ): boolean {
3132
+ const resources = this.#resources(input);
3133
+ const revision = resources.workspaces.getRevision(
3134
+ input.currentWorkspaceRevisionId,
3135
+ );
3136
+ return revision.entries[input.path]?.kind === "file";
3137
+ }
3138
+
3139
+ async runAttempt(
3140
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
3141
+ ): Promise<WorkflowAttemptOutcome> {
3142
+ if (input.signal.aborted) {
3143
+ return { kind: "operation-cancelled", code: "cancelled" };
3144
+ }
3145
+ const resources = await this.#prepareResources(
3146
+ {
3147
+ runId: input.runId,
3148
+ deliveryRevision: input.deliveryRevision,
3149
+ plan: input.plan,
3150
+ ...(input.baselineRevisionId
3151
+ ? { baselineRevisionId: input.baselineRevisionId }
3152
+ : {}),
3153
+ ...(input.currentWorkspaceRevisionId
3154
+ ? { currentWorkspaceRevisionId: input.currentWorkspaceRevisionId }
3155
+ : {}),
3156
+ },
3157
+ input.signal,
3158
+ );
3159
+ let selectedRoute: Required<WorkflowRouteFacts> | undefined;
3160
+ const retained = <T extends WorkflowAttemptOutcome>(outcome: T): T =>
3161
+ ({
3162
+ ...(selectedRoute ?? {}),
3163
+ ...outcome,
3164
+ baselineRevisionId: resources.baselineRevisionId,
3165
+ currentWorkspaceRevisionId: resources.currentRevisionId,
3166
+ }) as T;
3167
+ const ledger = this.#ledger(resources, input.deliveryRevision);
3168
+ let verificationBaseline: DurableVerificationBaseline | undefined;
3169
+ if (hasVerificationLifecycle(input.plan)) {
3170
+ emitWorkflowActivity(input.onActivity, { state: "verifying" });
3171
+ const captured = await this.#ensureVerificationBaseline({
3172
+ resources,
3173
+ ledger,
3174
+ signal: input.signal,
3175
+ });
3176
+ if (!captured.ok) {
3177
+ const outcome = captured.outcome;
3178
+ return retained(
3179
+ outcome.kind === "paused"
3180
+ ? {
3181
+ ...outcome,
3182
+ verification: {
3183
+ scope: "baseline",
3184
+ attribution: "environment",
3185
+ },
3186
+ }
3187
+ : outcome,
3188
+ );
3189
+ }
3190
+ verificationBaseline = captured.baseline;
3191
+ }
3192
+ this.#openTask(resources, ledger, input);
3193
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
3194
+ return retained(
3195
+ await this.#runRedArtifactCorrection({
3196
+ resources,
3197
+ ledger,
3198
+ ...(verificationBaseline ? { baseline: verificationBaseline } : {}),
3199
+ attempt: input,
3200
+ }),
3201
+ );
3202
+ }
3203
+ if (input.repair) {
3204
+ if (!verificationBaseline) {
3205
+ throw new Error("workflow-verification-baseline-unavailable");
3206
+ }
3207
+ return retained(
3208
+ await this.#runCumulativeRepair({
3209
+ resources,
3210
+ ledger,
3211
+ baseline: verificationBaseline,
3212
+ attempt: input,
3213
+ }),
3214
+ );
3215
+ }
3216
+ const policyFailure = await this.#revalidatePhasePolicy(
3217
+ resources,
3218
+ ledger,
3219
+ input.task,
3220
+ input.signal,
3221
+ );
3222
+ if (policyFailure) return retained(policyFailure);
3223
+ const replay = this.#replayedPhase(resources, ledger, input);
3224
+ if (replay) return retained(replay);
3225
+ const phase = input.task.phases[input.phase];
3226
+ if (!phase) throw new Error("workflow-task-phase-invalid");
3227
+ const approvedPaths = [
3228
+ ...new Set([...phase.write, ...phase.delete]),
3229
+ ].sort();
3230
+ const rollbackPaths = [
3231
+ ...new Set(
3232
+ Object.values(input.task.phases).flatMap((boundary) => [
3233
+ ...boundary.write,
3234
+ ...boundary.delete,
3235
+ ]),
3236
+ ),
3237
+ ].sort();
3238
+ if (approvedPaths.length === 0) {
3239
+ return retained({
3240
+ kind: "approval-needed",
3241
+ code: "task-write-set-empty",
3242
+ });
3243
+ }
3244
+ const baseRevisionId = resources.currentRevisionId;
3245
+ const pendingKey = `pending-${hash(input.taskId, input.phase, phase.verification.id).slice(0, 40)}`;
3246
+ const pending = this.#pendingCandidate(ledger, pendingKey, baseRevisionId);
3247
+ const proposalRoot = mkdtempSync(path.join(resources.root, "attempt-"));
3248
+ try {
3249
+ await resources.workspaces.materializeAsync(
3250
+ baseRevisionId,
3251
+ proposalRoot,
3252
+ input.signal,
3253
+ );
3254
+ if (input.routeId && !pending) {
3255
+ const rebound = this.#broker.resumeBinding({
3256
+ runId: input.runId,
3257
+ taskId: input.taskId,
3258
+ role: "implementation-worker",
3259
+ routeId: input.routeId,
3260
+ ...(input.routeFingerprint
3261
+ ? { expectedFingerprint: input.routeFingerprint }
3262
+ : {}),
3263
+ requirements: implementationRouteRequirements({
3264
+ task: input.task,
3265
+ ...(input.artifactCorrection
3266
+ ? { artifactCorrection: input.artifactCorrection }
3267
+ : {}),
3268
+ ...(input.repair ? { repair: input.repair } : {}),
3269
+ }),
3270
+ });
3271
+ if (!rebound.ok) {
3272
+ return retained({ kind: "paused", code: rebound.code });
3273
+ }
3274
+ }
3275
+ const ledgerProjection = ledger.projection({
3276
+ runId: input.runId,
3277
+ taskId: input.taskId,
3278
+ nextPhase: input.phase,
3279
+ });
3280
+ let identity: BeginCandidateInput | undefined = pending?.identity;
3281
+ const routed = pending
3282
+ ? {
3283
+ ok: true as const,
3284
+ value: pending.proposal,
3285
+ routeId: pending.identity.routeId,
3286
+ routeFingerprint: pending.identity.routeFingerprint,
3287
+ }
3288
+ : await this.#broker.run({
3289
+ runId: input.runId,
3290
+ taskId: input.taskId,
3291
+ operationId: `${input.operationId}:${input.taskId}:${input.phase}`,
3292
+ role: "implementation-worker",
3293
+ requirements: implementationRouteRequirements({
3294
+ task: input.task,
3295
+ ...(input.artifactCorrection
3296
+ ? { artifactCorrection: input.artifactCorrection }
3297
+ : {}),
3298
+ ...(input.repair ? { repair: input.repair } : {}),
3299
+ }),
3300
+ signal: input.signal,
3301
+ ...(input.onActivity ? { onActivity: input.onActivity } : {}),
3302
+ classifyResult: semanticRouteFailure,
3303
+ execute: async (attempt) => {
3304
+ identity = {
3305
+ candidateId: `candidate-${randomUUID()}`,
3306
+ runId: input.runId,
3307
+ deliveryRevision: input.deliveryRevision,
3308
+ taskId: input.taskId,
3309
+ phase: input.phase,
3310
+ attemptId: `attempt-${hash(
3311
+ input.operationId,
3312
+ input.taskId,
3313
+ input.phase,
3314
+ attempt.route.fingerprint,
3315
+ ).slice(0, 40)}`,
3316
+ approvedPaths,
3317
+ isolatedRevisionId: baseRevisionId,
3318
+ verificationId: phase.verification.id,
3319
+ routeId: attempt.route.id,
3320
+ routeFingerprint: attempt.route.fingerprint,
3321
+ };
3322
+ if (input.reserveCandidate && !input.reserveCandidate())
3323
+ return {
3324
+ kind: "paused" as const,
3325
+ code:
3326
+ input.additionalAttempt || input.verificationOnly
3327
+ ? "repair-attempts-exhausted"
3328
+ : "change-work-budget-exhausted",
3329
+ };
3330
+ return this.#options.proposeCandidate({
3331
+ runId: input.runId,
3332
+ operationId: input.operationId,
3333
+ deliveryRevision: input.deliveryRevision,
3334
+ taskId: input.taskId,
3335
+ phase: input.phase,
3336
+ task: structuredClone(input.task),
3337
+ contextReadPaths: input.contextReadPaths,
3338
+ workspaceRoot: proposalRoot,
3339
+ ledgerProjection,
3340
+ candidateArtifact: {
3341
+ ledger,
3342
+ identity,
3343
+ workspaceRoot: proposalRoot,
3344
+ writePaths: phase.write,
3345
+ deletePaths: phase.delete,
3346
+ },
3347
+ route: attempt.route,
3348
+ ...(input.contextRequest
3349
+ ? { contextRequest: input.contextRequest }
3350
+ : {}),
3351
+ ...(input.recoveryFeedback
3352
+ ? { recoveryFeedback: input.recoveryFeedback }
3353
+ : {}),
3354
+ signal: attempt.signal,
3355
+ onHeaders: attempt.onHeaders,
3356
+ onProgress: attempt.onProgress,
3357
+ });
3358
+ },
3359
+ });
3360
+ if (!routed.ok) {
3361
+ return retained(
3362
+ routed.state === "cancelled" || input.signal.aborted
3363
+ ? { kind: "operation-cancelled", code: "cancelled" }
3364
+ : { kind: "paused", code: routed.code },
3365
+ );
3366
+ }
3367
+ if (!identity || identity.routeId !== routed.routeId) {
3368
+ throw new Error("candidate-route-identity-invalid");
3369
+ }
3370
+ const correctionIdentity = identity;
3371
+ selectedRoute = {
3372
+ routeId: identity.routeId,
3373
+ routeFingerprint: identity.routeFingerprint,
3374
+ };
3375
+ let uncommittedRevisionId: string | undefined;
3376
+ const reject = async <T extends WorkflowAttemptOutcome>(
3377
+ outcome: T,
3378
+ correction: {
3379
+ category: "artifact" | "stale" | "verification" | "checkpoint";
3380
+ code: string;
3381
+ stage: string;
3382
+ },
3383
+ rollback = false,
3384
+ ): Promise<T> => {
3385
+ if (rollback && uncommittedRevisionId) {
3386
+ await this.#rollbackWorkspacePaths({
3387
+ resources,
3388
+ baseRevisionId,
3389
+ rejectedRevisionId: uncommittedRevisionId,
3390
+ paths: rollbackPaths,
3391
+ });
3392
+ }
3393
+ this.#recordCorrection(ledger, input, correctionIdentity, correction);
3394
+ return retained({ ...outcome, retryPolicy: correction.category });
3395
+ };
3396
+ const proposal = routed.value;
3397
+ if (
3398
+ proposal.kind !== "candidate" &&
3399
+ proposal.kind !== "sealed-candidate"
3400
+ ) {
3401
+ return proposal.kind === "operation-cancelled" ||
3402
+ proposal.kind === "paused" ||
3403
+ proposal.kind === "approval-needed"
3404
+ ? retained(proposal)
3405
+ : reject(proposal, {
3406
+ category: "artifact",
3407
+ code: proposal.code,
3408
+ stage: "candidate-submit",
3409
+ });
3410
+ }
3411
+ emitWorkflowActivity(input.onActivity, { state: "validating" });
3412
+ ledger.beginCandidate(identity);
3413
+ let bytes: Buffer;
3414
+ if (proposal.kind === "sealed-candidate") {
3415
+ if (
3416
+ proposal.candidateId !== identity.candidateId ||
3417
+ !SHA256.test(proposal.artifactHash) ||
3418
+ !Number.isSafeInteger(proposal.bytes) ||
3419
+ proposal.bytes < 1 ||
3420
+ !Array.isArray(proposal.paths)
3421
+ ) {
3422
+ return reject(
3423
+ { kind: "retryable", code: "candidate-diff-invalid" },
3424
+ {
3425
+ category: "artifact",
3426
+ code: "candidate-diff-invalid",
3427
+ stage: "candidate-seal",
3428
+ },
3429
+ );
3430
+ }
3431
+ bytes = Buffer.from(ledger.readSealedCandidate(identity.candidateId));
3432
+ if (
3433
+ bytes.length !== proposal.bytes ||
3434
+ sha256Bytes(bytes) !== proposal.artifactHash
3435
+ ) {
3436
+ throw new Error("candidate-seal-integrity-invalid");
3437
+ }
3438
+ } else {
3439
+ if (
3440
+ !(proposal.bytes instanceof Uint8Array) ||
3441
+ proposal.bytes.length === 0
3442
+ ) {
3443
+ return reject(
3444
+ { kind: "retryable", code: "candidate-diff-invalid" },
3445
+ {
3446
+ category: "artifact",
3447
+ code: "candidate-diff-invalid",
3448
+ stage: "candidate-submit",
3449
+ },
3450
+ );
3451
+ }
3452
+ bytes = Buffer.from(proposal.bytes);
3453
+ }
3454
+ let candidatePaths: string[];
3455
+ try {
3456
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3457
+ candidatePaths = diffWritePaths(text).paths;
3458
+ } catch {
3459
+ return reject(
3460
+ { kind: "retryable", code: "candidate-diff-invalid" },
3461
+ {
3462
+ category: "artifact",
3463
+ code: "candidate-diff-invalid",
3464
+ stage: "candidate-diff",
3465
+ },
3466
+ );
3467
+ }
3468
+ if (
3469
+ candidatePaths.some((relative) => !approvedPaths.includes(relative))
3470
+ ) {
3471
+ return reject(
3472
+ { kind: "retryable", code: "write-set-mismatch" },
3473
+ {
3474
+ category: "artifact",
3475
+ code: "write-set-mismatch",
3476
+ stage: "candidate-boundary",
3477
+ },
3478
+ );
3479
+ }
3480
+ if (proposal.kind === "sealed-candidate") {
3481
+ if (
3482
+ JSON.stringify([...proposal.paths].sort()) !==
3483
+ JSON.stringify([...candidatePaths].sort())
3484
+ ) {
3485
+ return reject(
3486
+ { kind: "retryable", code: "candidate-diff-invalid" },
3487
+ {
3488
+ category: "artifact",
3489
+ code: "candidate-diff-invalid",
3490
+ stage: "candidate-seal",
3491
+ },
3492
+ );
3493
+ }
3494
+ }
3495
+ const dependencySensitive = candidatePaths.some(
3496
+ (relative) => relative === "package.json" || LOCKFILES.has(relative),
3497
+ );
3498
+ let dependenciesBefore: Map<string, DependencyContractEntry> | undefined;
3499
+ if (dependencySensitive) {
3500
+ try {
3501
+ dependenciesBefore = dependencyContract(proposalRoot);
3502
+ } catch (error) {
3503
+ if (!(error instanceof DependencyManifestError)) throw error;
3504
+ return reject(
3505
+ { kind: "retryable", code: error.message },
3506
+ {
3507
+ category: "artifact",
3508
+ code: error.message,
3509
+ stage: "candidate-dependency",
3510
+ },
3511
+ );
3512
+ }
3513
+ }
3514
+ const checked = await runGitApply(
3515
+ proposalRoot,
3516
+ bytes,
3517
+ true,
3518
+ input.signal,
3519
+ );
3520
+ if (checked !== "ok") {
3521
+ return checked === "cancelled"
3522
+ ? retained({ kind: "operation-cancelled", code: "cancelled" })
3523
+ : reject(
3524
+ { kind: "retryable", code: "candidate-diff-invalid" },
3525
+ {
3526
+ category: "artifact",
3527
+ code: "candidate-diff-invalid",
3528
+ stage: "candidate-check",
3529
+ },
3530
+ );
3531
+ }
3532
+ const applied = await runGitApply(
3533
+ proposalRoot,
3534
+ bytes,
3535
+ false,
3536
+ input.signal,
3537
+ );
3538
+ if (applied !== "ok") {
3539
+ return applied === "cancelled"
3540
+ ? retained({ kind: "operation-cancelled", code: "cancelled" })
3541
+ : reject(
3542
+ { kind: "retryable", code: "candidate-diff-invalid" },
3543
+ {
3544
+ category: "artifact",
3545
+ code: "candidate-diff-invalid",
3546
+ stage: "candidate-apply",
3547
+ },
3548
+ );
3549
+ }
3550
+ if (dependenciesBefore) {
3551
+ let dependenciesAfter: Map<string, DependencyContractEntry>;
3552
+ try {
3553
+ dependenciesAfter = dependencyContract(proposalRoot);
3554
+ } catch (error) {
3555
+ if (!(error instanceof DependencyManifestError)) throw error;
3556
+ return reject(
3557
+ { kind: "retryable", code: error.message },
3558
+ {
3559
+ category: "artifact",
3560
+ code: error.message,
3561
+ stage: "candidate-dependency",
3562
+ },
3563
+ );
3564
+ }
3565
+ if (
3566
+ hasUnapprovedDependencyChange(
3567
+ dependenciesBefore,
3568
+ dependenciesAfter,
3569
+ input.task.approvedDependencies,
3570
+ candidatePaths.some((relative) => LOCKFILES.has(relative)),
3571
+ )
3572
+ ) {
3573
+ return reject(
3574
+ { kind: "approval-needed", code: "unapproved-dependency-change" },
3575
+ {
3576
+ category: "artifact",
3577
+ code: "unapproved-dependency-change",
3578
+ stage: "candidate-dependency",
3579
+ },
3580
+ );
3581
+ }
3582
+ }
3583
+ let artifactHash: string;
3584
+ if (proposal.kind === "sealed-candidate") {
3585
+ artifactHash = proposal.artifactHash;
3586
+ } else {
3587
+ let sequence = 0;
3588
+ for (
3589
+ let offset = 0;
3590
+ offset < bytes.length;
3591
+ offset += TASK_LEDGER_LIMITS.maxSegmentBytes
3592
+ ) {
3593
+ const segment = bytes.subarray(
3594
+ offset,
3595
+ Math.min(offset + TASK_LEDGER_LIMITS.maxSegmentBytes, bytes.length),
3596
+ );
3597
+ const accepted = ledger.appendCandidateSegment({
3598
+ ...identity,
3599
+ sequence,
3600
+ bytes: segment,
3601
+ segmentHash: sha256Bytes(segment),
3602
+ });
3603
+ if (!accepted.ok) {
3604
+ return reject(
3605
+ { kind: "paused", code: accepted.code },
3606
+ {
3607
+ category: "artifact",
3608
+ code: accepted.code,
3609
+ stage: "candidate-segment",
3610
+ },
3611
+ );
3612
+ }
3613
+ sequence += 1;
3614
+ }
3615
+ const sealed = ledger.sealCandidate({
3616
+ ...identity,
3617
+ segmentCount: sequence,
3618
+ totalBytes: bytes.length,
3619
+ candidateHash: sha256Bytes(bytes),
3620
+ });
3621
+ if (!sealed.ok) {
3622
+ return reject(
3623
+ { kind: "retryable", code: sealed.code },
3624
+ {
3625
+ category: "artifact",
3626
+ code: sealed.code,
3627
+ stage: "candidate-seal",
3628
+ },
3629
+ );
3630
+ }
3631
+ artifactHash = sealed.artifactHash;
3632
+ }
3633
+ const candidate = resources.workspaces.createRevision({
3634
+ parentRevisionId: baseRevisionId,
3635
+ changes: revisionChanges(proposalRoot, approvedPaths),
3636
+ });
3637
+ if (
3638
+ candidatePaths.some((relative) => {
3639
+ const entry = candidate.entries[relative];
3640
+ return phase.delete.includes(relative)
3641
+ ? entry?.kind !== "absent"
3642
+ : entry?.kind !== "file";
3643
+ })
3644
+ ) {
3645
+ return reject(
3646
+ { kind: "retryable", code: "write-set-mismatch" },
3647
+ {
3648
+ category: "artifact",
3649
+ code: "write-set-mismatch",
3650
+ stage: "candidate-output",
3651
+ },
3652
+ );
3653
+ }
3654
+ const requiredOutputs = input.plan.outputs.filter(
3655
+ (output) =>
3656
+ output.producer.taskId === input.taskId &&
3657
+ output.producer.phase === input.phase,
3658
+ );
3659
+ if (
3660
+ requiredOutputs.some(
3661
+ (output) => candidate.entries[output.path]?.kind !== "file",
3662
+ )
3663
+ ) {
3664
+ return reject(
3665
+ { kind: "retryable", code: "producer-output-unavailable" },
3666
+ {
3667
+ category: "artifact",
3668
+ code: "producer-output-unavailable",
3669
+ stage: "candidate-output",
3670
+ },
3671
+ );
3672
+ }
3673
+ emitWorkflowActivity(input.onActivity, { state: "verifying" });
3674
+ this.#savePendingCandidate(ledger, pendingKey, {
3675
+ identity,
3676
+ proposal: {
3677
+ kind: "sealed-candidate",
3678
+ candidateId: identity.candidateId,
3679
+ artifactHash,
3680
+ bytes: bytes.length,
3681
+ paths: candidatePaths,
3682
+ },
3683
+ });
3684
+ const verified = await this.#verifyPhase({
3685
+ executionWritePaths: this.#executionWritePaths(resources),
3686
+ runId: input.runId,
3687
+ deliveryRevision: input.deliveryRevision,
3688
+ taskId: input.taskId,
3689
+ phase: input.phase,
3690
+ root: proposalRoot,
3691
+ verification: structuredClone(phase.verification),
3692
+ signal: input.signal,
3693
+ });
3694
+ if (!verified.ok) {
3695
+ if (verified.kind === "paused") return retained(verified);
3696
+ this.#savePendingCandidate(ledger, pendingKey, null);
3697
+ return reject(verified, {
3698
+ category: "verification",
3699
+ code: verified.code,
3700
+ stage: "phase-verification",
3701
+ });
3702
+ }
3703
+ const expected = expectedClassification(input.phase);
3704
+ if (
3705
+ verified.classification !== expected ||
3706
+ (input.phase === "red"
3707
+ ? verified.exitCode === 0
3708
+ : verified.exitCode !== 0)
3709
+ ) {
3710
+ return reject(
3711
+ { kind: "retryable", code: "verification-rejected" },
3712
+ {
3713
+ category: "verification",
3714
+ code: "verification-rejected",
3715
+ stage: "phase-verification",
3716
+ },
3717
+ );
3718
+ }
3719
+ let merged: ReturnType<WorkspaceStore["mergeRevision"]>;
3720
+ try {
3721
+ merged = await this.#mergeCandidate(
3722
+ resources,
3723
+ input,
3724
+ baseRevisionId,
3725
+ candidate.revisionId,
3726
+ );
3727
+ uncommittedRevisionId = merged.revisionId;
3728
+ } catch (error) {
3729
+ if (
3730
+ error instanceof Error &&
3731
+ error.message === "workspace-revision-stale"
3732
+ ) {
3733
+ return reject(
3734
+ { kind: "retryable", code: "workspace-revision-stale" },
3735
+ {
3736
+ category: "stale",
3737
+ code: "workspace-revision-stale",
3738
+ stage: "candidate-merge",
3739
+ },
3740
+ );
3741
+ }
3742
+ throw error;
3743
+ }
3744
+ const outputFacts = approvedPaths.map((relative) => {
3745
+ const entry = merged.entries[relative];
3746
+ return entry?.kind === "file"
3747
+ ? {
3748
+ path: relative,
3749
+ kind: "file" as const,
3750
+ hash: entry.hash,
3751
+ bytes: entry.bytes,
3752
+ }
3753
+ : { path: relative, kind: "absent" as const };
3754
+ });
3755
+ if (
3756
+ requiredOutputs.some(
3757
+ (output) => merged.entries[output.path]?.kind !== "file",
3758
+ )
3759
+ ) {
3760
+ return reject(
3761
+ { kind: "retryable", code: "producer-output-unavailable" },
3762
+ {
3763
+ category: "artifact",
3764
+ code: "producer-output-unavailable",
3765
+ stage: "candidate-output",
3766
+ },
3767
+ true,
3768
+ );
3769
+ }
3770
+ const phaseEvent: VerifiedTaskEvent = {
3771
+ runId: input.runId,
3772
+ taskId: input.taskId,
3773
+ eventId: `${input.phase}-verified-r${input.deliveryRevision}`,
3774
+ kind: "phase-verified",
3775
+ phase: input.phase,
3776
+ commandId: phase.verification.id,
3777
+ exitCode: verified.exitCode,
3778
+ expectedClassification: expected,
3779
+ actualClassification: verified.classification,
3780
+ diagnostic: verified.diagnostic,
3781
+ artifactHash,
3782
+ isolatedRevisionId: merged.revisionId,
3783
+ outputFacts,
3784
+ routeId: identity.routeId,
3785
+ routeFingerprint: identity.routeFingerprint,
3786
+ };
3787
+ if (this.#options.verificationPolicy || resources.environmentIdentity)
3788
+ ledger.putDurableFact(
3789
+ this.#policyFactKey(
3790
+ resources,
3791
+ phaseEvent as unknown as Record<string, unknown>,
3792
+ ),
3793
+ { policy: this.#options.verificationPolicy ?? "legacy" },
3794
+ );
3795
+ const finalPhase = input.task.phases.refactor ? "refactor" : "green";
3796
+ if (!verificationBaseline || input.phase !== finalPhase) {
3797
+ ledger.commitVerifiedEvent(phaseEvent);
3798
+ return retained({
3799
+ kind: "phase-committed",
3800
+ artifactHash,
3801
+ isolatedRevisionId: merged.revisionId,
3802
+ exitCode: verified.exitCode,
3803
+ classification: verified.classification,
3804
+ baselineRevisionId: resources.baselineRevisionId,
3805
+ });
3806
+ }
3807
+
3808
+ const completionEvents: VerifiedTaskEvent[] = [phaseEvent];
3809
+ const discardUncommitted = async <T extends WorkflowAttemptOutcome>(
3810
+ outcome: T,
3811
+ ): Promise<T> => {
3812
+ if (uncommittedRevisionId) {
3813
+ await this.#rollbackWorkspacePaths({
3814
+ resources,
3815
+ baseRevisionId,
3816
+ rejectedRevisionId: uncommittedRevisionId,
3817
+ paths: rollbackPaths,
3818
+ });
3819
+ }
3820
+ return retained(outcome);
3821
+ };
3822
+ let affected = await this.#verifyTaskAffected({
3823
+ resources,
3824
+ baseline: verificationBaseline,
3825
+ task: input.task,
3826
+ revisionId: merged.revisionId,
3827
+ signal: input.signal,
3828
+ });
3829
+ let repairAttempts = 0;
3830
+ let completionAttribution: "none" | "pre-existing" | "introduced" =
3831
+ "none";
3832
+ while (affected.kind === "repairable") {
3833
+ completionAttribution = "introduced";
3834
+ if (
3835
+ !(
3836
+ input.recoveryDecision?.({
3837
+ kind: "affected-repair",
3838
+ attempt: repairAttempts + 1,
3839
+ }) ??
3840
+ decideRecoveryAction(
3841
+ input.plan,
3842
+ { kind: "affected-repair", attempt: repairAttempts + 1 },
3843
+ input,
3844
+ )
3845
+ ).allowed
3846
+ ) {
3847
+ return reject(
3848
+ { kind: "paused", code: "repair-attempts-exhausted" },
3849
+ {
3850
+ category: "verification",
3851
+ code: "repair-attempts-exhausted",
3852
+ stage: "task-affected-verification",
3853
+ },
3854
+ true,
3855
+ );
3856
+ }
3857
+ repairAttempts += 1;
3858
+ const failureIdentities = [...affected.failureIdentities];
3859
+ const repair = await this.#runRepairCandidate({
3860
+ resources,
3861
+ ledger,
3862
+ attempt: input,
3863
+ repairAttempt: repairAttempts,
3864
+ failureIdentities,
3865
+ });
3866
+ if (repair.kind !== "repair-committed") {
3867
+ return discardUncommitted(repair);
3868
+ }
3869
+ selectedRoute = {
3870
+ routeId: repair.commit.routeId,
3871
+ routeFingerprint: repair.commit.routeFingerprint,
3872
+ };
3873
+ uncommittedRevisionId = repair.commit.isolatedRevisionId;
3874
+ completionEvents.push({
3875
+ runId: input.runId,
3876
+ taskId: input.taskId,
3877
+ eventId: `repair-${repairAttempts}-verified-r${input.deliveryRevision}`,
3878
+ kind: "repair-verified",
3879
+ phase: input.phase,
3880
+ attempt: repairAttempts,
3881
+ commandId: input.task.repairVerification.id,
3882
+ exitCode: 0,
3883
+ diagnostic: repair.commit.diagnostic,
3884
+ artifactHash: repair.commit.artifactHash,
3885
+ isolatedRevisionId: repair.commit.isolatedRevisionId,
3886
+ outputFacts: repair.commit.outputFacts,
3887
+ attribution: "introduced",
3888
+ failureIdentities,
3889
+ routeId: repair.commit.routeId,
3890
+ routeFingerprint: repair.commit.routeFingerprint,
3891
+ });
3892
+ affected = await this.#verifyTaskAffected({
3893
+ resources,
3894
+ baseline: verificationBaseline,
3895
+ task: input.task,
3896
+ revisionId: repair.commit.isolatedRevisionId,
3897
+ signal: input.signal,
3898
+ });
3899
+ }
3900
+ if (affected.kind !== "verified") {
3901
+ return affected.kind === "operation-cancelled"
3902
+ ? discardUncommitted(affected)
3903
+ : reject(
3904
+ affected,
3905
+ {
3906
+ category: "verification",
3907
+ code: affected.code,
3908
+ stage: "task-affected-verification",
3909
+ },
3910
+ true,
3911
+ );
3912
+ }
3913
+ if (completionAttribution === "none") {
3914
+ completionAttribution = affected.attribution;
3915
+ }
3916
+ let completedRevision: WorkspaceRevision;
3917
+ try {
3918
+ completedRevision = this.#completeTracking(resources, input.taskId);
3919
+ } catch (error) {
3920
+ if (
3921
+ error instanceof Error &&
3922
+ error.message === "workflow-tracking-task-mismatch"
3923
+ ) {
3924
+ return reject(
3925
+ { kind: "paused", code: "tracking-contract-invalid" },
3926
+ {
3927
+ category: "checkpoint",
3928
+ code: "tracking-contract-invalid",
3929
+ stage: "tracking-checkpoint",
3930
+ },
3931
+ true,
3932
+ );
3933
+ }
3934
+ throw error;
3935
+ }
3936
+ const factKey = `task-final-${hash(input.taskId).slice(0, 40)}`;
3937
+ ledger.commitTaskCompletion({
3938
+ events: completionEvents,
3939
+ factKey,
3940
+ fact: {
3941
+ taskId: input.taskId,
3942
+ phase: input.phase,
3943
+ isolatedRevisionId: completedRevision.revisionId,
3944
+ attribution: completionAttribution,
3945
+ repairAttempts,
3946
+ },
3947
+ });
3948
+ return retained({
3949
+ kind: "phase-committed",
3950
+ artifactHash,
3951
+ isolatedRevisionId: completedRevision.revisionId,
3952
+ exitCode: verified.exitCode,
3953
+ classification: verified.classification,
3954
+ baselineRevisionId: resources.baselineRevisionId,
3955
+ });
3956
+ } finally {
3957
+ rmSync(proposalRoot, { recursive: true, force: true });
3958
+ }
3959
+ }
3960
+
3961
+ rebind(input: Parameters<WorkflowWorker["rebind"]>[0]) {
3962
+ const rebound = this.#broker.rebind({
3963
+ runId: input.runId,
3964
+ taskId: input.taskId,
3965
+ role: input.role,
3966
+ routeId: input.routeId,
3967
+ requirements: implementationRouteRequirements({ task: input.task }),
3968
+ });
3969
+ return rebound.ok
3970
+ ? {
3971
+ ok: true as const,
3972
+ routeId: rebound.route.id,
3973
+ routeFingerprint: rebound.route.fingerprint,
3974
+ }
3975
+ : { ok: false as const, code: rebound.code };
3976
+ }
3977
+
3978
+ async verify(
3979
+ input: Parameters<WorkflowChangeVerifier["verify"]>[0],
3980
+ ): Promise<Awaited<ReturnType<WorkflowChangeVerifier["verify"]>>> {
3981
+ if (!input.currentWorkspaceRevisionId) {
3982
+ return { kind: "paused", code: "workspace-revision-unavailable" };
3983
+ }
3984
+ const resources = await this.#prepareResources(
3985
+ {
3986
+ runId: input.runId,
3987
+ deliveryRevision: input.deliveryRevision,
3988
+ plan: input.plan,
3989
+ currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
3990
+ baselineRevisionId:
3991
+ input.baselineRevisionId ??
3992
+ this.#runs.get(input.runId)?.baselineRevisionId,
3993
+ },
3994
+ input.signal,
3995
+ );
3996
+ try {
3997
+ await this.#refreshEnvironment(resources, input.signal);
3998
+ } catch {
3999
+ return { kind: "paused", code: "verification-environment-unavailable" };
4000
+ }
4001
+ for (const reference of input.taskEvidence ?? []) {
4002
+ const task = input.plan.tasks.find(
4003
+ (task) => task.taskId === reference.taskId,
4004
+ );
4005
+ if (!task) throw new Error("workflow-ledger-task-invalid");
4006
+ const failure = await this.#revalidatePhasePolicy(
4007
+ resources,
4008
+ this.#ledger(resources, reference.deliveryRevision),
4009
+ task,
4010
+ input.signal,
4011
+ );
4012
+ if (failure)
4013
+ return {
4014
+ kind: "paused",
4015
+ code:
4016
+ "code" in failure ? failure.code : "verification-policy-rejected",
4017
+ };
4018
+ }
4019
+ const cumulativeRevision = resources.workspaces.getRevision(
4020
+ resources.currentRevisionId,
4021
+ );
4022
+ if (
4023
+ input.plan.outputs.some(
4024
+ (output) => cumulativeRevision.entries[output.path]?.kind !== "file",
4025
+ )
4026
+ ) {
4027
+ return { kind: "paused", code: "producer-output-unavailable" };
4028
+ }
4029
+ if (hasVerificationLifecycle(input.plan)) {
4030
+ const ledger = this.#ledger(resources, input.deliveryRevision);
4031
+ const captured = await this.#ensureVerificationBaseline({
4032
+ resources,
4033
+ ledger,
4034
+ signal: input.signal,
4035
+ });
4036
+ if (!captured.ok) {
4037
+ return captured.outcome.kind === "approval-needed"
4038
+ ? captured.outcome
4039
+ : captured.outcome.kind === "operation-cancelled"
4040
+ ? { kind: "paused", code: "cancelled" }
4041
+ : {
4042
+ ...captured.outcome,
4043
+ verification: {
4044
+ scope: "baseline",
4045
+ attribution: "environment",
4046
+ },
4047
+ };
4048
+ }
4049
+ for (const task of input.plan.tasks) {
4050
+ const baseline = captured.baseline.affected.find(
4051
+ (entry) => entry.taskId === task.taskId,
4052
+ );
4053
+ if (!baseline) throw new Error("workflow-task-baseline-unavailable");
4054
+ const current = await this.#observeVerification({
4055
+ resources,
4056
+ revisionId: resources.currentRevisionId,
4057
+ scope: "change-task-affected",
4058
+ verification: task.affectedVerification,
4059
+ taskId: task.taskId,
4060
+ signal: input.signal,
4061
+ });
4062
+ if (!current.ok) {
4063
+ return current.outcome.kind === "approval-needed"
4064
+ ? current.outcome
4065
+ : current.outcome.kind === "operation-cancelled"
4066
+ ? { kind: "paused", code: "cancelled" }
4067
+ : {
4068
+ ...current.outcome,
4069
+ verification: {
4070
+ scope: "change-task-affected",
4071
+ attribution: "environment",
4072
+ taskId: task.taskId,
4073
+ },
4074
+ };
4075
+ }
4076
+ if (current.observation.status === "failed") {
4077
+ const prior = new Set(baseline.observation.failureIdentities);
4078
+ const introduced = current.observation.failureIdentities.filter(
4079
+ (identity) => !prior.has(identity),
4080
+ );
4081
+ if (introduced.length > 0) {
4082
+ return {
4083
+ kind: "paused",
4084
+ code: "verification-repair-required",
4085
+ verification: {
4086
+ scope: "change-task-affected",
4087
+ attribution: "introduced",
4088
+ taskId: task.taskId,
4089
+ failureIdentities: introduced.slice(0, 256),
4090
+ },
4091
+ };
4092
+ }
4093
+ }
4094
+ }
4095
+ const fullSuite = await this.#observeVerification({
4096
+ resources,
4097
+ revisionId: resources.currentRevisionId,
4098
+ scope: "change-full-suite",
4099
+ verification: input.plan.verification.change.fullSuite,
4100
+ signal: input.signal,
4101
+ });
4102
+ if (!fullSuite.ok) {
4103
+ return fullSuite.outcome.kind === "approval-needed"
4104
+ ? fullSuite.outcome
4105
+ : fullSuite.outcome.kind === "operation-cancelled"
4106
+ ? { kind: "paused", code: "cancelled" }
4107
+ : {
4108
+ ...fullSuite.outcome,
4109
+ verification: {
4110
+ scope: "change-full-suite",
4111
+ attribution: "environment",
4112
+ },
4113
+ };
4114
+ }
4115
+ if (fullSuite.observation.status === "failed") {
4116
+ const baselineFailures = new Set(
4117
+ captured.baseline.fullSuite.failureIdentities,
4118
+ );
4119
+ const introduced = fullSuite.observation.failureIdentities.filter(
4120
+ (identity) => !baselineFailures.has(identity),
4121
+ );
4122
+ if (introduced.length > 0) {
4123
+ return {
4124
+ kind: "paused",
4125
+ code: "verification-attribution-unresolved",
4126
+ verification: {
4127
+ scope: "change-full-suite",
4128
+ attribution: "unresolved",
4129
+ failureIdentities: introduced.slice(0, 256),
4130
+ },
4131
+ };
4132
+ }
4133
+ }
4134
+ const checkpoint = input.plan.verification.agentsCheckpoint;
4135
+ if (checkpoint.required && checkpoint.verification) {
4136
+ const applied = this.#applyAgentsCheckpoint(resources);
4137
+ if (!applied.ok) {
4138
+ return { kind: "paused", code: applied.code };
4139
+ }
4140
+ const observed = await this.#observeVerification({
4141
+ resources,
4142
+ revisionId: applied.revision.revisionId,
4143
+ scope: "agents-checkpoint",
4144
+ verification: checkpoint.verification,
4145
+ signal: input.signal,
4146
+ });
4147
+ if (!observed.ok) {
4148
+ return observed.outcome.kind === "approval-needed"
4149
+ ? observed.outcome
4150
+ : observed.outcome.kind === "operation-cancelled"
4151
+ ? { kind: "paused", code: "cancelled" }
4152
+ : {
4153
+ ...observed.outcome,
4154
+ verification: {
4155
+ scope: "agents-checkpoint",
4156
+ attribution: "environment",
4157
+ },
4158
+ };
4159
+ }
4160
+ if (observed.observation.status === "failed") {
4161
+ return {
4162
+ kind: "paused",
4163
+ code: "agents-checkpoint-verification-failed",
4164
+ verification: {
4165
+ scope: "agents-checkpoint",
4166
+ attribution: "introduced",
4167
+ failureIdentities: observed.observation.failureIdentities.slice(
4168
+ 0,
4169
+ 256,
4170
+ ),
4171
+ },
4172
+ };
4173
+ }
4174
+ resources.currentRevisionId = applied.revision.revisionId;
4175
+ }
4176
+ const verificationId = `change-${hash(
4177
+ input.plan.changeId,
4178
+ String(input.deliveryRevision),
4179
+ resources.currentRevisionId,
4180
+ ...(resources.environmentIdentity
4181
+ ? [resources.environmentIdentity]
4182
+ : []),
4183
+ ).slice(0, 40)}`;
4184
+ const sealed = await verifyCumulativeRevision({
4185
+ workspaceStore: resources.workspaces,
4186
+ revisionId: resources.currentRevisionId,
4187
+ verificationId,
4188
+ signal: input.signal,
4189
+ execute: async () => ({
4190
+ ok: true as const,
4191
+ exitCode: 0 as const,
4192
+ classification: "lifecycle-verified",
4193
+ }),
4194
+ });
4195
+ if (!sealed.ok) return { kind: "paused", code: sealed.code };
4196
+ resources.verificationFact = sealed.fact;
4197
+ return {
4198
+ kind: "verified",
4199
+ verificationId: sealed.verificationId,
4200
+ currentWorkspaceRevisionId: resources.currentRevisionId,
4201
+ };
4202
+ }
4203
+ const verificationId = `change-${hash(
4204
+ input.plan.changeId,
4205
+ String(input.deliveryRevision),
4206
+ ...(resources.environmentIdentity ? [resources.environmentIdentity] : []),
4207
+ ).slice(0, 40)}`;
4208
+ const result = await verifyCumulativeRevision({
4209
+ workspaceStore: resources.workspaces,
4210
+ revisionId: resources.currentRevisionId,
4211
+ verificationId,
4212
+ signal: input.signal,
4213
+ execute: ({ root, signal }) =>
4214
+ this.#verifyChange({
4215
+ runId: input.runId,
4216
+ deliveryRevision: input.deliveryRevision,
4217
+ root,
4218
+ plan: structuredClone(input.plan),
4219
+ signal: signal ?? input.signal,
4220
+ }),
4221
+ });
4222
+ if (!result.ok) {
4223
+ return {
4224
+ kind:
4225
+ result.kind === "approval-boundary"
4226
+ ? "approval-needed"
4227
+ : result.kind === "verification"
4228
+ ? "retryable"
4229
+ : "paused",
4230
+ code: result.code,
4231
+ };
4232
+ }
4233
+ resources.verificationFact = result.fact;
4234
+ return { kind: "verified", verificationId: result.verificationId };
4235
+ }
4236
+
4237
+ async prepare(
4238
+ input: Parameters<WorkflowApplication["begin"]>[0],
4239
+ ): Promise<Record<string, unknown>> {
4240
+ if (!input.baselineRevisionId || !input.currentWorkspaceRevisionId) {
4241
+ return { state: "paused", code: "workspace-revision-unavailable" };
4242
+ }
4243
+ const resources = await this.#prepareResources(
4244
+ {
4245
+ runId: input.runId,
4246
+ deliveryRevision: input.deliveryRevision,
4247
+ plan: input.plan,
4248
+ baselineRevisionId: input.baselineRevisionId,
4249
+ currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
4250
+ },
4251
+ input.signal,
4252
+ );
4253
+ if (!resources.verificationFact) {
4254
+ return { state: "paused", code: "verification-fact-unavailable" };
4255
+ }
4256
+ if (this.#options.verificationEnvironment) {
4257
+ if (!resources.environmentIdentity)
4258
+ return {
4259
+ state: "paused",
4260
+ code: "verification-environment-unavailable",
4261
+ };
4262
+ this.#ledger(resources, resources.deliveryRevision).putDurableFact(
4263
+ `apply-environment-${hash(input.transactionId).slice(0, 40)}`,
4264
+ { identity: resources.environmentIdentity },
4265
+ );
4266
+ }
4267
+ const prepared = await resources.transactions.prepare({
4268
+ transactionId: input.transactionId,
4269
+ consumerRoot: input.consumerRoot,
4270
+ baselineRevisionId: resources.baselineRevisionId,
4271
+ finalRevisionId: resources.currentRevisionId,
4272
+ boundPaths: [
4273
+ ...new Set([
4274
+ ...deliveryBoundPaths(input.plan),
4275
+ ...(input.contextReadPaths ?? []),
4276
+ ]),
4277
+ ].sort(),
4278
+ verificationFact: resources.verificationFact,
4279
+ });
4280
+ return prepared;
4281
+ }
4282
+
4283
+ async applyPrepared(
4284
+ input: Parameters<WorkflowApplication["begin"]>[0],
4285
+ ): Promise<Record<string, unknown>> {
4286
+ const resources = await this.#prepareResources(
4287
+ {
4288
+ runId: input.runId,
4289
+ deliveryRevision: input.deliveryRevision,
4290
+ plan: input.plan,
4291
+ ...(input.baselineRevisionId
4292
+ ? { baselineRevisionId: input.baselineRevisionId }
4293
+ : {}),
4294
+ ...(input.currentWorkspaceRevisionId
4295
+ ? { currentWorkspaceRevisionId: input.currentWorkspaceRevisionId }
4296
+ : {}),
4297
+ },
4298
+ input.signal,
4299
+ );
4300
+ return resources.transactions.apply(input.transactionId, input.signal);
4301
+ }
4302
+
4303
+ async begin(
4304
+ input: Parameters<WorkflowApplication["begin"]>[0],
4305
+ ): Promise<Record<string, unknown>> {
4306
+ const prepared = await this.prepare(input);
4307
+ return prepared.state === "prepared" ? this.applyPrepared(input) : prepared;
4308
+ }
4309
+
4310
+ #resourcesForApplication(
4311
+ context: WorkflowApplicationContext | undefined,
4312
+ ): DurableRunResources {
4313
+ if (!context) throw new Error("workflow-application-context-unavailable");
4314
+ return this.#resources({
4315
+ runId: context.runId,
4316
+ deliveryRevision: context.deliveryRevision,
4317
+ plan: context.plan,
4318
+ ...(context.baselineRevisionId
4319
+ ? { baselineRevisionId: context.baselineRevisionId }
4320
+ : {}),
4321
+ ...(context.currentWorkspaceRevisionId
4322
+ ? { currentWorkspaceRevisionId: context.currentWorkspaceRevisionId }
4323
+ : {}),
4324
+ });
4325
+ }
4326
+
4327
+ requestControl(
4328
+ transactionId: string,
4329
+ intent: "cancel" | "discard",
4330
+ context?: WorkflowApplicationContext,
4331
+ ): Record<string, unknown> {
4332
+ return this.#resourcesForApplication(context).transactions.requestControl(
4333
+ transactionId,
4334
+ intent,
4335
+ );
4336
+ }
4337
+
4338
+ recover(
4339
+ transactionId: string,
4340
+ context?: WorkflowApplicationContext,
4341
+ ): Promise<Record<string, unknown>> {
4342
+ return this.#resourcesForApplication(context).transactions.recover(
4343
+ transactionId,
4344
+ );
4345
+ }
4346
+
4347
+ cleanup(runId: string): void {
4348
+ const resources = this.#runs.get(runId);
4349
+ if (resources) {
4350
+ this.#closeResources(resources);
4351
+ this.#runs.delete(runId);
4352
+ }
4353
+ rmSync(this.#runRoot(runId), { recursive: true, force: true });
4354
+ }
4355
+
4356
+ close(): void {
4357
+ for (const resources of this.#runs.values()) {
4358
+ this.#closeResources(resources);
4359
+ }
4360
+ this.#runs.clear();
4361
+ this.#routeHealth.close();
4362
+ }
4363
+ }
4364
+
4365
+ export function openDurableWorkflowEngine(
4366
+ options: DurableWorkflowEngineOptions,
4367
+ ): WorkflowEngine {
4368
+ const composition = new DurableWorkflowComposition(options);
4369
+ return WorkflowEngine.open({
4370
+ consumerRoot: options.consumerRoot,
4371
+ stateRoot: options.stateRoot,
4372
+ deliverySource: options.deliverySource,
4373
+ worker: composition,
4374
+ ...(options.workHardLimit !== undefined
4375
+ ? { workHardLimit: options.workHardLimit }
4376
+ : {}),
4377
+ changeVerifier: composition,
4378
+ application: composition,
4379
+ lifecycle: composition,
4380
+ ...(options.now ? { now: options.now } : {}),
4381
+ ...(options.leaseTtlMs !== undefined
4382
+ ? { leaseTtlMs: options.leaseTtlMs }
4383
+ : {}),
4384
+ });
4385
+ }