@kontextmind/kxm 0.6.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 (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -1,2856 +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";
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";
7
16
  import { fileURLToPath } from "node:url";
8
17
  import { Command, CommanderError } from "commander";
9
- import {
10
- createMemoryNote,
11
- formatMemoryBriefText,
12
- generateMemoryBrief,
13
- syncHarnessMemory,
14
- type MemoryScope,
15
- } from "./memory.ts";
16
- import { createBackup, restoreBackup } from "./database.ts";
17
- import { verifyArtifactExists } from "./artifacts-exist.ts";
18
- import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions } from "./workflow.ts";
19
- import { postWorkflowSignal, watchGithubChecks } from "./github-watch.ts";
20
- import { buildRetrospective, writeRetrospective } from "./retrospective.ts";
21
- import { redactSecrets } from "./redact.ts";
22
- import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "./skills.ts";
23
- import { writeCompiledWiki } from "./wiki.ts";
24
- import { agentWorker, gateWorker, workerResult, type Worker, type WorkerOutcome } from "./envelope.ts";
25
- import { appendTelemetry, inferImprovementTarget, makeTelemetryEvent, readTelemetry, readRoutingRecords, telemetryPath } from "./telemetry.ts";
26
- import { behavioralConfigHash, compareRoutingRecords, groupByBehavior, generateRoutingReport, formatRoutingReport } from "./routing.ts";
27
- import { loadPriceCatalog, type PriceCatalog } from "./prices.ts";
28
- import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "./session.ts";
29
- import { buildImprovementReport, formatImprovementReport, writeImprovementReport } from "./improve.ts";
30
- import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "./tui.ts";
31
- import { formatSessionBriefText, loadSessionBrief, loadSessionBriefAsync, type SessionHubStatus } from "./session-work.ts";
32
- import {
33
- HUB_BINDING_SCHEMA,
34
- HubBindingError,
35
- hubBindingFile,
36
- probeHubHealth,
37
- readHubBinding,
38
- removeHubBinding,
39
- validateHubUrl,
40
- writeHubBinding,
41
- type HubHealth,
42
- } from "./hub-binding.ts";
43
- import {
44
- fetchLatestKxmVersion,
45
- kxmReleaseAssetName,
46
- noticeFromVersions,
47
- planKxmPackageUpdate,
48
- readInstalledKxmVersion,
49
- readUpdateCache,
50
- verifyReleaseAssetDigest,
51
- writeUpdateCache,
52
- KxmUpdateConfigError,
53
- type KxmPackageUpdateStep,
54
- type KxmUpdateConfig,
55
- type KxmUpdateNotice,
56
- } from "./kxm-update.ts";
57
- import { loadKxmUpdateConfig } from "./kxm-update-config.ts";
58
- import {
59
- classifyInstallRoot,
60
- resolveInstallKind,
61
- type InstallKindReport,
62
- type InstallProbe,
63
- } from "./kxm-install-kind.ts";
64
- import { VnextConfigError, discoverVnextProjectRoot, type VnextInitializationPlan } from "./vnext-config.ts";
65
- import { vnextUserStateRoot } from "./vnext-bindings.ts";
66
- import { initializeVnextProject } from "./vnext-init.ts";
67
- import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "./vnext-migrate.ts";
68
- import { diffVnextProjectAgainstRevision, formatVnextPermissionDiff } from "./vnext-permission.ts";
69
- import { readVnextLocalBindings } from "./vnext-bindings.ts";
70
- import { loadVnextProject } from "./vnext-config.ts";
71
- import {
72
- ensureVnextSupervisor,
73
- vnextRuntimeRequest,
74
- vnextSupervisorStatus,
75
- } from "./vnext-runtime-supervisor.ts";
76
- import { vnextRuntimePaths } from "./vnext-runtime-store.ts";
77
- import {
78
- formatHarnessInventory,
79
- formatHarnessUpdate,
80
- planHarnessUpdate,
81
- probeHarnesses,
82
- runHarnessUpdate,
83
- type HarnessUpdateScope,
84
- } from "./vnext-harness.ts";
85
- import type { WorkflowEvidenceInput, WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
86
- import { HubClient } from "./client.ts";
87
- import { AGENT_COMMANDS_MAP, enforceToolPolicy, mintSessionToken } from "./commands.ts";
88
- import {
89
- loadKxmConfig,
90
- setKxmConfigValue,
91
- getKxmConfigValue,
92
- formatKxmConfig,
93
- } from "./config.ts";
94
- import { generateShellCompletion, type SupportedShell } from "./autocomplete.ts";
95
- import { suggestWorkflowAndRoles } from "./suggest.ts";
96
- import {
97
- createGoal,
98
- createTask,
99
- listGoals,
100
- listTasks,
101
- getTask,
102
- updateTaskStatus,
103
- syncTaskWithTracker,
104
- type TaskStatus,
105
- type TrackerType,
106
- } from "./task-manager.ts";
107
- import { parse as parseYaml } from "yaml";
108
- import { compileVnextWorkflow } from "./vnext-engine-compile.ts";
109
- import { generateStudioLayout } from "./studio-layout.ts";
110
-
111
- export interface CliSpawnResult {
112
- status: number | null;
113
- stdout: string;
114
- stderr: string;
115
- error?: Error;
116
- }
117
-
118
- export interface CliIo {
119
- stdout: (text: string) => void;
120
- stderr: (text: string) => void;
121
- fetchImpl?: typeof fetch;
122
- now?: () => number;
123
- sleep?: (ms: number) => Promise<void>;
124
- spawnHub?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
125
- spawnWorker?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
126
- installProbe?: Partial<InstallProbe>;
127
- spawnSync?: (command: string, args: readonly string[]) => CliSpawnResult;
128
- }
129
-
130
- const CLI_NAME = "kxm";
131
- const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
132
- const USAGE_ERROR_CODES = new Set([
133
- "commander.help",
134
- "commander.helpDisplayed",
135
- "commander.unknownCommand",
136
- "commander.unknownOption",
137
- "commander.missingArgument",
138
- "commander.missingMandatoryOptionValue",
139
- "commander.invalidArgument",
140
- "commander.excessArguments",
141
- "commander.variadicArgNotLast",
142
- "commander.invalidOptionArgument",
143
- "commander.optionMissingArgument",
144
- ]);
145
-
146
- interface CliContext {
147
- env: NodeJS.ProcessEnv;
148
- io: CliIo;
149
- cwd: string;
150
- }
151
-
152
- interface GlobalOpts {
153
- json?: boolean;
154
- dryRun?: boolean;
155
- workspace?: string;
156
- }
157
-
158
- interface Runtime extends CliContext, Required<Pick<GlobalOpts, "json" | "dryRun">> {
159
- workspaceFlag?: string;
160
- dirs: ReturnType<typeof workspaceDirs>;
161
- serverUrl: string;
162
- boundHubUrl?: string;
163
- fetchImpl: typeof fetch;
164
- }
165
-
166
- function spawnScript(scriptName: string, extraEnv: NodeJS.ProcessEnv = {}): Promise<number> {
167
- return new Promise((resolveExit) => {
168
- const child = spawn(process.execPath, [join(repoRoot, "scripts", scriptName)], {
169
- stdio: "inherit",
170
- env: { ...process.env, ...extraEnv },
171
- });
172
- child.once("error", () => resolveExit(1));
173
- child.once("exit", (code) => resolveExit(code ?? 1));
174
- });
175
- }
176
-
177
- function parseEvidencePairs(values: string[]): WorkflowEvidenceInput {
178
- const evidence = new Map<string, string>();
179
- for (const value of values) {
180
- const separator = value.indexOf("=");
181
- if (separator <= 0 || separator === value.length - 1) {
182
- throw new Error("evidence must use <required-key>=<evidence> syntax");
183
- }
184
- const requirement = canonicalWorkflowEvidenceKey(value.slice(0, separator));
185
- const proof = value.slice(separator + 1).trim();
186
- if (!requirement || !proof) throw new Error("evidence must use <required-key>=<evidence> syntax");
187
- if (evidence.has(requirement)) throw new Error(`duplicate normalized evidence key: ${requirement}`);
188
- evidence.set(requirement, proof);
189
- }
190
- return Object.fromEntries(evidence);
191
- }
192
-
193
- const CLI_RESULT_SCHEMA = "kxm.cli-result.v1";
194
-
195
- /** Every CLI payload carries a schema tag; payloads that already declare one
196
- * (worker envelopes) keep theirs. `ok:false` goes to stderr in both modes so
197
- * agents can split results from errors without parsing. */
198
- function print(io: CliIo, jsonMode: boolean, payload: object, text: string): void {
199
- const tagged = { schema: CLI_RESULT_SCHEMA, ...payload } as Record<string, unknown>;
200
- const safePayload = JSON.stringify(redactCliValue(tagged));
201
- const line = jsonMode ? `${safePayload}\n` : `${redactSecrets(text)}\n`;
202
- if (tagged.ok === false) io.stderr(line);
203
- else io.stdout(line);
204
- }
205
-
206
- function printWorker(
207
- runtime: Runtime,
208
- worker: Worker,
209
- payload: Record<string, unknown> & { command: string; ok: boolean },
210
- text: string,
211
- outcome?: WorkerOutcome,
212
- ): void {
213
- const sessionId = runtime.env.KXM_SESSION_ID?.trim();
214
- const envelope = workerResult(worker, {
215
- ...payload,
216
- summary: text,
217
- ...(outcome ? { outcome } : {}),
218
- ...(sessionId ? { sessionId } : {}),
219
- });
220
- if (!runtime.dryRun) {
221
- try {
222
- const safeEnvelope = redactCliValue(envelope) as typeof envelope;
223
- appendTelemetry(telemetryPath(runtime.dirs.logs), makeTelemetryEvent({
224
- envelope: safeEnvelope,
225
- ...(sessionId ? { sessionId } : {}),
226
- host: hostMode(runtime),
227
- target: inferImprovementTarget({
228
- ...(worker.project ? { project: worker.project } : {}),
229
- env: runtime.env,
230
- }),
231
- }));
232
- } catch {
233
- // Telemetry must never fail the operator command.
234
- }
235
- }
236
- print(runtime.io, runtime.json, envelope, text);
237
- }
238
-
239
- function hostMode(runtime: Runtime): "local" | "hub" {
240
- try {
241
- const hostname = new URL(runtime.serverUrl).hostname;
242
- if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
243
- } catch {
244
- // Default local when the hub URL is not a URL.
245
- }
246
- return "hub";
247
- }
248
-
249
- function gateOf(runtime: Runtime, name: string): ReturnType<typeof gateWorker> {
250
- const project = runtime.env.KXM_PROJECT?.trim();
251
- return gateWorker({ name, ...(project ? { project } : {}) });
252
- }
253
-
254
- function redactCliValue(value: unknown, field = ""): unknown {
255
- if (typeof value === "string") {
256
- // These fields are public audit digests, not credentials. Preserve
257
- // them only by exact field name and shape; every other 64-hex value keeps
258
- // the conservative generic redaction behavior.
259
- if (
260
- (field === "requestSha256"
261
- || field === "replySha256"
262
- || field === "behavioralSha256"
263
- || field === "workflowDefinitionSha256"
264
- || field === "verifierConfigSha256"
265
- || field === "rolePromptSha256"
266
- || field === "contentSha256"
267
- || field === "configRevision"
268
- || field === "baseSha256"
269
- || field === "localSha256"
270
- || field === "targetSha256"
271
- || field === "sourceTemplateRevision"
272
- || field === "targetTemplateRevision"
273
- || field === "sourceDigest"
274
- || field === "decisionDigest"
275
- || field === "receiptSha256"
276
- || field === "sha256"
277
- || field === "valueSha256"
278
- || field === "baseRevision"
279
- || field === "candidateRevision"
280
- || field === "baseValueSha256"
281
- || field === "candidateValueSha256")
282
- && /^(?:sha256:)?[a-f0-9]{64}$/.test(value)
283
- ) return value;
284
- return redactSecrets(value);
285
- }
286
- if (Array.isArray(value)) return value.map((candidate) => redactCliValue(candidate));
287
- if (value && typeof value === "object") {
288
- return Object.fromEntries(
289
- Object.entries(value as Record<string, unknown>)
290
- .map(([key, candidate]) => [key, redactCliValue(candidate, key)]),
291
- );
292
- }
293
- return value;
294
- }
295
-
296
- function workspaceDirs(cwd: string, workspaceFlag: string | undefined, env: NodeJS.ProcessEnv) {
297
- const workdir = resolve(env.KXM_WORKDIR?.trim() || cwd);
298
- const workspace = resolve(workdir, workspaceFlag || env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
299
- const derive = workspaceFlag !== undefined;
300
- return {
301
- workdir,
302
- workspace,
303
- config: derive ? join(workspace, "config") : resolve(workdir, env.KXM_CONFIG_DIR?.trim() || join(workspace, "config")),
304
- logs: derive ? join(workspace, "logs") : resolve(workdir, env.KXM_LOGS_DIR?.trim() || join(workspace, "logs")),
305
- assets: derive ? join(workspace, "assets") : resolve(workdir, env.KXM_ASSETS_DIR?.trim() || join(workspace, "assets")),
306
- state: derive ? join(workspace, "state") : resolve(workdir, env.KXM_STATE_DIR?.trim() || join(workspace, "state")),
307
- };
308
- }
309
-
310
- function maskEnvName(name: string): boolean {
311
- return /TOKEN|SECRET|KEY|PASSWORD/i.test(name);
312
- }
313
-
314
- function redactConfiguredValues(text: string, env: NodeJS.ProcessEnv): string {
315
- let safe = text;
316
- for (const [name, value] of Object.entries(env)) {
317
- if (!maskEnvName(name) || !value || value.length < 4) continue;
318
- safe = safe.replaceAll(value, "[redacted]");
319
- }
320
- const trailingNewline = safe.endsWith("\n") ? "\n" : "";
321
- try {
322
- const parsed = JSON.parse(safe) as unknown;
323
- return `${JSON.stringify(redactCliValue(parsed))}${trailingNewline}`;
324
- } catch {
325
- // Human-readable output and diagnostics keep conservative generic
326
- // redaction, including opaque 64-hex values.
327
- }
328
- return redactSecrets(safe);
329
- }
330
-
331
- function processExists(pid: number): boolean {
332
- try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
333
- }
334
-
335
- /** Authenticated hub POST for context operations. The CLI operates as the
336
- * control plane: the administrative token scopes one project per request. */
337
- export async function hubContextPost(input: {
338
- serverUrl: string;
339
- path: string;
340
- body: Record<string, unknown>;
341
- authToken?: string;
342
- fetchImpl: typeof fetch;
343
- }): Promise<{ ok: boolean; status: number; body: unknown }> {
344
- const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}${input.path}`, {
345
- method: "POST",
346
- headers: {
347
- "content-type": "application/json",
348
- ...(input.authToken ? { authorization: `Bearer ${input.authToken}` } : {}),
349
- },
350
- body: JSON.stringify(input.body),
351
- });
352
- const text = redactSecrets((await response.text()).slice(0, 64_000));
353
- let body: unknown = text;
354
- try {
355
- body = JSON.parse(text);
356
- } catch {
357
- // Keep text for diagnostics without treating it as a secret.
358
- }
359
- return { ok: response.ok, status: response.status, body };
360
- }
361
-
362
- async function hubGet(url: string, fetchImpl: typeof fetch): Promise<{ ok: boolean; status: number; body: unknown }> {
363
- try {
364
- const response = await fetchImpl(url);
365
- const text = redactSecrets((await response.text()).slice(0, 8_000));
366
- let body: unknown = text;
367
- try {
368
- body = JSON.parse(text);
369
- } catch {
370
- // Keep text for diagnostics without treating it as a secret.
371
- }
372
- return { ok: response.ok, status: response.status, body };
373
- } catch {
374
- return { ok: false, status: 0, body: { error: "hub_unreachable" } };
375
- }
376
- }
377
-
378
- function localWorkflowSnapshot(dataPath: string, runId?: string): { runs: WorkflowRun[]; journal: WorkflowJournalEntry[] } {
379
- if (!existsSync(dataPath)) throw new Error("state_database_not_found");
380
- const database = new DatabaseSync(dataPath, { readOnly: true });
381
- try {
382
- const rows = runId
383
- ? database.prepare("SELECT record FROM workflow_runs WHERE id = ?").all(runId) as Array<{ record: string }>
384
- : database.prepare("SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 200").all() as Array<{ record: string }>;
385
- const runs = rows.map((row) => JSON.parse(row.record) as WorkflowRun);
386
- const journal = runId
387
- ? (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)
388
- : [];
389
- return { runs, journal };
390
- } finally { database.close(); }
391
- }
392
-
393
- 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 }> {
394
- const payload = input.event && input.payload.event === undefined ? { ...input.payload, event: input.event } : input.payload;
395
- const body = JSON.stringify(payload);
396
- const signature = `sha256=${createHmac("sha256", input.secret).update(body).digest("hex")}`;
397
- const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}/v1/webhooks/${encodeURIComponent(input.definitionId)}`, {
398
- method: "POST",
399
- headers: { "content-type": "application/json", "x-hub-signature-256": signature, "x-kxm-delivery-id": input.deliveryId, ...(input.event ? { "x-github-event": input.event } : {}) },
400
- body,
401
- });
402
- const responseText = (await response.text()).slice(0, 8_000);
403
- let parsed: { run?: { id?: string }; duplicate?: boolean } = {};
404
- try { parsed = JSON.parse(responseText) as typeof parsed; } catch { /* bounded adapter error */ }
405
- if (!response.ok) throw new Error(`workflow_start_http_${response.status}`);
406
- return { status: response.status, ...(parsed.run?.id ? { runId: parsed.run.id } : {}), duplicate: parsed.duplicate === true };
407
- }
408
-
409
- async function postWorkflowDegradation(input: {
410
- serverUrl: string;
411
- authToken: string;
412
- runId: string;
413
- stageId: string;
414
- requirementKey: string;
415
- reason: string;
416
- fetchImpl: typeof fetch;
417
- }): Promise<{ status: number; duplicate: boolean; approvalId?: string }> {
418
- const response = await input.fetchImpl(
419
- `${input.serverUrl.replace(/\/$/, "")}/v1/workflows/${encodeURIComponent(input.runId)}/degradations`,
420
- {
421
- method: "POST",
422
- headers: {
423
- authorization: `Bearer ${input.authToken}`,
424
- "content-type": "application/json",
425
- },
426
- body: JSON.stringify({
427
- stageId: input.stageId,
428
- requirementKey: input.requirementKey,
429
- reason: input.reason,
430
- }),
431
- },
432
- );
433
- const text = (await response.text()).slice(0, 8_000);
434
- let parsed: { duplicate?: boolean; approval?: { id?: string } } = {};
435
- try { parsed = JSON.parse(text) as typeof parsed; } catch { /* bounded adapter error */ }
436
- if (!response.ok) throw new Error(`workflow_degradation_http_${response.status}`);
437
- return {
438
- status: response.status,
439
- duplicate: parsed.duplicate === true,
440
- ...(parsed.approval?.id ? { approvalId: parsed.approval.id } : {}),
441
- };
442
- }
443
-
444
- function addGlobalOptions(command: Command): Command {
445
- return command
446
- .option("--json", "Print machine-readable JSON")
447
- .option("--dry-run", "Plan without making changes")
448
- .option("--workspace <dir>", "Workspace directory");
449
- }
450
-
451
- function runtimeFrom(ctx: CliContext, command: Command): Runtime {
452
- const opts = command.optsWithGlobals() as GlobalOpts;
453
- const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
454
- let boundHubUrl: string | undefined;
455
- try {
456
- boundHubUrl = readHubBinding(ctx.env)?.url;
457
- } catch (error) {
458
- if (error instanceof HubBindingError) {
459
- ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
460
- } else {
461
- throw error;
462
- }
463
- }
464
- return {
465
- ...ctx,
466
- json: Boolean(opts.json),
467
- dryRun: Boolean(opts.dryRun),
468
- ...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
469
- dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
470
- serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
471
- ...(boundHubUrl ? { boundHubUrl } : {}),
472
- fetchImpl: ctx.io.fetchImpl ?? fetch,
473
- };
474
- }
475
-
476
- function workspaceEnv(runtime: Runtime): NodeJS.ProcessEnv {
477
- return {
478
- KXM_WORKDIR: runtime.dirs.workdir,
479
- KXM_WORKSPACE_DIR: runtime.dirs.workspace,
480
- KXM_CONFIG_DIR: runtime.dirs.config,
481
- KXM_LOGS_DIR: runtime.dirs.logs,
482
- KXM_ASSETS_DIR: runtime.dirs.assets,
483
- KXM_STATE_DIR: runtime.dirs.state,
484
- };
485
- }
486
-
487
- function activeWorkflowDefinition(runtime: Runtime, definitionId: string) {
488
- const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
489
- const file = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
490
- if (inline && file) {
491
- throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE");
492
- }
493
- if (!inline && !file) return undefined;
494
- let raw: string;
495
- if (file) {
496
- try {
497
- raw = readFileSync(resolve(runtime.cwd, file), "utf8");
498
- } catch {
499
- throw new Error("workflow definition file is unavailable");
500
- }
501
- } else {
502
- raw = inline!;
503
- }
504
- const definition = parseWorkflowDefinitions(raw, runtime.env).find((candidate) => candidate.id === definitionId);
505
- if (!definition) throw new Error(`workflow definition not found: ${definitionId}`);
506
- return definition;
507
- }
508
-
509
- function workflowCredential(runtime: Runtime, definitionId: string, kind: "start" | "signal"): string | undefined {
510
- const definition = activeWorkflowDefinition(runtime, definitionId);
511
- if (definition) return kind === "start" ? definition.secret : definition.signalSecret ?? definition.secret;
512
- return kind === "start"
513
- ? runtime.env.KXM_WORKFLOW_SECRET?.trim()
514
- : runtime.env.KXM_WORKFLOW_SIGNAL_SECRET?.trim();
515
- }
516
-
517
- function reportWorkflowConfigError(runtime: Runtime, error: unknown): number {
518
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid workflow configuration";
519
- runtime.io.stderr(`${message}\n`);
520
- return 2;
521
- }
522
-
523
- function initPlanPayload(plan: VnextInitializationPlan): Record<string, unknown> {
524
- return {
525
- mode: plan.mode,
526
- inspectedFrom: plan.inspectedFrom,
527
- ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}),
528
- ...(plan.legacyRoot ? { legacyRoot: plan.legacyRoot } : {}),
529
- changesRequired: plan.changesRequired,
530
- legacyInputs: plan.legacyInputs,
531
- issues: plan.issues,
532
- ...(plan.configRevision ? { configRevision: plan.configRevision } : {}),
533
- };
534
- }
535
-
536
- function explicitRepositoryBindings(values: readonly string[]): Readonly<Record<string, string>> {
537
- const result: Record<string, string> = Object.create(null) as Record<string, string>;
538
- for (const value of values) {
539
- const separator = value.indexOf("=");
540
- const repositoryId = separator < 0 ? "" : value.slice(0, separator).trim();
541
- const path = separator < 0 ? "" : value.slice(separator + 1).trim();
542
- if (!repositoryId || !path) {
543
- throw new VnextConfigError([{
544
- phase: "discovery",
545
- code: "repository_binding_argument_invalid",
546
- file: "--repository",
547
- message: "repository bindings must use <id=absolute-path>",
548
- }]);
549
- }
550
- if (Object.hasOwn(result, repositoryId)) {
551
- throw new VnextConfigError([{
552
- phase: "discovery",
553
- code: "repository_binding_argument_duplicate",
554
- file: "--repository",
555
- message: `repository binding ${repositoryId} was supplied more than once`,
556
- }]);
557
- }
558
- result[repositoryId] = path;
559
- }
560
- return result;
561
- }
562
-
563
- async function cmdVnextInit(runtime: Runtime, options: { name?: string; projectId?: string; repository?: string[] }): Promise<number> {
564
- if (runtime.workspaceFlag !== undefined) {
565
- print(runtime.io, runtime.json, {
566
- ok: false,
567
- command: "init",
568
- error: "workspace_option_unsupported",
569
- }, "kxm init discovers the authoritative Git root from the current directory; --workspace is not supported");
570
- return 2;
571
- }
572
- try {
573
- const initialized = initializeVnextProject(runtime.cwd, {
574
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
575
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
576
- repositoryBindings: explicitRepositoryBindings(options.repository ?? []),
577
- localStateRoot: vnextUserStateRoot({ env: runtime.env }),
578
- dryRun: runtime.dryRun,
579
- });
580
- const payload = {
581
- ok: initialized.action !== "planned" || runtime.dryRun,
582
- command: "init",
583
- action: initialized.action,
584
- ...initPlanPayload(initialized.plan),
585
- files: initialized.files,
586
- ...(initialized.configRevision ? { configRevision: initialized.configRevision } : {}),
587
- ...(initialized.localBindingFile ? { localBindingFile: initialized.localBindingFile } : {}),
588
- ...(initialized.bindingsChanged === undefined ? {} : { bindingsChanged: initialized.bindingsChanged }),
589
- ...(initialized.repairPlan === undefined ? {} : { repairPlan: initialized.repairPlan }),
590
- ...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
591
- ...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
592
- plannedOnly: initialized.action === "planned",
593
- };
594
- const finishInit = (code: number, text: string): number => {
595
- print(runtime.io, runtime.json, payload, text);
596
- return code;
597
- };
598
- if (initialized.action === "created") {
599
- return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
600
- }
601
- if (initialized.action === "joined") {
602
- return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
603
- }
604
- if (initialized.action === "repaired") {
605
- return finishInit(0, `repaired vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
606
- }
607
- if (initialized.action === "resumed") {
608
- return finishInit(0, `resumed vNext ${initialized.transactionKind ?? "initialization"} at ${initialized.projectRoot ?? runtime.cwd}`);
609
- }
610
- if (initialized.action === "validated") {
611
- return finishInit(0, `validated vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
612
- }
613
- if (runtime.dryRun) {
614
- return finishInit(0, `init plan: ${initialized.plan.mode}`);
615
- }
616
- const next = initialized.plan.mode === "migrate"
617
- ? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
618
- : initialized.repairPlan?.issues.length
619
- ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
620
- : "partial or provenance-free vNext state requires explicit repair; no files were overwritten";
621
- return finishInit(1, next);
622
- } catch (error) {
623
- if (error instanceof VnextConfigError) {
624
- print(runtime.io, runtime.json, {
625
- ok: false,
626
- command: "init",
627
- error: "vnext_initialization_failed",
628
- issues: error.issues,
629
- }, `vNext initialization failed: ${error.message}`);
630
- return 1;
631
- }
632
- print(runtime.io, runtime.json, {
633
- ok: false,
634
- command: "init",
635
- error: "vnext_initialization_io_failed",
636
- }, "vNext initialization failed because a local filesystem operation did not complete");
637
- return 1;
638
- }
639
- }
640
-
641
- async function cmdVnextMigratePlan(runtime: Runtime): Promise<number> {
642
- if (runtime.workspaceFlag !== undefined) {
643
- print(runtime.io, runtime.json, {
644
- ok: false,
645
- command: "migrate plan",
646
- error: "workspace_option_unsupported",
647
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
648
- return 2;
649
- }
650
- try {
651
- const result = planVnextMigration(runtime.cwd, {});
652
- const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
653
- const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
654
- const payload = {
655
- ok: result.plan.canApply === true,
656
- command: "migrate plan",
657
- plan: result.plan,
658
- plannedOnly: result.plan.canApply !== true,
659
- };
660
- if (result.plan.canApply === true) {
661
- print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
662
- return 0;
663
- }
664
- print(
665
- runtime.io,
666
- runtime.json,
667
- payload,
668
- `migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
669
- );
670
- return 1;
671
- } catch (error) {
672
- if (error instanceof VnextConfigError) {
673
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
674
- return 1;
675
- }
676
- 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");
677
- return 1;
678
- }
679
- }
680
-
681
- async function cmdVnextMigrateApply(runtime: Runtime, options: { decisions?: string; projectId?: string; name?: string }): Promise<number> {
682
- if (runtime.workspaceFlag !== undefined) {
683
- print(runtime.io, runtime.json, {
684
- ok: false,
685
- command: "migrate apply",
686
- error: "workspace_option_unsupported",
687
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
688
- return 2;
689
- }
690
- try {
691
- const result = applyVnextMigration(runtime.cwd, {
692
- ...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
693
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
694
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
695
- localStateRoot: vnextUserStateRoot({ env: runtime.env }),
696
- dryRun: runtime.dryRun,
697
- });
698
- const payload = {
699
- ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
700
- command: "migrate apply",
701
- action: result.action,
702
- files: result.files,
703
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
704
- ...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
705
- plannedOnly: result.action === "planned",
706
- };
707
- if (result.action === "applied") {
708
- print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
709
- return 0;
710
- }
711
- if (result.action === "already-migrated") {
712
- print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
713
- return 0;
714
- }
715
- if (runtime.dryRun && result.plan?.canApply === true) {
716
- print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
717
- return 0;
718
- }
719
- const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
720
- print(
721
- runtime.io,
722
- runtime.json,
723
- { ...payload, plan: result.plan },
724
- `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")}`,
725
- );
726
- return 1;
727
- } catch (error) {
728
- if (error instanceof VnextConfigError) {
729
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
730
- return 1;
731
- }
732
- 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");
733
- return 1;
734
- }
735
- }
736
-
737
- async function cmdVnextMigrateVerify(runtime: Runtime): Promise<number> {
738
- if (runtime.workspaceFlag !== undefined) {
739
- print(runtime.io, runtime.json, {
740
- ok: false,
741
- command: "migrate verify",
742
- error: "workspace_option_unsupported",
743
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
744
- return 2;
745
- }
746
- let result: ReturnType<typeof verifyVnextMigration>;
747
- try {
748
- result = verifyVnextMigration(runtime.cwd, {});
749
- } catch (error) {
750
- if (error instanceof VnextConfigError) {
751
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
752
- return 1;
753
- }
754
- 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");
755
- return 1;
756
- }
757
- const payload = {
758
- ok: result.ok,
759
- command: "migrate verify",
760
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
761
- issues: result.issues,
762
- };
763
- if (result.ok) {
764
- print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
765
- return 0;
766
- }
767
- print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
768
- return 1;
769
- }
770
-
771
- async function cmdBackup(runtime: Runtime, options: { out?: string }): Promise<number> {
772
- try {
773
- const { manifest, outDir } = createBackup({
774
- projectRoot: runtime.cwd,
775
- ...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
776
- });
777
- const payload = {
778
- ok: true,
779
- command: "backup",
780
- backupId: manifest.backupId,
781
- outDir,
782
- manifest,
783
- };
784
- const summary = [
785
- `Created SQLite backup with ${manifest.stores.length} store(s):`,
786
- ...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
787
- `Manifest: ${join(outDir, "manifest.json")}`,
788
- ].join("\n");
789
- print(runtime.io, runtime.json, payload, summary);
790
- return 0;
791
- } catch (error) {
792
- if (error instanceof VnextConfigError) {
793
- print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
794
- return 1;
795
- }
796
- 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)}`);
797
- return 1;
798
- }
799
- }
800
-
801
- async function cmdRestore(runtime: Runtime, manifestArg: string): Promise<number> {
802
- try {
803
- const result = restoreBackup(resolve(runtime.cwd, manifestArg), {
804
- projectRoot: runtime.cwd,
805
- });
806
- const payload = {
807
- ok: true,
808
- command: "restore",
809
- backupId: result.backupId,
810
- manifestPath: result.manifestPath,
811
- restoredStores: result.restoredStores,
812
- };
813
- const summary = [
814
- `Restored ${result.restoredStores.length} SQLite store(s) from ${result.manifestPath}:`,
815
- ...result.restoredStores.map((s) => ` - ${s.storeId}: -> ${s.sourcePath} (schema v${s.schemaVersion}, integrity ${s.integrity})`),
816
- ].join("\n");
817
- print(runtime.io, runtime.json, payload, summary);
818
- return 0;
819
- } catch (error) {
820
- if (error instanceof VnextConfigError) {
821
- print(runtime.io, runtime.json, { ok: false, command: "restore", error: "restore_failed", issues: error.issues }, `restore failed: ${error.message}`);
822
- return 1;
823
- }
824
- 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)}`);
825
- return 1;
826
- }
827
- }
828
-
829
- async function cmdVnextTrust(runtime: Runtime, check: boolean, options: { base?: string }): Promise<number> {
830
- if (runtime.workspaceFlag !== undefined) {
831
- print(runtime.io, runtime.json, {
832
- ok: false,
833
- command: check ? "trust check" : "trust diff",
834
- error: "workspace_option_unsupported",
835
- }, "kxm trust discovers the authoritative Git root from the current directory; --workspace is not supported");
836
- return 2;
837
- }
838
- const command = check ? "trust check" : "trust diff";
839
- try {
840
- // Host-local member bindings travel with the user state, not Git; pass
841
- // them so member repositories diff with the same identities on both sides.
842
- const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
843
- const bindings = readVnextLocalBindings(projectRoot, { stateRoot: vnextUserStateRoot({ env: runtime.env }) });
844
- const diff = diffVnextProjectAgainstRevision(projectRoot, options.base?.trim() || "HEAD", {
845
- repositoryBindings: bindings?.repositories ?? {},
846
- });
847
- const payload = {
848
- ok: !check || !diff.requiresReview,
849
- command,
850
- baseRevision: diff.baseRevision,
851
- candidateRevision: diff.candidateRevision,
852
- requiresReview: diff.requiresReview,
853
- expansions: diff.expansions.length,
854
- narrowings: diff.narrowings.length,
855
- neutralChanges: diff.neutralChanges.length,
856
- changes: diff.changes,
857
- };
858
- const text = formatVnextPermissionDiff(diff);
859
- if (check && diff.requiresReview) {
860
- print(runtime.io, runtime.json, payload, `${text}\ntrust check failed: review every expansion above before merging`);
861
- return 1;
862
- }
863
- print(runtime.io, runtime.json, payload, text);
864
- return 0;
865
- } catch (error) {
866
- if (error instanceof VnextConfigError) {
867
- print(runtime.io, runtime.json, { ok: false, command, error: "trust_diff_failed", issues: error.issues }, `permission diff failed: ${error.message}`);
868
- return 1;
869
- }
870
- 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");
871
- return 1;
872
- }
873
- }
874
-
875
- /** No step engine exists yet: `kxm run` durably accepts a run and stops.
876
- * The run engine replaces this notice with real progress. */
877
- const RUN_ENGINE_PHASE = "pre-3a";
878
- const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
879
-
880
- async function cmdVnextRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
881
- if (runtime.workspaceFlag !== undefined) {
882
- print(runtime.io, runtime.json, {
883
- ok: false,
884
- command: "run",
885
- error: "workspace_option_unsupported",
886
- }, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
887
- return 2;
888
- }
889
- if (!workflow) {
890
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
891
- return 2;
892
- }
893
- try {
894
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
895
- if (!projectRoot) {
896
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "project_required" }, "kxm run requires a vNext project (run kxm init first)");
897
- return 1;
898
- }
899
- const bundle = loadVnextProject(projectRoot, {});
900
- if (!bundle.workflows.has(workflow)) {
901
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
902
- return 1;
903
- }
904
- if (runtime.dryRun) {
905
- print(runtime.io, runtime.json, {
906
- ok: true,
907
- command: "run",
908
- dryRun: true,
909
- projectRoot,
910
- workflowId: workflow,
911
- configRevision: bundle.configRevision,
912
- }, `run plan: workflow ${workflow} at ${bundle.configRevision.slice(0, 19)}… (no run created)`);
913
- return 0;
914
- }
915
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
916
- const prompt = promptParts.join(" ").trim();
917
- const acceptance = await vnextRuntimeRequest(supervisor, "POST", "/v1/runs", {
918
- projectRoot,
919
- workflowId: workflow,
920
- prompt,
921
- });
922
- const run = acceptance.run as { runId: string; homeRuntimeId: string; status: string; configRevision: string };
923
- print(runtime.io, runtime.json, {
924
- ok: true,
925
- command: "run",
926
- phase: RUN_ENGINE_PHASE,
927
- idempotent: acceptance.idempotent === true,
928
- run,
929
- supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
930
- }, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${RUN_ENGINE_NOTICE}`);
931
- return 0;
932
- } catch (error) {
933
- if (error instanceof VnextConfigError) {
934
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_failed", issues: error.issues }, `run failed: ${error.message}`);
935
- return 1;
936
- }
937
- print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_io_failed" }, "run failed because a local operation did not complete");
938
- return 1;
939
- }
940
- }
941
-
942
- async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
943
- try {
944
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
945
- if (!projectRoot) {
946
- print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a vNext project (run kxm init first)");
947
- return 1;
948
- }
949
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
950
- const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
951
- const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
952
- print(runtime.io, runtime.json, { ok: true, command: "runs status", run }, `run ${run.runId}: ${run.status} (workflow ${run.workflowId}, updated ${run.updatedAt})`);
953
- return 0;
954
- } catch (error) {
955
- if (error instanceof VnextConfigError) {
956
- print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "run_status_failed", issues: error.issues }, `run status failed: ${error.message}`);
957
- return 1;
958
- }
959
- 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");
960
- return 1;
961
- }
962
- }
963
-
964
- async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
965
- try {
966
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
967
- if (!projectRoot) {
968
- print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "project_required" }, "kxm runs cancel requires a vNext project (run kxm init first)");
969
- return 1;
970
- }
971
- if (runtime.dryRun) {
972
- print(runtime.io, runtime.json, { ok: true, command: "runs cancel", dryRun: true, runId }, `cancel plan: run ${runId} (no events written)`);
973
- return 0;
974
- }
975
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
976
- const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/cancel?projectRoot=${encodeURIComponent(projectRoot)}`, {});
977
- const run = result.run as { runId: string; status: string };
978
- print(runtime.io, runtime.json, {
979
- ok: true,
980
- command: "runs cancel",
981
- idempotent: result.idempotent === true,
982
- run,
983
- }, `run ${run.runId}: ${run.status}`);
984
- return 0;
985
- } catch (error) {
986
- if (error instanceof VnextConfigError) {
987
- print(runtime.io, runtime.json, { ok: false, command: "runs cancel", error: "run_cancel_failed", issues: error.issues }, `run cancel failed: ${error.message}`);
988
- return 1;
989
- }
990
- 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");
991
- return 1;
992
- }
993
- }
994
-
995
- async function cmdVnextRunList(runtime: Runtime): Promise<number> {
996
- try {
997
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
998
- if (!projectRoot) {
999
- print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "project_required" }, "kxm runs list requires a vNext project (run kxm init first)");
1000
- return 1;
1001
- }
1002
- const bundle = loadVnextProject(projectRoot, {});
1003
- const projectId = String(bundle.project.value.id);
1004
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1005
- const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(projectRoot)}`);
1006
- const runs = (result.runs ?? []) as Array<{ runId: string; status: string; workflowId: string; createdAt: string }>;
1007
- print(
1008
- runtime.io,
1009
- runtime.json,
1010
- { ok: true, command: "runs list", runs },
1011
- runs.length === 0 ? "no runs" : runs.map((run) => `${run.runId} ${run.status} ${run.workflowId} ${run.createdAt}`).join("\n"),
1012
- );
1013
- return 0;
1014
- } catch (error) {
1015
- if (error instanceof VnextConfigError) {
1016
- print(runtime.io, runtime.json, { ok: false, command: "runs list", error: "run_list_failed", issues: error.issues }, `run list failed: ${error.message}`);
1017
- return 1;
1018
- }
1019
- 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");
1020
- return 1;
1021
- }
1022
- }
1023
-
1024
- async function cmdHarnessList(runtime: Runtime): Promise<number> {
1025
- const inventory = probeHarnesses({ env: runtime.env });
1026
- print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
1027
- return 0;
1028
- }
1029
-
1030
- function installProbeFrom(runtime: Runtime): InstallProbe {
1031
- const partial = runtime.io.installProbe ?? {};
1032
- return {
1033
- moduleDir: partial.moduleDir ?? dirname(fileURLToPath(import.meta.url)),
1034
- repoRoot: partial.repoRoot ?? repoRoot,
1035
- homeDir: partial.homeDir ?? homedir(),
1036
- platform: partial.platform ?? process.platform,
1037
- env: partial.env ?? runtime.env,
1038
- };
1039
- }
1040
-
1041
- function cliSpawn(runtime: Runtime, command: string, args: readonly string[], extra?: { timeout?: number }): CliSpawnResult {
1042
- if (runtime.io.spawnSync) return runtime.io.spawnSync(command, args);
1043
- const result = spawnSync(command, [...args], {
1044
- encoding: "utf8",
1045
- windowsHide: true,
1046
- shell: process.platform === "win32",
1047
- ...extra,
1048
- });
1049
- return {
1050
- status: result.status,
1051
- stdout: typeof result.stdout === "string" ? result.stdout : "",
1052
- stderr: typeof result.stderr === "string" ? result.stderr : "",
1053
- ...(result.error ? { error: result.error } : {}),
1054
- };
1055
- }
1056
-
1057
- function npmGlobalRootFn(runtime: Runtime): () => string | undefined {
1058
- return () => {
1059
- const result = cliSpawn(runtime, "npm", ["root", "-g"], { timeout: 5_000 });
1060
- if (result.error || result.status !== 0) return undefined;
1061
- const out = result.stdout.trim();
1062
- return out || undefined;
1063
- };
1064
- }
1065
-
1066
- function warnIgnoredProjectUpdateYaml(runtime: Runtime): void {
1067
- const projectFile = join(runtime.dirs.workspace, "update.yaml");
1068
- if (!existsSync(projectFile)) return;
1069
- const userFile = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
1070
- runtime.io.stderr(`kxm: ignoring .kxm/update.yaml in ${runtime.dirs.workdir}; update settings are read only from ${userFile}\n`);
1071
- }
1072
-
1073
- function installKindPayload(report: InstallKindReport): { installKind: string; root: string } {
1074
- return { installKind: report.kind, root: report.root };
1075
- }
1076
-
1077
- function formatPackageUpdateStep(step: KxmPackageUpdateStep): string {
1078
- if (step.kind === "verify") return `verify sha256 ${step.path}`;
1079
- return `${step.command} ${step.args.join(" ")}`;
1080
- }
1081
-
1082
- async function refreshKxmUpdateNotice(runtime: Runtime, config?: KxmUpdateConfig): Promise<KxmUpdateNotice> {
1083
- const resolved = config ?? loadKxmUpdateConfig(runtime.env);
1084
- const current = readInstalledKxmVersion(repoRoot);
1085
- const fetched = await fetchLatestKxmVersion(resolved.source, runtime.env, runtime.fetchImpl);
1086
- const notice = noticeFromVersions(current, fetched.latest, resolved, fetched.error, fetched.asset);
1087
- writeUpdateCache(runtime.dirs.state, notice);
1088
- return notice;
1089
- }
1090
-
1091
- function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice): { ok: boolean; detail: string; error?: string } {
1092
- if (!notice.latest) return { ok: false, detail: "no_latest_version" };
1093
- if (notice.source === "github" && !notice.asset?.sha256) {
1094
- const name = kxmReleaseAssetName(notice.latest);
1095
- return {
1096
- ok: false,
1097
- error: "release_digest_missing",
1098
- detail: `release v${notice.latest} has no sha256 digest for ${name}; refusing to install`,
1099
- };
1100
- }
1101
- const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
1102
- try {
1103
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
1104
- if (runtime.dryRun) {
1105
- return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
1106
- }
1107
- for (const step of planned) {
1108
- if (step.kind === "verify") {
1109
- if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
1110
- const actual = existsSync(step.path)
1111
- ? createHash("sha256").update(readFileSync(step.path)).digest("hex")
1112
- : "missing";
1113
- return {
1114
- ok: false,
1115
- error: "release_digest_mismatch",
1116
- detail: `${kxmReleaseAssetName(notice.latest)} sha256 ${actual} does not match release digest ${step.sha256}; refusing to install`,
1117
- };
1118
- }
1119
- continue;
1120
- }
1121
- const result = cliSpawn(runtime, step.command, step.args);
1122
- if (result.status !== 0) {
1123
- const detail = (result.stderr || result.stdout || result.error?.message || "update_failed").trim().slice(0, 500);
1124
- return { ok: false, detail };
1125
- }
1126
- }
1127
- return { ok: true, detail: `installed ${notice.latest} from ${notice.source}` };
1128
- } finally {
1129
- rmSync(releaseDir, { recursive: true, force: true });
1130
- }
1131
- }
1132
-
1133
- async function cmdUpdate(runtime: Runtime, harness: string | undefined, options: {
1134
- self?: boolean;
1135
- extensions?: boolean;
1136
- models?: boolean;
1137
- check?: boolean;
1138
- kxm?: boolean;
1139
- }): Promise<number> {
1140
- if (options.check && (options.kxm || options.self || options.extensions || options.models || harness)) {
1141
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "--check cannot be combined with other update flags");
1142
- return 2;
1143
- }
1144
- const selected = [options.self, options.extensions, options.models].filter(Boolean).length;
1145
- if (selected > 1) {
1146
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --self, --extensions, or --models");
1147
- return 2;
1148
- }
1149
- warnIgnoredProjectUpdateYaml(runtime);
1150
- const probe = installProbeFrom(runtime);
1151
- const classified = classifyInstallRoot(probe);
1152
- const current = readInstalledKxmVersion(repoRoot);
1153
- let notice: KxmUpdateNotice;
1154
- let kindReport = classified;
1155
- if (classified.kind === "source") {
1156
- if (options.check) {
1157
- const message = `kxm ${current} (running from source at ${classified.root})`;
1158
- print(runtime.io, runtime.json, {
1159
- ok: true,
1160
- command: "update check",
1161
- current,
1162
- available: false,
1163
- auto: false,
1164
- source: "github",
1165
- installKind: "source",
1166
- root: classified.root,
1167
- message,
1168
- }, message);
1169
- return 0;
1170
- }
1171
- if (options.kxm) {
1172
- print(runtime.io, runtime.json, {
1173
- ok: false,
1174
- command: "update",
1175
- error: "install_kind_source",
1176
- installKind: "source",
1177
- root: classified.root,
1178
- instruction: classified.instruction,
1179
- }, classified.instruction);
1180
- return 2;
1181
- }
1182
- notice = {
1183
- current,
1184
- available: false,
1185
- auto: false,
1186
- source: "github",
1187
- message: `kxm ${current} (running from source)`,
1188
- };
1189
- } else {
1190
- try {
1191
- notice = await refreshKxmUpdateNotice(runtime);
1192
- } catch (error) {
1193
- if (error instanceof KxmUpdateConfigError) {
1194
- print(runtime.io, runtime.json, { ok: false, command: "update", error: error.code, ...installKindPayload(classified) }, error.message);
1195
- return 2;
1196
- }
1197
- throw error;
1198
- }
1199
- }
1200
- if (options.check) {
1201
- print(runtime.io, runtime.json, {
1202
- ok: true,
1203
- command: "update check",
1204
- ...notice,
1205
- ...installKindPayload(classified),
1206
- }, notice.message);
1207
- return 0;
1208
- }
1209
- const applyKxm = Boolean(options.kxm || notice.auto);
1210
- let kxmApply: { ok: boolean; detail: string; error?: string } | undefined;
1211
- if (applyKxm) {
1212
- const resolved = resolveInstallKind(probe, npmGlobalRootFn(runtime));
1213
- kindReport = resolved;
1214
- if (resolved.kind !== "npm-global") {
1215
- if (options.kxm) {
1216
- print(runtime.io, runtime.json, {
1217
- ok: false,
1218
- command: "update",
1219
- error: `install_kind_${resolved.kind}`,
1220
- installKind: resolved.kind,
1221
- root: resolved.root,
1222
- instruction: resolved.instruction,
1223
- notice,
1224
- }, resolved.instruction);
1225
- return 2;
1226
- }
1227
- if (notice.available) runtime.io.stderr(`kxm: ${resolved.instruction}\n`);
1228
- } else if (notice.available) {
1229
- kxmApply = applyKxmPackageUpdate(runtime, notice);
1230
- if (!kxmApply.ok && options.kxm) {
1231
- print(runtime.io, runtime.json, {
1232
- ok: false,
1233
- command: "update",
1234
- error: kxmApply.error,
1235
- kxm: kxmApply,
1236
- notice,
1237
- ...installKindPayload(resolved),
1238
- }, kxmApply.detail);
1239
- return 1;
1240
- }
1241
- }
1242
- }
1243
- const skipHarness = Boolean(options.kxm && selected === 0 && !harness && !notice.auto);
1244
- if (skipHarness) {
1245
- print(
1246
- runtime.io,
1247
- runtime.json,
1248
- { ok: kxmApply?.ok !== false, command: "update", dryRun: runtime.dryRun, notice, kxm: kxmApply, ...installKindPayload(kindReport) },
1249
- kxmApply?.detail ?? notice.message,
1250
- );
1251
- return kxmApply?.ok === false ? 1 : 0;
1252
- }
1253
- const scope: HarnessUpdateScope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
1254
- const inventory = probeHarnesses({ env: runtime.env });
1255
- const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope });
1256
- const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
1257
- const failed = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
1258
- const skippedUnknown = steps.some((step) => step.detail === "unknown_harness");
1259
- const text = [notice.available ? notice.message : undefined, kxmApply?.detail, formatHarnessUpdate(steps)].filter(Boolean).join("\n");
1260
- print(runtime.io, runtime.json, {
1261
- ok: !failed && !skippedUnknown,
1262
- command: "update",
1263
- dryRun: runtime.dryRun,
1264
- scope,
1265
- notice,
1266
- ...(kxmApply ? { kxm: kxmApply } : {}),
1267
- steps,
1268
- ...installKindPayload(kindReport),
1269
- }, text);
1270
- if (skippedUnknown) return 2;
1271
- return failed ? 1 : 0;
1272
- }
1273
-
1274
- async function cmdVnextRuntime(runtime: Runtime, action: string): Promise<number> {
1275
- const paths = vnextRuntimePaths({ env: runtime.env });
1276
- try {
1277
- if (action === "start") {
1278
- if (runtime.dryRun) {
1279
- print(runtime.io, runtime.json, { ok: true, command: "runtime start", dryRun: true }, "runtime supervisor would auto-start");
1280
- return 0;
1281
- }
1282
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1283
- print(runtime.io, runtime.json, {
1284
- ok: true,
1285
- command: "runtime start",
1286
- runtimeId: supervisor.runtimeId,
1287
- port: supervisor.port,
1288
- started: supervisor.started,
1289
- }, `runtime supervisor ${supervisor.started ? "started" : "already running"}: ${supervisor.runtimeId} on 127.0.0.1:${supervisor.port}`);
1290
- return 0;
1291
- }
1292
- if (action === "status") {
1293
- const status = vnextSupervisorStatus(paths);
1294
- print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status }, status.running
1295
- ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}`
1296
- : "runtime supervisor is not running");
1297
- return status.running ? 0 : 1;
1298
- }
1299
- if (action === "stop") {
1300
- const status = vnextSupervisorStatus(paths);
1301
- if (!status.running || !status.port) {
1302
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: false }, "runtime supervisor is not running");
1303
- return 0;
1304
- }
1305
- if (runtime.dryRun) {
1306
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", dryRun: true }, `would stop runtime supervisor pid ${status.pid}`);
1307
- return 0;
1308
- }
1309
- // The stop request posts a bearer token, so prove the listener is the
1310
- // real supervisor (keyed healthz challenge) before sending it.
1311
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
1312
- await vnextRuntimeRequest(supervisor, "POST", "/v1/shutdown", {});
1313
- print(runtime.io, runtime.json, { ok: true, command: "runtime stop", stopped: true }, `runtime supervisor ${status.runtimeId} stopping`);
1314
- return 0;
1315
- }
1316
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "unknown_action" }, `unknown runtime action: ${action}`);
1317
- return 2;
1318
- } catch (error) {
1319
- if (error instanceof VnextConfigError) {
1320
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_failed", issues: error.issues }, `runtime failed: ${error.message}`);
1321
- return 1;
1322
- }
1323
- print(runtime.io, runtime.json, { ok: false, command: "runtime", error: "runtime_io_failed" }, "runtime failed because a local operation did not complete");
1324
- return 1;
1325
- }
1326
- }
1327
-
1328
- async function cmdValidate(runtime: Runtime, fileFlag?: string): Promise<number> {
1329
- const worker = gateOf(runtime, "validate");
1330
- const explicitFile = fileFlag?.trim();
1331
- const configuredFile = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
1332
- const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim();
1333
- if (!explicitFile && configuredFile && inline) {
1334
- printWorker(
1335
- runtime,
1336
- worker,
1337
- { ok: false, command: "validate", error: "ambiguous_workflow_source" },
1338
- "configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE",
1339
- );
1340
- return 2;
1341
- }
1342
- if (!explicitFile && !configuredFile && !inline) {
1343
- printWorker(
1344
- runtime,
1345
- worker,
1346
- { ok: false, command: "validate", error: "workflow_source_required" },
1347
- "provide --file or configure exactly one workflow source environment variable",
1348
- );
1349
- return 2;
1350
- }
1351
-
1352
- const selectedFile = explicitFile || configuredFile;
1353
- const file = selectedFile ? resolve(runtime.cwd, selectedFile) : undefined;
1354
- if (file && !existsSync(file)) {
1355
- printWorker(runtime, worker, { ok: false, command: "validate", error: "file_not_found", file }, `workflow file not found: ${file}`);
1356
- return 1;
1357
- }
1358
- try {
1359
- const raw = file ? readFileSync(file, "utf8") : inline!;
1360
- const warnings: string[] = [];
1361
- const definitions = parseWorkflowDefinitions(raw, runtime.env, (message) => warnings.push(message));
1362
- const secretEnvs = definitions.map((definition) => ({
1363
- id: definition.id,
1364
- secretConfigured: Boolean(definition.secret),
1365
- signalSecretConfigured: Boolean(definition.signalSecret),
1366
- }));
1367
- const source = file ? "file" : "inline";
1368
- printWorker(
1369
- runtime,
1370
- worker,
1371
- { ok: true, command: "validate", source, ...(file ? { file } : {}), workflows: secretEnvs, warnings },
1372
- `validated ${definitions.length} workflow(s) from ${source}${warnings.length ? ` with ${warnings.length} warning(s)` : ""}`,
1373
- warnings.length ? "warning" : undefined,
1374
- );
1375
- return 0;
1376
- } catch (error) {
1377
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid_workflow";
1378
- printWorker(runtime, worker, { ok: false, command: "validate", error: message }, message);
1379
- return 1;
1380
- }
1381
- }
1382
-
1383
- async function cmdArtifactsExist(runtime: Runtime, pathFlag: string): Promise<number> {
1384
- const worker = gateOf(runtime, "artifacts-exist");
1385
- const checked = verifyArtifactExists(runtime.dirs.assets, resolve(runtime.cwd, pathFlag));
1386
- if (!checked.ok) {
1387
- printWorker(
1388
- runtime,
1389
- worker,
1390
- { ok: false, command: "artifacts-exist", error: checked.error, path: checked.path },
1391
- `artifact verification failed: ${checked.error}`,
1392
- );
1393
- return 1;
1394
- }
1395
- printWorker(
1396
- runtime,
1397
- worker,
1398
- { ok: true, command: "artifacts-exist", path: checked.path, bytes: checked.bytes },
1399
- "artifact exists and is non-empty under workspace assets",
1400
- );
1401
- return 0;
1402
- }
1403
-
1404
- async function cmdStatus(runtime: Runtime): Promise<number> {
1405
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl);
1406
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl);
1407
- const payload = { ok: health.ok && ready.ok, command: "hub view", health: health.body, ready: ready.body };
1408
- print(runtime.io, runtime.json, payload, `hub health=${health.ok} ready=${ready.ok}`);
1409
- return payload.ok ? 0 : 1;
1410
- }
1411
-
1412
- async function cmdDash(runtime: Runtime, options: { screen?: string } = {}): Promise<number> {
1413
- const requested = options.screen?.trim();
1414
- if (requested && !(MESH_TUI_PANELS as readonly string[]).includes(requested)) {
1415
- print(runtime.io, runtime.json, { ok: false, command: "dash", error: "unknown_screen" }, `unknown screen ${requested}; use agents, tasks, workflows, plans, inbox, procs, or spend`);
1416
- return 2;
1417
- }
1418
- const screen = requested as MeshTuiPanel | undefined;
1419
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
1420
- if (runtime.dryRun) {
1421
- print(runtime.io, runtime.json, {
1422
- ok: true,
1423
- command: "dash",
1424
- dryRun: true,
1425
- serverUrl: runtime.serverUrl,
1426
- transport: "sse",
1427
- ...(screen ? { screen } : {}),
1428
- }, "would start kxm dash");
1429
- return 0;
1430
- }
1431
- if (runtime.json) {
1432
- runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
1433
- return 2;
1434
- }
1435
- const project = runtime.env.KXM_PROJECT?.trim() || basename(runtime.dirs.workdir) || "project";
1436
- const authToken = runtime.env.KXM_AUTH_TOKEN?.trim();
1437
- return await runMeshTui({
1438
- serverUrl: runtime.serverUrl,
1439
- dataPath,
1440
- stateDir: runtime.dirs.state,
1441
- project,
1442
- env: runtime.env,
1443
- ...(authToken ? { authToken } : {}),
1444
- ...(screen ? { screen } : {}),
1445
- fetchImpl: runtime.fetchImpl,
1446
- stdout: runtime.io.stdout,
1447
- stdin: process.stdin,
1448
- isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1449
- });
1450
- }
1451
-
1452
- async function cmdHub(runtime: Runtime): Promise<number> {
1453
- let refresh: Promise<unknown> | undefined;
1454
- if (!runtime.dryRun) {
1455
- const probe = installProbeFrom(runtime);
1456
- if (classifyInstallRoot(probe).kind !== "source") {
1457
- warnIgnoredProjectUpdateYaml(runtime);
1458
- const cached = readUpdateCache(runtime.dirs.state);
1459
- if (cached?.available) runtime.io.stderr(`${cached.message}\n`);
1460
- let config: KxmUpdateConfig | undefined;
1461
- try {
1462
- config = loadKxmUpdateConfig(runtime.env);
1463
- } catch (error) {
1464
- if (error instanceof KxmUpdateConfigError) {
1465
- const yamlPath = join(vnextUserStateRoot({ env: runtime.env }), "update.yaml");
1466
- runtime.io.stderr(`kxm: ${error.message}; update check skipped; fix or remove ${yamlPath}\n`);
1467
- } else {
1468
- throw error;
1469
- }
1470
- }
1471
- if (config) {
1472
- refresh = refreshKxmUpdateNotice(runtime, config).then((notice) => {
1473
- if (notice.available && !cached?.available) runtime.io.stderr(`${notice.message}\n`);
1474
- }).catch(() => undefined);
1475
- }
1476
- }
1477
- }
1478
- const extraEnv = workspaceEnv(runtime);
1479
- if (runtime.dryRun) {
1480
- print(runtime.io, runtime.json, { ok: true, command: "hub start", dryRun: true, workspace: runtime.dirs.workspace }, "would start hub");
1481
- return 0;
1482
- }
1483
- const code = await (runtime.io.spawnHub ?? ((launchEnv) => spawnScript("kxm-hub.mjs", launchEnv)))(extraEnv);
1484
- if (refresh) await refresh;
1485
- return code;
1486
- }
1487
-
1488
- function formatHubBindHealth(health: HubHealth): string {
1489
- if (health === "on") return "health=on";
1490
- if (health === "off") return "health=off (nothing answered; run kxm hub start)";
1491
- return "health=unknown (no reply within 300 ms)";
1492
- }
1493
-
1494
- async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<number> {
1495
- let url: string;
1496
- try {
1497
- url = validateHubUrl(rawUrl);
1498
- } catch (error) {
1499
- if (error instanceof HubBindingError) {
1500
- print(
1501
- runtime.io,
1502
- runtime.json,
1503
- { ok: false, command: "hub bind", error: "hub_url_invalid" },
1504
- "hub bind needs an http or https URL without credentials, query, or fragment",
1505
- );
1506
- return 2;
1507
- }
1508
- throw error;
1509
- }
1510
- const file = hubBindingFile(runtime.env);
1511
- if (runtime.dryRun) {
1512
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, file }, `would bind hub ${url}`);
1513
- return 0;
1514
- }
1515
- writeHubBinding({ schema: HUB_BINDING_SCHEMA, url, boundAt: new Date().toISOString() }, runtime.env);
1516
- const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
1517
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, file, health, probeMs }, `bound hub ${url} · ${formatHubBindHealth(health)}`);
1518
- return 0;
1519
- }
1520
-
1521
- async function cmdHubUnbind(runtime: Runtime): Promise<number> {
1522
- const file = hubBindingFile(runtime.env);
1523
- if (runtime.dryRun) {
1524
- print(
1525
- runtime.io,
1526
- runtime.json,
1527
- { ok: true, command: "hub unbind", dryRun: true, ...(runtime.boundHubUrl ? { url: runtime.boundHubUrl } : {}), file },
1528
- `would unbind hub${runtime.boundHubUrl ? ` ${runtime.boundHubUrl}` : ""}`,
1529
- );
1530
- return 0;
1531
- }
1532
- let url: string | undefined;
1533
- let malformed = false;
1534
- try {
1535
- url = readHubBinding(runtime.env)?.url;
1536
- } catch (error) {
1537
- if (error instanceof HubBindingError) malformed = true;
1538
- else throw error;
1539
- }
1540
- if (!malformed && !url) {
1541
- print(runtime.io, runtime.json, { ok: false, command: "hub unbind", error: "hub_not_bound" }, `no hub binding at ${file}`);
1542
- return 1;
1543
- }
1544
- removeHubBinding(runtime.env);
1545
- if (malformed) {
1546
- print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
1547
- return 0;
1548
- }
1549
- print(runtime.io, runtime.json, { ok: true, command: "hub unbind", url, file }, `unbound hub ${url}`);
1550
- return 0;
1551
- }
1552
-
1553
- async function cmdWorker(runtime: Runtime, options: {
1554
- name?: string;
1555
- project?: string;
1556
- model?: string;
1557
- fallbackModels?: string;
1558
- tools?: string;
1559
- sessionIsolation?: string;
1560
- continue?: boolean;
1561
- freshStart?: boolean;
1562
- }): Promise<number> {
1563
- const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
1564
- const project = options.project?.trim() || runtime.env.KXM_PROJECT?.trim();
1565
- const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
1566
- const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
1567
- const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
1568
- const sessionIsolation = options.sessionIsolation?.trim() || runtime.env.KXM_WORKER_SESSION_ISOLATION?.trim() || "off";
1569
- if (sessionIsolation !== "workflow" && sessionIsolation !== "off") {
1570
- runtime.io.stderr("worker --session-isolation must be workflow or off\n");
1571
- return 2;
1572
- }
1573
- const extraEnv = {
1574
- ...workspaceEnv(runtime),
1575
- ...(name ? { KXM_AGENT_NAME: name } : {}),
1576
- ...(project ? { KXM_PROJECT: project } : {}),
1577
- ...(model ? { KXM_WORKER_MODEL: model } : {}),
1578
- ...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
1579
- ...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
1580
- KXM_WORKER_SESSION_ISOLATION: sessionIsolation,
1581
- ...(options.continue === false ? { KXM_WORKER_CONTINUE: "false" } : {}),
1582
- ...(options.freshStart ? { KXM_WORKER_INITIAL_CONTINUE: "false" } : {}),
1583
- };
1584
- if (runtime.dryRun) {
1585
- printWorker(runtime, agentWorker({
1586
- name: name || "required",
1587
- project: project || "required",
1588
- ...(model ? { model } : {}),
1589
- }), {
1590
- ok: true,
1591
- command: "worker",
1592
- dryRun: true,
1593
- workspace: runtime.dirs.workspace,
1594
- name: name || "required",
1595
- project: project || "required",
1596
- model: model || "provider default",
1597
- fallbackModels: fallbackModels || "none",
1598
- tools: tools || "Pi defaults",
1599
- sessionIsolation,
1600
- continue: options.continue !== false,
1601
- freshStart: Boolean(options.freshStart),
1602
- }, "would start worker");
1603
- return 0;
1604
- }
1605
- if (!name || !project) {
1606
- runtime.io.stderr("worker requires --name and --project (or KXM_AGENT_NAME and KXM_PROJECT)\n");
1607
- return 2;
1608
- }
1609
- return await (runtime.io.spawnWorker ?? ((launchEnv) => spawnScript("kxm-worker.mjs", launchEnv)))(extraEnv);
1610
- }
1611
-
1612
- async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
1613
- const pids = existsSync(runtime.dirs.state)
1614
- ? readdirSync(runtime.dirs.state).filter((name) => name.endsWith(".pid"))
1615
- : [];
1616
- if (runtime.dryRun) {
1617
- print(runtime.io, runtime.json, { ok: true, command: "stop", dryRun: true, pidFiles: pids }, "would signal pid files");
1618
- return 0;
1619
- }
1620
- if (pids.length === 0) {
1621
- print(runtime.io, runtime.json, { ok: false, command: "stop", error: "no_pid_files" }, "no hub/worker pid files found");
1622
- return 1;
1623
- }
1624
- const requested: string[] = [];
1625
- const ignored: string[] = [];
1626
- const records = new Map<string, { pid: number; startedAt: string; generation?: string }>();
1627
- for (const file of pids) {
1628
- try {
1629
- const record = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { version?: number; pid?: number; role?: string; startedAt?: string; generation?: string; controlFile?: string };
1630
- const expectedControl = file === "hub.pid" ? "hub.stop" : file.startsWith("worker-") ? `${file.slice(0, -4)}.stop` : undefined;
1631
- const expectedRole = file === "hub.pid" ? "hub" : file.startsWith("worker-") ? "worker" : undefined;
1632
- 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; }
1633
- 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 });
1634
- requested.push(file); records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
1635
- } catch { ignored.push(file); }
1636
- }
1637
- if (requested.length === 0) { print(runtime.io, runtime.json, { ok: false, command: "stop", requested, ignored }, "no current managed processes found"); return 1; }
1638
- const waitMs = Math.min(30_000, Math.max(100, Number(waitMsFlag || 5_000)));
1639
- const deadline = Date.now() + waitMs;
1640
- const stopped = new Set<string>();
1641
- while (Date.now() <= deadline && stopped.size < requested.length) {
1642
- for (const [file, record] of records) {
1643
- try {
1644
- const current = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { pid?: number; startedAt?: string; generation?: string };
1645
- if (current.pid !== record.pid || current.startedAt !== record.startedAt || current.generation !== record.generation || !processExists(record.pid)) stopped.add(file);
1646
- } catch { stopped.add(file); }
1647
- }
1648
- if (stopped.size < requested.length) await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
1649
- }
1650
- const timedOut = requested.filter((file) => !stopped.has(file));
1651
- const ok = timedOut.length === 0;
1652
- print(runtime.io, runtime.json, { ok, command: "stop", requested, stopped: [...stopped], timedOut, ignored }, ok ? "managed processes stopped" : "stop request timed out");
1653
- return ok ? 0 : 1;
1654
- }
1655
-
1656
- async function cmdSessionStatus(runtime: Runtime): Promise<number> {
1657
- const stateDir = runtime.dirs.state;
1658
- const names = existsSync(stateDir) ? readdirSync(stateDir) : [];
1659
- const claims = [];
1660
- for (const file of names.filter((name) => name.endsWith(".pid"))) {
1661
- try {
1662
- const record = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as { pid?: number; role?: string; startedAt?: string; generation?: string };
1663
- claims.push({
1664
- file,
1665
- role: record.role,
1666
- pid: record.pid,
1667
- startedAt: record.startedAt,
1668
- live: Number.isInteger(record.pid) && record.pid! > 0 && processExists(record.pid!),
1669
- });
1670
- } catch {
1671
- claims.push({ file, live: false, error: "invalid_pid_record" });
1672
- }
1673
- }
1674
- const recoveries = [];
1675
- for (const file of names.filter((name) => name.startsWith("worker-recovery-") && name.endsWith(".json"))) {
1676
- try {
1677
- const envelope = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as {
1678
- reason?: string;
1679
- agentName?: string;
1680
- project?: string;
1681
- createdAt?: string;
1682
- runId?: string | null;
1683
- stageId?: string | null;
1684
- freshSession?: boolean;
1685
- };
1686
- recoveries.push({
1687
- file,
1688
- reason: envelope.reason,
1689
- agentName: envelope.agentName,
1690
- project: envelope.project,
1691
- createdAt: envelope.createdAt,
1692
- runId: envelope.runId ?? undefined,
1693
- stageId: envelope.stageId ?? undefined,
1694
- freshSession: envelope.freshSession === true,
1695
- });
1696
- } catch {
1697
- recoveries.push({ file, error: "invalid_recovery_envelope" });
1698
- }
1699
- }
1700
- print(
1701
- runtime.io,
1702
- runtime.json,
1703
- { ok: true, command: "session status", claims, recoveries },
1704
- `${claims.length} session claim(s), ${recoveries.length} recovery envelope(s)`,
1705
- );
1706
- return 0;
1707
- }
1708
-
1709
- async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean; token?: boolean } = {}): Promise<number> {
1710
- const sessionToken = mintSessionToken({ preset: "operator" });
1711
- if (options.token) {
1712
- print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
1713
- return 0;
1714
- }
1715
-
1716
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
1717
- const env = {
1718
- ...runtime.env,
1719
- KXM_STATE_DIR: runtime.dirs.state,
1720
- KXM_DATA_PATH: runtime.env.KXM_DATA_PATH?.trim() || dataPath,
1721
- };
1722
-
1723
- let hub: SessionHubStatus | undefined;
1724
- const targetUrl = runtime.env.KXM_SERVER_URL?.trim() || runtime.boundHubUrl || readHubBinding(runtime.env)?.url;
1725
- if (targetUrl) {
1726
- const { health } = await probeHubHealth(targetUrl, runtime.fetchImpl, 300);
1727
- hub = {
1728
- state: health,
1729
- evidence: health === "unknown" ? "timeout" : "probed",
1730
- online: health === "on",
1731
- url: targetUrl,
1732
- };
1733
- } else {
1734
- hub = { state: "off", evidence: "unconfigured", online: false };
1735
- }
1736
-
1737
- const brief = await loadSessionBriefAsync(runtime.dirs.workdir, env, undefined, hub, {
1738
- fetchImpl: runtime.fetchImpl,
1739
- sessionToken,
1740
- });
1741
-
1742
- if (options.status) {
1743
- print(runtime.io, runtime.json, brief, brief.statusLine);
1744
- return 0;
1745
- }
1746
- print(
1747
- runtime.io,
1748
- runtime.json,
1749
- brief,
1750
- `${formatSessionBriefText(brief)}\n\nSession token: ${sessionToken}\n`,
1751
- );
1752
- return 0;
1753
- }
1754
-
1755
- async function cmdSessionStart(runtime: Runtime, options: { id?: string; workflow?: string; mix?: string }): Promise<number> {
1756
- const id = options.id?.trim() || `session_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
1757
- const workflowId = options.workflow?.trim();
1758
- const mix = options.mix?.trim();
1759
- if (workflowId && mix) {
1760
- runtime.io.stderr("session start takes --workflow or --mix, not both\n");
1761
- return 2;
1762
- }
1763
- if (!workflowId && !mix) {
1764
- runtime.io.stderr("session start requires --workflow <id> or --mix <agent,gate,...>\n");
1765
- return 2;
1766
- }
1767
- const project = runtime.env.KXM_PROJECT?.trim();
1768
- let workers: Worker[];
1769
- try {
1770
- const names = mix
1771
- ? mix.split(",").map((name) => name.trim()).filter(Boolean)
1772
- : rosterNames(runtime.dirs.config);
1773
- workers = loadNamedWorkers(runtime.dirs.config, names, project);
1774
- } catch (error) {
1775
- const errorName = error && typeof error === "object" && "name" in error ? String(error.name) : "";
1776
- if (errorName !== "SessionConfigError") throw error;
1777
- const message = error instanceof Error ? redactSecrets(error.message) : "invalid session configuration";
1778
- runtime.io.stderr(`${message}\n`);
1779
- return 2;
1780
- }
1781
- const session = createSession({
1782
- id,
1783
- host: hostMode(runtime),
1784
- mode: workflowId ? "workflow" : "mix",
1785
- workers,
1786
- assetsDir: runtime.dirs.assets,
1787
- ...(workflowId ? { workflowId } : {}),
1788
- });
1789
- const created = [
1790
- ...sessionAssetDirs(runtime.dirs.assets, session.id),
1791
- ...(workflowId ? workflowAssetDirs(runtime.dirs.assets, workflowId) : []),
1792
- ];
1793
- if (!runtime.dryRun) {
1794
- for (const directory of created) mkdirSync(directory, { recursive: true });
1795
- writeSession(runtime.dirs.assets, session);
1796
- }
1797
- print(runtime.io, runtime.json, {
1798
- ok: true,
1799
- command: "session start",
1800
- dryRun: runtime.dryRun || undefined,
1801
- session,
1802
- created,
1803
- }, `session ${session.id} (${session.mode})`);
1804
- return 0;
1805
- }
1806
-
1807
- async function cmdImprove(runtime: Runtime, options: { file?: string; target?: string; outDir?: string } = {}): Promise<number> {
1808
- const file = options.file ? resolve(runtime.cwd, options.file) : telemetryPath(runtime.dirs.logs);
1809
- const routingRecords = existsSync(file)
1810
- ? readRoutingRecords(file).map((entry) => entry.routing)
1811
- : [];
1812
-
1813
- const candidatesDir = options.outDir
1814
- ? resolve(runtime.cwd, options.outDir)
1815
- : join(runtime.cwd, ".kxm", "candidates");
1816
-
1817
- const report = buildImprovementReport(routingRecords, {
1818
- candidatesDir,
1819
- projectRoot: runtime.cwd,
1820
- dryRun: runtime.dryRun,
1821
- });
1822
-
1823
- const reportDir = join(runtime.dirs.assets, "improvements");
1824
- const reportPath = writeImprovementReport(reportDir, report, runtime.dryRun);
1825
-
1826
- const text = formatImprovementReport(report);
1827
- print(runtime.io, runtime.json, {
1828
- ok: true,
1829
- command: "improve",
1830
- dryRun: runtime.dryRun || undefined,
1831
- path: reportPath,
1832
- events: report.recordsCount,
1833
- recordsCount: report.recordsCount,
1834
- groupsCount: report.groups.length,
1835
- candidatesCount: report.candidates.length,
1836
- candidates: report.candidates,
1837
- report,
1838
- }, text);
1839
- return 0;
1840
- }
1841
-
1842
- function parseContextKinds(value: string | undefined): string[] | undefined {
1843
- if (!value) return undefined;
1844
- return value.split(",").map((kind) => kind.trim()).filter((kind) => kind.length > 0);
1845
- }
1846
-
1847
- async function cmdContextGet(runtime: Runtime, project: string, options: { role: string; task: string; run?: string; stage?: string; budget?: string; kinds?: string }): Promise<number> {
1848
- const budget = options.budget === undefined ? undefined : Number(options.budget);
1849
- if (options.budget !== undefined && (!Number.isInteger(budget) || (budget as number) < 512 || (budget as number) > 200_000)) {
1850
- runtime.io.stderr("context get --budget must be an integer between 512 and 200000\n");
1851
- return 2;
1852
- }
1853
- const body: Record<string, unknown> = {
1854
- project,
1855
- role: options.role,
1856
- task: options.task,
1857
- };
1858
- if (options.run) body.workflowRunId = options.run;
1859
- if (options.stage) body.stageId = options.stage;
1860
- if (budget !== undefined) body.budgetTokens = budget;
1861
- const kinds = parseContextKinds(options.kinds);
1862
- if (kinds) body.includeKinds = kinds;
1863
- const response = await hubContextPost({
1864
- serverUrl: runtime.serverUrl,
1865
- path: "/v1/context/get",
1866
- body,
1867
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1868
- fetchImpl: runtime.fetchImpl,
1869
- });
1870
- 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})`}`);
1871
- return response.ok ? 0 : 1;
1872
- }
1873
-
1874
- async function cmdContextRecall(runtime: Runtime, project: string, options: { query?: string; kinds?: string; limit?: string }): Promise<number> {
1875
- const body: Record<string, unknown> = { project };
1876
- if (options.query) body.query = options.query;
1877
- const kinds = parseContextKinds(options.kinds);
1878
- if (kinds) body.kinds = kinds;
1879
- if (options.limit) body.limit = Number(options.limit);
1880
- const response = await hubContextPost({
1881
- serverUrl: runtime.serverUrl,
1882
- path: "/v1/context/recall",
1883
- body,
1884
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1885
- fetchImpl: runtime.fetchImpl,
1886
- });
1887
- 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})`}`);
1888
- return response.ok ? 0 : 1;
1889
- }
1890
-
1891
- async function cmdContextState(runtime: Runtime, project: string, key: string, options: { asOf?: string }): Promise<number> {
1892
- const body: Record<string, unknown> = { project, key };
1893
- if (options.asOf) body.asOf = options.asOf;
1894
- const response = await hubContextPost({
1895
- serverUrl: runtime.serverUrl,
1896
- path: "/v1/context/state",
1897
- body,
1898
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1899
- fetchImpl: runtime.fetchImpl,
1900
- });
1901
- 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})`}`);
1902
- return response.ok ? 0 : 1;
1903
- }
1904
-
1905
- async function cmdContextEpisode(runtime: Runtime, project: string, options: { run?: string }): Promise<number> {
1906
- const body: Record<string, unknown> = { project };
1907
- if (options.run) body.workflowRunId = options.run;
1908
- const response = await hubContextPost({
1909
- serverUrl: runtime.serverUrl,
1910
- path: "/v1/context/episode",
1911
- body,
1912
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1913
- fetchImpl: runtime.fetchImpl,
1914
- });
1915
- 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})`}`);
1916
- return response.ok ? 0 : 1;
1917
- }
1918
-
1919
- async function cmdContextPromote(runtime: Runtime, project: string, proposalId: string, options: { evidence: string }): Promise<number> {
1920
- const evidence = options.evidence.split(",").map((ref) => ref.trim()).filter((ref) => ref.length > 0);
1921
- if (evidence.length === 0) {
1922
- runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n");
1923
- return 2;
1924
- }
1925
- const response = await hubContextPost({
1926
- serverUrl: runtime.serverUrl,
1927
- path: "/v1/context/state/promote",
1928
- body: { project, proposalId, evidence },
1929
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1930
- fetchImpl: runtime.fetchImpl,
1931
- });
1932
- 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})`}`);
1933
- return response.ok ? 0 : 1;
1934
- }
1935
-
1936
- async function cmdContextExplain(runtime: Runtime, project: string, itemId: string): Promise<number> {
1937
- const response = await hubContextPost({
1938
- serverUrl: runtime.serverUrl,
1939
- path: "/v1/context/explain",
1940
- body: { project, id: itemId },
1941
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1942
- fetchImpl: runtime.fetchImpl,
1943
- });
1944
- 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})`}`);
1945
- return response.ok ? 0 : 1;
1946
- }
1947
-
1948
- async function cmdContextWikiCompile(runtime: Runtime, project: string, options: { out?: string }): Promise<number> {
1949
- const response = await hubContextPost({
1950
- serverUrl: runtime.serverUrl,
1951
- path: "/v1/context/wiki/compile",
1952
- body: { project },
1953
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1954
- fetchImpl: runtime.fetchImpl,
1955
- });
1956
- if (!response.ok) {
1957
- print(runtime.io, runtime.json, { ok: false, command: "context wiki-compile", status: response.status, body: response.body }, `wiki compile failed (${response.status})`);
1958
- return 1;
1959
- }
1960
- const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] };
1961
- let written: string[] = [];
1962
- if (options.out) {
1963
- const pages = new Map(compiled.pages.map((page) => [page.path, page.content]));
1964
- 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: "" } });
1965
- }
1966
- 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)"}`);
1967
- return 0;
1968
- }
1969
-
1970
- async function cmdContextWikiLint(runtime: Runtime, project: string): Promise<number> {
1971
- const response = await hubContextPost({
1972
- serverUrl: runtime.serverUrl,
1973
- path: "/v1/context/wiki/compile",
1974
- body: { project },
1975
- ...(runtime.env.KXM_AUTH_TOKEN?.trim() ? { authToken: runtime.env.KXM_AUTH_TOKEN.trim() } : {}),
1976
- fetchImpl: runtime.fetchImpl,
1977
- });
1978
- if (!response.ok) {
1979
- print(runtime.io, runtime.json, { ok: false, command: "context wiki-lint", status: response.status, body: response.body }, `wiki lint failed (${response.status})`);
1980
- return 1;
1981
- }
1982
- const compiled = response.body as {
1983
- audit: { stateItems: number; contextItems: number; contradictions: number; compiledAt: string };
1984
- lint: { severity: string; rule: string; path: string; message: string }[];
1985
- };
1986
- const issues = compiled.lint;
1987
- 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)`);
1988
- return issues.every((issue) => issue.severity !== "error") ? 0 : 1;
1989
- }
1990
-
1991
- function skillStateFromFlag(value: string): SkillState {
1992
- if (value === "candidate" || value === "promoted" || value === "quarantined" || value === "rejected") return value;
1993
- throw new Error(`invalid skill state ${value}`);
1994
- }
1995
-
1996
- function skillsRoot(runtime: Runtime): string {
1997
- return join(runtime.dirs.workdir, ".kxm", "skills");
1998
- }
1999
-
2000
- function csv(value: string | undefined): string[] | undefined {
2001
- if (!value) return undefined;
2002
- const items = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
2003
- return items.length > 0 ? items : undefined;
2004
- }
2005
-
2006
- 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> {
2007
- if (runtime.dryRun) {
2008
- print(runtime.io, runtime.json, { ok: true, command: "skills create", dryRun: true, name: options.name }, "would create skill candidate");
2009
- return 0;
2010
- }
2011
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2012
- try {
2013
- const metadata = lifecycle.create({
2014
- name: options.name,
2015
- description: options.description ?? "",
2016
- content: readFileSync(options.file, "utf8"),
2017
- createdBy: options.createdBy,
2018
- sources: {
2019
- runIds: csv(options.run) ?? [],
2020
- journalEntryIds: csv(options.journal) ?? [],
2021
- evidenceReceipts: csv(options.receipt) ?? [],
2022
- },
2023
- compatibility: { harness: options.harness, models: csv(options.models) ?? [] },
2024
- ...(options.supersedes ? { supersedes: options.supersedes } : {}),
2025
- });
2026
- print(runtime.io, runtime.json, { ok: true, command: "skills create", metadata }, `created skill candidate ${metadata.id}`);
2027
- return 0;
2028
- } catch (error) {
2029
- const message = error instanceof Error ? error.message : String(error);
2030
- runtime.io.stderr(`skills create failed: ${message}\n`);
2031
- return 1;
2032
- }
2033
- }
2034
-
2035
- async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean; score?: string; details?: string }): Promise<number> {
2036
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2037
- try {
2038
- const outcome = lifecycle.evaluate(skillId, {
2039
- kind: options.kind as SkillEvaluationKind,
2040
- evaluatorVersion: options.evaluator,
2041
- passed: options.fail !== true,
2042
- ...(options.score !== undefined ? { score: Number(options.score) } : {}),
2043
- ...(options.details ? { details: options.details } : {}),
2044
- });
2045
- 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)" : ""}`);
2046
- return 0;
2047
- } catch (error) {
2048
- const message = error instanceof Error ? error.message : String(error);
2049
- runtime.io.stderr(`skills evaluate failed: ${message}\n`);
2050
- return 1;
2051
- }
2052
- }
2053
-
2054
- async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string }): Promise<number> {
2055
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2056
- try {
2057
- const promoted = lifecycle.promote(skillId, {
2058
- decidedBy: options.decidedBy,
2059
- reason: options.reason ?? "passed protected evaluation",
2060
- evidenceRefs: csv(options.evidence) ?? [],
2061
- });
2062
- print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`);
2063
- return 0;
2064
- } catch (error) {
2065
- const message = error instanceof Error ? error.message : String(error);
2066
- runtime.io.stderr(`skills promote failed: ${message}\n`);
2067
- return 1;
2068
- }
2069
- }
2070
-
2071
- async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string }): Promise<number> {
2072
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2073
- try {
2074
- const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" });
2075
- print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`);
2076
- return 0;
2077
- } catch (error) {
2078
- const message = error instanceof Error ? error.message : String(error);
2079
- runtime.io.stderr(`skills reject failed: ${message}\n`);
2080
- return 1;
2081
- }
2082
- }
2083
-
2084
- async function cmdSkillsList(runtime: Runtime, options: { state: string }): Promise<number> {
2085
- try {
2086
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2087
- const state = skillStateFromFlag(options.state);
2088
- const items = lifecycle.list(state).map((metadata) => ({
2089
- id: metadata.id,
2090
- name: metadata.name,
2091
- version: metadata.version,
2092
- createdBy: metadata.createdBy,
2093
- createdAt: metadata.createdAt,
2094
- models: metadata.compatibility.models,
2095
- }));
2096
- print(runtime.io, runtime.json, { ok: true, command: "skills list", state: options.state, skills: items }, `${items.length} ${state} skill(s)`);
2097
- return 0;
2098
- } catch (error) {
2099
- const message = error instanceof Error ? error.message : String(error);
2100
- runtime.io.stderr(`skills list failed: ${message}\n`);
2101
- return 1;
2102
- }
2103
- }
2104
-
2105
- async function cmdSkillsVerify(runtime: Runtime, skillId: string, options: { state: string }): Promise<number> {
2106
- try {
2107
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
2108
- const state = skillStateFromFlag(options.state);
2109
- const metadata = lifecycle.verify(state, skillId);
2110
- print(runtime.io, runtime.json, { ok: true, command: "skills verify", skillId, state: options.state, contentSha256: metadata.contentSha256 }, `skill ${skillId} integrity verified`);
2111
- return 0;
2112
- } catch (error) {
2113
- const message = error instanceof Error ? error.message : String(error);
2114
- runtime.io.stderr(`skills verify failed: ${message}\n`);
2115
- return 1;
2116
- }
2117
- }
2118
-
2119
- async function cmdMemoryBrief(runtime: Runtime): Promise<number> {
2120
- try {
2121
- const brief = generateMemoryBrief(runtime.cwd);
2122
- const text = formatMemoryBriefText(brief);
2123
- print(runtime.io, runtime.json, { ok: true, command: "memory brief", brief }, text);
2124
- return 0;
2125
- } catch (error) {
2126
- const message = error instanceof Error ? error.message : String(error);
2127
- runtime.io.stderr(`memory brief failed: ${message}\n`);
2128
- return 1;
2129
- }
2130
- }
2131
-
2132
- async function cmdMemoryNote(
2133
- runtime: Runtime,
2134
- fact: string,
2135
- options: { scope?: string; kind?: string; body?: string },
2136
- ): Promise<number> {
2137
- try {
2138
- const { record, path } = createMemoryNote(runtime.cwd, fact, {
2139
- scope: (options.scope ?? "project") as MemoryScope,
2140
- ...(options.kind !== undefined ? { kind: options.kind } : {}),
2141
- ...(options.body !== undefined ? { body: options.body } : {}),
2142
- });
2143
- const relPath = relative(runtime.cwd, path);
2144
- print(
2145
- runtime.io,
2146
- runtime.json,
2147
- { ok: true, command: "memory note", candidate: record, path: relPath },
2148
- `Recorded memory candidate ${record.id} in ${relPath} (promoted via PR)`,
2149
- );
2150
- return 0;
2151
- } catch (error) {
2152
- const message = error instanceof Error ? error.message : String(error);
2153
- runtime.io.stderr(`memory note failed: ${message}\n`);
2154
- return 1;
2155
- }
2156
- }
2157
-
2158
- async function cmdMemorySync(runtime: Runtime): Promise<number> {
2159
- try {
2160
- const result = syncHarnessMemory(runtime.cwd);
2161
- print(
2162
- runtime.io,
2163
- runtime.json,
2164
- { ok: true, command: "memory sync", ...result },
2165
- `Synced project memory across AGENTS.md, CLAUDE.md, and GEMINI.md`,
2166
- );
2167
- return 0;
2168
- } catch (error) {
2169
- const message = error instanceof Error ? error.message : String(error);
2170
- runtime.io.stderr(`memory sync failed: ${message}\n`);
2171
- return 1;
2172
- }
2173
- }
2174
-
2175
- async function cmdConfigGet(runtime: Runtime, key: string): Promise<number> {
2176
- try {
2177
- const config = loadKxmConfig(runtime.cwd);
2178
- const value = getKxmConfigValue(config, key);
2179
- print(
2180
- runtime.io,
2181
- runtime.json,
2182
- { ok: true, command: "config get", key, value },
2183
- value !== undefined ? String(value) : "(undefined)",
2184
- );
2185
- return 0;
2186
- } catch (error) {
2187
- const message = error instanceof Error ? error.message : String(error);
2188
- runtime.io.stderr(`config get failed: ${message}\n`);
2189
- return 1;
2190
- }
2191
- }
2192
-
2193
- async function cmdConfigSet(
2194
- runtime: Runtime,
2195
- key: string,
2196
- value: string,
2197
- options: { scope: string },
2198
- ): Promise<number> {
2199
- try {
2200
- const scope = options.scope === "user" ? "user" : "project";
2201
- let parsedVal: unknown = value;
2202
- try {
2203
- parsedVal = JSON.parse(value);
2204
- } catch {
2205
- // keep string
2206
- }
2207
- setKxmConfigValue(runtime.cwd, key, parsedVal, { scope });
2208
- print(
2209
- runtime.io,
2210
- runtime.json,
2211
- { ok: true, command: "config set", key, value: parsedVal, scope },
2212
- `Set ${key} = ${value} in ${scope} config`,
2213
- );
2214
- return 0;
2215
- } catch (error) {
2216
- const message = error instanceof Error ? error.message : String(error);
2217
- runtime.io.stderr(`config set failed: ${message}\n`);
2218
- return 1;
2219
- }
2220
- }
2221
-
2222
- async function cmdConfigList(runtime: Runtime): Promise<number> {
2223
- try {
2224
- const config = loadKxmConfig(runtime.cwd);
2225
- const text = formatKxmConfig(config);
2226
- print(runtime.io, runtime.json, { ok: true, command: "config list", config }, text);
2227
- return 0;
2228
- } catch (error) {
2229
- const message = error instanceof Error ? error.message : String(error);
2230
- runtime.io.stderr(`config list failed: ${message}\n`);
2231
- return 1;
2232
- }
2233
- }
2234
-
2235
- async function cmdCompletion(runtime: Runtime, shell: string): Promise<number> {
2236
- try {
2237
- if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
2238
- runtime.io.stderr(`unsupported shell: ${shell}; must be bash, zsh, or fish\n`);
2239
- return 1;
2240
- }
2241
- const script = generateShellCompletion(shell as SupportedShell);
2242
- runtime.io.stdout(script);
2243
- return 0;
2244
- } catch (error) {
2245
- const message = error instanceof Error ? error.message : String(error);
2246
- runtime.io.stderr(`completion generation failed: ${message}\n`);
2247
- return 1;
2248
- }
2249
- }
2250
-
2251
- async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
2252
- try {
2253
- const prompt = promptParts.join(" ").trim();
2254
- if (!prompt) {
2255
- runtime.io.stderr("prompt must be non-empty\n");
2256
- return 2;
2257
- }
2258
- const inventory = probeHarnesses({ env: runtime.env });
2259
- const availableHarnesses = inventory.harnesses.map((h) => ({
2260
- harness: h.id,
2261
- auth: h.authenticated === true ? "authenticated" : "unauthenticated",
2262
- }));
2263
- const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses });
2264
-
2265
- const text = [
2266
- `Suggested Workflow: ${suggestion.workflowId} (${suggestion.area})`,
2267
- `Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`,
2268
- `Reasons: ${suggestion.reasons.join("; ")}`,
2269
- `Suggested Skills: ${suggestion.suggestedSkills.join(", ") || "none"}`,
2270
- `Roles:`,
2271
- ` Planner: ${suggestion.roles.planner.harness} (${suggestion.roles.planner.model})`,
2272
- ` Writer: ${suggestion.roles.writer.harness} (${suggestion.roles.writer.model})`,
2273
- ` Critics: ${suggestion.roles.critics.map((c) => `${c.harness}:${c.model}`).join(", ")}`,
2274
- ` Verifier: ${suggestion.roles.verifier.command}`,
2275
- ``,
2276
- `Execute with:`,
2277
- ` ${suggestion.suggestedCommand}`,
2278
- ].join("\n");
2279
-
2280
- print(runtime.io, runtime.json, { ok: true, command: "suggest", prompt, ...suggestion }, text);
2281
- return 0;
2282
- } catch (error) {
2283
- const message = error instanceof Error ? error.message : String(error);
2284
- runtime.io.stderr(`suggest failed: ${message}\n`);
2285
- return 1;
2286
- }
2287
- }
2288
-
2289
- async function cmdGoalCreate(
2290
- runtime: Runtime,
2291
- title: string,
2292
- options: { area?: string; metric?: string[]; targetDate?: string },
2293
- ): Promise<number> {
2294
- try {
2295
- const goal = createGoal(runtime.cwd, {
2296
- title,
2297
- area: options.area,
2298
- successMetrics: options.metric,
2299
- targetDate: options.targetDate,
2300
- });
2301
- print(
2302
- runtime.io,
2303
- runtime.json,
2304
- { ok: true, command: "goal create", goal },
2305
- `Created goal ${goal.id}: ${goal.title}`,
2306
- );
2307
- return 0;
2308
- } catch (error) {
2309
- const message = error instanceof Error ? error.message : String(error);
2310
- runtime.io.stderr(`goal create failed: ${message}\n`);
2311
- return 1;
2312
- }
2313
- }
2314
-
2315
- async function cmdGoalList(runtime: Runtime): Promise<number> {
2316
- try {
2317
- const goals = listGoals(runtime.cwd);
2318
- const text = goals.length === 0
2319
- ? "No goals recorded in .kxm/goals/"
2320
- : goals.map((g) => `[${g.status}] ${g.id}: ${g.title} (${g.area})`).join("\n");
2321
- print(runtime.io, runtime.json, { ok: true, command: "goal list", count: goals.length, goals }, text);
2322
- return 0;
2323
- } catch (error) {
2324
- const message = error instanceof Error ? error.message : String(error);
2325
- runtime.io.stderr(`goal list failed: ${message}\n`);
2326
- return 1;
2327
- }
2328
- }
2329
-
2330
- async function cmdTaskCreate(
2331
- runtime: Runtime,
2332
- title: string,
2333
- options: { goal?: string; objective?: string; workflow?: string; tracker?: string; issue?: string },
2334
- ): Promise<number> {
2335
- try {
2336
- const task = createTask(runtime.cwd, {
2337
- title,
2338
- goalId: options.goal,
2339
- objective: options.objective ?? title,
2340
- assignedWorkflow: options.workflow,
2341
- trackerSync: options.tracker && options.issue
2342
- ? { tracker: options.tracker as TrackerType, issueKey: options.issue }
2343
- : undefined,
2344
- });
2345
- print(
2346
- runtime.io,
2347
- runtime.json,
2348
- { ok: true, command: "task create", task },
2349
- `Created task ${task.id}: ${task.title} [${task.status}]`,
2350
- );
2351
- return 0;
2352
- } catch (error) {
2353
- const message = error instanceof Error ? error.message : String(error);
2354
- runtime.io.stderr(`task create failed: ${message}\n`);
2355
- return 1;
2356
- }
2357
- }
2358
-
2359
- async function cmdTaskList(
2360
- runtime: Runtime,
2361
- options: { goal?: string; status?: string },
2362
- ): Promise<number> {
2363
- try {
2364
- const tasks = listTasks(runtime.cwd, {
2365
- goalId: options.goal,
2366
- status: options.status as TaskStatus | undefined,
2367
- });
2368
- const text = tasks.length === 0
2369
- ? "No tasks recorded in .kxm/tasks/"
2370
- : tasks.map((t) => `[${t.status}] ${t.id}: ${t.title}${t.assignedWorkflow ? ` -> ${t.assignedWorkflow}` : ""}`).join("\n");
2371
- print(runtime.io, runtime.json, { ok: true, command: "task list", count: tasks.length, tasks }, text);
2372
- return 0;
2373
- } catch (error) {
2374
- const message = error instanceof Error ? error.message : String(error);
2375
- runtime.io.stderr(`task list failed: ${message}\n`);
2376
- return 1;
2377
- }
2378
- }
2379
-
2380
- async function cmdTaskGet(runtime: Runtime, taskId: string): Promise<number> {
2381
- try {
2382
- const task = getTask(runtime.cwd, taskId);
2383
- if (!task) {
2384
- runtime.io.stderr(`Task ${taskId} not found\n`);
2385
- return 1;
2386
- }
2387
- const text = [
2388
- `Task: ${task.id}`,
2389
- `Title: ${task.title}`,
2390
- `Status: ${task.status}`,
2391
- `Objective: ${task.objective}`,
2392
- task.assignedWorkflow ? `Workflow: ${task.assignedWorkflow}` : "",
2393
- task.workflowRunId ? `Active Run: ${task.workflowRunId}` : "",
2394
- task.trackerSync ? `Tracker: ${task.trackerSync.tracker} (#${task.trackerSync.issueKey}) [${task.trackerSync.syncStatus}]` : "",
2395
- ].filter(Boolean).join("\n");
2396
- print(runtime.io, runtime.json, { ok: true, command: "task get", task }, text);
2397
- return 0;
2398
- } catch (error) {
2399
- const message = error instanceof Error ? error.message : String(error);
2400
- runtime.io.stderr(`task get failed: ${message}\n`);
2401
- return 1;
2402
- }
2403
- }
2404
-
2405
- async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
2406
- try {
2407
- const task = getTask(runtime.cwd, taskId);
2408
- if (!task) {
2409
- runtime.io.stderr(`Task ${taskId} not found\n`);
2410
- return 1;
2411
- }
2412
- const workflow = task.assignedWorkflow ?? "software-engineering/feature-implementation";
2413
- const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
2414
- if (exitCode === 0) {
2415
- updateTaskStatus(runtime.cwd, taskId, "in_progress");
2416
- }
2417
- return exitCode;
2418
- } catch (error) {
2419
- const message = error instanceof Error ? error.message : String(error);
2420
- runtime.io.stderr(`task run failed: ${message}\n`);
2421
- return 1;
2422
- }
2423
- }
2424
-
2425
- async function cmdTaskSync(runtime: Runtime, taskId: string): Promise<number> {
2426
- try {
2427
- const synced = syncTaskWithTracker(runtime.cwd, taskId);
2428
- print(
2429
- runtime.io,
2430
- runtime.json,
2431
- { ok: true, command: "task sync", task: synced },
2432
- `Synced task ${taskId} with ${synced.trackerSync?.tracker} #${synced.trackerSync?.issueKey}`,
2433
- );
2434
- return 0;
2435
- } catch (error) {
2436
- const message = error instanceof Error ? error.message : String(error);
2437
- runtime.io.stderr(`task sync failed: ${message}\n`);
2438
- return 1;
2439
- }
2440
- }
2441
-
2442
- async function cmdStudioLayout(runtime: Runtime, workflowPath?: string): Promise<number> {
2443
- try {
2444
- const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
2445
- let filePath = workflowPath;
2446
- if (!filePath) {
2447
- filePath = resolve(projectRoot, ".kxm", "workflows", "feature-implementation.yaml");
2448
- }
2449
- let yamlContent: string;
2450
- let workflowId = "software-engineering/feature-implementation";
2451
- if (existsSync(filePath)) {
2452
- yamlContent = readFileSync(filePath, "utf8");
2453
- } else {
2454
- yamlContent = `schema: kxm.workflow.v1
2455
- description: Feature implementation workflow
2456
- coordinator: coordinator
2457
- limits:
2458
- maxTransitions: 12
2459
- steps:
2460
- - id: plan
2461
- kind: agent
2462
- agent: planner
2463
- maxAttempts: 2
2464
- on:
2465
- passed: implement
2466
- failed:
2467
- target: $terminal
2468
- terminalStatus: failed
2469
- - id: implement
2470
- kind: agent
2471
- agent: writer
2472
- maxAttempts: 3
2473
- on:
2474
- passed: verify
2475
- failed:
2476
- target: $terminal
2477
- terminalStatus: failed
2478
- - id: verify
2479
- kind: gate
2480
- gate: verify-gate
2481
- expect: pass
2482
- maxAttempts: 2
2483
- on:
2484
- passed:
2485
- target: $terminal
2486
- terminalStatus: completed
2487
- failed:
2488
- target: implement
2489
- maxTransitions: 2
2490
- `;
2491
- }
2492
- const parsedYaml = parseYaml(yamlContent) as any;
2493
- const plan = compileVnextWorkflow({ id: workflowId, value: parsedYaml });
2494
- const layout = generateStudioLayout(plan);
2495
- print(runtime.io, runtime.json, { ok: true, command: "studio layout", layout }, JSON.stringify(layout, null, 2));
2496
- return 0;
2497
- } catch (error) {
2498
- const message = error instanceof Error ? error.message : String(error);
2499
- runtime.io.stderr(`studio layout failed: ${message}\n`);
2500
- return 1;
2501
- }
2502
- }
2503
-
2504
- async function cmdRoutingReport(
2505
- runtime: Runtime,
2506
- options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string },
2507
- ): Promise<number> {
2508
- const file = options.file ?? telemetryPath(runtime.dirs.logs);
2509
- const records = readRoutingRecords(file).map((entry) => entry.routing);
2510
- const includeEquivalentListCost = Boolean(options.equivalentListCost || options.listPrices);
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";
2511
26
 
2512
- let catalog: PriceCatalog | undefined;
2513
- if (includeEquivalentListCost) {
2514
- try {
2515
- const pricesPath = options.prices ? resolve(runtime.cwd, options.prices) : join(runtime.dirs.workspace, "prices.yaml");
2516
- catalog = loadPriceCatalog(pricesPath);
2517
- } catch {
2518
- // price catalog optional / best effort
2519
- }
2520
- }
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";
2521
37
 
2522
- const report = generateRoutingReport(records, { catalog, includeEquivalentListCost });
38
+ import {
39
+ cmdRoleList,
40
+ cmdRoleGet,
41
+ cmdRoleAdd,
42
+ cmdRoleRemove,
43
+ cmdRoleModify,
44
+ cmdRoleHosts,
45
+ cmdRoleSetHost,
46
+ cmdRoleResume,
47
+ } from "./cli/roles.ts";
2523
48
 
2524
- if (records.length === 0) {
2525
- print(runtime.io, runtime.json, { ok: true, command: "routing report", file, configurations: [], report }, "no routing records in telemetry");
2526
- return 0;
2527
- }
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";
2528
61
 
2529
- const v1Records = records.filter((r) => r.schema === "kxm.routing-record.v1") as any[];
2530
- const configurations = v1Records.length > 0
2531
- ? [...groupByBehavior(v1Records).entries()]
2532
- .map(([hash, group]) => ({ ...compareRoutingRecords(group), behavioralSha256: hash }))
2533
- .sort((left, right) => right.runs - left.runs || left.behavioralSha256.localeCompare(right.behavioralSha256))
2534
- : [];
2535
-
2536
- const text = formatRoutingReport(report, { equivalentListCost: includeEquivalentListCost });
2537
- print(
2538
- runtime.io,
2539
- runtime.json,
2540
- { ok: true, command: "routing report", file, configurations, report },
2541
- text,
2542
- );
2543
- return 0;
2544
- }
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";
2545
74
 
