@kontextmind/kxm 0.7.52 → 0.7.54

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.
@@ -11504,20 +11504,11 @@ function openDatabase(file, description, spec) {
11504
11504
  database.exec(spec.schema);
11505
11505
  database.exec(`PRAGMA user_version = ${spec.version}`);
11506
11506
  } else if (version < spec.version) {
11507
- let currentVersion = version;
11508
- while (currentVersion < spec.version) {
11509
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
11510
- if (!step) {
11511
- throw databaseError(
11512
- "runtime_schema_outdated",
11513
- file,
11514
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
11515
- );
11516
- }
11517
- step.migrate(database);
11518
- currentVersion = step.toVersion;
11519
- database.exec(`PRAGMA user_version = ${currentVersion}`);
11520
- }
11507
+ throw databaseError(
11508
+ "runtime_schema_outdated",
11509
+ file,
11510
+ `${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`
11511
+ );
11521
11512
  }
11522
11513
  if (spec.tables) {
11523
11514
  verifyExpectedTables(database, file, description, spec.tables);
@@ -11683,100 +11674,10 @@ var HUB_STORE_SCHEMA_V3 = `
11683
11674
  ) STRICT;
11684
11675
  CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
11685
11676
  `;
11686
- var HUB_STORE_MIGRATIONS = Object.freeze([
11687
- {
11688
- fromVersion: 1,
11689
- toVersion: 2,
11690
- migrate: (db) => {
11691
- db.exec(`
11692
- CREATE TABLE IF NOT EXISTS agents (
11693
- id TEXT PRIMARY KEY,
11694
- record TEXT NOT NULL
11695
- ) STRICT;
11696
- CREATE TABLE IF NOT EXISTS messages (
11697
- id TEXT PRIMARY KEY,
11698
- record TEXT NOT NULL
11699
- ) STRICT;
11700
- CREATE TABLE IF NOT EXISTS workflow_runs (
11701
- id TEXT PRIMARY KEY,
11702
- definition_id TEXT NOT NULL,
11703
- delivery_id TEXT NOT NULL,
11704
- record TEXT NOT NULL,
11705
- UNIQUE(definition_id, delivery_id)
11706
- ) STRICT;
11707
- CREATE TABLE IF NOT EXISTS workflow_journal (
11708
- id TEXT PRIMARY KEY,
11709
- run_id TEXT NOT NULL,
11710
- category TEXT NOT NULL,
11711
- area TEXT NOT NULL,
11712
- record TEXT NOT NULL
11713
- ) STRICT;
11714
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
11715
- `);
11716
- }
11717
- },
11718
- {
11719
- fromVersion: 2,
11720
- toVersion: 3,
11721
- migrate: (db) => {
11722
- db.exec(`
11723
- CREATE TABLE IF NOT EXISTS agents (
11724
- id TEXT PRIMARY KEY,
11725
- record TEXT NOT NULL
11726
- ) STRICT;
11727
- CREATE TABLE IF NOT EXISTS messages (
11728
- id TEXT PRIMARY KEY,
11729
- record TEXT NOT NULL
11730
- ) STRICT;
11731
- CREATE TABLE IF NOT EXISTS workflow_runs (
11732
- id TEXT PRIMARY KEY,
11733
- definition_id TEXT NOT NULL,
11734
- delivery_id TEXT NOT NULL,
11735
- record TEXT NOT NULL,
11736
- UNIQUE(definition_id, delivery_id)
11737
- ) STRICT;
11738
- CREATE TABLE IF NOT EXISTS workflow_journal (
11739
- id TEXT PRIMARY KEY,
11740
- run_id TEXT NOT NULL,
11741
- category TEXT NOT NULL,
11742
- area TEXT NOT NULL,
11743
- record TEXT NOT NULL
11744
- ) STRICT;
11745
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
11746
- CREATE TABLE IF NOT EXISTS context_items (
11747
- id TEXT PRIMARY KEY,
11748
- project TEXT NOT NULL,
11749
- kind TEXT NOT NULL,
11750
- record TEXT NOT NULL
11751
- ) STRICT;
11752
- CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
11753
- CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
11754
- ON messages(
11755
- json_extract(record, '$.from'),
11756
- json_extract(record, '$.idempotencyKey')
11757
- ) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
11758
- CREATE INDEX IF NOT EXISTS messages_to_seq
11759
- ON messages(
11760
- json_extract(record, '$.to'),
11761
- COALESCE(json_extract(record, '$.seq'), 0)
11762
- );
11763
- CREATE TABLE IF NOT EXISTS consumer_cursors (
11764
- agent_id TEXT PRIMARY KEY,
11765
- cursor INTEGER NOT NULL
11766
- ) STRICT;
11767
- CREATE TABLE IF NOT EXISTS agent_sequences (
11768
- agent_id TEXT PRIMARY KEY,
11769
- next_seq INTEGER NOT NULL
11770
- ) STRICT;
11771
- `);
11772
- }
11773
- }
11774
- ]);
11775
11677
  var HUB_STORE_SCHEMA_SPEC = Object.freeze({
11776
11678
  schema: HUB_STORE_SCHEMA_V3,
11777
11679
  version: HUB_STORE_SCHEMA_VERSION,
11778
- tables: HUB_STORE_TABLES,
11779
- migrations: HUB_STORE_MIGRATIONS
11680
+ tables: HUB_STORE_TABLES
11780
11681
  });
11781
11682
  var MessageMap = class extends Map {
11782
11683
  store;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.52",
3
+ "version": "0.7.54",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,7 +14,7 @@ authority, admit writers, or replace `.kxm/roster.yaml` trusted policy.
14
14
 
15
15
  The KXM Agent Skills suite is organized by functional areas:
16
16
 
17
- - **Project Setup**: Use `kxm-project-setup` for `init`, `migrate`, `trust`, `config`, `completion`
17
+ - **Project Setup**: Use `kxm-project-setup` for `init`, `trust`, `config`, `completion`
18
18
  - **Harness & Auth**: Use `kxm-harness-auth` for `harness`, `auth`, `update`, `runtime`, `agent`
19
19
  - **Hub Operations**: Use `kxm-hub-ops` for `hub`, `backup`, `restore`
20
20
  - **Session Management**: Use `kxm-session` for `session`, `dash`, `studio`
@@ -1,21 +1,19 @@
1
1
  ---
2
2
  name: kxm-project-setup
3
- description: Initialize, migrate, review permission changes, configure, and add shell completion for KXM projects.
3
+ description: Initialize, review permission changes, configure, and add shell completion for KXM projects.
4
4
  ---
5
5
 
6
6
  # KXM Project Setup
7
7
 
8
8
  Use the current CLI. Inspect `kxm <command> --help` before mutations. Do not
9
- invent `force`, domain-trust, or extra migrate verbs.
9
+ invent `force`, domain-trust, or legacy-migration verbs: this build converts
10
+ nothing.
10
11
 
11
12
  ## Commands
12
13
 
13
14
  | Command | Purpose | Options / arguments |
14
15
  |---|---|---|
15
- | `kxm init` | Create, validate, or plan migration of a KXM project | `--json`, `--dry-run`, `--name`, `--project-id`, `--repository <id=absolute-path>` |
16
- | `kxm migrate plan` | Compute the legacy-to-KXM plan without writes | `--json` |
17
- | `kxm migrate apply` | Install a reviewed migration with a hash-linked receipt | `--decisions <file>`, `--project-id`, `--name` |
18
- | `kxm migrate verify` | Verify a migration receipt | `--json` |
16
+ | `kxm init` | Create, validate, repair, or join a KXM project (a legacy tree is reported as `mode: "legacy"` and never converted) | `--json`, `--dry-run`, `--name`, `--project-id`, `--repository <id=absolute-path>` |
19
17
  | `kxm trust diff` | Structured permission diff against a Git revision | `--base <revision>` |
20
18
  | `kxm trust check` | Fail when the working tree expands permissions | `--base <revision>` |
21
19
  | `kxm config get <key>` | Get a configuration value | `--json` |
@@ -25,7 +23,6 @@ invent `force`, domain-trust, or extra migrate verbs.
25
23
 
26
24
  ```bash
27
25
  kxm init --dry-run --json
28
- kxm migrate plan --json
29
26
  kxm trust diff --base HEAD --json
30
27
  kxm config list --json
31
28
  kxm completion install
@@ -2,7 +2,6 @@ export type SupportedShell = "bash" | "zsh" | "fish";
2
2
 
3
3
  const TOP_LEVEL_COMMANDS = [
4
4
  "init",
5
- "migrate",
6
5
  "backup",
7
6
  "restore",
8
7
  "run",
@@ -36,7 +35,6 @@ const TOP_LEVEL_COMMANDS = [
36
35
 
37
36
  const SUBCOMMANDS: Record<string, string[]> = {
38
37
  runs: ["status", "cancel", "list"],
39
- migrate: ["plan", "apply", "verify"],
40
38
  harness: ["list"],
41
39
  runtime: ["start", "status", "stop"],
42
40
  trust: ["diff", "check"],
@@ -132,7 +130,6 @@ _kxm() {
132
130
  local -a commands
133
131
  commands=(
134
132
  'init:Create or validate a KXM project'
135
- 'migrate:Plan, apply, and verify legacy configuration migration'
136
133
  'backup:Create a verified SQLite backup manifest'
137
134
  'restore:Restore SQLite stores from a backup manifest'
138
135
  'run:Create a KXM workflow run'
@@ -12,7 +12,6 @@ import {
12
12
  type KxmInitializationPlan,
13
13
  } from "../project-config.ts";
14
14
  import { initializeKxmProject } from "../init.ts";
15
- import { applyKxmMigration, planKxmMigration, verifyKxmMigration } from "../migrate.ts";
16
15
  import { diffKxmProjectAgainstRevision, formatKxmPermissionDiff } from "../permission.ts";
17
16
  import { readKxmLocalBindings, kxmUserStateRoot } from "../bindings.ts";
18
17
  import { loadKxmProject } from "../project-config.ts";
@@ -151,8 +150,8 @@ export async function cmdKxmInit(
151
150
  if (runtime.dryRun) {
152
151
  return finishInit(0, `init plan: ${initialized.plan.mode}`);
153
152
  }
154
- const next = initialized.plan.mode === "migrate"
155
- ? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
153
+ const next = initialized.plan.mode === "legacy"
154
+ ? "legacy state is not migrated by this build: initialise a fresh project directory and copy the YAML definitions you want to keep"
156
155
  : initialized.repairPlan?.issues.length
157
156
  ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
158
157
  : "partial or provenance-free KXM state requires explicit repair; no files were overwritten";
@@ -176,136 +175,6 @@ export async function cmdKxmInit(
176
175
  }
177
176
  }
178
177
 
179
- export async function cmdKxmMigratePlan(runtime: Runtime): Promise<number> {
180
- if (runtime.workspaceFlag !== undefined) {
181
- print(runtime.io, runtime.json, {
182
- ok: false,
183
- command: "migrate plan",
184
- error: "workspace_option_unsupported",
185
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
186
- return 2;
187
- }
188
- try {
189
- const result = planKxmMigration(runtime.cwd, {});
190
- const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
191
- const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
192
- const payload = {
193
- ok: result.plan.canApply === true,
194
- command: "migrate plan",
195
- plan: result.plan,
196
- plannedOnly: result.plan.canApply !== true,
197
- };
198
- if (result.plan.canApply === true) {
199
- print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
200
- return 0;
201
- }
202
- print(
203
- runtime.io,
204
- runtime.json,
205
- payload,
206
- `migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
207
- );
208
- return 1;
209
- } catch (error) {
210
- if (error instanceof KxmConfigError) {
211
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
212
- return 1;
213
- }
214
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_io_failed" }, "migration plan failed because a local filesystem operation did not complete");
215
- return 1;
216
- }
217
- }
218
-
219
- export async function cmdKxmMigrateApply(runtime: Runtime, options: { decisions?: string | undefined; projectId?: string | undefined; name?: string | undefined }): Promise<number> {
220
- if (runtime.workspaceFlag !== undefined) {
221
- print(runtime.io, runtime.json, {
222
- ok: false,
223
- command: "migrate apply",
224
- error: "workspace_option_unsupported",
225
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
226
- return 2;
227
- }
228
- try {
229
- const result = applyKxmMigration(runtime.cwd, {
230
- ...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
231
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
232
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
233
- localStateRoot: kxmUserStateRoot({ env: runtime.env }),
234
- dryRun: runtime.dryRun,
235
- });
236
- const payload = {
237
- ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
238
- command: "migrate apply",
239
- action: result.action,
240
- files: result.files,
241
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
242
- ...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
243
- plannedOnly: result.action === "planned",
244
- };
245
- if (result.action === "applied") {
246
- print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
247
- return 0;
248
- }
249
- if (result.action === "already-migrated") {
250
- print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
251
- return 0;
252
- }
253
- if (runtime.dryRun && result.plan?.canApply === true) {
254
- print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
255
- return 0;
256
- }
257
- const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
258
- print(
259
- runtime.io,
260
- runtime.json,
261
- { ...payload, plan: result.plan },
262
- `migration blocked by ${ambiguities.length} unresolved decision(s); review 'kxm migrate plan' and pass --decisions:\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
263
- );
264
- return 1;
265
- } catch (error) {
266
- if (error instanceof KxmConfigError) {
267
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
268
- return 1;
269
- }
270
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_io_failed" }, "migration apply failed because a local filesystem operation did not complete");
271
- return 1;
272
- }
273
- }
274
-
275
- export async function cmdKxmMigrateVerify(runtime: Runtime): Promise<number> {
276
- if (runtime.workspaceFlag !== undefined) {
277
- print(runtime.io, runtime.json, {
278
- ok: false,
279
- command: "migrate verify",
280
- error: "workspace_option_unsupported",
281
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
282
- return 2;
283
- }
284
- let result: ReturnType<typeof verifyKxmMigration>;
285
- try {
286
- result = verifyKxmMigration(runtime.cwd, {});
287
- } catch (error) {
288
- if (error instanceof KxmConfigError) {
289
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
290
- return 1;
291
- }
292
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_io_failed" }, "migration verification failed because a local filesystem operation did not complete");
293
- return 1;
294
- }
295
- const payload = {
296
- ok: result.ok,
297
- command: "migrate verify",
298
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
299
- issues: result.issues,
300
- };
301
- if (result.ok) {
302
- print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
303
- return 0;
304
- }
305
- print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
306
- return 1;
307
- }
308
-
309
178
  export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined }): Promise<number> {
310
179
  try {
311
180
  const { manifest, outDir } = createBackup({
@@ -7,7 +7,7 @@
7
7
  * - Role & workflow governance (`kxm role`, `kxm workflow`, `kxm gate`, `kxm signal`)
8
8
  * - Task & goal management (`kxm goal`, `kxm task`, `kxm suggest`, `kxm studio`)
9
9
  * - Knowledge, skills, and memory (`kxm context`, `kxm skills`, `kxm memory`)
10
- * - KXM runtime & initialization (`kxm init`, `kxm migrate`, `kxm trust`, `kxm run`, `kxm harness`)
10
+ * - KXM runtime & initialization (`kxm init`, `kxm trust`, `kxm run`, `kxm harness`)
11
11
  * - Hub, workers, and dashboard (`kxm hub`, `kxm worker`, `kxm dash`, `kxm session`, `kxm auth`)
12
12
  * - System, update, and configuration (`kxm update`, `kxm config`, `kxm completion`, `kxm improve`)
13
13
  */
@@ -98,10 +98,7 @@ import {
98
98
 
99
99
  import {
100
100
  cmdKxmInit,
101
- cmdKxmMigratePlan,
102
- cmdKxmMigrateApply,
103
- cmdKxmMigrateVerify,
104
- cmdBackup,
101
+ cmdBackup,
105
102
  cmdRestore,
106
103
  cmdKxmTrust,
107
104
  cmdKxmRun,
@@ -334,7 +331,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
334
331
  .helpCommand("help", "Show help");
335
332
  addGlobalOptions(program);
336
333
 
337
- program.command("init").description("Create, validate, or plan migration of a KXM project")
334
+ program.command("init").description("Create, validate, repair, or join a KXM project")
338
335
  .option("--json", "Print machine-readable JSON")
339
336
  .option("--dry-run", "Plan without making changes")
340
337
  .option("--name <name>", "Project display name for a new project")
@@ -347,24 +344,6 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
347
344
  });
348
345
  });
349
346
 
350
- const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration"));
351
- migrate.helpCommand("help", "Show migrate help");
352
- addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-KXM migration plan without writes"))
353
- .action(async function migratePlanAction(this: Command) {
354
- result.code = await cmdKxmMigratePlan(runtimeFrom(ctx, this));
355
- });
356
- addGlobalOptions(migrate.command("apply").description("Install a reviewed migration with a hash-linked receipt"))
357
- .option("--decisions <file>", "Reviewed kxm.migration-decision.v1 YAML file")
358
- .option("--project-id <id>", "Stable project ID for controlled provisioning")
359
- .option("--name <name>", "Project display name")
360
- .action(async function migrateApplyAction(this: Command, options: { decisions?: string; projectId?: string; name?: string }) {
361
- result.code = await cmdKxmMigrateApply(runtimeFrom(ctx, this), options);
362
- });
363
- addGlobalOptions(migrate.command("verify").description("Verify a migration receipt against current sources and target bundle"))
364
- .action(async function migrateVerifyAction(this: Command) {
365
- result.code = await cmdKxmMigrateVerify(runtimeFrom(ctx, this));
366
- });
367
-
368
347
  addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of all stores with a hashed manifest"))
369
348
  .option("--out <dir>", "Directory to write backup and manifest")
370
349
  .action(async function backupAction(this: Command, options: { out?: string }) {
@@ -14,17 +14,10 @@ import { basename, dirname, join, resolve } from "node:path";
14
14
  import { DatabaseSync } from "./sqlite.ts";
15
15
  import { KxmConfigError, type KxmConfigIssue } from "./project-config.ts";
16
16
 
17
- export interface DatabaseMigrationStep {
18
- fromVersion: number;
19
- toVersion: number;
20
- migrate: (database: DatabaseSync) => void;
21
- }
22
-
23
17
  export interface DatabaseSchemaSpec {
24
18
  schema: string;
25
19
  version: number;
26
20
  tables?: Readonly<Record<string, readonly string[]>>;
27
- migrations?: readonly DatabaseMigrationStep[];
28
21
  timeoutMs?: number;
29
22
  }
30
23
 
@@ -179,20 +172,15 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
179
172
  database.exec(spec.schema);
180
173
  database.exec(`PRAGMA user_version = ${spec.version}`);
181
174
  } else if (version < spec.version) {
182
- let currentVersion = version;
183
- while (currentVersion < spec.version) {
184
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
185
- if (!step) {
186
- throw databaseError(
187
- "runtime_schema_outdated",
188
- file,
189
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`,
190
- );
191
- }
192
- step.migrate(database);
193
- currentVersion = step.toVersion;
194
- database.exec(`PRAGMA user_version = ${currentVersion}`);
195
- }
175
+ // No migration lanes. This is a single-operator tool: an older database is
176
+ // re-initialised, not upgraded in place, and the code never carries two schema
177
+ // shapes at once. Silently accepting an older file would mean every query has to
178
+ // work against shapes it no longer tests.
179
+ throw databaseError(
180
+ "runtime_schema_outdated",
181
+ file,
182
+ `${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`,
183
+ );
196
184
  }
197
185
 
198
186
  if (spec.tables) {
@@ -16,6 +16,7 @@ import {
16
16
  lstatSync,
17
17
  } from "node:fs";
18
18
  import { dirname, join, resolve } from "node:path";
19
+ import { openDatabase } from "./database.ts";
19
20
  import { spawnSync } from "node:child_process";
20
21
 
21
22
  export const EXTERNAL_EFFECT_SCHEMA = "kxm.external-effect-receipt.v1" as const;
@@ -114,6 +115,33 @@ export function computeEffectKey(
114
115
  return `eff_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
115
116
  }
116
117
 
118
+ const EXTERNAL_EFFECTS_SCHEMA_VERSION = 1;
119
+
120
+ const EXTERNAL_EFFECTS_DDL = `
121
+ CREATE TABLE IF NOT EXISTS external_effects (
122
+ effect_key TEXT PRIMARY KEY,
123
+ run_id TEXT NOT NULL,
124
+ step_id TEXT NOT NULL,
125
+ attempt_id TEXT NOT NULL,
126
+ action_kind TEXT NOT NULL,
127
+ target_ref TEXT NOT NULL,
128
+ status TEXT NOT NULL,
129
+ payload_hash TEXT NOT NULL,
130
+ receipt_payload TEXT NOT NULL,
131
+ executed_at TEXT NOT NULL,
132
+ last_heartbeat_at TEXT,
133
+ completed_at TEXT
134
+ );
135
+ CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
136
+ `;
137
+
138
+ const EXTERNAL_EFFECTS_SHAPE = {
139
+ external_effects: [
140
+ "effect_key", "run_id", "step_id", "attempt_id", "action_kind", "target_ref",
141
+ "status", "payload_hash", "receipt_payload", "executed_at", "last_heartbeat_at", "completed_at",
142
+ ],
143
+ } as const;
144
+
117
145
  export class ExternalEffectsLedger {
118
146
  private db: DatabaseSync;
119
147
 
@@ -121,33 +149,15 @@ export class ExternalEffectsLedger {
121
149
  if (dbPath !== ":memory:") {
122
150
  mkdirSync(dirname(dbPath), { recursive: true });
123
151
  }
124
- this.db = new DatabaseSync(dbPath);
125
- this.initSchema();
126
- }
127
-
128
- private initSchema(): void {
129
- this.db.exec(`
130
- CREATE TABLE IF NOT EXISTS external_effects (
131
- effect_key TEXT PRIMARY KEY,
132
- run_id TEXT NOT NULL,
133
- step_id TEXT NOT NULL,
134
- attempt_id TEXT NOT NULL,
135
- action_kind TEXT NOT NULL,
136
- target_ref TEXT NOT NULL,
137
- status TEXT NOT NULL,
138
- payload_hash TEXT NOT NULL,
139
- receipt_payload TEXT NOT NULL,
140
- executed_at TEXT NOT NULL,
141
- last_heartbeat_at TEXT,
142
- completed_at TEXT
143
- );
144
- CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
145
- `);
146
- try {
147
- this.db.exec(`ALTER TABLE external_effects ADD COLUMN last_heartbeat_at TEXT;`);
148
- } catch {
149
- // Column already present in schema
150
- }
152
+ // Goes through the shared opener, not a bare `new DatabaseSync`: that is what gives this
153
+ // store the same contract as every other one — path checks, WAL, the `user_version` gate
154
+ // that refuses an older stamp outright, and the column-shape check that turns a missing
155
+ // column into `runtime_schema_shape_invalid` here instead of a bare SQL error on first use.
156
+ this.db = openDatabase(dbPath, "external effects ledger", {
157
+ schema: EXTERNAL_EFFECTS_DDL,
158
+ version: EXTERNAL_EFFECTS_SCHEMA_VERSION,
159
+ tables: EXTERNAL_EFFECTS_SHAPE,
160
+ });
151
161
  }
152
162
 
153
163
  /**
@@ -207,9 +207,18 @@ function initializeKxmProjectAtGitRoot(
207
207
  const loaderOptions = configOptions(options, repositoryBindings);
208
208
  const transactionOptions = repairOptions(options, repositoryBindings);
209
209
 
210
+ const plan = planKxmInitialization(start, loaderOptions);
211
+ if (plan.mode === "legacy") {
212
+ // Classified **before** any transaction or lock work. A legacy tree used to reach the
213
+ // recovery branch first, so an interrupted create/repair journal made ordinary init
214
+ // acquire the project mutation lock, clean or resume the transaction, and write files
215
+ // while reporting `mode: "legacy"`. Nothing is resumed, cleaned, or written here: the
216
+ // pending journal stays exactly as it is until the legacy inputs are dealt with.
217
+ return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
218
+ }
219
+
210
220
  if (hasKxmInitTransaction(gitRoot)) {
211
221
  const operation = inspectKxmInitTransaction(gitRoot, options.schemasDir);
212
- const plan = planKxmInitialization(start, loaderOptions);
213
222
  if (!operation) {
214
223
  if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, resumePending: true, files: [] };
215
224
  if (!mutationLock) throw new Error("project mutation lock is required to clean an empty transaction");
@@ -267,11 +276,6 @@ function initializeKxmProjectAtGitRoot(
267
276
  }
268
277
  }
269
278
 
270
- const plan = planKxmInitialization(start, loaderOptions);
271
- if (plan.mode === "migrate") {
272
- return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
273
- }
274
-
275
279
  if (plan.mode === "repair") {
276
280
  const projectRoot = plan.projectRoot ?? gitRoot;
277
281
  let templateRepair: KxmTemplateRepairPlan | undefined;
@@ -102,20 +102,15 @@ export function kxmCeilingHash(authority: KxmCoordinatorAuthority): string {
102
102
  }
103
103
 
104
104
  /**
105
- * Does an already-stored coordinator express this ceiling?
105
+ * Does a stored coordinator already express the ceiling being requested?
106
106
  *
107
- * A row written before set normalisation existed carries a fingerprint that
108
- * `kxmCeilingHash` no longer reproduces, so comparing the stored hash alone is
109
- * not enough: recompute over the authority it kept. Every path that asks this
110
- * question — the initial slot lookup and **both** lost-write read-backs — must
111
- * go through here, or an upgrade makes the same row equivalent on lookup and a
112
- * `coordinator_write_lost` conflict on the race path.
113
- *
114
- * A legacy row is returned as stored, so its `ceilingHash` is historical: a
115
- * caller must not assume every persisted hash uses today's algorithm.
107
+ * Stored fingerprint only. Recomputing the fingerprint over the stored authority — the
108
+ * previous behaviour — existed to tolerate rows written before set canonicalisation, and
109
+ * that compatibility is gone with the single-operator decision: no migration lanes, no
110
+ * second schema or hash shape carried forward. A stale row is re-bound, not forgiven.
116
111
  */
117
112
  function ceilingsMatch(stored: KxmCoordinatorRecord, ceilingHash: string): boolean {
118
- return stored.ceilingHash === ceilingHash || kxmCeilingHash(stored.authority) === ceilingHash;
113
+ return stored.ceilingHash === ceilingHash;
119
114
  }
120
115
 
121
116
  function normalizeAuthority(authority: KxmCoordinatorAuthority): KxmCoordinatorAuthority {
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.52";
11
+ const VERSION = "0.7.54";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;