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

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-MFH4WG3T.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-P4CPOIFW.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";
@@ -3852,7 +3951,7 @@ If any facilitate call fails:
3852
3951
  description: "Confirmed problem statement and appetite",
3853
3952
  format: "structured"
3854
3953
  },
3855
- kbCollection: "bets",
3954
+ kbCollection: "chains",
3856
3955
  maxDurationHint: "10 min"
3857
3956
  },
3858
3957
  {
@@ -3922,7 +4021,7 @@ If any facilitate call fails:
3922
4021
  description: "Final capture summary \u2014 entries committed, relations created",
3923
4022
  format: "structured"
3924
4023
  },
3925
- kbCollection: "bets",
4024
+ kbCollection: "chains",
3926
4025
  maxDurationHint: "5 min"
3927
4026
  }
3928
4027
  ],
@@ -3930,7 +4029,7 @@ If any facilitate call fails:
3930
4029
  primaryRecord: {
3931
4030
  routing: {
3932
4031
  mode: "fixed",
3933
- collection: "bets"
4032
+ collection: "chains"
3934
4033
  }
3935
4034
  },
3936
4035
  emits: [
@@ -3956,7 +4055,7 @@ If any facilitate call fails:
3956
4055
  },
3957
4056
  {
3958
4057
  kind: "update",
3959
- collection: "bets",
4058
+ collection: "chains",
3960
4059
  description: "Bet status and no-go updates during finalize."
3961
4060
  }
3962
4061
  ]
