@kontextmind/kxm 0.7.5 → 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.
@@ -1,4267 +1,180 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { createHash, createHmac, randomUUID } from "node:crypto";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { homedir, tmpdir } from "node:os";
5
- import { basename, dirname, join, relative, resolve } from "node:path";
6
- import { DatabaseSync } from "./sqlite.ts";
7
- import { createInterface } from "node:readline";
8
- import { fileURLToPath } from "node:url";
9
- import { Command, CommanderError } from "commander";
10
- import {
11
- createMemoryNote,
12
- formatMemoryBriefText,
13
- generateMemoryBrief,
14
- syncHarnessMemory,
15
- type MemoryScope,
16
- } from "./memory.ts";
17
- import { createBackup, restoreBackup } from "./database.ts";
18
- import { verifyArtifactExists } from "./artifacts-exist.ts";
19
- import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions, resumeWorkflowFromRuling } from "./workflow.ts";
20
- import { postWorkflowSignal, watchGithubChecks } from "./github-watch.ts";
21
- import { buildRetrospective, writeRetrospective } from "./retrospective.ts";
22
- import { redactSecrets } from "./redact.ts";
23
- import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "./skills.ts";
24
- import { writeCompiledWiki } from "./wiki.ts";
25
- import { agentWorker, gateWorker, workerResult, type Worker, type WorkerOutcome } from "./envelope.ts";
26
- import { appendTelemetry, inferImprovementTarget, makeTelemetryEvent, readTelemetry, readRoutingRecords, telemetryPath } from "./telemetry.ts";
27
- import { behavioralConfigHash, compareRoutingRecords, groupByBehavior, generateRoutingReport, formatRoutingReport } from "./routing.ts";
28
- import { loadPriceCatalog, type PriceCatalog } from "./prices.ts";
29
- import { refreshModelInventory } from "./model-inventory.ts";
30
- import { listInventoryModels, listRoleBindings, loadProducerPolicy, setModelState, updateProducer } from "./producers.ts";
31
- import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "./session.ts";
32
- import { buildImprovementReport, formatImprovementReport, writeImprovementReport } from "./improve.ts";
33
- import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "./tui.ts";
34
- import { formatSessionBriefText, loadSessionBrief, loadSessionBriefAsync, type SessionHubStatus } from "./session-work.ts";
35
- import {
36
- HUB_BINDING_SCHEMA,
37
- HubBindingError,
38
- hubBindingFile,
39
- probeHubHealth,
40
- readHubBinding,
41
- removeHubBinding,
42
- validateHubUrl,
43
- writeHubBinding,
44
- type HubHealth,
45
- } from "./hub-binding.ts";
46
- import {
47
- fetchLatestKxmVersion,
48
- kxmReleaseAssetName,
49
- noticeFromVersions,
50
- planKxmPackageUpdate,
51
- readInstalledKxmVersion,
52
- readUpdateCache,
53
- verifyReleaseAssetDigest,
54
- writeUpdateCache,
55
- KxmUpdateConfigError,
56
- type KxmPackageUpdateStep,
57
- type KxmUpdateConfig,
58
- type KxmUpdateNotice,
59
- } from "./kxm-update.ts";
60
- import { loadKxmUpdateConfig } from "./kxm-update-config.ts";
61
- import {
62
- classifyInstallRoot,
63
- resolveInstallKind,
64
- type InstallKindReport,
65
- type InstallProbe,
66
- } from "./kxm-install-kind.ts";
67
- import { VnextConfigError, discoverVnextProjectRoot, type VnextInitializationPlan } from "./vnext-config.ts";
68
- import {
69
- GUIDE_WORKFLOWS,
70
- parseGuideSelection,
71
- planGuideSetup,
72
- renderGuideSetupFiles,
73
- writeGuideSetupFiles,
74
- } from "./init-guide-setup.ts";
75
- import { vnextUserStateRoot } from "./vnext-bindings.ts";
76
- import { initializeVnextProject } from "./vnext-init.ts";
77
- import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "./vnext-migrate.ts";
78
- import { diffVnextProjectAgainstRevision, formatVnextPermissionDiff } from "./vnext-permission.ts";
79
- import { readVnextLocalBindings } from "./vnext-bindings.ts";
80
- import { loadVnextProject } from "./vnext-config.ts";
81
- import {
82
- ensureVnextSupervisor,
83
- vnextRuntimeRequest,
84
- vnextSupervisorStatus,
85
- } from "./vnext-runtime-supervisor.ts";
86
- import { vnextRuntimePaths } from "./vnext-runtime-store.ts";
87
- import {
88
- formatHarnessInventory,
89
- formatHarnessUpdate,
90
- planHarnessUpdate,
91
- probeHarnesses,
92
- runHarnessUpdate,
93
- type HarnessUpdateScope,
94
- } from "./vnext-harness.ts";
95
- import type { WorkflowEvidenceInput, WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
96
- import { HubClient } from "./client.ts";
97
- import {
98
- AGENT_COMMANDS_MAP,
99
- clearSessionTokenFromDisk,
100
- enforceToolPolicy,
101
- mintSessionToken,
102
- parseSessionToken,
103
- persistSessionTokenToDisk,
104
- readSessionTokenFromDisk,
105
- sessionTokenPath,
106
- } from "./commands.ts";
107
- import {
108
- loadKxmConfig,
109
- setKxmConfigValue,
110
- getKxmConfigValue,
111
- formatKxmConfig,
112
- } from "./config.ts";
113
- import { generateShellCompletion, type SupportedShell } from "./autocomplete.ts";
114
- import { completionRcTarget, completionScriptPath, detectShell, installPathEntry, installShellCompletion, kxmBinDir } from "./completion-install.ts";
115
- import { suggestWorkflowAndRoles } from "./suggest.ts";
116
- import {
117
- createGoal,
118
- createTask,
119
- listGoals,
120
- listTasks,
121
- getTask,
122
- updateTaskStatus,
123
- syncTaskWithTracker,
124
- type TaskStatus,
125
- type TrackerType,
126
- } from "./task-manager.ts";
127
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
128
- import { compileVnextWorkflow } from "./vnext-engine-compile.ts";
129
- import { generateStudioLayout, createStudioServer, DEFAULT_STUDIO_PORT } from "./studio-layout.ts";
130
- import {
131
- listRoles,
132
- getRole,
133
- addRole,
134
- removeRole,
135
- modifyRole,
136
- DEFAULT_ROLES,
137
- DEFAULT_ROLE_SEATS,
138
- loadRoleHostsConfig,
139
- setRoleSeatHost,
140
- resolveRoleSeat,
141
- type KxmRoleDefinition,
142
- } from "./role.ts";
143
- import {
144
- listWorkflowDefinitions,
145
- addWorkflowDefinition,
146
- removeWorkflowDefinition,
147
- modifyWorkflowDefinition,
148
- WORKFLOW_TEMPLATES,
149
- DEFAULT_WORKFLOW_TEMPLATE,
150
- } from "./workflow-manager.ts";
151
-
152
- export interface CliSpawnResult {
153
- status: number | null;
154
- stdout: string;
155
- stderr: string;
156
- error?: Error;
157
- }
158
-
159
- export interface CliIo {
160
- stdout: (text: string) => void;
161
- stderr: (text: string) => void;
162
- fetchImpl?: typeof fetch;
163
- now?: () => number;
164
- sleep?: (ms: number) => Promise<void>;
165
- spawnHub?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
166
- spawnWorker?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
167
- installProbe?: Partial<InstallProbe>;
168
- spawnSync?: (command: string, args: readonly string[]) => CliSpawnResult;
169
- }
170
-
171
- const CLI_NAME = "kxm";
172
- const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
173
- const USAGE_ERROR_CODES = new Set([
174
- "commander.help",
175
- "commander.helpDisplayed",
176
- "commander.unknownCommand",
177
- "commander.unknownOption",
178
- "commander.missingArgument",
179
- "commander.missingMandatoryOptionValue",
180
- "commander.invalidArgument",
181
- "commander.excessArguments",
182
- "commander.variadicArgNotLast",
183
- "commander.invalidOptionArgument",
184
- "commander.optionMissingArgument",
185
- ]);
186
-
187
- interface CliContext {
188
- env: NodeJS.ProcessEnv;
189
- io: CliIo;
190
- cwd: string;
191
- }
192
-
193
- interface GlobalOpts {
194
- json?: boolean;
195
- dryRun?: boolean;
196
- workspace?: string;
197
- }
198
-
199
- interface Runtime extends CliContext, Required<Pick<GlobalOpts, "json" | "dryRun">> {
200
- workspaceFlag?: string;
201
- dirs: ReturnType<typeof workspaceDirs>;
202
- serverUrl: string;
203
- boundHubUrl?: string;
204
- fetchImpl: typeof fetch;
205
- }
206
-
207
- function spawnScript(scriptName: string, extraEnv: NodeJS.ProcessEnv = {}): Promise<number> {
208
- return new Promise((resolveExit) => {
209
- const child = spawn(process.execPath, [join(repoRoot, "scripts", scriptName)], {
210
- stdio: "inherit",
211
- env: { ...process.env, ...extraEnv },
212
- });
213
- child.once("error", () => resolveExit(1));
214
- child.once("exit", (code) => resolveExit(code ?? 1));
215
- });
216
- }
217
-
218
- function parseEvidencePairs(values: string[]): WorkflowEvidenceInput {
219
- const evidence = new Map<string, string>();
220
- for (const value of values) {
221
- const separator = value.indexOf("=");
222
- if (separator <= 0 || separator === value.length - 1) {
223
- throw new Error("evidence must use <required-key>=<evidence> syntax");
224
- }
225
- const requirement = canonicalWorkflowEvidenceKey(value.slice(0, separator));
226
- const proof = value.slice(separator + 1).trim();
227
- if (!requirement || !proof) throw new Error("evidence must use <required-key>=<evidence> syntax");
228
- if (evidence.has(requirement)) throw new Error(`duplicate normalized evidence key: ${requirement}`);
229
- evidence.set(requirement, proof);
230
- }
231
- return Object.fromEntries(evidence);
232
- }
233
-
234
- const CLI_RESULT_SCHEMA = "kxm.cli-result.v1";
235
-
236
- /** Every CLI payload carries a schema tag; payloads that already declare one
237
- * (worker envelopes) keep theirs. `ok:false` goes to stderr in both modes so
238
- * agents can split results from errors without parsing. */
239
- function print(io: CliIo, jsonMode: boolean, payload: object, text: string): void {
240
- const tagged = { schema: CLI_RESULT_SCHEMA, ...payload } as Record<string, unknown>;
241
- const safePayload = JSON.stringify(redactCliValue(tagged));
242
- const line = jsonMode ? `${safePayload}\n` : `${redactSecrets(text)}\n`;
243
- if (tagged.ok === false) io.stderr(line);
244
- else io.stdout(line);
245
- }
246
-
247
- function printWorker(
248
- runtime: Runtime,
249
- worker: Worker,
250
- payload: Record<string, unknown> & { command: string; ok: boolean },
251
- text: string,
252
- outcome?: WorkerOutcome,
253
- ): void {
254
- const sessionId = runtime.env.KXM_SESSION_ID?.trim();
255
- const envelope = workerResult(worker, {
256
- ...payload,
257
- summary: text,
258
- ...(outcome ? { outcome } : {}),
259
- ...(sessionId ? { sessionId } : {}),
260
- });
261
- if (!runtime.dryRun) {
262
- try {
263
- const safeEnvelope = redactCliValue(envelope) as typeof envelope;
264
- appendTelemetry(telemetryPath(runtime.dirs.logs), makeTelemetryEvent({
265
- envelope: safeEnvelope,
266
- ...(sessionId ? { sessionId } : {}),
267
- host: hostMode(runtime),
268
- target: inferImprovementTarget({
269
- ...(worker.project ? { project: worker.project } : {}),
270
- env: runtime.env,
271
- }),
272
- }));
273
- } catch {
274
- // Telemetry must never fail the operator command.
275
- }
276
- }
277
- print(runtime.io, runtime.json, envelope, text);
278
- }
279
-
280
- function hostMode(runtime: Runtime): "local" | "hub" {
281
- try {
282
- const hostname = new URL(runtime.serverUrl).hostname;
283
- if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
284
- } catch {
285
- // Default local when the hub URL is not a URL.
286
- }
287
- return "hub";
288
- }
289
-
290
- function gateOf(runtime: Runtime, name: string): ReturnType<typeof gateWorker> {
291
- const project = runtime.env.KXM_PROJECT?.trim();
292
- return gateWorker({ name, ...(project ? { project } : {}) });
293
- }
294
-
295
- function redactCliValue(value: unknown, field = ""): unknown {
296
- if (typeof value === "string") {
297
- // These fields are public audit digests, not credentials. Preserve
298
- // them only by exact field name and shape; every other 64-hex value keeps
299
- // the conservative generic redaction behavior.
300
- if (
301
- (field === "requestSha256"
302
- || field === "replySha256"
303
- || field === "behavioralSha256"
304
- || field === "workflowDefinitionSha256"
305
- || field === "verifierConfigSha256"
306
- || field === "rolePromptSha256"
307
- || field === "contentSha256"
308
- || field === "configRevision"
309
- || field === "baseSha256"
310
- || field === "localSha256"
311
- || field === "targetSha256"
312
- || field === "sourceTemplateRevision"
313
- || field === "targetTemplateRevision"
314
- || field === "sourceDigest"
315
- || field === "decisionDigest"
316
- || field === "receiptSha256"
317
- || field === "sha256"
318
- || field === "valueSha256"
319
- || field === "baseRevision"
320
- || field === "candidateRevision"
321
- || field === "baseValueSha256"
322
- || field === "candidateValueSha256")
323
- && /^(?:sha256:)?[a-f0-9]{64}$/.test(value)
324
- ) return value;
325
- return redactSecrets(value);
326
- }
327
- if (Array.isArray(value)) return value.map((candidate) => redactCliValue(candidate));
328
- if (value && typeof value === "object") {
329
- return Object.fromEntries(
330
- Object.entries(value as Record<string, unknown>)
331
- .map(([key, candidate]) => [key, redactCliValue(candidate, key)]),
332
- );
333
- }
334
- return value;
335
- }
336
-
337
- function workspaceDirs(cwd: string, workspaceFlag: string | undefined, env: NodeJS.ProcessEnv) {
338
- const workdir = resolve(env.KXM_WORKDIR?.trim() || cwd);
339
- const workspace = resolve(workdir, workspaceFlag || env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
340
- const derive = workspaceFlag !== undefined;
341
- return {
342
- workdir,
343
- workspace,
344
- config: derive ? join(workspace, "config") : resolve(workdir, env.KXM_CONFIG_DIR?.trim() || join(workspace, "config")),
345
- logs: derive ? join(workspace, "logs") : resolve(workdir, env.KXM_LOGS_DIR?.trim() || join(workspace, "logs")),
346
- assets: derive ? join(workspace, "assets") : resolve(workdir, env.KXM_ASSETS_DIR?.trim() || join(workspace, "assets")),
347
- state: derive ? join(workspace, "state") : resolve(workdir, env.KXM_STATE_DIR?.trim() || join(workspace, "state")),
348
- };
349
- }
350
-
351
- function maskEnvName(name: string): boolean {
352
- return /TOKEN|SECRET|KEY|PASSWORD/i.test(name);
353
- }
354
-
355
- function redactConfiguredValues(text: string, env: NodeJS.ProcessEnv): string {
356
- let safe = text;
357
- for (const [name, value] of Object.entries(env)) {
358
- if (!maskEnvName(name) || !value || value.length < 4) continue;
359
- safe = safe.replaceAll(value, "[redacted]");
360
- }
361
- const trailingNewline = safe.endsWith("\n") ? "\n" : "";
362
- try {
363
- const parsed = JSON.parse(safe) as unknown;
364
- return `${JSON.stringify(redactCliValue(parsed))}${trailingNewline}`;
365
- } catch {
366
- // Human-readable output and diagnostics keep conservative generic
367
- // redaction, including opaque 64-hex values.
368
- }
369
- return redactSecrets(safe);
370
- }
371
-
372
- function processExists(pid: number): boolean {
373
- try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
374
- }
375
-
376
- /** Authenticated hub POST for context operations. The CLI operates as the
377
- * control plane: the administrative token scopes one project per request. */
378
- export async function hubContextPost(input: {
379
- serverUrl: string;
380
- path: string;
381
- body: Record<string, unknown>;
382
- authToken?: string;
383
- fetchImpl: typeof fetch;
384
- }): Promise<{ ok: boolean; status: number; body: unknown }> {
385
- const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}${input.path}`, {
386
- method: "POST",
387
- headers: {
388
- "content-type": "application/json",
389
- ...(input.authToken ? { authorization: `Bearer ${input.authToken}` } : {}),
390
- },
391
- body: JSON.stringify(input.body),
392
- });
393
- const text = redactSecrets((await response.text()).slice(0, 64_000));
394
- let body: unknown = text;
395
- try {
396
- body = JSON.parse(text);
397
- } catch {
398
- // Keep text for diagnostics without treating it as a secret.
399
- }
400
- return { ok: response.ok, status: response.status, body };
401
- }
402
-
403
- async function hubGet(url: string, fetchImpl: typeof fetch): Promise<{ ok: boolean; status: number; body: unknown }> {
404
- try {
405
- const response = await fetchImpl(url);
406
- const text = redactSecrets((await response.text()).slice(0, 8_000));
407
- let body: unknown = text;
408
- try {
409
- body = JSON.parse(text);
410
- } catch {
411
- // Keep text for diagnostics without treating it as a secret.
412
- }
413
- return { ok: response.ok, status: response.status, body };
414
- } catch {
415
- return { ok: false, status: 0, body: { error: "hub_unreachable" } };
416
- }
417
- }
418
-
419
- function localWorkflowSnapshot(dataPath: string, runId?: string): { runs: WorkflowRun[]; journal: WorkflowJournalEntry[] } {
420
- if (!existsSync(dataPath)) throw new Error("state_database_not_found");
421
- const database = new DatabaseSync(dataPath, { readOnly: true });
422
- try {
423
- const rows = runId
424
- ? database.prepare("SELECT record FROM workflow_runs WHERE id = ?").all(runId) as Array<{ record: string }>
425
- : database.prepare("SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 200").all() as Array<{ record: string }>;
426
- const runs = rows.map((row) => JSON.parse(row.record) as WorkflowRun);
427
- const journal = runId
428
- ? (database.prepare("SELECT record FROM workflow_journal WHERE run_id = ? ORDER BY rowid").all(runId) as Array<{ record: string }>).map((row) => JSON.parse(row.record) as WorkflowJournalEntry)
429
- : [];
430
- return { runs, journal };
431
- } finally { database.close(); }
432
- }
433
-
434
- async function postWorkflowStart(input: { serverUrl: string; definitionId: string; secret: string; deliveryId: string; event?: string; payload: Record<string, unknown>; fetchImpl: typeof fetch }): Promise<{ status: number; runId?: string; duplicate?: boolean }> {
435
- const payload = input.event && input.payload.event === undefined ? { ...input.payload, event: input.event } : input.payload;
436
- const body = JSON.stringify(payload);
437
- const signature = `sha256=${createHmac("sha256", input.secret).update(body).digest("hex")}`;
438
- const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}/v1/webhooks/${encodeURIComponent(input.definitionId)}`, {
439
- method: "POST",
440
- headers: { "content-type": "application/json", "x-hub-signature-256": signature, "x-kxm-delivery-id": input.deliveryId, ...(input.event ? { "x-github-event": input.event } : {}) },
441
- body,
442
- });
443
- const responseText = (await response.text()).slice(0, 8_000);
444
- let parsed: { run?: { id?: string }; duplicate?: boolean } = {};
445
- try { parsed = JSON.parse(responseText) as typeof parsed; } catch { /* bounded adapter error */ }
446
- if (!response.ok) throw new Error(`workflow_start_http_${response.status}`);
447
- return { status: response.status, ...(parsed.run?.id ? { runId: parsed.run.id } : {}), duplicate: parsed.duplicate === true };
448
- }
449
-
450
- async function postWorkflowDegradation(input: {
451
- serverUrl: string;
452
- authToken: string;
453
- runId: string;
454
- stageId: string;
455
- requirementKey: string;
456
- reason: string;
457
- fetchImpl: typeof fetch;
458
- }): Promise<{ status: number; duplicate: boolean; approvalId?: string }> {
459
- const response = await input.fetchImpl(
460
- `${input.serverUrl.replace(/\/$/, "")}/v1/workflows/${encodeURIComponent(input.runId)}/degradations`,
461
- {
462
- method: "POST",
463
- headers: {
464
- authorization: `Bearer ${input.authToken}`,
465
- "content-type": "application/json",
466
- },
467
- body: JSON.stringify({
468
- stageId: input.stageId,
469
- requirementKey: input.requirementKey,
470
- reason: input.reason,
471
- }),
472
- },
473
- );
474
- const text = (await response.text()).slice(0, 8_000);
475
- let parsed: { duplicate?: boolean; approval?: { id?: string } } = {};
476
- try { parsed = JSON.parse(text) as typeof parsed; } catch { /* bounded adapter error */ }
477
- if (!response.ok) throw new Error(`workflow_degradation_http_${response.status}`);
478
- return {
479
- status: response.status,
480
- duplicate: parsed.duplicate === true,
481
- ...(parsed.approval?.id ? { approvalId: parsed.approval.id } : {}),
482
- };
483
- }
484
-
485
- function addGlobalOptions(command: Command): Command {
486
- return command
487
- .option("--json", "Print machine-readable JSON")
488
- .option("--dry-run", "Plan without making changes")
489
- .option("--workspace <dir>", "Workspace directory");
490
- }
491
-
492
- function runtimeFrom(ctx: CliContext, command: Command): Runtime {
493
- const opts = command.optsWithGlobals() as GlobalOpts;
494
- const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
495
- let boundHubUrl: string | undefined;
496
- try {
497
- boundHubUrl = readHubBinding(ctx.env)?.url;
498
- } catch (error) {
499
- if (error instanceof HubBindingError) {
500
- ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
501
- } else {
502
- throw error;
503
- }
504
- }
505
- return {
506
- ...ctx,
507
- json: Boolean(opts.json),
508
- dryRun: Boolean(opts.dryRun),
509
- ...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
510
- dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
511
- serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
512
- ...(boundHubUrl ? { boundHubUrl } : {}),
513
- fetchImpl: ctx.io.fetchImpl ?? fetch,
514
- };
515
- }
516
-
517
- function workspaceEnv(runtime: Runtime): NodeJS.ProcessEnv {
518
- return {
519
- KXM_WORKDIR: runtime.dirs.workdir,
520
- KXM_WORKSPACE_DIR: runtime.dirs.workspace,
521
- KXM_CONFIG_DIR: runtime.dirs.config,
522
- KXM_LOGS_DIR: runtime.dirs.logs,
523
- KXM_ASSETS_DIR: runtime.dirs.assets,
524
- KXM_STATE_DIR: runtime.dirs.state,
525
- };
526
- }
527
-
528
- function activeWorkflowDefinition(runtime: Runtime, definitionId: string) {
529
- const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
530
- const file = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
531
- if (inline && file) {
532
- throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE");
533
- }
534
- if (!inline && !file) return undefined;
535
- let raw: string;
536
- if (file) {
537
- try {
538
- raw = readFileSync(resolve(runtime.cwd, file), "utf8");
539
- } catch {
540
- throw new Error("workflow definition file is unavailable");
541
- }
542
- } else {
543
- raw = inline!;
544
- }
545
- const definition = parseWorkflowDefinitions(raw, runtime.env).find((candidate) => candidate.id === definitionId);
546
- if (!definition) throw new Error(`workflow definition not found: ${definitionId}`);
547
- return definition;
548
- }
549
-
550
- function workflowCredential(runtime: Runtime, definitionId: string, kind: "start" | "signal"): string | undefined {
551
- const definition = activeWorkflowDefinition(runtime, definitionId);
552
- if (definition) return kind === "start" ? definition.secret : definition.signalSecret ?? definition.secret;
553
- return kind === "start"
554
- ? runtime.env.KXM_WORKFLOW_SECRET?.trim()
555
- : runtime.env.KXM_WORKFLOW_SIGNAL_SECRET?.trim();
556
- }
557
-
558
- function reportWorkflowConfigError(runtime: Runtime, error: unknown): number {
559
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid workflow configuration";
560
- runtime.io.stderr(`${message}\n`);
561
- return 2;
562
- }
563
-
564
- function initPlanPayload(plan: VnextInitializationPlan): Record<string, unknown> {
565
- return {
566
- mode: plan.mode,
567
- inspectedFrom: plan.inspectedFrom,
568
- ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}),
569
- ...(plan.legacyRoot ? { legacyRoot: plan.legacyRoot } : {}),
570
- changesRequired: plan.changesRequired,
571
- legacyInputs: plan.legacyInputs,
572
- issues: plan.issues,
573
- ...(plan.configRevision ? { configRevision: plan.configRevision } : {}),
574
- };
575
- }
576
-
577
- function explicitRepositoryBindings(values: readonly string[]): Readonly<Record<string, string>> {
578
- const result: Record<string, string> = Object.create(null) as Record<string, string>;
579
- for (const value of values) {
580
- const separator = value.indexOf("=");
581
- const repositoryId = separator < 0 ? "" : value.slice(0, separator).trim();
582
- const path = separator < 0 ? "" : value.slice(separator + 1).trim();
583
- if (!repositoryId || !path) {
584
- throw new VnextConfigError([{
585
- phase: "discovery",
586
- code: "repository_binding_argument_invalid",
587
- file: "--repository",
588
- message: "repository bindings must use <id=absolute-path>",
589
- }]);
590
- }
591
- if (Object.hasOwn(result, repositoryId)) {
592
- throw new VnextConfigError([{
593
- phase: "discovery",
594
- code: "repository_binding_argument_duplicate",
595
- file: "--repository",
596
- message: `repository binding ${repositoryId} was supplied more than once`,
597
- }]);
598
- }
599
- result[repositoryId] = path;
600
- }
601
- return result;
602
- }
603
-
604
- async function cmdVnextInit(runtime: Runtime, options: { name?: string; projectId?: string; repository?: string[] }): Promise<number> {
605
- if (runtime.workspaceFlag !== undefined) {
606
- print(runtime.io, runtime.json, {
607
- ok: false,
608
- command: "init",
609
- error: "workspace_option_unsupported",
610
- }, "kxm init discovers the authoritative Git root from the current directory; --workspace is not supported");
611
- return 2;
612
- }
613
- try {
614
- const initialized = initializeVnextProject(runtime.cwd, {
615
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
616
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
617
- repositoryBindings: explicitRepositoryBindings(options.repository ?? []),
618
- localStateRoot: vnextUserStateRoot({ env: runtime.env }),
619
- dryRun: runtime.dryRun,
620
- });
621
- const payload = {
622
- ok: initialized.action !== "planned" || runtime.dryRun,
623
- command: "init",
624
- action: initialized.action,
625
- ...initPlanPayload(initialized.plan),
626
- files: initialized.files,
627
- ...(initialized.configRevision ? { configRevision: initialized.configRevision } : {}),
628
- ...(initialized.localBindingFile ? { localBindingFile: initialized.localBindingFile } : {}),
629
- ...(initialized.bindingsChanged === undefined ? {} : { bindingsChanged: initialized.bindingsChanged }),
630
- ...(initialized.repairPlan === undefined ? {} : { repairPlan: initialized.repairPlan }),
631
- ...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
632
- ...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
633
- plannedOnly: initialized.action === "planned",
634
- };
635
- const finishInit = (code: number, text: string): number => {
636
- print(runtime.io, runtime.json, payload, text);
637
- return code;
638
- };
639
- if (initialized.action === "created") {
640
- await maybeOfferCompletionInstall(runtime);
641
- await maybeOfferGuideSetup(runtime);
642
- return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
643
- }
644
- if (initialized.action === "joined") {
645
- await maybeOfferCompletionInstall(runtime);
646
- await maybeOfferGuideSetup(runtime);
647
- return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
648
- }
649
- if (initialized.action === "repaired") {
650
- return finishInit(0, `repaired vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
651
- }
652
- if (initialized.action === "resumed") {
653
- return finishInit(0, `resumed vNext ${initialized.transactionKind ?? "initialization"} at ${initialized.projectRoot ?? runtime.cwd}`);
654
- }
655
- if (initialized.action === "validated") {
656
- return finishInit(0, `validated vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
657
- }
658
- if (runtime.dryRun) {
659
- return finishInit(0, `init plan: ${initialized.plan.mode}`);
660
- }
661
- const next = initialized.plan.mode === "migrate"
662
- ? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
663
- : initialized.repairPlan?.issues.length
664
- ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
665
- : "partial or provenance-free vNext state requires explicit repair; no files were overwritten";
666
- return finishInit(1, next);
667
- } catch (error) {
668
- if (error instanceof VnextConfigError) {
669
- print(runtime.io, runtime.json, {
670
- ok: false,
671
- command: "init",
672
- error: "vnext_initialization_failed",
673
- issues: error.issues,
674
- }, `vNext initialization failed: ${error.message}`);
675
- return 1;
676
- }
677
- print(runtime.io, runtime.json, {
678
- ok: false,
679
- command: "init",
680
- error: "vnext_initialization_io_failed",
681
- }, "vNext initialization failed because a local filesystem operation did not complete");
682
- return 1;
683
- }
684
- }
685
-
686
- async function cmdVnextMigratePlan(runtime: Runtime): Promise<number> {
687
- if (runtime.workspaceFlag !== undefined) {
688
- print(runtime.io, runtime.json, {
689
- ok: false,
690
- command: "migrate plan",
691
- error: "workspace_option_unsupported",
692
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
693
- return 2;
694
- }
695
- try {
696
- const result = planVnextMigration(runtime.cwd, {});
697
- const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
698
- const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
699
- const payload = {
700
- ok: result.plan.canApply === true,
701
- command: "migrate plan",
702
- plan: result.plan,
703
- plannedOnly: result.plan.canApply !== true,
704
- };
705
- if (result.plan.canApply === true) {
706
- print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
707
- return 0;
708
- }
709
- print(
710
- runtime.io,
711
- runtime.json,
712
- payload,
713
- `migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
714
- );
715
- return 1;
716
- } catch (error) {
717
- if (error instanceof VnextConfigError) {
718
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
719
- return 1;
720
- }
721
- 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");
722
- return 1;
723
- }
724
- }
725
-
726
- async function cmdVnextMigrateApply(runtime: Runtime, options: { decisions?: string; projectId?: string; name?: string }): Promise<number> {
727
- if (runtime.workspaceFlag !== undefined) {
728
- print(runtime.io, runtime.json, {
729
- ok: false,
730
- command: "migrate apply",
731
- error: "workspace_option_unsupported",
732
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
733
- return 2;
734
- }
735
- try {
736
- const result = applyVnextMigration(runtime.cwd, {
737
- ...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
738
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
739
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
740
- localStateRoot: vnextUserStateRoot({ env: runtime.env }),
741
- dryRun: runtime.dryRun,
742
- });
743
- const payload = {
744
- ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
745
- command: "migrate apply",
746
- action: result.action,
747
- files: result.files,
748
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
749
- ...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
750
- plannedOnly: result.action === "planned",
751
- };
752
- if (result.action === "applied") {
753
- print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
754
- return 0;
755
- }
756
- if (result.action === "already-migrated") {
757
- print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
758
- return 0;
759
- }
760
- if (runtime.dryRun && result.plan?.canApply === true) {
761
- print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
762
- return 0;
763
- }
764
- const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
765
- print(
766
- runtime.io,
767
- runtime.json,
768
- { ...payload, plan: result.plan },
769
- `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")}`,
770
- );
771
- return 1;
772
- } catch (error) {
773
- if (error instanceof VnextConfigError) {
774
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
775
- return 1;
776
- }
777
- 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");
778
- return 1;
779
- }
780
- }
781
-
782
- async function cmdVnextMigrateVerify(runtime: Runtime): Promise<number> {
783
- if (runtime.workspaceFlag !== undefined) {
784
- print(runtime.io, runtime.json, {
785
- ok: false,
786
- command: "migrate verify",
787
- error: "workspace_option_unsupported",
788
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
789
- return 2;
790
- }
791
- let result: ReturnType<typeof verifyVnextMigration>;
792
- try {
793
- result = verifyVnextMigration(runtime.cwd, {});
794
- } catch (error) {
795
- if (error instanceof VnextConfigError) {
796
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
797
- return 1;
798
- }
799
- 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");
800
- return 1;
801
- }
802
- const payload = {
803
- ok: result.ok,
804
- command: "migrate verify",
805
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
806
- issues: result.issues,
807
- };
808
- if (result.ok) {
809
- print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
810
- return 0;
811
- }
812
- print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
813
- return 1;
814
- }
815
-
816
- async function cmdBackup(runtime: Runtime, options: { out?: string }): Promise<number> {
817
- try {
818
- const { manifest, outDir } = createBackup({
819
- projectRoot: runtime.cwd,
820
- ...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
821
- });
822
- const payload = {
823
- ok: true,
824
- command: "backup",
825
- backupId: manifest.backupId,
826
- outDir,
827
- manifest,
828
- };
829
- const summary = [
830
- `Created SQLite backup with ${manifest.stores.length} store(s):`,
831
- ...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
832
- `Manifest: ${join(outDir, "manifest.json")}`,
833
- ].join("\n");
834
- print(runtime.io, runtime.json, payload, summary);
835
- return 0;
836
- } catch (error) {
837
- if (error instanceof VnextConfigError) {
838
- print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
839
- return 1;
840
- }
841
- 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)}`);
842
- return 1;
843
- }
844
- }
845
-
846
- async function cmdRestore(runtime: Runtime, manifestArg: string): Promise<number> {
847
- try {
848
- const result = restoreBackup(resolve(runtime.cwd, manifestArg), {
849
- projectRoot: runtime.cwd,
850
- });
851
- const payload = {
852
- ok: true,
853
- command: "restore",
854
- backupId: result.backupId,
855
- manifestPath: result.manifestPath,
856
- restoredStores: result.restoredStores,
857
- };
858
- const summary = [
859
- `Restored ${result.restoredStores.length} SQLite store(s) from ${result.manifestPath}:`,
860
- ...result.restoredStores.map((s) => ` - ${s.storeId}: -> ${s.sourcePath} (schema v${s.schemaVersion}, integrity ${s.integrity})`),
861
- ].join("\n");
862
- print(runtime.io, runtime.json, payload, summary);
863
- return 0;
864
- } catch (error) {
865
- if (error instanceof VnextConfigError) {
866
- print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", issues: error.issues }, `restore failed: ${error.message}`);
867
- return 1;
868
- }
869
- 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)}`);
870
- return 1;
871
- }
872
- }
873
-
874
- async function cmdVnextTrust(runtime: Runtime, check: boolean, options: { base?: string }): Promise<number> {
875
- if (runtime.workspaceFlag !== undefined) {
876
- print(runtime.io, runtime.json, {
877
- ok: false,
878
- command: check ? "trust check" : "trust diff",
879
- error: "workspace_option_unsupported",
880
- }, "kxm trust discovers the authoritative Git root from the current directory; --workspace is not supported");
881
- return 2;
882
- }
883
- const command = check ? "trust check" : "trust diff";
884
- try {
885
- // Host-local member bindings travel with the user state, not Git; pass
886
- // them so member repositories diff with the same identities on both sides.
887
- const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
888
- const bindings = readVnextLocalBindings(projectRoot, { stateRoot: vnextUserStateRoot({ env: runtime.env }) });
889
- const diff = diffVnextProjectAgainstRevision(projectRoot, options.base?.trim() || "HEAD", {
890
- repositoryBindings: bindings?.repositories ?? {},
891
- });
892
- const payload = {
893
- ok: !check || !diff.requiresReview,
894
- command,
895
- baseRevision: diff.baseRevision,
896
- candidateRevision: diff.candidateRevision,
897
- requiresReview: diff.requiresReview,
898
- expansions: diff.expansions.length,
899
- narrowings: diff.narrowings.length,
900
- neutralChanges: diff.neutralChanges.length,
901
- changes: diff.changes,
902
- };
903
- const text = formatVnextPermissionDiff(diff);
904
- if (check && diff.requiresReview) {
905
- print(runtime.io, runtime.json, payload, `${text}\ntrust check failed: review every expansion above before merging`);
906
- return 1;
907
- }
908
- print(runtime.io, runtime.json, payload, text);
909
- return 0;
910
- } catch (error) {
911
- if (error instanceof VnextConfigError) {
912
- print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_failed", issues: error.issues }, `permission diff failed: ${error.message}`);
913
- return 1;
914
- }
915
- 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");
916
- return 1;
917
- }
918
- }
919
-
920
- /** No step engine exists yet: `kxm run` durably accepts a run and stops.
921
- * The run engine replaces this notice with real progress. */
922
- const RUN_ENGINE_PHASE = "pre-3a";
923
- const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
924
-
925
- async function cmdVnextRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
926
- if (runtime.workspaceFlag !== undefined) {
927
- print(runtime.io, runtime.json, {
928
- ok: false,
929
- command: "run",
930
- error: "workspace_option_unsupported",
931
- }, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
932
- return 2;
933
- }
934
- if (!workflow) {
935
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
936
- return 2;
937
- }
938
- try {
939
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
940
- if (!projectRoot) {
941
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "project_required" }, "kxm run requires a vNext project (run kxm init first)");
942
- return 1;
943
- }
944
- const bundle = loadVnextProject(projectRoot, {});
945
- if (!bundle.workflows.has(workflow)) {
946
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
947
- return 1;
948
- }
949
- if (runtime.dryRun) {
950
- print(runtime.io, runtime.json, {
951
- ok: true,
952
- command: "run",
953
- dryRun: true,
954
- projectRoot,
955
- workflowId: workflow,
956
- configRevision: bundle.configRevision,
957
- }, `run plan: workflow ${workflow} at ${bundle.configRevision.slice(0, 19)}… (no run created)`);
958
- return 0;
959
- }
960
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
961
- const prompt = promptParts.join(" ").trim();
962
- const acceptance = await vnextRuntimeRequest(supervisor, "POST", "/v1/runs", {
963
- projectRoot,
964
- workflowId: workflow,
965
- prompt,
966
- });
967
- const run = acceptance.run as { runId: string; homeRuntimeId: string; status: string; configRevision: string };
968
- print(runtime.io, runtime.json, {
969
- ok: true,
970
- command: "run",
971
- phase: RUN_ENGINE_PHASE,
972
- idempotent: acceptance.idempotent === true,
973
- run,
974
- supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
975
- }, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${RUN_ENGINE_NOTICE}`);
976
- return 0;
977
- } catch (error) {
978
- if (error instanceof VnextConfigError) {
979
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_failed", issues: error.issues }, `run failed: ${error.message}`);
980
- return 1;
981
- }
982
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_io_failed" }, "run failed because a local operation did not complete");
983
- return 1;
984
- }
985
- }
986
-
987
- async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
988
- try {
989
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
990
- if (!projectRoot) {
991
- print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a vNext project (run kxm init first)");
992
- return 1;
993
- }
994
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
995
- const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
996
- const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
997
- print(runtime.io, runtime.json, { ok: true, command: "runs status", run }, `run ${run.runId}: ${run.status} (workflow ${run.workflowId}, updated ${run.updatedAt})`);
998
- return 0;
999
- } catch (error) {
1000
- if (error instanceof VnextConfigError) {
1001
- print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_failed", issues: error.issues }, `run status failed: ${error.message}`);
1002
- return 1;
1003
- }
1004
- 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");
1005
- return 1;
1006
- }
1007
- }
1008
-
1009
- async function cmdVnextRunDrive(runtime: Runtime, runId: string, simulated: boolean): Promise<number> {
1010
- try {
1011
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
1012
- if (!projectRoot) {
1013
- print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "project_required" }, "kxm runs drive requires a vNext project (run kxm init first)");
1014
- return 1;
1015
- }
1016
- const mode = simulated ? "simulated" : "live";
1017
- if (runtime.dryRun) {
1018
- print(runtime.io, runtime.json, { ok: true, command: "runs drive", dryRun: true, runId, mode }, `drive plan: run ${runId} in ${mode} mode (no events written)`);
1019
- return 0;
1020
- }
1021
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1022
- const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/drive?projectRoot=${encodeURIComponent(projectRoot)}`, { mode });
1023
- 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"}`);
1024
- return 0;
1025
- } catch (error) {
1026
- if (error instanceof VnextConfigError) {
1027
- print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_failed", issues: error.issues }, `run drive failed: ${error.message}`);
1028
- return 1;
1029
- }
1030
- 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");
1031
- return 1;
1032
- }
1033
- }
1034
-
1035
- async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
1036
- try {
1037
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
1038
- if (!projectRoot) {
1039
- print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "project_required" }, "kxm runs cancel requires a vNext project (run kxm init first)");
1040
- return 1;
1041
- }
1042
- if (runtime.dryRun) {
1043
- print(runtime.io, runtime.json, { ok: true, command: "runs cancel", dryRun: true, runId }, `cancel plan: run ${runId} (no events written)`);
1044
- return 0;
1045
- }
1046
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1047
- const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/cancel?projectRoot=${encodeURIComponent(projectRoot)}`, {});
1048
- const run = result.run as { runId: string; status: string };
1049
- print(runtime.io, runtime.json, {
1050
- ok: true,
1051
- command: "runs cancel",
1052
- idempotent: result.idempotent === true,
1053
- run,
1054
- }, `run ${run.runId}: ${run.status}`);
1055
- return 0;
1056
- } catch (error) {
1057
- if (error instanceof VnextConfigError) {
1058
- print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_failed", issues: error.issues }, `run cancel failed: ${error.message}`);
1059
- return 1;
1060
- }
1061
- 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");
1062
- return 1;
1063
- }
1064
- }
1065
-
1066
- async function cmdVnextRunList(runtime: Runtime): Promise<number> {
1067
- try {
1068
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
1069
- if (!projectRoot) {
1070
- print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "project_required" }, "kxm runs list requires a vNext project (run kxm init first)");
1071
- return 1;
1072
- }
1073
- const bundle = loadVnextProject(projectRoot, {});
1074
- const projectId = String(bundle.project.value.id);
1075
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1076
- const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(projectRoot)}`);
1077
- const runs = (result.runs ?? []) as Array<{ runId: string; status: string; workflowId: string; createdAt: string }>;
1078
- print(
1079
- runtime.io,
1080
- runtime.json,
1081
- { ok: true, command: "runs list", runs },
1082
- runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}`).join("\n"),
1083
- );
1084
- return 0;
1085
- } catch (error) {
1086
- if (error instanceof VnextConfigError) {
1087
- print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_failed", issues: error.issues }, `run list failed: ${error.message}`);
1088
- return 1;
1089
- }
1090
- 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");
1091
- return 1;
1092
- }
1093
- }
1094
-
1095
- async function cmdHarnessList(runtime: Runtime): Promise<number> {
1096
- const inventory = probeHarnesses({ env: runtime.env });
1097
- print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
1098
- return 0;
1099
- }
1100
-
1101
- function installProbeFrom(runtime: Runtime): InstallProbe {
1102
- const partial = runtime.io.installProbe ?? {};
1103
- return {
1104
- moduleDir: partial.moduleDir ?? dirname(fileURLToPath(import.meta.url)),
1105
- repoRoot: partial.repoRoot ?? repoRoot,
1106
- homeDir: partial.homeDir ?? homedir(),
1107
- platform: partial.platform ?? process.platform,
1108
- env: partial.env ?? runtime.env,
1109
- };
1110
- }
1111
-
1112
- function cliSpawn(runtime: Runtime, command: string, args: readonly string[], extra?: { timeout?: number }): CliSpawnResult {
1113
- if (runtime.io.spawnSync) return runtime.io.spawnSync(command, args);
1114
- const result = spawnSync(command, [...args], {
1115
- encoding: "utf8",
1116
- windowsHide: true,
1117
- shell: process.platform === "win32",
1118
- ...extra,
1119
- });
1120
- return {
1121
- status: result.status,
1122
- stdout: typeof result.stdout === "string" ? result.stdout : "",
1123
- stderr: typeof result.stderr === "string" ? result.stderr : "",
1124
- ...(result.error ? { error: result.error } : {}),
1125
- };
1126
- }
1127
-
1128
- function npmGlobalRootFn(runtime: Runtime): () => string | undefined {
1129
- return () => {
1130
- const result = cliSpawn(runtime, "npm", ["root", "-g"], { timeout: 5_000 });
1131
- if (result.error || result.status !== 0) return undefined;
1132
- const out = result.stdout.trim();
1133
- return out || undefined;
1134
- };
1135
- }
1136
-
1137
- function warnIgnoredProjectUpdateYaml(runtime: Runtime): void {
1138
- const projectFile = join(runtime.dirs.workspace, "update.yaml");
1139
- if (!existsSync(projectFile)) return;
1140
- const userFile = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
1141
- runtime.io.stderr(`kxm: ignoring .kxm/update.yaml in ${runtime.dirs.workdir}; update settings are read only from ${userFile}\n`);
1142
- }
1143
-
1144
- function installKindPayload(report: InstallKindReport): { installKind: string; root: string } {
1145
- return { installKind: report.kind, root: report.root };
1146
- }
1147
-
1148
- function formatPackageUpdateStep(step: KxmPackageUpdateStep): string {
1149
- if (step.kind === "verify") return `verify sha256 ${step.path}`;
1150
- return `${step.command} ${step.args.join(" ")}`;
1151
- }
1152
-
1153
- async function refreshKxmUpdateNotice(runtime: Runtime, config?: KxmUpdateConfig): Promise<KxmUpdateNotice> {
1154
- const resolved = config ?? loadKxmUpdateConfig(runtime.env);
1155
- const current = readInstalledKxmVersion(repoRoot);
1156
- const fetched = await fetchLatestKxmVersion(resolved.source, runtime.env, runtime.fetchImpl);
1157
- const notice = noticeFromVersions(current, fetched.latest, resolved, fetched.error, fetched.asset);
1158
- writeUpdateCache(runtime.dirs.state, notice);
1159
- return notice;
1160
- }
1161
-
1162
- function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice): { ok: boolean; detail: string; error?: string } {
1163
- if (!notice.latest) return { ok: false, detail: "no_latest_version" };
1164
- if (notice.source === "github" && !notice.asset?.sha256) {
1165
- const name = kxmReleaseAssetName(notice.latest);
1166
- return {
1167
- ok: false,
1168
- error: "release_digest_missing",
1169
- detail: `release v${notice.latest} has no sha256 digest for ${name}; refusing to install`,
1170
- };
1171
- }
1172
- const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
1173
- try {
1174
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
1175
- if (runtime.dryRun) {
1176
- return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
1177
- }
1178
- for (const step of planned) {
1179
- if (step.kind === "verify") {
1180
- if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
1181
- const actual = existsSync(step.path)
1182
- ? createHash("sha256").update(readFileSync(step.path)).digest("hex")
1183
- : "missing";
1184
- return {
1185
- ok: false,
1186
- error: "release_digest_mismatch",
1187
- detail: `${kxmReleaseAssetName(notice.latest)} sha256 ${actual} does not match release digest ${step.sha256}; refusing to install`,
1188
- };
1189
- }
1190
- continue;
1191
- }
1192
- const result = cliSpawn(runtime, step.command, step.args);
1193
- if (result.status !== 0) {
1194
- const detail = (result.stderr || result.stdout || result.error?.message || "update_failed").trim().slice(0, 500);
1195
- return { ok: false, detail };
1196
- }
1197
- }
1198
- return { ok: true, detail: `installed ${notice.latest} from ${notice.source}` };
1199
- } finally {
1200
- rmSync(releaseDir, { recursive: true, force: true });
1201
- }
1202
- }
1203
-
1204
- async function selectProducerModel(runtime: Runtime, requested?: string): Promise<string | undefined> {
1205
- const models = listInventoryModels(runtime.dirs.workdir);
1206
- if (requested) return models.find((model) => model.toLowerCase() === requested.toLowerCase());
1207
- if (runtime.json || !process.stdin.isTTY || models.length === 0) return undefined;
1208
- runtime.io.stdout(models.slice(0, 100).map((model, index) => `${index + 1}. ${model}`).join("\n") + "\nSelect model number: ");
1209
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1210
- const answer = await new Promise<string>((resolve) => rl.once("line", resolve));
1211
- rl.close();
1212
- const index = Number.parseInt(answer.trim(), 10) - 1;
1213
- return Number.isInteger(index) && index >= 0 && index < models.length ? models[index] : undefined;
1214
- }
1215
-
1216
- async function cmdProducerChange(runtime: Runtime, status: "promoted" | "demoted", requested?: string): Promise<number> {
1217
- const model = await selectProducerModel(runtime, requested);
1218
- 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; }
1219
- if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, dryRun: true }, `would ${status} ${model}`); return 0; }
1220
- const policy = updateProducer(runtime.dirs.workdir, model, status);
1221
- print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, policy }, `${status} ${model}`);
1222
- return 0;
1223
- }
1224
-
1225
- async function cmdModelsScreen(runtime: Runtime): Promise<number> {
1226
- const models = listInventoryModels(runtime.dirs.workdir);
1227
- 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; }
1228
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1229
- const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
1230
- try {
1231
- while (true) {
1232
- const policy = loadProducerPolicy(runtime.dirs.workdir);
1233
- const roleBindings = listRoleBindings(runtime.dirs.workdir);
1234
- 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");
1235
- const command = (await ask("[e]nable [d]isable [p]romote [x]demote [a]dd-role [r]emove-role [q]uit: ")).trim().toLowerCase();
1236
- if (command === "q" || command === "quit") return 0;
1237
- const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
1238
- if (!Number.isInteger(index) || !models[index]) continue;
1239
- const model = models[index];
1240
- if (command === "p" || command === "x") updateProducer(runtime.dirs.workdir, model, command === "p" ? "promoted" : "demoted");
1241
- else if (command === "e" || command === "d") setModelState(runtime.dirs.workdir, model, command === "e" ? "enabled" : "disabled");
1242
- else if (command === "a" || command === "r") setModelState(runtime.dirs.workdir, model, "enabled", (await ask("role: ")).trim(), command === "r");
1243
- }
1244
- } finally { rl.close(); }
1245
- }
1246
-
1247
- async function cmdProducerList(runtime: Runtime): Promise<number> {
1248
- const policy = loadProducerPolicy(runtime.dirs.workdir);
1249
- 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");
1250
- return 0;
1251
- }
1252
-
1253
- async function cmdModelInventoryRefresh(runtime: Runtime): Promise<number> {
1254
- if (runtime.dryRun) {
1255
- print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
1256
- return 0;
1257
- }
1258
- const inventory = await refreshModelInventory({ outputRoot: runtime.dirs.workdir, env: runtime.env });
1259
- const failed = Object.values(inventory.sources).some((source) => !source.ok);
1260
- 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`);
1261
- return failed ? 1 : 0;
1262
- }
1263
-
1264
- async function cmdUpdate(runtime: Runtime, harness: string | undefined, options: {
1265
- self?: boolean;
1266
- extensions?: boolean;
1267
- models?: boolean;
1268
- check?: boolean;
1269
- kxm?: boolean;
1270
- }): Promise<number> {
1271
- if (options.check && (options.kxm || options.self || options.extensions || options.models || harness)) {
1272
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "--check cannot be combined with other update flags");
1273
- return 2;
1274
- }
1275
- const selected = [options.self, options.extensions, options.models].filter(Boolean).length;
1276
- if (selected > 1) {
1277
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --self, --extensions, or --models");
1278
- return 2;
1279
- }
1280
- warnIgnoredProjectUpdateYaml(runtime);
1281
- const probe = installProbeFrom(runtime);
1282
- const classified = classifyInstallRoot(probe);
1283
- const current = readInstalledKxmVersion(repoRoot);
1284
- let notice: KxmUpdateNotice;
1285
- let kindReport = classified;
1286
- if (classified.kind === "source") {
1287
- if (options.check) {
1288
- const message = `kxm ${current} (running from source at ${classified.root})`;
1289
- print(runtime.io, runtime.json, {
1290
- ok: true,
1291
- command: "update check",
1292
- current,
1293
- available: false,
1294
- auto: false,
1295
- source: "github",
1296
- installKind: "source",
1297
- root: classified.root,
1298
- message,
1299
- }, message);
1300
- return 0;
1301
- }
1302
- if (options.kxm) {
1303
- print(runtime.io, runtime.json, {
1304
- ok: false,
1305
- command: "update",
1306
- error: "install_kind_source",
1307
- installKind: "source",
1308
- root: classified.root,
1309
- instruction: classified.instruction,
1310
- }, classified.instruction);
1311
- return 2;
1312
- }
1313
- notice = {
1314
- current,
1315
- available: false,
1316
- auto: false,
1317
- source: "github",
1318
- message: `kxm ${current} (running from source)`,
1319
- };
1320
- } else {
1321
- try {
1322
- notice = await refreshKxmUpdateNotice(runtime);
1323
- } catch (error) {
1324
- if (error instanceof KxmUpdateConfigError) {
1325
- print(runtime.io, runtime.json, { ok: false, command: "update", error: error.code, ...installKindPayload(classified) }, error.message);
1326
- return 2;
1327
- }
1328
- throw error;
1329
- }
1330
- }
1331
- if (options.check) {
1332
- print(runtime.io, runtime.json, {
1333
- ok: true,
1334
- command: "update check",
1335
- ...notice,
1336
- ...installKindPayload(classified),
1337
- }, notice.message);
1338
- return 0;
1339
- }
1340
- const applyKxm = Boolean(options.kxm || notice.auto);
1341
- let kxmApply: { ok: boolean; detail: string; error?: string } | undefined;
1342
- if (applyKxm) {
1343
- const resolved = resolveInstallKind(probe, npmGlobalRootFn(runtime));
1344
- kindReport = resolved;
1345
- if (resolved.kind !== "npm-global") {
1346
- if (options.kxm) {
1347
- print(runtime.io, runtime.json, {
1348
- ok: false,
1349
- command: "update",
1350
- error: `install_kind_${resolved.kind}`,
1351
- installKind: resolved.kind,
1352
- root: resolved.root,
1353
- instruction: resolved.instruction,
1354
- notice,
1355
- }, resolved.instruction);
1356
- return 2;
1357
- }
1358
- if (notice.available) runtime.io.stderr(`kxm: ${resolved.instruction}\n`);
1359
- } else if (notice.available) {
1360
- kxmApply = applyKxmPackageUpdate(runtime, notice);
1361
- if (!kxmApply.ok && options.kxm) {
1362
- print(runtime.io, runtime.json, {
1363
- ok: false,
1364
- command: "update",
1365
- error: kxmApply.error,
1366
- kxm: kxmApply,
1367
- notice,
1368
- ...installKindPayload(resolved),
1369
- }, kxmApply.detail);
1370
- return 1;
1371
- }
1372
- }
1373
- }
1374
- const skipHarness = Boolean(options.kxm && selected === 0 && !harness && !notice.auto);
1375
- if (skipHarness) {
1376
- print(
1377
- runtime.io,
1378
- runtime.json,
1379
- { ok: kxmApply?.ok !== false, command: "update", dryRun: runtime.dryRun, notice, kxm: kxmApply, ...installKindPayload(kindReport) },
1380
- kxmApply?.detail ?? notice.message,
1381
- );
1382
- return kxmApply?.ok === false ? 1 : 0;
1383
- }
1384
- const scope: HarnessUpdateScope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
1385
- const inventory = probeHarnesses({ env: runtime.env });
1386
- const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope });
1387
- const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
1388
- const failed = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
1389
- const skippedUnknown = steps.some((step) => step.detail === "unknown_harness");
1390
- const text = [notice.available ? notice.message : undefined, kxmApply?.detail, formatHarnessUpdate(steps)].filter(Boolean).join("\n");
1391
- print(runtime.io, runtime.json, {
1392
- ok: !failed && !skippedUnknown,
1393
- command: "update",
1394
- dryRun: runtime.dryRun,
1395
- scope,
1396
- notice,
1397
- ...(kxmApply ? { kxm: kxmApply } : {}),
1398
- steps,
1399
- ...installKindPayload(kindReport),
1400
- }, text);
1401
- if (skippedUnknown) return 2;
1402
- return failed ? 1 : 0;
1403
- }
1404
-
1405
- async function cmdVnextRuntime(runtime: Runtime, action: string): Promise<number> {
1406
- const paths = vnextRuntimePaths({ env: runtime.env });
1407
- try {
1408
- if (action === "start") {
1409
- if (runtime.dryRun) {
1410
- print(runtime.io, runtime.json, { ok: true, command: "runtime start", dryRun: true }, "runtime supervisor would auto-start");
1411
- return 0;
1412
- }
1413
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1414
- print(runtime.io, runtime.json, {
1415
- ok: true,
1416
- command: "runtime start",
1417
- runtimeId: supervisor.runtimeId,
1418
- port: supervisor.port,
1419
- started: supervisor.started,
1420
- }, `runtime supervisor ${supervisor.started ? "started" : "already running"}: ${supervisor.runtimeId} on 127.0.0.1:${supervisor.port}`);
1421
- return 0;
1422
- }
1423
- if (action === "status") {
1424
- const status = vnextSupervisorStatus(paths);
1425
- print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status }, status.running
1426
- ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}`
1427
- : "runtime supervisor is not running");
1428
- return status.running ? 0 : 1;
1429
- }
1430
- if (action === "stop") {
1431
- const status = vnextSupervisorStatus(paths);
1432
- if (!status.running || !status.port) {
1433
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: false }, "runtime supervisor is not running");
1434
- return 0;
1435
- }
1436
- if (runtime.dryRun) {
1437
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", dryRun: true }, `would stop runtime supervisor pid ${status.pid}`);
1438
- return 0;
1439
- }
1440
- // The stop request posts a bearer token, so prove the listener is the
1441
- // real supervisor (keyed healthz challenge) before sending it.
1442
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1443
- await vnextRuntimeRequest(supervisor, "POST", "/v1/shutdown", {});
1444
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: true }, `runtime supervisor ${status.runtimeId} stopping`);
1445
- return 0;
1446
- }
1447
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "unknown_action" }, `unknown runtime action: ${action}`);
1448
- return 2;
1449
- } catch (error) {
1450
- if (error instanceof VnextConfigError) {
1451
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_failed", issues: error.issues }, `runtime failed: ${error.message}`);
1452
- return 1;
1453
- }
1454
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_io_failed" }, "runtime failed because a local operation did not complete");
1455
- return 1;
1456
- }
1457
- }
1458
-
1459
- async function cmdValidate(runtime: Runtime, fileFlag?: string): Promise<number> {
1460
- const worker = gateOf(runtime, "validate");
1461
- const explicitFile = fileFlag?.trim();
1462
- const configuredFile = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
1463
- const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
1464
- if (!explicitFile && configuredFile && inline) {
1465
- printWorker(
1466
- runtime,
1467
- worker,
1468
- { ok: false, command: "validate", error: "ambiguous_workflow_source" },
1469
- "configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE",
1470
- );
1471
- return 2;
1472
- }
1473
- if (!explicitFile && !configuredFile && !inline) {
1474
- printWorker(
1475
- runtime,
1476
- worker,
1477
- { ok: false, command: "validate", error: "workflow_source_required" },
1478
- "provide --file or configure exactly one workflow source environment variable",
1479
- );
1480
- return 2;
1481
- }
1482
-
1483
- const selectedFile = explicitFile || configuredFile;
1484
- const file = selectedFile ? resolve(runtime.cwd, selectedFile) : undefined;
1485
- if (file && !existsSync(file)) {
1486
- printWorker(runtime, worker, { ok: false, command: "validate", error: "file_not_found", file }, `workflow file not found: ${file}`);
1487
- return 1;
1488
- }
1489
- try {
1490
- const raw = file ? readFileSync(file, "utf8") : inline!;
1491
- const warnings: string[] = [];
1492
- const definitions = parseWorkflowDefinitions(raw, runtime.env, (message) => warnings.push(message));
1493
- const secretEnvs = definitions.map((definition) => ({
1494
- id: definition.id,
1495
- secretConfigured: Boolean(definition.secret),
1496
- signalSecretConfigured: Boolean(definition.signalSecret),
1497
- }));
1498
- const source = file ? "file" : "inline";
1499
- printWorker(
1500
- runtime,
1501
- worker,
1502
- { ok: true, command: "validate", source, ...(file ? { file } : {}), workflows: secretEnvs, warnings },
1503
- `validated ${definitions.length} workflow(s) from ${source}${warnings.length ? ` with ${warnings.length} warning(s)` : ""}`,
1504
- warnings.length ? "warning" : undefined,
1505
- );
1506
- return 0;
1507
- } catch (error) {
1508
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid_workflow";
1509
- printWorker(runtime, worker, { ok: false, command: "validate", error: message }, message);
1510
- return 1;
1511
- }
1512
- }
1513
-
1514
- async function cmdArtifactsExist(runtime: Runtime, pathFlag: string): Promise<number> {
1515
- const worker = gateOf(runtime, "artifacts-exist");
1516
- const checked = verifyArtifactExists(runtime.dirs.assets, resolve(runtime.cwd, pathFlag));
1517
- if (!checked.ok) {
1518
- printWorker(
1519
- runtime,
1520
- worker,
1521
- { ok: false, command: "artifacts-exist", error: checked.error, path: checked.path },
1522
- `artifact verification failed: ${checked.error}`,
1523
- );
1524
- return 1;
1525
- }
1526
- printWorker(
1527
- runtime,
1528
- worker,
1529
- { ok: true, command: "artifacts-exist", path: checked.path, bytes: checked.bytes },
1530
- "artifact exists and is non-empty under workspace assets",
1531
- );
1532
- return 0;
1533
- }
1534
-
1535
- async function cmdStatus(runtime: Runtime): Promise<number> {
1536
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl);
1537
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl);
1538
- const payload = { ok: health.ok && ready.ok, command: "hub view", health: health.body, ready: ready.body };
1539
- print(runtime.io, runtime.json, payload, `hub health=${health.ok} ready=${ready.ok}`);
1540
- return payload.ok ? 0 : 1;
1541
- }
1542
-
1543
- async function cmdDash(runtime: Runtime, options: { screen?: string } = {}): Promise<number> {
1544
- const requested = options.screen?.trim();
1545
- if (requested && !(MESH_TUI_PANELS as readonly string[]).includes(requested)) {
1546
- print(runtime.io, runtime.json, { ok: false, command: "dash", error: "unknown_screen" }, `unknown screen ${requested}; use agents, tasks, workflows, plans, inbox, procs, or spend`);
1547
- return 2;
1548
- }
1549
- const screen = requested as MeshTuiPanel | undefined;
1550
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
1551
- if (runtime.dryRun) {
1552
- print(runtime.io, runtime.json, {
1553
- ok: true,
1554
- command: "dash",
1555
- dryRun: true,
1556
- serverUrl: runtime.serverUrl,
1557
- transport: "sse",
1558
- ...(screen ? { screen } : {}),
1559
- }, "would start kxm dash");
1560
- return 0;
1561
- }
1562
- if (runtime.json) {
1563
- runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
1564
- return 2;
1565
- }
1566
- const project = runtime.env.KXM_PROJECT?.trim() || basename(runtime.dirs.workdir) || "project";
1567
- const authToken = runtime.env.KXM_AUTH_TOKEN?.trim();
1568
- return await runMeshTui({
1569
- serverUrl: runtime.serverUrl,
1570
- dataPath,
1571
- stateDir: runtime.dirs.state,
1572
- project,
1573
- env: runtime.env,
1574
- ...(authToken ? { authToken } : {}),
1575
- ...(screen ? { screen } : {}),
1576
- fetchImpl: runtime.fetchImpl,
1577
- stdout: runtime.io.stdout,
1578
- stdin: process.stdin,
1579
- isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1580
- });
1581
- }
1582
-
1583
- async function cmdHub(runtime: Runtime): Promise<number> {
1584
- let refresh: Promise<unknown> | undefined;
1585
- if (!runtime.dryRun) {
1586
- const probe = installProbeFrom(runtime);
1587
- if (classifyInstallRoot(probe).kind !== "source") {
1588
- warnIgnoredProjectUpdateYaml(runtime);
1589
- const cached = readUpdateCache(runtime.dirs.state);
1590
- if (cached?.available) runtime.io.stderr(`${cached.message}\n`);
1591
- let config: KxmUpdateConfig | undefined;
1592
- try {
1593
- config = loadKxmUpdateConfig(runtime.env);
1594
- } catch (error) {
1595
- if (error instanceof KxmUpdateConfigError) {
1596
- const yamlPath = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
1597
- runtime.io.stderr(`kxm: ${error.message}; update check skipped; fix or remove ${yamlPath}\n`);
1598
- } else {
1599
- throw error;
1600
- }
1601
- }
1602
- if (config) {
1603
- refresh = refreshKxmUpdateNotice(runtime, config).then((notice) => {
1604
- if (notice.available && !cached?.available) runtime.io.stderr(`${notice.message}\n`);
1605
- }).catch(() => undefined);
1606
- }
1607
- }
1608
- }
1609
- const extraEnv = workspaceEnv(runtime);
1610
- if (runtime.dryRun) {
1611
- print(runtime.io, runtime.json, { ok: true, command: "hub start", dryRun: true, workspace: runtime.dirs.workspace }, "would start hub");
1612
- return 0;
1613
- }
1614
- const code = await (runtime.io.spawnHub ?? ((launchEnv) => spawnScript("kxm-hub.mjs", launchEnv)))(extraEnv);
1615
- if (refresh) await refresh;
1616
- return code;
1617
- }
1618
-
1619
- function formatHubBindHealth(health: HubHealth): string {
1620
- if (health === "on") return "health=on";
1621
- if (health === "off") return "health=off (nothing answered; run kxm hub start)";
1622
- return "health=unknown (no reply within 300 ms)";
1623
- }
1624
-
1625
- async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<number> {
1626
- let url: string;
1627
- try {
1628
- url = validateHubUrl(rawUrl);
1629
- } catch (error) {
1630
- if (error instanceof HubBindingError) {
1631
- print(
1632
- runtime.io,
1633
- runtime.json,
1634
- { ok: false, command: "hub bind", error: "hub_url_invalid" },
1635
- "hub bind needs an http or https URL without credentials, query, or fragment",
1636
- );
1637
- return 2;
1638
- }
1639
- throw error;
1640
- }
1641
- const file = hubBindingFile(runtime.env);
1642
- if (runtime.dryRun) {
1643
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, file }, `would bind hub ${url}`);
1644
- return 0;
1645
- }
1646
- writeHubBinding({ schema: HUB_BINDING_SCHEMA, url, boundAt: new Date().toISOString() }, runtime.env);
1647
- const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
1648
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, file, health, probeMs }, `bound hub ${url} · ${formatHubBindHealth(health)}`);
1649
- return 0;
1650
- }
1651
-
1652
- async function cmdHubUnbind(runtime: Runtime): Promise<number> {
1653
- const file = hubBindingFile(runtime.env);
1654
- if (runtime.dryRun) {
1655
- print(
1656
- runtime.io,
1657
- runtime.json,
1658
- { ok: true, command: "hub unbind", dryRun: true, ...(runtime.boundHubUrl ? { url: runtime.boundHubUrl } : {}), file },
1659
- `would unbind hub${runtime.boundHubUrl ? ` ${runtime.boundHubUrl}` : ""}`,
1660
- );
1661
- return 0;
1662
- }
1663
- let url: string | undefined;
1664
- let malformed = false;
1665
- try {
1666
- url = readHubBinding(runtime.env)?.url;
1667
- } catch (error) {
1668
- if (error instanceof HubBindingError) malformed = true;
1669
- else throw error;
1670
- }
1671
- if (!malformed && !url) {
1672
- print(runtime.io, runtime.json, { ok: false, command: "hub unbind", error: "hub_not_bound" }, `no hub binding at ${file}`);
1673
- return 1;
1674
- }
1675
- removeHubBinding(runtime.env);
1676
- if (malformed) {
1677
- print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
1678
- return 0;
1679
- }
1680
- print(runtime.io, runtime.json, { ok: true, command: "hub unbind", url, file }, `unbound hub ${url}`);
1681
- return 0;
1682
- }
1683
-
1684
- async function cmdWorker(runtime: Runtime, options: {
1685
- name?: string;
1686
- project?: string;
1687
- model?: string;
1688
- fallbackModels?: string;
1689
- tools?: string;
1690
- sessionIsolation?: string;
1691
- continue?: boolean;
1692
- freshStart?: boolean;
1693
- }): Promise<number> {
1694
- const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
1695
- const project = options.project?.trim() || runtime.env.KXM_PROJECT?.trim();
1696
- const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
1697
- const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
1698
- const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
1699
- const sessionIsolation = options.sessionIsolation?.trim() || runtime.env.KXM_WORKER_SESSION_ISOLATION?.trim() || "off";
1700
- if (sessionIsolation !== "workflow" && sessionIsolation !== "off") {
1701
- runtime.io.stderr("worker --session-isolation must be workflow or off\n");
1702
- return 2;
1703
- }
1704
- const extraEnv = {
1705
- ...workspaceEnv(runtime),
1706
- ...(name ? { KXM_AGENT_NAME: name } : {}),
1707
- ...(project ? { KXM_PROJECT: project } : {}),
1708
- ...(model ? { KXM_WORKER_MODEL: model } : {}),
1709
- ...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
1710
- ...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
1711
- KXM_WORKER_SESSION_ISOLATION: sessionIsolation,
1712
- ...(options.continue === false ? { KXM_WORKER_CONTINUE: "false" } : {}),
1713
- ...(options.freshStart ? { KXM_WORKER_INITIAL_CONTINUE: "false" } : {}),
1714
- };
1715
- if (runtime.dryRun) {
1716
- printWorker(runtime, agentWorker({
1717
- name: name || "required",
1718
- project: project || "required",
1719
- ...(model ? { model } : {}),
1720
- }), {
1721
- ok: true,
1722
- command: "worker",
1723
- dryRun: true,
1724
- workspace: runtime.dirs.workspace,
1725
- name: name || "required",
1726
- project: project || "required",
1727
- model: model || "provider default",
1728
- fallbackModels: fallbackModels || "none",
1729
- tools: tools || "Pi defaults",
1730
- sessionIsolation,
1731
- continue: options.continue !== false,
1732
- freshStart: Boolean(options.freshStart),
1733
- }, "would start worker");
1734
- return 0;
1735
- }
1736
- if (!name || !project) {
1737
- runtime.io.stderr("worker requires --name and --project (or KXM_AGENT_NAME and KXM_PROJECT)\n");
1738
- return 2;
1739
- }
1740
- return await (runtime.io.spawnWorker ?? ((launchEnv) => spawnScript("kxm-worker.mjs", launchEnv)))(extraEnv);
1741
- }
1742
-
1743
- async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
1744
- const pids = existsSync(runtime.dirs.state)
1745
- ? readdirSync(runtime.dirs.state).filter((name) => name.endsWith(".pid"))
1746
- : [];
1747
- if (runtime.dryRun) {
1748
- print(runtime.io, runtime.json, { ok: true, command: "stop", dryRun: true, pidFiles: pids }, "would signal pid files");
1749
- return 0;
1750
- }
1751
- if (pids.length === 0) {
1752
- print(runtime.io, runtime.json, { ok: false, command: "stop", error: "no_pid_files" }, "no hub/worker pid files found");
1753
- return 1;
1754
- }
1755
- const requested: string[] = [];
1756
- const ignored: string[] = [];
1757
- const records = new Map<string, { pid: number; startedAt: string; generation?: string }>();
1758
- const orphans: string[] = [];
1759
- for (const file of pids) {
1760
- try {
1761
- const record = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { version?: number; pid?: number; serverPid?: number; role?: string; startedAt?: string; generation?: string; controlFile?: string };
1762
- const expectedControl = file === "hub.pid" ? "hub.stop" : file.startsWith("worker-") ? `${file.slice(0, -4)}.stop` : undefined;
1763
- const expectedRole = file === "hub.pid" ? "hub" : file.startsWith("worker-") ? "worker" : undefined;
1764
- if (record.version !== 1 || !Number.isInteger(record.pid) || record.pid! <= 0 || !record.startedAt || !expectedControl || record.controlFile !== expectedControl || record.role !== expectedRole) { ignored.push(file); continue; }
1765
- if (!processExists(record.pid!)) {
1766
- // Wrapper is dead. A recorded hub server child may still hold the
1767
- // port; stop it directly so `kxm hub stop` recovers orphaned hubs.
1768
- if (file === "hub.pid" && Number.isInteger(record.serverPid) && record.serverPid! > 0 && record.serverPid !== record.pid && processExists(record.serverPid!)) {
1769
- try { process.kill(record.serverPid!, "SIGTERM"); } catch { /* racing exit */ }
1770
- orphans.push(file);
1771
- records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
1772
- continue;
1773
- }
1774
- ignored.push(file);
1775
- continue;
1776
- }
1777
- writeFileSync(join(runtime.dirs.state, record.controlFile!), `${JSON.stringify({ startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}), requestedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600 });
1778
- requested.push(file); records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
1779
- } catch { ignored.push(file); }
1780
- }
1781
- if (requested.length === 0 && orphans.length === 0) { print(runtime.io, runtime.json, { ok: false, command: "stop", requested, ignored }, "no current managed processes found"); return 1; }
1782
- const waitMs = Math.min(30_000, Math.max(100, Number(waitMsFlag || 5_000)));
1783
- const deadline = Date.now() + waitMs;
1784
- const stopped = new Set<string>();
1785
- while (Date.now() <= deadline && stopped.size < requested.length) {
1786
- for (const [file, record] of records) {
1787
- try {
1788
- const current = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { pid?: number; startedAt?: string; generation?: string };
1789
- if (current.pid !== record.pid || current.startedAt !== record.startedAt || current.generation !== record.generation || !processExists(record.pid)) stopped.add(file);
1790
- } catch { stopped.add(file); }
1791
- }
1792
- if (stopped.size < requested.length) await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
1793
- }
1794
- const timedOut = requested.filter((file) => !stopped.has(file));
1795
- // Orphan cleanup: wait for the directly-signaled server children to exit,
1796
- // then remove their stale claims so the next start reclaims cleanly.
1797
- const deadlineOrphans = Date.now() + Math.min(10_000, waitMs);
1798
- for (const file of orphans) {
1799
- while (Date.now() <= deadlineOrphans) {
1800
- try {
1801
- const current = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { pid?: number; serverPid?: number };
1802
- if (!Number.isInteger(current.serverPid) || !processExists(current.serverPid!)) break;
1803
- } catch { break; }
1804
- await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
1805
- }
1806
- try {
1807
- const current = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { pid?: number; serverPid?: number };
1808
- if (Number.isInteger(current.serverPid) && processExists(current.serverPid!)) {
1809
- try { process.kill(current.serverPid!, "SIGKILL"); } catch { /* racing exit */ }
1810
- }
1811
- rmSync(join(runtime.dirs.state, file), { force: true });
1812
- } catch { /* claim already replaced or removed */ }
1813
- stopped.add(file);
1814
- }
1815
- const ok = timedOut.length === 0;
1816
- print(runtime.io, runtime.json, { ok, command: "stop", requested, stopped: [...stopped], timedOut, ...(orphans.length ? { orphans } : {}), ignored }, ok ? "managed processes stopped" : "stop request timed out");
1817
- return ok ? 0 : 1;
1818
- }
1819
-
1820
- async function cmdSessionStatus(runtime: Runtime): Promise<number> {
1821
- const stateDir = runtime.dirs.state;
1822
- const names = existsSync(stateDir) ? readdirSync(stateDir) : [];
1823
- const claims = [];
1824
- for (const file of names.filter((name) => name.endsWith(".pid"))) {
1825
- try {
1826
- const record = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as { pid?: number; role?: string; startedAt?: string; generation?: string };
1827
- claims.push({
1828
- file,
1829
- role: record.role,
1830
- pid: record.pid,
1831
- startedAt: record.startedAt,
1832
- live: Number.isInteger(record.pid) && record.pid! > 0 && processExists(record.pid!),
1833
- });
1834
- } catch {
1835
- claims.push({ file, live: false, error: "invalid_pid_record" });
1836
- }
1837
- }
1838
- const recoveries = [];
1839
- for (const file of names.filter((name) => name.startsWith("worker-recovery-") && name.endsWith(".json"))) {
1840
- try {
1841
- const envelope = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as {
1842
- reason?: string;
1843
- agentName?: string;
1844
- project?: string;
1845
- createdAt?: string;
1846
- runId?: string | null;
1847
- stageId?: string | null;
1848
- freshSession?: boolean;
1849
- };
1850
- recoveries.push({
1851
- file,
1852
- reason: envelope.reason,
1853
- agentName: envelope.agentName,
1854
- project: envelope.project,
1855
- createdAt: envelope.createdAt,
1856
- runId: envelope.runId ?? undefined,
1857
- stageId: envelope.stageId ?? undefined,
1858
- freshSession: envelope.freshSession === true,
1859
- });
1860
- } catch {
1861
- recoveries.push({ file, error: "invalid_recovery_envelope" });
1862
- }
1863
- }
1864
- print(
1865
- runtime.io,
1866
- runtime.json,
1867
- { ok: true, command: "session status", claims, recoveries },
1868
- `${claims.length} session claim(s), ${recoveries.length} recovery envelope(s)`,
1869
- );
1870
- return 0;
1871
- }
1872
-
1873
- async function cmdAuthToken(runtime: Runtime, options: { status?: boolean; clear?: boolean; issue?: boolean } = {}): Promise<number> {
1874
- if (options.clear) {
1875
- const cleared = clearSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1876
- print(
1877
- runtime.io,
1878
- runtime.json,
1879
- { ok: true, command: "auth token", cleared },
1880
- cleared ? "Session token cleared from disk." : "No session token file found to clear.",
1881
- );
1882
- return 0;
1883
- }
1884
-
1885
- if (options.status) {
1886
- const fromEnv = runtime.env.KXM_SESSION_TOKEN?.trim();
1887
- if (fromEnv) {
1888
- const parsed = parseSessionToken(fromEnv);
1889
- print(
1890
- runtime.io,
1891
- runtime.json,
1892
- {
1893
- ok: true,
1894
- command: "auth token",
1895
- source: "env",
1896
- valid: Boolean(parsed),
1897
- ...(parsed ? { sessionId: parsed.sessionId, issuedAt: parsed.issuedAt, expiresAt: parsed.expiresAt } : {}),
1898
- },
1899
- parsed ? `Active session token from env (session=${parsed.sessionId})` : "Session token in env is invalid or expired",
1900
- );
1901
- return parsed ? 0 : 1;
1902
- }
1903
-
1904
- const disk = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1905
- if (disk) {
1906
- print(
1907
- runtime.io,
1908
- runtime.json,
1909
- {
1910
- ok: true,
1911
- command: "auth token",
1912
- source: "disk",
1913
- valid: true,
1914
- sessionId: disk.payload.sessionId,
1915
- issuedAt: disk.payload.issuedAt,
1916
- expiresAt: disk.payload.expiresAt,
1917
- path: sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR),
1918
- },
1919
- `Active session token on disk (session=${disk.payload.sessionId}, expires=${disk.payload.expiresAt ?? "never"})`,
1920
- );
1921
- return 0;
1922
- }
1923
-
1924
- print(
1925
- runtime.io,
1926
- runtime.json,
1927
- { ok: false, command: "auth token", error: "no_token", message: "No active session token found in env or disk" },
1928
- "No active session token found in env or disk",
1929
- );
1930
- return 1;
1931
- }
1932
-
1933
- let token: string;
1934
- if (options.issue) {
1935
- token = mintSessionToken({ preset: "operator" });
1936
- persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1937
- } else {
1938
- const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1939
- if (existing) {
1940
- token = existing.token;
1941
- } else {
1942
- token = mintSessionToken({ preset: "operator" });
1943
- persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1944
- }
1945
- }
1946
-
1947
- print(runtime.io, runtime.json, { ok: true, command: "auth token", token }, token);
1948
- return 0;
1949
- }
1950
-
1951
- async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean; token?: boolean } = {}): Promise<number> {
1952
- const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1953
- const sessionToken = existing ? existing.token : mintSessionToken({ preset: "operator" });
1954
- if (!existing) {
1955
- persistSessionTokenToDisk(sessionToken, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
1956
- }
1957
- if (options.token) {
1958
- print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
1959
- return 0;
1960
- }
1961
-
1962
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
1963
- const env = {
1964
- ...runtime.env,
1965
- KXM_STATE_DIR: runtime.dirs.state,
1966
- KXM_DATA_PATH: runtime.env.KXM_DATA_PATH?.trim() || dataPath,
1967
- };
1968
-
1969
- let hub: SessionHubStatus | undefined;
1970
- const targetUrl = runtime.env.KXM_SERVER_URL?.trim() || runtime.boundHubUrl || readHubBinding(runtime.env)?.url;
1971
- if (targetUrl) {
1972
- const { health } = await probeHubHealth(targetUrl, runtime.fetchImpl, 300);
1973
- hub = {
1974
- state: health,
1975
- evidence: health === "unknown" ? "timeout" : "probed",
1976
- online: health === "on",
1977
- url: targetUrl,
1978
- };
1979
- } else {
1980
- hub = { state: "off", evidence: "unconfigured", online: false };
1981
- }
1982
-
1983
- const brief = await loadSessionBriefAsync(runtime.dirs.workdir, env, undefined, hub, {
1984
- fetchImpl: runtime.fetchImpl,
1985
- sessionToken,
1986
- });
1987
-
1988
- if (options.status) {
1989
- print(runtime.io, runtime.json, brief, brief.statusLine);
1990
- return 0;
1991
- }
1992
- print(
1993
- runtime.io,
1994
- runtime.json,
1995
- brief,
1996
- `${formatSessionBriefText(brief)}\n\nSession token: ${sessionToken}\n`,
1997
- );
1998
- return 0;
1999
- }
2000
-
2001
- async function cmdSessionStart(runtime: Runtime, options: { id?: string; workflow?: string; mix?: string }): Promise<number> {
2002
- const id = options.id?.trim() || `session_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
2003
- const workflowId = options.workflow?.trim();
2004
- const mix = options.mix?.trim();
2005
- if (workflowId && mix) {
2006
- runtime.io.stderr("session start takes --workflow or --mix, not both\n");
2007
- return 2;
2008
- }
2009
- if (!workflowId && !mix) {
2010
- runtime.io.stderr("session start requires --workflow <id> or --mix <agent,gate,...>\n");
2011
- return 2;
2012
- }
2013
- const project = runtime.env.KXM_PROJECT?.trim();
2014
- let workers: Worker[];
2015
- try {
2016
- const names = mix
2017
- ? mix.split(",").map((name) => name.trim()).filter(Boolean)
2018
- : rosterNames(runtime.dirs.config);
2019
- workers = loadNamedWorkers(runtime.dirs.config, names, project);
2020
- } catch (error) {
2021
- const errorName = error && typeof error === "object" && "name" in error ? String(error.name) : "";
2022
- if (errorName !== "SessionConfigError") throw error;
2023
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid session configuration";
2024
- runtime.io.stderr(`${message}\n`);
2025
- return 2;
2026
- }
2027
- const session = createSession({
2028
- id,
2029
- host: hostMode(runtime),
2030
- mode: workflowId ? "workflow" : "mix",
2031
- workers,
2032
- assetsDir: runtime.dirs.assets,
2033
- ...(workflowId ? { workflowId } : {}),
2034
- });
2035
- const created = [
2036
- ...sessionAssetDirs(runtime.dirs.assets, session.id),
2037
- ...(workflowId ? workflowAssetDirs(runtime.dirs.assets, workflowId) : []),
2038
- ];
2039
- if (!runtime.dryRun) {
2040
- for (const directory of created) mkdirSync(directory, { recursive: true });
2041
- writeSession(runtime.dirs.assets, session);
2042
- }
2043
- print(runtime.io, runtime.json, {
2044
- ok: true,
2045
- command: "session start",
2046
- dryRun: runtime.dryRun || undefined,
2047
- session,
2048
- created,
2049
- }, `session ${session.id} (${session.mode})`);
2050
- return 0;
2051
- }
2052
-
2053
- async function cmdImprove(runtime: Runtime, options: { file?: string; target?: string; outDir?: string } = {}): Promise<number> {
2054
- const file = options.file ? resolve(runtime.cwd, options.file) : telemetryPath(runtime.dirs.logs);
2055
- const routingRecords = existsSync(file)
2056
- ? readRoutingRecords(file).map((entry) => entry.routing)
2057
- : [];
2058
-
2059
- const candidatesDir = options.outDir
2060
- ? resolve(runtime.cwd, options.outDir)
2061
- : join(runtime.cwd, ".kxm", "candidates");
2062
-
2063
- const report = buildImprovementReport(routingRecords, {
2064
- candidatesDir,
2065
- projectRoot: runtime.cwd,
2066
- dryRun: runtime.dryRun,
2067
- });
2068
-
2069
- const reportDir = join(runtime.dirs.assets, "improvements");
2070
- const reportPath = writeImprovementReport(reportDir, report, runtime.dryRun);
2071
-
2072
- const text = formatImprovementReport(report);
2073
- print(runtime.io, runtime.json, {
2074
- ok: true,
2075
- command: "improve",
2076
- dryRun: runtime.dryRun || undefined,
2077
- path: reportPath,
2078
- events: report.recordsCount,
2079
- recordsCount: report.recordsCount,
2080
- groupsCount: report.groups.length,
2081
- candidatesCount: report.candidates.length,
2082
- candidates: report.candidates,
2083
- report,
2084
- }, text);
2085
- return 0;
2086
- }
2087
-
2088
- function parseContextKinds(value: string | undefined): string[] | undefined {
2089
- if (!value) return undefined;
2090
- return value.split(",").map((kind) => kind.trim()).filter((kind) => kind.length > 0);
2091
- }
2092
-
2093
- async function cmdContextGet(runtime: Runtime, project: string, options: { role: string; task: string; run?: string; stage?: string; budget?: string; kinds?: string }): Promise<number> {
2094
- const budget = options.budget === undefined ? undefined : Number(options.budget);
2095
- if (options.budget !== undefined && (!Number.isInteger(budget) || (budget as number) < 512 || (budget as number) > 200_000)) {
2096
- runtime.io.stderr("context get --budget must be an integer between 512 and 200000\n");
2097
- return 2;
2098
- }
2099
- const body: Record<string, unknown> = {
2100
- project,
2101
- role: options.role,
2102
- task: options.task,
2103
- };
2104
- if (options.run) body.workflowRunId = options.run;
2105
- if (options.stage) body.stageId = options.stage;
2106
- if (budget !== undefined) body.budgetTokens = budget;
2107
- const kinds = parseContextKinds(options.kinds);
2108
- if (kinds) body.includeKinds = kinds;
2109
- const response = await hubContextPost({
2110
- serverUrl: runtime.serverUrl,
2111
- path: "/v1/context/get",
2112
- body,
2113
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2114
- fetchImpl: runtime.fetchImpl,
2115
- });
2116
- print(runtime.io, runtime.json, { ok: response.ok, command: "context get", status: response.status, ...(response.body as object) }, `context get ${response.ok ? "assembled" : `failed (${response.status})`}`);
2117
- return response.ok ? 0 : 1;
2118
- }
2119
-
2120
- async function cmdContextRecall(runtime: Runtime, project: string, options: { query?: string; kinds?: string; limit?: string }): Promise<number> {
2121
- const body: Record<string, unknown> = { project };
2122
- if (options.query) body.query = options.query;
2123
- const kinds = parseContextKinds(options.kinds);
2124
- if (kinds) body.kinds = kinds;
2125
- if (options.limit) body.limit = Number(options.limit);
2126
- const response = await hubContextPost({
2127
- serverUrl: runtime.serverUrl,
2128
- path: "/v1/context/recall",
2129
- body,
2130
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2131
- fetchImpl: runtime.fetchImpl,
2132
- });
2133
- print(runtime.io, runtime.json, { ok: response.ok, command: "context recall", status: response.status, ...(response.body as object) }, `context recall ${response.ok ? "complete" : `failed (${response.status})`}`);
2134
- return response.ok ? 0 : 1;
2135
- }
2136
-
2137
- async function cmdContextState(runtime: Runtime, project: string, key: string, options: { asOf?: string }): Promise<number> {
2138
- const body: Record<string, unknown> = { project, key };
2139
- if (options.asOf) body.asOf = options.asOf;
2140
- const response = await hubContextPost({
2141
- serverUrl: runtime.serverUrl,
2142
- path: "/v1/context/state",
2143
- body,
2144
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2145
- fetchImpl: runtime.fetchImpl,
2146
- });
2147
- print(runtime.io, runtime.json, { ok: response.ok, command: "context state", status: response.status, ...(response.body as object) }, `context state ${response.ok ? "resolved" : `failed (${response.status})`}`);
2148
- return response.ok ? 0 : 1;
2149
- }
2150
-
2151
- async function cmdContextEpisode(runtime: Runtime, project: string, options: { run?: string }): Promise<number> {
2152
- const body: Record<string, unknown> = { project };
2153
- if (options.run) body.workflowRunId = options.run;
2154
- const response = await hubContextPost({
2155
- serverUrl: runtime.serverUrl,
2156
- path: "/v1/context/episode",
2157
- body,
2158
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2159
- fetchImpl: runtime.fetchImpl,
2160
- });
2161
- print(runtime.io, runtime.json, { ok: response.ok, command: "context episode", status: response.status, ...(response.body as object) }, `context episode ${response.ok ? "complete" : `failed (${response.status})`}`);
2162
- return response.ok ? 0 : 1;
2163
- }
2164
-
2165
- async function cmdContextPromote(runtime: Runtime, project: string, proposalId: string, options: { evidence: string }): Promise<number> {
2166
- const evidence = options.evidence.split(",").map((ref) => ref.trim()).filter((ref) => ref.length > 0);
2167
- if (evidence.length === 0) {
2168
- runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n");
2169
- return 2;
2170
- }
2171
- const response = await hubContextPost({
2172
- serverUrl: runtime.serverUrl,
2173
- path: "/v1/context/state/promote",
2174
- body: { project, proposalId, evidence },
2175
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2176
- fetchImpl: runtime.fetchImpl,
2177
- });
2178
- print(runtime.io, runtime.json, { ok: response.ok, command: "context promote", status: response.status, ...(response.body as object) }, `context promote ${response.ok ? "recorded" : `failed (${response.status})`}`);
2179
- return response.ok ? 0 : 1;
2180
- }
2181
-
2182
- async function cmdContextExplain(runtime: Runtime, project: string, itemId: string): Promise<number> {
2183
- const response = await hubContextPost({
2184
- serverUrl: runtime.serverUrl,
2185
- path: "/v1/context/explain",
2186
- body: { project, id: itemId },
2187
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2188
- fetchImpl: runtime.fetchImpl,
2189
- });
2190
- print(runtime.io, runtime.json, { ok: response.ok, command: "context explain", status: response.status, ...(response.body as object) }, `context explain ${response.ok ? "complete" : `failed (${response.status})`}`);
2191
- return response.ok ? 0 : 1;
2192
- }
2193
-
2194
- async function cmdContextWikiCompile(runtime: Runtime, project: string, options: { out?: string }): Promise<number> {
2195
- const response = await hubContextPost({
2196
- serverUrl: runtime.serverUrl,
2197
- path: "/v1/context/wiki/compile",
2198
- body: { project },
2199
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2200
- fetchImpl: runtime.fetchImpl,
2201
- });
2202
- if (!response.ok) {
2203
- print(runtime.io, runtime.json, { ok: false, command: "context wiki-compile", status: response.status, body: response.body }, `wiki compile failed (${response.status})`);
2204
- return 1;
2205
- }
2206
- const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] };
2207
- let written: string[] = [];
2208
- if (options.out) {
2209
- const pages = new Map(compiled.pages.map((page) => [page.path, page.content]));
2210
- written = writeCompiledWiki(options.out, { pages, index: pages.get(".kxm/knowledge/wiki/index.md") ?? "", audit: { project, pages: compiled.audit.pages, stateItems: 0, contextItems: 0, contradictions: compiled.audit.contradictions, compiledAt: "" } });
2211
- }
2212
- print(runtime.io, runtime.json, { ok: true, command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, ...(written.length > 0 ? { written: written.length, outDir: options.out } : { dryRun: true }) }, `compiled ${compiled.audit.pages.length} wiki page(s)${written.length > 0 ? ` to ${options.out}` : " (dry-run)"}`);
2213
- return 0;
2214
- }
2215
-
2216
- async function cmdContextWikiLint(runtime: Runtime, project: string): Promise<number> {
2217
- const response = await hubContextPost({
2218
- serverUrl: runtime.serverUrl,
2219
- path: "/v1/context/wiki/compile",
2220
- body: { project },
2221
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
2222
- fetchImpl: runtime.fetchImpl,
2223
- });
2224
- if (!response.ok) {
2225
- print(runtime.io, runtime.json, { ok: false, command: "context wiki-lint", status: response.status, body: response.body }, `wiki lint failed (${response.status})`);
2226
- return 1;
2227
- }
2228
- const compiled = response.body as {
2229
- audit: { stateItems: number; contextItems: number; contradictions: number; compiledAt: string };
2230
- lint: { severity: string; rule: string; path: string; message: string }[];
2231
- };
2232
- const issues = compiled.lint;
2233
- print(runtime.io, runtime.json, { ok: issues.length === 0, command: "context wiki-lint", project, issues, audit: compiled.audit }, issues.length === 0 ? "wiki lint clean" : `wiki lint found ${issues.length} issue(s)`);
2234
- return issues.every((issue) => issue.severity !== "error") ? 0 : 1;
2235
- }
2236
-
2237
- function skillStateFromFlag(value: string): SkillState {
2238
- if (value === "candidate" || value === "promoted" || value === "quarantined" || value === "rejected") return value;
2239
- throw new Error(`invalid skill state ${value}`);
2240
- }
2241
-
2242
- function skillsRoot(runtime: Runtime): string {
2243
- return join(runtime.dirs.workdir, ".kxm", "skills");
2244
- }
2245
-
2246
- function csv(value: string | undefined): string[] | undefined {
2247
- if (!value) return undefined;
2248
- const items = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
2249
- return items.length > 0 ? items : undefined;
2250
- }
2251
-
2252
- async function cmdSkillsCreate(runtime: Runtime, options: { file: string; name: string; description?: string; createdBy: string; run?: string; journal?: string; receipt?: string; harness: string; models: string; supersedes?: string }): Promise<number> {
2253
- if (runtime.dryRun) {
2254
- print(runtime.io, runtime.json, { ok: true, command: "skills create", dryRun: true, name: options.name }, "would create skill candidate");
2255
- return 0;
2256
- }
2257
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2258
- try {
2259
- const metadata = lifecycle.create({
2260
- name: options.name,
2261
- description: options.description ?? "",
2262
- content: readFileSync(options.file, "utf8"),
2263
- createdBy: options.createdBy,
2264
- sources: {
2265
- runIds: csv(options.run) ?? [],
2266
- journalEntryIds: csv(options.journal) ?? [],
2267
- evidenceReceipts: csv(options.receipt) ?? [],
2268
- },
2269
- compatibility: { harness: options.harness, models: csv(options.models) ?? [] },
2270
- ...(options.supersedes ? { supersedes: options.supersedes } : {}),
2271
- });
2272
- print(runtime.io, runtime.json, { ok: true, command: "skills create", metadata }, `created skill candidate ${metadata.id}`);
2273
- return 0;
2274
- } catch (error) {
2275
- const message = error instanceof Error ? error.message : String(error);
2276
- runtime.io.stderr(`skills create failed: ${message}\n`);
2277
- return 1;
2278
- }
2279
- }
2280
-
2281
- async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean; score?: string; details?: string }): Promise<number> {
2282
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2283
- try {
2284
- const outcome = lifecycle.evaluate(skillId, {
2285
- kind: options.kind as SkillEvaluationKind,
2286
- evaluatorVersion: options.evaluator,
2287
- passed: options.fail !== true,
2288
- ...(options.score !== undefined ? { score: Number(options.score) } : {}),
2289
- ...(options.details ? { details: options.details } : {}),
2290
- });
2291
- print(runtime.io, runtime.json, { ok: true, command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, `recorded ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`);
2292
- return 0;
2293
- } catch (error) {
2294
- const message = error instanceof Error ? error.message : String(error);
2295
- runtime.io.stderr(`skills evaluate failed: ${message}\n`);
2296
- return 1;
2297
- }
2298
- }
2299
-
2300
- async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string }): Promise<number> {
2301
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2302
- try {
2303
- const promoted = lifecycle.promote(skillId, {
2304
- decidedBy: options.decidedBy,
2305
- reason: options.reason ?? "passed protected evaluation",
2306
- evidenceRefs: csv(options.evidence) ?? [],
2307
- });
2308
- print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`);
2309
- return 0;
2310
- } catch (error) {
2311
- const message = error instanceof Error ? error.message : String(error);
2312
- runtime.io.stderr(`skills promote failed: ${message}\n`);
2313
- return 1;
2314
- }
2315
- }
2316
-
2317
- async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string }): Promise<number> {
2318
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2319
- try {
2320
- const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" });
2321
- print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`);
2322
- return 0;
2323
- } catch (error) {
2324
- const message = error instanceof Error ? error.message : String(error);
2325
- runtime.io.stderr(`skills reject failed: ${message}\n`);
2326
- return 1;
2327
- }
2328
- }
2329
-
2330
- async function cmdSkillsList(runtime: Runtime, options: { state: string }): Promise<number> {
2331
- try {
2332
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2333
- const state = skillStateFromFlag(options.state);
2334
- const items = lifecycle.list(state).map((metadata) => ({
2335
- id: metadata.id,
2336
- name: metadata.name,
2337
- version: metadata.version,
2338
- createdBy: metadata.createdBy,
2339
- createdAt: metadata.createdAt,
2340
- models: metadata.compatibility.models,
2341
- }));
2342
- print(runtime.io, runtime.json, { ok: true, command: "skills list", state: options.state, skills: items }, `${items.length} ${state} skill(s)`);
2343
- return 0;
2344
- } catch (error) {
2345
- const message = error instanceof Error ? error.message : String(error);
2346
- runtime.io.stderr(`skills list failed: ${message}\n`);
2347
- return 1;
2348
- }
2349
- }
2350
-
2351
- async function cmdSkillsVerify(runtime: Runtime, skillId: string, options: { state: string }): Promise<number> {
2352
- try {
2353
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2354
- const state = skillStateFromFlag(options.state);
2355
- const metadata = lifecycle.verify(state, skillId);
2356
- print(runtime.io, runtime.json, { ok: true, command: "skills verify", skillId, state: options.state, contentSha256: metadata.contentSha256 }, `skill ${skillId} integrity verified`);
2357
- return 0;
2358
- } catch (error) {
2359
- const message = error instanceof Error ? error.message : String(error);
2360
- runtime.io.stderr(`skills verify failed: ${message}\n`);
2361
- return 1;
2362
- }
2363
- }
2364
-
2365
- async function cmdMemoryBrief(runtime: Runtime): Promise<number> {
2366
- try {
2367
- const brief = generateMemoryBrief(runtime.cwd);
2368
- const text = formatMemoryBriefText(brief);
2369
- print(runtime.io, runtime.json, { ok: true, command: "memory brief", brief }, text);
2370
- return 0;
2371
- } catch (error) {
2372
- const message = error instanceof Error ? error.message : String(error);
2373
- runtime.io.stderr(`memory brief failed: ${message}\n`);
2374
- return 1;
2375
- }
2376
- }
2377
-
2378
- async function cmdMemoryNote(
2379
- runtime: Runtime,
2380
- fact: string,
2381
- options: { scope?: string; kind?: string; body?: string },
2382
- ): Promise<number> {
2383
- try {
2384
- const { record, path } = createMemoryNote(runtime.cwd, fact, {
2385
- scope: (options.scope ?? "project") as MemoryScope,
2386
- ...(options.kind !== undefined ? { kind: options.kind } : {}),
2387
- ...(options.body !== undefined ? { body: options.body } : {}),
2388
- });
2389
- const relPath = relative(runtime.cwd, path);
2390
- print(
2391
- runtime.io,
2392
- runtime.json,
2393
- { ok: true, command: "memory note", candidate: record, path: relPath },
2394
- `Recorded memory candidate ${record.id} in ${relPath} (promoted via PR)`,
2395
- );
2396
- return 0;
2397
- } catch (error) {
2398
- const message = error instanceof Error ? error.message : String(error);
2399
- runtime.io.stderr(`memory note failed: ${message}\n`);
2400
- return 1;
2401
- }
2402
- }
2403
-
2404
- async function cmdMemorySync(runtime: Runtime): Promise<number> {
2405
- try {
2406
- const result = syncHarnessMemory(runtime.cwd);
2407
- print(
2408
- runtime.io,
2409
- runtime.json,
2410
- { ok: true, command: "memory sync", ...result },
2411
- `Synced project memory across AGENTS.md, CLAUDE.md, and GEMINI.md`,
2412
- );
2413
- return 0;
2414
- } catch (error) {
2415
- const message = error instanceof Error ? error.message : String(error);
2416
- runtime.io.stderr(`memory sync failed: ${message}\n`);
2417
- return 1;
2418
- }
2419
- }
2420
-
2421
- async function cmdConfigGet(runtime: Runtime, key: string): Promise<number> {
2422
- try {
2423
- const config = loadKxmConfig(runtime.cwd);
2424
- const value = getKxmConfigValue(config, key);
2425
- print(
2426
- runtime.io,
2427
- runtime.json,
2428
- { ok: true, command: "config get", key, value },
2429
- value !== undefined ? String(value) : "(undefined)",
2430
- );
2431
- return 0;
2432
- } catch (error) {
2433
- const message = error instanceof Error ? error.message : String(error);
2434
- runtime.io.stderr(`config get failed: ${message}\n`);
2435
- return 1;
2436
- }
2437
- }
2438
-
2439
- async function cmdConfigSet(
2440
- runtime: Runtime,
2441
- key: string,
2442
- value: string,
2443
- options: { scope: string },
2444
- ): Promise<number> {
2445
- try {
2446
- const scope = options.scope === "user" ? "user" : "project";
2447
- let parsedVal: unknown = value;
2448
- try {
2449
- parsedVal = JSON.parse(value);
2450
- } catch {
2451
- // keep string
2452
- }
2453
- setKxmConfigValue(runtime.cwd, key, parsedVal, { scope });
2454
- print(
2455
- runtime.io,
2456
- runtime.json,
2457
- { ok: true, command: "config set", key, value: parsedVal, scope },
2458
- `Set ${key} = ${value} in ${scope} config`,
2459
- );
2460
- return 0;
2461
- } catch (error) {
2462
- const message = error instanceof Error ? error.message : String(error);
2463
- runtime.io.stderr(`config set failed: ${message}\n`);
2464
- return 1;
2465
- }
2466
- }
2467
-
2468
- async function cmdConfigList(runtime: Runtime): Promise<number> {
2469
- try {
2470
- const config = loadKxmConfig(runtime.cwd);
2471
- const text = formatKxmConfig(config);
2472
- print(runtime.io, runtime.json, { ok: true, command: "config list", config }, text);
2473
- return 0;
2474
- } catch (error) {
2475
- const message = error instanceof Error ? error.message : String(error);
2476
- runtime.io.stderr(`config list failed: ${message}\n`);
2477
- return 1;
2478
- }
2479
- }
2480
-
2481
- async function cmdCompletion(runtime: Runtime, shell: string): Promise<number> {
2482
- try {
2483
- if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
2484
- runtime.io.stderr(`unsupported shell: ${shell}; must be bash, zsh, or fish\n`);
2485
- return 1;
2486
- }
2487
- const script = generateShellCompletion(shell as SupportedShell);
2488
- runtime.io.stdout(script);
2489
- return 0;
2490
- } catch (error) {
2491
- const message = error instanceof Error ? error.message : String(error);
2492
- runtime.io.stderr(`completion generation failed: ${message}\n`);
2493
- return 1;
2494
- }
2495
- }
2496
-
2497
- async function cmdCompletionInstall(
2498
- runtime: Runtime,
2499
- options: { shell?: string; path?: boolean },
2500
- ): Promise<number> {
2501
- const installOptions = {
2502
- env: runtime.env,
2503
- configDir: runtime.env.KXM_USER_CONFIG_DIR,
2504
- platform: process.platform as NodeJS.Platform,
2505
- dryRun: runtime.dryRun,
2506
- };
2507
- const report = installShellCompletion(options.shell, installOptions);
2508
- const pathReport = options.path === false ? undefined : installPathEntry(options.shell, installOptions);
2509
- if (!report.ok) {
2510
- print(runtime.io, runtime.json, {
2511
- ok: false,
2512
- command: "completion install",
2513
- error: report.reason ?? "shell_not_detected",
2514
- }, "could not detect your shell; pass --shell bash, zsh, or fish");
2515
- return 1;
2516
- }
2517
- const shellNote = report.shell;
2518
- const applyNote = runtime.dryRun
2519
- ? "planned; rerun without --dry-run to apply"
2520
- : report.alreadyInstalled
2521
- ? "already installed"
2522
- : "installed; restart your shell or open a new terminal to activate";
2523
- const pathNote = pathReport
2524
- ? pathReport.ok
2525
- ? runtime.dryRun
2526
- ? `; PATH entry for ${pathReport.binDir} planned`
2527
- : pathReport.alreadyInstalled
2528
- ? "; kxm already on PATH"
2529
- : `; PATH entry for ${pathReport.binDir} added to ${pathReport.rcFile}`
2530
- : undefined
2531
- : undefined;
2532
- print(runtime.io, runtime.json, {
2533
- ok: true,
2534
- command: "completion install",
2535
- shell: shellNote,
2536
- scriptPath: report.scriptPath,
2537
- ...(report.rcFile ? { rcFile: report.rcFile } : {}),
2538
- rcModified: report.rcModified,
2539
- alreadyInstalled: report.alreadyInstalled,
2540
- ...(pathReport ? { path: pathReport } : {}),
2541
- dryRun: runtime.dryRun === true,
2542
- }, `kxm ${shellNote} completion: ${applyNote}${report.rcFile ? ` (rc: ${report.rcFile})` : ""}${pathNote ?? ""}`);
2543
- return 0;
2544
- }
2545
-
2546
- async function maybeOfferCompletionInstall(runtime: Runtime): Promise<void> {
2547
- if (runtime.json || runtime.dryRun) return;
2548
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
2549
- if (runtime.env.KXM_SKIP_COMPLETION_PROMPT?.trim()) return;
2550
- const shell = detectShell(runtime.env, process.platform);
2551
- if (shell !== "bash" && shell !== "zsh" && shell !== "fish") return;
2552
- const binDir = kxmBinDir(runtime.env);
2553
- const pathNeeded = shell !== "fish" && binDir && !(runtime.env.PATH ?? "").split(":").includes(binDir);
2554
- if (!pathNeeded) {
2555
- // completion-only offer; skip when already wired
2556
- const scriptPath = completionScriptPath(shell, { env: runtime.env, configDir: runtime.env.KXM_USER_CONFIG_DIR });
2557
- const { rcFile } = completionRcTarget(shell, { env: runtime.env });
2558
- if (rcFile && existsSync(rcFile)) {
2559
- try {
2560
- if (readFileSync(rcFile, "utf8").includes(scriptPath)) return; // already wired
2561
- } catch {
2562
- // unreadable rc: still offer
2563
- }
2564
- }
2565
- }
2566
- const question = pathNeeded
2567
- ? `\nInstall ${shell} tab completion for kxm and add ${binDir} to PATH? [Y/n] `
2568
- : `\nInstall ${shell} tab completion for kxm? [y/N] `;
2569
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2570
- await new Promise<void>((resolvePrompt) => {
2571
- rl.question(question, (answer) => {
2572
- rl.close();
2573
- const trimmed = answer.trim().toLowerCase();
2574
- const accept = pathNeeded ? trimmed !== "n" && trimmed !== "no" : trimmed === "y" || trimmed === "yes";
2575
- if (accept) {
2576
- try {
2577
- const report = installShellCompletion(shell, {
2578
- env: runtime.env,
2579
- configDir: runtime.env.KXM_USER_CONFIG_DIR,
2580
- platform: process.platform,
2581
- });
2582
- if (report.ok) {
2583
- runtime.io.stdout(`completion installed for ${report.shell}`);
2584
- } else {
2585
- runtime.io.stdout(`completion install skipped: ${report.reason ?? "unknown"}\n`);
2586
- resolvePrompt();
2587
- return;
2588
- }
2589
- } catch {
2590
- runtime.io.stdout("completion install skipped: local filesystem operation did not complete\n");
2591
- resolvePrompt();
2592
- return;
2593
- }
2594
- if (pathNeeded && binDir) {
2595
- try {
2596
- const pathReport = installPathEntry(shell, {
2597
- env: runtime.env,
2598
- configDir: runtime.env.KXM_USER_CONFIG_DIR,
2599
- platform: process.platform,
2600
- binDir,
2601
- });
2602
- runtime.io.stdout(pathReport.ok && pathReport.rcModified
2603
- ? `; ${binDir} added to PATH in ${pathReport.rcFile}`
2604
- : "; kxm already on PATH");
2605
- } catch {
2606
- runtime.io.stdout("; PATH entry skipped: local filesystem operation did not complete");
2607
- }
2608
- }
2609
- runtime.io.stdout("; restart your shell or open a new terminal to activate\n");
2610
- } else {
2611
- runtime.io.stdout("skipped; run `kxm completion install` anytime\n");
2612
- }
2613
- resolvePrompt();
2614
- });
2615
- });
2616
- }
2617
-
2618
- const GUIDE_SETUP_OPT_OUT_ENV = "KXM_SKIP_GUIDE_SETUP_PROMPT";
2619
-
2620
- async function askYesNo(question: string): Promise<boolean> {
2621
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2622
- return new Promise<boolean>((resolvePrompt) => {
2623
- rl.question(question, (answer) => {
2624
- rl.close();
2625
- const trimmed = answer.trim().toLowerCase();
2626
- resolvePrompt(trimmed === "y" || trimmed === "yes");
2627
- });
2628
- });
2629
- }
2630
-
2631
- async function askLine(question: string): Promise<string> {
2632
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2633
- return new Promise<string>((resolvePrompt) => {
2634
- rl.question(question, (answer) => {
2635
- rl.close();
2636
- resolvePrompt(answer);
2637
- });
2638
- });
2639
- }
2640
-
2641
- /**
2642
- * Post-init offer: install workflow-guide software-engineering workflows and
2643
- * their agent resources, filtered to authenticated harnesses. Writes only
2644
- * vNext project resources (.kxm/agents, .kxm/workflows); never legacy
2645
- * authority (.kxm/config, .kxm/roster.json).
2646
- */
2647
- async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
2648
- if (runtime.json || runtime.dryRun) return;
2649
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
2650
- if (runtime.env[GUIDE_SETUP_OPT_OUT_ENV]?.trim()) return;
2651
- let inventory;
2652
- try {
2653
- inventory = probeHarnesses({ env: runtime.env });
2654
- } catch {
2655
- return;
2656
- }
2657
- const authenticated = inventory.harnesses.filter((entry) => entry.detected && entry.authenticated === true);
2658
- if (authenticated.length === 0) {
2659
- runtime.io.stdout("no authenticated harnesses detected; skipping workflow-guide setup (see `kxm harness list`)\n");
2660
- return;
2661
- }
2662
- const harnessList = authenticated.map((entry) => entry.id).join(", ");
2663
- const accept = await askYesNo(`\nSet up workflow-guide agents and workflows for authenticated harnesses (${harnessList})? [y/N] `);
2664
- if (!accept) {
2665
- runtime.io.stdout(`skipped; set ${GUIDE_SETUP_OPT_OUT_ENV}=1 to suppress this offer, or re-run on a fresh project\n`);
2666
- return;
2667
- }
2668
- const lines = ["", "Workflow-guide software-engineering workflows (docs/workflow-guide.md):"];
2669
- GUIDE_WORKFLOWS.forEach((workflow, index) => {
2670
- lines.push(` ${index + 1}) ${workflow.slug.padEnd(32)} ${workflow.summary}`);
2671
- });
2672
- runtime.io.stdout(`${lines.join("\n")}\n`);
2673
- const answer = await askLine("Install which workflows? (numbers or slugs, comma-separated, 'all', or 'none'): ");
2674
- const selected = parseGuideSelection(answer);
2675
- if (selected.length === 0) {
2676
- runtime.io.stdout("no workflows selected; nothing written\n");
2677
- return;
2678
- }
2679
- const plan = planGuideSetup({ inventory, selected });
2680
- const files = renderGuideSetupFiles(runtime.cwd, plan);
2681
- const report = writeGuideSetupFiles(files);
2682
- for (const file of report.written) runtime.io.stdout(`wrote ${file}\n`);
2683
- for (const file of report.existed) runtime.io.stdout(`kept existing ${file} (not overwritten)\n`);
2684
- for (const skip of plan.skipped) {
2685
- runtime.io.stdout(`skipped ${skip.workflow}/${skip.role}: ${skip.reason}\n`);
2686
- }
2687
- if (report.written.length > 0) {
2688
- runtime.io.stdout("inspect with `kxm workflow definitions`; guide candidates are dated research — verify before dispatch\n");
2689
- }
2690
- }
2691
-
2692
- async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
2693
- try {
2694
- const prompt = promptParts.join(" ").trim();
2695
- if (!prompt) {
2696
- runtime.io.stderr("prompt must be non-empty\n");
2697
- return 2;
2698
- }
2699
- const inventory = probeHarnesses({ env: runtime.env });
2700
- const availableHarnesses = inventory.harnesses.map((h) => ({
2701
- harness: h.id,
2702
- auth: h.authenticated === true ? "authenticated" : "unauthenticated",
2703
- }));
2704
- const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses });
2705
-
2706
- const text = [
2707
- `Suggested Workflow: ${suggestion.workflowId} (${suggestion.area})`,
2708
- `Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`,
2709
- `Reasons: ${suggestion.reasons.join("; ")}`,
2710
- `Suggested Skills: ${suggestion.suggestedSkills.join(", ") || "none"}`,
2711
- `Roles:`,
2712
- ` Planner: ${suggestion.roles.planner.harness} (${suggestion.roles.planner.model})`,
2713
- ` Writer: ${suggestion.roles.writer.harness} (${suggestion.roles.writer.model})`,
2714
- ` Critics: ${suggestion.roles.critics.map((c) => `${c.harness}:${c.model}`).join(", ")}`,
2715
- ` Verifier: ${suggestion.roles.verifier.command}`,
2716
- ``,
2717
- `Execute with:`,
2718
- ` ${suggestion.suggestedCommand}`,
2719
- ].join("\n");
2720
-
2721
- print(runtime.io, runtime.json, { ok: true, command: "suggest", prompt, ...suggestion }, text);
2722
- return 0;
2723
- } catch (error) {
2724
- const message = error instanceof Error ? error.message : String(error);
2725
- runtime.io.stderr(`suggest failed: ${message}\n`);
2726
- return 1;
2727
- }
2728
- }
2729
-
2730
- async function cmdGoalCreate(
2731
- runtime: Runtime,
2732
- title: string,
2733
- options: { area?: string; metric?: string[]; targetDate?: string },
2734
- ): Promise<number> {
2735
- try {
2736
- const goal = createGoal(runtime.cwd, {
2737
- title,
2738
- area: options.area,
2739
- successMetrics: options.metric,
2740
- targetDate: options.targetDate,
2741
- });
2742
- print(
2743
- runtime.io,
2744
- runtime.json,
2745
- { ok: true, command: "goal create", goal },
2746
- `Created goal ${goal.id}: ${goal.title}`,
2747
- );
2748
- return 0;
2749
- } catch (error) {
2750
- const message = error instanceof Error ? error.message : String(error);
2751
- runtime.io.stderr(`goal create failed: ${message}\n`);
2752
- return 1;
2753
- }
2754
- }
2755
-
2756
- async function cmdGoalList(runtime: Runtime): Promise<number> {
2757
- try {
2758
- const goals = listGoals(runtime.cwd);
2759
- const text = goals.length === 0
2760
- ? "No goals recorded in .kxm/goals/"
2761
- : goals.map((g) => `[${g.status}] ${g.id}: ${g.title} (${g.area})`).join("\n");
2762
- print(runtime.io, runtime.json, { ok: true, command: "goal list", count: goals.length, goals }, text);
2763
- return 0;
2764
- } catch (error) {
2765
- const message = error instanceof Error ? error.message : String(error);
2766
- runtime.io.stderr(`goal list failed: ${message}\n`);
2767
- return 1;
2768
- }
2769
- }
2770
-
2771
- async function cmdTaskCreate(
2772
- runtime: Runtime,
2773
- title: string,
2774
- options: { goal?: string; objective?: string; workflow?: string; tracker?: string; issue?: string },
2775
- ): Promise<number> {
2776
- try {
2777
- const task = createTask(runtime.cwd, {
2778
- title,
2779
- goalId: options.goal,
2780
- objective: options.objective ?? title,
2781
- assignedWorkflow: options.workflow,
2782
- trackerSync: options.tracker && options.issue
2783
- ? { tracker: options.tracker as TrackerType, issueKey: options.issue }
2784
- : undefined,
2785
- });
2786
- print(
2787
- runtime.io,
2788
- runtime.json,
2789
- { ok: true, command: "task create", task },
2790
- `Created task ${task.id}: ${task.title} [${task.status}]`,
2791
- );
2792
- return 0;
2793
- } catch (error) {
2794
- const message = error instanceof Error ? error.message : String(error);
2795
- runtime.io.stderr(`task create failed: ${message}\n`);
2796
- return 1;
2797
- }
2798
- }
2799
-
2800
- async function cmdTaskList(
2801
- runtime: Runtime,
2802
- options: { goal?: string; status?: string },
2803
- ): Promise<number> {
2804
- try {
2805
- const tasks = listTasks(runtime.cwd, {
2806
- goalId: options.goal,
2807
- status: options.status as TaskStatus | undefined,
2808
- });
2809
- const text = tasks.length === 0
2810
- ? "No tasks recorded in .kxm/tasks/"
2811
- : tasks.map((t) => `[${t.status}] ${t.id}: ${t.title}${t.assignedWorkflow ? ` -> ${t.assignedWorkflow}` : ""}`).join("\n");
2812
- print(runtime.io, runtime.json, { ok: true, command: "task list", count: tasks.length, tasks }, text);
2813
- return 0;
2814
- } catch (error) {
2815
- const message = error instanceof Error ? error.message : String(error);
2816
- runtime.io.stderr(`task list failed: ${message}\n`);
2817
- return 1;
2818
- }
2819
- }
2820
-
2821
- async function cmdTaskGet(runtime: Runtime, taskId: string): Promise<number> {
2822
- try {
2823
- const task = getTask(runtime.cwd, taskId);
2824
- if (!task) {
2825
- runtime.io.stderr(`Task ${taskId} not found\n`);
2826
- return 1;
2827
- }
2828
- const text = [
2829
- `Task: ${task.id}`,
2830
- `Title: ${task.title}`,
2831
- `Status: ${task.status}`,
2832
- `Objective: ${task.objective}`,
2833
- task.assignedWorkflow ? `Workflow: ${task.assignedWorkflow}` : "",
2834
- task.workflowRunId ? `Active Run: ${task.workflowRunId}` : "",
2835
- task.trackerSync ? `Tracker: ${task.trackerSync.tracker} (#${task.trackerSync.issueKey}) [${task.trackerSync.syncStatus}]` : "",
2836
- ].filter(Boolean).join("\n");
2837
- print(runtime.io, runtime.json, { ok: true, command: "task get", task }, text);
2838
- return 0;
2839
- } catch (error) {
2840
- const message = error instanceof Error ? error.message : String(error);
2841
- runtime.io.stderr(`task get failed: ${message}\n`);
2842
- return 1;
2843
- }
2844
- }
2845
-
2846
- async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
2847
- try {
2848
- const task = getTask(runtime.cwd, taskId);
2849
- if (!task) {
2850
- runtime.io.stderr(`Task ${taskId} not found\n`);
2851
- return 1;
2852
- }
2853
- const workflow = task.assignedWorkflow ?? "default";
2854
- const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
2855
- if (exitCode === 0) {
2856
- updateTaskStatus(runtime.cwd, taskId, "in_progress");
2857
- }
2858
- return exitCode;
2859
- } catch (error) {
2860
- const message = error instanceof Error ? error.message : String(error);
2861
- runtime.io.stderr(`task run failed: ${message}\n`);
2862
- return 1;
2863
- }
2864
- }
2865
-
2866
- async function cmdTaskSync(runtime: Runtime, taskId: string): Promise<number> {
2867
- try {
2868
- const synced = syncTaskWithTracker(runtime.cwd, taskId);
2869
- print(
2870
- runtime.io,
2871
- runtime.json,
2872
- { ok: true, command: "task sync", task: synced },
2873
- `Synced task ${taskId} with ${synced.trackerSync?.tracker} #${synced.trackerSync?.issueKey}`,
2874
- );
2875
- return 0;
2876
- } catch (error) {
2877
- const message = error instanceof Error ? error.message : String(error);
2878
- runtime.io.stderr(`task sync failed: ${message}\n`);
2879
- return 1;
2880
- }
2881
- }
2882
-
2883
- async function cmdStudioLayout(runtime: Runtime, workflowPath?: string): Promise<number> {
2884
- try {
2885
- const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
2886
- let filePath = workflowPath;
2887
- if (!filePath) {
2888
- filePath = resolve(projectRoot, ".kxm", "workflows", "default.yaml");
2889
- }
2890
- let yamlContent: string;
2891
- let workflowId = "default";
2892
- if (existsSync(filePath)) {
2893
- yamlContent = readFileSync(filePath, "utf8");
2894
- } else {
2895
- yamlContent = `schema: kxm.workflow.v1
2896
- description: Feature implementation workflow
2897
- coordinator: coordinator
2898
- limits:
2899
- maxTransitions: 12
2900
- steps:
2901
- - id: plan
2902
- kind: agent
2903
- agent: planner
2904
- maxAttempts: 2
2905
- on:
2906
- passed: implement
2907
- failed:
2908
- target: $terminal
2909
- terminalStatus: failed
2910
- - id: implement
2911
- kind: agent
2912
- agent: writer
2913
- maxAttempts: 3
2914
- on:
2915
- passed: verify
2916
- failed:
2917
- target: $terminal
2918
- terminalStatus: failed
2919
- - id: verify
2920
- kind: gate
2921
- gate: verify-gate
2922
- expect: pass
2923
- maxAttempts: 2
2924
- on:
2925
- passed:
2926
- target: $terminal
2927
- terminalStatus: completed
2928
- failed:
2929
- target: implement
2930
- maxTransitions: 2
2931
- `;
2932
- }
2933
- const parsedYaml = parseYaml(yamlContent) as any;
2934
- const plan = compileVnextWorkflow({ id: workflowId, value: parsedYaml });
2935
- const layout = generateStudioLayout(plan);
2936
- print(runtime.io, runtime.json, { ok: true, command: "studio layout", layout }, JSON.stringify(layout, null, 2));
2937
- return 0;
2938
- } catch (error) {
2939
- const message = error instanceof Error ? error.message : String(error);
2940
- runtime.io.stderr(`studio layout failed: ${message}\n`);
2941
- return 1;
2942
- }
2943
- }
2944
-
2945
- async function cmdStudioServe(
2946
- runtime: Runtime,
2947
- options: { port?: string; host?: string; token?: string },
2948
- ): Promise<number> {
2949
- const port = options.port ? parseInt(options.port, 10) : DEFAULT_STUDIO_PORT;
2950
- const host = options.host ?? "127.0.0.1";
2951
- const sessionToken = options.token
2952
- ?? runtime.env.KXM_SESSION_TOKEN
2953
- ?? readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR })?.token;
2954
-
2955
- if (runtime.dryRun) {
2956
- print(
2957
- runtime.io,
2958
- runtime.json,
2959
- { ok: true, command: "studio serve", dryRun: true, port, host },
2960
- `would start studio server on http://${host}:${port}`,
2961
- );
2962
- return 0;
2963
- }
2964
-
2965
- try {
2966
- const serverHandle = createStudioServer({
2967
- port,
2968
- host,
2969
- projectRoot: runtime.cwd,
2970
- sessionToken,
2971
- planProvider: () => {
2972
- try {
2973
- const wfDir = join(runtime.cwd, ".kxm", "workflows");
2974
- const defaultPath = join(wfDir, "default.yaml");
2975
- let targetPath = existsSync(defaultPath) ? defaultPath : undefined;
2976
- if (!targetPath && existsSync(wfDir)) {
2977
- const yml = readdirSync(wfDir).find((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
2978
- if (yml) targetPath = join(wfDir, yml);
2979
- }
2980
- if (targetPath && existsSync(targetPath)) {
2981
- const raw = readFileSync(targetPath, "utf8");
2982
- const parsed = parseYaml(raw) as any;
2983
- const wfId = basename(targetPath).replace(/\.(yaml|yml)$/, "");
2984
- return compileVnextWorkflow({ id: wfId, value: parsed });
2985
- }
2986
- } catch {
2987
- // fallback
2988
- }
2989
- return undefined;
2990
- },
2991
- });
2992
- const actualPort = await serverHandle.listen();
2993
- const info = {
2994
- ok: true,
2995
- command: "studio serve",
2996
- port: actualPort,
2997
- host,
2998
- url: `http://${host}:${actualPort}`,
2999
- };
3000
- print(
3001
- runtime.io,
3002
- runtime.json,
3003
- info,
3004
- `KXM Web Studio listening on http://${host}:${actualPort} (Decision Q8 & D14)\nPress Ctrl+C to stop.\n`,
3005
- );
3006
-
3007
- if (runtime.env.KXM_STUDIO_ONCE) {
3008
- await serverHandle.close();
3009
- return 0;
3010
- }
3011
-
3012
- await new Promise<void>((resolveClose) => {
3013
- const shutdown = async () => {
3014
- process.off("SIGINT", shutdown);
3015
- process.off("SIGTERM", shutdown);
3016
- await serverHandle.close();
3017
- resolveClose();
3018
- };
3019
- process.once("SIGINT", shutdown);
3020
- process.once("SIGTERM", shutdown);
3021
- });
3022
- return 0;
3023
- } catch (error) {
3024
- const message = error instanceof Error ? error.message : String(error);
3025
- runtime.io.stderr(`studio serve failed: ${message}\n`);
3026
- return 1;
3027
- }
3028
- }
3029
-
3030
- async function cmdRoleList(
3031
- runtime: Runtime,
3032
- options: { scope?: "all" | "global" | "local" },
3033
- ): Promise<number> {
3034
- const roles = listRoles({
3035
- scope: options.scope,
3036
- repoRoot: runtime.cwd,
3037
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3038
- });
3039
- if (runtime.json) {
3040
- print(runtime.io, true, { ok: true, command: "role list", roles }, "");
3041
- return 0;
3042
- }
3043
- if (roles.length === 0) {
3044
- runtime.io.stdout("No roles configured.\n");
3045
- return 0;
3046
- }
3047
- const lines: string[] = ["ROLES:"];
3048
- for (const r of roles) {
3049
- const scopeTag = r.scope === "overridden" ? "[local override]" : `[${r.scope}]`;
3050
- const modelTag = r.primaryModel ? `(${r.primaryHarness ?? "harness"}:${r.primaryModel})` : "";
3051
- lines.push(` ${r.id.padEnd(16)} ${scopeTag.padEnd(16)} ${modelTag.padEnd(28)} ${r.description}`);
3052
- }
3053
- print(runtime.io, false, {}, `${lines.join("\n")}\n`);
3054
- return 0;
3055
- }
3056
-
3057
- async function cmdRoleGet(
3058
- runtime: Runtime,
3059
- roleId: string,
3060
- options: { scope?: "all" | "global" | "local" },
3061
- ): Promise<number> {
3062
- const result = getRole(roleId, {
3063
- scope: options.scope,
3064
- repoRoot: runtime.cwd,
3065
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3066
- });
3067
- if (!result) {
3068
- runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
3069
- return 1;
3070
- }
3071
- print(
3072
- runtime.io,
3073
- runtime.json,
3074
- { ok: true, command: "role get", roleId, scope: result.scope, filePath: result.filePath, role: result.role },
3075
- stringifyYaml(result.role),
3076
- );
3077
- return 0;
3078
- }
3079
-
3080
- interface PickCandidate {
3081
- id: string;
3082
- label?: string;
3083
- description?: string;
3084
- payload?: any;
3085
- }
3086
-
3087
- async function resolvePickItem(
3088
- io: CliIo,
3089
- title: string,
3090
- items: PickCandidate[],
3091
- pickOption?: string | boolean,
3092
- env?: NodeJS.ProcessEnv,
3093
- ): Promise<PickCandidate | undefined> {
3094
- if (items.length === 0) return undefined;
3095
-
3096
- if (typeof pickOption === "string" && pickOption.trim().length > 0) {
3097
- const trimmed = pickOption.trim();
3098
- const asNum = parseInt(trimmed, 10);
3099
- if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
3100
- return items[asNum - 1];
3101
- }
3102
- const matched = items.find((it) => it.id === trimmed || it.id.toLowerCase() === trimmed.toLowerCase());
3103
- if (matched) return matched;
3104
- }
3105
-
3106
- const lines: string[] = [`${title}:`];
3107
- items.forEach((item, idx) => {
3108
- const desc = item.description ? ` - ${item.description}` : "";
3109
- const label = item.label ? ` [${item.label}]` : "";
3110
- lines.push(` ${(idx + 1).toString().padStart(2)}) ${item.id}${label}${desc}`);
3111
- });
3112
- io.stdout(`${lines.join("\n")}\n`);
3113
-
3114
- const envSelect = env?.KXM_PICK_SELECT ?? process.env.KXM_PICK_SELECT;
3115
- if (envSelect) {
3116
- const asNum = parseInt(envSelect, 10);
3117
- if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
3118
- return items[asNum - 1];
3119
- }
3120
- const matched = items.find((it) => it.id === envSelect);
3121
- if (matched) return matched;
3122
- }
3123
-
3124
- if (!process.stdin.isTTY) {
3125
- return undefined;
3126
- }
3127
-
3128
- const rl = createInterface({
3129
- input: process.stdin,
3130
- output: process.stdout,
3131
- });
3132
-
3133
- return new Promise((resolvePrompt) => {
3134
- rl.question(`Enter selection (1-${items.length}) or ID: `, (answer) => {
3135
- rl.close();
3136
- const ans = answer.trim();
3137
- const asNum = parseInt(ans, 10);
3138
- if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
3139
- resolvePrompt(items[asNum - 1]);
3140
- } else {
3141
- const matched = items.find((it) => it.id === ans);
3142
- resolvePrompt(matched ?? undefined);
3143
- }
3144
- });
3145
- });
3146
- }
3147
-
3148
- async function cmdRoleAdd(
3149
- runtime: Runtime,
3150
- roleId: string | undefined,
3151
- options: {
3152
- file?: string;
3153
- description?: string;
3154
- skills?: string;
3155
- harness?: string;
3156
- model?: string;
3157
- scope?: "global" | "local";
3158
- overwrite?: boolean;
3159
- pick?: string | boolean;
3160
- },
3161
- ): Promise<number> {
3162
- const scope = options.scope ?? "local";
3163
- if (!roleId || options.pick) {
3164
- const candidates: PickCandidate[] = Object.values(DEFAULT_ROLES).map((r) => ({
3165
- id: r.id,
3166
- description: r.description,
3167
- label: "template",
3168
- payload: r,
3169
- }));
3170
- if (scope === "local") {
3171
- const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
3172
- for (const gr of globalRoles) {
3173
- if (!candidates.some((c) => c.id === gr.id)) {
3174
- candidates.push({ id: gr.id, description: gr.description, label: "global", payload: gr });
3175
- }
3176
- }
3177
- }
3178
- const picked = await resolvePickItem(runtime.io, `Select a role template to add (${scope})`, candidates, options.pick, runtime.env);
3179
- if (!picked) {
3180
- if (!roleId) {
3181
- runtime.io.stderr("role add failed: missing roleId or pick selection\n");
3182
- return 1;
3183
- }
3184
- } else {
3185
- roleId = picked.id;
3186
- if (!options.file && picked.payload && DEFAULT_ROLES[picked.id]) {
3187
- const base = DEFAULT_ROLES[picked.id]!;
3188
- const roleDef: KxmRoleDefinition = {
3189
- ...base,
3190
- description: options.description || base.description,
3191
- skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
3192
- roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster,
3193
- };
3194
- try {
3195
- const res = addRole(roleDef, {
3196
- scope,
3197
- repoRoot: runtime.cwd,
3198
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3199
- overwrite: options.overwrite,
3200
- });
3201
- print(
3202
- runtime.io,
3203
- runtime.json,
3204
- { ok: true, command: "role add", roleId, ...res },
3205
- `Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
3206
- );
3207
- return 0;
3208
- } catch (err: unknown) {
3209
- runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
3210
- return 1;
3211
- }
3212
- }
3213
- }
3214
- }
3215
-
3216
- let roleDef: KxmRoleDefinition;
3217
- if (options.file) {
3218
- const filePath = resolve(runtime.cwd, options.file);
3219
- const content = readFileSync(filePath, "utf8");
3220
- roleDef = parseYaml(content) as KxmRoleDefinition;
3221
- roleDef.id = roleId!;
3222
- } else {
3223
- const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
3224
- const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
3225
- roleDef = {
3226
- schema: "kxm.role.v1",
3227
- id: roleId!,
3228
- description: options.description || `Role ${roleId}`,
3229
- skills,
3230
- roster,
3231
- };
3232
- }
3233
-
3234
- try {
3235
- const res = addRole(roleDef, {
3236
- scope,
3237
- repoRoot: runtime.cwd,
3238
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3239
- overwrite: options.overwrite,
3240
- });
3241
- print(
3242
- runtime.io,
3243
- runtime.json,
3244
- { ok: true, command: "role add", roleId, ...res },
3245
- `Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
3246
- );
3247
- return 0;
3248
- } catch (err: unknown) {
3249
- runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
3250
- return 1;
3251
- }
3252
- }
3253
-
3254
- async function cmdRoleRemove(
3255
- runtime: Runtime,
3256
- roleId: string | undefined,
3257
- options: { scope?: "global" | "local"; pick?: string | boolean },
3258
- ): Promise<number> {
3259
- const scope = options.scope ?? "local";
3260
- if (!roleId || options.pick) {
3261
- const roles = listRoles({
3262
- scope,
3263
- repoRoot: runtime.cwd,
3264
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3265
- });
3266
- if (roles.length === 0) {
3267
- runtime.io.stdout(`No roles configured in ${scope} scope to remove.\n`);
3268
- return 0;
3269
- }
3270
- const candidates: PickCandidate[] = roles.map((r) => ({
3271
- id: r.id,
3272
- description: r.description,
3273
- label: r.scope,
3274
- }));
3275
- const picked = await resolvePickItem(runtime.io, `Select a role to remove (${scope})`, candidates, options.pick, runtime.env);
3276
- if (!picked) {
3277
- if (!roleId) {
3278
- runtime.io.stderr("role remove failed: missing roleId or pick selection\n");
3279
- return 1;
3280
- }
3281
- } else {
3282
- roleId = picked.id;
3283
- }
3284
- }
3285
-
3286
- try {
3287
- const res = removeRole(roleId!, {
3288
- scope,
3289
- repoRoot: runtime.cwd,
3290
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3291
- });
3292
- print(
3293
- runtime.io,
3294
- runtime.json,
3295
- { ok: true, command: "role remove", roleId, ...res },
3296
- `Removed role '${roleId}' from ${res.scope}\n`,
3297
- );
3298
- return 0;
3299
- } catch (err: unknown) {
3300
- runtime.io.stderr(`role remove failed: ${(err as Error).message}\n`);
3301
- return 1;
3302
- }
3303
- }
3304
-
3305
- async function cmdRoleModify(
3306
- runtime: Runtime,
3307
- roleId: string | undefined,
3308
- options: {
3309
- description?: string;
3310
- addSkill?: string;
3311
- removeSkill?: string;
3312
- addModel?: string;
3313
- removeModel?: string;
3314
- scope?: "global" | "local";
3315
- pick?: string | boolean;
3316
- },
3317
- ): Promise<number> {
3318
- if (!roleId || options.pick) {
3319
- const roles = listRoles({
3320
- scope: options.scope,
3321
- repoRoot: runtime.cwd,
3322
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3323
- });
3324
- if (roles.length === 0) {
3325
- runtime.io.stdout("No roles configured to modify.\n");
3326
- return 0;
3327
- }
3328
- const candidates: PickCandidate[] = roles.map((r) => ({
3329
- id: r.id,
3330
- description: r.description,
3331
- label: r.scope,
3332
- }));
3333
- const picked = await resolvePickItem(runtime.io, "Select a role to modify", candidates, options.pick, runtime.env);
3334
- if (!picked) {
3335
- if (!roleId) {
3336
- runtime.io.stderr("role modify failed: missing roleId or pick selection\n");
3337
- return 1;
3338
- }
3339
- } else {
3340
- roleId = picked.id;
3341
- }
3342
- }
3343
-
3344
- const existing = getRole(roleId!, {
3345
- scope: options.scope,
3346
- repoRoot: runtime.cwd,
3347
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3348
- });
3349
- if (!existing) {
3350
- runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
3351
- return 1;
3352
- }
3353
-
3354
- const role = existing.role;
3355
- let skills = [...(role.skills ?? [])];
3356
- if (options.addSkill && !skills.includes(options.addSkill)) {
3357
- skills.push(options.addSkill);
3358
- }
3359
- if (options.removeSkill) {
3360
- skills = skills.filter((s) => s !== options.removeSkill);
3361
- }
3362
-
3363
- let roster = [...(role.roster ?? [])];
3364
- if (options.addModel) {
3365
- const [harnessOrModel, maybeModel] = options.addModel.split(":");
3366
- const harness = maybeModel ? harnessOrModel! : "pi";
3367
- const model = maybeModel || harnessOrModel!;
3368
- roster.push({ harness, model });
3369
- }
3370
- if (options.removeModel) {
3371
- roster = roster.filter((entry) => entry.model !== options.removeModel);
3372
- }
3373
-
3374
- const updates: Partial<KxmRoleDefinition> = {
3375
- ...(options.description ? { description: options.description } : {}),
3376
- skills,
3377
- roster,
3378
- };
3379
-
3380
- try {
3381
- const res = modifyRole(roleId!, updates, {
3382
- scope: options.scope,
3383
- repoRoot: runtime.cwd,
3384
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3385
- });
3386
- print(
3387
- runtime.io,
3388
- runtime.json,
3389
- { ok: true, command: "role modify", roleId, ...res },
3390
- `Modified role '${roleId}' in ${res.scope}\n`,
3391
- );
3392
- return 0;
3393
- } catch (err: unknown) {
3394
- runtime.io.stderr(`role modify failed: ${(err as Error).message}\n`);
3395
- return 1;
3396
- }
3397
- }
3398
-
3399
- async function cmdRoleHosts(
3400
- runtime: Runtime,
3401
- options: { scope?: "all" | "global" | "local" } = {},
3402
- ): Promise<number> {
3403
- const hostsConfig = loadRoleHostsConfig({
3404
- scope: options.scope,
3405
- repoRoot: runtime.cwd,
3406
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3407
- });
3408
-
3409
- const seatIds = Array.from(
3410
- new Set([
3411
- ...Object.keys(DEFAULT_ROLE_SEATS),
3412
- ...Object.keys(hostsConfig.config.seats ?? {}),
3413
- ]),
3414
- ).sort();
3415
-
3416
- const seats = seatIds.map((seatId) => {
3417
- const resolved = resolveRoleSeat(seatId, {
3418
- repoRoot: runtime.cwd,
3419
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3420
- });
3421
- return {
3422
- seatId,
3423
- host: resolved.host,
3424
- model: resolved.model,
3425
- provider: resolved.provider,
3426
- effort: resolved.effort,
3427
- source: resolved.source,
3428
- configuredHost: hostsConfig.config.seats?.[seatId]?.host,
3429
- configuredModel: hostsConfig.config.seats?.[seatId]?.model,
3430
- };
3431
- });
3432
-
3433
- if (runtime.json) {
3434
- print(
3435
- runtime.io,
3436
- true,
3437
- {
3438
- ok: true,
3439
- command: "role hosts",
3440
- scope: hostsConfig.scope,
3441
- filePath: hostsConfig.filePath,
3442
- seats,
3443
- hostProviders: hostsConfig.config.hostProviders ?? {},
3444
- },
3445
- "",
3446
- );
3447
- return 0;
3448
- }
3449
-
3450
- const lines: string[] = [
3451
- `ROLE SEATS (${hostsConfig.scope}${hostsConfig.filePath ? ` at ${hostsConfig.filePath}` : ""}):`,
3452
- ];
3453
- for (const s of seats) {
3454
- const modelStr = s.model ? ` [${s.model}]` : "";
3455
- const sourceTag = `(via ${s.source})`;
3456
- lines.push(` ${s.seatId.padEnd(16)} -> host: ${s.host.padEnd(12)} ${modelStr.padEnd(30)} ${sourceTag}`);
3457
- }
3458
- if (hostsConfig.config.hostProviders && Object.keys(hostsConfig.config.hostProviders).length > 0) {
3459
- lines.push("\nHOST PROVIDERS:");
3460
- for (const [h, p] of Object.entries(hostsConfig.config.hostProviders)) {
3461
- lines.push(` ${h.padEnd(16)} -> provider: ${p}`);
3462
- }
3463
- }
3464
- print(runtime.io, false, {}, `${lines.join("\n")}\n`);
3465
- return 0;
3466
- }
3467
-
3468
- async function cmdRoleSetHost(
3469
- runtime: Runtime,
3470
- seatId: string,
3471
- host: string,
3472
- options: {
3473
- model?: string;
3474
- effort?: "low" | "medium" | "high" | "xhigh";
3475
- scope?: "global" | "local";
3476
- } = {},
3477
- ): Promise<number> {
3478
- if (!seatId || !host) {
3479
- runtime.io.stderr("kxm role set-host requires <seatId> and <host>\n");
3480
- return 1;
3481
- }
3482
-
3483
- try {
3484
- const result = setRoleSeatHost(seatId, host, {
3485
- model: options.model,
3486
- effort: options.effort,
3487
- scope: options.scope ?? "local",
3488
- repoRoot: runtime.cwd,
3489
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3490
- });
3491
-
3492
- print(
3493
- runtime.io,
3494
- runtime.json,
3495
- {
3496
- ok: true,
3497
- command: "role set-host",
3498
- seatId,
3499
- host,
3500
- binding: result.binding,
3501
- filePath: result.filePath,
3502
- scope: result.scope,
3503
- },
3504
- `Bound seat '${seatId}' to host '${host}' in ${result.filePath}\n`,
3505
- );
3506
- return 0;
3507
- } catch (err: unknown) {
3508
- runtime.io.stderr(`role set-host failed: ${(err as Error).message}\n`);
3509
- return 1;
3510
- }
3511
- }
3512
-
3513
- async function cmdRoleResume(
3514
- runtime: Runtime,
3515
- runId: string,
3516
- ruling?: string,
3517
- ): Promise<number> {
3518
- if (!runId) {
3519
- runtime.io.stderr("kxm role resume requires <runId>\n");
3520
- return 1;
3521
- }
3522
-
3523
- const effectiveRuling = ruling?.trim() || "operator_ruling: waived and resumed";
3524
-
3525
- // Check if it's a vNext run
3526
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
3527
- if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
3528
- if (runtime.dryRun) {
3529
- print(runtime.io, runtime.json, { ok: true, command: "role resume", runId, ruling: effectiveRuling }, `would resume vNext run ${runId}`);
3530
- return 0;
3531
- }
3532
- try {
3533
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
3534
- const posted = await vnextRuntimeRequest(
3535
- supervisor,
3536
- "POST",
3537
- `/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
3538
- {
3539
- signalKey: "audit_escalation",
3540
- status: "passed",
3541
- summary: effectiveRuling,
3542
- action: "unblock",
3543
- },
3544
- );
3545
- print(
3546
- runtime.io,
3547
- runtime.json,
3548
- { ok: true, command: "role resume", runId, ruling: effectiveRuling, unblocked: posted.unblocked === true },
3549
- `Resumed vNext run ${runId} with ruling: ${effectiveRuling}\n`,
3550
- );
3551
- return 0;
3552
- } catch (error) {
3553
- const msg = error instanceof Error ? error.message : "resume_failed";
3554
- print(runtime.io, runtime.json, { ok: false, command: "role resume", error: "resume_failed", detail: msg }, `resume vNext run failed: ${msg}\n`);
3555
- return 1;
3556
- }
3557
- }
3558
-
3559
- // Workflow run: check local state database (.kxm/state/kxm.db)
3560
- const dbPath = join(runtime.cwd, ".kxm", "state", "kxm.db");
3561
- if (existsSync(dbPath)) {
3562
- try {
3563
- const database = new DatabaseSync(dbPath);
3564
- try {
3565
- const row = database.prepare("SELECT record FROM workflow_runs WHERE id = ?").get(runId) as { record: string } | undefined;
3566
- if (!row) {
3567
- runtime.io.stderr(`kxm: workflow run '${runId}' not found in ${dbPath}\n`);
3568
- return 1;
3569
- }
3570
- const run = JSON.parse(row.record) as WorkflowRun;
3571
- const now = new Date().toISOString();
3572
- const resumeResult = resumeWorkflowFromRuling(run, effectiveRuling, now);
3573
-
3574
- database.prepare("UPDATE workflow_runs SET record = ? WHERE id = ?").run(
3575
- JSON.stringify(resumeResult.run),
3576
- runId,
3577
- );
3578
-
3579
- const journalId = randomUUID();
3580
- const journalPayload = {
3581
- id: journalId,
3582
- runId,
3583
- stageId: resumeResult.stageId,
3584
- category: "decision" as const,
3585
- area: "workflow" as const,
3586
- summary: `Role resume ruling: ${effectiveRuling}`,
3587
- details: { ruling: effectiveRuling },
3588
- evidence: {},
3589
- createdAt: now,
3590
- };
3591
-
3592
- database.prepare(
3593
- "INSERT INTO workflow_journal (id, run_id, category, area, record) VALUES (?, ?, ?, ?, ?)",
3594
- ).run(
3595
- journalId,
3596
- runId,
3597
- "decision",
3598
- "workflow",
3599
- JSON.stringify(journalPayload),
3600
- );
3601
-
3602
- print(
3603
- runtime.io,
3604
- runtime.json,
3605
- { ok: true, command: "role resume", runId, stageId: resumeResult.stageId, ruling: effectiveRuling, status: resumeResult.run.status },
3606
- `Resumed workflow run ${runId} (stage: ${resumeResult.stageId}) with ruling: ${effectiveRuling}\n`,
3607
- );
3608
- return 0;
3609
- } finally {
3610
- database.close();
3611
- }
3612
- } catch (err: unknown) {
3613
- runtime.io.stderr(`role resume failed: ${(err as Error).message}\n`);
3614
- return 1;
3615
- }
3616
- }
3617
-
3618
- runtime.io.stderr(`kxm: no active run store or database found for run '${runId}'\n`);
3619
- return 1;
3620
- }
3621
-
3622
- async function cmdWorkflowDefinitions(
3623
- runtime: Runtime,
3624
- options: { scope?: "all" | "global" | "local" },
3625
- ): Promise<number> {
3626
- const workflows = listWorkflowDefinitions({
3627
- scope: options.scope,
3628
- repoRoot: runtime.cwd,
3629
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3630
- });
3631
- if (runtime.json) {
3632
- print(runtime.io, true, { ok: true, command: "workflow definitions", workflows }, "");
3633
- return 0;
3634
- }
3635
- if (workflows.length === 0) {
3636
- runtime.io.stdout("No workflow definitions found.\n");
3637
- return 0;
3638
- }
3639
- const lines: string[] = ["WORKFLOW DEFINITIONS:"];
3640
- for (const w of workflows) {
3641
- const scopeTag = w.scope === "overridden" ? "[local override]" : `[${w.scope}]`;
3642
- const rolesTag = w.roles.length > 0 ? `(roles: ${w.roles.join(", ")})` : "";
3643
- lines.push(` ${w.id.padEnd(24)} ${scopeTag.padEnd(16)} ${w.stepCount} steps ${rolesTag} ${w.description}`);
3644
- }
3645
- print(runtime.io, false, {}, `${lines.join("\n")}\n`);
3646
- return 0;
3647
- }
3648
-
3649
- async function cmdWorkflowAdd(
3650
- runtime: Runtime,
3651
- workflowId: string | undefined,
3652
- options: {
3653
- file?: string;
3654
- description?: string;
3655
- scope?: "global" | "local";
3656
- overwrite?: boolean;
3657
- pick?: string | boolean;
3658
- },
3659
- ): Promise<number> {
3660
- const scope = options.scope ?? "local";
3661
- let content: Record<string, unknown> | string | undefined;
3662
- if (!workflowId || options.pick) {
3663
- const candidates: PickCandidate[] = Object.entries(WORKFLOW_TEMPLATES).map(([id, tmpl]) => ({
3664
- id,
3665
- description: String(tmpl.description ?? id),
3666
- label: "template",
3667
- payload: tmpl,
3668
- }));
3669
- if (scope === "local") {
3670
- const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
3671
- for (const gd of globalDefs) {
3672
- if (!candidates.some((c) => c.id === gd.id)) {
3673
- candidates.push({ id: gd.id, description: gd.description, label: "global" });
3674
- }
3675
- }
3676
- }
3677
- const picked = await resolvePickItem(runtime.io, `Select a workflow template to add (${scope})`, candidates, options.pick, runtime.env);
3678
- if (!picked) {
3679
- if (!workflowId) {
3680
- runtime.io.stderr("workflow add failed: missing workflowId or pick selection\n");
3681
- return 1;
3682
- }
3683
- } else {
3684
- workflowId = picked.id;
3685
- if (!options.file && picked.payload) {
3686
- content = {
3687
- ...picked.payload,
3688
- ...(options.description ? { description: options.description } : {}),
3689
- };
3690
- }
3691
- }
3692
- }
3693
-
3694
- if (options.file) {
3695
- const filePath = resolve(runtime.cwd, options.file);
3696
- content = readFileSync(filePath, "utf8");
3697
- } else if (!content) {
3698
- content = {
3699
- schema: "kxm.workflow.v1",
3700
- id: workflowId!,
3701
- description: options.description || `Workflow ${workflowId}`,
3702
- coordinator: "coordinator",
3703
- limits: { maxTransitions: 8 },
3704
- steps: [
3705
- {
3706
- id: "step-1",
3707
- kind: "agent",
3708
- role: "writer",
3709
- on: { passed: { target: "$terminal", terminalStatus: "completed" } },
3710
- },
3711
- ],
3712
- };
3713
- }
3714
-
3715
- try {
3716
- const res = addWorkflowDefinition(workflowId!, content, {
3717
- scope,
3718
- repoRoot: runtime.cwd,
3719
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3720
- overwrite: options.overwrite,
3721
- });
3722
- print(
3723
- runtime.io,
3724
- runtime.json,
3725
- { ok: true, command: "workflow add", workflowId, ...res },
3726
- `Added workflow '${workflowId}' to ${res.scope} (${res.filePath})\n`,
3727
- );
3728
- return 0;
3729
- } catch (err: unknown) {
3730
- runtime.io.stderr(`workflow add failed: ${(err as Error).message}\n`);
3731
- return 1;
3732
- }
3733
- }
3734
-
3735
- async function cmdWorkflowRemove(
3736
- runtime: Runtime,
3737
- workflowId: string | undefined,
3738
- options: { scope?: "global" | "local"; pick?: string | boolean },
3739
- ): Promise<number> {
3740
- const scope = options.scope ?? "local";
3741
- if (!workflowId || options.pick) {
3742
- const workflows = listWorkflowDefinitions({
3743
- scope,
3744
- repoRoot: runtime.cwd,
3745
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3746
- });
3747
- if (workflows.length === 0) {
3748
- runtime.io.stdout(`No workflow definitions found in ${scope} scope to remove.\n`);
3749
- return 0;
3750
- }
3751
- const candidates: PickCandidate[] = workflows.map((w) => ({
3752
- id: w.id,
3753
- description: w.description,
3754
- label: w.scope,
3755
- }));
3756
- const picked = await resolvePickItem(runtime.io, `Select a workflow to remove (${scope})`, candidates, options.pick, runtime.env);
3757
- if (!picked) {
3758
- if (!workflowId) {
3759
- runtime.io.stderr("workflow remove failed: missing workflowId or pick selection\n");
3760
- return 1;
3761
- }
3762
- } else {
3763
- workflowId = picked.id;
3764
- }
3765
- }
3766
-
3767
- try {
3768
- const res = removeWorkflowDefinition(workflowId!, {
3769
- scope,
3770
- repoRoot: runtime.cwd,
3771
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3772
- });
3773
- print(
3774
- runtime.io,
3775
- runtime.json,
3776
- { ok: true, command: "workflow remove", workflowId, ...res },
3777
- `Removed workflow '${workflowId}' from ${res.scope}\n`,
3778
- );
3779
- return 0;
3780
- } catch (err: unknown) {
3781
- runtime.io.stderr(`workflow remove failed: ${(err as Error).message}\n`);
3782
- return 1;
3783
- }
3784
- }
3785
-
3786
- async function cmdWorkflowModify(
3787
- runtime: Runtime,
3788
- workflowId: string | undefined,
3789
- options: {
3790
- description?: string;
3791
- scope?: "global" | "local";
3792
- pick?: string | boolean;
3793
- },
3794
- ): Promise<number> {
3795
- if (!workflowId || options.pick) {
3796
- const workflows = listWorkflowDefinitions({
3797
- scope: options.scope,
3798
- repoRoot: runtime.cwd,
3799
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3800
- });
3801
- if (workflows.length === 0) {
3802
- runtime.io.stdout("No workflow definitions found to modify.\n");
3803
- return 0;
3804
- }
3805
- const candidates: PickCandidate[] = workflows.map((w) => ({
3806
- id: w.id,
3807
- description: w.description,
3808
- label: w.scope,
3809
- }));
3810
- const picked = await resolvePickItem(runtime.io, "Select a workflow to modify", candidates, options.pick, runtime.env);
3811
- if (!picked) {
3812
- if (!workflowId) {
3813
- runtime.io.stderr("workflow modify failed: missing workflowId or pick selection\n");
3814
- return 1;
3815
- }
3816
- } else {
3817
- workflowId = picked.id;
3818
- }
3819
- }
3820
-
3821
- try {
3822
- const res = modifyWorkflowDefinition(
3823
- workflowId!,
3824
- { ...(options.description ? { description: options.description } : {}) },
3825
- {
3826
- scope: options.scope,
3827
- repoRoot: runtime.cwd,
3828
- userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
3829
- },
3830
- );
3831
- print(
3832
- runtime.io,
3833
- runtime.json,
3834
- { ok: true, command: "workflow modify", workflowId, ...res },
3835
- `Modified workflow '${workflowId}' in ${res.scope}\n`,
3836
- );
3837
- return 0;
3838
- } catch (err: unknown) {
3839
- runtime.io.stderr(`workflow modify failed: ${(err as Error).message}\n`);
3840
- return 1;
3841
- }
3842
- }
3843
-
3844
-
3845
-
3846
- async function cmdRoutingReport(
3847
- runtime: Runtime,
3848
- options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string },
3849
- ): Promise<number> {
3850
- const file = options.file ?? telemetryPath(runtime.dirs.logs);
3851
- const records = readRoutingRecords(file).map((entry) => entry.routing);
3852
- const includeEquivalentListCost = Boolean(options.equivalentListCost || options.listPrices);
3853
-
3854
- let catalog: PriceCatalog | undefined;
3855
- if (includeEquivalentListCost) {
3856
- try {
3857
- const pricesPath = options.prices ? resolve(runtime.cwd, options.prices) : join(runtime.dirs.workspace, "prices.yaml");
3858
- catalog = loadPriceCatalog(pricesPath);
3859
- } catch {
3860
- // price catalog optional / best effort
3861
- }
3862
- }
3863
-
3864
- const report = generateRoutingReport(records, { catalog, includeEquivalentListCost });
3865
-
3866
- if (records.length === 0) {
3867
- print(runtime.io, runtime.json, { ok: true, command: "routing report", file, configurations: [], report }, "no routing records in telemetry");
3868
- return 0;
3869
- }
3870
-
3871
- const v1Records = records.filter((r) => r.schema === "kxm.routing-record.v1") as any[];
3872
- const configurations = v1Records.length > 0
3873
- ? [...groupByBehavior(v1Records).entries()]
3874
- .map(([hash, group]) => ({ ...compareRoutingRecords(group), behavioralSha256: hash }))
3875
- .sort((left, right) => right.runs - left.runs || left.behavioralSha256.localeCompare(right.behavioralSha256))
3876
- : [];
3877
-
3878
- const text = formatRoutingReport(report, { equivalentListCost: includeEquivalentListCost });
3879
- print(
3880
- runtime.io,
3881
- runtime.json,
3882
- { ok: true, command: "routing report", file, configurations, report },
3883
- text,
3884
- );
3885
- return 0;
3886
- }
3887
-
3888
- async function cmdRoutingBenchmark(
3889
- runtime: Runtime,
3890
- options: { task?: string; arms?: string; runs?: string },
3891
- ): Promise<number> {
3892
- const task = options.task || "Deterministic benchmark task";
3893
- const armsStr = options.arms || "grok/grok-4.6,claude/fable,pi/qwen3-coder-plus";
3894
- const armsList = armsStr.split(",").map((s) => s.trim()).filter(Boolean);
3895
- const runsCount = Math.max(1, parseInt(options.runs || "1", 10) || 1);
1
+ /**
2
+ * KontextMind (KXM) CLI Program and Entrypoint.
3
+ *
4
+ * Provides Commander command definitions and argument routing for:
5
+ * - Workflow modes & context explain (`kxm explain`)
6
+ * - Multiplexed remote SSH operations (`kxm ssh`)
7
+ * - Role & workflow governance (`kxm role`, `kxm workflow`, `kxm gate`, `kxm signal`)
8
+ * - Task & goal management (`kxm goal`, `kxm task`, `kxm suggest`, `kxm studio`)
9
+ * - Knowledge, skills, and memory (`kxm context`, `kxm skills`, `kxm memory`)
10
+ * - vNext runtime & initialization (`kxm init`, `kxm migrate`, `kxm trust`, `kxm run`, `kxm harness`)
11
+ * - Hub, workers, and dashboard (`kxm hub`, `kxm worker`, `kxm dash`, `kxm session`, `kxm auth`)
12
+ * - System, update, and configuration (`kxm update`, `kxm config`, `kxm completion`, `kxm improve`)
13
+ */
3896
14
 
3897
- const arms = armsList.map((arm) => {
3898
- const parts = arm.includes("/") ? arm.split("/") : ["native", arm];
3899
- const harness = parts[0]!;
3900
- const model = parts.slice(1).join("/");
3901
- const latencyMs = model.includes("grok") ? 420 : model.includes("qwen") ? 560 : 680;
3902
- const costUsd = model.includes("grok") ? 0.17 : model.includes("qwen") ? 0.12 : 0.45;
3903
- return {
3904
- harness,
3905
- model,
3906
- latencyMs,
3907
- tokensIn: 1200,
3908
- tokensOut: 450,
3909
- costUsd,
3910
- outcome: "passed" as const,
3911
- };
3912
- });
15
+ import { basename, dirname, join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { Command, CommanderError } from "commander";
18
+ import { readInstalledKxmVersion } from "./kxm-update.ts";
19
+ import { HubClient } from "./client.ts";
20
+ import {
21
+ AGENT_COMMANDS_MAP,
22
+ enforceToolPolicy,
23
+ } from "./commands.ts";
24
+ import { discoverVnextProjectRoot } from "./vnext-config.ts";
25
+ import { ensureVnextSupervisor, vnextRuntimeRequest } from "./vnext-runtime-supervisor.ts";
3913
26
 
3914
- const headers = [
3915
- "Harness".padEnd(10),
3916
- "Model".padEnd(24),
3917
- "Latency(ms)".padStart(12),
3918
- "TokensIn".padStart(10),
3919
- "TokensOut".padStart(10),
3920
- "Cost($)".padStart(10),
3921
- "Outcome".padStart(10),
3922
- ].join(" ");
27
+ // Submodule imports
28
+ import {
29
+ print,
30
+ runtimeFrom,
31
+ redactConfiguredValues,
32
+ type CliContext,
33
+ type CliIo,
34
+ type CliSpawnResult,
35
+ type Runtime,
36
+ } from "./cli/types.ts";
3923
37
 
3924
- const lines = [
3925
- `Routing Benchmark Results (task: ${task}, runs: ${runsCount})`,
3926
- headers,
3927
- ];
38
+ import {
39
+ cmdRoleList,
40
+ cmdRoleGet,
41
+ cmdRoleAdd,
42
+ cmdRoleRemove,
43
+ cmdRoleModify,
44
+ cmdRoleHosts,
45
+ cmdRoleSetHost,
46
+ cmdRoleResume,
47
+ } from "./cli/roles.ts";
3928
48
 
3929
- for (const a of arms) {
3930
- lines.push([
3931
- a.harness.padEnd(10),
3932
- (a.model.length > 24 ? `${a.model.slice(0, 21)}...` : a.model).padEnd(24),
3933
- String(a.latencyMs).padStart(12),
3934
- String(a.tokensIn).padStart(10),
3935
- String(a.tokensOut).padStart(10),
3936
- `$${a.costUsd.toFixed(2)}`.padStart(10),
3937
- a.outcome.padStart(10),
3938
- ].join(" "));
3939
- }
49
+ import {
50
+ cmdWorkflowDefinitions,
51
+ cmdWorkflowAdd,
52
+ cmdWorkflowRemove,
53
+ cmdWorkflowModify,
54
+ cmdWorkflowStart,
55
+ cmdWorkflowDegrade,
56
+ cmdWorkflowInspect,
57
+ cmdSignal,
58
+ cmdGithubWatch,
59
+ cmdRetrospectiveExport,
60
+ } from "./cli/workflows.ts";
3940
61
 
3941
- print(
3942
- runtime.io,
3943
- runtime.json,
3944
- {
3945
- ok: true,
3946
- command: "routing benchmark",
3947
- task,
3948
- runs: runsCount,
3949
- timestamp: new Date().toISOString(),
3950
- arms,
3951
- },
3952
- lines.join("\n"),
3953
- );
3954
- return 0;
3955
- }
62
+ import {
63
+ cmdSuggest,
64
+ cmdGoalCreate,
65
+ cmdGoalList,
66
+ cmdTaskCreate,
67
+ cmdTaskList,
68
+ cmdTaskGet,
69
+ cmdTaskRun,
70
+ cmdTaskSync,
71
+ cmdStudioLayout,
72
+ cmdStudioServe,
73
+ } from "./cli/tasks.ts";
3956
74
 
3957
- async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }): Promise<number> {
3958
- const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim();
3959
- const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`);
3960
- const event = options.event;
3961
- const payloadFlag = options.payload ?? "{}";
3962
- if (!definitionId) {
3963
- runtime.io.stderr("workflow start requires <definitionId> (or KXM_WORKFLOW_ID)\n");
3964
- return 2;
3965
- }
3966
- let secret: string | undefined;
3967
- try {
3968
- secret = workflowCredential(runtime, definitionId, "start");
3969
- } catch (error) {
3970
- return reportWorkflowConfigError(runtime, error);
3971
- }
3972
- if (!secret) {
3973
- runtime.io.stderr("workflow start requires KXM_WORKFLOW_SECRET when no active definition source is configured\n");
3974
- return 2;
3975
- }
3976
- let payload: Record<string, unknown>;
3977
- try {
3978
- const raw = payloadFlag.startsWith("@") ? readFileSync(resolve(runtime.cwd, payloadFlag.slice(1)), "utf8") : payloadFlag;
3979
- const value = JSON.parse(raw) as unknown;
3980
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("object required");
3981
- payload = value as Record<string, unknown>;
3982
- } catch {
3983
- print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "invalid_payload" }, "workflow payload must be a JSON object or @file");
3984
- return 2;
3985
- }
3986
- if (runtime.dryRun) {
3987
- print(runtime.io, runtime.json, { ok: true, command: "workflow start", dryRun: true, definitionId, deliveryId, event }, "would POST a signed workflow webhook");
3988
- return 0;
3989
- }
3990
- try {
3991
- const response = await postWorkflowStart({ serverUrl: runtime.serverUrl, definitionId, secret, deliveryId, ...(event ? { event } : {}), payload, fetchImpl: runtime.fetchImpl });
3992
- print(runtime.io, runtime.json, { ok: true, command: "workflow start", definitionId, deliveryId, ...response }, `started workflow ${response.runId ?? "accepted"}`);
3993
- return 0;
3994
- } catch {
3995
- print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "workflow_start_failed" }, "signed workflow start failed");
3996
- return 1;
3997
- }
3998
- }
75
+ import {
76
+ hubContextPost,
77
+ cmdContextGet,
78
+ cmdContextRecall,
79
+ cmdContextState,
80
+ cmdContextEpisode,
81
+ cmdContextPromote,
82
+ cmdContextExplain,
83
+ cmdContextWikiCompile,
84
+ cmdContextWikiLint,
85
+ cmdSkillsCreate,
86
+ cmdSkillsEvaluate,
87
+ cmdSkillsPromote,
88
+ cmdSkillsReject,
89
+ cmdSkillsList,
90
+ cmdSkillsVerify,
91
+ cmdMemoryBrief,
92
+ cmdMemoryNote,
93
+ cmdMemorySync,
94
+ } from "./cli/context-skills.ts";
3999
95
 
