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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ bootstrap,
4
+ closeAgentSession,
5
+ getAgentSessionId,
6
+ getApiKeyScope,
7
+ getAuditLog,
8
+ getWorkspaceContext,
9
+ getWorkspaceId,
10
+ initAnalytics,
11
+ isSessionOriented,
12
+ mcpCall,
13
+ mcpMutation,
14
+ mcpQuery,
15
+ orphanAgentSession,
16
+ recordSessionActivity,
17
+ recoverSessionState,
18
+ registerSmartCaptureTools,
19
+ requireWriteAccess,
20
+ setSessionOriented,
21
+ shutdownAnalytics,
22
+ startAgentSession,
23
+ trackSessionStarted
24
+ } from "./chunk-YLVX2CON.js";
25
+
2
26
  // src/index.ts
3
27
  import { readFileSync as readFileSync3 } from "fs";
4
28
  import { resolve as resolve3 } from "path";
@@ -7,179 +31,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
31
 
8
32
  // src/tools/knowledge.ts
9
33
  import { z } from "zod";
10
-
11
- // src/analytics.ts
12
- import { userInfo } from "os";
13
- import { PostHog } from "posthog-node";
14
- var client = null;
15
- var distinctId = "anonymous";
16
- var POSTHOG_HOST = "https://eu.i.posthog.com";
17
- function log(msg) {
18
- if (process.env.MCP_DEBUG === "1") {
19
- process.stderr.write(msg);
20
- }
21
- }
22
- function initAnalytics() {
23
- const apiKey = process.env.POSTHOG_MCP_KEY || "";
24
- if (!apiKey) {
25
- log("[MCP-ANALYTICS] No PostHog key \u2014 tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\n");
26
- return;
27
- }
28
- client = new PostHog(apiKey, {
29
- host: POSTHOG_HOST,
30
- flushAt: 1,
31
- flushInterval: 5e3
32
- });
33
- distinctId = process.env.MCP_USER_ID || fallbackDistinctId();
34
- log(`[MCP-ANALYTICS] Initialized \u2014 host=${POSTHOG_HOST} distinctId=${distinctId}
35
- `);
36
- }
37
- function fallbackDistinctId() {
38
- try {
39
- return userInfo().username;
40
- } catch {
41
- return `os-${process.pid}`;
42
- }
43
- }
44
- function trackSessionStarted(workspaceSlug, workspaceId2, serverVersion) {
45
- if (!client) return;
46
- client.capture({
47
- distinctId,
48
- event: "mcp_session_started",
49
- properties: {
50
- workspace_slug: workspaceSlug,
51
- workspace_id: workspaceId2,
52
- server_version: serverVersion,
53
- source: "mcp-server",
54
- $groups: { workspace: workspaceId2 }
55
- }
56
- });
57
- }
58
- function trackToolCall(fn, status, durationMs, workspaceId2, errorMsg) {
59
- const properties = {
60
- tool: fn,
61
- status,
62
- duration_ms: durationMs,
63
- workspace_slug: process.env.WORKSPACE_SLUG ?? "unknown",
64
- source: "mcp-server",
65
- $groups: { workspace: workspaceId2 }
66
- };
67
- if (errorMsg) properties.error = errorMsg;
68
- if (!client) return;
69
- client.capture({
70
- distinctId,
71
- event: "mcp_tool_called",
72
- properties
73
- });
74
- }
75
- async function shutdownAnalytics() {
76
- await client?.shutdown();
77
- }
78
-
79
- // src/client.ts
80
- var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
81
- var cachedWorkspaceId = null;
82
- var cloudMode = false;
83
- var AUDIT_BUFFER_SIZE = 50;
84
- var auditBuffer = [];
85
- function bootstrapCloudMode() {
86
- const pbKey = process.env.PRODUCTBRAIN_API_KEY;
87
- if (pbKey?.startsWith("pb_sk_")) {
88
- const cloudUrl = process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
89
- process.env.CONVEX_SITE_URL ??= cloudUrl;
90
- process.env.MCP_API_KEY ??= pbKey;
91
- cloudMode = true;
92
- }
93
- }
94
- function getEnv(key) {
95
- const value = process.env[key];
96
- if (!value) throw new Error(`${key} environment variable is required`);
97
- return value;
98
- }
99
- function shouldLogAudit(status) {
100
- return status === "error" || process.env.MCP_DEBUG === "1";
101
- }
102
- function audit(fn, status, durationMs, errorMsg) {
103
- const ts = (/* @__PURE__ */ new Date()).toISOString();
104
- const workspace = cachedWorkspaceId ?? "unresolved";
105
- const entry = { ts, fn, workspace, status, durationMs };
106
- if (errorMsg) entry.error = errorMsg;
107
- auditBuffer.push(entry);
108
- if (auditBuffer.length > AUDIT_BUFFER_SIZE) auditBuffer.shift();
109
- trackToolCall(fn, status, durationMs, workspace, errorMsg);
110
- if (!shouldLogAudit(status)) return;
111
- const base = `[MCP-AUDIT] ${ts} fn=${fn} workspace=${workspace} status=${status} duration=${durationMs}ms`;
112
- if (status === "error" && errorMsg) {
113
- process.stderr.write(`${base} error=${JSON.stringify(errorMsg)}
114
- `);
115
- } else {
116
- process.stderr.write(`${base}
117
- `);
118
- }
119
- }
120
- function getAuditLog() {
121
- return auditBuffer;
122
- }
123
- async function mcpCall(fn, args = {}) {
124
- const siteUrl = getEnv("CONVEX_SITE_URL").replace(/\/$/, "");
125
- const apiKey = getEnv("MCP_API_KEY");
126
- const start = Date.now();
127
- let res;
128
- try {
129
- res = await fetch(`${siteUrl}/api/mcp`, {
130
- method: "POST",
131
- headers: {
132
- "Content-Type": "application/json",
133
- Authorization: `Bearer ${apiKey}`
134
- },
135
- body: JSON.stringify({ fn, args })
136
- });
137
- } catch (err) {
138
- audit(fn, "error", Date.now() - start, err.message);
139
- throw new Error(`MCP call "${fn}" network error: ${err.message}`);
140
- }
141
- const json = await res.json();
142
- if (!res.ok || json.error) {
143
- audit(fn, "error", Date.now() - start, json.error);
144
- throw new Error(`MCP call "${fn}" failed (${res.status}): ${json.error ?? "unknown error"}`);
145
- }
146
- audit(fn, "ok", Date.now() - start);
147
- return json.data;
148
- }
149
- async function getWorkspaceId() {
150
- if (cachedWorkspaceId) return cachedWorkspaceId;
151
- if (cloudMode) {
152
- const workspace2 = await mcpCall(
153
- "resolveWorkspace",
154
- { slug: "__cloud__" }
155
- );
156
- if (!workspace2) {
157
- throw new Error("Cloud key is valid but no workspace is associated. Run `npx productbrain setup` again.");
158
- }
159
- cachedWorkspaceId = workspace2._id;
160
- return cachedWorkspaceId;
161
- }
162
- const slug = getEnv("WORKSPACE_SLUG");
163
- const workspace = await mcpCall(
164
- "resolveWorkspace",
165
- { slug }
166
- );
167
- if (!workspace) {
168
- throw new Error(`Workspace with slug "${slug}" not found`);
169
- }
170
- cachedWorkspaceId = workspace._id;
171
- return cachedWorkspaceId;
172
- }
173
- async function mcpQuery(fn, args = {}) {
174
- const workspaceId2 = await getWorkspaceId();
175
- return mcpCall(fn, { ...args, workspaceId: workspaceId2 });
176
- }
177
- async function mcpMutation(fn, args = {}) {
178
- const workspaceId2 = await getWorkspaceId();
179
- return mcpCall(fn, { ...args, workspaceId: workspaceId2 });
180
- }
181
-
182
- // src/tools/knowledge.ts
183
34
  function extractPreview(data, maxLen) {
184
35
  if (!data || typeof data !== "object") return "";
185
36
  const raw = data.description ?? data.canonical ?? data.detail ?? "";
@@ -195,7 +46,7 @@ function registerKnowledgeTools(server2) {
195
46
  annotations: { readOnlyHint: true }
196
47
  },
197
48
  async () => {
198
- const collections = await mcpQuery("kb.listCollections");
49
+ const collections = await mcpQuery("chain.listCollections");
199
50
  if (collections.length === 0) {
200
51
  return { content: [{ type: "text", text: "No collections found in this workspace." }] };
201
52
  }
@@ -230,10 +81,10 @@ ${formatted}` }]
230
81
  async ({ collection, status, tag, label }) => {
231
82
  let entries;
232
83
  if (label) {
233
- entries = await mcpQuery("kb.listEntriesByLabel", { labelSlug: label });
84
+ entries = await mcpQuery("chain.listEntriesByLabel", { labelSlug: label });
234
85
  if (status) entries = entries.filter((e) => e.status === status);
235
86
  } else {
236
- entries = await mcpQuery("kb.listEntries", {
87
+ entries = await mcpQuery("chain.listEntries", {
237
88
  collectionSlug: collection,
238
89
  status,
239
90
  tag
@@ -267,7 +118,7 @@ ${formatted}` }]
267
118
  annotations: { readOnlyHint: true }
268
119
  },
269
120
  async ({ entryId }) => {
270
- const entry = await mcpQuery("kb.getEntry", { entryId });
121
+ const entry = await mcpQuery("chain.getEntry", { entryId });
271
122
  if (!entry) {
272
123
  return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
273
124
  }
@@ -324,21 +175,26 @@ ${formatted}` }]
324
175
  },
325
176
  async ({ entryId, name, status, data, order, autoPublish }) => {
326
177
  try {
327
- const id = await mcpMutation("kb.updateEntry", {
178
+ requireWriteAccess();
179
+ const id = await mcpMutation("chain.updateEntry", {
328
180
  entryId,
329
181
  name,
330
182
  status,
331
183
  data,
332
184
  order,
333
- autoPublish
185
+ autoPublish,
186
+ changedBy: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0
334
187
  });
188
+ await recordSessionActivity({ entryModified: entryId });
189
+ const wsCtx = await getWorkspaceContext();
335
190
  const mode = autoPublish ? "published" : "saved as draft";
336
191
  return {
337
192
  content: [{ type: "text", text: `# Entry Updated
338
193
 
339
194
  **${entryId}** has been ${mode}.
340
195
 
341
- Internal ID: ${id}` }]
196
+ Internal ID: ${id}
197
+ **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
342
198
  };
343
199
  } catch (error) {
344
200
  const msg = error instanceof Error ? error.message : String(error);
@@ -380,8 +236,8 @@ Process criteria (TBD): e.g. 3+ users approved, or 7 days without valid concerns
380
236
  const scope = collection ? ` in \`${collection}\`` : "";
381
237
  await server2.sendLoggingMessage({ level: "info", data: `Searching${scope} for "${query}"...`, logger: "product-os" });
382
238
  const [results, collections] = await Promise.all([
383
- mcpQuery("kb.searchEntries", { query, collectionSlug: collection, status }),
384
- mcpQuery("kb.listCollections")
239
+ mcpQuery("chain.searchEntries", { query, collectionSlug: collection, status }),
240
+ mcpQuery("chain.listCollections")
385
241
  ]);
386
242
  if (results.length === 0) {
387
243
  return { content: [{ type: "text", text: `No results for "${query}"${scope}. Try a broader search or check list-collections for available data.` }] };
@@ -430,7 +286,7 @@ ${footer}` }]
430
286
  annotations: { readOnlyHint: true }
431
287
  },
432
288
  async ({ entryId }) => {
433
- const history = await mcpQuery("kb.listEntryHistory", { entryId });
289
+ const history = await mcpQuery("chain.listEntryHistory", { entryId });
434
290
  if (history.length === 0) {
435
291
  return { content: [{ type: "text", text: `No history found for \`${entryId}\`.` }] };
436
292
  }
@@ -459,18 +315,73 @@ ${formatted}` }]
459
315
  annotations: { destructiveHint: false }
460
316
  },
461
317
  async ({ from, to, type }) => {
462
- await mcpMutation("kb.createEntryRelation", {
318
+ requireWriteAccess();
319
+ await mcpMutation("chain.createEntryRelation", {
463
320
  fromEntryId: from,
464
321
  toEntryId: to,
465
322
  type
466
323
  });
324
+ await recordSessionActivity({ relationCreated: true });
325
+ const wsCtx = await getWorkspaceContext();
467
326
  return {
468
327
  content: [{ type: "text", text: `# Relation Created
469
328
 
470
- **${from}** \u2014[${type}]\u2192 **${to}**` }]
329
+ **${from}** \u2014[${type}]\u2192 **${to}**
330
+ **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
471
331
  };
472
332
  }
473
333
  );
334
+ server2.registerTool(
335
+ "batch-relate",
336
+ {
337
+ title: "Batch Link Entries",
338
+ description: "Create multiple relations in one call. Accepts an array of {from, to, type} objects. Use after suggest-links to apply several suggestions at once instead of calling relate-entries repeatedly.\n\nEach relation is created independently \u2014 if one fails, the others still succeed. Returns a summary of created and failed relations.",
339
+ inputSchema: {
340
+ relations: z.array(z.object({
341
+ from: z.string().describe("Source entry ID"),
342
+ to: z.string().describe("Target entry ID"),
343
+ type: z.string().describe("Relation type")
344
+ })).min(1).max(20).describe("Array of relations to create")
345
+ },
346
+ annotations: { destructiveHint: false }
347
+ },
348
+ async ({ relations }) => {
349
+ requireWriteAccess();
350
+ const results = [];
351
+ for (const rel of relations) {
352
+ try {
353
+ await mcpMutation("chain.createEntryRelation", {
354
+ fromEntryId: rel.from,
355
+ toEntryId: rel.to,
356
+ type: rel.type
357
+ });
358
+ results.push({ ...rel, ok: true });
359
+ } catch (e) {
360
+ results.push({ ...rel, ok: false, error: e.message || "Unknown error" });
361
+ }
362
+ }
363
+ const created = results.filter((r) => r.ok);
364
+ const failed = results.filter((r) => !r.ok);
365
+ const lines = [`# Batch Link Results
366
+ `];
367
+ lines.push(`**${created.length}** created, **${failed.length}** failed out of ${relations.length} total.
368
+ `);
369
+ if (created.length > 0) {
370
+ lines.push("## Created");
371
+ for (const r of created) {
372
+ lines.push(`- **${r.from}** \u2014[${r.type}]\u2192 **${r.to}**`);
373
+ }
374
+ }
375
+ if (failed.length > 0) {
376
+ lines.push("");
377
+ lines.push("## Failed");
378
+ for (const r of failed) {
379
+ lines.push(`- **${r.from}** \u2192 **${r.to}** (${r.type}): _${r.error}_`);
380
+ }
381
+ }
382
+ return { content: [{ type: "text", text: lines.join("\n") }] };
383
+ }
384
+ );
474
385
  server2.registerTool(
475
386
  "find-related",
476
387
  {
@@ -483,11 +394,11 @@ ${formatted}` }]
483
394
  annotations: { readOnlyHint: true }
484
395
  },
485
396
  async ({ entryId, direction }) => {
486
- const relations = await mcpQuery("kb.listEntryRelations", { entryId });
397
+ const relations = await mcpQuery("chain.listEntryRelations", { entryId });
487
398
  if (relations.length === 0) {
488
399
  return { content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relate-entries to create connections.` }] };
489
400
  }
490
- const sourceEntry = await mcpQuery("kb.getEntry", { entryId });
401
+ const sourceEntry = await mcpQuery("chain.getEntry", { entryId });
491
402
  if (!sourceEntry) {
492
403
  return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
493
404
  }
@@ -502,12 +413,12 @@ ${formatted}` }]
502
413
  }
503
414
  const otherEntries = /* @__PURE__ */ new Map();
504
415
  for (const id of otherIds) {
505
- const entry = await mcpQuery("kb.getEntry", { id });
416
+ const entry = await mcpQuery("chain.getEntry", { id });
506
417
  if (entry) {
507
418
  otherEntries.set(entry._id, { entryId: entry.entryId, name: entry.name, collectionId: entry.collectionId });
508
419
  }
509
420
  }
510
- const collections = await mcpQuery("kb.listCollections");
421
+ const collections = await mcpQuery("chain.listCollections");
511
422
  const collMap = /* @__PURE__ */ new Map();
512
423
  for (const c of collections) collMap.set(c._id, c.slug);
513
424
  const lines = [`# Relations for ${entryId}: ${sourceEntry.name}`, ""];
