@kontextmind/kxm 0.7.0 → 0.7.10

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