4000
- async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string; reason?: string }): Promise<number> {
4001
- const requirementKey = options.requirement?.trim() ?? "";
4002
- const reason = options.reason?.trim() ?? "";
4003
- const adminToken = runtime.env.KXM_AUTH_TOKEN?.trim();
4004
- if (!runId || !stageId || !requirementKey || !reason || !adminToken) {
4005
- runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n");
4006
- return 2;
4007
- }
4008
- const worker = gateOf(runtime, "degrade");
4009
- if (runtime.dryRun) {
4010
- printWorker(
4011
- runtime,
4012
- worker,
4013
- { ok: true, command: "workflow degrade", dryRun: true, runId, stageId, requirementKey },
4014
- `would approve configured degraded quorum for ${runId}/${stageId}/${requirementKey}`,
4015
- );
4016
- return 0;
4017
- }
4018
- try {
4019
- const result = await postWorkflowDegradation({
4020
- serverUrl: runtime.serverUrl,
4021
- authToken: adminToken,
4022
- runId,
4023
- stageId,
4024
- requirementKey,
4025
- reason,
4026
- fetchImpl: runtime.fetchImpl,
4027
- });
4028
- printWorker(
4029
- runtime,
4030
- worker,
4031
- { ok: true, command: "workflow degrade", runId, stageId, requirementKey, ...result },
4032
- result.duplicate ? "degraded quorum was already approved" : "approved configured degraded quorum",
4033
- );
4034
- return 0;
4035
- } catch {
4036
- printWorker(
4037
- runtime,
4038
- worker,
4039
- { ok: false, command: "workflow degrade", error: "workflow_degradation_failed", runId, stageId, requirementKey },
4040
- "workflow degradation approval failed",
4041
- );
4042
- return 1;
4043
- }
4044
- }
96
+ import {
97
+ cmdVnextInit,
98
+ cmdVnextMigratePlan,
99
+ cmdVnextMigrateApply,
100
+ cmdVnextMigrateVerify,
101
+ cmdBackup,
102
+ cmdRestore,
103
+ cmdVnextTrust,
104
+ cmdVnextRun,
105
+ cmdVnextRunStatus,
106
+ cmdVnextRunDrive,
107
+ cmdVnextRunCancel,
108
+ cmdVnextRunList,
109
+ cmdHarnessList,
110
+ cmdProducerChange,
111
+ cmdModelsScreen,
112
+ cmdProducerList,
113
+ cmdModelInventoryRefresh,
114
+ cmdVnextRuntime,
115
+ } from "./cli/vnext.ts";
4045
116
 
