@abelxiaoxing/cadence 1.3.0 → 1.4.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 (45) hide show
  1. package/README.md +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. package/skills/abel-workflow/SKILL.md +0 -205
@@ -0,0 +1,509 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ type AtomicVerificationContract,
6
+ type StructuredVerificationContract,
7
+ type VerificationObservation,
8
+ verificationSteps,
9
+ } from "./contracts.ts";
10
+ import { canonicalJson } from "./implement-graph.ts";
11
+ import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
12
+ import { observeSafePath } from "./safe-path.ts";
13
+ import {
14
+ bindCurrentVerificationCapability,
15
+ isVerificationCapabilityCurrent,
16
+ type VerificationRunnerBinding,
17
+ } from "./verification-capability.ts";
18
+ import { prepareVerificationEnvironment } from "./verification-environment.ts";
19
+
20
+ function sha256(value: Uint8Array | string): string {
21
+ return createHash("sha256").update(value).digest("hex");
22
+ }
23
+
24
+ function packageScriptInvocation(
25
+ packageManager: string,
26
+ script: string,
27
+ args: string[],
28
+ ): string[] {
29
+ return packageManager === "npm"
30
+ ? ["run", script, "--", ...args]
31
+ : ["run", script, ...args];
32
+ }
33
+
34
+ function bindingFor(
35
+ bindings: readonly VerificationRunnerBinding[],
36
+ command: string,
37
+ ): VerificationRunnerBinding | undefined {
38
+ return bindings.find((binding) => binding.command === command);
39
+ }
40
+
41
+ function verificationInvocation(
42
+ step: AtomicVerificationContract,
43
+ bindings: readonly VerificationRunnerBinding[],
44
+ reportFile: string,
45
+ ): { executable: string; args: string[] } | undefined {
46
+ const reporter =
47
+ step.kind === "vitest"
48
+ ? ["--reporter=json", `--outputFile.json=${reportFile}`]
49
+ : [];
50
+ if (step.kind === "package-script") {
51
+ const binding = bindingFor(bindings, step.packageManager);
52
+ return binding
53
+ ? {
54
+ executable: binding.executablePath,
55
+ args: [
56
+ ...(binding.fixedArgs ?? []),
57
+ ...packageScriptInvocation(
58
+ step.packageManager,
59
+ step.script,
60
+ step.args,
61
+ ),
62
+ ],
63
+ }
64
+ : undefined;
65
+ }
66
+ const runner = step.runner;
67
+ if (runner.kind === "package-script") {
68
+ const binding = bindingFor(bindings, runner.packageManager);
69
+ const args =
70
+ step.kind === "vitest"
71
+ ? [...step.testFiles, ...step.args, ...reporter]
72
+ : step.args;
73
+ return binding
74
+ ? {
75
+ executable: binding.executablePath,
76
+ args: [
77
+ ...(binding.fixedArgs ?? []),
78
+ ...packageScriptInvocation(
79
+ runner.packageManager,
80
+ runner.script,
81
+ args,
82
+ ),
83
+ ],
84
+ }
85
+ : undefined;
86
+ }
87
+ if (runner.kind === "local-binary") {
88
+ const binding = bindingFor(bindings, runner.executable);
89
+ return {
90
+ executable:
91
+ binding?.executablePath ??
92
+ `/workspace/node_modules/.bin/${runner.executable}`,
93
+ args: [
94
+ ...(binding?.fixedArgs ?? []),
95
+ ...(step.kind === "vitest"
96
+ ? [...step.testFiles, ...step.args, ...reporter]
97
+ : step.args),
98
+ ],
99
+ };
100
+ }
101
+ const command = runner.kind === "node" ? "node" : "npx";
102
+ const binding = bindingFor(bindings, command);
103
+ if (!binding) return undefined;
104
+ return {
105
+ executable: binding.executablePath,
106
+ args:
107
+ runner.kind === "node"
108
+ ? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
109
+ : [
110
+ ...(binding.fixedArgs ?? []),
111
+ "--no-install",
112
+ runner.executable,
113
+ ...(step.kind === "vitest" ? step.testFiles : []),
114
+ ...step.args,
115
+ ...reporter,
116
+ ],
117
+ };
118
+ }
119
+
120
+ function vitestReport(json: string):
121
+ | {
122
+ total: number;
123
+ failed: number;
124
+ success: boolean;
125
+ failures: string[];
126
+ failedText: string[];
127
+ }
128
+ | undefined {
129
+ let value: unknown;
130
+ try {
131
+ value = JSON.parse(json);
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
136
+ return undefined;
137
+ }
138
+ const report = value as Record<string, unknown>;
139
+ if (
140
+ !Number.isSafeInteger(report.numTotalTests) ||
141
+ !Number.isSafeInteger(report.numFailedTests) ||
142
+ typeof report.success !== "boolean" ||
143
+ (report.numTotalTests as number) < 0 ||
144
+ (report.numFailedTests as number) < 0 ||
145
+ (report.numFailedTests as number) > (report.numTotalTests as number) ||
146
+ !Array.isArray(report.testResults) ||
147
+ (report.numRuntimeErrorTestSuites !== undefined &&
148
+ report.numRuntimeErrorTestSuites !== 0) ||
149
+ (Array.isArray(report.unhandledErrors) && report.unhandledErrors.length > 0)
150
+ ) {
151
+ return undefined;
152
+ }
153
+ let assertionCount = 0;
154
+ for (const suite of report.testResults) {
155
+ if (
156
+ !suite ||
157
+ typeof suite !== "object" ||
158
+ !Array.isArray(suite.assertionResults)
159
+ )
160
+ return undefined;
161
+ for (const assertion of suite.assertionResults) {
162
+ if (
163
+ !assertion ||
164
+ typeof assertion !== "object" ||
165
+ ![
166
+ "passed",
167
+ "failed",
168
+ "pending",
169
+ "skipped",
170
+ "todo",
171
+ "disabled",
172
+ ].includes(assertion.status)
173
+ )
174
+ return undefined;
175
+ assertionCount++;
176
+ }
177
+ }
178
+ if (assertionCount !== report.numTotalTests) return undefined;
179
+ const failedAssertions = (
180
+ Array.isArray(report.testResults) ? report.testResults : []
181
+ ).flatMap((suite): Array<{ identity: string; text: string[] }> => {
182
+ if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
183
+ const record = suite as Record<string, unknown>;
184
+ const suiteName =
185
+ typeof record.name === "string"
186
+ ? record.name.replace(/^\/workspace\//u, "")
187
+ : "unknown-suite";
188
+ if (!Array.isArray(record.assertionResults)) return [];
189
+ return record.assertionResults.flatMap(
190
+ (assertion): Array<{ identity: string; text: string[] }> => {
191
+ if (
192
+ !assertion ||
193
+ typeof assertion !== "object" ||
194
+ Array.isArray(assertion)
195
+ ) {
196
+ return [];
197
+ }
198
+ const entry = assertion as Record<string, unknown>;
199
+ if (entry.status !== "failed") return [];
200
+ const title =
201
+ typeof entry.fullName === "string"
202
+ ? entry.fullName
203
+ : typeof entry.title === "string"
204
+ ? entry.title
205
+ : "unknown-assertion";
206
+ const diagnostics = Array.isArray(entry.failureMessages)
207
+ ? entry.failureMessages.filter(
208
+ (message): message is string => typeof message === "string",
209
+ )
210
+ : [];
211
+ return [
212
+ {
213
+ identity: `${suiteName}\0${title}`,
214
+ text: [title, ...diagnostics],
215
+ },
216
+ ];
217
+ },
218
+ );
219
+ });
220
+ if (
221
+ failedAssertions.length !== report.numFailedTests ||
222
+ report.success !== (report.numFailedTests === 0)
223
+ )
224
+ return undefined;
225
+ return {
226
+ total: report.numTotalTests as number,
227
+ failed: report.numFailedTests as number,
228
+ success: report.success,
229
+ failures: failedAssertions.map((failure) => failure.identity),
230
+ failedText: failedAssertions.flatMap((failure) => failure.text),
231
+ };
232
+ }
233
+
234
+ function normalizedFailureIdentities(input: {
235
+ step: AtomicVerificationContract;
236
+ report?: ReturnType<typeof vitestReport>;
237
+ output: string;
238
+ exitCode: number;
239
+ }): string[] {
240
+ const vitestFailures = input.report?.failures ?? [];
241
+ if (vitestFailures.length > 0) {
242
+ return [...new Set(vitestFailures)]
243
+ .sort()
244
+ .map((failure) => sha256(`vitest-failure\0${failure}`));
245
+ }
246
+ const normalizedOutput = input.output
247
+ .replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
248
+ .replace(/\r\n?/gu, "\n")
249
+ .replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
250
+ .slice(0, 64 * 1024);
251
+ return [
252
+ sha256(
253
+ canonicalJson({
254
+ obligation:
255
+ input.step.kind === "package-script"
256
+ ? {
257
+ kind: "package-script",
258
+ packageManager: input.step.packageManager,
259
+ script: input.step.script,
260
+ command: input.step.command,
261
+ args: input.step.args,
262
+ }
263
+ : {
264
+ kind: input.step.kind,
265
+ runner: input.step.runner,
266
+ args: input.step.args,
267
+ },
268
+ exitCode: input.exitCode,
269
+ output: normalizedOutput,
270
+ }),
271
+ ),
272
+ ];
273
+ }
274
+
275
+ export const VERIFICATION_REPORT_LIMIT = 64 * 1024 * 1024;
276
+
277
+ function readReport(root: string, relative: string, maximum: number): string {
278
+ if (observeSafePath(root, relative).kind !== "file")
279
+ throw new Error("verification-report-unavailable");
280
+ const descriptor = openSync(
281
+ path.join(root, relative),
282
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
283
+ );
284
+ try {
285
+ const stat = fstatSync(descriptor);
286
+ if (!stat.isFile()) throw new Error("verification-report-unavailable");
287
+ if (stat.size > maximum) throw new Error("verification-report-too-large");
288
+ const bytes = Buffer.alloc(stat.size + 1);
289
+ let offset = 0;
290
+ while (offset < bytes.length) {
291
+ const count = readSync(
292
+ descriptor,
293
+ bytes,
294
+ offset,
295
+ bytes.length - offset,
296
+ null,
297
+ );
298
+ if (count === 0) break;
299
+ offset += count;
300
+ }
301
+ if (offset !== stat.size) throw new Error("verification-report-invalid");
302
+ return bytes.subarray(0, offset).toString("utf8");
303
+ } finally {
304
+ closeSync(descriptor);
305
+ }
306
+ }
307
+
308
+ export async function executePackageVerification(input: {
309
+ root: string;
310
+ dependencyOwner: string;
311
+ verification: StructuredVerificationContract;
312
+ signal: AbortSignal;
313
+ maxReportBytes?: number;
314
+ executionWritePaths?: readonly string[];
315
+ }): Promise<VerificationObservation> {
316
+ const unavailable = (
317
+ code: string,
318
+ category: "environment" | "adapter" | "resource" = "adapter",
319
+ ): VerificationObservation => ({
320
+ kind: "unavailable",
321
+ category,
322
+ code,
323
+ verificationId: input.verification.id,
324
+ });
325
+ if (input.signal.aborted) return { kind: "cancelled" };
326
+ const maxReportBytes = input.maxReportBytes ?? VERIFICATION_REPORT_LIMIT;
327
+ if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 1)
328
+ return unavailable("verification-report-limit-invalid", "resource");
329
+ const capability = bindCurrentVerificationCapability(
330
+ input.root,
331
+ input.verification,
332
+ {
333
+ dependencyOwner: input.dependencyOwner,
334
+ executionWritePaths: input.executionWritePaths,
335
+ },
336
+ );
337
+ if (!capability.ok) return unavailable(capability.diagnostic.code);
338
+ let environment: ReturnType<typeof prepareVerificationEnvironment>;
339
+ try {
340
+ environment = prepareVerificationEnvironment(
341
+ input.root,
342
+ input.dependencyOwner,
343
+ capability.value.runnerBindings,
344
+ );
345
+ } catch {
346
+ return unavailable("verification-environment-unavailable", "environment");
347
+ }
348
+ try {
349
+ const isolation = new BubblewrapIsolationBackend();
350
+ let final: VerificationObservation | undefined;
351
+ let sequence = 0;
352
+ for (const step of verificationSteps(input.verification)) {
353
+ const relative = `reports/${sequence++}.json`;
354
+ const invocation = verificationInvocation(
355
+ step,
356
+ environment.bindings,
357
+ `/cadence/${relative}`,
358
+ );
359
+ if (!invocation) return unavailable("runner-missing");
360
+ const executed = await isolation.run({
361
+ root: input.root,
362
+ ...invocation,
363
+ mounts: environment.mounts,
364
+ environment: environment.environment,
365
+ ...(step.kind !== "vitest" && step.classification === "expected-red"
366
+ ? { outputWitness: step.expectedFailure }
367
+ : {}),
368
+ signal: input.signal,
369
+ });
370
+ if (!executed.ok) {
371
+ if (executed.state === "cancelled") return { kind: "cancelled" };
372
+ return unavailable(
373
+ executed.code,
374
+ executed.code === "isolation-execution-timeout"
375
+ ? "resource"
376
+ : "environment",
377
+ );
378
+ }
379
+ let report: ReturnType<typeof vitestReport>;
380
+ if (step.kind === "vitest") {
381
+ try {
382
+ report = vitestReport(
383
+ readReport(environment.privateRoot, relative, maxReportBytes),
384
+ );
385
+ } catch (error) {
386
+ const large =
387
+ error instanceof Error &&
388
+ error.message === "verification-report-too-large";
389
+ return unavailable(
390
+ large
391
+ ? "verification-report-too-large"
392
+ : "verification-report-unavailable",
393
+ large ? "resource" : "adapter",
394
+ );
395
+ }
396
+ if (!report || (executed.exitCode === 0) !== report.success)
397
+ return unavailable("verification-report-invalid");
398
+ }
399
+ // A shell could not start its program; this is not evidence about the product.
400
+ if ([126, 127].includes(executed.exitCode))
401
+ return unavailable("verification-runner-launch-failed", "environment");
402
+ if (!isVerificationCapabilityCurrent(input.root, capability.value))
403
+ return unavailable("verification-input-unavailable");
404
+ const output = `${executed.stdout}\n${executed.stderr}`;
405
+ const identity =
406
+ step.classification !== "expected-red" ||
407
+ (step.kind === "vitest"
408
+ ? report?.failedText.some((text) =>
409
+ text.includes(step.expectedFailure ?? ""),
410
+ ) === true
411
+ : executed.outputWitnessMatched === true);
412
+ const accepted =
413
+ step.classification === "expected-red"
414
+ ? executed.exitCode !== 0 &&
415
+ identity &&
416
+ (step.kind !== "vitest" ||
417
+ (report !== undefined &&
418
+ report.failed > 0 &&
419
+ report.total >= step.minTests))
420
+ : executed.exitCode === 0 &&
421
+ (step.kind !== "vitest" ||
422
+ (report?.success === true && report.total >= step.minTests));
423
+ const evidence = {
424
+ id: step.id,
425
+ exitCode: executed.exitCode,
426
+ classification: step.classification,
427
+ policy: "report-file-v3" as const,
428
+ ...(report ? { tests: report.total } : {}),
429
+ failureIdentities: accepted
430
+ ? []
431
+ : normalizedFailureIdentities({
432
+ step,
433
+ report,
434
+ output,
435
+ exitCode: executed.exitCode,
436
+ }),
437
+ };
438
+ if (!accepted)
439
+ return {
440
+ kind: "rejected",
441
+ code:
442
+ step.classification === "expected-red" && executed.exitCode === 0
443
+ ? "red-not-witnessed"
444
+ : "verification-rejected",
445
+ evidence,
446
+ };
447
+ final = { kind: "accepted", evidence };
448
+ }
449
+ return final ?? unavailable("verification-contract-unsupported");
450
+ } finally {
451
+ environment.cleanup();
452
+ }
453
+ }
454
+
455
+ /** Compatibility at the execution service boundary, without guessing from error strings. */
456
+ export function phaseVerificationResult(result: VerificationObservation) {
457
+ if (result.kind === "cancelled")
458
+ throw new DOMException("cancelled", "AbortError");
459
+ if (result.kind === "accepted")
460
+ return {
461
+ ok: true as const,
462
+ exitCode: result.evidence.exitCode,
463
+ classification: result.evidence.classification,
464
+ diagnostic: { kind: "assertion" as const, id: result.evidence.id },
465
+ };
466
+ if (result.kind === "rejected")
467
+ return {
468
+ ok: false as const,
469
+ kind: "retryable" as const,
470
+ code: result.code,
471
+ failureIdentities: result.evidence.failureIdentities,
472
+ };
473
+ return {
474
+ ok: false as const,
475
+ kind: "paused" as const,
476
+ code: result.code,
477
+ category: result.category,
478
+ };
479
+ }
480
+
481
+ export function changeVerificationResult(result: VerificationObservation) {
482
+ if (result.kind === "accepted")
483
+ return {
484
+ ok: true as const,
485
+ exitCode: 0 as const,
486
+ classification: result.evidence.classification,
487
+ };
488
+ if (result.kind === "cancelled")
489
+ return {
490
+ ok: false as const,
491
+ kind: "cancelled" as const,
492
+ code: "cancelled",
493
+ };
494
+ if (result.kind === "rejected")
495
+ return {
496
+ ok: false as const,
497
+ kind: "verification" as const,
498
+ code: result.code,
499
+ failureIdentities: result.evidence.failureIdentities,
500
+ };
501
+ return {
502
+ ok: false as const,
503
+ kind:
504
+ result.category === "adapter"
505
+ ? ("verification-adapter" as const)
506
+ : ("environment" as const),
507
+ code: result.code,
508
+ };
509
+ }
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { lstatSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
+ import { canonicalJson } from "./canonical.ts";
5
6
  import { ROLES } from "./contracts.ts";
6
7
 
7
8
  export const ROUTE_DIALECTS = [
@@ -166,17 +167,6 @@ function httpUrl(value: unknown): value is string {
166
167
  }
167
168
  }
168
169
 
169
- function canonicalJson(value: unknown): string {
170
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
171
- if (isRecord(value)) {
172
- return `{${Object.entries(value)
173
- .sort(([left], [right]) => left.localeCompare(right))
174
- .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
175
- .join(",")}}`;
176
- }
177
- return JSON.stringify(value) ?? "null";
178
- }
179
-
180
170
  function routeFingerprint(route: Record<string, unknown>): string {
181
171
  return createHash("sha256").update(canonicalJson(route)).digest("hex");
182
172
  }
package/src/run-state.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { canonicalJson, compareCanonicalStrings } from "./canonical.ts";
2
3
 
3
4
  export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
4
5
  export type RunStage = (typeof RUN_STAGES)[number];
@@ -161,21 +162,6 @@ const LEGAL_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
161
162
  rejected: [],
162
163
  };
