@abelxiaoxing/cadence 1.1.1 → 1.2.1

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.
@@ -11,6 +11,7 @@ const SHA256 = /^[a-f0-9]{64}$/u;
11
11
  const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
12
12
  const ZERO_HASH = "0".repeat(64);
13
13
  const MAX_PLAN_BYTES = 16 * 1024 * 1024;
14
+ const MAX_CONTRACT_BYTES = 256 * 1024;
14
15
 
15
16
  const SCHEMA = `
16
17
  CREATE TABLE IF NOT EXISTS design_facts (
@@ -144,6 +145,7 @@ export interface DesignGateProjection {
144
145
  | "behavior-decision-changed"
145
146
  | "technical-decision-changed"
146
147
  | "plan-recompiled"
148
+ | "gate-a-reapproved"
147
149
  | "gate-a-stale"
148
150
  | "plan-missing";
149
151
  }
@@ -223,6 +225,21 @@ function sha256(value: Uint8Array | string): string {
223
225
  return createHash("sha256").update(value).digest("hex");
224
226
  }
225
227
 
228
+ function contractHash(domain: string, value: string): string {
229
+ if (
230
+ typeof value !== "string" ||
231
+ value.trim().length === 0 ||
232
+ Buffer.byteLength(value, "utf8") > MAX_CONTRACT_BYTES
233
+ ) {
234
+ throw new Error("design-transient-input-invalid");
235
+ }
236
+ return createHash("sha256")
237
+ .update(domain)
238
+ .update("\0")
239
+ .update(value.replace(/\r\n?/gu, "\n").trim())
240
+ .digest("hex");
241
+ }
242
+
226
243
  function requireIdentifier(value: string, label: string): void {
227
244
  if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
228
245
  }
@@ -877,11 +894,14 @@ export class DesignJournal {
877
894
  operationId: string;
878
895
  decisionId: string;
879
896
  category: DecisionCategory;
880
- contractHash: string;
897
+ contract: string;
881
898
  refs: string[];
882
899
  }): Record<string, unknown> {
883
900
  requireIdentifier(input.decisionId, "design-decision-id");
884
- requireHash(input.contractHash, "design-decision-hash");
901
+ const decisionContractHash = contractHash(
902
+ "abel-design-decision-v1",
903
+ input.contract,
904
+ );
885
905
  if (input.category !== "behavior" && input.category !== "technical") {
886
906
  throw new Error("design-decision-category-invalid");
887
907
  }
@@ -890,7 +910,7 @@ export class DesignJournal {
890
910
  operation: "record-decision",
891
911
  decisionId: input.decisionId,
892
912
  category: input.category,
893
- contractHash: input.contractHash,
913
+ contractHash: decisionContractHash,
894
914
  refs,
895
915
  };
896
916
  return this.#transaction(() => {
@@ -909,7 +929,7 @@ export class DesignJournal {
909
929
  let decision: DesignDecisionProjection;
910
930
  if (
911
931
  prior &&
912
- prior.contractHash === input.contractHash &&
932
+ prior.contractHash === decisionContractHash &&
913
933
  canonicalJson(prior.refs) === canonicalJson(refs)
914
934
  ) {
915
935
  decision = prior;
@@ -919,7 +939,7 @@ export class DesignJournal {
919
939
  decisionId: input.decisionId,
920
940
  category: input.category,
921
941
  revision,
922
- contractHash: input.contractHash,
942
+ contractHash: decisionContractHash,
923
943
  refs,
924
944
  };
925
945
  const fact = this.#appendFact(
@@ -980,12 +1000,20 @@ export class DesignJournal {
980
1000
  return undefined;
981
1001
  }
982
1002
 
983
- approveGate(input: {
984
- runId: string;
985
- operationId: string;
986
- gate: DesignGate;
987
- contractHash: string;
988
- }): {
1003
+ approveGate(
1004
+ input:
1005
+ | {
1006
+ runId: string;
1007
+ operationId: string;
1008
+ gate: "gate-a";
1009
+ contract: string;
1010
+ }
1011
+ | {
1012
+ runId: string;
1013
+ operationId: string;
1014
+ gate: "gate-b";
1015
+ },
1016
+ ): {
989
1017
  operation: "approve-gate";
990
1018
  runId: string;
991
1019
  gate: DesignGate;
@@ -994,14 +1022,17 @@ export class DesignJournal {
994
1022
  if (input.gate !== "gate-a" && input.gate !== "gate-b") {
995
1023
  throw new Error("design-gate-invalid");
996
1024
  }
997
- requireHash(input.contractHash, "design-decision-hash");
998
- const request = {
999
- operation: "approve-gate",
1000
- gate: input.gate,
1001
- contractHash: input.contractHash,
1002
- };
1025
+ const gateAContractHash =
1026
+ input.gate === "gate-a"
1027
+ ? contractHash("abel-design-gate-a-v1", input.contract)
1028
+ : undefined;
1003
1029
  return this.#transaction(() => {
1004
1030
  this.#run(input.runId, true);
1031
+ const request = {
1032
+ operation: "approve-gate",
1033
+ gate: input.gate,
1034
+ ...(gateAContractHash ? { contractHash: gateAContractHash } : {}),
1035
+ };
1005
1036
  const replay = this.#operationReplay(
1006
1037
  input.runId,
1007
1038
  input.operationId,
@@ -1012,15 +1043,26 @@ export class DesignJournal {
1012
1043
  return structuredClone(replay) as ReturnType<
1013
1044
  DesignJournal["approveGate"]
1014
1045
  >;
1046
+ let approvedContractHash = gateAContractHash;
1015
1047
  if (input.gate === "gate-b") {
1016
1048
  const current = this.#gateStatus(input.runId);
1017
1049
  if (!current.gateA.current) throw new Error("design-gate-a-required");
1018
1050
  const plan = this.#currentPlanProjection(input.runId);
1019
1051
  if (!plan) throw new Error("design-plan-required");
1020
- if (plan.canonicalHash !== input.contractHash) {
1021
- throw new Error("design-gate-b-plan-mismatch");
1052
+ const latestDecisionSequence = this.#facts(input.runId)
1053
+ .filter((fact) => fact.kind === "decision")
1054
+ .at(-1)?.sequence;
1055
+ if (
1056
+ plan.sequence <= (current.gateA.approvedSequence ?? 0) ||
1057
+ (latestDecisionSequence !== undefined &&
1058
+ plan.sequence <= latestDecisionSequence)
1059
+ ) {
1060
+ throw new Error("design-plan-stale");
1022
1061
  }
1062
+ approvedContractHash = plan.canonicalHash;
1023
1063
  }
1064
+ if (!approvedContractHash)
1065
+ throw new Error("design-transient-input-invalid");
1024
1066
  const existing = this.#latestApproval(input.runId, input.gate);
1025
1067
  const currentGate = this.#gateStatus(input.runId)[
1026
1068
  input.gate === "gate-a" ? "gateA" : "gateB"
@@ -1029,7 +1071,7 @@ export class DesignJournal {
1029
1071
  if (
1030
1072
  existing &&
1031
1073
  currentGate.current &&
1032
- existing.proof.contractHash === input.contractHash
1074
+ existing.proof.contractHash === approvedContractHash
1033
1075
  ) {
1034
1076
  proof = existing.proof;
1035
1077
  } else {
@@ -1037,7 +1079,7 @@ export class DesignJournal {
1037
1079
  const payload = {
1038
1080
  gate: input.gate,
1039
1081
  revision,
1040
- contractHash: input.contractHash,
1082
+ contractHash: approvedContractHash,
1041
1083
  };
1042
1084
  const fact = this.#appendFact(
1043
1085
  input.runId,
@@ -1047,7 +1089,7 @@ export class DesignJournal {
1047
1089
  );
1048
1090
  proof = {
1049
1091
  revision,
1050
- contractHash: input.contractHash,
1092
+ contractHash: approvedContractHash,
1051
1093
  recordHash: fact.record_hash,
1052
1094
  };
1053
1095
  }
@@ -1506,6 +1548,11 @@ export class DesignJournal {
1506
1548
  latestTechnical > gateBApproval.sequence
1507
1549
  ) {
1508
1550
  staleReason = "technical-decision-changed";
1551
+ } else if (
1552
+ gateAApproval !== undefined &&
1553
+ gateAApproval.sequence > gateBApproval.sequence
1554
+ ) {
1555
+ staleReason = "gate-a-reapproved";
1509
1556
  } else if (plan && plan.sequence > gateBApproval.sequence) {
1510
1557
  staleReason = "plan-recompiled";
1511
1558
  } else if (!plan) {
package/src/index.ts CHANGED
@@ -37,7 +37,10 @@ import {
37
37
  type StructuredVerificationContract,
38
38
  verificationSteps,
39
39
  } from "./contracts.ts";
40
- import { validateControlCommand } from "./control-contracts.ts";
40
+ import {
41
+ canonicalizeControlCommandToolInput,
42
+ validateControlCommand,
43
+ } from "./control-contracts.ts";
41
44
  import {
42
45
  assessDeliveryTraceability,
43
46
  compileImplementPlan,
@@ -210,6 +213,42 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
210
213
  description:
211
214
  "One closed durable Design control operation. The operation body is strict and idempotent by operationId.",
212
215
  oneOf: [
216
+ ...(["requirement", "change"] as const).map((field) => ({
217
+ type: "object" as const,
218
+ properties: {
219
+ operation: { type: "string" as const, enum: ["start"] },
220
+ operationId: { type: "string" as const },
221
+ [field]: {
222
+ type: "string" as const,
223
+ description:
224
+ field === "requirement"
225
+ ? "Raw Design requirement; code derives and stores only its stable hash."
226
+ : "Existing canonical change name to resume or revise.",
227
+ },
228
+ },
229
+ required: ["operation", "operationId", field],
230
+ additionalProperties: false,
231
+ })),
232
+ {
233
+ type: "object",
234
+ properties: {
235
+ operation: { type: "string", enum: ["status"] },
236
+ runId: { type: "string" },
237
+ },
238
+ required: ["operation", "runId"],
239
+ additionalProperties: false,
240
+ },
241
+ {
242
+ type: "object",
243
+ properties: {
244
+ operation: { type: "string", enum: ["bind-change"] },
245
+ runId: { type: "string" },
246
+ operationId: { type: "string" },
247
+ change: { type: "string" },
248
+ },
249
+ required: ["operation", "runId", "operationId", "change"],
250
+ additionalProperties: false,
251
+ },
213
252
  {
214
253
  type: "object",
215
254
  properties: {
@@ -218,7 +257,11 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
218
257
  operationId: { type: "string" },
219
258
  decisionId: { type: "string" },
220
259
  category: { type: "string", enum: ["behavior", "technical"] },
221
- contractHash: { type: "string" },
260
+ contract: {
261
+ type: "string",
262
+ description:
263
+ "Normalized decision contract; code hashes it and never persists the text.",
264
+ },
222
265
  refs: { type: "array", items: { type: "string" } },
223
266
  },
224
267
  required: [
@@ -227,7 +270,7 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
227
270
  "operationId",
228
271
  "decisionId",
229
272
  "category",
230
- "contractHash",
273
+ "contract",
231
274
  "refs",
232
275
  ],
233
276
  additionalProperties: false,
@@ -238,10 +281,25 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
238
281
  operation: { type: "string", enum: ["approve-gate"] },
239
282
  runId: { type: "string" },
240
283
  operationId: { type: "string" },
241
- gate: { type: "string", enum: ["gate-a", "gate-b"] },
242
- contractHash: { type: "string" },
284
+ gate: { type: "string", enum: ["gate-a"] },
285
+ contract: {
286
+ type: "string",
287
+ description:
288
+ "Complete approved WHAT contract; code hashes it and never persists the text.",
289
+ },
243
290
  },
244
- required: ["operation", "runId", "operationId", "gate", "contractHash"],
291
+ required: ["operation", "runId", "operationId", "gate", "contract"],
292
+ additionalProperties: false,
293
+ },
294
+ {
295
+ type: "object",
296
+ properties: {
297
+ operation: { type: "string", enum: ["approve-gate"] },
298
+ runId: { type: "string" },
299
+ operationId: { type: "string" },
300
+ gate: { type: "string", enum: ["gate-b"] },
301
+ },
302
+ required: ["operation", "runId", "operationId", "gate"],
245
303
  additionalProperties: false,
246
304
  },
247
305
  {
@@ -417,7 +475,7 @@ export interface PackageDeliverySourceOptions {
417
475
 
418
476
  export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
419
477
  discoverLatest(input: {
420
- stage: "abel-design" | "abel-implement";
478
+ stage: "abel-implement";
421
479
  change: string;
422
480
  }): Promise<WorkflowAvailableDelivery | undefined>;
423
481
  }
@@ -1418,6 +1476,7 @@ export function openPackageWorkflowControlEngine(
1418
1476
  const routeResolution = loadRoutePolicy({
1419
1477
  cwd: consumerRoot,
1420
1478
  home: homedir(),
1479
+ ...(initialContext.model ? { parentModel: initialContext.model } : {}),
1421
1480
  });
1422
1481
  const stateRoot = resolveStateRoot({
1423
1482
  consumerRoot,
@@ -1614,6 +1673,7 @@ export function openPackageWorkflowControlEngine(
1614
1673
  const operationRouteResolution = loadRoutePolicy({
1615
1674
  cwd: consumerRoot,
1616
1675
  home: homedir(),
1676
+ ...(context.model ? { parentModel: context.model } : {}),
1617
1677
  });
1618
1678
  engine.updateRoutePolicy(
1619
1679
  operationRouteResolution.ok
@@ -1636,12 +1696,7 @@ export function openPackageWorkflowControlEngine(
1636
1696
  operationRouteResolution,
1637
1697
  engine.routePolicyStatus(),
1638
1698
  );
1639
- const designStatus =
1640
- validation.value.stage === "abel-design" &&
1641
- typeof outcome.runId === "string"
1642
- ? { design: design.status(outcome.runId) }
1643
- : {};
1644
- return { ...outcome, ...designStatus, routePolicy };
1699
+ return { ...outcome, routePolicy };
1645
1700
  });
1646
1701
  },
1647
1702
  executeDesign(request: unknown) {
@@ -1781,16 +1836,15 @@ export function registerWorkflowControl(
1781
1836
  },
1782
1837
  stage: {
1783
1838
  type: "string",
1784
- enum: ["abel-design", "abel-implement"],
1839
+ enum: ["abel-implement"],
1785
1840
  },
1786
1841
  change: { type: "string" },
1787
- provisionalKey: { type: "string" },
1788
1842
  operationId: { type: "string" },
1789
1843
  deliveryRevision: { type: "integer", minimum: 1 },
1790
1844
  receiptHash: { type: "string" },
1791
1845
  routeId: { type: "string" },
1792
1846
  },
1793
- required: ["command", "stage"],
1847
+ required: ["command", "stage", "change"],
1794
1848
  additionalProperties: false,
1795
1849
  } as const;
1796
1850
  const PACKET_PARAMETERS = {
@@ -1818,9 +1872,15 @@ export function registerWorkflowControl(
1818
1872
  label: "Abel Control",
1819
1873
  description:
1820
1874
  kind === "command"
1821
- ? "Private stage-bound Abel workflow control. Accepts durable Design and Implement change commands."
1875
+ ? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
1822
1876
  : "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
1823
1877
  executionMode: "parallel",
1878
+ ...(kind === "command"
1879
+ ? {
1880
+ prepareArguments: (args: unknown) =>
1881
+ canonicalizeControlCommandToolInput(args) as never,
1882
+ }
1883
+ : {}),
1824
1884
  parameters:
1825
1885
  kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
1826
1886
  async execute(
@@ -1953,7 +2013,9 @@ export function registerWorkflowControl(
1953
2013
  ...(usage === undefined ? {} : { usage }),
1954
2014
  };
1955
2015
  }
1956
- const validation = validateControlCommand(params);
2016
+ const validation = validateControlCommand(
2017
+ canonicalizeControlCommandToolInput(params),
2018
+ );
1957
2019
  if (!validation.ok) {
1958
2020
  const error = new Error(validation.code);
1959
2021
  error.name = "ControlCommandError";
@@ -2007,10 +2069,6 @@ export function registerWorkflowControl(
2007
2069
  ["completed", "discarded", "rejected"].includes(String(payload.state))
2008
2070
  ) {
2009
2071
  await deactivateStage();
2010
- } else if (validation.value.stage === "abel-design") {
2011
- // Design starts/statuses through the command surface, then switches to
2012
- // its bounded evidence and artifact packet surface for the next turn.
2013
- registerDispatchTool("packet");
2014
2072
  }
2015
2073
  let display: ReturnType<typeof activity.finalizeWorkflow>;
2016
2074
  if (tuiControl) {
@@ -2068,7 +2126,7 @@ export function registerWorkflowControl(
2068
2126
  if (activePrompt === "abel-design" && prompt !== "abel-design") {
2069
2127
  restoreDesignTools();
2070
2128
  }
2071
- registerDispatchTool(prompt === "abel-diagnose" ? "packet" : "command");
2129
+ registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
2072
2130
  activePrompt = prompt;
2073
2131
  const sessionId = ctx.sessionManager?.getSessionId?.();
2074
2132
  if (typeof sessionId === "string") {
@@ -20,7 +20,6 @@ import { runtimeForWorkerRoute } from "./parent-provider.ts";
20
20
  import {
21
21
  loadRoutePolicy,
22
22
  type RoutePolicy,
23
- unavailableRoutePolicy,
24
23
  type WorkerRole,
25
24
  } from "./route-policy.ts";
26
25
  import { type BrokerActivityUpdate, RunWorkerBroker } from "./worker-broker.ts";
@@ -319,7 +318,7 @@ export class PacketRuntime {
319
318
  if (!agent) {
320
319
  throw new Error("packet agent definition is unavailable");
321
320
  }
322
- const broker = this.#brokerFor(context.cwd);
321
+ const broker = this.#brokerFor(context);
323
322
  const routed = await broker.run({
324
323
  runId: `${packet.stage}:${packet.id}`,
325
324
  operationId: packet.id,
@@ -405,8 +404,8 @@ export class PacketRuntime {
405
404
  return routed.value.child;
406
405
  }
407
406
 
408
- #brokerFor(cwd: string): RunWorkerBroker {
409
- const root = path.resolve(cwd);
407
+ #brokerFor(context: PacketContext): RunWorkerBroker {
408
+ const root = path.resolve(context.cwd);
410
409
  const existing = this.#brokers.get(root);
411
410
  let policy = this.#routePolicy;
412
411
  if (policy && existing) return existing;
@@ -414,12 +413,13 @@ export class PacketRuntime {
414
413
  const resolution = loadRoutePolicy({
415
414
  cwd: root,
416
415
  ...(this.#routePolicyHome ? { home: this.#routePolicyHome } : {}),
416
+ ...(context.model ? { parentModel: context.model } : {}),
417
417
  });
418
- if (!resolution.ok && resolution.source.kind !== "none") {
418
+ if (!resolution.ok) {
419
419
  const code = resolution.diagnostics[0]?.code ?? "policy-invalid";
420
420
  throw new Error(`route-policy-unavailable:${code}`);
421
421
  }
422
- policy = resolution.ok ? resolution.policy : unavailableRoutePolicy();
422
+ policy = resolution.policy;
423
423
  }
424
424
  const serializedPolicy = JSON.stringify(policy);
425
425
  if (existing && this.#brokerPolicies.get(root) === serializedPolicy) {
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, lstatSync, readFileSync } from "node:fs";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { ROLES } from "./contracts.ts";
@@ -21,6 +21,11 @@ export interface RouteCapabilities {
21
21
  maxTokens: number;
22
22
  }
23
23
 
24
+ export interface ParentModelCapabilities {
25
+ contextWindow: number;
26
+ maxTokens: number;
27
+ }
28
+
24
29
  interface RouteBase {
25
30
  id: string;
26
31
  kind: "inherited" | "custom";
@@ -50,10 +55,9 @@ export interface RoutePolicy {
50
55
 
51
56
  export type RoutePolicySource =
52
57
  | { kind: "project" | "user"; path: string }
53
- | { kind: "none" };
58
+ | { kind: "default" };
54
59
 
55
60
  export type RoutePolicyDiagnosticCode =
56
- | "policy-missing"
57
61
  | "policy-unreadable"
58
62
  | "policy-invalid-json"
59
63
  | "policy-invalid"
@@ -77,7 +81,7 @@ export type ParsedRoutePolicy =
77
81
  export type RoutePolicyResolution =
78
82
  | {
79
83
  ok: true;
80
- source: Extract<RoutePolicySource, { kind: "project" | "user" }>;
84
+ source: RoutePolicySource;
81
85
  policy: RoutePolicy;
82
86
  }
83
87
  | {
@@ -87,8 +91,8 @@ export type RoutePolicyResolution =
87
91
  };
88
92
 
89
93
  /**
90
- * Internal fail-closed policy used while the selected external policy is
91
- * missing or invalid. It deliberately contains no inherited-parent route:
94
+ * Internal fail-closed policy used while a selected explicit policy is
95
+ * invalid. It deliberately contains no inherited-parent route:
92
96
  * the broker therefore reports endpoint-unavailable without making a Worker
93
97
  * request, while the local durable control plane remains available.
94
98
  *
@@ -351,8 +355,16 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
351
355
  };
352
356
  }
353
357
 
354
- /** Parent-model route used by read-only Design and Diagnose packets. */
355
- export function parentRoutePolicy(): RoutePolicy {
358
+ /** Default parent-model route used when no explicit policy file exists. */
359
+ export function parentRoutePolicy(
360
+ parentModel?: ParentModelCapabilities,
361
+ ): RoutePolicy {
362
+ const contextWindow = positiveInteger(parentModel?.contextWindow)
363
+ ? parentModel.contextWindow
364
+ : 1;
365
+ const maxTokens = positiveInteger(parentModel?.maxTokens)
366
+ ? Math.min(parentModel.maxTokens, contextWindow)
367
+ : 1;
356
368
  const parsed = parseRoutePolicy({
357
369
  version: ROUTE_POLICY_VERSION,
358
370
  routes: {
@@ -361,8 +373,8 @@ export function parentRoutePolicy(): RoutePolicy {
361
373
  capabilities: {
362
374
  roles: [...ROLES],
363
375
  dialects: [...ROUTE_DIALECTS],
364
- contextWindow: 1_000_000,
365
- maxTokens: 500_000,
376
+ contextWindow,
377
+ maxTokens,
366
378
  },
367
379
  },
368
380
  },
@@ -372,25 +384,41 @@ export function parentRoutePolicy(): RoutePolicy {
372
384
  return parsed.policy;
373
385
  }
374
386
 
375
- function selectedSource(cwd: string, home: string): RoutePolicySource {
387
+ function selectedSource(
388
+ cwd: string,
389
+ home: string,
390
+ ): { kind: "project" | "user"; path: string } | { kind: "none" } {
391
+ const mayExist = (candidate: string): boolean => {
392
+ try {
393
+ return lstatSync(candidate, { throwIfNoEntry: false }) !== undefined;
394
+ } catch {
395
+ // Permission and path-observation failures are explicit unreadable
396
+ // policy candidates, not proof that the policy is absent.
397
+ return true;
398
+ }
399
+ };
376
400
  const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
377
- if (existsSync(projectPath)) return { kind: "project", path: projectPath };
401
+ if (mayExist(projectPath)) return { kind: "project", path: projectPath };
378
402
  const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
379
- if (existsSync(userPath)) return { kind: "user", path: userPath };
403
+ if (mayExist(userPath)) return { kind: "user", path: userPath };
380
404
  return { kind: "none" };
381
405
  }
382
406
 
383
407
  export function loadRoutePolicy(
384
- options: { cwd?: string; home?: string } = {},
408
+ options: {
409
+ cwd?: string;
410
+ home?: string;
411
+ parentModel?: ParentModelCapabilities;
412
+ } = {},
385
413
  ): RoutePolicyResolution {
386
414
  const cwd = path.resolve(options.cwd ?? process.cwd());
387
415
  const home = path.resolve(options.home ?? homedir());
388
416
  const source = selectedSource(cwd, home);
389
417
  if (source.kind === "none") {
390
418
  return {
391
- ok: false,
392
- source,
393
- diagnostics: [{ code: "policy-missing" }],
419
+ ok: true,
420
+ source: { kind: "default" },
421
+ policy: parentRoutePolicy(options.parentModel),
394
422
  };
395
423
  }
396
424
  let bytes: string;
package/src/run-state.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { ControlStage } from "./control-contracts.ts";
2
+
3
+ export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
4
+ export type RunStage = (typeof RUN_STAGES)[number];
3
5
 
4
6
  export const RUN_STATES = [
5
7
  "created",
@@ -57,7 +59,7 @@ export interface DeliveryBindingProjection {
57
59
  export interface RunProjection {
58
60
  runId: string;
59
61
  rootHash: string;
60
- stage: ControlStage;
62
+ stage: RunStage;
61
63
  change?: string;
62
64
  provisionalKey?: string;
63
65
  state: RunState;
package/src/run-store.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
3
3
  import { DatabaseSync } from "node:sqlite";
4
- import type { ControlStage } from "./control-contracts.ts";
5
4
  import {
6
5
  createRunEvent,
7
6
  type DeliveryGate,
8
7
  type RunEvent,
9
8
  type RunProjection,
9
+ type RunStage,
10
10
  type RunState,
11
11
  reduceRunEvents,
12
12
  ZERO_EVENT_HASH,
@@ -37,6 +37,7 @@ interface EventRow {
37
37
  }
38
38
 
39
39
  interface OperationRow {
40
+ kind: string;
40
41
  state: string;
41
42
  outcome_json: string | null;
42
43
  lease_token: string | null;
@@ -49,7 +50,7 @@ export interface RunStoreOptions {
49
50
  }
50
51
 
51
52
  export interface StartRunInput {
52
- stage: ControlStage;
53
+ stage: RunStage;
53
54
  change?: string;
54
55
  provisionalKey?: string;
55
56
  operationId: string;
@@ -338,7 +339,7 @@ export class RunStore {
338
339
  #operation(runId: string, operationId: string): OperationRow | undefined {
339
340
  return this.#database
340
341
  .prepare(
341
- `SELECT state, outcome_json, lease_token, lease_expires_at
342
+ `SELECT kind, state, outcome_json, lease_token, lease_expires_at
342
343
  FROM operations WHERE run_id = ? AND operation_id = ?`,
