klypix-mcp 1.48.0 → 1.49.1

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.
package/README.md CHANGED
@@ -292,12 +292,43 @@ Apache-2.0 and work with no app installed. The app's interface is available in E
292
292
 
293
293
  ## Git and concurrency
294
294
 
295
- One file in your repo, committed with your code — versioned, branchable, portable.
295
+ One file in your repo, committed with your code — versioned, branchable, portable. So two
296
+ developers already share one brain the way they share code: clone, branch, pull.
296
297
 
297
- Be precise about what git does here: `brain.klypix` is a binary ZIP. Git shows
298
- `Bin 1308328 -> 1309005 bytes`, produces zero line diffs, and a merge conflict on it is an
299
- all-or-nothing take-ours or take-theirs. You cannot review a brain change in a PR diff. **All
300
- card-level merge safety comes from the KLYPIX engine, not from git.**
298
+ Be precise about what git does on its own: `brain.klypix` is a binary ZIP. Git shows
299
+ `Bin 1308328 -> 1309005 bytes` and produces zero line diffs, so out of the box a conflict on it is
300
+ an all-or-nothing take-ours or take-theirs, and a reviewer sees nothing. **Card-level merge safety
301
+ comes from the KLYPIX engine** — but since 1.48.0 you can hand that engine to git and read its
302
+ output in a PR:
303
+
304
+ ```bash
305
+ npx klypix-mcp git-driver install # once per clone, in any repo
306
+ ```
307
+
308
+ That registers a merge driver for `*.klypix` (a per-machine git config line plus a `.gitattributes`
309
+ rule you commit) and provisions the engine it needs. When two people change the brain and one
310
+ pulls, git calls the engine instead of stopping: new cards from both sides are kept, a card only
311
+ one side edited takes that edit, and a card edited differently on both sides keeps **both**
312
+ versions — the second as a linked twin, never a silent overwrite. Before returning, the merge
313
+ asserts it still contains every surviving card from both sides and refuses rather than hand back a
314
+ result that lost one.
315
+
316
+ The honest boundary: a machine that has not run `git-driver install` simply gets the old binary
317
+ conflict — safe degradation, not corruption — and git keeps both parents of every merge, so even a
318
+ merge you dislike is reconstructable. It is a merge *on pull*, not live sync.
319
+
320
+ For review, two commands turn a binary blob into something a human can read:
321
+
322
+ ```bash
323
+ npx klypix-mcp diff main # card-level: what was added / updated / removed
324
+ npx klypix-mcp pr-brief origin/main # the brain cards that reference this PR's changed files
325
+ ```
326
+
327
+ `diff` compares meaning rather than bytes (a re-save restamps timestamps; that is not a change).
328
+ `pr-brief` matches a card's `#file-…` evidence anchors against the changed paths, so a reviewer
329
+ sees the decisions already recorded about the code in front of them. `examples/github/brain-pr.yml`
330
+ wires both into a sticky pull-request comment using nothing but the checkout and the default
331
+ `GITHUB_TOKEN` — no KLYPIX service in the path.
301
332
 
302
333
  Concurrent sessions serialize behind a capture lock, and each write is a temp file plus an atomic
303
334
  rename, so a crash mid-write leaves the previous good file intact. The lock is advisory with a
@@ -307,6 +338,24 @@ was judged worse — but it is a real limit, not a guarantee.
307
338
 
308
339
  ---
309
340
 