163
164
 
164
- export function canonicalJson(value: unknown): string {
165
- if (Array.isArray(value)) {
166
- return `[${value.map(canonicalJson).join(",")}]`;
167
- }
168
- if (value && typeof value === "object") {
169
- const entries = Object.entries(value as Record<string, unknown>)
170
- .filter(([, entry]) => entry !== undefined)
171
- .sort(([left], [right]) => left.localeCompare(right));
172
- return `{${entries
173
- .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
174
- .join(",")}}`;
175
- }
176
- return JSON.stringify(value) ?? "null";
177
- }
178
-
179
165
  export function hashRunEvent(event: Omit<RunEvent, "hash">): string {
180
166
  return createHash("sha256").update(canonicalJson(event)).digest("hex");
181
167
  }
@@ -329,7 +315,7 @@ function applyVerifiedEvent(
329
315
  : [...projection.deliveryBindings, binding].sort(
330
316
  (left, right) =>
331
317
  left.revision - right.revision ||
332
- left.gate.localeCompare(right.gate),
318
+ compareCanonicalStrings(left.gate, right.gate),
333
319
  );
334
320
  return {
335
321
  ...projection,
@@ -430,3 +416,5 @@ export function legalControlCommands(state: RunState): string[] {
430
416
  if (state === "recovering") return ["status", "resume", "discard"];
431
417
  return ["status", "cancel", "discard"];
432
418
  }
419
+
420
+ export { canonicalJson } from "./canonical.ts";