@kontextmind/kxm 0.7.53 → 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.
@@ -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 }) {
@@ -179,7 +179,7 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
179
179
  throw databaseError(
180
180
  "runtime_schema_outdated",
181
181
  file,
182
- `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh — upgrading old state in place is deliberately unsupported`,
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
183
  );
184
184
  }
185
185
 
@@ -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;
@@ -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.53";
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;
@@ -58,7 +58,6 @@ export interface KxmProjectBundle {
58
58
  environments: readonly KxmResource[];
59
59
  gateRegistry?: KxmResource;
60
60
  templateProvenance?: JsonObject;
61
- migrationReceipt?: JsonObject;
62
61
  resources: readonly KxmResource[];
63
62
  configRevision: string;
64
63
  }
@@ -69,11 +68,9 @@ export interface KxmConfigOptions {
69
68
  registeredExecutors?: Iterable<string>;
70
69
  registeredToolPresets?: Iterable<string>;
71
70
  registeredHarnesses?: Iterable<string>;
72
- /** Internal: migration apply validates the staged target before the receipt exists. */
73
- allowUnreceiptedLegacyConfig?: boolean;
74
71
  }
75
72
 
76
- export type KxmInitializationMode = "create" | "migrate" | "repair" | "ready";
73
+ export type KxmInitializationMode = "create" | "legacy" | "repair" | "ready";
77
74
 
78
75
  export interface KxmInitializationPlan {
79
76
  mode: KxmInitializationMode;
@@ -161,9 +158,6 @@ export class KxmSchemaRegistry {
161
158
  readonly localBindingsValidator: ValidateFunction;
162
159
  readonly templateProvenanceValidator: ValidateFunction;
163
160
  readonly initOperationValidator: ValidateFunction;
164
- readonly migrationPlanValidator: ValidateFunction;
165
- readonly migrationDecisionValidator: ValidateFunction;
166
- readonly migrationReceiptValidator: ValidateFunction;
167
161
  readonly permissionDiffValidator: ValidateFunction;
168
162
  readonly runEventValidator: ValidateFunction;
169
163
  readonly driveReceiptValidator: ValidateFunction;
@@ -181,9 +175,6 @@ export class KxmSchemaRegistry {
181
175
  const localBindingsFile = "local-repository-bindings.schema.json";
182
176
  const templateProvenanceFile = "template-provenance.schema.json";
183
177
  const initOperationFile = "init-operation.schema.json";
184
- const migrationPlanFile = "migration-plan.schema.json";
185
- const migrationDecisionFile = "migration-decision.schema.json";
186
- const migrationReceiptFile = "migration-receipt.schema.json";
187
178
  const permissionDiffFile = "permission-diff.schema.json";
188
179
  const runEventFile = "run-event.schema.json";
189
180
  const driveReceiptFile = "drive-receipt.schema.json";
@@ -192,9 +183,6 @@ export class KxmSchemaRegistry {
192
183
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, localBindingsFile)));
193
184
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, templateProvenanceFile)));
194
185
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, initOperationFile)));
195
- this.ajv.addSchema(readJsonObject(join(this.schemasDir, migrationPlanFile)));
196
- this.ajv.addSchema(readJsonObject(join(this.schemasDir, migrationDecisionFile)));
197
- this.ajv.addSchema(readJsonObject(join(this.schemasDir, migrationReceiptFile)));
198
186
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, permissionDiffFile)));
199
187
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, runEventFile)));
200
188
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, driveReceiptFile)));
@@ -208,9 +196,6 @@ export class KxmSchemaRegistry {
208
196
  const localBindingsValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${localBindingsFile}`);
209
197
  const templateProvenanceValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${templateProvenanceFile}`);
210
198
  const initOperationValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${initOperationFile}`);
211
- const migrationPlanValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationPlanFile}`);
212
- const migrationDecisionValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationDecisionFile}`);
213
- const migrationReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationReceiptFile}`);
214
199
  const permissionDiffValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${permissionDiffFile}`);
215
200
  const runEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${runEventFile}`);
