@productbrain/mcp 0.0.1-beta.3 → 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
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-DGUM43GV.js";
3
-
4
2
  // src/index.ts
5
3
  import { readFileSync as readFileSync3 } from "fs";
6
4
  import { resolve as resolve3 } from "path";
@@ -193,7 +191,7 @@ function registerKnowledgeTools(server2) {
193
191
  "list-collections",
194
192
  {
195
193
  title: "Browse Collections",
196
- 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.",
197
195
  annotations: { readOnlyHint: true }
198
196
  },
199
197
  async () => {
@@ -262,7 +260,7 @@ ${formatted}` }]
262
260
  "get-entry",
263
261
  {
264
262
  title: "Look Up Entry",
265
- 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.",
266
264
  inputSchema: {
267
265
  entryId: z.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001', 'EVT-workspace_created'")
268
266
  },
@@ -271,7 +269,7 @@ ${formatted}` }]
271
269
  async ({ entryId }) => {
272
270
  const entry = await mcpQuery("kb.getEntry", { entryId });
273
271
  if (!entry) {
274
- 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.` }] };
275
273
  }
276
274
  const lines = [
277
275
  `# ${entry.entryId ? `${entry.entryId}: ` : ""}${entry.name}`,
@@ -309,81 +307,6 @@ ${formatted}` }]
309
307
  return { content: [{ type: "text", text: lines.join("\n") }] };
310
308
  }
311
309
  );
312
- const governedCollections = /* @__PURE__ */ new Set([
313
- "glossary",
314
- "business-rules",
315
- "principles",
316
- "standards",
317
- "strategy"
318
- ]);
319
- server2.registerTool(
320
- "create-entry",
321
- {
322
- title: "Create Entry",
323
- 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.",
324
- inputSchema: {
325
- collection: z.string().describe("Collection slug, e.g. 'tracking-events', 'standards', 'glossary'"),
326
- entryId: z.string().optional().describe("Human-readable ID, e.g. 'EVT-workspace_created', 'STD-posthog-events'"),
327
- name: z.string().describe("Display name"),
328
- status: z.string().default("draft").describe("Lifecycle status: draft | active | verified | deprecated"),
329
- data: z.record(z.unknown()).describe("Data object \u2014 keys must match the collection's field definitions"),
330
- order: z.number().optional().describe("Manual sort order within the collection")
331
- },
332
- annotations: { destructiveHint: false }
333
- },
334
- async ({ collection, entryId, name, status, data, order }) => {
335
- if (governedCollections.has(collection) && status !== "draft" && status !== "deprecated") {
336
- return {
337
- content: [{
338
- type: "text",
339
- text: `# Governance Required
340
-
341
- The \`${collection}\` collection is governed. New entries must be created with status \`draft\`.
342
-
343
- **How to proceed:**
344
- 1. Create the entry with status \`draft\` (treated as a proposal)
345
- 2. Raise a tension in the \`tensions\` collection to request promotion
346
- 3. After approval, use \`update-entry\` to change status to \`active\` or \`verified\``
347
- }]
348
- };
349
- }
350
- try {
351
- const id = await mcpMutation("kb.createEntry", {
352
- collectionSlug: collection,
353
- entryId,
354
- name,
355
- status,
356
- data,
357
- order
358
- });
359
- return {
360
- content: [{ type: "text", text: `# Entry Created
361
-
362
- **${entryId ?? name}** added to \`${collection}\` as \`${status}\`.
363
-
364
- Internal ID: ${id}` }]
365
- };
366
- } catch (error) {
367
- const msg = error instanceof Error ? error.message : String(error);
368
- if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
369
- return {
370
- content: [{
371
- type: "text",
372
- text: `# Cannot Create \u2014 Duplicate Detected
373
-
374
- ${msg}
375
-
376
- **What to do:**
377
- - Use \`get-entry\` to inspect the existing entry
378
- - Use \`update-entry\` to modify it
379
- - If a genuinely new entry is needed, raise a tension to propose it`
380
- }]
381
- };
382
- }
383
- throw error;
384
- }
385
- }
386
- );
387
310
  server2.registerTool(
388
311
  "update-entry",
389
312
  {
@@ -428,7 +351,7 @@ Internal ID: ${id}` }]
428
351
  Tension status (open, in-progress, closed) must be changed through the defined process, not via MCP.
429
352
 
430
353
  **What you can do:**
431
- - Create tensions: \`create-entry collection=tensions name="..." status=open\`
354
+ - Create tensions: \`capture collection=tensions name="..." description="..."\`
432
355
  - List tensions: \`list-entries collection=tensions\`
433
356
  - Update non-status fields (raised, date, priority, description) via \`update-entry\`
434
357
  - After process approval, a human uses the Product Brain UI to change status
@@ -442,10 +365,10 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
442
365
  }
443
366
  );
444
367
  server2.registerTool(
445
- "kb-search",
368
+ "search",
446
369
  {
447
- title: "Search Knowledge Base",
448
- 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.",
449
372
  inputSchema: {
450
373
  query: z.string().describe("Search text (min 2 characters)"),
451
374
  collection: z.string().optional().describe("Scope to a collection slug, e.g. 'business-rules', 'glossary', 'tracking-events'"),
@@ -566,7 +489,7 @@ ${formatted}` }]
566
489
  }
567
490
  const sourceEntry = await mcpQuery("kb.getEntry", { entryId });
568
491
  if (!sourceEntry) {
569
- 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.` }] };
570
493
  }
571
494
  const sourceInternalId = sourceEntry._id;
572
495
  const MAX_RELATIONS = 25;
@@ -625,18 +548,113 @@ ${formatted}` }]
625
548
  server2.registerTool(
626
549
  "gather-context",
627
550
  {
628
- title: "Gather Full Context",
629
- 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.",
630
553
  inputSchema: {
631
- entryId: z.string().describe("Entry ID, e.g. 'FEAT-001', 'GT-019', 'BR-007'"),
632
- 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)")
633
558
  },
634
559
  annotations: { readOnlyHint: true }
635
560
  },
636
- 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
+ }
637
655
  const result = await mcpQuery("kb.gatherContext", { entryId, maxHops });
638
656
  if (!result?.root) {
639
- 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.` }] };
640
658
  }
641
659
  if (result.related.length === 0) {
642
660
  return {
@@ -678,7 +696,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
678
696
  "suggest-links",
679
697
  {
680
698
  title: "Suggest Links",
681
- 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.",
682
700
  inputSchema: {
683
701
  entryId: z.string().describe("Entry ID to find suggestions for, e.g. 'FEAT-001'"),
684
702
  limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return")
@@ -688,7 +706,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
688
706
  async ({ entryId, limit }) => {
689
707
  const entry = await mcpQuery("kb.getEntry", { entryId });
690
708
  if (!entry) {
691
- 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.` }] };
692
710
  }
693
711
  const searchTerms = [entry.name];
694
712
  if (entry.data?.description) searchTerms.push(entry.data.description);
@@ -701,7 +719,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
701
719
  }
702
720
  const results = await mcpQuery("kb.searchEntries", { query: queryText });
