@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
|
@@ -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
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
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;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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`, `
|
|
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,
|
|
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
|
|
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
|
|
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 === "
|
|
155
|
-
? "legacy state
|
|
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({
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
/**
|
package/plugins/kxm/src/init.ts
CHANGED
|
@@ -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
|
|
105
|
+
* Does a stored coordinator already express the ceiling being requested?
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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
|
|
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.
|
|
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;
|