@abelxiaoxing/cadence 1.3.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 (76) hide show
  1. package/README.md +172 -13
  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 +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. 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
+ }