703
721
  if (!results || results.length === 0) {
704
- 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.` }] };
705
723
  }
706
724
  const existingRelations = await mcpQuery("kb.listEntryRelations", { entryId });
707
725
  const relatedIds = new Set(
@@ -736,273 +754,59 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
736
754
  return { content: [{ type: "text", text: lines.join("\n") }] };
737
755
  }
738
756
  );
739
- server2.registerTool(
740
- "quick-capture",
741
- {
742
- title: "Quick Capture",
743
- 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).",
744
- inputSchema: {
745
- collection: z.string().describe("Collection slug, e.g. 'business-rules', 'glossary', 'tensions', 'decisions'"),
746
- name: z.string().describe("Display name for the entry"),
747
- description: z.string().describe("Short description \u2014 the essential context to capture now"),
748
- entryId: z.string().optional().describe("Optional human-readable ID (e.g. 'SOS-020', 'GT-031')")
749
- },
750
- annotations: { destructiveHint: false }
751
- },
752
- async ({ collection, name, description, entryId }) => {
753
- const col = await mcpQuery("kb.getCollection", { slug: collection });
754
- if (!col) {
755
- return { content: [{ type: "text", text: `Collection \`${collection}\` not found. Use list-collections to see available collections.` }] };
756
- }
757
- const data = {};
758
- const emptyFields = [];
759
- for (const field of col.fields ?? []) {
760
- const key = field.key;
761
- if (key === "description" || key === "canonical" || key === "detail") {
762
- data[key] = description;
763
- } else if (field.type === "array" || field.type === "multi-select") {
764
- data[key] = [];
765
- emptyFields.push(key);
766
- } else if (field.type === "select") {
767
- data[key] = field.options?.[0] ?? "";
768
- emptyFields.push(key);
769
- } else {
770
- data[key] = "";
771
- emptyFields.push(key);
772
- }
773
- }
774
- if (!data.description && !data.canonical && !data.detail) {
775
- data.description = description;
776
- }
777
- try {
778
- const id = await mcpMutation("kb.createEntry", {
779
- collectionSlug: collection,
780
- entryId,
781
- name,
782
- status: "draft",
783
- data
784
- });
785
- const emptyNote = emptyFields.length > 0 ? `
786
-
787
- **Fields to fill later** (via \`update-entry\`):
788
- ${emptyFields.map((f) => `- \`${f}\``).join("\n")}` : "";
789
- return {
790
- content: [{
791
- type: "text",
792
- text: `# Quick Capture \u2014 Done
793
-
794
- **${entryId ?? name}** added to \`${collection}\` as \`draft\`.
795
-
796
- Internal ID: ${id}${emptyNote}
797
-
798
- _Use \`update-entry\` to fill in details when ready. Use \`get-entry\` to review._`
799
- }]
800
- };
801
- } catch (error) {
802
- const msg = error instanceof Error ? error.message : String(error);
803
- if (msg.includes("Duplicate") || msg.includes("already exists")) {
804
- return {
805
- content: [{
806
- type: "text",
807
- text: `# Cannot Capture \u2014 Duplicate Detected
808
-
809
- ${msg}
810
-
811
- Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
812
- }]
813
- };
814
- }
815
- throw error;
816
- }
817
- }
818
- );
819
- server2.registerTool(
820
- "load-context-for-task",
821
- {
822
- title: "Load Context for Task",
823
- 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",
824
- inputSchema: {
825
- taskDescription: z.string().describe("Natural-language description of the task or user message"),
826
- maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return (default 10)"),
827
- maxHops: z.number().min(1).max(3).default(2).optional().describe("Graph traversal depth from each search hit (default 2)")
828
- },
829
- annotations: { readOnlyHint: true, openWorldHint: true }
830
- },
831
- async ({ taskDescription, maxResults, maxHops }) => {
832
- await server2.sendLoggingMessage({
833
- level: "info",
834
- data: `Loading context for task: "${taskDescription.substring(0, 80)}..."`,
835
- logger: "product-brain"
836
- });
837
- const result = await mcpQuery("kb.loadContextForTask", {
838
- taskDescription,
839
- maxResults: maxResults ?? 10,
840
- maxHops: maxHops ?? 2
841
- });
842
- if (result.confidence === "none" || result.entries.length === 0) {
843
- return {
844
- content: [{
845
- type: "text",
846
- text: `# Context Loaded
847
-
848
- **Confidence:** None
849
-
850
- No KB context found for this task. The knowledge base may not cover this area yet.
851
-
852
- _Consider capturing domain knowledge discovered during this task via \`smart-capture\`._`
853
- }]
854
- };
855
- }
856
- const byCollection = /* @__PURE__ */ new Map();
857
- for (const entry of result.entries) {
858
- const key = entry.collectionName;
859
- if (!byCollection.has(key)) byCollection.set(key, []);
860
- byCollection.get(key).push(entry);
861
- }
862
- const lines = [
863
- `# Context Loaded`,
864
- `**Confidence:** ${result.confidence.charAt(0).toUpperCase() + result.confidence.slice(1)}`,
865
- `**Matched:** ${result.entries.length} entries across ${byCollection.size} collection${byCollection.size === 1 ? "" : "s"}`,
866
- ""
867
- ];
868
- for (const [collName, entries] of byCollection) {
869
- lines.push(`### ${collName} (${entries.length})`);
870
- for (const e of entries) {
871
- const id = e.entryId ? `**${e.entryId}:** ` : "";
872
- const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}${e.relationType ? `, ${e.relationType}` : ""})_` : "";
873
- const preview = e.descriptionPreview ? `
874
- ${e.descriptionPreview}` : "";
875
- const codePaths = e.codePaths.length > 0 ? `
876
- Code: ${e.codePaths.join(", ")}` : "";
877
- lines.push(`- ${id}${e.name}${hopLabel}${preview}${codePaths}`);
878
- }
879
- lines.push("");
880
- }
881
- lines.push(`_Use \`get-entry\` for full details on any entry._`);
882
- return {
883
- content: [{ type: "text", text: lines.join("\n") }]
884
- };
885
- }
886
- );
887
- server2.registerTool(
888
- "review-rules",
889
- {
890
- title: "Review Business Rules",
891
- 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.",
892
- inputSchema: {
893
- domain: z.string().describe("Business rule domain, e.g. 'AI & MCP Integration', 'Governance & Decision-Making'"),
894
- context: z.string().optional().describe("What you're reviewing \u2014 code change, design decision, file path, etc.")
895
- },
896
- annotations: { readOnlyHint: true }
897
- },
898
- async ({ domain, context }) => {
899
- const entries = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
900
- const domainLower = domain.toLowerCase();
901
- const rules = entries.filter((e) => {
902
- const ruleDomain = e.data?.domain ?? "";
903
- return ruleDomain.toLowerCase() === domainLower || ruleDomain.toLowerCase().includes(domainLower);
904
- });
905
- if (rules.length === 0) {
906
- const allDomains = [...new Set(entries.map((e) => e.data?.domain).filter(Boolean))];
907
- return {
908
- content: [{
909
- type: "text",
910
- text: `# No Rules Found for "${domain}"
911
-
912
- Available domains:
913
- ${allDomains.map((d) => `- ${d}`).join("\n")}
914
-
915
- _Try one of the domains above, or use kb-search to find rules by keyword._`
916
- }]
917
- };
918
- }
919
- const header = context ? `# Business Rules: ${domain}
920
-
921
- **Review context:** ${context}
922
-
923
- For each rule, assess: compliant, at risk, violation, or not applicable.
924
- ` : `# Business Rules: ${domain}
925
- `;
926
- const formatted = rules.map((r) => {
927
- const id = r.entryId ? `**${r.entryId}:** ` : "";
928
- const severity = r.data?.severity ? ` | Severity: ${r.data.severity}` : "";
929
- const desc = r.data?.description ?? "";
930
- const impact = r.data?.dataImpact ? `
931
- Data impact: ${r.data.dataImpact}` : "";
932
- const related = (r.data?.relatedRules ?? []).length > 0 ? `
933
- Related: ${r.data.relatedRules.join(", ")}` : "";
934
- return `### ${id}${r.name} \`${r.status}\`${severity}
935
-
936
- ${desc}${impact}${related}`;
937
- }).join("\n\n---\n\n");
938
- const footer = `
939
- _${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._`;
940
- return {
941
- content: [{ type: "text", text: `${header}
942
- ${formatted}
943
- ${footer}` }]
944
- };
945
- }
946
- );
947
757
  }
948
758
 
949
759
  // src/tools/labels.ts
950
760
  import { z as z2 } from "zod";
951
761
  function registerLabelTools(server2) {
952
762
  server2.registerTool(
953
- "list-labels",
954
- {
955
- title: "Browse Labels",
956
- 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.",
957
- annotations: { readOnlyHint: true }
958
- },
959
- async () => {
960
- const labels = await mcpQuery("kb.listLabels");
961
- if (labels.length === 0) {
962
- return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
963
- }
964
- const groups = labels.filter((l) => l.isGroup);
965
- const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
966
- const children = (parentId) => labels.filter((l) => l.parentId === parentId);
967
- const lines = ["# Workspace Labels"];
968
- for (const group of groups) {
969
- lines.push(`
970
- ## ${group.name}`);
971
- if (group.description) lines.push(`_${group.description}_`);
972
- for (const child of children(group._id)) {
973
- const color = child.color ? ` ${child.color}` : "";
974
- lines.push(` - \`${child.slug}\` ${child.name}${color}`);
975
- }
976
- }
977
- if (ungrouped.length > 0) {
978
- lines.push("\n## Ungrouped");
979
- for (const label of ungrouped) {
980
- const color = label.color ? ` ${label.color}` : "";
981
- lines.push(`- \`${label.slug}\` ${label.name}${color}${label.description ? ` \u2014 _${label.description}_` : ""}`);
982
- }
983
- }
984
- return {
985
- content: [{ type: "text", text: lines.join("\n") }]
986
- };
987
- }
988
- );
989
- server2.registerTool(
990
- "manage-labels",
763
+ "labels",
991
764
  {
992
- title: "Manage Labels",
993
- 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).",
994
767
  inputSchema: {
995
- action: z2.enum(["create", "update", "delete"]).describe("What to do"),
996
- 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)"),
997
770
  name: z2.string().optional().describe("Display name (required for create)"),
998
771
  color: z2.string().optional().describe("Hex color, e.g. '#ef4444'"),
999
772
  description: z2.string().optional().describe("What this label means"),
1000
773
  parentSlug: z2.string().optional().describe("Parent group slug for label hierarchy"),
1001
774
  isGroup: z2.boolean().optional().describe("True if this is a group container, not a taggable label"),
1002
- 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")
1003
777
  }
1004
778
  },