343
344
  )
344
345
  .get(runId, operationId) as OperationRow | undefined;
@@ -347,8 +348,10 @@ export class RunStore {
347
348
  #operationOutcome(
348
349
  runId: string,
349
350
  operationId: string,
351
+ kind: string,
350
352
  ): RunProjection | undefined {
351
353
  const row = this.#operation(runId, operationId);
354
+ if (row && row.kind !== kind) throw new Error("operation-id-conflict");
352
355
  if (row?.state !== "committed" || row.outcome_json === null)
353
356
  return undefined;
354
357
  return parseProjection(row.outcome_json);
@@ -465,6 +468,7 @@ export class RunStore {
465
468
  const replay = this.#operationOutcome(
466
469
  existing.run_id,
467
470
  input.operationId,
471
+ "start",
468
472
  );
469
473
  if (replay) return replay;
470
474
  const projection = parseProjection(existing.projection_json);
@@ -539,7 +543,11 @@ export class RunStore {
539
543
  requireIdentifier(input.operationId, "operation-id");
540
544
  if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
541
545
  return this.#transaction(() => {
542
- const replay = this.#operationOutcome(input.runId, input.operationId);
546
+ const replay = this.#operationOutcome(
547
+ input.runId,
548
+ input.operationId,
549
+ "change-bound",
550
+ );
543
551
  if (replay) return replay;
544
552
  const current = this.status(input.runId);
545
553
  if (current.stage !== "abel-design" || !current.provisionalKey) {
@@ -597,7 +605,11 @@ export class RunStore {
597
605
  if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
598
606
  this.#assertLease(input.lease);
599
607
  }
600
- const replay = this.#operationOutcome(input.runId, input.operationId);
608
+ const replay = this.#operationOutcome(
609
+ input.runId,
610
+ input.operationId,
611
+ "delivery-bound",
612
+ );
601
613
  if (replay) return replay;
602
614
  const existing = this.#database
603
615
  .prepare(
@@ -911,7 +923,11 @@ export class RunStore {
911
923
  if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
912
924
  this.#assertLease(input.lease);
913
925
  }
914
- const replay = this.#operationOutcome(input.runId, input.operationId);
926
+ const replay = this.#operationOutcome(
927
+ input.runId,
928
+ input.operationId,
929
+ "state-transitioned",
930
+ );
915
931
  if (replay) return replay;
916
932
  const current = this.status(input.runId);
917
933
  const projection = this.#appendEvent(input.runId, "state-transitioned", {