@kontextmind/kxm 0.7.128 → 0.7.131

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 (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/README.md +4 -1
  3. package/.kxm/agents/coordinator.yaml +1 -0
  4. package/.kxm/agents/critic-arch.yaml +1 -4
  5. package/.kxm/agents/critic-cli.yaml +1 -4
  6. package/.kxm/agents/implementer.yaml +1 -4
  7. package/.kxm/workflows/implement-only.yaml +1 -1
  8. package/.kxm/workflows/review-arch-only.yaml +1 -1
  9. package/.kxm/workflows/review-cli-only.yaml +1 -1
  10. package/CHANGELOG.md +42 -1
  11. package/docs/architecture/inventory.md +1 -1
  12. package/docs/concepts/architecture.md +1 -1
  13. package/docs/concepts/data-and-storage.md +1 -1
  14. package/docs/contracts/README.md +1 -1
  15. package/docs/contracts/migration.md +12 -8
  16. package/docs/contracts/routing.md +3 -3
  17. package/docs/contributing/assignment-runner.md +14 -15
  18. package/docs/contributing/development.md +22 -6
  19. package/docs/contributing/harness-routing-internals.md +8 -8
  20. package/docs/glossary.md +1 -1
  21. package/docs/guides/agent-skills.md +1 -1
  22. package/docs/operations/backup-and-restore.md +7 -7
  23. package/docs/operations/upgrade.md +2 -2
  24. package/docs/reference/cli-reference.md +37 -14
  25. package/docs/reference/config-reference.md +81 -155
  26. package/docs/reference/harness-routing.md +29 -21
  27. package/docs/reference/workflow-catalog.md +4 -4
  28. package/docs/start/first-workflow.md +1 -1
  29. package/examples/project/.kxm/agents/coordinator.yaml +1 -2
  30. package/examples/project/.kxm/agents/critic-1.yaml +1 -3
  31. package/examples/project/.kxm/agents/critic-2.yaml +1 -2
  32. package/examples/project/.kxm/agents/critic-3.yaml +2 -3
  33. package/examples/project/.kxm/agents/implementer.yaml +1 -2
  34. package/examples/project/.kxm/agents/planner.yaml +1 -2
  35. package/examples/project/.kxm/agents/reproducer.yaml +1 -2
  36. package/examples/project/.kxm/agents/reviewer.yaml +1 -2
  37. package/examples/project/.kxm/workflows/fix.yaml +0 -12
  38. package/package.json +8 -8
  39. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  40. package/plugins/kxm/dist/cli.js +2565 -465
  41. package/plugins/kxm/dist/mcp-server.js +1 -1
  42. package/plugins/kxm/dist/runtime-supervisor.js +683 -348
  43. package/plugins/kxm/dist/runtime.js +694 -358
  44. package/plugins/kxm/dist/server.js +17 -14
  45. package/plugins/kxm/package.json +1 -1
  46. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  47. package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
  48. package/plugins/kxm/src/cli/lanes.ts +109 -4
  49. package/plugins/kxm/src/cli/project.ts +6 -6
  50. package/plugins/kxm/src/database.ts +25 -19
  51. package/plugins/kxm/src/engine.ts +168 -163
  52. package/plugins/kxm/src/init-guide-setup.ts +75 -15
  53. package/plugins/kxm/src/mcp-server.ts +1 -1
  54. package/plugins/kxm/src/oneshot-producer.ts +3 -15
  55. package/plugins/kxm/src/project-config.ts +83 -70
  56. package/plugins/kxm/src/routes.ts +26 -18
  57. package/plugins/kxm/src/runtime-service.ts +1 -0
  58. package/plugins/kxm/src/runtime-store.ts +390 -77
  59. package/plugins/kxm/src/runtime-supervisor.ts +87 -21
  60. package/plugins/kxm/src/suggest.ts +2 -2
  61. package/plugins/kxm/src/template.ts +2 -14
  62. package/schemas/agent.schema.json +8 -0
  63. package/scripts/roster-policy.d.mts +2 -1
  64. package/scripts/roster-policy.mjs +119 -22
  65. package/scripts/run-bounded.mjs +53 -0
@@ -19567,22 +19567,25 @@ function ensureWalJournalMode(database, file, description, timeoutMs = 5e3) {
19567
19567
  Atomics.wait(sleeper, 0, 0, 10);
19568
19568
  }
19569
19569
  }
