@abelxiaoxing/cadence 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +58 -4
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +5 -3
  8. package/prompts/abel-design.md +93 -44
  9. package/src/activity-contracts.ts +82 -0
  10. package/src/candidate-artifact.ts +143 -0
  11. package/src/candidate-patch.ts +31 -11
  12. package/src/candidate-workspace.ts +74 -0
  13. package/src/change-contract-schema.ts +191 -0
  14. package/src/change-contract.ts +5 -0
  15. package/src/change-verification.ts +896 -0
  16. package/src/child-model.ts +129 -0
  17. package/src/child-session.ts +293 -326
  18. package/src/contracts.ts +184 -76
  19. package/src/delivery-compiler.ts +103 -120
  20. package/src/delivery-revision.ts +42 -0
  21. package/src/design-control.ts +113 -4
  22. package/src/design-diagnostics.ts +150 -0
  23. package/src/durable-contracts.ts +210 -0
  24. package/src/durable-workflow.ts +148 -3570
  25. package/src/evidence-draft.ts +122 -0
  26. package/src/implement-graph.ts +3 -0
  27. package/src/implement-plan.ts +70 -0
  28. package/src/index.ts +59 -522
  29. package/src/model-source.ts +22 -0
  30. package/src/package-candidate.ts +224 -0
  31. package/src/package-workflow.ts +253 -0
  32. package/src/packet-runtime.ts +27 -48
  33. package/src/parent-provider.ts +86 -198
  34. package/src/phase-execution.ts +2248 -0
  35. package/src/pi-adapter.ts +19 -0
  36. package/src/plan-draft-summary.ts +146 -0
  37. package/src/plan-draft.ts +493 -0
  38. package/src/subagent-activity.ts +32 -66
  39. package/src/submit-tool.ts +20 -59
  40. package/src/transport-budget.ts +32 -0
  41. package/src/worker-broker.ts +37 -57
  42. package/src/workflow-policy.ts +14 -53
  43. package/src/workflow-recovery-policy.ts +95 -0
  44. package/src/workflow-scheduling.ts +105 -0
  45. package/src/workflow-state-machine.ts +105 -497
  46. package/src/workflow-status.ts +384 -0
  47. package/src/workspace-io-worker.mjs +7 -0
  48. package/src/empty-resource-loader.ts +0 -38
  49. package/src/parent-payload-bridge.ts +0 -523
@@ -1,260 +1,59 @@
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;
1
+ import {
2
+ ChangeVerification,
3
+ parseVerificationBaseline,
4
+ sha256Bytes,
5
+ verificationBaselineFact,
6
+ } from "./change-verification.ts";
7
+ import type {
8
+ DurableExecutionResources,
9
+ DurablePhaseFact,
10
+ DurableResourceInput,
11
+ DurableWorkflowEngineOptions,
12
+ } from "./durable-contracts.ts";
13
+ import { PhaseExecution } from "./phase-execution.ts";
14
+
15
+ export type {
16
+ DurableChangeVerificationResult,
17
+ DurablePhaseVerificationResult,
18
+ DurableWorkflowEngineOptions,
19
+ } from "./durable-contracts.ts";
20
+
21
+ interface DurableRunResources extends DurableExecutionResources {
22
+ transactions: ApplyTransaction;
23
+ closed: boolean;
12
24
  }
