@productbrain/mcp 0.0.1-beta.4 → 0.0.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -191,7 +191,7 @@ function registerKnowledgeTools(server2) {
191
191
  "list-collections",
192
192
  {
193
193
  title: "Browse Collections",
194
- description: "List every knowledge collection in the workspace \u2014 glossary, business rules, tracking events, standards, etc. Returns each collection's slug, name, description, and field schema. Start here before create-entry so you know which collections exist and what fields they expect.",
194
+ description: "List every knowledge collection in the workspace \u2014 glossary, business rules, tracking events, standards, etc. Returns each collection's slug, name, description, and field schema. Start here before capture so you know which collections exist and what fields they expect.",
195
195
  annotations: { readOnlyHint: true }
196
196
  },
197
197
  async () => {
@@ -260,7 +260,7 @@ ${formatted}` }]
260
260
  "get-entry",
261
261
  {
262
262
  title: "Look Up Entry",
263
- description: "Retrieve a single knowledge entry by its human-readable ID (e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'). Returns the full record: all data fields, labels, relations, and change history. Use kb-search or list-entries first to discover entry IDs.",
263
+ description: "Retrieve a single knowledge entry by its human-readable ID (e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'). Returns the full record: all data fields, labels, relations, and change history. Use search or list-entries first to discover entry IDs.",
264
264
  inputSchema: {
265
265
  entryId: z.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'")
266
266
  },
@@ -269,7 +269,7 @@ ${formatted}` }]
269
269
  async ({ entryId }) => {
270
270
  const entry = await mcpQuery("kb.getEntry", { entryId });
271
271
  if (!entry) {
272
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try kb-search to find the right ID.` }] };
272
+ return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
273
273
  }
274
274
  const lines = [
275
275
  `# ${entry.entryId ? `${entry.entryId}: ` : ""}${entry.name}`,
@@ -307,81 +307,6 @@ ${formatted}` }]
307
307
  return { content: [{ type: "text", text: lines.join("\n") }] };
308
308
  }
309
309
  );
310
- const governedCollections = /* @__PURE__ */ new Set([
311
- "glossary",
312
- "business-rules",
313
- "principles",
314
- "standards",
315
- "strategy"
316
- ]);
317
- server2.registerTool(
318
- "create-entry",
319
- {
320
- title: "Create Entry",
321
- description: "Create a new knowledge entry. Provide the collection slug, a display name, status, and data matching the collection's field schema. Call list-collections first to see available collections and their field definitions. Governed collections (glossary, business-rules, principles, standards, strategy) require status 'draft' \u2014 to promote to 'active' or 'verified', raise a tension or use update-entry after approval.",
322
- inputSchema: {
323
- collection: z.string().describe("Collection slug, e.g. 'tracking-events', 'standards', 'glossary'"),
324
- entryId: z.string().optional().describe("Human-readable ID, e.g. 'EVT-workspace_created', 'STD-posthog-events'"),
325
- name: z.string().describe("Display name"),
326
- status: z.string().default("draft").describe("Lifecycle status: draft | active | verified | deprecated"),
327
- data: z.record(z.unknown()).describe("Data object \u2014 keys must match the collection's field definitions"),
328
- order: z.number().optional().describe("Manual sort order within the collection")
329
- },
330
- annotations: { destructiveHint: false }
331
- },
332
- async ({ collection, entryId, name, status, data, order }) => {
333
- if (governedCollections.has(collection) && status !== "draft" && status !== "deprecated") {
334
- return {
335
- content: [{
336
- type: "text",
337
- text: `# Governance Required
338
-
339
- The \`${collection}\` collection is governed. New entries must be created with status \`draft\`.
340
-
341
- **How to proceed:**
342
- 1. Create the entry with status \`draft\` (treated as a proposal)
343
- 2. Raise a tension in the \`tensions\` collection to request promotion
344
- 3. After approval, use \`update-entry\` to change status to \`active\` or \`verified\``
345
- }]
346
- };
347
- }
348
- try {
349
- const id = await mcpMutation("kb.createEntry", {
350
- collectionSlug: collection,
351
- entryId,
352
- name,
353
- status,
354
- data,
355
- order
356
- });
357
- return {
358
- content: [{ type: "text", text: `# Entry Created
359
-
360
- **${entryId ?? name}** added to \`${collection}\` as \`${status}\`.
361
-
362
- Internal ID: ${id}` }]
363
- };
364
- } catch (error) {
365
- const msg = error instanceof Error ? error.message : String(error);
366
- if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
367
- return {
368
- content: [{
369
- type: "text",
370
- text: `# Cannot Create \u2014 Duplicate Detected
371
-
372
- ${msg}
373
-
374
- **What to do:**
375
- - Use \`get-entry\` to inspect the existing entry
376
- - Use \`update-entry\` to modify it
377
- - If a genuinely new entry is needed, raise a tension to propose it`
378
- }]
379
- };
380
- }
381
- throw error;
382
- }
383
- }
384
- );
385
310
  server2.registerTool(
386
311
  "update-entry",
387
312
  {
@@ -426,7 +351,7 @@ Internal ID: ${id}` }]
426
351
  Tension status (open, in-progress, closed) must be changed through the defined process, not via MCP.
427
352
 
428
353
  **What you can do:**
429
- - Create tensions: \`create-entry collection=tensions name="..." status=open\`
354
+ - Create tensions: \`capture collection=tensions name="..." description="..."\`
430
355
  - List tensions: \`list-entries collection=tensions\`
431
356
  - Update non-status fields (raised, date, priority, description) via \`update-entry\`
432
357
  - After process approval, a human uses the Product Brain UI to change status
@@ -440,10 +365,10 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
440
365
  }
441
366
  );
442
367
  server2.registerTool(
443
- "kb-search",
368
+ "search",
444
369
  {
445
- title: "Search Knowledge Base",
446
- description: "Full-text search across all knowledge entries. Returns entry names, collection, status, and a description preview. Scope results to a specific collection (e.g. collection='business-rules') or filter by status (e.g. status='active'). Use this to discover entries before calling get-entry for full details.",
370
+ title: "Search the Chain",
371
+ description: "Full-text search across all entries on the Chain. Returns entry names, collection, status, and a description preview. Scope results to a specific collection (e.g. collection='business-rules') or filter by status (e.g. status='active'). Use this to discover entries before calling get-entry for full details.",
447
372
  inputSchema: {
448
373
  query: z.string().describe("Search text (min 2 characters)"),
449
374
  collection: z.string().optional().describe("Scope to a collection slug, e.g. 'business-rules', 'glossary', 'tracking-events'"),
@@ -564,7 +489,7 @@ ${formatted}` }]
564
489
  }
565
490
  const sourceEntry = await mcpQuery("kb.getEntry", { entryId });
566
491
  if (!sourceEntry) {
567
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try kb-search to find the right ID.` }] };
492
+ return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
568
493
  }
569
494
  const sourceInternalId = sourceEntry._id;
570
495
  const MAX_RELATIONS = 25;
@@ -623,18 +548,113 @@ ${formatted}` }]
623
548
  server2.registerTool(
624
549
  "gather-context",
625
550
  {
626
- title: "Gather Full Context",
627
- description: "Assemble the full knowledge context around an entry by traversing the knowledge graph. Returns all related entries grouped by collection \u2014 glossary terms, business rules, tensions, decisions, features, etc. Use this when starting work on a feature or investigating an area to get the complete picture in one call.\n\nExample: gather-context entryId='FEAT-001' returns all glossary terms, business rules, and tensions linked to that feature.",
551
+ title: "Gather Context",
552
+ description: "Assemble knowledge context in one call. Two modes:\n\n1. **By entry** (entryId): Traverse the knowledge graph around a specific entry. Returns all related entries grouped by collection.\n2. **By task** (task): Auto-load relevant domain knowledge for a natural-language task. Searches the chain, traverses the graph, and returns ranked entries with confidence scores.\n\nUse mode 1 when you have a specific entry ID. Use mode 2 at the start of a conversation to ground the agent in domain context before writing code or making recommendations.",
628
553
  inputSchema: {
629
- entryId: z.string().describe("Entry ID, e.g. 'FEAT-001', 'GT-019', 'BR-007'"),
630
- maxHops: z.number().min(1).max(3).default(2).describe("How many relation hops to traverse (1=direct only, 2=default, 3=wide net)")
554
+ entryId: z.string().optional().describe("Entry ID for graph traversal, e.g. 'FEAT-001', 'GT-019'"),
555
+ task: z.string().optional().describe("Natural-language task description for auto-loading relevant context"),
556
+ maxHops: z.number().min(1).max(3).default(2).describe("How many relation hops to traverse (1=direct only, 2=default, 3=wide net)"),
557
+ maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return in task mode (default 10)")
631
558
  },
632
559
  annotations: { readOnlyHint: true }
633
560
  },
634
- async ({ entryId, maxHops }) => {
561
+ async ({ entryId, task, maxHops, maxResults }) => {
562
+ if (!entryId && !task) {
563
+ return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
564
+ }
565
+ if (task && !entryId) {
566
+ await server2.sendLoggingMessage({
567
+ level: "info",
568
+ data: `Loading context for task: "${task.substring(0, 80)}..."`,
569
+ logger: "product-brain"
570
+ });
571
+ const searchTerms = task.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
572
+ const searchResults = await mcpQuery("kb.searchEntries", {
573
+ query: searchTerms
574
+ });
575
+ if (!searchResults || searchResults.length === 0) {
576
+ return {
577
+ content: [{
578
+ type: "text",
579
+ text: `# Context Loaded
580
+
581
+ **Confidence:** None
582
+
583
+ No context found for this task. The chain may not cover this area yet.
584
+
585
+ _Consider capturing domain knowledge discovered during this task via \`capture\`._`
586
+ }]
587
+ };
588
+ }
589
+ const limit = maxResults ?? 10;
590
+ const topHits = searchResults.slice(0, Math.min(limit, 5));
591
+ const allEntries = [];
592
+ const seenIds = /* @__PURE__ */ new Set();
593
+ for (const hit of topHits) {
594
+ const hitId = hit.entryId ?? hit._id;
595
+ if (!hitId || seenIds.has(hitId)) continue;
596
+ seenIds.add(hitId);
597
+ allEntries.push({
598
+ entryId: hit.entryId,
599
+ name: hit.name ?? "Untitled",
600
+ collectionName: hit.collectionName ?? hit.collectionSlug ?? "unknown",
601
+ hop: 0
602
+ });
603
+ if (hit.entryId) {
604
+ try {
605
+ const graph = await mcpQuery("kb.gatherContext", {
606
+ entryId: hit.entryId,
607
+ maxHops: maxHops ?? 2
608
+ });
609
+ if (graph?.related) {
610
+ for (const rel of graph.related) {
611
+ const relId = rel.entryId ?? rel._id;
612
+ if (relId && !seenIds.has(relId)) {
613
+ seenIds.add(relId);
614
+ allEntries.push({
615
+ entryId: rel.entryId,
616
+ name: rel.name ?? "Untitled",
617
+ collectionName: rel.collectionName ?? "unknown",
618
+ hop: rel.hop ?? 1,
619
+ relationType: rel.relationType
620
+ });
621
+ }
622
+ }
623
+ }
624
+ } catch {
625
+ }
626
+ }
627
+ if (allEntries.length >= limit) break;
628
+ }
629
+ const trimmed = allEntries.slice(0, limit);
630
+ const confidence = trimmed.length >= 5 ? "high" : trimmed.length >= 2 ? "medium" : "low";
631
+ const byCollection2 = /* @__PURE__ */ new Map();
632
+ for (const entry of trimmed) {
633
+ const key = entry.collectionName;
634
+ if (!byCollection2.has(key)) byCollection2.set(key, []);
635
+ byCollection2.get(key).push(entry);
636
+ }
637
+ const lines2 = [
638
+ `# Context Loaded`,
639
+ `**Confidence:** ${confidence.charAt(0).toUpperCase() + confidence.slice(1)}`,
640
+ `**Matched:** ${trimmed.length} entries across ${byCollection2.size} collection${byCollection2.size === 1 ? "" : "s"}`,
641
+ ""
642
+ ];
643
+ for (const [collName, entries] of byCollection2) {
644
+ lines2.push(`### ${collName} (${entries.length})`);
645
+ for (const e of entries) {
646
+ const id = e.entryId ? `**${e.entryId}:** ` : "";
647
+ const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}${e.relationType ? `, ${e.relationType}` : ""})_` : "";
648
+ lines2.push(`- ${id}${e.name}${hopLabel}`);
649
+ }
650
+ lines2.push("");
651
+ }
652
+ lines2.push(`_Use \`get-entry\` for full details on any entry._`);
653
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
654
+ }
635
655
  const result = await mcpQuery("kb.gatherContext", { entryId, maxHops });
636
656
  if (!result?.root) {
637
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try kb-search to find the right ID.` }] };
657
+ return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
638
658
  }
639
659
  if (result.related.length === 0) {
640
660
  return {
@@ -676,7 +696,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
676
696
  "suggest-links",
677
697
  {
678
698
  title: "Suggest Links",
679
- description: "Discover potential connections for an entry by scanning the knowledge base for related content. Returns ranked suggestions based on text similarity \u2014 review them and use relate-entries to create the ones that make sense.\n\nThis is a discovery tool, not auto-linking. Always review suggestions before linking.",
699
+ description: "Discover potential connections for an entry by scanning the chain for related content. Returns ranked suggestions based on text similarity \u2014 review them and use relate-entries to create the ones that make sense.\n\nThis is a discovery tool, not auto-linking. Always review suggestions before linking.",
680
700
  inputSchema: {
681
701
  entryId: z.string().describe("Entry ID to find suggestions for, e.g. 'FEAT-001'"),
682
702
  limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return")
@@ -686,7 +706,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
686
706
  async ({ entryId, limit }) => {
687
707
  const entry = await mcpQuery("kb.getEntry", { entryId });
688
708
  if (!entry) {
689
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try kb-search to find the right ID.` }] };
709
+ return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
690
710
  }