216
201
  const driveReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${driveReceiptFile}`);
@@ -219,9 +204,6 @@ export class KxmSchemaRegistry {
219
204
  if (!localBindingsValidator) throw new Error(`schema did not compile: ${localBindingsFile}`);
220
205
  if (!templateProvenanceValidator) throw new Error(`schema did not compile: ${templateProvenanceFile}`);
221
206
  if (!initOperationValidator) throw new Error(`schema did not compile: ${initOperationFile}`);
222
- if (!migrationPlanValidator) throw new Error(`schema did not compile: ${migrationPlanFile}`);
223
- if (!migrationDecisionValidator) throw new Error(`schema did not compile: ${migrationDecisionFile}`);
224
- if (!migrationReceiptValidator) throw new Error(`schema did not compile: ${migrationReceiptFile}`);
225
207
  if (!permissionDiffValidator) throw new Error(`schema did not compile: ${permissionDiffFile}`);
226
208
  if (!runEventValidator) throw new Error(`schema did not compile: ${runEventFile}`);
227
209
  if (!driveReceiptValidator) throw new Error(`schema did not compile: ${driveReceiptFile}`);
@@ -230,9 +212,6 @@ export class KxmSchemaRegistry {
230
212
  this.localBindingsValidator = localBindingsValidator;
231
213
  this.templateProvenanceValidator = templateProvenanceValidator;
232
214
  this.initOperationValidator = initOperationValidator;
233
- this.migrationPlanValidator = migrationPlanValidator;
234
- this.migrationDecisionValidator = migrationDecisionValidator;
235
- this.migrationReceiptValidator = migrationReceiptValidator;
236
215
  this.permissionDiffValidator = permissionDiffValidator;
237
216
  this.runEventValidator = runEventValidator;
238
217
  this.driveReceiptValidator = driveReceiptValidator;
@@ -263,18 +242,6 @@ export class KxmSchemaRegistry {
263
242
  return this.validateAuxiliary(value, file, "kxm.init-operation.v1", this.initOperationValidator);
264
243
  }
265
244
 
266
- validateMigrationPlan(value: JsonObject, file: string): KxmConfigIssue[] {
267
- return this.validateAuxiliary(value, file, "kxm.migration-plan.v1", this.migrationPlanValidator);
268
- }
269
-
270
- validateMigrationDecision(value: JsonObject, file: string): KxmConfigIssue[] {
271
- return this.validateAuxiliary(value, file, "kxm.migration-decision.v1", this.migrationDecisionValidator);
272
- }
273
-
274
- validateMigrationReceipt(value: JsonObject, file: string): KxmConfigIssue[] {
275
- return this.validateAuxiliary(value, file, "kxm.migration-receipt.v1", this.migrationReceiptValidator);
276
- }
277
-
278
245
  validatePermissionDiff(value: JsonObject, file: string): KxmConfigIssue[] {
279
246
  return this.validateAuxiliary(value, file, "kxm.permission-diff.v1", this.permissionDiffValidator);
280
247
  }
@@ -1279,11 +1246,17 @@ export function assertNoRegisteredGates(options: object): void {
1279
1246
  export function loadKxmProject(projectRoot: string, options: KxmConfigOptions = {}): KxmProjectBundle {
1280
1247
  assertNoRegisteredGates(options);
1281
1248
  const root = resolve(projectRoot);
1282
- let migrationReceipt: JsonObject | undefined;
1283
- if (legacyConfigFilesAt(root).length > 0 && options.allowUnreceiptedLegacyConfig !== true) {
1284
- const receiptCheck = readKxmMigrationReceipt(root, options);
1285
- if (receiptCheck.issues.length > 0) throw new KxmConfigError(receiptCheck.issues);
1286
- migrationReceipt = receiptCheck.receipt;
1249
+ const legacyPresent = legacyConfigFilesAt(root);
1250
+ if (legacyPresent.length > 0) {
1251
+ // Nothing can unlock this any more. The migration path was deleted under the
1252
+ // single-operator decision, so a tree still holding legacy JSON fails closed here
1253
+ // rather than being loadable on proof of a past conversion.
1254
+ throw new KxmConfigError(legacyPresent.map((file) => issue(
1255
+ "semantic",
1256
+ "legacy_state_unsupported",
1257
+ file,
1258
+ "legacy JSON configuration is present and this build does not migrate it: delete these files once their YAML replacements exist in .kxm/, or initialise a fresh project",
1259
+ )));
1287
1260
  }
1288
1261
  const registry = new KxmSchemaRegistry(options.schemasDir);
1289
1262
  const project = readResource(registry, root, join(root, ".kxm", "project.yaml"), ".kxm/project.yaml", "project");
@@ -1296,9 +1269,6 @@ export function loadKxmProject(projectRoot: string, options: KxmConfigOptions =
1296
1269
  earlyIssues.push(issue("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
1297
1270
  }
1298
1271
  }
1299
- if (migrationReceipt && stringValue(migrationReceipt.projectId) !== stringValue(project.value.id)) {
1300
- earlyIssues.push(issue("semantic", "migration_project_mismatch", KXM_MIGRATION_RECEIPT_PATH, "migration receipt belongs to a different project identity"));
1301
- }
1302
1272
  const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories")
1303
1273
  .map((candidate) => stringValue(objectValue(candidate)?.id))
1304
1274
  .filter((candidate): candidate is string => candidate !== undefined));
@@ -1383,6 +1353,18 @@ export function loadKxmProject(projectRoot: string, options: KxmConfigOptions =
1383
1353
  continue;
1384
1354
  }
1385
1355
  }
1356
+ // A member worktree is authoritative for its own repo.yaml/env.yaml, so the legacy
1357
+ // refusal has to cover it too — checking only the control root let a member full of
1358
+ // legacy JSON load normally. Throws before a single byte of that member is read.
1359
+ const memberLegacy = binding === root ? [] : legacyConfigFilesAt(binding);
1360
+ if (memberLegacy.length > 0) {
1361
+ throw new KxmConfigError(memberLegacy.map((file) => issue(
1362
+ "semantic",
1363
+ "legacy_state_unsupported",
1364
+ `${repositoryId}/${file}`,
1365
+ `repository ${repositoryId} holds legacy JSON configuration and this build does not migrate it: delete those files once their YAML replacements exist, or bind a clean worktree`,
1366
+ )));
1367
+ }
1386
1368
  const foldedBinding = canonicalHostPath(binding).toLocaleLowerCase("en-US");
1387
1369
  const priorBinding = seenBindings.get(foldedBinding);
1388
1370
  if (priorBinding && priorBinding !== repositoryId) {
@@ -1434,64 +1416,12 @@ export function loadKxmProject(projectRoot: string, options: KxmConfigOptions =
1434
1416
  environments,
1435
1417
  ...(gateRegistry ? { gateRegistry } : {}),
1436
1418
  ...(templateProvenance === undefined ? {} : { templateProvenance }),
1437
- ...(migrationReceipt === undefined ? {} : { migrationReceipt }),
1438
1419
  resources,
1439
1420
  configRevision: bundleRevision(resources),
1440
1421
  };
1441
1422
  }
1442
1423
 
1443
- /**
1444
- * Fully verify a migration receipt against the loaded target bundle: target
1445
- * revision and per-resource bytes. Load-time coexistence only pins the
1446
- * legacy sources; this stricter check backs `kxm migrate verify`.
1447
- */
1448
- export function verifyKxmMigrationReceiptTarget(
1449
- root: string,
1450
- receipt: JsonObject,
1451
- resources: readonly KxmResource[],
1452
- configRevision: string,
1453
- ): KxmConfigIssue[] {
1454
- const issues: KxmConfigIssue[] = [];
1455
- if (receipt.configRevision !== configRevision) {
1456
- issues.push(issue("semantic", "migration_target_changed", KXM_MIGRATION_RECEIPT_PATH, "target configuration revision no longer matches the migration receipt"));
1457
- }
1458
- const declared = new Map<string, JsonObject>();
1459
- for (const candidate of Array.isArray(receipt.resources) ? receipt.resources : []) {
1460
- if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
1461
- const record = candidate as JsonObject;
1462
- if (typeof record.path === "string") declared.set(record.path, record);
1463
- }
1464
- }
1465
- const installedPaths = new Set<string>();
1466
- for (const resource of resources) {
1467
- // Repository resources are loaded from their bound worktree paths; only
1468
- // control-tree files participate in receipt byte checks.
1469
- const relative = resource.file.startsWith(root) ? relativePortable(root, resource.file) : undefined;
1470
- if (!relative) continue;
1471
- installedPaths.add(relative);
1472
- const record = declared.get(relative);
1473
- if (!record) {
1474
- issues.push(issue("semantic", "migration_target_unreceipted", relative, "installed resource is not covered by the migration receipt"));
1475
- continue;
1476
- }
1477
- const current = hashFileRecord(root, relative);
1478
- if (!current || current.sha256 !== record.sha256 || current.bytes !== record.bytes) {
1479
- issues.push(issue("semantic", "migration_target_modified", relative, "installed resource bytes differ from the migration receipt"));
1480
- }
1481
- }
1482
- for (const path of declared.keys()) {
1483
- if (!installedPaths.has(path)) {
1484
- issues.push(issue("semantic", "migration_target_missing", path, "receipt-covered resource is not part of the validated configuration"));
1485
- }
1486
- }
1487
- return sortIssues(issues);
1488
- }
1489
1424
 
1490
- function relativePortable(root: string, absolute: string): string | undefined {
1491
- const rel = relative(root, absolute);
1492
- if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) return undefined;
1493
- return rel.split(sep).join("/");
1494
- }
1495
1425
 
1496
1426
  function legacyInputsAt(root: string): string[] {
1497
1427
  const candidates = [
@@ -1503,7 +1433,6 @@ function legacyInputsAt(root: string): string[] {
1503
1433
  return candidates.filter((candidate) => existsSync(join(root, ...candidate.split("/"))));
1504
1434
  }
1505
1435
 
1506
- export const KXM_MIGRATION_RECEIPT_PATH = ".kxm/migration-receipt.yaml";
1507
1436
  const LEGACY_CONFIG_FILES = [".kxm/config/agents.json", ".kxm/config/gates.json"] as const;
1508
1437
 
1509
1438
  /** Individual legacy configuration files (not directories, not runtime state) that a migration receipt must cover. */
@@ -1540,83 +1469,9 @@ export function legacyConfigFilesAt(root: string): string[] {
1540
1469
  return files;
1541
1470
  }
1542
1471
 
1543
- export interface KxmMigrationReceiptCheck {
1544
- receipt?: JsonObject;
1545
- issues: KxmConfigIssue[];
1546
- }
1547
1472
 
1548
- function hashFileRecord(root: string, relativePath: string): { path: string; sha256: string; bytes: number } | undefined {
1549
- const absolute = join(root, ...relativePath.split("/"));
1550
- if (!existsSync(absolute)) return undefined;
1551
- const bytes = readFileSync(absolute);
1552
- return {
1553
- path: relativePath,
1554
- sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
1555
- bytes: bytes.byteLength,
1556
- };
1557
- }
1558
1473
 
1559
- export function migrationReceiptSelfHash(receipt: JsonObject): string {
1560
- const { receiptSha256: _ignored, ...unsigned } = receipt;
1561
- return `sha256:${createHash("sha256").update(kxmCanonicalJson(unsigned as JsonObject), "utf8").digest("hex")}`;
1562
- }
1563
1474
 
1564
- /**
1565
- * Read and structurally verify a migration receipt: schema, self-hash, and
1566
- * exact coverage of the legacy configuration files still present. Does not
1567
- * compare the target configuration revision (computed by the caller).
1568
- */
1569
- export function readKxmMigrationReceipt(root: string, options: KxmConfigOptions = {}): KxmMigrationReceiptCheck {
1570
- const receiptPath = join(root, ...KXM_MIGRATION_RECEIPT_PATH.split("/"));
1571
- const legacyFiles = legacyConfigFilesAt(root);
1572
- if (!existsSync(receiptPath)) {
1573
- return {
1574
- issues: legacyFiles.map((file) => issue("semantic", "legacy_kxm_conflict", file, "legacy and KXM configuration cannot coexist before an accepted migration receipt")),
1575
- };
1576
- }
1577
- const receiptStat = lstatSync(receiptPath);
1578
- if (receiptStat.isSymbolicLink() || !receiptStat.isFile()) {
1579
- return { issues: [issue("path", "migration_receipt_invalid", KXM_MIGRATION_RECEIPT_PATH, "migration receipt must be a regular file, not a link")] };
1580
- }
1581
- let receipt: JsonObject;
1582
- try {
1583
- receipt = parseRestrictedYaml(readFileSync(receiptPath), KXM_MIGRATION_RECEIPT_PATH);
1584
- } catch (error) {
1585
- if (error instanceof KxmConfigError) return { issues: [...error.issues] };
1586
- throw error;
1587
- }
1588
- const registry = new KxmSchemaRegistry(options.schemasDir);
1589
- const schemaIssues = registry.validateMigrationReceipt(receipt, KXM_MIGRATION_RECEIPT_PATH);
1590
- if (schemaIssues.length > 0) return { issues: schemaIssues };
1591
- if (receipt.receiptSha256 !== migrationReceiptSelfHash(receipt)) {
1592
- return { issues: [issue("semantic", "migration_receipt_hash_mismatch", KXM_MIGRATION_RECEIPT_PATH, "receipt self-hash does not match its content")] };
1593
- }
1594
- const sources = (Array.isArray(receipt.sources) ? receipt.sources : [])
1595
- .map((candidate) => (candidate && typeof candidate === "object" && !Array.isArray(candidate) ? (candidate as JsonObject).path : undefined))
1596
- .filter((candidate): candidate is string => typeof candidate === "string")
1597
- .sort(compareCodeUnits);
1598
- const issues: KxmConfigIssue[] = [];
1599
- const declared = new Set(sources);
1600
- for (const file of legacyFiles) {
1601
- if (!declared.has(file)) {
1602
- issues.push(issue("semantic", "migration_source_unmigrated", file, "legacy configuration file is not covered by the migration receipt"));
1603
- continue;
1604
- }
1605
- const record = (Array.isArray(receipt.sources) ? receipt.sources : [])
1606
- .map((candidate) => (candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate as JsonObject : undefined))
1607
- .find((candidate) => candidate?.path === file);
1608
- const current = hashFileRecord(root, file);
1609
- if (!current || current.sha256 !== record?.sha256 || current.bytes !== record?.bytes) {
1610
- issues.push(issue("semantic", "migration_source_changed", file, "legacy configuration changed after the migration receipt was issued"));
1611
- }
1612
- }
1613
- for (const file of sources) {
1614
- if (!legacyFiles.includes(file)) {
1615
- issues.push(issue("semantic", "migration_source_missing", file, "receipt covers a legacy configuration file that no longer exists"));
1616
- }
1617
- }
1618
- return issues.length > 0 ? { issues } : { receipt, issues: [] };
1619
- }
1620
1475
 
1621
1476
  function discoverLegacyRoot(start: string): { root: string; inputs: string[] } | undefined {
1622
1477
  let initial = resolve(start);
@@ -1628,7 +1483,11 @@ function discoverLegacyRoot(start: string): { root: string; inputs: string[] } |
1628
1483
  return inputs.length > 0 ? { root: candidate, inputs } : undefined;
1629
1484
  }
1630
1485
 
1631
- /** Classify init without changing files. Applying create/repair/migration is a separate, explicit operation. */
1486
+ /**
1487
+ * Classify init without changing files. Applying create/repair is a separate, explicit
1488
+ * operation; a legacy-only tree is **reported and never written** — this build carries no
1489
+ * migration path, so init says what to do instead of doing it.
1490
+ */
1632
1491
  export function planKxmInitialization(start = process.cwd(), options: KxmConfigOptions = {}): KxmInitializationPlan {
1633
1492
  const inspectedFrom = resolve(start);
1634
1493
  const projectRoot = discoverKxmProjectRoot(inspectedFrom);
@@ -1638,16 +1497,38 @@ export function planKxmInitialization(start = process.cwd(), options: KxmConfigO
1638
1497
  return { mode: "ready", inspectedFrom, projectRoot, changesRequired: false, issues: [], legacyInputs: [], configRevision: bundle.configRevision };
1639
1498
  } catch (error) {
1640
1499
  if (!(error instanceof KxmConfigError)) throw error;
1500
+ const legacyIssues = error.issues.filter((candidate) => candidate.code === "legacy_state_unsupported");
1641
1501
  const legacyInputs = legacyInputsAt(projectRoot);
1642
- if (legacyInputs.length > 0) {
1643
- return { mode: "migrate", inspectedFrom, projectRoot, legacyRoot: projectRoot, changesRequired: true, issues: error.issues, legacyInputs };
1502
+ if (legacyInputs.length > 0 || legacyIssues.length > 0) {
1503
+ // Terminal for **any** legacy input, including one that lives in a bound member
1504
+ // worktree. Classifying a member's legacy JSON as `repair` let init take the
1505
+ // mutation lock and resume an interrupted create/repair journal, installing
1506
+ // resources into a tree the loader had just refused.
1507
+ return {
1508
+ mode: "legacy",
1509
+ inspectedFrom,
1510
+ projectRoot,
1511
+ legacyRoot: projectRoot,
1512
+ changesRequired: true,
1513
+ issues: [...error.issues, ...(legacyInputs.length > 0
1514
+ ? [issue("discovery", "legacy_state_unsupported", ".kxm", "legacy configuration is present and this build does not migrate it: initialise a fresh project directory and carry over the YAML definitions you want to keep")]
1515
+ : [])],
1516
+ legacyInputs: [...new Set([...legacyInputs, ...legacyIssues.map((candidate) => candidate.file)])],
1517
+ };
1644
1518
  }
1645
1519
  return { mode: "repair", inspectedFrom, projectRoot, changesRequired: true, issues: error.issues, legacyInputs: [] };
1646
1520
  }
1647
1521
  }
1648
1522
  const legacy = discoverLegacyRoot(inspectedFrom);
1649
1523
  if (legacy) {
1650
- return { mode: "migrate", inspectedFrom, legacyRoot: legacy.root, changesRequired: true, issues: [], legacyInputs: legacy.inputs };
1524
+ return {
1525
+ mode: "legacy",
1526
+ inspectedFrom,
1527
+ legacyRoot: legacy.root,
1528
+ changesRequired: true,
1529
+ issues: [issue("discovery", "legacy_state_unsupported", legacy.root, "legacy KXM/Mesh configuration discovered; this build does not migrate it — initialise a fresh project directory and copy the YAML definitions you want to keep")],
1530
+ legacyInputs: legacy.inputs,
1531
+ };
1651
1532
  }
1652
1533
  const gitRoot = discoverGitRoot(inspectedFrom);
1653
1534
  const candidateRoot = gitRoot ?? inspectedFrom;
package/schemas/README.md CHANGED
@@ -14,9 +14,6 @@ Runtime-local records, and JSON events/results for the planned KXM contract.
14
14
  | `template-provenance.schema.json` | `kxm.template-provenance.v1` |
15
15
  | `local-repository-bindings.schema.json` | `kxm.local-repository-bindings.v1` |
16
16
  | `init-operation.schema.json` | `kxm.init-operation.v1` |
17
- | `migration-plan.schema.json` | `kxm.migration-plan.v1` |
18
- | `migration-decision.schema.json` | `kxm.migration-decision.v1` |
19
- | `migration-receipt.schema.json` | `kxm.migration-receipt.v1` |
20
17
  | `permission-diff.schema.json` | `kxm.permission-diff.v1` |
21
18
  | `run-event.schema.json` | `kxm.run-event.v1` |
22
19
  | `coordinator.schema.json` | `kxm.coordinator.v1` |