@@ -549,19 +460,67 @@ ${formatted}` }]
549
460
  "gather-context",
550
461
  {
551
462
  title: "Gather Context",
552
- description: "Assemble knowledge context in one call. Two modes:\n\n1. **By entry** (entryId): Traverse the knowledge graph around a specific entry. Returns all related entries grouped by collection.\n2. **By task** (task): Auto-load relevant domain knowledge for a natural-language task. Searches the chain, traverses the graph, and returns ranked entries with confidence scores.\n\nUse mode 1 when you have a specific entry ID. Use mode 2 at the start of a conversation to ground the agent in domain context before writing code or making recommendations.",
463
+ description: "Assemble knowledge context in one call. Three modes:\n\n1. **By entry** (entryId): Traverse the knowledge graph around a specific entry. Returns all related entries grouped by collection.\n2. **By task** (task): Auto-load relevant domain knowledge for a natural-language task. Searches the chain, traverses the graph, and returns ranked entries with confidence scores.\n3. **Graph mode** (entryId + mode='graph'): Enhanced graph traversal with provenance \u2014 each entry includes the full path that led to it (via what relations, from what starting point).\n\nUse mode 1/3 when you have a specific entry ID. Use mode 2 at the start of a conversation to ground the agent in domain context before writing code or making recommendations.",
553
464
  inputSchema: {
554
465
  entryId: z.string().optional().describe("Entry ID for graph traversal, e.g. 'FEAT-001', 'GT-019'"),
555
466
  task: z.string().optional().describe("Natural-language task description for auto-loading relevant context"),
467
+ mode: z.enum(["search", "graph"]).default("search").optional().describe("'search' (default, backward-compatible) or 'graph' (enhanced with provenance paths)"),
556
468
  maxHops: z.number().min(1).max(3).default(2).describe("How many relation hops to traverse (1=direct only, 2=default, 3=wide net)"),
557
469
  maxResults: z.number().min(1).max(25).default(10).optional().describe("Max entries to return in task mode (default 10)")
558
470
  },
559
471
  annotations: { readOnlyHint: true }
560
472
  },
561
- async ({ entryId, task, maxHops, maxResults }) => {
473
+ async ({ entryId, task, mode, maxHops, maxResults }) => {
562
474
  if (!entryId && !task) {
563
475
  return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
564
476
  }
477
+ if (entryId && mode === "graph") {
478
+ const result2 = await mcpQuery("chain.graphGatherContext", {
479
+ entryId,
480
+ maxHops: maxHops ?? 2
481
+ });
482
+ if (!result2?.root) {
483
+ return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
484
+ }
485
+ if (result2.context.length === 0) {
486
+ return {
487
+ content: [{
488
+ type: "text",
489
+ text: `# Context for ${result2.root.entryId}: ${result2.root.name}
490
+
491
+ _No relations found._ This entry is not yet connected to the knowledge graph.
492
+
493
+ Use \`suggest-links\` to discover potential connections.`
494
+ }]
495
+ };
496
+ }
497
+ const byCollection2 = /* @__PURE__ */ new Map();
498
+ for (const entry of result2.context) {
499
+ const key = entry.collectionName;
500
+ if (!byCollection2.has(key)) byCollection2.set(key, []);
501
+ byCollection2.get(key).push(entry);
502
+ }
503
+ const lines2 = [
504
+ `# Context for ${result2.root.entryId}: ${result2.root.name} (graph mode)`,
505
+ `_${result2.totalFound} related entries across ${byCollection2.size} collections (${result2.hopsTraversed} hops)_`,
506
+ ""
507
+ ];
508
+ for (const [collName, entries] of byCollection2) {
509
+ lines2.push(`## ${collName} (${entries.length})`);
510
+ for (const e of entries) {
511
+ const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
512
+ const id = e.entryId ? `${e.entryId}: ` : "";
513
+ const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
514
+ const provenancePath = e.provenance && e.provenance.length > 1 ? `
515
+ _Path: ${e.provenance.map((p) => `${p.entryId ?? p.name} [${p.relationType}]`).join(" \u2192 ")}_` : "";
516
+ const preview = e.preview ? `
517
+ ${e.preview.substring(0, 120)}` : "";
518
+ lines2.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}${provenancePath}${preview}`);
519
+ }
520
+ lines2.push("");
521
+ }
522
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
523
+ }
565
524
  if (task && !entryId) {
566
525
  await server2.sendLoggingMessage({
567
526
  level: "info",
@@ -569,7 +528,7 @@ ${formatted}` }]
569
528
  logger: "product-brain"
570
529
  });
571
530
  const searchTerms = task.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