691
711
  const searchTerms = [entry.name];
692
712
  if (entry.data?.description) searchTerms.push(entry.data.description);
@@ -699,7 +719,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
699
719
  }
700
720
  const results = await mcpQuery("kb.searchEntries", { query: queryText });
701
721
  if (!results || results.length === 0) {
702
- return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The knowledge base may need more entries.` }] };
722
+ return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The chain may need more entries.` }] };
703
723
  }
704
724
  const existingRelations = await mcpQuery("kb.listEntryRelations", { entryId });
705
725
  const relatedIds = new Set(
@@ -734,273 +754,59 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
734
754
  return { content: [{ type: "text", text: lines.join("\n") }] };
735
755
  }
736
756
  );
737
- server2.registerTool(
738
- "quick-capture",
739
- {
740
- title: "Quick Capture",
741
- description: "Quickly capture a knowledge entry with just a name and description \u2014 no need to look up the full field schema first. Always creates as 'draft' status with sensible defaults for remaining fields. Use update-entry later to fill in details. Embodies 'Capture Now, Curate Later' (PRI-81cbdq).",
742
- inputSchema: {
743
- collection: z.string().describe("Collection slug, e.g. 'business-rules', 'glossary', 'tensions', 'decisions'"),
744
- name: z.string().describe("Display name for the entry"),
745
- description: z.string().describe("Short description \u2014 the essential context to capture now"),
746
- entryId: z.string().optional().describe("Optional human-readable ID (e.g. 'SOS-020', 'GT-031')")
747
- },
748
- annotations: { destructiveHint: false }
749
- },
750
- async ({ collection, name, description, entryId }) => {
751
- const col = await mcpQuery("kb.getCollection", { slug: collection });
752
- if (!col) {
753
- return { content: [{ type: "text", text: `Collection \`${collection}\` not found. Use list-collections to see available collections.` }] };
754
- }
755
- const data = {};
756
- const emptyFields = [];
757
- for (const field of col.fields ?? []) {
758
- const key = field.key;
759
- if (key === "description" || key === "canonical" || key === "detail") {
760
- data[key] = description;
761
- } else if (field.type === "array" || field.type === "multi-select") {
762
- data[key] = [];
763
- emptyFields.push(key);
764
- } else if (field.type === "select") {
765
- data[key] = field.options?.[0] ?? "";
766
- emptyFields.push(key);
767
- } else {
768
- data[key] = "";
769
- emptyFields.push(key);
770
- }
771
- }
772
- if (!data.description && !data.canonical && !data.detail) {
773
- data.description = description;
774
- }
775
- try {
776
- const id = await mcpMutation("kb.createEntry", {
777
- collectionSlug: collection,
778
- entryId,
779
- name,
780
- status: "draft",
781
- data
782
- });
783
- const emptyNote = emptyFields.length > 0 ? `
784
-
785
- **Fields to fill later** (via \`update-entry\`):
786
- ${emptyFields.map((f) => `- \`${f}\``).join("\n")}` : "";
787
- return {
788
- content: [{
789
- type: "text",
790
- text: `# Quick Capture \u2014 Done
791
-
792
- **${entryId ?? name}** added to \`${collection}\` as \`draft\`.
793
-
794
- Internal ID: ${id}${emptyNote}
795
-
796
- _Use \`update-entry\` to fill in details when ready. Use \`get-entry\` to review._`
797
- }]
798
- };
799
- } catch (error) {
800
- const msg = error instanceof Error ? error.message : String(error);
801
- if (msg.includes("Duplicate") || msg.includes("already exists")) {
802
- return {
803
- content: [{
804
- type: "text",
805
- text: `# Cannot Capture \u2014 Duplicate Detected
806
-
807
- ${msg}
808
-
809
- Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
810
- }]
811
- };
812
- }
813
- throw error;
814
- }
815
- }
816
- );
817
- server2.registerTool(
818
- "load-context-for-task",
819
- {
820
- title: "Load Context for Task",
821
- description: "Auto-load relevant domain knowledge for a task in a single call. Pass a natural-language task description; the tool searches the KB, traverses the knowledge graph, and returns a ranked set of entries (business rules, glossary terms, decisions, features, etc.) grouped by collection with a confidence score.\n\nUse this at the start of a conversation to ground the agent in domain context before writing code or making recommendations.\n\nConfidence levels:\n- high: 3+ direct KB matches \u2014 strong domain coverage\n- medium: 1-2 direct matches \u2014 partial coverage, may want to drill deeper\n- low: no direct matches but related entries found via graph traversal\n- none: no relevant entries found \u2014 KB may not cover this area yet",
822
- inputSchema: {
823
- taskDescription: z.string().describe("Natural-language description of the task or user message"),
824
- maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return (default 10)"),
825
- maxHops: z.number().min(1).max(3).default(2).optional().describe("Graph traversal depth from each search hit (default 2)")
826
- },
827
- annotations: { readOnlyHint: true, openWorldHint: true }
828
- },
829
- async ({ taskDescription, maxResults, maxHops }) => {
830
- await server2.sendLoggingMessage({
831
- level: "info",
832
- data: `Loading context for task: "${taskDescription.substring(0, 80)}..."`,
833
- logger: "product-brain"
834
- });
835
- const result = await mcpQuery("kb.loadContextForTask", {
836
- taskDescription,
837
- maxResults: maxResults ?? 10,
838
- maxHops: maxHops ?? 2
839
- });
840
- if (result.confidence === "none" || result.entries.length === 0) {
841
- return {
842
- content: [{
843
- type: "text",
844
- text: `# Context Loaded
845
-
846
- **Confidence:** None
847
-
848
- No KB context found for this task. The knowledge base may not cover this area yet.
849
-
850
- _Consider capturing domain knowledge discovered during this task via \`smart-capture\`._`
851
- }]
852
- };
853
- }
854
- const byCollection = /* @__PURE__ */ new Map();
855
- for (const entry of result.entries) {
856
- const key = entry.collectionName;
857
- if (!byCollection.has(key)) byCollection.set(key, []);
858
- byCollection.get(key).push(entry);
859
- }
860
- const lines = [
861
- `# Context Loaded`,
862
- `**Confidence:** ${result.confidence.charAt(0).toUpperCase() + result.confidence.slice(1)}`,
863
- `**Matched:** ${result.entries.length} entries across ${byCollection.size} collection${byCollection.size === 1 ? "" : "s"}`,
864
- ""
865
- ];
866
- for (const [collName, entries] of byCollection) {
867
- lines.push(`### ${collName} (${entries.length})`);
868
- for (const e of entries) {
869
- const id = e.entryId ? `**${e.entryId}:** ` : "";
870
- const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}${e.relationType ? `, ${e.relationType}` : ""})_` : "";
871
- const preview = e.descriptionPreview ? `
872
- ${e.descriptionPreview}` : "";
873
- const codePaths = e.codePaths.length > 0 ? `
874
- Code: ${e.codePaths.join(", ")}` : "";
875
- lines.push(`- ${id}${e.name}${hopLabel}${preview}${codePaths}`);
876
- }
877
- lines.push("");
878
- }
879
- lines.push(`_Use \`get-entry\` for full details on any entry._`);
880
- return {
881
- content: [{ type: "text", text: lines.join("\n") }]
882
- };
883
- }
884
- );
885
- server2.registerTool(
886
- "review-rules",
887
- {
888
- title: "Review Business Rules",
889
- description: "Surface all active business rules for a domain, formatted for compliance review. Use when reviewing code, designs, or decisions against Product Brain governance. Optionally provide context (what you're building or reviewing) to help focus the review. This is the tool form of the review-against-rules prompt.",
890
- inputSchema: {
891
- domain: z.string().describe("Business rule domain, e.g. 'AI & MCP Integration', 'Governance & Decision-Making'"),
892
- context: z.string().optional().describe("What you're reviewing \u2014 code change, design decision, file path, etc.")
893
- },
894
- annotations: { readOnlyHint: true }
895
- },
896
- async ({ domain, context }) => {
897
- const entries = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
898
- const domainLower = domain.toLowerCase();
899
- const rules = entries.filter((e) => {
900
- const ruleDomain = e.data?.domain ?? "";
901
- return ruleDomain.toLowerCase() === domainLower || ruleDomain.toLowerCase().includes(domainLower);
902
- });
903
- if (rules.length === 0) {
904
- const allDomains = [...new Set(entries.map((e) => e.data?.domain).filter(Boolean))];
905
- return {
906
- content: [{
907
- type: "text",
908
- text: `# No Rules Found for "${domain}"
909
-
910
- Available domains:
911
- ${allDomains.map((d) => `- ${d}`).join("\n")}
912
-
913
- _Try one of the domains above, or use kb-search to find rules by keyword._`
914
- }]
915
- };
916
- }
917
- const header = context ? `# Business Rules: ${domain}
918
-
919
- **Review context:** ${context}
920
-
921
- For each rule, assess: compliant, at risk, violation, or not applicable.
922
- ` : `# Business Rules: ${domain}
923
- `;
924
- const formatted = rules.map((r) => {
925
- const id = r.entryId ? `**${r.entryId}:** ` : "";
926
- const severity = r.data?.severity ? ` | Severity: ${r.data.severity}` : "";
927
- const desc = r.data?.description ?? "";
928
- const impact = r.data?.dataImpact ? `
929
- Data impact: ${r.data.dataImpact}` : "";
930
- const related = (r.data?.relatedRules ?? []).length > 0 ? `
931
- Related: ${r.data.relatedRules.join(", ")}` : "";
932
- return `### ${id}${r.name} \`${r.status}\`${severity}
933
-
934
- ${desc}${impact}${related}`;
935
- }).join("\n\n---\n\n");
936
- const footer = `
937
- _${rules.length} rule${rules.length === 1 ? "" : "s"} in this domain. Use \`get-entry\` for full details. Use the \`draft-rule-from-context\` prompt to propose new rules._`;
938
- return {
939
- content: [{ type: "text", text: `${header}
940
- ${formatted}
941
- ${footer}` }]
942
- };
943
- }
944
- );
945
757
  }
946
758
 
947
759
  // src/tools/labels.ts
948
760
  import { z as z2 } from "zod";