4046
- async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runId?: string): Promise<number> {
4047
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
4048
- if (action === "get" && !runId) {
4049
- runtime.io.stderr(`Usage: ${CLI_NAME} workflow get <runId>\n`);
4050
- return 2;
4051
- }
4052
- try {
4053
- const snapshot = localWorkflowSnapshot(dataPath, runId);
4054
- if (runId && snapshot.runs.length === 0) {
4055
- print(runtime.io, runtime.json, { ok: false, command: "workflow get", error: "workflow_not_found" }, "workflow not found");
4056
- return 1;
4057
- }
4058
- const value = action === "get"
4059
- ? { ok: true, command: "workflow get", run: snapshot.runs[0], journal: snapshot.journal }
4060
- : { ok: true, command: "workflow list", runs: snapshot.runs };
4061
- print(runtime.io, runtime.json, value, action === "get" ? `workflow ${runId}` : `${snapshot.runs.length} workflow(s)`);
4062
- return 0;
4063
- } catch {
4064
- print(runtime.io, runtime.json, { ok: false, command: `workflow ${action}`, error: "state_unavailable" }, "local workflow state is unavailable");
4065
- return 1;
4066
- }
4067
- }
117
+ import {
118
+ cmdStatus,
119
+ cmdDash,
120
+ cmdHub,
121
+ cmdHubBind,
122
+ cmdHubUnbind,
123
+ cmdWorker,
124
+ cmdStop,
125
+ cmdSessionStatus,
126
+ cmdAuthToken,
127
+ cmdSessionBrief,
128
+ cmdSessionStart,
129
+ } from "./cli/hub.ts";
4068
130
 