@@ -5823,7 +5922,8 @@ function generateBuildContract(ctx) {
5823
5922
  function computeCommitBlockers(opts) {
5824
5923
  const { betEntryId, betDocId, relations, hasStrategyLink, betData, sessionDrafts } = opts;
5825
5924
  const blockers = [];
5826
- const str = (key) => (betData[key] ?? "").trim();
5925
+ const links = betData.links ?? {};
5926
+ const str = (key) => (links[key] ?? "").trim();
5827
5927
  if (!hasStrategyLink) {
5828
5928
  blockers.push({
5829
5929
  entryId: betEntryId,
@@ -6187,7 +6287,8 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
6187
6287
  }
6188
6288
  }
6189
6289
  function buildScoringContext(userInput, betData, constellation, overlapIds, chainResults = [], activeDimension, source = "user") {
6190
- const str = (key) => betData[key] ?? "";
6290
+ const links = betData.links ?? {};
6291
+ const str = (key) => links[key] ?? "";
6191
6292
  const accParts = [
6192
6293
  str("problem"),
6193
6294
  str("appetite"),
@@ -6342,17 +6443,20 @@ async function processCaptures(opts) {
6342
6443
  };
6343
6444
  const FALLBACK_DEFAULTS = { dataField: "description", relationType: "related_to" };
6344
6445
  let runningBetData = { ...betData };
6446
+ const runningLinks = () => ({ ...runningBetData.links ?? {} });
6345
6447
  for (const item of captureItems) {
6346
6448
  if (item.type === "noGo") {
6449
+ const curLinks = runningLinks();
6347
6450
  const updatedNoGos = appendNoGo(
6348
- runningBetData.noGos,
6451
+ curLinks.noGos,
6349
6452
  { title: item.name, explanation: item.description }
6350
6453
  );
6351
- runningBetData.noGos = updatedNoGos;
6454
+ const newLinks = { ...curLinks, noGos: updatedNoGos };
6455
+ runningBetData.links = newLinks;
6352
6456
  try {
6353
6457
  await mcpMutation("chain.updateEntry", {
6354
6458
  entryId: betEntryId,
6355
- data: { noGos: updatedNoGos },
6459
+ data: { links: newLinks },
6356
6460
  changeNote: `Added no-go: ${item.name}`
6357
6461
  });
6358
6462
  await recordSessionActivity({ entryModified: betDocId });
@@ -6436,15 +6540,17 @@ async function processCaptures(opts) {
6436
6540
  }
6437
6541
  }
6438
6542
  if (item.type === "element") {
6543
+ const curLinks = runningLinks();
6439
6544
  const updatedElements = appendElement(
6440
- runningBetData.elements,
6545
+ curLinks.elements,
6441
6546
  { name: item.name, description: item.description, entryId: capturedEntryId }
6442
6547
  );
6443
- runningBetData.elements = updatedElements;
6548
+ const newLinks = { ...curLinks, elements: updatedElements };
6549
+ runningBetData.links = newLinks;
6444
6550
  try {
6445
6551
  await mcpMutation("chain.updateEntry", {
6446
6552
  entryId: betEntryId,
6447
- data: { elements: updatedElements },
6553
+ data: { links: newLinks },
6448
6554
  changeNote: `Added element: ${item.name}`
6449
6555
  });
6450
6556
  await recordSessionActivity({ entryModified: betDocId });
@@ -6455,15 +6561,17 @@ async function processCaptures(opts) {
6455
6561
  });
6456
6562
  }
6457
6563
  } else if (item.type === "risk") {
6564
+ const curLinks = runningLinks();
6458
6565
  const updatedRisks = appendRabbitHole(
6459
- runningBetData.rabbitHoles,
6566
+ curLinks.rabbitHoles,
6460
6567
  { name: item.name, description: item.description, theme: item.theme, entryId: capturedEntryId }
6461
6568
  );
6462
- runningBetData.rabbitHoles = updatedRisks;
6569
+ const newLinks = { ...curLinks, rabbitHoles: updatedRisks };
6570
+ runningBetData.links = newLinks;
6463
6571
  try {
6464
6572
  await mcpMutation("chain.updateEntry", {
6465
6573
  entryId: betEntryId,
6466
- data: { rabbitHoles: updatedRisks },
6574
+ data: { links: newLinks },
6467
6575
  changeNote: `Added risk: ${item.name}`
6468
6576
  });
6469
6577
  await recordSessionActivity({ entryModified: betDocId });
@@ -6499,7 +6607,8 @@ async function computeAndUpdateScores(opts) {
6499
6607
  const alreadyCheckedOverlap = typeof refreshedData._overlapIds === "string";
6500
6608
  let overlap = [];
6501
6609
  if (!alreadyCheckedOverlap && captureItems.length === 0) {
6502
- const problemText = refreshedData.problem ?? userInput;
6610
+ const refreshedLinks = refreshedData.links ?? {};
6611
+ const problemText = refreshedLinks.problem ?? userInput;
6503
6612
  const overlapResults = await searchChain(problemText, { maxResults: 5, excludeIds: [betEntryId, ...constellationEntryIds] });
6504
6613
  overlap = overlapResults.map((r) => ({
6505
6614
  entryId: r.entryId,
@@ -6508,7 +6617,8 @@ async function computeAndUpdateScores(opts) {
6508
6617
  }));
6509
6618
  }
6510
6619
  const overlapIds = overlap.map((o) => o.entryId);
6511
- const appetiteText = refreshedData.appetite ?? "";
6620
+ const refreshedLinksForAppetite = refreshedData.links ?? {};
6621
+ const appetiteText = refreshedLinksForAppetite.appetite ?? "";
6512
6622
  const isSmallBatch = detectSmallBatch(
6513
6623
  argDimension === "appetite" ? userInput : appetiteText
6514
6624
  );
@@ -6534,15 +6644,20 @@ async function computeAndUpdateScores(opts) {
6534
6644
  const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
6535
6645
  try {
6536
6646
  const fieldUpdates = {};
6647
+ const linksUpdates = {};
6648
+ const currentLinks = refreshedData.links ?? {};
6537
6649
  const persist = (dim, field, minLen) => {
6538
- if (activeDimension === dim && !refreshedData[field] && userInput.length > minLen) {
6539
- fieldUpdates[field] = userInput;
6650
+ if (activeDimension === dim && !currentLinks[field] && userInput.length > minLen) {
6651
+ linksUpdates[field] = userInput;
6540
6652
  }
6541
6653
  };
6542
6654
  persist("problem_clarity", "problem", 50);
6543
6655
  persist("appetite", "appetite", 20);
6544
6656
  persist("architecture", "architecture", 50);
6545
6657
  persist("done_when", "done_when", 30);
6658
+ if (Object.keys(linksUpdates).length > 0) {
6659
+ fieldUpdates.links = { ...currentLinks, ...linksUpdates };
6660
+ }
6546
6661
  if (!refreshedData._overlapIds) {
6547
6662
  fieldUpdates._overlapIds = overlapIds.length > 0 ? overlapIds.join(",") : "_checked";
6548
6663
  }
@@ -6567,7 +6682,7 @@ async function computeAndUpdateScores(opts) {
6567
6682
  });
6568
6683
  }
6569
6684
  const alignment = [];
6570
- const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "bets"]);
6685
+ const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "chains"]);
6571
6686
  for (const result of chainSurfaced) {
6572
6687
  if (governanceCollections.has(result.collection)) {
6573
6688
  alignment.push({ entryId: result.entryId, relationship: `governs [${result.collection}]` });
@@ -6586,11 +6701,13 @@ async function computeAndUpdateScores(opts) {
6586
6701
  relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.name }))
6587
6702
  };
6588
6703
  buildContract = generateBuildContract(contractCtx);
6589
- if (refreshedData.buildContract !== buildContract) {
6704
+ const currentBCLinks = refreshedData.links ?? {};
6705
+ if (currentBCLinks.buildContract !== buildContract) {
6590
6706
  try {
6707
+ const bcLinks = { ...refreshedData.links ?? {}, buildContract };
6591
6708
  await mcpMutation("chain.updateEntry", {
6592
6709
  entryId: betEntryId,
6593
- data: { buildContract },
6710
+ data: { links: bcLinks },
6594
6711
  changeNote: "Updated build contract from shaping session"
6595
6712
  });
6596
6713
  await recordSessionActivity({ entryModified: refreshedBet?._id ?? betEntry._id });
@@ -6667,9 +6784,10 @@ function assembleResponse(opts) {
6667
6784
  } = opts;
6668
6785
  const studioUrl = buildStudioUrl(workspaceSlug, betEntryId);
6669
6786
  const suggested = suggestCaptures(scoringCtx, activeDimension);
6670
- const betProblem = refreshedData.problem ?? "";
6787
+ const assembleLinks = refreshedData.links ?? {};
6788
+ const betProblem = assembleLinks.problem ?? "";
6671
6789
  const responseBetName = refreshedData.description ?? betName;
6672
- 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()) ?? [];
6673
6791
  const investigationBrief = buildInvestigationBrief(phase, responseBetName, betEntryId, betProblem, elementNames) ?? void 0;
6674
6792
  const response = {
6675
6793
  version: 2,
@@ -6757,20 +6875,23 @@ async function handleRespond(args) {
6757
6875
  const result = await mcpMutation(
6758
6876
  "chain.createEntry",
6759
6877
  {
6760
- collectionSlug: "bets",
6878
+ collectionSlug: "chains",
6761
6879
  name: betName,
6762
6880
  status: "draft",
6763
6881
  data: {
6764
- problem: userInput,
6765
- appetite: "",
6766
- elements: "",
6767
- rabbitHoles: "",
6768
- noGos: "",
6769
- architecture: "",
6770
- buildContract: "",
6882
+ chainTypeId: "bet",
6771
6883
  description: `Shaping session for: ${betName}`,
6772
6884
  status: "shaping",
6773
- shapingSessionActive: true
6885
+ shapingSessionActive: true,
6886
+ links: {
6887
+ problem: userInput,
6888
+ appetite: "",
6889
+ elements: "",
6890
+ rabbitHoles: "",
6891
+ noGos: "",
6892
+ architecture: "",
6893
+ buildContract: ""
6894
+ }
6774
6895
  },
6775
6896
  createdBy: agentId ? `agent:${agentId}` : "facilitate",
6776
6897
  sessionId: agentId ?? void 0
@@ -6876,7 +6997,8 @@ async function handleScore(args) {
6876
6997
  }
6877
6998
  const constellation = await loadConstellationState(betId, betEntry._id);
6878
6999
  const betData = betEntry.data ?? {};
6879
- const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
7000
+ const scoreLinks = betData.links ?? {};
7001
+ const isSmallBatch = detectSmallBatch(scoreLinks.appetite ?? "");
6880
7002
  const scoringCtx = buildCachedScoringContext(betData, constellation);
6881
7003
  const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
6882
7004
  const dims = activeDimensions(isSmallBatch);
@@ -6979,7 +7101,8 @@ async function handleResume(args) {
6979
7101
  }
6980
7102
  const constellation = await loadConstellationState(betId, betEntry._id);
6981
7103
  const betData = betEntry.data ?? {};
6982
- const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
7104
+ const resumeLinks = betData.links ?? {};
7105
+ const isSmallBatch = detectSmallBatch(resumeLinks.appetite ?? "");
6983
7106
  const scoringCtx = buildCachedScoringContext(betData, constellation);
6984
7107
  const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
6985
7108
  const dims = activeDimensions(isSmallBatch);
@@ -7130,8 +7253,9 @@ async function handleCommitConstellation(args) {
7130
7253
  }
7131
7254
  let contradictionWarnings = [];
7132
7255
  try {
7133
- const { runContradictionCheck } = await import("./smart-capture-Q64ZXK65.js");
7134
- const descField = betData.problem ?? betData.description ?? "";
7256
+ const { runContradictionCheck } = await import("./smart-capture-P4CPOIFW.js");
7257
+ const commitLinks = betData.links ?? {};
7258
+ const descField = commitLinks.problem ?? betData.description ?? "";
7135
7259
  contradictionWarnings = await runContradictionCheck(
7136
7260
  betEntry.name ?? betId,
7137
7261
  descField
@@ -7799,7 +7923,7 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
7799
7923
  return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
7800
7924
  }
7801
7925
  const betHits = (taskContextHits ?? []).filter(
7802
- (e) => e.collectionSlug === "bets"
7926
+ (e) => e.collectionSlug === "chains"
7803
7927
  );
7804
7928
  if (betHits.length > 0) {
7805
7929
  return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
@@ -7951,73 +8075,16 @@ var interviewExtractionSchema = z14.object({
7951
8075
  keyDecisions: z14.array(z14.string()).optional().describe("Recent significant decisions made (each as a concise statement)"),
7952
8076
  tensions: z14.array(z14.string()).optional().describe("Pain points or friction the product is solving")
7953
8077
  });
7954
- function extractionToBatchEntries(extracted) {
7955
- const entries = [];
7956
- if (extracted.vision) {
7957
- entries.push({
7958
- collection: "strategy",
7959
- name: "Product Vision",
7960
- description: extracted.vision
7961
- });
7962
- }
7963
- if (extracted.audience) {
7964
- const audienceName = extracted.audience.split(/\s+/).slice(0, 6).join(" ");
7965
- entries.push({
7966
- collection: "audiences",
7967
- name: audienceName,
7968
- description: extracted.audience
7969
- });
7970
- }
7971
- if (extracted.techStack?.length) {
7972
- entries.push({
7973
- collection: "architecture",
7974
- name: "Tech Stack",
7975
- description: `Technology choices: ${extracted.techStack.join(", ")}`
7976
- });
7977
- for (const tech of extracted.techStack.slice(0, 3)) {
7978
- entries.push({ collection: "glossary", name: tech, description: `${tech} \u2014 part of the tech stack` });
7979
- }
7980
- }
7981
- if (extracted.keyTerms?.length) {
7982
- for (const term of extracted.keyTerms) {
7983
- const alreadyAdded = entries.some(
7984
- (e) => e.collection === "glossary" && e.name.toLowerCase() === term.toLowerCase()
7985
- );
7986
- if (!alreadyAdded) {
7987
- entries.push({ collection: "glossary", name: term, description: `Core domain term: ${term}` });
7988
- }
7989
- }
7990
- }
7991
- if (extracted.keyDecisions?.length) {
7992
- for (const decision of extracted.keyDecisions) {
7993
- entries.push({
7994
- collection: "decisions",
7995
- name: decision.slice(0, 80),
7996
- description: decision
7997
- });
7998
- }
7999
- }
8000
- if (extracted.tensions?.length) {
8001
- for (const tension of extracted.tensions) {
8002
- entries.push({
8003
- collection: "tensions",
8004
- name: tension.slice(0, 80),
8005
- description: tension
8006
- });
8007
- }
8008
- }
8009
- return entries;
8010
- }
8011
8078
  function getInterviewInstructions(workspaceName) {
8012
8079
  return {
8013
- 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.`,
8014
- 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.)`,
8015
- 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.)`,
8016
8083
  extractionGuidance: `After the user answers, extract:
8017
8084
  - vision: the core product purpose (required)
8018
8085
  - audience: who it's for (optional)
8019
8086
  - techStack: technologies mentioned (optional, array)
8020
- - 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)
8021
8088
  - keyDecisions: any decisions stated (optional, array)
8022
8089
  - tensions: any pain points stated (optional, array)
8023
8090
 
@@ -8028,7 +8095,7 @@ Map to batch-capture entries:
8028
8095
  - keyTerms \u2192 glossary (1 per term)
8029
8096
  - keyDecisions \u2192 decisions (1 per decision)
8030
8097
  - tensions \u2192 tensions (1 per tension)`,
8031
- 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.`,
8032
8099
  qualityNote: (
8033
8100
  // FEAT-149: Retrieval-First Proof Moment.
8034
8101
  // The aha is friction elimination ("I'll never have to explain this again"),
@@ -8048,25 +8115,73 @@ That's context you won't have to explain again."
8048
8115
 
8049
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."`
8050
8117
  ),
8051
- scanOffer: `After the proof moment, offer the codebase scan as a way to deepen the knowledge:
8052
- "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."`
8053
8120
  };
8054
8121
  }
8055
8122
  function buildInterviewResponse(workspaceName) {
8056
8123
  const instructions = getInterviewInstructions(workspaceName);
8057
8124
  return [
8058
- "## Let's activate your workspace",
8125
+ "## Let's get to know your product",
8059
8126
  "",
8060
8127
  instructions.systemPrompt,
8061
8128
  "",
8062
- "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.",
8063
8130
  "",
8064
- `**${instructions.question1}**`,
8131
+ instructions.question1,
8065
8132
  "",
8066
- "_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._"
8067
8134
  ].join("\n");
8068
8135
  }
8069
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
+
8070
8185
  // src/tools/start.ts
8071
8186
  async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
8072
8187
  if (!agentSessionId) return { oriented: false, orientationStatus: "no_session" };
@@ -8105,7 +8220,7 @@ function registerStartTools(server) {
8105
8220
  try {
8106
8221
  wsCtx = await getWorkspaceContext();
8107
8222
  } catch (e) {
8108
- errors.push(`Workspace: ${e.message}`);
8223
+ errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
8109
8224
  }
8110
8225
  if (!wsCtx) {
8111
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.");
@@ -8254,7 +8369,7 @@ function buildBlankResponse(wsCtx, sessionCtx) {
8254
8369
  }
8255
8370
  async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
8256
8371
  const stage = readiness?.stage ?? "seeded";
8257
- const score = readiness?.score;
8372
+ const score = readiness?.score ?? null;
8258
8373
  const gaps = readiness?.gaps ?? [];
8259
8374
  const lines = [
8260
8375
  `# ${wsCtx.workspaceName}`,
@@ -8262,14 +8377,8 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
8262
8377
  ""
8263
8378
  ];
8264
8379
  if (gaps.length > 0) {
8265
- lines.push("Here are the most impactful gaps to fill next:");
8266
- lines.push("");
8267
- const topGaps = gaps.slice(0, 3);
8268
- for (let i = 0; i < topGaps.length; i++) {
8269
- const gap = topGaps[i];
8270
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Describe your ${gap.label.toLowerCase()}.`;
8271
- lines.push(`${i + 1}. **${gap.label}** \u2014 ${cta}`);
8272
- }
8380
+ const gapLines = formatGapList(gaps, 3);
8381
+ lines.push(...gapLines);
8273
8382
  lines.push("");
8274
8383
  lines.push("Pick any to start \u2014 or begin with **#1** and I'll guide you through it.");
8275
8384
  } else {
@@ -8384,21 +8493,13 @@ function computeWorkspaceAge(createdAt) {
8384
8493
  const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
8385
8494
  return { ageDays, isNeglected: ageDays >= 30 };
8386
8495
  }
8387
- function pickNextAction(gaps, openTensions, priorSessions) {
8388
- if (gaps.length === 0 && openTensions.length === 0) return null;
8389
- if (gaps.length > 0) {
8390
- const gap = gaps[0];
8391
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
8392
- return { action: gap.label, cta };
8393
- }
8394
- if (openTensions.length > 0) {
8395
- const t = openTensions[0];
8396
- return {
8397
- action: `Open tension: ${t.name}`,
8398
- cta: "Want to discuss this tension or capture a decision about it?"
8399
- };
8400
- }
8401
- 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
+ };
8402
8503
  }
8403
8504
  async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8404
8505
  const wsFullCtx = await getWorkspaceContext();
@@ -8421,7 +8522,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8421
8522
  try {
8422
8523
  readiness = await mcpQuery("chain.workspaceReadiness");
8423
8524
  } catch (e) {
8424
- errors.push(`Readiness: ${e.message}`);
8525
+ errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
8425
8526
  }
8426
8527
  const lines = [];
8427
8528
  const isLowReadiness = readiness !== null && readiness.score < 50;
@@ -8441,29 +8542,42 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
8441
8542
  lines.push("Let's get your workspace active.");
8442
8543
  lines.push("");
8443
8544
  }
8444
- if (isLowReadiness) {
8445
- const nextAction = pickNextAction(readiness.gaps ?? [], openTensions, priorSessions);
8446
- if (nextAction) {
8447
- lines.push("## Recommended next step");
8448
- lines.push(`**${nextAction.action}**`);
8449
- lines.push("");
8450
- 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));
8451
8555
  lines.push("");
8452
8556
  lines.push(captureBehaviorNote);
8453
8557
  lines.push("");
8454
- const remainingGaps = (readiness.gaps ?? []).length - 1;
8558
+ const remainingGaps = gaps.length - 1;
8455
8559
  if (remainingGaps > 0 || openTensions.length > 0) {
8456
- 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);
8457
8571
  lines.push("");
8458
8572
  }
8459
8573
  }
8460
- 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._");
8461
8575
  lines.push("");
8462
8576
  } else if (isHighReadiness) {
8463
8577
  let activeBets = [];
8464
8578
  try {
8465
- const betEntries = await mcpQuery("chain.listEntries", { collectionSlug: "bets" });
8466
- 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);
8467
8581
  } catch {
8468
8582
  }
8469
8583
  if (task) {
@@ -10258,90 +10372,17 @@ function formatScanReport(result) {
10258
10372
  return lines.join("\n");
10259
10373
  }
10260
10374
 
10261
- // src/tools/health.ts
10262
- var CALL_CATEGORIES = {
10263
- "chain.getEntry": "read",
10264
- "chain.batchGetEntries": "read",
10265
- "chain.listEntries": "read",
10266
- "chain.listEntryHistory": "read",
10267
- "chain.listEntryRelations": "read",
10268
- "chain.listEntriesByLabel": "read",
10269
- "chain.searchEntries": "search",
10270
- "chain.createEntry": "write",
10271
- "chain.updateEntry": "write",
10272
- "chain.moveToCollection": "write",
10273
- "chain.createEntryRelation": "write",
10274
- "chain.applyLabel": "label",
10275
- "chain.removeLabel": "label",
10276
- "chain.createLabel": "label",
10277
- "chain.updateLabel": "label",
10278
- "chain.deleteLabel": "label",
10279
- "chain.createCollection": "write",
10280
- "chain.updateCollection": "write",
10281
- "chain.listCollections": "meta",
10282
- "chain.getCollection": "meta",
10283
- "chain.listLabels": "meta",
10284
- "resolveWorkspace": "meta"
10285
- };
10286
- function categorize(fn) {
10287
- return CALL_CATEGORIES[fn] ?? "meta";
10288
- }
10289
- function formatDuration(ms) {
10290
- if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
10291
- const mins = Math.floor(ms / 6e4);
10292
- const secs = Math.round(ms % 6e4 / 1e3);
10293
- return `${mins}m ${secs}s`;
10294
- }
10295
- function buildSessionSummary(log) {
10296
- if (log.length === 0) return "";
10297
- const byCategory = /* @__PURE__ */ new Map();
10298
- let errorCount = 0;
10299
- let writeCreates = 0;
10300
- let writeUpdates = 0;
10301
- for (const entry of log) {
10302
- const cat = categorize(entry.fn);
10303
- if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
10304
- const fnCounts = byCategory.get(cat);
10305
- fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
10306
- if (entry.status === "error") errorCount++;
10307
- if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
10308
- if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
10309
- }
10310
- const firstTs = new Date(log[0].ts).getTime();
10311
- const lastTs = new Date(log[log.length - 1].ts).getTime();
10312
- const duration = formatDuration(lastTs - firstTs);
10313
- const lines = [`# Session Summary (${duration})
10314
- `];
10315
- const categoryLabels = [
10316
- ["read", "Reads"],
10317
- ["search", "Searches"],
10318
- ["write", "Writes"],
10319
- ["label", "Labels"],
10320
- ["meta", "Meta"]
10321
- ];
10322
- for (const [cat, label] of categoryLabels) {
10323
- const fnCounts = byCategory.get(cat);
10324
- if (!fnCounts || fnCounts.size === 0) continue;
10325
- const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
10326
- const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
10327
- lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
10328
- }
10329
- lines.push(`- **Errors:** ${errorCount}`);
10330
- if (writeCreates > 0 || writeUpdates > 0) {
10331
- lines.push("");
10332
- lines.push("## Knowledge Contribution");
10333
- if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
10334
- if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
10335
- }
10336
- return lines.join("\n");
10337
- }
10375
+ // src/tools/orient.ts
10376
+ import { z as z20 } from "zod";
10338
10377
  function extractSessionEntryIds(priorSessions) {
10339
10378
  const allSeen = /* @__PURE__ */ new Set();
10340
10379
  const all = [];
10341
10380
  let lastSessionOnly = [];
10342
10381
  for (let i = 0; i < priorSessions.length; i++) {
10343
10382
  const s = priorSessions[i];
10344
- 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);
10345
10386
  if (i === 0) {
10346
10387
  lastSessionOnly = [...new Set(ids)].slice(0, 5);
10347
10388
  }
@@ -10356,445 +10397,30 @@ function extractSessionEntryIds(priorSessions) {
10356
10397
  }
10357
10398
  return { all, lastSessionOnly };
10358
10399
  }
10359
- function computeOrganisationHealth(entries) {
10360
- let agreements = 0;
10361
- let disagreements = 0;
10362
- let abstentions = 0;
10363
- const flagMap = /* @__PURE__ */ new Map();
10364
- for (const entry of entries) {
10365
- const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
10366
- const description = typeof entry.data?.description === "string" ? entry.data.description : "";
10367
- const result = classifyCollection(entry.name, description);
10368
- if (!result) {
10369
- abstentions++;
10370
- continue;
10371
- }
10372
- if (result.collection === slug) {
10373
- agreements++;
10374
- } else {
10375
- disagreements++;
10376
- if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
10377
- const suggestions = flagMap.get(slug);
10378
- suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
10379
- }
10380
- }
10381
- const opinionated = agreements + disagreements;
10382
- const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
10383
- const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
10384
- const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
10385
- const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
10386
- return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
10387
- }).sort((a, b) => b.count - a.count).slice(0, 3);
10388
- return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
10389
- }
10390
- function formatOrgHealthLines(orgHealth, maxFlags = 3) {
10391
- const lines = [];
10392
- if (orgHealth.disagreements > 0) {
10393
- lines.push(
10394
- `${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
10395
- );
10396
- for (const flag of orgHealth.flags.slice(0, maxFlags)) {
10397
- lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
10398
- }
10399
- } else if (orgHealth.reviewed > 0) {
10400
- lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
10401
- }
10402
- return lines;
10403
- }
10404
- async function fetchOrganisationHealth() {
10405
- try {
10406
- const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
10407
- if (!allEntries || allEntries.length === 0) return null;
10408
- return computeOrganisationHealth(allEntries);
10409
- } catch (err) {
10410
- process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
10411
- `);
10412
- return null;
10413
- }
10414
- }
10415
- async function handleHealthCheck() {
10416
- const start = Date.now();
10417
- const errors = [];
10418
- let workspaceId;
10419
- try {
10420
- workspaceId = await getWorkspaceId();
10421
- } catch (e) {
10422
- errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
10423
- }
10424
- let collections = [];
10425
- try {
10426
- collections = await mcpQuery("chain.listCollections");
10427
- } catch (e) {
10428
- errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
10429
- }
10430
- let totalEntries = 0;
10431
- if (collections.length > 0) {
10432
- try {
10433
- const entries = await mcpQuery("chain.listEntries", {});
10434
- totalEntries = entries.length;
10435
- } catch (e) {
10436
- errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
10437
- }
10438
- }
10439
- let wsCtx = null;
10440
- try {
10441
- wsCtx = await getWorkspaceContext();
10442
- } catch {
10443
- }
10444
- const durationMs = Date.now() - start;
10445
- const healthy = errors.length === 0;
10446
- const lines = [
10447
- `# ${healthy ? "Healthy" : "Degraded"}`,
10448
- "",
10449
- `**Workspace:** ${workspaceId ?? "unresolved"}`,
10450
- `**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
10451
- `**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
10452
- `**Collections:** ${collections.length}`,
10453
- `**Entries:** ${totalEntries}`,
10454
- `**Latency:** ${durationMs}ms`
10455
- ];
10456
- if (errors.length > 0) {
10457
- lines.push("", "## Errors");
10458
- for (const err of errors) {
10459
- lines.push(`- ${err}`);
10460
- }
10461
- }
10462
- const healthData = {
10463
- healthy,
10464
- collections: collections.length,
10465
- entries: totalEntries,
10466
- latencyMs: durationMs,
10467
- workspace: workspaceId ?? "unresolved"
10468
- };
10469
- return {
10470
- content: [{ type: "text", text: lines.join("\n") }],
10471
- structuredContent: success(
10472
- healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
10473
- healthData
10474
- )
10475
- };
10476
- }
10477
- async function handleWhoami() {
10478
- const ctx = await getWorkspaceContext();
10479
- const sessionId = getAgentSessionId();
10480
- const scope = getApiKeyScope();
10481
- const oriented = isSessionOriented();
10482
- const lines = [
10483
- `# Session Identity`,
10484
- "",
10485
- `**Workspace ID:** ${ctx.workspaceId}`,
10486
- `**Workspace Slug:** ${ctx.workspaceSlug}`,
10487
- `**Workspace Name:** ${ctx.workspaceName}`
10488
- ];
10489
- return {
10490
- content: [{ type: "text", text: lines.join("\n") }],
10491
- structuredContent: success(
10492
- `Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
10493
- { workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
10494
- )
10495
- };
10496
- }
10497
- var STAGE_LABELS = {
10498
- blank: "Blank",
10499
- seeded: "Seeded",
10500
- grounded: "Grounded",
10501
- connected: "Connected"
10502
- };
10503
- var STAGE_DESCRIPTIONS = {
10504
- blank: "No knowledge captured yet.",
10505
- seeded: "Early knowledge is in place \u2014 keep building.",
10506
- grounded: "Solid foundations \u2014 a few gaps remain.",
10507
- connected: "Well-connected knowledge graph \u2014 your Brain is useful."
10508
- };
10509
- async function handleWorkspaceStatus() {
10510
- const result = await mcpQuery("chain.workspaceReadiness");
10511
- const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
10512
- const scoringVersion = result.scoringVersion ?? "v1";
10513
- const stage = result.stage ?? "seeded";
10514
- const stageLabel = STAGE_LABELS[stage] ?? stage;
10515
- const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
10516
- const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
10517
- const lines = [
10518
- `# Brain Status: ${stageLabel}`,
10519
- `_${stageDescription}_`,
10520
- "",
10521
- `${scoreBar} ${stageLabel} \xB7 ${score}%`,
10522
- `**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
10523
- "",
10524
- "## Stats",
10525
- `- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
10526
- `- **Relations:** ${stats.totalRelations}`,
10527
- `- **Collections:** ${stats.collectionCount}`,
10528
- `- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
10529
- ""
10530
- ];
10531
- if (gaps.length > 0) {
10532
- lines.push("## Gaps");
10533
- for (const gap of gaps) {
10534
- const action = gap.capabilityGuidance ?? gap.guidance;
10535
- lines.push(`- [ ] **${gap.label}**`);
10536
- lines.push(` _${action}_`);
10537
- }
10538
- lines.push("");
10539
- }
10540
- const passed = checks.filter((c) => c.passed);
10541
- if (passed.length > 0) {
10542
- lines.push("## Passing checks");
10543
- for (const check of passed) {
10544
- lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
10545
- }
10546
- lines.push("");
10547
- }
10548
- const orgHealth = await fetchOrganisationHealth();
10549
- if (orgHealth && orgHealth.reviewed > 0) {
10550
- lines.push("## Organisation Health");
10551
- lines.push(...formatOrgHealthLines(orgHealth));
10552
- lines.push("");
10553
- }
10554
- const statusData = {
10555
- stage,
10556
- scoringVersion,
10557
- readinessScore: score,
10558
- activeEntries: stats.activeCount,
10559
- totalRelations: stats.totalRelations,
10560
- orphanedEntries: stats.orphanedCount,
10561
- gaps: gaps.map((g) => ({
10562
- id: g.id,
10563
- label: g.label,
10564
- guidance: g.capabilityGuidance ?? g.guidance
10565
- })),
10566
- ...orgHealth && { organisationHealth: orgHealth }
10567
- };
10568
- return {
10569
- content: [{ type: "text", text: lines.join("\n") }],
10570
- structuredContent: success(
10571
- `Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
10572
- statusData
10573
- )
10574
- };
10575
- }
10576
- async function handleAudit(limit) {
10577
- const log = getAuditLog();
10578
- const recent = log.slice(-limit);
10579
- if (recent.length === 0) {
10580
- return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
10581
- }
10582
- const summary = buildSessionSummary(log);
10583
- const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
10584
- `];
10585
- for (const entry of recent) {
10586
- const icon = entry.status === "ok" ? "\u2713" : "\u2717";
10587
- const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
10588
- const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
10589
- logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
10590
- }
10591
- const auditData = {
10592
- totalCalls: log.length,
10593
- calls: recent.map((entry) => ({
10594
- tool: entry.fn,
10595
- ...entry.toolContext?.action && { action: entry.toolContext.action },
10596
- timestamp: entry.ts,
10597
- ...entry.durationMs != null && { durationMs: entry.durationMs }
10598
- }))
10599
- };
10600
- return {
10601
- content: [{ type: "text", text: `${summary}
10602
-
10603
- ---
10604
-
10605
- ${logLines.join("\n")}` }],
10606
- structuredContent: success(
10607
- `Audit: ${log.length} total calls, showing last ${recent.length}.`,
10608
- auditData
10609
- )
10610
- };
10611
- }
10612
- var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
10613
- var healthSchema = z20.object({
10614
- action: z20.enum(HEALTH_ACTIONS).describe(
10615
- "'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
10616
- ),
10617
- limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
10618
- });
10619
10400
  var orientSchema = z20.object({
10620
10401
  mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
10621
10402
  task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
10622
10403
  });
10623
- var healthCheckOutputSchema = z20.object({
10624
- healthy: z20.boolean(),
10625
- collections: z20.number(),
10626
- entries: z20.number(),
10627
- latencyMs: z20.number(),
10628
- workspace: z20.string()
10629
- });
10630
- var organisationHealthSchema = z20.object({
10631
- reviewed: z20.number(),
10632
- agreements: z20.number(),
10633
- disagreements: z20.number(),
10634
- abstentions: z20.number(),
10635
- agreementRate: z20.number(),
10636
- flags: z20.array(z20.object({
10637
- collection: z20.string(),
10638
- count: z20.number(),
10639
- suggestedCollection: z20.string()
10640
- }))
10641
- });
10642
- var healthStatusOutputSchema = z20.object({
10643
- stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
10644
- scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
10645
- readinessScore: z20.number(),
10646
- activeEntries: z20.number(),
10647
- totalRelations: z20.number(),
10648
- orphanedEntries: z20.number(),
10649
- gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() })),
10650
- organisationHealth: organisationHealthSchema.optional()
10651
- });
10652
- var healthAuditOutputSchema = z20.object({
10653
- totalCalls: z20.number(),
10654
- calls: z20.array(z20.object({
10655
- tool: z20.string(),
10656
- action: z20.string().optional(),
10657
- timestamp: z20.string(),
10658
- durationMs: z20.number().optional()
10659
- }))
10660
- });
10661
- var healthWhoamiOutputSchema = z20.object({
10662
- workspaceId: z20.string(),
10663
- workspaceName: z20.string(),
10664
- scope: z20.string(),
10665
- sessionId: z20.union([z20.string(), z20.null()]),
10666
- oriented: z20.boolean()
10667
- });
10668
- var ALL_TOOL_SCHEMAS = [
10669
- { name: "entries", schema: entriesSchema },
10670
- { name: "relations", schema: relationsSchema },
10671
- { name: "graph", schema: graphSchema },
10672
- { name: "context", schema: contextSchema },
10673
- { name: "collections", schema: collectionsSchema },
10674
- { name: "session", schema: sessionSchema },
10675
- { name: "health", schema: healthSchema },
10676
- { name: "orient", schema: orientSchema },
10677
- { name: "quality", schema: qualitySchema },
10678
- { name: "workflows", schema: workflowsSchema },
10679
- { name: "session-wrapup", schema: wrapupSchema },
10680
- { name: "labels", schema: labelsSchema },
10681
- { name: "verify", schema: verifySchema },
10682
- { name: "capture", schema: captureSchema },
10683
- { name: "batch-capture", schema: batchCaptureSchema },
10684
- { name: "update-entry", schema: updateEntrySchema },
10685
- { name: "get-history", schema: getHistorySchema },
10686
- { name: "commit-entry", schema: commitEntrySchema },
10687
- { name: "start", schema: startSchema },
10688
- { name: "get-usage-summary", schema: usageSummarySchema },
10689
- { name: "chain", schema: chainSchema },
10690
- { name: "chain-version", schema: chainVersionSchema },
10691
- { name: "chain-branch", schema: chainBranchSchema },
10692
- { name: "chain-review", schema: chainReviewSchema },
10693
- { name: "create-audience-map-set", schema: createAudienceMapSetSchema },
10694
- { name: "map", schema: mapSchema },
10695
- { name: "map-slot", schema: mapSlotSchema },
10696
- { name: "map-version", schema: mapVersionSchema },
10697
- { name: "map-suggest", schema: mapSuggestSchema },
10698
- { name: "architecture", schema: architectureSchema },
10699
- { name: "architecture-admin", schema: architectureAdminSchema },
10700
- { name: "facilitate", schema: facilitateSchema }
10701
- ];
10702
- var selfTestOutputSchema = z20.object({
10703
- passed: z20.number(),
10704
- failed: z20.number(),
10705
- total: z20.number(),
10706
- results: z20.array(z20.object({
10707
- tool: z20.string(),
10708
- valid: z20.boolean(),
10709
- error: z20.string().optional()
10710
- }))
10711
- });
10712
- function handleSelfTest() {
10713
- const results = [];
10714
- for (const { name, schema } of ALL_TOOL_SCHEMAS) {
10715
- try {
10716
- if (!schema || typeof schema.safeParse !== "function") {
10717
- results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
10718
- continue;
10719
- }
10720
- const test = schema.safeParse({});
10721
- if (test.success || test.error) {
10722
- results.push({ tool: name, valid: true });
10723
- }
10724
- } catch (e) {
10725
- results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
10726
- }
10727
- }
10728
- const passed = results.filter((r) => r.valid).length;
10729
- const failed = results.filter((r) => !r.valid).length;
10730
- const total = results.length;
10731
- const lines = [
10732
- `# Self-Test: Tool Schema Validation`,
10733
- `**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
10734
- `**Schemas validated:** ${passed}/${total}`,
10735
- ""
10736
- ];
10737
- if (failed > 0) {
10738
- lines.push("## Failures");
10739
- for (const r of results.filter((r2) => !r2.valid)) {
10740
- lines.push(`- **${r.tool}**: ${r.error}`);
10741
- }
10742
- lines.push("");
10743
- }
10744
- lines.push("## All Tools");
10745
- for (const r of results) {
10746
- lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
10747
- }
10748
- return {
10749
- content: [{ type: "text", text: lines.join("\n") }],
10750
- structuredContent: success(
10751
- failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
10752
- { passed, failed, total, results }
10753
- )
10754
- };
10755
- }
10756
- function registerHealthTools(server) {
10757
- server.registerTool(
10758
- "health",
10759
- {
10760
- title: "Health",
10761
- 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.",
10762
- inputSchema: healthSchema,
10763
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
10764
- },
10765
- withEnvelope(async (args) => {
10766
- const parsed = parseOrFail(healthSchema, args);
10767
- if (!parsed.ok) return parsed.result;
10768
- const { action, limit } = parsed.data;
10769
- return runWithToolContext({ tool: "health", action }, async () => {
10770
- if (action === "check") return handleHealthCheck();
10771
- if (action === "whoami") return handleWhoami();
10772
- if (action === "status") return handleWorkspaceStatus();
10773
- if (action === "audit") return handleAudit(limit ?? 20);
10774
- if (action === "self-test") return handleSelfTest();
10775
- return unknownAction(action, HEALTH_ACTIONS);
10776
- });
10777
- })
10778
- );
10779
- server.registerTool(
10780
- "orient",
10781
- {
10782
- title: "Orient \u2014 Start Here",
10783
- description: "The single entry point for starting a session. Returns workspace context with a single recommended next action for low-readiness workspaces, or a standup-style briefing for established workspaces.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.\n\n**mode:** `full` (default) returns full context. `brief` returns only vision, bet/tension counts, readiness, active bet names, and last-session summary \u2014 use for mid-session re-orientation.\n\n**task:** Optional natural-language task description. When provided, returns task-scoped context (scored, relevant entries) in addition to standard orient sections.",
10784
- inputSchema: orientSchema,
10785
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
10786
- },
10787
- withEnvelope(async ({ mode = "full", task } = {}) => {
10788
- const errors = [];
10789
- const agentSessionId = getAgentSessionId();
10790
- if (isSessionOriented() && mode === "brief" && !task) {
10791
- return {
10792
- content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
10793
- structuredContent: success(
10794
- "Already oriented. Session active, writes unlocked.",
10795
- { alreadyOriented: true, sessionId: agentSessionId }
10796
- )
10797
- };
10404
+ function registerOrientTool(server) {
10405
+ server.registerTool(
10406
+ "orient",
10407
+ {
10408
+ title: "Orient \u2014 Start Here",
10409
+ description: "The single entry point for starting a session. Returns workspace context with a single recommended next action for low-readiness workspaces, or a standup-style briefing for established workspaces.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.\n\n**mode:** `full` (default) returns full context. `brief` returns only vision, bet/tension counts, readiness, active bet names, and last-session summary \u2014 use for mid-session re-orientation.\n\n**task:** Optional natural-language task description. When provided, returns task-scoped context (scored, relevant entries) in addition to standard orient sections.",
10410
+ inputSchema: orientSchema,
10411
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
10412
+ },
10413
+ withEnvelope(async ({ mode = "full", task } = {}) => {
10414
+ const errors = [];
10415
+ const agentSessionId = getAgentSessionId();
10416
+ if (isSessionOriented() && mode === "brief" && !task) {
10417
+ return {
10418
+ content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
10419
+ structuredContent: success(
10420
+ "Already oriented. Session active, writes unlocked.",
10421
+ { alreadyOriented: true, sessionId: agentSessionId }
10422
+ )
10423
+ };
10798
10424
  }
10799
10425
  let wsCtx = null;
10800
10426
  try {
@@ -10898,6 +10524,11 @@ function registerHealthTools(server) {
10898
10524
  lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
10899
10525
  }
10900
10526
  }
10527
+ const briefGaps = readiness?.gaps ?? [];
10528
+ if (briefGaps.length > 0) {
10529
+ const oneLiner = formatGapOneLiner(briefGaps[0]);
10530
+ if (oneLiner) lines.push(oneLiner);
10531
+ }
10901
10532
  if (recoveryBlock) {
10902
10533
  lines.push("");
10903
10534
  lines.push(...formatRecoveryBlock(recoveryBlock));
@@ -10968,29 +10599,31 @@ function registerHealthTools(server) {
10968
10599
  lines.push("");
10969
10600
  }
10970
10601
  }
10602
+ let fullCoherenceSnapshot;
10971
10603
  if (isLowReadiness) {
10972
- lines.push(`**Brain stage: ${orientStage}.**`);
10973
- lines.push("");
10974
- 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._";
10975
10605
  const gaps = readiness.gaps ?? [];
10976
10606
  if (gaps.length > 0) {
10977
- const gap = gaps[0];
10978
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
10979
- lines.push("## Recommended next step");
10980
- lines.push(`**${gap.label}**`);
10981
- lines.push("");
10982
- 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));
10983
10614
  lines.push("");
10984
10615
  lines.push(captureBehaviorNote);
10985
10616
  lines.push("");
10986
10617
  const remainingGaps = gaps.length - 1;
10987
10618
  if (remainingGaps > 0 || openTensions.length > 0) {
10988
- 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._`);
10989
10623
  lines.push("");
10990
10624
  }
10991
10625
  }
10992
- lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)?_");
10993
- 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._");
10994
10627
  lines.push("");
10995
10628
  } else if (readiness) {
10996
10629
  const fmt = (e) => {
@@ -10998,10 +10631,9 @@ function registerHealthTools(server) {
10998
10631
  const stratum = e.stratum ?? "?";
10999
10632
  return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
11000
10633
  };
11001
- let fullCoherenceSnapshot2;
11002
10634
  const fullCoherence = buildCoherenceSection();
11003
10635
  if (fullCoherence) {
11004
- fullCoherenceSnapshot2 = fullCoherence.snapshot;
10636
+ fullCoherenceSnapshot = fullCoherence.snapshot;
11005
10637
  }
11006
10638
  if (task) {
11007
10639
  lines.push(`**Brain stage: ${orientStage}.** Working on: **${task}**`);
@@ -11089,6 +10721,13 @@ function registerHealthTools(server) {
11089
10721
  orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
11090
10722
  lines.push("");
11091
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
+ }
11092
10731
  if (orientEntries.recentDecisions?.length > 0) {
11093
10732
  lines.push("## Recent decisions");
11094
10733
  orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
@@ -11168,40 +10807,535 @@ function registerHealthTools(server) {
11168
10807
  for (const err of errors) lines.push(`- ${err}`);
11169
10808
  lines.push("");
11170
10809
  }
11171
- if (agentSessionId) {
11172
- try {
11173
- await mcpCall("agent.markOriented", {
11174
- sessionId: agentSessionId,
11175
- ...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
11176
- });
11177
- setSessionOriented(true);
11178
- lines.push("---");
11179
- lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
11180
- } catch {
11181
- lines.push("---");
11182
- lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
11183
- }
11184
- try {
11185
- await mcpMutation("chain.recordSessionSignal", {
11186
- sessionId: agentSessionId,
11187
- signalType: "immediate_context_load",
11188
- metadata: { source: "orient" }
11189
- });
11190
- } catch (err) {
11191
- process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
11192
- `);
11193
- }
11194
- } else {
11195
- lines.push("---");
11196
- 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 });
11197
11285
  }
11198
- return {
11199
- content: [{ type: "text", text: lines.join("\n") }],
11200
- structuredContent: success(
11201
- `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
11202
- { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
11203
- )
11204
- };
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
+ });
11205
11339
  })
11206
11340
  );
11207
11341
  }
@@ -11650,12 +11784,197 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
11650
11784
  }
11651
11785
 
11652
11786
  // src/prompts/index.ts
11653
- 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
11654
11973
  function registerPrompts(server) {
11655
11974
  server.prompt(
11656
11975
  "review-against-rules",
11657
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.",
11658
- { 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')") },
11659
11978
  async ({ domain }) => {
11660
11979
  const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
11661
11980
  const rules = entries.filter((e) => e.data?.domain === domain);
@@ -11708,7 +12027,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
11708
12027
  server.prompt(
11709
12028
  "name-check",
11710
12029
  "Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
11711
- { 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')") },
11712
12031
  async ({ names }) => {
11713
12032
  const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
11714
12033
  const glossaryContext = terms.map(
@@ -11744,7 +12063,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
11744
12063
  server.prompt(
11745
12064
  "draft-decision-record",
11746
12065
  "Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
11747
- { 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...')") },
11748
12067
  async ({ context }) => {
11749
12068
  const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
11750
12069
  const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
@@ -11782,8 +12101,8 @@ After drafting, I can log it using the capture tool with collection "decisions".
11782
12101
  "draft-rule-from-context",
11783
12102
  "Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
11784
12103
  {
11785
- observation: z21.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
11786
- 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')")
11787
12106
  },
11788
12107
  async ({ observation, domain }) => {
11789
12108
  const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
@@ -11824,10 +12143,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
11824
12143
  "run-workflow",
11825
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.",
11826
12145
  {
11827
- workflow: z21.string().describe(
12146
+ workflow: z23.string().describe(
11828
12147
  "Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
11829
12148
  ),
11830
- context: z21.string().optional().describe(
12149
+ context: z23.string().optional().describe(
11831
12150
  "Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
11832
12151
  )
11833
12152
  },
@@ -11972,7 +12291,7 @@ ${wf.errorRecovery}
11972
12291
  "shape-a-bet",
11973
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.",
11974
12293
  {
11975
- 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')")
11976
12295
  },
11977
12296
  async ({ idea }) => {
11978
12297
  let strategicContext = "";
@@ -12133,9 +12452,9 @@ Walk away mid-session and everything captured so far exists as drafts. \`facilit
12133
12452
  "capture-and-connect",
12134
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.",
12135
12454
  {
12136
- collection: z21.string().describe("Collection slug (e.g. 'glossary', 'business-rules', 'decisions', 'tensions')"),
12137
- name: z21.string().describe("Entry name"),
12138
- 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")
12139
12458
  },
12140
12459
  async ({ collection, name, description }) => {
12141
12460
  const collections = await mcpQuery("chain.listCollections");
@@ -12189,7 +12508,7 @@ Only call \`commit-entry\` when the user explicitly confirms.
12189
12508
  "deep-dive",
12190
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.",
12191
12510
  {
12192
- 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')")
12193
12512
  },
12194
12513
  async ({ topic }) => {
12195
12514
  const isEntryId = /^[A-Z]+-\d+$/i.test(topic) || /^[A-Z]+-[a-z0-9]+$/i.test(topic);
@@ -12272,7 +12591,7 @@ ${contextInstructions}`
12272
12591
  "pre-commit-check",
12273
12592
  "Run a readiness check before committing an entry to the Chain. Validates quality score, required relations, and business rule compliance.",
12274
12593
  {
12275
- 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')")
12276
12595
  },
12277
12596
  async ({ entryId }) => {
12278
12597
  return {
@@ -12328,9 +12647,9 @@ If ready, ask the user to confirm. If not, suggest specific improvements.
12328
12647
  );
12329
12648
  server.prompt(
12330
12649
  "project-scan",
12331
- "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.",
12332
12651
  {
12333
- 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)")
12334
12653
  },
12335
12654
  async ({ workspaceContext }) => {
12336
12655
  let collectionsContext = "";
@@ -12345,17 +12664,19 @@ ${colList}
12345
12664
  }
12346
12665
  } catch {
12347
12666
  }
12348
- const schemaFields = Object.entries(interviewExtractionSchema.shape).map(([key, field]) => `- \`${key}\`: ${field.description ?? ""}`).join("\n");
12349
- const exampleExtraction = {
12350
- 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",
12351
12670
  audience: "Solo developers using Cursor, Claude Code, or similar AI IDEs",
12352
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"],
12353
12676
  keyTerms: ["Chain", "Capture", "Orientation"],
12354
- keyDecisions: ["Use Convex for real-time backend to avoid managing infrastructure"],
12355
- tensions: ["Activation requires skill \u2014 users need to know what to capture"]
12356
- };
12357
- const exampleEntries = extractionToBatchEntries(exampleExtraction);
12358
- 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);
12359
12680
  return {
12360
12681
  messages: [{
12361
12682
  role: "user",
@@ -12364,84 +12685,94 @@ ${colList}
12364
12685
  text: `# Project Scan \u2014 Agent-Side Knowledge Extraction
12365
12686
 
12366
12687
  You are scanning the user's local project to extract structured knowledge for Product Brain.
12367
- 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.
12368
12689
 
12369
12690
  ` + (workspaceContext ? `## Workspace Context
12370
12691
  ${workspaceContext}
12371
12692
 
12372
12693
  ` : "") + collectionsContext + `## Step 1: Read Project Files
12373
12694
 
12374
- Read these files in this order (skip gracefully if not present):
12375
- 1. \`README.md\` or \`readme.md\` \u2014 product description, purpose, setup
12376
- 2. \`package.json\` \u2014 name, description, dependencies (framework, database, auth)
12377
- 3. \`tsconfig.json\` or \`tsconfig.app.json\` \u2014 compilation target, paths
12378
- 4. Top-level folder structure (list directories only, max 2 levels deep)
12379
- 5. \`.cursorrules\` or \`CLAUDE.md\` or \`AGENTS.md\` \u2014 project conventions, constraints
12380
- 6. \`git log --oneline -20\` \u2014 recent commit messages for decisions/tensions
12695
+ Read these files in order. Skip gracefully if not present.
12381
12696
 
12382
- **ICP stack only (V1):** README, package.json, tsconfig, folder structure, .cursorrules/CLAUDE.md, git log.
12383
- 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.
12384
12706
 
12385
12707
  ## Step 2: Extract Structured Data
12386
12708
 
12387
- After reading, extract the following schema:
12709
+ After reading all files, extract into this schema:
12388
12710
 
12389
12711
  \`\`\`
12390
12712
  ${schemaFields}
12391
12713
  \`\`\`
12392
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
+
12393
12726
  **Quality rules (RH2 mitigation):**
12394
- - Prefer 8 good entries over 20 mediocre ones
12395
- - Only include terms you have direct evidence for from the files
12396
- - Skip fields you cannot confidently fill \u2014 empty arrays > hallucinated data
12397
- - 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
12398
12731
 
12399
- ## Step 3: Validate Before Capture
12732
+ ## Step 3: Validate Before Presenting
12400
12733
 
12401
- Before calling batch-capture, check your extraction:
12402
- - vision is present and at least 10 characters
12403
- - each entry has a non-empty name and description
12734
+ Before presenting, check each entry:
12735
+ - vision is at least 10 characters if present
12404
12736
  - no duplicate names within the same collection
12405
- - 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")
12406
12740
 
12407
- 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.
12408
12742
 
12409
- Then present the inferred entries as a numbered list. Ask which to keep, and do NOT call batch-capture yet.
12410
- Only after the user confirms which entries to keep should you capture the confirmed entries.
12743
+ ## Step 4: Present for Confirmation
12411
12744
 
12412
- ## 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.
12413
12747
 
12414
- Map your extracted data to entries using these rules:
12415
- - \`vision\` \u2192 strategy collection, name: "Product Vision"
12416
- - \`audience\` \u2192 audiences collection
12417
- - \`techStack\` \u2192 architecture collection ("Tech Stack") + top 3 items \u2192 glossary
12418
- - \`keyTerms\` \u2192 glossary collection (1 entry per term)
12419
- - \`keyDecisions\` \u2192 decisions collection (1 entry per decision)
12420
- - \`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)
12421
12760
 
12422
12761
  **Example output:**
12423
12762
  \`\`\`json
12424
12763
  ${exampleOutput}
12425
12764
  \`\`\`
12426
12765
 
12427
- ## Step 5: Call batch-capture
12428
-
12429
- After the user confirms which entries to keep, call batch-capture with only those confirmed entries.
12430
- Omit \`autoCommit\` to let normal workspace governance apply.
12431
-
12432
- \`\`\`
12433
- batch-capture entries=[...your confirmed entries...]
12434
- \`\`\`
12435
-
12436
- 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.
12437
12768
 
12438
12769
  ## Step 6: Connect + Prove Value
12439
12770
 
12440
12771
  After capture:
12441
12772
  1. Call \`graph action=suggest\` on 2\u20133 key entries (vision, architecture, a decision)
12442
- 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>"\`
12443
12774
  3. Present what Product Brain now knows in that task context \u2014 this is the proof moment
12444
- 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
12445
12776
 
12446
12777
  **Begin with Step 1 now.** Read the files, then report what you found before extracting.`
12447
12778
  }
@@ -12449,6 +12780,108 @@ After capture:
12449
12780
  };
12450
12781
  }
12451
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
+ );
12452
12885
  }
12453
12886
 
12454
12887
  // src/server.ts
@@ -12543,6 +12976,7 @@ function createProductBrainServer() {
12543
12976
  registerKnowledgeTools(server);
12544
12977
  registerLabelTools(server);
12545
12978
  registerHealthTools(server);
12979
+ registerOrientTool(server);
12546
12980
  registerVerifyTools(server);
12547
12981
  registerSmartCaptureTools(server);
12548
12982
  registerQualityTools(server);
@@ -12562,4 +12996,4 @@ export {
12562
12996
  SERVER_VERSION,
12563
12997
  createProductBrainServer
12564
12998
  };
12565
- //# sourceMappingURL=chunk-2GMFQHAF.js.map
12999
+ //# sourceMappingURL=chunk-2BHEXDVG.js.map