@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
@@ -0,0 +1,2248 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ inspectCandidateArtifact,
6
+ sealCandidateArtifact,
7
+ } from "./candidate-artifact.ts";
8
+ import { revisionChanges, runGitApply } from "./candidate-workspace.ts";
9
+
10
+ import type { StructuredVerificationContract } from "./contracts.ts";
11
+ import type { PlanTaskDraft } from "./delivery-compiler.ts";
12
+ import type {
13
+ DurableAffectedResult,
14
+ DurableBaselineResult,
15
+ DurableCandidateCommit,
16
+ DurablePhaseVerificationResult,
17
+ DurableRepairResult,
18
+ DurableResourceInput,
19
+ DurableVerificationBaseline,
20
+ DurableWorkflowEngineOptions,
21
+ PhaseResources,
22
+ } from "./durable-contracts.ts";
23
+ import type {
24
+ BeginCandidateInput,
25
+ TaskLedger,
26
+ VerifiedTaskEvent,
27
+ } from "./task-ledger.ts";
28
+ import type { RunWorkerBroker } from "./worker-broker.ts";
29
+ import {
30
+ type DependencyContractEntry,
31
+ DependencyManifestError,
32
+ type DurableCandidateProposal,
33
+ decideRecoveryAction,
34
+ deliveryTrackingPath,
35
+ dependencyContract,
36
+ emitWorkflowActivity,
37
+ expectedClassification,
38
+ hash,
39
+ hasUnapprovedDependencyChange,
40
+ hasVerificationLifecycle,
41
+ implementationRouteRequirements,
42
+ isRecord,
43
+ LOCKFILES,
44
+ SHA256,
45
+ semanticRouteFailure,
46
+ type WorkflowAttemptOutcome,
47
+ type WorkflowRouteFacts,
48
+ type WorkflowWorker,
49
+ workspaceEntryEqual,
50
+ } from "./workflow-policy.ts";
51
+ import type { WorkspaceRevision, WorkspaceStore } from "./workspace-store.ts";
52
+
53
+ export interface PhaseExecutionServices {
54
+ prepareResources(
55
+ input: DurableResourceInput,
56
+ signal: AbortSignal,
57
+ ): Promise<PhaseResources>;
58
+ ledger(resources: PhaseResources, deliveryRevision: number): TaskLedger;
59
+ ensureVerificationBaseline(input: {
60
+ resources: PhaseResources;
61
+ ledger: TaskLedger;
62
+ signal: AbortSignal;
63
+ }): Promise<DurableBaselineResult>;
64
+ verifyTaskAffected(input: {
65
+ resources: PhaseResources;
66
+ baseline: DurableVerificationBaseline;
67
+ task: PlanTaskDraft;
68
+ revisionId: string;
69
+ signal: AbortSignal;
70
+ }): Promise<DurableAffectedResult>;
71
+ verifyPhase(
72
+ input: Parameters<DurableWorkflowEngineOptions["verifyPhase"]>[0],
73
+ ): Promise<DurablePhaseVerificationResult>;
74
+ }
75
+ export class PhaseExecution {
76
+ readonly #services: PhaseExecutionServices;
77
+ readonly #options: Pick<
78
+ DurableWorkflowEngineOptions,
79
+ "verificationPolicy" | "verificationEnvironment" | "proposeCandidate"
80
+ >;
81
+ readonly #broker: RunWorkerBroker;
82
+ constructor(
83
+ options: Pick<
84
+ DurableWorkflowEngineOptions,
85
+ "verificationPolicy" | "verificationEnvironment" | "proposeCandidate"
86
+ >,
87
+ broker: RunWorkerBroker,
88
+ services: PhaseExecutionServices,
89
+ ) {
90
+ this.#options = options;
91
+ this.#broker = broker;
92
+ this.#services = services;
93
+ }
94
+ #recordCorrection(
95
+ ledger: TaskLedger,
96
+ input: {
97
+ runId: string;
98
+ operationId: string;
99
+ taskId: string;
100
+ phase: "red" | "green" | "refactor";
101
+ },
102
+ identity: BeginCandidateInput,
103
+ correction: {
104
+ category: "artifact" | "stale" | "verification" | "checkpoint";
105
+ code: string;
106
+ stage: string;
107
+ },
108
+ ): void {
109
+ ledger.commitVerifiedEvent({
110
+ runId: input.runId,
111
+ taskId: input.taskId,
112
+ eventId: `correction-${hash(
113
+ input.operationId,
114
+ identity.candidateId,
115
+ correction.category,
116
+ correction.code,
117
+ correction.stage,
118
+ ).slice(0, 40)}`,
119
+ kind: "artifact-correction",
120
+ phase: input.phase,
121
+ correctionCategory: correction.category,
122
+ safeFailure: { code: correction.code, stage: correction.stage },
123
+ candidateId: identity.candidateId,
124
+ attemptId: identity.attemptId,
125
+ routeId: identity.routeId,
126
+ routeFingerprint: identity.routeFingerprint,
127
+ });
128
+ }
129
+
130
+ completeTracking(
131
+ resources: PhaseResources,
132
+ taskId: string,
133
+ ): WorkspaceRevision {
134
+ const current = resources.workspaces.getRevision(
135
+ resources.currentRevisionId,
136
+ );
137
+ const trackingPath = deliveryTrackingPath(resources.plan);
138
+ if (!trackingPath) return current;
139
+ const entry = current.entries[trackingPath];
140
+ if (!entry || entry.kind === "absent") return current;
141
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(
142
+ resources.artifacts.read(entry.hash),
143
+ );
144
+ const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
145
+ const taskIdentity = new RegExp(
146
+ `(?:^|[^A-Za-z0-9._:-])${escaped}(?![A-Za-z0-9._:-])`,
147
+ "u",
148
+ );
149
+ let matches = 0;
150
+ let changed = false;
151
+ const updated = text
152
+ .split(/(?<=\n)/u)
153
+ .map((segment) => {
154
+ const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
155
+ if (!/^\s*-\s+\[[ xX]\]/u.test(line) || !taskIdentity.test(line)) {
156
+ return segment;
157
+ }
158
+ matches += 1;
159
+ if (/^\s*-\s+\[[xX]\]/u.test(line)) return segment;
160
+ changed = true;
161
+ return segment.replace(/^(\s*-\s+)\[ \]/u, "$1[x]");
162
+ })
163
+ .join("");
164
+ if (matches !== 1) throw new Error("workflow-tracking-task-mismatch");
165
+ if (!changed) return current;
166
+ const next = resources.workspaces.createRevision({
167
+ parentRevisionId: current.revisionId,
168
+ changes: {
169
+ [trackingPath]: {
170
+ kind: "file",
171
+ bytes: new TextEncoder().encode(updated),
172
+ mode: entry.mode,
173
+ },
174
+ },
175
+ });
176
+ resources.currentRevisionId = next.revisionId;
177
+ return next;
178
+ }
179
+
180
+ #openTask(
181
+ resources: PhaseResources,
182
+ ledger: TaskLedger,
183
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
184
+ ): void {
185
+ const revision = resources.workspaces.getRevision(
186
+ resources.baselineRevisionId,
187
+ );
188
+ const contextRefs = [...new Set(input.task.phases.red.read)]
189
+ .sort()
190
+ .flatMap((relative) => {
191
+ const entry = revision.entries[relative];
192
+ return entry?.kind === "file"
193
+ ? [{ kind: "path" as const, path: relative, hash: entry.hash }]
194
+ : [];
195
+ });
196
+ ledger.openTask({
197
+ runId: input.runId,
198
+ deliveryRevision: input.deliveryRevision,
199
+ taskId: input.taskId,
200
+ boundaryHash: hash("durable-task-boundary", JSON.stringify(input.task)),
201
+ objective: input.task.objective,
202
+ contextRefs,
203
+ initialPhase:
204
+ input.task.verificationMode &&
205
+ input.task.verificationMode !== "behavior"
206
+ ? "green"
207
+ : "red",
208
+ });
209
+ }
210
+
211
+ #executionWritePaths(resources: PhaseResources): string[] {
212
+ return [
213
+ ...new Set(
214
+ resources.plan.tasks.flatMap((task) =>
215
+ Object.values(task.phases).flatMap((phase) => [
216
+ ...phase.write,
217
+ ...phase.delete,
218
+ ]),
219
+ ),
220
+ ),
221
+ ];
222
+ }
223
+
224
+ #policyFactKey(
225
+ resources: PhaseResources,
226
+ event: Record<string, unknown>,
227
+ ): string {
228
+ 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)}`;
229
+ }
230
+
231
+ async revalidatePhasePolicy(
232
+ resources: PhaseResources,
233
+ ledger: TaskLedger,
234
+ task: PlanTaskDraft,
235
+ signal: AbortSignal,
236
+ ): Promise<WorkflowAttemptOutcome | undefined> {
237
+ if (
238
+ !this.#options.verificationPolicy &&
239
+ !this.#options.verificationEnvironment
240
+ )
241
+ return undefined;
242
+ const projection = ledger.projection({
243
+ runId: resources.runId,
244
+ taskId: task.taskId,
245
+ nextPhase: "green",
246
+ }) as { history: Array<Record<string, unknown>> };
247
+ for (const event of projection.history.filter(
248
+ (event) =>
249
+ event.kind === "phase-verified" || event.kind === "repair-verified",
250
+ )) {
251
+ const key = this.#policyFactKey(resources, event);
252
+ if (ledger.durableFact(key) !== undefined) continue;
253
+ const phase = event.phase as "red" | "green" | "refactor";
254
+ const boundary = task.phases[phase];
255
+ const repair = event.kind === "repair-verified";
256
+ const verification = repair
257
+ ? task.repairVerification
258
+ : boundary?.verification;
259
+ const expected = repair
260
+ ? "expected-green"
261
+ : expectedClassification(phase);
262
+ if (
263
+ !boundary ||
264
+ !verification ||
265
+ typeof event.isolatedRevisionId !== "string"
266
+ )
267
+ throw new Error("workflow-ledger-phase-invalid");
268
+ const root = mkdtempSync(
269
+ path.join(resources.root, "policy-verification-"),
270
+ );
271
+ try {
272
+ await resources.workspaces.materializeAsync(
273
+ event.isolatedRevisionId,
274
+ root,
275
+ signal,
276
+ );
277
+ const result = await this.#services.verifyPhase({
278
+ executionWritePaths: this.#executionWritePaths(resources),
279
+ runId: resources.runId,
280
+ deliveryRevision: resources.deliveryRevision,
281
+ taskId: task.taskId,
282
+ phase,
283
+ root,
284
+ verification,
285
+ signal,
286
+ });
287
+ if (!result.ok)
288
+ return {
289
+ kind: "paused",
290
+ code:
291
+ result.kind === "paused"
292
+ ? result.code
293
+ : "verification-policy-rejected",
294
+ };
295
+ if (
296
+ result.classification !== expected ||
297
+ (expected === "expected-red"
298
+ ? result.exitCode === 0
299
+ : result.exitCode !== 0)
300
+ )
301
+ return { kind: "paused", code: "verification-policy-rejected" };
302
+ ledger.putDurableFact(key, {
303
+ policy: this.#options.verificationPolicy ?? "legacy",
304
+ });
305
+ } finally {
306
+ rmSync(root, { recursive: true, force: true });
307
+ }
308
+ }
309
+ return undefined;
310
+ }
311
+
312
+ #pendingCandidate(
313
+ ledger: TaskLedger,
314
+ key: string,
315
+ baseRevisionId: string,
316
+ ):
317
+ | {
318
+ identity: BeginCandidateInput;
319
+ proposal: Extract<
320
+ DurableCandidateProposal,
321
+ { kind: "sealed-candidate" }
322
+ >;
323
+ }
324
+ | undefined {
325
+ const value = ledger.durableFact(key);
326
+ if (
327
+ !isRecord(value) ||
328
+ !isRecord(value.identity) ||
329
+ !isRecord(value.proposal) ||
330
+ value.identity.isolatedRevisionId !== baseRevisionId
331
+ )
332
+ return undefined;
333
+ return value as unknown as {
334
+ identity: BeginCandidateInput;
335
+ proposal: Extract<DurableCandidateProposal, { kind: "sealed-candidate" }>;
336
+ };
337
+ }
338
+
339
+ #savePendingCandidate(ledger: TaskLedger, key: string, value: unknown): void {
340
+ const previous = ledger.durableFact(key);
341
+ if (previous === undefined) ledger.putDurableFact(key, value);
342
+ else ledger.replaceDurableFact(key, previous, value);
343
+ }
344
+
345
+ #replayedPhase(
346
+ resources: PhaseResources,
347
+ ledger: TaskLedger,
348
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
349
+ ): WorkflowAttemptOutcome | undefined {
350
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
351
+ return undefined;
352
+ }
353
+ const projection = ledger.projection({
354
+ runId: input.runId,
355
+ taskId: input.taskId,
356
+ nextPhase: input.phase,
357
+ }) as {
358
+ history: Array<Record<string, unknown>>;
359
+ };
360
+ const event = projection.history.find(
361
+ (candidate) =>
362
+ candidate.kind === "phase-verified" && candidate.phase === input.phase,
363
+ );
364
+ if (!event) return undefined;
365
+ if (
366
+ typeof event.artifactHash !== "string" ||
367
+ !SHA256.test(event.artifactHash) ||
368
+ typeof event.isolatedRevisionId !== "string" ||
369
+ !SHA256.test(event.isolatedRevisionId) ||
370
+ typeof event.exitCode !== "number" ||
371
+ event.actualClassification !== expectedClassification(input.phase)
372
+ ) {
373
+ throw new Error("workflow-ledger-phase-invalid");
374
+ }
375
+ let isolatedRevisionId = event.isolatedRevisionId;
376
+ const finalPhase = input.task.phases.refactor ? "refactor" : "green";
377
+ if (hasVerificationLifecycle(input.plan) && input.phase === finalPhase) {
378
+ const fact = ledger.durableFact(
379
+ `task-final-${hash(input.taskId).slice(0, 40)}`,
380
+ );
381
+ if (
382
+ !isRecord(fact) ||
383
+ fact.taskId !== input.taskId ||
384
+ fact.phase !== input.phase ||
385
+ typeof fact.isolatedRevisionId !== "string" ||
386
+ !SHA256.test(fact.isolatedRevisionId)
387
+ ) {
388
+ throw new Error("workflow-task-completion-fact-invalid");
389
+ }
390
+ isolatedRevisionId = fact.isolatedRevisionId;
391
+ }
392
+ resources.workspaces.getRevision(isolatedRevisionId);
393
+ if (
394
+ this.#revisionDescendsFrom(
395
+ resources,
396
+ resources.currentRevisionId,
397
+ isolatedRevisionId,
398
+ )
399
+ ) {
400
+ isolatedRevisionId = resources.currentRevisionId;
401
+ } else if (
402
+ !this.#revisionDescendsFrom(
403
+ resources,
404
+ isolatedRevisionId,
405
+ resources.currentRevisionId,
406
+ )
407
+ ) {
408
+ throw new Error("workspace-revision-stale");
409
+ }
410
+ resources.currentRevisionId = isolatedRevisionId;
411
+ return {
412
+ kind: "phase-committed",
413
+ artifactHash: event.artifactHash,
414
+ isolatedRevisionId,
415
+ exitCode: event.exitCode,
416
+ classification: expectedClassification(input.phase),
417
+ baselineRevisionId: resources.baselineRevisionId,
418
+ };
419
+ }
420
+
421
+ async #mergeCandidate(
422
+ resources: PhaseResources,
423
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
424
+ baseRevisionId: string,
425
+ candidateRevisionId: string,
426
+ boundPaths?: string[],
427
+ ) {
428
+ let release!: () => void;
429
+ const prior = resources.mergeTail;
430
+ resources.mergeTail = new Promise<void>((resolve) => {
431
+ release = resolve;
432
+ });
433
+ await prior;
434
+ try {
435
+ const phase = input.task.phases[input.phase];
436
+ if (!phase) throw new Error("workflow-task-phase-invalid");
437
+ const merged = resources.workspaces.mergeRevision({
438
+ baseRevisionId,
439
+ currentRevisionId: resources.currentRevisionId,
440
+ candidateRevisionId,
441
+ boundPaths: [
442
+ ...new Set([
443
+ ...(boundPaths ?? [...phase.read, ...phase.write, ...phase.delete]),
444
+ ...(input.contextReadPaths ?? []),
445
+ ]),
446
+ ],
447
+ });
448
+ resources.currentRevisionId = merged.revisionId;
449
+ return merged;
450
+ } finally {
451
+ release();
452
+ }
453
+ }
454
+
455
+ async #rollbackWorkspacePaths(input: {
456
+ resources: PhaseResources;
457
+ baseRevisionId: string;
458
+ rejectedRevisionId: string;
459
+ paths: readonly string[];
460
+ }): Promise<void> {
461
+ const { resources } = input;
462
+ let release!: () => void;
463
+ const prior = resources.mergeTail;
464
+ resources.mergeTail = new Promise<void>((resolve) => {
465
+ release = resolve;
466
+ });
467
+ await prior;
468
+ try {
469
+ const base = resources.workspaces.getRevision(input.baseRevisionId);
470
+ const rejected = resources.workspaces.getRevision(
471
+ input.rejectedRevisionId,
472
+ );
473
+ const current = resources.workspaces.getRevision(
474
+ resources.currentRevisionId,
475
+ );
476
+ const changes: Record<
477
+ string,
478
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
479
+ > = {};
480
+ for (const relative of [...new Set(input.paths)].sort()) {
481
+ const before = base.entries[relative];
482
+ const rejectedEntry = rejected.entries[relative];
483
+ if (workspaceEntryEqual(before, rejectedEntry)) continue;
484
+ const latest = current.entries[relative];
485
+ if (workspaceEntryEqual(latest, before)) continue;
486
+ if (!workspaceEntryEqual(latest, rejectedEntry)) {
487
+ throw new Error("workspace-revision-stale");
488
+ }
489
+ changes[relative] =
490
+ before?.kind === "file"
491
+ ? {
492
+ kind: "file",
493
+ bytes: resources.artifacts.read(before.hash),
494
+ mode: before.mode,
495
+ }
496
+ : { kind: "absent" };
497
+ }
498
+ if (Object.keys(changes).length === 0) return;
499
+ const rolledBack = resources.workspaces.createRevision({
500
+ parentRevisionId: current.revisionId,
501
+ changes,
502
+ });
503
+ resources.currentRevisionId = rolledBack.revisionId;
504
+ } finally {
505
+ release();
506
+ }
507
+ }
508
+
509
+ async #runRepairCandidate(input: {
510
+ resources: PhaseResources;
511
+ ledger: TaskLedger;
512
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
513
+ repairAttempt: number;
514
+ failureIdentities: string[];
515
+ }): Promise<DurableRepairResult> {
516
+ const { resources, ledger } = input;
517
+ const request = input.attempt;
518
+ if (request.signal.aborted) {
519
+ return { kind: "operation-cancelled", code: "cancelled" };
520
+ }
521
+ const phase = request.task.phases[request.phase];
522
+ if (!phase) throw new Error("workflow-task-phase-invalid");
523
+ const allPhases = Object.values(request.task.phases);
524
+ const approvedWritePaths = [
525
+ ...new Set(allPhases.flatMap((boundary) => boundary.write)),
526
+ ].sort();
527
+ const approvedDeletePaths = [
528
+ ...new Set(allPhases.flatMap((boundary) => boundary.delete)),
529
+ ].sort();
530
+ const approvedPaths = [
531
+ ...new Set([...approvedWritePaths, ...approvedDeletePaths]),
532
+ ].sort();
533
+ const boundPaths = [
534
+ ...new Set(
535
+ allPhases.flatMap((boundary) => [
536
+ ...boundary.read,
537
+ ...boundary.write,
538
+ ...boundary.delete,
539
+ ]),
540
+ ),
541
+ ].sort();
542
+ if (approvedPaths.length === 0) {
543
+ return { kind: "approval-needed", code: "task-write-set-empty" };
544
+ }
545
+ const baseRevisionId = resources.currentRevisionId;
546
+ const pendingKey = `pending-${hash(request.taskId, request.phase, request.task.repairVerification.id).slice(0, 40)}`;
547
+ const pending = this.#pendingCandidate(ledger, pendingKey, baseRevisionId);
548
+ const proposalRoot = mkdtempSync(path.join(resources.root, "repair-"));
549
+ try {
550
+ await resources.workspaces.materializeAsync(
551
+ baseRevisionId,
552
+ proposalRoot,
553
+ request.signal,
554
+ );
555
+ if (request.routeId && !pending) {
556
+ const rebound = this.#broker.resumeBinding({
557
+ runId: request.runId,
558
+ taskId: request.taskId,
559
+ role: "implementation-worker",
560
+ routeId: request.routeId,
561
+ ...(request.routeFingerprint
562
+ ? { expectedFingerprint: request.routeFingerprint }
563
+ : {}),
564
+ requirements: implementationRouteRequirements({
565
+ task: request.task,
566
+ ...(request.artifactCorrection
567
+ ? { artifactCorrection: request.artifactCorrection }
568
+ : {}),
569
+ repair: { failureIdentities: input.failureIdentities },
570
+ }),
571
+ });
572
+ if (!rebound.ok) return { kind: "paused", code: rebound.code };
573
+ }
574
+ const ledgerProjection = ledger.projection({
575
+ runId: request.runId,
576
+ taskId: request.taskId,
577
+ nextPhase: request.phase,
578
+ });
579
+ const repair = {
580
+ attempt: input.repairAttempt,
581
+ attribution: "introduced" as const,
582
+ failureIdentities: [...input.failureIdentities],
583
+ };
584
+ let identity: BeginCandidateInput | undefined = pending?.identity;
585
+ const routed = pending
586
+ ? {
587
+ ok: true as const,
588
+ value: pending.proposal,
589
+ routeId: pending.identity.routeId,
590
+ routeFingerprint: pending.identity.routeFingerprint,
591
+ }
592
+ : await this.#broker.run({
593
+ runId: request.runId,
594
+ taskId: request.taskId,
595
+ operationId: `${request.operationId}:${request.taskId}:repair:${input.repairAttempt}`,
596
+ role: "implementation-worker",
597
+ requirements: implementationRouteRequirements({
598
+ task: request.task,
599
+ ...(request.artifactCorrection
600
+ ? { artifactCorrection: request.artifactCorrection }
601
+ : {}),
602
+ repair: { failureIdentities: input.failureIdentities },
603
+ }),
604
+ signal: request.signal,
605
+ ...(request.onActivity ? { onActivity: request.onActivity } : {}),
606
+ classifyResult: semanticRouteFailure,
607
+ execute: async (attempt) => {
608
+ identity = {
609
+ candidateId: `candidate-${randomUUID()}`,
610
+ runId: request.runId,
611
+ deliveryRevision: request.deliveryRevision,
612
+ taskId: request.taskId,
613
+ phase: request.phase,
614
+ attemptId: `repair-${hash(
615
+ request.operationId,
616
+ request.taskId,
617
+ String(input.repairAttempt),
618
+ attempt.route.fingerprint,
619
+ ).slice(0, 40)}`,
620
+ approvedPaths,
621
+ isolatedRevisionId: baseRevisionId,
622
+ verificationId: request.task.repairVerification.id,
623
+ routeId: attempt.route.id,
624
+ routeFingerprint: attempt.route.fingerprint,
625
+ };
626
+ if (request.reserveCandidate && !request.reserveCandidate())
627
+ return {
628
+ kind: "paused" as const,
629
+ code:
630
+ request.additionalAttempt || request.verificationOnly
631
+ ? "repair-attempts-exhausted"
632
+ : "change-work-budget-exhausted",
633
+ };
634
+ return this.#options.proposeCandidate({
635
+ runId: request.runId,
636
+ operationId: request.operationId,
637
+ deliveryRevision: request.deliveryRevision,
638
+ taskId: request.taskId,
639
+ phase: request.phase,
640
+ task: structuredClone(request.task),
641
+ contextReadPaths: request.contextReadPaths,
642
+ workspaceRoot: proposalRoot,
643
+ ledgerProjection,
644
+ candidateArtifact: {
645
+ ledger,
646
+ identity,
647
+ workspaceRoot: proposalRoot,
648
+ writePaths: approvedWritePaths,
649
+ deletePaths: approvedDeletePaths,
650
+ },
651
+ route: attempt.route,
652
+ repair,
653
+ ...(request.artifactCorrection
654
+ ? { artifactCorrection: request.artifactCorrection }
655
+ : {}),
656
+ ...(request.contextRequest
657
+ ? { contextRequest: request.contextRequest }
658
+ : {}),
659
+ ...(request.recoveryFeedback
660
+ ? { recoveryFeedback: request.recoveryFeedback }
661
+ : {}),
662
+ signal: attempt.signal,
663
+ onRequestStart: attempt.onRequestStart,
664
+ onHeaders: attempt.onHeaders,
665
+ onProgress: attempt.onProgress,
666
+ });
667
+ },
668
+ });
669
+ if (!routed.ok) {
670
+ return routed.state === "cancelled" || request.signal.aborted
671
+ ? { kind: "operation-cancelled", code: "cancelled" }
672
+ : { kind: "paused", code: routed.code };
673
+ }
674
+ if (!identity || identity.routeId !== routed.routeId) {
675
+ throw new Error("candidate-route-identity-invalid");
676
+ }
677
+ const correctionIdentity = identity;
678
+ const withRoute = <T extends object>(outcome: T) => ({
679
+ ...outcome,
680
+ routeId: correctionIdentity.routeId,
681
+ routeFingerprint: correctionIdentity.routeFingerprint,
682
+ });
683
+ let uncommittedRevisionId: string | undefined;
684
+ const reject = async <T extends object>(
685
+ outcome: T,
686
+ correction: {
687
+ category: "artifact" | "stale" | "verification" | "checkpoint";
688
+ code: string;
689
+ stage: string;
690
+ },
691
+ rollback = false,
692
+ ): Promise<T> => {
693
+ if (rollback && uncommittedRevisionId) {
694
+ await this.#rollbackWorkspacePaths({
695
+ resources,
696
+ baseRevisionId,
697
+ rejectedRevisionId: uncommittedRevisionId,
698
+ paths: approvedPaths,
699
+ });
700
+ }
701
+ this.#recordCorrection(ledger, request, correctionIdentity, correction);
702
+ return withRoute({ ...outcome, retryPolicy: correction.category });
703
+ };
704
+ const proposal = routed.value;
705
+ if (
706
+ proposal.kind !== "candidate" &&
707
+ proposal.kind !== "sealed-candidate"
708
+ ) {
709
+ return proposal.kind === "operation-cancelled" ||
710
+ proposal.kind === "paused" ||
711
+ proposal.kind === "approval-needed"
712
+ ? withRoute(proposal)
713
+ : reject(proposal, {
714
+ category: "artifact",
715
+ code: proposal.code,
716
+ stage: "candidate-submit",
717
+ });
718
+ }
719
+ emitWorkflowActivity(request.onActivity, { state: "validating" });
720
+ const inspected = inspectCandidateArtifact({
721
+ ledger,
722
+ identity,
723
+ proposal,
724
+ approvedPaths,
725
+ });
726
+ if (!inspected.ok) {
727
+ const code =
728
+ inspected.code === "write-set-mismatch"
729
+ ? "repair-boundary-expansion"
730
+ : inspected.code;
731
+ return reject(
732
+ {
733
+ kind:
734
+ inspected.code === "write-set-mismatch"
735
+ ? "approval-needed"
736
+ : "retryable",
737
+ code,
738
+ },
739
+ { category: "artifact", code, stage: inspected.stage },
740
+ );
741
+ }
742
+ const { bytes, paths: candidatePaths } = inspected;
743
+ const dependencySensitive = candidatePaths.some(
744
+ (relative) => relative === "package.json" || LOCKFILES.has(relative),
745
+ );
746
+ let dependenciesBefore: Map<string, DependencyContractEntry> | undefined;
747
+ if (dependencySensitive) {
748
+ try {
749
+ dependenciesBefore = dependencyContract(proposalRoot);
750
+ } catch (error) {
751
+ if (!(error instanceof DependencyManifestError)) throw error;
752
+ return reject(
753
+ { kind: "retryable", code: error.message },
754
+ {
755
+ category: "artifact",
756
+ code: error.message,
757
+ stage: "candidate-dependency",
758
+ },
759
+ );
760
+ }
761
+ }
762
+
763
+ const checked = await runGitApply(
764
+ proposalRoot,
765
+ bytes,
766
+ true,
767
+ request.signal,
768
+ );
769
+ if (checked !== "ok") {
770
+ return checked === "cancelled"
771
+ ? withRoute({ kind: "operation-cancelled", code: "cancelled" })
772
+ : reject(
773
+ { kind: "retryable", code: "candidate-diff-invalid" },
774
+ {
775
+ category: "artifact",
776
+ code: "candidate-diff-invalid",
777
+ stage: "candidate-check",
778
+ },
779
+ );
780
+ }
781
+ const applied = await runGitApply(
782
+ proposalRoot,
783
+ bytes,
784
+ false,
785
+ request.signal,
786
+ );
787
+ if (applied !== "ok") {
788
+ return applied === "cancelled"
789
+ ? withRoute({ kind: "operation-cancelled", code: "cancelled" })
790
+ : reject(
791
+ { kind: "retryable", code: "candidate-diff-invalid" },
792
+ {
793
+ category: "artifact",
794
+ code: "candidate-diff-invalid",
795
+ stage: "candidate-apply",
796
+ },
797
+ );
798
+ }
799
+ if (dependenciesBefore) {
800
+ let dependenciesAfter: Map<string, DependencyContractEntry>;
801
+ try {
802
+ dependenciesAfter = dependencyContract(proposalRoot);
803
+ } catch (error) {
804
+ if (!(error instanceof DependencyManifestError)) throw error;
805
+ return reject(
806
+ { kind: "retryable", code: error.message },
807
+ {
808
+ category: "artifact",
809
+ code: error.message,
810
+ stage: "candidate-dependency",
811
+ },
812
+ );
813
+ }
814
+ if (
815
+ hasUnapprovedDependencyChange(
816
+ dependenciesBefore,
817
+ dependenciesAfter,
818
+ request.task.approvedDependencies,
819
+ candidatePaths.some((relative) => LOCKFILES.has(relative)),
820
+ )
821
+ ) {
822
+ return reject(
823
+ { kind: "approval-needed", code: "unapproved-dependency-change" },
824
+ {
825
+ category: "artifact",
826
+ code: "unapproved-dependency-change",
827
+ stage: "candidate-dependency",
828
+ },
829
+ );
830
+ }
831
+ }
832
+
833
+ const sealed = sealCandidateArtifact({
834
+ ledger,
835
+ identity,
836
+ proposal,
837
+ bytes,
838
+ });
839
+ if (!sealed.ok) {
840
+ return reject(
841
+ { kind: sealed.kind, code: sealed.code },
842
+ { category: "artifact", code: sealed.code, stage: sealed.stage },
843
+ );
844
+ }
845
+ const { artifactHash } = sealed;
846
+ const candidate = resources.workspaces.createRevision({
847
+ parentRevisionId: baseRevisionId,
848
+ changes: revisionChanges(proposalRoot, approvedPaths),
849
+ });
850
+ const requiredOutputs = request.plan.outputs.filter(
851
+ (output) =>
852
+ output.producer.taskId === request.taskId &&
853
+ (!request.artifactCorrection || output.producer.phase !== "refactor"),
854
+ );
855
+ if (
856
+ requiredOutputs.some(
857
+ (output) => candidate.entries[output.path]?.kind !== "file",
858
+ )
859
+ ) {
860
+ return reject(
861
+ { kind: "retryable", code: "producer-output-unavailable" },
862
+ {
863
+ category: "artifact",
864
+ code: "producer-output-unavailable",
865
+ stage: "candidate-output",
866
+ },
867
+ );
868
+ }
869
+ emitWorkflowActivity(request.onActivity, { state: "verifying" });
870
+ this.#savePendingCandidate(ledger, pendingKey, {
871
+ identity,
872
+ proposal: {
873
+ kind: "sealed-candidate",
874
+ candidateId: identity.candidateId,
875
+ artifactHash,
876
+ bytes: bytes.length,
877
+ paths: candidatePaths,
878
+ },
879
+ });
880
+ const verified = await this.#services.verifyPhase({
881
+ executionWritePaths: this.#executionWritePaths(resources),
882
+ runId: request.runId,
883
+ deliveryRevision: request.deliveryRevision,
884
+ taskId: request.taskId,
885
+ phase: request.phase,
886
+ root: proposalRoot,
887
+ verification: structuredClone(request.task.repairVerification),
888
+ signal: request.signal,
889
+ });
890
+ if (!verified.ok) {
891
+ if (verified.kind === "paused") return verified;
892
+ this.#savePendingCandidate(ledger, pendingKey, null);
893
+ return reject(verified, {
894
+ category: "verification",
895
+ code: verified.code,
896
+ stage: "phase-verification",
897
+ });
898
+ }
899
+ if (
900
+ verified.classification !== "expected-green" ||
901
+ verified.exitCode !== 0
902
+ ) {
903
+ return reject(
904
+ { kind: "retryable", code: "verification-rejected" },
905
+ {
906
+ category: "verification",
907
+ code: "verification-rejected",
908
+ stage: "phase-verification",
909
+ },
910
+ );
911
+ }
912
+ let merged: ReturnType<WorkspaceStore["mergeRevision"]>;
913
+ try {
914
+ merged = await this.#mergeCandidate(
915
+ resources,
916
+ request,
917
+ baseRevisionId,
918
+ candidate.revisionId,
919
+ boundPaths,
920
+ );
921
+ uncommittedRevisionId = merged.revisionId;
922
+ } catch (error) {
923
+ if (
924
+ error instanceof Error &&
925
+ error.message === "workspace-revision-stale"
926
+ ) {
927
+ return reject(
928
+ { kind: "retryable", code: "workspace-revision-stale" },
929
+ {
930
+ category: "stale",
931
+ code: "workspace-revision-stale",
932
+ stage: "candidate-merge",
933
+ },
934
+ );
935
+ }
936
+ throw error;
937
+ }
938
+ if (
939
+ requiredOutputs.some(
940
+ (output) => merged.entries[output.path]?.kind !== "file",
941
+ )
942
+ ) {
943
+ return reject(
944
+ { kind: "retryable", code: "producer-output-unavailable" },
945
+ {
946
+ category: "artifact",
947
+ code: "producer-output-unavailable",
948
+ stage: "candidate-output",
949
+ },
950
+ true,
951
+ );
952
+ }
953
+ const outputFacts = approvedPaths.map((relative) => {
954
+ const entry = merged.entries[relative];
955
+ return entry?.kind === "file"
956
+ ? {
957
+ path: relative,
958
+ kind: "file" as const,
959
+ hash: entry.hash,
960
+ bytes: entry.bytes,
961
+ }
962
+ : { path: relative, kind: "absent" as const };
963
+ });
964
+ return {
965
+ kind: "repair-committed",
966
+ commit: {
967
+ artifactHash,
968
+ isolatedRevisionId: merged.revisionId,
969
+ exitCode: verified.exitCode,
970
+ classification: verified.classification,
971
+ diagnostic: verified.diagnostic,
972
+ outputFacts,
973
+ routeId: identity.routeId,
974
+ routeFingerprint: identity.routeFingerprint,
975
+ },
976
+ };
977
+ } finally {
978
+ rmSync(proposalRoot, { recursive: true, force: true });
979
+ }
980
+ }
981
+
982
+ #revisionDescendsFrom(
983
+ resources: PhaseResources,
984
+ descendantRevisionId: string,
985
+ ancestorRevisionId: string,
986
+ ): boolean {
987
+ let current = resources.workspaces.getRevision(descendantRevisionId);
988
+ const visited = new Set<string>();
989
+ for (;;) {
990
+ if (current.revisionId === ancestorRevisionId) return true;
991
+ if (
992
+ current.parentRevisionId === null ||
993
+ visited.has(current.revisionId)
994
+ ) {
995
+ return false;
996
+ }
997
+ visited.add(current.revisionId);
998
+ current = resources.workspaces.getRevision(current.parentRevisionId);
999
+ }
1000
+ }
1001
+
1002
+ #verifiedPhaseReplay(
1003
+ ledger: TaskLedger,
1004
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1005
+ ): { artifactHash: string; exitCode: number; isolatedRevisionId: string } {
1006
+ const projection = ledger.projection({
1007
+ runId: input.runId,
1008
+ taskId: input.taskId,
1009
+ nextPhase: input.phase,
1010
+ }) as { history: Array<Record<string, unknown>> };
1011
+ const event = projection.history.find(
1012
+ (candidate) =>
1013
+ candidate.kind === "phase-verified" && candidate.phase === input.phase,
1014
+ );
1015
+ if (
1016
+ !event ||
1017
+ typeof event.artifactHash !== "string" ||
1018
+ !SHA256.test(event.artifactHash) ||
1019
+ typeof event.isolatedRevisionId !== "string" ||
1020
+ !SHA256.test(event.isolatedRevisionId) ||
1021
+ typeof event.exitCode !== "number" ||
1022
+ event.actualClassification !== expectedClassification(input.phase)
1023
+ ) {
1024
+ throw new Error("workflow-ledger-phase-invalid");
1025
+ }
1026
+ return {
1027
+ artifactHash: event.artifactHash,
1028
+ exitCode: event.exitCode,
1029
+ isolatedRevisionId: event.isolatedRevisionId,
1030
+ };
1031
+ }
1032
+
1033
+ async #runRedArtifactCorrection(input: {
1034
+ resources: PhaseResources;
1035
+ ledger: TaskLedger;
1036
+ baseline?: DurableVerificationBaseline;
1037
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
1038
+ }): Promise<WorkflowAttemptOutcome> {
1039
+ const { resources, ledger } = input;
1040
+ const request = input.attempt;
1041
+ if (
1042
+ request.phase !== "green" ||
1043
+ request.artifactCorrection?.code !== "red-artifact-constraint"
1044
+ ) {
1045
+ throw new Error("workflow-red-artifact-correction-invalid");
1046
+ }
1047
+ const priorRed = this.#verifiedPhaseReplay(ledger, {
1048
+ ...request,
1049
+ phase: "red",
1050
+ });
1051
+ if (!hasVerificationLifecycle(request.plan)) {
1052
+ return { kind: "paused", code: "red-artifact-constraint" };
1053
+ }
1054
+ const stableRevisionId = resources.currentRevisionId;
1055
+ const correctionPaths = [
1056
+ ...new Set(
1057
+ Object.values(request.task.phases).flatMap((phase) => [
1058
+ ...phase.write,
1059
+ ...phase.delete,
1060
+ ]),
1061
+ ),
1062
+ ];
1063
+ const rejectCorrection = async <T extends WorkflowAttemptOutcome>(
1064
+ outcome: T,
1065
+ ): Promise<T> => {
1066
+ if (resources.currentRevisionId !== stableRevisionId) {
1067
+ await this.#rollbackWorkspacePaths({
1068
+ resources,
1069
+ baseRevisionId: stableRevisionId,
1070
+ rejectedRevisionId: resources.currentRevisionId,
1071
+ paths: correctionPaths,
1072
+ });
1073
+ }
1074
+ return outcome;
1075
+ };
1076
+ const failureIdentities = [
1077
+ hash(
1078
+ "red-artifact-constraint",
1079
+ request.taskId,
1080
+ request.task.phases.red.verification.id,
1081
+ ),
1082
+ ];
1083
+ const correctionAttemptUsed = request.artifactCorrection.attempt;
1084
+ const correctionAction = {
1085
+ kind: "red-correction" as const,
1086
+ attempt:
1087
+ request.additionalAttempt || request.verificationOnly
1088
+ ? 1
1089
+ : correctionAttemptUsed,
1090
+ };
1091
+ const correctionDecision =
1092
+ request.recoveryDecision?.(correctionAction) ??
1093
+ decideRecoveryAction(request.plan, correctionAction, request);
1094
+ if (!correctionDecision.allowed)
1095
+ return { kind: "paused", code: correctionDecision.code };
1096
+ const repaired = await this.#runRepairCandidate({
1097
+ resources,
1098
+ ledger,
1099
+ attempt: request,
1100
+ repairAttempt: correctionAttemptUsed,
1101
+ failureIdentities,
1102
+ });
1103
+ if (repaired.kind !== "repair-committed") {
1104
+ return rejectCorrection(repaired);
1105
+ }
1106
+
1107
+ const correctedRevision = resources.workspaces.getRevision(
1108
+ repaired.commit.isolatedRevisionId,
1109
+ );
1110
+ const acceptedRedRevision = resources.workspaces.getRevision(
1111
+ priorRed.isolatedRevisionId,
1112
+ );
1113
+ if (!acceptedRedRevision.parentRevisionId) {
1114
+ throw new Error("workflow-ledger-revision-base-invalid");
1115
+ }
1116
+ const correctedRedChanges: Record<
1117
+ string,
1118
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1119
+ > = {};
1120
+ for (const relative of [
1121
+ ...new Set([
1122
+ ...request.task.phases.red.write,
1123
+ ...request.task.phases.red.delete,
1124
+ ]),
1125
+ ]) {
1126
+ const entry = correctedRevision.entries[relative];
1127
+ correctedRedChanges[relative] =
1128
+ entry?.kind === "file"
1129
+ ? {
1130
+ kind: "file",
1131
+ bytes: resources.artifacts.read(entry.hash),
1132
+ mode: entry.mode,
1133
+ }
1134
+ : { kind: "absent" };
1135
+ }
1136
+ const correctedRedRevision = resources.workspaces.createRevision({
1137
+ parentRevisionId: acceptedRedRevision.parentRevisionId,
1138
+ changes: correctedRedChanges,
1139
+ });
1140
+ const verifyCorrectedContract = async (input: {
1141
+ phase: "red" | "green";
1142
+ revisionId: string;
1143
+ verification: StructuredVerificationContract;
1144
+ }): Promise<DurablePhaseVerificationResult> => {
1145
+ const root = mkdtempSync(path.join(resources.root, "correction-check-"));
1146
+ try {
1147
+ await resources.workspaces.materializeAsync(
1148
+ input.revisionId,
1149
+ root,
1150
+ request.signal,
1151
+ );
1152
+ return await this.#services.verifyPhase({
1153
+ executionWritePaths: this.#executionWritePaths(resources),
1154
+ runId: request.runId,
1155
+ deliveryRevision: request.deliveryRevision,
1156
+ taskId: request.taskId,
1157
+ phase: input.phase,
1158
+ root,
1159
+ verification: structuredClone(input.verification),
1160
+ signal: request.signal,
1161
+ });
1162
+ } finally {
1163
+ rmSync(root, { recursive: true, force: true });
1164
+ }
1165
+ };
1166
+ emitWorkflowActivity(request.onActivity, { state: "verifying" });
1167
+ const redVerification = await verifyCorrectedContract({
1168
+ phase: "red",
1169
+ revisionId: correctedRedRevision.revisionId,
1170
+ verification: request.task.phases.red.verification,
1171
+ });
1172
+ if (!redVerification.ok) {
1173
+ return rejectCorrection({
1174
+ ...redVerification,
1175
+ ...(redVerification.kind === "retryable"
1176
+ ? { retryPolicy: "verification" as const }
1177
+ : {}),
1178
+ });
1179
+ }
1180
+ if (
1181
+ redVerification.classification !== "expected-red" ||
1182
+ redVerification.exitCode === 0
1183
+ ) {
1184
+ return rejectCorrection({
1185
+ kind: "retryable",
1186
+ code: "verification-rejected",
1187
+ retryPolicy: "verification",
1188
+ });
1189
+ }
1190
+ const greenVerification = await verifyCorrectedContract({
1191
+ phase: "green",
1192
+ revisionId: repaired.commit.isolatedRevisionId,
1193
+ verification: request.task.phases.green.verification,
1194
+ });
1195
+ if (!greenVerification.ok) {
1196
+ return rejectCorrection({
1197
+ ...greenVerification,
1198
+ ...(greenVerification.kind === "retryable"
1199
+ ? { retryPolicy: "verification" as const }
1200
+ : {}),
1201
+ });
1202
+ }
1203
+ if (
1204
+ greenVerification.classification !== "expected-green" ||
1205
+ greenVerification.exitCode !== 0
1206
+ ) {
1207
+ return rejectCorrection({
1208
+ kind: "retryable",
1209
+ code: "verification-rejected",
1210
+ retryPolicy: "verification",
1211
+ });
1212
+ }
1213
+
1214
+ const repairEvent: VerifiedTaskEvent = {
1215
+ runId: request.runId,
1216
+ taskId: request.taskId,
1217
+ eventId: `red-artifact-correction-${hash(
1218
+ request.operationId,
1219
+ request.taskId,
1220
+ repaired.commit.artifactHash,
1221
+ ).slice(0, 40)}`,
1222
+ kind: "repair-verified",
1223
+ phase: request.phase,
1224
+ attempt: correctionAttemptUsed,
1225
+ commandId: request.task.repairVerification.id,
1226
+ exitCode: 0,
1227
+ diagnostic: repaired.commit.diagnostic,
1228
+ artifactHash: repaired.commit.artifactHash,
1229
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
1230
+ outputFacts: repaired.commit.outputFacts,
1231
+ attribution: "introduced",
1232
+ failureIdentities: [
1233
+ hash(
1234
+ "red-artifact-constraint",
1235
+ request.taskId,
1236
+ request.task.phases.red.verification.id,
1237
+ ),
1238
+ ],
1239
+ routeId: repaired.commit.routeId,
1240
+ routeFingerprint: repaired.commit.routeFingerprint,
1241
+ };
1242
+ const greenPaths = new Set([
1243
+ ...request.task.phases.green.write,
1244
+ ...request.task.phases.green.delete,
1245
+ ]);
1246
+ const greenEvent: VerifiedTaskEvent = {
1247
+ runId: request.runId,
1248
+ taskId: request.taskId,
1249
+ eventId: `green-corrected-${hash(
1250
+ request.operationId,
1251
+ request.taskId,
1252
+ repaired.commit.artifactHash,
1253
+ ).slice(0, 40)}`,
1254
+ kind: "phase-verified",
1255
+ phase: "green",
1256
+ commandId: request.task.phases.green.verification.id,
1257
+ exitCode: greenVerification.exitCode,
1258
+ expectedClassification: "expected-green",
1259
+ actualClassification: greenVerification.classification,
1260
+ diagnostic: greenVerification.diagnostic,
1261
+ artifactHash: repaired.commit.artifactHash,
1262
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
1263
+ outputFacts: repaired.commit.outputFacts.filter((fact) =>
1264
+ greenPaths.has(fact.path),
1265
+ ),
1266
+ routeId: repaired.commit.routeId,
1267
+ routeFingerprint: repaired.commit.routeFingerprint,
1268
+ };
1269
+ const events: VerifiedTaskEvent[] = [repairEvent, greenEvent];
1270
+ if (input.baseline) {
1271
+ const affected = await this.#services.verifyTaskAffected({
1272
+ resources,
1273
+ baseline: input.baseline,
1274
+ task: request.task,
1275
+ revisionId: repaired.commit.isolatedRevisionId,
1276
+ signal: request.signal,
1277
+ });
1278
+ if (affected.kind === "repairable") {
1279
+ return rejectCorrection({
1280
+ kind: "retryable",
1281
+ code: "red-artifact-constraint",
1282
+ retryPolicy: "artifact",
1283
+ });
1284
+ }
1285
+ if (affected.kind !== "verified") return rejectCorrection(affected);
1286
+ }
1287
+ if (request.task.phases.refactor) {
1288
+ ledger.commitVerifiedEvents(events);
1289
+ return {
1290
+ kind: "phase-committed",
1291
+ artifactHash: repaired.commit.artifactHash,
1292
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
1293
+ exitCode: greenVerification.exitCode,
1294
+ classification: greenVerification.classification,
1295
+ routeId: repaired.commit.routeId,
1296
+ routeFingerprint: repaired.commit.routeFingerprint,
1297
+ };
1298
+ }
1299
+ const completedRevision = this.completeTracking(resources, request.taskId);
1300
+ ledger.commitTaskCompletion({
1301
+ events,
1302
+ factKey: `task-final-${hash(request.taskId).slice(0, 40)}`,
1303
+ fact: {
1304
+ taskId: request.taskId,
1305
+ phase: request.phase,
1306
+ isolatedRevisionId: completedRevision.revisionId,
1307
+ attribution: "introduced",
1308
+ repairAttempts: correctionAttemptUsed,
1309
+ },
1310
+ });
1311
+ return {
1312
+ kind: "phase-committed",
1313
+ artifactHash: repaired.commit.artifactHash,
1314
+ isolatedRevisionId: completedRevision.revisionId,
1315
+ exitCode: greenVerification.exitCode,
1316
+ classification: greenVerification.classification,
1317
+ routeId: repaired.commit.routeId,
1318
+ routeFingerprint: repaired.commit.routeFingerprint,
1319
+ };
1320
+ }
1321
+
1322
+ async #runCumulativeRepair(input: {
1323
+ resources: PhaseResources;
1324
+ ledger: TaskLedger;
1325
+ baseline: DurableVerificationBaseline;
1326
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
1327
+ }): Promise<WorkflowAttemptOutcome> {
1328
+ const { resources, ledger } = input;
1329
+ const request = input.attempt;
1330
+ const finalPhase = request.task.phases.refactor ? "refactor" : "green";
1331
+ const requestedFailures = request.repair?.failureIdentities;
1332
+ if (
1333
+ request.phase !== finalPhase ||
1334
+ request.repair?.attribution !== "introduced" ||
1335
+ !Array.isArray(requestedFailures) ||
1336
+ requestedFailures.length === 0 ||
1337
+ requestedFailures.length > 256 ||
1338
+ requestedFailures.some(
1339
+ (identity) => typeof identity !== "string" || !SHA256.test(identity),
1340
+ )
1341
+ ) {
1342
+ throw new Error("workflow-repair-context-invalid");
1343
+ }
1344
+ this.#verifiedPhaseReplay(ledger, request);
1345
+ const projection = ledger.projection({
1346
+ runId: request.runId,
1347
+ taskId: request.taskId,
1348
+ nextPhase: request.phase,
1349
+ }) as { history: Array<Record<string, unknown>> };
1350
+ const priorRepairs = projection.history.filter(
1351
+ (event) =>
1352
+ event.kind === "repair-verified" && event.phase === request.phase,
1353
+ );
1354
+ let stableRevisionId = resources.currentRevisionId;
1355
+ let affected: DurableAffectedResult = {
1356
+ kind: "repairable",
1357
+ attribution: "introduced",
1358
+ failureIdentities: [...new Set(requestedFailures)].sort(),
1359
+ };
1360
+ const replayedRepair = priorRepairs.at(-1);
1361
+ if (replayedRepair) {
1362
+ if (
1363
+ typeof replayedRepair.artifactHash !== "string" ||
1364
+ !SHA256.test(replayedRepair.artifactHash) ||
1365
+ typeof replayedRepair.isolatedRevisionId !== "string" ||
1366
+ !SHA256.test(replayedRepair.isolatedRevisionId)
1367
+ ) {
1368
+ throw new Error("workflow-ledger-phase-invalid");
1369
+ }
1370
+ if (
1371
+ this.#revisionDescendsFrom(
1372
+ resources,
1373
+ replayedRepair.isolatedRevisionId,
1374
+ stableRevisionId,
1375
+ )
1376
+ ) {
1377
+ resources.currentRevisionId = replayedRepair.isolatedRevisionId;
1378
+ stableRevisionId = replayedRepair.isolatedRevisionId;
1379
+ const replayedAffected = await this.#services.verifyTaskAffected({
1380
+ resources,
1381
+ baseline: input.baseline,
1382
+ task: request.task,
1383
+ revisionId: stableRevisionId,
1384
+ signal: request.signal,
1385
+ });
1386
+ if (replayedAffected.kind === "verified") {
1387
+ return {
1388
+ kind: "phase-committed",
1389
+ artifactHash: replayedRepair.artifactHash,
1390
+ isolatedRevisionId: stableRevisionId,
1391
+ exitCode: 0,
1392
+ classification: expectedClassification(request.phase),
1393
+ };
1394
+ }
1395
+ if (replayedAffected.kind !== "repairable") return replayedAffected;
1396
+ affected = replayedAffected;
1397
+ }
1398
+ }
1399
+
1400
+ const repairEvents: VerifiedTaskEvent[] = [];
1401
+ let lastCommit: DurableCandidateCommit | undefined;
1402
+ let repairAttempt = 0;
1403
+ let uncommittedRevisionId: string | undefined;
1404
+ const rollbackPaths = [
1405
+ ...new Set(
1406
+ Object.values(request.task.phases).flatMap((phase) => [
1407
+ ...phase.write,
1408
+ ...phase.delete,
1409
+ ]),
1410
+ ),
1411
+ ];
1412
+ const discardUncommitted = async <T extends WorkflowAttemptOutcome>(
1413
+ outcome: T,
1414
+ ): Promise<T> => {
1415
+ if (uncommittedRevisionId) {
1416
+ await this.#rollbackWorkspacePaths({
1417
+ resources,
1418
+ baseRevisionId: stableRevisionId,
1419
+ rejectedRevisionId: uncommittedRevisionId,
1420
+ paths: rollbackPaths,
1421
+ });
1422
+ }
1423
+ return outcome;
1424
+ };
1425
+ while (affected.kind === "repairable") {
1426
+ if (
1427
+ !(
1428
+ request.recoveryDecision?.({
1429
+ kind: "cumulative-repair",
1430
+ attempt: repairAttempt + 1,
1431
+ }) ??
1432
+ decideRecoveryAction(
1433
+ request.plan,
1434
+ { kind: "cumulative-repair", attempt: repairAttempt + 1 },
1435
+ request,
1436
+ )
1437
+ ).allowed
1438
+ ) {
1439
+ return discardUncommitted({
1440
+ kind: "paused",
1441
+ code: "repair-attempts-exhausted",
1442
+ });
1443
+ }
1444
+ repairAttempt += 1;
1445
+ const failureIdentities = [...affected.failureIdentities];
1446
+ const repair = await this.#runRepairCandidate({
1447
+ resources,
1448
+ ledger,
1449
+ attempt: request,
1450
+ repairAttempt,
1451
+ failureIdentities,
1452
+ });
1453
+ if (repair.kind !== "repair-committed") {
1454
+ return discardUncommitted(repair);
1455
+ }
1456
+ lastCommit = repair.commit;
1457
+ uncommittedRevisionId = repair.commit.isolatedRevisionId;
1458
+ repairEvents.push({
1459
+ runId: request.runId,
1460
+ taskId: request.taskId,
1461
+ eventId: `cumulative-repair-${hash(
1462
+ stableRevisionId,
1463
+ request.taskId,
1464
+ String(repairAttempt),
1465
+ ).slice(0, 40)}`,
1466
+ kind: "repair-verified",
1467
+ phase: request.phase,
1468
+ attempt: repairAttempt,
1469
+ commandId: request.task.repairVerification.id,
1470
+ exitCode: 0,
1471
+ diagnostic: repair.commit.diagnostic,
1472
+ artifactHash: repair.commit.artifactHash,
1473
+ isolatedRevisionId: repair.commit.isolatedRevisionId,
1474
+ outputFacts: repair.commit.outputFacts,
1475
+ attribution: "introduced",
1476
+ failureIdentities,
1477
+ routeId: repair.commit.routeId,
1478
+ routeFingerprint: repair.commit.routeFingerprint,
1479
+ });
1480
+ affected = await this.#services.verifyTaskAffected({
1481
+ resources,
1482
+ baseline: input.baseline,
1483
+ task: request.task,
1484
+ revisionId: repair.commit.isolatedRevisionId,
1485
+ signal: request.signal,
1486
+ });
1487
+ }
1488
+ if (affected.kind !== "verified") return discardUncommitted(affected);
1489
+ if (!lastCommit || repairEvents.length === 0) {
1490
+ throw new Error("workflow-repair-commit-unavailable");
1491
+ }
1492
+ ledger.commitVerifiedEvents(repairEvents);
1493
+ return {
1494
+ kind: "phase-committed",
1495
+ artifactHash: lastCommit.artifactHash,
1496
+ isolatedRevisionId: lastCommit.isolatedRevisionId,
1497
+ exitCode: 0,
1498
+ classification: expectedClassification(request.phase),
1499
+ routeId: lastCommit.routeId,
1500
+ routeFingerprint: lastCommit.routeFingerprint,
1501
+ };
1502
+ }
1503
+
1504
+ async hasPendingVerification(
1505
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1506
+ ): Promise<boolean> {
1507
+ if (!input.baselineRevisionId || !input.currentWorkspaceRevisionId)
1508
+ return false;
1509
+ const resources = await this.#services.prepareResources(
1510
+ input,
1511
+ input.signal,
1512
+ );
1513
+ const ledger = this.#services.ledger(resources, input.deliveryRevision);
1514
+ const verificationIds = [
1515
+ input.task.phases[input.phase]?.verification.id,
1516
+ input.task.repairVerification?.id,
1517
+ ];
1518
+ return verificationIds.some(
1519
+ (id) =>
1520
+ id &&
1521
+ this.#pendingCandidate(
1522
+ ledger,
1523
+ `pending-${hash(input.taskId, input.phase, id).slice(0, 40)}`,
1524
+ resources.currentRevisionId,
1525
+ ) !== undefined,
1526
+ );
1527
+ }
1528
+
1529
+ async runAttempt(
1530
+ input: Parameters<WorkflowWorker["runAttempt"]>[0],
1531
+ ): Promise<WorkflowAttemptOutcome> {
1532
+ if (input.signal.aborted) {
1533
+ return { kind: "operation-cancelled", code: "cancelled" };
1534
+ }
1535
+ const resources = await this.#services.prepareResources(
1536
+ {
1537
+ runId: input.runId,
1538
+ deliveryRevision: input.deliveryRevision,
1539
+ plan: input.plan,
1540
+ ...(input.baselineRevisionId
1541
+ ? { baselineRevisionId: input.baselineRevisionId }
1542
+ : {}),
1543
+ ...(input.currentWorkspaceRevisionId
1544
+ ? { currentWorkspaceRevisionId: input.currentWorkspaceRevisionId }
1545
+ : {}),
1546
+ },
1547
+ input.signal,
1548
+ );
1549
+ let selectedRoute: Required<WorkflowRouteFacts> | undefined;
1550
+ const retained = <T extends WorkflowAttemptOutcome>(outcome: T): T =>
1551
+ ({
1552
+ ...(selectedRoute ?? {}),
1553
+ ...outcome,
1554
+ baselineRevisionId: resources.baselineRevisionId,
1555
+ currentWorkspaceRevisionId: resources.currentRevisionId,
1556
+ }) as T;
1557
+ const ledger = this.#services.ledger(resources, input.deliveryRevision);
1558
+ let verificationBaseline: DurableVerificationBaseline | undefined;
1559
+ if (hasVerificationLifecycle(input.plan)) {
1560
+ emitWorkflowActivity(input.onActivity, { state: "verifying" });
1561
+ const captured = await this.#services.ensureVerificationBaseline({
1562
+ resources,
1563
+ ledger,
1564
+ signal: input.signal,
1565
+ });
1566
+ if (!captured.ok) {
1567
+ const outcome = captured.outcome;
1568
+ return retained(
1569
+ outcome.kind === "paused"
1570
+ ? {
1571
+ ...outcome,
1572
+ verification: {
1573
+ scope: "baseline",
1574
+ attribution: "environment",
1575
+ },
1576
+ }
1577
+ : outcome,
1578
+ );
1579
+ }
1580
+ verificationBaseline = captured.baseline;
1581
+ }
1582
+ this.#openTask(resources, ledger, input);
1583
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
1584
+ return retained(
1585
+ await this.#runRedArtifactCorrection({
1586
+ resources,
1587
+ ledger,
1588
+ ...(verificationBaseline ? { baseline: verificationBaseline } : {}),
1589
+ attempt: input,
1590
+ }),
1591
+ );
1592
+ }
1593
+ if (input.repair) {
1594
+ if (!verificationBaseline) {
1595
+ throw new Error("workflow-verification-baseline-unavailable");
1596
+ }
1597
+ return retained(
1598
+ await this.#runCumulativeRepair({
1599
+ resources,
1600
+ ledger,
1601
+ baseline: verificationBaseline,
1602
+ attempt: input,
1603
+ }),
1604
+ );
1605
+ }
1606
+ const policyFailure = await this.revalidatePhasePolicy(
1607
+ resources,
1608
+ ledger,
1609
+ input.task,
1610
+ input.signal,
1611
+ );
1612
+ if (policyFailure) return retained(policyFailure);
1613
+ const replay = this.#replayedPhase(resources, ledger, input);
1614
+ if (replay) return retained(replay);
1615
+ const phase = input.task.phases[input.phase];
1616
+ if (!phase) throw new Error("workflow-task-phase-invalid");
1617
+ const approvedPaths = [
1618
+ ...new Set([...phase.write, ...phase.delete]),
1619
+ ].sort();
1620
+ const rollbackPaths = [
1621
+ ...new Set(
1622
+ Object.values(input.task.phases).flatMap((boundary) => [
1623
+ ...boundary.write,
1624
+ ...boundary.delete,
1625
+ ]),
1626
+ ),
1627
+ ].sort();
1628
+ if (approvedPaths.length === 0) {
1629
+ return retained({
1630
+ kind: "approval-needed",
1631
+ code: "task-write-set-empty",
1632
+ });
1633
+ }
1634
+ const baseRevisionId = resources.currentRevisionId;
1635
+ const pendingKey = `pending-${hash(input.taskId, input.phase, phase.verification.id).slice(0, 40)}`;
1636
+ const pending = this.#pendingCandidate(ledger, pendingKey, baseRevisionId);
1637
+ const proposalRoot = mkdtempSync(path.join(resources.root, "attempt-"));
1638
+ try {
1639
+ await resources.workspaces.materializeAsync(
1640
+ baseRevisionId,
1641
+ proposalRoot,
1642
+ input.signal,
1643
+ );
1644
+ if (input.routeId && !pending) {
1645
+ const rebound = this.#broker.resumeBinding({
1646
+ runId: input.runId,
1647
+ taskId: input.taskId,
1648
+ role: "implementation-worker",
1649
+ routeId: input.routeId,
1650
+ ...(input.routeFingerprint
1651
+ ? { expectedFingerprint: input.routeFingerprint }
1652
+ : {}),
1653
+ requirements: implementationRouteRequirements({
1654
+ task: input.task,
1655
+ ...(input.artifactCorrection
1656
+ ? { artifactCorrection: input.artifactCorrection }
1657
+ : {}),
1658
+ ...(input.repair ? { repair: input.repair } : {}),
1659
+ }),
1660
+ });
1661
+ if (!rebound.ok) {
1662
+ return retained({ kind: "paused", code: rebound.code });
1663
+ }
1664
+ }
1665
+ const ledgerProjection = ledger.projection({
1666
+ runId: input.runId,
1667
+ taskId: input.taskId,
1668
+ nextPhase: input.phase,
1669
+ });
1670
+ let identity: BeginCandidateInput | undefined = pending?.identity;
1671
+ const routed = pending
1672
+ ? {
1673
+ ok: true as const,
1674
+ value: pending.proposal,
1675
+ routeId: pending.identity.routeId,
1676
+ routeFingerprint: pending.identity.routeFingerprint,
1677
+ }
1678
+ : await this.#broker.run({
1679
+ runId: input.runId,
1680
+ taskId: input.taskId,
1681
+ operationId: `${input.operationId}:${input.taskId}:${input.phase}`,
1682
+ role: "implementation-worker",
1683
+ requirements: implementationRouteRequirements({
1684
+ task: input.task,
1685
+ ...(input.artifactCorrection
1686
+ ? { artifactCorrection: input.artifactCorrection }
1687
+ : {}),
1688
+ ...(input.repair ? { repair: input.repair } : {}),
1689
+ }),
1690
+ signal: input.signal,
1691
+ ...(input.onActivity ? { onActivity: input.onActivity } : {}),
1692
+ classifyResult: semanticRouteFailure,
1693
+ execute: async (attempt) => {
1694
+ identity = {
1695
+ candidateId: `candidate-${randomUUID()}`,
1696
+ runId: input.runId,
1697
+ deliveryRevision: input.deliveryRevision,
1698
+ taskId: input.taskId,
1699
+ phase: input.phase,
1700
+ attemptId: `attempt-${hash(
1701
+ input.operationId,
1702
+ input.taskId,
1703
+ input.phase,
1704
+ attempt.route.fingerprint,
1705
+ ).slice(0, 40)}`,
1706
+ approvedPaths,
1707
+ isolatedRevisionId: baseRevisionId,
1708
+ verificationId: phase.verification.id,
1709
+ routeId: attempt.route.id,
1710
+ routeFingerprint: attempt.route.fingerprint,
1711
+ };
1712
+ if (input.reserveCandidate && !input.reserveCandidate())
1713
+ return {
1714
+ kind: "paused" as const,
1715
+ code:
1716
+ input.additionalAttempt || input.verificationOnly
1717
+ ? "repair-attempts-exhausted"
1718
+ : "change-work-budget-exhausted",
1719
+ };
1720
+ return this.#options.proposeCandidate({
1721
+ runId: input.runId,
1722
+ operationId: input.operationId,
1723
+ deliveryRevision: input.deliveryRevision,
1724
+ taskId: input.taskId,
1725
+ phase: input.phase,
1726
+ task: structuredClone(input.task),
1727
+ contextReadPaths: input.contextReadPaths,
1728
+ workspaceRoot: proposalRoot,
1729
+ ledgerProjection,
1730
+ candidateArtifact: {
1731
+ ledger,
1732
+ identity,
1733
+ workspaceRoot: proposalRoot,
1734
+ writePaths: phase.write,
1735
+ deletePaths: phase.delete,
1736
+ },
1737
+ route: attempt.route,
1738
+ ...(input.contextRequest
1739
+ ? { contextRequest: input.contextRequest }
1740
+ : {}),
1741
+ ...(input.recoveryFeedback
1742
+ ? { recoveryFeedback: input.recoveryFeedback }
1743
+ : {}),
1744
+ signal: attempt.signal,
1745
+ onRequestStart: attempt.onRequestStart,
1746
+ onHeaders: attempt.onHeaders,
1747
+ onProgress: attempt.onProgress,
1748
+ });
1749
+ },
1750
+ });
1751
+ if (!routed.ok) {
1752
+ return retained(
1753
+ routed.state === "cancelled" || input.signal.aborted
1754
+ ? { kind: "operation-cancelled", code: "cancelled" }
1755
+ : { kind: "paused", code: routed.code },
1756
+ );
1757
+ }
1758
+ if (!identity || identity.routeId !== routed.routeId) {
1759
+ throw new Error("candidate-route-identity-invalid");
1760
+ }
1761
+ const correctionIdentity = identity;
1762
+ selectedRoute = {
1763
+ routeId: identity.routeId,
1764
+ routeFingerprint: identity.routeFingerprint,
1765
+ };
1766
+ let uncommittedRevisionId: string | undefined;
1767
+ const reject = async <T extends WorkflowAttemptOutcome>(
1768
+ outcome: T,
1769
+ correction: {
1770
+ category: "artifact" | "stale" | "verification" | "checkpoint";
1771
+ code: string;
1772
+ stage: string;
1773
+ },
1774
+ rollback = false,
1775
+ ): Promise<T> => {
1776
+ if (rollback && uncommittedRevisionId) {
1777
+ await this.#rollbackWorkspacePaths({
1778
+ resources,
1779
+ baseRevisionId,
1780
+ rejectedRevisionId: uncommittedRevisionId,
1781
+ paths: rollbackPaths,
1782
+ });
1783
+ }
1784
+ this.#recordCorrection(ledger, input, correctionIdentity, correction);
1785
+ return retained({ ...outcome, retryPolicy: correction.category });
1786
+ };
1787
+ const proposal = routed.value;
1788
+ if (
1789
+ proposal.kind !== "candidate" &&
1790
+ proposal.kind !== "sealed-candidate"
1791
+ ) {
1792
+ return proposal.kind === "operation-cancelled" ||
1793
+ proposal.kind === "paused" ||
1794
+ proposal.kind === "approval-needed"
1795
+ ? retained(proposal)
1796
+ : reject(proposal, {
1797
+ category: "artifact",
1798
+ code: proposal.code,
1799
+ stage: "candidate-submit",
1800
+ });
1801
+ }
1802
+ emitWorkflowActivity(input.onActivity, { state: "validating" });
1803
+ const inspected = inspectCandidateArtifact({
1804
+ ledger,
1805
+ identity,
1806
+ proposal,
1807
+ approvedPaths,
1808
+ });
1809
+ if (!inspected.ok) {
1810
+ return reject(
1811
+ { kind: "retryable", code: inspected.code },
1812
+ {
1813
+ category: "artifact",
1814
+ code: inspected.code,
1815
+ stage: inspected.stage,
1816
+ },
1817
+ );
1818
+ }
1819
+ const { bytes, paths: candidatePaths } = inspected;
1820
+ const dependencySensitive = candidatePaths.some(
1821
+ (relative) => relative === "package.json" || LOCKFILES.has(relative),
1822
+ );
1823
+ let dependenciesBefore: Map<string, DependencyContractEntry> | undefined;
1824
+ if (dependencySensitive) {
1825
+ try {
1826
+ dependenciesBefore = dependencyContract(proposalRoot);
1827
+ } catch (error) {
1828
+ if (!(error instanceof DependencyManifestError)) throw error;
1829
+ return reject(
1830
+ { kind: "retryable", code: error.message },
1831
+ {
1832
+ category: "artifact",
1833
+ code: error.message,
1834
+ stage: "candidate-dependency",
1835
+ },
1836
+ );
1837
+ }
1838
+ }
1839
+ const checked = await runGitApply(
1840
+ proposalRoot,
1841
+ bytes,
1842
+ true,
1843
+ input.signal,
1844
+ );
1845
+ if (checked !== "ok") {
1846
+ return checked === "cancelled"
1847
+ ? retained({ kind: "operation-cancelled", code: "cancelled" })
1848
+ : reject(
1849
+ { kind: "retryable", code: "candidate-diff-invalid" },
1850
+ {
1851
+ category: "artifact",
1852
+ code: "candidate-diff-invalid",
1853
+ stage: "candidate-check",
1854
+ },
1855
+ );
1856
+ }
1857
+ const applied = await runGitApply(
1858
+ proposalRoot,
1859
+ bytes,
1860
+ false,
1861
+ input.signal,
1862
+ );
1863
+ if (applied !== "ok") {
1864
+ return applied === "cancelled"
1865
+ ? retained({ kind: "operation-cancelled", code: "cancelled" })
1866
+ : reject(
1867
+ { kind: "retryable", code: "candidate-diff-invalid" },
1868
+ {
1869
+ category: "artifact",
1870
+ code: "candidate-diff-invalid",
1871
+ stage: "candidate-apply",
1872
+ },
1873
+ );
1874
+ }
1875
+ if (dependenciesBefore) {
1876
+ let dependenciesAfter: Map<string, DependencyContractEntry>;
1877
+ try {
1878
+ dependenciesAfter = dependencyContract(proposalRoot);
1879
+ } catch (error) {
1880
+ if (!(error instanceof DependencyManifestError)) throw error;
1881
+ return reject(
1882
+ { kind: "retryable", code: error.message },
1883
+ {
1884
+ category: "artifact",
1885
+ code: error.message,
1886
+ stage: "candidate-dependency",
1887
+ },
1888
+ );
1889
+ }
1890
+ if (
1891
+ hasUnapprovedDependencyChange(
1892
+ dependenciesBefore,
1893
+ dependenciesAfter,
1894
+ input.task.approvedDependencies,
1895
+ candidatePaths.some((relative) => LOCKFILES.has(relative)),
1896
+ )
1897
+ ) {
1898
+ return reject(
1899
+ { kind: "approval-needed", code: "unapproved-dependency-change" },
1900
+ {
1901
+ category: "artifact",
1902
+ code: "unapproved-dependency-change",
1903
+ stage: "candidate-dependency",
1904
+ },
1905
+ );
1906
+ }
1907
+ }
1908
+ const sealed = sealCandidateArtifact({
1909
+ ledger,
1910
+ identity,
1911
+ proposal,
1912
+ bytes,
1913
+ });
1914
+ if (!sealed.ok) {
1915
+ return reject(
1916
+ { kind: sealed.kind, code: sealed.code },
1917
+ { category: "artifact", code: sealed.code, stage: sealed.stage },
1918
+ );
1919
+ }
1920
+ const { artifactHash } = sealed;
1921
+ const candidate = resources.workspaces.createRevision({
1922
+ parentRevisionId: baseRevisionId,
1923
+ changes: revisionChanges(proposalRoot, approvedPaths),
1924
+ });
1925
+ if (
1926
+ candidatePaths.some((relative) => {
1927
+ const entry = candidate.entries[relative];
1928
+ return phase.delete.includes(relative)
1929
+ ? entry?.kind !== "absent"
1930
+ : entry?.kind !== "file";
1931
+ })
1932
+ ) {
1933
+ return reject(
1934
+ { kind: "retryable", code: "write-set-mismatch" },
1935
+ {
1936
+ category: "artifact",
1937
+ code: "write-set-mismatch",
1938
+ stage: "candidate-output",
1939
+ },
1940
+ );
1941
+ }
1942
+ const requiredOutputs = input.plan.outputs.filter(
1943
+ (output) =>
1944
+ output.producer.taskId === input.taskId &&
1945
+ output.producer.phase === input.phase,
1946
+ );
1947
+ if (
1948
+ requiredOutputs.some(
1949
+ (output) => candidate.entries[output.path]?.kind !== "file",
1950
+ )
1951
+ ) {
1952
+ return reject(
1953
+ { kind: "retryable", code: "producer-output-unavailable" },
1954
+ {
1955
+ category: "artifact",
1956
+ code: "producer-output-unavailable",
1957
+ stage: "candidate-output",
1958
+ },
1959
+ );
1960
+ }
1961
+ emitWorkflowActivity(input.onActivity, { state: "verifying" });
1962
+ this.#savePendingCandidate(ledger, pendingKey, {
1963
+ identity,
1964
+ proposal: {
1965
+ kind: "sealed-candidate",
1966
+ candidateId: identity.candidateId,
1967
+ artifactHash,
1968
+ bytes: bytes.length,
1969
+ paths: candidatePaths,
1970
+ },
1971
+ });
1972
+ const verified = await this.#services.verifyPhase({
1973
+ executionWritePaths: this.#executionWritePaths(resources),
1974
+ runId: input.runId,
1975
+ deliveryRevision: input.deliveryRevision,
1976
+ taskId: input.taskId,
1977
+ phase: input.phase,
1978
+ root: proposalRoot,
1979
+ verification: structuredClone(phase.verification),
1980
+ signal: input.signal,
1981
+ });
1982
+ if (!verified.ok) {
1983
+ if (verified.kind === "paused") return retained(verified);
1984
+ this.#savePendingCandidate(ledger, pendingKey, null);
1985
+ return reject(verified, {
1986
+ category: "verification",
1987
+ code: verified.code,
1988
+ stage: "phase-verification",
1989
+ });
1990
+ }
1991
+ const expected = expectedClassification(input.phase);
1992
+ if (
1993
+ verified.classification !== expected ||
1994
+ (input.phase === "red"
1995
+ ? verified.exitCode === 0
1996
+ : verified.exitCode !== 0)
1997
+ ) {
1998
+ return reject(
1999
+ { kind: "retryable", code: "verification-rejected" },
2000
+ {
2001
+ category: "verification",
2002
+ code: "verification-rejected",
2003
+ stage: "phase-verification",
2004
+ },
2005
+ );
2006
+ }
2007
+ let merged: ReturnType<WorkspaceStore["mergeRevision"]>;
2008
+ try {
2009
+ merged = await this.#mergeCandidate(
2010
+ resources,
2011
+ input,
2012
+ baseRevisionId,
2013
+ candidate.revisionId,
2014
+ );
2015
+ uncommittedRevisionId = merged.revisionId;
2016
+ } catch (error) {
2017
+ if (
2018
+ error instanceof Error &&
2019
+ error.message === "workspace-revision-stale"
2020
+ ) {
2021
+ return reject(
2022
+ { kind: "retryable", code: "workspace-revision-stale" },
2023
+ {
2024
+ category: "stale",
2025
+ code: "workspace-revision-stale",
2026
+ stage: "candidate-merge",
2027
+ },
2028
+ );
2029
+ }
2030
+ throw error;
2031
+ }
2032
+ const outputFacts = approvedPaths.map((relative) => {
2033
+ const entry = merged.entries[relative];
2034
+ return entry?.kind === "file"
2035
+ ? {
2036
+ path: relative,
2037
+ kind: "file" as const,
2038
+ hash: entry.hash,
2039
+ bytes: entry.bytes,
2040
+ }
2041
+ : { path: relative, kind: "absent" as const };
2042
+ });
2043
+ if (
2044
+ requiredOutputs.some(
2045
+ (output) => merged.entries[output.path]?.kind !== "file",
2046
+ )
2047
+ ) {
2048
+ return reject(
2049
+ { kind: "retryable", code: "producer-output-unavailable" },
2050
+ {
2051
+ category: "artifact",
2052
+ code: "producer-output-unavailable",
2053
+ stage: "candidate-output",
2054
+ },
2055
+ true,
2056
+ );
2057
+ }
2058
+ const phaseEvent: VerifiedTaskEvent = {
2059
+ runId: input.runId,
2060
+ taskId: input.taskId,
2061
+ eventId: `${input.phase}-verified-r${input.deliveryRevision}`,
2062
+ kind: "phase-verified",
2063
+ phase: input.phase,
2064
+ commandId: phase.verification.id,
2065
+ exitCode: verified.exitCode,
2066
+ expectedClassification: expected,
2067
+ actualClassification: verified.classification,
2068
+ diagnostic: verified.diagnostic,
2069
+ artifactHash,
2070
+ isolatedRevisionId: merged.revisionId,
2071
+ outputFacts,
2072
+ routeId: identity.routeId,
2073
+ routeFingerprint: identity.routeFingerprint,
2074
+ };
2075
+ if (this.#options.verificationPolicy || resources.environmentIdentity)
2076
+ ledger.putDurableFact(
2077
+ this.#policyFactKey(
2078
+ resources,
2079
+ phaseEvent as unknown as Record<string, unknown>,
2080
+ ),
2081
+ { policy: this.#options.verificationPolicy ?? "legacy" },
2082
+ );
2083
+ const finalPhase = input.task.phases.refactor ? "refactor" : "green";
2084
+ if (!verificationBaseline || input.phase !== finalPhase) {
2085
+ ledger.commitVerifiedEvent(phaseEvent);
2086
+ return retained({
2087
+ kind: "phase-committed",
2088
+ artifactHash,
2089
+ isolatedRevisionId: merged.revisionId,
2090
+ exitCode: verified.exitCode,
2091
+ classification: verified.classification,
2092
+ baselineRevisionId: resources.baselineRevisionId,
2093
+ });
2094
+ }
2095
+
2096
+ const completionEvents: VerifiedTaskEvent[] = [phaseEvent];
2097
+ const discardUncommitted = async <T extends WorkflowAttemptOutcome>(
2098
+ outcome: T,
2099
+ ): Promise<T> => {
2100
+ if (uncommittedRevisionId) {
2101
+ await this.#rollbackWorkspacePaths({
2102
+ resources,
2103
+ baseRevisionId,
2104
+ rejectedRevisionId: uncommittedRevisionId,
2105
+ paths: rollbackPaths,
2106
+ });
2107
+ }
2108
+ return retained(outcome);
2109
+ };
2110
+ let affected = await this.#services.verifyTaskAffected({
2111
+ resources,
2112
+ baseline: verificationBaseline,
2113
+ task: input.task,
2114
+ revisionId: merged.revisionId,
2115
+ signal: input.signal,
2116
+ });
2117
+ let repairAttempts = 0;
2118
+ let completionAttribution: "none" | "pre-existing" | "introduced" =
2119
+ "none";
2120
+ while (affected.kind === "repairable") {
2121
+ completionAttribution = "introduced";
2122
+ if (
2123
+ !(
2124
+ input.recoveryDecision?.({
2125
+ kind: "affected-repair",
2126
+ attempt: repairAttempts + 1,
2127
+ }) ??
2128
+ decideRecoveryAction(
2129
+ input.plan,
2130
+ { kind: "affected-repair", attempt: repairAttempts + 1 },
2131
+ input,
2132
+ )
2133
+ ).allowed
2134
+ ) {
2135
+ return reject(
2136
+ { kind: "paused", code: "repair-attempts-exhausted" },
2137
+ {
2138
+ category: "verification",
2139
+ code: "repair-attempts-exhausted",
2140
+ stage: "task-affected-verification",
2141
+ },
2142
+ true,
2143
+ );
2144
+ }
2145
+ repairAttempts += 1;
2146
+ const failureIdentities = [...affected.failureIdentities];
2147
+ const repair = await this.#runRepairCandidate({
2148
+ resources,
2149
+ ledger,
2150
+ attempt: input,
2151
+ repairAttempt: repairAttempts,
2152
+ failureIdentities,
2153
+ });
2154
+ if (repair.kind !== "repair-committed") {
2155
+ return discardUncommitted(repair);
2156
+ }
2157
+ selectedRoute = {
2158
+ routeId: repair.commit.routeId,
2159
+ routeFingerprint: repair.commit.routeFingerprint,
2160
+ };
2161
+ uncommittedRevisionId = repair.commit.isolatedRevisionId;
2162
+ completionEvents.push({
2163
+ runId: input.runId,
2164
+ taskId: input.taskId,
2165
+ eventId: `repair-${repairAttempts}-verified-r${input.deliveryRevision}`,
2166
+ kind: "repair-verified",
2167
+ phase: input.phase,
2168
+ attempt: repairAttempts,
2169
+ commandId: input.task.repairVerification.id,
2170
+ exitCode: 0,
2171
+ diagnostic: repair.commit.diagnostic,
2172
+ artifactHash: repair.commit.artifactHash,
2173
+ isolatedRevisionId: repair.commit.isolatedRevisionId,
2174
+ outputFacts: repair.commit.outputFacts,
2175
+ attribution: "introduced",
2176
+ failureIdentities,
2177
+ routeId: repair.commit.routeId,
2178
+ routeFingerprint: repair.commit.routeFingerprint,
2179
+ });
2180
+ affected = await this.#services.verifyTaskAffected({
2181
+ resources,
2182
+ baseline: verificationBaseline,
2183
+ task: input.task,
2184
+ revisionId: repair.commit.isolatedRevisionId,
2185
+ signal: input.signal,
2186
+ });
2187
+ }
2188
+ if (affected.kind !== "verified") {
2189
+ return affected.kind === "operation-cancelled"
2190
+ ? discardUncommitted(affected)
2191
+ : reject(
2192
+ affected,
2193
+ {
2194
+ category: "verification",
2195
+ code: affected.code,
2196
+ stage: "task-affected-verification",
2197
+ },
2198
+ true,
2199
+ );
2200
+ }
2201
+ if (completionAttribution === "none") {
2202
+ completionAttribution = affected.attribution;
2203
+ }
2204
+ let completedRevision: WorkspaceRevision;
2205
+ try {
2206
+ completedRevision = this.completeTracking(resources, input.taskId);
2207
+ } catch (error) {
2208
+ if (
2209
+ error instanceof Error &&
2210
+ error.message === "workflow-tracking-task-mismatch"
2211
+ ) {
2212
+ return reject(
2213
+ { kind: "paused", code: "tracking-contract-invalid" },
2214
+ {
2215
+ category: "checkpoint",
2216
+ code: "tracking-contract-invalid",
2217
+ stage: "tracking-checkpoint",
2218
+ },
2219
+ true,
2220
+ );
2221
+ }
2222
+ throw error;
2223
+ }
2224
+ const factKey = `task-final-${hash(input.taskId).slice(0, 40)}`;
2225
+ ledger.commitTaskCompletion({
2226
+ events: completionEvents,
2227
+ factKey,
2228
+ fact: {
2229
+ taskId: input.taskId,
2230
+ phase: input.phase,
2231
+ isolatedRevisionId: completedRevision.revisionId,
2232
+ attribution: completionAttribution,
2233
+ repairAttempts,
2234
+ },
2235
+ });
2236
+ return retained({
2237
+ kind: "phase-committed",
2238
+ artifactHash,
2239
+ isolatedRevisionId: completedRevision.revisionId,
2240
+ exitCode: verified.exitCode,
2241
+ classification: verified.classification,
2242
+ baselineRevisionId: resources.baselineRevisionId,
2243
+ });
2244
+ } finally {
2245
+ rmSync(proposalRoot, { recursive: true, force: true });
2246
+ }
2247
+ }
2248
+ }