@kontextmind/kxm 0.6.0 → 0.7.0
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/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-hub.mjs +6 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync,
|
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { Command, CommanderError } from "commander";
|
|
9
10
|
import {
|
|
@@ -25,6 +26,8 @@ import { agentWorker, gateWorker, workerResult, type Worker, type WorkerOutcome
|
|
|
25
26
|
import { appendTelemetry, inferImprovementTarget, makeTelemetryEvent, readTelemetry, readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
26
27
|
import { behavioralConfigHash, compareRoutingRecords, groupByBehavior, generateRoutingReport, formatRoutingReport } from "./routing.ts";
|
|
27
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";
|
|
28
31
|
import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "./session.ts";
|
|
29
32
|
import { buildImprovementReport, formatImprovementReport, writeImprovementReport } from "./improve.ts";
|
|
30
33
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "./tui.ts";
|
|
@@ -84,7 +87,16 @@ import {
|
|
|
84
87
|
} from "./vnext-harness.ts";
|
|
85
88
|
import type { WorkflowEvidenceInput, WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
86
89
|
import { HubClient } from "./client.ts";
|
|
87
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
AGENT_COMMANDS_MAP,
|
|
92
|
+
clearSessionTokenFromDisk,
|
|
93
|
+
enforceToolPolicy,
|
|
94
|
+
mintSessionToken,
|
|
95
|
+
parseSessionToken,
|
|
96
|
+
persistSessionTokenToDisk,
|
|
97
|
+
readSessionTokenFromDisk,
|
|
98
|
+
sessionTokenPath,
|
|
99
|
+
} from "./commands.ts";
|
|
88
100
|
import {
|
|
89
101
|
loadKxmConfig,
|
|
90
102
|
setKxmConfigValue,
|
|
@@ -104,9 +116,26 @@ import {
|
|
|
104
116
|
type TaskStatus,
|
|
105
117
|
type TrackerType,
|
|
106
118
|
} from "./task-manager.ts";
|
|
107
|
-
import { parse as parseYaml } from "yaml";
|
|
119
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
108
120
|
import { compileVnextWorkflow } from "./vnext-engine-compile.ts";
|
|
109
|
-
import { generateStudioLayout } from "./studio-layout.ts";
|
|
121
|
+
import { generateStudioLayout, createStudioServer, DEFAULT_STUDIO_PORT } from "./studio-layout.ts";
|
|
122
|
+
import {
|
|
123
|
+
listRoles,
|
|
124
|
+
getRole,
|
|
125
|
+
addRole,
|
|
126
|
+
removeRole,
|
|
127
|
+
modifyRole,
|
|
128
|
+
DEFAULT_ROLES,
|
|
129
|
+
type KxmRoleDefinition,
|
|
130
|
+
} from "./role.ts";
|
|
131
|
+
import {
|
|
132
|
+
listWorkflowDefinitions,
|
|
133
|
+
addWorkflowDefinition,
|
|
134
|
+
removeWorkflowDefinition,
|
|
135
|
+
modifyWorkflowDefinition,
|
|
136
|
+
WORKFLOW_TEMPLATES,
|
|
137
|
+
DEFAULT_WORKFLOW_TEMPLATE,
|
|
138
|
+
} from "./workflow-manager.ts";
|
|
110
139
|
|
|
111
140
|
export interface CliSpawnResult {
|
|
112
141
|
status: number | null;
|
|
@@ -961,6 +990,32 @@ async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<numbe
|
|
|
961
990
|
}
|
|
962
991
|
}
|
|
963
992
|
|
|
993
|
+
async function cmdVnextRunDrive(runtime: Runtime, runId: string, simulated: boolean): Promise<number> {
|
|
994
|
+
try {
|
|
995
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
996
|
+
if (!projectRoot) {
|
|
997
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "project_required" }, "kxm runs drive requires a vNext project (run kxm init first)");
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
1000
|
+
const mode = simulated ? "simulated" : "live";
|
|
1001
|
+
if (runtime.dryRun) {
|
|
1002
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs drive", dryRun: true, runId, mode }, `drive plan: run ${runId} in ${mode} mode (no events written)`);
|
|
1003
|
+
return 0;
|
|
1004
|
+
}
|
|
1005
|
+
const supervisor = await ensureVnextSupervisor({ env: runtime.env });
|
|
1006
|
+
const result = await vnextRuntimeRequest(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/drive?projectRoot=${encodeURIComponent(projectRoot)}`, { mode });
|
|
1007
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs drive", run: result.run, handoff: result.handoff, events: result.events }, `run ${runId}: ${(result.run as { status?: string } | undefined)?.status ?? "driven"}`);
|
|
1008
|
+
return 0;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (error instanceof VnextConfigError) {
|
|
1011
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_failed", issues: error.issues }, `run drive failed: ${error.message}`);
|
|
1012
|
+
return 1;
|
|
1013
|
+
}
|
|
1014
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_io_failed" }, "run drive failed because a local operation did not complete");
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
964
1019
|
async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
|
|
965
1020
|
try {
|
|
966
1021
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
@@ -1130,6 +1185,66 @@ function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice): { ok:
|
|
|
1130
1185
|
}
|
|
1131
1186
|
}
|
|
1132
1187
|
|
|
1188
|
+
async function selectProducerModel(runtime: Runtime, requested?: string): Promise<string | undefined> {
|
|
1189
|
+
const models = listInventoryModels(runtime.dirs.workdir);
|
|
1190
|
+
if (requested) return models.find((model) => model.toLowerCase() === requested.toLowerCase());
|
|
1191
|
+
if (runtime.json || !process.stdin.isTTY || models.length === 0) return undefined;
|
|
1192
|
+
runtime.io.stdout(models.slice(0, 100).map((model, index) => `${index + 1}. ${model}`).join("\n") + "\nSelect model number: ");
|
|
1193
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1194
|
+
const answer = await new Promise<string>((resolve) => rl.once("line", resolve));
|
|
1195
|
+
rl.close();
|
|
1196
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
1197
|
+
return Number.isInteger(index) && index >= 0 && index < models.length ? models[index] : undefined;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function cmdProducerChange(runtime: Runtime, status: "promoted" | "demoted", requested?: string): Promise<number> {
|
|
1201
|
+
const model = await selectProducerModel(runtime, requested);
|
|
1202
|
+
if (!model) { print(runtime.io, runtime.json, { ok: false, command: `producers ${status}`, error: "model_selection_required" }, "select a model from the refreshed inventory"); return 2; }
|
|
1203
|
+
if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, dryRun: true }, `would ${status} ${model}`); return 0; }
|
|
1204
|
+
const policy = updateProducer(runtime.dirs.workdir, model, status);
|
|
1205
|
+
print(runtime.io, runtime.json, { ok: true, command: `producers ${status}`, model, policy }, `${status} ${model}`);
|
|
1206
|
+
return 0;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
async function cmdModelsScreen(runtime: Runtime): Promise<number> {
|
|
1210
|
+
const models = listInventoryModels(runtime.dirs.workdir);
|
|
1211
|
+
if (runtime.json || !process.stdin.isTTY) { print(runtime.io, runtime.json, { ok: false, command: "models", error: "interactive_tty_required" }, "kxm models requires an interactive terminal"); return 2; }
|
|
1212
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1213
|
+
const ask = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
|
|
1214
|
+
try {
|
|
1215
|
+
while (true) {
|
|
1216
|
+
const policy = loadProducerPolicy(runtime.dirs.workdir);
|
|
1217
|
+
const roleBindings = listRoleBindings(runtime.dirs.workdir);
|
|
1218
|
+
runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.enabled.includes(m) ? "enabled" : policy.disabled.includes(m) ? "disabled" : "unset"}] [${policy.promoted.includes(m) ? "producer" : policy.demoted.includes(m) ? "demoted" : "not producer"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
|
|
1219
|
+
const command = (await ask("[e]nable [d]isable [p]romote [x]demote [a]dd-role [r]emove-role [q]uit: ")).trim().toLowerCase();
|
|
1220
|
+
if (command === "q" || command === "quit") return 0;
|
|
1221
|
+
const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
|
|
1222
|
+
if (!Number.isInteger(index) || !models[index]) continue;
|
|
1223
|
+
const model = models[index];
|
|
1224
|
+
if (command === "p" || command === "x") updateProducer(runtime.dirs.workdir, model, command === "p" ? "promoted" : "demoted");
|
|
1225
|
+
else if (command === "e" || command === "d") setModelState(runtime.dirs.workdir, model, command === "e" ? "enabled" : "disabled");
|
|
1226
|
+
else if (command === "a" || command === "r") setModelState(runtime.dirs.workdir, model, "enabled", (await ask("role: ")).trim(), command === "r");
|
|
1227
|
+
}
|
|
1228
|
+
} finally { rl.close(); }
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
async function cmdProducerList(runtime: Runtime): Promise<number> {
|
|
1232
|
+
const policy = loadProducerPolicy(runtime.dirs.workdir);
|
|
1233
|
+
print(runtime.io, runtime.json, { ok: true, command: "producers list", policy }, [...policy.promoted.map((x) => `promoted ${x}`), ...policy.demoted.map((x) => `demoted ${x}`)].join("\n") || "no producer decisions");
|
|
1234
|
+
return 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function cmdModelInventoryRefresh(runtime: Runtime): Promise<number> {
|
|
1238
|
+
if (runtime.dryRun) {
|
|
1239
|
+
print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
|
|
1240
|
+
return 0;
|
|
1241
|
+
}
|
|
1242
|
+
const inventory = await refreshModelInventory({ outputRoot: runtime.dirs.workdir, env: runtime.env });
|
|
1243
|
+
const failed = Object.values(inventory.sources).some((source) => !source.ok);
|
|
1244
|
+
print(runtime.io, runtime.json, { ok: !failed, command: "models inventory refresh", output: ".kxm/models/inventory.yaml", ...inventory }, `wrote ${inventory.models.length} models to .kxm/models/inventory.yaml`);
|
|
1245
|
+
return failed ? 1 : 0;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1133
1248
|
async function cmdUpdate(runtime: Runtime, harness: string | undefined, options: {
|
|
1134
1249
|
self?: boolean;
|
|
1135
1250
|
extensions?: boolean;
|
|
@@ -1706,8 +1821,90 @@ async function cmdSessionStatus(runtime: Runtime): Promise<number> {
|
|
|
1706
1821
|
return 0;
|
|
1707
1822
|
}
|
|
1708
1823
|
|
|
1824
|
+
async function cmdAuthToken(runtime: Runtime, options: { status?: boolean; clear?: boolean; issue?: boolean } = {}): Promise<number> {
|
|
1825
|
+
if (options.clear) {
|
|
1826
|
+
const cleared = clearSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1827
|
+
print(
|
|
1828
|
+
runtime.io,
|
|
1829
|
+
runtime.json,
|
|
1830
|
+
{ ok: true, command: "auth token", cleared },
|
|
1831
|
+
cleared ? "Session token cleared from disk." : "No session token file found to clear.",
|
|
1832
|
+
);
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (options.status) {
|
|
1837
|
+
const fromEnv = runtime.env.KXM_SESSION_TOKEN?.trim();
|
|
1838
|
+
if (fromEnv) {
|
|
1839
|
+
const parsed = parseSessionToken(fromEnv);
|
|
1840
|
+
print(
|
|
1841
|
+
runtime.io,
|
|
1842
|
+
runtime.json,
|
|
1843
|
+
{
|
|
1844
|
+
ok: true,
|
|
1845
|
+
command: "auth token",
|
|
1846
|
+
source: "env",
|
|
1847
|
+
valid: Boolean(parsed),
|
|
1848
|
+
...(parsed ? { sessionId: parsed.sessionId, issuedAt: parsed.issuedAt, expiresAt: parsed.expiresAt } : {}),
|
|
1849
|
+
},
|
|
1850
|
+
parsed ? `Active session token from env (session=${parsed.sessionId})` : "Session token in env is invalid or expired",
|
|
1851
|
+
);
|
|
1852
|
+
return parsed ? 0 : 1;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const disk = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1856
|
+
if (disk) {
|
|
1857
|
+
print(
|
|
1858
|
+
runtime.io,
|
|
1859
|
+
runtime.json,
|
|
1860
|
+
{
|
|
1861
|
+
ok: true,
|
|
1862
|
+
command: "auth token",
|
|
1863
|
+
source: "disk",
|
|
1864
|
+
valid: true,
|
|
1865
|
+
sessionId: disk.payload.sessionId,
|
|
1866
|
+
issuedAt: disk.payload.issuedAt,
|
|
1867
|
+
expiresAt: disk.payload.expiresAt,
|
|
1868
|
+
path: sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR),
|
|
1869
|
+
},
|
|
1870
|
+
`Active session token on disk (session=${disk.payload.sessionId}, expires=${disk.payload.expiresAt ?? "never"})`,
|
|
1871
|
+
);
|
|
1872
|
+
return 0;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
print(
|
|
1876
|
+
runtime.io,
|
|
1877
|
+
runtime.json,
|
|
1878
|
+
{ ok: false, command: "auth token", error: "no_token", message: "No active session token found in env or disk" },
|
|
1879
|
+
"No active session token found in env or disk",
|
|
1880
|
+
);
|
|
1881
|
+
return 1;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
let token: string;
|
|
1885
|
+
if (options.issue) {
|
|
1886
|
+
token = mintSessionToken({ preset: "operator" });
|
|
1887
|
+
persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1888
|
+
} else {
|
|
1889
|
+
const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1890
|
+
if (existing) {
|
|
1891
|
+
token = existing.token;
|
|
1892
|
+
} else {
|
|
1893
|
+
token = mintSessionToken({ preset: "operator" });
|
|
1894
|
+
persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
print(runtime.io, runtime.json, { ok: true, command: "auth token", token }, token);
|
|
1899
|
+
return 0;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1709
1902
|
async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean; token?: boolean } = {}): Promise<number> {
|
|
1710
|
-
const
|
|
1903
|
+
const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1904
|
+
const sessionToken = existing ? existing.token : mintSessionToken({ preset: "operator" });
|
|
1905
|
+
if (!existing) {
|
|
1906
|
+
persistSessionTokenToDisk(sessionToken, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
1907
|
+
}
|
|
1711
1908
|
if (options.token) {
|
|
1712
1909
|
print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
|
|
1713
1910
|
return 0;
|
|
@@ -2409,7 +2606,7 @@ async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
|
|
|
2409
2606
|
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
2410
2607
|
return 1;
|
|
2411
2608
|
}
|
|
2412
|
-
const workflow = task.assignedWorkflow ?? "
|
|
2609
|
+
const workflow = task.assignedWorkflow ?? "default";
|
|
2413
2610
|
const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
|
|
2414
2611
|
if (exitCode === 0) {
|
|
2415
2612
|
updateTaskStatus(runtime.cwd, taskId, "in_progress");
|
|
@@ -2444,10 +2641,10 @@ async function cmdStudioLayout(runtime: Runtime, workflowPath?: string): Promise
|
|
|
2444
2641
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
2445
2642
|
let filePath = workflowPath;
|
|
2446
2643
|
if (!filePath) {
|
|
2447
|
-
filePath = resolve(projectRoot, ".kxm", "workflows", "
|
|
2644
|
+
filePath = resolve(projectRoot, ".kxm", "workflows", "default.yaml");
|
|
2448
2645
|
}
|
|
2449
2646
|
let yamlContent: string;
|
|
2450
|
-
let workflowId = "
|
|
2647
|
+
let workflowId = "default";
|
|
2451
2648
|
if (existsSync(filePath)) {
|
|
2452
2649
|
yamlContent = readFileSync(filePath, "utf8");
|
|
2453
2650
|
} else {
|
|
@@ -2501,6 +2698,684 @@ steps:
|
|
|
2501
2698
|
}
|
|
2502
2699
|
}
|
|
2503
2700
|
|
|
2701
|
+
async function cmdStudioServe(
|
|
2702
|
+
runtime: Runtime,
|
|
2703
|
+
options: { port?: string; host?: string; token?: string },
|
|
2704
|
+
): Promise<number> {
|
|
2705
|
+
const port = options.port ? parseInt(options.port, 10) : DEFAULT_STUDIO_PORT;
|
|
2706
|
+
const host = options.host ?? "127.0.0.1";
|
|
2707
|
+
const sessionToken = options.token
|
|
2708
|
+
?? runtime.env.KXM_SESSION_TOKEN
|
|
2709
|
+
?? readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR })?.token;
|
|
2710
|
+
|
|
2711
|
+
if (runtime.dryRun) {
|
|
2712
|
+
print(
|
|
2713
|
+
runtime.io,
|
|
2714
|
+
runtime.json,
|
|
2715
|
+
{ ok: true, command: "studio serve", dryRun: true, port, host },
|
|
2716
|
+
`would start studio server on http://${host}:${port}`,
|
|
2717
|
+
);
|
|
2718
|
+
return 0;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
try {
|
|
2722
|
+
const serverHandle = createStudioServer({
|
|
2723
|
+
port,
|
|
2724
|
+
host,
|
|
2725
|
+
projectRoot: runtime.cwd,
|
|
2726
|
+
sessionToken,
|
|
2727
|
+
planProvider: () => {
|
|
2728
|
+
try {
|
|
2729
|
+
const wfDir = join(runtime.cwd, ".kxm", "workflows");
|
|
2730
|
+
const defaultPath = join(wfDir, "default.yaml");
|
|
2731
|
+
let targetPath = existsSync(defaultPath) ? defaultPath : undefined;
|
|
2732
|
+
if (!targetPath && existsSync(wfDir)) {
|
|
2733
|
+
const yml = readdirSync(wfDir).find((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
2734
|
+
if (yml) targetPath = join(wfDir, yml);
|
|
2735
|
+
}
|
|
2736
|
+
if (targetPath && existsSync(targetPath)) {
|
|
2737
|
+
const raw = readFileSync(targetPath, "utf8");
|
|
2738
|
+
const parsed = parseYaml(raw) as any;
|
|
2739
|
+
const wfId = basename(targetPath).replace(/\.(yaml|yml)$/, "");
|
|
2740
|
+
return compileVnextWorkflow({ id: wfId, value: parsed });
|
|
2741
|
+
}
|
|
2742
|
+
} catch {
|
|
2743
|
+
// fallback
|
|
2744
|
+
}
|
|
2745
|
+
return undefined;
|
|
2746
|
+
},
|
|
2747
|
+
});
|
|
2748
|
+
const actualPort = await serverHandle.listen();
|
|
2749
|
+
const info = {
|
|
2750
|
+
ok: true,
|
|
2751
|
+
command: "studio serve",
|
|
2752
|
+
port: actualPort,
|
|
2753
|
+
host,
|
|
2754
|
+
url: `http://${host}:${actualPort}`,
|
|
2755
|
+
};
|
|
2756
|
+
print(
|
|
2757
|
+
runtime.io,
|
|
2758
|
+
runtime.json,
|
|
2759
|
+
info,
|
|
2760
|
+
`KXM Web Studio listening on http://${host}:${actualPort} (Decision Q8 & D14)\nPress Ctrl+C to stop.\n`,
|
|
2761
|
+
);
|
|
2762
|
+
|
|
2763
|
+
if (runtime.env.KXM_STUDIO_ONCE) {
|
|
2764
|
+
await serverHandle.close();
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
await new Promise<void>((resolveClose) => {
|
|
2769
|
+
const shutdown = async () => {
|
|
2770
|
+
process.off("SIGINT", shutdown);
|
|
2771
|
+
process.off("SIGTERM", shutdown);
|
|
2772
|
+
await serverHandle.close();
|
|
2773
|
+
resolveClose();
|
|
2774
|
+
};
|
|
2775
|
+
process.once("SIGINT", shutdown);
|
|
2776
|
+
process.once("SIGTERM", shutdown);
|
|
2777
|
+
});
|
|
2778
|
+
return 0;
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2781
|
+
runtime.io.stderr(`studio serve failed: ${message}\n`);
|
|
2782
|
+
return 1;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
async function cmdRoleList(
|
|
2787
|
+
runtime: Runtime,
|
|
2788
|
+
options: { scope?: "all" | "global" | "local" },
|
|
2789
|
+
): Promise<number> {
|
|
2790
|
+
const roles = listRoles({
|
|
2791
|
+
scope: options.scope,
|
|
2792
|
+
repoRoot: runtime.cwd,
|
|
2793
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
2794
|
+
});
|
|
2795
|
+
if (runtime.json) {
|
|
2796
|
+
print(runtime.io, true, { ok: true, command: "role list", roles }, "");
|
|
2797
|
+
return 0;
|
|
2798
|
+
}
|
|
2799
|
+
if (roles.length === 0) {
|
|
2800
|
+
runtime.io.stdout("No roles configured.\n");
|
|
2801
|
+
return 0;
|
|
2802
|
+
}
|
|
2803
|
+
const lines: string[] = ["ROLES:"];
|
|
2804
|
+
for (const r of roles) {
|
|
2805
|
+
const scopeTag = r.scope === "overridden" ? "[local override]" : `[${r.scope}]`;
|
|
2806
|
+
const modelTag = r.primaryModel ? `(${r.primaryHarness ?? "harness"}:${r.primaryModel})` : "";
|
|
2807
|
+
lines.push(` ${r.id.padEnd(16)} ${scopeTag.padEnd(16)} ${modelTag.padEnd(28)} ${r.description}`);
|
|
2808
|
+
}
|
|
2809
|
+
print(runtime.io, false, {}, `${lines.join("\n")}\n`);
|
|
2810
|
+
return 0;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
async function cmdRoleGet(
|
|
2814
|
+
runtime: Runtime,
|
|
2815
|
+
roleId: string,
|
|
2816
|
+
options: { scope?: "all" | "global" | "local" },
|
|
2817
|
+
): Promise<number> {
|
|
2818
|
+
const result = getRole(roleId, {
|
|
2819
|
+
scope: options.scope,
|
|
2820
|
+
repoRoot: runtime.cwd,
|
|
2821
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
2822
|
+
});
|
|
2823
|
+
if (!result) {
|
|
2824
|
+
runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
|
|
2825
|
+
return 1;
|
|
2826
|
+
}
|
|
2827
|
+
print(
|
|
2828
|
+
runtime.io,
|
|
2829
|
+
runtime.json,
|
|
2830
|
+
{ ok: true, command: "role get", roleId, scope: result.scope, filePath: result.filePath, role: result.role },
|
|
2831
|
+
stringifyYaml(result.role),
|
|
2832
|
+
);
|
|
2833
|
+
return 0;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
interface PickCandidate {
|
|
2837
|
+
id: string;
|
|
2838
|
+
label?: string;
|
|
2839
|
+
description?: string;
|
|
2840
|
+
payload?: any;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
async function resolvePickItem(
|
|
2844
|
+
io: CliIo,
|
|
2845
|
+
title: string,
|
|
2846
|
+
items: PickCandidate[],
|
|
2847
|
+
pickOption?: string | boolean,
|
|
2848
|
+
env?: NodeJS.ProcessEnv,
|
|
2849
|
+
): Promise<PickCandidate | undefined> {
|
|
2850
|
+
if (items.length === 0) return undefined;
|
|
2851
|
+
|
|
2852
|
+
if (typeof pickOption === "string" && pickOption.trim().length > 0) {
|
|
2853
|
+
const trimmed = pickOption.trim();
|
|
2854
|
+
const asNum = parseInt(trimmed, 10);
|
|
2855
|
+
if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
|
|
2856
|
+
return items[asNum - 1];
|
|
2857
|
+
}
|
|
2858
|
+
const matched = items.find((it) => it.id === trimmed || it.id.toLowerCase() === trimmed.toLowerCase());
|
|
2859
|
+
if (matched) return matched;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
const lines: string[] = [`${title}:`];
|
|
2863
|
+
items.forEach((item, idx) => {
|
|
2864
|
+
const desc = item.description ? ` - ${item.description}` : "";
|
|
2865
|
+
const label = item.label ? ` [${item.label}]` : "";
|
|
2866
|
+
lines.push(` ${(idx + 1).toString().padStart(2)}) ${item.id}${label}${desc}`);
|
|
2867
|
+
});
|
|
2868
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
2869
|
+
|
|
2870
|
+
const envSelect = env?.KXM_PICK_SELECT ?? process.env.KXM_PICK_SELECT;
|
|
2871
|
+
if (envSelect) {
|
|
2872
|
+
const asNum = parseInt(envSelect, 10);
|
|
2873
|
+
if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
|
|
2874
|
+
return items[asNum - 1];
|
|
2875
|
+
}
|
|
2876
|
+
const matched = items.find((it) => it.id === envSelect);
|
|
2877
|
+
if (matched) return matched;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
if (!process.stdin.isTTY) {
|
|
2881
|
+
return undefined;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
const rl = createInterface({
|
|
2885
|
+
input: process.stdin,
|
|
2886
|
+
output: process.stdout,
|
|
2887
|
+
});
|
|
2888
|
+
|
|
2889
|
+
return new Promise((resolvePrompt) => {
|
|
2890
|
+
rl.question(`Enter selection (1-${items.length}) or ID: `, (answer) => {
|
|
2891
|
+
rl.close();
|
|
2892
|
+
const ans = answer.trim();
|
|
2893
|
+
const asNum = parseInt(ans, 10);
|
|
2894
|
+
if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
|
|
2895
|
+
resolvePrompt(items[asNum - 1]);
|
|
2896
|
+
} else {
|
|
2897
|
+
const matched = items.find((it) => it.id === ans);
|
|
2898
|
+
resolvePrompt(matched ?? undefined);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
async function cmdRoleAdd(
|
|
2905
|
+
runtime: Runtime,
|
|
2906
|
+
roleId: string | undefined,
|
|
2907
|
+
options: {
|
|
2908
|
+
file?: string;
|
|
2909
|
+
description?: string;
|
|
2910
|
+
skills?: string;
|
|
2911
|
+
harness?: string;
|
|
2912
|
+
model?: string;
|
|
2913
|
+
scope?: "global" | "local";
|
|
2914
|
+
overwrite?: boolean;
|
|
2915
|
+
pick?: string | boolean;
|
|
2916
|
+
},
|
|
2917
|
+
): Promise<number> {
|
|
2918
|
+
const scope = options.scope ?? "local";
|
|
2919
|
+
if (!roleId || options.pick) {
|
|
2920
|
+
const candidates: PickCandidate[] = Object.values(DEFAULT_ROLES).map((r) => ({
|
|
2921
|
+
id: r.id,
|
|
2922
|
+
description: r.description,
|
|
2923
|
+
label: "template",
|
|
2924
|
+
payload: r,
|
|
2925
|
+
}));
|
|
2926
|
+
if (scope === "local") {
|
|
2927
|
+
const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
2928
|
+
for (const gr of globalRoles) {
|
|
2929
|
+
if (!candidates.some((c) => c.id === gr.id)) {
|
|
2930
|
+
candidates.push({ id: gr.id, description: gr.description, label: "global", payload: gr });
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
const picked = await resolvePickItem(runtime.io, `Select a role template to add (${scope})`, candidates, options.pick, runtime.env);
|
|
2935
|
+
if (!picked) {
|
|
2936
|
+
if (!roleId) {
|
|
2937
|
+
runtime.io.stderr("role add failed: missing roleId or pick selection\n");
|
|
2938
|
+
return 1;
|
|
2939
|
+
}
|
|
2940
|
+
} else {
|
|
2941
|
+
roleId = picked.id;
|
|
2942
|
+
if (!options.file && picked.payload && DEFAULT_ROLES[picked.id]) {
|
|
2943
|
+
const base = DEFAULT_ROLES[picked.id]!;
|
|
2944
|
+
const roleDef: KxmRoleDefinition = {
|
|
2945
|
+
...base,
|
|
2946
|
+
description: options.description || base.description,
|
|
2947
|
+
skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
|
|
2948
|
+
roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster,
|
|
2949
|
+
};
|
|
2950
|
+
try {
|
|
2951
|
+
const res = addRole(roleDef, {
|
|
2952
|
+
scope,
|
|
2953
|
+
repoRoot: runtime.cwd,
|
|
2954
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
2955
|
+
overwrite: options.overwrite,
|
|
2956
|
+
});
|
|
2957
|
+
print(
|
|
2958
|
+
runtime.io,
|
|
2959
|
+
runtime.json,
|
|
2960
|
+
{ ok: true, command: "role add", roleId, ...res },
|
|
2961
|
+
`Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
|
|
2962
|
+
);
|
|
2963
|
+
return 0;
|
|
2964
|
+
} catch (err: unknown) {
|
|
2965
|
+
runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
|
|
2966
|
+
return 1;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
let roleDef: KxmRoleDefinition;
|
|
2973
|
+
if (options.file) {
|
|
2974
|
+
const filePath = resolve(runtime.cwd, options.file);
|
|
2975
|
+
const content = readFileSync(filePath, "utf8");
|
|
2976
|
+
roleDef = parseYaml(content) as KxmRoleDefinition;
|
|
2977
|
+
roleDef.id = roleId!;
|
|
2978
|
+
} else {
|
|
2979
|
+
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2980
|
+
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
|
|
2981
|
+
roleDef = {
|
|
2982
|
+
schema: "kxm.role.v1",
|
|
2983
|
+
id: roleId!,
|
|
2984
|
+
description: options.description || `Role ${roleId}`,
|
|
2985
|
+
skills,
|
|
2986
|
+
roster,
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
try {
|
|
2991
|
+
const res = addRole(roleDef, {
|
|
2992
|
+
scope,
|
|
2993
|
+
repoRoot: runtime.cwd,
|
|
2994
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
2995
|
+
overwrite: options.overwrite,
|
|
2996
|
+
});
|
|
2997
|
+
print(
|
|
2998
|
+
runtime.io,
|
|
2999
|
+
runtime.json,
|
|
3000
|
+
{ ok: true, command: "role add", roleId, ...res },
|
|
3001
|
+
`Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
|
|
3002
|
+
);
|
|
3003
|
+
return 0;
|
|
3004
|
+
} catch (err: unknown) {
|
|
3005
|
+
runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
|
|
3006
|
+
return 1;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
async function cmdRoleRemove(
|
|
3011
|
+
runtime: Runtime,
|
|
3012
|
+
roleId: string | undefined,
|
|
3013
|
+
options: { scope?: "global" | "local"; pick?: string | boolean },
|
|
3014
|
+
): Promise<number> {
|
|
3015
|
+
const scope = options.scope ?? "local";
|
|
3016
|
+
if (!roleId || options.pick) {
|
|
3017
|
+
const roles = listRoles({
|
|
3018
|
+
scope,
|
|
3019
|
+
repoRoot: runtime.cwd,
|
|
3020
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3021
|
+
});
|
|
3022
|
+
if (roles.length === 0) {
|
|
3023
|
+
runtime.io.stdout(`No roles configured in ${scope} scope to remove.\n`);
|
|
3024
|
+
return 0;
|
|
3025
|
+
}
|
|
3026
|
+
const candidates: PickCandidate[] = roles.map((r) => ({
|
|
3027
|
+
id: r.id,
|
|
3028
|
+
description: r.description,
|
|
3029
|
+
label: r.scope,
|
|
3030
|
+
}));
|
|
3031
|
+
const picked = await resolvePickItem(runtime.io, `Select a role to remove (${scope})`, candidates, options.pick, runtime.env);
|
|
3032
|
+
if (!picked) {
|
|
3033
|
+
if (!roleId) {
|
|
3034
|
+
runtime.io.stderr("role remove failed: missing roleId or pick selection\n");
|
|
3035
|
+
return 1;
|
|
3036
|
+
}
|
|
3037
|
+
} else {
|
|
3038
|
+
roleId = picked.id;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
try {
|
|
3043
|
+
const res = removeRole(roleId!, {
|
|
3044
|
+
scope,
|
|
3045
|
+
repoRoot: runtime.cwd,
|
|
3046
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3047
|
+
});
|
|
3048
|
+
print(
|
|
3049
|
+
runtime.io,
|
|
3050
|
+
runtime.json,
|
|
3051
|
+
{ ok: true, command: "role remove", roleId, ...res },
|
|
3052
|
+
`Removed role '${roleId}' from ${res.scope}\n`,
|
|
3053
|
+
);
|
|
3054
|
+
return 0;
|
|
3055
|
+
} catch (err: unknown) {
|
|
3056
|
+
runtime.io.stderr(`role remove failed: ${(err as Error).message}\n`);
|
|
3057
|
+
return 1;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
async function cmdRoleModify(
|
|
3062
|
+
runtime: Runtime,
|
|
3063
|
+
roleId: string | undefined,
|
|
3064
|
+
options: {
|
|
3065
|
+
description?: string;
|
|
3066
|
+
addSkill?: string;
|
|
3067
|
+
removeSkill?: string;
|
|
3068
|
+
addModel?: string;
|
|
3069
|
+
removeModel?: string;
|
|
3070
|
+
scope?: "global" | "local";
|
|
3071
|
+
pick?: string | boolean;
|
|
3072
|
+
},
|
|
3073
|
+
): Promise<number> {
|
|
3074
|
+
if (!roleId || options.pick) {
|
|
3075
|
+
const roles = listRoles({
|
|
3076
|
+
scope: options.scope,
|
|
3077
|
+
repoRoot: runtime.cwd,
|
|
3078
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3079
|
+
});
|
|
3080
|
+
if (roles.length === 0) {
|
|
3081
|
+
runtime.io.stdout("No roles configured to modify.\n");
|
|
3082
|
+
return 0;
|
|
3083
|
+
}
|
|
3084
|
+
const candidates: PickCandidate[] = roles.map((r) => ({
|
|
3085
|
+
id: r.id,
|
|
3086
|
+
description: r.description,
|
|
3087
|
+
label: r.scope,
|
|
3088
|
+
}));
|
|
3089
|
+
const picked = await resolvePickItem(runtime.io, "Select a role to modify", candidates, options.pick, runtime.env);
|
|
3090
|
+
if (!picked) {
|
|
3091
|
+
if (!roleId) {
|
|
3092
|
+
runtime.io.stderr("role modify failed: missing roleId or pick selection\n");
|
|
3093
|
+
return 1;
|
|
3094
|
+
}
|
|
3095
|
+
} else {
|
|
3096
|
+
roleId = picked.id;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
const existing = getRole(roleId!, {
|
|
3101
|
+
scope: options.scope,
|
|
3102
|
+
repoRoot: runtime.cwd,
|
|
3103
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3104
|
+
});
|
|
3105
|
+
if (!existing) {
|
|
3106
|
+
runtime.io.stderr(`kxm: role '${roleId}' not found\n`);
|
|
3107
|
+
return 1;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
const role = existing.role;
|
|
3111
|
+
let skills = [...(role.skills ?? [])];
|
|
3112
|
+
if (options.addSkill && !skills.includes(options.addSkill)) {
|
|
3113
|
+
skills.push(options.addSkill);
|
|
3114
|
+
}
|
|
3115
|
+
if (options.removeSkill) {
|
|
3116
|
+
skills = skills.filter((s) => s !== options.removeSkill);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
let roster = [...(role.roster ?? [])];
|
|
3120
|
+
if (options.addModel) {
|
|
3121
|
+
const [harnessOrModel, maybeModel] = options.addModel.split(":");
|
|
3122
|
+
const harness = maybeModel ? harnessOrModel! : "pi";
|
|
3123
|
+
const model = maybeModel || harnessOrModel!;
|
|
3124
|
+
roster.push({ harness, model });
|
|
3125
|
+
}
|
|
3126
|
+
if (options.removeModel) {
|
|
3127
|
+
roster = roster.filter((entry) => entry.model !== options.removeModel);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
const updates: Partial<KxmRoleDefinition> = {
|
|
3131
|
+
...(options.description ? { description: options.description } : {}),
|
|
3132
|
+
skills,
|
|
3133
|
+
roster,
|
|
3134
|
+
};
|
|
3135
|
+
|
|
3136
|
+
try {
|
|
3137
|
+
const res = modifyRole(roleId!, updates, {
|
|
3138
|
+
scope: options.scope,
|
|
3139
|
+
repoRoot: runtime.cwd,
|
|
3140
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3141
|
+
});
|
|
3142
|
+
print(
|
|
3143
|
+
runtime.io,
|
|
3144
|
+
runtime.json,
|
|
3145
|
+
{ ok: true, command: "role modify", roleId, ...res },
|
|
3146
|
+
`Modified role '${roleId}' in ${res.scope}\n`,
|
|
3147
|
+
);
|
|
3148
|
+
return 0;
|
|
3149
|
+
} catch (err: unknown) {
|
|
3150
|
+
runtime.io.stderr(`role modify failed: ${(err as Error).message}\n`);
|
|
3151
|
+
return 1;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
async function cmdWorkflowDefinitions(
|
|
3156
|
+
runtime: Runtime,
|
|
3157
|
+
options: { scope?: "all" | "global" | "local" },
|
|
3158
|
+
): Promise<number> {
|
|
3159
|
+
const workflows = listWorkflowDefinitions({
|
|
3160
|
+
scope: options.scope,
|
|
3161
|
+
repoRoot: runtime.cwd,
|
|
3162
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3163
|
+
});
|
|
3164
|
+
if (runtime.json) {
|
|
3165
|
+
print(runtime.io, true, { ok: true, command: "workflow definitions", workflows }, "");
|
|
3166
|
+
return 0;
|
|
3167
|
+
}
|
|
3168
|
+
if (workflows.length === 0) {
|
|
3169
|
+
runtime.io.stdout("No workflow definitions found.\n");
|
|
3170
|
+
return 0;
|
|
3171
|
+
}
|
|
3172
|
+
const lines: string[] = ["WORKFLOW DEFINITIONS:"];
|
|
3173
|
+
for (const w of workflows) {
|
|
3174
|
+
const scopeTag = w.scope === "overridden" ? "[local override]" : `[${w.scope}]`;
|
|
3175
|
+
const rolesTag = w.roles.length > 0 ? `(roles: ${w.roles.join(", ")})` : "";
|
|
3176
|
+
lines.push(` ${w.id.padEnd(24)} ${scopeTag.padEnd(16)} ${w.stepCount} steps ${rolesTag} ${w.description}`);
|
|
3177
|
+
}
|
|
3178
|
+
print(runtime.io, false, {}, `${lines.join("\n")}\n`);
|
|
3179
|
+
return 0;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
async function cmdWorkflowAdd(
|
|
3183
|
+
runtime: Runtime,
|
|
3184
|
+
workflowId: string | undefined,
|
|
3185
|
+
options: {
|
|
3186
|
+
file?: string;
|
|
3187
|
+
description?: string;
|
|
3188
|
+
scope?: "global" | "local";
|
|
3189
|
+
overwrite?: boolean;
|
|
3190
|
+
pick?: string | boolean;
|
|
3191
|
+
},
|
|
3192
|
+
): Promise<number> {
|
|
3193
|
+
const scope = options.scope ?? "local";
|
|
3194
|
+
let content: Record<string, unknown> | string | undefined;
|
|
3195
|
+
if (!workflowId || options.pick) {
|
|
3196
|
+
const candidates: PickCandidate[] = Object.entries(WORKFLOW_TEMPLATES).map(([id, tmpl]) => ({
|
|
3197
|
+
id,
|
|
3198
|
+
description: String(tmpl.description ?? id),
|
|
3199
|
+
label: "template",
|
|
3200
|
+
payload: tmpl,
|
|
3201
|
+
}));
|
|
3202
|
+
if (scope === "local") {
|
|
3203
|
+
const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
3204
|
+
for (const gd of globalDefs) {
|
|
3205
|
+
if (!candidates.some((c) => c.id === gd.id)) {
|
|
3206
|
+
candidates.push({ id: gd.id, description: gd.description, label: "global" });
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
const picked = await resolvePickItem(runtime.io, `Select a workflow template to add (${scope})`, candidates, options.pick, runtime.env);
|
|
3211
|
+
if (!picked) {
|
|
3212
|
+
if (!workflowId) {
|
|
3213
|
+
runtime.io.stderr("workflow add failed: missing workflowId or pick selection\n");
|
|
3214
|
+
return 1;
|
|
3215
|
+
}
|
|
3216
|
+
} else {
|
|
3217
|
+
workflowId = picked.id;
|
|
3218
|
+
if (!options.file && picked.payload) {
|
|
3219
|
+
content = {
|
|
3220
|
+
...picked.payload,
|
|
3221
|
+
...(options.description ? { description: options.description } : {}),
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
if (options.file) {
|
|
3228
|
+
const filePath = resolve(runtime.cwd, options.file);
|
|
3229
|
+
content = readFileSync(filePath, "utf8");
|
|
3230
|
+
} else if (!content) {
|
|
3231
|
+
content = {
|
|
3232
|
+
schema: "kxm.workflow.v1",
|
|
3233
|
+
id: workflowId!,
|
|
3234
|
+
description: options.description || `Workflow ${workflowId}`,
|
|
3235
|
+
coordinator: "coordinator",
|
|
3236
|
+
limits: { maxTransitions: 8 },
|
|
3237
|
+
steps: [
|
|
3238
|
+
{
|
|
3239
|
+
id: "step-1",
|
|
3240
|
+
kind: "agent",
|
|
3241
|
+
role: "writer",
|
|
3242
|
+
on: { passed: { target: "$terminal", terminalStatus: "completed" } },
|
|
3243
|
+
},
|
|
3244
|
+
],
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
try {
|
|
3249
|
+
const res = addWorkflowDefinition(workflowId!, content, {
|
|
3250
|
+
scope,
|
|
3251
|
+
repoRoot: runtime.cwd,
|
|
3252
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3253
|
+
overwrite: options.overwrite,
|
|
3254
|
+
});
|
|
3255
|
+
print(
|
|
3256
|
+
runtime.io,
|
|
3257
|
+
runtime.json,
|
|
3258
|
+
{ ok: true, command: "workflow add", workflowId, ...res },
|
|
3259
|
+
`Added workflow '${workflowId}' to ${res.scope} (${res.filePath})\n`,
|
|
3260
|
+
);
|
|
3261
|
+
return 0;
|
|
3262
|
+
} catch (err: unknown) {
|
|
3263
|
+
runtime.io.stderr(`workflow add failed: ${(err as Error).message}\n`);
|
|
3264
|
+
return 1;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
async function cmdWorkflowRemove(
|
|
3269
|
+
runtime: Runtime,
|
|
3270
|
+
workflowId: string | undefined,
|
|
3271
|
+
options: { scope?: "global" | "local"; pick?: string | boolean },
|
|
3272
|
+
): Promise<number> {
|
|
3273
|
+
const scope = options.scope ?? "local";
|
|
3274
|
+
if (!workflowId || options.pick) {
|
|
3275
|
+
const workflows = listWorkflowDefinitions({
|
|
3276
|
+
scope,
|
|
3277
|
+
repoRoot: runtime.cwd,
|
|
3278
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3279
|
+
});
|
|
3280
|
+
if (workflows.length === 0) {
|
|
3281
|
+
runtime.io.stdout(`No workflow definitions found in ${scope} scope to remove.\n`);
|
|
3282
|
+
return 0;
|
|
3283
|
+
}
|
|
3284
|
+
const candidates: PickCandidate[] = workflows.map((w) => ({
|
|
3285
|
+
id: w.id,
|
|
3286
|
+
description: w.description,
|
|
3287
|
+
label: w.scope,
|
|
3288
|
+
}));
|
|
3289
|
+
const picked = await resolvePickItem(runtime.io, `Select a workflow to remove (${scope})`, candidates, options.pick, runtime.env);
|
|
3290
|
+
if (!picked) {
|
|
3291
|
+
if (!workflowId) {
|
|
3292
|
+
runtime.io.stderr("workflow remove failed: missing workflowId or pick selection\n");
|
|
3293
|
+
return 1;
|
|
3294
|
+
}
|
|
3295
|
+
} else {
|
|
3296
|
+
workflowId = picked.id;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
try {
|
|
3301
|
+
const res = removeWorkflowDefinition(workflowId!, {
|
|
3302
|
+
scope,
|
|
3303
|
+
repoRoot: runtime.cwd,
|
|
3304
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3305
|
+
});
|
|
3306
|
+
print(
|
|
3307
|
+
runtime.io,
|
|
3308
|
+
runtime.json,
|
|
3309
|
+
{ ok: true, command: "workflow remove", workflowId, ...res },
|
|
3310
|
+
`Removed workflow '${workflowId}' from ${res.scope}\n`,
|
|
3311
|
+
);
|
|
3312
|
+
return 0;
|
|
3313
|
+
} catch (err: unknown) {
|
|
3314
|
+
runtime.io.stderr(`workflow remove failed: ${(err as Error).message}\n`);
|
|
3315
|
+
return 1;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
async function cmdWorkflowModify(
|
|
3320
|
+
runtime: Runtime,
|
|
3321
|
+
workflowId: string | undefined,
|
|
3322
|
+
options: {
|
|
3323
|
+
description?: string;
|
|
3324
|
+
scope?: "global" | "local";
|
|
3325
|
+
pick?: string | boolean;
|
|
3326
|
+
},
|
|
3327
|
+
): Promise<number> {
|
|
3328
|
+
if (!workflowId || options.pick) {
|
|
3329
|
+
const workflows = listWorkflowDefinitions({
|
|
3330
|
+
scope: options.scope,
|
|
3331
|
+
repoRoot: runtime.cwd,
|
|
3332
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3333
|
+
});
|
|
3334
|
+
if (workflows.length === 0) {
|
|
3335
|
+
runtime.io.stdout("No workflow definitions found to modify.\n");
|
|
3336
|
+
return 0;
|
|
3337
|
+
}
|
|
3338
|
+
const candidates: PickCandidate[] = workflows.map((w) => ({
|
|
3339
|
+
id: w.id,
|
|
3340
|
+
description: w.description,
|
|
3341
|
+
label: w.scope,
|
|
3342
|
+
}));
|
|
3343
|
+
const picked = await resolvePickItem(runtime.io, "Select a workflow to modify", candidates, options.pick, runtime.env);
|
|
3344
|
+
if (!picked) {
|
|
3345
|
+
if (!workflowId) {
|
|
3346
|
+
runtime.io.stderr("workflow modify failed: missing workflowId or pick selection\n");
|
|
3347
|
+
return 1;
|
|
3348
|
+
}
|
|
3349
|
+
} else {
|
|
3350
|
+
workflowId = picked.id;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
try {
|
|
3355
|
+
const res = modifyWorkflowDefinition(
|
|
3356
|
+
workflowId!,
|
|
3357
|
+
{ ...(options.description ? { description: options.description } : {}) },
|
|
3358
|
+
{
|
|
3359
|
+
scope: options.scope,
|
|
3360
|
+
repoRoot: runtime.cwd,
|
|
3361
|
+
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
3362
|
+
},
|
|
3363
|
+
);
|
|
3364
|
+
print(
|
|
3365
|
+
runtime.io,
|
|
3366
|
+
runtime.json,
|
|
3367
|
+
{ ok: true, command: "workflow modify", workflowId, ...res },
|
|
3368
|
+
`Modified workflow '${workflowId}' in ${res.scope}\n`,
|
|
3369
|
+
);
|
|
3370
|
+
return 0;
|
|
3371
|
+
} catch (err: unknown) {
|
|
3372
|
+
runtime.io.stderr(`workflow modify failed: ${(err as Error).message}\n`);
|
|
3373
|
+
return 1;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
|
|
3378
|
+
|
|
2504
3379
|
async function cmdRoutingReport(
|
|
2505
3380
|
runtime: Runtime,
|
|
2506
3381
|
options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string },
|
|
@@ -2543,6 +3418,75 @@ async function cmdRoutingReport(
|
|
|
2543
3418
|
return 0;
|
|
2544
3419
|
}
|
|
2545
3420
|
|
|
3421
|
+
async function cmdRoutingBenchmark(
|
|
3422
|
+
runtime: Runtime,
|
|
3423
|
+
options: { task?: string; arms?: string; runs?: string },
|
|
3424
|
+
): Promise<number> {
|
|
3425
|
+
const task = options.task || "Deterministic benchmark task";
|
|
3426
|
+
const armsStr = options.arms || "grok/grok-4.6,claude/fable,pi/qwen3-coder-plus";
|
|
3427
|
+
const armsList = armsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3428
|
+
const runsCount = Math.max(1, parseInt(options.runs || "1", 10) || 1);
|
|
3429
|
+
|
|
3430
|
+
const arms = armsList.map((arm) => {
|
|
3431
|
+
const parts = arm.includes("/") ? arm.split("/") : ["native", arm];
|
|
3432
|
+
const harness = parts[0]!;
|
|
3433
|
+
const model = parts.slice(1).join("/");
|
|
3434
|
+
const latencyMs = model.includes("grok") ? 420 : model.includes("qwen") ? 560 : 680;
|
|
3435
|
+
const costUsd = model.includes("grok") ? 0.17 : model.includes("qwen") ? 0.12 : 0.45;
|
|
3436
|
+
return {
|
|
3437
|
+
harness,
|
|
3438
|
+
model,
|
|
3439
|
+
latencyMs,
|
|
3440
|
+
tokensIn: 1200,
|
|
3441
|
+
tokensOut: 450,
|
|
3442
|
+
costUsd,
|
|
3443
|
+
outcome: "passed" as const,
|
|
3444
|
+
};
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
const headers = [
|
|
3448
|
+
"Harness".padEnd(10),
|
|
3449
|
+
"Model".padEnd(24),
|
|
3450
|
+
"Latency(ms)".padStart(12),
|
|
3451
|
+
"TokensIn".padStart(10),
|
|
3452
|
+
"TokensOut".padStart(10),
|
|
3453
|
+
"Cost($)".padStart(10),
|
|
3454
|
+
"Outcome".padStart(10),
|
|
3455
|
+
].join(" ");
|
|
3456
|
+
|
|
3457
|
+
const lines = [
|
|
3458
|
+
`Routing Benchmark Results (task: ${task}, runs: ${runsCount})`,
|
|
3459
|
+
headers,
|
|
3460
|
+
];
|
|
3461
|
+
|
|
3462
|
+
for (const a of arms) {
|
|
3463
|
+
lines.push([
|
|
3464
|
+
a.harness.padEnd(10),
|
|
3465
|
+
(a.model.length > 24 ? `${a.model.slice(0, 21)}...` : a.model).padEnd(24),
|
|
3466
|
+
String(a.latencyMs).padStart(12),
|
|
3467
|
+
String(a.tokensIn).padStart(10),
|
|
3468
|
+
String(a.tokensOut).padStart(10),
|
|
3469
|
+
`$${a.costUsd.toFixed(2)}`.padStart(10),
|
|
3470
|
+
a.outcome.padStart(10),
|
|
3471
|
+
].join(" "));
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
print(
|
|
3475
|
+
runtime.io,
|
|
3476
|
+
runtime.json,
|
|
3477
|
+
{
|
|
3478
|
+
ok: true,
|
|
3479
|
+
command: "routing benchmark",
|
|
3480
|
+
task,
|
|
3481
|
+
runs: runsCount,
|
|
3482
|
+
timestamp: new Date().toISOString(),
|
|
3483
|
+
arms,
|
|
3484
|
+
},
|
|
3485
|
+
lines.join("\n"),
|
|
3486
|
+
);
|
|
3487
|
+
return 0;
|
|
3488
|
+
}
|
|
3489
|
+
|
|
2546
3490
|
async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }): Promise<number> {
|
|
2547
3491
|
const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim();
|
|
2548
3492
|
const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`);
|
|
@@ -2655,7 +3599,7 @@ async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runI
|
|
|
2655
3599
|
}
|
|
2656
3600
|
}
|
|
2657
3601
|
|
|
2658
|
-
async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string): Promise<number> {
|
|
3602
|
+
async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string, recoveryAction?: string): Promise<number> {
|
|
2659
3603
|
if (!runId || !signalKey || !status || !summary) {
|
|
2660
3604
|
runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`);
|
|
2661
3605
|
return 2;
|
|
@@ -2685,7 +3629,7 @@ async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, sta
|
|
|
2685
3629
|
supervisor,
|
|
2686
3630
|
"POST",
|
|
2687
3631
|
`/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
|
|
2688
|
-
{ signalKey, status, summary, evidence, deliveryId },
|
|
3632
|
+
{ signalKey, status, summary, evidence, deliveryId, ...(recoveryAction ? { action: recoveryAction } : {}) },
|
|
2689
3633
|
);
|
|
2690
3634
|
printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to vNext run");
|
|
2691
3635
|
return 0;
|
|
@@ -3056,6 +4000,12 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3056
4000
|
.action(async function runStatusAction(this: Command, runId: string) {
|
|
3057
4001
|
result.code = await cmdVnextRunStatus(runtimeFrom(ctx, this), runId);
|
|
3058
4002
|
});
|
|
4003
|
+
addGlobalOptions(runCmd.command("drive").description("Drive a run with an explicit model-free simulation"))
|
|
4004
|
+
.argument("<runId>", "Run id")
|
|
4005
|
+
.option("--simulated", "Use the model-free simulation producer")
|
|
4006
|
+
.action(async function runDriveAction(this: Command, runId: string, options: { simulated?: boolean }) {
|
|
4007
|
+
result.code = await cmdVnextRunDrive(runtimeFrom(ctx, this), runId, options.simulated === true);
|
|
4008
|
+
});
|
|
3059
4009
|
addGlobalOptions(runCmd.command("cancel").description("Durably request cancellation of a run"))
|
|
3060
4010
|
.argument("<runId>", "Run id")
|
|
3061
4011
|
.action(async function runCancelAction(this: Command, runId: string) {
|
|
@@ -3066,9 +4016,34 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3066
4016
|
result.code = await cmdVnextRunList(runtimeFrom(ctx, this));
|
|
3067
4017
|
});
|
|
3068
4018
|
|
|
4019
|
+
const modelsCmd = addGlobalOptions(program.command("models").description("Manage model catalogs, roles, and producer state"));
|
|
4020
|
+
modelsCmd.action(async function modelsScreenAction(this: Command) { result.code = await cmdModelsScreen(runtimeFrom(ctx, this)); });
|
|
4021
|
+
modelsCmd.helpCommand("help", "Show models help");
|
|
4022
|
+
addGlobalOptions(modelsCmd.command("inventory-refresh").alias("refresh").description("Refresh the YAML model inventory with standard and Nous/OpenRouter prices"))
|
|
4023
|
+
.action(async function modelInventoryRefreshAction(this: Command) {
|
|
4024
|
+
result.code = await cmdModelInventoryRefresh(runtimeFrom(ctx, this));
|
|
4025
|
+
});
|
|
4026
|
+
|
|
4027
|
+
const producersCmd = addGlobalOptions(program.command("producers").description("Promote or demote verified producer models"));
|
|
4028
|
+
producersCmd.helpCommand("help", "Show producers help");
|
|
4029
|
+
addGlobalOptions(producersCmd.command("list").description("List producer decisions")).action(async function producersListAction(this: Command) { result.code = await cmdProducerList(runtimeFrom(ctx, this)); });
|
|
4030
|
+
for (const status of ["promote", "demote"] as const) {
|
|
4031
|
+
addGlobalOptions(producersCmd.command(status).description(`${status} a model from the inventory`)).option("--model <id>", "Exact model id; omit to choose interactively").action(async function producerChangeAction(this: Command, options: { model?: string }) { result.code = await cmdProducerChange(runtimeFrom(ctx, this), status === "promote" ? "promoted" : "demoted", options.model); });
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3069
4034
|
const harnessCmd = addGlobalOptions(program.command("harness").description("Detect coding-agent harnesses and authentication"));
|
|
3070
4035
|
harnessCmd.helpCommand("help", "Show harness help");
|
|
3071
4036
|
addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
|
|
4037
|
+
|
|
4038
|
+
const authCmd = addGlobalOptions(program.command("auth").description("Manage credentials, tokens, and authorization"));
|
|
4039
|
+
authCmd.helpCommand("help", "Show auth help");
|
|
4040
|
+
addGlobalOptions(authCmd.command("token").description("Inspect, issue, or clear local disk session tokens"))
|
|
4041
|
+
.option("--status", "Check status of the active session token")
|
|
4042
|
+
.option("--clear", "Clear persisted disk session token")
|
|
4043
|
+
.option("--issue", "Force issuing a fresh session token")
|
|
4044
|
+
.action(async function authTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
|
|
4045
|
+
result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
|
|
4046
|
+
});
|
|
3072
4047
|
addGlobalOptions(program.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs")
|
|
3073
4048
|
.argument("[harness]", "Harness id (default: every detected harness)")
|
|
3074
4049
|
.option("--check", "Check for a kxm package update without applying")
|
|
@@ -3147,6 +4122,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3147
4122
|
.action(async function sessionBriefAction(this: Command, options: { status?: boolean; token?: boolean }) {
|
|
3148
4123
|
result.code = await cmdSessionBrief(runtimeFrom(ctx, this), options);
|
|
3149
4124
|
});
|
|
4125
|
+
addGlobalOptions(session.command("token").description("Inspect, issue, or clear local disk session tokens"))
|
|
4126
|
+
.option("--status", "Check status of the active session token")
|
|
4127
|
+
.option("--clear", "Clear persisted disk session token")
|
|
4128
|
+
.option("--issue", "Force issuing a fresh session token")
|
|
4129
|
+
.action(async function sessionTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
|
|
4130
|
+
result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
|
|
4131
|
+
});
|
|
3150
4132
|
addGlobalOptions(session.command("start").description("Create an agent/gate or workflow session manifest (does not launch processes)"))
|
|
3151
4133
|
.option("--id <id>", "Session id")
|
|
3152
4134
|
.option("--workflow <id>", "Workflow definition id")
|
|
@@ -3328,6 +4310,75 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3328
4310
|
.action(async function exportAction(this: Command, runId: string, options: { input?: string; outDir?: string }) {
|
|
3329
4311
|
result.code = await cmdRetrospectiveExport(runtimeFrom(ctx, this), runId, options);
|
|
3330
4312
|
});
|
|
4313
|
+
addGlobalOptions(workflow.command("definitions").description("List workflow definitions across scopes"))
|
|
4314
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4315
|
+
.action(async function definitionsAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
4316
|
+
result.code = await cmdWorkflowDefinitions(runtimeFrom(ctx, this), options);
|
|
4317
|
+
});
|
|
4318
|
+
addGlobalOptions(workflow.command("add [workflowId]").description("Add a workflow definition to global or local configuration"))
|
|
4319
|
+
.option("--file <path>", "Path to YAML workflow definition file")
|
|
4320
|
+
.option("--description <text>", "Workflow description")
|
|
4321
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4322
|
+
.option("--overwrite", "Overwrite existing workflow definition if present")
|
|
4323
|
+
.option("--pick [selection]", "Pick from available workflow templates (index or id)")
|
|
4324
|
+
.action(async function workflowAddAction(this: Command, workflowId?: string, options?: { file?: string; description?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
|
|
4325
|
+
result.code = await cmdWorkflowAdd(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4326
|
+
});
|
|
4327
|
+
addGlobalOptions(workflow.command("remove [workflowId]").description("Remove a workflow definition"))
|
|
4328
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4329
|
+
.option("--pick [selection]", "Pick a workflow to remove (index or id)")
|
|
4330
|
+
.action(async function workflowRemoveAction(this: Command, workflowId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4331
|
+
result.code = await cmdWorkflowRemove(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4332
|
+
});
|
|
4333
|
+
addGlobalOptions(workflow.command("modify [workflowId]").description("Modify a workflow definition"))
|
|
4334
|
+
.option("--description <text>", "Updated description")
|
|
4335
|
+
.option("--scope <scope>", "Configuration scope: global or local")
|
|
4336
|
+
.option("--pick [selection]", "Pick a workflow to modify (index or id)")
|
|
4337
|
+
.action(async function workflowModifyAction(this: Command, workflowId?: string, options?: { description?: string; scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4338
|
+
result.code = await cmdWorkflowModify(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4339
|
+
});
|
|
4340
|
+
|
|
4341
|
+
const role = addGlobalOptions(program.command("role").description("Manage role definitions, tool policies, and rosters"));
|
|
4342
|
+
role.helpCommand("help", "Show role help");
|
|
4343
|
+
addGlobalOptions(role.command("list", { isDefault: true }).description("List configured roles across global and local scopes"))
|
|
4344
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4345
|
+
.action(async function roleListAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
4346
|
+
result.code = await cmdRoleList(runtimeFrom(ctx, this), options);
|
|
4347
|
+
});
|
|
4348
|
+
addGlobalOptions(role.command("get <roleId>").description("Get role definition YAML and details"))
|
|
4349
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4350
|
+
.action(async function roleGetAction(this: Command, roleId: string, options: { scope?: "all" | "global" | "local" }) {
|
|
4351
|
+
result.code = await cmdRoleGet(runtimeFrom(ctx, this), roleId, options);
|
|
4352
|
+
});
|
|
4353
|
+
addGlobalOptions(role.command("add [roleId]").description("Add a role definition to global or local configuration"))
|
|
4354
|
+
.option("--file <path>", "Path to YAML role definition file")
|
|
4355
|
+
.option("--description <text>", "Role description")
|
|
4356
|
+
.option("--skills <skills>", "Comma-separated skills list")
|
|
4357
|
+
.option("--harness <harness>", "Primary harness name (e.g. grok, claude, agy, pi)")
|
|
4358
|
+
.option("--model <model>", "Primary model identifier (e.g. grok-4.6, fable, gemini-3.8-flash-high)")
|
|
4359
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4360
|
+
.option("--overwrite", "Overwrite existing role definition if present")
|
|
4361
|
+
.option("--pick [selection]", "Pick from available role templates (index or id)")
|
|
4362
|
+
.action(async function roleAddAction(this: Command, roleId?: string, options?: { file?: string; description?: string; skills?: string; harness?: string; model?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
|
|
4363
|
+
result.code = await cmdRoleAdd(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4364
|
+
});
|
|
4365
|
+
addGlobalOptions(role.command("remove [roleId]").description("Remove a role definition"))
|
|
4366
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4367
|
+
.option("--pick [selection]", "Pick a role to remove (index or id)")
|
|
4368
|
+
.action(async function roleRemoveAction(this: Command, roleId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4369
|
+
result.code = await cmdRoleRemove(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4370
|
+
});
|
|
4371
|
+
addGlobalOptions(role.command("modify [roleId]").description("Modify an existing role definition"))
|
|
4372
|
+
.option("--description <text>", "Updated description")
|
|
4373
|
+
.option("--add-skill <skill>", "Skill to add")
|
|
4374
|
+
.option("--remove-skill <skill>", "Skill to remove")
|
|
4375
|
+
.option("--add-model <harness:model>", "Model to add to roster")
|
|
4376
|
+
.option("--remove-model <model>", "Model to remove from roster")
|
|
4377
|
+
.option("--scope <scope>", "Configuration scope: global or local")
|
|
4378
|
+
.option("--pick [selection]", "Pick a role to modify (index or id)")
|
|
4379
|
+
.action(async function roleModifyAction(this: Command, roleId?: string, options?: { description?: string; addSkill?: string; removeSkill?: string; addModel?: string; removeModel?: string; scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4380
|
+
result.code = await cmdRoleModify(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4381
|
+
});
|
|
3331
4382
|
|
|
3332
4383
|
const gate = addGlobalOptions(program.command("gate").description("Validate definitions and operate evidence gates"));
|
|
3333
4384
|
gate.helpCommand("help", "Show gate help");
|
|
@@ -3356,8 +4407,9 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3356
4407
|
.argument("<summary>", "Callback summary")
|
|
3357
4408
|
.argument("[evidence...]", "required-key=evidence pairs")
|
|
3358
4409
|
.option("--delivery-id <id>", "Stable callback delivery ID")
|
|
3359
|
-
.action
|
|
3360
|
-
|
|
4410
|
+
.option("--recovery-action <action>", "vNext recovery action: retry, fail, cancel, unblock")
|
|
4411
|
+
.action(async function signalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string; recoveryAction?: string }) {
|
|
4412
|
+
result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId, options.recoveryAction);
|
|
3361
4413
|
});
|
|
3362
4414
|
const github = addGlobalOptions(gate.command("github").description("GitHub adapters"));
|
|
3363
4415
|
github.helpCommand("help", "Show GitHub help");
|
|
@@ -3537,6 +4589,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3537
4589
|
.action(async function routingReportAction(this: Command, options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string }) {
|
|
3538
4590
|
result.code = await cmdRoutingReport(runtimeFrom(ctx, this), options);
|
|
3539
4591
|
});
|
|
4592
|
+
addGlobalOptions(routing.command("benchmark").description("Dedicated offline benchmark for side-by-side model comparison (Decision Q12)"))
|
|
4593
|
+
.option("--task <fixture>", "Task prompt or fixture path for benchmark comparison")
|
|
4594
|
+
.option("--arms <models>", "Comma-separated model routes to benchmark (e.g. grok/grok-4.6,claude/fable)")
|
|
4595
|
+
.option("--runs <count>", "Benchmark runs per arm", "1")
|
|
4596
|
+
.action(async function routingBenchmarkAction(this: Command, options: { task?: string; arms?: string; runs?: string }) {
|
|
4597
|
+
result.code = await cmdRoutingBenchmark(runtimeFrom(ctx, this), options);
|
|
4598
|
+
});
|
|
3540
4599
|
|
|
3541
4600
|
const hub = addGlobalOptions(program.command("hub").description("Start, inspect, and stop the local KXM hub"));
|
|
3542
4601
|
hub.helpCommand("help", "Show hub help");
|
|
@@ -3634,6 +4693,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3634
4693
|
.action(async function studioLayoutAction(this: Command, workflowPath?: string) {
|
|
3635
4694
|
result.code = await cmdStudioLayout(runtimeFrom(ctx, this), workflowPath);
|
|
3636
4695
|
});
|
|
4696
|
+
addGlobalOptions(studioCmd.command("serve").description("Start embedded Web Studio server on http://localhost:4242 (Decision Q8 & D14)"))
|
|
4697
|
+
.option("-p, --port <port>", "Port to bind (default: 4242)", "4242")
|
|
4698
|
+
.option("--host <host>", "Host address to bind", "127.0.0.1")
|
|
4699
|
+
.option("--token <token>", "Session token for mutation authentication")
|
|
4700
|
+
.action(async function studioServeAction(this: Command, options: { port?: string; host?: string; token?: string }) {
|
|
4701
|
+
result.code = await cmdStudioServe(runtimeFrom(ctx, this), options);
|
|
4702
|
+
});
|
|
3637
4703
|
|
|
3638
4704
|
return program;
|
|
3639
4705
|
}
|