@kontextmind/kxm 0.7.92 → 0.7.93
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/workflows/default.yaml +1 -1
- package/CHANGELOG.md +204 -0
- package/README.md +3 -0
- package/docs/README.md +3 -0
- package/docs/agent-skills.md +123 -60
- package/docs/architecture.md +5 -2
- package/docs/cli-reference.md +3527 -0
- package/docs/config-reference.md +1943 -0
- package/docs/configuration.md +29 -3
- package/docs/continuous-improvement.md +122 -10
- package/docs/contracts/routing.md +95 -11
- package/docs/harness-routing.md +616 -0
- package/docs/kxm-handbook.md +106 -19
- package/docs/templates/README.md +1 -1
- package/docs/test-matrix.md +12 -6
- package/docs/troubleshooting.md +2 -2
- package/examples/project/.kxm/workflows/fix.yaml +1 -1
- package/examples/project/.kxm/workflows/improve.yaml +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +9 -10
- package/plugins/kxm/README.md +238 -56
- package/plugins/kxm/dist/claude-hook.js +10083 -0
- package/plugins/kxm/dist/cli.js +3068 -2446
- package/plugins/kxm/dist/client.js +64 -0
- package/plugins/kxm/dist/core.js +102 -9
- package/plugins/kxm/dist/extension.js +210 -68
- package/plugins/kxm/dist/mcp-server.js +217 -40
- package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
- package/plugins/kxm/dist/runtime.js +1874 -298
- package/plugins/kxm/dist/server.js +416 -82
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +48 -24
- package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +61 -21
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
- package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
- package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
- package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
- package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
- package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
- package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
- package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
- package/plugins/kxm/src/arbiter.ts +67 -22
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/claude-hook.ts +192 -0
- package/plugins/kxm/src/cli/project.ts +11 -5
- package/plugins/kxm/src/cli/system.ts +85 -13
- package/plugins/kxm/src/cli/types.ts +4 -1
- package/plugins/kxm/src/cli/workflows.ts +18 -16
- package/plugins/kxm/src/cli.ts +23 -13
- package/plugins/kxm/src/client.ts +15 -4
- package/plugins/kxm/src/commands.ts +19 -9
- package/plugins/kxm/src/config.ts +42 -7
- package/plugins/kxm/src/context-packet.ts +14 -2
- package/plugins/kxm/src/context.ts +16 -5
- package/plugins/kxm/src/dispatch-context.ts +286 -0
- package/plugins/kxm/src/engine-plan.ts +40 -0
- package/plugins/kxm/src/engine.ts +138 -6
- package/plugins/kxm/src/hub-env.ts +17 -1
- package/plugins/kxm/src/hub.ts +92 -29
- package/plugins/kxm/src/improve-sources.ts +228 -0
- package/plugins/kxm/src/improve.ts +325 -140
- package/plugins/kxm/src/local-snapshot.ts +101 -42
- package/plugins/kxm/src/mcp-server.ts +129 -30
- package/plugins/kxm/src/project-config.ts +25 -0
- package/plugins/kxm/src/protocol.ts +11 -0
- package/plugins/kxm/src/relevance.ts +138 -0
- package/plugins/kxm/src/retrospective.ts +16 -10
- package/plugins/kxm/src/runtime-service.ts +8 -1
- package/plugins/kxm/src/runtime-supervisor.ts +16 -2
- package/plugins/kxm/src/session-token-hint.ts +17 -0
- package/plugins/kxm/src/suggest.ts +7 -7
- package/plugins/kxm/src/workflow-manager.ts +80 -78
- package/plugins/kxm/src/workflow.ts +202 -12
- package/scripts/build-runtime.mjs +7 -1
- package/scripts/check-generated.mjs +1 -0
- package/scripts/emit-codex-artifacts.mjs +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code plugin hook entry, bundled as dist/claude-hook.js.
|
|
3
|
+
*
|
|
4
|
+
* `node dist/claude-hook.js session-start` prints one SessionStart JSON object
|
|
5
|
+
* whose additionalContext holds a short status for this project (hub state,
|
|
6
|
+
* this project's active runs, the open request count for this agent, fix hints
|
|
7
|
+
* addressed to the user) followed by the project memory brief.
|
|
8
|
+
*
|
|
9
|
+
* The hook is read-only and scoped to the project Claude Code opened: it
|
|
10
|
+
* writes no files, mints no token, spawns nothing, and never prints a token,
|
|
11
|
+
* a plugin option other than server_url, agent_name and project, or journal
|
|
12
|
+
* text. Outside a KXM project it prints nothing. It always exits 0.
|
|
13
|
+
*
|
|
14
|
+
* Bounds: stdin 300 ms, hub health probe 300 ms, SQLite busy timeout 250 ms
|
|
15
|
+
* across at most three databases. The plugin manifest timeout is the only
|
|
16
|
+
* hard limit; an internal timer cannot fire while a synchronous SQLite read
|
|
17
|
+
* blocks.
|
|
18
|
+
*
|
|
19
|
+
* The heavy modules load through dynamic import so a failure while they load
|
|
20
|
+
* (for example a Node without node:sqlite) is caught and the hook stays
|
|
21
|
+
* silent instead of exiting non-zero.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { join, resolve } from "node:path";
|
|
25
|
+
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
26
|
+
|
|
27
|
+
const STDIN_LIMIT_BYTES = 64 * 1024;
|
|
28
|
+
const STDIN_WAIT_MS = 300;
|
|
29
|
+
const HUB_PROBE_MS = 300;
|
|
30
|
+
const SNAPSHOT_BUSY_TIMEOUT_MS = 250;
|
|
31
|
+
const STATUS_MAX_CHARS = 1500;
|
|
32
|
+
const MAX_ACTIVE_RUNS = 3;
|
|
33
|
+
const ACTIVE_RUN_STATUSES = new Set(["running", "waiting", "created", "preparing"]);
|
|
34
|
+
const DEFAULT_SERVER_URL = "http://127.0.0.1:7331";
|
|
35
|
+
|
|
36
|
+
interface HookInput {
|
|
37
|
+
cwd?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readHookInput(): Promise<HookInput> {
|
|
41
|
+
const stdin = process.stdin;
|
|
42
|
+
if (stdin.isTTY) return Promise.resolve({});
|
|
43
|
+
return new Promise((resolveInput) => {
|
|
44
|
+
const chunks: Buffer[] = [];
|
|
45
|
+
let size = 0;
|
|
46
|
+
let done = false;
|
|
47
|
+
const finish = (overflow: boolean): void => {
|
|
48
|
+
if (done) return;
|
|
49
|
+
done = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
stdin.off("data", onData);
|
|
52
|
+
stdin.off("end", onEnd);
|
|
53
|
+
stdin.off("error", onEnd);
|
|
54
|
+
stdin.pause();
|
|
55
|
+
if (overflow) {
|
|
56
|
+
resolveInput({});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
61
|
+
resolveInput(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as HookInput : {});
|
|
62
|
+
} catch {
|
|
63
|
+
resolveInput({});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const onData = (chunk: Buffer | string): void => {
|
|
67
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
68
|
+
size += buffer.length;
|
|
69
|
+
if (size > STDIN_LIMIT_BYTES) finish(true);
|
|
70
|
+
else chunks.push(buffer);
|
|
71
|
+
};
|
|
72
|
+
const onEnd = (): void => finish(false);
|
|
73
|
+
const timer = setTimeout(onEnd, STDIN_WAIT_MS);
|
|
74
|
+
stdin.on("data", onData);
|
|
75
|
+
stdin.once("end", onEnd);
|
|
76
|
+
stdin.once("error", onEnd);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isDirectory(path: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
return statSync(path).isDirectory();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Values read from disk or options stay on one line of the context. */
|
|
89
|
+
function oneLine(value: string): string {
|
|
90
|
+
return value.replace(/\s+/g, " ").trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function truncate(text: string, max: number): string {
|
|
94
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function sessionStartContext(input: HookInput, env: NodeJS.ProcessEnv): Promise<string | undefined> {
|
|
98
|
+
const inputCwd = typeof input.cwd === "string" ? input.cwd.trim() : "";
|
|
99
|
+
const root = resolve(env.CLAUDE_PROJECT_DIR?.trim() || inputCwd || process.cwd());
|
|
100
|
+
if (!isDirectory(join(root, ".kxm"))) return undefined;
|
|
101
|
+
|
|
102
|
+
const [
|
|
103
|
+
{ resolveSessionHubStatus },
|
|
104
|
+
{ loadLocalMeshSnapshot, resolveKxmSnapshotPaths },
|
|
105
|
+
{ formatMemoryBriefText, generateMemoryBrief },
|
|
106
|
+
{ defaultProjectName },
|
|
107
|
+
{ enforceToolPolicy },
|
|
108
|
+
{ parse },
|
|
109
|
+
] = await Promise.all([
|
|
110
|
+
import("./session-work.ts"),
|
|
111
|
+
import("./local-snapshot.ts"),
|
|
112
|
+
import("./memory.ts"),
|
|
113
|
+
import("./project-name.ts"),
|
|
114
|
+
import("./commands.ts"),
|
|
115
|
+
import("yaml"),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
// Same inputs .mcp.json hands the MCP server.
|
|
119
|
+
const hubProject = defaultProjectName(root, { ...env, KXM_PROJECT: env.CLAUDE_PLUGIN_OPTION_PROJECT ?? "" });
|
|
120
|
+
const recipientName = env.CLAUDE_PLUGIN_OPTION_AGENT_NAME?.trim() || undefined;
|
|
121
|
+
const url = (env.CLAUDE_PLUGIN_OPTION_SERVER_URL?.trim() || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
122
|
+
let runtimeProjectId: string | undefined;
|
|
123
|
+
try {
|
|
124
|
+
const project = parse(readFileSync(join(root, ".kxm", "project.yaml"), "utf8")) as { id?: unknown } | null;
|
|
125
|
+
if (typeof project?.id === "string" && project.id.trim()) runtimeProjectId = project.id.trim();
|
|
126
|
+
} catch {
|
|
127
|
+
// No readable project.yaml: no Runtime runs.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hub = await resolveSessionHubStatus(url, fetch, HUB_PROBE_MS);
|
|
131
|
+
const lines = [`KXM project ${oneLine(hubProject)} · hub ${hub.state} at ${url}`];
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const paths = resolveKxmSnapshotPaths(root, env);
|
|
135
|
+
const snapshot = loadLocalMeshSnapshot(paths.dataPath, paths.stateDir, {
|
|
136
|
+
env,
|
|
137
|
+
projectRoot: root,
|
|
138
|
+
busyTimeoutMs: SNAPSHOT_BUSY_TIMEOUT_MS,
|
|
139
|
+
scope: { hubProject, runtimeProjectId, recipientName },
|
|
140
|
+
});
|
|
141
|
+
const active = snapshot.runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status)).slice(0, MAX_ACTIVE_RUNS);
|
|
142
|
+
if (active.length > 0) {
|
|
143
|
+
lines.push("Active workflow runs in this project:");
|
|
144
|
+
for (const run of active) {
|
|
145
|
+
const stage = run.currentStage ? ` stage=${oneLine(run.currentStage)}` : "";
|
|
146
|
+
const mine = recipientName && run.targetAgentName === recipientName ? " (assigned to you)" : "";
|
|
147
|
+
lines.push(`- ${oneLine(run.id)} ${oneLine(run.definitionId)} ${oneLine(run.status)}${stage}${mine}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (recipientName) lines.push(`Open peer requests to ${oneLine(recipientName)}: ${snapshot.openMessageTotal}`);
|
|
151
|
+
} catch {
|
|
152
|
+
// Unreadable local state: report the hub and hints without runs.
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push("Call kxm_context with your role and task before planning; kxm_workflow_get <runId> for an assigned run; kxm_inbox then kxm_reply for peer requests.");
|
|
156
|
+
if (hub.state !== "on") {
|
|
157
|
+
lines.push(`The KXM hub is ${hub.state} at ${url}, so kxm_* tools will fail. Ask the user to start it with \`kxm hub start\` (the kxm CLI installs separately: npm install --global --omit=peer @kontextmind/kxm).`);
|
|
158
|
+
}
|
|
159
|
+
const tokenHint = sessionTokenFixHint(enforceToolPolicy("kxm_list", env));
|
|
160
|
+
if (tokenHint) lines.push(tokenHint);
|
|
161
|
+
|
|
162
|
+
const status = truncate(lines.join("\n"), STATUS_MAX_CHARS);
|
|
163
|
+
let memory: string | undefined;
|
|
164
|
+
try {
|
|
165
|
+
// Unmodified and untruncated: the same facts `kxm memory brief` prints.
|
|
166
|
+
memory = formatMemoryBriefText(generateMemoryBrief(root));
|
|
167
|
+
} catch {
|
|
168
|
+
// A malformed memory file: keep the status sections.
|
|
169
|
+
}
|
|
170
|
+
return memory === undefined ? status : `${status}\n\n${memory}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function main(argv: readonly string[]): Promise<void> {
|
|
174
|
+
let output = "";
|
|
175
|
+
try {
|
|
176
|
+
if (argv[2] === "session-start") {
|
|
177
|
+
const context = await sessionStartContext(await readHookInput(), process.env);
|
|
178
|
+
if (context !== undefined) {
|
|
179
|
+
output = `${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } })}\n`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
output = "";
|
|
184
|
+
}
|
|
185
|
+
if (!output) process.exit(0);
|
|
186
|
+
process.stdout.write(output, () => process.exit(0));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.on("uncaughtException", () => process.exit(0));
|
|
190
|
+
process.on("unhandledRejection", () => process.exit(0));
|
|
191
|
+
process.stdout.on("error", () => process.exit(0));
|
|
192
|
+
void main(process.argv);
|
|
@@ -159,7 +159,9 @@ export async function cmdKxmInit(
|
|
|
159
159
|
: initialized.repairPlan?.issues.length
|
|
160
160
|
? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
|
|
161
161
|
: "partial or provenance-free KXM state requires explicit repair; no files were overwritten";
|
|
162
|
-
|
|
162
|
+
// Name each validation issue, so a definition error reads as one instead of
|
|
163
|
+
// only as a repair refusal.
|
|
164
|
+
return finishInit(1, [next, ...initialized.plan.issues.map((issue) => `${issue.file}: ${issue.code}: ${issue.message}`)].join("\n"));
|
|
163
165
|
} catch (error) {
|
|
164
166
|
if (error instanceof KxmConfigError) {
|
|
165
167
|
print(runtime.io, runtime.json, {
|
|
@@ -324,7 +326,11 @@ async function readSupervisor(runtime: Runtime, command: string): Promise<Awaite
|
|
|
324
326
|
}
|
|
325
327
|
|
|
326
328
|
export const RUN_ENGINE_PHASE = "pre-3a";
|
|
327
|
-
|
|
329
|
+
|
|
330
|
+
/** The next step `kxm run` prints for the run it just created. */
|
|
331
|
+
export function runEngineNotice(runId: string): string {
|
|
332
|
+
return `drive it model-free: kxm runs drive ${runId} --simulated --wait (or cancel: kxm runs cancel ${runId})`;
|
|
333
|
+
}
|
|
328
334
|
|
|
329
335
|
/** Where `kxm run <workflow>` would create its run, or the exit code of the
|
|
330
336
|
* refusal it already printed. Reads only; shared with `task run --dry-run`. */
|
|
@@ -371,9 +377,9 @@ export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined,
|
|
|
371
377
|
}, `run plan: workflow ${target.workflowId} at ${target.configRevision.slice(0, 19)}… (no run created)`);
|
|
372
378
|
return 0;
|
|
373
379
|
}
|
|
374
|
-
const supervisor = await ensureKxmSupervisor({ env: runtime.env });
|
|
380
|
+
const supervisor = await (kxmDriveCliSeams.ensureSupervisor ?? ensureKxmSupervisor)({ env: runtime.env });
|
|
375
381
|
const prompt = promptParts.join(" ").trim();
|
|
376
|
-
const acceptance = await kxmRuntimeRequest(supervisor, "POST", "/v1/runs", {
|
|
382
|
+
const acceptance = await (kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest)(supervisor, "POST", "/v1/runs", {
|
|
377
383
|
projectRoot,
|
|
378
384
|
workflowId: target.workflowId,
|
|
379
385
|
prompt,
|
|
@@ -386,7 +392,7 @@ export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined,
|
|
|
386
392
|
idempotent: acceptance.idempotent === true,
|
|
387
393
|
run,
|
|
388
394
|
supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
|
|
389
|
-
}, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${
|
|
395
|
+
}, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${runEngineNotice(run.runId)}`);
|
|
390
396
|
return 0;
|
|
391
397
|
} catch (error) {
|
|
392
398
|
if (error instanceof KxmConfigError) {
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
groupByBehavior,
|
|
18
18
|
generateRoutingReport,
|
|
19
19
|
formatRoutingReport,
|
|
20
|
+
type RoutingRecord,
|
|
21
|
+
type RoutingRecordV2,
|
|
20
22
|
} from "../routing.ts";
|
|
21
23
|
import { loadPriceCatalog, type PriceCatalog } from "../prices.ts";
|
|
22
24
|
import {
|
|
@@ -24,6 +26,11 @@ import {
|
|
|
24
26
|
formatImprovementReport,
|
|
25
27
|
writeImprovementReport,
|
|
26
28
|
} from "../improve.ts";
|
|
29
|
+
import {
|
|
30
|
+
loadRoutingSources,
|
|
31
|
+
type LoadedRoutingSources,
|
|
32
|
+
type RoutingSourceSummary,
|
|
33
|
+
} from "../improve-sources.ts";
|
|
27
34
|
import {
|
|
28
35
|
loadModesConfig,
|
|
29
36
|
resolveActiveMode,
|
|
@@ -514,26 +521,77 @@ export async function cmdArtifactsExist(runtime: Runtime, pathFlag: string): Pro
|
|
|
514
521
|
return 0;
|
|
515
522
|
}
|
|
516
523
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
524
|
+
const IMPROVE_SOURCE_UNREADABLE = "improve_source_unreadable";
|
|
525
|
+
|
|
526
|
+
/** Load routing sources, or print the unreadable-source failure (exit 1). */
|
|
527
|
+
function loadRoutingSourcesOrReport(runtime: Runtime, command: string, file?: string | undefined): LoadedRoutingSources | undefined {
|
|
528
|
+
try {
|
|
529
|
+
return loadRoutingSources({
|
|
530
|
+
cwd: runtime.cwd,
|
|
531
|
+
env: runtime.env,
|
|
532
|
+
logsDir: runtime.dirs.logs,
|
|
533
|
+
...(file !== undefined ? { file } : {}),
|
|
534
|
+
});
|
|
535
|
+
} catch (error) {
|
|
536
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
537
|
+
const detail = message.startsWith(`${IMPROVE_SOURCE_UNREADABLE}: `) ? message.slice(IMPROVE_SOURCE_UNREADABLE.length + 2) : message;
|
|
538
|
+
print(
|
|
539
|
+
runtime.io,
|
|
540
|
+
runtime.json,
|
|
541
|
+
{ ok: false, command, error: IMPROVE_SOURCE_UNREADABLE, detail },
|
|
542
|
+
`${IMPROVE_SOURCE_UNREADABLE}: ${detail}`,
|
|
543
|
+
);
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatRoutingSource(source: RoutingSourceSummary): string {
|
|
549
|
+
const counts = (["records", "skippedInvalid", "excludedSimulated", "undecided", "duplicatesDropped"] as const)
|
|
550
|
+
.filter((key) => source[key] !== undefined)
|
|
551
|
+
.map((key) => `${key}=${source[key]}`);
|
|
552
|
+
return ` ${source.kind.padEnd(9)} ${source.path} (exists=${source.exists}, ${counts.join(", ")})`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export async function cmdImprove(runtime: Runtime, options: { file?: string | undefined; outDir?: string | undefined } = {}): Promise<number> {
|
|
556
|
+
const loaded = loadRoutingSourcesOrReport(runtime, "improve", options.file);
|
|
557
|
+
if (!loaded) return 1;
|
|
558
|
+
const root = loaded.projectRoot ?? runtime.cwd;
|
|
559
|
+
|
|
560
|
+
let config: ReturnType<typeof loadKxmConfig>;
|
|
561
|
+
try {
|
|
562
|
+
const userConfigDir = runtime.env.KXM_USER_CONFIG_DIR?.trim();
|
|
563
|
+
config = loadKxmConfig(root, userConfigDir ? { userConfigDir } : {});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
566
|
+
print(runtime.io, runtime.json, { ok: false, command: "improve", error: "config_invalid", detail: message }, `config_invalid: ${message}`);
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
522
569
|
|
|
523
570
|
const candidatesDir = options.outDir
|
|
524
571
|
? resolve(runtime.cwd, options.outDir)
|
|
525
|
-
: join(
|
|
572
|
+
: join(root, ".kxm", "candidates");
|
|
526
573
|
|
|
527
|
-
const report = buildImprovementReport(
|
|
574
|
+
const report = buildImprovementReport(loaded.records, {
|
|
528
575
|
candidatesDir,
|
|
529
|
-
projectRoot:
|
|
576
|
+
projectRoot: root,
|
|
530
577
|
dryRun: runtime.dryRun,
|
|
578
|
+
promotionPolicy: config.improvement.promotionPolicy,
|
|
579
|
+
autoThreshold: config.improvement.autoThreshold,
|
|
580
|
+
halfLifeDays: config.improvement.telemetryHalfLifeDays,
|
|
531
581
|
});
|
|
532
582
|
|
|
533
583
|
const reportDir = join(runtime.dirs.assets, "improvements");
|
|
534
584
|
const reportPath = writeImprovementReport(reportDir, report, runtime.dryRun);
|
|
535
585
|
|
|
536
|
-
const text =
|
|
586
|
+
const text = [
|
|
587
|
+
"Sources:",
|
|
588
|
+
...loaded.sources.map(formatRoutingSource),
|
|
589
|
+
loaded.projectRoot !== undefined
|
|
590
|
+
? `Project root: ${loaded.projectRoot}`
|
|
591
|
+
: `Runtime store not read: no KXM project at ${runtime.cwd}`,
|
|
592
|
+
"",
|
|
593
|
+
formatImprovementReport(report),
|
|
594
|
+
].join("\n");
|
|
537
595
|
print(runtime.io, runtime.json, {
|
|
538
596
|
ok: true,
|
|
539
597
|
command: "improve",
|
|
@@ -545,6 +603,8 @@ export async function cmdImprove(runtime: Runtime, options: { file?: string | un
|
|
|
545
603
|
candidatesCount: report.candidates.length,
|
|
546
604
|
candidates: report.candidates,
|
|
547
605
|
report,
|
|
606
|
+
sources: loaded.sources,
|
|
607
|
+
projectRoot: loaded.projectRoot ?? null,
|
|
548
608
|
}, text);
|
|
549
609
|
return 0;
|
|
550
610
|
}
|
|
@@ -821,8 +881,20 @@ export async function cmdRoutingReport(
|
|
|
821
881
|
runtime: Runtime,
|
|
822
882
|
options: { file?: string | undefined; equivalentListCost?: boolean | undefined; listPrices?: boolean | undefined; prices?: string | undefined },
|
|
823
883
|
): Promise<number> {
|
|
824
|
-
|
|
825
|
-
|
|
884
|
+
let file: string;
|
|
885
|
+
let records: Array<RoutingRecord | RoutingRecordV2>;
|
|
886
|
+
let sources: RoutingSourceSummary[] | undefined;
|
|
887
|
+
if (options.file !== undefined) {
|
|
888
|
+
file = options.file;
|
|
889
|
+
records = readRoutingRecords(file).map((entry) => entry.routing);
|
|
890
|
+
} else {
|
|
891
|
+
// The project's Runtime store (when cwd is in a KXM project), then telemetry.
|
|
892
|
+
const loaded = loadRoutingSourcesOrReport(runtime, "routing report");
|
|
893
|
+
if (!loaded) return 1;
|
|
894
|
+
file = telemetryPath(runtime.dirs.logs);
|
|
895
|
+
records = loaded.records;
|
|
896
|
+
sources = loaded.sources;
|
|
897
|
+
}
|
|
826
898
|
const includeEquivalentListCost = Boolean(options.equivalentListCost || options.listPrices);
|
|
827
899
|
|
|
828
900
|
let catalog: PriceCatalog | undefined;
|
|
@@ -838,7 +910,7 @@ export async function cmdRoutingReport(
|
|
|
838
910
|
const report = generateRoutingReport(records, { catalog, includeEquivalentListCost });
|
|
839
911
|
|
|
840
912
|
if (records.length === 0) {
|
|
841
|
-
print(runtime.io, runtime.json, { ok: true, command: "routing report", file, configurations: [], report }, "no routing records in telemetry");
|
|
913
|
+
print(runtime.io, runtime.json, { ok: true, command: "routing report", file, ...(sources ? { sources } : {}), configurations: [], report }, "no routing records in telemetry");
|
|
842
914
|
return 0;
|
|
843
915
|
}
|
|
844
916
|
|
|
@@ -853,7 +925,7 @@ export async function cmdRoutingReport(
|
|
|
853
925
|
print(
|
|
854
926
|
runtime.io,
|
|
855
927
|
runtime.json,
|
|
856
|
-
{ ok: true, command: "routing report", file, configurations, report },
|
|
928
|
+
{ ok: true, command: "routing report", file, ...(sources ? { sources } : {}), configurations, report },
|
|
857
929
|
text,
|
|
858
930
|
);
|
|
859
931
|
return 0;
|
|
@@ -206,7 +206,10 @@ export function redactCliValue(value: unknown, field = ""): unknown {
|
|
|
206
206
|
|| field === "baseRevision"
|
|
207
207
|
|| field === "candidateRevision"
|
|
208
208
|
|| field === "baseValueSha256"
|
|
209
|
-
|| field === "candidateValueSha256"
|
|
209
|
+
|| field === "candidateValueSha256"
|
|
210
|
+
|| field === "workflowHash"
|
|
211
|
+
|| field === "promptHash"
|
|
212
|
+
|| field === "askSha256")
|
|
210
213
|
&& /^(?:sha256:)?[a-f0-9]{64}$/.test(value)
|
|
211
214
|
) return value;
|
|
212
215
|
return redactSecrets(value);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
addWorkflowDefinition,
|
|
11
11
|
removeWorkflowDefinition,
|
|
12
12
|
modifyWorkflowDefinition,
|
|
13
|
+
scaffoldWorkflowDefinition,
|
|
13
14
|
WORKFLOW_TEMPLATES,
|
|
14
15
|
} from "../workflow-manager.ts";
|
|
15
16
|
import {
|
|
@@ -203,11 +204,26 @@ export async function cmdWorkflowAdd(
|
|
|
203
204
|
scope?: "global" | "local" | undefined;
|
|
204
205
|
overwrite?: boolean | undefined;
|
|
205
206
|
pick?: string | boolean | undefined;
|
|
207
|
+
template?: string | undefined;
|
|
206
208
|
},
|
|
207
209
|
): Promise<number> {
|
|
208
210
|
const scope = options.scope ?? "local";
|
|
209
211
|
let content: Record<string, unknown> | string | undefined;
|
|
210
|
-
if (
|
|
212
|
+
if (options.template !== undefined) {
|
|
213
|
+
const refuse = (error: string, text: string): number => {
|
|
214
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
215
|
+
return 2;
|
|
216
|
+
};
|
|
217
|
+
if (options.file !== undefined || options.pick !== undefined) {
|
|
218
|
+
return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick");
|
|
219
|
+
}
|
|
220
|
+
if (!workflowId) return refuse("workflow_id_required", "usage: kxm workflow add <workflowId> --template <name>");
|
|
221
|
+
const template = Object.hasOwn(WORKFLOW_TEMPLATES, options.template) ? WORKFLOW_TEMPLATES[options.template] : undefined;
|
|
222
|
+
if (!template) {
|
|
223
|
+
return refuse("workflow_template_unknown", `unknown template ${options.template}; choose ${Object.keys(WORKFLOW_TEMPLATES).join(", ")}`);
|
|
224
|
+
}
|
|
225
|
+
content = { ...template, ...(options.description ? { description: options.description } : {}) };
|
|
226
|
+
} else if (!workflowId || options.pick) {
|
|
211
227
|
const candidates: PickCandidate[] = Object.entries(WORKFLOW_TEMPLATES).map(([id, tmpl]) => ({
|
|
212
228
|
id,
|
|
213
229
|
description: String(tmpl.description ?? id),
|
|
@@ -243,21 +259,7 @@ export async function cmdWorkflowAdd(
|
|
|
243
259
|
const filePath = resolve(runtime.cwd, options.file);
|
|
244
260
|
content = readFileSync(filePath, "utf8");
|
|
245
261
|
} else if (!content) {
|
|
246
|
-
content = {
|
|
247
|
-
schema: "kxm.workflow.v1",
|
|
248
|
-
id: workflowId!,
|
|
249
|
-
description: options.description || `Workflow ${workflowId}`,
|
|
250
|
-
coordinator: "coordinator",
|
|
251
|
-
limits: { maxTransitions: 8 },
|
|
252
|
-
steps: [
|
|
253
|
-
{
|
|
254
|
-
id: "step-1",
|
|
255
|
-
kind: "agent",
|
|
256
|
-
role: "writer",
|
|
257
|
-
on: { passed: { target: "$terminal", terminalStatus: "completed" } },
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
};
|
|
262
|
+
content = scaffoldWorkflowDefinition(options.description || `Workflow ${workflowId}`);
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
try {
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
enforceToolPolicy,
|
|
26
26
|
} from "./commands.ts";
|
|
27
27
|
import { discoverKxmProjectRoot } from "./project-config.ts";
|
|
28
|
+
import { JOURNAL_CATEGORIES } from "./workflow.ts";
|
|
28
29
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "./runtime-supervisor.ts";
|
|
29
30
|
|
|
30
31
|
// Submodule imports
|
|
@@ -371,7 +372,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
371
372
|
|
|
372
373
|
const program = new Command(CLI_NAME);
|
|
373
374
|
program
|
|
374
|
-
.description("
|
|
375
|
+
.description("KXM local-first orchestration CLI")
|
|
375
376
|
.version(readInstalledKxmVersion(findKxmRepoRoot(import.meta.url)), "-V, --version", "Print the installed kxm version")
|
|
376
377
|
.exitOverride()
|
|
377
378
|
.configureOutput({
|
|
@@ -412,7 +413,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
412
413
|
result.code = await cmdRestore(runtimeFrom(ctx, this), manifest);
|
|
413
414
|
});
|
|
414
415
|
|
|
415
|
-
addGlobalOptions(program.command("run").description("Create a KXM run (offline-first;
|
|
416
|
+
addGlobalOptions(program.command("run").description("Create a KXM run (offline-first; kxm runs drive <runId> --simulated executes it model-free)")
|
|
416
417
|
.argument("[workflow]", "Workflow id to run")
|
|
417
418
|
.argument("[prompt...]", "Run prompt (hashed, never stored raw)")
|
|
418
419
|
.action(async function runAction(this: Command, workflow: string | undefined, promptParts: string[]) {
|
|
@@ -696,10 +697,15 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
696
697
|
};
|
|
697
698
|
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_workflow_checkpoint", options);
|
|
698
699
|
});
|
|
699
|
-
addGlobalOptions(workflow.command("record
|
|
700
|
+
addGlobalOptions(workflow.command("record").description("Record workflow journal knowledge"))
|
|
701
|
+
.argument("[runId]", "Workflow run ID")
|
|
702
|
+
.argument("[category]", "Journal category (see --category)")
|
|
703
|
+
.argument("[area]", "Optional improvement area; with three positionals and no --summary, the third is the summary")
|
|
704
|
+
.argument("[summary]", "Entry summary")
|
|
700
705
|
.option("--run-id <id>", "Workflow run ID")
|
|
701
|
-
.option("--category <category>", "
|
|
702
|
-
.option("--area <area>", "harness, gates, implementation, workflow, documentation, security, other")
|
|
706
|
+
.option("--category <category>", JOURNAL_CATEGORIES.join(", "))
|
|
707
|
+
.option("--area <area>", "harness, gates, implementation, workflow, documentation, security, other (defaults to the stage area with --stage-id)")
|
|
708
|
+
.option("--stage-id <id>", "Stage the entry is about; the hub derives attempt and default area")
|
|
703
709
|
.option("--severity <level>", "info, warning, error")
|
|
704
710
|
.option("--summary <text>", "Entry summary")
|
|
705
711
|
.option("--details <text>", "Detailed text")
|
|
@@ -707,12 +713,16 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
707
713
|
.option("--related-entry-ids <ids...>", "Related entry IDs")
|
|
708
714
|
.option("--payload <json>", "JSON payload")
|
|
709
715
|
.action(async function recordAction(this: Command, runId?: string, category?: string, area?: string, summary?: string, opts?: Record<string, unknown>) {
|
|
716
|
+
// Area is optional: `record <runId> <category> <summary>` reads the third
|
|
717
|
+
// positional as the summary when neither a fourth positional nor --summary
|
|
718
|
+
// supplies one.
|
|
719
|
+
const areaIsSummary = area !== undefined && summary === undefined && opts?.summary === undefined;
|
|
710
720
|
const options = {
|
|
711
721
|
...opts,
|
|
712
722
|
...(runId ? { runId } : {}),
|
|
713
723
|
...(category ? { category } : {}),
|
|
714
|
-
...(area ? { area } : {}),
|
|
715
|
-
...(summary ? { summary } : {}),
|
|
724
|
+
...(area && !areaIsSummary ? { area } : {}),
|
|
725
|
+
...(areaIsSummary ? { summary: area } : summary ? { summary } : {}),
|
|
716
726
|
};
|
|
717
727
|
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_workflow_record", options);
|
|
718
728
|
});
|
|
@@ -771,7 +781,8 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
771
781
|
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
772
782
|
.option("--overwrite", "Overwrite existing workflow definition if present")
|
|
773
783
|
.option("--pick [selection]", "Pick from available workflow templates (index or id)")
|
|
774
|
-
.
|
|
784
|
+
.option("--template <name>", "Start from a built-in template: implement-and-verify, dual-critic-review, or spec-and-plan")
|
|
785
|
+
.action(async function workflowAddAction(this: Command, workflowId?: string, options?: { file?: string; description?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean; template?: string }) {
|
|
775
786
|
result.code = await cmdWorkflowAdd(runtimeFrom(ctx, this), workflowId, options ?? {});
|
|
776
787
|
});
|
|
777
788
|
addGlobalOptions(workflow.command("remove [workflowId]").description("Remove a workflow definition"))
|
|
@@ -903,13 +914,12 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
903
914
|
result.code = await cmdGithubWatch(runtimeFrom(ctx, this), options);
|
|
904
915
|
});
|
|
905
916
|
|
|
906
|
-
const improve = addGlobalOptions(program.command("improve").description("Propose
|
|
917
|
+
const improve = addGlobalOptions(program.command("improve").description("Propose coded-repeat candidates from this project's Runtime routing records and telemetry"));
|
|
907
918
|
improve.helpCommand("help", "Show improve help");
|
|
908
919
|
addGlobalOptions(improve.command("report", { isDefault: true }).description("Generate improvement report and candidates from routing records"))
|
|
909
|
-
.option("--file <path>", "
|
|
910
|
-
.option("--target <cli|project>", "Limit proposals to cli or project")
|
|
920
|
+
.option("--file <path>", "Read only this routing-record JSONL instead of the project's Runtime store and telemetry")
|
|
911
921
|
.option("--out-dir <path>", "Directory for candidates (default .kxm/candidates)")
|
|
912
|
-
.action(async function improveReportAction(this: Command, options: { file?: string;
|
|
922
|
+
.action(async function improveReportAction(this: Command, options: { file?: string; outDir?: string }) {
|
|
913
923
|
result.code = await cmdImprove(runtimeFrom(ctx, this), options);
|
|
914
924
|
});
|
|
915
925
|
|
|
@@ -1048,7 +1058,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
1048
1058
|
const routing = addGlobalOptions(program.command("routing").description("Model/harness routing telemetry and behavioral comparisons"));
|
|
1049
1059
|
routing.helpCommand("help", "Show routing help");
|
|
1050
1060
|
addGlobalOptions(routing.command("report").description("Compare verified completion, cost, and rework per behavioral configuration"))
|
|
1051
|
-
.option("-f, --file <path>", "
|
|
1061
|
+
.option("-f, --file <path>", "Read only this telemetry or event log JSONL file (default: this project's Runtime event store plus workspace telemetry)")
|
|
1052
1062
|
.option("-l, --equivalent-list-cost", "Include equivalent list price column using price catalog")
|
|
1053
1063
|
.option("--list-prices", "Alias for --equivalent-list-cost")
|
|
1054
1064
|
.option("--prices <path>", "Path to price catalog (default: .kxm/prices.yaml)")
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
canonicalWorkflowEvidenceKey,
|
|
8
8
|
type ImprovementArea,
|
|
9
9
|
type ImprovementAreaReport,
|
|
10
|
+
type ImprovementSignal,
|
|
10
11
|
type JournalCategory,
|
|
11
12
|
type WorkflowCheckpointStatus,
|
|
12
13
|
type WorkflowEvidenceInput,
|
|
@@ -26,8 +27,16 @@ export interface ContextRequestAudit {
|
|
|
26
27
|
candidateCount: number;
|
|
27
28
|
excludedSuperseded: number;
|
|
28
29
|
unresolvedGaps: string[];
|
|
30
|
+
/** Numbers only: distinct task tokens, eligible candidates sharing a task
|
|
31
|
+
* token, and each selected item's rounded relevance (index-aligned with
|
|
32
|
+
* selectedIds). */
|
|
33
|
+
relevance: { taskTokens: number; matchedCandidates: number; selected: number[] };
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
/** One recall result: bounded metadata plus the item's rounded relevance to
|
|
37
|
+
* the query. Never includes the summary. */
|
|
38
|
+
export type ContextRecallItem = ContextItemAuditMetadata & { relevance: number };
|
|
39
|
+
|
|
31
40
|
export interface ContextExplanation {
|
|
32
41
|
found: boolean;
|
|
33
42
|
lineage: string[];
|
|
@@ -422,13 +431,15 @@ export class HubClient {
|
|
|
422
431
|
runId: string,
|
|
423
432
|
input: {
|
|
424
433
|
category: JournalCategory;
|
|
425
|
-
area
|
|
434
|
+
/** Required unless stageId names a stage that declares an area. */
|
|
435
|
+
area?: ImprovementArea;
|
|
426
436
|
severity?: "info" | "warning" | "error";
|
|
427
437
|
summary: string;
|
|
428
438
|
details?: string;
|
|
429
439
|
evidence?: string[];
|
|
430
440
|
relatedEntryIds?: string[];
|
|
431
|
-
/** Stage the entry is recorded against; binds run/stage/attempt
|
|
441
|
+
/** Stage the entry is recorded against; binds run/stage/attempt
|
|
442
|
+
* provenance. The hub derives the attempt; callers never supply one. */
|
|
432
443
|
stageId?: string;
|
|
433
444
|
},
|
|
434
445
|
): Promise<WorkflowJournalEntry> {
|
|
@@ -439,7 +450,7 @@ export class HubClient {
|
|
|
439
450
|
return result.entry;
|
|
440
451
|
}
|
|
441
452
|
|
|
442
|
-
async improvementReport(): Promise<{ reports: ImprovementAreaReport[]; entries: number }> {
|
|
453
|
+
async improvementReport(): Promise<{ reports: ImprovementAreaReport[]; signals: ImprovementSignal[]; entries: number }> {
|
|
443
454
|
return await this.request("/v1/improvements");
|
|
444
455
|
}
|
|
445
456
|
|
|
@@ -462,7 +473,7 @@ export class HubClient {
|
|
|
462
473
|
query?: string;
|
|
463
474
|
kinds?: ContextItemKind[];
|
|
464
475
|
limit?: number;
|
|
465
|
-
}): Promise<{ items:
|
|
476
|
+
}): Promise<{ items: ContextRecallItem[]; unresolvedGaps: string[] }> {
|
|
466
477
|
return await this.request("/v1/context/recall", { method: "POST", body: JSON.stringify(input) });
|
|
467
478
|
}
|
|
468
479
|
|