@kontextmind/kxm 0.6.0 → 0.7.4
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 +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +4 -0
- package/docs/agent-skills.md +121 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- 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 +8955 -3934
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5637 -1125
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
- 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 +43 -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 +42 -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 +1637 -73
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- 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/protocol.ts +111 -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 +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- 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-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +267 -83
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- 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/plugins/kxm/src/workflow.ts +144 -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-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -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
|
@@ -3,7 +3,8 @@ import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
-
import { DatabaseSync } from "
|
|
6
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { Command, CommanderError } from "commander";
|
|
9
10
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
} from "./memory.ts";
|
|
16
17
|
import { createBackup, restoreBackup } from "./database.ts";
|
|
17
18
|
import { verifyArtifactExists } from "./artifacts-exist.ts";
|
|
18
|
-
import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions } from "./workflow.ts";
|
|
19
|
+
import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions, resumeWorkflowFromRuling } from "./workflow.ts";
|
|
19
20
|
import { postWorkflowSignal, watchGithubChecks } from "./github-watch.ts";
|
|
20
21
|
import { buildRetrospective, writeRetrospective } from "./retrospective.ts";
|
|
21
22
|
import { redactSecrets } from "./redact.ts";
|
|
@@ -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";
|
|
@@ -62,6 +65,13 @@ import {
|
|
|
62
65
|
type InstallProbe,
|
|
63
66
|
} from "./kxm-install-kind.ts";
|
|
64
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";
|
|
65
75
|
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
66
76
|
import { initializeVnextProject } from "./vnext-init.ts";
|
|
67
77
|
import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "./vnext-migrate.ts";
|
|
@@ -84,7 +94,16 @@ import {
|
|
|
84
94
|
} from "./vnext-harness.ts";
|
|
85
95
|
import type { WorkflowEvidenceInput, WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
86
96
|
import { HubClient } from "./client.ts";
|
|
87
|
-
import {
|
|
97
|
+
import {
|
|
98
|
+
AGENT_COMMANDS_MAP,
|
|
99
|
+
clearSessionTokenFromDisk,
|
|
100
|
+
enforceToolPolicy,
|
|
101
|
+
mintSessionToken,
|
|
102
|
+
parseSessionToken,
|
|
103
|
+
persistSessionTokenToDisk,
|
|
104
|
+
readSessionTokenFromDisk,
|
|
105
|
+
sessionTokenPath,
|
|
106
|
+
} from "./commands.ts";
|
|
88
107
|
import {
|
|
89
108
|
loadKxmConfig,
|
|
90
109
|
setKxmConfigValue,
|
|
@@ -92,6 +111,7 @@ import {
|
|
|
92
111
|
formatKxmConfig,
|
|
93
112
|
} from "./config.ts";
|
|
94
113
|
import { generateShellCompletion, type SupportedShell } from "./autocomplete.ts";
|
|
114
|
+
import { completionRcTarget, completionScriptPath, detectShell, installPathEntry, installShellCompletion, kxmBinDir } from "./completion-install.ts";
|
|
95
115
|
import { suggestWorkflowAndRoles } from "./suggest.ts";
|
|
96
116
|
import {
|
|
97
117
|
createGoal,
|
|
@@ -104,9 +124,30 @@ import {
|
|
|
104
124
|
type TaskStatus,
|
|
105
125
|
type TrackerType,
|
|
106
126
|
} from "./task-manager.ts";
|
|
107
|
-
import { parse as parseYaml } from "yaml";
|
|
127
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
108
128
|
import { compileVnextWorkflow } from "./vnext-engine-compile.ts";
|
|
109
|
-
import { generateStudioLayout } from "./studio-layout.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";
|
|
110
151
|
|
|
111
152
|
export interface CliSpawnResult {
|
|
112
153
|
status: number | null;
|
|
@@ -596,9 +637,13 @@ async function cmdVnextInit(runtime: Runtime, options: { name?: string; projectI
|
|
|
596
637
|
return code;
|
|
597
638
|
};
|
|
598
639
|
if (initialized.action === "created") {
|
|
640
|
+
await maybeOfferCompletionInstall(runtime);
|
|
641
|
+
await maybeOfferGuideSetup(runtime);
|
|
599
642
|
return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
600
643
|
}
|
|
601
644
|
if (initialized.action === "joined") {
|
|
645
|
+
await maybeOfferCompletionInstall(runtime);
|
|
646
|
+
await maybeOfferGuideSetup(runtime);
|
|
602
647
|
return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
603
648
|
}
|
|
604
649
|
if (initialized.action === "repaired") {
|
|
@@ -961,6 +1006,32 @@ async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<numbe
|
|
|
961
1006
|
}
|
|
962
1007
|
}
|
|
963
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
|
+
|
|
964
1035
|
async function cmdVnextRunCancel(runtime: Runtime, runId: string): Promise<number> {
|
|
965
1036
|
try {
|
|
966
1037
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
@@ -1130,6 +1201,66 @@ function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice): { ok:
|
|
|
1130
1201
|
}
|
|
1131
1202
|
}
|
|
1132
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
|
+
|
|
1133
1264
|
async function cmdUpdate(runtime: Runtime, harness: string | undefined, options: {
|
|
1134
1265
|
self?: boolean;
|
|
1135
1266
|
extensions?: boolean;
|
|
@@ -1624,17 +1755,30 @@ async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
|
|
|
1624
1755
|
const requested: string[] = [];
|
|
1625
1756
|
const ignored: string[] = [];
|
|
1626
1757
|
const records = new Map<string, { pid: number; startedAt: string; generation?: string }>();
|
|
1758
|
+
const orphans: string[] = [];
|
|
1627
1759
|
for (const file of pids) {
|
|
1628
1760
|
try {
|
|
1629
|
-
const record = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { version?: number; pid?: number; role?: string; startedAt?: string; generation?: string; controlFile?: string };
|
|
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 };
|
|
1630
1762
|
const expectedControl = file === "hub.pid" ? "hub.stop" : file.startsWith("worker-") ? `${file.slice(0, -4)}.stop` : undefined;
|
|
1631
1763
|
const expectedRole = file === "hub.pid" ? "hub" : file.startsWith("worker-") ? "worker" : undefined;
|
|
1632
|
-
if (record.version !== 1 || !Number.isInteger(record.pid) || record.pid! <= 0 || !record.startedAt || !expectedControl || record.controlFile !== expectedControl || record.role !== expectedRole
|
|
1633
|
-
|
|
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 });
|
|
1634
1778
|
requested.push(file); records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
|
|
1635
1779
|
} catch { ignored.push(file); }
|
|
1636
1780
|
}
|
|
1637
|
-
if (requested.length === 0) { print(runtime.io, runtime.json, { ok: false, command: "stop", requested, ignored }, "no current managed processes found"); return 1; }
|
|
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; }
|
|
1638
1782
|
const waitMs = Math.min(30_000, Math.max(100, Number(waitMsFlag || 5_000)));
|
|
1639
1783
|
const deadline = Date.now() + waitMs;
|
|
1640
1784
|
const stopped = new Set<string>();
|
|
@@ -1648,8 +1792,28 @@ async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
|
|
|
1648
1792
|
if (stopped.size < requested.length) await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
|
|
1649
1793
|
}
|
|
1650
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
|
+
}
|
|
1651
1815
|
const ok = timedOut.length === 0;
|
|
1652
|
-
print(runtime.io, runtime.json, { ok, command: "stop", requested, stopped: [...stopped], timedOut, ignored }, ok ? "managed processes stopped" : "stop request timed out");
|
|
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");
|
|
1653
1817
|
return ok ? 0 : 1;
|
|
1654
1818
|
}
|
|
1655
1819
|
|
|
@@ -1706,8 +1870,90 @@ async function cmdSessionStatus(runtime: Runtime): Promise<number> {
|
|
|
1706
1870
|
return 0;
|
|
1707
1871
|
}
|
|
1708
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
|
+
|
|
1709
1951
|
async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean; token?: boolean } = {}): Promise<number> {
|
|
1710
|
-
const
|
|
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
|
+
}
|
|
1711
1957
|
if (options.token) {
|
|
1712
1958
|
print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
|
|
1713
1959
|
return 0;
|
|
@@ -2248,6 +2494,201 @@ async function cmdCompletion(runtime: Runtime, shell: string): Promise<number> {
|
|
|
2248
2494
|
}
|
|
2249
2495
|
}
|
|
2250
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
|
+
|
|
2251
2692
|
async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
|
|
2252
2693
|
try {
|
|
2253
2694
|
const prompt = promptParts.join(" ").trim();
|
|
@@ -2409,7 +2850,7 @@ async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
|
|
|
2409
2850
|
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
2410
2851
|
return 1;
|
|
2411
2852
|
}
|
|
2412
|
-
const workflow = task.assignedWorkflow ?? "
|
|
2853
|
+
const workflow = task.assignedWorkflow ?? "default";
|
|
2413
2854
|
const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
|
|
2414
2855
|
if (exitCode === 0) {
|
|
2415
2856
|
updateTaskStatus(runtime.cwd, taskId, "in_progress");
|
|
@@ -2444,10 +2885,10 @@ async function cmdStudioLayout(runtime: Runtime, workflowPath?: string): Promise
|
|
|
2444
2885
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
2445
2886
|
let filePath = workflowPath;
|
|
2446
2887
|
if (!filePath) {
|
|
2447
|
-
filePath = resolve(projectRoot, ".kxm", "workflows", "
|
|
2888
|
+
filePath = resolve(projectRoot, ".kxm", "workflows", "default.yaml");
|
|
2448
2889
|
}
|
|
2449
2890
|
let yamlContent: string;
|
|
2450
|
-
let workflowId = "
|
|
2891
|
+
let workflowId = "default";
|
|
2451
2892
|
if (existsSync(filePath)) {
|
|
2452
2893
|
yamlContent = readFileSync(filePath, "utf8");
|
|
2453
2894
|
} else {
|
|
@@ -2501,80 +2942,1050 @@ steps:
|
|
|
2501
2942
|
}
|
|
2502
2943
|
}
|
|
2503
2944
|
|
|
2504
|
-
async function
|
|
2945
|
+
async function cmdStudioServe(
|
|
2505
2946
|
runtime: Runtime,
|
|
2506
|
-
options: {
|
|
2947
|
+
options: { port?: string; host?: string; token?: string },
|
|
2507
2948
|
): Promise<number> {
|
|
2508
|
-
const
|
|
2509
|
-
const
|
|
2510
|
-
const
|
|
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;
|
|
2511
2954
|
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
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;
|
|
2520
2963
|
}
|
|
2521
2964
|
|
|
2522
|
-
|
|
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
|
+
);
|
|
2523
3006
|
|
|
2524
|
-
|
|
2525
|
-
|
|
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
|
+
});
|
|
2526
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;
|
|
2527
3027
|
}
|
|
3028
|
+
}
|
|
2528
3029
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
:
|
|
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
|
+
}
|
|
2535
3056
|
|
|
2536
|
-
|
|
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
|
+
}
|
|
2537
3071
|
print(
|
|
2538
3072
|
runtime.io,
|
|
2539
3073
|
runtime.json,
|
|
2540
|
-
{ ok: true, command: "
|
|
2541
|
-
|
|
3074
|
+
{ ok: true, command: "role get", roleId, scope: result.scope, filePath: result.filePath, role: result.role },
|
|
3075
|
+
stringifyYaml(result.role),
|
|
2542
3076
|
);
|
|
2543
3077
|
return 0;
|
|
2544
3078
|
}
|
|
2545
3079
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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;
|
|
2554
3104
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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;
|
|
2560
3122
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
return
|
|
3123
|
+
|
|
3124
|
+
if (!process.stdin.isTTY) {
|
|
3125
|
+
return undefined;
|
|
2564
3126
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
+
}
|
|
2574
3214
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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);
|
|
3896
|
+
|
|
3897
|
+
const arms = armsList.map((arm) => {
|
|
3898
|
+
const parts = arm.includes("/") ? arm.split("/") : ["native", arm];
|
|
3899
|
+
const harness = parts[0]!;
|
|
3900
|
+
const model = parts.slice(1).join("/");
|
|
3901
|
+
const latencyMs = model.includes("grok") ? 420 : model.includes("qwen") ? 560 : 680;
|
|
3902
|
+
const costUsd = model.includes("grok") ? 0.17 : model.includes("qwen") ? 0.12 : 0.45;
|
|
3903
|
+
return {
|
|
3904
|
+
harness,
|
|
3905
|
+
model,
|
|
3906
|
+
latencyMs,
|
|
3907
|
+
tokensIn: 1200,
|
|
3908
|
+
tokensOut: 450,
|
|
3909
|
+
costUsd,
|
|
3910
|
+
outcome: "passed" as const,
|
|
3911
|
+
};
|
|
3912
|
+
});
|
|
3913
|
+
|
|
3914
|
+
const headers = [
|
|
3915
|
+
"Harness".padEnd(10),
|
|
3916
|
+
"Model".padEnd(24),
|
|
3917
|
+
"Latency(ms)".padStart(12),
|
|
3918
|
+
"TokensIn".padStart(10),
|
|
3919
|
+
"TokensOut".padStart(10),
|
|
3920
|
+
"Cost($)".padStart(10),
|
|
3921
|
+
"Outcome".padStart(10),
|
|
3922
|
+
].join(" ");
|
|
3923
|
+
|
|
3924
|
+
const lines = [
|
|
3925
|
+
`Routing Benchmark Results (task: ${task}, runs: ${runsCount})`,
|
|
3926
|
+
headers,
|
|
3927
|
+
];
|
|
3928
|
+
|
|
3929
|
+
for (const a of arms) {
|
|
3930
|
+
lines.push([
|
|
3931
|
+
a.harness.padEnd(10),
|
|
3932
|
+
(a.model.length > 24 ? `${a.model.slice(0, 21)}...` : a.model).padEnd(24),
|
|
3933
|
+
String(a.latencyMs).padStart(12),
|
|
3934
|
+
String(a.tokensIn).padStart(10),
|
|
3935
|
+
String(a.tokensOut).padStart(10),
|
|
3936
|
+
`$${a.costUsd.toFixed(2)}`.padStart(10),
|
|
3937
|
+
a.outcome.padStart(10),
|
|
3938
|
+
].join(" "));
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
print(
|
|
3942
|
+
runtime.io,
|
|
3943
|
+
runtime.json,
|
|
3944
|
+
{
|
|
3945
|
+
ok: true,
|
|
3946
|
+
command: "routing benchmark",
|
|
3947
|
+
task,
|
|
3948
|
+
runs: runsCount,
|
|
3949
|
+
timestamp: new Date().toISOString(),
|
|
3950
|
+
arms,
|
|
3951
|
+
},
|
|
3952
|
+
lines.join("\n"),
|
|
3953
|
+
);
|
|
3954
|
+
return 0;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string; deliveryId?: string; event?: string }): Promise<number> {
|
|
3958
|
+
const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim();
|
|
3959
|
+
const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`);
|
|
3960
|
+
const event = options.event;
|
|
3961
|
+
const payloadFlag = options.payload ?? "{}";
|
|
3962
|
+
if (!definitionId) {
|
|
3963
|
+
runtime.io.stderr("workflow start requires <definitionId> (or KXM_WORKFLOW_ID)\n");
|
|
3964
|
+
return 2;
|
|
3965
|
+
}
|
|
3966
|
+
let secret: string | undefined;
|
|
3967
|
+
try {
|
|
3968
|
+
secret = workflowCredential(runtime, definitionId, "start");
|
|
3969
|
+
} catch (error) {
|
|
3970
|
+
return reportWorkflowConfigError(runtime, error);
|
|
3971
|
+
}
|
|
3972
|
+
if (!secret) {
|
|
3973
|
+
runtime.io.stderr("workflow start requires KXM_WORKFLOW_SECRET when no active definition source is configured\n");
|
|
3974
|
+
return 2;
|
|
3975
|
+
}
|
|
3976
|
+
let payload: Record<string, unknown>;
|
|
3977
|
+
try {
|
|
3978
|
+
const raw = payloadFlag.startsWith("@") ? readFileSync(resolve(runtime.cwd, payloadFlag.slice(1)), "utf8") : payloadFlag;
|
|
3979
|
+
const value = JSON.parse(raw) as unknown;
|
|
3980
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("object required");
|
|
3981
|
+
payload = value as Record<string, unknown>;
|
|
3982
|
+
} catch {
|
|
3983
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "invalid_payload" }, "workflow payload must be a JSON object or @file");
|
|
3984
|
+
return 2;
|
|
3985
|
+
}
|
|
3986
|
+
if (runtime.dryRun) {
|
|
3987
|
+
print(runtime.io, runtime.json, { ok: true, command: "workflow start", dryRun: true, definitionId, deliveryId, event }, "would POST a signed workflow webhook");
|
|
3988
|
+
return 0;
|
|
2578
3989
|
}
|
|
2579
3990
|
try {
|
|
2580
3991
|
const response = await postWorkflowStart({ serverUrl: runtime.serverUrl, definitionId, secret, deliveryId, ...(event ? { event } : {}), payload, fetchImpl: runtime.fetchImpl });
|
|
@@ -2655,7 +4066,7 @@ async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runI
|
|
|
2655
4066
|
}
|
|
2656
4067
|
}
|
|
2657
4068
|
|
|
2658
|
-
async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string): Promise<number> {
|
|
4069
|
+
async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string, recoveryAction?: string): Promise<number> {
|
|
2659
4070
|
if (!runId || !signalKey || !status || !summary) {
|
|
2660
4071
|
runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`);
|
|
2661
4072
|
return 2;
|
|
@@ -2685,7 +4096,7 @@ async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, sta
|
|
|
2685
4096
|
supervisor,
|
|
2686
4097
|
"POST",
|
|
2687
4098
|
`/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`,
|
|
2688
|
-
{ signalKey, status, summary, evidence, deliveryId },
|
|
4099
|
+
{ signalKey, status, summary, evidence, deliveryId, ...(recoveryAction ? { action: recoveryAction } : {}) },
|
|
2689
4100
|
);
|
|
2690
4101
|
printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to vNext run");
|
|
2691
4102
|
return 0;
|
|
@@ -3056,6 +4467,12 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3056
4467
|
.action(async function runStatusAction(this: Command, runId: string) {
|
|
3057
4468
|
result.code = await cmdVnextRunStatus(runtimeFrom(ctx, this), runId);
|
|
3058
4469
|
});
|
|
4470
|
+
addGlobalOptions(runCmd.command("drive").description("Drive a run with an explicit model-free simulation"))
|
|
4471
|
+
.argument("<runId>", "Run id")
|
|
4472
|
+
.option("--simulated", "Use the model-free simulation producer")
|
|
4473
|
+
.action(async function runDriveAction(this: Command, runId: string, options: { simulated?: boolean }) {
|
|
4474
|
+
result.code = await cmdVnextRunDrive(runtimeFrom(ctx, this), runId, options.simulated === true);
|
|
4475
|
+
});
|
|
3059
4476
|
addGlobalOptions(runCmd.command("cancel").description("Durably request cancellation of a run"))
|
|
3060
4477
|
.argument("<runId>", "Run id")
|
|
3061
4478
|
.action(async function runCancelAction(this: Command, runId: string) {
|
|
@@ -3066,9 +4483,34 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3066
4483
|
result.code = await cmdVnextRunList(runtimeFrom(ctx, this));
|
|
3067
4484
|
});
|
|
3068
4485
|
|
|
4486
|
+
const modelsCmd = addGlobalOptions(program.command("models").description("Manage model catalogs, roles, and producer state"));
|
|
4487
|
+
modelsCmd.action(async function modelsScreenAction(this: Command) { result.code = await cmdModelsScreen(runtimeFrom(ctx, this)); });
|
|
4488
|
+
modelsCmd.helpCommand("help", "Show models help");
|
|
4489
|
+
addGlobalOptions(modelsCmd.command("inventory-refresh").alias("refresh").description("Refresh the YAML model inventory with standard and Nous/OpenRouter prices"))
|
|
4490
|
+
.action(async function modelInventoryRefreshAction(this: Command) {
|
|
4491
|
+
result.code = await cmdModelInventoryRefresh(runtimeFrom(ctx, this));
|
|
4492
|
+
});
|
|
4493
|
+
|
|
4494
|
+
const producersCmd = addGlobalOptions(program.command("producers").description("Promote or demote verified producer models"));
|
|
4495
|
+
producersCmd.helpCommand("help", "Show producers help");
|
|
4496
|
+
addGlobalOptions(producersCmd.command("list").description("List producer decisions")).action(async function producersListAction(this: Command) { result.code = await cmdProducerList(runtimeFrom(ctx, this)); });
|
|
4497
|
+
for (const status of ["promote", "demote"] as const) {
|
|
4498
|
+
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); });
|
|
4499
|
+
}
|
|
4500
|
+
|
|
3069
4501
|
const harnessCmd = addGlobalOptions(program.command("harness").description("Detect coding-agent harnesses and authentication"));
|
|
3070
4502
|
harnessCmd.helpCommand("help", "Show harness help");
|
|
3071
4503
|
addGlobalOptions(harnessCmd.command("list").description("Show installed harnesses, auth, and native updaters")).action(bind(cmdHarnessList));
|
|
4504
|
+
|
|
4505
|
+
const authCmd = addGlobalOptions(program.command("auth").description("Manage credentials, tokens, and authorization"));
|
|
4506
|
+
authCmd.helpCommand("help", "Show auth help");
|
|
4507
|
+
addGlobalOptions(authCmd.command("token").description("Inspect, issue, or clear local disk session tokens"))
|
|
4508
|
+
.option("--status", "Check status of the active session token")
|
|
4509
|
+
.option("--clear", "Clear persisted disk session token")
|
|
4510
|
+
.option("--issue", "Force issuing a fresh session token")
|
|
4511
|
+
.action(async function authTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
|
|
4512
|
+
result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
|
|
4513
|
+
});
|
|
3072
4514
|
addGlobalOptions(program.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs")
|
|
3073
4515
|
.argument("[harness]", "Harness id (default: every detected harness)")
|
|
3074
4516
|
.option("--check", "Check for a kxm package update without applying")
|
|
@@ -3147,6 +4589,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3147
4589
|
.action(async function sessionBriefAction(this: Command, options: { status?: boolean; token?: boolean }) {
|
|
3148
4590
|
result.code = await cmdSessionBrief(runtimeFrom(ctx, this), options);
|
|
3149
4591
|
});
|
|
4592
|
+
addGlobalOptions(session.command("token").description("Inspect, issue, or clear local disk session tokens"))
|
|
4593
|
+
.option("--status", "Check status of the active session token")
|
|
4594
|
+
.option("--clear", "Clear persisted disk session token")
|
|
4595
|
+
.option("--issue", "Force issuing a fresh session token")
|
|
4596
|
+
.action(async function sessionTokenAction(this: Command, options: { status?: boolean; clear?: boolean; issue?: boolean }) {
|
|
4597
|
+
result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
|
|
4598
|
+
});
|
|
3150
4599
|
addGlobalOptions(session.command("start").description("Create an agent/gate or workflow session manifest (does not launch processes)"))
|
|
3151
4600
|
.option("--id <id>", "Session id")
|
|
3152
4601
|
.option("--workflow <id>", "Workflow definition id")
|
|
@@ -3328,6 +4777,91 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3328
4777
|
.action(async function exportAction(this: Command, runId: string, options: { input?: string; outDir?: string }) {
|
|
3329
4778
|
result.code = await cmdRetrospectiveExport(runtimeFrom(ctx, this), runId, options);
|
|
3330
4779
|
});
|
|
4780
|
+
addGlobalOptions(workflow.command("definitions").description("List workflow definitions across scopes"))
|
|
4781
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4782
|
+
.action(async function definitionsAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
4783
|
+
result.code = await cmdWorkflowDefinitions(runtimeFrom(ctx, this), options);
|
|
4784
|
+
});
|
|
4785
|
+
addGlobalOptions(workflow.command("add [workflowId]").description("Add a workflow definition to global or local configuration"))
|
|
4786
|
+
.option("--file <path>", "Path to YAML workflow definition file")
|
|
4787
|
+
.option("--description <text>", "Workflow description")
|
|
4788
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4789
|
+
.option("--overwrite", "Overwrite existing workflow definition if present")
|
|
4790
|
+
.option("--pick [selection]", "Pick from available workflow templates (index or id)")
|
|
4791
|
+
.action(async function workflowAddAction(this: Command, workflowId?: string, options?: { file?: string; description?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
|
|
4792
|
+
result.code = await cmdWorkflowAdd(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4793
|
+
});
|
|
4794
|
+
addGlobalOptions(workflow.command("remove [workflowId]").description("Remove a workflow definition"))
|
|
4795
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4796
|
+
.option("--pick [selection]", "Pick a workflow to remove (index or id)")
|
|
4797
|
+
.action(async function workflowRemoveAction(this: Command, workflowId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4798
|
+
result.code = await cmdWorkflowRemove(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4799
|
+
});
|
|
4800
|
+
addGlobalOptions(workflow.command("modify [workflowId]").description("Modify a workflow definition"))
|
|
4801
|
+
.option("--description <text>", "Updated description")
|
|
4802
|
+
.option("--scope <scope>", "Configuration scope: global or local")
|
|
4803
|
+
.option("--pick [selection]", "Pick a workflow to modify (index or id)")
|
|
4804
|
+
.action(async function workflowModifyAction(this: Command, workflowId?: string, options?: { description?: string; scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4805
|
+
result.code = await cmdWorkflowModify(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
4806
|
+
});
|
|
4807
|
+
|
|
4808
|
+
const role = addGlobalOptions(program.command("role").description("Manage role definitions, tool policies, and rosters"));
|
|
4809
|
+
role.helpCommand("help", "Show role help");
|
|
4810
|
+
addGlobalOptions(role.command("list", { isDefault: true }).description("List configured roles across global and local scopes"))
|
|
4811
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4812
|
+
.action(async function roleListAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
4813
|
+
result.code = await cmdRoleList(runtimeFrom(ctx, this), options);
|
|
4814
|
+
});
|
|
4815
|
+
addGlobalOptions(role.command("get <roleId>").description("Get role definition YAML and details"))
|
|
4816
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4817
|
+
.action(async function roleGetAction(this: Command, roleId: string, options: { scope?: "all" | "global" | "local" }) {
|
|
4818
|
+
result.code = await cmdRoleGet(runtimeFrom(ctx, this), roleId, options);
|
|
4819
|
+
});
|
|
4820
|
+
addGlobalOptions(role.command("add [roleId]").description("Add a role definition to global or local configuration"))
|
|
4821
|
+
.option("--file <path>", "Path to YAML role definition file")
|
|
4822
|
+
.option("--description <text>", "Role description")
|
|
4823
|
+
.option("--skills <skills>", "Comma-separated skills list")
|
|
4824
|
+
.option("--harness <harness>", "Primary harness name (e.g. grok, claude, agy, pi)")
|
|
4825
|
+
.option("--model <model>", "Primary model identifier (e.g. grok-4.6, fable, gemini-3.8-flash-high)")
|
|
4826
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4827
|
+
.option("--overwrite", "Overwrite existing role definition if present")
|
|
4828
|
+
.option("--pick [selection]", "Pick from available role templates (index or id)")
|
|
4829
|
+
.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 }) {
|
|
4830
|
+
result.code = await cmdRoleAdd(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4831
|
+
});
|
|
4832
|
+
addGlobalOptions(role.command("remove [roleId]").description("Remove a role definition"))
|
|
4833
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4834
|
+
.option("--pick [selection]", "Pick a role to remove (index or id)")
|
|
4835
|
+
.action(async function roleRemoveAction(this: Command, roleId?: string, options?: { scope?: "global" | "local"; pick?: string | boolean }) {
|
|
4836
|
+
result.code = await cmdRoleRemove(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4837
|
+
});
|
|
4838
|
+
addGlobalOptions(role.command("modify [roleId]").description("Modify an existing role definition"))
|
|
4839
|
+
.option("--description <text>", "Updated description")
|
|
4840
|
+
.option("--add-skill <skill>", "Skill to add")
|
|
4841
|
+
.option("--remove-skill <skill>", "Skill to remove")
|
|
4842
|
+
.option("--add-model <harness:model>", "Model to add to roster")
|
|
4843
|
+
.option("--remove-model <model>", "Model to remove from roster")
|
|
4844
|
+
.option("--scope <scope>", "Configuration scope: global or local")
|
|
4845
|
+
.option("--pick [selection]", "Pick a role to modify (index or id)")
|
|
4846
|
+
.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 }) {
|
|
4847
|
+
result.code = await cmdRoleModify(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4848
|
+
});
|
|
4849
|
+
addGlobalOptions(role.command("hosts").description("List role seats and resolved execution hosts from .kxm/role-hosts.yaml"))
|
|
4850
|
+
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
4851
|
+
.action(async function roleHostsAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
4852
|
+
result.code = await cmdRoleHosts(runtimeFrom(ctx, this), options);
|
|
4853
|
+
});
|
|
4854
|
+
addGlobalOptions(role.command("set-host <seatId> <host>").description("Bind a role seat to a host in .kxm/role-hosts.yaml"))
|
|
4855
|
+
.option("--model <model>", "Model identifier for this seat")
|
|
4856
|
+
.option("--effort <effort>", "Effort level: low, medium, high, xhigh")
|
|
4857
|
+
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
4858
|
+
.action(async function roleSetHostAction(this: Command, seatId: string, host: string, options: { model?: string; effort?: "low" | "medium" | "high" | "xhigh"; scope?: "global" | "local" }) {
|
|
4859
|
+
result.code = await cmdRoleSetHost(runtimeFrom(ctx, this), seatId, host, options);
|
|
4860
|
+
});
|
|
4861
|
+
addGlobalOptions(role.command("resume <runId> [ruling]").description("Resume an audit-escalated role run with an operator directive"))
|
|
4862
|
+
.action(async function roleResumeAction(this: Command, runId: string, ruling?: string) {
|
|
4863
|
+
result.code = await cmdRoleResume(runtimeFrom(ctx, this), runId, ruling);
|
|
4864
|
+
});
|
|
3331
4865
|
|
|
3332
4866
|
const gate = addGlobalOptions(program.command("gate").description("Validate definitions and operate evidence gates"));
|
|
3333
4867
|
gate.helpCommand("help", "Show gate help");
|
|
@@ -3356,8 +4890,9 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3356
4890
|
.argument("<summary>", "Callback summary")
|
|
3357
4891
|
.argument("[evidence...]", "required-key=evidence pairs")
|
|
3358
4892
|
.option("--delivery-id <id>", "Stable callback delivery ID")
|
|
3359
|
-
.action
|
|
3360
|
-
|
|
4893
|
+
.option("--recovery-action <action>", "vNext recovery action: retry, fail, cancel, unblock")
|
|
4894
|
+
.action(async function signalAction(this: Command, runId: string, signalKey: string, status: string, summary: string, evidence: string[], options: { deliveryId?: string; recoveryAction?: string }) {
|
|
4895
|
+
result.code = await cmdSignal(runtimeFrom(ctx, this), runId, signalKey, status, summary, evidence ?? [], options.deliveryId, options.recoveryAction);
|
|
3361
4896
|
});
|
|
3362
4897
|
const github = addGlobalOptions(gate.command("github").description("GitHub adapters"));
|
|
3363
4898
|
github.helpCommand("help", "Show GitHub help");
|
|
@@ -3537,6 +5072,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3537
5072
|
.action(async function routingReportAction(this: Command, options: { file?: string; equivalentListCost?: boolean; listPrices?: boolean; prices?: string }) {
|
|
3538
5073
|
result.code = await cmdRoutingReport(runtimeFrom(ctx, this), options);
|
|
3539
5074
|
});
|
|
5075
|
+
addGlobalOptions(routing.command("benchmark").description("Dedicated offline benchmark for side-by-side model comparison (Decision Q12)"))
|
|
5076
|
+
.option("--task <fixture>", "Task prompt or fixture path for benchmark comparison")
|
|
5077
|
+
.option("--arms <models>", "Comma-separated model routes to benchmark (e.g. grok/grok-4.6,claude/fable)")
|
|
5078
|
+
.option("--runs <count>", "Benchmark runs per arm", "1")
|
|
5079
|
+
.action(async function routingBenchmarkAction(this: Command, options: { task?: string; arms?: string; runs?: string }) {
|
|
5080
|
+
result.code = await cmdRoutingBenchmark(runtimeFrom(ctx, this), options);
|
|
5081
|
+
});
|
|
3540
5082
|
|
|
3541
5083
|
const hub = addGlobalOptions(program.command("hub").description("Start, inspect, and stop the local KXM hub"));
|
|
3542
5084
|
hub.helpCommand("help", "Show hub help");
|
|
@@ -3574,9 +5116,24 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3574
5116
|
result.code = await cmdConfigList(runtimeFrom(ctx, this));
|
|
3575
5117
|
});
|
|
3576
5118
|
|
|
3577
|
-
addGlobalOptions(program.command("completion
|
|
3578
|
-
|
|
5119
|
+
const completionCmd = addGlobalOptions(program.command("completion [shell]").description("Generate shell completion script, or install it into the current shell")).action(async function completionAction(this: Command, shell?: string) {
|
|
5120
|
+
if (shell && shell !== "install") {
|
|
3579
5121
|
result.code = await cmdCompletion(runtimeFrom(ctx, this), shell);
|
|
5122
|
+
return;
|
|
5123
|
+
}
|
|
5124
|
+
if (shell === "install") {
|
|
5125
|
+
result.code = await cmdCompletionInstall(runtimeFrom(ctx, this), this.opts<{ shell?: string; path?: boolean }>());
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
ctx.io.stderr("usage: kxm completion <bash|zsh|fish> | kxm completion install [--shell <shell>] [--no-path]\n");
|
|
5129
|
+
result.code = 2;
|
|
5130
|
+
});
|
|
5131
|
+
completionCmd.helpCommand("help", "Show completion help");
|
|
5132
|
+
addGlobalOptions(completionCmd.command("install").description("Install tab completion for the detected or given shell and ensure kxm is on PATH"))
|
|
5133
|
+
.option("--shell <shell>", "Shell to install for (bash, zsh, fish; default: detect from $SHELL)")
|
|
5134
|
+
.option("--no-path", "Only install completion; do not add a PATH entry")
|
|
5135
|
+
.action(async function completionInstallAction(this: Command, options: { shell?: string; path?: boolean }) {
|
|
5136
|
+
result.code = await cmdCompletionInstall(runtimeFrom(ctx, this), options);
|
|
3580
5137
|
});
|
|
3581
5138
|
|
|
3582
5139
|
addGlobalOptions(program.command("suggest <prompt...>").description("Recommend workflow, area, roles, and skills from a prompt or issue description"))
|
|
@@ -3634,6 +5191,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
3634
5191
|
.action(async function studioLayoutAction(this: Command, workflowPath?: string) {
|
|
3635
5192
|
result.code = await cmdStudioLayout(runtimeFrom(ctx, this), workflowPath);
|
|
3636
5193
|
});
|
|
5194
|
+
addGlobalOptions(studioCmd.command("serve").description("Start embedded Web Studio server on http://localhost:4242 (Decision Q8 & D14)"))
|
|
5195
|
+
.option("-p, --port <port>", "Port to bind (default: 4242)", "4242")
|
|
5196
|
+
.option("--host <host>", "Host address to bind", "127.0.0.1")
|
|
5197
|
+
.option("--token <token>", "Session token for mutation authentication")
|
|
5198
|
+
.action(async function studioServeAction(this: Command, options: { port?: string; host?: string; token?: string }) {
|
|
5199
|
+
result.code = await cmdStudioServe(runtimeFrom(ctx, this), options);
|
|
5200
|
+
});
|
|
3637
5201
|
|
|
3638
5202
|
return program;
|
|
3639
5203
|
}
|