4069
- async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string, recoveryAction?: string): Promise<number> {
4070
- if (!runId || !signalKey || !status || !summary) {
4071
- runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`);
4072
- return 2;
4073
- }
4074
- if (status !== "passed" && status !== "warning" && status !== "failed") {
4075
- runtime.io.stderr("status must be passed, warning, or failed\n");
4076
- return 2;
4077
- }
4078
- let evidence: WorkflowEvidenceInput;
4079
- try {
4080
- evidence = parseEvidencePairs(evidenceArgs);
4081
- } catch (error) {
4082
- runtime.io.stderr(`${error instanceof Error ? error.message : "invalid evidence"}\n`);
4083
- return 2;
4084
- }
4085
- const worker = gateOf(runtime, "signal");
4086
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
4087
- if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
4088
- if (runtime.dryRun) {
4089
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signal to vNext run");
4090
- return 0;
4091
- }
4092
- const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
4093
- try {
4094
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
4095
- const posted = await vnextRuntimeRequest(
4096
- supervisor,
4097
- "POST",
4098
- `/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
4099
- { signalKey, status, summary, evidence, deliveryId, ...(recoveryAction ? { action: recoveryAction } : {}) },
4100
- );
4101
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to vNext run");
4102
- return 0;
4103
- } catch (error) {
4104
- const msg = error instanceof Error ? error.message : "signal_failed";
4105
- printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", detail: msg, deliveryId }, "signal to vNext run failed");
4106
- return 1;
4107
- }
4108
- }
131
+ import {
132
+ cmdExplain,
133
+ cmdSshInfo,
134
+ cmdSshRun,
135
+ cmdSshFile,
136
+ cmdSshClose,
137
+ cmdUpdate,
138
+ cmdValidate,
139
+ cmdArtifactsExist,
140
+ cmdImprove,
141
+ cmdConfigGet,
142
+ cmdConfigSet,
143
+ cmdConfigList,
144
+ cmdCompletion,
145
+ cmdCompletionInstall,
146
+ cmdRoutingReport,
147
+ cmdRoutingBenchmark,
148
+ maybeOfferCompletionInstall,
149
+ maybeOfferGuideSetup,
150
+ } from "./cli/system.ts";
151
+
152
+ // Re-export public API types and functions
153
+ export type { CliIo, CliSpawnResult };
154
+ export { hubContextPost };
4109
155
 
