@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,11 +1,18 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { DatabaseSync } from "node:sqlite";
3
+ import { compareCanonicalStrings } from "./canonical.ts";
4
+ import {
5
+ type ChangeContract,
6
+ normalizeChangeContract,
7
+ } from "./change-contract.ts";
3
8
  import {
4
9
  type DesignEvidenceResult,
5
10
  validateEvidenceResult,
6
11
  } from "./contracts.ts";
7
12
  import { canonicalJson } from "./run-state.ts";
13
+ import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
8
14
  import { prepareStateRoot, type ResolvedStateRoot } from "./state-root.ts";
15
+ import { DESIGN_SCHEMA as SCHEMA } from "./storage-schema.ts";
9
16
 
10
17
  const SHA256 = /^[a-f0-9]{64}$/u;
11
18
  const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
@@ -13,51 +20,6 @@ const ZERO_HASH = "0".repeat(64);
13
20
  const MAX_PLAN_BYTES = 16 * 1024 * 1024;
14
21
  const MAX_CONTRACT_BYTES = 256 * 1024;
15
22
 
16
- const SCHEMA = `
17
- CREATE TABLE IF NOT EXISTS design_facts (
18
- run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
19
- sequence INTEGER NOT NULL,
20
- kind TEXT NOT NULL,
21
- identity TEXT NOT NULL,
22
- payload_json TEXT NOT NULL,
23
- canonical_hash TEXT NOT NULL,
24
- prior_record_hash TEXT NOT NULL,
25
- record_hash TEXT NOT NULL,
26
- PRIMARY KEY (run_id, sequence),
27
- UNIQUE (run_id, kind, identity),
28
- UNIQUE (run_id, record_hash)
29
- ) STRICT;
30
-
31
- CREATE TABLE IF NOT EXISTS design_operations (
32
- run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
33
- operation_id TEXT NOT NULL,
34
- operation TEXT NOT NULL,
35
- request_hash TEXT NOT NULL,
36
- outcome_json TEXT NOT NULL,
37
- PRIMARY KEY (run_id, operation_id)
38
- ) STRICT;
39
-
40
- CREATE TABLE IF NOT EXISTS design_compiled_plans (
41
- run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
42
- revision INTEGER NOT NULL,
43
- fact_sequence INTEGER NOT NULL,
44
- raw_sha256 TEXT NOT NULL,
45
- canonical_hash TEXT NOT NULL,
46
- plan_bytes BLOB NOT NULL,
47
- PRIMARY KEY (run_id, revision),
48
- UNIQUE (run_id, fact_sequence),
49
- FOREIGN KEY (run_id, fact_sequence)
50
- REFERENCES design_facts(run_id, sequence) ON DELETE CASCADE
51
- ) STRICT;
52
-
53
- CREATE TABLE IF NOT EXISTS design_finalization_leases (
54
- run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
55
- operation_id TEXT NOT NULL,
56
- token TEXT NOT NULL UNIQUE,
57
- expires_at INTEGER NOT NULL
58
- ) STRICT;
59
- `;
60
-
61
23
  type DecisionCategory = "behavior" | "technical";
62
24
  type DesignGate = "gate-a" | "gate-b";
63
25
 
@@ -151,6 +113,7 @@ export interface DesignGateProjection {
151
113
  }
152
114
 
153
115
  export interface DesignStatusProjection {
116
+ changeContract?: ChangeContract;
154
117
  runId: string;
155
118
  change?: string;
156
119
  evidence: DesignEvidenceProjection[];
@@ -283,7 +246,9 @@ function normalizeRefs(refs: readonly string[]): string[] {
283
246
  ) {
284
247
  throw new Error("design-decision-refs-invalid");
285
248
  }
286
- return [...new Set(refs)].sort((left, right) => left.localeCompare(right));
249
+ return [...new Set(refs)].sort((left, right) =>
250
+ compareCanonicalStrings(left, right),
251
+ );
287
252
  }
