@productbrain/mcp 0.0.1-beta.70 → 0.0.1-beta.71

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.
@@ -2363,6 +2363,7 @@ function toSnapshot(report, maxTopGaps = 10) {
2363
2363
  import { readFileSync } from "fs";
2364
2364
  import { resolve } from "path";
2365
2365
  function resolveRegistries(projectRoot, manifest) {
2366
+ if (!projectRoot) return {};
2366
2367
  const resolved = {};
2367
2368
  for (const ref of manifest) {
2368
2369
  try {
@@ -2374,7 +2375,10 @@ function resolveRegistries(projectRoot, manifest) {
2374
2375
  if (keys.size > 0) {
2375
2376
  resolved[ref.id] = keys;
2376
2377
  }
2377
- } catch {
2378
+ } catch (err) {
2379
+ if (process.env.DEBUG_COHERENCE) {
2380
+ console.debug(`[coherence] Skipping ${ref.id} (${ref.path}):`, err instanceof Error ? err.message : err);
2381
+ }
2378
2382
  }
2379
2383
  }
2380
2384
  return resolved;
@@ -2409,14 +2413,19 @@ function extractBalanced(content, openPos, openChar, closeChar) {
2409
2413
  i = skipString(content, i, ch);
2410
2414
  continue;
2411
2415
  }
2412
- if (ch === "/" && content[i + 1] === "/") {
2416
+ if (ch === "/" && i + 1 < content.length && content[i + 1] === "/") {
2413
2417
  while (i < content.length && content[i] !== "\n") i++;
2414
2418
  continue;
2415
2419
  }
2416
- if (ch === "/" && content[i + 1] === "*") {
2420
+ if (ch === "/" && i + 1 < content.length && content[i + 1] === "*") {
2417
2421
  i += 2;
2418
- while (i < content.length && !(content[i - 1] === "*" && content[i] === "/")) i++;
2419
- i++;
2422
+ while (i < content.length) {
2423
+ if (content[i] === "/" && i > 0 && content[i - 1] === "*") {
2424
+ i++;
2425
+ break;
2426
+ }
2427
+ i++;
2428
+ }
2420
2429
  continue;
2421
2430
  }
2422
2431
  if (ch === openChar) depth++;
@@ -2429,11 +2438,12 @@ function extractBalanced(content, openPos, openChar, closeChar) {
2429
2438
  function skipString(content, start, quote) {
2430
2439
  let i = start + 1;
2431
2440
  while (i < content.length) {
2432
- if (content[i] === "\\" && quote !== "`") {
2441
+ const ch = content[i];
2442
+ if (ch === "\\") {
2433
2443
  i += 2;
2434
2444
  continue;
2435
2445
  }
2436
- if (content[i] === quote) return i + 1;
2446
+ if (ch === quote) return i + 1;
2437
2447
  i++;
2438
2448
  }
2439
2449
  return i;
@@ -2454,14 +2464,19 @@ function extractObjectKeys(block) {
2454
2464
  let i = 0;
2455
2465
  while (i < block.length) {
2456
2466
  const ch = block[i];
2457
- if (ch === "/" && block[i + 1] === "/") {
2467
+ if (ch === "/" && i + 1 < block.length && block[i + 1] === "/") {
2458
2468
  while (i < block.length && block[i] !== "\n") i++;
2459
2469
  continue;
2460
2470
  }
2461
- if (ch === "/" && block[i + 1] === "*") {
2471
+ if (ch === "/" && i + 1 < block.length && block[i + 1] === "*") {
2462
2472
  i += 2;
2463
- while (i < block.length && !(block[i - 1] === "*" && block[i] === "/")) i++;
2464
- i++;
2473
+ while (i < block.length) {
2474
+ if (block[i] === "/" && i > 0 && block[i - 1] === "*") {
2475
+ i++;
2476
+ break;
2477
+ }
2478
+ i++;
2479
+ }
2465
2480
  continue;
2466
2481
  }
2467
2482
  if (depth === 0) {
@@ -2515,6 +2530,7 @@ function extractSetValues(block) {
2515
2530
  // src/lib/coherence/git-detection.ts
2516
2531
  import { execSync } from "child_process";
2517
2532
  function detectTouchedRegistries(projectRoot) {
2533
+ if (!projectRoot) return /* @__PURE__ */ new Set();
2518
2534
  const manifestPaths = new Set(REGISTRY_MANIFEST.map((r) => r.path));
2519
2535
  const touched = /* @__PURE__ */ new Set();
2520
2536
  try {
@@ -2529,7 +2545,10 @@ function detectTouchedRegistries(projectRoot) {
2529
2545
  touched.add(trimmed);
2530
2546
  }
2531
2547
  }
2532
- } catch {
2548
+ } catch (err) {
2549
+ if (process.env.DEBUG_COHERENCE) {
2550
+ console.debug("[coherence] Git detection failed:", err instanceof Error ? err.message : err);
2551
+ }
2533
2552
  }
2534
2553
  return touched;
2535
2554
  }
@@ -2599,13 +2618,16 @@ function computeDelta(before, after) {
2599
2618
  function renderCoherenceDelta(delta) {
2600
2619
  const lines = ["### Coherence Delta"];
2601
2620
  const p = (n) => n === 1 ? "" : "s";
2621
+ const absNet = Math.abs(delta.netChange);
2622
+ const approx = absNet > delta.gapsFixed + delta.gapsIntroduced;
2623
+ const prefix = approx ? "at least " : "";
2602
2624
  if (delta.verdict === "improved") {
2603
2625
  lines.push(
2604
- `Registry coherence **improved**: ${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
2626
+ `Registry coherence **improved**: ${prefix}${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
2605
2627
  );
2606
2628
  } else if (delta.verdict === "degraded") {
2607
2629
  lines.push(
2608
- `**Registry coherence degraded**: ${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
2630
+ `**Registry coherence degraded**: ${prefix}${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
2609
2631
  );
2610
2632
  } else {
2611
2633
  lines.push(
@@ -2615,6 +2637,58 @@ function renderCoherenceDelta(delta) {
2615
2637
  lines.push("");
2616
2638
  return lines;
2617
2639
  }
2640
+ function findPersistentGaps(delta, currentViolations) {
2641
+ const beforeSet = new Set(delta.before.topGaps.map((g) => `${g.registry}::${g.slug}`));
2642
+ const afterSet = new Set(delta.after.topGaps.map((g) => `${g.registry}::${g.slug}`));
2643
+ const persistentKeys = /* @__PURE__ */ new Set();
2644
+ for (const key of beforeSet) {
2645
+ if (afterSet.has(key)) persistentKeys.add(key);
2646
+ }
2647
+ if (persistentKeys.size === 0) return [];
2648
+ const violationMap = /* @__PURE__ */ new Map();
2649
+ for (const v of currentViolations) {
2650
+ violationMap.set(`${v.registryId}::${v.collectionSlug}`, v);
2651
+ }
2652
+ return [...persistentKeys].map((key) => {
2653
+ const [registry = "", slug = ""] = key.split("::", 2);
2654
+ return {
2655
+ registry,
2656
+ slug,
2657
+ severity: delta.after.topGaps.find((g) => g.registry === registry && g.slug === slug)?.severity ?? "warning",
2658
+ violation: violationMap.get(key) ?? null
2659
+ };
2660
+ });
2661
+ }
2662
+ function renderPersistentGapOffers(gaps) {
2663
+ if (gaps.length === 0) return [];
2664
+ const lines = [
2665
+ "### Persistent Coherence Gaps",
2666
+ "",
2667
+ "These gaps existed before and after this session \u2014 they represent structural registry drift.",
2668
+ "Want me to capture them as tensions on the Chain?",
2669
+ ""
2670
+ ];
2671
+ const SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
2672
+ const errorsFirst = [...gaps].sort(
2673
+ (a, b) => SEVERITY_RANK2[a.severity] - SEVERITY_RANK2[b.severity]
2674
+ );
2675
+ for (const gap of errorsFirst.slice(0, 5)) {
2676
+ const v = gap.violation;
2677
+ const fixHint = v?.fix ? ` \u2014 ${v.fix}` : "";
2678
+ lines.push(
2679
+ `- **\`${gap.slug}\`** missing from \`${gap.registry}\` (${gap.severity})${fixHint}`
2680
+ );
2681
+ }
2682
+ if (errorsFirst.length > 5) {
2683
+ lines.push(`- ...and ${errorsFirst.length - 5} more`);
2684
+ }
2685
+ lines.push("");
2686
+ lines.push(
2687
+ '_Say "capture these as tensions" to commit them to the Chain, or skip to continue._'
2688
+ );
2689
+ lines.push("");
2690
+ return lines;
2691
+ }
2618
2692
 
2619
2693
  // src/lib/resolve-project-root.ts
2620
2694
  import { existsSync } from "fs";
@@ -2623,6 +2697,8 @@ function resolveProjectRoot() {
2623
2697
  const candidates = [
2624
2698
  process.env.WORKSPACE_PATH,
2625
2699
  process.cwd(),
2700
+ // Parent of cwd: handles monorepo subpackages (e.g. packages/mcp-server)
2701
+ // where the Convex schema lives one level up.
2626
2702
  resolve2(process.cwd(), "..")
2627
2703
  ].filter(Boolean);
2628
2704
  for (const dir of candidates) {
@@ -2652,16 +2728,24 @@ async function mapWithConcurrency(items, mapper, concurrency = 3) {
2652
2728
  );
2653
2729
  return results;
2654
2730
  }
2731
+ function isValidTopGap(g) {
2732
+ if (typeof g !== "object" || g === null) return false;
2733
+ const obj = g;
2734
+ return typeof obj.registry === "string" && typeof obj.slug === "string" && typeof obj.severity === "string" && (obj.severity === "error" || obj.severity === "warning" || obj.severity === "info");
2735
+ }
2655
2736
  async function fetchSessionCoherenceSnapshot(sessionId) {
2656
2737
  try {
2657
2738
  const session = await mcpCall("agent.getSession", {
2658
2739
  sessionId
2659
2740
  });
2660
2741
  const raw = session?.coherenceSnapshot;
2661
- if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number") {
2742
+ if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number" && Array.isArray(raw.topGaps) && raw.topGaps.every(isValidTopGap)) {
2662
2743
  return raw;
2663
2744
  }
2664
- } catch {
2745
+ } catch (err) {
2746
+ if (process.env.DEBUG_COHERENCE) {
2747
+ console.debug("[coherence] Snapshot fetch failed:", err instanceof Error ? err.message : err);
2748
+ }
2665
2749
  }
2666
2750
  return null;
2667
2751
  }
@@ -2760,6 +2844,7 @@ async function runWrapupReview() {
2760
2844
  lines.push("");
2761
2845
  }
2762
2846
  let coherenceVerdict;
2847
+ let persistentGaps;
2763
2848
  try {
2764
2849
  const projectRoot = resolveProjectRoot();
2765
2850
  if (projectRoot) {
@@ -2781,6 +2866,10 @@ async function runWrapupReview() {
2781
2866
  );
2782
2867
  lines.push("");
2783
2868
  }
2869
+ persistentGaps = findPersistentGaps(delta, report.violations);
2870
+ if (persistentGaps.length > 0) {
2871
+ lines.push(...renderPersistentGapOffers(persistentGaps));
2872
+ }
2784
2873
  } else if (touchedFiles.size > 0) {
2785
2874
  lines.push("### Coherence Check");
2786
2875
  lines.push(
@@ -2797,7 +2886,10 @@ async function runWrapupReview() {
2797
2886
  }
2798
2887
  }
2799
2888
  }
2800
- } catch {
2889
+ } catch (err) {
2890
+ if (process.env.DEBUG_COHERENCE) {
2891
+ console.debug("[coherence] Wrapup coherence check failed:", err instanceof Error ? err.message : err);
2892
+ }
2801
2893
  }
2802
2894
  if (data.drafts.length > 0) {
2803
2895
  const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
@@ -2807,7 +2899,7 @@ async function runWrapupReview() {
2807
2899
  lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
2808
2900
  }
2809
2901
  const gapCount = getSessionGaps().length;
2810
- return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict };
2902
+ return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict, persistentGaps };
2811
2903
  }
2812
2904
  async function runWrapupCommitAll(data, cachedSuggestions) {
2813
2905
  requireWriteAccess();
@@ -2932,7 +3024,7 @@ function registerWrapupTools(server) {
2932
3024
  }
2933
3025
  );
2934
3026
  }
2935
- const { text, data, suggestions, failureCode, gapCount, coherenceVerdict } = await runWrapupReview();
3027
+ const { text, data, suggestions, failureCode, gapCount, coherenceVerdict, persistentGaps } = await runWrapupReview();
2936
3028
  lastReviewData = data;
2937
3029
  lastReviewSuggestions = suggestions;
2938
3030
  lastReviewSessionId = getAgentSessionId();
@@ -2945,16 +3037,18 @@ ${text}` : text;
2945
3037
  const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
2946
3038
  const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
2947
3039
  const coherenceSummary = coherenceVerdict ? `, coherence: ${coherenceVerdict}` : "";
3040
+ const persistentSummary = persistentGaps?.length ? `, ${persistentGaps.length} persistent coherence gaps` : "";
2948
3041
  return successResult(
2949
3042
  fullText,
2950
- `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}.`,
3043
+ `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}${persistentSummary}.`,
2951
3044
  {
2952
3045
  drafts: data.drafts.length,
2953
3046
  committed: data.committed.length,
2954
3047
  uncommitted: data.summary.uncommitted,
2955
3048
  suggestedLinks: suggestions.length,
2956
3049
  knowledgeGaps: gapCount ?? 0,
2957
- ...coherenceVerdict ? { coherenceVerdict } : {}
3050
+ ...coherenceVerdict ? { coherenceVerdict } : {},
3051
+ ...persistentGaps?.length ? { persistentCoherenceGaps: persistentGaps.length } : {}
2958
3052
  },
2959
3053
  next
2960
3054
  );
@@ -7687,16 +7781,19 @@ function buildCoherenceSection(projectRoot) {
7687
7781
  try {
7688
7782
  const root = projectRoot ?? resolveProjectRoot() ?? process.cwd();
7689
7783
  return checkAndRender(root);
7690
- } catch {
7784
+ } catch (err) {
7785
+ if (process.env.DEBUG_COHERENCE) {
7786
+ console.debug("[coherence] buildCoherenceSection failed:", err instanceof Error ? err.message : err);
7787
+ }
7691
7788
  return null;
7692
7789
  }
7693
7790
  }
7694
7791
  function runAlignmentCheck(task, activeBets, taskContextHits) {
7695
7792
  const betNames = activeBets.map((b) => b.name);
7696
- const taskWords = task.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
7793
+ const taskWords = extractKeywords(task);
7697
7794
  const matchingBet = activeBets.find((b) => {
7698
- const name = (b.name ?? "").toLowerCase();
7699
- return taskWords.some((w) => name.includes(w));
7795
+ const words = (b.name ?? "").toLowerCase().split(/\s+/);
7796
+ return taskWords.some((tw) => words.some((w) => w === tw || w.startsWith(tw + "-")));
7700
7797
  });
7701
7798
  if (matchingBet) {
7702
7799
  return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
@@ -7711,11 +7808,11 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
7711
7808
  }
7712
7809
  function buildAlignmentCheckLines(result) {
7713
7810
  const lines = ["## Alignment Check"];
7714
- if (result.aligned && result.matchSource === "active_bet") {
7811
+ if (result.aligned && result.matchSource === "active_bet" && result.matchedBet) {
7715
7812
  lines.push(
7716
7813
  `Task aligns with active bet: **${result.matchedBet}**. Proceed.`
7717
7814
  );
7718
- } else if (result.aligned && result.matchSource === "task_context") {
7815
+ } else if (result.aligned && result.matchSource === "task_context" && result.matchedBet) {
7719
7816
  const noActiveBets = result.betNames.length === 0;
7720
7817
  lines.push(
7721
7818
  `Task related to **${result.matchedBet}** via task context${noActiveBets ? " (no active bets in horizon=now)" : ""}. Proceed with caution \u2014 confirm scope with the user if uncertain.`
@@ -7745,7 +7842,15 @@ var RULE5_COMPACT = '**Validate against governance.** Before proposing or buildi
7745
7842
  var MAX_ENTRIES_NO_TASK = 3;
7746
7843
  var MAX_ENTRIES_WITH_TASK = 5;
7747
7844
  function extractKeywords(text) {
7748
- return [...new Set(text.toLowerCase().split(/\s+/).filter((w) => w.length > 2))];
7845
+ const normalized = text.toLowerCase().replace(/[^\w\s-]/g, " ");
7846
+ const tokens = [];
7847
+ for (const raw of normalized.split(/\s+/)) {
7848
+ if (!raw) continue;
7849
+ const parts = raw.split(/[-_]/);
7850
+ if (parts.length > 1) tokens.push(raw.replace(/[-_]/g, ""));
7851
+ for (const p of parts) if (p) tokens.push(p);
7852
+ }
7853
+ return [...new Set(tokens.filter((w) => w.length > 2))];
7749
7854
  }
7750
7855
  function scoreEntry(entry, keywords) {
7751
7856
  const text = `${entry.name} ${entry.description ?? ""}`.toLowerCase();
@@ -12457,4 +12562,4 @@ export {
12457
12562
  SERVER_VERSION,
12458
12563
  createProductBrainServer
12459
12564
  };
12460
- //# sourceMappingURL=chunk-JOJWCU7A.js.map
12565
+ //# sourceMappingURL=chunk-2GMFQHAF.js.map