klypix-mcp 1.21.2 → 1.22.0

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
@@ -75,6 +75,23 @@ or the Claude Code project-brain hook. (`search_all_brains` is also hook-fed —
75
75
  reads the cross-project registry the hook writes, so it stays empty until a hook
76
76
  has registered at least one brain.)
77
77
 
78
+ ### Updates — the propagation contract
79
+
80
+ `npx klypix-mcp install` lays the whole brain (hooks + engine + local MCP server)
81
+ into `~/.claude/project-brain`, and the emitted MCP config runs the server **from
82
+ that installed bundle** (no npx cache to go stale). From then on updates are
83
+ automatic: at session start the hook checks npm (≤ once/24h, fail-open, disable
84
+ with `KLYPIX_AUTO_UPDATE=0`) and self-installs a newer release, so a publish
85
+ reaches every machine by its **next session**.
86
+
87
+ One honest caveat: a running stdio MCP server can't hot-swap, and **resuming a
88
+ session (or opening a new chat in the same app) does not respawn it** — only a
89
+ full app quit + reopen (or `/mcp` reconnect after the old process exits) starts
90
+ the new binary. `brain_doctor` tells you when that's needed: its RUNNING line
91
+ compares the *live server's* self-reported version against the installed bundle
92
+ and npm, and reads `DRIFTED → /mcp reconnect` instead of pretending a stale
93
+ server is current.
94
+
78
95
  ## Also speaks A2A (Agent-to-Agent)
79
96
 
80
97
  The same engine is exposed as an **A2A agent** so other agents and orchestrators