2546
- async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }): Promise<number> {
2547
- const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim();
2548
- const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`);
2549
- const event = options.event;
2550
- const payloadFlag = options.payload ?? "{}";
2551
- if (!definitionId) {
2552
- runtime.io.stderr("workflow start requires <definitionId> (or KXM_WORKFLOW_ID)\n");
2553
- return 2;
2554
- }
2555
- let secret: string | undefined;
2556
- try {
2557
- secret = workflowCredential(runtime, definitionId, "start");
2558
- } catch (error) {
2559
- return reportWorkflowConfigError(runtime, error);
2560
- }
2561
- if (!secret) {
2562
- runtime.io.stderr("workflow start requires KXM_WORKFLOW_SECRET when no active definition source is configured\n");
2563
- return 2;
2564
- }
2565
- let payload: Record<string, unknown>;
2566
- try {
2567
- const raw = payloadFlag.startsWith("@") ? readFileSync(resolve(runtime.cwd, payloadFlag.slice(1)), "utf8") : payloadFlag;
2568
- const value = JSON.parse(raw) as unknown;
2569
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("object required");
2570
- payload = value as Record<string, unknown>;
2571
- } catch {
2572
- print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "invalid_payload" }, "workflow payload must be a JSON object or @file");
2573
- return 2;
2574
- }
2575
- if (runtime.dryRun) {
2576
- print(runtime.io, runtime.json, { ok: true, command: "workflow start", dryRun: true, definitionId, deliveryId, event }, "would POST a signed workflow webhook");
2577
- return 0;
2578
- }
2579
- try {
2580
- const response = await postWorkflowStart({ serverUrl: runtime.serverUrl, definitionId, secret, deliveryId, ...(event ? { event } : {}), payload, fetchImpl: runtime.fetchImpl });
2581
- print(runtime.io, runtime.json, { ok: true, command: "workflow start", definitionId, deliveryId, ...response }, `started workflow ${response.runId ?? "accepted"}`);
2582
- return 0;
2583
- } catch {
2584
- print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "workflow_start_failed" }, "signed workflow start failed");
2585
- return 1;
2586
- }
2587
- }
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";
2588
95
 
2589
- async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string; reason?: string }): Promise<number> {
2590
- const requirementKey = options.requirement?.trim() ?? "";
2591
- const reason = options.reason?.trim() ?? "";
2592
- const adminToken = runtime.env.KXM_AUTH_TOKEN?.trim();
2593
- if (!runId || !stageId || !requirementKey || !reason || !adminToken) {
2594
- runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n");
2595
- return 2;
2596
- }
2597
- const worker = gateOf(runtime, "degrade");
2598
- if (runtime.dryRun) {
2599
- printWorker(
2600
- runtime,
2601
- worker,
2602
- { ok: true, command: "workflow degrade", dryRun: true, runId, stageId, requirementKey },
2603
- `would approve configured degraded quorum for ${runId}/${stageId}/${requirementKey}`,
2604
- );
2605
- return 0;
2606
- }
2607
- try {
2608
- const result = await postWorkflowDegradation({
2609
- serverUrl: runtime.serverUrl,
2610
- authToken: adminToken,
2611
- runId,
2612
- stageId,
2613
- requirementKey,
2614
- reason,
2615
- fetchImpl: runtime.fetchImpl,
2616
- });
2617
- printWorker(
2618
- runtime,
2619
- worker,
2620
- { ok: true, command: "workflow degrade", runId, stageId, requirementKey, ...result },
2621
- result.duplicate ? "degraded quorum was already approved" : "approved configured degraded quorum",
2622
- );
2623
- return 0;
2624
- } catch {
2625
- printWorker(
2626
- runtime,
2627
- worker,
2628
- { ok: false, command: "workflow degrade", error: "workflow_degradation_failed", runId, stageId, requirementKey },
2629
- "workflow degradation approval failed",
2630
- );
2631
- return 1;
2632
- }
2633
- }
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";
2634
116
 
2635
- async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runId?: string): Promise<number> {
2636
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
2637
- if (action === "get" && !runId) {
2638
- runtime.io.stderr(`Usage: ${CLI_NAME} workflow get <runId>\n`);
2639
- return 2;
2640
- }
2641
- try {
2642
- const snapshot = localWorkflowSnapshot(dataPath, runId);
2643
- if (runId && snapshot.runs.length === 0) {
2644
- print(runtime.io, runtime.json, { ok: false, command: "workflow get", error: "workflow_not_found" }, "workflow not found");
2645
- return 1;
2646
- }
2647
- const value = action === "get"
2648
- ? { ok: true, command: "workflow get", run: snapshot.runs[0], journal: snapshot.journal }
2649
- : { ok: true, command: "workflow list", runs: snapshot.runs };
2650
- print(runtime.io, runtime.json, value, action === "get" ? `workflow ${runId}` : `${snapshot.runs.length} workflow(s)`);
2651
- return 0;
2652
- } catch {
2653
- print(runtime.io, runtime.json, { ok: false, command: `workflow ${action}`, error: "state_unavailable" }, "local workflow state is unavailable");
2654
- return 1;
2655
- }
2656
- }
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";
2657
130
 
2658
- async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string): Promise<number> {
2659
- if (!runId || !signalKey || !status || !summary) {
2660
- runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`);
2661
- return 2;
2662
- }
2663
- if (status !== "passed" && status !== "warning" && status !== "failed") {
2664
- runtime.io.stderr("status must be passed, warning, or failed\n");
2665
- return 2;
2666
- }
2667
- let evidence: WorkflowEvidenceInput;
2668
- try {
2669
- evidence = parseEvidencePairs(evidenceArgs);
2670
- } catch (error) {
2671
- runtime.io.stderr(`${error instanceof Error ? error.message : "invalid evidence"}\n`);
2672
- return 2;
2673
- }
2674
- const worker = gateOf(runtime, "signal");
2675
- const projectRoot = discoverVnextProjectRoot(runtime.cwd);
2676
- if (projectRoot && /^run_[a-f0-9]{32}$/i.test(runId)) {
2677
- if (runtime.dryRun) {
2678
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signal to vNext run");
2679
- return 0;
2680
- }
2681
- const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
2682
- try {
2683
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
2684
- const posted = await vnextRuntimeRequest(
2685
- supervisor,
2686
- "POST",
2687
- `/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
2688
- { signalKey, status, summary, evidence, deliveryId },
2689
- );
2690
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to vNext run");
2691
- return 0;
2692
- } catch (error) {
2693
- const msg = error instanceof Error ? error.message : "signal_failed";
2694
- printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", detail: msg, deliveryId }, "signal to vNext run failed");
2695
- return 1;
2696
- }
2697
- }
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 };
2698
155
 
2699
- const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
2700
- if (!definitionId) {
2701
- runtime.io.stderr("signal requires KXM_WORKFLOW_ID\n");
2702
- return 2;
2703
- }
2704
- let signalSecret: string | undefined;
2705
- try {
2706
- signalSecret = workflowCredential(runtime, definitionId, "signal");
2707
- } catch (error) {
2708
- return reportWorkflowConfigError(runtime, error);
2709
- }
2710
- if (!signalSecret) {
2711
- runtime.io.stderr("signal requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
2712
- return 2;
2713
- }
2714
- if (runtime.dryRun) {
2715
- printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, summary, evidence }, "would post signed signal");
2716
- return 0;
2717
- }
2718
- const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`);
2719
- try {
2720
- const posted = await postWorkflowSignal({
2721
- serverUrl: runtime.serverUrl,
2722
- definitionId,
2723
- signalSecret,
2724
- runId,
2725
- signalKey,
2726
- status,
2727
- summary,
2728
- evidence,
2729
- deliveryId,
2730
- fetchImpl: runtime.fetchImpl,
2731
- });
2732
- printWorker(runtime, worker, { ok: true, command: "signal", duplicate: posted.duplicate, deliveryId }, "posted signed signal");
2733
- return 0;
2734
- } catch {
2735
- printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", deliveryId }, "signed signal failed");
2736
- return 1;
2737
- }
2738
- }
156
+ const CLI_NAME = "kxm";
157
+ const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
2739
158
 
2740
- async function cmdGithubWatch(runtime: Runtime, options: {
2741
- runId?: string;
2742
- stageId?: string;
2743
- signalKey?: string;
2744
- repo?: string;
2745
- pr?: string;
2746
- required?: string;
2747
- timeoutMs?: string;
2748
- intervalMs?: string;
2749
- deliveryId?: string;
2750
- }): Promise<number> {
2751
- const token = runtime.env.GITHUB_TOKEN?.trim() || runtime.env.GH_TOKEN?.trim();
2752
- const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim();
2753
- const runId = String(options.runId || "");
2754
- const stageId = String(options.stageId || "");
2755
- const signalKey = String(options.signalKey || "");
2756
- const repo = String(options.repo || "");
2757
- const pr = Number(options.pr);
2758
- if (!definitionId || !runId || !stageId || !signalKey || !repo || !Number.isInteger(pr)) {
2759
- runtime.io.stderr("github watch requires KXM_WORKFLOW_ID, --run-id, --stage-id, --signal-key, --repo, --pr\n");
2760
- return 2;
2761
- }
2762
- let signalSecret: string | undefined;
2763
- try {
2764
- signalSecret = workflowCredential(runtime, definitionId, "signal");
2765
- } catch (error) {
2766
- return reportWorkflowConfigError(runtime, error);
2767
- }
2768
- if (!signalSecret) {
2769
- runtime.io.stderr("github watch requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n");
2770
- return 2;
2771
- }
2772
- const result = await watchGithubChecks({
2773
- serverUrl: runtime.serverUrl,
2774
- definitionId,
2775
- signalSecret,
2776
- runId,
2777
- stageId,
2778
- signalKey,
2779
- repo,
2780
- pr,
2781
- required: options.required
2782
- ? [...new Set(String(options.required).split(",").map((name) => name.trim()).filter(Boolean))]
2783
- : [],
2784
- timeoutMs: Number(options.timeoutMs || 1_800_000),
2785
- intervalMs: Number(options.intervalMs || 15_000),
2786
- ...(options.deliveryId ? { deliveryId: options.deliveryId } : {}),
2787
- ...(token ? { token } : {}),
2788
- dryRun: runtime.dryRun,
2789
- fetchImpl: runtime.fetchImpl,
2790
- ...(runtime.io.now ? { now: runtime.io.now } : {}),
2791
- ...(runtime.io.sleep ? { sleep: runtime.io.sleep } : {}),
2792
- });
2793
- const worker = gateOf(runtime, "github-watch");
2794
- const payload = {
2795
- ok: result.exitCode === 0,
2796
- command: "github watch",
2797
- posted: result.posted,
2798
- status: result.status,
2799
- summary: result.summary,
2800
- evidence: result.evidence,
2801
- deliveryId: result.deliveryId,
2802
- skipped: result.skipped,
2803
- };
2804
- const outcome: WorkerOutcome = result.exitCode === 0
2805
- ? (result.status === "warning" ? "warning" : "passed")
2806
- : "failed";
2807
- if (JSON.stringify(payload).includes(token ?? "___never___") || Object.keys(runtime.env).some((key) => maskEnvName(key) && JSON.stringify(payload).includes(String(runtime.env[key])))) {
2808
- printWorker(runtime, worker, { ok: false, command: "github watch", error: "redaction_failure" }, "refusing to print a payload that contains a secret");
2809
- return 1;
2810
- }
2811
- printWorker(runtime, worker, payload, result.summary, outcome);
2812
- return result.exitCode;
2813
- }
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
+ ]);
2814
172
 
