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

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.
@@ -43,7 +43,7 @@ import {
43
43
  unknownAction,
44
44
  validationResult,
45
45
  withEnvelope
46
- } from "./chunk-TH5AUVVM.js";
46
+ } from "./chunk-XLMQ7POI.js";
47
47
  import {
48
48
  trackKnowledgeGap,
49
49
  trackQualityCheck,
@@ -69,7 +69,7 @@ var WORKFLOW_STATUS_VALUES = [
69
69
  "conflict",
70
70
  "processing",
71
71
  "closed",
72
- // Bets lifecycle
72
+ // Chains (bet type) lifecycle
73
73
  "shaped",
74
74
  "bet",
75
75
  "building",
@@ -91,7 +91,7 @@ var updateEntrySchema = z.object({
91
91
  z.enum(["draft", "active", "deprecated", "archived"]),
92
92
  z.enum(WORKFLOW_STATUS_VALUES)
93
93
  ]).optional().describe("Lifecycle status: draft | active | deprecated | archived. **Workflow values (open, pending, decided\u2026) are deprecated here \u2014 use `workflowStatus` instead. Passing a workflow value as `status` will be auto-routed with a warning until 2026-09-03, then hard-errored.**"),
94
- workflowStatus: z.enum(WORKFLOW_STATUS_VALUES).optional().describe("Collection workflow state. Each collection restricts which values are valid (e.g. bets: 'shaped' | 'bet' | 'building' | 'shipped'; assumptions: 'untested' | 'testing' | 'validated' | 'invalidated'; decisions: 'pending' | 'decided'; tensions: 'open' | 'processing' | 'decided' | 'closed'). The backend will reject values invalid for the target collection."),
94
+ workflowStatus: z.enum(WORKFLOW_STATUS_VALUES).optional().describe("Collection workflow state. Each collection restricts which values are valid (e.g. chains with chainTypeId='bet': 'shaped' | 'bet' | 'building' | 'shipped'; assumptions: 'untested' | 'testing' | 'validated' | 'invalidated'; decisions: 'pending' | 'decided'; tensions: 'open' | 'processing' | 'decided' | 'closed'). The backend will reject values invalid for the target collection."),
95
95
  data: z.record(z.unknown()).optional().describe("Fields to update (merged with existing data)"),
96
96
  order: z.number().optional().describe("New sort order"),
97
97
  canonicalKey: z.string().optional().describe("Semantic type (e.g. 'decision', 'tension'). Only changeable on draft/uncommitted entries."),
@@ -238,7 +238,7 @@ ${formatted}` }],
238
238
  },
239
239
  withEnvelope(async ({ entryId }) => {
240
240
  requireWriteAccess();
241
- const { runContradictionCheck } = await import("./smart-capture-Q64ZXK65.js");
241
+ const { runContradictionCheck } = await import("./smart-capture-QFYRKMBM.js");
242
242
  const entry = await mcpQuery("chain.getEntry", { entryId });
243
243
  if (!entry) {
244
244
  return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
@@ -1691,7 +1691,7 @@ async function handleBuild(server, entryId, maxHops) {
1691
1691
 
1692
1692
  // src/tools/collections.ts
1693
1693
  import { z as z6 } from "zod";
1694
- var COLLECTIONS_ACTIONS = ["list", "create", "update"];
1694
+ var COLLECTIONS_ACTIONS = ["list", "create", "update", "describe"];
1695
1695
  var fieldSchema = z6.object({
1696
1696
  key: z6.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
1697
1697
  label: z6.string().describe("Display label, e.g. 'Description', 'Severity'"),
@@ -1705,7 +1705,7 @@ var fieldSchema = z6.object({
1705
1705
  });
1706
1706
  var collectionsSchema = z6.object({
1707
1707
  action: z6.enum(COLLECTIONS_ACTIONS).describe(
1708
- "'list': browse all collections. 'create': create a new collection. 'update': update an existing collection."
1708
+ "'list': browse all collections. 'create': create a new collection. 'update': update an existing collection. 'describe': full documentation for one collection \u2014 fields, option guides, usage guidance, examples."
1709
1709
  ),
1710
1710
  slug: z6.string().optional().describe("URL-safe identifier for create/update, e.g. 'glossary', 'tech-debt'"),
1711
1711
  name: z6.string().optional().describe("Display name for create, or new name for update"),
@@ -1720,7 +1720,7 @@ function registerCollectionsTools(server) {
1720
1720
  "collections",
1721
1721
  {
1722
1722
  title: "Collections",
1723
- description: "Manage knowledge collections. Three actions:\n\n- **list**: Browse all collections \u2014 glossary, business rules, tracking events, etc. Returns slug, name, description, and field schema. Use before capture to see what exists.\n- **create**: Create a new collection. Provide slug, name, and field schema. Use when setting up a workspace or tracking a new type of knowledge.\n- **update**: Update an existing collection's name, description, purpose, icon, navGroup, or fields. Only provide the fields you want to change.",
1723
+ description: "Manage knowledge collections. Four actions:\n\n- **list**: Browse all collections \u2014 glossary, business rules, tracking events, etc. Returns slug, name, description, and field schema. Use before capture to see what exists.\n- **describe**: Full documentation for a single collection. Returns field help text, option decision guides, usage guidance, examples, and cross-references. Use when you need to understand a collection's purpose, field semantics, or option values.\n- **create**: Create a new collection. Provide slug, name, and field schema. Use when setting up a workspace or tracking a new type of knowledge.\n- **update**: Update an existing collection's name, description, purpose, icon, navGroup, or fields. Only provide the fields you want to change.",
1724
1724
  inputSchema: collectionsSchema,
1725
1725
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
1726
1726
  },
@@ -1732,6 +1732,12 @@ function registerCollectionsTools(server) {
1732
1732
  if (action === "list") {
1733
1733
  return handleList2();
1734
1734
  }
1735
+ if (action === "describe") {
1736
+ if (!slug) {
1737
+ return validationResult("slug is required when action is 'describe'.");
1738
+ }
1739
+ return handleDescribe(slug);
1740
+ }
1735
1741
  if (action === "create") {
1736
1742
  if (!slug || !name || !fields || fields.length === 0) {
1737
1743
  return validationResult("slug, name, and fields are required when action is 'create'.");
@@ -1753,6 +1759,99 @@ function registerCollectionsTools(server) {
1753
1759
  );
1754
1760
  trackWriteTool(tool);
1755
1761
  }
1762
+ async function handleDescribe(slug) {
1763
+ const col = await mcpQuery("chain.getCollection", { slug });
1764
+ if (!col) {
1765
+ return failureResult(
1766
+ `Collection '${slug}' not found.`,
1767
+ "NOT_FOUND",
1768
+ `No collection with slug '${slug}' in this workspace.`,
1769
+ "Use `collections action=list` to see available collections."
1770
+ );
1771
+ }
1772
+ const sections = [];
1773
+ sections.push(`# ${col.name} (\`${col.slug}\`)`);
1774
+ if (col.description) sections.push(col.description);
1775
+ const meta = [
1776
+ col.governed ? "**Governed**" : null,
1777
+ col.navGroup ? `**Nav group:** ${col.navGroup}` : null,
1778
+ col.idPrefix ? `**Prefix:** ${col.idPrefix}-*` : null,
1779
+ col.stratum ? `**Stratum:** ${col.stratum}` : null
1780
+ ].filter(Boolean).join(" \xB7 ");
1781
+ if (meta) sections.push(meta);
1782
+ if (col.usageGuidance) {
1783
+ sections.push(`
1784
+ ## When to Use
1785
+
1786
+ ${col.usageGuidance}`);
1787
+ } else if (col.purpose) {
1788
+ sections.push(`
1789
+ ## Purpose
1790
+
1791
+ ${col.purpose}`);
1792
+ }
1793
+ if (col.fields.length > 0) {
1794
+ const fieldDocs = col.fields.map((f) => {
1795
+ const parts = [];
1796
+ const typeInfo = `${f.type}${f.required ? ", required" : ""}`;
1797
+ parts.push(`### \`${f.key}\` \u2014 ${f.label ?? f.key} (${typeInfo})`);
1798
+ if (f.helpText) parts.push(f.helpText);
1799
+ if (f.options && f.options.length > 0) {
1800
+ const optLines = f.options.map((opt) => {
1801
+ const desc = f.optionDescriptions?.[opt];
1802
+ return desc ? ` - **${opt}**: ${desc}` : ` - ${opt}`;
1803
+ });
1804
+ parts.push(`
1805
+ Options:
1806
+ ${optLines.join("\n")}`);
1807
+ }
1808
+ return parts.join("\n");
1809
+ });
1810
+ sections.push(`
1811
+ ## Fields
1812
+
1813
+ ${fieldDocs.join("\n\n")}`);
1814
+ }
1815
+ if (col.examples && col.examples.length > 0) {
1816
+ const exList = col.examples.map((ex) => `- **${ex.name}** \u2014 ${ex.description}`).join("\n");
1817
+ sections.push(`
1818
+ ## Examples
1819
+
1820
+ ${exList}`);
1821
+ }
1822
+ if (col.crossReferences && col.crossReferences.length > 0) {
1823
+ const refList = col.crossReferences.map(
1824
+ (r) => `- **${r.slug}** (${r.relationship}): ${r.guidance}`
1825
+ ).join("\n");
1826
+ sections.push(`
1827
+ ## Related Collections
1828
+
1829
+ ${refList}`);
1830
+ }
1831
+ return {
1832
+ content: [{ type: "text", text: sections.join("\n\n") }],
1833
+ structuredContent: success(
1834
+ `Documentation for collection '${col.name}' (${col.slug}).`,
1835
+ {
1836
+ slug: col.slug,
1837
+ name: col.name,
1838
+ description: col.description,
1839
+ usageGuidance: col.usageGuidance ?? null,
1840
+ fieldCount: col.fields.length,
1841
+ fields: col.fields.map((f) => ({
1842
+ key: f.key,
1843
+ type: f.type,
1844
+ required: f.required,
1845
+ helpText: f.helpText ?? null,
1846
+ options: f.options ?? null,
1847
+ optionDescriptions: f.optionDescriptions ?? null
1848
+ })),
1849
+ examples: col.examples ?? [],
1850
+ crossReferences: col.crossReferences ?? []
1851
+ }
1852
+ )
1853
+ };
1854
+ }
1756
1855
  async function handleList2() {
1757
1856
  const collections = await mcpQuery("chain.listCollections");
1758
1857
  if (collections.length === 0) {
@@ -1997,7 +2096,7 @@ function registerLabelTools(server) {
1997
2096
  }
1998
2097
 
1999
2098
  // src/tools/health.ts
2000
- import { z as z20 } from "zod";
2099
+ import { z as z21 } from "zod";
2001
2100
 
2002
2101
  // src/tools/session.ts
2003
2102
  import { z as z9 } from "zod";
@@ -2363,6 +2462,7 @@ function toSnapshot(report, maxTopGaps = 10) {
2363
2462
  import { readFileSync } from "fs";
2364
2463
  import { resolve } from "path";
2365
2464
  function resolveRegistries(projectRoot, manifest) {
2465
+ if (!projectRoot) return {};
2366
2466
  const resolved = {};
2367
2467
  for (const ref of manifest) {
2368
2468
  try {
@@ -2374,7 +2474,10 @@ function resolveRegistries(projectRoot, manifest) {
2374
2474
  if (keys.size > 0) {
2375
2475
  resolved[ref.id] = keys;
2376
2476
  }
2377
- } catch {
2477
+ } catch (err) {
2478
+ if (process.env.DEBUG_COHERENCE) {
2479
+ console.debug(`[coherence] Skipping ${ref.id} (${ref.path}):`, err instanceof Error ? err.message : err);
2480
+ }
2378
2481
  }
2379
2482
  }
2380
2483
  return resolved;
@@ -2409,14 +2512,19 @@ function extractBalanced(content, openPos, openChar, closeChar) {
2409
2512
  i = skipString(content, i, ch);
2410
2513
  continue;
2411
2514
  }
2412
- if (ch === "/" && content[i + 1] === "/") {
2515
+ if (ch === "/" && i + 1 < content.length && content[i + 1] === "/") {
2413
2516
  while (i < content.length && content[i] !== "\n") i++;
2414
2517
  continue;
2415
2518
  }
2416
- if (ch === "/" && content[i + 1] === "*") {
2519
+ if (ch === "/" && i + 1 < content.length && content[i + 1] === "*") {
2417
2520
  i += 2;
2418
- while (i < content.length && !(content[i - 1] === "*" && content[i] === "/")) i++;
2419
- i++;
2521
+ while (i < content.length) {
2522
+ if (content[i] === "/" && i > 0 && content[i - 1] === "*") {
2523
+ i++;
2524
+ break;
2525
+ }
2526
+ i++;
2527
+ }
2420
2528
  continue;
2421
2529
  }
2422
2530
  if (ch === openChar) depth++;
@@ -2429,11 +2537,12 @@ function extractBalanced(content, openPos, openChar, closeChar) {
2429
2537
  function skipString(content, start, quote) {
2430
2538
  let i = start + 1;
2431
2539
  while (i < content.length) {
2432
- if (content[i] === "\\" && quote !== "`") {
2540
+ const ch = content[i];
2541
+ if (ch === "\\") {
2433
2542
  i += 2;
2434
2543
  continue;
2435
2544
  }
2436
- if (content[i] === quote) return i + 1;
2545
+ if (ch === quote) return i + 1;
2437
2546
  i++;
2438
2547
  }
2439
2548
  return i;
@@ -2454,14 +2563,19 @@ function extractObjectKeys(block) {
2454
2563
  let i = 0;
2455
2564
  while (i < block.length) {
2456
2565
  const ch = block[i];
2457
- if (ch === "/" && block[i + 1] === "/") {
2566
+ if (ch === "/" && i + 1 < block.length && block[i + 1] === "/") {
2458
2567
  while (i < block.length && block[i] !== "\n") i++;
2459
2568
  continue;
2460
2569
  }
2461
- if (ch === "/" && block[i + 1] === "*") {
2570
+ if (ch === "/" && i + 1 < block.length && block[i + 1] === "*") {
2462
2571
  i += 2;
2463
- while (i < block.length && !(block[i - 1] === "*" && block[i] === "/")) i++;
2464
- i++;
2572
+ while (i < block.length) {
2573
+ if (block[i] === "/" && i > 0 && block[i - 1] === "*") {
2574
+ i++;
2575
+ break;
2576
+ }
2577
+ i++;
2578
+ }
2465
2579
  continue;
2466
2580
  }
2467
2581
  if (depth === 0) {
@@ -2515,6 +2629,7 @@ function extractSetValues(block) {
2515
2629
  // src/lib/coherence/git-detection.ts
2516
2630
  import { execSync } from "child_process";
2517
2631
  function detectTouchedRegistries(projectRoot) {
2632
+ if (!projectRoot) return /* @__PURE__ */ new Set();
2518
2633
  const manifestPaths = new Set(REGISTRY_MANIFEST.map((r) => r.path));
2519
2634
  const touched = /* @__PURE__ */ new Set();
2520
2635
  try {
@@ -2529,7 +2644,10 @@ function detectTouchedRegistries(projectRoot) {
2529
2644
  touched.add(trimmed);
2530
2645
  }
2531
2646
  }
2532
- } catch {
2647
+ } catch (err) {
2648
+ if (process.env.DEBUG_COHERENCE) {
2649
+ console.debug("[coherence] Git detection failed:", err instanceof Error ? err.message : err);
2650
+ }
2533
2651
  }
2534
2652
  return touched;
2535
2653
  }
@@ -2599,13 +2717,16 @@ function computeDelta(before, after) {
2599
2717
  function renderCoherenceDelta(delta) {
2600
2718
  const lines = ["### Coherence Delta"];
2601
2719
  const p = (n) => n === 1 ? "" : "s";
2720
+ const absNet = Math.abs(delta.netChange);
2721
+ const approx = absNet > delta.gapsFixed + delta.gapsIntroduced;
2722
+ const prefix = approx ? "at least " : "";
2602
2723
  if (delta.verdict === "improved") {
2603
2724
  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" : ""}.`
2725
+ `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
2726
  );
2606
2727
  } else if (delta.verdict === "degraded") {
2607
2728
  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.`
2729
+ `**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
2730
  );
2610
2731
  } else {
2611
2732
  lines.push(
@@ -2615,6 +2736,58 @@ function renderCoherenceDelta(delta) {
2615
2736
  lines.push("");
2616
2737
  return lines;
2617
2738
  }
2739
+ function findPersistentGaps(delta, currentViolations) {
2740
+ const beforeSet = new Set(delta.before.topGaps.map((g) => `${g.registry}::${g.slug}`));
2741
+ const afterSet = new Set(delta.after.topGaps.map((g) => `${g.registry}::${g.slug}`));
2742
+ const persistentKeys = /* @__PURE__ */ new Set();
2743
+ for (const key of beforeSet) {
2744
+ if (afterSet.has(key)) persistentKeys.add(key);
2745
+ }
2746
+ if (persistentKeys.size === 0) return [];
2747
+ const violationMap = /* @__PURE__ */ new Map();
2748
+ for (const v of currentViolations) {
2749
+ violationMap.set(`${v.registryId}::${v.collectionSlug}`, v);
2750
+ }
2751
+ return [...persistentKeys].map((key) => {
2752
+ const [registry = "", slug = ""] = key.split("::", 2);
2753
+ return {
2754
+ registry,
2755
+ slug,
2756
+ severity: delta.after.topGaps.find((g) => g.registry === registry && g.slug === slug)?.severity ?? "warning",
2757
+ violation: violationMap.get(key) ?? null
2758
+ };
2759
+ });
2760
+ }
2761
+ function renderPersistentGapOffers(gaps) {
2762
+ if (gaps.length === 0) return [];
2763
+ const lines = [
2764
+ "### Persistent Coherence Gaps",
2765
+ "",
2766
+ "These gaps existed before and after this session \u2014 they represent structural registry drift.",
2767
+ "Want me to capture them as tensions on the Chain?",
2768
+ ""
2769
+ ];
2770
+ const SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
2771
+ const errorsFirst = [...gaps].sort(
2772
+ (a, b) => SEVERITY_RANK2[a.severity] - SEVERITY_RANK2[b.severity]
2773
+ );
2774
+ for (const gap of errorsFirst.slice(0, 5)) {
2775
+ const v = gap.violation;
2776
+ const fixHint = v?.fix ? ` \u2014 ${v.fix}` : "";
2777
+ lines.push(
2778
+ `- **\`${gap.slug}\`** missing from \`${gap.registry}\` (${gap.severity})${fixHint}`
2779
+ );
2780
+ }
2781
+ if (errorsFirst.length > 5) {
2782
+ lines.push(`- ...and ${errorsFirst.length - 5} more`);
2783
+ }
2784
+ lines.push("");
2785
+ lines.push(
2786
+ '_Say "capture these as tensions" to commit them to the Chain, or skip to continue._'
2787
+ );
2788
+ lines.push("");
2789
+ return lines;
2790
+ }
2618
2791
 
2619
2792
  // src/lib/resolve-project-root.ts
2620
2793
  import { existsSync } from "fs";
@@ -2623,6 +2796,8 @@ function resolveProjectRoot() {
2623
2796
  const candidates = [
2624
2797
  process.env.WORKSPACE_PATH,
2625
2798
  process.cwd(),
2799
+ // Parent of cwd: handles monorepo subpackages (e.g. packages/mcp-server)
2800
+ // where the Convex schema lives one level up.
2626
2801
  resolve2(process.cwd(), "..")
2627
2802
  ].filter(Boolean);
2628
2803
  for (const dir of candidates) {
@@ -2652,16 +2827,24 @@ async function mapWithConcurrency(items, mapper, concurrency = 3) {
2652
2827
  );
2653
2828
  return results;
2654
2829
  }
2830
+ function isValidTopGap(g) {
2831
+ if (typeof g !== "object" || g === null) return false;
2832
+ const obj = g;
2833
+ return typeof obj.registry === "string" && typeof obj.slug === "string" && typeof obj.severity === "string" && (obj.severity === "error" || obj.severity === "warning" || obj.severity === "info");
2834
+ }
2655
2835
  async function fetchSessionCoherenceSnapshot(sessionId) {
2656
2836
  try {
2657
2837
  const session = await mcpCall("agent.getSession", {
2658
2838
  sessionId
2659
2839
  });
2660
2840
  const raw = session?.coherenceSnapshot;
2661
- if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number") {
2841
+ if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number" && Array.isArray(raw.topGaps) && raw.topGaps.every(isValidTopGap)) {
2662
2842
  return raw;
2663
2843
  }
2664
- } catch {
2844
+ } catch (err) {
2845
+ if (process.env.DEBUG_COHERENCE) {
2846
+ console.debug("[coherence] Snapshot fetch failed:", err instanceof Error ? err.message : err);
2847
+ }
2665
2848
  }
2666
2849
  return null;
2667
2850
  }
@@ -2760,6 +2943,7 @@ async function runWrapupReview() {
2760
2943
  lines.push("");
2761
2944
  }
2762
2945
  let coherenceVerdict;
2946
+ let persistentGaps;
2763
2947
  try {
2764
2948
  const projectRoot = resolveProjectRoot();
2765
2949
  if (projectRoot) {
@@ -2781,6 +2965,10 @@ async function runWrapupReview() {
2781
2965
  );
2782
2966
  lines.push("");
2783
2967
  }
2968
+ persistentGaps = findPersistentGaps(delta, report.violations);
2969
+ if (persistentGaps.length > 0) {
2970
+ lines.push(...renderPersistentGapOffers(persistentGaps));
2971
+ }
2784
2972
  } else if (touchedFiles.size > 0) {
2785
2973
  lines.push("### Coherence Check");
2786
2974
  lines.push(
@@ -2797,7 +2985,10 @@ async function runWrapupReview() {
2797
2985
  }
2798
2986
  }
2799
2987
  }
2800
- } catch {
2988
+ } catch (err) {
2989
+ if (process.env.DEBUG_COHERENCE) {
2990
+ console.debug("[coherence] Wrapup coherence check failed:", err instanceof Error ? err.message : err);
2991
+ }
2801
2992
  }
2802
2993
  if (data.drafts.length > 0) {
2803
2994
  const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
@@ -2807,7 +2998,7 @@ async function runWrapupReview() {
2807
2998
  lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
2808
2999
  }
2809
3000
  const gapCount = getSessionGaps().length;
2810
- return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict };
3001
+ return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict, persistentGaps };
2811
3002
  }
2812
3003
  async function runWrapupCommitAll(data, cachedSuggestions) {
2813
3004
  requireWriteAccess();
@@ -2932,7 +3123,7 @@ function registerWrapupTools(server) {
2932
3123
  }
2933
3124
  );
2934
3125
  }
2935
- const { text, data, suggestions, failureCode, gapCount, coherenceVerdict } = await runWrapupReview();
3126
+ const { text, data, suggestions, failureCode, gapCount, coherenceVerdict, persistentGaps } = await runWrapupReview();
2936
3127
  lastReviewData = data;
2937
3128
  lastReviewSuggestions = suggestions;
2938
3129
  lastReviewSessionId = getAgentSessionId();
@@ -2945,16 +3136,18 @@ ${text}` : text;
2945
3136
  const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
2946
3137
  const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
2947
3138
  const coherenceSummary = coherenceVerdict ? `, coherence: ${coherenceVerdict}` : "";
3139
+ const persistentSummary = persistentGaps?.length ? `, ${persistentGaps.length} persistent coherence gaps` : "";
2948
3140
  return successResult(
2949
3141
  fullText,
2950
- `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}.`,
3142
+ `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}${persistentSummary}.`,
2951
3143
  {
2952
3144
  drafts: data.drafts.length,
2953
3145
  committed: data.committed.length,
2954
3146
  uncommitted: data.summary.uncommitted,
2955
3147
  suggestedLinks: suggestions.length,
2956
3148
  knowledgeGaps: gapCount ?? 0,
2957
- ...coherenceVerdict ? { coherenceVerdict } : {}
3149
+ ...coherenceVerdict ? { coherenceVerdict } : {},
3150
+ ...persistentGaps?.length ? { persistentCoherenceGaps: persistentGaps.length } : {}
2958
3151
  },
2959
3152
  next
2960
3153
  );
@@ -3758,7 +3951,7 @@ If any facilitate call fails:
3758
3951
  description: "Confirmed problem statement and appetite",
3759
3952
  format: "structured"
3760
3953
  },
3761
- kbCollection: "bets",
3954
+ kbCollection: "chains",
3762
3955
  maxDurationHint: "10 min"
3763
3956
  },
3764
3957
  {
@@ -3828,7 +4021,7 @@ If any facilitate call fails:
3828
4021
  description: "Final capture summary \u2014 entries committed, relations created",
3829
4022
  format: "structured"
3830
4023
  },
3831
- kbCollection: "bets",
4024
+ kbCollection: "chains",
3832
4025
  maxDurationHint: "5 min"
3833
4026
  }
3834
4027
  ],
@@ -3836,7 +4029,7 @@ If any facilitate call fails:
3836
4029
  primaryRecord: {
3837
4030
  routing: {
3838
4031
  mode: "fixed",
3839
- collection: "bets"
4032
+ collection: "chains"
3840
4033
  }
3841
4034
  },
3842
4035
  emits: [
@@ -3862,7 +4055,7 @@ If any facilitate call fails:
3862
4055
  },
3863
4056
  {
3864
4057
  kind: "update",
3865
- collection: "bets",
4058
+ collection: "chains",
3866
4059
  description: "Bet status and no-go updates during finalize."
3867
4060
  }
3868
4061
  ]
@@ -5729,7 +5922,8 @@ function generateBuildContract(ctx) {
5729
5922
  function computeCommitBlockers(opts) {
5730
5923
  const { betEntryId, betDocId, relations, hasStrategyLink, betData, sessionDrafts } = opts;
5731
5924
  const blockers = [];
5732
- const str = (key) => (betData[key] ?? "").trim();
5925
+ const links = betData.links ?? {};
5926
+ const str = (key) => (links[key] ?? "").trim();
5733
5927
  if (!hasStrategyLink) {
5734
5928
  blockers.push({
5735
5929
  entryId: betEntryId,
@@ -6093,7 +6287,8 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
6093
6287
  }
6094
6288
  }
6095
6289
  function buildScoringContext(userInput, betData, constellation, overlapIds, chainResults = [], activeDimension, source = "user") {
6096
- const str = (key) => betData[key] ?? "";
6290
+ const links = betData.links ?? {};
6291
+ const str = (key) => links[key] ?? "";
6097
6292
  const accParts = [
6098
6293
  str("problem"),
6099
6294
  str("appetite"),
@@ -6248,17 +6443,20 @@ async function processCaptures(opts) {
6248
6443
  };
6249
6444
  const FALLBACK_DEFAULTS = { dataField: "description", relationType: "related_to" };
6250
6445
  let runningBetData = { ...betData };
6446
+ const runningLinks = () => ({ ...runningBetData.links ?? {} });
6251
6447
  for (const item of captureItems) {
6252
6448
  if (item.type === "noGo") {
6449
+ const curLinks = runningLinks();
6253
6450
  const updatedNoGos = appendNoGo(
6254
- runningBetData.noGos,
6451
+ curLinks.noGos,
6255
6452
  { title: item.name, explanation: item.description }
6256
6453
  );
6257
- runningBetData.noGos = updatedNoGos;
6454
+ const newLinks = { ...curLinks, noGos: updatedNoGos };
6455
+ runningBetData.links = newLinks;
6258
6456
  try {
6259
6457
  await mcpMutation("chain.updateEntry", {
6260
6458
  entryId: betEntryId,
6261
- data: { noGos: updatedNoGos },
6459
+ data: { links: newLinks },
6262
6460
  changeNote: `Added no-go: ${item.name}`
6263
6461
  });
6264
6462
  await recordSessionActivity({ entryModified: betDocId });
@@ -6342,15 +6540,17 @@ async function processCaptures(opts) {
6342
6540
  }
6343
6541
  }
6344
6542
  if (item.type === "element") {
6543
+ const curLinks = runningLinks();
6345
6544
  const updatedElements = appendElement(
6346
- runningBetData.elements,
6545
+ curLinks.elements,
6347
6546
  { name: item.name, description: item.description, entryId: capturedEntryId }
6348
6547
  );
6349
- runningBetData.elements = updatedElements;
6548
+ const newLinks = { ...curLinks, elements: updatedElements };
6549
+ runningBetData.links = newLinks;
6350
6550
  try {
6351
6551
  await mcpMutation("chain.updateEntry", {
6352
6552
  entryId: betEntryId,
6353
- data: { elements: updatedElements },
6553
+ data: { links: newLinks },
6354
6554
  changeNote: `Added element: ${item.name}`
6355
6555
  });
6356
6556
  await recordSessionActivity({ entryModified: betDocId });
@@ -6361,15 +6561,17 @@ async function processCaptures(opts) {
6361
6561
  });
6362
6562
  }
6363
6563
  } else if (item.type === "risk") {
6564
+ const curLinks = runningLinks();
6364
6565
  const updatedRisks = appendRabbitHole(
6365
- runningBetData.rabbitHoles,
6566
+ curLinks.rabbitHoles,
6366
6567
  { name: item.name, description: item.description, theme: item.theme, entryId: capturedEntryId }
6367
6568
  );
6368
- runningBetData.rabbitHoles = updatedRisks;
6569
+ const newLinks = { ...curLinks, rabbitHoles: updatedRisks };
6570
+ runningBetData.links = newLinks;
6369
6571
  try {
6370
6572
  await mcpMutation("chain.updateEntry", {
6371
6573
  entryId: betEntryId,
6372
- data: { rabbitHoles: updatedRisks },
6574
+ data: { links: newLinks },
6373
6575
  changeNote: `Added risk: ${item.name}`
6374
6576
  });
6375
6577
  await recordSessionActivity({ entryModified: betDocId });
@@ -6405,7 +6607,8 @@ async function computeAndUpdateScores(opts) {
6405
6607
  const alreadyCheckedOverlap = typeof refreshedData._overlapIds === "string";
6406
6608
  let overlap = [];
6407
6609
  if (!alreadyCheckedOverlap && captureItems.length === 0) {
6408
- const problemText = refreshedData.problem ?? userInput;
6610
+ const refreshedLinks = refreshedData.links ?? {};
6611
+ const problemText = refreshedLinks.problem ?? userInput;
6409
6612
  const overlapResults = await searchChain(problemText, { maxResults: 5, excludeIds: [betEntryId, ...constellationEntryIds] });
6410
6613
  overlap = overlapResults.map((r) => ({
6411
6614
  entryId: r.entryId,
@@ -6414,7 +6617,8 @@ async function computeAndUpdateScores(opts) {
6414
6617
  }));
6415
6618
  }
6416
6619
  const overlapIds = overlap.map((o) => o.entryId);
6417
- const appetiteText = refreshedData.appetite ?? "";
6620
+ const refreshedLinksForAppetite = refreshedData.links ?? {};
6621
+ const appetiteText = refreshedLinksForAppetite.appetite ?? "";
6418
6622
  const isSmallBatch = detectSmallBatch(
6419
6623
  argDimension === "appetite" ? userInput : appetiteText
6420
6624
  );
@@ -6440,15 +6644,20 @@ async function computeAndUpdateScores(opts) {
6440
6644
  const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
6441
6645
  try {
6442
6646
  const fieldUpdates = {};
6647
+ const linksUpdates = {};
6648
+ const currentLinks = refreshedData.links ?? {};
6443
6649
  const persist = (dim, field, minLen) => {
6444
- if (activeDimension === dim && !refreshedData[field] && userInput.length > minLen) {
6445
- fieldUpdates[field] = userInput;
6650
+ if (activeDimension === dim && !currentLinks[field] && userInput.length > minLen) {
6651
+ linksUpdates[field] = userInput;
6446
6652
  }
6447
6653
  };
6448
6654
  persist("problem_clarity", "problem", 50);
6449
6655
  persist("appetite", "appetite", 20);
6450
6656
  persist("architecture", "architecture", 50);
6451
6657
  persist("done_when", "done_when", 30);
6658
+ if (Object.keys(linksUpdates).length > 0) {
6659
+ fieldUpdates.links = { ...currentLinks, ...linksUpdates };
6660
+ }
6452
6661
  if (!refreshedData._overlapIds) {
6453
6662
  fieldUpdates._overlapIds = overlapIds.length > 0 ? overlapIds.join(",") : "_checked";
6454
6663
  }
@@ -6473,7 +6682,7 @@ async function computeAndUpdateScores(opts) {
6473
6682
  });
6474
6683
  }
6475
6684
  const alignment = [];
6476
- const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "bets"]);
6685
+ const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "chains"]);
6477
6686
  for (const result of chainSurfaced) {
6478
6687
  if (governanceCollections.has(result.collection)) {
6479
6688
  alignment.push({ entryId: result.entryId, relationship: `governs [${result.collection}]` });
@@ -6492,11 +6701,13 @@ async function computeAndUpdateScores(opts) {
6492
6701
  relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.name }))
6493
6702
  };
6494
6703
  buildContract = generateBuildContract(contractCtx);
6495
- if (refreshedData.buildContract !== buildContract) {
6704
+ const currentBCLinks = refreshedData.links ?? {};
6705
+ if (currentBCLinks.buildContract !== buildContract) {
6496
6706
  try {
6707
+ const bcLinks = { ...refreshedData.links ?? {}, buildContract };
6497
6708
  await mcpMutation("chain.updateEntry", {
6498
6709
  entryId: betEntryId,
6499
- data: { buildContract },
6710
+ data: { links: bcLinks },
6500
6711
  changeNote: "Updated build contract from shaping session"
6501
6712
  });
6502
6713
  await recordSessionActivity({ entryModified: refreshedBet?._id ?? betEntry._id });
@@ -6573,9 +6784,10 @@ function assembleResponse(opts) {
6573
6784
  } = opts;
6574
6785
  const studioUrl = buildStudioUrl(workspaceSlug, betEntryId);
6575
6786
  const suggested = suggestCaptures(scoringCtx, activeDimension);
6576
- const betProblem = refreshedData.problem ?? "";
6787
+ const assembleLinks = refreshedData.links ?? {};
6788
+ const betProblem = assembleLinks.problem ?? "";
6577
6789
  const responseBetName = refreshedData.description ?? betName;
6578
- const elementNames = (refreshedData.elements ?? "").match(/###\s*Element\s*\d+:\s*(.+)/gi)?.map((h) => h.replace(/###\s*Element\s*\d+:\s*/i, "").trim()) ?? [];
6790
+ const elementNames = (assembleLinks.elements ?? "").match(/###\s*Element\s*\d+:\s*(.+)/gi)?.map((h) => h.replace(/###\s*Element\s*\d+:\s*/i, "").trim()) ?? [];
6579
6791
  const investigationBrief = buildInvestigationBrief(phase, responseBetName, betEntryId, betProblem, elementNames) ?? void 0;
6580
6792
  const response = {
6581
6793
  version: 2,
@@ -6663,20 +6875,23 @@ async function handleRespond(args) {
6663
6875
  const result = await mcpMutation(
6664
6876
  "chain.createEntry",
6665
6877
  {
6666
- collectionSlug: "bets",
6878
+ collectionSlug: "chains",
6667
6879
  name: betName,
6668
6880
  status: "draft",
6669
6881
  data: {
6670
- problem: userInput,
6671
- appetite: "",
6672
- elements: "",
6673
- rabbitHoles: "",
6674
- noGos: "",
6675
- architecture: "",
6676
- buildContract: "",
6882
+ chainTypeId: "bet",
6677
6883
  description: `Shaping session for: ${betName}`,
6678
6884
  status: "shaping",
6679
- shapingSessionActive: true
6885
+ shapingSessionActive: true,
6886
+ links: {
6887
+ problem: userInput,
6888
+ appetite: "",
6889
+ elements: "",
6890
+ rabbitHoles: "",
6891
+ noGos: "",
6892
+ architecture: "",
6893
+ buildContract: ""
6894
+ }
6680
6895
  },
6681
6896
  createdBy: agentId ? `agent:${agentId}` : "facilitate",
6682
6897
  sessionId: agentId ?? void 0
@@ -6782,7 +6997,8 @@ async function handleScore(args) {
6782
6997
  }
6783
6998
  const constellation = await loadConstellationState(betId, betEntry._id);
6784
6999
  const betData = betEntry.data ?? {};
6785
- const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
7000
+ const scoreLinks = betData.links ?? {};
7001
+ const isSmallBatch = detectSmallBatch(scoreLinks.appetite ?? "");
6786
7002
  const scoringCtx = buildCachedScoringContext(betData, constellation);
6787
7003
  const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
6788
7004
  const dims = activeDimensions(isSmallBatch);
@@ -6885,7 +7101,8 @@ async function handleResume(args) {
6885
7101
  }
6886
7102
  const constellation = await loadConstellationState(betId, betEntry._id);
6887
7103
  const betData = betEntry.data ?? {};
6888
- const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
7104
+ const resumeLinks = betData.links ?? {};
7105
+ const isSmallBatch = detectSmallBatch(resumeLinks.appetite ?? "");
6889
7106
  const scoringCtx = buildCachedScoringContext(betData, constellation);
6890
7107
  const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
6891
7108
  const dims = activeDimensions(isSmallBatch);
@@ -7036,8 +7253,9 @@ async function handleCommitConstellation(args) {
7036
7253
  }
7037
7254
  let contradictionWarnings = [];
7038
7255
  try {
7039
- const { runContradictionCheck } = await import("./smart-capture-Q64ZXK65.js");
7040
- const descField = betData.problem ?? betData.description ?? "";
7256
+ const { runContradictionCheck } = await import("./smart-capture-QFYRKMBM.js");
7257
+ const commitLinks = betData.links ?? {};
7258
+ const descField = commitLinks.problem ?? betData.description ?? "";
7041
7259
  contradictionWarnings = await runContradictionCheck(
7042
7260
  betEntry.name ?? betId,
7043
7261
  descField
@@ -7687,22 +7905,25 @@ function buildCoherenceSection(projectRoot) {
7687
7905
  try {
7688
7906
  const root = projectRoot ?? resolveProjectRoot() ?? process.cwd();
7689
7907
  return checkAndRender(root);
7690
- } catch {
7908
+ } catch (err) {
7909
+ if (process.env.DEBUG_COHERENCE) {
7910
+ console.debug("[coherence] buildCoherenceSection failed:", err instanceof Error ? err.message : err);
7911
+ }
7691
7912
  return null;
7692
7913
  }
7693
7914
  }
7694
7915
  function runAlignmentCheck(task, activeBets, taskContextHits) {
7695
7916
  const betNames = activeBets.map((b) => b.name);
7696
- const taskWords = task.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
7917
+ const taskWords = extractKeywords(task);
7697
7918
  const matchingBet = activeBets.find((b) => {
7698
- const name = (b.name ?? "").toLowerCase();
7699
- return taskWords.some((w) => name.includes(w));
7919
+ const words = (b.name ?? "").toLowerCase().split(/\s+/);
7920
+ return taskWords.some((tw) => words.some((w) => w === tw || w.startsWith(tw + "-")));
7700
7921
  });
7701
7922
  if (matchingBet) {
7702
7923
  return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
7703
7924
  }
7704
7925
  const betHits = (taskContextHits ?? []).filter(
7705
- (e) => e.collectionSlug === "bets"
7926
+ (e) => e.collectionSlug === "chains"
7706
7927
  );
7707
7928
  if (betHits.length > 0) {
7708
7929
  return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
@@ -7711,11 +7932,11 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
7711
7932
  }
7712
7933
  function buildAlignmentCheckLines(result) {
7713
7934
  const lines = ["## Alignment Check"];
7714
- if (result.aligned && result.matchSource === "active_bet") {
7935
+ if (result.aligned && result.matchSource === "active_bet" && result.matchedBet) {
7715
7936
  lines.push(
7716
7937
  `Task aligns with active bet: **${result.matchedBet}**. Proceed.`
7717
7938
  );
7718
- } else if (result.aligned && result.matchSource === "task_context") {
7939
+ } else if (result.aligned && result.matchSource === "task_context" && result.matchedBet) {
7719
7940
  const noActiveBets = result.betNames.length === 0;
7720
7941
  lines.push(
7721
7942
  `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 +7966,15 @@ var RULE5_COMPACT = '**Validate against governance.** Before proposing or buildi
7745
7966
  var MAX_ENTRIES_NO_TASK = 3;
7746
7967
  var MAX_ENTRIES_WITH_TASK = 5;
7747
7968
  function extractKeywords(text) {
7748
- return [...new Set(text.toLowerCase().split(/\s+/).filter((w) => w.length > 2))];
7969
+ const normalized = text.toLowerCase().replace(/[^\w\s-]/g, " ");
7970
+ const tokens = [];
7971
+ for (const raw of normalized.split(/\s+/)) {
7972
+ if (!raw) continue;
7973
+ const parts = raw.split(/[-_]/);
7974
+ if (parts.length > 1) tokens.push(raw.replace(/[-_]/g, ""));
7975
+ for (const p of parts) if (p) tokens.push(p);
7976
+ }
7977
+ return [...new Set(tokens.filter((w) => w.length > 2))];
7749
7978
  }
7750
7979
  function scoreEntry(entry, keywords) {
7751
7980
  const text = `${entry.name} ${entry.description ?? ""}`.toLowerCase();
@@ -7846,73 +8075,16 @@ var interviewExtractionSchema = z14.object({
7846
8075
  keyDecisions: z14.array(z14.string()).optional().describe("Recent significant decisions made (each as a concise statement)"),
7847
8076
  tensions: z14.array(z14.string()).optional().describe("Pain points or friction the product is solving")
7848
8077
  });
7849
- function extractionToBatchEntries(extracted) {
7850
- const entries = [];
7851
- if (extracted.vision) {
7852
- entries.push({
7853
- collection: "strategy",
7854
- name: "Product Vision",
7855
- description: extracted.vision
7856
- });
7857
- }
7858
- if (extracted.audience) {
7859
- const audienceName = extracted.audience.split(/\s+/).slice(0, 6).join(" ");
7860
- entries.push({
7861
- collection: "audiences",
7862
- name: audienceName,
7863
- description: extracted.audience
7864
- });
7865
- }
7866
- if (extracted.techStack?.length) {
7867
- entries.push({
7868
- collection: "architecture",
7869
- name: "Tech Stack",
7870
- description: `Technology choices: ${extracted.techStack.join(", ")}`
7871
- });
7872
- for (const tech of extracted.techStack.slice(0, 3)) {
7873
- entries.push({ collection: "glossary", name: tech, description: `${tech} \u2014 part of the tech stack` });
7874
- }
7875
- }
7876
- if (extracted.keyTerms?.length) {
7877
- for (const term of extracted.keyTerms) {
7878
- const alreadyAdded = entries.some(
7879
- (e) => e.collection === "glossary" && e.name.toLowerCase() === term.toLowerCase()
7880
- );
7881
- if (!alreadyAdded) {
7882
- entries.push({ collection: "glossary", name: term, description: `Core domain term: ${term}` });
7883
- }
7884
- }
7885
- }
7886
- if (extracted.keyDecisions?.length) {
7887
- for (const decision of extracted.keyDecisions) {
7888
- entries.push({
7889
- collection: "decisions",
7890
- name: decision.slice(0, 80),
7891
- description: decision
7892
- });
7893
- }
7894
- }
7895
- if (extracted.tensions?.length) {
7896
- for (const tension of extracted.tensions) {
7897
- entries.push({
7898
- collection: "tensions",
7899
- name: tension.slice(0, 80),
7900
- description: tension
7901
- });
7902
- }
7903
- }
7904
- return entries;
7905
- }
7906
8078
  function getInterviewInstructions(workspaceName) {
7907
8079
  return {
7908
- systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and feed it to batch-capture. In Open governance mode, entries are committed automatically. In consensus/role mode, they stay as drafts for review.`,
7909
- question1: `**Q1 \u2014 What are you building and for whom?** Describe your product in 1\u20132 sentences and who it's for. (This becomes your Product Vision and primary Audience.)`,
7910
- question2: `**Q2 (optional)** What's one word or phrase that would trip someone up if they didn't know your context? (This becomes your first glossary term \u2014 the one that matters most.)`,
8080
+ systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and batch-capture it. Let the user talk naturally \u2014 you do the structuring. In Open governance mode, entries commit automatically. In consensus/role mode, they stay as drafts for review.`,
8081
+ question1: `**What are you building, and who is it for?** A sentence or two is plenty \u2014 I'll pull out the structure.`,
8082
+ question2: `**What's one word or phrase that would trip someone up if they didn't know your world?** (That becomes your first glossary term \u2014 the one that saves explanations later.)`,
7911
8083
  extractionGuidance: `After the user answers, extract:
7912
8084
  - vision: the core product purpose (required)
7913
8085
  - audience: who it's for (optional)
7914
8086
  - techStack: technologies mentioned (optional, array)
7915
- - keyTerms: domain terms \u2014 Q2 answer (the word/phrase that trips people up) goes here (optional, array)
8087
+ - keyTerms: domain terms \u2014 Q2 answer goes here (optional, array)
7916
8088
  - keyDecisions: any decisions stated (optional, array)
7917
8089
  - tensions: any pain points stated (optional, array)
7918
8090
 
@@ -7923,7 +8095,7 @@ Map to batch-capture entries:
7923
8095
  - keyTerms \u2192 glossary (1 per term)
7924
8096
  - keyDecisions \u2192 decisions (1 per decision)
7925
8097
  - tensions \u2192 tensions (1 per tension)`,
7926
- captureInstructions: `Call batch-capture with the extracted entries. Omit \`autoCommit\` to follow workspace governance automatically, or pass \`autoCommit: false\` if the user wants a review-first pass. Keep descriptions concise (1\u20132 sentences each). Prefer 8 good entries over 15 mediocre ones \u2014 quality over volume. If batch-capture returns failedEntries, tell the user and retry those individually.`,
8098
+ captureInstructions: `Call batch-capture with the extracted entries. Omit \`autoCommit\` to follow workspace governance automatically, or pass \`autoCommit: false\` if the user wants review-first. Keep descriptions concise (1\u20132 sentences each). Prefer 8 good entries over 15 mediocre ones \u2014 quality over volume. If batch-capture returns failedEntries, tell the user and retry individually.`,
7927
8099
  qualityNote: (
7928
8100
  // FEAT-149: Retrieval-First Proof Moment.
7929
8101
  // The aha is friction elimination ("I'll never have to explain this again"),
@@ -7943,25 +8115,73 @@ That's context you won't have to explain again."
7943
8115
 
7944
8116
  End with: "**Try it right now** \u2014 ask me something you'd normally have to re-explain first. I'll answer like I've known your product for months."`
7945
8117
  ),
7946
- scanOffer: `After the proof moment, offer the codebase scan as a way to deepen the knowledge:
7947
- "Want me to learn even more? I can scan your project files (README, package.json, source structure) and pick up technical decisions, conventions, and architecture that I missed. Takes about 2 minutes."`
8118
+ scanOffer: `After the proof moment, offer the codebase scan:
8119
+ "Want me to learn more? I can read your project files \u2014 README, package.json, source structure \u2014 and pick up technical decisions, conventions, and architecture I missed. Takes about two minutes."`
7948
8120
  };
7949
8121
  }
7950
8122
  function buildInterviewResponse(workspaceName) {
7951
8123
  const instructions = getInterviewInstructions(workspaceName);
7952
8124
  return [
7953
- "## Let's activate your workspace",
8125
+ "## Let's get to know your product",
7954
8126
  "",
7955
8127
  instructions.systemPrompt,
7956
8128
  "",
7957
- "I'll ask you 1\u20132 questions. Your answers become the foundation of your product knowledge.",
8129
+ "I'll ask you one or two questions. Your answers become the foundation of your Brain.",
7958
8130
  "",
7959
- `**${instructions.question1}**`,
8131
+ instructions.question1,
7960
8132
  "",
7961
- "_Take your time \u2014 a sentence or two is enough. I'll extract the structure from your answer._"
8133
+ "_Take your time \u2014 I'll pull out the structure from whatever you say._"
7962
8134
  ].join("\n");
7963
8135
  }
7964
8136
 
8137
+ // src/lib/gapToPrompt.ts
8138
+ function cleanLabel(label) {
8139
+ return label.replace(/ has entries$/i, "").replace(/ coverage$/i, "").replace(/^Strategy — /i, "");
8140
+ }
8141
+ function progressFrame(ctx) {
8142
+ if (ctx.passedChecks === 0) {
8143
+ return "Your Brain is just getting started.";
8144
+ }
8145
+ if (ctx.passedChecks === 1) {
8146
+ return "Your Brain has one area covered so far.";
8147
+ }
8148
+ if (ctx.score >= 70) {
8149
+ return `Your Brain covers ${ctx.passedChecks} areas \u2014 almost there.`;
8150
+ }
8151
+ return `Your Brain covers ${ctx.passedChecks} of ${ctx.totalChecks} areas.`;
8152
+ }
8153
+ function formatTopGapPrompt(gap, remaining, ctx) {
8154
+ const prompt = gap.capabilityGuidance ?? gap.guidance;
8155
+ const label = cleanLabel(gap.label);
8156
+ const lines = [
8157
+ progressFrame(ctx),
8158
+ remaining > 0 ? `The biggest gap right now: **${label}**.` : `One area would strengthen it: **${label}**.`,
8159
+ "",
8160
+ prompt
8161
+ ];
8162
+ return lines.join("\n");
8163
+ }
8164
+ function formatGapList(gaps, limit = 3) {
8165
+ const topGaps = gaps.slice(0, limit);
8166
+ if (topGaps.length === 0) return [];
8167
+ const lines = [
8168
+ "Here's where your Brain would benefit most:",
8169
+ ""
8170
+ ];
8171
+ for (let i = 0; i < topGaps.length; i++) {
8172
+ const gap = topGaps[i];
8173
+ const prompt = gap.capabilityGuidance ?? gap.guidance;
8174
+ const label = cleanLabel(gap.label);
8175
+ lines.push(`${i + 1}. **${label}** \u2014 ${prompt}`);
8176
+ }
8177
+ return lines;
8178
+ }
8179
+ function formatGapOneLiner(gap) {
8180
+ if (!gap) return null;
8181
+ const prompt = gap.capabilityGuidance ?? gap.guidance;
8182
+ return `Next gap: ${prompt}`;
8183
+ }
8184
+
7965
8185
  // src/tools/start.ts
7966
8186
  async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
7967
8187
  if (!agentSessionId) return { oriented: false, orientationStatus: "no_session" };
@@ -8000,7 +8220,7 @@ function registerStartTools(server) {
8000
8220
  try {
8001
8221
  wsCtx = await getWorkspaceContext();
8002
8222
  } catch (e) {
8003
- errors.push(`Workspace: ${e.message}`);
8223
+ errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
8004
8224
  }
8005
8225
  if (!wsCtx) {
8006
8226
  const text = "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.");
@@ -8149,7 +8369,7 @@ function buildBlankResponse(wsCtx, sessionCtx) {
8149
8369
  }
8150
8370
  async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
8151
8371
  const stage = readiness?.stage ?? "seeded";
8152
- const score = readiness?.score;
8372
+ const score = readiness?.score ?? null;
8153
8373
  const gaps = readiness?.gaps ?? [];
8154
8374
  const lines = [
8155
8375
  `# ${wsCtx.workspaceName}`,
@@ -8157,14 +8377,8 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
8157
8377
  ""
8158
8378
  ];
8159
8379
  if (gaps.length > 0) {
8160
- lines.push("Here are the most impactful gaps to fill next:");
8161
- lines.push("");
8162
- const topGaps = gaps.slice(0, 3);
8163
- for (let i = 0; i < topGaps.length; i++) {
8164
- const gap = topGaps[i];
8165
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Describe your ${gap.label.toLowerCase()}.`;
8166
- lines.push(`${i + 1}. **${gap.label}** \u2014 ${cta}`);
8167
- }
8380
+ const gapLines = formatGapList(gaps, 3);
8381
+ lines.push(...gapLines);
8168
8382
  lines.push("");
8169
8383
  lines.push("Pick any to start \u2014 or begin with **#1** and I'll guide you through it.");
8170
8384
  } else {
@@ -8279,21 +8493,13 @@ function computeWorkspaceAge(createdAt) {
8279
8493
  const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
8280
8494
  return { ageDays, isNeglected: ageDays >= 30 };
8281
8495
  }
8282
- function pickNextAction(gaps, openTensions, priorSessions) {
8283
- if (gaps.length === 0 && openTensions.length === 0) return null;
8284
- if (gaps.length > 0) {
8285
- const gap = gaps[0];
8286
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
8287
- return { action: gap.label, cta };
8288
- }
8289
- if (openTensions.length > 0) {
8290
- const t = openTensions[0];
8291
- return {
8292
- action: `Open tension: ${t.name}`,
8293
- cta: "Want to discuss this tension or capture a decision about it?"
8294
- };
8295
- }
8296
- return null;
8496
+ function pickNextTensionPrompt(openTensions) {
8497
+ if (openTensions.length === 0) return null;
8498
+ const t = openTensions[0];
8499
+ return {
8500
+ action: `Open tension: ${t.name}`,
8501
+ cta: "Want to discuss this tension or capture a decision about it?"
8502
+ };
8297
8503
  }
8298
8504
  async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8299
8505
  const wsFullCtx = await getWorkspaceContext();
@@ -8316,7 +8522,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8316
8522
  try {
8317
8523
  readiness = await mcpQuery("chain.workspaceReadiness");
8318
8524
  } catch (e) {
8319
- errors.push(`Readiness: ${e.message}`);
8525
+ errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
8320
8526
  }
8321
8527
  const lines = [];
8322
8528
  const isLowReadiness = readiness !== null && readiness.score < 50;
@@ -8336,29 +8542,42 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8336
8542
  lines.push("Let's get your workspace active.");
8337
8543
  lines.push("");
8338
8544
  }
8339
- if (isLowReadiness) {
8340
- const nextAction = pickNextAction(readiness.gaps ?? [], openTensions, priorSessions);
8341
- if (nextAction) {
8342
- lines.push("## Recommended next step");
8343
- lines.push(`**${nextAction.action}**`);
8344
- lines.push("");
8345
- lines.push(nextAction.cta);
8545
+ if (isLowReadiness && readiness) {
8546
+ const gaps = readiness.gaps ?? [];
8547
+ if (gaps.length > 0) {
8548
+ const gapCtx = {
8549
+ stage: stage ?? "seeded",
8550
+ passedChecks: readiness.passedChecks ?? 0,
8551
+ totalChecks: readiness.totalChecks ?? 0,
8552
+ score: readiness.score ?? 0
8553
+ };
8554
+ lines.push(formatTopGapPrompt(gaps[0], gaps.length - 1, gapCtx));
8346
8555
  lines.push("");
8347
8556
  lines.push(captureBehaviorNote);
8348
8557
  lines.push("");
8349
- const remainingGaps = (readiness.gaps ?? []).length - 1;
8558
+ const remainingGaps = gaps.length - 1;
8350
8559
  if (remainingGaps > 0 || openTensions.length > 0) {
8351
- lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more area${remainingGaps === 1 ? "" : "s"} to cover` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask for full status to see everything._`);
8560
+ const parts = [];
8561
+ if (remainingGaps > 0) parts.push(`${remainingGaps} more area${remainingGaps === 1 ? "" : "s"} to cover`);
8562
+ if (openTensions.length > 0) parts.push(`${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}`);
8563
+ lines.push(`_${parts.join(" and ")} \u2014 ask for full status to see everything._`);
8564
+ lines.push("");
8565
+ }
8566
+ } else {
8567
+ const tensionPrompt = pickNextTensionPrompt(openTensions);
8568
+ if (tensionPrompt) {
8569
+ lines.push(`**${tensionPrompt.action}**`);
8570
+ lines.push(tensionPrompt.cta);
8352
8571
  lines.push("");
8353
8572
  }
8354
8573
  }
8355
- lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)? Just ask \u2014 I'll set it up._");
8574
+ lines.push("_Need a collection that doesn't exist yet? Just ask \u2014 I'll set it up._");
8356
8575
  lines.push("");
8357
8576
  } else if (isHighReadiness) {
8358
8577
  let activeBets = [];
8359
8578
  try {
8360
- const betEntries = await mcpQuery("chain.listEntries", { collectionSlug: "bets" });
8361
- activeBets = (betEntries ?? []).filter((e) => e.status === "active" && e.data?.horizon === "now").slice(0, 8);
8579
+ const chainEntries = await mcpQuery("chain.listEntries", { collectionSlug: "chains" });
8580
+ activeBets = (chainEntries ?? []).filter((e) => e.data?.chainTypeId === "bet" && e.status === "active" && e.data?.horizon === "now").slice(0, 8);
8362
8581
  } catch {
8363
8582
  }
8364
8583
  if (task) {
@@ -10153,90 +10372,17 @@ function formatScanReport(result) {
10153
10372
  return lines.join("\n");
10154
10373
  }
10155
10374
 
10156
- // src/tools/health.ts
10157
- var CALL_CATEGORIES = {
10158
- "chain.getEntry": "read",
10159
- "chain.batchGetEntries": "read",
10160
- "chain.listEntries": "read",
10161
- "chain.listEntryHistory": "read",
10162
- "chain.listEntryRelations": "read",
10163
- "chain.listEntriesByLabel": "read",
10164
- "chain.searchEntries": "search",
10165
- "chain.createEntry": "write",
10166
- "chain.updateEntry": "write",
10167
- "chain.moveToCollection": "write",
10168
- "chain.createEntryRelation": "write",
10169
- "chain.applyLabel": "label",
10170
- "chain.removeLabel": "label",
10171
- "chain.createLabel": "label",
10172
- "chain.updateLabel": "label",
10173
- "chain.deleteLabel": "label",
10174
- "chain.createCollection": "write",
10175
- "chain.updateCollection": "write",
10176
- "chain.listCollections": "meta",
10177
- "chain.getCollection": "meta",
10178
- "chain.listLabels": "meta",
10179
- "resolveWorkspace": "meta"
10180
- };
10181
- function categorize(fn) {
10182
- return CALL_CATEGORIES[fn] ?? "meta";
10183
- }
10184
- function formatDuration(ms) {
10185
- if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
10186
- const mins = Math.floor(ms / 6e4);
10187
- const secs = Math.round(ms % 6e4 / 1e3);
10188
- return `${mins}m ${secs}s`;
10189
- }
10190
- function buildSessionSummary(log) {
10191
- if (log.length === 0) return "";
10192
- const byCategory = /* @__PURE__ */ new Map();
10193
- let errorCount = 0;
10194
- let writeCreates = 0;
10195
- let writeUpdates = 0;
10196
- for (const entry of log) {
10197
- const cat = categorize(entry.fn);
10198
- if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
10199
- const fnCounts = byCategory.get(cat);
10200
- fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
10201
- if (entry.status === "error") errorCount++;
10202
- if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
10203
- if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
10204
- }
10205
- const firstTs = new Date(log[0].ts).getTime();
10206
- const lastTs = new Date(log[log.length - 1].ts).getTime();
10207
- const duration = formatDuration(lastTs - firstTs);
10208
- const lines = [`# Session Summary (${duration})
10209
- `];
10210
- const categoryLabels = [
10211
- ["read", "Reads"],
10212
- ["search", "Searches"],
10213
- ["write", "Writes"],
10214
- ["label", "Labels"],
10215
- ["meta", "Meta"]
10216
- ];
10217
- for (const [cat, label] of categoryLabels) {
10218
- const fnCounts = byCategory.get(cat);
10219
- if (!fnCounts || fnCounts.size === 0) continue;
10220
- const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
10221
- const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
10222
- lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
10223
- }
10224
- lines.push(`- **Errors:** ${errorCount}`);
10225
- if (writeCreates > 0 || writeUpdates > 0) {
10226
- lines.push("");
10227
- lines.push("## Knowledge Contribution");
10228
- if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
10229
- if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
10230
- }
10231
- return lines.join("\n");
10232
- }
10375
+ // src/tools/orient.ts
10376
+ import { z as z20 } from "zod";
10233
10377
  function extractSessionEntryIds(priorSessions) {
10234
10378
  const allSeen = /* @__PURE__ */ new Set();
10235
10379
  const all = [];
10236
10380
  let lastSessionOnly = [];
10237
10381
  for (let i = 0; i < priorSessions.length; i++) {
10238
10382
  const s = priorSessions[i];
10239
- const ids = [...s.entriesCreated ?? [], ...s.entriesModified ?? []].filter((id) => id);
10383
+ const created = Array.isArray(s.entriesCreated) ? s.entriesCreated : [];
10384
+ const modified = Array.isArray(s.entriesModified) ? s.entriesModified : [];
10385
+ const ids = [...created, ...modified].filter((id) => typeof id === "string" && id.length > 0);
10240
10386
  if (i === 0) {
10241
10387
  lastSessionOnly = [...new Set(ids)].slice(0, 5);
10242
10388
  }
@@ -10251,426 +10397,11 @@ function extractSessionEntryIds(priorSessions) {
10251
10397
  }
10252
10398
  return { all, lastSessionOnly };
10253
10399
  }
10254
- function computeOrganisationHealth(entries) {
10255
- let agreements = 0;
10256
- let disagreements = 0;
10257
- let abstentions = 0;
10258
- const flagMap = /* @__PURE__ */ new Map();
10259
- for (const entry of entries) {
10260
- const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
10261
- const description = typeof entry.data?.description === "string" ? entry.data.description : "";
10262
- const result = classifyCollection(entry.name, description);
10263
- if (!result) {
10264
- abstentions++;
10265
- continue;
10266
- }
10267
- if (result.collection === slug) {
10268
- agreements++;
10269
- } else {
10270
- disagreements++;
10271
- if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
10272
- const suggestions = flagMap.get(slug);
10273
- suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
10274
- }
10275
- }
10276
- const opinionated = agreements + disagreements;
10277
- const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
10278
- const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
10279
- const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
10280
- const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
10281
- return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
10282
- }).sort((a, b) => b.count - a.count).slice(0, 3);
10283
- return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
10284
- }
10285
- function formatOrgHealthLines(orgHealth, maxFlags = 3) {
10286
- const lines = [];
10287
- if (orgHealth.disagreements > 0) {
10288
- lines.push(
10289
- `${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
10290
- );
10291
- for (const flag of orgHealth.flags.slice(0, maxFlags)) {
10292
- lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
10293
- }
10294
- } else if (orgHealth.reviewed > 0) {
10295
- lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
10296
- }
10297
- return lines;
10298
- }
10299
- async function fetchOrganisationHealth() {
10300
- try {
10301
- const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
10302
- if (!allEntries || allEntries.length === 0) return null;
10303
- return computeOrganisationHealth(allEntries);
10304
- } catch (err) {
10305
- process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
10306
- `);
10307
- return null;
10308
- }
10309
- }
10310
- async function handleHealthCheck() {
10311
- const start = Date.now();
10312
- const errors = [];
10313
- let workspaceId;
10314
- try {
10315
- workspaceId = await getWorkspaceId();
10316
- } catch (e) {
10317
- errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
10318
- }
10319
- let collections = [];
10320
- try {
10321
- collections = await mcpQuery("chain.listCollections");
10322
- } catch (e) {
10323
- errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
10324
- }
10325
- let totalEntries = 0;
10326
- if (collections.length > 0) {
10327
- try {
10328
- const entries = await mcpQuery("chain.listEntries", {});
10329
- totalEntries = entries.length;
10330
- } catch (e) {
10331
- errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
10332
- }
10333
- }
10334
- let wsCtx = null;
10335
- try {
10336
- wsCtx = await getWorkspaceContext();
10337
- } catch {
10338
- }
10339
- const durationMs = Date.now() - start;
10340
- const healthy = errors.length === 0;
10341
- const lines = [
10342
- `# ${healthy ? "Healthy" : "Degraded"}`,
10343
- "",
10344
- `**Workspace:** ${workspaceId ?? "unresolved"}`,
10345
- `**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
10346
- `**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
10347
- `**Collections:** ${collections.length}`,
10348
- `**Entries:** ${totalEntries}`,
10349
- `**Latency:** ${durationMs}ms`
10350
- ];
10351
- if (errors.length > 0) {
10352
- lines.push("", "## Errors");
10353
- for (const err of errors) {
10354
- lines.push(`- ${err}`);
10355
- }
10356
- }
10357
- const healthData = {
10358
- healthy,
10359
- collections: collections.length,
10360
- entries: totalEntries,
10361
- latencyMs: durationMs,
10362
- workspace: workspaceId ?? "unresolved"
10363
- };
10364
- return {
10365
- content: [{ type: "text", text: lines.join("\n") }],
10366
- structuredContent: success(
10367
- healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
10368
- healthData
10369
- )
10370
- };
10371
- }
10372
- async function handleWhoami() {
10373
- const ctx = await getWorkspaceContext();
10374
- const sessionId = getAgentSessionId();
10375
- const scope = getApiKeyScope();
10376
- const oriented = isSessionOriented();
10377
- const lines = [
10378
- `# Session Identity`,
10379
- "",
10380
- `**Workspace ID:** ${ctx.workspaceId}`,
10381
- `**Workspace Slug:** ${ctx.workspaceSlug}`,
10382
- `**Workspace Name:** ${ctx.workspaceName}`
10383
- ];
10384
- return {
10385
- content: [{ type: "text", text: lines.join("\n") }],
10386
- structuredContent: success(
10387
- `Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
10388
- { workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
10389
- )
10390
- };
10391
- }
10392
- var STAGE_LABELS = {
10393
- blank: "Blank",
10394
- seeded: "Seeded",
10395
- grounded: "Grounded",
10396
- connected: "Connected"
10397
- };
10398
- var STAGE_DESCRIPTIONS = {
10399
- blank: "No knowledge captured yet.",
10400
- seeded: "Early knowledge is in place \u2014 keep building.",
10401
- grounded: "Solid foundations \u2014 a few gaps remain.",
10402
- connected: "Well-connected knowledge graph \u2014 your Brain is useful."
10403
- };
10404
- async function handleWorkspaceStatus() {
10405
- const result = await mcpQuery("chain.workspaceReadiness");
10406
- const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
10407
- const scoringVersion = result.scoringVersion ?? "v1";
10408
- const stage = result.stage ?? "seeded";
10409
- const stageLabel = STAGE_LABELS[stage] ?? stage;
10410
- const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
10411
- const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
10412
- const lines = [
10413
- `# Brain Status: ${stageLabel}`,
10414
- `_${stageDescription}_`,
10415
- "",
10416
- `${scoreBar} ${stageLabel} \xB7 ${score}%`,
10417
- `**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
10418
- "",
10419
- "## Stats",
10420
- `- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
10421
- `- **Relations:** ${stats.totalRelations}`,
10422
- `- **Collections:** ${stats.collectionCount}`,
10423
- `- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
10424
- ""
10425
- ];
10426
- if (gaps.length > 0) {
10427
- lines.push("## Gaps");
10428
- for (const gap of gaps) {
10429
- const action = gap.capabilityGuidance ?? gap.guidance;
10430
- lines.push(`- [ ] **${gap.label}**`);
10431
- lines.push(` _${action}_`);
10432
- }
10433
- lines.push("");
10434
- }
10435
- const passed = checks.filter((c) => c.passed);
10436
- if (passed.length > 0) {
10437
- lines.push("## Passing checks");
10438
- for (const check of passed) {
10439
- lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
10440
- }
10441
- lines.push("");
10442
- }
10443
- const orgHealth = await fetchOrganisationHealth();
10444
- if (orgHealth && orgHealth.reviewed > 0) {
10445
- lines.push("## Organisation Health");
10446
- lines.push(...formatOrgHealthLines(orgHealth));
10447
- lines.push("");
10448
- }
10449
- const statusData = {
10450
- stage,
10451
- scoringVersion,
10452
- readinessScore: score,
10453
- activeEntries: stats.activeCount,
10454
- totalRelations: stats.totalRelations,
10455
- orphanedEntries: stats.orphanedCount,
10456
- gaps: gaps.map((g) => ({
10457
- id: g.id,
10458
- label: g.label,
10459
- guidance: g.capabilityGuidance ?? g.guidance
10460
- })),
10461
- ...orgHealth && { organisationHealth: orgHealth }
10462
- };
10463
- return {
10464
- content: [{ type: "text", text: lines.join("\n") }],
10465
- structuredContent: success(
10466
- `Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
10467
- statusData
10468
- )
10469
- };
10470
- }
10471
- async function handleAudit(limit) {
10472
- const log = getAuditLog();
10473
- const recent = log.slice(-limit);
10474
- if (recent.length === 0) {
10475
- return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
10476
- }
10477
- const summary = buildSessionSummary(log);
10478
- const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
10479
- `];
10480
- for (const entry of recent) {
10481
- const icon = entry.status === "ok" ? "\u2713" : "\u2717";
10482
- const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
10483
- const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
10484
- logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
10485
- }
10486
- const auditData = {
10487
- totalCalls: log.length,
10488
- calls: recent.map((entry) => ({
10489
- tool: entry.fn,
10490
- ...entry.toolContext?.action && { action: entry.toolContext.action },
10491
- timestamp: entry.ts,
10492
- ...entry.durationMs != null && { durationMs: entry.durationMs }
10493
- }))
10494
- };
10495
- return {
10496
- content: [{ type: "text", text: `${summary}
10497
-
10498
- ---
10499
-
10500
- ${logLines.join("\n")}` }],
10501
- structuredContent: success(
10502
- `Audit: ${log.length} total calls, showing last ${recent.length}.`,
10503
- auditData
10504
- )
10505
- };
10506
- }
10507
- var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
10508
- var healthSchema = z20.object({
10509
- action: z20.enum(HEALTH_ACTIONS).describe(
10510
- "'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
10511
- ),
10512
- limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
10513
- });
10514
10400
  var orientSchema = z20.object({
10515
10401
  mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
10516
10402
  task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
10517
10403
  });
10518
- var healthCheckOutputSchema = z20.object({
10519
- healthy: z20.boolean(),
10520
- collections: z20.number(),
10521
- entries: z20.number(),
10522
- latencyMs: z20.number(),
10523
- workspace: z20.string()
10524
- });
10525
- var organisationHealthSchema = z20.object({
10526
- reviewed: z20.number(),
10527
- agreements: z20.number(),
10528
- disagreements: z20.number(),
10529
- abstentions: z20.number(),
10530
- agreementRate: z20.number(),
10531
- flags: z20.array(z20.object({
10532
- collection: z20.string(),
10533
- count: z20.number(),
10534
- suggestedCollection: z20.string()
10535
- }))
10536
- });
10537
- var healthStatusOutputSchema = z20.object({
10538
- stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
10539
- scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
10540
- readinessScore: z20.number(),
10541
- activeEntries: z20.number(),
10542
- totalRelations: z20.number(),
10543
- orphanedEntries: z20.number(),
10544
- gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() })),
10545
- organisationHealth: organisationHealthSchema.optional()
10546
- });
10547
- var healthAuditOutputSchema = z20.object({
10548
- totalCalls: z20.number(),
10549
- calls: z20.array(z20.object({
10550
- tool: z20.string(),
10551
- action: z20.string().optional(),
10552
- timestamp: z20.string(),
10553
- durationMs: z20.number().optional()
10554
- }))
10555
- });
10556
- var healthWhoamiOutputSchema = z20.object({
10557
- workspaceId: z20.string(),
10558
- workspaceName: z20.string(),
10559
- scope: z20.string(),
10560
- sessionId: z20.union([z20.string(), z20.null()]),
10561
- oriented: z20.boolean()
10562
- });
10563
- var ALL_TOOL_SCHEMAS = [
10564
- { name: "entries", schema: entriesSchema },
10565
- { name: "relations", schema: relationsSchema },
10566
- { name: "graph", schema: graphSchema },
10567
- { name: "context", schema: contextSchema },
10568
- { name: "collections", schema: collectionsSchema },
10569
- { name: "session", schema: sessionSchema },
10570
- { name: "health", schema: healthSchema },
10571
- { name: "orient", schema: orientSchema },
10572
- { name: "quality", schema: qualitySchema },
10573
- { name: "workflows", schema: workflowsSchema },
10574
- { name: "session-wrapup", schema: wrapupSchema },
10575
- { name: "labels", schema: labelsSchema },
10576
- { name: "verify", schema: verifySchema },
10577
- { name: "capture", schema: captureSchema },
10578
- { name: "batch-capture", schema: batchCaptureSchema },
10579
- { name: "update-entry", schema: updateEntrySchema },
10580
- { name: "get-history", schema: getHistorySchema },
10581
- { name: "commit-entry", schema: commitEntrySchema },
10582
- { name: "start", schema: startSchema },
10583
- { name: "get-usage-summary", schema: usageSummarySchema },
10584
- { name: "chain", schema: chainSchema },
10585
- { name: "chain-version", schema: chainVersionSchema },
10586
- { name: "chain-branch", schema: chainBranchSchema },
10587
- { name: "chain-review", schema: chainReviewSchema },
10588
- { name: "create-audience-map-set", schema: createAudienceMapSetSchema },
10589
- { name: "map", schema: mapSchema },
10590
- { name: "map-slot", schema: mapSlotSchema },
10591
- { name: "map-version", schema: mapVersionSchema },
10592
- { name: "map-suggest", schema: mapSuggestSchema },
10593
- { name: "architecture", schema: architectureSchema },
10594
- { name: "architecture-admin", schema: architectureAdminSchema },
10595
- { name: "facilitate", schema: facilitateSchema }
10596
- ];
10597
- var selfTestOutputSchema = z20.object({
10598
- passed: z20.number(),
10599
- failed: z20.number(),
10600
- total: z20.number(),
10601
- results: z20.array(z20.object({
10602
- tool: z20.string(),
10603
- valid: z20.boolean(),
10604
- error: z20.string().optional()
10605
- }))
10606
- });
10607
- function handleSelfTest() {
10608
- const results = [];
10609
- for (const { name, schema } of ALL_TOOL_SCHEMAS) {
10610
- try {
10611
- if (!schema || typeof schema.safeParse !== "function") {
10612
- results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
10613
- continue;
10614
- }
10615
- const test = schema.safeParse({});
10616
- if (test.success || test.error) {
10617
- results.push({ tool: name, valid: true });
10618
- }
10619
- } catch (e) {
10620
- results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
10621
- }
10622
- }
10623
- const passed = results.filter((r) => r.valid).length;
10624
- const failed = results.filter((r) => !r.valid).length;
10625
- const total = results.length;
10626
- const lines = [
10627
- `# Self-Test: Tool Schema Validation`,
10628
- `**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
10629
- `**Schemas validated:** ${passed}/${total}`,
10630
- ""
10631
- ];
10632
- if (failed > 0) {
10633
- lines.push("## Failures");
10634
- for (const r of results.filter((r2) => !r2.valid)) {
10635
- lines.push(`- **${r.tool}**: ${r.error}`);
10636
- }
10637
- lines.push("");
10638
- }
10639
- lines.push("## All Tools");
10640
- for (const r of results) {
10641
- lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
10642
- }
10643
- return {
10644
- content: [{ type: "text", text: lines.join("\n") }],
10645
- structuredContent: success(
10646
- failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
10647
- { passed, failed, total, results }
10648
- )
10649
- };
10650
- }
10651
- function registerHealthTools(server) {
10652
- server.registerTool(
10653
- "health",
10654
- {
10655
- title: "Health",
10656
- description: "Diagnostics and session identity. Four actions:\n\n- **check**: Verify connectivity \u2014 workspace, collections, entries, latency.\n- **whoami**: Session identity \u2014 workspace ID, slug, name.\n- **status**: Workspace readiness \u2014 score, gaps, stats (entries, relations, orphans).\n- **audit**: Session audit log \u2014 last N backend calls with summary.",
10657
- inputSchema: healthSchema,
10658
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
10659
- },
10660
- withEnvelope(async (args) => {
10661
- const parsed = parseOrFail(healthSchema, args);
10662
- if (!parsed.ok) return parsed.result;
10663
- const { action, limit } = parsed.data;
10664
- return runWithToolContext({ tool: "health", action }, async () => {
10665
- if (action === "check") return handleHealthCheck();
10666
- if (action === "whoami") return handleWhoami();
10667
- if (action === "status") return handleWorkspaceStatus();
10668
- if (action === "audit") return handleAudit(limit ?? 20);
10669
- if (action === "self-test") return handleSelfTest();
10670
- return unknownAction(action, HEALTH_ACTIONS);
10671
- });
10672
- })
10673
- );
10404
+ function registerOrientTool(server) {
10674
10405
  server.registerTool(
10675
10406
  "orient",
10676
10407
  {
@@ -10793,6 +10524,11 @@ function registerHealthTools(server) {
10793
10524
  lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
10794
10525
  }
10795
10526
  }
10527
+ const briefGaps = readiness?.gaps ?? [];
10528
+ if (briefGaps.length > 0) {
10529
+ const oneLiner = formatGapOneLiner(briefGaps[0]);
10530
+ if (oneLiner) lines.push(oneLiner);
10531
+ }
10796
10532
  if (recoveryBlock) {
10797
10533
  lines.push("");
10798
10534
  lines.push(...formatRecoveryBlock(recoveryBlock));
@@ -10863,29 +10599,31 @@ function registerHealthTools(server) {
10863
10599
  lines.push("");
10864
10600
  }
10865
10601
  }
10602
+ let fullCoherenceSnapshot;
10866
10603
  if (isLowReadiness) {
10867
- lines.push(`**Brain stage: ${orientStage}.**`);
10868
- lines.push("");
10869
- const captureBehaviorNote = (readiness?.governanceMode ?? wsCtx?.governanceMode ?? "open") === "open" ? "_In Open mode, user-authored captures commit immediately unless you ask me to keep them as drafts._" : '_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._';
10604
+ const captureBehaviorNote = (readiness?.governanceMode ?? wsCtx?.governanceMode ?? "open") === "open" ? "_In Open mode, captures commit automatically unless you ask me to keep them as drafts._" : "_Everything stays as a draft until you confirm._";
10870
10605
  const gaps = readiness.gaps ?? [];
10871
10606
  if (gaps.length > 0) {
10872
- const gap = gaps[0];
10873
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
10874
- lines.push("## Recommended next step");
10875
- lines.push(`**${gap.label}**`);
10876
- lines.push("");
10877
- lines.push(cta);
10607
+ const gapCtx = {
10608
+ stage: orientStage,
10609
+ passedChecks: readiness.passedChecks ?? 0,
10610
+ totalChecks: readiness.totalChecks ?? 0,
10611
+ score: readiness.score ?? 0
10612
+ };
10613
+ lines.push(formatTopGapPrompt(gaps[0], gaps.length - 1, gapCtx));
10878
10614
  lines.push("");
10879
10615
  lines.push(captureBehaviorNote);
10880
10616
  lines.push("");
10881
10617
  const remainingGaps = gaps.length - 1;
10882
10618
  if (remainingGaps > 0 || openTensions.length > 0) {
10883
- lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
10619
+ const parts = [];
10620
+ if (remainingGaps > 0) parts.push(`${remainingGaps} more area${remainingGaps === 1 ? "" : "s"} to cover`);
10621
+ if (openTensions.length > 0) parts.push(`${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}`);
10622
+ lines.push(`_${parts.join(" and ")} \u2014 ask for full status to see everything._`);
10884
10623
  lines.push("");
10885
10624
  }
10886
10625
  }
10887
- lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)?_");
10888
- lines.push("_Use `collections action=create` to add it, or ask me to propose collections for your domain._");
10626
+ lines.push("_Need a collection that doesn't exist yet? Just ask \u2014 I'll set it up._");
10889
10627
  lines.push("");
10890
10628
  } else if (readiness) {
10891
10629
  const fmt = (e) => {
@@ -10893,10 +10631,9 @@ function registerHealthTools(server) {
10893
10631
  const stratum = e.stratum ?? "?";
10894
10632
  return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
10895
10633
  };
10896
- let fullCoherenceSnapshot2;
10897
10634
  const fullCoherence = buildCoherenceSection();
10898
10635
  if (fullCoherence) {
10899
- fullCoherenceSnapshot2 = fullCoherence.snapshot;
10636
+ fullCoherenceSnapshot = fullCoherence.snapshot;
10900
10637
  }
10901
10638
  if (task) {
10902
10639
  lines.push(`**Brain stage: ${orientStage}.** Working on: **${task}**`);
@@ -10984,6 +10721,13 @@ function registerHealthTools(server) {
10984
10721
  orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
10985
10722
  lines.push("");
10986
10723
  }
10724
+ if (orientEntries.strategyHighlights?.length > 0) {
10725
+ lines.push("## Strategy highlights");
10726
+ lines.push("_One-sentence strategy, positioning, moat, business model, GTM \u2014 high-level strategic context._");
10727
+ lines.push("");
10728
+ orientEntries.strategyHighlights.forEach((e) => lines.push(fmt(e)));
10729
+ lines.push("");
10730
+ }
10987
10731
  if (orientEntries.recentDecisions?.length > 0) {
10988
10732
  lines.push("## Recent decisions");
10989
10733
  orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
@@ -11063,40 +10807,535 @@ function registerHealthTools(server) {
11063
10807
  for (const err of errors) lines.push(`- ${err}`);
11064
10808
  lines.push("");
11065
10809
  }
11066
- if (agentSessionId) {
11067
- try {
11068
- await mcpCall("agent.markOriented", {
11069
- sessionId: agentSessionId,
11070
- ...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
11071
- });
11072
- setSessionOriented(true);
11073
- lines.push("---");
11074
- lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
11075
- } catch {
11076
- lines.push("---");
11077
- lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
11078
- }
11079
- try {
11080
- await mcpMutation("chain.recordSessionSignal", {
11081
- sessionId: agentSessionId,
11082
- signalType: "immediate_context_load",
11083
- metadata: { source: "orient" }
11084
- });
11085
- } catch (err) {
11086
- process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
11087
- `);
11088
- }
11089
- } else {
11090
- lines.push("---");
11091
- lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
10810
+ if (agentSessionId) {
10811
+ try {
10812
+ await mcpCall("agent.markOriented", {
10813
+ sessionId: agentSessionId,
10814
+ ...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
10815
+ });
10816
+ setSessionOriented(true);
10817
+ lines.push("---");
10818
+ lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
10819
+ } catch {
10820
+ lines.push("---");
10821
+ lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
10822
+ }
10823
+ try {
10824
+ await mcpMutation("chain.recordSessionSignal", {
10825
+ sessionId: agentSessionId,
10826
+ signalType: "immediate_context_load",
10827
+ metadata: { source: "orient" }
10828
+ });
10829
+ } catch (err) {
10830
+ process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
10831
+ `);
10832
+ }
10833
+ } else {
10834
+ lines.push("---");
10835
+ lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
10836
+ }
10837
+ return {
10838
+ content: [{ type: "text", text: lines.join("\n") }],
10839
+ structuredContent: success(
10840
+ `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
10841
+ { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
10842
+ )
10843
+ };
10844
+ })
10845
+ );
10846
+ }
10847
+
10848
+ // src/tools/health.ts
10849
+ var CALL_CATEGORIES = {
10850
+ "chain.getEntry": "read",
10851
+ "chain.batchGetEntries": "read",
10852
+ "chain.listEntries": "read",
10853
+ "chain.listEntryHistory": "read",
10854
+ "chain.listEntryRelations": "read",
10855
+ "chain.listEntriesByLabel": "read",
10856
+ "chain.searchEntries": "search",
10857
+ "chain.createEntry": "write",
10858
+ "chain.updateEntry": "write",
10859
+ "chain.moveToCollection": "write",
10860
+ "chain.createEntryRelation": "write",
10861
+ "chain.applyLabel": "label",
10862
+ "chain.removeLabel": "label",
10863
+ "chain.createLabel": "label",
10864
+ "chain.updateLabel": "label",
10865
+ "chain.deleteLabel": "label",
10866
+ "chain.createCollection": "write",
10867
+ "chain.updateCollection": "write",
10868
+ "chain.listCollections": "meta",
10869
+ "chain.getCollection": "meta",
10870
+ "chain.listLabels": "meta",
10871
+ "resolveWorkspace": "meta"
10872
+ };
10873
+ function categorize(fn) {
10874
+ return CALL_CATEGORIES[fn] ?? "meta";
10875
+ }
10876
+ function formatDuration(ms) {
10877
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
10878
+ const mins = Math.floor(ms / 6e4);
10879
+ const secs = Math.round(ms % 6e4 / 1e3);
10880
+ return `${mins}m ${secs}s`;
10881
+ }
10882
+ function buildSessionSummary(log) {
10883
+ if (log.length === 0) return "";
10884
+ const byCategory = /* @__PURE__ */ new Map();
10885
+ let errorCount = 0;
10886
+ let writeCreates = 0;
10887
+ let writeUpdates = 0;
10888
+ for (const entry of log) {
10889
+ const cat = categorize(entry.fn);
10890
+ if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
10891
+ const fnCounts = byCategory.get(cat);
10892
+ fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
10893
+ if (entry.status === "error") errorCount++;
10894
+ if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
10895
+ if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
10896
+ }
10897
+ const firstTs = new Date(log[0].ts).getTime();
10898
+ const lastTs = new Date(log[log.length - 1].ts).getTime();
10899
+ const duration = formatDuration(lastTs - firstTs);
10900
+ const lines = [`# Session Summary (${duration})
10901
+ `];
10902
+ const categoryLabels = [
10903
+ ["read", "Reads"],
10904
+ ["search", "Searches"],
10905
+ ["write", "Writes"],
10906
+ ["label", "Labels"],
10907
+ ["meta", "Meta"]
10908
+ ];
10909
+ for (const [cat, label] of categoryLabels) {
10910
+ const fnCounts = byCategory.get(cat);
10911
+ if (!fnCounts || fnCounts.size === 0) continue;
10912
+ const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
10913
+ const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
10914
+ lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
10915
+ }
10916
+ lines.push(`- **Errors:** ${errorCount}`);
10917
+ if (writeCreates > 0 || writeUpdates > 0) {
10918
+ lines.push("");
10919
+ lines.push("## Knowledge Contribution");
10920
+ if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
10921
+ if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
10922
+ }
10923
+ return lines.join("\n");
10924
+ }
10925
+ function computeOrganisationHealth(entries) {
10926
+ let agreements = 0;
10927
+ let disagreements = 0;
10928
+ let abstentions = 0;
10929
+ const flagMap = /* @__PURE__ */ new Map();
10930
+ for (const entry of entries) {
10931
+ const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
10932
+ const description = typeof entry.data?.description === "string" ? entry.data.description : "";
10933
+ const result = classifyCollection(entry.name, description);
10934
+ if (!result) {
10935
+ abstentions++;
10936
+ continue;
10937
+ }
10938
+ if (result.collection === slug) {
10939
+ agreements++;
10940
+ } else {
10941
+ disagreements++;
10942
+ if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
10943
+ const suggestions = flagMap.get(slug);
10944
+ suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
10945
+ }
10946
+ }
10947
+ const opinionated = agreements + disagreements;
10948
+ const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
10949
+ const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
10950
+ const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
10951
+ const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
10952
+ return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
10953
+ }).sort((a, b) => b.count - a.count).slice(0, 3);
10954
+ return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
10955
+ }
10956
+ function formatOrgHealthLines(orgHealth, maxFlags = 3) {
10957
+ const lines = [];
10958
+ if (orgHealth.disagreements > 0) {
10959
+ lines.push(
10960
+ `${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
10961
+ );
10962
+ for (const flag of orgHealth.flags.slice(0, maxFlags)) {
10963
+ lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
10964
+ }
10965
+ } else if (orgHealth.reviewed > 0) {
10966
+ lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
10967
+ }
10968
+ return lines;
10969
+ }
10970
+ async function fetchOrganisationHealth() {
10971
+ try {
10972
+ const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
10973
+ if (!allEntries || allEntries.length === 0) return null;
10974
+ return computeOrganisationHealth(allEntries);
10975
+ } catch (err) {
10976
+ process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
10977
+ `);
10978
+ return null;
10979
+ }
10980
+ }
10981
+ async function handleHealthCheck() {
10982
+ const start = Date.now();
10983
+ const errors = [];
10984
+ let workspaceId;
10985
+ try {
10986
+ workspaceId = await getWorkspaceId();
10987
+ } catch (e) {
10988
+ errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
10989
+ }
10990
+ let collections = [];
10991
+ try {
10992
+ collections = await mcpQuery("chain.listCollections");
10993
+ } catch (e) {
10994
+ errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
10995
+ }
10996
+ let totalEntries = 0;
10997
+ if (collections.length > 0) {
10998
+ try {
10999
+ const entries = await mcpQuery("chain.listEntries", {});
11000
+ totalEntries = entries.length;
11001
+ } catch (e) {
11002
+ errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
11003
+ }
11004
+ }
11005
+ let wsCtx = null;
11006
+ try {
11007
+ wsCtx = await getWorkspaceContext();
11008
+ } catch {
11009
+ }
11010
+ const durationMs = Date.now() - start;
11011
+ const healthy = errors.length === 0;
11012
+ const lines = [
11013
+ `# ${healthy ? "Healthy" : "Degraded"}`,
11014
+ "",
11015
+ `**Workspace:** ${workspaceId ?? "unresolved"}`,
11016
+ `**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
11017
+ `**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
11018
+ `**Collections:** ${collections.length}`,
11019
+ `**Entries:** ${totalEntries}`,
11020
+ `**Latency:** ${durationMs}ms`
11021
+ ];
11022
+ if (errors.length > 0) {
11023
+ lines.push("", "## Errors");
11024
+ for (const err of errors) {
11025
+ lines.push(`- ${err}`);
11026
+ }
11027
+ }
11028
+ const healthData = {
11029
+ healthy,
11030
+ collections: collections.length,
11031
+ entries: totalEntries,
11032
+ latencyMs: durationMs,
11033
+ workspace: workspaceId ?? "unresolved"
11034
+ };
11035
+ return {
11036
+ content: [{ type: "text", text: lines.join("\n") }],
11037
+ structuredContent: success(
11038
+ healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
11039
+ healthData
11040
+ )
11041
+ };
11042
+ }
11043
+ async function handleWhoami() {
11044
+ const ctx = await getWorkspaceContext();
11045
+ const sessionId = getAgentSessionId();
11046
+ const scope = getApiKeyScope();
11047
+ const oriented = isSessionOriented();
11048
+ const lines = [
11049
+ `# Session Identity`,
11050
+ "",
11051
+ `**Workspace ID:** ${ctx.workspaceId}`,
11052
+ `**Workspace Slug:** ${ctx.workspaceSlug}`,
11053
+ `**Workspace Name:** ${ctx.workspaceName}`
11054
+ ];
11055
+ return {
11056
+ content: [{ type: "text", text: lines.join("\n") }],
11057
+ structuredContent: success(
11058
+ `Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
11059
+ { workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
11060
+ )
11061
+ };
11062
+ }
11063
+ var STAGE_LABELS = {
11064
+ blank: "Blank",
11065
+ seeded: "Seeded",
11066
+ grounded: "Grounded",
11067
+ connected: "Connected"
11068
+ };
11069
+ var STAGE_DESCRIPTIONS = {
11070
+ blank: "No knowledge captured yet.",
11071
+ seeded: "Early knowledge is in place \u2014 keep building.",
11072
+ grounded: "Solid foundations \u2014 a few gaps remain.",
11073
+ connected: "Well-connected knowledge graph \u2014 your Brain is useful."
11074
+ };
11075
+ async function handleWorkspaceStatus() {
11076
+ const result = await mcpQuery("chain.workspaceReadiness");
11077
+ const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
11078
+ const scoringVersion = result.scoringVersion ?? "v1";
11079
+ const stage = result.stage ?? "seeded";
11080
+ const stageLabel = STAGE_LABELS[stage] ?? stage;
11081
+ const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
11082
+ const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
11083
+ const lines = [
11084
+ `# Brain Status: ${stageLabel}`,
11085
+ `_${stageDescription}_`,
11086
+ "",
11087
+ `${scoreBar} ${stageLabel} \xB7 ${score}%`,
11088
+ `**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
11089
+ "",
11090
+ "## Stats",
11091
+ `- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
11092
+ `- **Relations:** ${stats.totalRelations}`,
11093
+ `- **Collections:** ${stats.collectionCount}`,
11094
+ `- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
11095
+ ""
11096
+ ];
11097
+ if (gaps.length > 0) {
11098
+ lines.push("## Gaps");
11099
+ for (const gap of gaps) {
11100
+ const action = gap.capabilityGuidance ?? gap.guidance;
11101
+ lines.push(`- [ ] **${gap.label}**`);
11102
+ lines.push(` _${action}_`);
11103
+ }
11104
+ lines.push("");
11105
+ }
11106
+ const passed = checks.filter((c) => c.passed);
11107
+ if (passed.length > 0) {
11108
+ lines.push("## Passing checks");
11109
+ for (const check of passed) {
11110
+ lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
11111
+ }
11112
+ lines.push("");
11113
+ }
11114
+ const orgHealth = await fetchOrganisationHealth();
11115
+ if (orgHealth && orgHealth.reviewed > 0) {
11116
+ lines.push("## Organisation Health");
11117
+ lines.push(...formatOrgHealthLines(orgHealth));
11118
+ lines.push("");
11119
+ }
11120
+ const statusData = {
11121
+ stage,
11122
+ scoringVersion,
11123
+ readinessScore: score,
11124
+ activeEntries: stats.activeCount,
11125
+ totalRelations: stats.totalRelations,
11126
+ orphanedEntries: stats.orphanedCount,
11127
+ gaps: gaps.map((g) => ({
11128
+ id: g.id,
11129
+ label: g.label,
11130
+ guidance: g.capabilityGuidance ?? g.guidance
11131
+ })),
11132
+ ...orgHealth && { organisationHealth: orgHealth }
11133
+ };
11134
+ return {
11135
+ content: [{ type: "text", text: lines.join("\n") }],
11136
+ structuredContent: success(
11137
+ `Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
11138
+ statusData
11139
+ )
11140
+ };
11141
+ }
11142
+ async function handleAudit(limit) {
11143
+ const log = getAuditLog();
11144
+ const recent = log.slice(-limit);
11145
+ if (recent.length === 0) {
11146
+ return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
11147
+ }
11148
+ const summary = buildSessionSummary(log);
11149
+ const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
11150
+ `];
11151
+ for (const entry of recent) {
11152
+ const icon = entry.status === "ok" ? "\u2713" : "\u2717";
11153
+ const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
11154
+ const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
11155
+ logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
11156
+ }
11157
+ const auditData = {
11158
+ totalCalls: log.length,
11159
+ calls: recent.map((entry) => ({
11160
+ tool: entry.fn,
11161
+ ...entry.toolContext?.action && { action: entry.toolContext.action },
11162
+ timestamp: entry.ts,
11163
+ ...entry.durationMs != null && { durationMs: entry.durationMs }
11164
+ }))
11165
+ };
11166
+ return {
11167
+ content: [{ type: "text", text: `${summary}
11168
+
11169
+ ---
11170
+
11171
+ ${logLines.join("\n")}` }],
11172
+ structuredContent: success(
11173
+ `Audit: ${log.length} total calls, showing last ${recent.length}.`,
11174
+ auditData
11175
+ )
11176
+ };
11177
+ }
11178
+ var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
11179
+ var healthSchema = z21.object({
11180
+ action: z21.enum(HEALTH_ACTIONS).describe(
11181
+ "'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
11182
+ ),
11183
+ limit: z21.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
11184
+ });
11185
+ var healthCheckOutputSchema = z21.object({
11186
+ healthy: z21.boolean(),
11187
+ collections: z21.number(),
11188
+ entries: z21.number(),
11189
+ latencyMs: z21.number(),
11190
+ workspace: z21.string()
11191
+ });
11192
+ var organisationHealthSchema = z21.object({
11193
+ reviewed: z21.number(),
11194
+ agreements: z21.number(),
11195
+ disagreements: z21.number(),
11196
+ abstentions: z21.number(),
11197
+ agreementRate: z21.number(),
11198
+ flags: z21.array(z21.object({
11199
+ collection: z21.string(),
11200
+ count: z21.number(),
11201
+ suggestedCollection: z21.string()
11202
+ }))
11203
+ });
11204
+ var healthStatusOutputSchema = z21.object({
11205
+ stage: z21.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
11206
+ scoringVersion: z21.enum(["v1", "v2"]).optional().default("v1"),
11207
+ readinessScore: z21.number(),
11208
+ activeEntries: z21.number(),
11209
+ totalRelations: z21.number(),
11210
+ orphanedEntries: z21.number(),
11211
+ gaps: z21.array(z21.object({ id: z21.string(), label: z21.string(), guidance: z21.string() })),
11212
+ organisationHealth: organisationHealthSchema.optional()
11213
+ });
11214
+ var healthAuditOutputSchema = z21.object({
11215
+ totalCalls: z21.number(),
11216
+ calls: z21.array(z21.object({
11217
+ tool: z21.string(),
11218
+ action: z21.string().optional(),
11219
+ timestamp: z21.string(),
11220
+ durationMs: z21.number().optional()
11221
+ }))
11222
+ });
11223
+ var healthWhoamiOutputSchema = z21.object({
11224
+ workspaceId: z21.string(),
11225
+ workspaceName: z21.string(),
11226
+ scope: z21.string(),
11227
+ sessionId: z21.union([z21.string(), z21.null()]),
11228
+ oriented: z21.boolean()
11229
+ });
11230
+ var ALL_TOOL_SCHEMAS = [
11231
+ { name: "entries", schema: entriesSchema },
11232
+ { name: "relations", schema: relationsSchema },
11233
+ { name: "graph", schema: graphSchema },
11234
+ { name: "context", schema: contextSchema },
11235
+ { name: "collections", schema: collectionsSchema },
11236
+ { name: "session", schema: sessionSchema },
11237
+ { name: "health", schema: healthSchema },
11238
+ { name: "orient", schema: orientSchema },
11239
+ { name: "quality", schema: qualitySchema },
11240
+ { name: "workflows", schema: workflowsSchema },
11241
+ { name: "session-wrapup", schema: wrapupSchema },
11242
+ { name: "labels", schema: labelsSchema },
11243
+ { name: "verify", schema: verifySchema },
11244
+ { name: "capture", schema: captureSchema },
11245
+ { name: "batch-capture", schema: batchCaptureSchema },
11246
+ { name: "update-entry", schema: updateEntrySchema },
11247
+ { name: "get-history", schema: getHistorySchema },
11248
+ { name: "commit-entry", schema: commitEntrySchema },
11249
+ { name: "start", schema: startSchema },
11250
+ { name: "get-usage-summary", schema: usageSummarySchema },
11251
+ { name: "chain", schema: chainSchema },
11252
+ { name: "chain-version", schema: chainVersionSchema },
11253
+ { name: "chain-branch", schema: chainBranchSchema },
11254
+ { name: "chain-review", schema: chainReviewSchema },
11255
+ { name: "create-audience-map-set", schema: createAudienceMapSetSchema },
11256
+ { name: "map", schema: mapSchema },
11257
+ { name: "map-slot", schema: mapSlotSchema },
11258
+ { name: "map-version", schema: mapVersionSchema },
11259
+ { name: "map-suggest", schema: mapSuggestSchema },
11260
+ { name: "architecture", schema: architectureSchema },
11261
+ { name: "architecture-admin", schema: architectureAdminSchema },
11262
+ { name: "facilitate", schema: facilitateSchema }
11263
+ ];
11264
+ var selfTestOutputSchema = z21.object({
11265
+ passed: z21.number(),
11266
+ failed: z21.number(),
11267
+ total: z21.number(),
11268
+ results: z21.array(z21.object({
11269
+ tool: z21.string(),
11270
+ valid: z21.boolean(),
11271
+ error: z21.string().optional()
11272
+ }))
11273
+ });
11274
+ function handleSelfTest() {
11275
+ const results = [];
11276
+ for (const { name, schema } of ALL_TOOL_SCHEMAS) {
11277
+ try {
11278
+ if (!schema || typeof schema.safeParse !== "function") {
11279
+ results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
11280
+ continue;
11281
+ }
11282
+ const test = schema.safeParse({});
11283
+ if (test.success || test.error) {
11284
+ results.push({ tool: name, valid: true });
11092
11285
  }
11093
- return {
11094
- content: [{ type: "text", text: lines.join("\n") }],
11095
- structuredContent: success(
11096
- `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
11097
- { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
11098
- )
11099
- };
11286
+ } catch (e) {
11287
+ results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
11288
+ }
11289
+ }
11290
+ const passed = results.filter((r) => r.valid).length;
11291
+ const failed = results.filter((r) => !r.valid).length;
11292
+ const total = results.length;
11293
+ const lines = [
11294
+ `# Self-Test: Tool Schema Validation`,
11295
+ `**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
11296
+ `**Schemas validated:** ${passed}/${total}`,
11297
+ ""
11298
+ ];
11299
+ if (failed > 0) {
11300
+ lines.push("## Failures");
11301
+ for (const r of results.filter((r2) => !r2.valid)) {
11302
+ lines.push(`- **${r.tool}**: ${r.error}`);
11303
+ }
11304
+ lines.push("");
11305
+ }
11306
+ lines.push("## All Tools");
11307
+ for (const r of results) {
11308
+ lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
11309
+ }
11310
+ return {
11311
+ content: [{ type: "text", text: lines.join("\n") }],
11312
+ structuredContent: success(
11313
+ failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
11314
+ { passed, failed, total, results }
11315
+ )
11316
+ };
11317
+ }
11318
+ function registerHealthTools(server) {
11319
+ server.registerTool(
11320
+ "health",
11321
+ {
11322
+ title: "Health",
11323
+ description: "Diagnostics and session identity. Four actions:\n\n- **check**: Verify connectivity \u2014 workspace, collections, entries, latency.\n- **whoami**: Session identity \u2014 workspace ID, slug, name.\n- **status**: Workspace readiness \u2014 score, gaps, stats (entries, relations, orphans).\n- **audit**: Session audit log \u2014 last N backend calls with summary.",
11324
+ inputSchema: healthSchema,
11325
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
11326
+ },
11327
+ withEnvelope(async (args) => {
11328
+ const parsed = parseOrFail(healthSchema, args);
11329
+ if (!parsed.ok) return parsed.result;
11330
+ const { action, limit } = parsed.data;
11331
+ return runWithToolContext({ tool: "health", action }, async () => {
11332
+ if (action === "check") return handleHealthCheck();
11333
+ if (action === "whoami") return handleWhoami();
11334
+ if (action === "status") return handleWorkspaceStatus();
11335
+ if (action === "audit") return handleAudit(limit ?? 20);
11336
+ if (action === "self-test") return handleSelfTest();
11337
+ return unknownAction(action, HEALTH_ACTIONS);
11338
+ });
11100
11339
  })
11101
11340
  );
11102
11341
  }
@@ -11545,12 +11784,197 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
11545
11784
  }
11546
11785
 
11547
11786
  // src/prompts/index.ts
11548
- import { z as z21 } from "zod";
11787
+ import { z as z23 } from "zod";
11788
+
11789
+ // src/tools/project-scan.ts
11790
+ import { z as z22 } from "zod";
11791
+ var scanExtractionSchema = z22.object({
11792
+ vision: z22.string().min(10).optional().describe("Product purpose from README"),
11793
+ audience: z22.string().optional().describe("Primary user or customer segment"),
11794
+ techStack: z22.array(z22.string()).optional().describe("Key technologies from package.json"),
11795
+ modules: z22.array(z22.object({ name: z22.string(), purpose: z22.string() })).max(8).optional().describe("Top-level modules from folder structure"),
11796
+ businessRules: z22.array(z22.string()).max(10).optional().describe("System constraints from .cursorrules/AGENTS.md/CLAUDE.md"),
11797
+ conventions: z22.array(z22.string()).max(8).optional().describe("Coding or process standards from .cursorrules"),
11798
+ keyDecisions: z22.array(z22.string()).optional().describe("Significant architectural decisions from git log or README"),
11799
+ tensions: z22.array(z22.string()).optional().describe("Friction points or known trade-offs"),
11800
+ keyTerms: z22.array(z22.string()).optional().describe("Domain-specific vocabulary found in files")
11801
+ });
11802
+ var COLLECTION_PRIORITY = {
11803
+ strategy: 0,
11804
+ audiences: 1,
11805
+ architecture: 2,
11806
+ decisions: 3,
11807
+ "business-rules": 4,
11808
+ standards: 5,
11809
+ glossary: 6,
11810
+ tensions: 7
11811
+ };
11812
+ var GENERIC_MODULE_NAMES = /* @__PURE__ */ new Set(["src", "lib", "utils"]);
11813
+ function truncate(str, maxLen) {
11814
+ return str.length <= maxLen ? str : str.slice(0, maxLen);
11815
+ }
11816
+ function firstNWords(str, n) {
11817
+ return str.split(/\s+/).slice(0, n).join(" ");
11818
+ }
11819
+ function scanToBatchEntries(extracted) {
11820
+ const entries = [];
11821
+ const techStackGlossaryNames = /* @__PURE__ */ new Set();
11822
+ if (extracted.vision) {
11823
+ entries.push({
11824
+ collection: "strategy",
11825
+ name: "Product Vision",
11826
+ description: extracted.vision
11827
+ });
11828
+ }
11829
+ if (extracted.audience) {
11830
+ entries.push({
11831
+ collection: "audiences",
11832
+ name: firstNWords(extracted.audience, 6),
11833
+ description: extracted.audience
11834
+ });
11835
+ }
11836
+ if (extracted.techStack && extracted.techStack.length > 0) {
11837
+ entries.push({
11838
+ collection: "architecture",
11839
+ name: "Tech Stack",
11840
+ description: `Technology choices: ${extracted.techStack.join(", ")}`
11841
+ });
11842
+ for (const tech of extracted.techStack.slice(0, 3)) {
11843
+ const name = tech;
11844
+ techStackGlossaryNames.add(name.toLowerCase());
11845
+ entries.push({
11846
+ collection: "glossary",
11847
+ name,
11848
+ description: `${tech} \u2014 part of the tech stack`
11849
+ });
11850
+ }
11851
+ }
11852
+ if (extracted.modules) {
11853
+ let count = 0;
11854
+ for (const mod of extracted.modules) {
11855
+ if (count >= 5) break;
11856
+ if (GENERIC_MODULE_NAMES.has(mod.name.toLowerCase())) continue;
11857
+ entries.push({
11858
+ collection: "architecture",
11859
+ name: mod.name,
11860
+ description: mod.purpose
11861
+ });
11862
+ count++;
11863
+ }
11864
+ }
11865
+ for (const rule of extracted.businessRules ?? []) {
11866
+ entries.push({
11867
+ collection: "business-rules",
11868
+ name: truncate(rule, 70),
11869
+ description: rule
11870
+ });
11871
+ }
11872
+ for (const convention of extracted.conventions ?? []) {
11873
+ entries.push({
11874
+ collection: "standards",
11875
+ name: truncate(convention, 70),
11876
+ description: convention
11877
+ });
11878
+ }
11879
+ for (const decision of extracted.keyDecisions ?? []) {
11880
+ entries.push({
11881
+ collection: "decisions",
11882
+ name: truncate(decision, 80),
11883
+ description: decision
11884
+ });
11885
+ }
11886
+ for (const tension of extracted.tensions ?? []) {
11887
+ entries.push({
11888
+ collection: "tensions",
11889
+ name: truncate(tension, 80),
11890
+ description: tension
11891
+ });
11892
+ }
11893
+ for (const term of extracted.keyTerms ?? []) {
11894
+ if (techStackGlossaryNames.has(term.toLowerCase())) continue;
11895
+ entries.push({
11896
+ collection: "glossary",
11897
+ name: term,
11898
+ description: `Core domain term: ${term}`
11899
+ });
11900
+ }
11901
+ const seen = /* @__PURE__ */ new Set();
11902
+ const deduped = entries.filter((e) => {
11903
+ const key = `${e.collection}:${e.name.toLowerCase()}`;
11904
+ if (seen.has(key)) return false;
11905
+ seen.add(key);
11906
+ return true;
11907
+ });
11908
+ if (deduped.length <= 30) return deduped;
11909
+ return deduped.sort((a, b) => {
11910
+ const pa = COLLECTION_PRIORITY[a.collection] ?? 99;
11911
+ const pb = COLLECTION_PRIORITY[b.collection] ?? 99;
11912
+ return pa - pb;
11913
+ }).slice(0, 30);
11914
+ }
11915
+
11916
+ // src/tools/draft-review.ts
11917
+ var REVIEW_COLLECTION_ORDER = [
11918
+ "strategy",
11919
+ "chains",
11920
+ "audiences",
11921
+ "architecture",
11922
+ "decisions",
11923
+ "business-rules",
11924
+ "standards",
11925
+ "glossary",
11926
+ "tensions"
11927
+ ];
11928
+ function groupDraftsByCollection(drafts) {
11929
+ const buckets = /* @__PURE__ */ new Map();
11930
+ for (const draft of drafts) {
11931
+ const existing = buckets.get(draft.collection);
11932
+ if (existing) {
11933
+ existing.push(draft);
11934
+ } else {
11935
+ buckets.set(draft.collection, [draft]);
11936
+ }
11937
+ }
11938
+ const knownKeys = REVIEW_COLLECTION_ORDER.filter((k) => buckets.has(k));
11939
+ const unknownKeys = [...buckets.keys()].filter((k) => !REVIEW_COLLECTION_ORDER.includes(k)).sort();
11940
+ const ordered = /* @__PURE__ */ new Map();
11941
+ for (const key of [...knownKeys, ...unknownKeys]) {
11942
+ ordered.set(key, buckets.get(key));
11943
+ }
11944
+ return ordered;
11945
+ }
11946
+ var COLLECTION_DISPLAY_NAMES = {
11947
+ "business-rules": "Business Rules",
11948
+ "tracking-events": "Tracking Events"
11949
+ };
11950
+ function collectionLabel(slug) {
11951
+ return COLLECTION_DISPLAY_NAMES[slug] ?? slug.charAt(0).toUpperCase() + slug.slice(1);
11952
+ }
11953
+ function formatDraftGroups(grouped, total) {
11954
+ if (total === 0) {
11955
+ return "No drafts to review \u2014 your workspace is fully committed.";
11956
+ }
11957
+ const lines = [`## Drafts Ready for Review (${total} total)`];
11958
+ let counter = 1;
11959
+ for (const [collection, entries] of grouped) {
11960
+ const label = collectionLabel(collection);
11961
+ const count = entries.length;
11962
+ lines.push(`
11963
+ ### ${label} (${count} ${count === 1 ? "entry" : "entries"})`);
11964
+ for (const entry of entries) {
11965
+ lines.push(`${counter}. ${entry.name} [${entry.entryId}]`);
11966
+ counter++;
11967
+ }
11968
+ }
11969
+ return lines.join("\n");
11970
+ }
11971
+
11972
+ // src/prompts/index.ts
11549
11973
  function registerPrompts(server) {
11550
11974
  server.prompt(
11551
11975
  "review-against-rules",
11552
11976
  "Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
11553
- { domain: z21.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
11977
+ { domain: z23.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
11554
11978
  async ({ domain }) => {
11555
11979
  const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
11556
11980
  const rules = entries.filter((e) => e.data?.domain === domain);
@@ -11603,7 +12027,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
11603
12027
  server.prompt(
11604
12028
  "name-check",
11605
12029
  "Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
11606
- { names: z21.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
12030
+ { names: z23.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
11607
12031
  async ({ names }) => {
11608
12032
  const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
11609
12033
  const glossaryContext = terms.map(
@@ -11639,7 +12063,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
11639
12063
  server.prompt(
11640
12064
  "draft-decision-record",
11641
12065
  "Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
11642
- { context: z21.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
12066
+ { context: z23.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
11643
12067
  async ({ context }) => {
11644
12068
  const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
11645
12069
  const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
@@ -11677,8 +12101,8 @@ After drafting, I can log it using the capture tool with collection "decisions".
11677
12101
  "draft-rule-from-context",
11678
12102
  "Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
11679
12103
  {
11680
- observation: z21.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
11681
- domain: z21.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
12104
+ observation: z23.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
12105
+ domain: z23.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
11682
12106
  },
11683
12107
  async ({ observation, domain }) => {
11684
12108
  const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
@@ -11719,10 +12143,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
11719
12143
  "run-workflow",
11720
12144
  "Launch any registered Chainwork workflow in Facilitator Mode. Returns the full workflow definition, facilitation instructions, and round structure. The agent enters Facilitator Mode and guides the participant through each round.",
11721
12145
  {
11722
- workflow: z21.string().describe(
12146
+ workflow: z23.string().describe(
11723
12147
  "Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
11724
12148
  ),
11725
- context: z21.string().optional().describe(
12149
+ context: z23.string().optional().describe(
11726
12150
  "Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
11727
12151
  )
11728
12152
  },
@@ -11867,7 +12291,7 @@ ${wf.errorRecovery}
11867
12291
  "shape-a-bet",
11868
12292
  "Launch a coached shaping session powered by the facilitate tool. Dynamically loads workspace context, governance constraints, and coaching rubrics. Use when the user wants to shape a bet, define a pitch, or scope work.",
11869
12293
  {
11870
- idea: z21.string().describe("Brief description of the idea or feature to shape (e.g. 'Improve the glossary editing flow')")
12294
+ idea: z23.string().describe("Brief description of the idea or feature to shape (e.g. 'Improve the glossary editing flow')")
11871
12295
  },
11872
12296
  async ({ idea }) => {
11873
12297
  let strategicContext = "";
@@ -12028,9 +12452,9 @@ Walk away mid-session and everything captured so far exists as drafts. \`facilit
12028
12452
  "capture-and-connect",
12029
12453
  "Guided workflow: capture a knowledge entry, discover graph connections, create relations, and prepare for commit. Encodes the capture \u2192 suggest \u2192 batch-create \u2192 commit choreography as a step-by-step guide.",
12030
12454
  {
12031
- collection: z21.string().describe("Collection slug (e.g. 'glossary', 'business-rules', 'decisions', 'tensions')"),
12032
- name: z21.string().describe("Entry name"),
12033
- description: z21.string().describe("Entry description")
12455
+ collection: z23.string().describe("Collection slug (e.g. 'glossary', 'business-rules', 'decisions', 'tensions')"),
12456
+ name: z23.string().describe("Entry name"),
12457
+ description: z23.string().describe("Entry description")
12034
12458
  },
12035
12459
  async ({ collection, name, description }) => {
12036
12460
  const collections = await mcpQuery("chain.listCollections");
@@ -12084,7 +12508,7 @@ Only call \`commit-entry\` when the user explicitly confirms.
12084
12508
  "deep-dive",
12085
12509
  "Explore everything the Chain knows about a topic or entry. Assembles entry details, graph context, related business rules, and glossary terms into a comprehensive briefing.",
12086
12510
  {
12087
- topic: z21.string().describe("Entry ID (e.g. 'BR-001') or topic to explore (e.g. 'authentication')")
12511
+ topic: z23.string().describe("Entry ID (e.g. 'BR-001') or topic to explore (e.g. 'authentication')")
12088
12512
  },
12089
12513
  async ({ topic }) => {
12090
12514
  const isEntryId = /^[A-Z]+-\d+$/i.test(topic) || /^[A-Z]+-[a-z0-9]+$/i.test(topic);
@@ -12167,7 +12591,7 @@ ${contextInstructions}`
12167
12591
  "pre-commit-check",
12168
12592
  "Run a readiness check before committing an entry to the Chain. Validates quality score, required relations, and business rule compliance.",
12169
12593
  {
12170
- entryId: z21.string().describe("Entry ID to check (e.g. 'GT-019', 'DEC-005')")
12594
+ entryId: z23.string().describe("Entry ID to check (e.g. 'GT-019', 'DEC-005')")
12171
12595
  },
12172
12596
  async ({ entryId }) => {
12173
12597
  return {
@@ -12223,9 +12647,9 @@ If ready, ask the user to confirm. If not, suggest specific improvements.
12223
12647
  );
12224
12648
  server.prompt(
12225
12649
  "project-scan",
12226
- "Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack, key terms, and decisions into batch-capture entries. The trust boundary is confirmation before capture: only capture entries the user explicitly keeps, then let normal workspace governance apply.",
12650
+ "Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack, modules, business rules, conventions, decisions, and domain terms into batch-capture entries. The trust boundary is confirmation before capture: only capture entries the user explicitly keeps, then let normal workspace governance apply.",
12227
12651
  {
12228
- workspaceContext: z21.string().optional().describe("Optional context about the workspace preset and existing collections (paste from start/orient output)")
12652
+ workspaceContext: z23.string().optional().describe("Optional context about the workspace preset and existing collections (paste from start/orient output)")
12229
12653
  },
12230
12654
  async ({ workspaceContext }) => {
12231
12655
  let collectionsContext = "";
@@ -12240,17 +12664,19 @@ ${colList}
12240
12664
  }
12241
12665
  } catch {
12242
12666
  }
12243
- const schemaFields = Object.entries(interviewExtractionSchema.shape).map(([key, field]) => `- \`${key}\`: ${field.description ?? ""}`).join("\n");
12244
- const exampleExtraction = {
12245
- vision: "A task management app for solo developers who work with AI coding assistants",
12667
+ const schemaFields = Object.entries(scanExtractionSchema.shape).map(([key, field]) => `- \`${key}\`: ${field.description ?? ""}`).join("\n");
12668
+ const exampleEntries = scanToBatchEntries({
12669
+ vision: "A knowledge management tool for solo developers working with AI coding assistants",
12246
12670
  audience: "Solo developers using Cursor, Claude Code, or similar AI IDEs",
12247
12671
  techStack: ["SvelteKit", "Convex", "TypeScript"],
12672
+ modules: [{ name: "Product Brain MCP", purpose: "MCP server that exposes knowledge capture tools to the IDE agent" }],
12673
+ businessRules: ["All public functions must validate arguments with Zod before processing"],
12674
+ conventions: ["Use batch-capture for multi-entry writes; never call capture in a loop"],
12675
+ keyDecisions: ["Use Convex for real-time backend \u2014 avoids managing infrastructure"],
12248
12676
  keyTerms: ["Chain", "Capture", "Orientation"],
12249
- keyDecisions: ["Use Convex for real-time backend to avoid managing infrastructure"],
12250
- tensions: ["Activation requires skill \u2014 users need to know what to capture"]
12251
- };
12252
- const exampleEntries = extractionToBatchEntries(exampleExtraction);
12253
- const exampleOutput = JSON.stringify({ entries: exampleEntries.slice(0, 4) }, null, 2);
12677
+ tensions: ["Activation requires skill \u2014 users need to know what to capture before value compounds"]
12678
+ });
12679
+ const exampleOutput = JSON.stringify({ entries: exampleEntries.slice(0, 5) }, null, 2);
12254
12680
  return {
12255
12681
  messages: [{
12256
12682
  role: "user",
@@ -12259,84 +12685,94 @@ ${colList}
12259
12685
  text: `# Project Scan \u2014 Agent-Side Knowledge Extraction
12260
12686
 
12261
12687
  You are scanning the user's local project to extract structured knowledge for Product Brain.
12262
- PB runs remotely \u2014 you do the file reading and extraction, then call \`batch-capture\`.
12688
+ PB runs remotely \u2014 you read the files and extract, then call \`batch-capture\` after confirmation.
12263
12689
 
12264
12690
  ` + (workspaceContext ? `## Workspace Context
12265
12691
  ${workspaceContext}
12266
12692
 
12267
12693
  ` : "") + collectionsContext + `## Step 1: Read Project Files
12268
12694
 
12269
- Read these files in this order (skip gracefully if not present):
12270
- 1. \`README.md\` or \`readme.md\` \u2014 product description, purpose, setup
12271
- 2. \`package.json\` \u2014 name, description, dependencies (framework, database, auth)
12272
- 3. \`tsconfig.json\` or \`tsconfig.app.json\` \u2014 compilation target, paths
12273
- 4. Top-level folder structure (list directories only, max 2 levels deep)
12274
- 5. \`.cursorrules\` or \`CLAUDE.md\` or \`AGENTS.md\` \u2014 project conventions, constraints
12275
- 6. \`git log --oneline -20\` \u2014 recent commit messages for decisions/tensions
12695
+ Read these files in order. Skip gracefully if not present.
12276
12696
 
12277
- **ICP stack only (V1):** README, package.json, tsconfig, folder structure, .cursorrules/CLAUDE.md, git log.
12278
- Do not attempt to read Python, Rust, Go, or mobile-specific files in this pass.
12697
+ 1. **\`README.md\`** \u2014 Look for: product purpose (vision), who it's for (audience), key concepts, architecture notes, known issues
12698
+ 2. **\`package.json\`** \u2014 Look for: \`name\`, \`description\`, and **dependencies** \u2014 identify the framework, database, auth library, UI library (these become tech stack + glossary). Ignore dev tools and test libraries.
12699
+ 3. **\`tsconfig.json\` / \`tsconfig.app.json\`** \u2014 Look for: path aliases (e.g. \`@/\`) that hint at module structure
12700
+ 4. **Top-level folder structure** (list directories only, max 2 levels) \u2014 Look for: named modules, feature folders, service boundaries. Note names that are specific enough to be meaningful.
12701
+ 5. **\`.cursorrules\` / \`CLAUDE.md\` / \`AGENTS.md\`** \u2014 Look for: **business rules** (constraints the system enforces), **conventions** (how work is done here), no-gos, domain vocabulary
12702
+ 6. **\`git log --oneline -20\`** \u2014 Look for: commits that imply decisions ("migrate from X to Y", "switch to Z for reason"), repeated friction ("fix broken", "revert", "hotfix same area twice")
12703
+
12704
+ **ICP stack only (V1):** README, package.json, tsconfig, folder structure, .cursorrules/CLAUDE.md/AGENTS.md, git log.
12705
+ Do not attempt Python, Rust, Go, or mobile-specific files in this pass.
12279
12706
 
12280
12707
  ## Step 2: Extract Structured Data
12281
12708
 
12282
- After reading, extract the following schema:
12709
+ After reading all files, extract into this schema:
12283
12710
 
12284
12711
  \`\`\`
12285
12712
  ${schemaFields}
12286
12713
  \`\`\`
12287
12714
 
12715
+ **Field-by-field guidance:**
12716
+ - \`vision\`: one sentence product purpose. Source: README intro or package.json description. Required if README exists.
12717
+ - \`audience\`: who it's for. Source: README target-user section or "built for" language.
12718
+ - \`techStack\`: main runtime dependencies only \u2014 framework, database, auth, UI. Max 8. Not dev tools, test libs, or linters.
12719
+ - \`modules\`: named subsystems with clear purposes. Source: folder structure + tsconfig path aliases. Skip generic names (src, lib, utils alone). Max 5.
12720
+ - \`businessRules\`: constraints the system enforces. Source: .cursorrules/AGENTS.md. Write as: "All X must Y" or "The system must Z". Keep as written.
12721
+ - \`conventions\`: how work is done here. Source: .cursorrules standards sections. Write as: "Use X not Y" or "Always do Z".
12722
+ - \`keyDecisions\`: architectural choices made, preferably with rationale. Source: git log patterns, README architecture notes. Each as a declarative statement ("Use Convex for real-time backend").
12723
+ - \`tensions\`: known friction or trade-offs. Source: git log hotfixes/reverts, README "known issues".
12724
+ - \`keyTerms\`: domain vocabulary specific to this project \u2014 not tech names, but product/business terms. Source: README, .cursorrules.
12725
+
12288
12726
  **Quality rules (RH2 mitigation):**
12289
- - Prefer 8 good entries over 20 mediocre ones
12290
- - Only include terms you have direct evidence for from the files
12291
- - Skip fields you cannot confidently fill \u2014 empty arrays > hallucinated data
12292
- - Keep descriptions concise: 1\u20132 sentences max
12727
+ - Only include items with direct evidence from the files \u2014 no inference, no hallucination
12728
+ - Prefer 8 precise entries over 20 vague ones
12729
+ - Business rules and conventions must be actual constraints, not generic advice
12730
+ - Empty arrays beat fabricated data
12293
12731
 
12294
- ## Step 3: Validate Before Capture
12732
+ ## Step 3: Validate Before Presenting
12295
12733
 
12296
- Before calling batch-capture, check your extraction:
12297
- - vision is present and at least 10 characters
12298
- - each entry has a non-empty name and description
12734
+ Before presenting, check each entry:
12735
+ - vision is at least 10 characters if present
12299
12736
  - no duplicate names within the same collection
12300
- - if techStack is present, at least one glossary entry per technology
12737
+ - each entry has a non-empty name and description
12738
+ - techStack items are actual technologies, not version strings or config flags
12739
+ - module names are specific (not just "src", "lib", "utils")
12301
12740
 
12302
- If validation fails for any entry, drop it rather than sending malformed data.
12741
+ Drop any entry that fails. If no vision AND no techStack exist, the scan is too sparse \u2014 tell the user which files were missing and stop.
12303
12742
 
12304
- Then present the inferred entries as a numbered list. Ask which to keep, and do NOT call batch-capture yet.
12305
- Only after the user confirms which entries to keep should you capture the confirmed entries.
12743
+ ## Step 4: Present for Confirmation
12306
12744
 
12307
- ## Step 4: Map to batch-capture
12745
+ Present inferred entries as a numbered list grouped by collection. Ask which to keep.
12746
+ **Do NOT call batch-capture yet.** Only after the user confirms.
12308
12747
 
12309
- Map your extracted data to entries using these rules:
12310
- - \`vision\` \u2192 strategy collection, name: "Product Vision"
12311
- - \`audience\` \u2192 audiences collection
12312
- - \`techStack\` \u2192 architecture collection ("Tech Stack") + top 3 items \u2192 glossary
12313
- - \`keyTerms\` \u2192 glossary collection (1 entry per term)
12314
- - \`keyDecisions\` \u2192 decisions collection (1 entry per decision)
12315
- - \`tensions\` \u2192 tensions collection (1 entry per tension)
12748
+ ## Step 5: Map to batch-capture
12749
+
12750
+ After confirmation, map entries using these rules:
12751
+ - \`vision\` \u2192 strategy, name: "Product Vision"
12752
+ - \`audience\` \u2192 audiences
12753
+ - \`techStack\` \u2192 architecture ("Tech Stack") + top 3 items individually \u2192 glossary
12754
+ - \`modules\` \u2192 architecture (1 per module, max 5)
12755
+ - \`businessRules\` \u2192 business-rules (1 per rule)
12756
+ - \`conventions\` \u2192 standards (1 per convention)
12757
+ - \`keyDecisions\` \u2192 decisions (1 per decision)
12758
+ - \`tensions\` \u2192 tensions (1 per tension)
12759
+ - \`keyTerms\` \u2192 glossary (skip names already added from techStack)
12316
12760
 
12317
12761
  **Example output:**
12318
12762
  \`\`\`json
12319
12763
  ${exampleOutput}
12320
12764
  \`\`\`
12321
12765
 
12322
- ## Step 5: Call batch-capture
12323
-
12324
- After the user confirms which entries to keep, call batch-capture with only those confirmed entries.
12325
- Omit \`autoCommit\` to let normal workspace governance apply.
12326
-
12327
- \`\`\`
12328
- batch-capture entries=[...your confirmed entries...]
12329
- \`\`\`
12330
-
12331
- If \`failed > 0\` in the response, inspect \`failedEntries\` and retry those individually.
12766
+ Call batch-capture with confirmed entries only. Omit \`autoCommit\` to follow workspace governance.
12767
+ If \`failed > 0\`, inspect \`failedEntries\` and retry those individually.
12332
12768
 
12333
12769
  ## Step 6: Connect + Prove Value
12334
12770
 
12335
12771
  After capture:
12336
12772
  1. Call \`graph action=suggest\` on 2\u20133 key entries (vision, architecture, a decision)
12337
- 2. Invent a plausible next task and call \`context action=gather task="<that task>"\`
12773
+ 2. Invent a plausible next task the user might actually do and call \`context action=gather task="<that task>"\`
12338
12774
  3. Present what Product Brain now knows in that task context \u2014 this is the proof moment
12339
- 4. Then show one clear result summary so the user sees exactly what was added
12775
+ 4. Show one clear result summary so the user sees exactly what was added
12340
12776
 
12341
12777
  **Begin with Step 1 now.** Read the files, then report what you found before extracting.`
12342
12778
  }
@@ -12344,6 +12780,108 @@ After capture:
12344
12780
  };
12345
12781
  }
12346
12782
  );
12783
+ server.prompt(
12784
+ "review-drafts",
12785
+ "Review all uncommitted workspace drafts grouped by collection and commit them in batches. After each committed batch, surfaces the most non-obvious graph connection found as the WOW moment \u2014 the signal that Product Brain has started compounding.",
12786
+ {
12787
+ focus: z23.string().optional().describe("Optional: a specific collection to review first (e.g. 'glossary', 'decisions')")
12788
+ },
12789
+ async ({ focus }) => {
12790
+ let allDrafts = [];
12791
+ try {
12792
+ const raw = await mcpQuery("chain.listEntries", { status: "draft" });
12793
+ if (Array.isArray(raw)) {
12794
+ allDrafts = raw.filter(
12795
+ (e) => typeof e === "object" && e !== null && typeof e.entryId === "string" && typeof e.name === "string" && typeof e.collection === "string"
12796
+ );
12797
+ }
12798
+ } catch {
12799
+ }
12800
+ if (focus) {
12801
+ allDrafts.sort((a, b) => {
12802
+ const aMatch = a.collection === focus ? -1 : 0;
12803
+ const bMatch = b.collection === focus ? -1 : 0;
12804
+ return aMatch - bMatch;
12805
+ });
12806
+ }
12807
+ const grouped = groupDraftsByCollection(allDrafts);
12808
+ const draftList = formatDraftGroups(grouped, allDrafts.length);
12809
+ const hasDrafts = allDrafts.length > 0;
12810
+ return {
12811
+ messages: [{
12812
+ role: "user",
12813
+ content: {
12814
+ type: "text",
12815
+ text: `# Review Drafts \u2014 Batch Commit Queue
12816
+
12817
+ ` + (hasDrafts ? draftList + "\n\n" : "No drafts found in this workspace. Everything is already committed.\n\n") + (hasDrafts ? `---
12818
+
12819
+ ## How to proceed
12820
+
12821
+ Work through each collection group with the user. The goal is: every entry the user wants to keep gets committed.
12822
+
12823
+ **Step 1 \u2014 Offer a starting point**
12824
+
12825
+ Ask the user which group to tackle first, or suggest the most important one. A good default: start with Strategy and Decisions \u2014 these are high-signal and there are usually few of them. Glossary and Architecture entries can be batched quickly afterward.
12826
+
12827
+ **Step 2 \u2014 Confirm, then commit**
12828
+
12829
+ For each group the user confirms, call \`commit-entry\` once per entry. These must be sequential \u2014 commit each before moving to the next.
12830
+
12831
+ \`\`\`
12832
+ commit-entry entryId="[ID]"
12833
+ \`\`\`
12834
+
12835
+ Report any failures and skip rather than block. If commit returns \`proposal_created\`, tell the user \u2014 this workspace uses governed commits and proposals need approval.
12836
+
12837
+ **Step 3 \u2014 Surface the WOW connection (once per committed batch)**
12838
+
12839
+ After finishing a collection group, call \`graph action=suggest\` on up to 2 of the most important entries you just committed:
12840
+ - First choice: any strategy entry
12841
+ - Second choice: architecture or decisions entries
12842
+ - Maximum: 2 \`graph action=suggest\` calls per batch \u2014 not one per entry
12843
+
12844
+ From the suggestions returned, pick the **most non-obvious connection** \u2014 the one that crosses collection boundaries (e.g. a vision entry connecting to a decision, not just two glossary terms). Prefer suggestions with specific reasoning over generic ones. Ignore suggestions with no reasoning.
12845
+
12846
+ If a strong connection exists, present it exactly like this:
12847
+
12848
+ ---
12849
+ **The graph noticed something.**
12850
+
12851
+ Your **[Entry A]** connects to **[Entry B]** \u2014 [reasoning from the graph, in one sentence].
12852
+
12853
+ You didn't explain this relationship. Product Brain inferred it from what you've built so far. This is the compounding starting.
12854
+
12855
+ Want to add this link?
12856
+ \`\`\`
12857
+ relations action=create from=[A] to=[B] type=[relationType]
12858
+ \`\`\`
12859
+ ---
12860
+
12861
+ If there are no specific suggestions (or all reasoning is generic), skip this step silently. Do not fabricate a connection.
12862
+
12863
+ **Step 4 \u2014 The offer**
12864
+
12865
+ After the first batch is committed (especially if it includes a strategy or decisions entry), offer:
12866
+
12867
+ > "Ask me anything about your product \u2014 I'll answer using everything you just committed."
12868
+
12869
+ This is not a formality. If the user asks a question, actually answer it using \`context action=gather task="<their question>"\`. Show them what the Brain now knows, not just that it captured the data.
12870
+
12871
+ **Step 5 \u2014 Continue or wrap**
12872
+
12873
+ After each batch, ask if the user wants to continue with the next group. Once all groups are done, close with a summary:
12874
+
12875
+ - How many entries were committed this session
12876
+ - Which collections are now populated
12877
+ - One sentence: what Product Brain now knows about their product
12878
+
12879
+ End with: **"Your Brain is ready."**` : `Your workspace has no uncommitted entries. If you recently captured new knowledge, check that \`batch-capture\` completed successfully and try \`entries action=list status=draft\`.`)
12880
+ }
12881
+ }]
12882
+ };
12883
+ }
12884
+ );
12347
12885
  }
12348
12886
 
12349
12887
  // src/server.ts
@@ -12438,6 +12976,7 @@ function createProductBrainServer() {
12438
12976
  registerKnowledgeTools(server);
12439
12977
  registerLabelTools(server);
12440
12978
  registerHealthTools(server);
12979
+ registerOrientTool(server);
12441
12980
  registerVerifyTools(server);
12442
12981
  registerSmartCaptureTools(server);
12443
12982
  registerQualityTools(server);
@@ -12457,4 +12996,4 @@ export {
12457
12996
  SERVER_VERSION,
12458
12997
  createProductBrainServer
12459
12998
  };
12460
- //# sourceMappingURL=chunk-JOJWCU7A.js.map
12999
+ //# sourceMappingURL=chunk-ZPEIKRYG.js.map