@abelxiaoxing/cadence 1.2.4 → 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 (46) hide show
  1. package/README.md +155 -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 +83 -15
  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 +399 -1079
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/openspec-cli.ts +494 -0
  23. package/src/package-delivery.ts +486 -0
  24. package/src/package-verification.ts +509 -0
  25. package/src/route-policy.ts +1 -11
  26. package/src/run-state.ts +4 -16
  27. package/src/run-store.ts +5 -144
  28. package/src/scoped-grep-worker.mjs +23 -0
  29. package/src/scoped-tools.ts +312 -116
  30. package/src/sqlite-schema.ts +171 -0
  31. package/src/storage-schema.ts +352 -0
  32. package/src/subagent-activity.ts +14 -1
  33. package/src/submit-tool.ts +20 -2
  34. package/src/task-ledger.ts +14 -55
  35. package/src/verification-capability.ts +298 -102
  36. package/src/verification-environment.ts +213 -0
  37. package/src/verification-identity.ts +77 -0
  38. package/src/worker-broker.ts +97 -34
  39. package/src/workflow-engine.ts +28 -8285
  40. package/src/workflow-policy.ts +1427 -0
  41. package/src/workflow-state-machine.ts +4478 -0
  42. package/src/workspace-io-entry.mjs +58 -0
  43. package/src/workspace-io-worker.mjs +984 -0
  44. package/src/workspace-io.ts +84 -0
  45. package/src/workspace-store.ts +125 -19
  46. package/skills/abel-workflow/SKILL.md +0 -205
