@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +34 -0
- package/docs/agent-skills.md +1 -1
- package/docs/configuration.md +0 -3
- package/docs/contracts/migration.md +50 -220
- package/docs/contracts/validation.md +15 -33
- package/docs/test-matrix.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +380 -2146
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +23 -178
- package/plugins/kxm/dist/runtime.js +23 -178
- package/plugins/kxm/dist/server.js +6 -105
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +4 -7
- package/plugins/kxm/src/autocomplete.ts +0 -3
- package/plugins/kxm/src/cli/project.ts +2 -133
- package/plugins/kxm/src/cli.ts +3 -24
- package/plugins/kxm/src/database.ts +9 -21
- package/plugins/kxm/src/external-effects.ts +37 -27
- package/plugins/kxm/src/init.ts +10 -6
- package/plugins/kxm/src/intake.ts +6 -11
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +54 -173
- package/plugins/kxm/src/runtime-store.ts +0 -69
- package/plugins/kxm/src/store.ts +0 -92
- package/schemas/README.md +0 -3
- package/plugins/kxm/src/migrate.ts +0 -1848
- package/schemas/migration-decision.schema.json +0 -26
- package/schemas/migration-plan.schema.json +0 -123
- package/schemas/migration-receipt.schema.json +0 -52
|
@@ -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" | "
|
|
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
|
-
|
|
1283
|
-
if (
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
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;
|
|
@@ -782,74 +782,6 @@ CREATE TABLE project_controls (
|
|
|
782
782
|
) STRICT;
|
|
783
783
|
`;
|
|
784
784
|
|
|
785
|
-
const COORDINATOR_INTAKE_DDL = `
|
|
786
|
-
CREATE TABLE IF NOT EXISTS coordinators (
|
|
787
|
-
coordinator_id TEXT PRIMARY KEY,
|
|
788
|
-
project_id TEXT NOT NULL,
|
|
789
|
-
role TEXT NOT NULL,
|
|
790
|
-
channel TEXT NOT NULL,
|
|
791
|
-
ceiling_hash TEXT NOT NULL,
|
|
792
|
-
config_revision TEXT NOT NULL,
|
|
793
|
-
bound_at TEXT NOT NULL,
|
|
794
|
-
schema TEXT NOT NULL,
|
|
795
|
-
record TEXT NOT NULL
|
|
796
|
-
) STRICT;
|
|
797
|
-
CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
|
|
798
|
-
CREATE TABLE IF NOT EXISTS intake_messages (
|
|
799
|
-
message_id TEXT PRIMARY KEY,
|
|
800
|
-
project_id TEXT NOT NULL,
|
|
801
|
-
coordinator_id TEXT NOT NULL,
|
|
802
|
-
idempotency_key TEXT NOT NULL,
|
|
803
|
-
content_hash TEXT NOT NULL,
|
|
804
|
-
received_at TEXT NOT NULL,
|
|
805
|
-
dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
|
|
806
|
-
schema TEXT NOT NULL,
|
|
807
|
-
record TEXT NOT NULL
|
|
808
|
-
) STRICT;
|
|
809
|
-
CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
|
|
810
|
-
CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
|
|
811
|
-
CREATE TABLE IF NOT EXISTS project_controls (
|
|
812
|
-
project_id TEXT PRIMARY KEY,
|
|
813
|
-
paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
|
|
814
|
-
reason TEXT,
|
|
815
|
-
updated_at TEXT NOT NULL,
|
|
816
|
-
actor TEXT NOT NULL,
|
|
817
|
-
schema TEXT NOT NULL,
|
|
818
|
-
record TEXT NOT NULL
|
|
819
|
-
) STRICT;
|
|
820
|
-
`;
|
|
821
|
-
|
|
822
|
-
const DRIVE_RECEIPTS_DDL = `
|
|
823
|
-
CREATE TABLE IF NOT EXISTS drive_receipts (
|
|
824
|
-
drive_id TEXT PRIMARY KEY,
|
|
825
|
-
run_id TEXT NOT NULL,
|
|
826
|
-
project_id TEXT NOT NULL,
|
|
827
|
-
opened_sequence INTEGER NOT NULL,
|
|
828
|
-
last_sequence INTEGER NOT NULL,
|
|
829
|
-
closed_at TEXT NOT NULL,
|
|
830
|
-
schema TEXT NOT NULL,
|
|
831
|
-
receipt TEXT NOT NULL
|
|
832
|
-
) STRICT;
|
|
833
|
-
CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
|
|
834
|
-
`;
|
|
835
|
-
|
|
836
|
-
const EVENT_STORE_MIGRATIONS = [
|
|
837
|
-
{
|
|
838
|
-
fromVersion: 3,
|
|
839
|
-
toVersion: 4,
|
|
840
|
-
migrate(database: DatabaseSync): void {
|
|
841
|
-
database.exec(DRIVE_RECEIPTS_DDL);
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
fromVersion: 4,
|
|
846
|
-
toVersion: 5,
|
|
847
|
-
migrate(database: DatabaseSync): void {
|
|
848
|
-
database.exec(COORDINATOR_INTAKE_DDL);
|
|
849
|
-
},
|
|
850
|
-
},
|
|
851
|
-
];
|
|
852
|
-
|
|
853
785
|
/** One persisted coordinator identity (`kxm.coordinator.v1`). */
|
|
854
786
|
export interface KxmCoordinatorRow {
|
|
855
787
|
coordinatorId: string;
|
|
@@ -894,7 +826,6 @@ export class KxmRunEventStore {
|
|
|
894
826
|
schema: EVENT_STORE_SCHEMA,
|
|
895
827
|
version: KXM_EVENT_STORE_SCHEMA_VERSION,
|
|
896
828
|
tables: EVENT_STORE_TABLES,
|
|
897
|
-
migrations: EVENT_STORE_MIGRATIONS,
|
|
898
829
|
});
|
|
899
830
|
}
|
|
900
831
|
|
package/plugins/kxm/src/store.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
|
6
6
|
import {
|
|
7
7
|
openDatabase,
|
|
8
8
|
withDatabaseTransaction,
|
|
9
|
-
type DatabaseMigrationStep,
|
|
10
9
|
type DatabaseSchemaSpec,
|
|
11
10
|
} from "./database.ts";
|
|
12
11
|
|
|
@@ -73,101 +72,10 @@ export const HUB_STORE_SCHEMA_V3 = `
|
|
|
73
72
|
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
74
73
|
`;
|
|
75
74
|
|
|
76
|
-
export const HUB_STORE_MIGRATIONS: readonly DatabaseMigrationStep[] = Object.freeze([
|
|
77
|
-
{
|
|
78
|
-
fromVersion: 1,
|
|
79
|
-
toVersion: 2,
|
|
80
|
-
migrate: (db: DatabaseSync) => {
|
|
81
|
-
db.exec(`
|
|
82
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
83
|
-
id TEXT PRIMARY KEY,
|
|
84
|
-
record TEXT NOT NULL
|
|
85
|
-
) STRICT;
|
|
86
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
87
|
-
id TEXT PRIMARY KEY,
|
|
88
|
-
record TEXT NOT NULL
|
|
89
|
-
) STRICT;
|
|
90
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
91
|
-
id TEXT PRIMARY KEY,
|
|
92
|
-
definition_id TEXT NOT NULL,
|
|
93
|
-
delivery_id TEXT NOT NULL,
|
|
94
|
-
record TEXT NOT NULL,
|
|
95
|
-
UNIQUE(definition_id, delivery_id)
|
|
96
|
-
) STRICT;
|
|
97
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
98
|
-
id TEXT PRIMARY KEY,
|
|
99
|
-
run_id TEXT NOT NULL,
|
|
100
|
-
category TEXT NOT NULL,
|
|
101
|
-
area TEXT NOT NULL,
|
|
102
|
-
record TEXT NOT NULL
|
|
103
|
-
) STRICT;
|
|
104
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
105
|
-
`);
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
fromVersion: 2,
|
|
110
|
-
toVersion: 3,
|
|
111
|
-
migrate: (db: DatabaseSync) => {
|
|
112
|
-
db.exec(`
|
|
113
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
114
|
-
id TEXT PRIMARY KEY,
|
|
115
|
-
record TEXT NOT NULL
|
|
116
|
-
) STRICT;
|
|
117
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
118
|
-
id TEXT PRIMARY KEY,
|
|
119
|
-
record TEXT NOT NULL
|
|
120
|
-
) STRICT;
|
|
121
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
122
|
-
id TEXT PRIMARY KEY,
|
|
123
|
-
definition_id TEXT NOT NULL,
|
|
124
|
-
delivery_id TEXT NOT NULL,
|
|
125
|
-
record TEXT NOT NULL,
|
|
126
|
-
UNIQUE(definition_id, delivery_id)
|
|
127
|
-
) STRICT;
|
|
128
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
129
|
-
id TEXT PRIMARY KEY,
|
|
130
|
-
run_id TEXT NOT NULL,
|
|
131
|
-
category TEXT NOT NULL,
|
|
132
|
-
area TEXT NOT NULL,
|
|
133
|
-
record TEXT NOT NULL
|
|
134
|
-
) STRICT;
|
|
135
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
136
|
-
CREATE TABLE IF NOT EXISTS context_items (
|
|
137
|
-
id TEXT PRIMARY KEY,
|
|
138
|
-
project TEXT NOT NULL,
|
|
139
|
-
kind TEXT NOT NULL,
|
|
140
|
-
record TEXT NOT NULL
|
|
141
|
-
) STRICT;
|
|
142
|
-
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
143
|
-
CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
|
|
144
|
-
ON messages(
|
|
145
|
-
json_extract(record, '$.from'),
|
|
146
|
-
json_extract(record, '$.idempotencyKey')
|
|
147
|
-
) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
|
|
148
|
-
CREATE INDEX IF NOT EXISTS messages_to_seq
|
|
149
|
-
ON messages(
|
|
150
|
-
json_extract(record, '$.to'),
|
|
151
|
-
COALESCE(json_extract(record, '$.seq'), 0)
|
|
152
|
-
);
|
|
153
|
-
CREATE TABLE IF NOT EXISTS consumer_cursors (
|
|
154
|
-
agent_id TEXT PRIMARY KEY,
|
|
155
|
-
cursor INTEGER NOT NULL
|
|
156
|
-
) STRICT;
|
|
157
|
-
CREATE TABLE IF NOT EXISTS agent_sequences (
|
|
158
|
-
agent_id TEXT PRIMARY KEY,
|
|
159
|
-
next_seq INTEGER NOT NULL
|
|
160
|
-
) STRICT;
|
|
161
|
-
`);
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
75
|
export const HUB_STORE_SCHEMA_SPEC: DatabaseSchemaSpec = Object.freeze({
|
|
167
76
|
schema: HUB_STORE_SCHEMA_V3,
|
|
168
77
|
version: HUB_STORE_SCHEMA_VERSION,
|
|
169
78
|
tables: HUB_STORE_TABLES,
|
|
170
|
-
migrations: HUB_STORE_MIGRATIONS,
|
|
171
79
|
});
|
|
172
80
|
|
|
173
81
|
export interface StoredAgent extends AgentRecord {
|
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` |
|