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