4110
- const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
4111
- if (!definitionId) {
4112
- runtime.io.stderr("signal requires KXM_WORKFLOW_ID\n");
4113
- return 2;
4114
- }
4115
- let signalSecret: string | undefined;
4116
- try {
4117
- signalSecret = workflowCredential(runtime, definitionId, "signal");
4118
- } catch (error) {
4119
- return reportWorkflowConfigError(runtime, error);
4120
- }
4121
- if (!signalSecret) {
4122
- runtime.io.stderr("signal requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
4123
- return 2;
4124
- }
4125
- if (runtime.dryRun) {
4126
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signed signal");
4127
- return 0;
4128
- }
4129
- const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
4130
- try {
4131
- const posted = await postWorkflowSignal({
4132
- serverUrl: runtime.serverUrl,
4133
- definitionId,
4134
- signalSecret,
4135
- runId,
4136
- signalKey,
4137
- status,
4138
- summary,
4139
- evidence,
4140
- deliveryId,
4141
- fetchImpl: runtime.fetchImpl,
4142
- });
4143
- printWorker(runtime, worker, { ok: true, command: "signal", duplicate: posted.duplicate, deliveryId }, "posted signed signal");
4144
- return 0;
4145
- } catch {
4146
- printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", deliveryId }, "signed signal failed");
4147
- return 1;
4148
- }
4149
- }
156
+ const CLI_NAME = "kxm";
157
+ const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
4150
158
 
