@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.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +1 -1
  3. package/CHANGELOG.md +204 -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 +29 -3
  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 +3068 -2446
  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 +61 -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/project-config.ts +25 -0
  80. package/plugins/kxm/src/protocol.ts +11 -0
  81. package/plugins/kxm/src/relevance.ts +138 -0
  82. package/plugins/kxm/src/retrospective.ts +16 -10
  83. package/plugins/kxm/src/runtime-service.ts +8 -1
  84. package/plugins/kxm/src/runtime-supervisor.ts +16 -2
  85. package/plugins/kxm/src/session-token-hint.ts +17 -0
  86. package/plugins/kxm/src/suggest.ts +7 -7
  87. package/plugins/kxm/src/workflow-manager.ts +80 -78
  88. package/plugins/kxm/src/workflow.ts +202 -12
  89. package/scripts/build-runtime.mjs +7 -1
  90. package/scripts/check-generated.mjs +1 -0
  91. package/scripts/emit-codex-artifacts.mjs +1 -1
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
+ IMPROVEMENT_AREAS,
3
4
  MAX_MESSAGE_TTL_MS,
4
5
  MIN_MESSAGE_TTL_MS,
5
6
  ProtocolError,
@@ -20,6 +21,8 @@ import {
20
21
  type WorkflowEvidenceReferenceInput,
21
22
  type WorkflowMessageContext,
22
23
  } from "./protocol.ts";
24
+ import { redactSecrets } from "./redact.ts";
25
+ import { compareCodeUnitIds } from "./relevance.ts";
23
26
 