19570
+ function assertDatabaseFile(file, description) {
19571
+ checkedParent(file, description);
19572
+ const stat = lstatSync2(file, { throwIfNoEntry: false });
19573
+ if (stat) {
19574
+ if (stat.isSymbolicLink() || !stat.isFile()) {
19575
+ throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
19576
+ }
19577
+ }
19578
+ for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
19579
+ const info = lstatSync2(sidecar, { throwIfNoEntry: false });
19580
+ if (info?.isSymbolicLink()) {
19581
+ throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
19582
+ }
19583
+ }
19584
+ }
19570
19585
  function openDatabase(file, description, spec) {
19571
19586
  const isMemory = file === ":memory:";
19572
19587
  if (!isMemory) {
19573
- checkedParent(file, description);
19574
- const stat = lstatSync2(file, { throwIfNoEntry: false });
19575
- if (stat) {
19576
- if (stat.isSymbolicLink() || !stat.isFile()) {
19577
- throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
19578
- }
19579
- }
19580
- for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
19581
- const info = lstatSync2(sidecar, { throwIfNoEntry: false });
19582
- if (info?.isSymbolicLink()) {
19583
- throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
19584
- }
19585
- }
19588
+ assertDatabaseFile(file, description);
19586
19589
  }
19587
19590
  const database = new DatabaseSync(file);
19588
19591
  let transaction = false;