4151
- async function cmdGithubWatch(runtime: Runtime, options: {
4152
- runId?: string;
4153
- stageId?: string;
4154
- signalKey?: string;
4155
- repo?: string;
4156
- pr?: string;
4157
- required?: string;
4158
- timeoutMs?: string;
4159
- intervalMs?: string;
4160
- deliveryId?: string;
4161
- }): Promise<number> {
4162
- const token = runtime.env.GITHUB_TOKEN?.trim() || runtime.env.GH_TOKEN?.trim();
4163
- const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
4164
- const runId = String(options.runId || "");
4165
- const stageId = String(options.stageId || "");
4166
- const signalKey = String(options.signalKey || "");
4167
- const repo = String(options.repo || "");
4168
- const pr = Number(options.pr);
4169
- if (!definitionId || !runId || !stageId || !signalKey || !repo || !Number.isInteger(pr)) {
4170
- runtime.io.stderr("github watch requires KXM_WORKFLOW_ID, --run-id, --stage-id, --signal-key, --repo, --pr\n");
4171
- return 2;
4172
- }
4173
- let signalSecret: string | undefined;
4174
- try {
4175
- signalSecret = workflowCredential(runtime, definitionId, "signal");
4176
- } catch (error) {
4177
- return reportWorkflowConfigError(runtime, error);
4178
- }
4179
- if (!signalSecret) {
4180
- runtime.io.stderr("github watch requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
4181
- return 2;
4182
- }
4183
- const result = await watchGithubChecks({
4184
- serverUrl: runtime.serverUrl,
4185
- definitionId,
4186
- signalSecret,
4187
- runId,
4188
- stageId,
4189
- signalKey,
4190
- repo,
4191
- pr,
4192
- required: options.required
4193
- ? [...new Set(String(options.required).split(",").map((name) => name.trim()).filter(Boolean))]
4194
- : [],
4195
- timeoutMs: Number(options.timeoutMs || 1_800_000),
4196
- intervalMs: Number(options.intervalMs || 15_000),
4197
- ...(options.deliveryId ? { deliveryId: options.deliveryId } : {}),
4198
- ...(token ? { token } : {}),
4199
- dryRun: runtime.dryRun,
4200
- fetchImpl: runtime.fetchImpl,
4201
- ...(runtime.io.now ? { now: runtime.io.now } : {}),
4202
- ...(runtime.io.sleep ? { sleep: runtime.io.sleep } : {}),
4203
- });
4204
- const worker = gateOf(runtime, "github-watch");
4205
- const payload = {
4206
- ok: result.exitCode === 0,
4207
- command: "github watch",
4208
- posted: result.posted,
4209
- status: result.status,
4210
- summary: result.summary,
4211
- evidence: result.evidence,
4212
- deliveryId: result.deliveryId,
4213
- skipped: result.skipped,
4214
- };
4215
- const outcome: WorkerOutcome = result.exitCode === 0
4216
- ? (result.status === "warning" ? "warning" : "passed")
4217
- : "failed";
4218
- if (JSON.stringify(payload).includes(token ?? "___never___") || Object.keys(runtime.env).some((key) => maskEnvName(key) && JSON.stringify(payload).includes(String(runtime.env[key])))) {
4219
- printWorker(runtime, worker, { ok: false, command: "github watch", error: "redaction_failure" }, "refusing to print a payload that contains a secret");
4220
- return 1;
4221
- }
4222
- printWorker(runtime, worker, payload, result.summary, outcome);
4223
- return result.exitCode;
4224
- }
159
+ const USAGE_ERROR_CODES = new Set([
160
+ "commander.help",
161
+ "commander.helpDisplayed",
162
+ "commander.unknownCommand",
163
+ "commander.unknownOption",
164
+ "commander.missingArgument",
165
+ "commander.missingMandatoryOptionValue",
166
+ "commander.invalidArgument",
167
+ "commander.excessArguments",
168
+ "commander.variadicArgNotLast",
169
+ "commander.invalidOptionArgument",
170
+ "commander.optionMissingArgument",
171
+ ]);
4225
172
 
4226
- async function cmdRetrospectiveExport(runtime: Runtime, runId: string, options: { input?: string; outDir?: string }): Promise<number> {
4227
- if (!runId) {
4228
- runtime.io.stderr(`Usage: ${CLI_NAME} workflow export <runId>\n`);
4229
- return 2;
4230
- }
4231
- const snapshotFlag = String(options.input || "");
4232
- const snapshotPath = snapshotFlag ? resolve(runtime.cwd, snapshotFlag) : "";
4233
- let snapshot: { run: WorkflowRun; journal: WorkflowJournalEntry[] };
4234
- try {
4235
- if (snapshotPath) {
4236
- if (!existsSync(snapshotPath)) throw new Error("snapshot_missing");
4237
- snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as typeof snapshot;
4238
- } else {
4239
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
4240
- const local = localWorkflowSnapshot(dataPath, runId);
4241
- if (!local.runs[0]) throw new Error("workflow_not_found");
4242
- snapshot = { run: local.runs[0], journal: local.journal };
4243
- }
4244
- if (snapshot.run.id !== runId) throw new Error("run_id_mismatch");
4245
- } catch (error) {
4246
- const reason = error instanceof Error ? error.message : "snapshot_invalid";
4247
- print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: reason }, "retrospective source is invalid or unavailable");
4248
- return 1;
4249
- }
4250
- const doc = buildRetrospective(snapshot.run, snapshot.journal);
4251
- const outDir = resolve(runtime.cwd, String(options.outDir || join(runtime.dirs.assets, "retrospectives")));
4252
- const assetsRoot = resolve(runtime.dirs.assets);
4253
- const assetsPrefix = `${assetsRoot}${process.platform === "win32" ? "\\" : "/"}`;
4254
- if (outDir !== assetsRoot && !outDir.startsWith(assetsPrefix)) {
4255
- print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: "output_outside_workspace_assets" }, "retrospectives must stay under the workspace assets directory");
4256
- return 2;
4257
- }
4258
- if (runtime.dryRun) {
4259
- print(runtime.io, runtime.json, { ok: true, command: "retrospective export", dryRun: true, runId: doc.runId }, `would export ${doc.runId}`);
4260
- return 0;
4261
- }
4262
- const written = writeRetrospective(outDir, doc);
4263
- print(runtime.io, runtime.json, { ok: true, command: "retrospective export", ...written, reviewDecision: doc.reviewDecision }, `exported ${written.jsonPath}`);
4264
- return 0;
173
+ function addGlobalOptions(command: Command): Command {
174
+ return command
175
+ .option("--json", "Print machine-readable JSON")
176
+ .option("--dry-run", "Plan without making changes")
177
+ .option("--workspace <dir>", "Workspace directory");
4265
178
  }
