@kontextmind/kxm 0.7.188 → 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.188",
14
+ "version": "0.7.189",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
@@ -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` |
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.188",
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.188",
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.188";
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.188",
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.188";
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();