klypix-mcp 1.31.0 → 1.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
4
4
  "description": "Every project gets a brain — one open .klypix file your AI agents read, write, and argue from, over MCP. Works with Claude, Cursor, Cline, any model.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -2645,6 +2645,26 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
2645
2645
  };
2646
2646
  canvas.connections = Array.isArray(canvas.connections) ? canvas.connections : [];
2647
2647
 
2648
+ // Fallback-add near-dup guard (live incident 2026-07-23): ✓/~ markers
2649
+ // BYPASS the hook's seen-dedup by design (self-heal re-stamps a drifted
2650
+ // fact), so a marker that stops matching — e.g. a ✓ whose target was
2651
+ // archived by its own FIRST run — re-fires on every Stop while it sits
2652
+ // in the transcript tail. Its no-match FALLBACK path must therefore
2653
+ // never re-add: 4 duplicate 🏁 fallback cards landed from one ✓ before
2654
+ // this guard. Checks live cards AND this batch's queued adds.
2655
+ const nearDupExists = (text) => {
2656
+ const t = tokenSet(text);
2657
+ if (t.size < 3) return false;
2658
+ for (const c of liveTextCards()) if (overlapScore(t, tokenSet(c.text)) >= 0.9) return true;
2659
+ for (const c of cards) if (c && c.text && overlapScore(t, tokenSet(c.text)) >= 0.9) return true;
2660
+ return false;
2661
+ };
2662
+ // Engine-authored text inserted into LIVE cards (fallback 🏁 cards, ✔
2663
+ // partial stamps) must never carry lifecycle glyphs from the marker's
2664
+ // prose — a ✓ whose text MENTIONED "❓/🎯" made its fallback card
2665
+ // classify as an open question in every brief (glyph-in-prose trap).
2666
+ const stripLifecycleGlyphs = (s) => String(s || '').replace(/[❓🎯🏁🛠✅✔↩⤵️]/gu, '').replace(/\s+/g, ' ').trim();
2667
+
2648
2668
  // RESOLVE (✓ markers) — best live match in the area, PLUS its near-tie
2649
2669
  // twins: a rephrased duplicate ❓ scores within a hair of its sibling, and
2650
2670
  // resolving only the first left the twin open forever (it kept surfacing
@@ -2685,12 +2705,13 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
2685
2705
  const uncoveredItems = items.filter(it => coverageOf(it.tokens, rTok) < 0.5);
2686
2706
  const partial = (coveredItems.length && uncoveredItems.length) || (/🏁/.test(best.text) && items.length > 0);
2687
2707
  if (partial) {
2708
+ const cleanR = stripLifecycleGlyphs(r.text);
2688
2709
  const still = uncoveredItems.length ? ` — still open: ${uncoveredItems.map(x => x.text.slice(0, 50)).join(' + ').slice(0, 160)}` : '';
2689
2710
  await rewriteCard(best.id, j => {
2690
- j.content = `${j.content}\n✔ partial ${today}: ${r.text.slice(0, 100)}${still}`;
2711
+ j.content = `${j.content}\n✔ partial ${today}: ${cleanR.slice(0, 100)}${still}`;
2691
2712
  j.borderColor = 'rgba(16,185,129,0.45)';
2692
2713
  });
2693
- best.text += `\n✔ partial ${today}: ${r.text}`;
2714
+ best.text += `\n✔ partial ${today}: ${cleanR}`;
2694
2715
  stats.partialResolved = (stats.partialResolved || 0) + 1;
2695
2716
  continue;
2696
2717
  }
@@ -2706,7 +2727,11 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
2706
2727
  // __fromResolve: an unmatched-✓ fallback card must not seed the
2707
2728
  // fulfillment cross-check (it would cover its own source item at
2708
2729
  // cov 1.0 and self-feed a suggestion loop — review-traced).
2709
- milestonesFallback.push({ text: (r.area ? `${r.area}: ` : '') + `🏁 ${r.text}`, area: r.area, borderColor: 'rgba(59,130,246,0.8)', __fromResolve: true });
2730
+ // Glyph-stripped + near-dup-guarded (see helpers above).
2731
+ const cleanR = stripLifecycleGlyphs(r.text);
2732
+ if (cleanR && !nearDupExists(cleanR)) {
2733
+ milestonesFallback.push({ text: (r.area ? `${r.area}: ` : '') + `🏁 ${cleanR}`, area: r.area, borderColor: 'rgba(59,130,246,0.8)', __fromResolve: true });
2734
+ }
2710
2735
  }
2711
2736
  }
2712
2737
 
@@ -2757,7 +2782,9 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
2757
2782
  });
2758
2783
  best.text = isTerseConfirm ? `${best.text}\n(re-affirmed ${today}: ${u.text})` : u.text;
2759
2784
  stats.updated++;
2760
- } else {
2785
+ } else if (!nearDupExists(u.text)) {
2786
+ // ~ fallback add is guarded like ✓'s: an unmatched ~ re-harvested
2787
+ // from the transcript tail must not stack a copy every turn.
2761
2788
  cards.push({ text: (u.area ? `${u.area}: ` : '') + u.text + (u.area ? `\n#${u.area.toLowerCase().replace(/[^a-z0-9]+/g, '-')}` : ''), area: u.area, createdVia: u.createdVia, ...(Array.isArray(u.evidence) && u.evidence.length ? { evidence: u.evidence } : {}) });
2762
2789
  }
2763
2790
  }