@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,1299 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import {
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- unlinkSync,
9
- writeFileSync,
10
- } from "node:fs";
11
- import path from "node:path";
12
- import {
13
- type ChangeContract,
14
- normalizeChangeContract,
15
- } from "./change-contract.ts";
16
- import { changeContractDiagnostics } from "./change-contract-schema.ts";
17
- import { isValidRelativePath } from "./contracts.ts";
18
- import {
19
- assessDeliveryTraceability,
20
- bindTaskVerifications,
21
- compileGateAReceipt,
22
- compileImplementPlan,
23
- compileReadyReceipt,
24
- type DeliveryArtifactBinding,
25
- DesignPlanValidationError,
26
- type GateApprovalProof,
27
- parseGateAReceipt,
28
- parseImplementPlan,
29
- parseReadyReceipt,
30
- } from "./delivery-compiler.ts";
31
- import type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
32
- import {
33
- type DesignArtifactOperationFacts,
34
- type DesignArtifactOperationOutcome,
35
- DesignJournal,
36
- type DesignStatusProjection,
37
- } from "./design-journal.ts";
38
- import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
39
- import { planAuthoringSummary } from "./plan-draft-summary.ts";
40
- import { canonicalJson } from "./run-state.ts";
41
- import { RunStore } from "./run-store.ts";
42
- import { observeSafePath } from "./safe-path.ts";
43
- import type { ResolvedStateRoot } from "./state-root.ts";
44
-
45
- const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
46
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
47
- const ARTIFACT_SEGMENT = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/iu;
48
- const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
49
- const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
50
- const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
51
-
52
- function validGateContract(value: unknown): boolean {
53
- if (typeof value === "string")
54
- return (
55
- value.trim().length > 0 &&
56
- Buffer.byteLength(value, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
57
- );
58
- try {
59
- normalizeChangeContract(value);
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- export interface DesignOpenSpecInspection {
67
- change: string;
68
- schema: string;
69
- planningComplete: boolean;
70
- strictValid: boolean;
71
- artifactPaths: string[];
72
- }
73
-
74
- export type DesignControlRequest =
75
- | {
76
- operation: "start";
77
- operationId: string;
78
- requirement: string;
79
- }
80
- | {
81
- operation: "start";
82
- operationId: string;
83
- change: string;
84
- }
85
- | {
86
- operation: "status";
87
- runId: string;
88
- }
89
- | {
90
- operation: "bind-change";
91
- runId: string;
92
- operationId: string;
93
- change: string;
94
- }
95
- | {
96
- operation: "record-decision";
97
- runId: string;
98
- operationId: string;
99
- decisionId: string;
100
- category: "behavior" | "technical";
101
- contract: string;
102
- refs: string[];
103
- }
104
- | {
105
- operation: "approve-gate";
106
- runId: string;
107
- operationId: string;
108
- gate: "gate-a";
109
- contract: string | ChangeContract;
110
- }
111
- | {
112
- operation: "approve-gate";
113
- runId: string;
114
- operationId: string;
115
- gate: "gate-b";
116
- }
117
- | {
118
- operation: "validate-plan-draft";
119
- runId: string;
120
- }
121
- | {
122
- operation: "compile-plan";
123
- runId: string;
124
- operationId: string;
125
- }
126
- | {
127
- operation: "finalize-delivery";
128
- runId: string;
129
- operationId: string;
130
- }
131
- | {
132
- operation: "write-artifact";
133
- runId: string;
134
- operationId: string;
135
- path: string;
136
- content: string;
137
- }
138
- | {
139
- operation: "delete-artifact";
140
- runId: string;
141
- operationId: string;
142
- path: string;
143
- };
144
-
145
- export interface DesignControllerOptions {
146
- consumerRoot: string;
147
- stateRoot: ResolvedStateRoot;
148
- now?: () => number;
149
- finalizationLeaseMs?: number;
150
- inspectOpenSpec: (
151
- consumerRoot: string,
152
- change: string,
153
- ) => Promise<DesignOpenSpecInspection>;
154
- }
155
-
156
- export class DesignFinalizationError extends Error {
157
- readonly diagnostics: readonly string[];
158
-
159
- readonly openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>;
160
-
161
- constructor(
162
- diagnostics: readonly string[],
163
- openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>,
164
- ) {
165
- const normalized = [...new Set(diagnostics)].sort();
166
- super("design-finalization-invalid");
167
- this.name = "DesignFinalizationError";
168
- this.diagnostics = Object.freeze(normalized);
169
- this.openSpecDiagnostic = openSpecDiagnostic;
170
- }
171
- }
172
-
173
- function isRecord(value: unknown): value is Record<string, unknown> {
174
- return value !== null && typeof value === "object" && !Array.isArray(value);
175
- }
176
-
177
- function exactKeys(
178
- value: Record<string, unknown>,
179
- keys: readonly string[],
180
- ): boolean {
181
- const actual = Object.keys(value);
182
- return (
183
- actual.length === keys.length && actual.every((key) => keys.includes(key))
184
- );
185
- }
186
-
187
- function normalizedTransientText(value: string): string {
188
- return value.replace(/\r\n?/gu, "\n").trim();
189
- }
190
-
191
- function transientHash(domain: string, value: string): string {
192
- return createHash("sha256")
193
- .update(domain)
194
- .update("\0")
195
- .update(normalizedTransientText(value))
196
- .digest("hex");
197
- }
198
-
199
- function lifecycleOperationId(kind: string, runId: string): string {
200
- return `design-${kind}-${createHash("sha256")
201
- .update(runId)
202
- .digest("hex")
203
- .slice(0, 32)}`;
204
- }
205
-
206
- export class DesignControlValidationError extends DesignPlanValidationError {
207
- constructor(diagnostics: readonly DesignPlanDiagnostic[]) {
208
- super("invalid-design-control-request", diagnostics);
209
- }
210
- }
211
- export function validateDesignControlRequest(value: unknown):
212
- | { ok: true; value: DesignControlRequest }
213
- | {
214
- ok: false;
215
- code: "invalid-design-control-request";
216
- diagnostics: DesignPlanDiagnostic[];
217
- } {
218
- const parsed = parseDesignControlRequest(value);
219
- if (parsed.ok) return parsed;
220
- let diagnostics: DesignPlanDiagnostic[] = [];
221
- if (
222
- isRecord(value) &&
223
- value.operation === "approve-gate" &&
224
- value.gate === "gate-a" &&
225
- typeof value.contract !== "string"
226
- )
227
- diagnostics = changeContractDiagnostics(value.contract);
228
- if (!diagnostics.length) {
229
- const field = !isRecord(value)
230
- ? "request"
231
- : typeof value.operation !== "string"
232
- ? "operation"
233
- : value.operation !== "start" &&
234
- (typeof value.runId !== "string" || !IDENTIFIER.test(value.runId))
235
- ? "runId"
236
- : !["status", "validate-plan-draft"].includes(
237
- String(value.operation),
238
- ) &&
239
- (typeof value.operationId !== "string" ||
240
- !IDENTIFIER.test(value.operationId))
241
- ? "operationId"
242
- : value.operation === "approve-gate"
243
- ? value.gate === "gate-a"
244
- ? "contract"
245
- : "gate"
246
- : "request";
247
- diagnostics = [{ code: "design-control-field-invalid", field }];
248
- }
249
- return { ...parsed, diagnostics };
250
- }
251
-
252
- function parseDesignControlRequest(
253
- value: unknown,
254
- ):
255
- | { ok: true; value: DesignControlRequest }
256
- | { ok: false; code: "invalid-design-control-request" } {
257
- if (!isRecord(value))
258
- return { ok: false, code: "invalid-design-control-request" };
259
- if (value.operation === "start") {
260
- const operationValid =
261
- typeof value.operationId === "string" &&
262
- IDENTIFIER.test(value.operationId);
263
- if (!operationValid)
264
- return { ok: false, code: "invalid-design-control-request" };
265
- if (
266
- exactKeys(value, ["operation", "operationId", "requirement"]) &&
267
- typeof value.requirement === "string" &&
268
- value.requirement.trim().length > 0 &&
269
- Buffer.byteLength(value.requirement, "utf8") <=
270
- MAX_DESIGN_REQUIREMENT_BYTES
271
- ) {
272
- return {
273
- ok: true,
274
- value: structuredClone(value) as DesignControlRequest,
275
- };
276
- }
277
- return exactKeys(value, ["operation", "operationId", "change"]) &&
278
- typeof value.change === "string" &&
279
- CHANGE_NAME.test(value.change)
280
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
281
- : { ok: false, code: "invalid-design-control-request" };
282
- }
283
- if (
284
- value.operation === "status" ||
285
- value.operation === "validate-plan-draft"
286
- ) {
287
- return exactKeys(value, ["operation", "runId"]) &&
288
- typeof value.runId === "string" &&
289
- IDENTIFIER.test(value.runId)
290
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
291
- : { ok: false, code: "invalid-design-control-request" };
292
- }
293
- const commonValid =
294
- typeof value.runId === "string" &&
295
- IDENTIFIER.test(value.runId) &&
296
- typeof value.operationId === "string" &&
297
- IDENTIFIER.test(value.operationId);
298
- if (!commonValid)
299
- return { ok: false, code: "invalid-design-control-request" };
300
- if (value.operation === "bind-change") {
301
- return exactKeys(value, ["operation", "runId", "operationId", "change"]) &&
302
- typeof value.change === "string" &&
303
- CHANGE_NAME.test(value.change)
304
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
305
- : { ok: false, code: "invalid-design-control-request" };
306
- }
307
- if (value.operation === "write-artifact") {
308
- return exactKeys(value, [
309
- "operation",
310
- "runId",
311
- "operationId",
312
- "path",
313
- "content",
314
- ]) &&
315
- typeof value.path === "string" &&
316
- typeof value.content === "string"
317
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
318
- : { ok: false, code: "invalid-design-control-request" };
319
- }
320
- if (value.operation === "delete-artifact") {
321
- return exactKeys(value, ["operation", "runId", "operationId", "path"]) &&
322
- typeof value.path === "string"
323
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
324
- : { ok: false, code: "invalid-design-control-request" };
325
- }
326
- if (
327
- value.operation === "compile-plan" ||
328
- value.operation === "finalize-delivery"
329
- ) {
330
- return exactKeys(value, ["operation", "runId", "operationId"])
331
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
332
- : { ok: false, code: "invalid-design-control-request" };
333
- }
334
- if (value.operation === "approve-gate") {
335
- if (value.gate === "gate-b") {
336
- return exactKeys(value, ["operation", "runId", "operationId", "gate"])
337
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
338
- : { ok: false, code: "invalid-design-control-request" };
339
- }
340
- return value.gate === "gate-a" &&
341
- exactKeys(value, [
342
- "operation",
343
- "runId",
344
- "operationId",
345
- "gate",
346
- "contract",
347
- ]) &&
348
- validGateContract(value.contract)
349
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
350
- : { ok: false, code: "invalid-design-control-request" };
351
- }
352
- if (value.operation !== "record-decision") {
353
- return { ok: false, code: "invalid-design-control-request" };
354
- }
355
- return exactKeys(value, [
356
- "operation",
357
- "runId",
358
- "operationId",
359
- "decisionId",
360
- "category",
361
- "contract",
362
- "refs",
363
- ]) &&
364
- typeof value.decisionId === "string" &&
365
- IDENTIFIER.test(value.decisionId) &&
366
- (value.category === "behavior" || value.category === "technical") &&
367
- typeof value.contract === "string" &&
368
- value.contract.trim().length > 0 &&
369
- Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES &&
370
- Array.isArray(value.refs) &&
371
- value.refs.every((ref) => typeof ref === "string")
372
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
373
- : { ok: false, code: "invalid-design-control-request" };
374
- }
375
-
376
- function safeRelative(change: string, name: string): string {
377
- if (!CHANGE_NAME.test(change)) throw new Error("design-change-invalid");
378
- return `openspec/changes/${change}/${name}`;
379
- }
380
-
381
- function isAllowedDesignArtifactPath(relative: string): boolean {
382
- if (!isValidRelativePath(relative)) return false;
383
- if (
384
- [
385
- ".openspec.yaml",
386
- "proposal.md",
387
- "design.md",
388
- "tasks.md",
389
- "plan-draft.json",
390
- ].includes(relative)
391
- ) {
392
- return true;
393
- }
394
- const segments = relative.split("/");
395
- return (
396
- segments.length >= 3 &&
397
- segments[0] === "specs" &&
398
- segments.at(-1) === "spec.md" &&
399
- segments.slice(1, -1).every((segment) => ARTIFACT_SEGMENT.test(segment))
400
- );
401
- }
402
-
403
- function designArtifactRelative(change: string, relative: string): string {
404
- if (!isAllowedDesignArtifactPath(relative)) {
405
- throw new Error("design-artifact-path-invalid");
406
- }
407
- return safeRelative(change, relative);
408
- }
409
-
410
- function ensureSafeDirectory(root: string, relative: string): void {
411
- const segments = relative.split("/");
412
- for (let index = 0; index < segments.length; index += 1) {
413
- const current = segments.slice(0, index + 1).join("/");
414
- const observation = observeSafePath(root, current);
415
- if (observation.kind === "directory") continue;
416
- if (observation.kind !== "absent") {
417
- throw new Error("design-artifact-path-unsafe");
418
- }
419
- const parent = index === 0 ? "." : segments.slice(0, index).join("/");
420
- if (observeSafePath(root, parent).kind !== "directory") {
421
- throw new Error("design-artifact-path-unsafe");
422
- }
423
- try {
424
- mkdirSync(path.join(root, ...current.split("/")), { mode: 0o755 });
425
- } catch {
426
- if (observeSafePath(root, current).kind !== "directory") {
427
- throw new Error("design-artifact-path-unsafe");
428
- }
429
- }
430
- if (observeSafePath(root, current).kind !== "directory") {
431
- throw new Error("design-artifact-path-unsafe");
432
- }
433
- }
434
- }
435
-
436
- function boundedArtifactBytes(root: string, relative: string): Buffer {
437
- if (observeSafePath(root, relative).kind !== "file") {
438
- throw new Error("design-artifact-path-unsafe");
439
- }
440
- const absolute = path.join(root, ...relative.split("/"));
441
- const size = lstatSync(absolute).size;
442
- if (size > MAX_DESIGN_FILE_BYTES) {
443
- throw new Error("design-artifact-content-too-large");
444
- }
445
- return readFileSync(absolute);
446
- }
447
-
448
- function readSafeFile(
449
- root: string,
450
- relative: string,
451
- maximum = MAX_DESIGN_FILE_BYTES,
452
- ): Buffer {
453
- if (observeSafePath(root, relative).kind !== "file") {
454
- throw new Error("design-file-unavailable");
455
- }
456
- const absolute = path.join(root, ...relative.split("/"));
457
- const size = lstatSync(absolute).size;
458
- if (size < 1 || size > maximum) throw new Error("design-file-size-invalid");
459
- return readFileSync(absolute);
460
- }
461
-
462
- function removeSafeFile(root: string, relative: string): void {
463
- const observation = observeSafePath(root, relative);
464
- if (observation.kind === "absent") return;
465
- if (observation.kind !== "file") throw new Error("design-file-unsafe");
466
- unlinkSync(path.join(root, ...relative.split("/")));
467
- }
468
-
469
- function removeSafeFileWithHash(
470
- root: string,
471
- relative: string,
472
- expectedHash: string,
473
- ): void {
474
- const observation = observeSafePath(root, relative);
475
- if (observation.kind === "absent") return;
476
- if (observation.kind !== "file") throw new Error("design-file-unsafe");
477
- const bytes = readSafeFile(root, relative, 4 * 1024 * 1024);
478
- if (createHash("sha256").update(bytes).digest("hex") !== expectedHash) return;
479
- unlinkSync(path.join(root, ...relative.split("/")));
480
- }
481
-
482
- function atomicWrite(root: string, relative: string, bytes: Uint8Array): void {
483
- const parent = path.posix.dirname(relative);
484
- if (observeSafePath(root, parent).kind !== "directory") {
485
- throw new Error("design-file-parent-unsafe");
486
- }
487
- const targetObservation = observeSafePath(root, relative);
488
- if (
489
- targetObservation.kind !== "file" &&
490
- targetObservation.kind !== "absent"
491
- ) {
492
- throw new Error("design-file-unsafe");
493
- }
494
- const temporary = `${parent}/.abel-${path.posix.basename(relative)}-${randomUUID()}.tmp`;
495
- if (observeSafePath(root, temporary).kind !== "absent") {
496
- throw new Error("design-temporary-file-conflict");
497
- }
498
- const temporaryAbsolute = path.join(root, ...temporary.split("/"));
499
- try {
500
- writeFileSync(temporaryAbsolute, bytes, { flag: "wx", mode: 0o600 });
501
- renameSync(temporaryAbsolute, path.join(root, ...relative.split("/")));
502
- } catch (error) {
503
- try {
504
- if (observeSafePath(root, temporary).kind === "file") {
505
- unlinkSync(temporaryAbsolute);
506
- }
507
- } catch {
508
- // The original safe failure remains authoritative.
509
- }
510
- throw error;
511
- }
512
- }
513
-
514
- function proof(status: DesignStatusProjection, gate: "gateA" | "gateB") {
515
- const projection = status.gates[gate];
516
- if (!projection.current || !projection.proof) {
517
- throw new DesignFinalizationError([
518
- gate === "gateA" ? "design-gate-a-stale" : "design-gate-b-stale",
519
- ]);
520
- }
521
- return structuredClone(projection.proof) as GateApprovalProof;
522
- }
523
-
524
- export class DesignController {
525
- readonly consumerRoot: string;
526
- readonly #journal: DesignJournal;
527
- readonly #runs: RunStore;
528
- readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
529
- #closed = false;
530
- readonly #authority = new AsyncLocalStorage<() => void>();
531
-
532
- private constructor(options: DesignControllerOptions) {
533
- this.consumerRoot = path.resolve(options.consumerRoot);
534
- this.#runs = RunStore.open(options.stateRoot);
535
- try {
536
- this.#journal = DesignJournal.open(options.stateRoot, {
537
- ...(options.now ? { now: options.now } : {}),
538
- ...(options.finalizationLeaseMs !== undefined
539
- ? { finalizationLeaseMs: options.finalizationLeaseMs }
540
- : {}),
541
- });
542
- } catch (error) {
543
- try {
544
- this.#runs.close();
545
- } catch {
546
- // The Design journal initialization failure remains authoritative.
547
- }
548
- throw error;
549
- }
550
- this.#inspectOpenSpec = options.inspectOpenSpec;
551
- }
552
-
553
- static open(options: DesignControllerOptions): DesignController {
554
- return new DesignController(options);
555
- }
556
-
557
- executeWithAuthority(
558
- value: unknown,
559
- assertAuthority: () => void,
560
- ): Promise<Record<string, unknown>> {
561
- return this.#authority.run(assertAuthority, () => this.execute(value));
562
- }
563
-
564
- #assertFinalizationLease(
565
- lease: Parameters<DesignJournal["assertFinalizationLease"]>[0],
566
- ): void {
567
- this.#authority.getStore()?.();
568
- this.#journal.assertFinalizationLease(lease);
569
- }
570
-
571
- #assertOpen(): void {
572
- this.#authority.getStore()?.();
573
- if (this.#closed) throw new Error("design-controller-closed");
574
- }
575
-
576
- status(runId: string): DesignStatusProjection {
577
- this.#assertOpen();
578
- return this.#journal.status(runId);
579
- }
580
-
581
- assertDesignRun(runId: string): void {
582
- this.#assertOpen();
583
- this.#journal.assertDesignRun(runId);
584
- }
585
-
586
- recordEvidence(input: Parameters<DesignJournal["recordEvidence"]>[0]) {
587
- this.#assertOpen();
588
- return this.#journal.recordEvidence(input);
589
- }
590
-
591
- verifyGateProof(input: Parameters<DesignJournal["verifyGateProof"]>[0]) {
592
- this.#assertOpen();
593
- return this.#journal.verifyGateProof(input);
594
- }
595
-
596
- verifyFinalizedDelivery(
597
- input: Parameters<DesignJournal["verifyFinalizedDelivery"]>[0],
598
- ) {
599
- this.#assertOpen();
600
- return this.#journal.verifyFinalizedDelivery(input);
601
- }
602
-
603
- async execute(value: unknown): Promise<Record<string, unknown>> {
604
- this.#assertOpen();
605
- const validation = validateDesignControlRequest(value);
606
- if (!validation.ok)
607
- throw new DesignControlValidationError(validation.diagnostics);
608
- const request = validation.value;
609
- switch (request.operation) {
610
- case "start":
611
- return this.#start(request);
612
- case "status":
613
- return this.#statusOutcome(request.runId);
614
- case "bind-change":
615
- return this.#bindChange(request);
616
- case "record-decision":
617
- return this.#journal.recordDecision(request);
618
- case "approve-gate":
619
- return this.#journal.approveGate(request);
620
- case "validate-plan-draft":
621
- return this.#validatePlanDraft(request);
622
- case "compile-plan":
623
- return this.#compilePlan(request);
624
- case "finalize-delivery":
625
- return this.#finalizeDelivery(request);
626
- case "write-artifact":
627
- return this.#writeArtifact(request);
628
- case "delete-artifact":
629
- return this.#deleteArtifact(request);
630
- }
631
- }
632
-
633
- #statusOutcome(runId: string): Record<string, unknown> {
634
- const run = this.#runs.status(runId);
635
- if (run.stage !== "abel-design") throw new Error("design-run-invalid");
636
- const terminal = ["completed", "discarded", "rejected"].includes(run.state);
637
- const design = this.#journal.status(runId);
638
- const latestDecisionSequence = design.decisions.reduce(
639
- (latest, decision) => Math.max(latest, decision.sequence),
640
- 0,
641
- );
642
- const planReadyForGateB =
643
- design.gates.gateA.current &&
644
- design.plan !== null &&
645
- design.plan.sequence >
646
- Math.max(
647
- design.gates.gateA.approvedSequence ?? 0,
648
- latestDecisionSequence,
649
- );
650
- const canApproveGate =
651
- !design.gates.gateA.current ||
652
- (planReadyForGateB && !design.gates.gateB.current);
653
- const legalOperations = terminal
654
- ? ["status"]
655
- : [
656
- "status",
657
- "record-decision",
658
- ...(canApproveGate ? ["approve-gate"] : []),
659
- ...(design.gates.gateA.current && !design.change
660
- ? ["bind-change"]
661
- : []),
662
- ...(design.gates.gateA.current && design.change
663
- ? [
664
- "write-artifact",
665
- "delete-artifact",
666
- "validate-plan-draft",
667
- "compile-plan",
668
- ]
669
- : []),
670
- ...(design.change &&
671
- design.plan &&
672
- design.gates.gateA.current &&
673
- design.gates.gateB.current
674
- ? ["finalize-delivery"]
675
- : []),
676
- ];
677
- return {
678
- runId: run.runId,
679
- stage: run.stage,
680
- ...(run.change ? { change: run.change } : {}),
681
- state: run.state,
682
- completed: run.state === "completed",
683
- ...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
684
- legalOperations,
685
- nextStep: terminal
686
- ? null
687
- : !design.gates.gateA.current
688
- ? {
689
- operation: "approve-gate",
690
- instruction:
691
- "Read applicable instructions, the affected source and existing tests, and the verification manifest together. Resolve remaining substantive choices in Design; use existing same-task approval. Include the goal, acceptance, constraints, and scope for likely implementation repairs in the accepted structured ChangeContract. The parent owns later technical choices within that policy; do not defer user decisions to Implement.",
692
- }
693
- : !design.change
694
- ? {
695
- operation: "bind-change",
696
- instruction: "Bind the approved change name to this runId.",
697
- }
698
- : !design.plan || !design.gates.gateB.current
699
- ? {
700
- operation: "write-artifact",
701
- instruction:
702
- "Write proposal, design, specs, author task checkboxes with exact Scenario references, and plan-draft.json. Use the installed example named in the tool description; omit derived identities. Preflight and inspect summary.implementation for the accepted recovery policy and summary.parallelism for independent tasks and serialization causes. Resolve known execution prerequisites in Design. Compile to supply verification bindings and Gate B.",
703
- }
704
- : {
705
- operation: "finalize-delivery",
706
- instruction:
707
- "Finalize the prepared artifacts. If tasks or the draft changed since compilation, preflight and compile again first. No separate Gate B approval is needed.",
708
- },
709
- packetActions: terminal ? [] : ["finish"],
710
- design,
711
- };
712
- }
713
-
714
- #start(request: Extract<DesignControlRequest, { operation: "start" }>) {
715
- const fromRequirement = "requirement" in request;
716
- let run: ReturnType<RunStore["startRun"]>;
717
- try {
718
- run = this.#runs.startRun({
719
- stage: "abel-design",
720
- ...(fromRequirement
721
- ? {
722
- provisionalKey: transientHash(
723
- "abel-design-requirement",
724
- request.requirement,
725
- ),
726
- provisionalAliases: [
727
- transientHash(
728
- "abel-design-requirement-v1",
729
- request.requirement,
730
- ),
731
- ],
732
- }
733
- : { change: request.change }),
734
- operationId: request.operationId,
735
- });
736
- } catch (error) {
737
- if (error instanceof Error && error.message === "operation-id-conflict") {
738
- throw new Error("design-operation-conflict", { cause: error });
739
- }
740
- throw error;
741
- }
742
- if (run.state === "created") {
743
- this.#journal.inheritFinalizedAuthority(run.runId);
744
- this.#runs.transition({
745
- runId: run.runId,
746
- to: "paused",
747
- operationId: lifecycleOperationId(
748
- fromRequirement ? "await-gate-a" : "await-evidence",
749
- run.runId,
750
- ),
751
- code: fromRequirement
752
- ? "design-awaiting-gate-a"
753
- : "design-awaiting-evidence",
754
- });
755
- }
756
- return this.#statusOutcome(run.runId);
757
- }
758
-
759
- #bindChange(
760
- request: Extract<DesignControlRequest, { operation: "bind-change" }>,
761
- ) {
762
- const run = this.#runs.status(request.runId);
763
- if (run.change && run.change !== request.change) {
764
- throw new Error("design-operation-conflict");
765
- }
766
- if (!run.change) {
767
- const design = this.#journal.status(request.runId);
768
- if (!design.gates.gateA.current)
769
- throw new Error("design-gate-a-required");
770
- }
771
- const bound = this.#runs.bindChange({
772
- runId: request.runId,
773
- change: request.change,
774
- operationId: request.operationId,
775
- });
776
- if (bound.state === "paused") {
777
- this.#runs.transition({
778
- runId: request.runId,
779
- to: "paused",
780
- operationId: lifecycleOperationId("bound", request.runId),
781
- code: "design-awaiting-evidence",
782
- });
783
- }
784
- return this.#statusOutcome(request.runId);
785
- }
786
-
787
- #artifactRun(runId: string): { change: string; gateACurrent: boolean } {
788
- const status = this.#journal.status(runId);
789
- if (!status.change) throw new Error("design-change-required");
790
- return {
791
- change: status.change,
792
- gateACurrent: status.gates.gateA.current,
793
- };
794
- }
795
-
796
- #writeArtifact(
797
- request: Extract<DesignControlRequest, { operation: "write-artifact" }>,
798
- ): DesignArtifactOperationOutcome {
799
- const artifactRun = this.#artifactRun(request.runId);
800
- const change = artifactRun.change;
801
- const relative = designArtifactRelative(change, request.path);
802
- const bytes = Buffer.from(request.content, "utf8");
803
- if (bytes.toString("utf8") !== request.content) {
804
- throw new Error("design-artifact-content-invalid");
805
- }
806
- if (bytes.length > MAX_DESIGN_FILE_BYTES) {
807
- throw new Error("design-artifact-content-too-large");
808
- }
809
- const facts: DesignArtifactOperationFacts = {
810
- runId: request.runId,
811
- operationId: request.operationId,
812
- operation: request.operation,
813
- path: request.path,
814
- bytes: bytes.length,
815
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
816
- };
817
- const replay = this.#journal.artifactOperationState(facts);
818
- if (replay?.state === "committed") return replay.outcome;
819
- if (!replay && !artifactRun.gateACurrent) {
820
- throw new Error("design-gate-a-required");
821
- }
822
- const lease = this.#journal.acquireFinalizationLease({
823
- runId: request.runId,
824
- operationId: request.operationId,
825
- });
826
- try {
827
- const outcome: DesignArtifactOperationOutcome = {
828
- operation: request.operation,
829
- runId: request.runId,
830
- path: request.path,
831
- bytes: facts.bytes,
832
- rawSha256: facts.rawSha256,
833
- };
834
- ensureSafeDirectory(this.consumerRoot, path.posix.dirname(relative));
835
- const target = observeSafePath(this.consumerRoot, relative);
836
- if (target.kind !== "file" && target.kind !== "absent") {
837
- throw new Error("design-artifact-path-unsafe");
838
- }
839
- const prepared = this.#journal.prepareArtifactOperation(
840
- facts,
841
- lease,
842
- outcome,
843
- );
844
- if (prepared.state === "committed") return prepared.outcome;
845
- atomicWrite(this.consumerRoot, relative, bytes);
846
- return this.#journal.commitArtifactOperation(facts, lease);
847
- } finally {
848
- this.#journal.releaseFinalizationLease(lease);
849
- }
850
- }
851
-
852
- #deleteArtifact(
853
- request: Extract<DesignControlRequest, { operation: "delete-artifact" }>,
854
- ): DesignArtifactOperationOutcome {
855
- const artifactRun = this.#artifactRun(request.runId);
856
- const change = artifactRun.change;
857
- const relative = designArtifactRelative(change, request.path);
858
- const facts: DesignArtifactOperationFacts = {
859
- runId: request.runId,
860
- operationId: request.operationId,
861
- operation: request.operation,
862
- path: request.path,
863
- };
864
- const replay = this.#journal.artifactOperationState(facts);
865
- if (replay?.state === "committed") return replay.outcome;
866
- if (!replay && !artifactRun.gateACurrent) {
867
- throw new Error("design-gate-a-required");
868
- }
869
- const lease = this.#journal.acquireFinalizationLease({
870
- runId: request.runId,
871
- operationId: request.operationId,
872
- });
873
- try {
874
- let outcome = replay?.outcome;
875
- if (!outcome) {
876
- const observation = observeSafePath(this.consumerRoot, relative);
877
- if (observation.kind === "absent") {
878
- outcome = {
879
- operation: request.operation,
880
- runId: request.runId,
881
- path: request.path,
882
- deleted: false,
883
- };
884
- } else if (observation.kind === "file") {
885
- const bytes = boundedArtifactBytes(this.consumerRoot, relative);
886
- outcome = {
887
- operation: request.operation,
888
- runId: request.runId,
889
- path: request.path,
890
- deleted: true,
891
- bytes: bytes.length,
892
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
893
- };
894
- } else {
895
- throw new Error("design-artifact-path-unsafe");
896
- }
897
- }
898
- const prepared = this.#journal.prepareArtifactOperation(
899
- facts,
900
- lease,
901
- outcome,
902
- );
903
- if (prepared.state === "committed") return prepared.outcome;
904
- const intended = prepared.outcome;
905
- if (intended.operation !== "delete-artifact") {
906
- throw new Error("design-artifact-operation-outcome-invalid");
907
- }
908
- if (intended.deleted) {
909
- const observation = observeSafePath(this.consumerRoot, relative);
910
- if (observation.kind === "file") {
911
- const current = boundedArtifactBytes(this.consumerRoot, relative);
912
- if (
913
- current.length !== intended.bytes ||
914
- createHash("sha256").update(current).digest("hex") !==
915
- intended.rawSha256
916
- ) {
917
- throw new Error("design-artifact-currentness-invalid");
918
- }
919
- unlinkSync(path.join(this.consumerRoot, ...relative.split("/")));
920
- } else if (observation.kind !== "absent") {
921
- throw new Error("design-artifact-path-unsafe");
922
- }
923
- }
924
- return this.#journal.commitArtifactOperation(facts, lease);
925
- } finally {
926
- this.#journal.releaseFinalizationLease(lease);
927
- }
928
- }
929
-
930
- #readCompiledDraft(runId: string) {
931
- const status = this.#journal.status(runId);
932
- if (!status.change) throw new Error("design-change-required");
933
- if (!status.gates.gateA.current) throw new Error("design-gate-a-required");
934
- const draftPath = safeRelative(status.change, "plan-draft.json");
935
- const draftBytes = readSafeFile(this.consumerRoot, draftPath);
936
- let draft: unknown;
937
- try {
938
- draft = JSON.parse(draftBytes.toString("utf8"));
939
- } catch {
940
- throw new DesignPlanValidationError("design-plan-draft-invalid", [
941
- { code: "design-plan-draft-invalid", field: "plan-draft.json" },
942
- ]);
943
- }
944
- const authoredDraft = structuredClone(draft);
945
- if (status.changeContract) {
946
- if (!draft || typeof draft !== "object" || Array.isArray(draft))
947
- throw new Error("change-contract-invalid");
948
- const record = draft as Record<string, unknown>;
949
- if (
950
- record.changeContract !== undefined &&
951
- canonicalJson(normalizeChangeContract(record.changeContract)) !==
952
- canonicalJson(status.changeContract)
953
- )
954
- throw new Error("change-contract-mismatch");
955
- record.changeContract = status.changeContract;
956
- } else if (draft && typeof draft === "object" && "changeContract" in draft)
957
- throw new Error("change-contract-gate-a-required");
958
- const compiled = compileImplementPlan(draft, {
959
- bindExecutionInputs: true,
960
- consumerRoot: this.consumerRoot,
961
- });
962
- if (compiled.plan.changeId !== status.change) {
963
- throw new DesignPlanValidationError("design-plan-change-mismatch", [
964
- { code: "design-plan-change-mismatch", field: "changeId" },
965
- ]);
966
- }
967
- return { status, compiled, authoredDraft };
968
- }
969
-
970
- #validatePlanDraft(
971
- request: Extract<
972
- DesignControlRequest,
973
- { operation: "validate-plan-draft" }
974
- >,
975
- ) {
976
- const { compiled, authoredDraft } = this.#readCompiledDraft(request.runId);
977
- return {
978
- operation: request.operation,
979
- runId: request.runId,
980
- valid: true,
981
- checks: { ...compiled.checks, gateA: "current" },
982
- summary: planAuthoringSummary(compiled.plan, authoredDraft),
983
- plan: {
984
- taskCount: compiled.plan.tasks.length,
985
- outputCount: compiled.plan.outputs.length,
986
- rawSha256: compiled.rawSha256,
987
- canonicalHash: compiled.planHash,
988
- },
989
- };
990
- }
991
-
992
- #compilePlan(
993
- request: Extract<DesignControlRequest, { operation: "compile-plan" }>,
994
- ) {
995
- const replay = this.#journal.operationOutcome(
996
- request.runId,
997
- request.operationId,
998
- request.operation,
999
- );
1000
- if (replay) return replay;
1001
- const lease = this.#journal.acquireFinalizationLease({
1002
- runId: request.runId,
1003
- operationId: request.operationId,
1004
- });
1005
- try {
1006
- this.#journal.assertNoPendingArtifactOperations(request.runId);
1007
- const { status, compiled } = this.#readCompiledDraft(request.runId);
1008
- const change = status.change;
1009
- if (!change) throw new Error("design-change-required");
1010
- const readyPath = safeRelative(change, "ready.yaml");
1011
- const gateAPath = safeRelative(change, "gate-a.yaml");
1012
- const tasksPath = safeRelative(change, "tasks.md");
1013
- if (observeSafePath(this.consumerRoot, tasksPath).kind === "absent") {
1014
- throw new DesignPlanValidationError(
1015
- "design-traceability-input-unavailable",
1016
- [{ code: "design-traceability-input-unavailable", path: "tasks.md" }],
1017
- );
1018
- }
1019
- const priorTasks = boundedArtifactBytes(this.consumerRoot, tasksPath);
1020
- const tasksBytes = Buffer.from(
1021
- bindTaskVerifications(
1022
- new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
1023
- priorTasks,
1024
- ),
1025
- compiled.plan,
1026
- ),
1027
- "utf8",
1028
- );
1029
- if (tasksBytes.length > MAX_DESIGN_FILE_BYTES) {
1030
- throw new Error("design-artifact-content-too-large");
1031
- }
1032
- const tasksUnchanged = tasksBytes.equals(priorTasks);
1033
- const unchanged =
1034
- tasksUnchanged &&
1035
- status.gates.gateB.current &&
1036
- status.plan?.rawSha256 === compiled.rawSha256 &&
1037
- status.plan.canonicalHash === compiled.planHash;
1038
- if (!unchanged) {
1039
- removeSafeFile(this.consumerRoot, readyPath);
1040
- removeSafeFile(this.consumerRoot, gateAPath);
1041
- }
1042
- this.#assertFinalizationLease(lease);
1043
- if (
1044
- !boundedArtifactBytes(this.consumerRoot, tasksPath).equals(priorTasks)
1045
- ) {
1046
- throw new Error("design-artifact-currentness-mismatch");
1047
- }
1048
- // The compilation journal commits only after both files are installed.
1049
- // A crash before that point can replay this deterministic projection;
1050
- // finalization always rechecks the plan and task bytes together.
1051
- if (!tasksUnchanged)
1052
- atomicWrite(this.consumerRoot, tasksPath, tasksBytes);
1053
- atomicWrite(
1054
- this.consumerRoot,
1055
- safeRelative(change, "implement-plan.json"),
1056
- compiled.bytes,
1057
- );
1058
- return this.#journal.recordCompiledPlan({
1059
- runId: request.runId,
1060
- operationId: request.operationId,
1061
- bytes: compiled.bytes,
1062
- rawSha256: compiled.rawSha256,
1063
- canonicalHash: compiled.planHash,
1064
- lease,
1065
- certify: true,
1066
- });
1067
- } finally {
1068
- this.#journal.releaseFinalizationLease(lease);
1069
- }
1070
- }
1071
-
1072
- async #finalizeDelivery(
1073
- request: Extract<DesignControlRequest, { operation: "finalize-delivery" }>,
1074
- ) {
1075
- const replay = this.#journal.operationOutcome(
1076
- request.runId,
1077
- request.operationId,
1078
- request.operation,
1079
- );
1080
- if (replay) {
1081
- this.#completeFinalizedRun(request.runId, request.operationId);
1082
- return replay;
1083
- }
1084
- const committed = this.#journal.latestFinalizationOutcome(request.runId);
1085
- if (committed) {
1086
- this.#completeFinalizedRun(request.runId, committed.operationId);
1087
- return committed.outcome;
1088
- }
1089
- const status = this.#journal.status(request.runId);
1090
- if (!status.change) throw new Error("design-change-required");
1091
- const gateA = proof(status, "gateA");
1092
- const gateB = proof(status, "gateB");
1093
- const storedPlan = this.#journal.currentCompiledPlan(request.runId);
1094
- if (!storedPlan) throw new DesignFinalizationError(["design-plan-missing"]);
1095
- const change = status.change;
1096
- const readyPath = safeRelative(change, "ready.yaml");
1097
- const gateAPath = safeRelative(change, "gate-a.yaml");
1098
- const lease = this.#journal.acquireFinalizationLease({
1099
- runId: request.runId,
1100
- operationId: request.operationId,
1101
- });
1102
- let readyInstalled = false;
1103
- let readyHash: string | undefined;
1104
- let finalizationCommitted = false;
1105
- try {
1106
- this.#journal.assertNoPendingArtifactOperations(request.runId);
1107
- removeSafeFile(this.consumerRoot, readyPath);
1108
- const diagnostics = new Set<string>();
1109
- let inspection: DesignOpenSpecInspection | undefined;
1110
- let openSpecDiagnostic: Readonly<OpenSpecDiagnostic> | undefined;
1111
- try {
1112
- inspection = await this.#inspectOpenSpec(this.consumerRoot, change);
1113
- } catch (error) {
1114
- if (error instanceof OpenSpecCliError)
1115
- openSpecDiagnostic = error.diagnostic;
1116
- diagnostics.add("design-openspec-unavailable");
1117
- }
1118
- if (inspection) {
1119
- if (inspection.change !== change)
1120
- diagnostics.add("design-change-mismatch");
1121
- if (!inspection.strictValid)
1122
- diagnostics.add("design-openspec-strict-invalid");
1123
- if (!inspection.planningComplete)
1124
- diagnostics.add("design-openspec-incomplete");
1125
- if (!inspection.schema)
1126
- diagnostics.add("design-openspec-schema-invalid");
1127
- }
1128
- const installedPlanBytes = readSafeFile(
1129
- this.consumerRoot,
1130
- safeRelative(change, "implement-plan.json"),
1131
- );
1132
- if (
1133
- !Buffer.from(installedPlanBytes).equals(Buffer.from(storedPlan.bytes))
1134
- ) {
1135
- diagnostics.add("design-plan-binding-invalid");
1136
- }
1137
- let compiled: ReturnType<typeof compileImplementPlan> | undefined;
1138
- try {
1139
- const plan = parseImplementPlan(installedPlanBytes, {
1140
- allowLegacyOrder: true,
1141
- });
1142
- compiled = compileImplementPlan(plan, {
1143
- consumerRoot: this.consumerRoot,
1144
- });
1145
- if (
1146
- compiled.planHash !== storedPlan.projection.canonicalHash ||
1147
- compiled.rawSha256 !== storedPlan.projection.rawSha256
1148
- ) {
1149
- diagnostics.add("design-plan-binding-invalid");
1150
- }
1151
- } catch {
1152
- diagnostics.add("design-plan-invalid");
1153
- }
1154
- const artifacts: DeliveryArtifactBinding[] = [];
1155
- const artifactBytes = new Map<string, Buffer>();
1156
- for (const relative of inspection?.artifactPaths ?? []) {
1157
- if (
1158
- [
1159
- "gate-a.yaml",
1160
- "ready.yaml",
1161
- "implement-plan.json",
1162
- "plan-draft.json",
1163
- ].includes(relative)
1164
- ) {
1165
- diagnostics.add(`design-artifact-reserved:${relative}`);
1166
- continue;
1167
- }
1168
- try {
1169
- const bytes = readSafeFile(
1170
- this.consumerRoot,
1171
- safeRelative(change, relative),
1172
- );
1173
- artifactBytes.set(relative, bytes);
1174
- artifacts.push({
1175
- path: relative,
1176
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
1177
- });
1178
- } catch {
1179
- diagnostics.add(`design-artifact-unavailable:${relative}`);
1180
- }
1181
- }
1182
- let traceability:
1183
- | ReturnType<typeof assessDeliveryTraceability>
1184
- | undefined;
1185
- if (compiled && inspection) {
1186
- const tasks = artifactBytes.get("tasks.md");
1187
- const specs = [...artifactBytes]
1188
- .filter(
1189
- ([relative]) =>
1190
- relative.startsWith("specs/") && relative.endsWith("/spec.md"),
1191
- )
1192
- .map(([relative, bytes]) => ({
1193
- path: relative,
1194
- text: bytes.toString("utf8"),
1195
- }));
1196
- if (!tasks || specs.length === 0) {
1197
- diagnostics.add("design-traceability-input-unavailable");
1198
- } else {
1199
- traceability = assessDeliveryTraceability({
1200
- tasksMarkdown: tasks.toString("utf8"),
1201
- specs,
1202
- plan: compiled.plan,
1203
- });
1204
- if (!traceability.ok) {
1205
- for (const diagnostic of traceability.diagnostics)
1206
- diagnostics.add(diagnostic);
1207
- }
1208
- }
1209
- }
1210
- if (
1211
- !inspection ||
1212
- !compiled ||
1213
- !traceability?.ok ||
1214
- diagnostics.size > 0
1215
- ) {
1216
- throw new DesignFinalizationError([...diagnostics], openSpecDiagnostic);
1217
- }
1218
- const gateAReceipt = compileGateAReceipt({
1219
- change,
1220
- schema: inspection.schema,
1221
- approval: gateA,
1222
- artifacts,
1223
- });
1224
- const deliveryRevision = this.#journal.nextDeliveryRevision(
1225
- request.runId,
1226
- );
1227
- const readyReceipt = compileReadyReceipt({
1228
- change,
1229
- schema: inspection.schema,
1230
- deliveryRevision,
1231
- gateA: { rawSha256: gateAReceipt.rawSha256 },
1232
- gateB,
1233
- artifacts,
1234
- compiledPlan: compiled,
1235
- traceability: traceability.value,
1236
- });
1237
- readyHash = readyReceipt.rawSha256;
1238
- this.#assertFinalizationLease(lease);
1239
- atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
1240
- atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
1241
- readyInstalled = true;
1242
- const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
1243
- const installedReady = readSafeFile(this.consumerRoot, readyPath);
1244
- parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
1245
- parseReadyReceipt(installedReady, { allowLegacyOrder: true });
1246
- if (
1247
- !Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
1248
- !Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))
1249
- ) {
1250
- throw new Error("design-installed-delivery-mismatch");
1251
- }
1252
- removeSafeFile(
1253
- this.consumerRoot,
1254
- safeRelative(change, "plan-draft.json"),
1255
- );
1256
- const outcome = this.#journal.recordFinalization({
1257
- runId: request.runId,
1258
- operationId: request.operationId,
1259
- lease,
1260
- deliveryRevision,
1261
- receiptHash: readyReceipt.rawSha256,
1262
- gateA,
1263
- gateB,
1264
- planCanonicalHash: compiled.planHash,
1265
- });
1266
- finalizationCommitted = true;
1267
- this.#completeFinalizedRun(request.runId, request.operationId);
1268
- return outcome;
1269
- } catch (error) {
1270
- if (readyInstalled && readyHash && !finalizationCommitted) {
1271
- try {
1272
- removeSafeFileWithHash(this.consumerRoot, readyPath, readyHash);
1273
- } catch {
1274
- // The original finalization failure remains authoritative.
1275
- }
1276
- }
1277
- throw error;
1278
- } finally {
1279
- this.#journal.releaseFinalizationLease(lease);
1280
- }
1281
- }
1282
-
1283
- #completeFinalizedRun(runId: string, operationId: string): void {
1284
- const currentRun = this.#runs.status(runId);
1285
- if (currentRun.state === "completed") return;
1286
- this.#runs.transition({
1287
- runId,
1288
- to: "completed",
1289
- operationId: `${operationId}:design-completed`,
1290
- });
1291
- }
1292
-
1293
- close(): void {
1294
- if (this.#closed) return;
1295
- this.#journal.close();
1296
- this.#runs.close();
1297
- this.#closed = true;
1298
- }
1299
- }