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