2815
- async function cmdRetrospectiveExport(runtime: Runtime, runId: string, options: { input?: string; outDir?: string }): Promise<number> {
2816
- if (!runId) {
2817
- runtime.io.stderr(`Usage: ${CLI_NAME} workflow export <runId>\n`);
2818
- return 2;
2819
- }
2820
- const snapshotFlag = String(options.input || "");
2821
- const snapshotPath = snapshotFlag ? resolve(runtime.cwd, snapshotFlag) : "";
2822
- let snapshot: { run: WorkflowRun; journal: WorkflowJournalEntry[] };
2823
- try {
2824
- if (snapshotPath) {
2825
- if (!existsSync(snapshotPath)) throw new Error("snapshot_missing");
2826
- snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as typeof snapshot;
2827
- } else {
2828
- const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db"));
2829
- const local = localWorkflowSnapshot(dataPath, runId);
2830
- if (!local.runs[0]) throw new Error("workflow_not_found");
2831
- snapshot = { run: local.runs[0], journal: local.journal };
2832
- }
2833
- if (snapshot.run.id !== runId) throw new Error("run_id_mismatch");
2834
- } catch (error) {
2835
- const reason = error instanceof Error ? error.message : "snapshot_invalid";
2836
- print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: reason }, "retrospective source is invalid or unavailable");
2837
- return 1;
2838
- }
2839
- const doc = buildRetrospective(snapshot.run, snapshot.journal);
2840
- const outDir = resolve(runtime.cwd, String(options.outDir || join(runtime.dirs.assets, "retrospectives")));
2841
- const assetsRoot = resolve(runtime.dirs.assets);
2842
- const assetsPrefix = `${assetsRoot}${process.platform === "win32" ? "\\" : "/"}`;
2843
- if (outDir !== assetsRoot && !outDir.startsWith(assetsPrefix)) {
2844
- print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: "output_outside_workspace_assets" }, "retrospectives must stay under the workspace assets directory");
2845
- return 2;
2846
- }
2847
- if (runtime.dryRun) {
2848
- print(runtime.io, runtime.json, { ok: true, command: "retrospective export", dryRun: true, runId: doc.runId }, `would export ${doc.runId}`);
2849
- return 0;
2850
- }
2851
- const written = writeRetrospective(outDir, doc);
2852
- print(runtime.io, runtime.json, { ok: true, command: "retrospective export", ...written, reviewDecision: doc.reviewDecision }, `exported ${written.jsonPath}`);
2853
- 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");
2854
178
  }
