@kontextmind/kxm 0.7.91 → 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.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +1 -1
  3. package/CHANGELOG.md +212 -0
  4. package/README.md +3 -0
  5. package/docs/README.md +3 -0
  6. package/docs/agent-skills.md +123 -60
  7. package/docs/architecture.md +5 -2
  8. package/docs/cli-reference.md +3527 -0
  9. package/docs/config-reference.md +1943 -0
  10. package/docs/configuration.md +30 -4
  11. package/docs/continuous-improvement.md +122 -10
  12. package/docs/contracts/routing.md +95 -11
  13. package/docs/harness-routing.md +616 -0
  14. package/docs/kxm-handbook.md +106 -19
  15. package/docs/templates/README.md +1 -1
  16. package/docs/test-matrix.md +12 -6
  17. package/docs/troubleshooting.md +2 -2
  18. package/examples/project/.kxm/workflows/fix.yaml +1 -1
  19. package/examples/project/.kxm/workflows/improve.yaml +1 -1
  20. package/package.json +1 -1
  21. package/plugins/kxm/.claude-plugin/plugin.json +9 -10
  22. package/plugins/kxm/README.md +238 -56
  23. package/plugins/kxm/dist/claude-hook.js +10083 -0
  24. package/plugins/kxm/dist/cli.js +2487 -1848
  25. package/plugins/kxm/dist/client.js +64 -0
  26. package/plugins/kxm/dist/core.js +102 -9
  27. package/plugins/kxm/dist/extension.js +210 -68
  28. package/plugins/kxm/dist/mcp-server.js +217 -40
  29. package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
  30. package/plugins/kxm/dist/runtime.js +1874 -298
  31. package/plugins/kxm/dist/server.js +416 -82
  32. package/plugins/kxm/package.json +1 -1
  33. package/plugins/kxm/skills/hints.json +1 -1
  34. package/plugins/kxm/skills/kxm/SKILL.md +48 -24
  35. package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
  36. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
  37. package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
  38. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
  39. package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
  40. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
  41. package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
  42. package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
  43. package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
  44. package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
  45. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
  46. package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
  47. package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
  48. package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
  49. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
  50. package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
  51. package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
  52. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
  53. package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
  54. package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
  55. package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
  56. package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
  57. package/plugins/kxm/src/arbiter.ts +67 -22
  58. package/plugins/kxm/src/autocomplete.ts +1 -1
  59. package/plugins/kxm/src/claude-hook.ts +192 -0
  60. package/plugins/kxm/src/cli/project.ts +11 -5
  61. package/plugins/kxm/src/cli/system.ts +85 -13
  62. package/plugins/kxm/src/cli/types.ts +4 -1
  63. package/plugins/kxm/src/cli/workflows.ts +18 -16
  64. package/plugins/kxm/src/cli.ts +23 -13
  65. package/plugins/kxm/src/client.ts +15 -4
  66. package/plugins/kxm/src/commands.ts +19 -9
  67. package/plugins/kxm/src/config.ts +42 -7
  68. package/plugins/kxm/src/context-packet.ts +14 -2
  69. package/plugins/kxm/src/context.ts +16 -5
  70. package/plugins/kxm/src/dispatch-context.ts +286 -0
  71. package/plugins/kxm/src/engine-plan.ts +40 -0
  72. package/plugins/kxm/src/engine.ts +138 -6
  73. package/plugins/kxm/src/hub-env.ts +17 -1
  74. package/plugins/kxm/src/hub.ts +92 -29
  75. package/plugins/kxm/src/improve-sources.ts +228 -0
  76. package/plugins/kxm/src/improve.ts +325 -140
  77. package/plugins/kxm/src/local-snapshot.ts +101 -42
  78. package/plugins/kxm/src/mcp-server.ts +129 -30
  79. package/plugins/kxm/src/memory.ts +43 -20
  80. package/plugins/kxm/src/project-config.ts +25 -0
  81. package/plugins/kxm/src/protocol.ts +11 -0
  82. package/plugins/kxm/src/relevance.ts +138 -0
  83. package/plugins/kxm/src/retrospective.ts +16 -10
  84. package/plugins/kxm/src/runtime-service.ts +8 -1
  85. package/plugins/kxm/src/runtime-supervisor.ts +16 -2
  86. package/plugins/kxm/src/session-token-hint.ts +17 -0
  87. package/plugins/kxm/src/suggest.ts +7 -7
  88. package/plugins/kxm/src/workflow-manager.ts +80 -78
  89. package/plugins/kxm/src/workflow.ts +202 -12
  90. package/scripts/build-runtime.mjs +7 -1
  91. package/scripts/check-generated.mjs +1 -0
  92. 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
- return finishInit(1, next);
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
- export const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
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${RUN_ENGINE_NOTICE}`);
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
- export async function cmdImprove(runtime: Runtime, options: { file?: string | undefined; target?: string | undefined; outDir?: string | undefined } = {}): Promise<number> {
518
- const file = options.file ? resolve(runtime.cwd, options.file) : telemetryPath(runtime.dirs.logs);
519
- const routingRecords = existsSync(file)
520
- ? readRoutingRecords(file).map((entry) => entry.routing)
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(runtime.cwd, ".kxm", "candidates");
572
+ : join(root, ".kxm", "candidates");
526
573
 
527
- const report = buildImprovementReport(routingRecords, {
574
+ const report = buildImprovementReport(loaded.records, {
528
575
  candidatesDir,
529
- projectRoot: runtime.cwd,
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 = formatImprovementReport(report);
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
- const file = options.file ?? telemetryPath(runtime.dirs.logs);
825
- const records = readRoutingRecords(file).map((entry) => entry.routing);
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 (!workflowId || options.pick) {
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 {
@@ -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("KontextMind local-first orchestration CLI")
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; no steps execute until the run engine lands)")
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 [runId] [category] [area] [summary]").description("Record workflow journal knowledge"))
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>", "plan, decision, contradiction, error, lesson")
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
- .action(async function workflowAddAction(this: Command, workflowId?: string, options?: { file?: string; description?: string; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
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 CLI or project improvements from routing records and telemetry"));
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>", "Telemetry JSONL file to read routing records from")
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; target?: string; outDir?: 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>", "Telemetry or event log JSONL file (default: workspace telemetry)")
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: ImprovementArea;
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 provenance. */
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: ContextItemAuditMetadata[]; unresolvedGaps: string[] }> {
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