@kontextmind/kxm 0.7.90 → 0.7.92

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.90",
14
+ "version": "0.7.92",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -185,6 +185,14 @@ All notable user-facing changes are documented here. The project follows [Semant
185
185
  none exist. The block is placed at the end of a file that has no markers yet, rather
186
186
  than before a `## Do not` heading that only this repository uses.
187
187
 
188
+ - **`kxm memory sync` refuses malformed memory markers instead of eating text.** Sync
189
+ used to replace from the first `<!-- kxm:memory:start -->` to the first
190
+ `<!-- kxm:memory:end -->` wherever they appeared, so an orphan start marker got a new
191
+ block appended and the following sync deleted everything between the orphan and that
192
+ block; an end before its start duplicated text; a second block went stale. A file
193
+ must now hold exactly one start marker followed by one end marker, or neither.
194
+ Otherwise sync exits non-zero naming the file and the problem, and writes no file.
195
+
188
196
  - **Release version surfaces cover workspace packages:** a merged PR no longer
189
197
  breaks the release pipeline. `scripts/kxm-bump-version.mjs` now writes the
190
198
  version into every package manifest under `packages/`, using the same package
@@ -303,7 +303,7 @@ The current hub command groups are `agent`, `session`, `workflow`, `gate`, `hub`
303
303
  | `kxm improve` | Bucket `.kxm/logs/telemetry.jsonl` events into a proposed-only improvement report; does not read the workflow journal |
304
304
  | `kxm context get \| recall \| state \| episode \| promote \| explain \| wiki-compile \| wiki-lint` | Context operating system: role-aware packets, metadata search, temporal state, episodes, evidence-backed lineage (`explain`), wiki compile/lint |
305
305
  | `kxm skills` | Governed skill candidate lifecycle |
306
- | `kxm memory sync` | Regenerate the read-only project-memory block — authored facts from `.kxm/memory/` only — between `<!-- kxm:memory:start -->` and `<!-- kxm:memory:end -->` in whichever of `AGENTS.md`, `CLAUDE.md` and `GEMINI.md` already exist in the current directory. Text outside the markers is never rewritten; a file without markers gets the block appended. Missing files are reported as `missing` and **not created** — which harness a project uses is its own choice — and with none present sync exits 1 without writing. `--json` reports `updated`, `unchanged` and `missing` |
306
+ | `kxm memory sync` | Regenerate the read-only project-memory block — authored facts from `.kxm/memory/` only — between `<!-- kxm:memory:start -->` and `<!-- kxm:memory:end -->` in whichever of `AGENTS.md`, `CLAUDE.md` and `GEMINI.md` already exist in the current directory. Text outside the markers is never rewritten; a file without markers gets the block appended. Each file must hold exactly one start marker followed by exactly one end marker, or neither: an orphan marker, an end before its start, or a second block makes sync exit 1 naming the file and the problem, and **no file is written**, the well-formed ones included. Missing files are reported as `missing` and **not created** — which harness a project uses is its own choice — and with none present sync exits 1 without writing. `--json` reports `updated`, `unchanged` and `missing` |
307
307
 
308
308
  Improvement telemetry is classified as `project` whenever a project or workflow identity is present, and as `cli` for unscoped operator behavior. Set `KXM_IMPROVE_TARGET=cli` or `KXM_IMPROVE_TARGET=project` only when an operator needs to override that generic classification; this changes report bucketing, not workflow state.
309
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.90",
3
+ "version": "0.7.92",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -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.90",
5
+ "version": "0.7.92",
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",
@@ -33618,26 +33618,27 @@ function formatHarnessMemoryBlock(records) {
33618
33618
  return contentLines.join("\n");
33619
33619
  }
33620
33620
  var HARNESS_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
33621
- function updateHarnessDocument(filePath, block, dryRun = false) {
33622
- const original = readFileSync24(filePath, "utf8");
33623
- let updated;
33624
- if (original.includes(MEMORY_MARKER_START) && original.includes(MEMORY_MARKER_END)) {
33625
- const startIdx = original.indexOf(MEMORY_MARKER_START);
33626
- const endIdx = original.indexOf(MEMORY_MARKER_END) + MEMORY_MARKER_END.length;
33627
- updated = original.slice(0, startIdx) + block + original.slice(endIdx);
33628
- } else {
33621
+ function memoryMarkerProblem(text) {
33622
+ const starts = text.split(MEMORY_MARKER_START).length - 1;
33623
+ const ends = text.split(MEMORY_MARKER_END).length - 1;
33624
+ if (starts > 1 || ends > 1) return `has ${starts} ${MEMORY_MARKER_START} and ${ends} ${MEMORY_MARKER_END} markers, not one block`;
33625
+ if (starts > ends) return `has ${MEMORY_MARKER_START} with no ${MEMORY_MARKER_END}`;
33626
+ if (ends > starts) return `has ${MEMORY_MARKER_END} with no ${MEMORY_MARKER_START}`;
33627
+ if (text.indexOf(MEMORY_MARKER_END) < text.indexOf(MEMORY_MARKER_START)) return `has ${MEMORY_MARKER_END} before ${MEMORY_MARKER_START}`;
33628
+ return void 0;
33629
+ }
33630
+ function withMemoryBlock(original, block) {
33631
+ const startIdx = original.indexOf(MEMORY_MARKER_START);
33632
+ if (startIdx === -1) {
33629
33633
  const head = original.trimEnd();
33630
- updated = head ? `${head}
33634
+ return head ? `${head}
33631
33635
 
33632
33636
  ${block}
33633
33637
  ` : `${block}
33634
33638
  `;
33635
33639
  }
33636
- if (updated !== original) {
33637
- if (!dryRun) writeFileSync18(filePath, updated, "utf8");
33638
- return true;
33639
- }
33640
- return false;
33640
+ const endIdx = original.indexOf(MEMORY_MARKER_END) + MEMORY_MARKER_END.length;
33641
+ return original.slice(0, startIdx) + block + original.slice(endIdx);
33641
33642
  }
33642
33643
  function syncHarnessMemory(repoRoot, options = {}) {
33643
33644
  const root = resolve22(repoRoot);
@@ -33648,11 +33649,27 @@ function syncHarnessMemory(repoRoot, options = {}) {
33648
33649
  `none of ${HARNESS_INSTRUCTION_FILES.join(", ")} exists in ${root}; sync updates the instruction files a project already has and does not create them`
33649
33650
  );
33650
33651
  }
33652
+ const documents = present.map((name) => {
33653
+ const original = readFileSync24(join28(root, name), "utf8");
33654
+ return { name, original, problem: memoryMarkerProblem(original) };
33655
+ });
33656
+ const malformed = documents.filter((doc) => doc.problem !== void 0);
33657
+ if (malformed.length > 0) {
33658
+ throw new Error(
33659
+ `${malformed.map((doc) => `${doc.name} ${doc.problem}`).join("; ")}; wrote no file. Keep exactly one ${MEMORY_MARKER_START} followed by one ${MEMORY_MARKER_END} in each file, or delete both so sync appends a fresh block`
33660
+ );
33661
+ }
33651
33662
  const block = formatHarnessMemoryBlock(loadAuthoredMemory(root));
33652
33663
  const updated = [];
33653
33664
  const unchanged = [];
33654
- for (const name of present) {
33655
- (updateHarnessDocument(join28(root, name), block, options.dryRun) ? updated : unchanged).push(name);
33665
+ for (const { name, original } of documents) {
33666
+ const next = withMemoryBlock(original, block);
33667
+ if (next === original) {
33668
+ unchanged.push(name);
33669
+ } else {
33670
+ if (!options.dryRun) writeFileSync18(join28(root, name), next, "utf8");
33671
+ updated.push(name);
33672
+ }
33656
33673
  }
33657
33674
  return { updated, unchanged, missing };
33658
33675
  }
@@ -17182,7 +17182,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17182
17182
  }
17183
17183
 
17184
17184
  // plugins/kxm/src/mcp-server.ts
17185
- var VERSION = "0.7.90";
17185
+ var VERSION = "0.7.92";
17186
17186
  var inbox = /* @__PURE__ */ new Map();
17187
17187
  var notifiedInbox = /* @__PURE__ */ new Set();
17188
17188
  var meshClient;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.90",
3
+ "version": "0.7.92",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -57,6 +57,12 @@ never creates an instruction file. If the project has none of the three, sync
57
57
  refuses; create the file your harness reads yourself rather than expecting KXM
58
58
  to author it.
59
59
 
60
+ Each file must hold exactly one start marker followed by exactly one end
61
+ marker, or neither. An orphan marker, an end before its start, or a second
62
+ block makes sync refuse, name the file and the problem, and write no file at
63
+ all. Fix the markers by hand — keep one pair, or delete both so sync appends a
64
+ fresh block — rather than letting sync guess which text is yours.
65
+
60
66
  Do not invent search, save, delete, list, or clear subcommands under memory.
61
67
  Use context retrieval for discovery and reviewed authored changes for
62
68
  corrections, respecting provenance and historical records.
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.90";
11
+ const VERSION = "0.7.92";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -323,26 +323,30 @@ export function formatHarnessMemoryBlock(records: MemoryRecord[]): string {
323
323
  * carry KXM's words instead of the project's. */
324
324
  const HARNESS_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"] as const;
325
325
 
326
- /** Replace the marker-delimited block, or append one when the file has none. Nothing
327
- * outside the markers is rewritten. Returns whether the block changed the file; `dryRun`
328
- * answers without writing. */
329
- export function updateHarnessDocument(filePath: string, block: string, dryRun = false): boolean {
330
- const original = readFileSync(filePath, "utf8");
331
- let updated: string;
332
- if (original.includes(MEMORY_MARKER_START) && original.includes(MEMORY_MARKER_END)) {
333
- const startIdx = original.indexOf(MEMORY_MARKER_START);
334
- const endIdx = original.indexOf(MEMORY_MARKER_END) + MEMORY_MARKER_END.length;
335
- updated = original.slice(0, startIdx) + block + original.slice(endIdx);
336
- } else {
337
- const head = original.trimEnd();
338
- updated = head ? `${head}\n\n${block}\n` : `${block}\n`;
339
- }
326
+ /** What is wrong with the memory markers in `text`, or undefined when it has exactly one
327
+ * start marker followed by exactly one end marker, or neither. Any other shape has no single
328
+ * block to replace: an orphan start would make the next sync delete the project's text up to
329
+ * a later end, an end before its start would duplicate text, and a second block goes stale. */
330
+ function memoryMarkerProblem(text: string): string | undefined {
331
+ const starts = text.split(MEMORY_MARKER_START).length - 1;
332
+ const ends = text.split(MEMORY_MARKER_END).length - 1;
333
+ if (starts > 1 || ends > 1) return `has ${starts} ${MEMORY_MARKER_START} and ${ends} ${MEMORY_MARKER_END} markers, not one block`;
334
+ if (starts > ends) return `has ${MEMORY_MARKER_START} with no ${MEMORY_MARKER_END}`;
335
+ if (ends > starts) return `has ${MEMORY_MARKER_END} with no ${MEMORY_MARKER_START}`;
336
+ if (text.indexOf(MEMORY_MARKER_END) < text.indexOf(MEMORY_MARKER_START)) return `has ${MEMORY_MARKER_END} before ${MEMORY_MARKER_START}`;
337
+ return undefined;
338
+ }
340
339
 
341
- if (updated !== original) {
342
- if (!dryRun) writeFileSync(filePath, updated, "utf8");
343
- return true;
340
+ /** `original` with its memory block replaced, or with one appended when it has no markers.
341
+ * Nothing outside the markers is rewritten. Markers must already pass `memoryMarkerProblem`. */
342
+ function withMemoryBlock(original: string, block: string): string {
343
+ const startIdx = original.indexOf(MEMORY_MARKER_START);
344
+ if (startIdx === -1) {
345
+ const head = original.trimEnd();
346
+ return head ? `${head}\n\n${block}\n` : `${block}\n`;
344
347
  }
345
- return false;
348
+ const endIdx = original.indexOf(MEMORY_MARKER_END) + MEMORY_MARKER_END.length;
349
+ return original.slice(0, startIdx) + block + original.slice(endIdx);
346
350
  }
347
351
 
348
352
  export function syncHarnessMemory(
@@ -358,11 +362,30 @@ export function syncHarnessMemory(
358
362
  );
359
363
  }
360
364
 
365
+ // Check every file before writing any: one malformed file leaves all of them untouched.
366
+ const documents = present.map((name) => {
367
+ const original = readFileSync(join(root, name), "utf8");
368
+ return { name, original, problem: memoryMarkerProblem(original) };
369
+ });
370
+ const malformed = documents.filter((doc) => doc.problem !== undefined);
371
+ if (malformed.length > 0) {
372
+ throw new Error(
373
+ `${malformed.map((doc) => `${doc.name} ${doc.problem}`).join("; ")}; wrote no file. `
374
+ + `Keep exactly one ${MEMORY_MARKER_START} followed by one ${MEMORY_MARKER_END} in each file, or delete both so sync appends a fresh block`,
375
+ );
376
+ }
377
+
361
378
  const block = formatHarnessMemoryBlock(loadAuthoredMemory(root));
362
379
  const updated: string[] = [];
363
380
  const unchanged: string[] = [];
364
- for (const name of present) {
365
- (updateHarnessDocument(join(root, name), block, options.dryRun) ? updated : unchanged).push(name);
381
+ for (const { name, original } of documents) {
382
+ const next = withMemoryBlock(original, block);
383
+ if (next === original) {
384
+ unchanged.push(name);
385
+ } else {
386
+ if (!options.dryRun) writeFileSync(join(root, name), next, "utf8");
387
+ updated.push(name);
388
+ }
366
389
  }
367
390
  return { updated, unchanged, missing };
368
391
  }