2855
179
 
2856
180
  async function ensureCliClient(runtime: Runtime): Promise<HubClient> {
@@ -3012,10 +336,14 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3012
336
  .option("--project-id <id>", "Stable project ID for controlled provisioning")
3013
337
  .option("--repository <id=absolute-path>", "Bind a member repository outside Git configuration", (value, previous: string[]) => [...previous, value], [])
3014
338
  .action(async function initAction(this: Command, options: { name?: string; projectId?: string; repository?: string[] }) {
3015
- result.code = await cmdVnextInit(runtimeFrom(ctx, this), options);
339
+ result.code = await cmdVnextInit(runtimeFrom(ctx, this), options, {
340
+ maybeOfferCompletionInstall,
341
+ maybeOfferGuideSetup,
342
+ });
3016
343
  });
3017
344
 
3018
- 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");
3019
347
  addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-vNext migration plan without writes"))
3020
348
  .action(async function migratePlanAction(this: Command) {
3021
349
  result.code = await cmdVnextMigratePlan(runtimeFrom(ctx, this));
@@ -3056,6 +384,12 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3056
384
  .action(async function runStatusAction(this: Command, runId: string) {
3057
385
  result.code = await cmdVnextRunStatus(runtimeFrom(ctx, this), runId);
3058
386
  });
387
+ addGlobalOptions(runCmd.command("drive").description("Drive a run with an explicit model-free simulation"))
388
+ .argument("<runId>", "Run id")
389
+ .option("--simulated", "Use the model-free simulation producer")
390
+ .action(async function runDriveAction(this: Command, runId: string, options: { simulated?: boolean }) {
391
+ result.code = await cmdVnextRunDrive(runtimeFrom(ctx, this), runId, options.simulated === true);
392
+ });
3059
393
  addGlobalOptions(runCmd.command("cancel").description("Durably request cancellation of a run"))
3060
394
  .argument("<runId>", "Run id")
3061
395
  .action(async function runCancelAction(this: Command, runId: string) {
@@ -3066,9 +400,34 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3066
400
  result.code = await cmdVnextRunList(runtimeFrom(ctx, this));
3067
401
  });
3068
402
 
403
+ const modelsCmd = addGlobalOptions(program.command("models").description("Manage model catalogs, roles, and producer state"));
404
+ modelsCmd.action(async function modelsScreenAction(this: Command) { result.code = await cmdModelsScreen(runtimeFrom(ctx, this)); });
405
+ modelsCmd.helpCommand("help", "Show models help");
406
+ addGlobalOptions(modelsCmd.command("inventory-refresh").alias("refresh").description("Refresh the YAML model inventory with standard and Nous/OpenRouter prices"))
407
+ .action(async function modelInventoryRefreshAction(this: Command) {
408
+ result.code = await cmdModelInventoryRefresh(runtimeFrom(ctx, this));
409
+ });
410
+
411
+ const producersCmd = addGlobalOptions(program.command("producers").description("Promote or demote verified producer models"));
412
+ producersCmd.helpCommand("help", "Show producers help");
413
+ addGlobalOptions(producersCmd.command("list").description("List producer decisions")).action(async function producersListAction(this: Command) { result.code = await cmdProducerList(runtimeFrom(ctx, this)); });
414
+ for (const status of ["promote", "demote"] as const) {
415
+ addGlobalOptions(producersCmd.command(status).description(`${status} a model from the inventory`)).option("--model <id>", "Exact model id; omit to choose interactively").action(async function producerChangeAction(this: Command, options: { model?: string }) { result.code = await cmdProducerChange(runtimeFrom(ctx, this), status === "promote" ? "promoted" : "demoted", options.model); });
416
+ }
417
+
3069
418
  const harnessCmd = addGlobalOptions(program.command("harness").description("Detect coding-agent harnesses and authentication"));
3070
419
  harnessCmd.helpCommand("help", "Show harness help");
3071
420
  addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
421
+
422
+ const authCmd = addGlobalOptions(program.command("auth").description("Manage credentials, tokens, and authorization"));
423
+ authCmd.helpCommand("help", "Show auth help");
424
+ addGlobalOptions(authCmd.command("token").description("Inspect, issue, or clear local disk session tokens"))
425
+ .option("--status", "Check status of the active session token")
426
+ .option("--clear", "Clear persisted disk session token")
427
+ .option("--issue", "Force issuing a fresh session token")
428
+ .action(async function authTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
429
+ result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
430
+ });
3072
431
  addGlobalOptions(program.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs")
3073
432
  .argument("[harness]", "Harness id (default: every detected harness)")
3074
433
  .option("--check", "Check for a kxm package update without applying")
@@ -3147,6 +506,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3147
506
  .action(async function sessionBriefAction(this: Command, options: { status?: boolean; token?: boolean }) {
3148
507
  result.code = await cmdSessionBrief(runtimeFrom(ctx, this), options);
3149
508
  });
509
+ addGlobalOptions(session.command("token").description("Inspect, issue, or clear local disk session tokens"))
510
+ .option("--status", "Check status of the active session token")
511
+ .option("--clear", "Clear persisted disk session token")
512
+ .option("--issue", "Force issuing a fresh session token")
513
+ .action(async function sessionTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
514
+ result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
515
+ });
3150
516
  addGlobalOptions(session.command("start").description("Create an agent/gate or workflow session manifest (does not launch processes)"))
3151
517
  .option("--id <id>", "Session id")
3152
518
  .option("--workflow <id>", "Workflow definition id")
@@ -3328,6 +694,91 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3328
694
  .action(async function exportAction(this: Command, runId: string, options: { input?: string; outDir?: string }) {
3329
695
  result.code = await cmdRetrospectiveExport(runtimeFrom(ctx, this), runId, options);
3330
696
  });
697
+ addGlobalOptions(workflow.command("definitions").description("List workflow definitions across scopes"))
698
+ .option("--scope <scope>", "Filter by scope: all, global, or local", "all")
699
+ .action(async function definitionsAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
700
+ result.code = await cmdWorkflowDefinitions(runtimeFrom(ctx, this), options);
701
+ });
702
+ addGlobalOptions(workflow.command("add [workflowId]").description("Add a workflow definition to global or local configuration"))
703
+ .option("--file <path>", "Path to YAML workflow definition file")
704
+ .option("--description <text>", "Workflow description")
705
+ .option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
706
+ .option("--overwrite", "Overwrite existing workflow definition if present")
707
+ .option("--pick [selection]", "Pick from available workflow templates (index or id)")
708
+ .action(async function workflowAddAction(this: Command, workflowId?: string, options?: { file?: string; description?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
709
+ result.code = await cmdWorkflowAdd(runtimeFrom(ctx, this), workflowId, options ?? {});
710
+ });
711
+ addGlobalOptions(workflow.command("remove [workflowId]").description("Remove a workflow definition"))
712
+ .option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
713
+ .option("--pick [selection]", "Pick a workflow to remove (index or id)")
714
+ .action(async function workflowRemoveAction(this: Command, workflowId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
715
+ result.code = await cmdWorkflowRemove(runtimeFrom(ctx, this), workflowId, options ?? {});
716
+ });
717
+ addGlobalOptions(workflow.command("modify [workflowId]").description("Modify a workflow definition"))
718
+ .option("--description <text>", "Updated description")
719
+ .option("--scope <scope>", "Configuration scope: global or local")
720
+ .option("--pick [selection]", "Pick a workflow to modify (index or id)")
721
+ .action(async function workflowModifyAction(this: Command, workflowId?: string, options?: { description?: string; scope?: "global" | "local"; pick?: string | boolean }) {
722
+ result.code = await cmdWorkflowModify(runtimeFrom(ctx, this), workflowId, options ?? {});
723
+ });
724
+
725
+ const role = addGlobalOptions(program.command("role").description("Manage role definitions, tool policies, and rosters"));
726
+ role.helpCommand("help", "Show role help");
727
+ addGlobalOptions(role.command("list", { isDefault: true }).description("List configured roles across global and local scopes"))
728
+ .option("--scope <scope>", "Filter by scope: all, global, or local", "all")
729
+ .action(async function roleListAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
730
+ result.code = await cmdRoleList(runtimeFrom(ctx, this), options);
731
+ });
732
+ addGlobalOptions(role.command("get <roleId>").description("Get role definition YAML and details"))
733
+ .option("--scope <scope>", "Filter by scope: all, global, or local", "all")
734
+ .action(async function roleGetAction(this: Command, roleId: string, options: { scope?: "all" | "global" | "local" }) {
735
+ result.code = await cmdRoleGet(runtimeFrom(ctx, this), roleId, options);
736
+ });
737
+ addGlobalOptions(role.command("add [roleId]").description("Add a role definition to global or local configuration"))
738
+ .option("--file <path>", "Path to YAML role definition file")
739
+ .option("--description <text>", "Role description")
740
+ .option("--skills <skills>", "Comma-separated skills list")
741
+ .option("--harness <harness>", "Primary harness name (e.g. grok, claude, agy, pi)")
742
+ .option("--model <model>", "Primary model identifier (e.g. grok-4.6, fable, gemini-3.8-flash-high)")
743
+ .option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
744
+ .option("--overwrite", "Overwrite existing role definition if present")
745
+ .option("--pick [selection]", "Pick from available role templates (index or id)")
746
+ .action(async function roleAddAction(this: Command, roleId?: string, options?: { file?: string; description?: string; skills?: string; harness?: string; model?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
747
+ result.code = await cmdRoleAdd(runtimeFrom(ctx, this), roleId, options ?? {});
748
+ });
749
+ addGlobalOptions(role.command("remove [roleId]").description("Remove a role definition"))
750
+ .option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
751
+ .option("--pick [selection]", "Pick a role to remove (index or id)")
752
+ .action(async function roleRemoveAction(this: Command, roleId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
753
+ result.code = await cmdRoleRemove(runtimeFrom(ctx, this), roleId, options ?? {});
754
+ });
755
+ addGlobalOptions(role.command("modify [roleId]").description("Modify an existing role definition"))
756
+ .option("--description <text>", "Updated description")
757
+ .option("--add-skill <skill>", "Skill to add")
758
+ .option("--remove-skill <skill>", "Skill to remove")
759
+ .option("--add-model <harness:model>", "Model to add to roster")
760
+ .option("--remove-model <model>", "Model to remove from roster")
761
+ .option("--scope <scope>", "Configuration scope: global or local")
762
+ .option("--pick [selection]", "Pick a role to modify (index or id)")
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 }) {
764
+ result.code = await cmdRoleModify(runtimeFrom(ctx, this), roleId, options ?? {});
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
+ });
3331
782
 