341
+ ## The command line
342
+
343
+ The MCP verbs below are what agents call. These are what **you** call:
344
+
345
+ | Command | What it does |
346
+ |---|---|
347
+ | `npx klypix-mcp init` | Seed a starter `brain.klypix` here and print an MCP config |
348
+ | `npx klypix-mcp install` | Install the engine + Claude Code hooks on this machine (see Quick start) |
349
+ | `npx klypix-mcp link` | Wire this project for Cursor, Cline, Windsurf, Copilot, Gemini CLI, Aider (`--check` audits) |
350
+ | `npx klypix-mcp doctor` | One verdict: version, hosts, live sessions, tool count, drift. Exits non-zero — usable as a CI gate |
351
+ | `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
352
+ | `npx klypix-mcp git-driver` | Register the lossless `.klypix` merge driver for a repo (`status` to check) |
353
+ | `npx klypix-mcp diff [ref]` | Card-level brain diff against a git ref, as markdown |
354
+ | `npx klypix-mcp pr-brief [ref]` | Brain cards referencing the files changed since a ref, as markdown |
355
+ | `npx klypix-mcp garden-code` | Print the human approval code `brain_garden` requires |
356
+
357
+ ---
358
+
310
359
  ## The 18 verbs
311
360
 
312
361
  | Tool | What it does |
@@ -477,9 +526,9 @@ Read this section before you build on any of it.
477
526
  Every number here is measured on our own project brain. Nothing below is published, benchmarked or
478
527
  independently validated.
479
528
 
480
- - **Dogfood scale.** KLYPIX itself is built with its own brain: **1,523 cards and 1,404
529
+ - **Dogfood scale.** KLYPIX itself is built with its own brain: **1,645 cards and 1,521
481
530
  connections**, written by multiple concurrent agent sessions, receipts in the file. Current as of
482
- 2026-07-30.
531
+ 2026-08-01.
483
532
  - **Recall.** 73% of past decisions recovered with one search round, 55% brief-only, 0% cold.
484
533
  Caveat that travels with it: n=20, our own brain, self-authored questions, LLM-judged.
485
534
  - **Ranker.** recall@5 of the true source card went **15% → 40%** across two upgrades (n=20 frozen
@@ -487,7 +536,7 @@ independently validated.
487
536
  experiment that *regressed* — contextual prefixes on short cards — is recorded next to the wins.
488
537
  - **What we do not publish.** No download count: this package's own 24-hour auto-updater generates
489
538
  most of it, so it is not a user count. No adoption, team or customer figures. No brief-token
490
- figure — the last one was measured at ~600 cards and is stale at 1,523.
539
+ figure — the last one was measured at ~600 cards and is stale at 1,645.
491
540
  - **The eval harness is not in this repo.** It lives in the private KLYPIX desktop repository. The
492
541
  numbers above are ours to defend, not yours to reproduce from here — treat them accordingly.
493
542
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.48.0",
3
+ "version": "1.49.1",
4
4
  "description": "Shared project brain and MCP coordination server for multi-agent coding.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -31,7 +31,7 @@
31
31
  // resolves to the edit — no-loss wins over delete).
32
32
 
33
33
  import fs from 'node:fs';
34
- import { mergeBrains } from './merge-brains.mjs';
34
+ import { mergeBrains, sameMeaning } from './merge-brains.mjs';
35
35
  import { parseKlypix, shard } from './klypix-format.mjs';
36
36
 
37
37
  // id -> verbatim item JSON string for one side (null for an empty/absent side).
@@ -68,8 +68,11 @@ try {
68
68
  const inA = ai.has(id), inB = ti.has(id);
69
69
  if (inA && inB) continue; // alive on both
70
70
  if (!inA && !inB) { deletedIds.push(id); continue; } // deleted on both
71
+ // "Untouched" by MEANING, not bytes — a side that merely re-saved the
72
+ // file restamps volatile fields (updatedAt), and a byte compare would
73
+ // read that as an edit and silently refuse to propagate a real delete.
71
74
  const survivorJson = inA ? ai.get(id) : ti.get(id);
72
- if (survivorJson === baseJson) deletedIds.push(id); // delete vs untouched → honor
75
+ if (sameMeaning(survivorJson, baseJson)) deletedIds.push(id); // delete vs untouched → honor
73
76
  // delete vs EDIT → no tombstone; union keeps the edited card
74
77
  }
75
78
  }
@@ -40,9 +40,56 @@ const isValidZKey = (k) => { try { generateKeyBetween(k, null); return true; } c
40
40
  const rand = () => Math.random().toString(36).slice(2, 10);
41
41
  const ARCHIVE = /^archive$/i;
42
42
 
43
- // Load one .klypix buffer into a flat, comparison-friendly shape. Unchanged
44
- // cards across ancestor/descendant buffers keep byte-identical item JSON, so a
45
- // simple string compare detects real content edits.
43
+ // ── Semantic item comparison (2026-08-01 field fix) ─────────────────────────
44
+ // A raw byte compare of item JSON was the change detector, on the assumption
45
+ // that "unchanged cards keep byte-identical JSON". That assumption DIED the
46
+ // day cards gained touch metadata: `updatedAt` is restamped whenever a card is
47
+ // written, so two sides holding the SAME card with the SAME text differ in
48
+ // bytes — and every first real sync spawned __agconf conflict twins for cards
49
+ // nobody edited (field-proven on the founder's pump-doctor brain: 5 twins,
50
+ // differing field list = ["updatedAt"] exactly).
51
+ //
52
+ // The fix is the same discipline the sync core and the brain diff already use:
53
+ // compare PARSED MEANING with volatile/derived fields stripped, key-sorted so
54
+ // two writers' key orders can't fake a difference. Byte-compare survives as the
55
+ // fallback for anything unparseable — a malformed item must never crash a merge.
56
+ //
57
+ // VOLATILE = written by the act of saving, not by a human/agent decision:
58
+ // updatedAt — touch timestamp zIndex — display order derived from zKey
59
+ // Everything else (content, colors, geometry, evidence, author…) stays load-
60
+ // bearing: a real edit to any of them is still a real conflict.
61
+ const VOLATILE_ITEM_FIELDS = ['updatedAt', 'zIndex'];
62
+
63
+ const sortedStable = (v) => JSON.stringify(v, (_k, val) =>
64
+ (val && typeof val === 'object' && !Array.isArray(val))
65
+ ? Object.fromEntries(Object.keys(val).sort().map(k => [k, val[k]]))
66
+ : val);
67
+
68
+ function itemSignature(json) {
69
+ if (json == null) return null;
70
+ try {
71
+ const obj = JSON.parse(json);
72
+ for (const f of VOLATILE_ITEM_FIELDS) delete obj[f];
73
+ return sortedStable(obj);
74
+ } catch {
75
+ return json; // unparseable → byte identity, as before
76
+ }
77
+ }
78
+
79
+ /** True when two item JSON strings mean the same thing (volatile fields aside).
80
+ * EXPORTED as the single definition of "did this card actually change" — the
81
+ * git merge driver and the Brain Sync core both decide committed-absence
82
+ * tombstones with it, so all three transports agree on what an edit is. */
83
+ export const sameMeaning = (a, b) => {
84
+ if (a === b) return true; // fast path: byte-identical
85
+ if (a == null || b == null) return false;
86
+ return itemSignature(a) === itemSignature(b);
87
+ };
88
+
89
+ // Load one .klypix buffer into a flat, comparison-friendly shape. Item JSON is
90
+ // kept VERBATIM (the merge must write back exactly what a side held); whether
91
+ // two versions actually differ is decided by sameMeaning(), never by these
92
+ // bytes — see its note on volatile fields.
46
93
  async function loadSide(buf) {
47
94
  if (!buf) return null;
48
95
  const { zip, canvas, manifest, struct } = await parseKlypix(buf);
@@ -128,9 +175,13 @@ export async function mergeBrains({ base = null, ours, theirs, deletedIds = [] }
128
175
  // ── Choose CONTENT ──────────────────────────────────────────────────────
129
176
  let json, side;
130
177
  if (inO && inT) {
131
- const oChg = !inB || O.items[id] !== baseItem(id);
132
- const tChg = !inB || T.items[id] !== baseItem(id);
133
- if (inB && oChg && tChg && O.items[id] !== T.items[id]) {
178
+ // Change + divergence are judged by MEANING, not bytes (see sameMeaning):
179
+ // a restamped `updatedAt` is not an edit, and two copies of one card that
180
+ // differ only in volatile fields are not in conflict.
181
+ const oChg = !inB || !sameMeaning(O.items[id], baseItem(id));
182
+ const tChg = !inB || !sameMeaning(T.items[id], baseItem(id));
183
+ const diverged = !sameMeaning(O.items[id], T.items[id]);
184
+ if (inB && oChg && tChg && diverged) {
134
185
  // GENUINE content conflict: a card that EXISTED at open, edited differently
135
186
  // on both sides → human stays live, agent version preserved as a twin.
136
187
  json = O.items[id]; side = 'ours';
@@ -138,7 +189,7 @@ export async function mergeBrains({ base = null, ours, theirs, deletedIds = [] }
138
189
  extras.push({ id: twinId, json: T.items[id], srcPos: T.positions[id] || O.positions[id], of: id });
139
190
  conflicts.push({ id, kind: 'content', keptLive: 'ours', twin: twinId });
140
191
  } else if (tChg && !oChg) { json = T.items[id]; side = 'theirs'; delta.updated.push(id); }
141
- else if (!inB && O.items[id] !== T.items[id]) {
192
+ else if (!inB && diverged) {
142
193
  // Same NEW card (same id) present on BOTH sides but never in base — e.g. an
143
194
  // agent card the app also holds via live-apply, re-serialized slightly
144
195
  // differently. It's the SAME card, NOT a conflict → take the disk/agent