@kontextmind/kxm 0.7.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/roles/writer.yaml +2 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +5 -2
- package/docs/configuration.md +10 -1
- package/docs/getting-started.md +21 -0
- package/docs/operations.md +24 -0
- package/docs/skills/repo-work-delivery.md +5 -0
- package/docs/skills.md +2 -0
- package/docs/troubleshooting.md +22 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2257 -439
- package/plugins/kxm/dist/core.js +57 -0
- package/plugins/kxm/dist/extension.js +40 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +140 -17
- package/plugins/kxm/dist/server.js +129 -4
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +136 -13
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/cli.ts +507 -9
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/external-effects.ts +1 -1
- package/plugins/kxm/src/hub-env.ts +193 -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/protocol.ts +111 -0
- package/plugins/kxm/src/role.ts +335 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +38 -1
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +16 -0
- package/plugins/kxm/src/vnext-harness.ts +4 -2
- package/plugins/kxm/src/vnext-oneshot-process.ts +21 -4
- package/plugins/kxm/src/vnext-oneshot-producer.ts +18 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
- package/plugins/kxm/src/workflow-tui.ts +1 -1
- package/plugins/kxm/src/workflow.ts +144 -0
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +145 -3
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { Command, CommanderError } from "commander";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./memory.ts";
|
|
17
17
|
import { createBackup, restoreBackup } from "./database.ts";
|
|
18
18
|
import { verifyArtifactExists } from "./artifacts-exist.ts";
|
|
19
|
-
import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions } from "./workflow.ts";
|
|
19
|
+
import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions, resumeWorkflowFromRuling } from "./workflow.ts";
|
|
20
20
|
import { postWorkflowSignal, watchGithubChecks } from "./github-watch.ts";
|
|
21
21
|
import { buildRetrospective, writeRetrospective } from "./retrospective.ts";
|
|
22
22
|
import { redactSecrets } from "./redact.ts";
|
|
@@ -65,6 +65,13 @@ import {
|
|
|
65
65
|
type InstallProbe,
|
|
66
66
|
} from "./kxm-install-kind.ts";
|
|
67
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";
|
|
68
75
|
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
69
76
|
import { initializeVnextProject } from "./vnext-init.ts";
|
|
70
77
|
import { applyVnextMigration, planVnextMigration, verifyVnextMigration } from "./vnext-migrate.ts";
|
|
@@ -104,6 +111,7 @@ import {
|
|
|
104
111
|
formatKxmConfig,
|
|
105
112
|
} from "./config.ts";
|
|
106
113
|
import { generateShellCompletion, type SupportedShell } from "./autocomplete.ts";
|
|
114
|
+
import { completionRcTarget, completionScriptPath, detectShell, installPathEntry, installShellCompletion, kxmBinDir } from "./completion-install.ts";
|
|
107
115
|
import { suggestWorkflowAndRoles } from "./suggest.ts";
|
|
108
116
|
import {
|
|
109
117
|
createGoal,
|
|
@@ -126,6 +134,10 @@ import {
|
|
|
126
134
|
removeRole,
|
|
127
135
|
modifyRole,
|
|
128
136
|
DEFAULT_ROLES,
|
|
137
|
+
DEFAULT_ROLE_SEATS,
|
|
138
|
+
loadRoleHostsConfig,
|
|
139
|
+
setRoleSeatHost,
|
|
140
|
+
resolveRoleSeat,
|
|
129
141
|
type KxmRoleDefinition,
|
|
130
142
|
} from "./role.ts";
|
|
131
143
|
import {
|
|
@@ -625,9 +637,13 @@ async function cmdVnextInit(runtime: Runtime, options: { name?: string; projectI
|
|
|
625
637
|
return code;
|
|
626
638
|
};
|
|
627
639
|
if (initialized.action === "created") {
|
|
640
|
+
await maybeOfferCompletionInstall(runtime);
|
|
641
|
+
await maybeOfferGuideSetup(runtime);
|
|
628
642
|
return finishInit(0, `initialized vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
629
643
|
}
|
|
630
644
|
if (initialized.action === "joined") {
|
|
645
|
+
await maybeOfferCompletionInstall(runtime);
|
|
646
|
+
await maybeOfferGuideSetup(runtime);
|
|
631
647
|
return finishInit(0, `joined vNext project at ${initialized.projectRoot ?? runtime.cwd}`);
|
|
632
648
|
}
|
|
633
649
|
if (initialized.action === "repaired") {
|
|
@@ -1739,17 +1755,30 @@ async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
|
|
|
1739
1755
|
const requested: string[] = [];
|
|
1740
1756
|
const ignored: string[] = [];
|
|
1741
1757
|
const records = new Map<string, { pid: number; startedAt: string; generation?: string }>();
|
|
1758
|
+
const orphans: string[] = [];
|
|
1742
1759
|
for (const file of pids) {
|
|
1743
1760
|
try {
|
|
1744
|
-
const record = JSON.parse(readFileSync(join(runtime.dirs.state, file), "utf8")) as { version?: number; pid?: number; role?: string; startedAt?: string; generation?: string; controlFile?: string };
|
|
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 };
|
|
1745
1762
|
const expectedControl = file === "hub.pid" ? "hub.stop" : file.startsWith("worker-") ? `${file.slice(0, -4)}.stop` : undefined;
|
|
1746
1763
|
const expectedRole = file === "hub.pid" ? "hub" : file.startsWith("worker-") ? "worker" : undefined;
|
|
1747
|
-
if (record.version !== 1 || !Number.isInteger(record.pid) || record.pid! <= 0 || !record.startedAt || !expectedControl || record.controlFile !== expectedControl || record.role !== expectedRole
|
|
1748
|
-
|
|
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 });
|
|
1749
1778
|
requested.push(file); records.set(file, { pid: record.pid!, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}) });
|
|
1750
1779
|
} catch { ignored.push(file); }
|
|
1751
1780
|
}
|
|
1752
|
-
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; }
|
|
1753
1782
|
const waitMs = Math.min(30_000, Math.max(100, Number(waitMsFlag || 5_000)));
|
|
1754
1783
|
const deadline = Date.now() + waitMs;
|
|
1755
1784
|
const stopped = new Set<string>();
|
|
@@ -1763,8 +1792,28 @@ async function cmdStop(runtime: Runtime, waitMsFlag?: string): Promise<number> {
|
|
|
1763
1792
|
if (stopped.size < requested.length) await (runtime.io.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))))(100);
|
|
1764
1793
|
}
|
|
1765
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
|
+
}
|
|
1766
1815
|
const ok = timedOut.length === 0;
|
|
1767
|
-
print(runtime.io, runtime.json, { ok, command: "stop", requested, stopped: [...stopped], timedOut, ignored }, ok ? "managed processes stopped" : "stop request timed out");
|
|
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");
|
|
1768
1817
|
return ok ? 0 : 1;
|
|
1769
1818
|
}
|
|
1770
1819
|
|
|
@@ -2445,6 +2494,201 @@ async function cmdCompletion(runtime: Runtime, shell: string): Promise<number> {
|
|
|
2445
2494
|
}
|
|
2446
2495
|
}
|
|
2447
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
|
+
|
|
2448
2692
|
async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
|
|
2449
2693
|
try {
|
|
2450
2694
|
const prompt = promptParts.join(" ").trim();
|
|
@@ -3152,6 +3396,229 @@ async function cmdRoleModify(
|
|
|
3152
3396
|
}
|
|
3153
3397
|
}
|
|
3154
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
|
+
|
|
3155
3622
|
async function cmdWorkflowDefinitions(
|
|
3156
3623
|
runtime: Runtime,
|
|
3157
3624
|
options: { scope?: "all" | "global" | "local" },
|
|
@@ -4379,6 +4846,22 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
4379
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 }) {
|
|
4380
4847
|
result.code = await cmdRoleModify(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
4381
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
|
+
});
|
|
4382
4865
|
|
|
4383
4866
|
const gate = addGlobalOptions(program.command("gate").description("Validate definitions and operate evidence gates"));
|
|
4384
4867
|
gate.helpCommand("help", "Show gate help");
|
|
@@ -4633,9 +5116,24 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
4633
5116
|
result.code = await cmdConfigList(runtimeFrom(ctx, this));
|
|
4634
5117
|
});
|
|
4635
5118
|
|
|
4636
|
-
addGlobalOptions(program.command("completion
|
|
4637
|
-
|
|
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") {
|
|
4638
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);
|
|
4639
5137
|
});
|
|
4640
5138
|
|
|
4641
5139
|
addGlobalOptions(program.command("suggest <prompt...>").description("Recommend workflow, area, roles, and skills from a prompt or issue description"))
|