13
25
 
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";
26
+ import { lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
23
27
  import path from "node:path";
24
28
  import { DatabaseSync } from "node:sqlite";
25
- import {
26
- ApplyTransaction,
27
- verifyCumulativeRevision,
28
- } from "./apply-transaction.ts";
29
+ import { ApplyTransaction } from "./apply-transaction.ts";
29
30
  import { ArtifactStore } from "./artifact-store.ts";
30
- import {
31
- diffWritePaths,
32
- type StructuredVerificationContract,
33
- } from "./contracts.ts";
31
+ import { compareCanonicalStrings } from "./canonical.ts";
34
32
  import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
35
- import type { RoutePolicy, WorkerRoutePolicy } from "./route-policy.ts";
33
+ import type { RoutePolicy } from "./route-policy.ts";
36
34
  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";
35
+ import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
36
+ import { ROUTE_HEALTH_SCHEMA } from "./storage-schema.ts";
37
+ import { TaskLedger } from "./task-ledger.ts";
45
38
  import { type RouteHealthStore, RunWorkerBroker } from "./worker-broker.ts";
46
39
  import {
47
- type DependencyContractEntry,
48
- DependencyManifestError,
49
- type DurableCandidateProposal,
50
- type DurableVerificationScope,
51
- decideRecoveryAction,
52
40
  deliveryBoundPaths,
53
41
  deliveryTrackingPath,
54
- dependencyContract,
55
- emitWorkflowActivity,
56
- expectedClassification,
57
42
  hash,
58
- hasUnapprovedDependencyChange,
43
+ hasVerificationLifecycle,
59
44
  IDENTIFIER,
60
45
  implementationRouteRequirements,
61
46
  isRecord,
62
- LOCKFILES,
63
- type RedArtifactCorrection,
64
47
  SHA256,
65
- semanticRouteFailure,
66
48
  type WorkflowApplication,
67
49
  type WorkflowApplicationContext,
68
- type WorkflowAttemptOutcome,
69
50
  type WorkflowChangeVerifier,
70
- type WorkflowContextRequest,
71
- type WorkflowDeliverySource,
72
- type WorkflowRecoveryFeedback,
73
- type WorkflowRouteFacts,
74
51
  type WorkflowRunLifecycle,
75
52
  type WorkflowWorker,
76
53
  workspaceEntryEqual,
77
54
  } from "./workflow-policy.ts";
78
55
  import { WorkflowEngine } from "./workflow-state-machine.ts";
79
56
  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
57
 
259
58
  type DurableRouteHealth = Parameters<RouteHealthStore["set"]>[1];
260
59
 
@@ -390,166 +189,6 @@ function ensurePrivateRunDirectory(directory: string): void {
390
189
  }
391
190
  }
392
191
 
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
192
  class DurableWorkflowComposition
554
193
  implements
555
194
  WorkflowWorker,
@@ -557,6 +196,8 @@ class DurableWorkflowComposition
557
196
  WorkflowApplication,
558
197
  WorkflowRunLifecycle
559
198
  {
199
+ readonly #phase: PhaseExecution;
200
+ readonly #verification: ChangeVerification;
560
201
  readonly #options: DurableWorkflowEngineOptions;
561
202
  readonly #routeHealth: DurableRouteHealthStore;
562
203
  #broker: RunWorkerBroker;
@@ -571,121 +212,34 @@ class DurableWorkflowComposition
571
212
  ...(options.now ? { now: options.now } : {}),
572
213
  healthStore: this.#routeHealth,
573
214
  });
215
+ this.#verification = new ChangeVerification(options, {
216
+ prepareResources: (input, signal) =>
217
+ this.#prepareResources(input, signal),
218
+ ledger: (resources, revision) => this.#ledger(resources, revision),
219
+ run: (runId) => this.#runs.get(runId),
220
+ revalidatePhasePolicy: (...args) =>
221
+ this.#phase.revalidatePhasePolicy(...args),
222
+ });
223
+ this.#phase = new PhaseExecution(options, this.#broker, {
224
+ prepareResources: (input, signal) =>
225
+ this.#prepareResources(input, signal),
226
+ ledger: (resources, revision) => this.#ledger(resources, revision),
227
+ ensureVerificationBaseline: (input) =>
228
+ this.#verification.ensureVerificationBaseline(input),
229
+ verifyTaskAffected: (input) =>
230
+ this.#verification.verifyTaskAffected(input),
231
+ verifyPhase: (input) => this.#verification.verifyPhase(input),
232
+ });
574
233
  }
575
234
 
576
235
  updateRoutePolicy(policy: RoutePolicy): void {
577
236
  this.#broker.updatePolicy(policy);
578
237
  }
579
238
 
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
239
  routePolicyStatus(): Record<string, unknown> {
650
240
  return this.#broker.status();
651
241
  }
652
242
 
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
243
  #runRoot(runId: string): string {
690
244
  if (!IDENTIFIER.test(runId)) throw new Error("workflow-run-id-invalid");
691
245
  const dataRoot = path.join(this.#options.stateRoot.rootDir, "run-data");
@@ -720,62 +274,6 @@ class DurableWorkflowComposition
720
274
  );
721
275
  }
722
276
 
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
277
  readonly #initializing = new Map<string, Promise<DurableRunResources>>();