949
761
  function registerLabelTools(server2) {
950
762
  server2.registerTool(
951
- "list-labels",
952
- {
953
- title: "Browse Labels",
954
- description: "List all workspace labels with their groups and hierarchy. Labels can be applied to any entry across any collection for cross-domain filtering. Similar to labels in Linear or GitHub.",
955
- annotations: { readOnlyHint: true }
956
- },
957
- async () => {
958
- const labels = await mcpQuery("kb.listLabels");
959
- if (labels.length === 0) {
960
- return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
961
- }
962
- const groups = labels.filter((l) => l.isGroup);
963
- const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
964
- const children = (parentId) => labels.filter((l) => l.parentId === parentId);
965
- const lines = ["# Workspace Labels"];
966
- for (const group of groups) {
967
- lines.push(`
968
- ## ${group.name}`);
969
- if (group.description) lines.push(`_${group.description}_`);
970
- for (const child of children(group._id)) {
971
- const color = child.color ? ` ${child.color}` : "";
972
- lines.push(` - \`${child.slug}\` ${child.name}${color}`);
973
- }
974
- }
975
- if (ungrouped.length > 0) {
976
- lines.push("\n## Ungrouped");
977
- for (const label of ungrouped) {
978
- const color = label.color ? ` ${label.color}` : "";
979
- lines.push(`- \`${label.slug}\` ${label.name}${color}${label.description ? ` \u2014 _${label.description}_` : ""}`);
980
- }
981
- }
982
- return {
983
- content: [{ type: "text", text: lines.join("\n") }]
984
- };
985
- }
986
- );
987
- server2.registerTool(
988
- "manage-labels",
763
+ "labels",
989
764
  {
990
- title: "Manage Labels",
991
- description: "Create, update, or delete a workspace label. Labels support hierarchy (groups with children). Use list-labels first to see what exists.",
765
+ title: "Labels",
766
+ description: "Manage workspace labels \u2014 list, create, update, delete, apply to entries, or remove from entries. Labels can be applied to any entry across any collection for cross-domain filtering. Similar to labels in Linear or GitHub. Labels support hierarchy (groups with children).",
992
767
  inputSchema: {
993
- action: z2.enum(["create", "update", "delete"]).describe("What to do"),
994
- slug: z2.string().describe("Label slug (machine name), e.g. 'p1-critical', 'needs-review'"),
768
+ action: z2.enum(["list", "create", "update", "delete", "apply", "remove"]).describe("Action: list all labels, create/update/delete a label, or apply/remove a label on an entry"),
769
+ slug: z2.string().optional().describe("Label slug (required for create/update/delete/apply/remove)"),
995
770
  name: z2.string().optional().describe("Display name (required for create)"),
996
771
  color: z2.string().optional().describe("Hex color, e.g. '#ef4444'"),
997
772
  description: z2.string().optional().describe("What this label means"),
998
773
  parentSlug: z2.string().optional().describe("Parent group slug for label hierarchy"),
999
774
  isGroup: z2.boolean().optional().describe("True if this is a group container, not a taggable label"),
1000
- order: z2.number().optional().describe("Sort order within its group")
775
+ order: z2.number().optional().describe("Sort order within its group"),
776
+ entryId: z2.string().optional().describe("Entry ID for apply/remove actions")
1001
777
  }
1002
778
  },
1003
- async ({ action, slug, name, color, description, parentSlug, isGroup, order }) => {
779
+ async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
780
+ if (action === "list") {
781
+ const labels = await mcpQuery("kb.listLabels");
782
+ if (labels.length === 0) {
783
+ return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
784
+ }
785
+ const groups = labels.filter((l) => l.isGroup);
786
+ const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
787
+ const children = (parentId) => labels.filter((l) => l.parentId === parentId);
788
+ const lines = ["# Workspace Labels"];
789
+ for (const group of groups) {
790
+ lines.push(`
791
+ ## ${group.name}`);
792
+ if (group.description) lines.push(`_${group.description}_`);
793
+ for (const child of children(group._id)) {
794
+ const c = child.color ? ` ${child.color}` : "";
795
+ lines.push(` - \`${child.slug}\` ${child.name}${c}`);
796
+ }
797
+ }
798
+ if (ungrouped.length > 0) {
799
+ lines.push("\n## Ungrouped");
800
+ for (const label of ungrouped) {
801
+ const c = label.color ? ` ${label.color}` : "";
802
+ lines.push(`- \`${label.slug}\` ${label.name}${c}${label.description ? ` \u2014 _${label.description}_` : ""}`);
803
+ }
804
+ }
805
+ return { content: [{ type: "text", text: lines.join("\n") }] };
806
+ }
807
+ if (!slug) {
808
+ return { content: [{ type: "text", text: "A `slug` is required for this action." }] };
809
+ }
1004
810
  if (action === "create") {
1005
811
  if (!name) {
1006
812
  return { content: [{ type: "text", text: "Cannot create a label without a name." }] };
@@ -1010,7 +816,7 @@ function registerLabelTools(server2) {
1010
816
  const labels = await mcpQuery("kb.listLabels");
1011
817
  const parent = labels.find((l) => l.slug === parentSlug);
1012
818
  if (!parent) {
1013
- return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use list-labels to see available groups.` }] };
819
+ return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
1014
820
  }
1015
821
  parentId = parent._id;
1016
822
  }
@@ -1031,27 +837,18 @@ function registerLabelTools(server2) {
1031
837
 
1032
838
  \`${slug}\` removed from all entries and deleted.` }] };
1033
839
  }
1034
- return { content: [{ type: "text", text: "Unknown action." }] };
1035
- }
1036
- );
1037
- server2.registerTool(
1038
- "label-entry",
1039
- {
1040
- title: "Tag Entry with Label",
1041
- description: "Apply or remove a label from a knowledge entry. Labels work across all collections for cross-domain filtering. Use list-labels to see available label slugs.",
1042
- inputSchema: {
1043
- action: z2.enum(["apply", "remove"]).describe("Apply or remove the label"),
1044
- entryId: z2.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'EVT-workspace_created'"),
1045
- label: z2.string().describe("Label slug to apply/remove")
1046
- }
1047
- },
1048
- async ({ action, entryId, label }) => {
1049
- if (action === "apply") {
1050
- await mcpMutation("kb.applyLabel", { entryId, labelSlug: label });
1051
- return { content: [{ type: "text", text: `Label \`${label}\` applied to **${entryId}**.` }] };
840
+ if (action === "apply" || action === "remove") {
841
+ if (!entryId) {
842
+ return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
843
+ }
844
+ if (action === "apply") {
845
+ await mcpMutation("kb.applyLabel", { entryId, labelSlug: slug });
846
+ return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
847
+ }
848
+ await mcpMutation("kb.removeLabel", { entryId, labelSlug: slug });
849
+ return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
1052
850
  }
1053
- await mcpMutation("kb.removeLabel", { entryId, labelSlug: label });
1054
- return { content: [{ type: "text", text: `Label \`${label}\` removed from **${entryId}**.` }] };
851
+ return { content: [{ type: "text", text: "Unknown action." }] };
1055
852
  }
1056
853
  );
1057
854
  }
@@ -1185,7 +982,7 @@ function registerHealthTools(server2) {
1185
982
  "mcp-audit",
1186
983
  {
1187
984
  title: "Session Audit Log",
1188
- description: "Show a session summary (reads, writes, searches, contributions) and the last N backend calls. Useful for debugging, tracing tool behavior, and seeing what you contributed to the knowledge base.",
985
+ description: "Show a session summary (reads, writes, searches, contributions) and the last N backend calls. Useful for debugging, tracing tool behavior, and seeing what you contributed to the chain.",
1189
986
  inputSchema: {
1190
987
  limit: z3.number().min(1).max(50).default(20).describe("How many recent calls to show (max 50)")
1191
988
  },
@@ -1367,7 +1164,7 @@ function registerVerifyTools(server2) {
1367
1164
  server2.registerTool(
1368
1165
  "verify",
1369
1166
  {
1370
- title: "Verify Knowledge Base",
1167
+ title: "Verify the Chain",
1371
1168
  description: "Verify knowledge entries against the actual codebase. Checks glossary code mappings (do referenced files and schema fields still exist?) and validates cross-references (do relatedRules point to real entries?). Produces a trust report with a trust score. Use mode='fix' to auto-update drifted codeMapping statuses.",
1372
1169
  inputSchema: {
1373
1170
  collection: z4.string().default("glossary").describe("Collection slug to verify (default: glossary)"),
@@ -1490,7 +1287,7 @@ function registerVerifyTools(server2) {
1490
1287
  import { z as z5 } from "zod";
1491
1288
  var AREA_KEYWORDS = {
1492
1289
  "Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
1493
- "Knowledge Base": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph"],
1290
+ "Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
1494
1291
  "AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
1495
1292
  "Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
1496
1293
  "Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
@@ -1530,7 +1327,7 @@ var COMMON_CHECKS = {
1530
1327
  id: "has-relations",
1531
1328
  label: "At least 1 relation created",
1532
1329
  check: (ctx) => ctx.linksCreated.length >= 1,
1533
- suggestion: () => "Use `suggest-links` and `relate-entries` to connect this entry to related knowledge."
1330
+ suggestion: () => "Use `suggest-links` and `relate-entries` to add more connections."
1534
1331
  },
1535
1332
  diverseRelations: {
1536
1333
  id: "diverse-relations",
@@ -1644,7 +1441,7 @@ var PROFILES = /* @__PURE__ */ new Map([
1644
1441
  if (area) {
1645
1442
  const categoryMap = {
1646
1443
  "Architecture": "Platform & Architecture",
1647
- "Knowledge Base": "Knowledge Management",
1444
+ "Chain": "Knowledge Management",
1648
1445
  "AI & MCP Integration": "AI & Developer Tools",
1649
1446
  "Developer Experience": "AI & Developer Tools",
1650
1447
  "Governance & Decision-Making": "Governance & Process",
@@ -1836,7 +1633,7 @@ async function checkEntryQuality(entryId) {
1836
1633
  const entry = await mcpQuery("kb.getEntry", { entryId });
1837
1634
  if (!entry) {
1838
1635
  return {
1839
- text: `Entry \`${entryId}\` not found. Try kb-search to find the right ID.`,
1636
+ text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
1840
1637
  quality: { score: 0, maxScore: 10, checks: [] }
1841
1638
  };
1842
1639
  }
@@ -1897,10 +1694,10 @@ var MAX_AUTO_LINKS = 5;
1897
1694
  var MAX_SUGGESTIONS = 5;
1898
1695
  function registerSmartCaptureTools(server2) {
1899
1696
  server2.registerTool(
1900
- "smart-capture",
1697
+ "capture",
1901
1698
  {
1902
- title: "Smart Capture",
1903
- description: "One-call knowledge capture: creates an entry, auto-links related entries, and returns a quality scorecard. Replaces the multi-step workflow of create-entry + suggest-links + relate-entries. Provide a collection, name, and description \u2014 everything else is inferred or auto-filled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions.\nAll other collections use sensible defaults (same as quick-capture + auto-linking).\n\nAlways creates as 'draft' for governed collections. Embodies 'Capture Now, Curate Later' (PRI-81cbdq).",
1699
+ title: "Capture",
1700
+ description: "The single tool for creating knowledge entries. Creates an entry, auto-links related entries, and returns a quality scorecard \u2014 all in one call. Provide a collection, name, and description \u2014 everything else is inferred or auto-filled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features.\nAll other collections use sensible defaults.\n\nAlways creates as 'draft' for governed collections. Use `update-entry` for post-creation adjustments.",
1904
1701
  inputSchema: {
1905
1702
  collection: z5.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
1906
1703
  name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
@@ -1968,7 +1765,7 @@ function registerSmartCaptureTools(server2) {
1968
1765
  name,
1969
1766
  status,
1970
1767
  data,
1971
- createdBy: "smart-capture"
1768
+ createdBy: "capture"
1972
1769
  });
1973
1770
  } catch (error) {
1974
1771
  const msg = error instanceof Error ? error.message : String(error);
@@ -2228,7 +2025,7 @@ var SEED_NODES = [
2228
2025
  { entryId: "ARCH-node-posthog", name: "PostHog", order: 1, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F4CA}", description: "Product analytics \u2014 workspace-scoped events, feature flags, session replay", filePaths: "src/lib/analytics.ts, src/lib/components/PostHogWorkspaceSync.svelte", owner: "Platform", rationale: "Infrastructure because PostHog is analytics plumbing \u2014 event collection and aggregation. It has no knowledge of business domains." } },
2229
2026
  { entryId: "ARCH-node-openrouter", name: "OpenRouter", order: 2, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F916}", description: "AI model routing for ChainWork artifact generation \u2014 streaming responses with format-aware prompts", filePaths: "src/routes/api/chainwork/generate/+server.ts", owner: "ChainWork", rationale: "Infrastructure because OpenRouter is an AI model gateway \u2014 it routes prompts to models. The strategy logic lives in ChainWork Engine (Core); this is just the pipe." } },
2230
2027
  // Core layer
2231
- { entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "20+ tools exposing the knowledge graph as AI-consumable operations \u2014 smart-capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 smart-capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
2028
+ { entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "~19 modular tools exposing the knowledge graph as AI-consumable operations \u2014 capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
2232
2029
  { entryId: "ARCH-node-knowledge-graph", name: "Knowledge Graph", order: 1, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F578}\uFE0F", description: "20 collections, 170+ entries with typed cross-collection relations. Smart capture, auto-linking, quality scoring", filePaths: "convex/mcpKnowledge.ts, convex/entries.ts", owner: "Knowledge", rationale: "Core layer because the knowledge graph IS the domain model \u2014 collections, entries, relations, versioning, quality scoring. Glossary DATA, business rules DATA, tension DATA all live here. Why not Features? Because the data model exists independently of any page. The Glossary page in Features is just one way to visualize terms that Core owns. Think: Core owns the dictionary, Features owns the dictionary app." } },
2233
2030
  { entryId: "ARCH-node-governance", name: "Governance Engine", order: 2, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u2696\uFE0F", description: "Circles, roles, consent-based decision-making, tension processing with IDM-inspired async workflows", filePaths: "convex/versioning.ts, src/lib/components/versioning/", owner: "Governance", rationale: "Core layer because governance logic (draft\u2192publish workflows, consent-based decisions, tension status rules) is business process that multiple UIs consume. Why not Features? Because the versioning system and proposal flow are reusable engines \u2014 the Governance Pages in Features are just one rendering of these rules." } },
2234
2031
  { entryId: "ARCH-node-chainwork-engine", name: "ChainWork Engine", order: 3, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u26D3", description: "Guided strategy creation through 5-step coherence chain \u2014 AI-generated artifacts with scoring and achievements", filePaths: "src/lib/components/chainwork/config.ts, src/lib/components/chainwork/scoring.ts", owner: "ChainWork", rationale: "Core layer because the coherence chain logic, scoring algorithm, and quality gates are business rules. config.ts defines chain steps, scoring.ts computes quality \u2014 these could power a CLI or API. Why not Features? Because the ChainWork UI wizard in Features is just one skin over this engine." } },
@@ -2246,7 +2043,7 @@ var SEED_NODES = [
2246
2043
  { entryId: "ARCH-node-linear", name: "Linear (planned)", order: 2, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F4D0}", description: "Issue tracking, roadmap sync, tension-to-issue pipeline (planned integration)", owner: "Platform", rationale: "Integration layer because Linear is an external issue tracker. Why not Infrastructure? Because Infra is generic plumbing we run. Linear is a third-party tool we connect to. The planned pipeline bridges tensions (Core) to Linear issues \u2014 a classic outward integration pattern." } }
2247
2044
  ];
2248
2045
  var SEED_FLOWS = [
2249
- { entryId: "ARCH-flow-smart-capture", name: "Smart Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls smart-capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
2046
+ { entryId: "ARCH-flow-smart-capture", name: "Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
2250
2047
  { entryId: "ARCH-flow-governance", name: "Governance Flow", order: 1, data: { archType: "flow", sourceNode: "ARCH-node-tensions", targetNode: "ARCH-node-governance", description: "Tension raised \u2192 appears in Command Center \u2192 triaged \u2192 processed via IDM \u2192 decision logged", color: "#6366f1" } },
2251
2048
  { entryId: "ARCH-flow-chainwork", name: "ChainWork Strategy Flow", order: 2, data: { archType: "flow", sourceNode: "ARCH-node-chainwork-ui", targetNode: "ARCH-node-artifacts", description: "Leader opens ChainWork \u2192 walks coherence chain \u2192 AI generates artifact \u2192 scored and published to knowledge graph", color: "#f59e0b" } },
2252
2049
  { entryId: "ARCH-flow-knowledge-trust", name: "Knowledge Trust Flow", order: 3, data: { archType: "flow", sourceNode: "ARCH-node-mcp", targetNode: "ARCH-node-glossary", description: "MCP verify tool checks entries against codebase \u2192 file existence, schema references validated \u2192 trust scores updated", color: "#22c55e" } },
@@ -2254,247 +2051,208 @@ var SEED_FLOWS = [
2254
2051
  ];
2255
2052
  function registerArchitectureTools(server2) {
2256
2053
  server2.registerTool(
2257
- "show-architecture",
2054
+ "architecture",
2258
2055
  {
2259
- title: "Show Architecture",
2260
- description: "Render the system architecture map \u2014 layered visualization showing where every component lives. Returns layers (Auth \u2192 Infrastructure \u2192 Core \u2192 Features \u2192 Integration) with component nodes and data flows. Optionally filter by a specific template.",
2056
+ title: "Architecture",
2057
+ description: "Explore the system architecture \u2014 show the full map, explore a specific layer, or visualize a data flow.\n\nActions:\n- `show`: Render the layered architecture map (Auth \u2192 Infra \u2192 Core \u2192 Features \u2192 Integration)\n- `explore`: Drill into a layer to see nodes, ownership, file paths\n- `flow`: Visualize a data flow path between nodes",
2261
2058
  inputSchema: {
2262
- template: z6.string().optional().describe("Template entry ID to filter by (default: first available template)")
2059
+ action: z6.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
2060
+ template: z6.string().optional().describe("Template entry ID to filter by (for show)"),
2061
+ layer: z6.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
2062
+ flow: z6.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
2263
2063
  },
2264
2064
  annotations: { readOnlyHint: true }
2265
2065
  },
2266
- async ({ template }) => {
2066
+ async ({ action, template, layer, flow }) => {
2267
2067
  await ensureCollection();
2268
2068
  const all = await listArchEntries();
2269
- const templates = byTag(all, "template");
2270
- const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
2271
- const templateName = activeTemplate?.name ?? "System Architecture";
2272
- const templateId = activeTemplate?.entryId;
2273
- const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
2274
- const nodes = byTag(all, "node");
2275
- const flows = byTag(all, "flow");
2276
- if (layers.length === 0) {
2277
- return {
2278
- content: [{
2279
- type: "text",
2280
- text: "# Architecture Explorer\n\nNo architecture data found. Use `seed-architecture` to populate the default Product OS architecture."
2281
- }]
2282
- };
2283
- }
2284
- const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
2285
- const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
2286
- (f) => `- **${f.name}**: ${f.data?.description ?? ""}`
2287
- ).join("\n") : "";
2288
- const text = `# ${templateName}
2069
+ if (action === "show") {
2070
+ const templates = byTag(all, "template");
2071
+ const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
2072
+ const templateName = activeTemplate?.name ?? "System Architecture";
2073
+ const templateId = activeTemplate?.entryId;
2074
+ const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
2075
+ const nodes = byTag(all, "node");
2076
+ const flows = byTag(all, "flow");
2077
+ if (layers.length === 0) {
2078
+ return {
2079
+ content: [{
2080
+ type: "text",
2081
+ text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
2082
+ }]
2083
+ };
2084
+ }
2085
+ const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
2086
+ const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
2087
+ (f) => `- **${f.name}**: ${f.data?.description ?? ""}`
2088
+ ).join("\n") : "";
2089
+ const text = `# ${templateName}
2289
2090
 
2290
2091
  ${textLayers}${textFlows}`;
2291
- const html = renderArchitectureHtml(layers, nodes, flows, templateName);
2292
- return {
2293
- content: [
2294
- { type: "text", text },
2295
- { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
2296
- ]
2297
- };
2298
- }
2299
- );
2300
- server2.registerTool(
2301
- "explore-layer",
2302
- {
2303
- title: "Explore Architecture Layer",
2304
- description: "Drill into a specific architecture layer to see all its component nodes with descriptions, team ownership, file paths, and linked entry counts.",
2305
- inputSchema: {
2306
- layer: z6.string().describe("Layer name or entry ID, e.g. 'Core' or 'ARCH-layer-core'")
2307
- },
2308
- annotations: { readOnlyHint: true }
2309
- },
2310
- async ({ layer }) => {
2311
- await ensureCollection();
2312
- const all = await listArchEntries();
2313
- const layers = byTag(all, "layer");
2314
- const target = layers.find(
2315
- (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
2316
- );
2317
- if (!target) {
2318
- const available = layers.map((l) => `\`${l.name}\``).join(", ");
2092
+ const html = renderArchitectureHtml(layers, nodes, flows, templateName);
2319
2093
  return {
2320
- content: [{
2321
- type: "text",
2322
- text: `Layer "${layer}" not found. Available layers: ${available}`
2323
- }]
2094
+ content: [
2095
+ { type: "text", text },
2096
+ { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
2097
+ ]
2324
2098
  };
2325
2099
  }
2326
- const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
2327
- const flows = byTag(all, "flow").filter(
2328
- (f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
2329
- );
2330
- const depRule = target.data?.dependsOn ? `
2100
+ if (action === "explore") {
2101
+ if (!layer) return { content: [{ type: "text", text: "A `layer` is required for explore." }] };
2102
+ const layers = byTag(all, "layer");
2103
+ const target = layers.find(
2104
+ (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
2105
+ );
2106
+ if (!target) {
2107
+ const available = layers.map((l) => `\`${l.name}\``).join(", ");
2108
+ return { content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }] };
2109
+ }
2110
+ const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
2111
+ const flows = byTag(all, "flow").filter(
2112
+ (f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
2113
+ );
2114
+ const depRule = target.data?.dependsOn ? `
2331
2115
  **Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
2332
2116
  ` : "";
2333
- const layerRationale = target.data?.rationale ? `
2117
+ const layerRationale = target.data?.rationale ? `
2334
2118
  > ${target.data.rationale}
2335
2119
  ` : "";
2336
- const nodeDetail = nodes.map((n) => {
2337
- const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
2338
- if (n.data?.description) lines.push(String(n.data.description));
2339
- if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
2340
- if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
2341
- if (n.data?.rationale) lines.push(`
2120
+ const nodeDetail = nodes.map((n) => {
2121
+ const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
2122
+ if (n.data?.description) lines.push(String(n.data.description));
2123
+ if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
2124
+ if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
2125
+ if (n.data?.rationale) lines.push(`
2342
2126
  **Why here?** ${n.data.rationale}`);
2343
- return lines.join("\n");
2344
- }).join("\n\n");
2345
- const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
2346
- return {
2347
- content: [{
2348
- type: "text",
2349
- text: `# ${target.data?.icon ?? ""} ${target.name} Layer
2127
+ return lines.join("\n");
2128
+ }).join("\n\n");
2129
+ const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
2130
+ return {
2131
+ content: [{
2132
+ type: "text",
2133
+ text: `# ${target.data?.icon ?? ""} ${target.name} Layer
2350
2134
 
2351
2135
  ${target.data?.description ?? ""}${depRule}${layerRationale}
2352
2136
  **${nodes.length} components**
2353
2137
 
2354
2138
  ${nodeDetail}${flowLines}`
2355
- }]
2356
- };
2357
- }
2358
- );
2359
- server2.registerTool(
2360
- "show-flow",
2361
- {
2362
- title: "Show Architecture Flow",
2363
- description: "Visualize a specific data flow path between architecture nodes \u2014 shows source, target, and description.",
2364
- inputSchema: {
2365
- flow: z6.string().describe("Flow name or entry ID, e.g. 'Smart Capture Flow' or 'ARCH-flow-smart-capture'")
2366
- },
2367
- annotations: { readOnlyHint: true }
2368
- },
2369
- async ({ flow }) => {
2370
- await ensureCollection();
2371
- const all = await listArchEntries();
2372
- const flows = byTag(all, "flow");
2373
- const target = flows.find(
2374
- (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
2375
- );
2376
- if (!target) {
2377
- const available = flows.map((f) => `\`${f.name}\``).join(", ");
2378
- return {
2379
- content: [{
2380
- type: "text",
2381
- text: `Flow "${flow}" not found. Available flows: ${available}`
2382
2139
  }]
2383
2140
  };
2384
2141
  }
2385
- const nodes = byTag(all, "node");
2386
- const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
2387
- const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
2388
- const lines = [
2389
- `# ${target.name}`,
2390
- "",
2391
- `**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
2392
- "",
2393
- String(target.data?.description ?? "")
2394
- ];
2395
- if (source) {
2396
- lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
2397
- if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
2398
- }
2399
- if (dest) {
2400
- lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
2401
- if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
2142
+ if (action === "flow") {
2143
+ if (!flow) return { content: [{ type: "text", text: "A `flow` name or entry ID is required." }] };
2144
+ const flows = byTag(all, "flow");
2145
+ const target = flows.find(
2146
+ (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
2147
+ );
2148
+ if (!target) {
2149
+ const available = flows.map((f) => `\`${f.name}\``).join(", ");
2150
+ return { content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }] };
2151
+ }
2152
+ const nodes = byTag(all, "node");
2153
+ const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
2154
+ const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
2155
+ const lines = [
2156
+ `# ${target.name}`,
2157
+ "",
2158
+ `**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
2159
+ "",
2160
+ String(target.data?.description ?? "")
2161
+ ];
2162
+ if (source) {
2163
+ lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
2164
+ if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
2165
+ }
2166
+ if (dest) {
2167
+ lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
2168
+ if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
2169
+ }
2170
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2402
2171
  }
2403
- return { content: [{ type: "text", text: lines.join("\n") }] };
2172
+ return { content: [{ type: "text", text: "Unknown action." }] };
2404
2173
  }
2405
2174
  );
2406
2175
  server2.registerTool(
2407
- "seed-architecture",
2176
+ "architecture-admin",
2408
2177
  {
2409
- title: "Seed Architecture Data",
2410
- description: "Populate the architecture collection with the default Product OS architecture map. Creates the template, layers, nodes, and flows. Safe to re-run \u2014 skips existing entries.",
2411
- annotations: { readOnlyHint: false }
2178
+ title: "Architecture Admin",
2179
+ description: "Architecture maintenance \u2014 seed the default architecture data or run a dependency health check.\n\nActions:\n- `seed`: Populate the architecture collection with the default Product OS map. Safe to re-run.\n- `check`: Scan the codebase for dependency direction violations against layer rules.",
2180
+ inputSchema: {
2181
+ action: z6.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
2182
+ }
2412
2183
  },
2413
- async () => {
2414
- await ensureCollection();
2415
- const existing = await listArchEntries();
2416
- const existingIds = new Set(existing.map((e) => e.entryId));
2417
- let created = 0;
2418
- let updated = 0;
2419
- let unchanged = 0;
2420
- const allSeeds = [
2421
- { ...SEED_TEMPLATE, order: 0, status: "active" },
2422
- ...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
2423
- ...SEED_NODES.map((n) => ({ ...n, status: "active" })),
2424
- ...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
2425
- ];
2426
- for (const seed of allSeeds) {
2427
- if (existingIds.has(seed.entryId)) {
2428
- const existingEntry = existing.find((e) => e.entryId === seed.entryId);
2429
- const existingData = existingEntry?.data ?? {};
2430
- const seedData = seed.data;
2431
- const hasChanges = Object.keys(seedData).some(
2432
- (k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
2433
- );
2434
- if (hasChanges) {
2435
- const mergedData = { ...existingData, ...seedData };
2436
- await mcpMutation("kb.updateEntry", {
2437
- entryId: seed.entryId,
2438
- data: mergedData
2439
- });
2440
- updated++;
2441
- } else {
2442
- unchanged++;
2184
+ async ({ action }) => {
2185
+ if (action === "seed") {
2186
+ await ensureCollection();
2187
+ const existing = await listArchEntries();
2188
+ const existingIds = new Set(existing.map((e) => e.entryId));
2189
+ let created = 0;
2190
+ let updated = 0;
2191
+ let unchanged = 0;
2192
+ const allSeeds = [
2193
+ { ...SEED_TEMPLATE, order: 0, status: "active" },
2194
+ ...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
2195
+ ...SEED_NODES.map((n) => ({ ...n, status: "active" })),
2196
+ ...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
2197
+ ];
2198
+ for (const seed of allSeeds) {
2199
+ if (existingIds.has(seed.entryId)) {
2200
+ const existingEntry = existing.find((e) => e.entryId === seed.entryId);
2201
+ const existingData = existingEntry?.data ?? {};
2202
+ const seedData = seed.data;
2203
+ const hasChanges = Object.keys(seedData).some(
2204
+ (k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
2205
+ );
2206
+ if (hasChanges) {
2207
+ const mergedData = { ...existingData, ...seedData };
2208
+ await mcpMutation("kb.updateEntry", { entryId: seed.entryId, data: mergedData });
2209
+ updated++;
2210
+ } else {
2211
+ unchanged++;
2212
+ }
2213
+ continue;
2443
2214
  }
2444
- continue;
2215
+ await mcpMutation("kb.createEntry", {
2216
+ collectionSlug: COLLECTION_SLUG,
2217
+ entryId: seed.entryId,
2218
+ name: seed.name,
2219
+ status: seed.status,
2220
+ data: seed.data,
2221
+ order: seed.order ?? 0
2222
+ });
2223
+ created++;
2445
2224
  }
2446
- await mcpMutation("kb.createEntry", {
2447
- collectionSlug: COLLECTION_SLUG,
2448
- entryId: seed.entryId,
2449
- name: seed.name,
2450
- status: seed.status,
2451
- data: seed.data,
2452
- order: seed.order ?? 0
2453
- });
2454
- created++;
2455
- }
2456
- return {
2457
- content: [{
2458
- type: "text",
2459
- text: `# Architecture Seeded
2225
+ return {
2226
+ content: [{
2227
+ type: "text",
2228
+ text: `# Architecture Seeded
2460
2229
 
2461
2230
  **Created:** ${created} entries
2462
2231
  **Updated:** ${updated} (merged new fields)
2463
2232
  **Unchanged:** ${unchanged}
2464
2233
 
2465
- Use \`show-architecture\` to view the map.`
2466
- }]
2467
- };
2468
- }
2469
- );
2470
- server2.registerTool(
2471
- "check-architecture",
2472
- {
2473
- title: "Check Architecture Health",
2474
- description: "Scan the codebase for dependency direction violations. Reads architecture layers, nodes, and their file paths from the knowledge base, then parses TypeScript/Svelte imports and checks them against the layer dependency rules (Auth \u2190 Infra \u2190 Core \u2190 Features; Integration \u2192 Core only). Returns a structured violation report.",
2475
- annotations: { readOnlyHint: true }
2476
- },
2477
- async () => {
2478
- const projectRoot = resolveProjectRoot2();
2479
- if (!projectRoot) {
2480
- return {
2481
- content: [{
2482
- type: "text",
2483
- text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
2234
+ Use \`architecture action=show\` to view the map.`
2484
2235
  }]
2485
2236
  };
2486
2237
  }
2487
- await ensureCollection();
2488
- const all = await listArchEntries();
2489
- const layers = byTag(all, "layer");
2490
- const nodes = byTag(all, "node");
2491
- const result = scanDependencies(projectRoot, layers, nodes);
2492
- return {
2493
- content: [{
2494
- type: "text",
2495
- text: formatScanReport(result)
2496
- }]
2497
- };
2238
+ if (action === "check") {
2239
+ const projectRoot = resolveProjectRoot2();
2240
+ if (!projectRoot) {
2241
+ return {
2242
+ content: [{
2243
+ type: "text",
2244
+ text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
2245
+ }]
2246
+ };
2247
+ }
2248
+ await ensureCollection();
2249
+ const all = await listArchEntries();
2250
+ const layers = byTag(all, "layer");
2251
+ const nodes = byTag(all, "node");
2252
+ const result = scanDependencies(projectRoot, layers, nodes);
2253
+ return { content: [{ type: "text", text: formatScanReport(result) }] };
2254
+ }
2255
+ return { content: [{ type: "text", text: "Unknown action." }] };
2498
2256
  }
2499
2257
  );
2500
2258
  }
@@ -2719,7 +2477,7 @@ import { z as z7 } from "zod";
2719
2477
  var RETRO_WORKFLOW = {
2720
2478
  id: "retro",
2721
2479
  name: "Retrospective",
2722
- shortDescription: "Structured team retrospective \u2014 reflect, surface patterns, commit to actions. Outputs a decision entry in the Knowledge Base.",
2480
+ shortDescription: "Structured team retrospective \u2014 reflect, surface patterns, commit to actions. Commits a decision entry to the Chain.",
2723
2481
  icon: "\u25CE",
2724
2482
  facilitatorPreamble: `You are now in **Facilitator Mode**. You are not a coding assistant \u2014 you are a facilitator running a structured retrospective.
2725
2483
 
@@ -2731,7 +2489,7 @@ var RETRO_WORKFLOW = {
2731
2489
  4. **Create a Plan** at the start showing all rounds as tasks. Update it as you progress.
2732
2490
  5. **Synthesize between rounds.** After collecting input, reflect back what you heard before moving on.
2733
2491
  6. **Never go silent.** If something fails, say what happened and what to do next.
2734
- 7. **Capture to KB** at the end using the Product OS smart-capture tool.
2492
+ 7. **Commit to the Chain** at the end using the Product OS capture tool.
2735
2493
  8. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
2736
2494
 
2737
2495
  ## Communication Style
@@ -2855,7 +2613,7 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
2855
2613
  label: "Close & Capture",
2856
2614
  type: "close",
2857
2615
  instruction: "One last thing \u2014 in one sentence, what's the single most important thing you're taking away from this retro?",
2858
- facilitatorGuidance: "Keep it brief. One sentence reflection. Then summarize the entire retro: scope, key wins, key pain points, patterns identified, actions committed. Ask if they want to save this to the Knowledge Base. If yes, use smart-capture to create a decision/tension entry. Thank them for the retro.",
2616
+ facilitatorGuidance: "Keep it brief. One sentence reflection. Then summarize the entire retro: scope, key wins, key pain points, patterns identified, actions committed. Ask if they want to commit this to the Chain. If yes, use capture to create a decision/tension entry. Thank them for the retro.",
2859
2617
  outputSchema: {
2860
2618
  field: "takeaway",
2861
2619
  description: "Single-sentence takeaway",
@@ -2872,11 +2630,11 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
2872
2630
  },
2873
2631
  errorRecovery: `If anything goes wrong during the retro:
2874
2632
 
2875
- 1. **MCP tool failure**: Skip the KB capture step. Summarize everything in the conversation instead. Suggest the participant runs \`smart-capture\` manually later.
2633
+ 1. **MCP tool failure**: Skip the chain commit step. Summarize everything in the conversation instead. Suggest the participant runs \`capture\` manually later.
2876
2634
  2. **AskQuestion not available**: Fall back to numbered options in plain text. "Reply with 1, 2, or 3."
2877
2635
  3. **Plan creation fails**: Continue without the Plan. The conversation IS the record.
2878
2636
  4. **Participant goes off-topic**: Gently redirect: "That's valuable \u2014 let's capture it. For now, let's stay with [current round]."
2879
- 5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to save partial results to KB.
2637
+ 5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to commit partial results to the Chain.
2880
2638
 
2881
2639
  The retro must never fail silently. Always communicate state.`
2882
2640
  };
@@ -2940,19 +2698,19 @@ ${cards}`
2940
2698
  "workflow-checkpoint",
2941
2699
  {
2942
2700
  title: "Workflow Checkpoint",
2943
- description: "Record the output of a workflow round. Captures the round's data to the Knowledge Base as a structured entry. Use this during Facilitator Mode after completing each round to persist progress \u2014 so if the conversation is interrupted, work is not lost.\n\nAt workflow completion, this tool can also generate the final summary entry.",
2701
+ description: "Record the output of a workflow round. Captures the round's data to the Chain as a structured entry. Use this during Facilitator Mode after completing each round to persist progress \u2014 so if the conversation is interrupted, work is not lost.\n\nAt workflow completion, this tool can also generate the final summary entry.",
2944
2702
  inputSchema: {
2945
2703
  workflowId: z7.string().describe("Workflow ID (e.g., 'retro')"),
2946
2704
  roundId: z7.string().describe("Round ID (e.g., 'what-went-well')"),
2947
2705
  output: z7.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
2948
2706
  isFinal: z7.boolean().optional().describe(
2949
- "If true, this is the final checkpoint and triggers the summary KB entry creation"
2707
+ "If true, this is the final checkpoint and triggers the summary chain entry creation"
2950
2708
  ),
2951
2709
  summaryName: z7.string().optional().describe(
2952
- "Name for the final KB entry (required when isFinal=true)"
2710
+ "Name for the final chain entry (required when isFinal=true)"
2953
2711
  ),
2954
2712
  summaryDescription: z7.string().optional().describe(
2955
- "Full description/rationale for the final KB entry (required when isFinal=true)"
2713
+ "Full description/rationale for the final chain entry (required when isFinal=true)"
2956
2714
  )
2957
2715
  },
2958
2716
  annotations: { destructiveHint: false }
@@ -3000,20 +2758,20 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3000
2758
  createdBy: `workflow:${wf.id}`
3001
2759
  });
3002
2760
  lines.push(
3003
- `**KB Entry Created**: \`${entryId}\``,
2761
+ `**Entry Committed**: \`${entryId}\``,
3004
2762
  `Collection: \`${wf.kbOutputCollection}\``,
3005
2763
  `Name: ${summaryName}`,
3006
2764
  "",
3007
- `The retro is now captured in the Knowledge Base. `,
2765
+ `The retro is now committed to the Chain. `,
3008
2766
  `Use \`suggest-links\` on this entry to connect it to related knowledge.`
3009
2767
  );
3010
2768
  } catch (err) {
3011
2769
  const msg = err instanceof Error ? err.message : String(err);
3012
2770
  lines.push(
3013
- `**KB capture failed**: ${msg}`,
2771
+ `**Chain commit failed**: ${msg}`,
3014
2772
  "",
3015
2773
  `The retro output is preserved in this conversation. `,
3016
- `You can manually create the entry later using \`smart-capture\` with:`,
2774
+ `You can manually create the entry later using \`capture\` with:`,
3017
2775
  `- Collection: \`${wf.kbOutputCollection}\``,
3018
2776
  `- Name: ${summaryName}`,
3019
2777
  `- Description: (copy from the conversation summary above)`
@@ -3035,7 +2793,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3035
2793
  } else {
3036
2794
  lines.push(
3037
2795
  "",
3038
- `**All rounds complete.** Call this tool again with \`isFinal: true\` to create the KB entry.`
2796
+ `**All rounds complete.** Call this tool again with \`isFinal: true\` to commit to the Chain.`
3039
2797
  );
3040
2798
  }
3041
2799
  }
@@ -3055,68 +2813,57 @@ function linkSummary(links) {
3055
2813
  }
3056
2814
  function registerGitChainTools(server2) {
3057
2815
  server2.registerTool(
3058
- "chain-create",
2816
+ "chain",
3059
2817
  {
3060
- title: "Create Chain",
3061
- description: "Create a new strategic chain in the knowledge base. Chains are versioned knowledge artifacts that follow a chain type definition (e.g. Strategy Coherence: Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome). Returns the chain's entry ID for subsequent operations.",
2818
+ title: "Chain",
2819
+ description: "Manage processes \u2014 create, get, list, or edit process links. Processes are versioned knowledge artifacts that follow a process template (e.g. Strategy Coherence: Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome).",
3062
2820
  inputSchema: {
3063
- title: z8.string().describe("Title for the chain, e.g. 'Q1 Growth Strategy'"),
3064
- chainTypeId: z8.string().default("strategy-coherence").describe(
3065
- "Chain type ID. Use 'strategy-coherence' for 5-link strategy chains (Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome) or 'idm-proposal' for IDM governance chains (Tension \u2192 Proposal \u2192 Objections \u2192 Integration \u2192 Decision)"
3066
- ),
3067
- description: z8.string().optional().describe("Optional description of what this chain is about"),
3068
- author: z8.string().optional().describe("Who is creating this chain (clerkUserId or person name). Defaults to 'mcp'.")
2821
+ action: z8.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
2822
+ chainEntryId: z8.string().optional().describe("Chain entry ID (required for get/edit)"),
2823
+ title: z8.string().optional().describe("Process title (required for create)"),
2824
+ chainTypeId: z8.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
2825
+ description: z8.string().optional().describe("Description (for create)"),
2826
+ linkId: z8.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
2827
+ content: z8.string().optional().describe("New content for the link (for edit action)"),
2828
+ status: z8.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
2829
+ author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3069
2830
  }
3070
2831
  },
3071
- async ({ title, chainTypeId, description, author }) => {
3072
- const result = await mcpMutation(
3073
- "gitchain.createChain",
3074
- { title, chainTypeId, description, author }
3075
- );
3076
- return {
3077
- content: [
3078
- {
2832
+ async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
2833
+ if (action === "create") {
2834
+ if (!title) return { content: [{ type: "text", text: "A `title` is required to create a process." }] };
2835
+ const result = await mcpMutation(
2836
+ "gitchain.createChain",
2837
+ { title, chainTypeId, description, author }
2838
+ );
2839
+ return {
2840
+ content: [{
3079
2841
  type: "text",
3080
- text: `# Chain Created
2842
+ text: `# Process Created
3081
2843
 
3082
2844
  - **Entry ID:** \`${result.entryId}\`
3083
2845
  - **Title:** ${title}
3084
2846
  - **Type:** ${chainTypeId}
3085
2847
  - **Status:** draft
3086
2848
 
3087
- Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in links.`
3088
- }
3089
- ]
3090
- };
3091
- }
3092
- );
3093
- server2.registerTool(
3094
- "chain-get",
3095
- {
3096
- title: "Get Chain",
3097
- description: "Retrieve a chain by its entry ID. Returns the full chain content, coherence scores, link fill status, and version info. Use chain-list first to discover chain entry IDs.",
3098
- inputSchema: {
3099
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
3100
- },
3101
- annotations: { readOnlyHint: true }
3102
- },
3103
- async ({ chainEntryId }) => {
3104
- const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
3105
- if (!chain) {
3106
- return {
3107
- content: [
3108
- {
3109
- type: "text",
3110
- text: `Chain "${chainEntryId}" not found.`
3111
- }
3112
- ]
2849
+ Use \`chain action=edit chainEntryId="${result.entryId}" linkId="problem" content="..."\` to start filling in links.`
2850
+ }]
3113
2851
  };
3114
2852
  }
3115
- const scoreSection = chain.scores ? `
2853
+ if (action === "get") {
2854
+ if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
2855
+ const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
2856
+ if (!chain) {
2857
+ return { content: [{ type: "text", text: `Process "${chainEntryId}" not found.` }] };
2858
+ }
2859
+ const scoreSection = chain.scores ? `
3116
2860
  ## Coherence: ${chain.coherenceScore}%
3117
2861
 
3118
2862
  ` + chain.scores.sections.map((s) => `- **${s.key}**: ${s.power}/100 (${"\u2605".repeat(Math.min(s.stars ?? 0, 5))}${"\u2606".repeat(Math.max(0, 5 - (s.stars ?? 0)))})`).join("\n") : "";
3119
- const text = `# ${chain.name}
2863
+ return {
2864
+ content: [{
2865
+ type: "text",
2866
+ text: `# ${chain.name}
3120
2867
 
3121
2868
  - **Entry ID:** \`${chain.entryId}\`
3122
2869
  - **Type:** ${chain.chainTypeName}
@@ -3129,29 +2876,32 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
3129
2876
 
3130
2877
  ## Links
3131
2878
 
3132
- ` + linkSummary(chain.links);
3133
- return { content: [{ type: "text", text }] };
3134
- }
3135
- );
3136
- server2.registerTool(
3137
- "chain-edit",
3138
- {
3139
- title: "Edit Chain Link",
3140
- description: "Edit a specific link in a chain. Each chain has named links (e.g. for strategy-coherence: problem, insight, choice, action, outcome). The link content is replaced entirely \u2014 pass the full new text.",
3141
- inputSchema: {
3142
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3143
- linkId: z8.string().describe(
3144
- "Which link to edit. For strategy-coherence: problem, insight, choice, action, outcome. For idm-proposal: tension, proposal, objections, integration, decision."
3145
- ),
3146
- content: z8.string().describe("The full new content for this link"),
3147
- author: z8.string().optional().describe("Who is making this edit. Defaults to 'mcp'.")
2879
+ ` + linkSummary(chain.links)
2880
+ }]
2881
+ };
3148
2882
  }
3149
- },
3150
- async ({ chainEntryId, linkId, content, author }) => {
3151
- const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
3152
- return {
3153
- content: [
3154
- {
2883
+ if (action === "list") {
2884
+ const chains = await mcpQuery("gitchain.listChains", {
2885
+ chainTypeId,
2886
+ status
2887
+ });
2888
+ if (chains.length === 0) {
2889
+ return { content: [{ type: "text", text: "No processes found. Use `chain action=create` to create one." }] };
2890
+ }
2891
+ const formatted = chains.map(
2892
+ (c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
2893
+ ).join("\n");
2894
+ return { content: [{ type: "text", text: `# Chains (${chains.length})
2895
+
2896
+ ${formatted}` }] };
2897
+ }
2898
+ if (action === "edit") {
2899
+ if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
2900
+ if (!linkId) return { content: [{ type: "text", text: "A `linkId` is required (e.g. problem, insight, choice, action, outcome)." }] };
2901
+ if (!content) return { content: [{ type: "text", text: "The `content` for the link is required." }] };
2902
+ const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
2903
+ return {
2904
+ content: [{
3155
2905
  type: "text",
3156
2906
  text: `# Link Updated
3157
2907
 
@@ -3160,114 +2910,38 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
3160
2910
  - **Chain status:** ${result.status}
3161
2911
  - **Content length:** ${content.length} chars
3162
2912
 
3163
- Use \`chain-get\` to see the full chain with updated scores.`
3164
- }
3165
- ]
3166
- };
3167
- }
3168
- );
3169
- server2.registerTool(
3170
- "chain-list",
3171
- {
3172
- title: "List Chains",
3173
- description: "List all chains in the workspace, optionally filtered by chain type or status. Returns entry IDs, titles, link fill progress, and coherence scores.",
3174
- inputSchema: {
3175
- chainTypeId: z8.string().optional().describe("Filter by chain type: 'strategy-coherence' or 'idm-proposal'"),
3176
- status: z8.string().optional().describe("Filter by status: 'draft' or 'active'")
3177
- },
3178
- annotations: { readOnlyHint: true }
3179
- },
3180
- async ({ chainTypeId, status }) => {
3181
- const chains = await mcpQuery("gitchain.listChains", {
3182
- chainTypeId,
3183
- status
3184
- });
3185
- if (chains.length === 0) {
3186
- return {
3187
- content: [
3188
- {
3189
- type: "text",
3190
- text: "No chains found. Use `chain-create` to create one."
3191
- }
3192
- ]
2913
+ Use \`chain action=get\` to see the full chain with updated scores.`
2914
+ }]
3193
2915
  };
3194
2916
  }
3195
- const formatted = chains.map(
3196
- (c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
3197
- ).join("\n");
3198
- return {
3199
- content: [
3200
- {
3201
- type: "text",
3202
- text: `# Chains (${chains.length})
3203
-
3204
- ${formatted}`
3205
- }
3206
- ]
3207
- };
2917
+ return { content: [{ type: "text", text: "Unknown action." }] };
3208
2918
  }
3209
2919
  );
3210
2920
  server2.registerTool(
3211
- "chain-history",
2921
+ "chain-version",
3212
2922
  {
3213
- title: "Chain History",
3214
- description: "View the edit history of a chain \u2014 all creation, update, and version events. Shows who changed what and when.",
2923
+ title: "Chain Version",
2924
+ description: "Manage process versions \u2014 commit snapshots, list commits, view history, diff versions, or revert. Commits record all link content, compute coherence scores, and track changes.",
3215
2925
  inputSchema: {
3216
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
2926
+ action: z8.enum(["commit", "list", "diff", "revert", "history"]).describe("Action: commit a snapshot, list commits, diff two versions, revert to a version, or view history"),
2927
+ chainEntryId: z8.string().describe("The chain's entry ID"),
2928
+ commitMessage: z8.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
2929
+ versionA: z8.number().optional().describe("Earlier version for diff"),
2930
+ versionB: z8.number().optional().describe("Later version for diff"),
2931
+ toVersion: z8.number().optional().describe("Version number to revert to"),
2932
+ author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3217
2933
  },
3218
- annotations: { readOnlyHint: true }
3219
- },
3220
- async ({ chainEntryId }) => {
3221
- const history = await mcpQuery("gitchain.getHistory", {
3222
- chainEntryId
3223
- });
3224
- if (history.length === 0) {
3225
- return {
3226
- content: [
3227
- {
3228
- type: "text",
3229
- text: `No history found for chain "${chainEntryId}".`
3230
- }
3231
- ]
3232
- };
3233
- }
3234
- const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
3235
- const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
3236
- return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
3237
- }).join("\n");
3238
- return {
3239
- content: [
3240
- {
3241
- type: "text",
3242
- text: `# History for ${chainEntryId} (${history.length} events)
3243
-
3244
- ${formatted}`
3245
- }
3246
- ]
3247
- };
3248
- }
3249
- );
3250
- server2.registerTool(
3251
- "chain-commit",
3252
- {
3253
- title: "Commit Chain",
3254
- description: "Create a version snapshot (commit) of the current chain state. Records all link content, computes coherence score, and tracks which links changed. Commit messages should follow: type(link): description. Types: edit, refine, rewrite, integrate, revert.",
3255
- inputSchema: {
3256
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3257
- commitMessage: z8.string().describe(
3258
- "Commit message following convention: type(link): description. Example: 'edit(outcome): Add Q1 revenue target'"
3259
- ),
3260
- author: z8.string().optional().describe("Who is committing. Defaults to 'mcp'.")
3261
- }
2934
+ annotations: { readOnlyHint: false }
3262
2935
  },
3263
- async ({ chainEntryId, commitMessage, author }) => {
3264
- const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
3265
- const warning = result.commitLintWarning ? `
2936
+ async ({ action, chainEntryId, commitMessage, versionA, versionB, toVersion, author }) => {
2937
+ if (action === "commit") {
2938
+ if (!commitMessage) return { content: [{ type: "text", text: "A `commitMessage` is required." }] };
2939
+ const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
2940
+ const warning = result.commitLintWarning ? `
3266
2941
 
3267
2942
  > **Lint warning:** ${result.commitLintWarning}` : "";
3268
- return {
3269
- content: [
3270
- {
2943
+ return {
2944
+ content: [{
3271
2945
  type: "text",
3272
2946
  text: `# Committed v${result.version}
3273
2947
 
@@ -3277,229 +2951,160 @@ ${formatted}`
3277
2951
  - **Coherence:** ${result.coherenceScore}%
3278
2952
  - **Links modified:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
3279
2953
  ` + warning
3280
- }
3281
- ]
3282
- };
3283
- }
3284
- );
3285
- server2.registerTool(
3286
- "chain-commits",
3287
- {
3288
- title: "List Chain Commits",
3289
- description: "List all version snapshots (commits) for a chain, newest first. Shows version number, commit message, author, which links were modified, and status.",
3290
- inputSchema: {
3291
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
3292
- },
3293
- annotations: { readOnlyHint: true }
3294
- },
3295
- async ({ chainEntryId }) => {
3296
- const commits = await mcpQuery("gitchain.listCommits", {
3297
- chainEntryId
3298
- });
3299
- if (commits.length === 0) {
3300
- return {
3301
- content: [
3302
- {
3303
- type: "text",
3304
- text: `No commits found for chain "${chainEntryId}". Use \`chain-commit\` to create the first snapshot.`
3305
- }
3306
- ]
2954
+ }]
3307
2955
  };
3308
2956
  }
3309
- const formatted = commits.map((c) => {
3310
- const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
3311
- const msg = c.commitMessage ?? c.changeNote ?? "(no message)";
3312
- const links = c.linksModified && c.linksModified.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
3313
- return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
3314
- }).join("\n");
3315
- return {
3316
- content: [
3317
- {
3318
- type: "text",
3319
- text: `# Commits for ${chainEntryId} (${commits.length})
3320
-
3321
- ${formatted}`
3322
- }
3323
- ]
3324
- };
3325
- }
3326
- );
3327
- server2.registerTool(
3328
- "chain-diff",
3329
- {
3330
- title: "Diff Chain Versions",
3331
- description: "Compare two versions of a chain. Shows which links changed, word-level diffs for modified links, and the coherence score delta. Use chain-commits first to see available version numbers.",
3332
- inputSchema: {
3333
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3334
- versionA: z8.number().describe("The earlier version number"),
3335
- versionB: z8.number().describe("The later version number")
3336
- },
3337
- annotations: { readOnlyHint: true }
3338
- },
3339
- async ({ chainEntryId, versionA, versionB }) => {
3340
- const diff = await mcpMutation("gitchain.diffVersions", {
3341
- chainEntryId,
3342
- versionA,
3343
- versionB
3344
- });
3345
- let text = `# Diff: v${versionA} \u2192 v${versionB}
2957
+ if (action === "list") {
2958
+ const commits = await mcpQuery("gitchain.listCommits", { chainEntryId });
2959
+ if (commits.length === 0) {
2960
+ return { content: [{ type: "text", text: `No commits found for chain "${chainEntryId}". Use \`chain-version action=commit\` to create the first snapshot.` }] };
2961
+ }
2962
+ const formatted = commits.map((c) => {
2963
+ const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
2964
+ const msg = c.commitMessage ?? c.changeNote ?? "(no message)";
2965
+ const links = c.linksModified?.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
2966
+ return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
2967
+ }).join("\n");
2968
+ return { content: [{ type: "text", text: `# Commits for ${chainEntryId} (${commits.length})
2969
+
2970
+ ${formatted}` }] };
2971
+ }
2972
+ if (action === "diff") {
2973
+ if (versionA == null || versionB == null) {
2974
+ return { content: [{ type: "text", text: "Both `versionA` and `versionB` are required for diff." }] };
2975
+ }
2976
+ const diff = await mcpMutation("gitchain.diffVersions", { chainEntryId, versionA, versionB });
2977
+ let text = `# Diff: v${versionA} \u2192 v${versionB}
3346
2978
 
3347
2979
  - **Chain:** \`${diff.chainEntryId}\`
3348
2980
  - **Coherence:** ${diff.coherenceBefore}% \u2192 ${diff.coherenceAfter}% (${diff.coherenceDelta >= 0 ? "+" : ""}${diff.coherenceDelta})
3349
2981
  - **Links changed:** ${diff.linksChanged.length > 0 ? diff.linksChanged.join(", ") : "none"}
3350
2982
  `;
3351
- for (const ld of diff.linkDiffs) {
3352
- if (ld.status === "unchanged") continue;
3353
- text += `
2983
+ for (const ld of diff.linkDiffs) {
2984
+ if (ld.status === "unchanged") continue;
2985
+ text += `
3354
2986
  ## ${ld.linkId} (${ld.status})
3355
2987
 
3356
2988
  `;
3357
- const wordDiff = diff.wordDiffs[ld.linkId];
3358
- if (wordDiff && wordDiff.length > 0) {
3359
- for (const w of wordDiff) {
3360
- if (w.type === "delete") {
3361
- text += `~~${w.value.substring(0, 200)}~~`;
3362
- } else if (w.type === "insert") {
3363
- text += `**${w.value.substring(0, 200)}**`;
3364
- } else {
3365
- text += w.value.substring(0, 200);
2989
+ const wordDiff = diff.wordDiffs[ld.linkId];
2990
+ if (wordDiff?.length > 0) {
2991
+ for (const w of wordDiff) {
2992
+ if (w.type === "delete") text += `~~${w.value.substring(0, 200)}~~`;
2993
+ else if (w.type === "insert") text += `**${w.value.substring(0, 200)}**`;
2994
+ else text += w.value.substring(0, 200);
3366
2995
  }
2996
+ text += "\n";
3367
2997
  }
3368
- text += "\n";
3369
2998
  }
2999
+ return { content: [{ type: "text", text }] };
3370
3000
  }
3371
- return { content: [{ type: "text", text }] };
3372
- }
3373
- );
3374
- server2.registerTool(
3375
- "chain-gate",
3376
- {
3377
- title: "Chain Coherence Gate",
3378
- description: "Run the coherence gate on a chain. Checks: coherence score >= 70%, all links filled, commit message follows convention. Returns pass/fail with detailed check results. This is the same gate that blocks publishing.",
3379
- inputSchema: {
3380
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3381
- commitMessage: z8.string().optional().describe("Optional commit message to lint")
3382
- },
3383
- annotations: { readOnlyHint: true }
3384
- },
3385
- async ({ chainEntryId, commitMessage }) => {
3386
- const gate = await mcpQuery("gitchain.runGate", {
3387
- chainEntryId,
3388
- commitMessage
3389
- });
3390
- const checkLines = gate.checks.map(
3391
- (c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`
3392
- ).join("\n");
3393
- const icon = gate.pass ? "PASS" : "BLOCKED";
3394
- return {
3395
- content: [
3396
- {
3001
+ if (action === "revert") {
3002
+ if (toVersion == null) return { content: [{ type: "text", text: "A `toVersion` is required for revert." }] };
3003
+ const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
3004
+ return {
3005
+ content: [{
3397
3006
  type: "text",
3398
- text: `# Gate: ${icon}
3007
+ text: `# Reverted
3399
3008
 
3400
- - **Score:** ${gate.score}%
3401
- - **Threshold:** ${gate.threshold}%
3009
+ - **Chain:** \`${result.entryId}\`
3010
+ - **Reverted to:** v${result.revertedTo}
3011
+ - **New version:** v${result.newVersion}
3012
+ - **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
3402
3013
 
3403
- ## Checks
3014
+ History is preserved \u2014 this created a new version, not a destructive reset.`
3015
+ }]
3016
+ };
3017
+ }
3018
+ if (action === "history") {
3019
+ const history = await mcpQuery("gitchain.getHistory", { chainEntryId });
3020
+ if (history.length === 0) {
3021
+ return { content: [{ type: "text", text: `No history found for chain "${chainEntryId}".` }] };
3022
+ }
3023
+ const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
3024
+ const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
3025
+ return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
3026
+ }).join("\n");
3027
+ return { content: [{ type: "text", text: `# History for ${chainEntryId} (${history.length} events)
3404
3028
 
3405
- ${checkLines}`
3406
- }
3407
- ]
3408
- };
3029
+ ${formatted}` }] };
3030
+ }
3031
+ return { content: [{ type: "text", text: "Unknown action." }] };
3409
3032
  }
3410
3033
  );
3411
3034
  server2.registerTool(
3412
3035
  "chain-branch",
3413
3036
  {
3414
- title: "Create or List Chain Branches",
3415
- description: "Create a new branch for isolated editing, or list all branches on a chain. Branches fork from the current published version of main. Use chain-merge to merge a branch back.",
3037
+ title: "Chain Branch",
3038
+ description: "Manage process branches \u2014 create a branch for isolated editing, list branches, merge a branch back into main, or check for conflicts.",
3416
3039
  inputSchema: {
3417
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3418
- action: z8.enum(["create", "list"]).describe("'create' to make a new branch, 'list' to see all branches"),
3419
- name: z8.string().optional().describe("Branch name for 'create' action. Auto-generated if not provided."),
3420
- author: z8.string().optional().describe("Who is creating the branch. Defaults to 'mcp'.")
3040
+ action: z8.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
3041
+ chainEntryId: z8.string().describe("The chain's entry ID"),
3042
+ branchName: z8.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
3043
+ strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
3044
+ author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3421
3045
  }
3422
3046
  },
3423
- async ({ chainEntryId, action, name, author }) => {
3047
+ async ({ action, chainEntryId, branchName, strategy, author }) => {
3424
3048
  if (action === "create") {
3425
- const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name, author });
3049
+ const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name: branchName, author });
3426
3050
  return {
3427
- content: [
3428
- {
3429
- type: "text",
3430
- text: `# Branch Created
3051
+ content: [{
3052
+ type: "text",
3053
+ text: `# Branch Created
3431
3054
 
3432
3055
  - **Name:** ${result.name}
3433
3056
  - **Based on:** v${result.baseVersion}
3434
3057
  - **Chain:** \`${chainEntryId}\`
3435
3058
 
3436
- Edit links and commit on this branch, then use \`chain-merge\` to land changes.`
3437
- }
3438
- ]
3059
+ Edit links and commit on this branch, then use \`chain-branch action=merge\` to land changes.`
3060
+ }]
3439
3061
  };
3440
3062
  }
3441
- const branches = await mcpMutation("gitchain.listBranches", {
3442
- chainEntryId
3443
- });
3444
- if (branches.length === 0) {
3445
- return {
3446
- content: [
3447
- {
3448
- type: "text",
3449
- text: `No branches found for chain "${chainEntryId}".`
3450
- }
3451
- ]
3452
- };
3063
+ if (action === "list") {
3064
+ const branches = await mcpMutation("gitchain.listBranches", { chainEntryId });
3065
+ if (branches.length === 0) {
3066
+ return { content: [{ type: "text", text: `No branches found for chain "${chainEntryId}".` }] };
3067
+ }
3068
+ const formatted = branches.map((b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`).join("\n");
3069
+ return { content: [{ type: "text", text: `# Branches for ${chainEntryId} (${branches.length})
3070
+
3071
+ ${formatted}` }] };
3453
3072
  }
3454
- const formatted = branches.map(
3455
- (b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`
3456
- ).join("\n");
3457
- return {
3458
- content: [
3459
- {
3073
+ if (action === "merge") {
3074
+ if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for merge." }] };
3075
+ const result = await mcpMutation("gitchain.mergeBranch", { chainEntryId, branchName, strategy, author });
3076
+ return {
3077
+ content: [{
3460
3078
  type: "text",
3461
- text: `# Branches for ${chainEntryId} (${branches.length})
3079
+ text: `# Branch Merged
3462
3080
 
3463
- ${formatted}`
3464
- }
3465
- ]
3466
- };
3467
- }
3468
- );
3469
- server2.registerTool(
3470
- "chain-conflicts",
3471
- {
3472
- title: "Check Branch Conflicts",
3473
- description: "Check if a branch has conflicts with other active branches. Conflicts occur when two branches modify the same chain link. Always check before merging.",
3474
- inputSchema: {
3475
- chainEntryId: z8.string().describe("The chain's entry ID"),
3476
- branchName: z8.string().describe("The branch name to check for conflicts")
3477
- },
3478
- annotations: { readOnlyHint: true }
3479
- },
3480
- async ({ chainEntryId, branchName }) => {
3481
- const result = await mcpMutation("gitchain.checkConflicts", {
3482
- chainEntryId,
3483
- branchName
3484
- });
3485
- if (!result.hasConflicts) {
3486
- return {
3487
- content: [
3488
- {
3489
- type: "text",
3490
- text: `# No Conflicts
3081
+ - **Chain:** \`${result.entryId}\`
3082
+ - **Branch:** ${result.branchName} (now closed)
3083
+ - **Version:** v${result.version}
3084
+ - **Strategy:** ${result.strategy}
3491
3085
 
3492
- Branch "${branchName}" on \`${chainEntryId}\` has no conflicts with other active branches. Safe to merge.`
3493
- }
3494
- ]
3086
+ Main is now at v${result.version}. The branch has been closed.`
3087
+ }]
3495
3088
  };
3496
3089
  }
3497
- const conflictLines = result.conflicts.map(
3498
- (c) => `- **${c.linkId}** \u2014 modified by: ${c.branches.map((b) => `${b.branchName} (${b.author})`).join(", ")}`
3499
- ).join("\n");
3500
- return {
3501
- content: [
3502
- {
3090
+ if (action === "conflicts") {
3091
+ if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for conflict check." }] };
3092
+ const result = await mcpMutation("gitchain.checkConflicts", { chainEntryId, branchName });
3093
+ if (!result.hasConflicts) {
3094
+ return {
3095
+ content: [{
3096
+ type: "text",
3097
+ text: `# No Conflicts
3098
+
3099
+ Branch "${branchName}" on \`${chainEntryId}\` has no conflicts. Safe to merge.`
3100
+ }]
3101
+ };
3102
+ }
3103
+ const conflictLines = result.conflicts.map(
3104
+ (c) => `- **${c.linkId}** \u2014 modified by: ${c.branches.map((b) => `${b.branchName} (${b.author})`).join(", ")}`
3105
+ ).join("\n");
3106
+ return {
3107
+ content: [{
3503
3108
  type: "text",
3504
3109
  text: `# Conflicts Detected
3505
3110
 
@@ -3507,67 +3112,52 @@ Branch "${branchName}" conflicts with other branches on these links:
3507
3112
 
3508
3113
  ` + conflictLines + `
3509
3114
 
3510
- Resolve conflicts before merging. One branch must update its link content to avoid overwriting.`
3511
- }
3512
- ]
3513
- };
3115
+ Resolve conflicts before merging.`
3116
+ }]
3117
+ };
3118
+ }
3119
+ return { content: [{ type: "text", text: "Unknown action." }] };
3514
3120
  }
3515
3121
  );
3516
3122
  server2.registerTool(
3517
- "chain-merge",
3123
+ "chain-review",
3518
3124
  {
3519
- title: "Merge Branch",
3520
- description: "Merge a branch back into main. Runs the coherence gate before merging. The branch is closed after merge. Check for conflicts with chain-conflicts first.",
3125
+ title: "Chain Review",
3126
+ description: "Review process quality \u2014 run the coherence gate or manage comments on process versions. The gate checks: coherence score >= 70%, all links filled, commit message convention.",
3521
3127
  inputSchema: {
3128
+ action: z8.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
3522
3129
  chainEntryId: z8.string().describe("The chain's entry ID"),
3523
- branchName: z8.string().describe("The branch to merge"),
3524
- strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) preserves history, 'squash' collapses into one version"),
3525
- author: z8.string().optional().describe("Who is merging. Defaults to 'mcp'.")
3526
- }
3130
+ commitMessage: z8.string().optional().describe("Commit message to lint (for gate action)"),
3131
+ versionNumber: z8.number().optional().describe("Version to comment on or list comments for"),
3132
+ linkId: z8.string().optional().describe("Link this comment targets (optional for comment)"),
3133
+ body: z8.string().optional().describe("Comment text (required for comment action)"),
3134
+ commentId: z8.string().optional().describe("Comment ID (required for resolve-comment)"),
3135
+ author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3136
+ },
3137
+ annotations: { readOnlyHint: false }
3527
3138
  },
3528
- async ({ chainEntryId, branchName, strategy, author }) => {
3529
- const result = await mcpMutation("gitchain.mergeBranch", {
3530
- chainEntryId,
3531
- branchName,
3532
- strategy,
3533
- author
3534
- });
3535
- return {
3536
- content: [
3537
- {
3139
+ async ({ action, chainEntryId, commitMessage, versionNumber, linkId, body, commentId, author }) => {
3140
+ if (action === "gate") {
3141
+ const gate = await mcpQuery("gitchain.runGate", { chainEntryId, commitMessage });
3142
+ const checkLines = gate.checks.map((c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`).join("\n");
3143
+ const icon = gate.pass ? "PASS" : "BLOCKED";
3144
+ return {
3145
+ content: [{
3538
3146
  type: "text",
3539
- text: `# Branch Merged
3147
+ text: `# Gate: ${icon}
3540
3148
 
3541
- - **Chain:** \`${result.entryId}\`
3542
- - **Branch:** ${result.branchName} (now closed)
3543
- - **Version:** v${result.version}
3544
- - **Strategy:** ${result.strategy}
3149
+ - **Score:** ${gate.score}%
3150
+ - **Threshold:** ${gate.threshold}%
3545
3151
 
3546
- Main is now at v${result.version}. The branch has been closed.`
3547
- }
3548
- ]
3549
- };
3550
- }
3551
- );
3552
- server2.registerTool(
3553
- "chain-comment",
3554
- {
3555
- title: "Comment on Chain Version",
3556
- description: "Add a threaded comment on a specific chain version, optionally targeting a specific link. Comments support threading via parentId. Use chain-comments to see existing comments.",
3557
- inputSchema: {
3558
- chainEntryId: z8.string().describe("The chain's entry ID"),
3559
- action: z8.enum(["add", "resolve", "list"]).describe("'add' a comment, 'resolve' a comment, or 'list' all comments"),
3560
- versionNumber: z8.number().optional().describe("Version number to comment on (required for 'add')"),
3561
- linkId: z8.string().optional().describe("Which link this comment targets (optional)"),
3562
- body: z8.string().optional().describe("Comment text (required for 'add')"),
3563
- commentId: z8.string().optional().describe("Comment ID to resolve (required for 'resolve')"),
3564
- author: z8.string().optional().describe("Who is commenting. Defaults to 'mcp'.")
3152
+ ## Checks
3153
+
3154
+ ${checkLines}`
3155
+ }]
3156
+ };
3565
3157
  }
3566
- },
3567
- async ({ chainEntryId, action, versionNumber, linkId, body, commentId, author }) => {
3568
- if (action === "add") {
3569
- if (!versionNumber) throw new Error("versionNumber is required for 'add'");
3570
- if (!body) throw new Error("body is required for 'add'");
3158
+ if (action === "comment") {
3159
+ if (!versionNumber) return { content: [{ type: "text", text: "A `versionNumber` is required." }] };
3160
+ if (!body) return { content: [{ type: "text", text: "A `body` is required." }] };
3571
3161
  const result = await mcpMutation("gitchain.addComment", {
3572
3162
  chainEntryId,
3573
3163
  versionNumber,
@@ -3576,92 +3166,47 @@ Main is now at v${result.version}. The branch has been closed.`
3576
3166
  author
3577
3167
  });
3578
3168
  return {
3579
- content: [
3580
- {
3581
- type: "text",
3582
- text: `# Comment Added
3169
+ content: [{
3170
+ type: "text",
3171
+ text: `# Comment Added
3583
3172
 
3584
3173
  - **Chain:** \`${result.chainEntryId}\`
3585
3174
  - **Version:** v${result.versionNumber}
3586
3175
  ` + (result.linkId ? `- **Link:** ${result.linkId}
3587
- ` : "") + `- **Body:** ${body?.substring(0, 200)}`
3588
- }
3589
- ]
3176
+ ` : "") + `- **Body:** ${body.substring(0, 200)}`
3177
+ }]
3590
3178
  };
3591
3179
  }
3592
- if (action === "resolve") {
3593
- if (!commentId) throw new Error("commentId is required for 'resolve'");
3180
+ if (action === "resolve-comment") {
3181
+ if (!commentId) return { content: [{ type: "text", text: "A `commentId` is required." }] };
3594
3182
  await mcpMutation("gitchain.resolveComment", { commentId });
3595
- return {
3596
- content: [
3597
- {
3598
- type: "text",
3599
- text: `Comment resolved.`
3600
- }
3601
- ]
3602
- };
3183
+ return { content: [{ type: "text", text: `Comment resolved.` }] };
3603
3184
  }
3604
- const comments = await mcpMutation("gitchain.listComments", {
3605
- chainEntryId,
3606
- versionNumber
3607
- });
3608
- if (comments.length === 0) {
3185
+ if (action === "list-comments") {
3186
+ const comments = await mcpMutation("gitchain.listComments", {
3187
+ chainEntryId,
3188
+ versionNumber
3189
+ });
3190
+ if (comments.length === 0) {
3191
+ return { content: [{ type: "text", text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.` }] };
3192
+ }
3193
+ const formatted = comments.map((c) => {
3194
+ const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
3195
+ const resolved = c.resolved ? " (RESOLVED)" : "";
3196
+ const link = c.linkId ? ` [${c.linkId}]` : "";
3197
+ return `- **v${c.version}${link}** ${date} by ${c.author}${resolved}: ${c.body.substring(0, 150)}`;
3198
+ }).join("\n");
3199
+ const unresolvedCount = comments.filter((c) => !c.resolved).length;
3609
3200
  return {
3610
- content: [
3611
- {
3612
- type: "text",
3613
- text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.`
3614
- }
3615
- ]
3616
- };
3617
- }
3618
- const formatted = comments.map((c) => {
3619
- const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
3620
- const resolved = c.resolved ? " (RESOLVED)" : "";
3621
- const link = c.linkId ? ` [${c.linkId}]` : "";
3622
- return `- **v${c.version}${link}** ${date} by ${c.author}${resolved}: ${c.body.substring(0, 150)}`;
3623
- }).join("\n");
3624
- const unresolvedCount = comments.filter((c) => !c.resolved).length;
3625
- return {
3626
- content: [
3627
- {
3201
+ content: [{
3628
3202
  type: "text",
3629
3203
  text: `# Comments for ${chainEntryId} (${comments.length}, ${unresolvedCount} unresolved)
3630
3204
 
3631
3205
  ${formatted}`
3632
- }
3633
- ]
3634
- };
3635
- }
3636
- );
3637
- server2.registerTool(
3638
- "chain-revert",
3639
- {
3640
- title: "Revert Chain",
3641
- description: "Safely revert a chain to a previous version. Creates a NEW version that restores the content of the target version \u2014 history is preserved, nothing is destroyed. Use chain-commits to see available versions.",
3642
- inputSchema: {
3643
- chainEntryId: z8.string().describe("The chain's entry ID"),
3644
- toVersion: z8.number().describe("The version number to revert to"),
3645
- author: z8.string().optional().describe("Who is reverting. Defaults to 'mcp'.")
3206
+ }]
3207
+ };
3646
3208
  }
3647
- },
3648
- async ({ chainEntryId, toVersion, author }) => {
3649
- const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
3650
- return {
3651
- content: [
3652
- {
3653
- type: "text",
3654
- text: `# Reverted
3655
-
3656
- - **Chain:** \`${result.entryId}\`
3657
- - **Reverted to:** v${result.revertedTo}
3658
- - **New version:** v${result.newVersion}
3659
- - **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
3660
-
3661
- History is preserved \u2014 this created a new version, not a destructive reset.`
3662
- }
3663
- ]
3664
- };
3209
+ return { content: [{ type: "text", text: "Unknown action." }] };
3665
3210
  }
3666
3211
  );
3667
3212
  }
@@ -3706,7 +3251,7 @@ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, h
3706
3251
  sections.push(
3707
3252
  `## Business Rules
3708
3253
  Collection: \`business-rules\` (${rulesCount}).
3709
- Find rules: \`kb-search\` for text search, \`list-entries collection=business-rules\` to browse.
3254
+ Find rules: \`search\` for text search, \`list-entries collection=business-rules\` to browse.
3710
3255
  Check compliance: use the \`review-against-rules\` prompt (pass a domain).
3711
3256
  Draft a new rule: use the \`draft-rule-from-context\` prompt.`
3712
3257
  );
@@ -3723,16 +3268,16 @@ Browse: \`list-entries collection=tracking-events\`. Full setup: \`docs/posthog-
3723
3268
  "## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept\n- `informs` \u2014 a decision or insight informs a feature\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `gather-context` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `suggest-links` \u2014 discover potential connections for an entry\n- `relate-entries` \u2014 create a typed link between two entries\n- `find-related` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `suggest-links` to discover and create relevant relations."
3724
3269
  );
3725
3270
  sections.push(
3726
- "## Creating Knowledge\nUse `smart-capture` as the primary tool for creating new entries. It handles the full workflow in one call:\n1. Creates the entry with collection-aware defaults (auto-fills dates, infers domains, sets priority)\n2. Auto-links related entries from across the knowledge base (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`.\nAll other collections use sensible defaults.\n\nExample: `smart-capture collection='tensions' name='...' description='...'`\n\nUse `quality-check` to score existing entries retroactively.\nUse `create-entry` only when you need full control over every field.\nUse `quick-capture` for minimal ceremony without auto-linking."
3271
+ "## Creating Knowledge\nUse `capture` as the only tool for creating new entries. It handles the full workflow in one call:\n1. Creates the entry with collection-aware defaults (auto-fills dates, infers domains, sets priority)\n2. Auto-links related entries from across the chain (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`, `features`.\nAll other collections use sensible defaults.\n\nExample: `capture collection='tensions' name='...' description='...'`\n\nUse `quality-check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments when you need full field control."
3727
3272
  );
3728
3273
  sections.push(
3729
- "## Where to Go Next\n- **Create entry** \u2192 `smart-capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (start here when working on a feature)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `list-labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Architecture map** \u2192 `show-architecture` tool (layered system visualization)\n- **Explore a layer** \u2192 `explore-layer` tool (drill into Auth, Core, Features, etc.)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
3274
+ "## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (by entry ID or task description)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Architecture map** \u2192 `architecture action=show` tool (layered system visualization)\n- **Explore a layer** \u2192 `architecture action=explore` tool (drill into Auth, Core, Features, etc.)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
3730
3275
  );
3731
3276
  return sections.join("\n\n---\n\n");
3732
3277
  }
3733
3278
  function registerResources(server2) {
3734
3279
  server2.resource(
3735
- "kb-orientation",
3280
+ "chain-orientation",
3736
3281
  "productbrain://orientation",
3737
3282
  async (uri) => {
3738
3283
  const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
@@ -3755,7 +3300,7 @@ function registerResources(server2) {
3755
3300
  }
3756
3301
  );
3757
3302
  server2.resource(
3758
- "kb-terminology",
3303
+ "chain-terminology",
3759
3304
  "productbrain://terminology",
3760
3305
  async (uri) => {
3761
3306
  const [glossaryResult, standardsResult] = await Promise.allSettled([
@@ -3770,7 +3315,7 @@ function registerResources(server2) {
3770
3315
 
3771
3316
  ${terms}`);
3772
3317
  } else {
3773
- lines.push("## Glossary\n\nNo glossary terms yet. Use `create-entry` with collection `glossary` to add terms.");
3318
+ lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
3774
3319
  }
3775
3320
  } else {
3776
3321
  lines.push("## Glossary\n\nCould not load glossary \u2014 use `list-entries collection=glossary` to browse manually.");
@@ -3782,7 +3327,7 @@ ${terms}`);
3782
3327
 
3783
3328
  ${stds}`);
3784
3329
  } else {
3785
- lines.push("## Standards\n\nNo standards yet. Use `create-entry` with collection `standards` to add standards.");
3330
+ lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
3786
3331
  }
3787
3332
  } else {
3788
3333
  lines.push("## Standards\n\nCould not load standards \u2014 use `list-entries collection=standards` to browse manually.");
@@ -3793,7 +3338,7 @@ ${stds}`);
3793
3338
  }
3794
3339
  );
3795
3340
  server2.resource(
3796
- "kb-collections",
3341
+ "chain-collections",
3797
3342
  "productbrain://collections",
3798
3343
  async (uri) => {
3799
3344
  const collections = await mcpQuery("kb.listCollections");
@@ -3816,7 +3361,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3816
3361
  }
3817
3362
  );
3818
3363
  server2.resource(
3819
- "kb-collection-entries",
3364
+ "chain-collection-entries",
3820
3365
  new ResourceTemplate("productbrain://{slug}/entries", {
3821
3366
  list: async () => {
3822
3367
  const collections = await mcpQuery("kb.listCollections");
@@ -3841,7 +3386,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3841
3386
  }
3842
3387
  );
3843
3388
  server2.resource(
3844
- "kb-labels",
3389
+ "chain-labels",
3845
3390
  "productbrain://labels",
3846
3391
  async (uri) => {
3847
3392
  const labels = await mcpQuery("kb.listLabels");
@@ -3995,7 +3540,7 @@ Structure the decision record with:
3995
3540
  6. **Alternatives considered**: What else was on the table
3996
3541
  7. **Related rules or tensions**: Any business rules or tensions this connects to
3997
3542
 
3998
- After drafting, I can log it using the create-entry tool with collection "decisions".`
3543
+ After drafting, I can log it using the capture tool with collection "decisions".`
3999
3544
  }
4000
3545
  }
4001
3546
  ]
@@ -4041,7 +3586,7 @@ Draft the rule with these fields:
4041
3586
  7. **data.platforms**: Which platforms are affected
4042
3587
  8. **data.relatedRules**: Any related existing rules
4043
3588
 
4044
- Make sure the rule is consistent with existing rules and doesn't contradict them. After drafting, I can create it using the create-entry tool with collection "business-rules".`
3589
+ Make sure the rule is consistent with existing rules and doesn't contradict them. After drafting, I can create it using the capture tool with collection "business-rules".`
4045
3590
  }
4046
3591
  }
4047
3592
  ]
@@ -4092,7 +3637,7 @@ Use one of these IDs to run a workflow.`
4092
3637
  ` + sorted.map((d) => `- ${d.entryId ?? ""}: ${d.name} [${d.status}]`).join("\n");
4093
3638
  }
4094
3639
  } catch {
4095
- kbContext = "\n_Could not load KB context \u2014 proceed without it._";
3640
+ kbContext = "\n_Could not load chain context \u2014 proceed without it._";
4096
3641
  }
4097
3642
  const roundsPlan = wf.rounds.map(
4098
3643
  (r) => `### Round ${r.num}: ${r.label}
@@ -4139,9 +3684,9 @@ ${wf.errorRecovery}
4139
3684
 
4140
3685
  ---
4141
3686
 
4142
- ## KB Output
3687
+ ## Chain Output
4143
3688
 
4144
- When complete, use \`smart-capture\` to create a \`${wf.kbOutputCollection}\` entry.
3689
+ When complete, use \`capture\` to create a \`${wf.kbOutputCollection}\` entry.
4145
3690
  Name template: ${wf.kbOutputTemplate.nameTemplate}
4146
3691
  Description field: ${wf.kbOutputTemplate.descriptionField}
4147
3692
  ` + kbContext + `
@@ -4172,7 +3717,7 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
4172
3717
  }
4173
3718
  }
4174
3719
  bootstrapCloudMode();
4175
- var SERVER_VERSION = "1.0.0";
3720
+ var SERVER_VERSION = "2.0.0";
4176
3721
  initAnalytics();
4177
3722
  var workspaceId;
4178
3723
  try {
@@ -4199,39 +3744,37 @@ var server = new McpServer2(
4199
3744
  "Product Brain \u2014 the single source of truth for product knowledge.",
4200
3745
  "Terminology, standards, and core data all live here \u2014 no need to check external docs.",
4201
3746
  "",
4202
- "Terminology & naming: For 'what is X?' or naming questions, fetch `productbrain://terminology`",
4203
- "or use the `name-check` prompt to validate names against the glossary.",
4204
- "",
4205
3747
  "Workflow:",
4206
3748
  " 1. Verify: call `health` to confirm connectivity.",
4207
- " 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt for naming questions.",
4208
- " 3. Discover: use `kb-search` to find entries by text, or `list-entries` to browse a collection.",
3749
+ " 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt.",
3750
+ " 3. Discover: use `search` to find entries, or `list-entries` to browse.",
4209
3751
  " 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
4210
- " 5. Capture: use `smart-capture` to create entries \u2014 it auto-links related entries and",
4211
- " returns a quality scorecard in one call. Use `create-entry` only when you need",
4212
- " full control over every field.",
4213
- " 6. Connect: use `suggest-links` then `relate-entries` to build the graph.",
4214
- " 7. Quality: use `quality-check` to assess entry completeness.",
4215
- " 8. Debug: use `mcp-audit` to see what backend calls happened this session.",
3752
+ " 5. Context: use `gather-context` with an entryId or a task description.",
3753
+ " 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
3754
+ " 7. Connect: use `suggest-links` then `relate-entries` to build the graph.",
3755
+ " 8. Quality: use `quality-check` to assess entry completeness.",
3756
+ " 9. Labels: use `labels` to list, create, or apply labels.",
3757
+ " 10. Debug: use `mcp-audit` to see what backend calls happened this session.",
4216
3758
  "",
4217
- "Always prefer `smart-capture` over `create-entry` or `quick-capture` for new entries.",
4218
- "Always prefer kb-search or list-entries before get-entry \u2014 discover, then drill in.",
3759
+ "Always discover before drilling in (search or list-entries before get-entry).",
4219
3760
  "",
4220
3761
  "Orientation:",
4221
- " When you need to understand the system \u2014 architecture, data model, rules,",
4222
- " or analytics \u2014 fetch the `productbrain://orientation` resource first.",
4223
- " It gives you the map. Then use the appropriate tool to drill in."
3762
+ " Fetch `productbrain://orientation` to understand architecture, data model, rules,",
3763
+ " and analytics. It is the map. Then use tools to drill in."
4224
3764
  ].join("\n")
4225
3765
  }
4226
3766
  );
3767
+ var enabledModules = new Set(
3768
+ (process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
3769
+ );
4227
3770
  registerKnowledgeTools(server);
4228
3771
  registerLabelTools(server);
4229
3772
  registerHealthTools(server);
4230
3773
  registerVerifyTools(server);
4231
3774
  registerSmartCaptureTools(server);
4232
- registerArchitectureTools(server);
4233
3775
  registerWorkflowTools(server);
4234
- registerGitChainTools(server);
3776
+ if (enabledModules.has("gitchain")) registerGitChainTools(server);
3777
+ if (enabledModules.has("arch")) registerArchitectureTools(server);
4235
3778
  registerResources(server);
4236
3779
  registerPrompts(server);
4237
3780
  var transport = new StdioServerTransport();