@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/cli/index.js +1 -1
- package/dist/index.js +647 -1106
- package/dist/index.js.map +1 -1
- package/dist/{setup-V6HIAYXL.js → setup-MJLPTQBL.js} +16 -9
- package/dist/setup-MJLPTQBL.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/setup-V6HIAYXL.js.map +0 -1
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
|
|
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
|
|
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
|
|
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: \`
|
|
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
|
-
"
|
|
368
|
+
"search",
|
|
446
369
|
{
|
|
447
|
-
title: "Search
|
|
448
|
-
description: "Full-text search across all
|
|
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
|
|
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
|
|
629
|
-
description: "Assemble
|
|
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'
|
|
632
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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: "
|
|
993
|
-
description: "
|
|
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("
|
|
996
|
-
slug: z2.string().describe("Label slug (
|
|
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
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
1697
|
+
"capture",
|
|
1903
1698
|
{
|
|
1904
|
-
title: "
|
|
1905
|
-
description: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
2054
|
+
"architecture",
|
|
2260
2055
|
{
|
|
2261
|
-
title: "
|
|
2262
|
-
description: "
|
|
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
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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:
|
|
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
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
2176
|
+
"architecture-admin",
|
|
2363
2177
|
{
|
|
2364
|
-
title: "
|
|
2365
|
-
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
|
-
|
|
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
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
entryId: seed.entryId,
|
|
2440
|
-
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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.
|
|
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. **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
`**
|
|
2761
|
+
`**Entry Committed**: \`${entryId}\``,
|
|
3006
2762
|
`Collection: \`${wf.kbOutputCollection}\``,
|
|
3007
2763
|
`Name: ${summaryName}`,
|
|
3008
2764
|
"",
|
|
3009
|
-
`The retro is now
|
|
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
|
-
`**
|
|
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 \`
|
|
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
|
|
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
|
|
2816
|
+
"chain",
|
|
3061
2817
|
{
|
|
3062
|
-
title: "
|
|
3063
|
-
description: "
|
|
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
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
),
|
|
3069
|
-
description: z8.string().optional().describe("
|
|
3070
|
-
|
|
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
|
-
|
|
3075
|
-
"
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
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: `#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
2921
|
+
"chain-version",
|
|
3214
2922
|
{
|
|
3215
|
-
title: "Chain
|
|
3216
|
-
description: "
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3267
|
-
|
|
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
|
-
|
|
3271
|
-
|
|
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
|
-
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
{
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
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
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
text +=
|
|
3364
|
-
|
|
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
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
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: `#
|
|
3007
|
+
text: `# Reverted
|
|
3401
3008
|
|
|
3402
|
-
- **
|
|
3403
|
-
- **
|
|
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
|
-
|
|
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
|
-
${
|
|
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: "
|
|
3417
|
-
description: "
|
|
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
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3444
|
-
chainEntryId
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
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
|
-
|
|
3457
|
-
(
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
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: `#
|
|
3079
|
+
text: `# Branch Merged
|
|
3464
3080
|
|
|
3465
|
-
|
|
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
|
-
|
|
3495
|
-
|
|
3496
|
-
]
|
|
3086
|
+
Main is now at v${result.version}. The branch has been closed.`
|
|
3087
|
+
}]
|
|
3497
3088
|
};
|
|
3498
3089
|
}
|
|
3499
|
-
|
|
3500
|
-
(
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
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
|
|
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-
|
|
3123
|
+
"chain-review",
|
|
3520
3124
|
{
|
|
3521
|
-
title: "
|
|
3522
|
-
description: "
|
|
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
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
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,
|
|
3531
|
-
|
|
3532
|
-
chainEntryId,
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
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: `#
|
|
3147
|
+
text: `# Gate: ${icon}
|
|
3542
3148
|
|
|
3543
|
-
- **
|
|
3544
|
-
- **
|
|
3545
|
-
- **Version:** v${result.version}
|
|
3546
|
-
- **Strategy:** ${result.strategy}
|
|
3149
|
+
- **Score:** ${gate.score}%
|
|
3150
|
+
- **Threshold:** ${gate.threshold}%
|
|
3547
3151
|
|
|
3548
|
-
|
|
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
|
-
|
|
3570
|
-
|
|
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
|
-
|
|
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
|
|
3590
|
-
|
|
3591
|
-
]
|
|
3176
|
+
` : "") + `- **Body:** ${body.substring(0, 200)}`
|
|
3177
|
+
}]
|
|
3592
3178
|
};
|
|
3593
3179
|
}
|
|
3594
|
-
if (action === "resolve") {
|
|
3595
|
-
if (!commentId)
|
|
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
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
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: \`
|
|
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 `
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
3687
|
+
## Chain Output
|
|
4145
3688
|
|
|
4146
|
-
When complete, use \`
|
|
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 = "
|
|
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
|
|
4210
|
-
" 3. Discover: use `
|
|
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.
|
|
4213
|
-
"
|
|
4214
|
-
"
|
|
4215
|
-
"
|
|
4216
|
-
"
|
|
4217
|
-
"
|
|
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
|
|
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
|
-
"
|
|
4224
|
-
"
|
|
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();
|