3332
783
  const gate = addGlobalOptions(program.command("gate").description("Validate definitions and operate evidence gates"));
3333
784
  gate.helpCommand("help", "Show gate help");
@@ -3356,8 +807,9 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3356
807
  .argument("<summary>", "Callback summary")
3357
808
  .argument("[evidence...]", "required-key=evidence pairs")
3358
809
  .option("--delivery-id <id>", "Stable callback delivery ID")
3359
- .action(async function signalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string }) {
3360
- result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId);
810
+ .option("--recovery-action <action>", "vNext recovery action: retry, fail, cancel, unblock")
811
+ .action(async function signalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string; recoveryAction?: string }) {
812
+ result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId, options.recoveryAction);
3361
813
  });
3362
814
  const github = addGlobalOptions(gate.command("github").description("GitHub adapters"));
3363
815
  github.helpCommand("help", "Show GitHub help");
@@ -3537,6 +989,48 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3537
989
  .action(async function routingReportAction(this: Command, options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string }) {
3538
990
  result.code = await cmdRoutingReport(runtimeFrom(ctx, this), options);
3539
991
  });
992
+ addGlobalOptions(routing.command("benchmark").description("Dedicated offline benchmark for side-by-side model comparison (Decision Q12)"))
993
+ .option("--task <fixture>", "Task prompt or fixture path for benchmark comparison")
994
+ .option("--arms <models>", "Comma-separated model routes to benchmark (e.g. grok/grok-4.6,claude/fable)")
995
+ .option("--runs <count>", "Benchmark runs per arm", "1")
996
+ .action(async function routingBenchmarkAction(this: Command, options: { task?: string; arms?: string; runs?: string }) {
997
+ result.code = await cmdRoutingBenchmark(runtimeFrom(ctx, this), options);
998
+ });
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
+ });
3540
1034
 
