@kontextmind/kxm 0.7.6 → 0.7.7

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.
@@ -0,0 +1,698 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createBackup, restoreBackup } from "../database.ts";
8
+ import { refreshModelInventory } from "../model-inventory.ts";
9
+ import { listInventoryModels, listRoleBindings, loadProducerPolicy, setModelState, updateProducer } from "../producers.ts";
10
+ import {
11
+ VnextConfigError,
12
+ discoverVnextProjectRoot,
13
+ type VnextInitializationPlan,
14
+ } from "../vnext-config.ts";
15
+ import { initializeVnextProject } from "../vnext-init.ts";
16
+ import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "../vnext-migrate.ts";
17
+ import { diffVnextProjectAgainstRevision, formatVnextPermissionDiff } from "../vnext-permission.ts";
18
+ import { readVnextLocalBindings, vnextUserStateRoot } from "../vnext-bindings.ts";
19
+ import { loadVnextProject } from "../vnext-config.ts";
20
+ import {
21
+ ensureVnextSupervisor,
22
+ vnextRuntimeRequest,
23
+ vnextSupervisorStatus,
24
+ } from "../vnext-runtime-supervisor.ts";
25
+ import { vnextRuntimePaths } from "../vnext-runtime-store.ts";
26
+ import {
27
+ formatHarnessInventory,
28
+ probeHarnesses,
29
+ } from "../vnext-harness.ts";
30
+ import {
31
+ fetchLatestKxmVersion,
32
+ kxmReleaseAssetName,
33
+ noticeFromVersions,
34
+ planKxmPackageUpdate,
35
+ readInstalledKxmVersion,
36
+ verifyReleaseAssetDigest,
37
+ writeUpdateCache,
38
+ type KxmPackageUpdateStep,
39
+ type KxmUpdateConfig,
40
+ type KxmUpdateNotice,
41
+ } from "../kxm-update.ts";
42
+ import { loadKxmUpdateConfig } from "../kxm-update-config.ts";
43
+ import type { InstallKindReport, InstallProbe } from "../kxm-install-kind.ts";
44
+ import { print, type CliIo, type CliSpawnResult, type Runtime } from "./types.ts";
45
+
46
+ const repoRoot = resolve(fileURLToPath(new URL("../../../../", import.meta.url)));
47
+
48
+ export function initPlanPayload(plan: VnextInitializationPlan): Record<string, unknown> {
49
+ return {
50
+ mode: plan.mode,
51
+ inspectedFrom: plan.inspectedFrom,
52
+ ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}),
53
+ ...(plan.legacyRoot ? { legacyRoot: plan.legacyRoot } : {}),
54
+ changesRequired: plan.changesRequired,
55
+ legacyInputs: plan.legacyInputs,
56
+ issues: plan.issues,
57
+ ...(plan.configRevision ? { configRevision: plan.configRevision } : {}),
58
+ };
59
+ }
60
+
61
+ export function explicitRepositoryBindings(values: readonly string[]): Readonly<Record<string, string>> {
62
+ const result: Record<string, string> = Object.create(null) as Record<string, string>;
63
+ for (const value of values) {
64
+ const separator = value.indexOf("=");
65
+ const repositoryId = separator < 0 ? "" : value.slice(0, separator).trim();
66
+ const path = separator < 0 ? "" : value.slice(separator + 1).trim();
67
+ if (!repositoryId || !path) {
68
+ throw new VnextConfigError([{
69
+ phase: "discovery",
70
+ code: "repository_binding_argument_invalid",
71
+ file: "--repository",
72
+ message: "repository bindings must use <id=absolute-path>",
73
+ }]);
74
+ }
75
+ if (Object.hasOwn(result, repositoryId)) {
76
+ throw new VnextConfigError([{
77
+ phase: "discovery",
78
+ code: "repository_binding_argument_duplicate",
79
+ file: "--repository",
80
+ message: `repository binding ${repositoryId} was supplied more than once`,
81
+ }]);
82
+ }
83
+ result[repositoryId] = path;
84
+ }
85
+ return result;
86
+ }
87
+
88
+ export async function cmdVnextInit(
89
+ runtime: Runtime,
90
+ options: { name?: string | undefined; projectId?: string | undefined; repository?: string[] | undefined },
91
+ postHooks?: {
92
+ maybeOfferCompletionInstall?: (runtime: Runtime) => Promise<void>;
93
+ maybeOfferGuideSetup?: (runtime: Runtime) => Promise<void>;
94
+ },
95
+ ): Promise<number> {
96
+ if (runtime.workspaceFlag !== undefined) {
97
+ print(runtime.io, runtime.json, {
98
+ ok: false,
99
+ command: "init",
100
+ error: "workspace_option_unsupported",
101
+ }, "kxm init discovers the authoritative Git root from the current directory; --workspace is not supported");
102
+ return 2;
103
+ }
104
+ try {
105
+ const initialized = initializeVnextProject(runtime.cwd, {
106
+ ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
107
+ ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
108
+ repositoryBindings: explicitRepositoryBindings(options.repository ?? []),
109
+ localStateRoot: vnextUserStateRoot({ env: runtime.env }),
110
+ dryRun: runtime.dryRun,
111
+ });
112
+ const payload = {
113
+ ok: initialized.action !== "planned" || runtime.dryRun,
114
+ command: "init",
115
+ action: initialized.action,
116
+ ...initPlanPayload(initialized.plan),
117
+ files: initialized.files,
118
+ ...(initialized.configRevision ? { configRevision: initialized.configRevision } : {}),
119
+ ...(initialized.localBindingFile ? { localBindingFile: initialized.localBindingFile } : {}),
120
+ ...(initialized.bindingsChanged === undefined ? {} : { bindingsChanged: initialized.bindingsChanged }),
121
+ ...(initialized.repairPlan === undefined ? {} : { repairPlan: initialized.repairPlan }),
122
+ ...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
123
+ ...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
124
+ plannedOnly: initialized.action === "planned",
125
+ };
126
+ const finishInit = (code: number, text: string): number => {
127
+ print(runtime.io, runtime.json, payload, text);
128
+ return code;
129
+ };
130
+ if (initialized.action === "created") {
131
+ if (postHooks?.maybeOfferCompletionInstall) await postHooks.maybeOfferCompletionInstall(runtime);
132
+ if (postHooks?.maybeOfferGuideSetup) await postHooks.maybeOfferGuideSetup(runtime);
133
+ return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
134
+ }
135
+ if (initialized.action === "joined") {
136
+ if (postHooks?.maybeOfferCompletionInstall) await postHooks.maybeOfferCompletionInstall(runtime);
137
+ if (postHooks?.maybeOfferGuideSetup) await postHooks.maybeOfferGuideSetup(runtime);
138
+ return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
139
+ }
140
+ if (initialized.action === "repaired") {
141
+ return finishInit(0, `repaired vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
142
+ }
143
+ if (initialized.action === "resumed") {
144
+ return finishInit(0, `resumed vNext ${initialized.transactionKind ?? "initialization"} at ${initialized.projectRoot ?? runtime.cwd}`);
145
+ }
146
+ if (initialized.action === "validated") {
147
+ return finishInit(0, `validated vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
148
+ }
149
+ if (runtime.dryRun) {
150
+ return finishInit(0, `init plan: ${initialized.plan.mode}`);
151
+ }
152
+ const next = initialized.plan.mode === "migrate"
153
+ ? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
154
+ : initialized.repairPlan?.issues.length
155
+ ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
156
+ : "partial or provenance-free vNext state requires explicit repair; no files were overwritten";
157
+ return finishInit(1, next);
158
+ } catch (error) {
159
+ if (error instanceof VnextConfigError) {
160
+ print(runtime.io, runtime.json, {
161
+ ok: false,
162
+ command: "init",
163
+ error: "vnext_initialization_failed",
164
+ issues: error.issues,
165
+ }, `vNext initialization failed: ${error.message}`);
166
+ return 1;
167
+ }
168
+ print(runtime.io, runtime.json, {
169
+ ok: false,
170
+ command: "init",
171
+ error: "vnext_initialization_io_failed",
172
+ }, "vNext initialization failed because a local filesystem operation did not complete");
173
+ return 1;
174
+ }
175
+ }
176
+
177
+ export async function cmdVnextMigratePlan(runtime: Runtime): Promise<number> {
178
+ if (runtime.workspaceFlag !== undefined) {
179
+ print(runtime.io, runtime.json, {
180
+ ok: false,
181
+ command: "migrate plan",
182
+ error: "workspace_option_unsupported",
183
+ }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
184
+ return 2;
185
+ }
186
+ try {
187
+ const result = planVnextMigration(runtime.cwd, {});
188
+ const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
189
+ const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
190
+ const payload = {
191
+ ok: result.plan.canApply === true,
192
+ command: "migrate plan",
193
+ plan: result.plan,
194
+ plannedOnly: result.plan.canApply !== true,
195
+ };
196
+ if (result.plan.canApply === true) {
197
+ print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
198
+ return 0;
199
+ }
200
+ print(
201
+ runtime.io,
202
+ runtime.json,
203
+ payload,
204
+ `migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
205
+ );
206
+ return 1;
207
+ } catch (error) {
208
+ if (error instanceof VnextConfigError) {
209
+ print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
210
+ return 1;
211
+ }
212
+ 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");
213
+ return 1;
214
+ }
215
+ }
216
+
217
+ export async function cmdVnextMigrateApply(runtime: Runtime, options: { decisions?: string | undefined; projectId?: string | undefined; name?: string | undefined }): Promise<number> {
218
+ if (runtime.workspaceFlag !== undefined) {
219
+ print(runtime.io, runtime.json, {
220
+ ok: false,
221
+ command: "migrate apply",
222
+ error: "workspace_option_unsupported",
223
+ }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
224
+ return 2;
225
+ }
226
+ try {
227
+ const result = applyVnextMigration(runtime.cwd, {
228
+ ...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
229
+ ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
230
+ ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
231
+ localStateRoot: vnextUserStateRoot({ env: runtime.env }),
232
+ dryRun: runtime.dryRun,
233
+ });
234
+ const payload = {
235
+ ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
236
+ command: "migrate apply",
237
+ action: result.action,
238
+ files: result.files,
239
+ ...(result.configRevision ? { configRevision: result.configRevision } : {}),
240
+ ...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
241
+ plannedOnly: result.action === "planned",
242
+ };
243
+ if (result.action === "applied") {
244
+ print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
245
+ return 0;
246
+ }
247
+ if (result.action === "already-migrated") {
248
+ print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
249
+ return 0;
250
+ }
251
+ if (runtime.dryRun && result.plan?.canApply === true) {
252
+ print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
253
+ return 0;
254
+ }
255
+ const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
256
+ print(
257
+ runtime.io,
258
+ runtime.json,
259
+ { ...payload, plan: result.plan },
260
+ `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")}`,
261
+ );
262
+ return 1;
263
+ } catch (error) {
264
+ if (error instanceof VnextConfigError) {
265
+ print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
266
+ return 1;
267
+ }
268
+ 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");
269
+ return 1;
270
+ }
271
+ }
272
+
273
+ export async function cmdVnextMigrateVerify(runtime: Runtime): Promise<number> {
274
+ if (runtime.workspaceFlag !== undefined) {
275
+ print(runtime.io, runtime.json, {
276
+ ok: false,
277
+ command: "migrate verify",
278
+ error: "workspace_option_unsupported",
279
+ }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
280
+ return 2;
281
+ }
282
+ let result: ReturnType<typeof verifyVnextMigration>;
283
+ try {
284
+ result = verifyVnextMigration(runtime.cwd, {});
285
+ } catch (error) {
286
+ if (error instanceof VnextConfigError) {
287
+ print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
288
+ return 1;
289
+ }
290
+ 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");
291
+ return 1;
292
+ }
293
+ const payload = {
294
+ ok: result.ok,
295
+ command: "migrate verify",
296
+ ...(result.configRevision ? { configRevision: result.configRevision } : {}),
297
+ issues: result.issues,
298
+ };
299
+ if (result.ok) {
300
+ print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
301
+ return 0;
302
+ }
303
+ print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
304
+ return 1;
305
+ }
306
+
307
+ export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined }): Promise<number> {
308
+ try {
309
+ const { manifest, outDir } = createBackup({
310
+ projectRoot: runtime.cwd,
311
+ ...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
312
+ });
313
+ const payload = {
314
+ ok: true,
315
+ command: "backup",
316
+ backupId: manifest.backupId,
317
+ outDir,
318
+ manifest,
319
+ };
320
+ const summary = [
321
+ `Created SQLite backup with ${manifest.stores.length} store(s):`,
322
+ ...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
323
+ `Manifest: ${join(outDir, "manifest.json")}`,
324
+ ].join("\n");
325
+ print(runtime.io, runtime.json, payload, summary);
326
+ return 0;
327
+ } catch (error) {
328
+ if (error instanceof VnextConfigError) {
329
+ print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
330
+ return 1;
331
+ }
332
+ print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", message: error instanceof Error ? error.message : String(error) }, `backup failed: ${error instanceof Error ? error.message : String(error)}`);
333
+ return 1;
334
+ }
335
+ }
336
+
337
+ export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise<number> {
338
+ try {
339
+ const result = restoreBackup(resolve(runtime.cwd, manifestArg), {
340
+ projectRoot: runtime.cwd,
341
+ });
342
+ const payload = {
343
+ ok: true,
344
+ command: "restore",
345
+ backupId: result.backupId,
346
+ manifestPath: result.manifestPath,
347
+ restoredStores: result.restoredStores,
348
+ };
349
+ const summary = [
350
+ `Restored ${result.restoredStores.length} SQLite store(s) from ${result.manifestPath}:`,
351
+ ...result.restoredStores.map((s) => ` - ${s.storeId}: -> ${s.sourcePath} (schema v${s.schemaVersion}, integrity ${s.integrity})`),
352
+ ].join("\n");
353
+ print(runtime.io, runtime.json, payload, summary);
354
+ return 0;
355
+ } catch (error) {
356
+ if (error instanceof VnextConfigError) {
357
+ print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", issues: error.issues }, `restore failed: ${error.message}`);
358
+ return 1;
359
+ }
360
+ print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", message: error instanceof Error ? error.message : String(error) }, `restore failed: ${error instanceof Error ? error.message : String(error)}`);
361
+ return 1;
362
+ }
363
+ }
364
+
365
+ export async function cmdVnextTrust(runtime: Runtime, check: boolean, options: { base?: string | undefined }): Promise<number> {
366
+ if (runtime.workspaceFlag !== undefined) {
367
+ print(runtime.io, runtime.json, {
368
+ ok: false,
369
+ command: check ? "trust check" : "trust diff",
370
+ error: "workspace_option_unsupported",
371
+ }, "kxm trust discovers the authoritative Git root from the current directory; --workspace is not supported");
372
+ return 2;
373
+ }
374
+ const command = check ? "trust check" : "trust diff";
375
+ try {
376
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
377
+ const bindings = readVnextLocalBindings(projectRoot, { stateRoot: vnextUserStateRoot({ env: runtime.env }) });
378
+ const diff = diffVnextProjectAgainstRevision(projectRoot, options.base?.trim() || "HEAD", {
379
+ repositoryBindings: bindings?.repositories ?? {},
380
+ });
381
+ const payload = {
382
+ ok: !check || !diff.requiresReview,
383
+ command,
384
+ baseRevision: diff.baseRevision,
385
+ candidateRevision: diff.candidateRevision,
386
+ requiresReview: diff.requiresReview,
387
+ expansions: diff.expansions.length,
388
+ narrowings: diff.narrowings.length,
389
+ neutralChanges: diff.neutralChanges.length,
390
+ changes: diff.changes,
391
+ };
392
+ const text = formatVnextPermissionDiff(diff);
393
+ if (check && diff.requiresReview) {
394
+ print(runtime.io, runtime.json, payload, `${text}\ntrust check failed: review every expansion above before merging`);
395
+ return 1;
396
+ }
397
+ print(runtime.io, runtime.json, payload, text);
398
+ return 0;
399
+ } catch (error) {
400
+ if (error instanceof VnextConfigError) {
401
+ print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_failed", issues: error.issues }, `permission diff failed: ${error.message}`);
402
+ return 1;
403
+ }
404
+ print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_io_failed" }, "permission diff failed because a local filesystem or Git operation did not complete");
405
+ return 1;
406
+ }
407
+ }
408
+
409
+ export const RUN_ENGINE_PHASE = "pre-3a";
410
+ export const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
411
+
412
+ export async function cmdVnextRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
413
+ if (runtime.workspaceFlag !== undefined) {
414
+ print(runtime.io, runtime.json, {
415
+ ok: false,
416
+ command: "run",
417
+ error: "workspace_option_unsupported",
418
+ }, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
419
+ return 2;
420
+ }
421
+ if (!workflow) {
422
+ print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
423
+ return 2;
424
+ }
425
+ try {
426
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
427
+ if (!projectRoot) {
428
+ print(runtime.io, runtime.json, { ok: false, command: "run", error: "project_required" }, "kxm run requires a vNext project (run kxm init first)");
429
+ return 1;
430
+ }
431
+ const bundle = loadVnextProject(projectRoot, {});
432
+ if (!bundle.workflows.has(workflow)) {
433
+ print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
434
+ return 1;
435
+ }
436
+ if (runtime.dryRun) {
437
+ print(runtime.io, runtime.json, {
438
+ ok: true,
439
+ command: "run",
440
+ dryRun: true,
441
+ projectRoot,
442
+ workflowId: workflow,
443
+ configRevision: bundle.configRevision,
444
+ }, `run plan: workflow ${workflow} at ${bundle.configRevision.slice(0, 19)}… (no run created)`);
445
+ return 0;
446
+ }
447
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
448
+ const prompt = promptParts.join(" ").trim();
449
+ const acceptance = await vnextRuntimeRequest(supervisor, "POST", "/v1/runs", {
450
+ projectRoot,
451
+ workflowId: workflow,
452
+ prompt,
453
+ });
454
+ const run = acceptance.run as { runId: string; homeRuntimeId: string; status: string; configRevision: string };
455
+ print(runtime.io, runtime.json, {
456
+ ok: true,
457
+ command: "run",
458
+ phase: RUN_ENGINE_PHASE,
459
+ idempotent: acceptance.idempotent === true,
460
+ run,
461
+ supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
462
+ }, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${RUN_ENGINE_NOTICE}`);
463
+ return 0;
464
+ } catch (error) {
465
+ if (error instanceof VnextConfigError) {
466
+ print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_failed", issues: error.issues }, `run failed: ${error.message}`);
467
+ return 1;
468
+ }
469
+ print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_io_failed" }, "run failed because a local operation did not complete");
470
+ return 1;
471
+ }
472
+ }
473
+
474
+ export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
475
+ try {
476
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
477
+ if (!projectRoot) {
478
+ print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a vNext project (run kxm init first)");
479
+ return 1;
480
+ }
481
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
482
+ const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
483
+ const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
484
+ print(runtime.io, runtime.json, { ok: true, command: "runs status", run }, `run ${run.runId}: ${run.status} (workflow ${run.workflowId}, updated ${run.updatedAt})`);
485
+ return 0;
486
+ } catch (error) {
487
+ if (error instanceof VnextConfigError) {
488
+ print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_failed", issues: error.issues }, `run status failed: ${error.message}`);
489
+ return 1;
490
+ }
491
+ print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_io_failed" }, "run status failed because a local operation did not complete");
492
+ return 1;
493
+ }
494
+ }
495
+
496
+ export async function cmdVnextRunDrive(runtime: Runtime, runId: string, simulated: boolean): Promise<number> {
497
+ try {
498
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
499
+ if (!projectRoot) {
500
+ print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "project_required" }, "kxm runs drive requires a vNext project (run kxm init first)");
501
+ return 1;
502
+ }
503
+ const mode = simulated ? "simulated" : "live";
504
+ if (runtime.dryRun) {
505
+ print(runtime.io, runtime.json, { ok: true, command: "runs drive", dryRun: true, runId, mode }, `drive plan: run ${runId} in ${mode} mode (no events written)`);
506
+ return 0;
507
+ }
508
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
509
+ const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/drive?projectRoot=${encodeURIComponent(projectRoot)}`, { mode });
510
+ print(runtime.io, runtime.json, { ok: true, command: "runs drive", run: result.run, handoff: result.handoff, events: result.events }, `run ${runId}: ${(result.run as { status?: string } | undefined)?.status ?? "driven"}`);
511
+ return 0;
512
+ } catch (error) {
513
+ if (error instanceof VnextConfigError) {
514
+ print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_failed", issues: error.issues }, `run drive failed: ${error.message}`);
515
+ return 1;
516
+ }
517
+ print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_io_failed" }, "run drive failed because a local operation did not complete");
518
+ return 1;
519
+ }
520
+ }
521
+
522
+ export async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
523
+ try {
524
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
525
+ if (!projectRoot) {
526
+ print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "project_required" }, "kxm runs cancel requires a vNext project (run kxm init first)");
527
+ return 1;
528
+ }
529
+ if (runtime.dryRun) {
530
+ print(runtime.io, runtime.json, { ok: true, command: "runs cancel", dryRun: true, runId }, `cancel plan: run ${runId} (no events written)`);
531
+ return 0;
532
+ }
533
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
534
+ const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/cancel?projectRoot=${encodeURIComponent(projectRoot)}`, {});
535
+ const run = result.run as { runId: string; status: string };
536
+ print(runtime.io, runtime.json, {
537
+ ok: true,
538
+ command: "runs cancel",
539
+ idempotent: result.idempotent === true,
540
+ run,
541
+ }, `run ${run.runId}: ${run.status}`);
542
+ return 0;
543
+ } catch (error) {
544
+ if (error instanceof VnextConfigError) {
545
+ print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_failed", issues: error.issues }, `run cancel failed: ${error.message}`);
546
+ return 1;
547
+ }
548
+ print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_io_failed" }, "run cancel failed because a local operation did not complete");
549
+ return 1;
550
+ }
551
+ }
552
+
553
+ export async function cmdVnextRunList(runtime: Runtime): Promise<number> {
554
+ try {
555
+ const projectRoot = discoverVnextProjectRoot(runtime.cwd);
556
+ if (!projectRoot) {
557
+ print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "project_required" }, "kxm runs list requires a vNext project (run kxm init first)");
558
+ return 1;
559
+ }
560
+ const bundle = loadVnextProject(projectRoot, {});
561
+ const projectId = String(bundle.project.value.id);
562
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
563
+ const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(projectRoot)}`);
564
+ const runs = (result.runs ?? []) as Array<{ runId: string; status: string; workflowId: string; createdAt: string }>;
565
+ print(
566
+ runtime.io,
567
+ runtime.json,
568
+ { ok: true, command: "runs list", runs },
569
+ runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}`).join("\n"),
570
+ );
571
+ return 0;
572
+ } catch (error) {
573
+ if (error instanceof VnextConfigError) {
574
+ print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_failed", issues: error.issues }, `run list failed: ${error.message}`);
575
+ return 1;
576
+ }
577
+ print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_io_failed" }, "run list failed because a local operation did not complete");
578
+ return 1;
579
+ }
580
+ }
581
+
582
+ export async function cmdHarnessList(runtime: Runtime): Promise<number> {
583
+ const inventory = probeHarnesses({ env: runtime.env });
584
+ print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
585
+ return 0;
586
+ }
587
+
588
+ export async function selectProducerModel(runtime: Runtime, requested?: string | undefined): Promise<string | undefined> {
589
+ const models = listInventoryModels(runtime.dirs.workdir);
590
+ if (requested) return models.find((model) => model.toLowerCase() === requested.toLowerCase());
591
+ if (runtime.json || !process.stdin.isTTY || models.length === 0) return undefined;
592
+ runtime.io.stdout(models.slice(0, 100).map((model, index) => `${index + 1}. ${model}`).join("\n") + "\nSelect model number: ");
593
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
594
+ const answer = await new Promise<string>((resolve) => rl.once("line", resolve));
595
+ rl.close();
596
+ const index = Number.parseInt(answer.trim(), 10) - 1;
597
+ return Number.isInteger(index) && index >= 0 && index < models.length ? models[index] : undefined;
598
+ }
599
+
600
+ export async function cmdProducerChange(runtime: Runtime, status: "promoted" | "demoted", requested?: string | undefined): Promise<number> {
601
+ const model = await selectProducerModel(runtime, requested);
602
+ if (!model) { print(runtime.io, runtime.json, { ok: false, command: `producers ${status}`, error: "model_selection_required" }, "select a model from the refreshed inventory"); return 2; }
603
+ if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, dryRun: true }, `would ${status} ${model}`); return 0; }
604
+ const policy = updateProducer(runtime.dirs.workdir, model, status);
605
+ print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, policy }, `${status} ${model}`);
606
+ return 0;
607
+ }
608
+
609
+ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
610
+ const models = listInventoryModels(runtime.dirs.workdir);
611
+ if (runtime.json || !process.stdin.isTTY) { print(runtime.io, runtime.json, { ok: false, command: "models", error: "interactive_tty_required" }, "kxm models requires an interactive terminal"); return 2; }
612
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
613
+ const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
614
+ try {
615
+ while (true) {
616
+ const policy = loadProducerPolicy(runtime.dirs.workdir);
617
+ const roleBindings = listRoleBindings(runtime.dirs.workdir);
618
+ runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.enabled.includes(m) ? "enabled" : policy.disabled.includes(m) ? "disabled" : "unset"}] [${policy.promoted.includes(m) ? "producer" : policy.demoted.includes(m) ? "demoted" : "not producer"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
619
+ const command = (await ask("[e]nable [d]isable [p]romote [x]demote [a]dd-role [r]emove-role [q]uit: ")).trim().toLowerCase();
620
+ if (command === "q" || command === "quit") return 0;
621
+ const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
622
+ if (!Number.isInteger(index) || !models[index]) continue;
623
+ const model = models[index];
624
+ if (command === "p" || command === "x") updateProducer(runtime.dirs.workdir, model, command === "p" ? "promoted" : "demoted");
625
+ else if (command === "e" || command === "d") setModelState(runtime.dirs.workdir, model, command === "e" ? "enabled" : "disabled");
626
+ else if (command === "a" || command === "r") setModelState(runtime.dirs.workdir, model, "enabled", (await ask("role: ")).trim(), command === "r");
627
+ }
628
+ } finally { rl.close(); }
629
+ }
630
+
631
+ export async function cmdProducerList(runtime: Runtime): Promise<number> {
632
+ const policy = loadProducerPolicy(runtime.dirs.workdir);
633
+ print(runtime.io, runtime.json, { ok: true, command: "producers list", policy }, [...policy.promoted.map((x) => `promoted ${x}`), ...policy.demoted.map((x) => `demoted ${x}`)].join("\n") || "no producer decisions");
634
+ return 0;
635
+ }
636
+
637
+ export async function cmdModelInventoryRefresh(runtime: Runtime): Promise<number> {
638
+ if (runtime.dryRun) {
639
+ print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
640
+ return 0;
641
+ }
642
+ const inventory = await refreshModelInventory({ outputRoot: runtime.dirs.workdir, env: runtime.env });
643
+ const failed = Object.values(inventory.sources).some((source) => !source.ok);
644
+ print(runtime.io, runtime.json, { ok: !failed, command: "models inventory refresh", output: ".kxm/models/inventory.yaml", ...inventory }, `wrote ${inventory.models.length} models to .kxm/models/inventory.yaml`);
645
+ return failed ? 1 : 0;
646
+ }
647
+
648
+ export async function cmdVnextRuntime(runtime: Runtime, action: string): Promise<number> {
649
+ const paths = vnextRuntimePaths({ env: runtime.env });
650
+ try {
651
+ if (action === "start") {
652
+ if (runtime.dryRun) {
653
+ print(runtime.io, runtime.json, { ok: true, command: "runtime start", dryRun: true }, "runtime supervisor would auto-start");
654
+ return 0;
655
+ }
656
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
657
+ print(runtime.io, runtime.json, {
658
+ ok: true,
659
+ command: "runtime start",
660
+ runtimeId: supervisor.runtimeId,
661
+ port: supervisor.port,
662
+ started: supervisor.started,
663
+ }, `runtime supervisor ${supervisor.started ? "started" : "already running"}: ${supervisor.runtimeId} on 127.0.0.1:${supervisor.port}`);
664
+ return 0;
665
+ }
666
+ if (action === "status") {
667
+ const status = vnextSupervisorStatus(paths);
668
+ print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status }, status.running
669
+ ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}`
670
+ : "runtime supervisor is not running");
671
+ return status.running ? 0 : 1;
672
+ }
673
+ if (action === "stop") {
674
+ const status = vnextSupervisorStatus(paths);
675
+ if (!status.running || !status.port) {
676
+ print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: false }, "runtime supervisor is not running");
677
+ return 0;
678
+ }
679
+ if (runtime.dryRun) {
680
+ print(runtime.io, runtime.json, { ok: true, command: "runtime stop", dryRun: true }, `would stop runtime supervisor pid ${status.pid}`);
681
+ return 0;
682
+ }
683
+ const supervisor = await ensureVnextSupervisor({ env: runtime.env });
684
+ await vnextRuntimeRequest(supervisor, "POST", "/v1/shutdown", {});
685
+ print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: true }, `runtime supervisor ${status.runtimeId} stopping`);
686
+ return 0;
687
+ }
688
+ print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "unknown_action" }, `unknown runtime action: ${action}`);
689
+ return 2;
690
+ } catch (error) {
691
+ if (error instanceof VnextConfigError) {
692
+ print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_failed", issues: error.issues }, `runtime failed: ${error.message}`);
693
+ return 1;
694
+ }
695
+ print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_io_failed" }, "runtime failed because a local operation did not complete");
696
+ return 1;
697
+ }
698
+ }