1005
- 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
+ }
1006
810
  if (action === "create") {
1007
811
  if (!name) {
1008
812
  return { content: [{ type: "text", text: "Cannot create a label without a name." }] };
@@ -1012,7 +816,7 @@ function registerLabelTools(server2) {
1012
816
  const labels = await mcpQuery("kb.listLabels");
1013
817
  const parent = labels.find((l) => l.slug === parentSlug);
1014
818
  if (!parent) {
1015
- 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.` }] };
1016
820
  }
1017
821
  parentId = parent._id;
1018
822
  }
@@ -1033,27 +837,18 @@ function registerLabelTools(server2) {
1033
837
 
1034
838
  \`${slug}\` removed from all entries and deleted.` }] };
1035
839
  }
1036
- return { content: [{ type: "text", text: "Unknown action." }] };
1037
- }
1038
- );
1039
- server2.registerTool(
1040
- "label-entry",
1041
- {
1042
- title: "Tag Entry with Label",
1043
- 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.",
1044
- inputSchema: {
1045
- action: z2.enum(["apply", "remove"]).describe("Apply or remove the label"),
1046
- entryId: z2.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'EVT-workspace_created'"),
1047
- label: z2.string().describe("Label slug to apply/remove")
1048
- }
1049
- },
1050
- async ({ action, entryId, label }) => {
1051
- if (action === "apply") {
1052
- await mcpMutation("kb.applyLabel", { entryId, labelSlug: label });
1053
- 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}**.` }] };
1054
850
  }
1055
- await mcpMutation("kb.removeLabel", { entryId, labelSlug: label });
1056
- return { content: [{ type: "text", text: `Label \`${label}\` removed from **${entryId}**.` }] };
851
+ return { content: [{ type: "text", text: "Unknown action." }] };
1057
852
  }
1058
853
  );
1059
854
  }