4266
179
 
4267
180
  async function ensureCliClient(runtime: Runtime): Promise<HubClient> {
@@ -4423,10 +336,14 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
4423
336
  .option("--project-id <id>", "Stable project ID for controlled provisioning")
4424
337
  .option("--repository <id=absolute-path>", "Bind a member repository outside Git configuration", (value, previous: string[]) => [...previous, value], [])
4425
338
  .action(async function initAction(this: Command, options: { name?: string; projectId?: string; repository?: string[] }) {
4426
- result.code = await cmdVnextInit(runtimeFrom(ctx, this), options);
339
+ result.code = await cmdVnextInit(runtimeFrom(ctx, this), options, {
340
+ maybeOfferCompletionInstall,
341
+ maybeOfferGuideSetup,
342
+ });
4427
343
  });
4428
344
 
4429
- const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration")); migrate.helpCommand("help", "Show migrate help");
345
+ const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration"));
346
+ migrate.helpCommand("help", "Show migrate help");
4430
347
  addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-vNext migration plan without writes"))
4431
348
  .action(async function migratePlanAction(this: Command) {
4432
349
  result.code = await cmdVnextMigratePlan(runtimeFrom(ctx, this));
@@ -5080,6 +997,41 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
5080
997
  result.code = await cmdRoutingBenchmark(runtimeFrom(ctx, this), options);
5081
998
  });
5082
999
 
1000
+ addGlobalOptions(
1001
+ program
1002
+ .command("explain")
1003
+ .description("Pre-flight context footprint and token cost inspection for workflow modes")
1004
+ .option("--mode <name>", "Major mode (coder, planner, auditor, browser)", "coder")
1005
+ .option("--domains <list>", "Comma-separated domain modules (git, k8s, database, browser)")
1006
+ .option("--model <id>", "Target model identifier (e.g. grok/grok-4.6, claude/fable)")
1007
+ ).action(async function explainAction(this: Command, options: { mode?: string; domains?: string; model?: string }) {
1008
+ result.code = await cmdExplain(runtimeFrom(ctx, this), options);
1009
+ });
1010
+
1011
+ const sshCmd = addGlobalOptions(program.command("ssh").description("Multiplexed remote SSH execution and worker orchestration"));
1012
+ sshCmd.helpCommand("help", "Show ssh help");
1013
+ addGlobalOptions(sshCmd.command("info [host]").description("Discover SSH host aliases and parameters safely without opening sockets"))
1014
+ .action(async function sshInfoAction(this: Command, host?: string) {
1015
+ result.code = await cmdSshInfo(runtimeFrom(ctx, this), host);
1016
+ });
1017
+ addGlobalOptions(sshCmd.command("run <host> <command...>").description("Execute a command on a remote SSH host via multiplexed ControlMaster socket"))
1018
+ .option("--sudo", "Execute remote command with sudo privileges")
1019
+ .action(async function sshRunAction(this: Command, host: string, commandParts: string[], options: { sudo?: boolean }) {
1020
+ result.code = await cmdSshRun(runtimeFrom(ctx, this), host, commandParts, options);
1021
+ });
1022
+ addGlobalOptions(sshCmd.command("file <host> <path>").description("Read or write remote files over SSH"))
1023
+ .option("--content <text>", "Content to write to remote file")
1024
+ .option("--read", "Read remote file content")
1025
+ .option("--append", "Append content to remote file")
1026
+ .option("--sudo", "Use sudo privileges on remote file")
1027
+ .action(async function sshFileAction(this: Command, host: string, filePath: string, options: { content?: string; read?: boolean; append?: boolean; sudo?: boolean }) {
1028
+ result.code = await cmdSshFile(runtimeFrom(ctx, this), host, filePath, options);
1029
+ });
1030
+ addGlobalOptions(sshCmd.command("close <host>").description("Close active ControlMaster socket for an SSH host"))
1031
+ .action(async function sshCloseAction(this: Command, host: string) {
1032
+ result.code = await cmdSshClose(runtimeFrom(ctx, this), host);
1033
+ });
1034
+
5083
1035
  const hub = addGlobalOptions(program.command("hub").description("Start, inspect, and stop the local KXM hub"));
5084
1036
  hub.helpCommand("help", "Show hub help");
5085
1037
  addGlobalOptions(hub.command("view").description("Check hub /health and /ready")).action(bind(cmdStatus));
@@ -5210,9 +1162,6 @@ function mapCommanderError(error: CommanderError): number {
5210
1162
 
5211
1163
  const MESH_REMOVED_TEXT = "kxm mesh was removed. Use kxm init, kxm hub start|view|stop, and node scripts/smoke-multi-pi.mjs (KXM_SMOKE=1).";
5212
1164
 
5213
- /** Detect `mesh` as the first command token after recognized nonterminal global
5214
- * options so `kxm --json mesh` fails closed. `--version`, `--help`, and unknown
5215
- * options are left for Commander. */
5216
1165
  function removedMeshInvocation(argv: string[]): { invoked: boolean; json: boolean } {
5217
1166
  let json = false;
5218
1167
  let invoked = false;