@@ -224,7 +224,8 @@ try {
224
224
  else console.error(`⚠ readiness: ${notWired.length} hook(s) did NOT take (${notWired.join(', ')}) — the brain will read but not capture/sync. Re-run \`npx klypix-mcp install --force\` or check ${SETTINGS}.`);
225
225
  console.log(`✓ MCP server runs from the local bundle (node ${fwd(path.join(BRAIN_DIR, 'klypix-mcp-server.mjs'))}) — no npx cache, works offline, always the installed version.`);
226
226
  if (migrated) console.log(`✓ migrated ${migrated.file} klypix-canvas server: ${migrated.from} → ${migrated.to} (backup: .mcp.json.klypix-bak). Reconnect (/mcp) or restart to pick it up.`);
227
- console.log(' Every project with a ./brain.klypix now auto-reads its brief + captures decisions. Restart open Claude Code sessions to load the hooks.');
227
+ console.log(' Every project with a ./brain.klypix now auto-reads its brief + captures decisions.');
228
+ console.log(' ⚠ Open sessions keep their OLD server until relaunched: fully quit & reopen the app (a session resume / new chat does NOT respawn the MCP server). `brain_doctor`\'s RUNNING line confirms when you\'re current.');
228
229
  console.log(' Verify anytime: `npx klypix-mcp doctor` (is the brain current + wired + in sync, who else is live).');
229
230
  } catch (e) {
230
231
  if (gotLock) releaseLock();
@@ -154,7 +154,7 @@ server.registerTool('brain_connect', {
154
154
 
155
155
  server.registerTool('brain_reconcile', {
156
156
  title: 'Reconcile the brain — contradictions between cards + unrecorded migrations',
157
- description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces. (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. Reads ONLY the filesystem — never the database, never the network — and changes nothing. Run it periodically, or when recall surfaces something you believe is stale.',
157
+ description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth, UNLESS the cue predates its counterpart: then the pair is marked "presumed superseded" and the newer card is presumed current — verify before retiring) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces (and its cue stops overlaying recall/ask for that pair). (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. Reads ONLY the filesystem — never the database, never the network — and changes nothing. Run it periodically, or when recall surfaces something you believe is stale.',
158
158
  inputSchema: {
159
159
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
160
160
  root: z.string().optional().describe("Project root holding the migrations dir (default: the brain file's folder)."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "description": "An open, local-first, agent-neutral canvas file your AI reads and writes over MCP — works with Claude, Cursor, Cline, any model.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "node": ">=18"
54
54
  },
55
55
  "scripts": {
56
- "test": "node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs"
56
+ "test": "node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs"
57
57
  },
58
58
  "dependencies": {
59
59
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -452,10 +452,13 @@ export async function opBrainReconcile({ vault, canvas, root, mode = 'all' }) {
452
452
 
453
453
  // (1) CONTRADICTIONS — the brain reconciled against ITSELF. Same-subject live
454
454
  // pairs where one side carries an explicit correction cue (that side is the
455
- // presumed truth) or the two use opposite polarity words (deferred↔wired,
456
- // broken↔fixed …). Candidates only — nothing is changed here; the agent/human
457
- // confirms each. This is the retroactive cleaner for stale/correction pairs
458
- // that slipped past capture (cross-area + reworded → no supersede possible).
455
+ // presumed truth — UNLESS the cue predates its counterpart, then the newer
456
+ // card is presumed to have superseded the correction and the pair is marked
457
+ // "presumed superseded") or the two use opposite polarity words
458
+ // (deferred↔wired, broken↔fixed …). Candidates only — nothing is changed
459
+ // here; the agent/human confirms each. This is the retroactive cleaner for
460
+ // stale/correction pairs that slipped past capture (cross-area + reworded →
461
+ // no supersede possible).
459
462
  if (mode === 'all' || mode === 'contradictions') {
460
463
  const pairs = detectContradictions(struct);
461
464
  if (pairs.length) {
@@ -464,7 +467,7 @@ export async function opBrainReconcile({ vault, canvas, root, mode = 'all' }) {
464
467
  `${i + 1}. ${p.why} · overlap ${p.overlap}\n`
465
468
  + ` · likely STALE [${p.stale.area || '?'}] (id ${p.stale.id}) ${flat(p.stale.text).slice(0, 180)}\n`
466
469
  + ` · likely CURRENT [${p.fresh.area || '?'}] (id ${p.fresh.id}) ${flat(p.fresh.text).slice(0, 180)}`);
467
- sections.push(`# ⚔️ ${pairs.length} contradiction candidate(s) — confirm, then reconcile\n_Candidates only — nothing was changed. For each REAL contradiction: retire the stale card with \`brain_note\` marker \`✓\` (text = what it resolved to), or record a correction-cue decision ("CORRECTION: …", uppercase) — capture auto-supersedes it across areas. Dismissing a FALSE positive (either kind — polarity OR correction-cue): \`brain_connect\` with \`pairs:[{fromId, toId}]\` and \`relationship:"not_contradiction"\` using the ids above — the dismissal is persisted, so that pair never resurfaces here again._\n\n${lines.join('\n')}`);
470
+ sections.push(`# ⚔️ ${pairs.length} contradiction candidate(s) — confirm, then reconcile\n_Candidates only — nothing was changed. For each REAL contradiction: retire the stale card with \`brain_note\` marker \`✓\` (text = what it resolved to), or record a correction-cue decision ("CORRECTION: …", uppercase) — capture auto-supersedes it across areas. A pair marked "presumed superseded" is INVERTED — its correction card PREDATES its counterpart (e.g. the old fact was re-captured after the correction): verify which side is real before retiring anything; if the correction still holds, re-assert it (a \`~\` update or a fresh CORRECTION card) instead of retiring it. Dismissing a FALSE positive (either kind — polarity OR correction-cue): \`brain_connect\` with \`pairs:[{fromId, toId}]\` and \`relationship:"not_contradiction"\` using the ids above — the dismissal is persisted, so that pair never resurfaces here (and its cue never re-attaches as a recall/ask overlay)._\n\n${lines.join('\n')}`);
468
471
  } else if (mode === 'contradictions') {
469
472
  sections.push('✓ No contradiction candidates — no live card pair shows a correction cue or a polarity flip over the same subject.');
470
473
  }
@@ -1378,8 +1378,22 @@ export function detectContradictions(struct, { minOverlap = 0.45, topK = 12 } =
1378
1378
  if (!subjectHit) continue;
1379
1379
  let why = null, staleC = null, freshC = null;
1380
1380
  if (aCue !== bCue) {
1381
- why = 'correction-cue'; // one side explicitly corrects — it is the presumed truth
1382
- freshC = aCue ? a : b; staleC = aCue ? b : a;
1381
+ const cueC = aCue ? a : b, otherC = aCue ? b : a;
1382
+ // Recency: the cue side is the presumed truth ONLY for cards that
1383
+ // existed when it was written. Against a STRICTLY NEWER card the
1384
+ // presumption inverts — the newer card superseded the correction
1385
+ // (field 2026-07-12: a 07-11 audit correction was flagged CURRENT
1386
+ // over the 07-12 R1 cards that post-dated it).
1387
+ const inverted = (cueC.createdAt || 0) && (otherC.createdAt || 0) && cueC.createdAt < otherC.createdAt;
1388
+ why = inverted ? 'correction-cue (cue predates its counterpart — presumed superseded)' : 'correction-cue';
1389
+ freshC = inverted ? otherC : cueC; staleC = inverted ? cueC : otherC;
1390
+ // Skills are standing reference (corrected in place with ~, never
1391
+ // retirable by ✓/supersede) — presenting one as "likely STALE"
1392
+ // invites a retire the engine would refuse; skip the pair. Checked
1393
+ // on the RESOLVED stale side, so it covers both directions — incl.
1394
+ // an inverted pair whose cue card is itself a 🛠 skill (a skill
1395
+ // documenting the CORRECTION convention carries the cue token).
1396
+ if (/🛠/.test(staleC.text || '')) continue;
1383
1397
  } else if (!aCue && !linked.has(a.id + '|' + b.id)) {
1384
1398
  const la = lower.get(a.id), lb = lower.get(b.id);
1385
1399
  for (const { x, y, rx, ry } of POLARITY_RES) {
@@ -1598,7 +1612,13 @@ const cueMatch = (a, b, bar) => {
1598
1612
  // • edge — an outgoing "superseded by"/"closed by" arrow (drawn by capture or
1599
1613
  // a confirmed reconcile) whose successor still has text;
1600
1614
  // • cue — a LIVE correction-cue card that lexically overlaps it ≥ `at`, ANY
1601
- // area (the un-edged pair the capture-time supersede missed).
1615
+ // area (the un-edged pair the capture-time supersede missed) — EXCEPT:
1616
+ // - a cue STRICTLY OLDER than the card (recency guard — an old correction
1617
+ // must never be served as the current truth for a card that post-dated
1618
+ // it; field 2026-07-12: 17 of 32 live overlays pointed backward),
1619
+ // - a 🛠 skill card as the overlay TARGET (standing reference, corrected
1620
+ // in place with ~ — never labeled stale by a lexical match),
1621
+ // - a pair dismissed with a not_contradiction edge (same human verdict).
1602
1622
  // The caller injects the corrector FIRST (labeled) and reduces the stale hit to
1603
1623
  // a headline — the stale text never stands alone. Pure + cheap: correction-cue
1604
1624
  // cards are rare and the hit list is ≤topK.
@@ -1608,8 +1628,14 @@ export function correctionOverlaysFor(struct, cards, { at = CORRECTION_SUPERSEDE
1608
1628
  const byId = new Map(struct.cards.map(c => [c.id, c]));
1609
1629
  const isArchived = (c) => /^archive$/i.test(c.area || '');
1610
1630
  const successorOf = new Map();
1631
+ // A confirmed not_contradiction dismissal is the same human verdict for the
1632
+ // overlay: that cue does not correct that card — the CUE path never
1633
+ // re-attaches the pair (an explicit superseded-by edge still wins: both are
1634
+ // deliberate verdicts and the edge is the stronger one).
1635
+ const dismissed = new Set();
1611
1636
  for (const cn of struct.connections || []) {
1612
1637
  if (cn.label === 'superseded by' || cn.label === 'closed by') successorOf.set(cn.fromId, cn.toId);
1638
+ if (cn.relationship === 'not_contradiction' || cn.label === 'not a contradiction') { dismissed.add(cn.fromId + '|' + cn.toId); dismissed.add(cn.toId + '|' + cn.fromId); }
1613
1639
  }
1614
1640
  const cues = struct.cards.filter(c => c.type !== 'container' && !isArchived(c) && (c.text || '').trim() && hasCorrectionCue(c.text));
1615
1641
  for (const card of cards) {
@@ -1617,10 +1643,20 @@ export function correctionOverlaysFor(struct, cards, { at = CORRECTION_SUPERSEDE
1617
1643
  const succ = successorOf.has(card.id) ? byId.get(successorOf.get(card.id)) : null;
1618
1644
  if (succ && (succ.text || '').trim()) { out.set(card.id, { kind: 'edge', by: succ }); continue; }
1619
1645
  if (hasCorrectionCue(card.text)) continue; // the hit IS a correction — nothing to overlay
1646
+ if (/🛠/.test(card.text || '')) continue; // skills are standing reference, corrected in place with ~ — never labeled STALE by a lexical cue (mirror the supersede/resolve guards)
1620
1647
  const cTok = tokenSet(card.text);
1621
1648
  let best = null, bestS = 0;
1622
1649
  for (const cue of cues) {
1623
1650
  if (cue.id === card.id) continue;
1651
+ if (dismissed.has(cue.id + '|' + card.id)) continue;
1652
+ // Recency guard: a correction can only correct facts that existed when
1653
+ // it was written — a cue STRICTLY older than the card must never be
1654
+ // served as its "current truth" (field 2026-07-12: a 07-11 audit
1655
+ // correction overlaid the 07-12 cards that superseded it, and a June
1656
+ // "deploy did not stick" correction poisoned July version answers,
1657
+ // steering synthesis BACKWARD). Equal/unknown stamps keep the overlay
1658
+ // (same-batch captures share a timestamp; missing dates can't be judged).
1659
+ if ((cue.createdAt || 0) && (card.createdAt || 0) && cue.createdAt < card.createdAt) continue;
1624
1660
  const s = cueMatch(cTok, stripCueMeta(tokenSet(cue.text)), at);
1625
1661
  if (s > bestS) { bestS = s; best = cue; }
1626
1662
  }