@@ -1187,7 +982,7 @@ function registerHealthTools(server2) {
1187
982
  "mcp-audit",
1188
983
  {
1189
984
  title: "Session Audit Log",
1190
- 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.",
1191
986
  inputSchema: {
1192
987
  limit: z3.number().min(1).max(50).default(20).describe("How many recent calls to show (max 50)")
1193
988
  },
@@ -1369,7 +1164,7 @@ function registerVerifyTools(server2) {
1369
1164
  server2.registerTool(
1370
1165
  "verify",
1371
1166
  {
1372
- title: "Verify Knowledge Base",
1167
+ title: "Verify the Chain",
1373
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.",
1374
1169
  inputSchema: {
1375
1170
  collection: z4.string().default("glossary").describe("Collection slug to verify (default: glossary)"),
@@ -1492,7 +1287,7 @@ function registerVerifyTools(server2) {
1492
1287
  import { z as z5 } from "zod";
1493
1288
  var AREA_KEYWORDS = {
1494
1289
  "Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
1495
- "Knowledge Base": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph"],
1290
+ "Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
1496
1291
  "AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
1497
1292
  "Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
1498
1293
  "Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
@@ -1532,7 +1327,7 @@ var COMMON_CHECKS = {
1532
1327
  id: "has-relations",
1533
1328
  label: "At least 1 relation created",
1534
1329
  check: (ctx) => ctx.linksCreated.length >= 1,
1535
- 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."
1536
1331
  },
1537
1332
  diverseRelations: {
1538
1333
  id: "diverse-relations",
@@ -1646,7 +1441,7 @@ var PROFILES = /* @__PURE__ */ new Map([
1646
1441
  if (area) {
1647
1442
  const categoryMap = {
1648
1443
  "Architecture": "Platform & Architecture",
1649
- "Knowledge Base": "Knowledge Management",
1444
+ "Chain": "Knowledge Management",
1650
1445
  "AI & MCP Integration": "AI & Developer Tools",
1651
1446
  "Developer Experience": "AI & Developer Tools",
1652
1447
  "Governance & Decision-Making": "Governance & Process",
@@ -1838,7 +1633,7 @@ async function checkEntryQuality(entryId) {
1838
1633
  const entry = await mcpQuery("kb.getEntry", { entryId });
1839
1634
  if (!entry) {
1840
1635
  return {
1841
- 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.`,
1842
1637
  quality: { score: 0, maxScore: 10, checks: [] }
1843
1638
  };
1844
1639
  }
@@ -1899,10 +1694,10 @@ var MAX_AUTO_LINKS = 5;
1899
1694
  var MAX_SUGGESTIONS = 5;
1900
1695
  function registerSmartCaptureTools(server2) {
1901
1696
  server2.registerTool(
1902
- "smart-capture",
1697
+ "capture",
1903
1698
  {
1904
- title: "Smart Capture",
1905
- 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.",
1906
1701
  inputSchema: {
1907
1702
  collection: z5.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
1908
1703
  name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
@@ -1970,7 +1765,7 @@ function registerSmartCaptureTools(server2) {
1970
1765
  name,
1971
1766
  status,
1972
1767
  data,
1973
- createdBy: "smart-capture"
1768
+ createdBy: "capture"
1974
1769
  });
1975
1770
  } catch (error) {
1976
1771
  const msg = error instanceof Error ? error.message : String(error);
@@ -2230,7 +2025,7 @@ var SEED_NODES = [
2230
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." } },
2231
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." } },
2232
2027
  // Core layer
2233
- { 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." } },
2234
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." } },
2235
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." } },
2236
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." } },
@@ -2248,7 +2043,7 @@ var SEED_NODES = [
2248
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." } }
2249
2044
  ];
2250
2045
  var SEED_FLOWS = [
2251
- { 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" } },
2252
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" } },
2253
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" } },
2254
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" } },
@@ -2256,247 +2051,208 @@ var SEED_FLOWS = [
2256
2051
  ];
2257
2052
  function registerArchitectureTools(server2) {
2258
2053
  server2.registerTool(
2259
- "show-architecture",
2054
+ "architecture",
2260
2055
  {
2261
- title: "Show Architecture",
2262
- 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",
2263
2058
  inputSchema: {
2264
- 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'")
2265
2063
  },
2266
2064
  annotations: { readOnlyHint: true }
2267
2065
  },
2268
- async ({ template }) => {
2066
+ async ({ action, template, layer, flow }) => {
2269
2067
  await ensureCollection();
2270
2068
  const all = await listArchEntries();
2271
- const templates = byTag(all, "template");
2272
- const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
2273
- const templateName = activeTemplate?.name ?? "System Architecture";
2274
- const templateId = activeTemplate?.entryId;
2275
- const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
2276
- const nodes = byTag(all, "node");
2277
- const flows = byTag(all, "flow");
2278
- if (layers.length === 0) {
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}
2090
+
2091
+ ${textLayers}${textFlows}`;
2092
+ const html = renderArchitectureHtml(layers, nodes, flows, templateName);
2093
+ return {
2094
+ content: [
2095
+ { type: "text", text },
2096
+ { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
2097
+ ]
2098
+ };
2099
+ }
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 ? `
2115
+ **Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
2116
+ ` : "";
2117
+ const layerRationale = target.data?.rationale ? `
2118
+ > ${target.data.rationale}
2119
+ ` : "";
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(`
2126
+ **Why here?** ${n.data.rationale}`);
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") : "";
2279
2130
  return {
2280
2131
  content: [{
2281
2132
  type: "text",
2282
- text: "# Architecture Explorer\n\nNo architecture data found. Use `seed-architecture` to populate the default Product OS architecture."
2133
+ text: `# ${target.data?.icon ?? ""} ${target.name} Layer
2134
+
2135
+ ${target.data?.description ?? ""}${depRule}${layerRationale}
2136
+ **${nodes.length} components**
2137
+
2138
+ ${nodeDetail}${flowLines}`
2283
2139
  }]
2284
2140
  };
2285
2141
  }
2286
- const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
2287
- const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
2288
- (f) => `- **${f.name}**: ${f.data?.description ?? ""}`
2289
- ).join("\n") : "";
2290
- const text = `# ${templateName}
2291
-
2292
- ${textLayers}${textFlows}`;
2293
- const html = renderArchitectureHtml(layers, nodes, flows, templateName);
2294
- return {
2295
- content: [
2296
- { type: "text", text },
2297
- { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
2298
- ]
2299
- };
2300
- }
2301
- );
2302
- server2.registerTool(
2303
- "explore-layer",
2304
- {
2305
- title: "Explore Architecture Layer",
2306
- description: "Drill into a specific architecture layer to see all its component nodes with descriptions, team ownership, file paths, and linked entry counts.",
2307
- inputSchema: {
2308
- layer: z6.string().describe("Layer name or entry ID, e.g. 'Core' or 'ARCH-layer-core'")
2309
- },
2310
- annotations: { readOnlyHint: true }
2311
- },
2312
- async ({ layer }) => {
2313
- await ensureCollection();
2314
- const all = await listArchEntries();
2315
- const layers = byTag(all, "layer");
2316
- const target = layers.find(
2317
- (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
2318
- );
2319
- if (!target) {
2320
- const available = layers.map((l) => `\`${l.name}\``).join(", ");
2321
- return {
2322
- content: [{
2323
- type: "text",
2324
- text: `Layer "${layer}" not found. Available layers: ${available}`
2325
- }]
2326
- };
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") }] };
2327
2171
  }
2328
- const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
2329
- const flows = byTag(all, "flow").filter(
2330
- (f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
2331
- );
2332
- const depRule = target.data?.dependsOn ? `
2333
- **Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
2334
- ` : "";
2335
- const layerRationale = target.data?.rationale ? `
2336
- > ${target.data.rationale}
2337
- ` : "";
2338
- const nodeDetail = nodes.map((n) => {
2339
- const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
2340
- if (n.data?.description) lines.push(String(n.data.description));
2341
- if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
2342
- if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
2343
- if (n.data?.rationale) lines.push(`
2344
- **Why here?** ${n.data.rationale}`);
2345
- return lines.join("\n");
2346
- }).join("\n\n");
2347
- const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
2348
- return {
2349
- content: [{
2350
- type: "text",
2351
- text: `# ${target.data?.icon ?? ""} ${target.name} Layer
2352
-
2353
- ${target.data?.description ?? ""}${depRule}${layerRationale}
2354
- **${nodes.length} components**
2355
-
2356
- ${nodeDetail}${flowLines}`
2357
- }]
2358
- };
2172
+ return { content: [{ type: "text", text: "Unknown action." }] };
2359
2173
  }
2360
2174
  );
2361
2175
  server2.registerTool(
2362
- "show-flow",
2176
+ "architecture-admin",
2363
2177
  {
2364
- title: "Show Architecture Flow",
2365
- description: "Visualize a specific data flow path between architecture nodes \u2014 shows source, target, and description.",
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.",
2366
2180
  inputSchema: {
2367
- flow: z6.string().describe("Flow name or entry ID, e.g. 'Smart Capture Flow' or 'ARCH-flow-smart-capture'")
2368
- },
2369
- annotations: { readOnlyHint: true }
2370
- },
2371
- async ({ flow }) => {
2372
- await ensureCollection();
2373
- const all = await listArchEntries();
2374
- const flows = byTag(all, "flow");
2375
- const target = flows.find(
2376
- (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
2377
- );
2378
- if (!target) {
2379
- const available = flows.map((f) => `\`${f.name}\``).join(", ");
2380
- return {
2381
- content: [{
2382
- type: "text",
2383
- text: `Flow "${flow}" not found. Available flows: ${available}`
2384
- }]
2385
- };
2386
- }
2387
- const nodes = byTag(all, "node");
2388
- const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
2389
- const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
2390
- const lines = [
2391
- `# ${target.name}`,
2392
- "",
2393
- `**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
2394
- "",
2395
- String(target.data?.description ?? "")
2396
- ];
2397
- if (source) {
2398
- lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
2399
- if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
2181
+ action: z6.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
2400
2182
  }
2401
- if (dest) {
2402
- lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
2403
- if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
2404
- }
2405
- return { content: [{ type: "text", text: lines.join("\n") }] };
2406
- }
2407
- );
2408
- server2.registerTool(
2409
- "seed-architecture",
2410
- {
2411
- title: "Seed Architecture Data",
2412
- 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.",
2413
- annotations: { readOnlyHint: false }
2414
2183
  },
2415
- async () => {
2416
- await ensureCollection();
2417
- const existing = await listArchEntries();
2418
- const existingIds = new Set(existing.map((e) => e.entryId));
2419
- let created = 0;
2420
- let updated = 0;
2421
- let unchanged = 0;
2422
- const allSeeds = [
2423
- { ...SEED_TEMPLATE, order: 0, status: "active" },
2424
- ...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
2425
- ...SEED_NODES.map((n) => ({ ...n, status: "active" })),
2426
- ...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
2427
- ];
2428
- for (const seed of allSeeds) {
2429
- if (existingIds.has(seed.entryId)) {
2430
- const existingEntry = existing.find((e) => e.entryId === seed.entryId);
2431
- const existingData = existingEntry?.data ?? {};
2432
- const seedData = seed.data;
2433
- const hasChanges = Object.keys(seedData).some(
2434
- (k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
2435
- );
2436
- if (hasChanges) {
2437
- const mergedData = { ...existingData, ...seedData };
2438
- await mcpMutation("kb.updateEntry", {
2439
- entryId: seed.entryId,
2440
- data: mergedData
2441
- });
2442
- updated++;
2443
- } else {
2444
- 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;
2445
2214
  }
2446
- 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++;
2447
2224
  }
2448
- await mcpMutation("kb.createEntry", {
2449
- collectionSlug: COLLECTION_SLUG,
2450
- entryId: seed.entryId,
2451
- name: seed.name,
2452
- status: seed.status,
2453
- data: seed.data,
2454
- order: seed.order ?? 0
2455
- });
2456
- created++;
2457
- }
2458
- return {
2459
- content: [{
2460
- type: "text",
2461
- text: `# Architecture Seeded
2225
+ return {
2226
+ content: [{
2227
+ type: "text",
2228
+ text: `# Architecture Seeded
2462
2229
 
2463
2230
  **Created:** ${created} entries
2464
2231
  **Updated:** ${updated} (merged new fields)
2465
2232
  **Unchanged:** ${unchanged}
2466
2233
 
2467
- Use \`show-architecture\` to view the map.`
2468
- }]
2469
- };
2470
- }
2471
- );
2472
- server2.registerTool(
2473
- "check-architecture",
2474
- {
2475
- title: "Check Architecture Health",
2476
- 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.",
2477
- annotations: { readOnlyHint: true }
2478
- },
2479
- async () => {
2480
- const projectRoot = resolveProjectRoot2();
2481
- if (!projectRoot) {
2482
- return {
2483
- content: [{
2484
- type: "text",
2485
- 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.`
2486
2235
  }]
2487
2236
  };
2488
2237
  }
2489
- await ensureCollection();
2490
- const all = await listArchEntries();
2491
- const layers = byTag(all, "layer");
2492
- const nodes = byTag(all, "node");
2493
- const result = scanDependencies(projectRoot, layers, nodes);
2494
- return {
2495
- content: [{
2496
- type: "text",
2497
- text: formatScanReport(result)
2498
- }]
2499
- };
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." }] };
2500
2256
  }
2501
2257
  );
2502
2258
  }
@@ -2721,7 +2477,7 @@ import { z as z7 } from "zod";
2721
2477
  var RETRO_WORKFLOW = {
2722
2478
  id: "retro",
2723
2479
  name: "Retrospective",
2724
- 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.",
2725
2481
  icon: "\u25CE",
2726
2482
  facilitatorPreamble: `You are now in **Facilitator Mode**. You are not a coding assistant \u2014 you are a facilitator running a structured retrospective.
2727
2483
 
@@ -2733,7 +2489,7 @@ var RETRO_WORKFLOW = {
2733
2489
  4. **Create a Plan** at the start showing all rounds as tasks. Update it as you progress.
2734
2490
  5. **Synthesize between rounds.** After collecting input, reflect back what you heard before moving on.
2735
2491
  6. **Never go silent.** If something fails, say what happened and what to do next.
2736
- 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.
2737
2493
  8. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
2738
2494
 
2739
2495
  ## Communication Style
@@ -2857,7 +2613,7 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
2857
2613
  label: "Close & Capture",
2858
2614
  type: "close",
2859
2615
  instruction: "One last thing \u2014 in one sentence, what's the single most important thing you're taking away from this retro?",
2860
- 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.",
2861
2617
  outputSchema: {
2862
2618
  field: "takeaway",
2863
2619
  description: "Single-sentence takeaway",
@@ -2874,11 +2630,11 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
2874
2630
  },
2875
2631
  errorRecovery: `If anything goes wrong during the retro:
2876
2632
 
2877
- 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.
2878
2634
  2. **AskQuestion not available**: Fall back to numbered options in plain text. "Reply with 1, 2, or 3."
2879
2635
  3. **Plan creation fails**: Continue without the Plan. The conversation IS the record.
2880
2636
  4. **Participant goes off-topic**: Gently redirect: "That's valuable \u2014 let's capture it. For now, let's stay with [current round]."
2881
- 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.
2882
2638
 
2883
2639
  The retro must never fail silently. Always communicate state.`
2884
2640
  };
@@ -2942,19 +2698,19 @@ ${cards}`
2942
2698
  "workflow-checkpoint",
2943
2699
  {
2944
2700
  title: "Workflow Checkpoint",
2945
- 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.",
2946
2702
  inputSchema: {
2947
2703
  workflowId: z7.string().describe("Workflow ID (e.g., 'retro')"),
2948
2704
  roundId: z7.string().describe("Round ID (e.g., 'what-went-well')"),
2949
2705
  output: z7.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
2950
2706
  isFinal: z7.boolean().optional().describe(
2951
- "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"
2952
2708
  ),
2953
2709
  summaryName: z7.string().optional().describe(
2954
- "Name for the final KB entry (required when isFinal=true)"
2710
+ "Name for the final chain entry (required when isFinal=true)"
2955
2711
  ),
2956
2712
  summaryDescription: z7.string().optional().describe(
2957
- "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)"
2958
2714
  )
2959
2715
  },
2960
2716
  annotations: { destructiveHint: false }
@@ -3002,20 +2758,20 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3002
2758
  createdBy: `workflow:${wf.id}`
3003
2759
  });
3004
2760
  lines.push(
3005
- `**KB Entry Created**: \`${entryId}\``,
2761
+ `**Entry Committed**: \`${entryId}\``,
3006
2762
  `Collection: \`${wf.kbOutputCollection}\``,
3007
2763
  `Name: ${summaryName}`,
3008
2764
  "",
3009
- `The retro is now captured in the Knowledge Base. `,
2765
+ `The retro is now committed to the Chain. `,
3010
2766
  `Use \`suggest-links\` on this entry to connect it to related knowledge.`
3011
2767
  );
3012
2768
  } catch (err) {
3013
2769
  const msg = err instanceof Error ? err.message : String(err);
3014
2770
  lines.push(
3015
- `**KB capture failed**: ${msg}`,
2771
+ `**Chain commit failed**: ${msg}`,
3016
2772
  "",
3017
2773
  `The retro output is preserved in this conversation. `,
3018
- `You can manually create the entry later using \`smart-capture\` with:`,
2774
+ `You can manually create the entry later using \`capture\` with:`,
3019
2775
  `- Collection: \`${wf.kbOutputCollection}\``,
3020
2776
  `- Name: ${summaryName}`,
3021
2777
  `- Description: (copy from the conversation summary above)`
@@ -3037,7 +2793,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3037
2793
  } else {
3038
2794
  lines.push(
3039
2795
  "",
3040
- `**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.`
3041
2797
  );
3042
2798
  }
3043
2799
  }
@@ -3057,68 +2813,57 @@ function linkSummary(links) {
3057
2813
  }
3058
2814
  function registerGitChainTools(server2) {
3059
2815
  server2.registerTool(
3060
- "chain-create",
2816
+ "chain",
3061
2817
  {
3062
- title: "Create Chain",
3063
- 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).",
3064
2820
  inputSchema: {
3065
- title: z8.string().describe("Title for the chain, e.g. 'Q1 Growth Strategy'"),
3066
- chainTypeId: z8.string().default("strategy-coherence").describe(
3067
- "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)"
3068
- ),
3069
- description: z8.string().optional().describe("Optional description of what this chain is about"),
3070
- 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'.")
3071
2830
  }
3072
2831
  },
3073
- async ({ title, chainTypeId, description, author }) => {
3074
- const result = await mcpMutation(
3075
- "gitchain.createChain",
3076
- { title, chainTypeId, description, author }
3077
- );
3078
- return {
3079
- content: [
3080
- {
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: [{
3081
2841
  type: "text",
3082
- text: `# Chain Created
2842
+ text: `# Process Created
3083
2843
 
3084
2844
  - **Entry ID:** \`${result.entryId}\`
3085
2845
  - **Title:** ${title}
3086
2846
  - **Type:** ${chainTypeId}
3087
2847
  - **Status:** draft
3088
2848
 
3089
- Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in links.`
3090
- }
3091
- ]
3092
- };
3093
- }
3094
- );
3095
- server2.registerTool(
3096
- "chain-get",
3097
- {
3098
- title: "Get Chain",
3099
- 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.",
3100
- inputSchema: {
3101
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
3102
- },
3103
- annotations: { readOnlyHint: true }
3104
- },
3105
- async ({ chainEntryId }) => {
3106
- const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
3107
- if (!chain) {
3108
- return {
3109
- content: [
3110
- {
3111
- type: "text",
3112
- text: `Chain "${chainEntryId}" not found.`
3113
- }
3114
- ]
2849
+ Use \`chain action=edit chainEntryId="${result.entryId}" linkId="problem" content="..."\` to start filling in links.`
2850
+ }]
3115
2851
  };
3116
2852
  }
3117
- 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 ? `
3118
2860
  ## Coherence: ${chain.coherenceScore}%
3119
2861
 
3120
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") : "";
3121
- const text = `# ${chain.name}
2863
+ return {
2864
+ content: [{
2865
+ type: "text",
2866
+ text: `# ${chain.name}
3122
2867
 
3123
2868
  - **Entry ID:** \`${chain.entryId}\`
3124
2869
  - **Type:** ${chain.chainTypeName}
@@ -3131,29 +2876,32 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
3131
2876
 
3132
2877
  ## Links
3133
2878
 
3134
- ` + linkSummary(chain.links);
3135
- return { content: [{ type: "text", text }] };
3136
- }
3137
- );
3138
- server2.registerTool(
3139
- "chain-edit",
3140
- {
3141
- title: "Edit Chain Link",
3142
- 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.",
3143
- inputSchema: {
3144
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3145
- linkId: z8.string().describe(
3146
- "Which link to edit. For strategy-coherence: problem, insight, choice, action, outcome. For idm-proposal: tension, proposal, objections, integration, decision."
3147
- ),
3148
- content: z8.string().describe("The full new content for this link"),
3149
- author: z8.string().optional().describe("Who is making this edit. Defaults to 'mcp'.")
2879
+ ` + linkSummary(chain.links)
2880
+ }]
2881
+ };
3150
2882
  }
3151
- },
3152
- async ({ chainEntryId, linkId, content, author }) => {
3153
- const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
3154
- return {
3155
- content: [
3156
- {
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: [{
3157
2905
  type: "text",
3158
2906
  text: `# Link Updated
3159
2907
 
@@ -3162,114 +2910,38 @@ Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in l
3162
2910
  - **Chain status:** ${result.status}
3163
2911
  - **Content length:** ${content.length} chars
3164
2912
 
3165
- Use \`chain-get\` to see the full chain with updated scores.`
3166
- }
3167
- ]
3168
- };
3169
- }
3170
- );
3171
- server2.registerTool(
3172
- "chain-list",
3173
- {
3174
- title: "List Chains",
3175
- description: "List all chains in the workspace, optionally filtered by chain type or status. Returns entry IDs, titles, link fill progress, and coherence scores.",
3176
- inputSchema: {
3177
- chainTypeId: z8.string().optional().describe("Filter by chain type: 'strategy-coherence' or 'idm-proposal'"),
3178
- status: z8.string().optional().describe("Filter by status: 'draft' or 'active'")
3179
- },
3180
- annotations: { readOnlyHint: true }
3181
- },
3182
- async ({ chainTypeId, status }) => {
3183
- const chains = await mcpQuery("gitchain.listChains", {
3184
- chainTypeId,
3185
- status
3186
- });
3187
- if (chains.length === 0) {
3188
- return {
3189
- content: [
3190
- {
3191
- type: "text",
3192
- text: "No chains found. Use `chain-create` to create one."
3193
- }
3194
- ]
2913
+ Use \`chain action=get\` to see the full chain with updated scores.`
2914
+ }]
3195
2915
  };
3196
2916
  }
3197
- const formatted = chains.map(
3198
- (c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
3199
- ).join("\n");
3200
- return {
3201
- content: [
3202
- {
3203
- type: "text",
3204
- text: `# Chains (${chains.length})
3205
-
3206
- ${formatted}`
3207
- }
3208
- ]
3209
- };
2917
+ return { content: [{ type: "text", text: "Unknown action." }] };
3210
2918
  }
3211
2919
  );
3212
2920
  server2.registerTool(
3213
- "chain-history",
2921
+ "chain-version",
3214
2922
  {
3215
- title: "Chain History",
3216
- 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.",
3217
2925
  inputSchema: {
3218
- 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'.")
3219
2933
  },
3220
- annotations: { readOnlyHint: true }
3221
- },
3222
- async ({ chainEntryId }) => {
3223
- const history = await mcpQuery("gitchain.getHistory", {
3224
- chainEntryId
3225
- });
3226
- if (history.length === 0) {
3227
- return {
3228
- content: [
3229
- {
3230
- type: "text",
3231
- text: `No history found for chain "${chainEntryId}".`
3232
- }
3233
- ]
3234
- };
3235
- }
3236
- const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
3237
- const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
3238
- return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
3239
- }).join("\n");
3240
- return {
3241
- content: [
3242
- {
3243
- type: "text",
3244
- text: `# History for ${chainEntryId} (${history.length} events)
3245
-
3246
- ${formatted}`
3247
- }
3248
- ]
3249
- };
3250
- }
3251
- );
3252
- server2.registerTool(
3253
- "chain-commit",
3254
- {
3255
- title: "Commit Chain",
3256
- 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.",
3257
- inputSchema: {
3258
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3259
- commitMessage: z8.string().describe(
3260
- "Commit message following convention: type(link): description. Example: 'edit(outcome): Add Q1 revenue target'"
3261
- ),
3262
- author: z8.string().optional().describe("Who is committing. Defaults to 'mcp'.")
3263
- }
2934
+ annotations: { readOnlyHint: false }
3264
2935
  },
3265
- async ({ chainEntryId, commitMessage, author }) => {
3266
- const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
3267
- 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 ? `
3268
2941
 
3269
2942
  > **Lint warning:** ${result.commitLintWarning}` : "";
3270
- return {
3271
- content: [
3272
- {
2943
+ return {
2944
+ content: [{
3273
2945
  type: "text",
3274
2946
  text: `# Committed v${result.version}
3275
2947
 
@@ -3279,229 +2951,160 @@ ${formatted}`
3279
2951
  - **Coherence:** ${result.coherenceScore}%
3280
2952
  - **Links modified:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
3281
2953
  ` + warning
3282
- }
3283
- ]
3284
- };
3285
- }
3286
- );
3287
- server2.registerTool(
3288
- "chain-commits",
3289
- {
3290
- title: "List Chain Commits",
3291
- description: "List all version snapshots (commits) for a chain, newest first. Shows version number, commit message, author, which links were modified, and status.",
3292
- inputSchema: {
3293
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
3294
- },
3295
- annotations: { readOnlyHint: true }
3296
- },
3297
- async ({ chainEntryId }) => {
3298
- const commits = await mcpQuery("gitchain.listCommits", {
3299
- chainEntryId
3300
- });
3301
- if (commits.length === 0) {
3302
- return {
3303
- content: [
3304
- {
3305
- type: "text",
3306
- text: `No commits found for chain "${chainEntryId}". Use \`chain-commit\` to create the first snapshot.`
3307
- }
3308
- ]
2954
+ }]
3309
2955
  };
3310
2956
  }
3311
- const formatted = commits.map((c) => {
3312
- const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
3313
- const msg = c.commitMessage ?? c.changeNote ?? "(no message)";
3314
- const links = c.linksModified && c.linksModified.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
3315
- return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
3316
- }).join("\n");
3317
- return {
3318
- content: [
3319
- {
3320
- type: "text",
3321
- text: `# Commits for ${chainEntryId} (${commits.length})
3322
-
3323
- ${formatted}`
3324
- }
3325
- ]
3326
- };
3327
- }
3328
- );
3329
- server2.registerTool(
3330
- "chain-diff",
3331
- {
3332
- title: "Diff Chain Versions",
3333
- 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.",
3334
- inputSchema: {
3335
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3336
- versionA: z8.number().describe("The earlier version number"),
3337
- versionB: z8.number().describe("The later version number")
3338
- },
3339
- annotations: { readOnlyHint: true }
3340
- },
3341
- async ({ chainEntryId, versionA, versionB }) => {
3342
- const diff = await mcpMutation("gitchain.diffVersions", {
3343
- chainEntryId,
3344
- versionA,
3345
- versionB
3346
- });
3347
- 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}
3348
2978
 
3349
2979
  - **Chain:** \`${diff.chainEntryId}\`
3350
2980
  - **Coherence:** ${diff.coherenceBefore}% \u2192 ${diff.coherenceAfter}% (${diff.coherenceDelta >= 0 ? "+" : ""}${diff.coherenceDelta})
3351
2981
  - **Links changed:** ${diff.linksChanged.length > 0 ? diff.linksChanged.join(", ") : "none"}
3352
2982
  `;
3353
- for (const ld of diff.linkDiffs) {
3354
- if (ld.status === "unchanged") continue;
3355
- text += `
2983
+ for (const ld of diff.linkDiffs) {
2984
+ if (ld.status === "unchanged") continue;
2985
+ text += `
3356
2986
  ## ${ld.linkId} (${ld.status})
3357
2987
 
3358
2988
  `;
3359
- const wordDiff = diff.wordDiffs[ld.linkId];
3360
- if (wordDiff && wordDiff.length > 0) {
3361
- for (const w of wordDiff) {
3362
- if (w.type === "delete") {
3363
- text += `~~${w.value.substring(0, 200)}~~`;
3364
- } else if (w.type === "insert") {
3365
- text += `**${w.value.substring(0, 200)}**`;
3366
- } else {
3367
- 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);
3368
2995
  }
2996
+ text += "\n";
3369
2997
  }
3370
- text += "\n";
3371
2998
  }
2999
+ return { content: [{ type: "text", text }] };
3372
3000
  }
3373
- return { content: [{ type: "text", text }] };
3374
- }
3375
- );
3376
- server2.registerTool(
3377
- "chain-gate",
3378
- {
3379
- title: "Chain Coherence Gate",
3380
- 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.",
3381
- inputSchema: {
3382
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3383
- commitMessage: z8.string().optional().describe("Optional commit message to lint")
3384
- },
3385
- annotations: { readOnlyHint: true }
3386
- },
3387
- async ({ chainEntryId, commitMessage }) => {
3388
- const gate = await mcpQuery("gitchain.runGate", {
3389
- chainEntryId,
3390
- commitMessage
3391
- });
3392
- const checkLines = gate.checks.map(
3393
- (c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`
3394
- ).join("\n");
3395
- const icon = gate.pass ? "PASS" : "BLOCKED";
3396
- return {
3397
- content: [
3398
- {
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: [{
3399
3006
  type: "text",
3400
- text: `# Gate: ${icon}
3007
+ text: `# Reverted
3401
3008
 
3402
- - **Score:** ${gate.score}%
3403
- - **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"}
3404
3013
 
3405
- ## 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)
3406
3028
 
3407
- ${checkLines}`
3408
- }
3409
- ]
3410
- };
3029
+ ${formatted}` }] };
3030
+ }
3031
+ return { content: [{ type: "text", text: "Unknown action." }] };
3411
3032
  }
3412
3033
  );
3413
3034
  server2.registerTool(
3414
3035
  "chain-branch",
3415
3036
  {
3416
- title: "Create or List Chain Branches",
3417
- 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.",
3418
3039
  inputSchema: {
3419
- chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
3420
- action: z8.enum(["create", "list"]).describe("'create' to make a new branch, 'list' to see all branches"),
3421
- name: z8.string().optional().describe("Branch name for 'create' action. Auto-generated if not provided."),
3422
- 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'.")
3423
3045
  }
3424
3046
  },
3425
- async ({ chainEntryId, action, name, author }) => {
3047
+ async ({ action, chainEntryId, branchName, strategy, author }) => {
3426
3048
  if (action === "create") {
3427
- const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name, author });
3049
+ const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name: branchName, author });
3428
3050
  return {
3429
- content: [
3430
- {
3431
- type: "text",
3432
- text: `# Branch Created
3051
+ content: [{
3052
+ type: "text",
3053
+ text: `# Branch Created
3433
3054
 
3434
3055
  - **Name:** ${result.name}
3435
3056
  - **Based on:** v${result.baseVersion}
3436
3057
  - **Chain:** \`${chainEntryId}\`
3437
3058
 
3438
- Edit links and commit on this branch, then use \`chain-merge\` to land changes.`
3439
- }
3440
- ]
3059
+ Edit links and commit on this branch, then use \`chain-branch action=merge\` to land changes.`
3060
+ }]
3441
3061
  };
3442
3062
  }
3443
- const branches = await mcpMutation("gitchain.listBranches", {
3444
- chainEntryId
3445
- });
3446
- if (branches.length === 0) {
3447
- return {
3448
- content: [
3449
- {
3450
- type: "text",
3451
- text: `No branches found for chain "${chainEntryId}".`
3452
- }
3453
- ]
3454
- };
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}` }] };
3455
3072
  }
3456
- const formatted = branches.map(
3457
- (b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`
3458
- ).join("\n");
3459
- return {
3460
- content: [
3461
- {
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: [{
3462
3078
  type: "text",
3463
- text: `# Branches for ${chainEntryId} (${branches.length})
3079
+ text: `# Branch Merged
3464
3080
 
3465
- ${formatted}`
3466
- }
3467
- ]
3468
- };
3469
- }
3470
- );
3471
- server2.registerTool(
3472
- "chain-conflicts",
3473
- {
3474
- title: "Check Branch Conflicts",
3475
- 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.",
3476
- inputSchema: {
3477
- chainEntryId: z8.string().describe("The chain's entry ID"),
3478
- branchName: z8.string().describe("The branch name to check for conflicts")
3479
- },
3480
- annotations: { readOnlyHint: true }
3481
- },
3482
- async ({ chainEntryId, branchName }) => {
3483
- const result = await mcpMutation("gitchain.checkConflicts", {
3484
- chainEntryId,
3485
- branchName
3486
- });
3487
- if (!result.hasConflicts) {
3488
- return {
3489
- content: [
3490
- {
3491
- type: "text",
3492
- text: `# No Conflicts
3081
+ - **Chain:** \`${result.entryId}\`
3082
+ - **Branch:** ${result.branchName} (now closed)
3083
+ - **Version:** v${result.version}
3084
+ - **Strategy:** ${result.strategy}
3493
3085
 
3494
- Branch "${branchName}" on \`${chainEntryId}\` has no conflicts with other active branches. Safe to merge.`
3495
- }
3496
- ]
3086
+ Main is now at v${result.version}. The branch has been closed.`
3087
+ }]
3497
3088
  };
3498
3089
  }
3499
- const conflictLines = result.conflicts.map(
3500
- (c) => `- **${c.linkId}** \u2014 modified by: ${c.branches.map((b) => `${b.branchName} (${b.author})`).join(", ")}`
3501
- ).join("\n");
3502
- return {
3503
- content: [
3504
- {
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: [{
3505
3108
  type: "text",
3506
3109
  text: `# Conflicts Detected
3507
3110
 
@@ -3509,67 +3112,52 @@ Branch "${branchName}" conflicts with other branches on these links:
3509
3112
 
3510
3113
  ` + conflictLines + `
3511
3114
 
3512
- Resolve conflicts before merging. One branch must update its link content to avoid overwriting.`
3513
- }
3514
- ]
3515
- };
3115
+ Resolve conflicts before merging.`
3116
+ }]
3117
+ };
3118
+ }
3119
+ return { content: [{ type: "text", text: "Unknown action." }] };
3516
3120
  }
3517
3121
  );
3518
3122
  server2.registerTool(
3519
- "chain-merge",
3123
+ "chain-review",
3520
3124
  {
3521
- title: "Merge Branch",
3522
- 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.",
3523
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"),
3524
3129
  chainEntryId: z8.string().describe("The chain's entry ID"),
3525
- branchName: z8.string().describe("The branch to merge"),
3526
- strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) preserves history, 'squash' collapses into one version"),
3527
- author: z8.string().optional().describe("Who is merging. Defaults to 'mcp'.")
3528
- }
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 }
3529
3138
  },
3530
- async ({ chainEntryId, branchName, strategy, author }) => {
3531
- const result = await mcpMutation("gitchain.mergeBranch", {
3532
- chainEntryId,
3533
- branchName,
3534
- strategy,
3535
- author
3536
- });
3537
- return {
3538
- content: [
3539
- {
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: [{
3540
3146
  type: "text",
3541
- text: `# Branch Merged
3147
+ text: `# Gate: ${icon}
3542
3148
 
3543
- - **Chain:** \`${result.entryId}\`
3544
- - **Branch:** ${result.branchName} (now closed)
3545
- - **Version:** v${result.version}
3546
- - **Strategy:** ${result.strategy}
3149
+ - **Score:** ${gate.score}%
3150
+ - **Threshold:** ${gate.threshold}%
3547
3151
 
3548
- Main is now at v${result.version}. The branch has been closed.`
3549
- }
3550
- ]
3551
- };
3552
- }
3553
- );
3554
- server2.registerTool(
3555
- "chain-comment",
3556
- {
3557
- title: "Comment on Chain Version",
3558
- 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.",
3559
- inputSchema: {
3560
- chainEntryId: z8.string().describe("The chain's entry ID"),
3561
- action: z8.enum(["add", "resolve", "list"]).describe("'add' a comment, 'resolve' a comment, or 'list' all comments"),
3562
- versionNumber: z8.number().optional().describe("Version number to comment on (required for 'add')"),
3563
- linkId: z8.string().optional().describe("Which link this comment targets (optional)"),
3564
- body: z8.string().optional().describe("Comment text (required for 'add')"),
3565
- commentId: z8.string().optional().describe("Comment ID to resolve (required for 'resolve')"),
3566
- author: z8.string().optional().describe("Who is commenting. Defaults to 'mcp'.")
3152
+ ## Checks
3153
+
3154
+ ${checkLines}`
3155
+ }]
3156
+ };
3567
3157
  }
3568
- },
3569
- async ({ chainEntryId, action, versionNumber, linkId, body, commentId, author }) => {
3570
- if (action === "add") {
3571
- if (!versionNumber) throw new Error("versionNumber is required for 'add'");
3572
- 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." }] };
3573
3161
  const result = await mcpMutation("gitchain.addComment", {
3574
3162
  chainEntryId,
3575
3163
  versionNumber,
@@ -3578,92 +3166,47 @@ Main is now at v${result.version}. The branch has been closed.`
3578
3166
  author
3579
3167
  });
3580
3168
  return {
3581
- content: [
3582
- {
3583
- type: "text",
3584
- text: `# Comment Added
3169
+ content: [{
3170
+ type: "text",
3171
+ text: `# Comment Added
3585
3172
 
3586
3173
  - **Chain:** \`${result.chainEntryId}\`
3587
3174
  - **Version:** v${result.versionNumber}
3588
3175
  ` + (result.linkId ? `- **Link:** ${result.linkId}
3589
- ` : "") + `- **Body:** ${body?.substring(0, 200)}`
3590
- }
3591
- ]
3176
+ ` : "") + `- **Body:** ${body.substring(0, 200)}`
3177
+ }]
3592
3178
  };
3593
3179
  }
3594
- if (action === "resolve") {
3595
- 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." }] };
3596
3182
  await mcpMutation("gitchain.resolveComment", { commentId });
3597
- return {
3598
- content: [
3599
- {
3600
- type: "text",
3601
- text: `Comment resolved.`
3602
- }
3603
- ]
3604
- };
3183
+ return { content: [{ type: "text", text: `Comment resolved.` }] };
3605
3184
  }
3606
- const comments = await mcpMutation("gitchain.listComments", {
3607
- chainEntryId,
3608
- versionNumber
3609
- });
3610
- 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;
3611
3200
  return {
3612
- content: [
3613
- {
3614
- type: "text",
3615
- text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.`
3616
- }
3617
- ]
3618
- };
3619
- }
3620
- const formatted = comments.map((c) => {
3621
- const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
3622
- const resolved = c.resolved ? " (RESOLVED)" : "";
3623
- const link = c.linkId ? ` [${c.linkId}]` : "";
3624
- return `- **v${c.version}${link}** ${date} by ${c.author}${resolved}: ${c.body.substring(0, 150)}`;
3625
- }).join("\n");
3626
- const unresolvedCount = comments.filter((c) => !c.resolved).length;
3627
- return {
3628
- content: [
3629
- {
3201
+ content: [{
3630
3202
  type: "text",
3631
3203
  text: `# Comments for ${chainEntryId} (${comments.length}, ${unresolvedCount} unresolved)
3632
3204
 
3633
3205
  ${formatted}`
3634
- }
3635
- ]
3636
- };
3637
- }
3638
- );
3639
- server2.registerTool(
3640
- "chain-revert",
3641
- {
3642
- title: "Revert Chain",
3643
- 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.",
3644
- inputSchema: {
3645
- chainEntryId: z8.string().describe("The chain's entry ID"),
3646
- toVersion: z8.number().describe("The version number to revert to"),
3647
- author: z8.string().optional().describe("Who is reverting. Defaults to 'mcp'.")
3206
+ }]
3207
+ };
3648
3208
  }
3649
- },
3650
- async ({ chainEntryId, toVersion, author }) => {
3651
- const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
3652
- return {
3653
- content: [
3654
- {
3655
- type: "text",
3656
- text: `# Reverted
3657
-
3658
- - **Chain:** \`${result.entryId}\`
3659
- - **Reverted to:** v${result.revertedTo}
3660
- - **New version:** v${result.newVersion}
3661
- - **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
3662
-
3663
- History is preserved \u2014 this created a new version, not a destructive reset.`
3664
- }
3665
- ]
3666
- };
3209
+ return { content: [{ type: "text", text: "Unknown action." }] };
3667
3210
  }
3668
3211
  );
3669
3212
  }
@@ -3708,7 +3251,7 @@ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, h
3708
3251
  sections.push(
3709
3252
  `## Business Rules
3710
3253
  Collection: \`business-rules\` (${rulesCount}).
3711
- 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.
3712
3255
  Check compliance: use the \`review-against-rules\` prompt (pass a domain).
3713
3256
  Draft a new rule: use the \`draft-rule-from-context\` prompt.`
3714
3257
  );
@@ -3725,16 +3268,16 @@ Browse: \`list-entries collection=tracking-events\`. Full setup: \`docs/posthog-
3725
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."
3726
3269
  );
3727
3270
  sections.push(
3728
- "## 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."
3729
3272
  );
3730
3273
  sections.push(
3731
- "## 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"
3732
3275
  );
3733
3276
  return sections.join("\n\n---\n\n");
3734
3277
  }
3735
3278
  function registerResources(server2) {
3736
3279
  server2.resource(
3737
- "kb-orientation",
3280
+ "chain-orientation",
3738
3281
  "productbrain://orientation",
3739
3282
  async (uri) => {
3740
3283
  const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
@@ -3757,7 +3300,7 @@ function registerResources(server2) {
3757
3300
  }
3758
3301
  );
3759
3302
  server2.resource(
3760
- "kb-terminology",
3303
+ "chain-terminology",
3761
3304
  "productbrain://terminology",
3762
3305
  async (uri) => {
3763
3306
  const [glossaryResult, standardsResult] = await Promise.allSettled([
@@ -3772,7 +3315,7 @@ function registerResources(server2) {
3772
3315
 
3773
3316
  ${terms}`);
3774
3317
  } else {
3775
- 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.");
3776
3319
  }
3777
3320
  } else {
3778
3321
  lines.push("## Glossary\n\nCould not load glossary \u2014 use `list-entries collection=glossary` to browse manually.");
@@ -3784,7 +3327,7 @@ ${terms}`);
3784
3327
 
3785
3328
  ${stds}`);
3786
3329
  } else {
3787
- 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.");
3788
3331
  }
3789
3332
  } else {
3790
3333
  lines.push("## Standards\n\nCould not load standards \u2014 use `list-entries collection=standards` to browse manually.");
@@ -3795,7 +3338,7 @@ ${stds}`);
3795
3338
  }
3796
3339
  );
3797
3340
  server2.resource(
3798
- "kb-collections",
3341
+ "chain-collections",
3799
3342
  "productbrain://collections",
3800
3343
  async (uri) => {
3801
3344
  const collections = await mcpQuery("kb.listCollections");
@@ -3818,7 +3361,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3818
3361
  }
3819
3362
  );
3820
3363
  server2.resource(
3821
- "kb-collection-entries",
3364
+ "chain-collection-entries",
3822
3365
  new ResourceTemplate("productbrain://{slug}/entries", {
3823
3366
  list: async () => {
3824
3367
  const collections = await mcpQuery("kb.listCollections");
@@ -3843,7 +3386,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3843
3386
  }
3844
3387
  );
3845
3388
  server2.resource(
3846
- "kb-labels",
3389
+ "chain-labels",
3847
3390
  "productbrain://labels",
3848
3391
  async (uri) => {
3849
3392
  const labels = await mcpQuery("kb.listLabels");
@@ -3997,7 +3540,7 @@ Structure the decision record with:
3997
3540
  6. **Alternatives considered**: What else was on the table
3998
3541
  7. **Related rules or tensions**: Any business rules or tensions this connects to
3999
3542
 
4000
- 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".`
4001
3544
  }