780
278
 
781
279
  async #prepareResources(
@@ -869,7 +367,7 @@ class DurableWorkflowComposition
869
367
  workspaces,
870
368
  hooks: {
871
369
  postApply: ({ transactionId, root: verificationRoot, signal }) =>
872
- this.#verifyPostApply(
370
+ this.#verification.verifyPostApply(
873
371
  resources,
874
372
  verificationRoot,
875
373
  signal,
@@ -905,7 +403,7 @@ class DurableWorkflowComposition
905
403
  }
906
404
 
907
405
  #ledger(
908
- resources: DurableRunResources,
406
+ resources: DurableExecutionResources,
909
407
  deliveryRevision: number,
910
408
  ): TaskLedger {
911
409
  const existing = resources.ledgers.get(deliveryRevision);
@@ -922,701 +420,56 @@ class DurableWorkflowComposition
922
420
  return ledger;
923
421
  }
924
422
 
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(
423
+ #expandedBaseline(
1154
424
  resources: DurableRunResources,
1155
- taskId: string,
425
+ plan: ImplementPlan,
1156
426
  ): 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),
427
+ const baseline = resources.workspaces.getRevision(
428
+ resources.baselineRevisionId,
1166
429
  );
1167
- const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1168
- const taskIdentity = new RegExp(
1169
- `(?:^|[^A-Za-z0-9._:-])${escaped}(?![A-Za-z0-9._:-])`,
1170
- "u",
430
+ const candidates = new Map<string, boolean>(
431
+ deliveryBoundPaths(plan).map((relative) => [relative, false]),
1171
432
  );
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;
433
+ for (const task of plan.tasks) {
434
+ for (const phase of Object.values(task.phases)) {
435
+ for (const relative of phase.read) candidates.set(relative, false);
436
+ for (const relative of [...phase.write, ...phase.delete]) {
437
+ candidates.set(relative, true);
1180
438
  }
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
439
  }
1217
- offset += value.length + 1;
1218
440
  }
441
+ for (const output of plan.outputs) candidates.set(output.path, true);
442
+ for (const task of plan.tasks) {
443
+ if (task.agents.impact === "none" || !task.agents.target) continue;
444
+ candidates.set(task.agents.target, task.agents.impact === "create-index");
445
+ }
446
+ const trackingPath = deliveryTrackingPath(plan);
1219
447
  if (
1220
- starts.length !== 1 ||
1221
- ends.length !== 1 ||
1222
- starts[0].line >= ends[0].line
448
+ trackingPath &&
449
+ (Object.hasOwn(baseline.entries, trackingPath) ||
450
+ observeSafePath(this.#options.consumerRoot, trackingPath).kind ===
451
+ "file")
1223
452
  ) {
1224
- return undefined;
453
+ candidates.set(trackingPath, false);
1225
454
  }
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
455
  const changes: Record<
1241
456
  string,
1242
457
  { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1243
458
  > = {};
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
- }
459
+ for (const [relative, mayBeAbsent] of [...candidates].sort(
460
+ ([left], [right]) => compareCanonicalStrings(left, right),
461
+ )) {
462
+ if (relative === ".") continue;
463
+ const observation = observeSafePath(this.#options.consumerRoot, relative);
464
+ if (observation.kind === "absent") {
465
+ if (baseline.entries[relative]?.kind === "absent") continue;
466
+ if (!mayBeAbsent)
467
+ throw new Error("workflow-baseline-input-unavailable");
468
+ changes[relative] = { kind: "absent" };
1273
469
  continue;
1274
470
  }
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");
471
+ if (observation.kind !== "file") {
472
+ throw new Error("workflow-baseline-path-unsafe");
1620
473
  }
1621
474
  const target = path.join(
1622
475
  this.#options.consumerRoot,
@@ -1884,7 +737,9 @@ class DurableWorkflowComposition
1884
737
  );
1885
738
  for (const deliveryRevision of retainedRevisions) {
1886
739
  const ledger = this.#ledger(resources, deliveryRevision);
1887
- const stored = ledger.durableFact(this.#baselineFactKey(resources));
740
+ const stored = ledger.durableFact(
741
+ this.#verification.baselineFactKey(resources),
742
+ );
1888
743
  resources.baselinePromises.delete(deliveryRevision);
1889
744
  if (stored === undefined) continue;
1890
745
  const baseline = parseVerificationBaseline(stored, resources.artifacts);
@@ -1893,7 +748,7 @@ class DurableWorkflowComposition
1893
748
  throw new Error("workflow-verification-baseline-conflict");
1894
749
  }
1895
750
  ledger.replaceDurableFact(
1896
- this.#baselineFactKey(resources),
751
+ this.#verification.baselineFactKey(resources),
1897
752
  stored,
1898
753
  verificationBaselineFact(
1899
754
  {
@@ -1910,7 +765,7 @@ class DurableWorkflowComposition
1910
765
  if (hasVerificationLifecycle(input.plan)) {
1911
766
  for (const task of input.tasks) {
1912
767
  if (task.state === "verified" && !invalidated.has(task.taskId)) {
1913
- this.#completeTracking(resources, task.taskId);
768
+ this.#phase.completeTracking(resources, task.taskId);
1914
769
  }
1915
770
  }
1916
771
  }
@@ -1921,2347 +776,61 @@ class DurableWorkflowComposition
1921
776
  };
1922
777
  }
1923
778
 
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
- }
779
+ isContextReadAvailable(
780
+ input: Parameters<NonNullable<WorkflowWorker["isContextReadAvailable"]>>[0],
781
+ ): boolean {
782
+ const resources = this.#resources(input);
783
+ const revision = resources.workspaces.getRevision(
784
+ input.currentWorkspaceRevisionId,
785
+ );
786
+ return revision.entries[input.path]?.kind === "file";
1956
787
  }
1957
788
 
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;
789
+ rebind(input: Parameters<WorkflowWorker["rebind"]>[0]) {
790
+ const rebound = this.#broker.rebind({
791
+ runId: input.runId,
792
+ taskId: input.taskId,
793
+ role: input.role,
794
+ routeId: input.routeId,
795
+ requirements: implementationRouteRequirements({ task: input.task }),
1969
796
  });
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");
797
+ return rebound.ok
798
+ ? {
799
+ ok: true as const,
800
+ routeId: rebound.route.id,
801
+ routeFingerprint: rebound.route.fingerprint,
1991
802
  }
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
- }
803
+ : { ok: false as const, code: rebound.code };
2010
804
  }
2011
805
 
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" };
806
+ async prepare(
807
+ input: Parameters<WorkflowApplication["begin"]>[0],
808
+ ): Promise<Record<string, unknown>> {
809
+ if (!input.baselineRevisionId || !input.currentWorkspaceRevisionId) {
810
+ return { state: "paused", code: "workspace-revision-unavailable" };
2023
811
  }
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" };
812
+ const resources = await this.#prepareResources(
813
+ {
814
+ runId: input.runId,
815
+ deliveryRevision: input.deliveryRevision,
816
+ plan: input.plan,
817
+ baselineRevisionId: input.baselineRevisionId,
818
+ currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
819
+ },
820
+ input.signal,
821
+ );
822
+ if (!resources.verificationFact) {
823
+ return { state: "paused", code: "verification-fact-unavailable" };
2047
824
  }
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 },
825
+ if (this.#options.verificationEnvironment) {
826
+ if (!resources.environmentIdentity)
827
+ return {
828
+ state: "paused",
829
+ code: "verification-environment-unavailable",
830
+ };
831
+ this.#ledger(resources, resources.deliveryRevision).putDurableFact(
832
+ `apply-environment-${hash(input.transactionId).slice(0, 40)}`,
833
+ { identity: resources.environmentIdentity },
4265
834
  );
4266
835
  }
4267
836
  const prepared = await resources.transactions.prepare({
@@ -4360,6 +929,15 @@ class DurableWorkflowComposition
4360
929
  this.#runs.clear();
4361
930
  this.#routeHealth.close();
4362
931
  }
932
+ runAttempt(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
933
+ return this.#phase.runAttempt(input);
934
+ }
935
+ hasPendingVerification(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
936
+ return this.#phase.hasPendingVerification(input);
937
+ }
938
+ verify(input: Parameters<WorkflowChangeVerifier["verify"]>[0]) {
939
+ return this.#verification.verify(input);
940
+ }
4363
941
  }
4364
942
 
4365
943
  export function openDurableWorkflowEngine(