3541
1035
  const hub = addGlobalOptions(program.command("hub").description("Start, inspect, and stop the local KXM hub"));
3542
1036
  hub.helpCommand("help", "Show hub help");
@@ -3574,9 +1068,24 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3574
1068
  result.code = await cmdConfigList(runtimeFrom(ctx, this));
3575
1069
  });
3576
1070
 
3577
- addGlobalOptions(program.command("completion <shell>").description("Generate shell completion script (bash, zsh, fish)"))
3578
- .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") {
3579
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);
3580
1089
  });
3581
1090
 
3582
1091
  addGlobalOptions(program.command("suggest <prompt...>").description("Recommend workflow, area, roles, and skills from a prompt or issue description"))
@@ -3634,6 +1143,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
3634
1143
  .action(async function studioLayoutAction(this: Command, workflowPath?: string) {
3635
1144
  result.code = await cmdStudioLayout(runtimeFrom(ctx, this), workflowPath);
3636
1145
  });
1146
+ addGlobalOptions(studioCmd.command("serve").description("Start embedded Web Studio server on http://localhost:4242 (Decision Q8 & D14)"))
1147
+ .option("-p, --port <port>", "Port to bind (default: 4242)", "4242")
1148
+ .option("--host <host>", "Host address to bind", "127.0.0.1")
1149
+ .option("--token <token>", "Session token for mutation authentication")
1150
+ .action(async function studioServeAction(this: Command, options: { port?: string; host?: string; token?: string }) {
1151
+ result.code = await cmdStudioServe(runtimeFrom(ctx, this), options);
1152
+ });
3637
1153
 
3638
1154
  return program;
3639
1155
  }
@@ -3646,9 +1162,6 @@ function mapCommanderError(error: CommanderError): number {
3646
1162
 
3647
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).";
3648
1164
 
3649
- /** Detect `mesh` as the first command token after recognized nonterminal global
3650
- * options so `kxm --json mesh` fails closed. `--version`, `--help`, and unknown
3651
- * options are left for Commander. */
3652
1165
  function removedMeshInvocation(argv: string[]): { invoked: boolean; json: boolean } {
3653
1166
  let json = false;
3654
1167
  let invoked = false;