4002
3545
  }
4003
3546
  ]
@@ -4043,7 +3586,7 @@ Draft the rule with these fields:
4043
3586
  7. **data.platforms**: Which platforms are affected
4044
3587
  8. **data.relatedRules**: Any related existing rules
4045
3588
 
4046
- 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".`
4047
3590
  }
4048
3591
  }
4049
3592
  ]
@@ -4094,7 +3637,7 @@ Use one of these IDs to run a workflow.`
4094
3637
  ` + sorted.map((d) => `- ${d.entryId ?? ""}: ${d.name} [${d.status}]`).join("\n");
4095
3638
  }
4096
3639
  } catch {
4097
- kbContext = "\n_Could not load KB context \u2014 proceed without it._";
3640
+ kbContext = "\n_Could not load chain context \u2014 proceed without it._";
4098
3641
  }
4099
3642
  const roundsPlan = wf.rounds.map(
4100
3643
  (r) => `### Round ${r.num}: ${r.label}
@@ -4141,9 +3684,9 @@ ${wf.errorRecovery}
4141
3684
 
4142
3685
  ---
4143
3686
 
4144
- ## KB Output
3687
+ ## Chain Output
4145
3688
 
4146
- When complete, use \`smart-capture\` to create a \`${wf.kbOutputCollection}\` entry.
3689
+ When complete, use \`capture\` to create a \`${wf.kbOutputCollection}\` entry.
4147
3690
  Name template: ${wf.kbOutputTemplate.nameTemplate}
4148
3691
  Description field: ${wf.kbOutputTemplate.descriptionField}
4149
3692
  ` + kbContext + `
@@ -4174,7 +3717,7 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
4174
3717
  }
4175
3718
  }
4176
3719
  bootstrapCloudMode();
4177
- var SERVER_VERSION = "1.0.0";
3720
+ var SERVER_VERSION = "2.0.0";
4178
3721
  initAnalytics();
4179
3722
  var workspaceId;
4180
3723
  try {
@@ -4201,39 +3744,37 @@ var server = new McpServer2(
4201
3744
  "Product Brain \u2014 the single source of truth for product knowledge.",
4202
3745
  "Terminology, standards, and core data all live here \u2014 no need to check external docs.",
4203
3746
  "",
4204
- "Terminology & naming: For 'what is X?' or naming questions, fetch `productbrain://terminology`",
4205
- "or use the `name-check` prompt to validate names against the glossary.",
4206
- "",
4207
3747
  "Workflow:",
4208
3748
  " 1. Verify: call `health` to confirm connectivity.",
4209
- " 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt for naming questions.",
4210
- " 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.",
4211
3751
  " 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
4212
- " 5. Capture: use `smart-capture` to create entries \u2014 it auto-links related entries and",
4213
- " returns a quality scorecard in one call. Use `create-entry` only when you need",
4214
- " full control over every field.",
4215
- " 6. Connect: use `suggest-links` then `relate-entries` to build the graph.",
4216
- " 7. Quality: use `quality-check` to assess entry completeness.",
4217
- " 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.",
4218
3758
  "",
4219
- "Always prefer `smart-capture` over `create-entry` or `quick-capture` for new entries.",
4220
- "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).",
4221
3760
  "",
4222
3761
  "Orientation:",
4223
- " When you need to understand the system \u2014 architecture, data model, rules,",
4224
- " or analytics \u2014 fetch the `productbrain://orientation` resource first.",
4225
- " 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."
4226
3764
  ].join("\n")
4227
3765
  }
4228
3766
  );
3767
+ var enabledModules = new Set(
3768
+ (process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
3769
+ );
4229
3770
  registerKnowledgeTools(server);
4230
3771
  registerLabelTools(server);
4231
3772
  registerHealthTools(server);
4232
3773
  registerVerifyTools(server);
4233
3774
  registerSmartCaptureTools(server);
4234
- registerArchitectureTools(server);
4235
3775
  registerWorkflowTools(server);
4236
- registerGitChainTools(server);
3776
+ if (enabledModules.has("gitchain")) registerGitChainTools(server);
3777
+ if (enabledModules.has("arch")) registerArchitectureTools(server);
4237
3778
  registerResources(server);
4238
3779
  registerPrompts(server);
4239
3780
  var transport = new StdioServerTransport();