@@ -1,4 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { compareCanonicalStrings } from "./canonical.ts";
3
+ import {
4
+ assertPlanWithinChangeContract,
5
+ type ChangeContract,
6
+ normalizeChangeContract,
7
+ } from "./change-contract.ts";
2
8
  import {
3
9
  type AgentsImpact,
4
10
  type ImplementGraphBoundary,
@@ -17,7 +23,10 @@ import {
17
23
  hashCanonicalValue,
18
24
  type ImplementGraphReadiness,
19
25
  } from "./implement-graph.ts";
20
- import { validateVerificationAdapterCapability } from "./verification-capability.ts";
26
+ import {
27
+ bindDraftVerificationInputs,
28
+ validateVerificationAdapterCapability,
29
+ } from "./verification-capability.ts";
21
30
 
22
31
  export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
23
32
  export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
@@ -117,7 +126,7 @@ export class DesignPlanValidationError extends Error {
117
126
  diagnostics
118
127
  .map((diagnostic) => structuredClone(diagnostic))
119
128
  .sort((left, right) =>
120
- canonicalJson(left).localeCompare(canonicalJson(right)),
129
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
121
130
  )
122
131
  .map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
123
132
  ).values(),
@@ -218,7 +227,9 @@ export function assessDeliveryTraceability(input: {
218
227
  }
219
228
  const verificationIds = input.plan.tasks
220
229
  .flatMap((task) => [
221
- task.phases.red.verification.id,
230
+ ...(task.verificationMode && task.verificationMode !== "behavior"
231
+ ? []
232
+ : [task.phases.red.verification.id]),
222
233
  task.phases.green.verification.id,
223
234
  ...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
224
235
  ])
@@ -304,6 +315,7 @@ export interface PlanTracking {
304
315
  }
305
316
 
306
317
  export interface PlanDraft {
318
+ changeContract?: ChangeContract;
307
319
  changeId: string;
308
320
  tasks: PlanTaskDraft[];
309
321
  outputs: ImplementGraphOutput[];
@@ -371,7 +383,9 @@ function normalizeArtifactBindings(
371
383
  paths.add(binding.path);
372
384
  return { path: binding.path, rawSha256: binding.rawSha256 };
373
385
  });
374
- return normalized.sort((left, right) => left.path.localeCompare(right.path));
386
+ return normalized.sort((left, right) =>
387
+ compareCanonicalStrings(left.path, right.path),
388
+ );
375
389
  }
376
390
 
377
391
  function requireReceiptIdentity(
@@ -405,7 +419,41 @@ function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
405
419
  return structuredClone(value);
406
420
  }
407
421
 
408
- function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
422
+ export interface DeliveryReadOptions {
423
+ /** Only the proof-bound delivery loader may admit historical collection ordering. */
424
+ allowLegacyOrder?: boolean;
425
+ }
426
+
427
+ const legacyPlans = new WeakMap<object, string>();
428
+
429
+ function unorderedIdentity(value: unknown): string {
430
+ if (Array.isArray(value))
431
+ return `[${value.map(unorderedIdentity).sort().join(",")}]`;
432
+ if (isRecord(value))
433
+ return `{${Object.entries(value)
434
+ .sort(([a], [b]) => compareCanonicalStrings(a, b))
435
+ .map(
436
+ ([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
437
+ )
438
+ .join(",")}}`;
439
+ return JSON.stringify(value) ?? "null";
440
+ }
441
+
442
+ function sameNormalized(
443
+ left: unknown,
444
+ right: unknown,
445
+ options: DeliveryReadOptions,
446
+ ): boolean {
447
+ return options.allowLegacyOrder
448
+ ? unorderedIdentity(left) === unorderedIdentity(right)
449
+ : canonicalJson(left) === canonicalJson(right);
450
+ }
451
+
452
+ function parseCanonicalReceipt<T>(
453
+ bytes: Uint8Array,
454
+ code: string,
455
+ options: DeliveryReadOptions = {},
456
+ ): T {
409
457
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
410
458
  let parsed: unknown;
411
459
  try {
@@ -413,7 +461,11 @@ function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
413
461
  } catch {
414
462
  throw new Error(code);
415
463
  }
416
- if (`${canonicalJson(parsed)}\n` !== text) throw new Error(code);
464
+ if (
465
+ `${canonicalJson(parsed)}\n` !== text &&
466
+ (!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
467
+ )
468
+ throw new Error(code);
417
469
  return parsed as T;
418
470
  }
419
471
 
@@ -438,10 +490,14 @@ export function compileGateAReceipt(input: {
438
490
  };
439
491
  }
440
492
 
441
- export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
493
+ export function parseGateAReceipt(
494
+ bytes: Uint8Array,
495
+ options: DeliveryReadOptions = {},
496
+ ): GateAReceipt {
442
497
  const receipt = parseCanonicalReceipt<GateAReceipt>(
443
498
  bytes,
444
499
  "gate-a-receipt-invalid",
500
+ options,
445
501
  );
446
502
  if (
447
503
  !isRecord(receipt) ||
@@ -455,8 +511,8 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
455
511
  const approval = normalizeGateProof(receipt.approval);
456
512
  const artifacts = normalizeArtifactBindings(receipt.artifacts);
457
513
  if (
458
- canonicalJson(approval) !== canonicalJson(receipt.approval) ||
459
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts)
514
+ !sameNormalized(approval, receipt.approval, options) ||
515
+ !sameNormalized(artifacts, receipt.artifacts, options)
460
516
  ) {
461
517
  throw new Error("gate-a-receipt-invalid");
462
518
  }
@@ -537,10 +593,14 @@ export function compileReadyReceipt(input: {
537
593
  };
538
594
  }
539
595
 
540
- export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
596
+ export function parseReadyReceipt(
597
+ bytes: Uint8Array,
598
+ options: DeliveryReadOptions = {},
599
+ ): ReadyReceipt {
541
600
  const receipt = parseCanonicalReceipt<ReadyReceipt>(
542
601
  bytes,
543
602
  "delivery-receipt-invalid",
603
+ options,
544
604
  );
545
605
  if (
546
606
  !isRecord(receipt) ||
@@ -592,9 +652,9 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
592
652
  const traceability = normalizeTraceability(receipt.traceability);
593
653
  if (
594
654
  gateB.contractHash !== receipt.plan.canonicalHash ||
595
- canonicalJson(gateB) !== canonicalJson(receipt.approvals.gateB) ||
596
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts) ||
597
- canonicalJson(traceability) !== canonicalJson(receipt.traceability)
655
+ !sameNormalized(gateB, receipt.approvals.gateB, options) ||
656
+ !sameNormalized(artifacts, receipt.artifacts, options) ||
657
+ !sameNormalized(traceability, receipt.traceability, options)
598
658
  ) {
599
659
  throw new Error("delivery-receipt-invalid");
600
660
  }
@@ -602,12 +662,14 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
602
662
  }
603
663
 
604
664
  function sortStrings(values: readonly string[]): string[] {
605
- return [...values].sort((left, right) => left.localeCompare(right));
665
+ return [...values].sort((left, right) =>
666
+ compareCanonicalStrings(left, right),
667
+ );
606
668
  }
607
669
 
608
670
  function sortCanonical<T>(values: readonly T[]): T[] {
609
671
  return [...values].sort((left, right) =>
610
- canonicalJson(left).localeCompare(canonicalJson(right)),
672
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
611
673
  );
612
674
  }
613
675
 
@@ -628,7 +690,7 @@ function normalizeVerification(
628
690
  return cloned;
629
691
  }
630
692
 
631
- function normalizePhase(value: unknown): PlanPhaseDraft {
693
+ function normalizePhase(value: unknown, readOnly = false): PlanPhaseDraft {
632
694
  if (
633
695
  !isRecord(value) ||
634
696
  !exactKeys(value, [
@@ -651,7 +713,7 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
651
713
  }
652
714
  const write = sortStrings(value.write as string[]);
653
715
  const deletions = sortStrings(value.delete as string[]);
654
- if (write.length + deletions.length === 0) {
716
+ if (!readOnly && write.length + deletions.length === 0) {
655
717
  throw new Error("delivery-plan-empty-phase-boundary");
656
718
  }
657
719
  if (write.some((candidate) => deletions.includes(candidate))) {
@@ -724,7 +786,12 @@ function normalizeTask(value: unknown): PlanTaskDraft {
724
786
  candidate: unknown,
725
787
  ) => {
726
788
  try {
727
- return normalizePhase(candidate);
789
+ return normalizePhase(
790
+ candidate,
791
+ phase === "red" &&
792
+ value.verificationMode !== undefined &&
793
+ value.verificationMode !== "behavior",
794
+ );
728
795
  } catch (error) {
729
796
  throw asPlanValidationError(error, {
730
797
  ...context,
@@ -758,8 +825,8 @@ function normalizeTask(value: unknown): PlanTaskDraft {
758
825
  searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
759
826
  relatedTests: [...cloned.impactClosure.relatedTests].sort(
760
827
  (left, right) =>
761
- left.path.localeCompare(right.path) ||
762
- left.disposition.localeCompare(right.disposition),
828
+ compareCanonicalStrings(left.path, right.path) ||
829
+ compareCanonicalStrings(left.disposition, right.disposition),
763
830
  ),
764
831
  affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
765
832
  },
@@ -936,7 +1003,7 @@ function normalizeAgentsCheckpointOperations(
936
1003
  : normalizeAgentsManagedBlock(operation.managedBlock),
937
1004
  };
938
1005
  })
939
- .sort((left, right) => left.target.localeCompare(right.target));
1006
+ .sort((left, right) => compareCanonicalStrings(left.target, right.target));
940
1007
  }
941
1008
 
942
1009
  function normalizeVerificationPlan(value: unknown): PlanVerification {
@@ -1059,6 +1126,7 @@ function normalizeTracking(value: unknown): PlanTracking {
1059
1126
  }
1060
1127
 
1061
1128
  function normalizeDraft(value: unknown): ImplementPlan {
1129
+ value = structuredClone(value);
1062
1130
  if (
1063
1131
  !isRecord(value) ||
1064
1132
  !exactKeys(value, [
@@ -1067,6 +1135,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
1067
1135
  "outputs",
1068
1136
  "verification",
1069
1137
  "tracking",
1138
+ ...(value.changeContract === undefined ? [] : ["changeContract"]),
1070
1139
  ]) ||
1071
1140
  typeof value.changeId !== "string" ||
1072
1141
  !CHANGE_NAME.test(value.changeId) ||
@@ -1075,9 +1144,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
1075
1144
  ) {
1076
1145
  throw new Error("delivery-plan-invalid");
1077
1146
  }
1147
+ const source = value;
1148
+ const taskDrafts = value.tasks.map((task: unknown) => {
1149
+ if (
1150
+ !isRecord(task) ||
1151
+ !["mechanical", "refactor"].includes(String(task.verificationMode))
1152
+ )
1153
+ return task;
1154
+ if (!isRecord(task.phases) || !isRecord(task.phases.green))
1155
+ throw new Error("delivery-plan-invalid-task-phases");
1156
+ if (!source.changeContract)
1157
+ throw new Error("change-contract-verification-mode-required");
1158
+ const green = task.phases.green;
1159
+ return {
1160
+ ...task,
1161
+ phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
1162
+ };
1163
+ });
1078
1164
  const draftGraph = validateImplementGraphBoundary({
1079
1165
  changeId: value.changeId,
1080
- tasks: value.tasks.map((candidate) => {
1166
+ tasks: taskDrafts.map((candidate) => {
1081
1167
  if (!isRecord(candidate)) return candidate;
1082
1168
  const {
1083
1169
  affectedVerification: _affectedVerification,
@@ -1095,12 +1181,17 @@ function normalizeDraft(value: unknown): ImplementPlan {
1095
1181
  );
1096
1182
  }
1097
1183
  const plan: ImplementPlan = {
1184
+ ...(value.changeContract === undefined
1185
+ ? {}
1186
+ : { changeContract: normalizeChangeContract(value.changeContract) }),
1098
1187
  changeId: value.changeId,
1099
- tasks: value.tasks
1188
+ tasks: taskDrafts
1100
1189
  .map(normalizeTask)
1101
- .sort((left, right) => left.taskId.localeCompare(right.taskId)),
1190
+ .sort((left, right) =>
1191
+ compareCanonicalStrings(left.taskId, right.taskId),
1192
+ ),
1102
1193
  outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
1103
- (left, right) => left.id.localeCompare(right.id),
1194
+ (left, right) => compareCanonicalStrings(left.id, right.id),
1104
1195
  ),
1105
1196
  verification: normalizeVerificationPlan(value.verification),
1106
1197
  tracking: normalizeTracking(value.tracking),
@@ -1141,6 +1232,11 @@ function normalizeDraft(value: unknown): ImplementPlan {
1141
1232
  ) {
1142
1233
  throw new Error("delivery-agents-checkpoint-mismatch");
1143
1234
  }
1235
+ if (plan.changeContract)
1236
+ assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
1237
+ plan.verification.change.fullSuite,
1238
+ plan.verification.change.postApply,
1239
+ ]);
1144
1240
  const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
1145
1241
  if (!graphValidation.ok) {
1146
1242
  throw new DesignPlanValidationError(
@@ -1158,7 +1254,12 @@ function renderTasks(plan: ImplementPlan): string {
1158
1254
  lines.push(
1159
1255
  ` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
1160
1256
  );
1161
- lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1257
+ if (!task.verificationMode || task.verificationMode === "behavior")
1258
+ lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1259
+ else
1260
+ lines.push(
1261
+ ` - Evidence mode: ${task.verificationMode}; baseline and postconditions required`,
1262
+ );
1162
1263
  lines.push(` - Green verification: ${task.phases.green.verification.id}`);
1163
1264
  }
1164
1265
  lines.push("");
@@ -1167,10 +1268,16 @@ function renderTasks(plan: ImplementPlan): string {
1167
1268
 
1168
1269
  export function compileImplementPlan(
1169
1270
  draft: unknown,
1170
- options: { consumerRoot: string },
1271
+ options: { consumerRoot: string; bindExecutionInputs?: boolean },
1171
1272
  ): CompiledDelivery {
1172
1273
  try {
1173
- const plan = normalizeDraft(draft);
1274
+ if (options.bindExecutionInputs)
1275
+ draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1276
+ const normalized = normalizeDraft(draft);
1277
+ const plan =
1278
+ isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
1279
+ ? (draft as unknown as ImplementPlan)
1280
+ : normalized;
1174
1281
  const graph = graphFromPlan(plan);
1175
1282
  const readiness = assessImplementGraphReadiness(
1176
1283
  options.consumerRoot,
@@ -1217,7 +1324,10 @@ export function compileImplementPlan(
1217
1324
  }
1218
1325
  }
1219
1326
 
1220
- export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1327
+ export function parseImplementPlan(
1328
+ bytes: Uint8Array,
1329
+ options: DeliveryReadOptions = {},
1330
+ ): ImplementPlan {
1221
1331
  const text = new TextDecoder().decode(bytes);
1222
1332
  let parsed: unknown;
1223
1333
  try {
@@ -1226,8 +1336,15 @@ export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1226
1336
  throw new Error("delivery-plan-json-invalid");
1227
1337
  }
1228
1338
  const normalized = normalizeDraft(parsed);
1229
- if (`${canonicalJson(normalized)}\n` !== text) {
1339
+ if (`${canonicalJson(normalized)}\n` === text) return normalized;
1340
+ if (
1341
+ !options.allowLegacyOrder ||
1342
+ `${canonicalJson(parsed)}\n` !== text ||
1343
+ !sameNormalized(normalized, parsed, options)
1344
+ ) {
1230
1345
  throw new Error("delivery-plan-not-canonical");
1231
1346
  }
1232
- return normalized;
1347
+ // Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
1348
+ legacyPlans.set(parsed as object, JSON.stringify(parsed));
1349
+ return parsed as ImplementPlan;
1233
1350
  }
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import {
3
4
  lstatSync,
@@ -8,6 +9,10 @@ import {
8
9
  writeFileSync,
9
10
  } from "node:fs";
10
11
  import path from "node:path";
12
+ import {
13
+ type ChangeContract,
14
+ normalizeChangeContract,
15
+ } from "./change-contract.ts";
11
16
  import { isValidRelativePath } from "./contracts.ts";
12
17
  import {
13
18
  assessDeliveryTraceability,
@@ -27,6 +32,8 @@ import {
27
32
  DesignJournal,
28
33
  type DesignStatusProjection,
29
34
  } from "./design-journal.ts";
35
+ import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
36
+ import { canonicalJson } from "./run-state.ts";
30
37
  import { RunStore } from "./run-store.ts";
31
38
  import { observeSafePath } from "./safe-path.ts";
32
39
  import type { ResolvedStateRoot } from "./state-root.ts";
@@ -38,6 +45,20 @@ const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
38
45
  const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
39
46
  const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
40
47
 
48
+ function validGateContract(value: unknown): boolean {
49
+ if (typeof value === "string")
50
+ return (
51
+ value.trim().length > 0 &&
52
+ Buffer.byteLength(value, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
53
+ );
54
+ try {
55
+ normalizeChangeContract(value);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
41
62
  export interface DesignOpenSpecInspection {
42
63
  change: string;
43
64
  schema: string;
@@ -81,7 +102,7 @@ export type DesignControlRequest =
81
102
  runId: string;
82
103
  operationId: string;
83
104
  gate: "gate-a";
84
- contract: string;
105
+ contract: string | ChangeContract;
85
106
  }
86
107
  | {
87
108
  operation: "approve-gate";
@@ -131,11 +152,17 @@ export interface DesignControllerOptions {
131
152
  export class DesignFinalizationError extends Error {
132
153
  readonly diagnostics: readonly string[];
133
154
 
134
- constructor(diagnostics: readonly string[]) {
155
+ readonly openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>;
156
+
157
+ constructor(
158
+ diagnostics: readonly string[],
159
+ openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>,
160
+ ) {
135
161
  const normalized = [...new Set(diagnostics)].sort();
136
162
  super("design-finalization-invalid");
137
163
  this.name = "DesignFinalizationError";
138
164
  this.diagnostics = Object.freeze(normalized);
165
+ this.openSpecDiagnostic = openSpecDiagnostic;
139
166
  }
140
167
  }
141
168
 
@@ -268,9 +295,7 @@ export function validateDesignControlRequest(
268
295
  "gate",
269
296
  "contract",
270
297
  ]) &&
271
- typeof value.contract === "string" &&
272
- value.contract.trim().length > 0 &&
273
- Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
298
+ validGateContract(value.contract)
274
299
  ? { ok: true, value: structuredClone(value) as DesignControlRequest }
275
300
  : { ok: false, code: "invalid-design-control-request" };
276
301
  }
@@ -452,6 +477,7 @@ export class DesignController {
452
477
  readonly #runs: RunStore;
453
478
  readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
454
479
  #closed = false;
480
+ readonly #authority = new AsyncLocalStorage<() => void>();
455
481
 
456
482
  private constructor(options: DesignControllerOptions) {
457
483
  this.consumerRoot = path.resolve(options.consumerRoot);
@@ -478,7 +504,22 @@ export class DesignController {
478
504
  return new DesignController(options);
479
505
  }
480
506
 
507
+ executeWithAuthority(
508
+ value: unknown,
509
+ assertAuthority: () => void,
510
+ ): Promise<Record<string, unknown>> {
511
+ return this.#authority.run(assertAuthority, () => this.execute(value));
512
+ }
513
+
514
+ #assertFinalizationLease(
515
+ lease: Parameters<DesignJournal["assertFinalizationLease"]>[0],
516
+ ): void {
517
+ this.#authority.getStore()?.();
518
+ this.#journal.assertFinalizationLease(lease);
519
+ }
520
+
481
521
  #assertOpen(): void {
522
+ this.#authority.getStore()?.();
482
523
  if (this.#closed) throw new Error("design-controller-closed");
483
524
  }
484
525
 
@@ -624,6 +665,7 @@ export class DesignController {
624
665
  throw error;
625
666
  }
626
667
  if (run.state === "created") {
668
+ this.#journal.inheritFinalizedAuthority(run.runId);
627
669
  this.#runs.transition({
628
670
  runId: run.runId,
629
671
  to: "paused",
@@ -824,7 +866,21 @@ export class DesignController {
824
866
  { code: "design-plan-draft-invalid", field: "plan-draft.json" },
825
867
  ]);
826
868
  }
869
+ if (status.changeContract) {
870
+ if (!draft || typeof draft !== "object" || Array.isArray(draft))
871
+ throw new Error("change-contract-invalid");
872
+ const record = draft as Record<string, unknown>;
873
+ if (
874
+ record.changeContract !== undefined &&
875
+ canonicalJson(normalizeChangeContract(record.changeContract)) !==
876
+ canonicalJson(status.changeContract)
877
+ )
878
+ throw new Error("change-contract-mismatch");
879
+ record.changeContract = status.changeContract;
880
+ } else if (draft && typeof draft === "object" && "changeContract" in draft)
881
+ throw new Error("change-contract-gate-a-required");
827
882
  const compiled = compileImplementPlan(draft, {
883
+ bindExecutionInputs: true,
828
884
  consumerRoot: this.consumerRoot,
829
885
  });
830
886
  if (compiled.plan.changeId !== status.change) {
@@ -875,9 +931,15 @@ export class DesignController {
875
931
  if (!change) throw new Error("design-change-required");
876
932
  const readyPath = safeRelative(change, "ready.yaml");
877
933
  const gateAPath = safeRelative(change, "gate-a.yaml");
878
- removeSafeFile(this.consumerRoot, readyPath);
879
- removeSafeFile(this.consumerRoot, gateAPath);
880
- this.#journal.assertFinalizationLease(lease);
934
+ const unchanged =
935
+ status.gates.gateB.current &&
936
+ status.plan?.rawSha256 === compiled.rawSha256 &&
937
+ status.plan.canonicalHash === compiled.planHash;
938
+ if (!unchanged) {
939
+ removeSafeFile(this.consumerRoot, readyPath);
940
+ removeSafeFile(this.consumerRoot, gateAPath);
941
+ }
942
+ this.#assertFinalizationLease(lease);
881
943
  atomicWrite(
882
944
  this.consumerRoot,
883
945
  safeRelative(change, "implement-plan.json"),
@@ -890,6 +952,7 @@ export class DesignController {
890
952
  rawSha256: compiled.rawSha256,
891
953
  canonicalHash: compiled.planHash,
892
954
  lease,
955
+ certify: true,
893
956
  });
894
957
  } finally {
895
958
  this.#journal.releaseFinalizationLease(lease);
@@ -934,9 +997,12 @@ export class DesignController {
934
997
  removeSafeFile(this.consumerRoot, readyPath);
935
998
  const diagnostics = new Set<string>();
936
999
  let inspection: DesignOpenSpecInspection | undefined;
1000
+ let openSpecDiagnostic: Readonly<OpenSpecDiagnostic> | undefined;
937
1001
  try {
938
1002
  inspection = await this.#inspectOpenSpec(this.consumerRoot, change);
939
- } catch {
1003
+ } catch (error) {
1004
+ if (error instanceof OpenSpecCliError)
1005
+ openSpecDiagnostic = error.diagnostic;
940
1006
  diagnostics.add("design-openspec-unavailable");
941
1007
  }
942
1008
  if (inspection) {
@@ -960,7 +1026,9 @@ export class DesignController {
960
1026
  }
961
1027
  let compiled: ReturnType<typeof compileImplementPlan> | undefined;
962
1028
  try {
963
- const plan = parseImplementPlan(installedPlanBytes);
1029
+ const plan = parseImplementPlan(installedPlanBytes, {
1030
+ allowLegacyOrder: true,
1031
+ });
964
1032
  compiled = compileImplementPlan(plan, {
965
1033
  consumerRoot: this.consumerRoot,
966
1034
  });
@@ -1004,7 +1072,7 @@ export class DesignController {
1004
1072
  let traceability:
1005
1073
  | ReturnType<typeof assessDeliveryTraceability>
1006
1074
  | undefined;
1007
- if (compiled) {
1075
+ if (compiled && inspection) {
1008
1076
  const tasks = artifactBytes.get("tasks.md");
1009
1077
  const specs = [...artifactBytes]
1010
1078
  .filter(
@@ -1035,7 +1103,7 @@ export class DesignController {
1035
1103
  !traceability?.ok ||
1036
1104
  diagnostics.size > 0
1037
1105
  ) {
1038
- throw new DesignFinalizationError([...diagnostics]);
1106
+ throw new DesignFinalizationError([...diagnostics], openSpecDiagnostic);
1039
1107
  }
1040
1108
  const gateAReceipt = compileGateAReceipt({
1041
1109
  change,
@@ -1057,14 +1125,14 @@ export class DesignController {
1057
1125
  traceability: traceability.value,
1058
1126
  });
1059
1127
  readyHash = readyReceipt.rawSha256;
1060
- this.#journal.assertFinalizationLease(lease);
1128
+ this.#assertFinalizationLease(lease);
1061
1129
  atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
1062
1130
  atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
1063
1131
  readyInstalled = true;
1064
1132
  const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
1065
1133
  const installedReady = readSafeFile(this.consumerRoot, readyPath);
1066
- parseGateAReceipt(installedGateA);
1067
- parseReadyReceipt(installedReady);
1134
+ parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
1135
+ parseReadyReceipt(installedReady, { allowLegacyOrder: true });
1068
1136
  if (
1069
1137
  !Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
1070
1138
  !Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))