572
- const searchResults = await mcpQuery("kb.searchEntries", {
531
+ const searchResults = await mcpQuery("chain.searchEntries", {
573
532
  query: searchTerms
574
533
  });
575
534
  if (!searchResults || searchResults.length === 0) {
@@ -602,7 +561,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
602
561
  });
603
562
  if (hit.entryId) {
604
563
  try {
605
- const graph = await mcpQuery("kb.gatherContext", {
564
+ const graph = await mcpQuery("chain.gatherContext", {
606
565
  entryId: hit.entryId,
607
566
  maxHops: maxHops ?? 2
608
567
  });
@@ -652,7 +611,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
652
611
  lines2.push(`_Use \`get-entry\` for full details on any entry._`);
653
612
  return { content: [{ type: "text", text: lines2.join("\n") }] };
654
613
  }
655
- const result = await mcpQuery("kb.gatherContext", { entryId, maxHops });
614
+ const result = await mcpQuery("chain.gatherContext", { entryId, maxHops });
656
615
  if (!result?.root) {
657
616
  return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
658
617
  }
@@ -696,61 +655,108 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
696
655
  "suggest-links",
697
656
  {
698
657
  title: "Suggest Links",
699
- description: "Discover potential connections for an entry by scanning the chain for related content. Returns ranked suggestions based on text similarity \u2014 review them and use relate-entries to create the ones that make sense.\n\nThis is a discovery tool, not auto-linking. Always review suggestions before linking.",
658
+ description: "Discover potential connections for an entry using graph-aware intelligence. Accepts an entry ID (e.g. 'FEAT-001') OR an entry name (e.g. 'Chain Intelligence'). Traverses the knowledge graph (2-hop BFS) and scores candidates by graph distance, text similarity, and relation type fit. Also finds text-similar entries not yet in the graph.\n\nReturns ranked suggestions with confidence scores, recommended relation types, and graph-path reasoning explaining WHY each connection matters.\n\nThis is a discovery tool \u2014 review suggestions and use relate-entries to create the ones that make sense.",
700
659
  inputSchema: {
701
- entryId: z.string().describe("Entry ID to find suggestions for, e.g. 'FEAT-001'"),
702
- limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return")
660
+ entryId: z.string().describe("Entry ID (e.g. 'FEAT-001') or entry name (e.g. 'Chain Intelligence') to find suggestions for"),
661
+ limit: z.number().min(1).max(20).default(10).describe("Max number of suggestions to return"),
662
+ depth: z.number().min(1).max(3).default(2).describe("Graph traversal depth: 1=direct neighbors only, 2=default, 3=wide net")
703
663
  },
704
664
  annotations: { readOnlyHint: true }
705
665
  },
706
- async ({ entryId, limit }) => {
707
- const entry = await mcpQuery("kb.getEntry", { entryId });
708
- if (!entry) {
709
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
710
- }
711
- const searchTerms = [entry.name];
712
- if (entry.data?.description) searchTerms.push(entry.data.description);
713
- if (entry.data?.canonical) searchTerms.push(entry.data.canonical);
714
- if (entry.data?.rationale) searchTerms.push(entry.data.rationale);
715
- if (entry.data?.rule) searchTerms.push(entry.data.rule);
716
- const queryText = searchTerms.join(" ").replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
717
- if (!queryText) {
718
- return { content: [{ type: "text", text: `Entry \`${entryId}\` has too little text content to generate suggestions.` }] };
719
- }
720
- const results = await mcpQuery("kb.searchEntries", { query: queryText });
721
- if (!results || results.length === 0) {
722
- return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\`. The chain may need more entries.` }] };
723
- }
724
- const existingRelations = await mcpQuery("kb.listEntryRelations", { entryId });
725
- const relatedIds = new Set(
726
- existingRelations.flatMap((r) => [r.fromId, r.toId])
727
- );
728
- const collections = await mcpQuery("kb.listCollections");
729
- const collMap = /* @__PURE__ */ new Map();
730
- for (const c of collections) collMap.set(c._id, c.slug);
731
- const suggestions = results.filter((r) => r._id !== entry._id && !relatedIds.has(r._id)).slice(0, limit).map((r) => ({
732
- entryId: r.entryId,
733
- name: r.name,
734
- collection: collMap.get(r.collectionId) ?? "unknown",
735
- preview: extractPreview(r.data, 80)
736
- }));
737
- if (suggestions.length === 0) {
738
- return { content: [{ type: "text", text: `No new link suggestions for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
666
+ async ({ entryId, limit, depth }) => {
667
+ const result = await mcpQuery("chain.graphSuggestLinks", {
668
+ entryId,
669
+ maxHops: depth ?? 2,
670
+ limit: limit ?? 10
671
+ });
672
+ if (!result || !result.suggestions || result.suggestions.length === 0) {
673
+ return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
739
674
  }
675
+ const resolved = result.resolvedEntry;
676
+ const resolvedLabel = resolved ? `\`${resolved.entryId ?? resolved.name}\` (${resolved.name})` : `\`${entryId}\``;
677
+ const suggestions = result.suggestions;
678
+ const graphCount = suggestions.filter((s) => s.graphDistance > 0).length;
679
+ const textCount = suggestions.filter((s) => s.graphDistance === -1).length;
680
+ const sourceId = resolved?.entryId ?? entryId;
740
681
  const lines = [
741
- `# Link Suggestions for ${entryId}: ${entry.name}`,
742
- `_${suggestions.length} potential connections found. Review and use \`relate-entries\` to create the ones that make sense._`,
682
+ `# Link Suggestions for ${resolvedLabel}`,
683
+ `_${suggestions.length} potential connections (${graphCount} via graph, ${textCount} via text similarity)._`,
743
684
  ""
744
685
  ];
745
- for (let i = 0; i < suggestions.length; i++) {
746
- const s = suggestions[i];
747
- const preview = s.preview ? ` \u2014 ${s.preview}` : "";
748
- lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
686
+ const top3 = suggestions.slice(0, 3);
687
+ lines.push("## Quick Wins (top 3)");
688
+ const batchArgs = [];
689
+ for (const s of top3) {
690
+ const tid = s.entryId ?? "(no ID)";
691
+ const relType = s.recommendedRelationType || "related_to";
692
+ lines.push(`- **${tid}**: ${s.name} [${s.collectionSlug}] \u2014 ${s.score}/100 \u2192 \`${relType}\``);
693
+ if (s.entryId) batchArgs.push(`{from:"${sourceId}",to:"${tid}",type:"${relType}"}`);
749
694
  }
750
695
  lines.push("");
751
- lines.push("**To link:** `relate-entries from='FEAT-001' to='GT-019' type='defines_term_for'`");
696
+ if (batchArgs.length > 0) {
697
+ lines.push(`**Link all 3:** \`batch-relate relations=[${batchArgs.join(",")}]\``);
698
+ lines.push("");
699
+ }
700
+ if (suggestions.length > 3) {
701
+ lines.push("## All Suggestions");
702
+ for (let i = 0; i < suggestions.length; i++) {
703
+ const s = suggestions[i];
704
+ const scoreBar = "\u2588".repeat(Math.round(s.score / 10)) + "\u2591".repeat(10 - Math.round(s.score / 10));
705
+ const hopLabel = s.graphDistance > 0 ? `${s.graphDistance}-hop` : "text";
706
+ const recType = s.recommendedRelationType !== "related_to" ? ` \u2192 \`${s.recommendedRelationType}\`` : "";
707
+ lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collectionSlug}] (${hopLabel})`);
708
+ lines.push(` ${scoreBar} ${s.score}/100${recType}`);
709
+ if (s.preview) {
710
+ lines.push(` ${s.preview}`);
711
+ }
712
+ lines.push(` _${s.reasoning}_`);
713
+ }
714
+ }
752
715
  lines.push("");
753
- lines.push("_Recommended relation types: governs, defines_term_for, belongs_to, informs, surfaces_tension_in, related_to, depends_on_");
716
+ lines.push(`**To link one:** \`relate-entries from="${sourceId}" to="{target_id}" type="{type}"\``);
717
+ return { content: [{ type: "text", text: lines.join("\n") }] };
718
+ }
719
+ );
720
+ server2.registerTool(
721
+ "commit-entry",
722
+ {
723
+ title: "Commit Entry to Chain",
724
+ description: "Promote a draft entry to committed status (SSOT on the Chain). Runs a keyword contradiction check against governance entries before committing. Warnings are advisory \u2014 they do not block the commit.\n\nUse after capture + suggest-links + relate-entries to finalize an entry.",
725
+ inputSchema: {
726
+ entryId: z.string().describe("Entry ID to commit, e.g. 'TEN-abc123', 'GT-019'")
727
+ },
728
+ annotations: { destructiveHint: false }
729
+ },
730
+ async ({ entryId }) => {
731
+ requireWriteAccess();
732
+ const { runContradictionCheck } = await import("./smart-capture-QGIC5N47.js");
733
+ const entry = await mcpQuery("chain.getEntry", { entryId });
734
+ if (!entry) {
735
+ return {
736
+ content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }]
737
+ };
738
+ }
739
+ const descField = entry.data?.description ?? entry.data?.canonical ?? entry.data?.rationale ?? "";
740
+ const warnings = await runContradictionCheck(entry.name, descField);
741
+ if (warnings.length > 0) {
742
+ await recordSessionActivity({ contradictionWarning: true });
743
+ }
744
+ const result = await mcpMutation("chain.commitEntry", { entryId });
745
+ await recordSessionActivity({ entryModified: entryId });
746
+ const wsCtx = await getWorkspaceContext();
747
+ const lines = [
748
+ `# Committed: ${entryId}`,
749
+ `**${entry.name}** promoted to SSOT on the Chain.`,
750
+ `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
751
+ ];
752
+ if (warnings.length > 0) {
753
+ lines.push("");
754
+ lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
755
+ for (const w of warnings) {
756
+ lines.push(`- ${w.name} (${w.collection}, ${w.entryId}) \u2014 has 'governs' relation to ${w.governsCount} entries`);
757
+ }
758
+ lines.push("Run gather-context on these entries before committing.");
759
+ }
754
760
  return { content: [{ type: "text", text: lines.join("\n") }] };
755
761
  }
756
762
  );
@@ -778,7 +784,7 @@ function registerLabelTools(server2) {
778
784
  },
779
785
  async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
780
786
  if (action === "list") {
781
- const labels = await mcpQuery("kb.listLabels");
787
+ const labels = await mcpQuery("chain.listLabels");
782
788
  if (labels.length === 0) {
783
789
  return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
784
790
  }
@@ -813,26 +819,26 @@ function registerLabelTools(server2) {
813
819
  }
814
820
  let parentId;
815
821
  if (parentSlug) {
816
- const labels = await mcpQuery("kb.listLabels");
822
+ const labels = await mcpQuery("chain.listLabels");
817
823
  const parent = labels.find((l) => l.slug === parentSlug);
818
824
  if (!parent) {
819
825
  return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
820
826
  }
821
827
  parentId = parent._id;
822
828
  }
823
- await mcpMutation("kb.createLabel", { slug, name, color, description, parentId, isGroup, order });
829
+ await mcpMutation("chain.createLabel", { slug, name, color, description, parentId, isGroup, order });
824
830
  return { content: [{ type: "text", text: `# Label Created
825
831
 
826
832
  **${name}** (\`${slug}\`)` }] };
827
833
  }
828
834
  if (action === "update") {
829
- await mcpMutation("kb.updateLabel", { slug, name, color, description, isGroup, order });
835
+ await mcpMutation("chain.updateLabel", { slug, name, color, description, isGroup, order });
830
836
  return { content: [{ type: "text", text: `# Label Updated
831
837
 
832
838
  \`${slug}\` has been updated.` }] };
833
839
  }
834
840
  if (action === "delete") {
835
- await mcpMutation("kb.deleteLabel", { slug });
841
+ await mcpMutation("chain.deleteLabel", { slug });
836
842
  return { content: [{ type: "text", text: `# Label Deleted
837
843
 
838
844
  \`${slug}\` removed from all entries and deleted.` }] };
@@ -842,10 +848,10 @@ function registerLabelTools(server2) {
842
848
  return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
843
849
  }
844
850
  if (action === "apply") {
845
- await mcpMutation("kb.applyLabel", { entryId, labelSlug: slug });
851
+ await mcpMutation("chain.applyLabel", { entryId, labelSlug: slug });
846
852
  return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
847
853
  }
848
- await mcpMutation("kb.removeLabel", { entryId, labelSlug: slug });
854
+ await mcpMutation("chain.removeLabel", { entryId, labelSlug: slug });
849
855
  return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
850
856
  }
851
857
  return { content: [{ type: "text", text: "Unknown action." }] };
@@ -856,23 +862,23 @@ function registerLabelTools(server2) {
856
862
  // src/tools/health.ts
857
863
  import { z as z3 } from "zod";
858
864
  var CALL_CATEGORIES = {
859
- "kb.getEntry": "read",
860
- "kb.listEntries": "read",
861
- "kb.listEntryHistory": "read",
862
- "kb.listEntryRelations": "read",
863
- "kb.listEntriesByLabel": "read",
864
- "kb.searchEntries": "search",
865
- "kb.createEntry": "write",
866
- "kb.updateEntry": "write",
867
- "kb.createEntryRelation": "write",
868
- "kb.applyLabel": "label",
869
- "kb.removeLabel": "label",
870
- "kb.createLabel": "label",
871
- "kb.updateLabel": "label",
872
- "kb.deleteLabel": "label",
873
- "kb.listCollections": "meta",
874
- "kb.getCollection": "meta",
875
- "kb.listLabels": "meta",
865
+ "chain.getEntry": "read",
866
+ "chain.listEntries": "read",
867
+ "chain.listEntryHistory": "read",
868
+ "chain.listEntryRelations": "read",
869
+ "chain.listEntriesByLabel": "read",
870
+ "chain.searchEntries": "search",
871
+ "chain.createEntry": "write",
872
+ "chain.updateEntry": "write",
873
+ "chain.createEntryRelation": "write",
874
+ "chain.applyLabel": "label",
875
+ "chain.removeLabel": "label",
876
+ "chain.createLabel": "label",
877
+ "chain.updateLabel": "label",
878
+ "chain.deleteLabel": "label",
879
+ "chain.listCollections": "meta",
880
+ "chain.getCollection": "meta",
881
+ "chain.listLabels": "meta",
876
882
  "resolveWorkspace": "meta"
877
883
  };
878
884
  function categorize(fn) {
@@ -884,23 +890,23 @@ function formatDuration(ms) {
884
890
  const secs = Math.round(ms % 6e4 / 1e3);
885
891
  return `${mins}m ${secs}s`;
886
892
  }
887
- function buildSessionSummary(log2) {
888
- if (log2.length === 0) return "";
893
+ function buildSessionSummary(log) {
894
+ if (log.length === 0) return "";
889
895
  const byCategory = /* @__PURE__ */ new Map();
890
896
  let errorCount = 0;
891
897
  let writeCreates = 0;
892
898
  let writeUpdates = 0;
893
- for (const entry of log2) {
899
+ for (const entry of log) {
894
900
  const cat = categorize(entry.fn);
895
901
  if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
896
902
  const fnCounts = byCategory.get(cat);
897
903
  fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
898
904
  if (entry.status === "error") errorCount++;
899
- if (entry.fn === "kb.createEntry" && entry.status === "ok") writeCreates++;
900
- if (entry.fn === "kb.updateEntry" && entry.status === "ok") writeUpdates++;
905
+ if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
906
+ if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
901
907
  }
902
- const firstTs = new Date(log2[0].ts).getTime();
903
- const lastTs = new Date(log2[log2.length - 1].ts).getTime();
908
+ const firstTs = new Date(log[0].ts).getTime();
909
+ const lastTs = new Date(log[log.length - 1].ts).getTime();
904
910
  const duration = formatDuration(lastTs - firstTs);
905
911
  const lines = [`# Session Summary (${duration})
906
912
  `];
@@ -915,7 +921,7 @@ function buildSessionSummary(log2) {
915
921
  const fnCounts = byCategory.get(cat);
916
922
  if (!fnCounts || fnCounts.size === 0) continue;
917
923
  const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
918
- const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("kb.", "")} x${count}`).join(", ");
924
+ const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
919
925
  lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
920
926
  }
921
927
  lines.push(`- **Errors:** ${errorCount}`);
@@ -938,33 +944,40 @@ function registerHealthTools(server2) {
938
944
  async () => {
939
945
  const start = Date.now();
940
946
  const errors = [];
941
- let workspaceId2;
947
+ let workspaceId;
942
948
  try {
943
- workspaceId2 = await getWorkspaceId();
949
+ workspaceId = await getWorkspaceId();
944
950
  } catch (e) {
945
951
  errors.push(`Workspace resolution failed: ${e.message}`);
946
952
  }
947
953
  let collections = [];
948
954
  try {
949
- collections = await mcpQuery("kb.listCollections");
955
+ collections = await mcpQuery("chain.listCollections");
950
956
  } catch (e) {
951
957
  errors.push(`Collection fetch failed: ${e.message}`);
952
958
  }
953
959
  let totalEntries = 0;
954
960
  if (collections.length > 0) {
955
961
  try {
956
- const entries = await mcpQuery("kb.listEntries", {});
962
+ const entries = await mcpQuery("chain.listEntries", {});
957
963
  totalEntries = entries.length;
958
964
  } catch (e) {
959
965
  errors.push(`Entry count failed: ${e.message}`);
960
966
  }
961
967
  }
968
+ let wsCtx = null;
969
+ try {
970
+ wsCtx = await getWorkspaceContext();
971
+ } catch {
972
+ }
962
973
  const durationMs = Date.now() - start;
963
974
  const healthy = errors.length === 0;
964
975
  const lines = [
965
976
  `# ${healthy ? "Healthy" : "Degraded"}`,
966
977
  "",
967
- `**Workspace:** ${workspaceId2 ?? "unresolved"}`,
978
+ `**Workspace:** ${workspaceId ?? "unresolved"}`,
979
+ `**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
980
+ `**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
968
981
  `**Collections:** ${collections.length}`,
969
982
  `**Entries:** ${totalEntries}`,
970
983
  `**Latency:** ${durationMs}ms`
@@ -978,6 +991,224 @@ function registerHealthTools(server2) {
978
991
  return { content: [{ type: "text", text: lines.join("\n") }] };
979
992
  }
980
993
  );
994
+ server2.registerTool(
995
+ "whoami",
996
+ {
997
+ title: "Session Identity",
998
+ description: "Returns the current workspace and auth context for this MCP session. Use at the start of a session to confirm you're operating on the right workspace before making writes.",
999
+ annotations: { readOnlyHint: true }
1000
+ },
1001
+ async () => {
1002
+ const ctx = await getWorkspaceContext();
1003
+ const lines = [
1004
+ `# Session Identity`,
1005
+ "",
1006
+ `**Workspace ID:** ${ctx.workspaceId}`,
1007
+ `**Workspace Slug:** ${ctx.workspaceSlug}`,
1008
+ `**Workspace Name:** ${ctx.workspaceName}`
1009
+ ];
1010
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1011
+ }
1012
+ );
1013
+ server2.registerTool(
1014
+ "workspace-status",
1015
+ {
1016
+ title: "Workspace Status",
1017
+ description: "The 'Monday morning' tool \u2014 returns workspace readiness score, specific gaps with suggested next actions, and workspace stats (entries, relations, orphans, drafts).\n\nUse this to understand how ready the workspace is, what foundational knowledge is missing, and what to work on next. Great for starting a session or planning knowledge work.",
1018
+ annotations: { readOnlyHint: true }
1019
+ },
1020
+ async () => {
1021
+ const result = await mcpQuery("chain.workspaceReadiness");
1022
+ const { score, totalChecks, passedChecks, checks, gaps, stats } = result;
1023
+ const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
1024
+ const lines = [
1025
+ `# Workspace Readiness: ${score}%`,
1026
+ `${scoreBar} ${passedChecks}/${totalChecks} requirements met`,
1027
+ "",
1028
+ "## Stats",
1029
+ `- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
1030
+ `- **Relations:** ${stats.totalRelations}`,
1031
+ `- **Collections:** ${stats.collectionCount}`,
1032
+ `- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
1033
+ ""
1034
+ ];
1035
+ if (gaps.length > 0) {
1036
+ lines.push("## Gaps (action required)");
1037
+ for (const gap of gaps) {
1038
+ lines.push(`- [ ] **${gap.label}** \u2014 ${gap.description}`);
1039
+ lines.push(` ${gap.current}/${gap.required} | _${gap.guidance}_`);
1040
+ }
1041
+ lines.push("");
1042
+ }
1043
+ const passed = checks.filter((c) => c.passed);
1044
+ if (passed.length > 0) {
1045
+ lines.push("## Passed");
1046
+ for (const check of passed) {
1047
+ lines.push(`- [x] **${check.label}** (${check.current}/${check.required})`);
1048
+ }
1049
+ }
1050
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1051
+ }
1052
+ );
1053
+ server2.registerTool(
1054
+ "orient",
1055
+ {
1056
+ title: "Orient \u2014 Start Here",
1057
+ description: "The single entry point for starting a session. Returns everything needed in one call: workspace identity, readiness score, prior session summaries, architecture/governance constraints, open tensions, and suggested next actions.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.",
1058
+ annotations: { readOnlyHint: true }
1059
+ },
1060
+ async () => {
1061
+ const errors = [];
1062
+ const agentSessionId = getAgentSessionId();
1063
+ let wsCtx = null;
1064
+ try {
1065
+ wsCtx = await getWorkspaceContext();
1066
+ } catch (e) {
1067
+ errors.push(`Workspace: ${e.message}`);
1068
+ }
1069
+ let priorSessions = [];
1070
+ let maxSessions = 3;
1071
+ if (wsCtx) {
1072
+ try {
1073
+ priorSessions = await mcpQuery("agent.recentSessions", { limit: maxSessions });
1074
+ } catch {
1075
+ }
1076
+ }
1077
+ let constraintEntries = [];
1078
+ let maxConstraints = 8;
1079
+ try {
1080
+ const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
1081
+ mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
1082
+ mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
1083
+ mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
1084
+ ]);
1085
+ const committed = [
1086
+ ...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
1087
+ ...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
1088
+ ...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
1089
+ ];
1090
+ constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, maxConstraints);
1091
+ } catch {
1092
+ }
1093
+ let openTensions = [];
1094
+ try {
1095
+ const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
1096
+ openTensions = (tensions ?? []).filter((e) => e.status === "draft");
1097
+ } catch {
1098
+ }
1099
+ let readiness = null;
1100
+ try {
1101
+ readiness = await mcpQuery("chain.workspaceReadiness");
1102
+ } catch (e) {
1103
+ errors.push(`Readiness: ${e.message}`);
1104
+ }
1105
+ const TOKEN_LIMIT = 6e3;
1106
+ const CHAR_PER_TOKEN = 4;
1107
+ const CHAR_LIMIT = TOKEN_LIMIT * CHAR_PER_TOKEN;
1108
+ const lines = [];
1109
+ let charCount = 0;
1110
+ function addLine(line) {
1111
+ lines.push(line);
1112
+ charCount += line.length + 1;
1113
+ }
1114
+ function addLines(ls) {
1115
+ for (const l of ls) addLine(l);
1116
+ }
1117
+ if (wsCtx) {
1118
+ addLine(`# ${wsCtx.workspaceName}`);
1119
+ addLine(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
1120
+ } else {
1121
+ addLine("# Workspace");
1122
+ addLine("_Could not resolve workspace._");
1123
+ }
1124
+ addLine("");
1125
+ if (readiness) {
1126
+ const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
1127
+ addLine(`## Readiness: ${readiness.score}%`);
1128
+ addLine(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
1129
+ addLine("");
1130
+ if (readiness.gaps && readiness.gaps.length > 0) {
1131
+ addLine("### Gaps");
1132
+ for (const gap of readiness.gaps) {
1133
+ addLine(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
1134
+ }
1135
+ addLine("");
1136
+ }
1137
+ }
1138
+ if (openTensions.length > 0) {
1139
+ addLine(`## Open Tensions (${openTensions.length})`);
1140
+ for (const t of openTensions) {
1141
+ const id = t.entryId ?? "(no ID)";
1142
+ const prio = t.data?.priority ?? "";
1143
+ addLine(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
1144
+ }
1145
+ addLine("");
1146
+ }
1147
+ if (charCount > CHAR_LIMIT * 0.6) {
1148
+ maxSessions = 2;
1149
+ maxConstraints = 5;
1150
+ priorSessions = priorSessions.slice(0, maxSessions);
1151
+ constraintEntries = constraintEntries.slice(0, maxConstraints);
1152
+ }
1153
+ if (priorSessions.length > 0) {
1154
+ addLine(`## Prior Agent Sessions (last ${priorSessions.length})`);
1155
+ for (const s of priorSessions) {
1156
+ const date = new Date(s.startedAt).toISOString().split("T")[0];
1157
+ const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
1158
+ const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
1159
+ const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
1160
+ const gates = (s.gateFailures ?? 0) > 0 ? `, ${s.gateFailures} gate failures` : "";
1161
+ const warns = (s.contradictionWarnings ?? 0) > 0 ? `, ${s.contradictionWarnings} contradiction warnings` : "";
1162
+ addLine(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}${gates}${warns}`);
1163
+ }
1164
+ addLine("");
1165
+ }
1166
+ if (constraintEntries.length > 0) {
1167
+ addLine(`## Active Constraints (${constraintEntries.length})`);
1168
+ addLine("_Architecture, business rules, and decisions that govern this workspace._");
1169
+ addLine("");
1170
+ for (const c of constraintEntries) {
1171
+ const id = c.entryId ?? "(no ID)";
1172
+ const col = c.collectionSlug ?? "";
1173
+ const desc = c.data?.description ?? c.data?.rationale ?? "";
1174
+ const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
1175
+ addLine(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
1176
+ }
1177
+ addLine("");
1178
+ }
1179
+ if (errors.length > 0) {
1180
+ addLine("## Errors");
1181
+ for (const err of errors) addLine(`- ${err}`);
1182
+ addLine("");
1183
+ }
1184
+ if (agentSessionId) {
1185
+ try {
1186
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
1187
+ setSessionOriented(true);
1188
+ const scope = getApiKeyScope();
1189
+ const sessionInfo = `Session ${agentSessionId}.`;
1190
+ const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
1191
+ const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
1192
+ let sessionSummary = "";
1193
+ if (priorSessions.length > 0) {
1194
+ const lastSession = priorSessions[0];
1195
+ const created = Array.isArray(lastSession.entriesCreated) ? lastSession.entriesCreated.length : lastSession.entriesCreated ?? 0;
1196
+ const modified = Array.isArray(lastSession.entriesModified) ? lastSession.entriesModified.length : lastSession.entriesModified ?? 0;
1197
+ sessionSummary = `Last session: ${created} created, ${modified} modified.`;
1198
+ }
1199
+ addLine("---");
1200
+ addLine(`Orientation complete. ${sessionInfo} Write tools now available. ${readinessInfo} ${tensionInfo} ${sessionSummary}`);
1201
+ } catch {
1202
+ addLine("---");
1203
+ addLine("_Warning: Could not mark session as oriented. Write tools may be restricted._");
1204
+ }
1205
+ } else {
1206
+ addLine("---");
1207
+ addLine("_No active agent session. Call `agent-start` to begin a tracked session._");
1208
+ }
1209
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1210
+ }
1211
+ );
981
1212
  server2.registerTool(
982
1213
  "mcp-audit",
983
1214
  {
@@ -989,13 +1220,13 @@ function registerHealthTools(server2) {
989
1220
  annotations: { readOnlyHint: true }
990
1221
  },
991
1222
  async ({ limit }) => {
992
- const log2 = getAuditLog();
993
- const recent = log2.slice(-limit);
1223
+ const log = getAuditLog();
1224
+ const recent = log.slice(-limit);
994
1225
  if (recent.length === 0) {
995
1226
  return { content: [{ type: "text", text: "No calls recorded yet this session." }] };
996
1227
  }
997
- const summary = buildSessionSummary(log2);
998
- const logLines = [`# Audit Log (last ${recent.length} of ${log2.length} total)
1228
+ const summary = buildSessionSummary(log);
1229
+ const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
999
1230
  `];
1000
1231
  for (const entry of recent) {
1001
1232
  const icon = entry.status === "ok" ? "\u2713" : "\u2717";
@@ -1188,7 +1419,7 @@ function registerVerifyTools(server2) {
1188
1419
  data: `Verifying "${collection}" against ${schema.size} schema tables at ${projectRoot}`,
1189
1420
  logger: "product-os"
1190
1421
  });
1191
- const scopedEntries = await mcpQuery("kb.listEntries", { collectionSlug: collection });
1422
+ const scopedEntries = await mcpQuery("chain.listEntries", { collectionSlug: collection });
1192
1423
  if (scopedEntries.length === 0) {
1193
1424
  return {
1194
1425
  content: [{ type: "text", text: `No entries found in \`${collection}\`. Nothing to verify.` }]
@@ -1196,7 +1427,7 @@ function registerVerifyTools(server2) {
1196
1427
  }
1197
1428
  let allEntryIds;
1198
1429
  try {
1199
- const allEntries = await mcpQuery("kb.listEntries", {});
1430
+ const allEntries = await mcpQuery("chain.listEntries", {});
1200
1431
  allEntryIds = new Set(allEntries.map((e) => e.entryId).filter(Boolean));
1201
1432
  } catch {
1202
1433
  allEntryIds = new Set(scopedEntries.map((e) => e.entryId).filter(Boolean));
@@ -1261,7 +1492,7 @@ function registerVerifyTools(server2) {
1261
1492
  const updated = (entry.data?.codeMapping ?? []).map(
1262
1493
  (cm) => cm.status === "aligned" && driftedFields.has(cm.field) ? { ...cm, status: "drifted" } : cm
1263
1494
  );
1264
- await mcpMutation("kb.updateEntry", {
1495
+ await mcpMutation("chain.updateEntry", {
1265
1496
  entryId: entry.entryId,
1266
1497
  data: { codeMapping: updated }
1267
1498
  });
@@ -1283,655 +1514,39 @@ function registerVerifyTools(server2) {
1283
1514
  );
1284
1515
  }
1285
1516
 
1286
- // src/tools/smart-capture.ts
1517
+ // src/tools/architecture.ts
1518
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1519
+ import { resolve as resolve2, relative, dirname, normalize } from "path";
1287
1520
  import { z as z5 } from "zod";
1288
- var AREA_KEYWORDS = {
1289
- "Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
1290
- "Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
1291
- "AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
1292
- "Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
1293
- "Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
1294
- "Analytics & Tracking": ["analytics", "posthog", "tracking", "event", "metric", "funnel"],
1295
- "Security": ["security", "auth", "api key", "permission", "access", "token"]
1296
- };
1297
- function inferArea(text) {
1298
- const lower = text.toLowerCase();
1299
- let bestArea = "";
1300
- let bestScore = 0;
1301
- for (const [area, keywords] of Object.entries(AREA_KEYWORDS)) {
1302
- const score = keywords.filter((kw) => lower.includes(kw)).length;
1303
- if (score > bestScore) {
1304
- bestScore = score;
1305
- bestArea = area;
1306
- }
1307
- }
1308
- return bestArea;
1521
+ var COLLECTION_SLUG = "architecture";
1522
+ var COLLECTION_FIELDS = [
1523
+ { key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
1524
+ { key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
1525
+ { key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
1526
+ { key: "description", label: "Description", type: "text", searchable: true },
1527
+ { key: "color", label: "Color", type: "text" },
1528
+ { key: "icon", label: "Icon", type: "text" },
1529
+ { key: "sourceNode", label: "Source Node (flows)", type: "text" },
1530
+ { key: "targetNode", label: "Target Node (flows)", type: "text" },
1531
+ { key: "filePaths", label: "File Paths", type: "text", searchable: true },
1532
+ { key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
1533
+ { key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
1534
+ { key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
1535
+ { key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
1536
+ ];
1537
+ async function ensureCollection() {
1538
+ const collections = await mcpQuery("chain.listCollections");
1539
+ if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
1540
+ await mcpMutation("chain.createCollection", {
1541
+ slug: COLLECTION_SLUG,
1542
+ name: "Architecture",
1543
+ icon: "\u{1F3D7}\uFE0F",
1544
+ description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
1545
+ fields: COLLECTION_FIELDS
1546
+ });
1309
1547
  }
1310
- function inferDomain(text) {
1311
- return inferArea(text) || "";
1312
- }
1313
- var COMMON_CHECKS = {
1314
- clearName: {
1315
- id: "clear-name",
1316
- label: "Clear, specific name (not vague)",
1317
- check: (ctx) => ctx.name.length > 10 && !["new tension", "new entry", "untitled", "test"].includes(ctx.name.toLowerCase()),
1318
- suggestion: () => "Rename to something specific \u2014 describe the actual problem or concept."
1319
- },
1320
- hasDescription: {
1321
- id: "has-description",
1322
- label: "Description provided (>50 chars)",
1323
- check: (ctx) => ctx.description.length > 50,
1324
- suggestion: () => "Add a fuller description explaining context and impact."
1325
- },
1326
- hasRelations: {
1327
- id: "has-relations",
1328
- label: "At least 1 relation created",
1329
- check: (ctx) => ctx.linksCreated.length >= 1,
1330
- suggestion: () => "Use `suggest-links` and `relate-entries` to add more connections."
1331
- },
1332
- diverseRelations: {
1333
- id: "diverse-relations",
1334
- label: "Relations span multiple collections",
1335
- check: (ctx) => {
1336
- const colls = new Set(ctx.linksCreated.map((l) => l.targetCollection));
1337
- return colls.size >= 2;
1338
- },
1339
- suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
1340
- }
1341
- };
1342
- var PROFILES = /* @__PURE__ */ new Map([
1343
- ["tensions", {
1344
- idPrefix: "TEN",
1345
- governedDraft: false,
1346
- descriptionField: "description",
1347
- defaults: [
1348
- { key: "priority", value: "medium" },
1349
- { key: "date", value: "today" },
1350
- { key: "raised", value: "infer" },
1351
- { key: "severity", value: "infer" }
1352
- ],
1353
- recommendedRelationTypes: ["surfaces_tension_in", "references", "belongs_to", "related_to"],
1354
- inferField: (ctx) => {
1355
- const fields = {};
1356
- const text = `${ctx.name} ${ctx.description}`;
1357
- const area = inferArea(text);
1358
- if (area) fields.raised = area;
1359
- if (text.toLowerCase().includes("critical") || text.toLowerCase().includes("blocker")) {
1360
- fields.severity = "critical";
1361
- } else if (text.toLowerCase().includes("bottleneck") || text.toLowerCase().includes("scaling") || text.toLowerCase().includes("breaking")) {
1362
- fields.severity = "high";
1363
- } else {
1364
- fields.severity = "medium";
1365
- }
1366
- if (area) fields.affectedArea = area;
1367
- return fields;
1368
- },
1369
- qualityChecks: [
1370
- COMMON_CHECKS.clearName,
1371
- COMMON_CHECKS.hasDescription,
1372
- COMMON_CHECKS.hasRelations,
1373
- {
1374
- id: "has-severity",
1375
- label: "Severity specified",
1376
- check: (ctx) => !!ctx.data.severity && ctx.data.severity !== "",
1377
- suggestion: (ctx) => {
1378
- const text = `${ctx.name} ${ctx.description}`.toLowerCase();
1379
- const inferred = text.includes("critical") ? "critical" : text.includes("bottleneck") ? "high" : "medium";
1380
- return `Set severity \u2014 suggest: ${inferred} (based on description keywords).`;
1381
- }
1382
- },
1383
- {
1384
- id: "has-affected-area",
1385
- label: "Affected area identified",
1386
- check: (ctx) => !!ctx.data.affectedArea && ctx.data.affectedArea !== "",
1387
- suggestion: (ctx) => {
1388
- const area = inferArea(`${ctx.name} ${ctx.description}`);
1389
- return area ? `Set affectedArea \u2014 suggest: "${area}" (inferred from content).` : "Specify which product area or domain this tension impacts.";
1390
- }
1391
- }
1392
- ]
1393
- }],
1394
- ["business-rules", {
1395
- idPrefix: "SOS",
1396
- governedDraft: true,
1397
- descriptionField: "description",
1398
- defaults: [
1399
- { key: "severity", value: "medium" },
1400
- { key: "domain", value: "infer" }
1401
- ],
1402
- recommendedRelationTypes: ["governs", "references", "conflicts_with", "related_to"],
1403
- inferField: (ctx) => {
1404
- const fields = {};
1405
- const domain = inferDomain(`${ctx.name} ${ctx.description}`);
1406
- if (domain) fields.domain = domain;
1407
- return fields;
1408
- },
1409
- qualityChecks: [
1410
- COMMON_CHECKS.clearName,
1411
- COMMON_CHECKS.hasDescription,
1412
- COMMON_CHECKS.hasRelations,
1413
- {
1414
- id: "has-rationale",
1415
- label: "Rationale provided",
1416
- check: (ctx) => typeof ctx.data.rationale === "string" && ctx.data.rationale.length > 10,
1417
- suggestion: () => "Add a rationale explaining why this rule exists via `update-entry`."
1418
- },
1419
- {
1420
- id: "has-domain",
1421
- label: "Domain specified",
1422
- check: (ctx) => !!ctx.data.domain && ctx.data.domain !== "",
1423
- suggestion: (ctx) => {
1424
- const domain = inferDomain(`${ctx.name} ${ctx.description}`);
1425
- return domain ? `Set domain \u2014 suggest: "${domain}" (inferred from content).` : "Specify the business domain this rule belongs to.";
1426
- }
1427
- }
1428
- ]
1429
- }],
1430
- ["glossary", {
1431
- idPrefix: "GT",
1432
- governedDraft: true,
1433
- descriptionField: "canonical",
1434
- defaults: [
1435
- { key: "category", value: "infer" }
1436
- ],
1437
- recommendedRelationTypes: ["defines_term_for", "confused_with", "related_to", "references"],
1438
- inferField: (ctx) => {
1439
- const fields = {};
1440
- const area = inferArea(`${ctx.name} ${ctx.description}`);
1441
- if (area) {
1442
- const categoryMap = {
1443
- "Architecture": "Platform & Architecture",
1444
- "Chain": "Knowledge Management",
1445
- "AI & MCP Integration": "AI & Developer Tools",
1446
- "Developer Experience": "AI & Developer Tools",
1447
- "Governance & Decision-Making": "Governance & Process",
1448
- "Analytics & Tracking": "Platform & Architecture",
1449
- "Security": "Platform & Architecture"
1450
- };
1451
- fields.category = categoryMap[area] ?? "";
1452
- }
1453
- return fields;
1454
- },
1455
- qualityChecks: [
1456
- COMMON_CHECKS.clearName,
1457
- {
1458
- id: "has-canonical",
1459
- label: "Canonical definition provided (>20 chars)",
1460
- check: (ctx) => {
1461
- const canonical = ctx.data.canonical;
1462
- return typeof canonical === "string" && canonical.length > 20;
1463
- },
1464
- suggestion: () => "Add a clear canonical definition \u2014 this is the single source of truth for this term."
1465
- },
1466
- COMMON_CHECKS.hasRelations,
1467
- {
1468
- id: "has-category",
1469
- label: "Category assigned",
1470
- check: (ctx) => !!ctx.data.category && ctx.data.category !== "",
1471
- suggestion: () => "Assign a category (e.g., 'Platform & Architecture', 'Governance & Process')."
1472
- }
1473
- ]
1474
- }],
1475
- ["decisions", {
1476
- idPrefix: "DEC",
1477
- governedDraft: false,
1478
- descriptionField: "rationale",
1479
- defaults: [
1480
- { key: "date", value: "today" },
1481
- { key: "decidedBy", value: "infer" }
1482
- ],
1483
- recommendedRelationTypes: ["informs", "references", "replaces", "related_to"],
1484
- inferField: (ctx) => {
1485
- const fields = {};
1486
- const area = inferArea(`${ctx.name} ${ctx.description}`);
1487
- if (area) fields.decidedBy = area;
1488
- return fields;
1489
- },
1490
- qualityChecks: [
1491
- COMMON_CHECKS.clearName,
1492
- {
1493
- id: "has-rationale",
1494
- label: "Rationale provided (>30 chars)",
1495
- check: (ctx) => {
1496
- const rationale = ctx.data.rationale;
1497
- return typeof rationale === "string" && rationale.length > 30;
1498
- },
1499
- suggestion: () => "Explain why this decision was made \u2014 what was considered and rejected?"
1500
- },
1501
- COMMON_CHECKS.hasRelations,
1502
- {
1503
- id: "has-date",
1504
- label: "Decision date recorded",
1505
- check: (ctx) => !!ctx.data.date && ctx.data.date !== "",
1506
- suggestion: () => "Record when this decision was made."
1507
- }
1508
- ]
1509
- }],
1510
- ["features", {
1511
- idPrefix: "FEAT",
1512
- governedDraft: false,
1513
- descriptionField: "description",
1514
- defaults: [],
1515
- recommendedRelationTypes: ["belongs_to", "depends_on", "surfaces_tension_in", "related_to"],
1516
- qualityChecks: [
1517
- COMMON_CHECKS.clearName,
1518
- COMMON_CHECKS.hasDescription,
1519
- COMMON_CHECKS.hasRelations,
1520
- {
1521
- id: "has-owner",
1522
- label: "Owner assigned",
1523
- check: (ctx) => !!ctx.data.owner && ctx.data.owner !== "",
1524
- suggestion: () => "Assign an owner team or product area."
1525
- },
1526
- {
1527
- id: "has-rationale",
1528
- label: "Rationale documented",
1529
- check: (ctx) => !!ctx.data.rationale && String(ctx.data.rationale).length > 20,
1530
- suggestion: () => "Explain why this feature matters \u2014 what problem does it solve?"
1531
- }
1532
- ]
1533
- }]
1534
- ]);
1535
- var FALLBACK_PROFILE = {
1536
- idPrefix: "",
1537
- governedDraft: false,
1538
- descriptionField: "description",
1539
- defaults: [],
1540
- recommendedRelationTypes: ["related_to", "references"],
1541
- qualityChecks: [
1542
- COMMON_CHECKS.clearName,
1543
- COMMON_CHECKS.hasDescription,
1544
- COMMON_CHECKS.hasRelations
1545
- ]
1546
- };
1547
- function generateEntryId(prefix) {
1548
- if (!prefix) return "";
1549
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
1550
- let suffix = "";
1551
- for (let i = 0; i < 6; i++) {
1552
- suffix += chars[Math.floor(Math.random() * chars.length)];
1553
- }
1554
- return `${prefix}-${suffix}`;
1555
- }
1556
- function extractSearchTerms(name, description) {
1557
- const text = `${name} ${description}`;
1558
- return text.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
1559
- }
1560
- function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceCollection, candidateCollection) {
1561
- const text = `${sourceName} ${sourceDescription}`.toLowerCase();
1562
- const candidateName = candidate.name.toLowerCase();
1563
- let score = 0;
1564
- if (text.includes(candidateName) && candidateName.length > 3) {
1565
- score += 40;
1566
- }
1567
- const candidateWords = candidateName.split(/\s+/).filter((w) => w.length > 3);
1568
- const matchingWords = candidateWords.filter((w) => text.includes(w));
1569
- score += matchingWords.length / Math.max(candidateWords.length, 1) * 30;
1570
- const HUB_COLLECTIONS = /* @__PURE__ */ new Set(["strategy", "features"]);
1571
- if (HUB_COLLECTIONS.has(candidateCollection)) {
1572
- score += 15;
1573
- }
1574
- if (candidateCollection !== sourceCollection) {
1575
- score += 10;
1576
- }
1577
- return Math.min(score, 100);
1578
- }
1579
- function inferRelationType(sourceCollection, targetCollection, profile) {
1580
- const typeMap = {
1581
- tensions: {
1582
- glossary: "surfaces_tension_in",
1583
- "business-rules": "references",
1584
- strategy: "belongs_to",
1585
- features: "surfaces_tension_in",
1586
- decisions: "references"
1587
- },
1588
- "business-rules": {
1589
- glossary: "references",
1590
- features: "governs",
1591
- strategy: "belongs_to",
1592
- tensions: "references"
1593
- },
1594
- glossary: {
1595
- features: "defines_term_for",
1596
- "business-rules": "references",
1597
- strategy: "references"
1598
- },
1599
- decisions: {
1600
- features: "informs",
1601
- "business-rules": "references",
1602
- strategy: "references",
1603
- tensions: "references"
1604
- }
1605
- };
1606
- return typeMap[sourceCollection]?.[targetCollection] ?? profile.recommendedRelationTypes[0] ?? "related_to";
1607
- }
1608
- function scoreQuality(ctx, profile) {
1609
- const checks = profile.qualityChecks.map((qc) => {
1610
- const passed2 = qc.check(ctx);
1611
- return {
1612
- id: qc.id,
1613
- label: qc.label,
1614
- passed: passed2,
1615
- suggestion: passed2 ? void 0 : qc.suggestion?.(ctx)
1616
- };
1617
- });
1618
- const passed = checks.filter((c) => c.passed).length;
1619
- const total = checks.length;
1620
- const score = total > 0 ? Math.round(passed / total * 10) : 10;
1621
- return { score, maxScore: 10, checks };
1622
- }
1623
- function formatQualityReport(result) {
1624
- const lines = [`## Quality: ${result.score}/${result.maxScore}`];
1625
- for (const check of result.checks) {
1626
- const icon = check.passed ? "[x]" : "[ ]";
1627
- const suggestion = check.passed ? "" : ` -- ${check.suggestion ?? ""}`;
1628
- lines.push(`${icon} ${check.label}${suggestion}`);
1629
- }
1630
- return lines.join("\n");
1631
- }
1632
- async function checkEntryQuality(entryId) {
1633
- const entry = await mcpQuery("kb.getEntry", { entryId });
1634
- if (!entry) {
1635
- return {
1636
- text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
1637
- quality: { score: 0, maxScore: 10, checks: [] }
1638
- };
1639
- }
1640
- const collections = await mcpQuery("kb.listCollections");
1641
- const collMap = /* @__PURE__ */ new Map();
1642
- for (const c of collections) collMap.set(c._id, c.slug);
1643
- const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
1644
- const profile = PROFILES.get(collectionSlug) ?? FALLBACK_PROFILE;
1645
- const relations = await mcpQuery("kb.listEntryRelations", { entryId });
1646
- const linksCreated = [];
1647
- for (const r of relations) {
1648
- const otherId = r.fromId === entry._id ? r.toId : r.fromId;
1649
- linksCreated.push({
1650
- targetEntryId: otherId,
1651
- targetName: "",
1652
- targetCollection: "",
1653
- relationType: r.type
1654
- });
1655
- }
1656
- const descField = profile.descriptionField;
1657
- const description = typeof entry.data?.[descField] === "string" ? entry.data[descField] : "";
1658
- const ctx = {
1659
- collection: collectionSlug,
1660
- name: entry.name,
1661
- description,
1662
- data: entry.data ?? {},
1663
- entryId: entry.entryId ?? "",
1664
- linksCreated,
1665
- linksSuggested: [],
1666
- collectionFields: []
1667
- };
1668
- const quality = scoreQuality(ctx, profile);
1669
- const lines = [
1670
- `# Quality Check: ${entry.entryId ?? entry.name}`,
1671
- `**${entry.name}** in \`${collectionSlug}\` [${entry.status}]`,
1672
- "",
1673
- formatQualityReport(quality)
1674
- ];
1675
- if (quality.score < 10) {
1676
- const failedChecks = quality.checks.filter((c) => !c.passed && c.suggestion);
1677
- if (failedChecks.length > 0) {
1678
- lines.push("");
1679
- lines.push(`_To improve: use \`update-entry\` to fill missing fields, or \`relate-entries\` to add connections._`);
1680
- }
1681
- }
1682
- return { text: lines.join("\n"), quality };
1683
- }
1684
- var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
1685
- "glossary",
1686
- "business-rules",
1687
- "principles",
1688
- "standards",
1689
- "strategy",
1690
- "features"
1691
- ]);
1692
- var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
1693
- var MAX_AUTO_LINKS = 5;
1694
- var MAX_SUGGESTIONS = 5;
1695
- function registerSmartCaptureTools(server2) {
1696
- server2.registerTool(
1697
- "capture",
1698
- {
1699
- title: "Capture",
1700
- description: "The single tool for creating knowledge entries. Creates an entry, auto-links related entries, and returns a quality scorecard \u2014 all in one call. Provide a collection, name, and description \u2014 everything else is inferred or auto-filled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features.\nAll other collections use sensible defaults.\n\nAlways creates as 'draft' for governed collections. Use `update-entry` for post-creation adjustments.",
1701
- inputSchema: {
1702
- collection: z5.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
1703
- name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
1704
- description: z5.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
1705
- context: z5.string().optional().describe("Optional additional context (e.g. 'Observed during gather-context calls taking 700ms+')"),
1706
- entryId: z5.string().optional().describe("Optional custom entry ID (e.g. 'TEN-my-id'). Auto-generated if omitted.")
1707
- },
1708
- annotations: { destructiveHint: false }
1709
- },
1710
- async ({ collection, name, description, context, entryId }) => {
1711
- const profile = PROFILES.get(collection) ?? FALLBACK_PROFILE;
1712
- const col = await mcpQuery("kb.getCollection", { slug: collection });
1713
- if (!col) {
1714
- return {
1715
- content: [{ type: "text", text: `Collection \`${collection}\` not found. Use \`list-collections\` to see available collections.` }]
1716
- };
1717
- }
1718
- const data = {};
1719
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1720
- for (const field of col.fields ?? []) {
1721
- const key = field.key;
1722
- if (key === profile.descriptionField) {
1723
- data[key] = description;
1724
- } else if (field.type === "array" || field.type === "multi-select") {
1725
- data[key] = [];
1726
- } else {
1727
- data[key] = "";
1728
- }
1729
- }
1730
- for (const def of profile.defaults) {
1731
- if (def.value === "today") {
1732
- data[def.key] = today;
1733
- } else if (def.value !== "infer") {
1734
- data[def.key] = def.value;
1735
- }
1736
- }
1737
- if (profile.inferField) {
1738
- const inferred = profile.inferField({
1739
- collection,
1740
- name,
1741
- description,
1742
- context,
1743
- data,
1744
- entryId: "",
1745
- linksCreated: [],
1746
- linksSuggested: [],
1747
- collectionFields: col.fields ?? []
1748
- });
1749
- for (const [key, val] of Object.entries(inferred)) {
1750
- if (val !== void 0 && val !== "") {
1751
- data[key] = val;
1752
- }
1753
- }
1754
- }
1755
- if (!data[profile.descriptionField] && !data.description && !data.canonical) {
1756
- data[profile.descriptionField || "description"] = description;
1757
- }
1758
- const status = GOVERNED_COLLECTIONS.has(collection) ? "draft" : "draft";
1759
- const finalEntryId = entryId ?? generateEntryId(profile.idPrefix);
1760
- let internalId;
1761
- try {
1762
- internalId = await mcpMutation("kb.createEntry", {
1763
- collectionSlug: collection,
1764
- entryId: finalEntryId || void 0,
1765
- name,
1766
- status,
1767
- data,
1768
- createdBy: "capture"
1769
- });
1770
- } catch (error) {
1771
- const msg = error instanceof Error ? error.message : String(error);
1772
- if (msg.includes("Duplicate") || msg.includes("already exists")) {
1773
- return {
1774
- content: [{
1775
- type: "text",
1776
- text: `# Cannot Capture \u2014 Duplicate Detected
1777
-
1778
- ${msg}
1779
-
1780
- Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
1781
- }]
1782
- };
1783
- }
1784
- throw error;
1785
- }
1786
- const linksCreated = [];
1787
- const linksSuggested = [];
1788
- const searchQuery = extractSearchTerms(name, description);
1789
- if (searchQuery) {
1790
- const [searchResults, allCollections] = await Promise.all([
1791
- mcpQuery("kb.searchEntries", { query: searchQuery }),
1792
- mcpQuery("kb.listCollections")
1793
- ]);
1794
- const collMap = /* @__PURE__ */ new Map();
1795
- for (const c of allCollections) collMap.set(c._id, c.slug);
1796
- const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId && r._id !== internalId).map((r) => ({
1797
- ...r,
1798
- collSlug: collMap.get(r.collectionId) ?? "unknown",
1799
- confidence: computeLinkConfidence(r, name, description, collection, collMap.get(r.collectionId) ?? "unknown")
1800
- })).sort((a, b) => b.confidence - a.confidence);
1801
- for (const c of candidates) {
1802
- if (linksCreated.length >= MAX_AUTO_LINKS) break;
1803
- if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
1804
- if (!c.entryId || !finalEntryId) continue;
1805
- const relationType = inferRelationType(collection, c.collSlug, profile);
1806
- try {
1807
- await mcpMutation("kb.createEntryRelation", {
1808
- fromEntryId: finalEntryId,
1809
- toEntryId: c.entryId,
1810
- type: relationType
1811
- });
1812
- linksCreated.push({
1813
- targetEntryId: c.entryId,
1814
- targetName: c.name,
1815
- targetCollection: c.collSlug,
1816
- relationType
1817
- });
1818
- } catch {
1819
- }
1820
- }
1821
- const linkedIds = new Set(linksCreated.map((l) => l.targetEntryId));
1822
- for (const c of candidates) {
1823
- if (linksSuggested.length >= MAX_SUGGESTIONS) break;
1824
- if (linkedIds.has(c.entryId)) continue;
1825
- if (c.confidence < 10) continue;
1826
- const preview = extractPreview2(c.data, 80);
1827
- const reason = c.confidence >= AUTO_LINK_CONFIDENCE_THRESHOLD ? "high relevance (already linked)" : `"${c.name.toLowerCase().split(/\s+/).filter((w) => `${name} ${description}`.toLowerCase().includes(w) && w.length > 3).slice(0, 2).join('", "')}" appears in content`;
1828
- linksSuggested.push({
1829
- entryId: c.entryId,
1830
- name: c.name,
1831
- collection: c.collSlug,
1832
- reason,
1833
- preview
1834
- });
1835
- }
1836
- }
1837
- const captureCtx = {
1838
- collection,
1839
- name,
1840
- description,
1841
- context,
1842
- data,
1843
- entryId: finalEntryId,
1844
- linksCreated,
1845
- linksSuggested,
1846
- collectionFields: col.fields ?? []
1847
- };
1848
- const quality = scoreQuality(captureCtx, profile);
1849
- const lines = [
1850
- `# Captured: ${finalEntryId || name}`,
1851
- `**${name}** added to \`${collection}\` as \`${status}\``
1852
- ];
1853
- if (linksCreated.length > 0) {
1854
- lines.push("");
1855
- lines.push(`## Auto-linked (${linksCreated.length})`);
1856
- for (const link of linksCreated) {
1857
- lines.push(`- -> **${link.relationType}** ${link.targetEntryId}: ${link.targetName} [${link.targetCollection}]`);
1858
- }
1859
- }
1860
- if (linksSuggested.length > 0) {
1861
- lines.push("");
1862
- lines.push("## Suggested links (review and use relate-entries)");
1863
- for (let i = 0; i < linksSuggested.length; i++) {
1864
- const s = linksSuggested[i];
1865
- const preview = s.preview ? ` \u2014 ${s.preview}` : "";
1866
- lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
1867
- }
1868
- }
1869
- lines.push("");
1870
- lines.push(formatQualityReport(quality));
1871
- const failedChecks = quality.checks.filter((c) => !c.passed);
1872
- if (failedChecks.length > 0) {
1873
- lines.push("");
1874
- lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
1875
- }
1876
- return { content: [{ type: "text", text: lines.join("\n") }] };
1877
- }
1878
- );
1879
- server2.registerTool(
1880
- "quality-check",
1881
- {
1882
- title: "Quality Check",
1883
- description: "Score an existing knowledge entry against collection-specific quality criteria. Returns a scorecard (X/10) with specific, actionable suggestions for improvement. Checks: name clarity, description completeness, relation connectedness, and collection-specific fields.\n\nUse after creating entries to assess their quality, or to audit existing entries.",
1884
- inputSchema: {
1885
- entryId: z5.string().describe("Entry ID to check, e.g. 'TEN-graph-db', 'GT-019', 'SOS-006'")
1886
- },
1887
- annotations: { readOnlyHint: true }
1888
- },
1889
- async ({ entryId }) => {
1890
- const result = await checkEntryQuality(entryId);
1891
- return { content: [{ type: "text", text: result.text }] };
1892
- }
1893
- );
1894
- }
1895
- function extractPreview2(data, maxLen) {
1896
- if (!data || typeof data !== "object") return "";
1897
- const raw = data.description ?? data.canonical ?? data.detail ?? data.rule ?? "";
1898
- if (typeof raw !== "string" || !raw) return "";
1899
- return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
1900
- }
1901
-
1902
- // src/tools/architecture.ts
1903
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1904
- import { resolve as resolve2, relative, dirname, normalize } from "path";
1905
- import { z as z6 } from "zod";
1906
- var COLLECTION_SLUG = "architecture";
1907
- var COLLECTION_FIELDS = [
1908
- { key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
1909
- { key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
1910
- { key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
1911
- { key: "description", label: "Description", type: "text", searchable: true },
1912
- { key: "color", label: "Color", type: "text" },
1913
- { key: "icon", label: "Icon", type: "text" },
1914
- { key: "sourceNode", label: "Source Node (flows)", type: "text" },
1915
- { key: "targetNode", label: "Target Node (flows)", type: "text" },
1916
- { key: "filePaths", label: "File Paths", type: "text", searchable: true },
1917
- { key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
1918
- { key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
1919
- { key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
1920
- { key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
1921
- ];
1922
- async function ensureCollection() {
1923
- const collections = await mcpQuery("kb.listCollections");
1924
- if (collections.some((c) => c.slug === COLLECTION_SLUG)) return;
1925
- await mcpMutation("kb.createCollection", {
1926
- slug: COLLECTION_SLUG,
1927
- name: "Architecture",
1928
- icon: "\u{1F3D7}\uFE0F",
1929
- description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
1930
- fields: COLLECTION_FIELDS
1931
- });
1932
- }
1933
- async function listArchEntries() {
1934
- return mcpQuery("kb.listEntries", { collectionSlug: COLLECTION_SLUG });
1548
+ async function listArchEntries() {
1549
+ return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
1935
1550
  }
1936
1551
  function byTag(entries, archType) {
1937
1552
  return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -2056,10 +1671,10 @@ function registerArchitectureTools(server2) {
2056
1671
  title: "Architecture",
2057
1672
  description: "Explore the system architecture \u2014 show the full map, explore a specific layer, or visualize a data flow.\n\nActions:\n- `show`: Render the layered architecture map (Auth \u2192 Infra \u2192 Core \u2192 Features \u2192 Integration)\n- `explore`: Drill into a layer to see nodes, ownership, file paths\n- `flow`: Visualize a data flow path between nodes",
2058
1673
  inputSchema: {
2059
- action: z6.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
2060
- template: z6.string().optional().describe("Template entry ID to filter by (for show)"),
2061
- layer: z6.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
2062
- flow: z6.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
1674
+ action: z5.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
1675
+ template: z5.string().optional().describe("Template entry ID to filter by (for show)"),
1676
+ layer: z5.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
1677
+ flow: z5.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
2063
1678
  },
2064
1679
  annotations: { readOnlyHint: true }
2065
1680
  },
@@ -2178,7 +1793,7 @@ ${nodeDetail}${flowLines}`
2178
1793
  title: "Architecture Admin",
2179
1794
  description: "Architecture maintenance \u2014 seed the default architecture data or run a dependency health check.\n\nActions:\n- `seed`: Populate the architecture collection with the default Product OS map. Safe to re-run.\n- `check`: Scan the codebase for dependency direction violations against layer rules.",
2180
1795
  inputSchema: {
2181
- action: z6.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
1796
+ action: z5.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
2182
1797
  }
2183
1798
  },
2184
1799
  async ({ action }) => {
@@ -2205,14 +1820,14 @@ ${nodeDetail}${flowLines}`
2205
1820
  );
2206
1821
  if (hasChanges) {
2207
1822
  const mergedData = { ...existingData, ...seedData };
2208
- await mcpMutation("kb.updateEntry", { entryId: seed.entryId, data: mergedData });
1823
+ await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
2209
1824
  updated++;
2210
1825
  } else {
2211
1826
  unchanged++;
2212
1827
  }
2213
1828
  continue;
2214
1829
  }
2215
- await mcpMutation("kb.createEntry", {
1830
+ await mcpMutation("chain.createEntry", {
2216
1831
  collectionSlug: COLLECTION_SLUG,
2217
1832
  entryId: seed.entryId,
2218
1833
  name: seed.name,
@@ -2471,7 +2086,7 @@ function formatScanReport(result) {
2471
2086
  }
2472
2087
 
2473
2088
  // src/tools/workflows.ts
2474
- import { z as z7 } from "zod";
2089
+ import { z as z6 } from "zod";
2475
2090
 
2476
2091
  // src/workflows/definitions.ts
2477
2092
  var RETRO_WORKFLOW = {
@@ -2700,16 +2315,16 @@ ${cards}`
2700
2315
  title: "Workflow Checkpoint",
2701
2316
  description: "Record the output of a workflow round. Captures the round's data to the Chain as a structured entry. Use this during Facilitator Mode after completing each round to persist progress \u2014 so if the conversation is interrupted, work is not lost.\n\nAt workflow completion, this tool can also generate the final summary entry.",
2702
2317
  inputSchema: {
2703
- workflowId: z7.string().describe("Workflow ID (e.g., 'retro')"),
2704
- roundId: z7.string().describe("Round ID (e.g., 'what-went-well')"),
2705
- output: z7.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
2706
- isFinal: z7.boolean().optional().describe(
2318
+ workflowId: z6.string().describe("Workflow ID (e.g., 'retro')"),
2319
+ roundId: z6.string().describe("Round ID (e.g., 'what-went-well')"),
2320
+ output: z6.string().describe("The round's output \u2014 synthesized by the facilitator from the conversation"),
2321
+ isFinal: z6.boolean().optional().describe(
2707
2322
  "If true, this is the final checkpoint and triggers the summary chain entry creation"
2708
2323
  ),
2709
- summaryName: z7.string().optional().describe(
2324
+ summaryName: z6.string().optional().describe(
2710
2325
  "Name for the final chain entry (required when isFinal=true)"
2711
2326
  ),
2712
- summaryDescription: z7.string().optional().describe(
2327
+ summaryDescription: z6.string().optional().describe(
2713
2328
  "Full description/rationale for the final chain entry (required when isFinal=true)"
2714
2329
  )
2715
2330
  },
@@ -2745,7 +2360,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2745
2360
  ];
2746
2361
  if (isFinal && summaryName && summaryDescription) {
2747
2362
  try {
2748
- const entryId = await mcpMutation("kb.createEntry", {
2363
+ const entryId = await mcpMutation("chain.createEntry", {
2749
2364
  collectionSlug: wf.kbOutputCollection,
2750
2365
  name: summaryName,
2751
2366
  status: "draft",
@@ -2802,8 +2417,139 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2802
2417
  );
2803
2418
  }
2804
2419
 
2420
+ // src/tools/session.ts
2421
+ function registerSessionTools(server2) {
2422
+ server2.registerTool(
2423
+ "agent-start",
2424
+ {
2425
+ title: "Start Agent Session",
2426
+ description: "Start an agent session. Creates a tracked session for this workspace with full attribution. If a session is already active, it gets superseded (graceful handover). Write tools are available after calling orient.",
2427
+ annotations: { readOnlyHint: false }
2428
+ },
2429
+ async () => {
2430
+ try {
2431
+ const result = await startAgentSession();
2432
+ const lines = [];
2433
+ if (result.superseded) {
2434
+ lines.push(
2435
+ `Previous session superseded. Session ${result.superseded.previousSessionId} (started ${result.superseded.startedAt}, initiated by ${result.superseded.initiatedBy}) was closed.`,
2436
+ ""
2437
+ );
2438
+ }
2439
+ lines.push(
2440
+ `Session ${result.sessionId} active. Initiated by ${result.initiatedBy}. Workspace ${result.workspaceName}. Scope: ${result.toolsScope}. Write tools available after orient.`
2441
+ );
2442
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2443
+ } catch (err) {
2444
+ return {
2445
+ content: [{ type: "text", text: `Failed to start agent session: ${err.message}` }],
2446
+ isError: true
2447
+ };
2448
+ }
2449
+ }
2450
+ );
2451
+ server2.registerTool(
2452
+ "agent-close",
2453
+ {
2454
+ title: "Close Agent Session",
2455
+ description: "Close the current agent session. Records structured session data (entries created, modified, relations, gate results). After this, write tools are blocked even if the MCP connection stays open.",
2456
+ annotations: { readOnlyHint: false }
2457
+ },
2458
+ async () => {
2459
+ try {
2460
+ const sessionId = getAgentSessionId();
2461
+ if (!sessionId) {
2462
+ return {
2463
+ content: [{ type: "text", text: "No active agent session to close." }]
2464
+ };
2465
+ }
2466
+ const session = await mcpCall("agent.getSession", {
2467
+ sessionId
2468
+ });
2469
+ await closeAgentSession();
2470
+ const lines = [
2471
+ `Session ${sessionId} closed.`,
2472
+ ""
2473
+ ];
2474
+ if (session) {
2475
+ const created = session.entriesCreated?.length ?? 0;
2476
+ const modified = session.entriesModified?.length ?? 0;
2477
+ const relations = session.relationsCreated ?? 0;
2478
+ const gates = session.gateFailures ?? 0;
2479
+ const warnings = session.contradictionWarnings ?? 0;
2480
+ lines.push(
2481
+ `| Metric | Count |`,
2482
+ `|--------|-------|`,
2483
+ `| Entries created | ${created} |`,
2484
+ `| Entries modified | ${modified} |`,
2485
+ `| Relations created | ${relations} |`,
2486
+ `| Gate failures | ${gates} |`,
2487
+ `| Contradiction warnings | ${warnings} |`
2488
+ );
2489
+ }
2490
+ lines.push("", "Write tools are now blocked. Session data saved for future orientation.");
2491
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2492
+ } catch (err) {
2493
+ return {
2494
+ content: [{ type: "text", text: `Failed to close agent session: ${err.message}` }],
2495
+ isError: true
2496
+ };
2497
+ }
2498
+ }
2499
+ );
2500
+ server2.registerTool(
2501
+ "agent-status",
2502
+ {
2503
+ title: "Agent Session Status",
2504
+ description: "Check the current agent session status \u2014 whether a session is active, whether orientation is complete, and session activity so far.",
2505
+ annotations: { readOnlyHint: true }
2506
+ },
2507
+ async () => {
2508
+ try {
2509
+ const sessionId = getAgentSessionId();
2510
+ if (!sessionId) {
2511
+ return {
2512
+ content: [{ type: "text", text: "No active agent session. Call `agent-start` to begin." }]
2513
+ };
2514
+ }
2515
+ const session = await mcpCall("agent.getSession", {
2516
+ sessionId
2517
+ });
2518
+ if (!session) {
2519
+ return {
2520
+ content: [{ type: "text", text: "Session ID cached but not found in Convex. Call `agent-start` to create a new session." }]
2521
+ };
2522
+ }
2523
+ const oriented = isSessionOriented();
2524
+ const created = session.entriesCreated?.length ?? 0;
2525
+ const modified = session.entriesModified?.length ?? 0;
2526
+ const lines = [
2527
+ `Session ${sessionId} ${session.status}.`,
2528
+ `Initiated by ${session.initiatedBy}.`,
2529
+ `Scope: ${session.toolsScope}.`,
2530
+ `Oriented: ${oriented ? "yes" : "no"}.`,
2531
+ "",
2532
+ `| Metric | Value |`,
2533
+ `|--------|-------|`,
2534
+ `| Entries created | ${created} |`,
2535
+ `| Entries modified | ${modified} |`,
2536
+ `| Relations created | ${session.relationsCreated ?? 0} |`,
2537
+ `| Started | ${new Date(session.startedAt).toISOString()} |`,
2538
+ `| Expires | ${new Date(session.expiresAt).toISOString()} |`
2539
+ ];
2540
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2541
+ } catch (err) {
2542
+ return {
2543
+ content: [{ type: "text", text: `Failed to get session status: ${err.message}` }],
2544
+ isError: true
2545
+ };
2546
+ }
2547
+ }
2548
+ );
2549
+ }
2550
+
2805
2551
  // src/tools/gitchain.ts
2806
- import { z as z8 } from "zod";
2552
+ import { z as z7 } from "zod";
2807
2553
  function linkSummary(links) {
2808
2554
  return Object.entries(links).map(([id, content]) => {
2809
2555
  const filled = typeof content === "string" && content.length > 0;
@@ -2818,15 +2564,15 @@ function registerGitChainTools(server2) {
2818
2564
  title: "Chain",
2819
2565
  description: "Manage processes \u2014 create, get, list, or edit process links. Processes are versioned knowledge artifacts that follow a process template (e.g. Strategy Coherence: Problem \u2192 Insight \u2192 Choice \u2192 Action \u2192 Outcome).",
2820
2566
  inputSchema: {
2821
- action: z8.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
2822
- chainEntryId: z8.string().optional().describe("Chain entry ID (required for get/edit)"),
2823
- title: z8.string().optional().describe("Process title (required for create)"),
2824
- chainTypeId: z8.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
2825
- description: z8.string().optional().describe("Description (for create)"),
2826
- linkId: z8.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
2827
- content: z8.string().optional().describe("New content for the link (for edit action)"),
2828
- status: z8.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
2829
- author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2567
+ action: z7.enum(["create", "get", "list", "edit"]).describe("Action: create a process, get process details, list all processes, or edit a process link"),
2568
+ chainEntryId: z7.string().optional().describe("Chain entry ID (required for get/edit)"),
2569
+ title: z7.string().optional().describe("Process title (required for create)"),
2570
+ chainTypeId: z7.string().optional().default("strategy-coherence").describe("Process template slug for create: 'strategy-coherence', 'idm-proposal', or any custom template slug"),
2571
+ description: z7.string().optional().describe("Description (for create)"),
2572
+ linkId: z7.string().optional().describe("Link to edit (for edit action): problem, insight, choice, action, outcome"),
2573
+ content: z7.string().optional().describe("New content for the link (for edit action)"),
2574
+ status: z7.string().optional().describe("Filter by status for list: 'draft' or 'active'"),
2575
+ author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2830
2576
  }
2831
2577
  },
2832
2578
  async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
@@ -2923,13 +2669,13 @@ Use \`chain action=get\` to see the full chain with updated scores.`
2923
2669
  title: "Chain Version",
2924
2670
  description: "Manage process versions \u2014 commit snapshots, list commits, view history, diff versions, or revert. Commits record all link content, compute coherence scores, and track changes.",
2925
2671
  inputSchema: {
2926
- action: z8.enum(["commit", "list", "diff", "revert", "history"]).describe("Action: commit a snapshot, list commits, diff two versions, revert to a version, or view history"),
2927
- chainEntryId: z8.string().describe("The chain's entry ID"),
2928
- commitMessage: z8.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
2929
- versionA: z8.number().optional().describe("Earlier version for diff"),
2930
- versionB: z8.number().optional().describe("Later version for diff"),
2931
- toVersion: z8.number().optional().describe("Version number to revert to"),
2932
- author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2672
+ action: z7.enum(["commit", "list", "diff", "revert", "history"]).describe("Action: commit a snapshot, list commits, diff two versions, revert to a version, or view history"),
2673
+ chainEntryId: z7.string().describe("The chain's entry ID"),
2674
+ commitMessage: z7.string().optional().describe("Commit message (required for commit). Convention: type(link): description"),
2675
+ versionA: z7.number().optional().describe("Earlier version for diff"),
2676
+ versionB: z7.number().optional().describe("Later version for diff"),
2677
+ toVersion: z7.number().optional().describe("Version number to revert to"),
2678
+ author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2933
2679
  },
2934
2680
  annotations: { readOnlyHint: false }
2935
2681
  },
@@ -3037,11 +2783,11 @@ ${formatted}` }] };
3037
2783
  title: "Chain Branch",
3038
2784
  description: "Manage process branches \u2014 create a branch for isolated editing, list branches, merge a branch back into main, or check for conflicts.",
3039
2785
  inputSchema: {
3040
- action: z8.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
3041
- chainEntryId: z8.string().describe("The chain's entry ID"),
3042
- branchName: z8.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
3043
- strategy: z8.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
3044
- author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2786
+ action: z7.enum(["create", "list", "merge", "conflicts"]).describe("Action: create a branch, list branches, merge a branch, or check for conflicts"),
2787
+ chainEntryId: z7.string().describe("The chain's entry ID"),
2788
+ branchName: z7.string().optional().describe("Branch name (required for merge/conflicts, optional for create)"),
2789
+ strategy: z7.enum(["merge_commit", "squash"]).optional().describe("Merge strategy: 'merge_commit' (default) or 'squash'"),
2790
+ author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3045
2791
  }
3046
2792
  },
3047
2793
  async ({ action, chainEntryId, branchName, strategy, author }) => {
@@ -3125,14 +2871,14 @@ Resolve conflicts before merging.`
3125
2871
  title: "Chain Review",
3126
2872
  description: "Review process quality \u2014 run the coherence gate or manage comments on process versions. The gate checks: coherence score >= 70%, all links filled, commit message convention.",
3127
2873
  inputSchema: {
3128
- action: z8.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
3129
- chainEntryId: z8.string().describe("The chain's entry ID"),
3130
- commitMessage: z8.string().optional().describe("Commit message to lint (for gate action)"),
3131
- versionNumber: z8.number().optional().describe("Version to comment on or list comments for"),
3132
- linkId: z8.string().optional().describe("Link this comment targets (optional for comment)"),
3133
- body: z8.string().optional().describe("Comment text (required for comment action)"),
3134
- commentId: z8.string().optional().describe("Comment ID (required for resolve-comment)"),
3135
- author: z8.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
2874
+ action: z7.enum(["gate", "comment", "resolve-comment", "list-comments"]).describe("Action: run coherence gate, add a comment, resolve a comment, or list comments"),
2875
+ chainEntryId: z7.string().describe("The chain's entry ID"),
2876
+ commitMessage: z7.string().optional().describe("Commit message to lint (for gate action)"),
2877
+ versionNumber: z7.number().optional().describe("Version to comment on or list comments for"),
2878
+ linkId: z7.string().optional().describe("Link this comment targets (optional for comment)"),
2879
+ body: z7.string().optional().describe("Comment text (required for comment action)"),
2880
+ commentId: z7.string().optional().describe("Comment ID (required for resolve-comment)"),
2881
+ author: z7.string().optional().describe("Who is performing the action. Defaults to 'mcp'.")
3136
2882
  },
3137
2883
  annotations: { readOnlyHint: false }
3138
2884
  },
@@ -3211,42 +2957,782 @@ ${formatted}`
3211
2957
  );
3212
2958
  }
3213
2959
 
3214
- // src/resources/index.ts
3215
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3216
- function formatEntryMarkdown(entry) {
3217
- const id = entry.entryId ? `${entry.entryId}: ` : "";
3218
- const lines = [`## ${id}${entry.name} [${entry.status}]`];
3219
- if (entry.data && typeof entry.data === "object") {
3220
- for (const [key, val] of Object.entries(entry.data)) {
3221
- if (val && key !== "rawData") {
3222
- lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
3223
- }
3224
- }
3225
- }
3226
- return lines.join("\n");
2960
+ // src/tools/maps.ts
2961
+ import { z as z8 } from "zod";
2962
+ function slotSummary(slots) {
2963
+ return Object.entries(slots).map(([id, refs]) => {
2964
+ if (!refs || refs.length === 0) return ` - **${id}**: (empty)`;
2965
+ const items = refs.map((r) => {
2966
+ const name = r.ingredientName ?? r.label ?? r.entryId;
2967
+ const version = r.pinnedVersion ? ` v${r.pinnedVersion}` : "";
2968
+ const status = r.ingredientStatus ? ` [${r.ingredientStatus}]` : "";
2969
+ return `${name}${version}${status}`;
2970
+ }).join(", ");
2971
+ return ` - **${id}**: ${items}`;
2972
+ }).join("\n");
3227
2973
  }
3228
- function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules) {
3229
- const sections = ["# Product Brain \u2014 Orientation"];
3230
- sections.push(
3231
- "## 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)."
3232
- );
3233
- if (collections) {
3234
- const collList = collections.map((c) => {
3235
- const prefix = c.icon ? `${c.icon} ` : "";
3236
- return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
3237
- }).join("\n");
3238
- sections.push(
3239
- `## Data Model (${collections.length} collections)
3240
- Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
3241
- Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, history via \`entryHistory\`, labels via \`labels\` + \`entryLabels\`.
3242
-
3243
- ` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
3244
- );
3245
- } else {
3246
- sections.push(
3247
- "## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
3248
- );
3249
- }
2974
+ function registerMapTools(server2) {
2975
+ server2.registerTool(
2976
+ "map",
2977
+ {
2978
+ title: "Map",
2979
+ description: "Manage composed framework maps \u2014 create, get, or list. Maps assemble ingredient entries into framework slots (e.g. Lean Canvas). Use map-slot to add/remove ingredients, map-version to commit and view history.",
2980
+ inputSchema: {
2981
+ action: z8.enum(["create", "get", "list"]).describe("Action: create a map, get map details, or list all maps"),
2982
+ mapEntryId: z8.string().optional().describe("Map entry ID (for get)"),
2983
+ title: z8.string().optional().describe("Map title (for create)"),
2984
+ templateId: z8.string().optional().default("lean-canvas").describe("Template slug for create: 'lean-canvas' or any composed template"),
2985
+ description: z8.string().optional().describe("Description (for create)"),
2986
+ slotIds: z8.array(z8.string()).optional().describe("Slot IDs to initialize (for create; auto-populated from template if omitted)"),
2987
+ status: z8.string().optional().describe("Filter by status for list")
2988
+ }
2989
+ },
2990
+ async ({ action, mapEntryId, title, templateId, description, slotIds, status }) => {
2991
+ if (action === "create") {
2992
+ if (!title) {
2993
+ return {
2994
+ content: [
2995
+ { type: "text", text: "A `title` is required to create a map." }
2996
+ ]
2997
+ };
2998
+ }
2999
+ const result = await mcpMutation(
3000
+ "maps.createMap",
3001
+ { title, templateId, description, slotIds }
3002
+ );
3003
+ const wsCtx = await getWorkspaceContext();
3004
+ return {
3005
+ content: [
3006
+ {
3007
+ type: "text",
3008
+ text: `# Map Created
3009
+
3010
+ - **Entry ID:** \`${result.entryId}\`
3011
+ - **Title:** ${title}
3012
+ - **Template:** ${templateId}
3013
+ - **Status:** draft
3014
+ - **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})
3015
+
3016
+ Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingredientEntryId="..."\` to start filling slots.`
3017
+ }
3018
+ ]
3019
+ };
3020
+ }
3021
+ if (action === "get") {
3022
+ if (!mapEntryId) {
3023
+ return {
3024
+ content: [{ type: "text", text: "A `mapEntryId` is required." }]
3025
+ };
3026
+ }
3027
+ const map = await mcpQuery("maps.getMap", { mapEntryId });
3028
+ if (!map) {
3029
+ return {
3030
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
3031
+ };
3032
+ }
3033
+ return {
3034
+ content: [
3035
+ {
3036
+ type: "text",
3037
+ text: `# ${map.name}
3038
+
3039
+ - **Entry ID:** \`${map.entryId}\`
3040
+ - **Template:** ${map.templateName}
3041
+ - **Status:** ${map.status}
3042
+ - **Version:** ${map.currentVersion}
3043
+ - **Completion:** ${map.completionScore}% (${map.filledSlots}/${map.totalSlots} slots)
3044
+
3045
+ ## Slots
3046
+
3047
+ ` + slotSummary(map.slots)
3048
+ }
3049
+ ]
3050
+ };
3051
+ }
3052
+ if (action === "list") {
3053
+ const maps = await mcpQuery("maps.listMaps", {
3054
+ templateId: templateId !== "lean-canvas" ? templateId : void 0,
3055
+ status
3056
+ });
3057
+ if (!maps || maps.length === 0) {
3058
+ return {
3059
+ content: [
3060
+ {
3061
+ type: "text",
3062
+ text: 'No maps found. Create one with `map action=create title="My Canvas"`.'
3063
+ }
3064
+ ]
3065
+ };
3066
+ }
3067
+ const lines = maps.map(
3068
+ (m) => `- **${m.name}** (\`${m.entryId}\`) \u2014 ${m.templateId}, ${m.completionScore}% complete, v${m.currentVersion}`
3069
+ );
3070
+ return {
3071
+ content: [
3072
+ {
3073
+ type: "text",
3074
+ text: `# Maps (${maps.length})
3075
+
3076
+ ${lines.join("\n")}`
3077
+ }
3078
+ ]
3079
+ };
3080
+ }
3081
+ return {
3082
+ content: [{ type: "text", text: `Unknown action: ${action}` }]
3083
+ };
3084
+ }
3085
+ );
3086
+ server2.registerTool(
3087
+ "map-slot",
3088
+ {
3089
+ title: "Map Slot",
3090
+ description: "Add, remove, or replace ingredient references in a map's slots. Ingredients are entries from any collection that fill a framework position.",
3091
+ inputSchema: {
3092
+ action: z8.enum(["add", "remove", "replace", "list"]).describe("Action: add/remove/replace an ingredient in a slot, or list slot contents"),
3093
+ mapEntryId: z8.string().describe("Map entry ID"),
3094
+ slotId: z8.string().optional().describe("Slot ID (e.g. 'problem', 'customer-segments')"),
3095
+ ingredientEntryId: z8.string().optional().describe("Ingredient entry ID to add/remove"),
3096
+ newIngredientEntryId: z8.string().optional().describe("New ingredient entry ID (for replace)"),
3097
+ label: z8.string().optional().describe("Display label override"),
3098
+ author: z8.string().optional().describe("Who is performing the action")
3099
+ }
3100
+ },
3101
+ async ({
3102
+ action,
3103
+ mapEntryId,
3104
+ slotId,
3105
+ ingredientEntryId,
3106
+ newIngredientEntryId,
3107
+ label,
3108
+ author
3109
+ }) => {
3110
+ if (action === "list") {
3111
+ const map = await mcpQuery("maps.getMap", { mapEntryId });
3112
+ if (!map) {
3113
+ return {
3114
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
3115
+ };
3116
+ }
3117
+ if (slotId && map.slots[slotId]) {
3118
+ return {
3119
+ content: [
3120
+ {
3121
+ type: "text",
3122
+ text: `# Slot: ${slotId}
3123
+
3124
+ ` + (map.slots[slotId].length === 0 ? "(empty)" : map.slots[slotId].map(
3125
+ (r) => `- **${r.ingredientName ?? r.entryId}** (\`${r.entryId}\`)${r.pinnedVersion ? ` v${r.pinnedVersion}` : ""}`
3126
+ ).join("\n"))
3127
+ }
3128
+ ]
3129
+ };
3130
+ }
3131
+ return {
3132
+ content: [
3133
+ { type: "text", text: `## All Slots
3134
+
3135
+ ${slotSummary(map.slots)}` }
3136
+ ]
3137
+ };
3138
+ }
3139
+ if (action === "add") {
3140
+ if (!slotId || !ingredientEntryId) {
3141
+ return {
3142
+ content: [
3143
+ {
3144
+ type: "text",
3145
+ text: "Both `slotId` and `ingredientEntryId` are required for add."
3146
+ }
3147
+ ]
3148
+ };
3149
+ }
3150
+ await mcpMutation("maps.addToSlot", {
3151
+ mapEntryId,
3152
+ slotId,
3153
+ ingredientEntryId,
3154
+ label,
3155
+ author
3156
+ });
3157
+ return {
3158
+ content: [
3159
+ {
3160
+ type: "text",
3161
+ text: `Added \`${ingredientEntryId}\` to slot "${slotId}" on map \`${mapEntryId}\`.`
3162
+ }
3163
+ ]
3164
+ };
3165
+ }
3166
+ if (action === "remove") {
3167
+ if (!slotId || !ingredientEntryId) {
3168
+ return {
3169
+ content: [
3170
+ {
3171
+ type: "text",
3172
+ text: "Both `slotId` and `ingredientEntryId` are required for remove."
3173
+ }
3174
+ ]
3175
+ };
3176
+ }
3177
+ await mcpMutation("maps.removeFromSlot", {
3178
+ mapEntryId,
3179
+ slotId,
3180
+ ingredientEntryId,
3181
+ author
3182
+ });
3183
+ return {
3184
+ content: [
3185
+ {
3186
+ type: "text",
3187
+ text: `Removed \`${ingredientEntryId}\` from slot "${slotId}" on map \`${mapEntryId}\`.`
3188
+ }
3189
+ ]
3190
+ };
3191
+ }
3192
+ if (action === "replace") {
3193
+ if (!slotId || !ingredientEntryId || !newIngredientEntryId) {
3194
+ return {
3195
+ content: [
3196
+ {
3197
+ type: "text",
3198
+ text: "`slotId`, `ingredientEntryId`, and `newIngredientEntryId` are required for replace."
3199
+ }
3200
+ ]
3201
+ };
3202
+ }
3203
+ await mcpMutation("maps.replaceInSlot", {
3204
+ mapEntryId,
3205
+ slotId,
3206
+ oldIngredientEntryId: ingredientEntryId,
3207
+ newIngredientEntryId,
3208
+ label,
3209
+ author
3210
+ });
3211
+ return {
3212
+ content: [
3213
+ {
3214
+ type: "text",
3215
+ text: `Replaced \`${ingredientEntryId}\` with \`${newIngredientEntryId}\` in slot "${slotId}".`
3216
+ }
3217
+ ]
3218
+ };
3219
+ }
3220
+ return {
3221
+ content: [{ type: "text", text: `Unknown action: ${action}` }]
3222
+ };
3223
+ }
3224
+ );
3225
+ server2.registerTool(
3226
+ "map-version",
3227
+ {
3228
+ title: "Map Version",
3229
+ description: "Commit a map (pins ingredient versions), list commit history, or view a specific commit.",
3230
+ inputSchema: {
3231
+ action: z8.enum(["commit", "list", "history"]).describe("Action: commit the map, list commits, or view commit history"),
3232
+ mapEntryId: z8.string().describe("Map entry ID"),
3233
+ commitMessage: z8.string().optional().describe("Commit message (for commit action)"),
3234
+ author: z8.string().optional().describe("Who is committing")
3235
+ }
3236
+ },
3237
+ async ({ action, mapEntryId, commitMessage, author }) => {
3238
+ if (action === "commit") {
3239
+ const result = await mcpMutation("maps.commitMap", {
3240
+ mapEntryId,
3241
+ commitMessage: commitMessage ?? "Map committed",
3242
+ author
3243
+ });
3244
+ return {
3245
+ content: [
3246
+ {
3247
+ type: "text",
3248
+ text: `# Map Committed
3249
+
3250
+ - **Version:** ${result.version}
3251
+ - **Completion:** ${result.completionScore}%
3252
+ - **Slots modified:** ${result.slotsModified.length > 0 ? result.slotsModified.join(", ") : "(none)"}
3253
+
3254
+ All ingredient references have been pinned at their current versions.`
3255
+ }
3256
+ ]
3257
+ };
3258
+ }
3259
+ if (action === "list" || action === "history") {
3260
+ const commits = await mcpQuery("maps.listMapCommits", {
3261
+ mapEntryId
3262
+ });
3263
+ if (!commits || commits.length === 0) {
3264
+ return {
3265
+ content: [
3266
+ {
3267
+ type: "text",
3268
+ text: `No commits yet for map \`${mapEntryId}\`. Use \`map-version action=commit\` to create the first snapshot.`
3269
+ }
3270
+ ]
3271
+ };
3272
+ }
3273
+ const lines = commits.map(
3274
+ (c) => `- **v${c.version}** (${new Date(c.createdAt).toLocaleDateString()}) \u2014 ${c.commitMessage ?? "(no message)"} \u2014 by ${c.author}` + (c.slotsModified.length > 0 ? `
3275
+ Slots changed: ${c.slotsModified.join(", ")}` : "")
3276
+ );
3277
+ return {
3278
+ content: [
3279
+ {
3280
+ type: "text",
3281
+ text: `# Map History: \`${mapEntryId}\` (${commits.length} commits)
3282
+
3283
+ ${lines.join("\n")}`
3284
+ }
3285
+ ]
3286
+ };
3287
+ }
3288
+ return {
3289
+ content: [{ type: "text", text: `Unknown action: ${action}` }]
3290
+ };
3291
+ }
3292
+ );
3293
+ server2.registerTool(
3294
+ "map-suggest",
3295
+ {
3296
+ title: "Map Suggest",
3297
+ description: "Given a map with empty slots, search the Chain for ingredients that could fill them. Uses keyword search and collection matching based on the template's suggested collections.",
3298
+ inputSchema: {
3299
+ mapEntryId: z8.string().describe("Map entry ID to suggest ingredients for"),
3300
+ slotId: z8.string().optional().describe("Specific slot to find ingredients for (or all empty slots)"),
3301
+ query: z8.string().optional().describe("Optional search query to narrow ingredient suggestions")
3302
+ }
3303
+ },
3304
+ async ({ mapEntryId, slotId, query }) => {
3305
+ const map = await mcpQuery("maps.getMap", { mapEntryId });
3306
+ if (!map) {
3307
+ return {
3308
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
3309
+ };
3310
+ }
3311
+ const emptySlots = slotId ? [slotId].filter((id) => (map.slots[id]?.length ?? 0) === 0) : Object.entries(map.slots).filter(([, refs]) => refs.length === 0).map(([id]) => id);
3312
+ if (emptySlots.length === 0) {
3313
+ return {
3314
+ content: [
3315
+ {
3316
+ type: "text",
3317
+ text: slotId ? `Slot "${slotId}" already has ingredients.` : "All slots have ingredients. Nothing to suggest."
3318
+ }
3319
+ ]
3320
+ };
3321
+ }
3322
+ const slotDefs = map.slotDefs ?? [];
3323
+ const suggestions = [];
3324
+ for (const sid of emptySlots) {
3325
+ const def = slotDefs.find((s) => s.id === sid);
3326
+ const searchQuery = query ?? def?.label ?? sid;
3327
+ const results = await mcpQuery("knowledge.search", {
3328
+ query: searchQuery,
3329
+ limit: 5
3330
+ });
3331
+ if (results && results.length > 0) {
3332
+ const items = results.map(
3333
+ (r) => ` - **${r.name}** (\`${r.entryId}\`, ${r.collectionName ?? "unknown"}) \u2014 ${r.status}`
3334
+ );
3335
+ suggestions.push(
3336
+ `### ${def?.label ?? sid}
3337
+ ${def?.description ?? ""}
3338
+
3339
+ ${items.join("\n")}`
3340
+ );
3341
+ } else {
3342
+ suggestions.push(
3343
+ `### ${def?.label ?? sid}
3344
+ _No matching entries found. Create ingredients first._`
3345
+ );
3346
+ }
3347
+ }
3348
+ return {
3349
+ content: [
3350
+ {
3351
+ type: "text",
3352
+ text: `# Ingredient Suggestions for "${map.name}"
3353
+
3354
+ ` + suggestions.join("\n\n") + `
3355
+
3356
+ Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntryId="..."\` to add ingredients.`
3357
+ }
3358
+ ]
3359
+ };
3360
+ }
3361
+ );
3362
+ }
3363
+
3364
+ // src/tools/start.ts
3365
+ import { z as z9 } from "zod";
3366
+
3367
+ // src/presets/collections.ts
3368
+ var COLLECTION_PRESETS = [
3369
+ {
3370
+ id: "software-product",
3371
+ name: "Software Product",
3372
+ description: "For teams building software products \u2014 glossary, features, architecture, tech debt, and API endpoints",
3373
+ collections: [
3374
+ { slug: "glossary", name: "Glossary", description: "Canonical terminology for the product domain", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "select", options: ["Platform & Architecture", "Knowledge Management", "AI & Developer Tools", "Governance & Process"] }, { key: "confusedWith", label: "Confused With", type: "array" }] },
3375
+ { slug: "features", name: "Features", description: "Product features and capabilities", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "scope", label: "Scope", type: "string" }, { key: "area", label: "Area", type: "string" }, { key: "status", label: "Status", type: "select", options: ["proposed", "in-progress", "shipped", "deprecated"] }] },
3376
+ { slug: "architecture", name: "Architecture", description: "System architecture layers and components", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "layer", label: "Layer", type: "string" }, { key: "dependencies", label: "Dependencies", type: "array" }] },
3377
+ { slug: "tech-debt", name: "Tech Debt", description: "Technical debt items to track and address", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "severity", label: "Severity", type: "select", options: ["low", "medium", "high", "critical"] }, { key: "area", label: "Area", type: "string" }, { key: "effort", label: "Effort", type: "select", options: ["small", "medium", "large"] }] },
3378
+ { slug: "api-endpoints", name: "API Endpoints", description: "REST/GraphQL endpoints and their contracts", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "method", label: "Method", type: "select", options: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, { key: "path", label: "Path", type: "string" }, { key: "auth", label: "Auth Required", type: "select", options: ["none", "api-key", "bearer", "session"] }] },
3379
+ { slug: "decisions", name: "Decisions", description: "Significant decisions with rationale and context", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }, { key: "alternatives", label: "Alternatives", type: "string" }] },
3380
+ { slug: "tensions", name: "Tensions", description: "Friction points, pain points, and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }, { key: "severity", label: "Severity", type: "select", options: ["low", "medium", "high", "critical"] }] }
3381
+ ]
3382
+ },
3383
+ {
3384
+ id: "content-business",
3385
+ name: "Content Business",
3386
+ description: "For content creators, publishers, and media companies \u2014 topics, audience segments, content calendar, and brand voice",
3387
+ collections: [
3388
+ { slug: "glossary", name: "Glossary", description: "Industry terminology and brand language", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
3389
+ { slug: "topics", name: "Topics", description: "Content topics and themes", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "pillar", label: "Content Pillar", type: "string" }, { key: "stage", label: "Stage", type: "select", options: ["idea", "researching", "drafting", "published", "evergreen"] }] },
3390
+ { slug: "audience-segments", name: "Audience Segments", description: "Target reader/viewer personas", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "painPoints", label: "Pain Points", type: "string" }, { key: "channels", label: "Channels", type: "array" }] },
3391
+ { slug: "brand-voice", name: "Brand Voice", description: "Tone, style, and voice guidelines", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "doThis", label: "Do This", type: "string" }, { key: "notThis", label: "Not This", type: "string" }] },
3392
+ { slug: "decisions", name: "Decisions", description: "Editorial and strategic decisions", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }] },
3393
+ { slug: "tensions", name: "Tensions", description: "Content gaps, audience friction, and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
3394
+ ]
3395
+ },
3396
+ {
3397
+ id: "agency",
3398
+ name: "Agency",
3399
+ description: "For agencies managing multiple clients \u2014 client profiles, project scopes, deliverables, and processes",
3400
+ collections: [
3401
+ { slug: "glossary", name: "Glossary", description: "Agency and client terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
3402
+ { slug: "clients", name: "Clients", description: "Client profiles and relationship context", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "industry", label: "Industry", type: "string" }, { key: "contact", label: "Primary Contact", type: "string" }] },
3403
+ { slug: "deliverables", name: "Deliverables", description: "Standard deliverable types and templates", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "type", label: "Type", type: "string" }, { key: "estimatedHours", label: "Estimated Hours", type: "string" }] },
3404
+ { slug: "processes", name: "Processes", description: "Standard operating procedures and workflows", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "steps", label: "Steps", type: "string" }, { key: "owner", label: "Owner", type: "string" }] },
3405
+ { slug: "decisions", name: "Decisions", description: "Strategic and operational decisions", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }] },
3406
+ { slug: "tensions", name: "Tensions", description: "Process bottlenecks and client friction", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
3407
+ ]
3408
+ },
3409
+ {
3410
+ id: "saas-api",
3411
+ name: "SaaS API",
3412
+ description: "For API-first SaaS products \u2014 endpoints, schemas, rate limits, changelog, and integration guides",
3413
+ collections: [
3414
+ { slug: "glossary", name: "Glossary", description: "API and domain terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
3415
+ { slug: "api-endpoints", name: "API Endpoints", description: "API routes and contracts", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "method", label: "Method", type: "select", options: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, { key: "path", label: "Path", type: "string" }, { key: "auth", label: "Auth", type: "select", options: ["none", "api-key", "bearer", "oauth"] }] },
3416
+ { slug: "schemas", name: "Schemas", description: "Data models and API schemas", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "format", label: "Format", type: "select", options: ["json", "protobuf", "graphql", "openapi"] }, { key: "version", label: "Version", type: "string" }] },
3417
+ { slug: "rate-limits", name: "Rate Limits", description: "Rate limiting policies and tiers", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "tier", label: "Tier", type: "string" }, { key: "limit", label: "Limit", type: "string" }] },
3418
+ { slug: "changelog", name: "Changelog", description: "API version history and breaking changes", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "version", label: "Version", type: "string" }, { key: "date", label: "Date", type: "string" }, { key: "breaking", label: "Breaking", type: "select", options: ["yes", "no"] }] },
3419
+ { slug: "decisions", name: "Decisions", description: "API design decisions and rationale", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }] }
3420
+ ]
3421
+ },
3422
+ {
3423
+ id: "general",
3424
+ name: "General",
3425
+ description: "A minimal starter set \u2014 glossary, decisions, and tensions. Add more collections as you need them.",
3426
+ collections: [
3427
+ { slug: "glossary", name: "Glossary", description: "Canonical terminology", fields: [{ key: "canonical", label: "Canonical", type: "string", required: true, searchable: true }, { key: "category", label: "Category", type: "string" }] },
3428
+ { slug: "decisions", name: "Decisions", description: "Decisions with rationale", fields: [{ key: "rationale", label: "Rationale", type: "string", required: true, searchable: true }, { key: "date", label: "Date", type: "string" }, { key: "decidedBy", label: "Decided By", type: "string" }] },
3429
+ { slug: "tensions", name: "Tensions", description: "Friction points and unmet needs", fields: [{ key: "description", label: "Description", type: "string", required: true, searchable: true }, { key: "priority", label: "Priority", type: "select", options: ["low", "medium", "high", "critical"] }] }
3430
+ ]
3431
+ }
3432
+ ];
3433
+ function getPreset(id) {
3434
+ return COLLECTION_PRESETS.find((p) => p.id === id);
3435
+ }
3436
+ function listPresets() {
3437
+ return COLLECTION_PRESETS.map((p) => ({
3438
+ id: p.id,
3439
+ name: p.name,
3440
+ description: p.description,
3441
+ collectionCount: p.collections.length
3442
+ }));
3443
+ }
3444
+
3445
+ // src/tools/start.ts
3446
+ function registerStartTools(server2) {
3447
+ server2.registerTool(
3448
+ "start",
3449
+ {
3450
+ title: "Start Product Brain",
3451
+ description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace**: asks what you're building, seeds tailored collections, and gets you ready to capture knowledge immediately.\n- **Existing workspace**: returns readiness score, recent activity, open tensions, and suggested next actions (same as orient).\n\nUse this as your first call. Replaces the need to call orient, workspace-status, or health separately.",
3452
+ inputSchema: {
3453
+ preset: z9.string().optional().describe(
3454
+ "Collection preset ID to seed (e.g. 'software-product', 'content-business', 'agency', 'saas-api', 'general'). Only used for fresh workspaces. If omitted on a fresh workspace, returns the preset menu."
3455
+ )
3456
+ },
3457
+ annotations: { readOnlyHint: false }
3458
+ },
3459
+ async ({ preset }) => {
3460
+ const errors = [];
3461
+ const agentSessionId = getAgentSessionId();
3462
+ let wsCtx = null;
3463
+ try {
3464
+ wsCtx = await getWorkspaceContext();
3465
+ } catch (e) {
3466
+ errors.push(`Workspace: ${e.message}`);
3467
+ }
3468
+ if (!wsCtx) {
3469
+ return {
3470
+ content: [
3471
+ {
3472
+ type: "text",
3473
+ text: "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.")
3474
+ }
3475
+ ]
3476
+ };
3477
+ }
3478
+ const isFresh = await detectFreshWorkspace();
3479
+ if (isFresh && !preset) {
3480
+ return { content: [{ type: "text", text: buildPresetMenu(wsCtx) }] };
3481
+ }
3482
+ if (isFresh && preset) {
3483
+ return { content: [{ type: "text", text: await seedPreset(wsCtx, preset, agentSessionId) }] };
3484
+ }
3485
+ return { content: [{ type: "text", text: await buildOrientResponse(wsCtx, agentSessionId, errors) }] };
3486
+ }
3487
+ );
3488
+ }
3489
+ async function detectFreshWorkspace() {
3490
+ try {
3491
+ const entries = await mcpQuery("chain.listEntries", {});
3492
+ const nonSystem = (entries ?? []).filter((e) => !e.isSystem);
3493
+ return nonSystem.length === 0;
3494
+ } catch {
3495
+ return false;
3496
+ }
3497
+ }
3498
+ function buildPresetMenu(wsCtx) {
3499
+ const presets = listPresets();
3500
+ const lines = [
3501
+ `# Welcome to ${wsCtx.workspaceName}`,
3502
+ "",
3503
+ "Your workspace is fresh \u2014 let's set it up. **What are you building?**",
3504
+ "",
3505
+ "Choose a preset to tailor your collections:",
3506
+ ""
3507
+ ];
3508
+ for (const p of presets) {
3509
+ lines.push(`- **${p.name}** (\`${p.id}\`) \u2014 ${p.description} (${p.collectionCount} collections)`);
3510
+ }
3511
+ lines.push(
3512
+ "",
3513
+ 'Call `start` again with your choice, e.g.: `start preset="software-product"`',
3514
+ "",
3515
+ "_You can always add, remove, or customize collections later._"
3516
+ );
3517
+ return lines.join("\n");
3518
+ }
3519
+ async function seedPreset(wsCtx, presetId, agentSessionId) {
3520
+ const preset = getPreset(presetId);
3521
+ if (!preset) {
3522
+ return `Preset "${presetId}" not found.
3523
+
3524
+ Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`;
3525
+ }
3526
+ const seeded = [];
3527
+ const skipped = [];
3528
+ for (const col of preset.collections) {
3529
+ try {
3530
+ await mcpCall("chain.createCollection", {
3531
+ slug: col.slug,
3532
+ name: col.name,
3533
+ description: col.description,
3534
+ icon: col.icon,
3535
+ fields: col.fields,
3536
+ isDefault: false,
3537
+ createdBy: "preset:" + presetId
3538
+ });
3539
+ seeded.push(col.name);
3540
+ } catch {
3541
+ skipped.push(`${col.name} (already exists)`);
3542
+ }
3543
+ }
3544
+ if (agentSessionId) {
3545
+ try {
3546
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
3547
+ setSessionOriented(true);
3548
+ } catch {
3549
+ }
3550
+ }
3551
+ const lines = [
3552
+ `# ${wsCtx.workspaceName} is ready`,
3553
+ "",
3554
+ `Seeded **${preset.name}** preset with ${seeded.length} collection${seeded.length === 1 ? "" : "s"}:`,
3555
+ ""
3556
+ ];
3557
+ for (const name of seeded) {
3558
+ lines.push(`- ${name}`);
3559
+ }
3560
+ if (skipped.length > 0) {
3561
+ lines.push("", "Skipped (already exist):");
3562
+ for (const name of skipped) {
3563
+ lines.push(`- ${name}`);
3564
+ }
3565
+ }
3566
+ lines.push(
3567
+ "",
3568
+ "## What's next?",
3569
+ "",
3570
+ "Start capturing knowledge. Here are example commands for your collections:",
3571
+ ""
3572
+ );
3573
+ for (const col of preset.collections.slice(0, 3)) {
3574
+ lines.push(
3575
+ `- \`capture collection="${col.slug}" name="..." description="..."\` \u2014 add to ${col.name}`
3576
+ );
3577
+ }
3578
+ lines.push(
3579
+ "",
3580
+ "After capturing a few entries, use `suggest-links` to connect them into a knowledge graph.",
3581
+ "",
3582
+ "---",
3583
+ `Orientation complete. Write tools are available.`
3584
+ );
3585
+ return lines.join("\n");
3586
+ }
3587
+ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
3588
+ let priorSessions = [];
3589
+ try {
3590
+ priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
3591
+ } catch {
3592
+ }
3593
+ let constraintEntries = [];
3594
+ try {
3595
+ const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
3596
+ mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
3597
+ mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
3598
+ mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
3599
+ ]);
3600
+ const committed = [
3601
+ ...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
3602
+ ...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
3603
+ ...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
3604
+ ];
3605
+ constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 8);
3606
+ } catch {
3607
+ }
3608
+ let openTensions = [];
3609
+ try {
3610
+ const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
3611
+ openTensions = (tensions ?? []).filter((e) => e.status === "draft");
3612
+ } catch {
3613
+ }
3614
+ let readiness = null;
3615
+ try {
3616
+ readiness = await mcpQuery("chain.workspaceReadiness");
3617
+ } catch (e) {
3618
+ errors.push(`Readiness: ${e.message}`);
3619
+ }
3620
+ const lines = [];
3621
+ lines.push(`# ${wsCtx.workspaceName}`);
3622
+ lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
3623
+ lines.push("");
3624
+ if (readiness) {
3625
+ const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
3626
+ lines.push(`## Readiness: ${readiness.score}%`);
3627
+ lines.push(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
3628
+ lines.push("");
3629
+ if (readiness.gaps?.length > 0) {
3630
+ lines.push("### Gaps");
3631
+ for (const gap of readiness.gaps) {
3632
+ lines.push(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
3633
+ }
3634
+ lines.push("");
3635
+ }
3636
+ }
3637
+ if (openTensions.length > 0) {
3638
+ lines.push(`## Open Tensions (${openTensions.length})`);
3639
+ for (const t of openTensions) {
3640
+ const id = t.entryId ?? "(no ID)";
3641
+ const prio = t.data?.priority ?? "";
3642
+ lines.push(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
3643
+ }
3644
+ lines.push("");
3645
+ }
3646
+ if (priorSessions.length > 0) {
3647
+ lines.push(`## Prior Agent Sessions (last ${priorSessions.length})`);
3648
+ for (const s of priorSessions) {
3649
+ const date = new Date(s.startedAt).toISOString().split("T")[0];
3650
+ const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
3651
+ const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
3652
+ const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
3653
+ lines.push(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}`);
3654
+ }
3655
+ lines.push("");
3656
+ }
3657
+ if (constraintEntries.length > 0) {
3658
+ lines.push(`## Active Constraints (${constraintEntries.length})`);
3659
+ lines.push("_Architecture, business rules, and decisions that govern this workspace._");
3660
+ lines.push("");
3661
+ for (const c of constraintEntries) {
3662
+ const id = c.entryId ?? "(no ID)";
3663
+ const col = c.collectionSlug ?? "";
3664
+ const desc = c.data?.description ?? c.data?.rationale ?? "";
3665
+ const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
3666
+ lines.push(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
3667
+ }
3668
+ lines.push("");
3669
+ }
3670
+ if (errors.length > 0) {
3671
+ lines.push("## Errors");
3672
+ for (const err of errors) lines.push(`- ${err}`);
3673
+ lines.push("");
3674
+ }
3675
+ if (agentSessionId) {
3676
+ try {
3677
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
3678
+ setSessionOriented(true);
3679
+ const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
3680
+ const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
3681
+ lines.push("---");
3682
+ lines.push(
3683
+ `Orientation complete. Session ${agentSessionId}. Write tools now available. ${readinessInfo} ${tensionInfo}`
3684
+ );
3685
+ } catch {
3686
+ lines.push("---");
3687
+ lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
3688
+ }
3689
+ } else {
3690
+ lines.push("---");
3691
+ lines.push("_No active agent session. Call `agent-start` to begin a tracked session._");
3692
+ }
3693
+ return lines.join("\n");
3694
+ }
3695
+
3696
+ // src/resources/index.ts
3697
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3698
+ function formatEntryMarkdown(entry) {
3699
+ const id = entry.entryId ? `${entry.entryId}: ` : "";
3700
+ const lines = [`## ${id}${entry.name} [${entry.status}]`];
3701
+ if (entry.data && typeof entry.data === "object") {
3702
+ for (const [key, val] of Object.entries(entry.data)) {
3703
+ if (val && key !== "rawData") {
3704
+ lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
3705
+ }
3706
+ }
3707
+ }
3708
+ return lines.join("\n");
3709
+ }
3710
+ function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules) {
3711
+ const sections = ["# Product Brain \u2014 Orientation"];
3712
+ sections.push(
3713
+ "## Core Product Architecture\nThe Chain is a versioned, connected, compounding knowledge base \u2014 the SSOT for a product team.\nEverything on the Chain is one of **three primitives**:\n\n- **Entry** (atom) \u2014 a discrete knowledge unit: target audience, business rule, glossary term, metric, tension. Lives in a collection.\n- **Process** (authored) \u2014 a narrative chain with named links (Strategy Coherence, IDM Proposal). Content is inline text. Collection: `chains`.\n- **Map** (composed) \u2014 a framework assembled by reference (Lean Canvas, Empathy Map). Slots point to ingredient entries. Collection: `maps`.\n\nEntries are atoms. Processes author narrative from atoms. Maps compose frameworks from atoms.\nAll three share the same versioning, branching, gating, and relation system.\n\nThe Chain compounds: each new relation makes entries discoverable from more starting points \u2192 gather-context returns richer results \u2192 AI processes have more context \u2192 quality scores improve \u2192 next commit is smarter than the last."
3714
+ );
3715
+ sections.push(
3716
+ "## 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)."
3717
+ );
3718
+ if (collections) {
3719
+ const collList = collections.map((c) => {
3720
+ const prefix = c.icon ? `${c.icon} ` : "";
3721
+ return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
3722
+ }).join("\n");
3723
+ sections.push(
3724
+ `## Data Model (${collections.length} collections)
3725
+ Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
3726
+ The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
3727
+ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
3728
+
3729
+ ` + collList + "\n\nUse `list-collections` for field schemas, `get-entry` for full records."
3730
+ );
3731
+ } else {
3732
+ sections.push(
3733
+ "## Data Model\nCould not load collections \u2014 use `list-collections` to browse manually."
3734
+ );
3735
+ }
3250
3736
  const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
3251
3737
  sections.push(
3252
3738
  `## Business Rules
@@ -3262,16 +3748,16 @@ Draft a new rule: use the \`draft-rule-from-context\` prompt.`
3262
3748
  Event catalog: \`tracking-events\` collection (${eventsCount}).
3263
3749
  ${conventionNote}
3264
3750
  Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
3265
- Browse: \`list-entries collection=tracking-events\`. Full setup: \`docs/posthog-setup.md\`.`
3751
+ Browse: \`list-entries collection=tracking-events\`.`
3266
3752
  );
3267
3753
  sections.push(
3268
- "## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept\n- `informs` \u2014 a decision or insight informs a feature\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `gather-context` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `suggest-links` \u2014 discover potential connections for an entry\n- `relate-entries` \u2014 create a typed link between two entries\n- `find-related` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `suggest-links` to discover and create relevant relations."
3754
+ "## 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- `fills_slot` \u2014 an ingredient entry fills a slot in a map\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."
3269
3755
  );
3270
3756
  sections.push(
3271
- "## Creating Knowledge\nUse `capture` as the only tool for creating new entries. It handles the full workflow in one call:\n1. Creates the entry with collection-aware defaults (auto-fills dates, infers domains, sets priority)\n2. Auto-links related entries from across the chain (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`, `features`.\nAll other collections use sensible defaults.\n\nExample: `capture collection='tensions' name='...' description='...'`\n\nUse `quality-check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments when you need full field control."
3757
+ "## Creating Knowledge\n**Entries:** Use `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 chain (up to 5 confident matches)\n3. Returns a quality scorecard (X/10) with actionable improvement suggestions\n\n**Smart profiles** exist for: `tensions`, `business-rules`, `glossary`, `decisions`, `features`, `audiences`, `strategy`, `standards`, `maps`, `chains`, `tracking-events`.\nAll other collections use the `ENT-{random}` fallback profile.\n\n**Processes:** Use `chain action=create` with a template (e.g., `strategy-coherence`, `idm-proposal`). Fill links with `chain action=edit`.\n\n**Maps:** Use `map action=create` with a template (e.g., `lean-canvas`). Fill slots with `map-slot action=add`. Commit with `map-version action=commit`.\nNote: committing a map version creates a snapshot but does NOT change the entry status. To activate: `update-entry entryId=MAP-xxx status=active autoPublish=true`.\nUse `map-suggest` to discover ingredients for empty slots.\n\n**Prompts** (invoke via MCP prompt protocol):\n- `name-check` \u2014 verify a name against glossary conventions\n- `draft-decision-record` \u2014 scaffold a decision entry\n- `review-against-rules` \u2014 check work against business rules for a domain\n- `draft-rule-from-context` \u2014 create a new business rule from context\n- `run-workflow` \u2014 run a structured ceremony (e.g., retrospective)\n\nUse `quality-check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments (status changes, field updates, deprecation)."
3272
3758
  );
3273
3759
  sections.push(
3274
- "## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (by entry ID or task description)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Architecture map** \u2192 `architecture action=show` tool (layered system visualization)\n- **Explore a layer** \u2192 `architecture action=explore` tool (drill into Auth, Core, Features, etc.)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
3760
+ "## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `gather-context` tool (by entry ID or task description)\n- **Discover links** \u2192 `suggest-links` tool\n- **Quality audit** \u2192 `quality-check` tool\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `list-collections` tool\n- **Labels** \u2192 `productbrain://labels` resource or `labels` tool\n- **Any collection** \u2192 `productbrain://{slug}/entries` resource\n- **Log a decision** \u2192 `draft-decision-record` prompt\n- **Build a map** \u2192 `map action=create` + `map-slot action=add` + `map-version action=commit`\n- **Architecture map** \u2192 `architecture action=show` tool (layered system visualization)\n- **Explore a layer** \u2192 `architecture action=explore` tool (drill into Auth, Core, Features, etc.)\n- **Growth funnel** \u2192 `productbrain://growth-funnel` resource or `list-entries collection=strategy status=draft`\n- **Audiences** \u2192 `productbrain://audiences/entries` resource or `list-entries collection=audiences`\n- **Session identity** \u2192 `whoami` tool (workspace + auth context)\n- **Health check** \u2192 `health` tool\n- **Debug MCP calls** \u2192 `mcp-audit` tool"
3275
3761
  );
3276
3762
  return sections.join("\n\n---\n\n");
3277
3763
  }
@@ -3281,10 +3767,10 @@ function registerResources(server2) {
3281
3767
  "productbrain://orientation",
3282
3768
  async (uri) => {
3283
3769
  const [collectionsResult, eventsResult, standardsResult, rulesResult] = await Promise.allSettled([
3284
- mcpQuery("kb.listCollections"),
3285
- mcpQuery("kb.listEntries", { collectionSlug: "tracking-events" }),
3286
- mcpQuery("kb.listEntries", { collectionSlug: "standards" }),
3287
- mcpQuery("kb.listEntries", { collectionSlug: "business-rules" })
3770
+ mcpQuery("chain.listCollections"),
3771
+ mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
3772
+ mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
3773
+ mcpQuery("chain.listEntries", { collectionSlug: "business-rules" })
3288
3774
  ]);
3289
3775
  const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
3290
3776
  const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
@@ -3304,8 +3790,8 @@ function registerResources(server2) {
3304
3790
  "productbrain://terminology",
3305
3791
  async (uri) => {
3306
3792
  const [glossaryResult, standardsResult] = await Promise.allSettled([
3307
- mcpQuery("kb.listEntries", { collectionSlug: "glossary" }),
3308
- mcpQuery("kb.listEntries", { collectionSlug: "standards" })
3793
+ mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
3794
+ mcpQuery("chain.listEntries", { collectionSlug: "standards" })
3309
3795
  ]);
3310
3796
  const lines = ["# Product Brain \u2014 Terminology"];
3311
3797
  if (glossaryResult.status === "fulfilled") {
@@ -3341,7 +3827,7 @@ ${stds}`);
3341
3827
  "chain-collections",
3342
3828
  "productbrain://collections",
3343
3829
  async (uri) => {
3344
- const collections = await mcpQuery("kb.listCollections");
3830
+ const collections = await mcpQuery("chain.listCollections");
3345
3831
  if (collections.length === 0) {
3346
3832
  return { contents: [{ uri: uri.href, text: "No collections in this workspace.", mimeType: "text/markdown" }] };
3347
3833
  }
@@ -3364,7 +3850,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3364
3850
  "chain-collection-entries",
3365
3851
  new ResourceTemplate("productbrain://{slug}/entries", {
3366
3852
  list: async () => {
3367
- const collections = await mcpQuery("kb.listCollections");
3853
+ const collections = await mcpQuery("chain.listCollections");
3368
3854
  return {
3369
3855
  resources: collections.map((c) => ({
3370
3856
  uri: `productbrain://${c.slug}/entries`,
@@ -3374,7 +3860,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3374
3860
  }
3375
3861
  }),
3376
3862
  async (uri, { slug }) => {
3377
- const entries = await mcpQuery("kb.listEntries", { collectionSlug: slug });
3863
+ const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug });
3378
3864
  const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
3379
3865
  return {
3380
3866
  contents: [{
@@ -3389,7 +3875,7 @@ ${formatted}`, mimeType: "text/markdown" }]
3389
3875
  "chain-labels",
3390
3876
  "productbrain://labels",
3391
3877
  async (uri) => {
3392
- const labels = await mcpQuery("kb.listLabels");
3878
+ const labels = await mcpQuery("chain.listLabels");
3393
3879
  if (labels.length === 0) {
3394
3880
  return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
3395
3881
  }
@@ -3419,14 +3905,14 @@ ${lines.join("\n")}`, mimeType: "text/markdown" }]
3419
3905
  }
3420
3906
 
3421
3907
  // src/prompts/index.ts
3422
- import { z as z9 } from "zod";
3908
+ import { z as z10 } from "zod";
3423
3909
  function registerPrompts(server2) {
3424
3910
  server2.prompt(
3425
3911
  "review-against-rules",
3426
3912
  "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.",
3427
- { domain: z9.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
3913
+ { domain: z10.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
3428
3914
  async ({ domain }) => {
3429
- const entries = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
3915
+ const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
3430
3916
  const rules = entries.filter((e) => e.data?.domain === domain);
3431
3917
  if (rules.length === 0) {
3432
3918
  return {
@@ -3477,9 +3963,9 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
3477
3963
  server2.prompt(
3478
3964
  "name-check",
3479
3965
  "Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
3480
- { names: z9.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
3966
+ { names: z10.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
3481
3967
  async ({ names }) => {
3482
- const terms = await mcpQuery("kb.listEntries", { collectionSlug: "glossary" });
3968
+ const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
3483
3969
  const glossaryContext = terms.map(
3484
3970
  (t) => `${t.name} (${t.entryId ?? ""}) [${t.status}]: ${t.data?.canonical ?? ""}` + (t.data?.confusedWith?.length > 0 ? ` \u2014 Often confused with: ${t.data.confusedWith.join(", ")}` : "") + (t.data?.codeMapping?.length > 0 ? `
3485
3971
  Code mappings: ${t.data.codeMapping.map((m) => `${m.platform}:${m.field}`).join(", ")}` : "")
@@ -3513,9 +3999,9 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
3513
3999
  server2.prompt(
3514
4000
  "draft-decision-record",
3515
4001
  "Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
3516
- { context: z9.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
4002
+ { context: z10.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
3517
4003
  async ({ context }) => {
3518
- const recentDecisions = await mcpQuery("kb.listEntries", { collectionSlug: "decisions" });
4004
+ const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
3519
4005
  const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
3520
4006
  const recentContext = sorted.length > 0 ? sorted.map((d) => `- [${d.status}] ${d.name} (${d.data?.date ?? "no date"})`).join("\n") : "No previous decisions recorded.";
3521
4007
  return {
@@ -3551,11 +4037,11 @@ After drafting, I can log it using the capture tool with collection "decisions".
3551
4037
  "draft-rule-from-context",
3552
4038
  "Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
3553
4039
  {
3554
- observation: z9.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
3555
- domain: z9.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
4040
+ observation: z10.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
4041
+ domain: z10.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
3556
4042
  },
3557
4043
  async ({ observation, domain }) => {
3558
- const allRules = await mcpQuery("kb.listEntries", { collectionSlug: "business-rules" });
4044
+ const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
3559
4045
  const existingRules = allRules.filter((r) => r.data?.domain === domain);
3560
4046
  const existingContext = existingRules.length > 0 ? existingRules.map((r) => `${r.entryId ?? ""}: ${r.name} [${r.status}] \u2014 ${r.data?.description ?? ""}`).join("\n") : "No existing rules for this domain.";
3561
4047
  const highestRuleNum = allRules.map((r) => parseInt((r.entryId ?? "").replace(/^[A-Z]+-/, ""), 10)).filter((n) => !isNaN(n)).sort((a, b) => b - a)[0] || 0;
@@ -3597,10 +4083,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
3597
4083
  "run-workflow",
3598
4084
  "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.",
3599
4085
  {
3600
- workflow: z9.string().describe(
4086
+ workflow: z10.string().describe(
3601
4087
  "Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
3602
4088
  ),
3603
- context: z9.string().optional().describe(
4089
+ context: z10.string().optional().describe(
3604
4090
  "Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
3605
4091
  )
3606
4092
  },
@@ -3627,7 +4113,7 @@ Use one of these IDs to run a workflow.`
3627
4113
  }
3628
4114
  let kbContext = "";
3629
4115
  try {
3630
- const recentDecisions = await mcpQuery("kb.listEntries", {
4116
+ const recentDecisions = await mcpQuery("chain.listEntries", {
3631
4117
  collectionSlug: wf.kbOutputCollection
3632
4118
  });
3633
4119
  const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
@@ -3716,23 +4202,9 @@ if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
3716
4202
  } catch {
3717
4203
  }
3718
4204
  }
3719
- bootstrapCloudMode();
4205
+ bootstrap();
3720
4206
  var SERVER_VERSION = "2.0.0";
3721
4207
  initAnalytics();
3722
- var workspaceId;
3723
- try {
3724
- workspaceId = await getWorkspaceId();
3725
- } catch (err) {
3726
- 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`." : "";
3727
- process.stderr.write(`[MCP] Startup failed: ${err.message}${hint}
3728
- `);
3729
- process.exit(1);
3730
- }
3731
- trackSessionStarted(
3732
- process.env.WORKSPACE_SLUG ?? "unknown",
3733
- workspaceId,
3734
- SERVER_VERSION
3735
- );
3736
4208
  var server = new McpServer2(
3737
4209
  {
3738
4210
  name: "Product Brain",
@@ -3745,28 +4217,27 @@ var server = new McpServer2(
3745
4217
  "Terminology, standards, and core data all live here \u2014 no need to check external docs.",
3746
4218
  "",
3747
4219
  "Workflow:",
3748
- " 1. Verify: call `health` to confirm connectivity.",
3749
- " 2. Terminology: fetch `productbrain://terminology` or use `name-check` prompt.",
4220
+ " 1. Start: call `agent-start` to begin a tracked session.",
4221
+ " 2. Orient: call `orient` to load workspace context and unlock write tools.",
3750
4222
  " 3. Discover: use `search` to find entries, or `list-entries` to browse.",
3751
4223
  " 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
3752
4224
  " 5. Context: use `gather-context` with an entryId or a task description.",
3753
4225
  " 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
3754
- " 7. Connect: use `suggest-links` then `relate-entries` to build the graph.",
3755
- " 8. Quality: use `quality-check` to assess entry completeness.",
3756
- " 9. Labels: use `labels` to list, create, or apply labels.",
3757
- " 10. Debug: use `mcp-audit` to see what backend calls happened this session.",
4226
+ " 7. Commit: use `commit-entry` to promote drafts to SSOT.",
4227
+ " 8. Connect: use `suggest-links` then `relate-entries` to build the graph.",
4228
+ " 9. Close: call `agent-close` when done \u2014 records session activity.",
3758
4229
  "",
3759
- "Always discover before drilling in (search or list-entries before get-entry).",
3760
- "",
3761
- "Orientation:",
3762
- " Fetch `productbrain://orientation` to understand architecture, data model, rules,",
3763
- " and analytics. It is the map. Then use tools to drill in."
4230
+ "Write tools (capture, update-entry, relate-entries, commit-entry) require:",
4231
+ " - An active agent session (call agent-start)",
4232
+ " - Completed orientation (call orient)",
4233
+ " - A readwrite API key scope"
3764
4234
  ].join("\n")
3765
4235
  }
3766
4236
  );
3767
4237
  var enabledModules = new Set(
3768
- (process.env.PB_MODULES ?? "core,gitchain").split(",").map((m) => m.trim().toLowerCase())
4238
+ (process.env.PB_MODULES ?? "core,gitchain,arch").split(",").map((m) => m.trim().toLowerCase())
3769
4239
  );
4240
+ registerSessionTools(server);
3770
4241
  registerKnowledgeTools(server);
3771
4242
  registerLabelTools(server);
3772
4243
  registerHealthTools(server);
@@ -3774,15 +4245,40 @@ registerVerifyTools(server);
3774
4245
  registerSmartCaptureTools(server);
3775
4246
  registerWorkflowTools(server);
3776
4247
  if (enabledModules.has("gitchain")) registerGitChainTools(server);
4248
+ if (enabledModules.has("gitchain")) registerMapTools(server);
3777
4249
  if (enabledModules.has("arch")) registerArchitectureTools(server);
4250
+ registerStartTools(server);
3778
4251
  registerResources(server);
3779
4252
  registerPrompts(server);
3780
4253
  var transport = new StdioServerTransport();
3781
4254
  await server.connect(transport);
4255
+ getWorkspaceId().then(async (wsId) => {
4256
+ trackSessionStarted(wsId, SERVER_VERSION);
4257
+ try {
4258
+ await startAgentSession();
4259
+ process.stderr.write("[MCP] Agent session started automatically.\n");
4260
+ } catch (err) {
4261
+ process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call agent-start manually.
4262
+ `);
4263
+ await recoverSessionState();
4264
+ }
4265
+ }).catch(() => {
4266
+ process.stderr.write("[MCP] Workspace resolution deferred \u2014 will retry on first tool call.\n");
4267
+ });
3782
4268
  async function gracefulShutdown() {
4269
+ if (getAgentSessionId()) {
4270
+ await orphanAgentSession();
4271
+ }
3783
4272
  await shutdownAnalytics();
3784
4273
  process.exit(0);
3785
4274
  }
3786
4275
  process.on("SIGINT", gracefulShutdown);
3787
4276
  process.on("SIGTERM", gracefulShutdown);
4277
+ process.stdin.on("end", async () => {
4278
+ if (getAgentSessionId()) {
4279
+ await orphanAgentSession();
4280
+ }
4281
+ await shutdownAnalytics();
4282
+ process.exit(0);
4283
+ });
3788
4284
  //# sourceMappingURL=index.js.map