24
27
  export type {
25
28
  ImprovementArea,
@@ -350,7 +353,7 @@ export interface WorkflowJournalEntry {
350
353
  relatedEntryIds: string[];
351
354
  createdAt: string;
352
355
  /** Stage the entry was recorded against. Optional: v0.4 entries and
353
- n * run-level entries have no stage binding. */
356
+ * run-level entries have no stage binding. */
354
357
  stageId?: string;
355
358
  /** Attempt the entry was recorded against, when stage-bound. */
356
359
  attempt?: number;
@@ -436,17 +439,8 @@ export interface ImprovementAreaReport {
436
439
  }
437
440
 
438
441
  export function improvementReport(entries: WorkflowJournalEntry[]): ImprovementAreaReport[] {
439
- const areas: ImprovementArea[] = [
440
- "harness",
441
- "gates",
442
- "implementation",
443
- "workflow",
444
- "documentation",
445
- "security",
446
- "other",
447
- ];
448
442
  const severityWeight = { error: 3, warning: 2, info: 1 } as const;
449
- return areas.map((area) => {
443
+ return IMPROVEMENT_AREAS.map((area) => {
450
444
  const matching = entries.filter((entry) => entry.area === area);
451
445
  const priorities = [...matching]
452
446
  .filter((entry) => entry.category === "error" || entry.category === "contradiction" || entry.category === "lesson" || entry.category === "skill-candidate")
@@ -463,6 +457,184 @@ export function improvementReport(entries: WorkflowJournalEntry[]): ImprovementA
463
457
  }).filter((report) => report.total > 0);
464
458
  }
465
459
 
460
+ /** Diagnostic classes that put a signal in the security tier regardless of
461
+ * its priority (diagnostics.ts). */
462
+ export const SECURITY_SIGNAL_CLASSES: readonly string[] = ["invalid_auth", "invalid_identity", "signal_mismatch"];
463
+
464
+ const SIGNAL_CATEGORIES: readonly JournalCategory[] = ["error", "contradiction", "lesson", "skill-candidate"];
465
+ const SIGNAL_SEVERITY_WEIGHT = { error: 3, warning: 2, info: 1 } as const;
466
+ const SIGNAL_CLASS = /^[a-z0-9_]{1,64}$/;
467
+ const MAX_SIGNAL_IDS = 16;
468
+ const MAX_SIGNAL_SUMMARY_KEY_CHARS = 160;
469
+
470
+ /** Collapse volatile tokens so the same failure in two runs keys the same.
471
+ * Callers redact first: lowercasing and digit folding would otherwise defeat
472
+ * the secret patterns. */
473
+ export function normalizeSignalSummary(summary: string): string {
474
+ return summary
475
+ .toLowerCase()
476
+ .replace(/\b[a-z]+_[0-9a-f]{8,}\b/g, "<id>")
477
+ .replace(/\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:z|[+-]\d{2}:?\d{2})?/g, "<ts>")
478
+ .replace(/\b[0-9a-f]{7,}\b/g, "<hex>")
479
+ .replace(/\d+/g, "#")
480
+ .replace(/\s+/g, " ")
481
+ .trim()
482
+ .slice(0, MAX_SIGNAL_SUMMARY_KEY_CHARS);
483
+ }
484
+
485
+ /** One improvement signal: journal entries from one or more runs that share
486
+ * a key, scored by frequency x severity x run-attempt cost x evidence
487
+ * confidence. Text is redacted; the key never carries raw summary text. */
488
+ export interface ImprovementSignal {
489
+ key: string;
490
+ /** Which rule produced the key: an evidence class, an error's stage, or the
491
+ * normalized summary. */
492
+ basis: "class" | "stage" | "summary";
493
+ category: JournalCategory;
494
+ /** Modal area of the grouped entries; ties follow IMPROVEMENT_AREAS order. */
495
+ area: ImprovementArea;
496
+ /** Security signals rank ahead of every priority. */
497
+ overrideTier?: "security";
498
+ /** Distinct runs that recorded this signal. */
499
+ frequency: number;
500
+ runIds: string[];
501
+ entryIds: string[];
502
+ severity: WorkflowJournalEntry["severity"];
503
+ severityWeight: number;
504
+ /** Mean run attempts (stage attempts plus transitions) over the known runs;
505
+ * null when none of the runs is known. */
506
+ workflowCost: number | null;
507
+ costBasis: "run-attempts" | "unknown";
508
+ /** 0.5 plus half the fraction of entries that cite evidence. */
509
+ confidence: number;
510
+ priority: number;
511
+ /** Redacted summary of the latest entry. */
512
+ summary: string;
513
+ }
514
+
515
+ function round3(value: number): number {
516
+ return Math.round(value * 1000) / 1000;
517
+ }
518
+
519
+ function signalKeyOf(
520
+ entry: WorkflowJournalEntry,
521
+ runs: ReadonlyMap<string, WorkflowRun>,
522
+ ): { key: string; basis: ImprovementSignal["basis"]; signalClass?: string } {
523
+ const classRef = entry.evidence.find((ref) => ref.startsWith("class:"));
524
+ const signalClass = classRef?.slice("class:".length);
525
+ if (signalClass !== undefined && SIGNAL_CLASS.test(signalClass) && signalClass !== "unknown") {
526
+ return { key: `${entry.category}|class:${signalClass}`, basis: "class", signalClass };
527
+ }
528
+ const run = runs.get(entry.runId);
529
+ if (entry.category === "error" && entry.stageId !== undefined && run) {
530
+ return { key: `error|stage:${run.definitionId}/${entry.stageId}`, basis: "stage" };
531
+ }
532
+ return {
533
+ key: `${entry.category}|summary:${normalizeSignalSummary(redactSecrets(entry.summary))}`,
534
+ basis: "summary",
535
+ };
536
+ }
537
+
538
+ function runAttemptCost(run: WorkflowRun): number {
539
+ let attempts = 0;
540
+ for (const stage of run.stages) attempts += stage.attempts;
541
+ return Math.max(1, attempts + (run.transitions?.length ?? 0));
542
+ }
543
+
544
+ /** Merge journal learning across runs into ranked, redacted signals. Only
545
+ * errors, open contradictions, lessons and still-proposed skill candidates
546
+ * count. Security signals come first, then priority, frequency and key; no
547
+ * id or insertion order decides a tie. */
548
+ export function rankImprovementSignals(
549
+ entries: readonly WorkflowJournalEntry[],
550
+ runs: ReadonlyMap<string, WorkflowRun>,
551
+ limit = 20,
552
+ ): ImprovementSignal[] {
553
+ const resolvedContradictions = new Set<string>();
554
+ for (const entry of entries) {
555
+ if (entry.category !== "decision" && entry.category !== "lesson") continue;
556
+ for (const related of entry.relatedEntryIds) resolvedContradictions.add(`${entry.runId}\u0000${related}`);
557
+ }
558
+
559
+ const groups = new Map<string, {
560
+ basis: ImprovementSignal["basis"];
561
+ category: JournalCategory;
562
+ signalClass?: string;
563
+ entries: WorkflowJournalEntry[];
564
+ }>();
565
+ for (const entry of entries) {
566
+ if (!SIGNAL_CATEGORIES.includes(entry.category)) continue;
567
+ if (entry.category === "contradiction" && resolvedContradictions.has(`${entry.runId}\u0000${entry.id}`)) continue;
568
+ const promotionState = journalPromotionState(entry);
569
+ if (promotionState !== undefined && promotionState !== "proposed") continue;
570
+ const { key, basis, signalClass } = signalKeyOf(entry, runs);
571
+ const group = groups.get(key);
572
+ if (group) group.entries.push(entry);
573
+ else groups.set(key, { basis, category: entry.category, ...(signalClass !== undefined ? { signalClass } : {}), entries: [entry] });
574
+ }
575
+
576
+ const areaRank = (area: string): number => {
577
+ const index = IMPROVEMENT_AREAS.indexOf(area as ImprovementArea);
578
+ return index === -1 ? IMPROVEMENT_AREAS.length : index;
579
+ };
580
+
581
+ const signals: ImprovementSignal[] = [];
582
+ for (const [key, group] of groups) {
583
+ const runIds = [...new Set(group.entries.map((entry) => entry.runId))].sort(compareCodeUnitIds);
584
+ const entryIds = group.entries.map((entry) => entry.id).sort(compareCodeUnitIds);
585
+
586
+ let severity: WorkflowJournalEntry["severity"] = "info";
587
+ for (const entry of group.entries) {
588
+ if ((SIGNAL_SEVERITY_WEIGHT[entry.severity] ?? 0) > SIGNAL_SEVERITY_WEIGHT[severity]) severity = entry.severity;
589
+ }
590
+ const severityWeight = SIGNAL_SEVERITY_WEIGHT[severity];
591
+
592
+ const knownRuns = runIds.map((runId) => runs.get(runId)).filter((run): run is WorkflowRun => run !== undefined);
593
+ const workflowCost = knownRuns.length > 0
594
+ ? knownRuns.reduce((total, run) => total + runAttemptCost(run), 0) / knownRuns.length
595
+ : null;
596
+
597
+ const withEvidence = group.entries.filter((entry) => entry.evidence.length > 0).length;
598
+ const confidence = 0.5 + 0.5 * (withEvidence / group.entries.length);
599
+
600
+ const areaCounts = new Map<ImprovementArea, number>();
601
+ for (const entry of group.entries) areaCounts.set(entry.area, (areaCounts.get(entry.area) ?? 0) + 1);
602
+ const area = [...areaCounts].sort((left, right) => right[1] - left[1] || areaRank(left[0]) - areaRank(right[0]))[0]![0];
603
+
604
+ const latest = [...group.entries].sort((left, right) =>
605
+ compareCodeUnitIds(left.createdAt, right.createdAt) || compareCodeUnitIds(left.id, right.id)).at(-1)!;
606
+
607
+ const security = area === "security"
608
+ || (group.signalClass !== undefined && SECURITY_SIGNAL_CLASSES.includes(group.signalClass));
609
+
610
+ signals.push({
611
+ key,
612
+ basis: group.basis,
613
+ category: group.category,
614
+ area,
615
+ ...(security ? { overrideTier: "security" as const } : {}),
616
+ frequency: runIds.length,
617
+ runIds: runIds.slice(0, MAX_SIGNAL_IDS),
618
+ entryIds: entryIds.slice(0, MAX_SIGNAL_IDS),
619
+ severity,
620
+ severityWeight,
621
+ workflowCost,
622
+ costBasis: workflowCost === null ? "unknown" : "run-attempts",
623
+ confidence,
624
+ priority: round3(runIds.length * severityWeight * (workflowCost ?? 1) * confidence),
625
+ summary: redactSecrets(latest.summary),
626
+ });
627
+ }
628
+
629
+ return signals
630
+ .sort((left, right) =>
631
+ (right.overrideTier === "security" ? 1 : 0) - (left.overrideTier === "security" ? 1 : 0)
632
+ || right.priority - left.priority
633
+ || right.frequency - left.frequency
634
+ || compareCodeUnitIds(left.key, right.key))
635
+ .slice(0, limit);
636
+ }
637
+
466
638
  function object(value: unknown, name: string): Record<string, unknown> {
467
639
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
468
640
  return value as Record<string, unknown>;
@@ -585,6 +757,24 @@ export function activeWorkflowAttempt(stage: Pick<WorkflowStageState, "attempts"
585
757
  return stage.attempts + 1;
586
758
  }
587
759
 
760
+ /** The attempt a stage-bound journal entry belongs to. An active or waiting
761
+ * stage is on its next attempt; a finished stage is on the last attempt it
762
+ * consumed; a pending stage that never ran has no attempt yet. The caller
763
+ * never supplies it. */
764
+ export function journalAttemptFor(stage: Pick<WorkflowStageState, "status" | "attempts">): number | undefined {
765
+ switch (stage.status) {
766
+ case "in_progress":
767
+ case "waiting":
768
+ return stage.attempts + 1;
769
+ case "pending":
770
+ return stage.attempts > 0 ? stage.attempts : undefined;
771
+ case "passed":
772
+ case "warning":
773
+ case "failed":
774
+ return Math.max(1, stage.attempts);
775
+ }
776
+ }
777
+
588
778
  export interface EvidenceLookup {
589
779
  getMessage(id: string): MessageRecord | undefined;
590
780
  }
@@ -998,7 +1188,7 @@ export function parseWorkflowDefinitions(
998
1188
  const area = stage.area
999
1189
  ? requireString(stage.area, "stage.area", { max: 24 }) as ImprovementArea
1000
1190
  : undefined;
1001
- if (area && !["harness", "gates", "implementation", "workflow", "documentation", "security", "other"].includes(area)) {
1191
+ if (area && !IMPROVEMENT_AREAS.includes(area)) {
1002
1192
  throw new Error(`stage ${stageId} area is invalid`);
1003
1193
  }
1004
1194
  const requiredEvidence = stringArray(stage.requiredEvidence ?? [], "stage.requiredEvidence")
@@ -3,7 +3,12 @@ import { build } from "esbuild";
3
3
 
4
4
 
5
5
  await build({
6
- entryPoints: ["plugins/kxm/src/cli.ts", "plugins/kxm/src/server.ts", "plugins/kxm/src/runtime-supervisor.ts"],
6
+ entryPoints: [
7
+ "plugins/kxm/src/cli.ts",
8
+ "plugins/kxm/src/server.ts",
9
+ "plugins/kxm/src/runtime-supervisor.ts",
10
+ "plugins/kxm/src/claude-hook.ts",
11
+ ],
7
12
  bundle: true,
8
13
  platform: "node",
9
14
  format: "esm",
@@ -41,6 +46,7 @@ for (const bundlePath of [
41
46
  "plugins/kxm/dist/cli.js",
42
47
  "plugins/kxm/dist/server.js",
43
48
  "plugins/kxm/dist/runtime-supervisor.js",
49
+ "plugins/kxm/dist/claude-hook.js",
44
50
  ]) {
45
51
  const bundled = readFileSync(bundlePath, "utf8");
46
52
  if (!bundled.startsWith(shebang)) throw new Error(`runtime bundle is missing its executable shebang: ${bundlePath}`);
@@ -15,6 +15,7 @@ export const STATIC_GENERATED_ARTIFACTS = Object.freeze([
15
15
  "plugins/kxm/dist/runtime.js",
16
16
  "plugins/kxm/dist/client.js",
17
17
  "plugins/kxm/dist/extension.js",
18
+ "plugins/kxm/dist/claude-hook.js",
18
19
  "packages/core/tui/dist/index.js",
19
20
  "AGENTS.md",
20
21
  "CLAUDE.md",
@@ -43,7 +43,7 @@ The \`kxm\` CLI is the unified agent surface for peer collaboration and workflow
43
43
  | Command | Purpose | Key options |
44
44
  |---|---|---|
45
45
  | \`kxm workflow checkpoint [runId] [stageId] [status] [summary]\` | Record stage result with verified evidence | \`--run-id\`, \`--stage-id\`, \`--status <passed\\|warning\\|failed>\`, \`--summary\`, \`--evidence <json>\`, \`--evidence-refs <json>\` |
46
- | \`kxm workflow record [runId] [category] [area] [summary]\` | Record plans, decisions, contradictions, errors, lessons | \`--run-id\`, \`--category <plan\\|decision\\|contradiction\\|error\\|lesson>\`, \`--area\`, \`--severity <info\\|warning\\|error>\`, \`--details\`, \`--evidence <items...>\` |
46
+ | \`kxm workflow record [runId] [category] [area] [summary]\` | Record journal knowledge in one of ten categories; area is optional with \`--stage-id\` | \`--run-id\`, \`--category <plan\\|decision\\|contradiction\\|error\\|lesson\\|observation\\|hypothesis\\|experiment\\|state-change\\|skill-candidate>\`, \`--stage-id\`, \`--area\`, \`--severity <info\\|warning\\|error>\`, \`--details\`, \`--evidence <items...>\` |
47
47
  | \`kxm workflow wait [runId] [stageId] [signalKey] [summary]\` | Pause stage until an external signed signal arrives | \`--run-id\`, \`--stage-id\`, \`--signal-key\`, \`--summary\`, \`--evidence <json>\`, \`--evidence-refs <json>\`, \`--timeout-ms\` |
48
48
  | \`kxm workflow signal <runId> <signalKey> <status> <summary>\` | Resume or unblock a waiting stage or KXM run | \`[evidence...]\`, \`--delivery-id\` |
49
49
  | \`kxm workflow list\` | List local workflow runs | \`--json\` |