@kontextmind/kxm 0.7.187 → 0.7.189

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.187",
14
+ "version": "0.7.189",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/.kxm/README.md CHANGED
@@ -19,6 +19,7 @@ a `default` workflow, `gates.yaml` and `template-provenance.yaml`.
19
19
  | `models/inventory.yaml` | The discovered model catalog | Generated; track it for a reviewed snapshot |
20
20
  | `config.yaml` | Shared personalization settings | Tracked if the project shares them |
21
21
  | `memory/` | Project memory facts that `kxm memory` projects into `AGENTS.md`, `CLAUDE.md` and `GEMINI.md` | Tracked |
22
+ | `knowledge/` | Authored project wiki. The hub-compiled view, when written, is `knowledge/wiki/` | Tracked |
22
23
  | `assets/` | Intentional workflow inputs and outputs; `assets/generated/` is for machine output | Tracked intentionally; `generated/` ignored |
23
24
  | `tasks/` | Task records from `kxm task` | Your choice |
24
25
  | `logs/` | Hub, worker and agent logs | Ignored |
package/docs/README.md CHANGED
@@ -110,6 +110,7 @@ KXM connects coding agents through a durable, authenticated [hub](glossary.md#hu
110
110
  |---|---|---|
111
111
  | [Develop KXM](contributing/development.md) | Contributors | Setup, build and verify, packaging, and the repository layout |
112
112
  | [CI and release](contributing/ci-and-release.md) | Maintainers | What CI runs on each change and how releases ship |
113
+ | [Project knowledge wiki](../.kxm/knowledge/index.md) | Maintainers | Authored notes on architecture, routing, release, and CI, with file-path sources |
113
114
  | [Write KXM documentation](contributing/writing-docs.md) | Contributors | The style rules, the page template and the docs checks |
114
115
  | [Test matrix](contributing/test-matrix.md) | Maintainers | Features and use cases mapped to automated evidence |
115
116
  | [Assignment runner](contributing/assignment-runner.md) | Maintainers | Developer assignments, witness verification and dual-critic acceptance |
@@ -18,3 +18,4 @@ against the current checkout.
18
18
 
19
19
  - [Roadmap](../roadmap/dashboard.md)
20
20
  - [Trust model](../concepts/trust-model.md)
21
+ - [Project knowledge wiki](../../.kxm/knowledge/architecture/overview.md)
@@ -39,7 +39,7 @@ together.
39
39
  |---|---|---|
40
40
  | Before you push | Local | `npm run verify` |
41
41
  | Pull request, push to `main`, or manual | `ci.yml` (`required`) | Aggregates the lanes below into one pass/fail check named `CI / required` |
42
- | Pull request and push | `ci.yml` (Docs lint) | `lint:docs` and `check:versions`, always |
42
+ | Pull request and push | `ci.yml` (Docs lint) | `lint:docs` (markdownlint and the `plans/` relative-link check) and `check:versions`, always |
43
43
  | Code pull request and code push | `ci.yml` (Unit engine and Unit, Linux Node 24) | `engine.test.ts` split by test name; `permission.test.ts`, `runtime.test.ts`, and `package-install.test.ts` one file at a time; every other unit file in two light shards. Typecheck and `check-generated` run on `light-1` |
44
44
  | Code pull request and code push | `ci.yml` (Python control plane) | Python 3.13 `unittest` contracts from the frozen uv workspace against PostgreSQL 17 started by `scripts/ci-postgres-17.sh` |
45
45
  | Platform-sensitive pull request | `ci.yml` (Unit, Windows Node 24) | The same unit lanes on `windows-latest` |
@@ -299,3 +299,4 @@ implying automated coverage.
299
299
  - [Test matrix](test-matrix.md): which test proves which behavior
300
300
  - [Write KXM documentation](writing-docs.md): doc gates and pinned paths
301
301
  - [Upgrade KXM](../operations/upgrade.md): the operator side of a new version
302
+ - [Project knowledge wiki](../../.kxm/knowledge/index.md): authored notes for [CI](../../.kxm/knowledge/patterns/ci.md) and [release](../../.kxm/knowledge/patterns/release-flow.md)
@@ -255,7 +255,7 @@ It runs three scripts in order:
255
255
  | Step | Script | What it checks |
256
256
  |---|---|---|
257
257
  | 1 | `npm test` | Build, then `test/core/*.test.ts` and `packages/core/*/tests/unit/*.test.ts`. The script passes `--test-force-exit` so a finished run leaves the process even if a handle is still open; a failing test is still reported before that exit |
258
- | 2 | `npm run check` | `tsc --noEmit`, `markdownlint-cli2`, and version surfaces |
258
+ | 2 | `npm run check` | `tsc --noEmit`, markdownlint, the `plans/` relative-link check, and version surfaces |
259
259
  | 3 | `npm run check:generated` | Generated artifacts are tracked and match the staged copy |
260
260
 
261
261
  Changes to the Python control plane also run its frozen-workspace contract
@@ -303,7 +303,7 @@ Other scripts you will use:
303
303
  | `npm run test:simulations` | `test/simulations/*.test.ts` |
304
304
  | `npm run test:coverage` | Core suite with coverage floors |
305
305
  | `npm run test:coverage:complete` | Core plus simulations with the higher nightly floors |
306
- | `npm run lint:docs` | Markdown lint only |
306
+ | `npm run lint:docs` | Markdown lint, then the `plans/` relative-link and heading check |
307
307
  | `npm run e2e` | Start Obscura if needed, then Playwright in `test/e2e/` (`node --test` does not collect that directory) |
308
308
  | `npm run validate:pr` | The three-minute CI gate; see [CI and release](ci-and-release.md) |
309
309
  | `npm run validate:ci` | Coverage suite, `check` and a package dry run (not run by CI today) |
@@ -297,8 +297,15 @@ exist only in prose, never in command form.
297
297
 
298
298
  ### Check links
299
299
 
300
- No CI job checks links yet. Before you open a pull request, run this check on
301
- the files you changed; it prints each relative link whose target is missing:
300
+ `npm run lint:docs` runs markdownlint, then `scripts/check-plan-links.mjs`.
301
+ That script checks every Markdown link under `plans/`. A relative target has
302
+ to exist. A `#heading` on a Markdown file has to match a heading in that
303
+ file. `related` entries in plan front matter are checked the same way, relative
304
+ to the file. Docs lint runs this on every pull request.
305
+
306
+ For a page outside `plans/`, run this check on the files you changed. It
307
+ prints each relative link whose target is missing, and it does not check
308
+ anchors:
302
309
 
303
310
  ```bash
304
311
  node --input-type=module -e '
package/docs/glossary.md CHANGED
@@ -397,7 +397,7 @@ Project facts stored as state keys with a `proposed → current → superseded`
397
397
 
398
398
  ### Wiki
399
399
 
400
- Markdown pages compiled from a project's context records by `kxm context wiki-compile` (a dry run unless you pass `--out`), with state, decisions, contradictions and history linked to their evidence. `kxm context wiki-lint` checks the result.
400
+ Markdown pages compiled from a project's context records by `kxm context wiki-compile` (a dry run unless you pass `--out`), with state, decisions, contradictions and history linked to their evidence. `kxm context wiki-lint` checks the result. This repository's authored notes live at [`.kxm/knowledge/`](../.kxm/knowledge/index.md) and are not that compiled view.
401
401
 
402
402
  ## Configuration and routing
403
403
 
@@ -230,6 +230,8 @@ kxm context wiki-lint demo
230
230
 
231
231
  Pages land under `.kxm/knowledge/wiki/`: one page per section (`architecture`, `decisions`, `incidents`, `patterns`), a temporal-state page that keeps superseded values visible, a contradictions page, a superseded-history page and `index.md`. Every claim links its record id and source. Compilation never resolves a contradiction. Output is deterministic except the timestamp in `index.md`, so diffs stay small. `wiki-lint` checks for broken references, orphan pages, stale state links and hidden contradictions, and exits 1 on an error.
232
232
 
233
+ This repository also keeps an authored project wiki at [`.kxm/knowledge/`](../../.kxm/knowledge/index.md). Those pages are maintainer notes with file-path sources. `kxm context wiki-compile` does not read them. With `--out`, the compiled view is still written under `.kxm/knowledge/wiki/`.
234
+
233
235
  ## Author Git memory
234
236
 
235
237
  Git memory is the durable, reviewed layer: facts a person has approved, versioned with the code. Each fact is a Markdown file with `kxm.memory.v1` front matter.
@@ -1733,6 +1733,7 @@ Everything under `.kxm/` at the project root falls into one of three groups.
1733
1733
  | `roles/`, `routes.yaml`, `prices.yaml`, `modes.yaml`, `the role files` | Tracked configuration outside the bundle | You and their commands |
1734
1734
  | `config.yaml` | Tracked if the project wants shared preferences; otherwise ignore it | `kxm config set` |
1735
1735
  | `memory/`, `skills/`, `candidates/`, `goals/` | Tracked durable records | Their commands |
1736
+ | `knowledge/` | Tracked authored wiki. `knowledge/wiki/` is the compiled view from `kxm context wiki-compile --out` | Maintainers, and `kxm context wiki-compile` for `wiki/` |
1736
1737
  | `models/inventory.yaml` | Generated; track it if you want a reviewed snapshot | `kxm models inventory-refresh` |
1737
1738
  | `assets/` | Tracked intentionally; `assets/generated/` is ignored | Workflows, `kxm improve`, `kxm session start` |
1738
1739
  | `tasks/` | Ignored in the KXM repository; your choice | `kxm task` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.187",
3
+ "version": "0.7.189",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -44,7 +44,7 @@
44
44
  "test:coverage:complete": "npm run build && node scripts/run-bounded.mjs 2400000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 --experimental-test-coverage --test-coverage-lines=93 --test-coverage-branches=80 --test-coverage-functions=93 --test-coverage-include=plugins/kxm/src/**/*.ts --test-coverage-exclude=plugins/kxm/src/server.ts --test-coverage-exclude=plugins/kxm/src/mcp-server.ts --test-coverage-exclude=plugins/kxm/src/runtime-supervisor.ts --test-coverage-include=packages/core/*/src/**/*.ts test/core/*.test.ts test/simulations/*.test.ts packages/core/*/tests/unit/*.test.ts",
45
45
  "test:coverage": "npm run test:coverage:core",
46
46
  "typecheck": "tsc --noEmit",
47
- "lint:docs": "markdownlint-cli2",
47
+ "lint:docs": "markdownlint-cli2 && node scripts/check-plan-links.mjs",
48
48
  "check:versions": "node scripts/check-versions.mjs",
49
49
  "check": "npm run typecheck && npm run lint:docs && npm run check:versions && node scripts/workforce-lint.mjs",
50
50
  "verify": "npm test && npm run check && npm run check:generated",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.187",
5
+ "version": "0.7.189",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
25462
25462
  }
25463
25463
 
25464
25464
  // plugins/kxm/src/mcp-server.ts
25465
- var VERSION = "0.7.187";
25465
+ var VERSION = "0.7.189";
25466
25466
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25467
25467
  var inbox = /* @__PURE__ */ new Map();
25468
25468
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.187",
3
+ "version": "0.7.189",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.187";
14
+ const VERSION = "0.7.189";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -0,0 +1,4 @@
1
+ export function githubSlug(value: string): string;
2
+ export function headingIds(markdown: string): Set<string>;
3
+ export function checkFile(file: string): string[];
4
+ export function checkPlans(root?: string): string[];
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Relative links and heading anchors under plans/.
4
+ // Docs lint runs this. It does not call the network.
5
+
6
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ export function githubSlug(value) {
13
+ return value.toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, "").replace(/ /g, "-");
14
+ }
15
+
16
+ function headingText(raw) {
17
+ let text = raw.trim().replace(/\s+#+\s*$/, "");
18
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
19
+ text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
20
+ text = text.replace(/<([^>\s]+)>/g, "$1");
21
+ text = text.replace(/`([^`]*)`/g, "$1");
22
+ text = text.replace(/<\/?[^>]+>/g, "");
23
+ text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
24
+ text = text.replace(/&quot;/g, '"').replace(/&#39;/g, "'");
25
+ return text;
26
+ }
27
+
28
+ export function headingIds(markdown) {
29
+ const lines = markdown.split("\n");
30
+ const ids = new Set();
31
+ const occurrences = Object.create(null);
32
+ const own = Object.hasOwnProperty;
33
+ let index = 0;
34
+ let fence = null;
35
+ if (lines[0]?.trim() === "---") {
36
+ index = 1;
37
+ while (index < lines.length && lines[index].trim() !== "---") index += 1;
38
+ index += 1;
39
+ }
40
+ function addSlug(text) {
41
+ const original = githubSlug(headingText(text));
42
+ let result = original;
43
+ while (own.call(occurrences, result)) {
44
+ occurrences[original] = (occurrences[original] || 0) + 1;
45
+ result = `${original}-${occurrences[original]}`;
46
+ }
47
+ occurrences[result] = 0;
48
+ ids.add(result);
49
+ }
50
+ for (; index < lines.length; index += 1) {
51
+ const line = lines[index];
52
+ const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
53
+ if (opened && !fence) {
54
+ fence = opened[1];
55
+ continue;
56
+ }
57
+ if (fence && new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) {
58
+ fence = null;
59
+ continue;
60
+ }
61
+ if (fence) continue;
62
+ const atx = line.match(/^(#{1,6})\s+(.+?)\s*$/);
63
+ if (atx) addSlug(atx[2]);
64
+ for (const match of line.matchAll(/\sid=["']([^"']+)["']/g)) ids.add(match[1]);
65
+ for (const match of line.matchAll(/\sname=["']([^"']+)["']/g)) ids.add(match[1]);
66
+ }
67
+ return ids;
68
+ }
69
+
70
+ function visibleLines(markdown) {
71
+ const lines = markdown.split("\n");
72
+ const visible = [];
73
+ let fence = null;
74
+ let comment = false;
75
+ for (const line of lines) {
76
+ if (!fence && !comment) {
77
+ const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
78
+ if (opened) {
79
+ fence = opened[1];
80
+ visible.push("");
81
+ continue;
82
+ }
83
+ }
84
+ if (fence) {
85
+ if (new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) fence = null;
86
+ visible.push("");
87
+ continue;
88
+ }
89
+ let rest = line;
90
+ let kept = "";
91
+ while (rest.length > 0) {
92
+ if (comment) {
93
+ const end = rest.indexOf("-->");
94
+ if (end < 0) {
95
+ rest = "";
96
+ break;
97
+ }
98
+ comment = false;
99
+ rest = rest.slice(end + 3);
100
+ continue;
101
+ }
102
+ const start = rest.indexOf("<!--");
103
+ if (start < 0) {
104
+ kept += rest;
105
+ rest = "";
106
+ break;
107
+ }
108
+ kept += rest.slice(0, start);
109
+ rest = rest.slice(start + 4);
110
+ comment = true;
111
+ }
112
+ visible.push(kept);
113
+ }
114
+ return visible;
115
+ }
116
+
117
+ function destinationOf(raw) {
118
+ let dest = raw.trim();
119
+ if (dest.startsWith("<") && dest.endsWith(">")) dest = dest.slice(1, -1).trim();
120
+ dest = dest.replace(/\s+"[^"]*"$/, "").replace(/\s+'[^']*'$/, "");
121
+ return dest;
122
+ }
123
+
124
+ function problemsForDestination(file, lineNumber, dest) {
125
+ if (!dest || /^[a-z][a-z0-9+.-]*:/i.test(dest)) return [];
126
+ if (dest.startsWith("//")) return [];
127
+ if (dest.startsWith("/")) {
128
+ return [`${file}:${lineNumber}: absolute path is not a repository link: ${dest}`];
129
+ }
130
+ const hash = dest.indexOf("#");
131
+ let pathPart = hash >= 0 ? dest.slice(0, hash) : dest;
132
+ const frag = hash >= 0 ? dest.slice(hash + 1) : "";
133
+ pathPart = pathPart.split("?")[0];
134
+ let decodedPath = pathPart;
135
+ let decodedFrag = frag;
136
+ try { decodedPath = decodeURIComponent(pathPart); } catch { /* keep raw */ }
137
+ try { decodedFrag = decodeURIComponent(frag); } catch { /* keep raw */ }
138
+ if (!decodedPath) {
139
+ if (!decodedFrag) return [];
140
+ const ids = headingIds(readFileSync(file, "utf8"));
141
+ if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor #${decodedFrag}`];
142
+ return [];
143
+ }
144
+ const target = resolve(dirname(file), decodedPath);
145
+ if (!existsSync(target)) return [`${file}:${lineNumber}: missing ${dest}`];
146
+ if (decodedFrag && target.endsWith(".md")) {
147
+ const ids = headingIds(readFileSync(target, "utf8"));
148
+ if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor ${dest}`];
149
+ }
150
+ return [];
151
+ }
152
+
153
+ function relatedProblems(file, markdown) {
154
+ if (!markdown.startsWith("---\n")) return [];
155
+ const end = markdown.indexOf("\n---", 4);
156
+ if (end < 0) return [];
157
+ const front = markdown.slice(4, end).split("\n");
158
+ const problems = [];
159
+ let inRelated = false;
160
+ for (let index = 0; index < front.length; index += 1) {
161
+ const line = front[index];
162
+ const inline = line.match(/^related:\s*\[(.*)\]\s*$/);
163
+ if (inline) {
164
+ for (const item of inline[1].split(",")) {
165
+ const rel = item.trim().replace(/^["']|["']$/g, "");
166
+ if (!rel || /^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
167
+ if (!existsSync(resolve(dirname(file), rel))) {
168
+ problems.push(`${file}:${index + 2}: missing related ${rel}`);
169
+ }
170
+ }
171
+ continue;
172
+ }
173
+ if (/^related:\s*$/.test(line)) {
174
+ inRelated = true;
175
+ continue;
176
+ }
177
+ if (inRelated && /^\S/.test(line)) inRelated = false;
178
+ if (!inRelated) continue;
179
+ const item = line.match(/^\s+-\s+(.+?)\s*$/);
180
+ if (!item) continue;
181
+ const rel = item[1].replace(/^["']|["']$/g, "");
182
+ if (/^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
183
+ if (!existsSync(resolve(dirname(file), rel))) {
184
+ problems.push(`${file}:${index + 2}: missing related ${rel}`);
185
+ }
186
+ }
187
+ return problems;
188
+ }
189
+
190
+ const linkRe = /(!?\[[^\]\n]*\])\(([^)\n]+)\)/g;
191
+ // Footnote bodies (`[^id]: prose`) are not link destinations. The URL inside
192
+ // the body is an ordinary inline link.
193
+ const refRe = /^ {0,3}\[([^\]^\n][^\]]*)\]:\s+(\S+)/;
194
+
195
+ export function checkFile(file) {
196
+ const markdown = readFileSync(file, "utf8");
197
+ const problems = relatedProblems(file, markdown);
198
+ const lines = visibleLines(markdown);
199
+ for (let index = 0; index < lines.length; index += 1) {
200
+ const line = lines[index];
201
+ for (const match of line.matchAll(linkRe)) {
202
+ problems.push(...problemsForDestination(file, index + 1, destinationOf(match[2])));
203
+ }
204
+ const ref = line.match(refRe);
205
+ if (ref) problems.push(...problemsForDestination(file, index + 1, destinationOf(ref[2])));
206
+ }
207
+ return problems;
208
+ }
209
+
210
+ function walk(dir, out = []) {
211
+ for (const name of readdirSync(dir)) {
212
+ const path = join(dir, name);
213
+ if (statSync(path).isDirectory()) walk(path, out);
214
+ else if (name.endsWith(".md")) out.push(path);
215
+ }
216
+ return out;
217
+ }
218
+
219
+ export function checkPlans(root = repoRoot) {
220
+ const dir = join(root, "plans");
221
+ return walk(dir).flatMap((file) => checkFile(file));
222
+ }
223
+
224
+ function main() {
225
+ const problems = checkPlans();
226
+ for (const problem of problems) console.error(problem);
227
+ if (problems.length > 0) {
228
+ console.error(`check-plan-links: ${problems.length} broken`);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
234
+ if (invoked) main();