288
253
 
289
254
  export class DesignJournal {
@@ -318,11 +283,8 @@ export class DesignJournal {
318
283
  const stateRoot = prepareStateRoot(unresolved);
319
284
  const database = new DatabaseSync(stateRoot.databasePath);
320
285
  try {
321
- database.exec("PRAGMA foreign_keys = ON");
322
- database.exec("PRAGMA journal_mode = WAL");
323
- database.exec("PRAGMA synchronous = FULL");
324
- database.exec("PRAGMA busy_timeout = 5000");
325
- database.exec(SCHEMA);
286
+ configureSqlite(database);
287
+ ensureSqliteSchema(database, SCHEMA);
326
288
  return new DesignJournal(stateRoot, database, options);
327
289
  } catch (error) {
328
290
  try {
@@ -868,10 +830,10 @@ export class DesignJournal {
868
830
  }))
869
831
  .sort(
870
832
  (left, right) =>
871
- left.path.localeCompare(right.path) ||
833
+ compareCanonicalStrings(left.path, right.path) ||
872
834
  left.lineStart - right.lineStart ||
873
835
  left.lineEnd - right.lineEnd ||
874
- left.claimHash.localeCompare(right.claimHash),
836
+ compareCanonicalStrings(left.claimHash, right.claimHash),
875
837
  ),
876
838
  };
877
839
  const fact = this.#appendFact(
@@ -954,6 +916,7 @@ export class DesignJournal {
954
916
  revision,
955
917
  contractHash: decisionContractHash,
956
918
  refs,
919
+ authorityChanged: prior?.contractHash !== decisionContractHash,
957
920
  };
958
921
  const fact = this.#appendFact(
959
922
  input.runId,
@@ -1019,7 +982,7 @@ export class DesignJournal {
1019
982
  runId: string;
1020
983
  operationId: string;
1021
984
  gate: "gate-a";
1022
- contract: string;
985
+ contract: string | ChangeContract;
1023
986
  }
1024
987
  | {
1025
988
  runId: string;
@@ -1035,9 +998,18 @@ export class DesignJournal {
1035
998
  if (input.gate !== "gate-a" && input.gate !== "gate-b") {
1036
999
  throw new Error("design-gate-invalid");
1037
1000
  }
1001
+ const changeContract =
1002
+ input.gate === "gate-a" && typeof input.contract !== "string"
1003
+ ? normalizeChangeContract(input.contract)
1004
+ : undefined;
1038
1005
  const gateAContractHash =
1039
1006
  input.gate === "gate-a"
1040
- ? contractHash("abel-design-gate-a", input.contract)
1007
+ ? contractHash(
1008
+ "abel-design-gate-a",
1009
+ changeContract
1010
+ ? canonicalJson(changeContract)
1011
+ : (input.contract as string),
1012
+ )
1041
1013
  : undefined;
1042
1014
  return this.#transaction(() => {
1043
1015
  this.#run(input.runId, true);
@@ -1062,9 +1034,9 @@ export class DesignJournal {
1062
1034
  if (!current.gateA.current) throw new Error("design-gate-a-required");
1063
1035
  const plan = this.#currentPlanProjection(input.runId);
1064
1036
  if (!plan) throw new Error("design-plan-required");
1065
- const latestDecisionSequence = this.#facts(input.runId)
1066
- .filter((fact) => fact.kind === "decision")
1067
- .at(-1)?.sequence;
1037
+ const latestDecisionSequence = this.#latestSubstantiveDecisionSequence(
1038
+ input.runId,
1039
+ );
1068
1040
  if (
1069
1041
  plan.sequence <= (current.gateA.approvedSequence ?? 0) ||
1070
1042
  (latestDecisionSequence !== undefined &&
@@ -1093,6 +1065,7 @@ export class DesignJournal {
1093
1065
  gate: input.gate,
1094
1066
  revision,
1095
1067
  contractHash: approvedContractHash,
1068
+ ...(changeContract ? { changeContract } : {}),
1096
1069
  };
1097
1070
  const fact = this.#appendFact(
1098
1071
  input.runId,
@@ -1148,6 +1121,20 @@ export class DesignJournal {
1148
1121
  return undefined;
1149
1122
  }
1150
1123
 
1124
+ #certifyPlan(runId: string, planHash: string): void {
1125
+ const gates = this.#gateStatus(runId);
1126
+ if (gates.gateB.current && gates.gateB.proof?.contractHash === planHash)
1127
+ return;
1128
+ const previous = this.#latestApproval(runId, "gate-b");
1129
+ const revision = (previous?.proof.revision ?? 0) + 1;
1130
+ this.#appendFact(runId, "approval", `gate-b:${revision}`, {
1131
+ gate: "gate-b",
1132
+ revision,
1133
+ contractHash: planHash,
1134
+ authority: "compiler",
1135
+ });
1136
+ }
1137
+
1151
1138
  recordCompiledPlan(input: {
1152
1139
  runId: string;
1153
1140
  operationId: string;
@@ -1155,6 +1142,7 @@ export class DesignJournal {
1155
1142
  rawSha256: string;
1156
1143
  canonicalHash: string;
1157
1144
  lease?: DesignFinalizationLease;
1145
+ certify?: boolean;
1158
1146
  }): Record<string, unknown> {
1159
1147
  requireHash(input.rawSha256, "design-plan-raw-hash");
1160
1148
  requireHash(input.canonicalHash, "design-plan-canonical-hash");
@@ -1193,6 +1181,32 @@ export class DesignJournal {
1193
1181
  throw new Error("design-gate-a-required");
1194
1182
  }
1195
1183
  const prior = this.#currentPlanProjection(input.runId);
1184
+ const status = this.status(input.runId);
1185
+ const latestDecision =
1186
+ this.#latestSubstantiveDecisionSequence(input.runId) ?? 0;
1187
+ if (
1188
+ prior &&
1189
+ prior.rawSha256 === input.rawSha256 &&
1190
+ prior.canonicalHash === input.canonicalHash &&
1191
+ prior.sequence > latestDecision &&
1192
+ prior.sequence > (status.gates.gateA.approvedSequence ?? 0)
1193
+ ) {
1194
+ if (input.certify) this.#certifyPlan(input.runId, input.canonicalHash);
1195
+ const outcome = {
1196
+ operation: "compile-plan",
1197
+ runId: input.runId,
1198
+ plan: prior,
1199
+ gates: this.#gateStatus(input.runId),
1200
+ };
1201
+ this.#recordOperation(
1202
+ input.runId,
1203
+ input.operationId,
1204
+ "compile-plan",
1205
+ request,
1206
+ outcome,
1207
+ );
1208
+ return structuredClone(outcome);
1209
+ }
1196
1210
  const revision = (prior?.revision ?? 0) + 1;
1197
1211
  const payload = {
1198
1212
  revision,
@@ -1227,6 +1241,7 @@ export class DesignJournal {
1227
1241
  recordHash: fact.record_hash,
1228
1242
  sequence: fact.sequence,
1229
1243
  };
1244
+ if (input.certify) this.#certifyPlan(input.runId, input.canonicalHash);
1230
1245
  const outcome = {
1231
1246
  operation: "compile-plan",
1232
1247
  runId: input.runId,
@@ -1511,26 +1526,34 @@ export class DesignJournal {
1511
1526
  });
1512
1527
  }
1513
1528
 
1514
- #gateStatus(runId: string): DesignStatusProjection["gates"] {
1515
- const facts = this.#facts(runId);
1516
- const latestBehavior = facts
1529
+ #latestSubstantiveDecisionSequence(
1530
+ runId: string,
1531
+ category?: DecisionCategory,
1532
+ ): number | undefined {
1533
+ return this.#facts(runId)
1517
1534
  .filter((fact) => {
1518
1535
  if (fact.kind !== "decision") return false;
1519
- return (
1520
- parseRecord(fact.payload_json, "design-decision-record-invalid")
1521
- .category === "behavior"
1536
+ const decision = parseRecord(
1537
+ fact.payload_json,
1538
+ "design-decision-record-invalid",
1522
1539
  );
1523
- })
1524
- .at(-1)?.sequence;
1525
- const latestTechnical = facts
1526
- .filter((fact) => {
1527
- if (fact.kind !== "decision") return false;
1528
1540
  return (
1529
- parseRecord(fact.payload_json, "design-decision-record-invalid")
1530
- .category === "technical"
1541
+ decision.authorityChanged !== false &&
1542
+ (category === undefined || decision.category === category)
1531
1543
  );
1532
1544
  })
1533
1545
  .at(-1)?.sequence;
1546
+ }
1547
+
1548
+ #gateStatus(runId: string): DesignStatusProjection["gates"] {
1549
+ const latestBehavior = this.#latestSubstantiveDecisionSequence(
1550
+ runId,
1551
+ "behavior",
1552
+ );
1553
+ const latestTechnical = this.#latestSubstantiveDecisionSequence(
1554
+ runId,
1555
+ "technical",
1556
+ );
1534
1557
  const plan = this.#currentPlanProjection(runId);
1535
1558
  const gateAApproval = this.#latestApproval(runId, "gate-a");
1536
1559
  const gateBApproval = this.#latestApproval(runId, "gate-b");
@@ -1587,6 +1610,119 @@ export class DesignJournal {
1587
1610
  return { gateA, gateB };
1588
1611
  }
1589
1612
 
1613
+ /** Seed a new revision only from a completed, privately finalized delivery. */
1614
+ inheritFinalizedAuthority(runId: string): void {
1615
+ this.#transaction(() => {
1616
+ const run = this.#run(runId, true);
1617
+ if (!run.change_name || this.#facts(runId).length > 0) return;
1618
+ const candidates = this.#database
1619
+ .prepare(`
1620
+ SELECT facts.run_id, facts.payload_json FROM design_facts AS facts
1621
+ JOIN runs ON runs.run_id = facts.run_id
1622
+ WHERE runs.root_hash = ? AND runs.stage = 'abel-design'
1623
+ AND runs.change_name = ? AND runs.state = 'completed'
1624
+ AND facts.kind = 'finalization' AND runs.run_id <> ?
1625
+ `)
1626
+ .all(run.root_hash, run.change_name, runId) as unknown as Array<{
1627
+ run_id: string;
1628
+ payload_json: string;
1629
+ }>;
1630
+ const source = candidates
1631
+ .map((row) => ({
1632
+ runId: row.run_id,
1633
+ fact: parseRecord(
1634
+ row.payload_json,
1635
+ "design-finalization-record-invalid",
1636
+ ),
1637
+ }))
1638
+ .sort(
1639
+ (left, right) =>
1640
+ Number(right.fact.deliveryRevision) -
1641
+ Number(left.fact.deliveryRevision),
1642
+ )[0];
1643
+ if (!source) return;
1644
+ const status = this.status(source.runId);
1645
+ const plan = this.currentCompiledPlan(source.runId);
1646
+ const gateA = status.gates.gateA.proof;
1647
+ const gateB = status.gates.gateB.proof;
1648
+ if (
1649
+ !plan ||
1650
+ !gateA ||
1651
+ !gateB ||
1652
+ !status.gates.gateA.current ||
1653
+ !status.gates.gateB.current ||
1654
+ sha256(plan.bytes) !== plan.projection.rawSha256 ||
1655
+ !this.verifyFinalizedDelivery({
1656
+ change: run.change_name,
1657
+ deliveryRevision: Number(source.fact.deliveryRevision),
1658
+ receiptHash: String(source.fact.receiptHash),
1659
+ gateA,
1660
+ gateB,
1661
+ planCanonicalHash: plan.projection.canonicalHash,
1662
+ })
1663
+ ) {
1664
+ throw new Error("design-inherited-authority-invalid");
1665
+ }
1666
+ this.#appendFact(runId, "inheritance", "finalized-authority", {
1667
+ sourceRunId: source.runId,
1668
+ deliveryRevision: source.fact.deliveryRevision,
1669
+ receiptHash: source.fact.receiptHash,
1670
+ gateA,
1671
+ gateB,
1672
+ });
1673
+ for (const decision of status.decisions) {
1674
+ this.#appendFact(
1675
+ runId,
1676
+ "decision",
1677
+ `${decision.decisionId}:${decision.revision}`,
1678
+ {
1679
+ decisionId: decision.decisionId,
1680
+ category: decision.category,
1681
+ revision: decision.revision,
1682
+ contractHash: decision.contractHash,
1683
+ refs: decision.refs,
1684
+ },
1685
+ );
1686
+ }
1687
+ this.#appendFact(runId, "approval", `gate-a:${gateA.revision}`, {
1688
+ gate: "gate-a",
1689
+ revision: gateA.revision,
1690
+ contractHash: gateA.contractHash,
1691
+ ...(status.changeContract
1692
+ ? { changeContract: status.changeContract }
1693
+ : {}),
1694
+ });
1695
+ const projection = plan.projection;
1696
+ const fact = this.#appendFact(
1697
+ runId,
1698
+ "plan",
1699
+ String(projection.revision),
1700
+ {
1701
+ revision: projection.revision,
1702
+ rawSha256: projection.rawSha256,
1703
+ canonicalHash: projection.canonicalHash,
1704
+ },
1705
+ );
1706
+ this.#database
1707
+ .prepare(`INSERT INTO design_compiled_plans(
1708
+ run_id, revision, fact_sequence, raw_sha256, canonical_hash, plan_bytes
1709
+ ) VALUES (?, ?, ?, ?, ?, ?)`)
1710
+ .run(
1711
+ runId,
1712
+ projection.revision,
1713
+ fact.sequence,
1714
+ projection.rawSha256,
1715
+ projection.canonicalHash,
1716
+ Buffer.from(plan.bytes),
1717
+ );
1718
+ this.#appendFact(runId, "approval", `gate-b:${gateB.revision}`, {
1719
+ gate: "gate-b",
1720
+ revision: gateB.revision,
1721
+ contractHash: gateB.contractHash,
1722
+ });
1723
+ });
1724
+ }
1725
+
1590
1726
  status(runId: string): DesignStatusProjection {
1591
1727
  this.#assertOpen();
1592
1728
  const run = this.#run(runId, false);
@@ -1609,12 +1745,20 @@ export class DesignJournal {
1609
1745
  );
1610
1746
  latestDecisions.set(decision.decisionId, decision);
1611
1747
  }
1748
+ const approved = facts
1749
+ .filter((fact) => fact.kind === "approval")
1750
+ .map((fact) => parseRecord(fact.payload_json, "design-approval-invalid"))
1751
+ .filter((fact) => fact.gate === "gate-a")
1752
+ .at(-1);
1612
1753
  return {
1613
1754
  runId,
1614
1755
  ...(run.change_name ? { change: run.change_name } : {}),
1756
+ ...(approved?.changeContract
1757
+ ? { changeContract: normalizeChangeContract(approved.changeContract) }
1758
+ : {}),
1615
1759
  evidence,
1616
1760
  decisions: [...latestDecisions.values()].sort((left, right) =>
1617
- left.decisionId.localeCompare(right.decisionId),
1761
+ compareCanonicalStrings(left.decisionId, right.decisionId),
1618
1762
  ),
1619
1763
  gates: this.#gateStatus(runId),
1620
1764
  plan: this.#currentPlanProjection(runId) ?? null,