@@ -19611,7 +19614,7 @@ function openDatabase(file, description, spec) {
19611
19614
  throw databaseError(
19612
19615
  "runtime_schema_outdated",
19613
19616
  file,
19614
- `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for registry/event stores); \`kxm init\` is project-only and rebuilds no database \u2014 upgrading old state in place is deliberately unsupported`
19617
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`
19615
19618
  );
19616
19619
  }
19617
19620
  if (spec.tables) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.128",
3
+ "version": "0.7.131",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,7 +7,7 @@ description: Entry point for KXM, the kxm CLI and the Claude Code plugin kxm_* M
7
7
 
8
8
  Pick the skill that owns the request, then follow it. These bundled skills
9
9
  document the current CLI. They do not switch runtime YAML authority, admit
10
- writers, or replace `.kxm/roster.yaml` trusted policy.
10
+ writers, or replace the trusted role and model policy.
11
11
 
12
12
  ## Route by request
13
13
 
@@ -10,6 +10,14 @@ references skills, constrains tools, and declares harness/model choices.
10
10
  A model appearing in a roster is not proof of authentication, capability,
11
11
  or permission to execute an assignment.
12
12
 
13
+ Dispatch reads `.kxm/roles/*.yaml` and `.kxm/models/*.yaml`. An agent binds
14
+ a role with `role:` and does not pin `harness` or `model`. An agent
15
+ `tools.preset` must be a registered preset. Narrowing that preset against
16
+ the role preset is not validated yet (P3). `kxm routes` prints
17
+ admission from `.kxm/routes.yaml` and membership from the role files.
18
+ The developer runner builds its policy object from those same files at
19
+ `refs/remotes/origin/main`.
20
+
13
21
  ## Inspect Before Editing
14
22
 
15
23
  ```bash
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
- import { discoverKxmProjectRoot, kxmResourceIdentifier } from "../project-config.ts";
5
- import { attachKxmSupervisor, ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
4
+ import { discoverKxmProjectRoot, kxmResourceIdentifier, loadKxmProject } from "../project-config.ts";
5
+ import { KxmRunEventStore, KxmRuntimeRegistry, kxmProjectRunEventsPath, kxmRuntimePaths } from "../runtime-store.ts";
6
+ import { attachKxmSupervisor, ensureKxmSupervisor, kxmRuntimeRequest, kxmSupervisorRecordIsLive } from "../runtime-supervisor.ts";
6
7
  import { print, printPlan, workspaceDirs, type Runtime } from "./types.ts";
7
8
 
8
9
  /** One worktree lane recorded in the control checkout. The sha is the base,
@@ -33,6 +34,7 @@ const LANE_SCHEMA = "kxm.lanes.v1";
33
34
  const SHA_RE = /^[a-f0-9]{40,64}$/;
34
35
  const TIMESTAMP_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/;
35
36
  const SETTLED_RUN = new Set(["completed", "failed", "cancelled"]);
37
+ const LANE_RUN_LIST_LIMIT = 10_000;
36
38
 
37
39
  function lanesFile(runtime: Runtime): string {
38
40
  return join(runtime.dirs.state, "lanes.json");
@@ -316,6 +318,65 @@ function isSettled(status: string): boolean {
316
318
  return SETTLED_RUN.has(status);
317
319
  }
318
320
 
321
+ /** Runs in the lane event store. Undefined when the lane has no store and no supervisor list. */
322
+ async function laneStoreRuns(runtime: Runtime, lanePath: string): Promise<Array<{ runId: string; status: string }> | undefined> {
323
+ const project = await import("./project.ts");
324
+ const request = project.kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest;
325
+ const handle = await attachKxmSupervisor({ env: runtime.env });
326
+ const projectId = (): string => String(loadKxmProject(lanePath, {}).project.value.id);
327
+ if (handle) {
328
+ try {
329
+ const result = await request(
330
+ handle,
331
+ "GET",
332
+ `/v1/projects/${encodeURIComponent(projectId())}/runs?projectRoot=${encodeURIComponent(lanePath)}&limit=${LANE_RUN_LIST_LIMIT}`,
333
+ );
334
+ const runs = Array.isArray(result.runs) ? result.runs : [];
335
+ return runs.flatMap((run) => {
336
+ if (!run || typeof run !== "object" || Array.isArray(run)) return [];
337
+ const record = run as { runId?: unknown; status?: unknown };
338
+ if (typeof record.runId !== "string" || record.runId.length === 0) return [];
339
+ const status = typeof record.status === "string" && record.status.length > 0 ? record.status : "unknown";
340
+ return [{ runId: record.runId, status }];
341
+ });
342
+ } catch {
343
+ // The supervisor could not list this root. The file read below still can.
344
+ }
345
+ }
346
+ const eventsPath = kxmProjectRunEventsPath(lanePath, runtime.env);
347
+ if (!existsSync(eventsPath)) return undefined;
348
+ const store = new KxmRunEventStore(eventsPath);
349
+ try {
350
+ return store.runsForProject(projectId(), LANE_RUN_LIST_LIMIT).map((run) => ({ runId: run.runId, status: run.status }));
351
+ } finally {
352
+ store.close();
353
+ }
354
+ }
355
+
356
+ async function refuseIfLaneStoreRunsOpen(runtime: Runtime, unit: string, command: string): Promise<number | undefined> {
357
+ const loaded = loadLanes(runtime, command);
358
+ if (typeof loaded === "number") return loaded;
359
+ const lane = loaded.lanes[unit];
360
+ if (!lane) return undefined;
361
+ let runs: Array<{ runId: string; status: string }> | undefined;
362
+ try {
363
+ runs = await laneStoreRuns(runtime, lane.path);
364
+ } catch (error) {
365
+ const detail = error instanceof Error ? error.message : "run list failed";
366
+ return refuse(runtime, command, "lane_run_open", `lane ${unit} runs could not be read: ${detail.slice(0, 200)}`, { unit });
367
+ }
368
+ if (!runs) return undefined;
369
+ const unsettled = runs.filter((run) => !isSettled(run.status)).map((run) => run.runId);
370
+ if (unsettled.length === 0) return undefined;
371
+ return refuse(
372
+ runtime,
373
+ command,
374
+ "lane_run_open",
375
+ `lane ${unit} run ${unsettled.join(", ")} is unsettled`,
376
+ { unit, runIds: unsettled },
377
+ );
378
+ }
379
+
319
380
  export async function refuseIfLaneRunOpen(runtime: Runtime, unit: string, command: string): Promise<number | undefined> {
320
381
  const loaded = loadLanes(runtime, command);
321
382
  if (typeof loaded === "number") return loaded;
@@ -431,12 +492,49 @@ export async function cmdLaneStatus(runtime: Runtime, unit: string): Promise<num
431
492
  print(
432
493
  runtime.io,
433
494
  runtime.json,
434
- { ok: true, command, lane: { ...view, status: runStatus } },
435
- `${formatLaneLine(view)} status=${runStatus}`,
495
+ { ok: true, command, root: record.path, lane: { ...view, status: runStatus } },
496
+ `${formatLaneLine(view)} status=${runStatus} root=${record.path}`,
436
497
  );
437
498
  return 0;
438
499
  }
439
500
 
501
+ /** Remove the lane root from the Runtime registry. A stopped supervisor is not
502
+ * an error: the row is deleted from the registry file when that file exists.
503
+ * The delete holds the write lock and rechecks the supervisor row; a live
504
+ * supervisor is left for the unregister route. */
505
+ async function unregisterLaneRoot(runtime: Runtime, projectRoot: string, force: boolean): Promise<string | undefined> {
506
+ const body = force ? { projectRoot, force: true as const } : { projectRoot };
507
+ try {
508
+ const project = await import("./project.ts");
509
+ const request = project.kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest;
510
+ const post = async (handle: { runtimeId: string; port: number; token: string; started: boolean }): Promise<void> => {
511
+ await request(handle, "POST", "/v1/projects/unregister", body);
512
+ };
513
+ const handle = await attachKxmSupervisor({ env: runtime.env });
514
+ if (handle) {
515
+ await post(handle);
516
+ return undefined;
517
+ }
518
+ const paths = kxmRuntimePaths({ env: runtime.env });
519
+ if (!existsSync(paths.registryDb)) return undefined;
520
+ const registry = new KxmRuntimeRegistry(paths.registryDb);
521
+ let outcome: "removed" | "absent" | "supervisor_live";
522
+ try {
523
+ outcome = registry.unregisterProjectIfIdle(projectRoot, kxmSupervisorRecordIsLive);
524
+ } finally {
525
+ registry.close();
526
+ }
527
+ if (outcome === "supervisor_live") {
528
+ const live = await attachKxmSupervisor({ env: runtime.env });
529
+ if (!live) return "runtime supervisor is live but could not be reached";
530
+ await post(live);
531
+ }
532
+ return undefined;
533
+ } catch (error) {
534
+ return error instanceof Error ? error.message : "lane root could not be unregistered";
535
+ }
536
+ }
537
+
440
538
  export async function cmdLaneDrop(runtime: Runtime, unit: string, options: { force?: boolean } = {}): Promise<number> {
441
539
  const command = "lane drop";
442
540
  const invalid = refuseUnit(runtime, command, unit);
@@ -458,16 +556,23 @@ export async function cmdLaneDrop(runtime: Runtime, unit: string, options: { for
458
556
  return refuse(runtime, command, "lane_dirty", `lane ${unit} has ${dirty} uncommitted change(s)`, { unit, dirty });
459
557
  }
460
558
  }
559
+ const listed = await refuseIfLaneStoreRunsOpen(runtime, unit, command);
560
+ if (listed !== undefined) return listed;
461
561
  const open = await refuseIfLaneRunOpen(runtime, unit, command);
462
562
  if (open !== undefined) return open;
463
563
  }
464
564
  if (runtime.dryRun) {
465
565
  printPlan(runtime, { command, unit, branchKept: record.branch }, [
566
+ { action: "delete", target: `runtime registry ${record.path}` },
466
567
  { action: "delete", target: record.path },
467
568
  { action: "write", target: lanesFile(runtime) },
468
569
  ], `drop lane ${unit}; branch ${record.branch} is not deleted`);
469
570
  return 0;
470
571
  }
572
+ const unregistered = await unregisterLaneRoot(runtime, record.path, force);
573
+ if (unregistered !== undefined) {
574
+ return refuse(runtime, command, "lane_unregister_failed", `lane ${unit} could not be unregistered: ${unregistered.slice(0, 300)}`, { unit });
575
+ }
471
576
  if (existsSync(record.path)) {
472
577
  const removed = git(root, ["worktree", "remove", ...(force ? ["--force"] : []), record.path]);
473
578
  if (removed.status !== 0) {
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
6
6
  import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore, type RestorePlan } from "../database.ts";
7
7
  import { readLiveHubClaim } from "../hub-autostart.ts";
8
8
  import { refreshModelInventory } from "../model-inventory.ts";
9
- import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
9
+ import { listInventoryModels, listRoleBindings, loadRoutePolicy, rolesForInventoryModel, setRouteState, updateRouteState } from "../routes.ts";
10
10
  import {
11
11
  KxmConfigError,
12
12
  discoverKxmProjectRoot,
@@ -600,8 +600,8 @@ export async function cmdKxmRunStatus(runtime: Runtime, runId: string): Promise<
600
600
  print(
601
601
  runtime.io,
602
602
  runtime.json,
603
- { ok: true, command: "runs status", run, ...(drive !== undefined ? { drive } : {}) },
604
- `${formatRunStatusLine(run, drive)}${driveLine ? `\n${driveLine}` : ""}`,
603
+ { ok: true, command: "runs status", projectRoot, run, ...(drive !== undefined ? { drive } : {}) },
604
+ `${formatRunStatusLine(run, drive)}\nroot ${projectRoot}${driveLine ? `\n${driveLine}` : ""}`,
605
605
  );
606
606
  return 0;
607
607
  } catch (error) {
@@ -889,8 +889,7 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
889
889
  try {
890
890
  while (true) {
891
891
  const policy = loadRoutePolicy(runtime.dirs.workdir);
892
- const roleBindings = listRoleBindings(runtime.dirs.workdir);
893
- runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
892
+ runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${rolesForInventoryModel(runtime.dirs.workdir, m).join(",") || "-"}`).join("\n") + "\n");
894
893
  const command = (await ask("[a]dmit [d]isable [r]ole-add [x]ole-remove [q]uit: ")).trim().toLowerCase();
895
894
  if (command === "q" || command === "quit") return 0;
896
895
  const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
@@ -910,7 +909,8 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
910
909
 
911
910
  export async function cmdRouteList(runtime: Runtime): Promise<number> {
912
911
  const policy = loadRoutePolicy(runtime.dirs.workdir);
913
- print(runtime.io, runtime.json, { ok: true, command: "routes list", policy }, [...policy.admitted.map((x) => `admitted ${x}`), ...policy.disabled.map((x) => `disabled ${x}`)].join("\n") || "no route decisions");
912
+ const membership = Object.entries(listRoleBindings(runtime.dirs.workdir)).flatMap(([role, ids]) => ids.map((id) => `${role} ${id}`));
913
+ print(runtime.io, runtime.json, { ok: true, command: "routes list", policy, membership }, [...policy.admitted.map((x) => `admitted ${x}`), ...policy.disabled.map((x) => `disabled ${x}`), ...membership].join("\n") || "no route decisions");
914
914
  return 0;
915
915
  }
916
916
 
@@ -154,22 +154,27 @@ export function ensureWalJournalMode(database: DatabaseSync, file: string, descr
154
154
  }
155
155
  }
156
156
 
157
+ /** Parent, file, and WAL sidecar must be regular filesystem objects. A symlink is refused before the file is opened. */
158
+ export function assertDatabaseFile(file: string, description: string): void {
159
+ checkedParent(file, description);
160
+ const stat = lstatSync(file, { throwIfNoEntry: false });
161
+ if (stat) {
162
+ if (stat.isSymbolicLink() || !stat.isFile()) {
163
+ throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
164
+ }
165
+ }
166
+ for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
167
+ const info = lstatSync(sidecar, { throwIfNoEntry: false });
168
+ if (info?.isSymbolicLink()) {
169
+ throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
170
+ }
171
+ }
172
+ }
173
+
157
174
  export function openDatabase(file: string, description: string, spec: DatabaseSchemaSpec): DatabaseSync {
158
175
  const isMemory = file === ":memory:";
159
176
  if (!isMemory) {
160
- checkedParent(file, description);
161
- const stat = lstatSync(file, { throwIfNoEntry: false });
162
- if (stat) {
163
- if (stat.isSymbolicLink() || !stat.isFile()) {
164
- throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
165
- }
166
- }
167
- for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
168
- const info = lstatSync(sidecar, { throwIfNoEntry: false });
169
- if (info?.isSymbolicLink()) {
170
- throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
171
- }
172
- }
177
+ assertDatabaseFile(file, description);
173
178
  }
174
179
 
175
180
  const database = new DatabaseSync(file);
@@ -199,14 +204,15 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
199
204
  database.exec(spec.schema);
200
205
  database.exec(`PRAGMA user_version = ${spec.version}`);
201
206
  } else if (version < spec.version) {
202
- // No migration lanes. This is a single-operator tool: an older database is
203
- // re-initialised, not upgraded in place, and the code never carries two schema
204
- // shapes at once. Silently accepting an older file would mean every query has to
205
- // work against shapes it no longer tests.
207
+ // No migration lanes, except the Runtime registry copy from schema 1 to 2
208
+ // (migrateRegistryLanes). This is a single-operator tool: every other older
209
+ // database is re-initialised, not upgraded in place, and the code never carries
210
+ // two schema shapes at once. Silently accepting an older file would mean every
211
+ // query has to work against shapes it no longer tests.
206
212
  throw databaseError(
207
213
  "runtime_schema_outdated",
208
214
  file,
209
- `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for registry/event stores); \`kxm init\` is project-only and rebuilds no database — upgrading old state in place is deliberately unsupported`,
215
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`,
210
216
  );
211
217
  }
212
218
 
@@ -628,7 +634,7 @@ export function restoreDatabaseFile(
628
634
  */
629
635
  export const KXM_BACKUP_CEILINGS = {
630
636
  "hub-store": 5,
631
- registry: 1,
637
+ registry: 2,
632
638
  "binding-store": 1,
633
639
  events: 7,
634
640
  } as const;