@productbrain/mcp 0.0.1-beta.1 → 0.0.1-beta.3
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/.env.mcp.example +4 -4
- package/dist/cli/index.js +1 -1
- package/dist/index.js +1905 -204
- package/dist/index.js.map +1 -1
- package/dist/{setup-5UDBP4VE.js → setup-V6HIAYXL.js} +4 -4
- package/dist/setup-V6HIAYXL.js.map +1 -0
- package/package.json +6 -25
- package/README.md +0 -239
- package/dist/setup-5UDBP4VE.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import "./chunk-DGUM43GV.js";
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { resolve as
|
|
5
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
6
|
+
import { resolve as resolve3 } from "path";
|
|
7
7
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
|
|
@@ -22,7 +22,7 @@ function log(msg) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
function initAnalytics() {
|
|
25
|
-
const apiKey = process.env.POSTHOG_MCP_KEY || "
|
|
25
|
+
const apiKey = process.env.POSTHOG_MCP_KEY || "";
|
|
26
26
|
if (!apiKey) {
|
|
27
27
|
log("[MCP-ANALYTICS] No PostHog key \u2014 tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\n");
|
|
28
28
|
return;
|
|
@@ -79,7 +79,7 @@ async function shutdownAnalytics() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// src/client.ts
|
|
82
|
-
var DEFAULT_CLOUD_URL = "https://
|
|
82
|
+
var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
|
|
83
83
|
var cachedWorkspaceId = null;
|
|
84
84
|
var cloudMode = false;
|
|
85
85
|
var AUDIT_BUFFER_SIZE = 50;
|
|
@@ -388,29 +388,32 @@ ${msg}
|
|
|
388
388
|
"update-entry",
|
|
389
389
|
{
|
|
390
390
|
title: "Update Entry",
|
|
391
|
-
description: "Update an existing entry by its human-readable ID. Only provide the fields you want to change \u2014 data fields are merged with existing values. Use get-entry first to see current values. SOS-020: Cannot update tension status via MCP \u2014 process decides (use
|
|
391
|
+
description: "Update an existing entry by its human-readable ID. Only provide the fields you want to change \u2014 data fields are merged with existing values. Creates a draft version by default. Set autoPublish=true to publish immediately. Use get-entry first to see current values. SOS-020: Cannot update tension status via MCP \u2014 process decides (use Product Brain UI after approval).",
|
|
392
392
|
inputSchema: {
|
|
393
393
|
entryId: z.string().describe("Entry ID to update, e.g. 'T-SUPPLIER', 'BR-001'"),
|
|
394
394
|
name: z.string().optional().describe("New display name"),
|
|
395
395
|
status: z.string().optional().describe("New status: draft | active | verified | deprecated"),
|
|
396
396
|
data: z.record(z.unknown()).optional().describe("Fields to update (merged with existing data)"),
|
|
397
|
-
order: z.number().optional().describe("New sort order")
|
|
397
|
+
order: z.number().optional().describe("New sort order"),
|
|
398
|
+
autoPublish: z.boolean().optional().default(false).describe("If true, publishes the change immediately. If false (default), creates a draft version.")
|
|
398
399
|
},
|
|
399
400
|
annotations: { idempotentHint: true, destructiveHint: false }
|
|
400
401
|
},
|
|
401
|
-
async ({ entryId, name, status, data, order }) => {
|
|
402
|
+
async ({ entryId, name, status, data, order, autoPublish }) => {
|
|
402
403
|
try {
|
|
403
404
|
const id = await mcpMutation("kb.updateEntry", {
|
|
404
405
|
entryId,
|
|
405
406
|
name,
|
|
406
407
|
status,
|
|
407
408
|
data,
|
|
408
|
-
order
|
|
409
|
+
order,
|
|
410
|
+
autoPublish
|
|
409
411
|
});
|
|
412
|
+
const mode = autoPublish ? "published" : "saved as draft";
|
|
410
413
|
return {
|
|
411
414
|
content: [{ type: "text", text: `# Entry Updated
|
|
412
415
|
|
|
413
|
-
**${entryId}** has been
|
|
416
|
+
**${entryId}** has been ${mode}.
|
|
414
417
|
|
|
415
418
|
Internal ID: ${id}` }]
|
|
416
419
|
};
|
|
@@ -428,7 +431,7 @@ Tension status (open, in-progress, closed) must be changed through the defined p
|
|
|
428
431
|
- Create tensions: \`create-entry collection=tensions name="..." status=open\`
|
|
429
432
|
- List tensions: \`list-entries collection=tensions\`
|
|
430
433
|
- Update non-status fields (raised, date, priority, description) via \`update-entry\`
|
|
431
|
-
- After process approval, a human uses the
|
|
434
|
+
- After process approval, a human uses the Product Brain UI to change status
|
|
432
435
|
|
|
433
436
|
Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns.`
|
|
434
437
|
}]
|
|
@@ -829,7 +832,7 @@ Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify i
|
|
|
829
832
|
await server2.sendLoggingMessage({
|
|
830
833
|
level: "info",
|
|
831
834
|
data: `Loading context for task: "${taskDescription.substring(0, 80)}..."`,
|
|
832
|
-
logger: "product-
|
|
835
|
+
logger: "product-brain"
|
|
833
836
|
});
|
|
834
837
|
const result = await mcpQuery("kb.loadContextForTask", {
|
|
835
838
|
taskDescription,
|
|
@@ -885,7 +888,7 @@ _Consider capturing domain knowledge discovered during this task via \`smart-cap
|
|
|
885
888
|
"review-rules",
|
|
886
889
|
{
|
|
887
890
|
title: "Review Business Rules",
|
|
888
|
-
description: "Surface all active business rules for a domain, formatted for compliance review. Use when reviewing code, designs, or decisions against
|
|
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.",
|
|
889
892
|
inputSchema: {
|
|
890
893
|
domain: z.string().describe("Business rule domain, e.g. 'AI & MCP Integration', 'Governance & Decision-Making'"),
|
|
891
894
|
context: z.string().optional().describe("What you're reviewing \u2014 code change, design decision, file path, etc.")
|
|
@@ -1134,7 +1137,7 @@ function registerHealthTools(server2) {
|
|
|
1134
1137
|
"health",
|
|
1135
1138
|
{
|
|
1136
1139
|
title: "Health Check",
|
|
1137
|
-
description: "Verify that
|
|
1140
|
+
description: "Verify that Product Brain is running and can reach its backend. Returns workspace status, collection count, entry count, and latency. Use this to confirm connectivity before doing real work.",
|
|
1138
1141
|
annotations: { readOnlyHint: true }
|
|
1139
1142
|
},
|
|
1140
1143
|
async () => {
|
|
@@ -1380,7 +1383,7 @@ function registerVerifyTools(server2) {
|
|
|
1380
1383
|
return {
|
|
1381
1384
|
content: [{
|
|
1382
1385
|
type: "text",
|
|
1383
|
-
text: "# Verification Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent directory).\n\nSet `WORKSPACE_PATH` in `.env.mcp` to the absolute path of the
|
|
1386
|
+
text: "# Verification Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent directory).\n\nSet `WORKSPACE_PATH` in `.env.mcp` to the absolute path of the Product Brain project root."
|
|
1384
1387
|
}]
|
|
1385
1388
|
};
|
|
1386
1389
|
}
|
|
@@ -1708,6 +1711,30 @@ var PROFILES = /* @__PURE__ */ new Map([
|
|
|
1708
1711
|
suggestion: () => "Record when this decision was made."
|
|
1709
1712
|
}
|
|
1710
1713
|
]
|
|
1714
|
+
}],
|
|
1715
|
+
["features", {
|
|
1716
|
+
idPrefix: "FEAT",
|
|
1717
|
+
governedDraft: false,
|
|
1718
|
+
descriptionField: "description",
|
|
1719
|
+
defaults: [],
|
|
1720
|
+
recommendedRelationTypes: ["belongs_to", "depends_on", "surfaces_tension_in", "related_to"],
|
|
1721
|
+
qualityChecks: [
|
|
1722
|
+
COMMON_CHECKS.clearName,
|
|
1723
|
+
COMMON_CHECKS.hasDescription,
|
|
1724
|
+
COMMON_CHECKS.hasRelations,
|
|
1725
|
+
{
|
|
1726
|
+
id: "has-owner",
|
|
1727
|
+
label: "Owner assigned",
|
|
1728
|
+
check: (ctx) => !!ctx.data.owner && ctx.data.owner !== "",
|
|
1729
|
+
suggestion: () => "Assign an owner team or product area."
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
id: "has-rationale",
|
|
1733
|
+
label: "Rationale documented",
|
|
1734
|
+
check: (ctx) => !!ctx.data.rationale && String(ctx.data.rationale).length > 20,
|
|
1735
|
+
suggestion: () => "Explain why this feature matters \u2014 what problem does it solve?"
|
|
1736
|
+
}
|
|
1737
|
+
]
|
|
1711
1738
|
}]
|
|
1712
1739
|
]);
|
|
1713
1740
|
var FALLBACK_PROFILE = {
|
|
@@ -2077,220 +2104,1784 @@ function extractPreview2(data, maxLen) {
|
|
|
2077
2104
|
return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
|
|
2078
2105
|
}
|
|
2079
2106
|
|
|
2080
|
-
// src/
|
|
2081
|
-
import {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2107
|
+
// src/tools/architecture.ts
|
|
2108
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
2109
|
+
import { resolve as resolve2, relative, dirname, normalize } from "path";
|
|
2110
|
+
import { z as z6 } from "zod";
|
|
2111
|
+
var COLLECTION_SLUG = "architecture";
|
|
2112
|
+
var COLLECTION_FIELDS = [
|
|
2113
|
+
{ key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
|
|
2114
|
+
{ key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
|
|
2115
|
+
{ key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
|
|
2116
|
+
{ key: "description", label: "Description", type: "text", searchable: true },
|
|
2117
|
+
{ key: "color", label: "Color", type: "text" },
|
|
2118
|
+
{ key: "icon", label: "Icon", type: "text" },
|
|
2119
|
+
{ key: "sourceNode", label: "Source Node (flows)", type: "text" },
|
|
2120
|
+
{ key: "targetNode", label: "Target Node (flows)", type: "text" },
|
|
2121
|
+
{ key: "filePaths", label: "File Paths", type: "text", searchable: true },
|
|
2122
|
+
{ key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
|
|
2123
|
+
{ key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
|
|
2124
|
+
{ key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
|
|
2125
|
+
{ key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
|
|
2126
|
+
];
|
|
2127
|
+
async function ensureCollection() {
|
|
2128
|
+
const collections = await mcpQuery("kb.listCollections");
|
|
2129
|
+
if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
|
|
2130
|
+
await mcpMutation("kb.createCollection", {
|
|
2131
|
+
slug: COLLECTION_SLUG,
|
|
2132
|
+
name: "Architecture",
|
|
2133
|
+
icon: "\u{1F3D7}\uFE0F",
|
|
2134
|
+
description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
|
|
2135
|
+
fields: COLLECTION_FIELDS
|
|
2136
|
+
});
|
|
2093
2137
|
}
|
|
2094
|
-
function
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
);
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
sections.push(
|
|
2105
|
-
`## Data Model (${collections.length} collections)
|
|
2106
|
-
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
2107
|
-
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, history via \`entryHistory\`, labels via \`labels\` + \`entryLabels\`.
|
|
2108
|
-
|
|
2109
|
-
` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
|
|
2110
|
-
);
|
|
2111
|
-
} else {
|
|
2112
|
-
sections.push(
|
|
2113
|
-
"## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
|
|
2138
|
+
async function listArchEntries() {
|
|
2139
|
+
return mcpQuery("kb.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
2140
|
+
}
|
|
2141
|
+
function byTag(entries, archType) {
|
|
2142
|
+
return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
2143
|
+
}
|
|
2144
|
+
function renderArchitectureHtml(layers, nodes, flows, templateName) {
|
|
2145
|
+
const layerHtml = layers.map((layer) => {
|
|
2146
|
+
const layerNodes = nodes.filter(
|
|
2147
|
+
(n) => n.data?.layerRef === layer.entryId
|
|
2114
2148
|
);
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
);
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2149
|
+
const nodeCards = layerNodes.map((n) => `
|
|
2150
|
+
<div class="node" title="${escHtml(String(n.data?.description ?? ""))}">
|
|
2151
|
+
<span class="node-icon">${escHtml(String(n.data?.icon ?? "\u25FB"))}</span>
|
|
2152
|
+
<span class="node-name">${escHtml(n.name)}</span>
|
|
2153
|
+
</div>
|
|
2154
|
+
`).join("");
|
|
2155
|
+
return `
|
|
2156
|
+
<div class="layer" style="--layer-color: ${escHtml(String(layer.data?.color ?? "#666"))}">
|
|
2157
|
+
<div class="layer-label">
|
|
2158
|
+
<span class="layer-dot"></span>
|
|
2159
|
+
<span class="layer-name">${escHtml(layer.name)}</span>
|
|
2160
|
+
<span class="layer-count">${layerNodes.length}</span>
|
|
2161
|
+
</div>
|
|
2162
|
+
<div class="layer-desc">${escHtml(String(layer.data?.description ?? ""))}</div>
|
|
2163
|
+
<div class="nodes">${nodeCards || '<span class="empty">No components</span>'}</div>
|
|
2164
|
+
</div>
|
|
2165
|
+
`;
|
|
2166
|
+
}).join("");
|
|
2167
|
+
return `<!DOCTYPE html>
|
|
2168
|
+
<html><head><meta charset="utf-8"><style>
|
|
2169
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
2170
|
+
body{font-family:-apple-system,system-ui,sans-serif;background:#1a1a2e;color:#e0e0e0;padding:16px}
|
|
2171
|
+
h1{font-size:14px;font-weight:600;color:#a0a0c0;margin-bottom:12px;letter-spacing:.04em}
|
|
2172
|
+
.layer{border-left:3px solid var(--layer-color);padding:8px 12px;margin-bottom:8px;background:rgba(255,255,255,.03);border-radius:0 6px 6px 0}
|
|
2173
|
+
.layer-label{display:flex;align-items:center;gap:8px;margin-bottom:4px}
|
|
2174
|
+
.layer-dot{width:8px;height:8px;border-radius:50%;background:var(--layer-color)}
|
|
2175
|
+
.layer-name{font-size:12px;font-weight:600;color:#fff;letter-spacing:.03em}
|
|
2176
|
+
.layer-count{font-size:10px;color:var(--layer-color);background:rgba(255,255,255,.06);padding:1px 6px;border-radius:8px}
|
|
2177
|
+
.layer-desc{font-size:11px;color:#888;margin-bottom:6px}
|
|
2178
|
+
.nodes{display:flex;flex-wrap:wrap;gap:6px}
|
|
2179
|
+
.node{display:flex;align-items:center;gap:4px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:4px;padding:4px 8px;font-size:11px;cursor:default;transition:border-color .15s}
|
|
2180
|
+
.node:hover{border-color:var(--layer-color)}
|
|
2181
|
+
.node-icon{font-size:12px}
|
|
2182
|
+
.node-name{color:#ddd}
|
|
2183
|
+
.empty{font-size:11px;color:#555;font-style:italic}
|
|
2184
|
+
.flows{margin-top:12px;border-top:1px solid rgba(255,255,255,.06);padding-top:8px}
|
|
2185
|
+
.flow{font-size:11px;color:#888;padding:2px 0}
|
|
2186
|
+
.flow-arrow{color:#6366f1;margin:0 4px}
|
|
2187
|
+
</style></head><body>
|
|
2188
|
+
<h1>${escHtml(templateName)}</h1>
|
|
2189
|
+
${layerHtml}
|
|
2190
|
+
${flows.length > 0 ? `<div class="flows"><div style="font-size:10px;color:#666;margin-bottom:4px;letter-spacing:.06em">DATA FLOWS</div>${flows.map((f) => `<div class="flow">${escHtml(f.name)}<span class="flow-arrow">\u2192</span><span style="color:#aaa">${escHtml(String(f.data?.description ?? ""))}</span></div>`).join("")}</div>` : ""}
|
|
2191
|
+
</body></html>`;
|
|
2143
2192
|
}
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
|
|
2157
|
-
const standards = standardsResult.status === "fulfilled" ? standardsResult.value : null;
|
|
2158
|
-
const businessRules = rulesResult.status === "fulfilled" ? rulesResult.value : null;
|
|
2159
|
-
return {
|
|
2160
|
-
contents: [{
|
|
2161
|
-
uri: uri.href,
|
|
2162
|
-
text: buildOrientationMarkdown(collections, trackingEvents, standards, businessRules),
|
|
2163
|
-
mimeType: "text/markdown"
|
|
2164
|
-
}]
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
);
|
|
2168
|
-
server2.resource(
|
|
2169
|
-
"kb-terminology",
|
|
2170
|
-
"productbrain://terminology",
|
|
2171
|
-
async (uri) => {
|
|
2172
|
-
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
2173
|
-
mcpQuery("kb.listEntries", { collectionSlug: "glossary" }),
|
|
2174
|
-
mcpQuery("kb.listEntries", { collectionSlug: "standards" })
|
|
2175
|
-
]);
|
|
2176
|
-
const lines = ["# ProductBrain \u2014 Terminology"];
|
|
2177
|
-
if (glossaryResult.status === "fulfilled") {
|
|
2178
|
-
if (glossaryResult.value.length > 0) {
|
|
2179
|
-
const terms = glossaryResult.value.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
|
|
2180
|
-
lines.push(`## Glossary (${glossaryResult.value.length} terms)
|
|
2193
|
+
function escHtml(s) {
|
|
2194
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2195
|
+
}
|
|
2196
|
+
function formatLayerText(layer, nodes) {
|
|
2197
|
+
const layerNodes = nodes.filter((n) => n.data?.layerRef === layer.entryId);
|
|
2198
|
+
const nodeList = layerNodes.map((n) => {
|
|
2199
|
+
const desc = n.data?.description ? ` \u2014 ${n.data.description}` : "";
|
|
2200
|
+
const owner = n.data?.owner ? ` (${n.data.owner})` : "";
|
|
2201
|
+
return ` - ${n.data?.icon ?? "\u25FB"} **${n.name}**${desc}${owner}`;
|
|
2202
|
+
}).join("\n");
|
|
2203
|
+
return `### ${layer.name}
|
|
2204
|
+
${layer.data?.description ?? ""}
|
|
2181
2205
|
|
|
2182
|
-
${
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2206
|
+
${nodeList || " _No components_"}`;
|
|
2207
|
+
}
|
|
2208
|
+
var SEED_TEMPLATE = {
|
|
2209
|
+
entryId: "ARCH-tpl-product-os",
|
|
2210
|
+
name: "Product OS Default",
|
|
2211
|
+
data: {
|
|
2212
|
+
archType: "template",
|
|
2213
|
+
description: "Default 4-layer architecture: Auth \u2192 Infrastructure \u2192 Core \u2192 Features, with an outward Integration layer",
|
|
2214
|
+
layerOrder: JSON.stringify(["auth", "infrastructure", "core", "features", "integration"])
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
var SEED_LAYERS = [
|
|
2218
|
+
{ entryId: "ARCH-layer-auth", name: "Auth", order: 0, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#22c55e", description: "Authentication, user management, and workspace-scoped access control", icon: "\u{1F510}", dependsOn: "none", rationale: "Foundation layer. Auth depends on nothing \u2014 it is the first gate. No layer may bypass auth. All other layers depend on auth to know who the user is and which workspace they belong to." } },
|
|
2219
|
+
{ entryId: "ARCH-layer-infra", name: "Infrastructure", order: 1, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#ec4899", description: "Real-time data, event tracking, and AI model infrastructure", icon: "\u2699\uFE0F", dependsOn: "Auth", rationale: "Infrastructure sits on top of Auth. It provides the database, analytics, and AI plumbing that Core and Features consume. Infra may import from Auth (needs workspace context) but never from Core or Features." } },
|
|
2220
|
+
{ entryId: "ARCH-layer-core", name: "Core", order: 2, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#8b5cf6", description: "Business logic, knowledge graph, workflow engines, and MCP tooling", icon: "\u{1F9E0}", dependsOn: "Auth, Infrastructure", rationale: "Core contains business logic and engines that are UI-agnostic. It may import from Auth and Infra. Features depend on Core, but Core must never depend on Features \u2014 this is what keeps engines reusable across different UIs." } },
|
|
2221
|
+
{ entryId: "ARCH-layer-features", name: "Features", order: 3, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#6366f1", description: "User-facing pages, components, and feature modules", icon: "\u2726", dependsOn: "Auth, Infrastructure, Core", rationale: "Features are the user-facing layer \u2014 SvelteKit routes, components, and page-level logic. Features may import from any lower layer but nothing above may import from Features. This is the outermost application layer." } },
|
|
2222
|
+
{ entryId: "ARCH-layer-integration", name: "Integration", order: 4, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#f59e0b", description: "Outward connections to external tools, IDEs, and services", icon: "\u{1F50C}", dependsOn: "Core", rationale: "Integration is a lateral/outward layer \u2014 it connects to external systems (IDE, GitHub, Linear). It depends on Core (to expose knowledge) but sits outside the main stack. External tools call into Core via Integration, never directly into Features." } }
|
|
2223
|
+
];
|
|
2224
|
+
var SEED_NODES = [
|
|
2225
|
+
// Auth layer
|
|
2226
|
+
{ entryId: "ARCH-node-clerk", name: "Clerk", order: 0, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F511}", description: "Authentication provider \u2014 sign-in/sign-up pages, session management, organization-level access control. UserSync.svelte is cross-cutting layout glue (not mapped here).", filePaths: "src/routes/sign-in/, src/routes/sign-up/", owner: "Platform", rationale: "Auth layer because Clerk is the identity gate. Every request flows through auth first. No other layer provides identity." } },
|
|
2227
|
+
{ entryId: "ARCH-node-workspace", name: "Workspace Scoping", order: 1, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F3E2}", description: "Multi-tenancy anchor \u2014 all data is workspace-scoped via workspaceId", filePaths: "src/lib/stores/workspace.ts, convex/workspaces.ts", owner: "Platform", rationale: "Auth layer because workspace scoping is the second gate after identity. All queries require workspaceId, making this foundational." } },
|
|
2228
|
+
// Infrastructure layer
|
|
2229
|
+
{ entryId: "ARCH-node-convex", name: "Convex", order: 0, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u26A1", description: "Reactive database with real-time sync, serverless functions, and type-safe API generation. Unified Collections + Entries model", filePaths: "convex/schema.ts, convex/entries.ts, convex/http.ts", owner: "Platform", rationale: "Infrastructure because Convex is raw persistence and reactivity plumbing. It stores data but has no business logic opinions. Core and Features consume it." } },
|
|
2230
|
+
{ 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
|
+
{ 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
|
+
// Core layer
|
|
2233
|
+
{ entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "20+ tools exposing the knowledge graph as AI-consumable operations \u2014 smart-capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 smart-capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
|
|
2234
|
+
{ 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
|
+
{ 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
|
+
{ 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." } },
|
|
2237
|
+
// Features layer
|
|
2238
|
+
{ entryId: "ARCH-node-command-center", name: "Command Center", order: 0, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u2B21", description: "Calm home screen \u2014 daily orientation, triage mode, pulse metrics, momentum tracking", filePaths: "src/routes/+page.svelte, src/lib/components/command-center/", owner: "Command Center", rationale: "Features layer because the Command Center is a SvelteKit page \u2014 PulseMetrics, NeedsAttention, DailyBriefing are UI components that assemble data from Core queries. Why not Core? Because it has no reusable business logic or engines \u2014 it is pure layout and presentation. If you deleted this page, no business rule would break." } },
|
|
2239
|
+
{ entryId: "ARCH-node-chainwork-ui", name: "ChainWork UI", order: 1, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26D3", description: "Multi-step wizard for strategy artifact creation \u2014 setup, chain steps, quality gates, output", filePaths: "src/routes/chainwork/, src/lib/components/chainwork/", owner: "ChainWork", rationale: "Features layer because the ChainWork UI is the wizard interface \u2014 step navigation, form inputs, output rendering. Why not Core? Because the scoring logic and chain config ARE in Core (ChainWork Engine). This is the presentation skin over that engine. Delete this page and the engine still works via MCP." } },
|
|
2240
|
+
{ entryId: "ARCH-node-glossary", name: "Glossary", order: 2, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "Aa", description: "Canonical vocabulary management \u2014 term detail, code drift detection, inline term linking", filePaths: "src/routes/glossary/, src/lib/components/glossary/", owner: "Knowledge", rationale: "Features layer \u2014 NOT Core or Infrastructure \u2014 because this is the Glossary PAGE: the SvelteKit route for browsing, editing, and viewing terms. Why not Core? Because Core owns the glossary DATA (Knowledge Graph) and term-linking logic. The MCP server also accesses glossary terms without any page. Why not Infrastructure? Because glossary is domain-specific vocabulary, not generic plumbing. The page is one consumer of the data \u2014 Core owns the dictionary, Features owns the dictionary app." } },
|
|
2241
|
+
{ entryId: "ARCH-node-tensions", name: "Tensions", order: 3, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26A1", description: "Tension capture and processing \u2014 raise, triage, resolve through governance workflow", filePaths: "src/routes/tensions/, src/lib/components/tensions/", owner: "Governance", rationale: "Features layer because this is the Tensions PAGE \u2014 the UI for raising, listing, and viewing tensions. Why not Core? Because tension data, status rules (SOS-020), and processing logic already live in Core (Governance Engine). This page is a form + list view that reads from and writes to Core. Delete it and the governance engine still processes tensions via MCP." } },
|
|
2242
|
+
{ entryId: "ARCH-node-strategy", name: "Strategy", order: 4, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25A3", description: "Product strategy pages \u2014 vision, ecosystem, product areas, decision frameworks, sequencing", filePaths: "src/routes/strategy/, src/routes/bridge/, src/routes/topology/", owner: "Strategy", rationale: "Features layer because Strategy is a set of SvelteKit pages (strategy, bridge, topology) that visualize strategy entries. Why not Core? Because the strategy data (vision, principles, ecosystem layers) lives in the Knowledge Graph (Core). These pages render and allow inline editing \u2014 they consume Core downward." } },
|
|
2243
|
+
{ entryId: "ARCH-node-governance-ui", name: "Governance Pages", order: 5, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25C8", description: "Roles, circles, principles, policies, decisions, proposals, business rules", filePaths: "src/routes/roles/, src/routes/circles/, src/routes/decisions/, src/routes/proposals/", owner: "Governance", rationale: "Features layer because these are governance PAGES \u2014 list views and detail views for roles, circles, decisions, proposals. Why not Core? Because the governance ENGINE (versioning, consent, IDM) IS in Core. These pages are the user-facing window into governance data. The logic doesn't live here, only the rendering." } },
|
|
2244
|
+
{ entryId: "ARCH-node-artifacts", name: "Artifacts", order: 6, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u{1F4C4}", description: "Strategy artifacts from ChainWork \u2014 pitches, briefs, one-pagers linked to the knowledge graph", filePaths: "src/routes/artifacts/", owner: "ChainWork", rationale: "Features layer because the Artifacts page is a list/detail view for strategy artifacts produced by ChainWork. Why not Core? Because artifact data and scoring live in Core (ChainWork Engine). This page just renders the output and links back to the knowledge graph." } },
|
|
2245
|
+
// Integration layer
|
|
2246
|
+
{ entryId: "ARCH-node-cursor", name: "Cursor IDE", order: 0, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F5A5}\uFE0F", description: "AI-assisted development with MCP-powered knowledge context \u2014 smart capture, verification, context assembly in the editor", filePaths: ".cursor/mcp.json, .cursor/rules/", owner: "AI DX", rationale: "Integration layer because Cursor is an external tool that connects INTO our system via MCP. Why not Core or Features? Because Cursor itself is not our code \u2014 it's a consumer. The .cursor/ config files define how it talks to us, but Cursor lives outside our deployment boundary." } },
|
|
2247
|
+
{ entryId: "ARCH-node-github", name: "GitHub", order: 1, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F419}", description: "Code repository, PR reviews with governance context, CI/CD", owner: "Platform", rationale: "Integration layer because GitHub is an external service. Why not Infrastructure? Because Infra is about plumbing we control (database, analytics). GitHub is a third-party that hooks into our Core (PR reviews checking governance rules) but is not part of our deployed application." } },
|
|
2248
|
+
{ 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
|
+
];
|
|
2250
|
+
var SEED_FLOWS = [
|
|
2251
|
+
{ entryId: "ARCH-flow-smart-capture", name: "Smart Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls smart-capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
|
|
2252
|
+
{ 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
|
+
{ 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
|
+
{ 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" } },
|
|
2255
|
+
{ entryId: "ARCH-flow-analytics", name: "Analytics Flow", order: 4, data: { archType: "flow", sourceNode: "ARCH-node-command-center", targetNode: "ARCH-node-posthog", description: "Feature views and actions tracked \u2192 workspace-scoped events \u2192 PostHog group analytics \u2192 Command Center metrics", color: "#ec4899" } }
|
|
2256
|
+
];
|
|
2257
|
+
function registerArchitectureTools(server2) {
|
|
2258
|
+
server2.registerTool(
|
|
2259
|
+
"show-architecture",
|
|
2260
|
+
{
|
|
2261
|
+
title: "Show Architecture",
|
|
2262
|
+
description: "Render the system architecture map \u2014 layered visualization showing where every component lives. Returns layers (Auth \u2192 Infrastructure \u2192 Core \u2192 Features \u2192 Integration) with component nodes and data flows. Optionally filter by a specific template.",
|
|
2263
|
+
inputSchema: {
|
|
2264
|
+
template: z6.string().optional().describe("Template entry ID to filter by (default: first available template)")
|
|
2265
|
+
},
|
|
2266
|
+
annotations: { readOnlyHint: true }
|
|
2267
|
+
},
|
|
2268
|
+
async ({ template }) => {
|
|
2269
|
+
await ensureCollection();
|
|
2270
|
+
const all = await listArchEntries();
|
|
2271
|
+
const templates = byTag(all, "template");
|
|
2272
|
+
const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
|
|
2273
|
+
const templateName = activeTemplate?.name ?? "System Architecture";
|
|
2274
|
+
const templateId = activeTemplate?.entryId;
|
|
2275
|
+
const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
|
|
2276
|
+
const nodes = byTag(all, "node");
|
|
2277
|
+
const flows = byTag(all, "flow");
|
|
2278
|
+
if (layers.length === 0) {
|
|
2279
|
+
return {
|
|
2280
|
+
content: [{
|
|
2281
|
+
type: "text",
|
|
2282
|
+
text: "# Architecture Explorer\n\nNo architecture data found. Use `seed-architecture` to populate the default Product OS architecture."
|
|
2283
|
+
}]
|
|
2284
|
+
};
|
|
2188
2285
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2286
|
+
const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
|
|
2287
|
+
const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
|
|
2288
|
+
(f) => `- **${f.name}**: ${f.data?.description ?? ""}`
|
|
2289
|
+
).join("\n") : "";
|
|
2290
|
+
const text = `# ${templateName}
|
|
2193
2291
|
|
|
2194
|
-
${
|
|
2195
|
-
|
|
2196
|
-
lines.push("## Standards\n\nNo standards yet. Use `create-entry` with collection `standards` to add standards.");
|
|
2197
|
-
}
|
|
2198
|
-
} else {
|
|
2199
|
-
lines.push("## Standards\n\nCould not load standards \u2014 use `list-entries collection=standards` to browse manually.");
|
|
2200
|
-
}
|
|
2292
|
+
${textLayers}${textFlows}`;
|
|
2293
|
+
const html = renderArchitectureHtml(layers, nodes, flows, templateName);
|
|
2201
2294
|
return {
|
|
2202
|
-
|
|
2295
|
+
content: [
|
|
2296
|
+
{ type: "text", text },
|
|
2297
|
+
{ type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
|
|
2298
|
+
]
|
|
2203
2299
|
};
|
|
2204
2300
|
}
|
|
2205
2301
|
);
|
|
2206
|
-
server2.
|
|
2207
|
-
"
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2302
|
+
server2.registerTool(
|
|
2303
|
+
"explore-layer",
|
|
2304
|
+
{
|
|
2305
|
+
title: "Explore Architecture Layer",
|
|
2306
|
+
description: "Drill into a specific architecture layer to see all its component nodes with descriptions, team ownership, file paths, and linked entry counts.",
|
|
2307
|
+
inputSchema: {
|
|
2308
|
+
layer: z6.string().describe("Layer name or entry ID, e.g. 'Core' or 'ARCH-layer-core'")
|
|
2309
|
+
},
|
|
2310
|
+
annotations: { readOnlyHint: true }
|
|
2311
|
+
},
|
|
2312
|
+
async ({ layer }) => {
|
|
2313
|
+
await ensureCollection();
|
|
2314
|
+
const all = await listArchEntries();
|
|
2315
|
+
const layers = byTag(all, "layer");
|
|
2316
|
+
const target = layers.find(
|
|
2317
|
+
(l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
|
|
2318
|
+
);
|
|
2319
|
+
if (!target) {
|
|
2320
|
+
const available = layers.map((l) => `\`${l.name}\``).join(", ");
|
|
2321
|
+
return {
|
|
2322
|
+
content: [{
|
|
2323
|
+
type: "text",
|
|
2324
|
+
text: `Layer "${layer}" not found. Available layers: ${available}`
|
|
2325
|
+
}]
|
|
2326
|
+
};
|
|
2213
2327
|
}
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
**
|
|
2220
|
-
|
|
2221
|
-
|
|
2328
|
+
const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
|
|
2329
|
+
const flows = byTag(all, "flow").filter(
|
|
2330
|
+
(f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
|
|
2331
|
+
);
|
|
2332
|
+
const depRule = target.data?.dependsOn ? `
|
|
2333
|
+
**Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
|
|
2334
|
+
` : "";
|
|
2335
|
+
const layerRationale = target.data?.rationale ? `
|
|
2336
|
+
> ${target.data.rationale}
|
|
2337
|
+
` : "";
|
|
2338
|
+
const nodeDetail = nodes.map((n) => {
|
|
2339
|
+
const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
|
|
2340
|
+
if (n.data?.description) lines.push(String(n.data.description));
|
|
2341
|
+
if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
|
|
2342
|
+
if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
|
|
2343
|
+
if (n.data?.rationale) lines.push(`
|
|
2344
|
+
**Why here?** ${n.data.rationale}`);
|
|
2345
|
+
return lines.join("\n");
|
|
2346
|
+
}).join("\n\n");
|
|
2347
|
+
const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
|
|
2222
2348
|
return {
|
|
2223
|
-
|
|
2349
|
+
content: [{
|
|
2350
|
+
type: "text",
|
|
2351
|
+
text: `# ${target.data?.icon ?? ""} ${target.name} Layer
|
|
2224
2352
|
|
|
2225
|
-
${
|
|
2353
|
+
${target.data?.description ?? ""}${depRule}${layerRationale}
|
|
2354
|
+
**${nodes.length} components**
|
|
2355
|
+
|
|
2356
|
+
${nodeDetail}${flowLines}`
|
|
2357
|
+
}]
|
|
2226
2358
|
};
|
|
2227
2359
|
}
|
|
2228
2360
|
);
|
|
2229
|
-
server2.
|
|
2230
|
-
"
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2361
|
+
server2.registerTool(
|
|
2362
|
+
"show-flow",
|
|
2363
|
+
{
|
|
2364
|
+
title: "Show Architecture Flow",
|
|
2365
|
+
description: "Visualize a specific data flow path between architecture nodes \u2014 shows source, target, and description.",
|
|
2366
|
+
inputSchema: {
|
|
2367
|
+
flow: z6.string().describe("Flow name or entry ID, e.g. 'Smart Capture Flow' or 'ARCH-flow-smart-capture'")
|
|
2368
|
+
},
|
|
2369
|
+
annotations: { readOnlyHint: true }
|
|
2370
|
+
},
|
|
2371
|
+
async ({ flow }) => {
|
|
2372
|
+
await ensureCollection();
|
|
2373
|
+
const all = await listArchEntries();
|
|
2374
|
+
const flows = byTag(all, "flow");
|
|
2375
|
+
const target = flows.find(
|
|
2376
|
+
(f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
|
|
2377
|
+
);
|
|
2378
|
+
if (!target) {
|
|
2379
|
+
const available = flows.map((f) => `\`${f.name}\``).join(", ");
|
|
2234
2380
|
return {
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
}
|
|
2381
|
+
content: [{
|
|
2382
|
+
type: "text",
|
|
2383
|
+
text: `Flow "${flow}" not found. Available flows: ${available}`
|
|
2384
|
+
}]
|
|
2239
2385
|
};
|
|
2240
2386
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
const
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
server2.resource(
|
|
2255
|
-
"kb-labels",
|
|
2256
|
-
"productbrain://labels",
|
|
2257
|
-
async (uri) => {
|
|
2258
|
-
const labels = await mcpQuery("kb.listLabels");
|
|
2259
|
-
if (labels.length === 0) {
|
|
2260
|
-
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
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}\``);
|
|
2261
2400
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
const lines = [];
|
|
2266
|
-
for (const group of groups) {
|
|
2267
|
-
lines.push(`## ${group.name}`);
|
|
2268
|
-
for (const child of children(group._id)) {
|
|
2269
|
-
lines.push(`- \`${child.slug}\` ${child.name}${child.color ? ` (${child.color})` : ""}`);
|
|
2270
|
-
}
|
|
2401
|
+
if (dest) {
|
|
2402
|
+
lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
|
|
2403
|
+
if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
|
|
2271
2404
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
+
},
|
|
2415
|
+
async () => {
|
|
2416
|
+
await ensureCollection();
|
|
2417
|
+
const existing = await listArchEntries();
|
|
2418
|
+
const existingIds = new Set(existing.map((e) => e.entryId));
|
|
2419
|
+
let created = 0;
|
|
2420
|
+
let updated = 0;
|
|
2421
|
+
let unchanged = 0;
|
|
2422
|
+
const allSeeds = [
|
|
2423
|
+
{ ...SEED_TEMPLATE, order: 0, status: "active" },
|
|
2424
|
+
...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
|
|
2425
|
+
...SEED_NODES.map((n) => ({ ...n, status: "active" })),
|
|
2426
|
+
...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
|
|
2427
|
+
];
|
|
2428
|
+
for (const seed of allSeeds) {
|
|
2429
|
+
if (existingIds.has(seed.entryId)) {
|
|
2430
|
+
const existingEntry = existing.find((e) => e.entryId === seed.entryId);
|
|
2431
|
+
const existingData = existingEntry?.data ?? {};
|
|
2432
|
+
const seedData = seed.data;
|
|
2433
|
+
const hasChanges = Object.keys(seedData).some(
|
|
2434
|
+
(k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
|
|
2435
|
+
);
|
|
2436
|
+
if (hasChanges) {
|
|
2437
|
+
const mergedData = { ...existingData, ...seedData };
|
|
2438
|
+
await mcpMutation("kb.updateEntry", {
|
|
2439
|
+
entryId: seed.entryId,
|
|
2440
|
+
data: mergedData
|
|
2441
|
+
});
|
|
2442
|
+
updated++;
|
|
2443
|
+
} else {
|
|
2444
|
+
unchanged++;
|
|
2445
|
+
}
|
|
2446
|
+
continue;
|
|
2276
2447
|
}
|
|
2448
|
+
await mcpMutation("kb.createEntry", {
|
|
2449
|
+
collectionSlug: COLLECTION_SLUG,
|
|
2450
|
+
entryId: seed.entryId,
|
|
2451
|
+
name: seed.name,
|
|
2452
|
+
status: seed.status,
|
|
2453
|
+
data: seed.data,
|
|
2454
|
+
order: seed.order ?? 0
|
|
2455
|
+
});
|
|
2456
|
+
created++;
|
|
2277
2457
|
}
|
|
2278
2458
|
return {
|
|
2279
|
-
|
|
2459
|
+
content: [{
|
|
2460
|
+
type: "text",
|
|
2461
|
+
text: `# Architecture Seeded
|
|
2280
2462
|
|
|
2281
|
-
${
|
|
2463
|
+
**Created:** ${created} entries
|
|
2464
|
+
**Updated:** ${updated} (merged new fields)
|
|
2465
|
+
**Unchanged:** ${unchanged}
|
|
2466
|
+
|
|
2467
|
+
Use \`show-architecture\` to view the map.`
|
|
2468
|
+
}]
|
|
2282
2469
|
};
|
|
2283
2470
|
}
|
|
2284
2471
|
);
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
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."
|
|
2486
|
+
}]
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
await ensureCollection();
|
|
2490
|
+
const all = await listArchEntries();
|
|
2491
|
+
const layers = byTag(all, "layer");
|
|
2492
|
+
const nodes = byTag(all, "node");
|
|
2493
|
+
const result = scanDependencies(projectRoot, layers, nodes);
|
|
2494
|
+
return {
|
|
2495
|
+
content: [{
|
|
2496
|
+
type: "text",
|
|
2497
|
+
text: formatScanReport(result)
|
|
2498
|
+
}]
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
function resolveProjectRoot2() {
|
|
2504
|
+
const candidates = [
|
|
2505
|
+
process.env.WORKSPACE_PATH,
|
|
2506
|
+
process.cwd(),
|
|
2507
|
+
resolve2(process.cwd(), "..")
|
|
2508
|
+
].filter(Boolean);
|
|
2509
|
+
for (const dir of candidates) {
|
|
2510
|
+
const resolved = resolve2(dir);
|
|
2511
|
+
if (existsSync2(resolve2(resolved, "convex/schema.ts"))) return resolved;
|
|
2512
|
+
}
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
function scanDependencies(projectRoot, layers, nodes) {
|
|
2516
|
+
const layerMap = /* @__PURE__ */ new Map();
|
|
2517
|
+
for (const l of layers) layerMap.set(l.entryId, l);
|
|
2518
|
+
const allowedDeps = buildAllowedDeps(layers);
|
|
2519
|
+
const nodePathPrefixes = buildNodePrefixes(nodes);
|
|
2520
|
+
const violations = [];
|
|
2521
|
+
const nodeResults = /* @__PURE__ */ new Map();
|
|
2522
|
+
let totalFiles = 0;
|
|
2523
|
+
let totalImports = 0;
|
|
2524
|
+
let unmapped = 0;
|
|
2525
|
+
for (const node of nodes) {
|
|
2526
|
+
const layerRef = String(node.data?.layerRef ?? "");
|
|
2527
|
+
const layer = layerMap.get(layerRef);
|
|
2528
|
+
if (!layer) continue;
|
|
2529
|
+
const filePaths = parseFilePaths(node);
|
|
2530
|
+
const nodeViolations = [];
|
|
2531
|
+
let nodeFileCount = 0;
|
|
2532
|
+
for (const fp of filePaths) {
|
|
2533
|
+
const absPath = resolve2(projectRoot, fp);
|
|
2534
|
+
const files = collectFiles(absPath);
|
|
2535
|
+
for (const file of files) {
|
|
2536
|
+
nodeFileCount++;
|
|
2537
|
+
totalFiles++;
|
|
2538
|
+
const relFile = relative(projectRoot, file);
|
|
2539
|
+
const imports = parseImports(file);
|
|
2540
|
+
for (const imp of imports) {
|
|
2541
|
+
totalImports++;
|
|
2542
|
+
const resolved = resolveImport(imp, file, projectRoot);
|
|
2543
|
+
if (!resolved) {
|
|
2544
|
+
unmapped++;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const targetNode = findNodeByPath(resolved, nodePathPrefixes);
|
|
2548
|
+
if (!targetNode) {
|
|
2549
|
+
unmapped++;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const targetLayerRef = String(targetNode.data?.layerRef ?? "");
|
|
2553
|
+
const targetLayer = layerMap.get(targetLayerRef);
|
|
2554
|
+
if (!targetLayer) continue;
|
|
2555
|
+
if (targetLayerRef === layerRef) continue;
|
|
2556
|
+
const allowed = allowedDeps.get(layerRef);
|
|
2557
|
+
if (allowed && !allowed.has(targetLayerRef)) {
|
|
2558
|
+
const v = {
|
|
2559
|
+
sourceNode: node.name,
|
|
2560
|
+
sourceLayer: layer.name,
|
|
2561
|
+
sourceFile: relFile,
|
|
2562
|
+
importPath: imp,
|
|
2563
|
+
targetNode: targetNode.name,
|
|
2564
|
+
targetLayer: targetLayer.name,
|
|
2565
|
+
rule: `${layer.name} cannot import from ${targetLayer.name}`
|
|
2566
|
+
};
|
|
2567
|
+
violations.push(v);
|
|
2568
|
+
nodeViolations.push(v);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
nodeResults.set(node.entryId, { violations: nodeViolations, filesScanned: nodeFileCount });
|
|
2574
|
+
}
|
|
2575
|
+
return { violations, filesScanned: totalFiles, importsChecked: totalImports, unmappedImports: unmapped, nodeResults };
|
|
2576
|
+
}
|
|
2577
|
+
function buildAllowedDeps(layers) {
|
|
2578
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
2579
|
+
for (const l of layers) nameToId.set(l.name.toLowerCase(), l.entryId);
|
|
2580
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
2581
|
+
for (const layer of layers) {
|
|
2582
|
+
const deps = String(layer.data?.dependsOn ?? "none");
|
|
2583
|
+
const set = /* @__PURE__ */ new Set();
|
|
2584
|
+
if (deps !== "none") {
|
|
2585
|
+
for (const dep of deps.split(",").map((d) => d.trim().toLowerCase())) {
|
|
2586
|
+
const id = nameToId.get(dep);
|
|
2587
|
+
if (id) set.add(id);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
allowed.set(layer.entryId, set);
|
|
2591
|
+
}
|
|
2592
|
+
return allowed;
|
|
2593
|
+
}
|
|
2594
|
+
function buildNodePrefixes(nodes) {
|
|
2595
|
+
const entries = [];
|
|
2596
|
+
for (const node of nodes) {
|
|
2597
|
+
for (const fp of parseFilePaths(node)) {
|
|
2598
|
+
entries.push({ prefix: normalize(fp), node });
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
entries.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
2602
|
+
return entries;
|
|
2603
|
+
}
|
|
2604
|
+
function parseFilePaths(node) {
|
|
2605
|
+
const raw = node.data?.filePaths;
|
|
2606
|
+
if (!raw || typeof raw !== "string") return [];
|
|
2607
|
+
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2608
|
+
}
|
|
2609
|
+
function collectFiles(absPath) {
|
|
2610
|
+
if (!existsSync2(absPath)) return [];
|
|
2611
|
+
const stat = statSync(absPath);
|
|
2612
|
+
if (stat.isFile()) {
|
|
2613
|
+
return isScannableFile(absPath) ? [absPath] : [];
|
|
2614
|
+
}
|
|
2615
|
+
if (!stat.isDirectory()) return [];
|
|
2616
|
+
const results = [];
|
|
2617
|
+
const walk = (dir) => {
|
|
2618
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2619
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2620
|
+
const full = resolve2(dir, entry.name);
|
|
2621
|
+
if (entry.isDirectory()) walk(full);
|
|
2622
|
+
else if (isScannableFile(full)) results.push(full);
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
walk(absPath);
|
|
2626
|
+
return results;
|
|
2627
|
+
}
|
|
2628
|
+
function isScannableFile(p) {
|
|
2629
|
+
return /\.(ts|js|svelte)$/.test(p) && !p.endsWith(".d.ts");
|
|
2630
|
+
}
|
|
2631
|
+
function parseImports(filePath) {
|
|
2632
|
+
try {
|
|
2633
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2634
|
+
const re = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
2635
|
+
const imports = [];
|
|
2636
|
+
let match;
|
|
2637
|
+
while ((match = re.exec(content)) !== null) {
|
|
2638
|
+
imports.push(match[1]);
|
|
2639
|
+
}
|
|
2640
|
+
return imports;
|
|
2641
|
+
} catch {
|
|
2642
|
+
return [];
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
var EXTENSIONS = [".ts", ".js", ".svelte", "/index.ts", "/index.js", "/index.svelte"];
|
|
2646
|
+
function tryResolveWithExtension(absPath) {
|
|
2647
|
+
if (existsSync2(absPath) && statSync(absPath).isFile()) return absPath;
|
|
2648
|
+
for (const ext of EXTENSIONS) {
|
|
2649
|
+
const withExt = absPath + ext;
|
|
2650
|
+
if (existsSync2(withExt)) return withExt;
|
|
2651
|
+
}
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2654
|
+
function resolveImport(imp, fromFile, root) {
|
|
2655
|
+
let rel = null;
|
|
2656
|
+
if (imp.startsWith("$lib/")) rel = imp.replace("$lib/", "src/lib/");
|
|
2657
|
+
else if (imp.startsWith("$convex/")) rel = imp.replace("$convex/", "convex/");
|
|
2658
|
+
else if (imp.startsWith("$env/") || imp.startsWith("$app/")) return null;
|
|
2659
|
+
else if (imp.startsWith("./") || imp.startsWith("../")) {
|
|
2660
|
+
const fromDir = dirname(fromFile);
|
|
2661
|
+
const abs2 = resolve2(fromDir, imp);
|
|
2662
|
+
rel = relative(root, abs2);
|
|
2663
|
+
}
|
|
2664
|
+
if (!rel) return null;
|
|
2665
|
+
const abs = resolve2(root, rel);
|
|
2666
|
+
const actual = tryResolveWithExtension(abs);
|
|
2667
|
+
return actual ? relative(root, actual) : rel;
|
|
2668
|
+
}
|
|
2669
|
+
function findNodeByPath(filePath, prefixes) {
|
|
2670
|
+
const normalized = normalize(filePath);
|
|
2671
|
+
for (const { prefix, node } of prefixes) {
|
|
2672
|
+
if (normalized.startsWith(prefix)) return node;
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
function formatScanReport(result) {
|
|
2677
|
+
const lines = [];
|
|
2678
|
+
if (result.violations.length === 0) {
|
|
2679
|
+
lines.push(
|
|
2680
|
+
`# Architecture Health Check Passed`,
|
|
2681
|
+
"",
|
|
2682
|
+
`**0 violations** across ${result.filesScanned} files (${result.importsChecked} imports checked, ${result.unmappedImports} unmapped).`,
|
|
2683
|
+
"",
|
|
2684
|
+
"All imports respect the layer dependency rules."
|
|
2685
|
+
);
|
|
2686
|
+
} else {
|
|
2687
|
+
lines.push(
|
|
2688
|
+
`# Architecture Health Check \u2014 ${result.violations.length} Violation${result.violations.length === 1 ? "" : "s"}`,
|
|
2689
|
+
"",
|
|
2690
|
+
`Scanned ${result.filesScanned} files, checked ${result.importsChecked} imports, found **${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}** (${result.unmappedImports} unmapped).`,
|
|
2691
|
+
""
|
|
2692
|
+
);
|
|
2693
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
2694
|
+
for (const v of result.violations) {
|
|
2695
|
+
if (!byNode.has(v.sourceNode)) byNode.set(v.sourceNode, []);
|
|
2696
|
+
byNode.get(v.sourceNode).push(v);
|
|
2697
|
+
}
|
|
2698
|
+
for (const [nodeName, vs] of byNode) {
|
|
2699
|
+
lines.push(`## ${nodeName} (${vs[0].sourceLayer})`);
|
|
2700
|
+
for (const v of vs) {
|
|
2701
|
+
lines.push(`- \`${v.sourceFile}\` imports \`${v.importPath}\` \u2192 **${v.targetNode}** (${v.targetLayer}) \u2014 ${v.rule}`);
|
|
2702
|
+
}
|
|
2703
|
+
lines.push("");
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
lines.push("---", "");
|
|
2707
|
+
const nodeEntries = [...result.nodeResults.entries()];
|
|
2708
|
+
const cleanCount = nodeEntries.filter(([, r]) => r.violations.length === 0 && r.filesScanned > 0).length;
|
|
2709
|
+
const dirtyCount = nodeEntries.filter(([, r]) => r.violations.length > 0).length;
|
|
2710
|
+
const emptyCount = nodeEntries.filter(([, r]) => r.filesScanned === 0).length;
|
|
2711
|
+
lines.push(
|
|
2712
|
+
`**Summary:** ${cleanCount} clean nodes, ${dirtyCount} with violations, ${emptyCount} with no files.`
|
|
2713
|
+
);
|
|
2714
|
+
return lines.join("\n");
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// src/tools/workflows.ts
|
|
2718
|
+
import { z as z7 } from "zod";
|
|
2719
|
+
|
|
2720
|
+
// src/workflows/definitions.ts
|
|
2721
|
+
var RETRO_WORKFLOW = {
|
|
2722
|
+
id: "retro",
|
|
2723
|
+
name: "Retrospective",
|
|
2724
|
+
shortDescription: "Structured team retrospective \u2014 reflect, surface patterns, commit to actions. Outputs a decision entry in the Knowledge Base.",
|
|
2725
|
+
icon: "\u25CE",
|
|
2726
|
+
facilitatorPreamble: `You are now in **Facilitator Mode**. You are not a coding assistant \u2014 you are a facilitator running a structured retrospective.
|
|
2727
|
+
|
|
2728
|
+
## Your Behavior
|
|
2729
|
+
|
|
2730
|
+
1. **Guide, don't solve.** Ask questions, reflect back, synthesize. Never jump to solutions.
|
|
2731
|
+
2. **One round at a time.** Complete each round fully before moving to the next. No skipping.
|
|
2732
|
+
3. **Use structured questions** (AskQuestion tool) for choices and multi-select. Use open conversation for reflection.
|
|
2733
|
+
4. **Create a Plan** at the start showing all rounds as tasks. Update it as you progress.
|
|
2734
|
+
5. **Synthesize between rounds.** After collecting input, reflect back what you heard before moving on.
|
|
2735
|
+
6. **Never go silent.** If something fails, say what happened and what to do next.
|
|
2736
|
+
7. **Capture to KB** at the end using the Product OS smart-capture tool.
|
|
2737
|
+
8. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
|
|
2738
|
+
|
|
2739
|
+
## Communication Style
|
|
2740
|
+
|
|
2741
|
+
- Start each round with its number, name, and a brief instruction
|
|
2742
|
+
- Use quotes and emphasis to reflect back what the participant said
|
|
2743
|
+
- End each round with a brief synthesis before transitioning
|
|
2744
|
+
- When a round is complete, mark it as done in the Plan
|
|
2745
|
+
- If the participant seems stuck, offer prompts \u2014 never pressure
|
|
2746
|
+
- If a tool call fails, explain what happened and offer an alternative path
|
|
2747
|
+
|
|
2748
|
+
## Error Recovery
|
|
2749
|
+
|
|
2750
|
+
If at any point a tool call or MCP operation fails:
|
|
2751
|
+
1. Tell the participant what you were trying to do
|
|
2752
|
+
2. Explain what went wrong (briefly, no stack traces)
|
|
2753
|
+
3. Offer a manual alternative (e.g., "I'll capture this in the conversation instead")
|
|
2754
|
+
4. Continue the workflow \u2014 never halt completely
|
|
2755
|
+
|
|
2756
|
+
## Plan Structure
|
|
2757
|
+
|
|
2758
|
+
Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you enter it, completed when done.`,
|
|
2759
|
+
rounds: [
|
|
2760
|
+
{
|
|
2761
|
+
id: "set-stage",
|
|
2762
|
+
num: "01",
|
|
2763
|
+
label: "Set the Stage",
|
|
2764
|
+
type: "choice",
|
|
2765
|
+
instruction: "Before we dive in, let's frame what we're reflecting on. What's the scope of this retro?",
|
|
2766
|
+
facilitatorGuidance: "Start warm. Ask the participant to pick or describe what they're retro-ing. Confirm the scope before proceeding. If they gave context already in their initial message, use it \u2014 don't make them repeat.",
|
|
2767
|
+
questions: [
|
|
2768
|
+
{
|
|
2769
|
+
id: "scope",
|
|
2770
|
+
prompt: "What are we reflecting on? Pick one or describe your own.",
|
|
2771
|
+
options: [
|
|
2772
|
+
{ id: "last-week", label: "Last week's work" },
|
|
2773
|
+
{ id: "last-sprint", label: "Last sprint/cycle" },
|
|
2774
|
+
{ id: "specific-project", label: "A specific project or feature" },
|
|
2775
|
+
{ id: "process", label: "A process or workflow" },
|
|
2776
|
+
{ id: "custom", label: "Something else \u2014 let me describe it" }
|
|
2777
|
+
]
|
|
2778
|
+
}
|
|
2779
|
+
],
|
|
2780
|
+
outputSchema: {
|
|
2781
|
+
field: "scope",
|
|
2782
|
+
description: "What timeframe or project is being retro'd",
|
|
2783
|
+
format: "freetext"
|
|
2784
|
+
},
|
|
2785
|
+
maxDurationHint: "2 min"
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
id: "what-went-well",
|
|
2789
|
+
num: "02",
|
|
2790
|
+
label: "What Went Well",
|
|
2791
|
+
type: "open",
|
|
2792
|
+
instruction: "Let's start with the good. What went well? What are you proud of? What should we do more of?",
|
|
2793
|
+
facilitatorGuidance: "Celebrate. Reflect back each win with genuine emphasis. Ask follow-ups like 'What made that work?' or 'Who else contributed to that?' Collect at least 3 items before synthesizing. Don't rush past the positive \u2014 teams skip this too fast.",
|
|
2794
|
+
outputSchema: {
|
|
2795
|
+
field: "wentWell",
|
|
2796
|
+
description: "List of things that went well",
|
|
2797
|
+
format: "list"
|
|
2798
|
+
},
|
|
2799
|
+
maxDurationHint: "5 min"
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
id: "what-didnt-go-well",
|
|
2803
|
+
num: "03",
|
|
2804
|
+
label: "What Didn't Go Well",
|
|
2805
|
+
type: "open",
|
|
2806
|
+
instruction: "Now the harder part. What didn't go well? What frustrated you? Where did things break down?",
|
|
2807
|
+
facilitatorGuidance: "Create safety. Acknowledge that this is harder. Don't judge or immediately problem-solve \u2014 just listen and capture. Ask 'What was the impact of that?' and 'When did you first notice it?'. Reflect back without softening. Collect at least 3 items.",
|
|
2808
|
+
outputSchema: {
|
|
2809
|
+
field: "didntGoWell",
|
|
2810
|
+
description: "List of things that didn't go well",
|
|
2811
|
+
format: "list"
|
|
2812
|
+
},
|
|
2813
|
+
maxDurationHint: "5 min"
|
|
2814
|
+
},
|
|
2815
|
+
{
|
|
2816
|
+
id: "patterns",
|
|
2817
|
+
num: "04",
|
|
2818
|
+
label: "Patterns & Insights",
|
|
2819
|
+
type: "synthesis",
|
|
2820
|
+
instruction: "Looking at what went well and what didn't \u2014 what patterns do you see? What's the deeper insight?",
|
|
2821
|
+
facilitatorGuidance: "This is YOUR moment as facilitator. Synthesize what you've heard across rounds 2 and 3. Surface themes, connections, and contradictions. Propose 2-3 patterns and ask the participant to react. This round transforms raw observations into actionable insights. Don't let the participant skip the 'why' \u2014 push for root causes.",
|
|
2822
|
+
outputSchema: {
|
|
2823
|
+
field: "patterns",
|
|
2824
|
+
description: "Synthesized patterns and insights",
|
|
2825
|
+
format: "structured"
|
|
2826
|
+
},
|
|
2827
|
+
maxDurationHint: "5 min"
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
id: "actions",
|
|
2831
|
+
num: "05",
|
|
2832
|
+
label: "Actions & Commitments",
|
|
2833
|
+
type: "commit",
|
|
2834
|
+
instruction: "Based on these patterns, what will we actually change? Be specific \u2014 who does what, by when?",
|
|
2835
|
+
facilitatorGuidance: "Push for specificity. 'Be better at X' is not an action. 'Randy will set up a 15-min weekly check-in by Friday' is. Each action needs an owner and a deadline. Aim for 2-4 concrete actions. Use AskQuestion to confirm the final list. These become the retro's output.",
|
|
2836
|
+
questions: [
|
|
2837
|
+
{
|
|
2838
|
+
id: "action-confirm",
|
|
2839
|
+
prompt: "Are these actions concrete enough to actually happen?",
|
|
2840
|
+
options: [
|
|
2841
|
+
{ id: "yes", label: "Yes \u2014 these are clear and actionable" },
|
|
2842
|
+
{ id: "refine", label: "Let me refine some of these" },
|
|
2843
|
+
{ id: "add", label: "I want to add more" }
|
|
2844
|
+
]
|
|
2845
|
+
}
|
|
2846
|
+
],
|
|
2847
|
+
outputSchema: {
|
|
2848
|
+
field: "actions",
|
|
2849
|
+
description: "Committed actions with owners and deadlines",
|
|
2850
|
+
format: "structured"
|
|
2851
|
+
},
|
|
2852
|
+
maxDurationHint: "5 min"
|
|
2853
|
+
},
|
|
2854
|
+
{
|
|
2855
|
+
id: "close",
|
|
2856
|
+
num: "06",
|
|
2857
|
+
label: "Close & Capture",
|
|
2858
|
+
type: "close",
|
|
2859
|
+
instruction: "One last thing \u2014 in one sentence, what's the single most important thing you're taking away from this retro?",
|
|
2860
|
+
facilitatorGuidance: "Keep it brief. One sentence reflection. Then summarize the entire retro: scope, key wins, key pain points, patterns identified, actions committed. Ask if they want to save this to the Knowledge Base. If yes, use smart-capture to create a decision/tension entry. Thank them for the retro.",
|
|
2861
|
+
outputSchema: {
|
|
2862
|
+
field: "takeaway",
|
|
2863
|
+
description: "Single-sentence takeaway",
|
|
2864
|
+
format: "freetext"
|
|
2865
|
+
},
|
|
2866
|
+
kbCollection: "decisions",
|
|
2867
|
+
maxDurationHint: "2 min"
|
|
2868
|
+
}
|
|
2869
|
+
],
|
|
2870
|
+
kbOutputCollection: "decisions",
|
|
2871
|
+
kbOutputTemplate: {
|
|
2872
|
+
nameTemplate: "Retro: {scope} \u2014 {date}",
|
|
2873
|
+
descriptionField: "rationale"
|
|
2874
|
+
},
|
|
2875
|
+
errorRecovery: `If anything goes wrong during the retro:
|
|
2876
|
+
|
|
2877
|
+
1. **MCP tool failure**: Skip the KB capture step. Summarize everything in the conversation instead. Suggest the participant runs \`smart-capture\` manually later.
|
|
2878
|
+
2. **AskQuestion not available**: Fall back to numbered options in plain text. "Reply with 1, 2, or 3."
|
|
2879
|
+
3. **Plan creation fails**: Continue without the Plan. The conversation IS the record.
|
|
2880
|
+
4. **Participant goes off-topic**: Gently redirect: "That's valuable \u2014 let's capture it. For now, let's stay with [current round]."
|
|
2881
|
+
5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to save partial results to KB.
|
|
2882
|
+
|
|
2883
|
+
The retro must never fail silently. Always communicate state.`
|
|
2884
|
+
};
|
|
2885
|
+
var WORKFLOWS = /* @__PURE__ */ new Map([
|
|
2886
|
+
["retro", RETRO_WORKFLOW]
|
|
2887
|
+
]);
|
|
2888
|
+
function getWorkflow(id) {
|
|
2889
|
+
return WORKFLOWS.get(id);
|
|
2890
|
+
}
|
|
2891
|
+
function listWorkflows() {
|
|
2892
|
+
return Array.from(WORKFLOWS.values());
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// src/tools/workflows.ts
|
|
2896
|
+
function formatWorkflowCard(wf) {
|
|
2897
|
+
const roundList = wf.rounds.map((r) => ` ${r.num}. ${r.label} (${r.type}, ~${r.maxDurationHint ?? "?"})`).join("\n");
|
|
2898
|
+
return `## ${wf.icon} ${wf.name}
|
|
2899
|
+
**ID**: \`${wf.id}\`
|
|
2900
|
+
${wf.shortDescription}
|
|
2901
|
+
|
|
2902
|
+
**Rounds** (${wf.rounds.length}):
|
|
2903
|
+
${roundList}
|
|
2904
|
+
|
|
2905
|
+
**Output**: Creates entries in \`${wf.kbOutputCollection}\` collection.
|
|
2906
|
+
_Use the \`run-workflow\` prompt with workflow="${wf.id}" to start._`;
|
|
2907
|
+
}
|
|
2908
|
+
function registerWorkflowTools(server2) {
|
|
2909
|
+
server2.registerTool(
|
|
2910
|
+
"list-workflows",
|
|
2911
|
+
{
|
|
2912
|
+
title: "List Workflows",
|
|
2913
|
+
description: "List all available Chainwork workflows \u2014 retro, shape-a-bet, IDM, etc. Each workflow is a structured multi-round facilitation ceremony that the agent runs in Facilitator Mode. Use the `run-workflow` prompt to actually launch one.",
|
|
2914
|
+
annotations: { readOnlyHint: true }
|
|
2915
|
+
},
|
|
2916
|
+
async () => {
|
|
2917
|
+
const workflows = listWorkflows();
|
|
2918
|
+
if (workflows.length === 0) {
|
|
2919
|
+
return {
|
|
2920
|
+
content: [{
|
|
2921
|
+
type: "text",
|
|
2922
|
+
text: "No workflows registered yet. Check back after the workflow definitions are configured."
|
|
2923
|
+
}]
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
const cards = workflows.map(formatWorkflowCard).join("\n\n---\n\n");
|
|
2927
|
+
return {
|
|
2928
|
+
content: [{
|
|
2929
|
+
type: "text",
|
|
2930
|
+
text: `# Available Chainwork Workflows
|
|
2931
|
+
|
|
2932
|
+
${workflows.length} workflow(s) available. Use the \`run-workflow\` prompt to launch one.
|
|
2933
|
+
|
|
2934
|
+
---
|
|
2935
|
+
|
|
2936
|
+
${cards}`
|
|
2937
|
+
}]
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
);
|
|
2941
|
+
server2.registerTool(
|
|
2942
|
+
"workflow-checkpoint",
|
|
2943
|
+
{
|
|
2944
|
+
title: "Workflow Checkpoint",
|
|
2945
|
+
description: "Record the output of a workflow round. Captures the round's data to the Knowledge Base as a structured entry. Use this during Facilitator Mode after completing each round to persist progress \u2014 so if the conversation is interrupted, work is not lost.\n\nAt workflow completion, this tool can also generate the final summary entry.",
|
|
2946
|
+
inputSchema: {
|
|
2947
|
+
workflowId: z7.string().describe("Workflow ID (e.g., 'retro')"),
|
|
2948
|
+
roundId: z7.string().describe("Round ID (e.g., 'what-went-well')"),
|
|
2949
|
+
output: z7.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
|
|
2950
|
+
isFinal: z7.boolean().optional().describe(
|
|
2951
|
+
"If true, this is the final checkpoint and triggers the summary KB entry creation"
|
|
2952
|
+
),
|
|
2953
|
+
summaryName: z7.string().optional().describe(
|
|
2954
|
+
"Name for the final KB entry (required when isFinal=true)"
|
|
2955
|
+
),
|
|
2956
|
+
summaryDescription: z7.string().optional().describe(
|
|
2957
|
+
"Full description/rationale for the final KB entry (required when isFinal=true)"
|
|
2958
|
+
)
|
|
2959
|
+
},
|
|
2960
|
+
annotations: { destructiveHint: false }
|
|
2961
|
+
},
|
|
2962
|
+
async ({ workflowId, roundId, output, isFinal, summaryName, summaryDescription }) => {
|
|
2963
|
+
const wf = getWorkflow(workflowId);
|
|
2964
|
+
if (!wf) {
|
|
2965
|
+
return {
|
|
2966
|
+
content: [{
|
|
2967
|
+
type: "text",
|
|
2968
|
+
text: `Workflow "${workflowId}" not found. Available: ${listWorkflows().map((w) => w.id).join(", ")}.
|
|
2969
|
+
|
|
2970
|
+
This checkpoint was NOT saved. Continue the conversation \u2014 the facilitator has the context.`
|
|
2971
|
+
}]
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
const round = wf.rounds.find((r) => r.id === roundId);
|
|
2975
|
+
if (!round) {
|
|
2976
|
+
return {
|
|
2977
|
+
content: [{
|
|
2978
|
+
type: "text",
|
|
2979
|
+
text: `Round "${roundId}" not found in workflow "${workflowId}". Available rounds: ${wf.rounds.map((r) => r.id).join(", ")}.
|
|
2980
|
+
|
|
2981
|
+
This checkpoint was NOT saved. The conversation context is preserved \u2014 continue facilitating.`
|
|
2982
|
+
}]
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
const lines = [
|
|
2986
|
+
`## Checkpoint: Round ${round.num} \u2014 ${round.label}`,
|
|
2987
|
+
`Workflow: ${wf.name} (\`${wf.id}\`)`,
|
|
2988
|
+
""
|
|
2989
|
+
];
|
|
2990
|
+
if (isFinal && summaryName && summaryDescription) {
|
|
2991
|
+
try {
|
|
2992
|
+
const entryId = await mcpMutation("kb.createEntry", {
|
|
2993
|
+
collectionSlug: wf.kbOutputCollection,
|
|
2994
|
+
name: summaryName,
|
|
2995
|
+
status: "draft",
|
|
2996
|
+
data: {
|
|
2997
|
+
[wf.kbOutputTemplate.descriptionField]: summaryDescription,
|
|
2998
|
+
workflowType: wf.id,
|
|
2999
|
+
completedRound: roundId,
|
|
3000
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
3001
|
+
},
|
|
3002
|
+
createdBy: `workflow:${wf.id}`
|
|
3003
|
+
});
|
|
3004
|
+
lines.push(
|
|
3005
|
+
`**KB Entry Created**: \`${entryId}\``,
|
|
3006
|
+
`Collection: \`${wf.kbOutputCollection}\``,
|
|
3007
|
+
`Name: ${summaryName}`,
|
|
3008
|
+
"",
|
|
3009
|
+
`The retro is now captured in the Knowledge Base. `,
|
|
3010
|
+
`Use \`suggest-links\` on this entry to connect it to related knowledge.`
|
|
3011
|
+
);
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3014
|
+
lines.push(
|
|
3015
|
+
`**KB capture failed**: ${msg}`,
|
|
3016
|
+
"",
|
|
3017
|
+
`The retro output is preserved in this conversation. `,
|
|
3018
|
+
`You can manually create the entry later using \`smart-capture\` with:`,
|
|
3019
|
+
`- Collection: \`${wf.kbOutputCollection}\``,
|
|
3020
|
+
`- Name: ${summaryName}`,
|
|
3021
|
+
`- Description: (copy from the conversation summary above)`
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
} else {
|
|
3025
|
+
lines.push(
|
|
3026
|
+
`Round ${round.num} output recorded.`,
|
|
3027
|
+
`Output: ${output.substring(0, 200)}${output.length > 200 ? "..." : ""}`
|
|
3028
|
+
);
|
|
3029
|
+
const currentIdx = wf.rounds.findIndex((r) => r.id === roundId);
|
|
3030
|
+
if (currentIdx < wf.rounds.length - 1) {
|
|
3031
|
+
const next = wf.rounds[currentIdx + 1];
|
|
3032
|
+
lines.push(
|
|
3033
|
+
"",
|
|
3034
|
+
`**Next**: Round ${next.num} \u2014 ${next.label}`,
|
|
3035
|
+
`_${next.instruction}_`
|
|
3036
|
+
);
|
|
3037
|
+
} else {
|
|
3038
|
+
lines.push(
|
|
3039
|
+
"",
|
|
3040
|
+
`**All rounds complete.** Call this tool again with \`isFinal: true\` to create the KB entry.`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3045
|
+
}
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// src/tools/gitchain.ts
|
|
3050
|
+
import { z as z8 } from "zod";
|
|
3051
|
+
function linkSummary(links) {
|
|
3052
|
+
return Object.entries(links).map(([id, content]) => {
|
|
3053
|
+
const filled = typeof content === "string" && content.length > 0;
|
|
3054
|
+
const preview = filled ? content.substring(0, 80) + (content.length > 80 ? "..." : "") : "(empty)";
|
|
3055
|
+
return ` - **${id}**: ${preview}`;
|
|
3056
|
+
}).join("\n");
|
|
3057
|
+
}
|
|
3058
|
+
function registerGitChainTools(server2) {
|
|
3059
|
+
server2.registerTool(
|
|
3060
|
+
"chain-create",
|
|
3061
|
+
{
|
|
3062
|
+
title: "Create Chain",
|
|
3063
|
+
description: "Create a new strategic chain in the knowledge base. Chains are versioned knowledge artifacts that follow a chain type definition (e.g. Strategy Coherence: Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome). Returns the chain's entry ID for subsequent operations.",
|
|
3064
|
+
inputSchema: {
|
|
3065
|
+
title: z8.string().describe("Title for the chain, e.g. 'Q1 Growth Strategy'"),
|
|
3066
|
+
chainTypeId: z8.string().default("strategy-coherence").describe(
|
|
3067
|
+
"Chain type ID. Use 'strategy-coherence' for 5-link strategy chains (Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome) or 'idm-proposal' for IDM governance chains (Tension \u2192 Proposal \u2192 Objections \u2192 Integration \u2192 Decision)"
|
|
3068
|
+
),
|
|
3069
|
+
description: z8.string().optional().describe("Optional description of what this chain is about"),
|
|
3070
|
+
author: z8.string().optional().describe("Who is creating this chain (clerkUserId or person name). Defaults to 'mcp'.")
|
|
3071
|
+
}
|
|
3072
|
+
},
|
|
3073
|
+
async ({ title, chainTypeId, description, author }) => {
|
|
3074
|
+
const result = await mcpMutation(
|
|
3075
|
+
"gitchain.createChain",
|
|
3076
|
+
{ title, chainTypeId, description, author }
|
|
3077
|
+
);
|
|
3078
|
+
return {
|
|
3079
|
+
content: [
|
|
3080
|
+
{
|
|
3081
|
+
type: "text",
|
|
3082
|
+
text: `# Chain Created
|
|
3083
|
+
|
|
3084
|
+
- **Entry ID:** \`${result.entryId}\`
|
|
3085
|
+
- **Title:** ${title}
|
|
3086
|
+
- **Type:** ${chainTypeId}
|
|
3087
|
+
- **Status:** draft
|
|
3088
|
+
|
|
3089
|
+
Use \`chain-edit\` with chainEntryId=\`${result.entryId}\` to start filling in links.`
|
|
3090
|
+
}
|
|
3091
|
+
]
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
);
|
|
3095
|
+
server2.registerTool(
|
|
3096
|
+
"chain-get",
|
|
3097
|
+
{
|
|
3098
|
+
title: "Get Chain",
|
|
3099
|
+
description: "Retrieve a chain by its entry ID. Returns the full chain content, coherence scores, link fill status, and version info. Use chain-list first to discover chain entry IDs.",
|
|
3100
|
+
inputSchema: {
|
|
3101
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
|
|
3102
|
+
},
|
|
3103
|
+
annotations: { readOnlyHint: true }
|
|
3104
|
+
},
|
|
3105
|
+
async ({ chainEntryId }) => {
|
|
3106
|
+
const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
|
|
3107
|
+
if (!chain) {
|
|
3108
|
+
return {
|
|
3109
|
+
content: [
|
|
3110
|
+
{
|
|
3111
|
+
type: "text",
|
|
3112
|
+
text: `Chain "${chainEntryId}" not found.`
|
|
3113
|
+
}
|
|
3114
|
+
]
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
const scoreSection = chain.scores ? `
|
|
3118
|
+
## Coherence: ${chain.coherenceScore}%
|
|
3119
|
+
|
|
3120
|
+
` + chain.scores.sections.map((s) => `- **${s.key}**: ${s.power}/100 (${"\u2605".repeat(Math.min(s.stars ?? 0, 5))}${"\u2606".repeat(Math.max(0, 5 - (s.stars ?? 0)))})`).join("\n") : "";
|
|
3121
|
+
const text = `# ${chain.name}
|
|
3122
|
+
|
|
3123
|
+
- **Entry ID:** \`${chain.entryId}\`
|
|
3124
|
+
- **Type:** ${chain.chainTypeName}
|
|
3125
|
+
- **Status:** ${chain.status}
|
|
3126
|
+
- **Links filled:** ${chain.filledCount}/${chain.totalCount}
|
|
3127
|
+
- **Version:** ${chain.currentVersion}
|
|
3128
|
+
- **Created by:** ${chain.createdBy ?? "unknown"}
|
|
3129
|
+
- **History events:** ${chain.historyCount}
|
|
3130
|
+
` + scoreSection + `
|
|
3131
|
+
|
|
3132
|
+
## Links
|
|
3133
|
+
|
|
3134
|
+
` + linkSummary(chain.links);
|
|
3135
|
+
return { content: [{ type: "text", text }] };
|
|
3136
|
+
}
|
|
3137
|
+
);
|
|
3138
|
+
server2.registerTool(
|
|
3139
|
+
"chain-edit",
|
|
3140
|
+
{
|
|
3141
|
+
title: "Edit Chain Link",
|
|
3142
|
+
description: "Edit a specific link in a chain. Each chain has named links (e.g. for strategy-coherence: problem, insight, choice, action, outcome). The link content is replaced entirely \u2014 pass the full new text.",
|
|
3143
|
+
inputSchema: {
|
|
3144
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3145
|
+
linkId: z8.string().describe(
|
|
3146
|
+
"Which link to edit. For strategy-coherence: problem, insight, choice, action, outcome. For idm-proposal: tension, proposal, objections, integration, decision."
|
|
3147
|
+
),
|
|
3148
|
+
content: z8.string().describe("The full new content for this link"),
|
|
3149
|
+
author: z8.string().optional().describe("Who is making this edit. Defaults to 'mcp'.")
|
|
3150
|
+
}
|
|
3151
|
+
},
|
|
3152
|
+
async ({ chainEntryId, linkId, content, author }) => {
|
|
3153
|
+
const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
|
|
3154
|
+
return {
|
|
3155
|
+
content: [
|
|
3156
|
+
{
|
|
3157
|
+
type: "text",
|
|
3158
|
+
text: `# Link Updated
|
|
3159
|
+
|
|
3160
|
+
- **Chain:** \`${result.entryId}\`
|
|
3161
|
+
- **Link:** ${result.linkId}
|
|
3162
|
+
- **Chain status:** ${result.status}
|
|
3163
|
+
- **Content length:** ${content.length} chars
|
|
3164
|
+
|
|
3165
|
+
Use \`chain-get\` to see the full chain with updated scores.`
|
|
3166
|
+
}
|
|
3167
|
+
]
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
);
|
|
3171
|
+
server2.registerTool(
|
|
3172
|
+
"chain-list",
|
|
3173
|
+
{
|
|
3174
|
+
title: "List Chains",
|
|
3175
|
+
description: "List all chains in the workspace, optionally filtered by chain type or status. Returns entry IDs, titles, link fill progress, and coherence scores.",
|
|
3176
|
+
inputSchema: {
|
|
3177
|
+
chainTypeId: z8.string().optional().describe("Filter by chain type: 'strategy-coherence' or 'idm-proposal'"),
|
|
3178
|
+
status: z8.string().optional().describe("Filter by status: 'draft' or 'active'")
|
|
3179
|
+
},
|
|
3180
|
+
annotations: { readOnlyHint: true }
|
|
3181
|
+
},
|
|
3182
|
+
async ({ chainTypeId, status }) => {
|
|
3183
|
+
const chains = await mcpQuery("gitchain.listChains", {
|
|
3184
|
+
chainTypeId,
|
|
3185
|
+
status
|
|
3186
|
+
});
|
|
3187
|
+
if (chains.length === 0) {
|
|
3188
|
+
return {
|
|
3189
|
+
content: [
|
|
3190
|
+
{
|
|
3191
|
+
type: "text",
|
|
3192
|
+
text: "No chains found. Use `chain-create` to create one."
|
|
3193
|
+
}
|
|
3194
|
+
]
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
const formatted = chains.map(
|
|
3198
|
+
(c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
|
|
3199
|
+
).join("\n");
|
|
3200
|
+
return {
|
|
3201
|
+
content: [
|
|
3202
|
+
{
|
|
3203
|
+
type: "text",
|
|
3204
|
+
text: `# Chains (${chains.length})
|
|
3205
|
+
|
|
3206
|
+
${formatted}`
|
|
3207
|
+
}
|
|
3208
|
+
]
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
);
|
|
3212
|
+
server2.registerTool(
|
|
3213
|
+
"chain-history",
|
|
3214
|
+
{
|
|
3215
|
+
title: "Chain History",
|
|
3216
|
+
description: "View the edit history of a chain \u2014 all creation, update, and version events. Shows who changed what and when.",
|
|
3217
|
+
inputSchema: {
|
|
3218
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'")
|
|
3219
|
+
},
|
|
3220
|
+
annotations: { readOnlyHint: true }
|
|
3221
|
+
},
|
|
3222
|
+
async ({ chainEntryId }) => {
|
|
3223
|
+
const history = await mcpQuery("gitchain.getHistory", {
|
|
3224
|
+
chainEntryId
|
|
3225
|
+
});
|
|
3226
|
+
if (history.length === 0) {
|
|
3227
|
+
return {
|
|
3228
|
+
content: [
|
|
3229
|
+
{
|
|
3230
|
+
type: "text",
|
|
3231
|
+
text: `No history found for chain "${chainEntryId}".`
|
|
3232
|
+
}
|
|
3233
|
+
]
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
|
|
3237
|
+
const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
|
|
3238
|
+
return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
|
|
3239
|
+
}).join("\n");
|
|
3240
|
+
return {
|
|
3241
|
+
content: [
|
|
3242
|
+
{
|
|
3243
|
+
type: "text",
|
|
3244
|
+
text: `# History for ${chainEntryId} (${history.length} events)
|
|
3245
|
+
|
|
3246
|
+
${formatted}`
|
|
3247
|
+
}
|
|
3248
|
+
]
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
);
|
|
3252
|
+
server2.registerTool(
|
|
3253
|
+
"chain-commit",
|
|
3254
|
+
{
|
|
3255
|
+
title: "Commit Chain",
|
|
3256
|
+
description: "Create a version snapshot (commit) of the current chain state. Records all link content, computes coherence score, and tracks which links changed. Commit messages should follow: type(link): description. Types: edit, refine, rewrite, integrate, revert.",
|
|
3257
|
+
inputSchema: {
|
|
3258
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3259
|
+
commitMessage: z8.string().describe(
|
|
3260
|
+
"Commit message following convention: type(link): description. Example: 'edit(outcome): Add Q1 revenue target'"
|
|
3261
|
+
),
|
|
3262
|
+
author: z8.string().optional().describe("Who is committing. Defaults to 'mcp'.")
|
|
3263
|
+
}
|
|
3264
|
+
},
|
|
3265
|
+
async ({ chainEntryId, commitMessage, author }) => {
|
|
3266
|
+
const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
|
|
3267
|
+
const warning = result.commitLintWarning ? `
|
|
3268
|
+
|
|
3269
|
+
> **Lint warning:** ${result.commitLintWarning}` : "";
|
|
3270
|
+
return {
|
|
3271
|
+
content: [
|
|
3272
|
+
{
|
|
3273
|
+
type: "text",
|
|
3274
|
+
text: `# Committed v${result.version}
|
|
3275
|
+
|
|
3276
|
+
- **Chain:** \`${result.entryId}\`
|
|
3277
|
+
- **Version:** ${result.version}
|
|
3278
|
+
- **Message:** ${commitMessage}
|
|
3279
|
+
- **Coherence:** ${result.coherenceScore}%
|
|
3280
|
+
- **Links modified:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
|
|
3281
|
+
` + 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
|
+
]
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
const formatted = commits.map((c) => {
|
|
3312
|
+
const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
|
|
3313
|
+
const msg = c.commitMessage ?? c.changeNote ?? "(no message)";
|
|
3314
|
+
const links = c.linksModified && c.linksModified.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
|
|
3315
|
+
return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
|
|
3316
|
+
}).join("\n");
|
|
3317
|
+
return {
|
|
3318
|
+
content: [
|
|
3319
|
+
{
|
|
3320
|
+
type: "text",
|
|
3321
|
+
text: `# Commits for ${chainEntryId} (${commits.length})
|
|
3322
|
+
|
|
3323
|
+
${formatted}`
|
|
3324
|
+
}
|
|
3325
|
+
]
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
);
|
|
3329
|
+
server2.registerTool(
|
|
3330
|
+
"chain-diff",
|
|
3331
|
+
{
|
|
3332
|
+
title: "Diff Chain Versions",
|
|
3333
|
+
description: "Compare two versions of a chain. Shows which links changed, word-level diffs for modified links, and the coherence score delta. Use chain-commits first to see available version numbers.",
|
|
3334
|
+
inputSchema: {
|
|
3335
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3336
|
+
versionA: z8.number().describe("The earlier version number"),
|
|
3337
|
+
versionB: z8.number().describe("The later version number")
|
|
3338
|
+
},
|
|
3339
|
+
annotations: { readOnlyHint: true }
|
|
3340
|
+
},
|
|
3341
|
+
async ({ chainEntryId, versionA, versionB }) => {
|
|
3342
|
+
const diff = await mcpMutation("gitchain.diffVersions", {
|
|
3343
|
+
chainEntryId,
|
|
3344
|
+
versionA,
|
|
3345
|
+
versionB
|
|
3346
|
+
});
|
|
3347
|
+
let text = `# Diff: v${versionA} \u2192 v${versionB}
|
|
3348
|
+
|
|
3349
|
+
- **Chain:** \`${diff.chainEntryId}\`
|
|
3350
|
+
- **Coherence:** ${diff.coherenceBefore}% \u2192 ${diff.coherenceAfter}% (${diff.coherenceDelta >= 0 ? "+" : ""}${diff.coherenceDelta})
|
|
3351
|
+
- **Links changed:** ${diff.linksChanged.length > 0 ? diff.linksChanged.join(", ") : "none"}
|
|
3352
|
+
`;
|
|
3353
|
+
for (const ld of diff.linkDiffs) {
|
|
3354
|
+
if (ld.status === "unchanged") continue;
|
|
3355
|
+
text += `
|
|
3356
|
+
## ${ld.linkId} (${ld.status})
|
|
3357
|
+
|
|
3358
|
+
`;
|
|
3359
|
+
const wordDiff = diff.wordDiffs[ld.linkId];
|
|
3360
|
+
if (wordDiff && wordDiff.length > 0) {
|
|
3361
|
+
for (const w of wordDiff) {
|
|
3362
|
+
if (w.type === "delete") {
|
|
3363
|
+
text += `~~${w.value.substring(0, 200)}~~`;
|
|
3364
|
+
} else if (w.type === "insert") {
|
|
3365
|
+
text += `**${w.value.substring(0, 200)}**`;
|
|
3366
|
+
} else {
|
|
3367
|
+
text += w.value.substring(0, 200);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
text += "\n";
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
return { content: [{ type: "text", text }] };
|
|
3374
|
+
}
|
|
3375
|
+
);
|
|
3376
|
+
server2.registerTool(
|
|
3377
|
+
"chain-gate",
|
|
3378
|
+
{
|
|
3379
|
+
title: "Chain Coherence Gate",
|
|
3380
|
+
description: "Run the coherence gate on a chain. Checks: coherence score >= 70%, all links filled, commit message follows convention. Returns pass/fail with detailed check results. This is the same gate that blocks publishing.",
|
|
3381
|
+
inputSchema: {
|
|
3382
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3383
|
+
commitMessage: z8.string().optional().describe("Optional commit message to lint")
|
|
3384
|
+
},
|
|
3385
|
+
annotations: { readOnlyHint: true }
|
|
3386
|
+
},
|
|
3387
|
+
async ({ chainEntryId, commitMessage }) => {
|
|
3388
|
+
const gate = await mcpQuery("gitchain.runGate", {
|
|
3389
|
+
chainEntryId,
|
|
3390
|
+
commitMessage
|
|
3391
|
+
});
|
|
3392
|
+
const checkLines = gate.checks.map(
|
|
3393
|
+
(c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`
|
|
3394
|
+
).join("\n");
|
|
3395
|
+
const icon = gate.pass ? "PASS" : "BLOCKED";
|
|
3396
|
+
return {
|
|
3397
|
+
content: [
|
|
3398
|
+
{
|
|
3399
|
+
type: "text",
|
|
3400
|
+
text: `# Gate: ${icon}
|
|
3401
|
+
|
|
3402
|
+
- **Score:** ${gate.score}%
|
|
3403
|
+
- **Threshold:** ${gate.threshold}%
|
|
3404
|
+
|
|
3405
|
+
## Checks
|
|
3406
|
+
|
|
3407
|
+
${checkLines}`
|
|
3408
|
+
}
|
|
3409
|
+
]
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
);
|
|
3413
|
+
server2.registerTool(
|
|
3414
|
+
"chain-branch",
|
|
3415
|
+
{
|
|
3416
|
+
title: "Create or List Chain Branches",
|
|
3417
|
+
description: "Create a new branch for isolated editing, or list all branches on a chain. Branches fork from the current published version of main. Use chain-merge to merge a branch back.",
|
|
3418
|
+
inputSchema: {
|
|
3419
|
+
chainEntryId: z8.string().describe("The chain's entry ID, e.g. 'CH-A1B2C3'"),
|
|
3420
|
+
action: z8.enum(["create", "list"]).describe("'create' to make a new branch, 'list' to see all branches"),
|
|
3421
|
+
name: z8.string().optional().describe("Branch name for 'create' action. Auto-generated if not provided."),
|
|
3422
|
+
author: z8.string().optional().describe("Who is creating the branch. Defaults to 'mcp'.")
|
|
3423
|
+
}
|
|
3424
|
+
},
|
|
3425
|
+
async ({ chainEntryId, action, name, author }) => {
|
|
3426
|
+
if (action === "create") {
|
|
3427
|
+
const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name, author });
|
|
3428
|
+
return {
|
|
3429
|
+
content: [
|
|
3430
|
+
{
|
|
3431
|
+
type: "text",
|
|
3432
|
+
text: `# Branch Created
|
|
3433
|
+
|
|
3434
|
+
- **Name:** ${result.name}
|
|
3435
|
+
- **Based on:** v${result.baseVersion}
|
|
3436
|
+
- **Chain:** \`${chainEntryId}\`
|
|
3437
|
+
|
|
3438
|
+
Edit links and commit on this branch, then use \`chain-merge\` to land changes.`
|
|
3439
|
+
}
|
|
3440
|
+
]
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
const branches = await mcpMutation("gitchain.listBranches", {
|
|
3444
|
+
chainEntryId
|
|
3445
|
+
});
|
|
3446
|
+
if (branches.length === 0) {
|
|
3447
|
+
return {
|
|
3448
|
+
content: [
|
|
3449
|
+
{
|
|
3450
|
+
type: "text",
|
|
3451
|
+
text: `No branches found for chain "${chainEntryId}".`
|
|
3452
|
+
}
|
|
3453
|
+
]
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
const formatted = branches.map(
|
|
3457
|
+
(b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`
|
|
3458
|
+
).join("\n");
|
|
3459
|
+
return {
|
|
3460
|
+
content: [
|
|
3461
|
+
{
|
|
3462
|
+
type: "text",
|
|
3463
|
+
text: `# Branches for ${chainEntryId} (${branches.length})
|
|
3464
|
+
|
|
3465
|
+
${formatted}`
|
|
3466
|
+
}
|
|
3467
|
+
]
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
);
|
|
3471
|
+
server2.registerTool(
|
|
3472
|
+
"chain-conflicts",
|
|
3473
|
+
{
|
|
3474
|
+
title: "Check Branch Conflicts",
|
|
3475
|
+
description: "Check if a branch has conflicts with other active branches. Conflicts occur when two branches modify the same chain link. Always check before merging.",
|
|
3476
|
+
inputSchema: {
|
|
3477
|
+
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3478
|
+
branchName: z8.string().describe("The branch name to check for conflicts")
|
|
3479
|
+
},
|
|
3480
|
+
annotations: { readOnlyHint: true }
|
|
3481
|
+
},
|
|
3482
|
+
async ({ chainEntryId, branchName }) => {
|
|
3483
|
+
const result = await mcpMutation("gitchain.checkConflicts", {
|
|
3484
|
+
chainEntryId,
|
|
3485
|
+
branchName
|
|
3486
|
+
});
|
|
3487
|
+
if (!result.hasConflicts) {
|
|
3488
|
+
return {
|
|
3489
|
+
content: [
|
|
3490
|
+
{
|
|
3491
|
+
type: "text",
|
|
3492
|
+
text: `# No Conflicts
|
|
3493
|
+
|
|
3494
|
+
Branch "${branchName}" on \`${chainEntryId}\` has no conflicts with other active branches. Safe to merge.`
|
|
3495
|
+
}
|
|
3496
|
+
]
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
const conflictLines = result.conflicts.map(
|
|
3500
|
+
(c) => `- **${c.linkId}** \u2014 modified by: ${c.branches.map((b) => `${b.branchName} (${b.author})`).join(", ")}`
|
|
3501
|
+
).join("\n");
|
|
3502
|
+
return {
|
|
3503
|
+
content: [
|
|
3504
|
+
{
|
|
3505
|
+
type: "text",
|
|
3506
|
+
text: `# Conflicts Detected
|
|
3507
|
+
|
|
3508
|
+
Branch "${branchName}" conflicts with other branches on these links:
|
|
3509
|
+
|
|
3510
|
+
` + conflictLines + `
|
|
3511
|
+
|
|
3512
|
+
Resolve conflicts before merging. One branch must update its link content to avoid overwriting.`
|
|
3513
|
+
}
|
|
3514
|
+
]
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
);
|
|
3518
|
+
server2.registerTool(
|
|
3519
|
+
"chain-merge",
|
|
3520
|
+
{
|
|
3521
|
+
title: "Merge Branch",
|
|
3522
|
+
description: "Merge a branch back into main. Runs the coherence gate before merging. The branch is closed after merge. Check for conflicts with chain-conflicts first.",
|
|
3523
|
+
inputSchema: {
|
|
3524
|
+
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3525
|
+
branchName: z8.string().describe("The branch to merge"),
|
|
3526
|
+
strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) preserves history, 'squash' collapses into one version"),
|
|
3527
|
+
author: z8.string().optional().describe("Who is merging. Defaults to 'mcp'.")
|
|
3528
|
+
}
|
|
3529
|
+
},
|
|
3530
|
+
async ({ chainEntryId, branchName, strategy, author }) => {
|
|
3531
|
+
const result = await mcpMutation("gitchain.mergeBranch", {
|
|
3532
|
+
chainEntryId,
|
|
3533
|
+
branchName,
|
|
3534
|
+
strategy,
|
|
3535
|
+
author
|
|
3536
|
+
});
|
|
3537
|
+
return {
|
|
3538
|
+
content: [
|
|
3539
|
+
{
|
|
3540
|
+
type: "text",
|
|
3541
|
+
text: `# Branch Merged
|
|
3542
|
+
|
|
3543
|
+
- **Chain:** \`${result.entryId}\`
|
|
3544
|
+
- **Branch:** ${result.branchName} (now closed)
|
|
3545
|
+
- **Version:** v${result.version}
|
|
3546
|
+
- **Strategy:** ${result.strategy}
|
|
3547
|
+
|
|
3548
|
+
Main is now at v${result.version}. The branch has been closed.`
|
|
3549
|
+
}
|
|
3550
|
+
]
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
);
|
|
3554
|
+
server2.registerTool(
|
|
3555
|
+
"chain-comment",
|
|
3556
|
+
{
|
|
3557
|
+
title: "Comment on Chain Version",
|
|
3558
|
+
description: "Add a threaded comment on a specific chain version, optionally targeting a specific link. Comments support threading via parentId. Use chain-comments to see existing comments.",
|
|
3559
|
+
inputSchema: {
|
|
3560
|
+
chainEntryId: z8.string().describe("The chain's entry ID"),
|
|
3561
|
+
action: z8.enum(["add", "resolve", "list"]).describe("'add' a comment, 'resolve' a comment, or 'list' all comments"),
|
|
3562
|
+
versionNumber: z8.number().optional().describe("Version number to comment on (required for 'add')"),
|
|
3563
|
+
linkId: z8.string().optional().describe("Which link this comment targets (optional)"),
|
|
3564
|
+
body: z8.string().optional().describe("Comment text (required for 'add')"),
|
|
3565
|
+
commentId: z8.string().optional().describe("Comment ID to resolve (required for 'resolve')"),
|
|
3566
|
+
author: z8.string().optional().describe("Who is commenting. Defaults to 'mcp'.")
|
|
3567
|
+
}
|
|
3568
|
+
},
|
|
3569
|
+
async ({ chainEntryId, action, versionNumber, linkId, body, commentId, author }) => {
|
|
3570
|
+
if (action === "add") {
|
|
3571
|
+
if (!versionNumber) throw new Error("versionNumber is required for 'add'");
|
|
3572
|
+
if (!body) throw new Error("body is required for 'add'");
|
|
3573
|
+
const result = await mcpMutation("gitchain.addComment", {
|
|
3574
|
+
chainEntryId,
|
|
3575
|
+
versionNumber,
|
|
3576
|
+
linkId,
|
|
3577
|
+
body,
|
|
3578
|
+
author
|
|
3579
|
+
});
|
|
3580
|
+
return {
|
|
3581
|
+
content: [
|
|
3582
|
+
{
|
|
3583
|
+
type: "text",
|
|
3584
|
+
text: `# Comment Added
|
|
3585
|
+
|
|
3586
|
+
- **Chain:** \`${result.chainEntryId}\`
|
|
3587
|
+
- **Version:** v${result.versionNumber}
|
|
3588
|
+
` + (result.linkId ? `- **Link:** ${result.linkId}
|
|
3589
|
+
` : "") + `- **Body:** ${body?.substring(0, 200)}`
|
|
3590
|
+
}
|
|
3591
|
+
]
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
if (action === "resolve") {
|
|
3595
|
+
if (!commentId) throw new Error("commentId is required for 'resolve'");
|
|
3596
|
+
await mcpMutation("gitchain.resolveComment", { commentId });
|
|
3597
|
+
return {
|
|
3598
|
+
content: [
|
|
3599
|
+
{
|
|
3600
|
+
type: "text",
|
|
3601
|
+
text: `Comment resolved.`
|
|
3602
|
+
}
|
|
3603
|
+
]
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
const comments = await mcpMutation("gitchain.listComments", {
|
|
3607
|
+
chainEntryId,
|
|
3608
|
+
versionNumber
|
|
3609
|
+
});
|
|
3610
|
+
if (comments.length === 0) {
|
|
3611
|
+
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
|
+
{
|
|
3630
|
+
type: "text",
|
|
3631
|
+
text: `# Comments for ${chainEntryId} (${comments.length}, ${unresolvedCount} unresolved)
|
|
3632
|
+
|
|
3633
|
+
${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'.")
|
|
3648
|
+
}
|
|
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
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// src/resources/index.ts
|
|
3672
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3673
|
+
function formatEntryMarkdown(entry) {
|
|
3674
|
+
const id = entry.entryId ? `${entry.entryId}: ` : "";
|
|
3675
|
+
const lines = [`## ${id}${entry.name} [${entry.status}]`];
|
|
3676
|
+
if (entry.data && typeof entry.data === "object") {
|
|
3677
|
+
for (const [key, val] of Object.entries(entry.data)) {
|
|
3678
|
+
if (val && key !== "rawData") {
|
|
3679
|
+
lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return lines.join("\n");
|
|
3684
|
+
}
|
|
3685
|
+
function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules) {
|
|
3686
|
+
const sections = ["# Product Brain \u2014 Orientation"];
|
|
3687
|
+
sections.push(
|
|
3688
|
+
"## Architecture\n```\nCursor (stdio) \u2192 MCP Server (mcp-server/src/index.ts)\n \u2192 POST /api/mcp with Bearer token\n \u2192 Convex HTTP Action (convex/http.ts)\n \u2192 internalQuery / internalMutation (convex/mcpKnowledge.ts)\n \u2192 Convex DB (workspace-scoped)\n```\nSecurity: API key auth on every request, workspace-scoped data, internal functions blocked from external clients.\nKey files: `packages/mcp-server/src/client.ts` (HTTP client + audit), `convex/schema.ts` (schema)."
|
|
3689
|
+
);
|
|
3690
|
+
if (collections) {
|
|
3691
|
+
const collList = collections.map((c) => {
|
|
3692
|
+
const prefix = c.icon ? `${c.icon} ` : "";
|
|
3693
|
+
return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
|
|
3694
|
+
}).join("\n");
|
|
3695
|
+
sections.push(
|
|
3696
|
+
`## Data Model (${collections.length} collections)
|
|
3697
|
+
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
3698
|
+
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, history via \`entryHistory\`, labels via \`labels\` + \`entryLabels\`.
|
|
3699
|
+
|
|
3700
|
+
` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
|
|
3701
|
+
);
|
|
3702
|
+
} else {
|
|
3703
|
+
sections.push(
|
|
3704
|
+
"## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
|
|
3708
|
+
sections.push(
|
|
3709
|
+
`## Business Rules
|
|
3710
|
+
Collection: \`business-rules\` (${rulesCount}).
|
|
3711
|
+
Find rules: \`kb-search\` for text search, \`list-entries collection=business-rules\` to browse.
|
|
3712
|
+
Check compliance: use the \`review-against-rules\` prompt (pass a domain).
|
|
3713
|
+
Draft a new rule: use the \`draft-rule-from-context\` prompt.`
|
|
3714
|
+
);
|
|
3715
|
+
const eventsCount = trackingEvents ? `${trackingEvents.length} events` : "not loaded \u2014 collection may not exist yet";
|
|
3716
|
+
const conventionNote = standards ? "Naming convention: `object_action` in snake_case with past-tense verbs (from standards collection)." : "Naming convention: `object_action` in snake_case with past-tense verbs.";
|
|
3717
|
+
sections.push(
|
|
3718
|
+
`## Analytics & Tracking
|
|
3719
|
+
Event catalog: \`tracking-events\` collection (${eventsCount}).
|
|
3720
|
+
${conventionNote}
|
|
3721
|
+
Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
|
|
3722
|
+
Browse: \`list-entries collection=tracking-events\`. Full setup: \`docs/posthog-setup.md\`.`
|
|
3723
|
+
);
|
|
3724
|
+
sections.push(
|
|
3725
|
+
"## 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
|
+
);
|
|
3727
|
+
sections.push(
|
|
3728
|
+
"## Creating Knowledge\nUse `smart-capture` as the primary tool for creating new entries. It handles the full workflow in one call:\n1. Creates the entry with collection-aware defaults (auto-fills dates, infers domains, sets priority)\n2. Auto-links related entries from across the knowledge base (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`.\nAll other collections use sensible defaults.\n\nExample: `smart-capture collection='tensions' name='...' description='...'`\n\nUse `quality-check` to score existing entries retroactively.\nUse `create-entry` only when you need full control over every field.\nUse `quick-capture` for minimal ceremony without auto-linking."
|
|
3729
|
+
);
|
|
3730
|
+
sections.push(
|
|
3731
|
+
"## Where to Go Next\n- **Create entry** \u2192 `smart-capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (start here when working on a feature)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `list-labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Architecture map** \u2192 `show-architecture` tool (layered system visualization)\n- **Explore a layer** \u2192 `explore-layer` tool (drill into Auth, Core, Features, etc.)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
|
|
3732
|
+
);
|
|
3733
|
+
return sections.join("\n\n---\n\n");
|
|
3734
|
+
}
|
|
3735
|
+
function registerResources(server2) {
|
|
3736
|
+
server2.resource(
|
|
3737
|
+
"kb-orientation",
|
|
3738
|
+
"productbrain://orientation",
|
|
3739
|
+
async (uri) => {
|
|
3740
|
+
const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
|
|
3741
|
+
mcpQuery("kb.listCollections"),
|
|
3742
|
+
mcpQuery("kb.listEntries", { collectionSlug: "tracking-events" }),
|
|
3743
|
+
mcpQuery("kb.listEntries", { collectionSlug: "standards" }),
|
|
3744
|
+
mcpQuery("kb.listEntries", { collectionSlug: "business-rules" })
|
|
3745
|
+
]);
|
|
3746
|
+
const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
|
|
3747
|
+
const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
|
|
3748
|
+
const standards = standardsResult.status === "fulfilled" ? standardsResult.value : null;
|
|
3749
|
+
const businessRules = rulesResult.status === "fulfilled" ? rulesResult.value : null;
|
|
3750
|
+
return {
|
|
3751
|
+
contents: [{
|
|
3752
|
+
uri: uri.href,
|
|
3753
|
+
text: buildOrientationMarkdown(collections, trackingEvents, standards, businessRules),
|
|
3754
|
+
mimeType: "text/markdown"
|
|
3755
|
+
}]
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
);
|
|
3759
|
+
server2.resource(
|
|
3760
|
+
"kb-terminology",
|
|
3761
|
+
"productbrain://terminology",
|
|
3762
|
+
async (uri) => {
|
|
3763
|
+
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
3764
|
+
mcpQuery("kb.listEntries", { collectionSlug: "glossary" }),
|
|
3765
|
+
mcpQuery("kb.listEntries", { collectionSlug: "standards" })
|
|
3766
|
+
]);
|
|
3767
|
+
const lines = ["# Product Brain \u2014 Terminology"];
|
|
3768
|
+
if (glossaryResult.status === "fulfilled") {
|
|
3769
|
+
if (glossaryResult.value.length > 0) {
|
|
3770
|
+
const terms = glossaryResult.value.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
|
|
3771
|
+
lines.push(`## Glossary (${glossaryResult.value.length} terms)
|
|
3772
|
+
|
|
3773
|
+
${terms}`);
|
|
3774
|
+
} else {
|
|
3775
|
+
lines.push("## Glossary\n\nNo glossary terms yet. Use `create-entry` with collection `glossary` to add terms.");
|
|
3776
|
+
}
|
|
3777
|
+
} else {
|
|
3778
|
+
lines.push("## Glossary\n\nCould not load glossary \u2014 use `list-entries collection=glossary` to browse manually.");
|
|
3779
|
+
}
|
|
3780
|
+
if (standardsResult.status === "fulfilled") {
|
|
3781
|
+
if (standardsResult.value.length > 0) {
|
|
3782
|
+
const stds = standardsResult.value.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${s.data?.description ?? ""}`).join("\n");
|
|
3783
|
+
lines.push(`## Standards (${standardsResult.value.length} entries)
|
|
3784
|
+
|
|
3785
|
+
${stds}`);
|
|
3786
|
+
} else {
|
|
3787
|
+
lines.push("## Standards\n\nNo standards yet. Use `create-entry` with collection `standards` to add standards.");
|
|
3788
|
+
}
|
|
3789
|
+
} else {
|
|
3790
|
+
lines.push("## Standards\n\nCould not load standards \u2014 use `list-entries collection=standards` to browse manually.");
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
);
|
|
3797
|
+
server2.resource(
|
|
3798
|
+
"kb-collections",
|
|
3799
|
+
"productbrain://collections",
|
|
3800
|
+
async (uri) => {
|
|
3801
|
+
const collections = await mcpQuery("kb.listCollections");
|
|
3802
|
+
if (collections.length === 0) {
|
|
3803
|
+
return { contents: [{ uri: uri.href, text: "No collections in this workspace.", mimeType: "text/markdown" }] };
|
|
3804
|
+
}
|
|
3805
|
+
const formatted = collections.map((c) => {
|
|
3806
|
+
const fieldList = c.fields.map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
|
|
3807
|
+
return `## ${c.icon ?? ""} ${c.name} (\`${c.slug}\`)
|
|
3808
|
+
${c.description || ""}
|
|
3809
|
+
|
|
3810
|
+
**Fields:**
|
|
3811
|
+
${fieldList}`;
|
|
3812
|
+
}).join("\n\n---\n\n");
|
|
3813
|
+
return {
|
|
3814
|
+
contents: [{ uri: uri.href, text: `# Knowledge Collections (${collections.length})
|
|
3815
|
+
|
|
3816
|
+
${formatted}`, mimeType: "text/markdown" }]
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
);
|
|
3820
|
+
server2.resource(
|
|
3821
|
+
"kb-collection-entries",
|
|
3822
|
+
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
3823
|
+
list: async () => {
|
|
3824
|
+
const collections = await mcpQuery("kb.listCollections");
|
|
3825
|
+
return {
|
|
3826
|
+
resources: collections.map((c) => ({
|
|
3827
|
+
uri: `productbrain://${c.slug}/entries`,
|
|
3828
|
+
name: `${c.icon ?? ""} ${c.name}`.trim()
|
|
3829
|
+
}))
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
}),
|
|
3833
|
+
async (uri, { slug }) => {
|
|
3834
|
+
const entries = await mcpQuery("kb.listEntries", { collectionSlug: slug });
|
|
3835
|
+
const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
|
|
3836
|
+
return {
|
|
3837
|
+
contents: [{
|
|
3838
|
+
uri: uri.href,
|
|
3839
|
+
text: formatted || "No entries in this collection.",
|
|
3840
|
+
mimeType: "text/markdown"
|
|
3841
|
+
}]
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
);
|
|
3845
|
+
server2.resource(
|
|
3846
|
+
"kb-labels",
|
|
3847
|
+
"productbrain://labels",
|
|
3848
|
+
async (uri) => {
|
|
3849
|
+
const labels = await mcpQuery("kb.listLabels");
|
|
3850
|
+
if (labels.length === 0) {
|
|
3851
|
+
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
3852
|
+
}
|
|
3853
|
+
const groups = labels.filter((l) => l.isGroup);
|
|
3854
|
+
const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
|
|
3855
|
+
const children = (parentId) => labels.filter((l) => l.parentId === parentId);
|
|
3856
|
+
const lines = [];
|
|
3857
|
+
for (const group of groups) {
|
|
3858
|
+
lines.push(`## ${group.name}`);
|
|
3859
|
+
for (const child of children(group._id)) {
|
|
3860
|
+
lines.push(`- \`${child.slug}\` ${child.name}${child.color ? ` (${child.color})` : ""}`);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
if (ungrouped.length > 0) {
|
|
3864
|
+
lines.push("## Ungrouped");
|
|
3865
|
+
for (const l of ungrouped) {
|
|
3866
|
+
lines.push(`- \`${l.slug}\` ${l.name}${l.color ? ` (${l.color})` : ""}`);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
return {
|
|
3870
|
+
contents: [{ uri: uri.href, text: `# Workspace Labels (${labels.length})
|
|
3871
|
+
|
|
3872
|
+
${lines.join("\n")}`, mimeType: "text/markdown" }]
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
// src/prompts/index.ts
|
|
3879
|
+
import { z as z9 } from "zod";
|
|
2289
3880
|
function registerPrompts(server2) {
|
|
2290
3881
|
server2.prompt(
|
|
2291
3882
|
"review-against-rules",
|
|
2292
3883
|
"Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
|
|
2293
|
-
{ domain:
|
|
3884
|
+
{ domain: z9.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
2294
3885
|
async ({ domain }) => {
|
|
2295
3886
|
const entries = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
|
|
2296
3887
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
@@ -2343,7 +3934,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
2343
3934
|
server2.prompt(
|
|
2344
3935
|
"name-check",
|
|
2345
3936
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
2346
|
-
{ names:
|
|
3937
|
+
{ names: z9.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
2347
3938
|
async ({ names }) => {
|
|
2348
3939
|
const terms = await mcpQuery("kb.listEntries", { collectionSlug: "glossary" });
|
|
2349
3940
|
const glossaryContext = terms.map(
|
|
@@ -2379,7 +3970,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
2379
3970
|
server2.prompt(
|
|
2380
3971
|
"draft-decision-record",
|
|
2381
3972
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
2382
|
-
{ context:
|
|
3973
|
+
{ context: z9.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
2383
3974
|
async ({ context }) => {
|
|
2384
3975
|
const recentDecisions = await mcpQuery("kb.listEntries", { collectionSlug: "decisions" });
|
|
2385
3976
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -2417,8 +4008,8 @@ After drafting, I can log it using the create-entry tool with collection "decisi
|
|
|
2417
4008
|
"draft-rule-from-context",
|
|
2418
4009
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
2419
4010
|
{
|
|
2420
|
-
observation:
|
|
2421
|
-
domain:
|
|
4011
|
+
observation: z9.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
|
|
4012
|
+
domain: z9.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
|
|
2422
4013
|
},
|
|
2423
4014
|
async ({ observation, domain }) => {
|
|
2424
4015
|
const allRules = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
|
|
@@ -2459,13 +4050,120 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
|
|
|
2459
4050
|
};
|
|
2460
4051
|
}
|
|
2461
4052
|
);
|
|
4053
|
+
server2.prompt(
|
|
4054
|
+
"run-workflow",
|
|
4055
|
+
"Launch a Chainwork workflow (retro, shape, IDM) in Facilitator Mode. Returns the full workflow definition, facilitation instructions, and round structure. The agent enters Facilitator Mode and guides the participant through each round.",
|
|
4056
|
+
{
|
|
4057
|
+
workflow: z9.string().describe(
|
|
4058
|
+
"Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
|
|
4059
|
+
),
|
|
4060
|
+
context: z9.string().optional().describe(
|
|
4061
|
+
"Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
|
|
4062
|
+
)
|
|
4063
|
+
},
|
|
4064
|
+
async ({ workflow: workflowId, context }) => {
|
|
4065
|
+
const wf = getWorkflow(workflowId);
|
|
4066
|
+
if (!wf) {
|
|
4067
|
+
const available = listWorkflows().map((w) => `- **${w.id}**: ${w.name} \u2014 ${w.shortDescription}`).join("\n");
|
|
4068
|
+
return {
|
|
4069
|
+
messages: [
|
|
4070
|
+
{
|
|
4071
|
+
role: "user",
|
|
4072
|
+
content: {
|
|
4073
|
+
type: "text",
|
|
4074
|
+
text: `Workflow "${workflowId}" not found.
|
|
4075
|
+
|
|
4076
|
+
Available workflows:
|
|
4077
|
+
${available}
|
|
4078
|
+
|
|
4079
|
+
Use one of these IDs to run a workflow.`
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
]
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
let kbContext = "";
|
|
4086
|
+
try {
|
|
4087
|
+
const recentDecisions = await mcpQuery("kb.listEntries", {
|
|
4088
|
+
collectionSlug: wf.kbOutputCollection
|
|
4089
|
+
});
|
|
4090
|
+
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
4091
|
+
if (sorted.length > 0) {
|
|
4092
|
+
kbContext = `
|
|
4093
|
+
## Recent ${wf.kbOutputCollection} entries (for context)
|
|
4094
|
+
` + sorted.map((d) => `- ${d.entryId ?? ""}: ${d.name} [${d.status}]`).join("\n");
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
4097
|
+
kbContext = "\n_Could not load KB context \u2014 proceed without it._";
|
|
4098
|
+
}
|
|
4099
|
+
const roundsPlan = wf.rounds.map(
|
|
4100
|
+
(r) => `### Round ${r.num}: ${r.label}
|
|
4101
|
+
**Type**: ${r.type} | **Duration**: ~${r.maxDurationHint ?? "5 min"}
|
|
4102
|
+
**Instruction**: ${r.instruction}
|
|
4103
|
+
**Facilitator guidance**: ${r.facilitatorGuidance}
|
|
4104
|
+
` + (r.questions ? r.questions.map(
|
|
4105
|
+
(q) => `**Question**: ${q.prompt}
|
|
4106
|
+
` + (q.options ? q.options.map((o) => ` - ${o.id}: ${o.label}`).join("\n") : " _(open response)_")
|
|
4107
|
+
).join("\n") : "") + `
|
|
4108
|
+
**Output**: Capture to \`${r.outputSchema.field}\` (${r.outputSchema.format})`
|
|
4109
|
+
).join("\n\n---\n\n");
|
|
4110
|
+
const contextLine = context ? `
|
|
4111
|
+
The participant provided this context: "${context}"
|
|
4112
|
+
Use it \u2014 don't make them repeat themselves.
|
|
4113
|
+
` : "";
|
|
4114
|
+
return {
|
|
4115
|
+
messages: [
|
|
4116
|
+
{
|
|
4117
|
+
role: "user",
|
|
4118
|
+
content: {
|
|
4119
|
+
type: "text",
|
|
4120
|
+
text: `# ${wf.icon} ${wf.name} Workflow \u2014 Facilitator Mode
|
|
4121
|
+
|
|
4122
|
+
${wf.shortDescription}
|
|
4123
|
+
` + contextLine + `
|
|
4124
|
+
---
|
|
4125
|
+
|
|
4126
|
+
## Facilitator Instructions
|
|
4127
|
+
|
|
4128
|
+
${wf.facilitatorPreamble}
|
|
4129
|
+
|
|
4130
|
+
---
|
|
4131
|
+
|
|
4132
|
+
## Rounds
|
|
4133
|
+
|
|
4134
|
+
${roundsPlan}
|
|
4135
|
+
|
|
4136
|
+
---
|
|
4137
|
+
|
|
4138
|
+
## Error Recovery
|
|
4139
|
+
|
|
4140
|
+
${wf.errorRecovery}
|
|
4141
|
+
|
|
4142
|
+
---
|
|
4143
|
+
|
|
4144
|
+
## KB Output
|
|
4145
|
+
|
|
4146
|
+
When complete, use \`smart-capture\` to create a \`${wf.kbOutputCollection}\` entry.
|
|
4147
|
+
Name template: ${wf.kbOutputTemplate.nameTemplate}
|
|
4148
|
+
Description field: ${wf.kbOutputTemplate.descriptionField}
|
|
4149
|
+
` + kbContext + `
|
|
4150
|
+
|
|
4151
|
+
---
|
|
4152
|
+
|
|
4153
|
+
**BEGIN THE WORKFLOW NOW.** Start with Round 01. Create a Plan first.`
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
]
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
);
|
|
2462
4160
|
}
|
|
2463
4161
|
|
|
2464
4162
|
// src/index.ts
|
|
2465
4163
|
if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
|
|
2466
4164
|
try {
|
|
2467
|
-
const envPath =
|
|
2468
|
-
for (const line of
|
|
4165
|
+
const envPath = resolve3(process.cwd(), ".env.mcp");
|
|
4166
|
+
for (const line of readFileSync3(envPath, "utf-8").split("\n")) {
|
|
2469
4167
|
const trimmed = line.trim();
|
|
2470
4168
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2471
4169
|
const eqIdx = trimmed.indexOf("=");
|
|
@@ -2482,7 +4180,7 @@ var workspaceId;
|
|
|
2482
4180
|
try {
|
|
2483
4181
|
workspaceId = await getWorkspaceId();
|
|
2484
4182
|
} catch (err) {
|
|
2485
|
-
const hint = !process.env.PRODUCTBRAIN_API_KEY && !process.env.CONVEX_SITE_URL ? "\n[MCP] Hint: Set PRODUCTBRAIN_API_KEY in your MCP config env block
|
|
4183
|
+
const hint = !process.env.PRODUCTBRAIN_API_KEY && !process.env.CONVEX_SITE_URL ? "\n[MCP] Hint: Set PRODUCTBRAIN_API_KEY in your MCP config env block, or run `npx productbrain setup`." : "";
|
|
2486
4184
|
process.stderr.write(`[MCP] Startup failed: ${err.message}${hint}
|
|
2487
4185
|
`);
|
|
2488
4186
|
process.exit(1);
|
|
@@ -2500,7 +4198,7 @@ var server = new McpServer2(
|
|
|
2500
4198
|
{
|
|
2501
4199
|
capabilities: { logging: {} },
|
|
2502
4200
|
instructions: [
|
|
2503
|
-
"
|
|
4201
|
+
"Product Brain \u2014 the single source of truth for product knowledge.",
|
|
2504
4202
|
"Terminology, standards, and core data all live here \u2014 no need to check external docs.",
|
|
2505
4203
|
"",
|
|
2506
4204
|
"Terminology & naming: For 'what is X?' or naming questions, fetch `productbrain://terminology`",
|
|
@@ -2533,6 +4231,9 @@ registerLabelTools(server);
|
|
|
2533
4231
|
registerHealthTools(server);
|
|
2534
4232
|
registerVerifyTools(server);
|
|
2535
4233
|
registerSmartCaptureTools(server);
|
|
4234
|
+
registerArchitectureTools(server);
|
|
4235
|
+
registerWorkflowTools(server);
|
|
4236
|
+
registerGitChainTools(server);
|
|
2536
4237
|
registerResources(server);
|
|
2537
4238
|
registerPrompts(server);
|
|
2538
4239
|
var transport = new StdioServerTransport();
|