@abelxiaoxing/cadence 1.8.4 → 2.0.0

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