@productbrain/mcp 0.0.1-beta.55 → 0.0.1-beta.56

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.
@@ -423,8 +423,7 @@ function registerEntriesTools(server) {
423
423
  title: "Entries",
424
424
  description: 'Read entries from the Chain. One tool for all entry reading.\n\n- **list**: Browse entries with optional filters (collection, status, tag, label). Use collections action=list first to discover slugs.\n- **get**: Fetch a single entry by ID \u2014 full record with data, labels, relations, history.\n- **batch**: Fetch multiple entries (max 20) in one call. Same shape as get per entry.\n- **search**: Full-text search across entries. Scope by collection or filter by status.\n\nUse `entries action=get entryId="..."` to fetch one. Use `entries action=search query="..."` to discover.',
425
425
  inputSchema: entriesSchema,
426
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
427
- _meta: { ui: { resourceUri: "ui://entries/entry-cards.html" } }
426
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
428
427
  },
429
428
  withEnvelope(async (args) => {
430
429
  const parsed = parseOrFail(entriesSchema, args);
@@ -776,8 +775,7 @@ function registerGraphTools(server) {
776
775
  title: "Graph",
777
776
  description: "Read from the knowledge graph. Two actions:\n\n- **find**: Traverse relations from a specific entry \u2014 shows incoming/outgoing connections. Use after entries action=get to explore connections. This is NOT full-text search (use entries action=search).\n- **suggest**: Discover potential relations for an entry using graph-aware intelligence. Returns ranked suggestions with confidence scores. Use relations action=create or relations action=batch-create to create the ones that make sense.",
778
777
  inputSchema: graphSchema,
779
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
780
- _meta: { ui: { resourceUri: "ui://graph/constellation.html" } }
778
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
781
779
  },
782
780
  withEnvelope(async (args) => {
783
781
  const parsed = parseOrFail(graphSchema, args);
@@ -1213,8 +1211,7 @@ function registerContextTools(server) {
1213
1211
  title: "Context",
1214
1212
  description: "Assemble knowledge context in one call. Two actions:\n\n- **gather**: Three modes \u2014 (1) By entry: traverse the graph around a specific entry. (2) By task: auto-load relevant domain knowledge for a natural-language task. (3) Graph mode (entryId + mode='graph'): enhanced traversal with provenance paths.\n- **build**: Structured build spec for any entry \u2014 data, related entries, business rules, glossary terms, chain refs. Use when starting a build. Requires active session.",
1215
1213
  inputSchema: contextSchema,
1216
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
1217
- _meta: { ui: { resourceUri: "ui://graph/constellation.html" } }
1214
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
1218
1215
  },
1219
1216
  withEnvelope(async (args) => {
1220
1217
  const parsed = parseOrFail(contextSchema, args);
@@ -3249,7 +3246,11 @@ When reviewing test results: (a) test staleness \u2014 code changed, test not up
3249
3246
  summaryCapture: {
3250
3247
  createsPrimaryRecord: true,
3251
3248
  nameTemplate: "Implementation Review \u2014 {date}",
3252
- descriptionField: "description"
3249
+ descriptionField: "description",
3250
+ descriptionSource: {
3251
+ mode: "step-output-field",
3252
+ field: "synthesis"
3253
+ }
3253
3254
  }
3254
3255
  },
3255
3256
  runtime: {
@@ -3367,7 +3368,11 @@ var PROCESS_CHANGE_WORKFLOW_DESCRIPTOR = {
3367
3368
  },
3368
3369
  summaryCapture: {
3369
3370
  createsPrimaryRecord: true,
3370
- nameTemplate: "{whatAndWhy}"
3371
+ nameTemplate: "{whatAndWhy}",
3372
+ descriptionSource: {
3373
+ mode: "step-output-field",
3374
+ field: "whatAndWhy"
3375
+ }
3371
3376
  }
3372
3377
  },
3373
3378
  runtime: {
@@ -3472,7 +3477,8 @@ var workflowsSchema = z11.object({
3472
3477
  isFinal: z11.boolean().optional().describe("If true, finalize an existing durable workflow run from its terminal round and create the summary chain entry."),
3473
3478
  restart: z11.boolean().optional().describe("If true, start a new durable run from the workflow's first round in the current session."),
3474
3479
  summaryName: z11.string().optional().describe("Optional name for final chain entry. If omitted, the workflow summary template is used."),
3475
- summaryDescription: z11.string().optional().describe("Optional override for the final chain entry description. Defaults to the final round output text.")
3480
+ summaryDescription: z11.string().optional().describe("Optional override for the final chain entry description. Defaults to the final round output text."),
3481
+ summaryEntryId: z11.string().optional().describe("Link an existing entry as the run's summary instead of creating one. Used by facilitated workflows (e.g. shape) where the primary record is created by the specialized tool.")
3476
3482
  });
3477
3483
  function formatWorkflowCard(wf) {
3478
3484
  const roundList = wf.rounds.map((r) => ` ${r.num}. ${r.label} (${r.type}, ~${r.maxDurationHint ?? "?"})`).join("\n");
@@ -3519,7 +3525,8 @@ function registerWorkflowTools(server) {
3519
3525
  isFinal,
3520
3526
  restart,
3521
3527
  summaryName,
3522
- summaryDescription
3528
+ summaryDescription,
3529
+ summaryEntryId
3523
3530
  } = parsed.data;
3524
3531
  return runWithToolContext({ tool: "workflows", action }, async () => {
3525
3532
  if (action === "list") {
@@ -3546,7 +3553,8 @@ function registerWorkflowTools(server) {
3546
3553
  isFinal,
3547
3554
  restart,
3548
3555
  summaryName,
3549
- summaryDescription
3556
+ summaryDescription,
3557
+ summaryEntryId
3550
3558
  );
3551
3559
  }
3552
3560
  return unknownAction(action, WORKFLOWS_ACTIONS);
@@ -3654,7 +3662,7 @@ async function handleGetRun(runId, workflowId) {
3654
3662
  )
3655
3663
  };
3656
3664
  }
3657
- async function handleCheckpoint(workflowId, roundId, output, isFinal, restart, summaryName, summaryDescription) {
3665
+ async function handleCheckpoint(workflowId, roundId, output, isFinal, restart, summaryName, summaryDescription, summaryEntryId) {
3658
3666
  const wf = getWorkflow(workflowId);
3659
3667
  if (!wf) {
3660
3668
  return {
@@ -3727,9 +3735,9 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3727
3735
  descriptionField: wf.kbOutputTemplate.descriptionField,
3728
3736
  descriptionSource: "final-output-text"
3729
3737
  } : null;
3730
- if (!summaryCapture) {
3738
+ if (!summaryCapture && !summaryEntryId) {
3731
3739
  return validationResult(
3732
- `Workflow '${workflowId}' does not support final summary capture through workflows.checkpoint. Follow the workflow guidance and use capture directly for its primary record.`
3740
+ `Workflow '${workflowId}' does not support final summary capture through workflows.checkpoint. Follow the workflow guidance and use capture directly for its primary record, or pass summaryEntryId to link an existing entry.`
3733
3741
  );
3734
3742
  }
3735
3743
  requireWriteAccess();
@@ -3739,6 +3747,17 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3739
3747
  "Active agent session required for workflow checkpoint persistence."
3740
3748
  );
3741
3749
  }
3750
+ if (summaryEntryId && !summaryCapture) {
3751
+ return handleFacilitatedFinalization(
3752
+ wf,
3753
+ descriptor,
3754
+ roundId,
3755
+ normalizedOutput,
3756
+ agentSessionId,
3757
+ summaryEntryId,
3758
+ lines
3759
+ );
3760
+ }
3742
3761
  const existingRun = await mcpQuery(
3743
3762
  "chainwork.getLatestWorkflowRun",
3744
3763
  {
@@ -3868,7 +3887,8 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3868
3887
  output: normalizedOutput,
3869
3888
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
3870
3889
  },
3871
- restart
3890
+ restart,
3891
+ ...summaryEntryId ? { summaryEntryId } : {}
3872
3892
  });
3873
3893
  lines.push(
3874
3894
  `Round ${round.num} output recorded.`,
@@ -3877,7 +3897,12 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3877
3897
  );
3878
3898
  const nextWorkflowRound = checkpointResult.projection.currentRoundId ? wf.rounds.find((workflowRound) => workflowRound.id === checkpointResult.projection.currentRoundId) : void 0;
3879
3899
  const nextRound = nextWorkflowRound ? { id: nextWorkflowRound.id, num: nextWorkflowRound.num, label: nextWorkflowRound.label } : void 0;
3880
- if (nextWorkflowRound) {
3900
+ if (checkpointResult.summary?.linked) {
3901
+ lines.push(
3902
+ "",
3903
+ `**Run completed.** Summary linked to existing entry \`${checkpointResult.summary.entryId}\`.`
3904
+ );
3905
+ } else if (nextWorkflowRound) {
3881
3906
  lines.push(
3882
3907
  "",
3883
3908
  `**Next**: Round ${nextWorkflowRound.num} \u2014 ${nextWorkflowRound.label}`,
@@ -3927,6 +3952,81 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3927
3952
  }
3928
3953
  }
3929
3954
  }
3955
+ async function handleFacilitatedFinalization(wf, descriptor, roundId, normalizedOutput, agentSessionId, summaryEntryId, lines) {
3956
+ const template = descriptor ? getWorkflowTemplateMetadata(descriptor) : { slug: wf.id, name: wf.name };
3957
+ try {
3958
+ const checkpointResult = await mcpMutation("chainwork.recordWorkflowCheckpoint", {
3959
+ agentSessionId,
3960
+ workflowId: wf.id,
3961
+ workflowName: wf.name,
3962
+ templateSlug: template.slug,
3963
+ templateName: template.name,
3964
+ steps: wf.rounds.map((workflowRound) => ({
3965
+ roundId: workflowRound.id,
3966
+ num: workflowRound.num,
3967
+ label: workflowRound.label,
3968
+ type: workflowRound.type,
3969
+ outputField: workflowRound.outputSchema.field,
3970
+ outputFormat: workflowRound.outputSchema.format
3971
+ })),
3972
+ checkpoint: {
3973
+ roundId,
3974
+ output: normalizedOutput,
3975
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
3976
+ },
3977
+ summaryEntryId
3978
+ });
3979
+ const linked = checkpointResult.summary?.linked ?? false;
3980
+ lines.push(
3981
+ `**Run finalized**: Linked to existing entry \`${summaryEntryId}\`.`,
3982
+ `Status: ${checkpointResult.run.status}`,
3983
+ `Progress: ${checkpointResult.projection.completedCount}/${checkpointResult.projection.totalCount}`
3984
+ );
3985
+ if (!linked && checkpointResult.run.status !== "completed") {
3986
+ lines.push(
3987
+ "",
3988
+ `Note: The run is not yet completed \u2014 the summary will be linked when all rounds are checkpointed.`
3989
+ );
3990
+ }
3991
+ return {
3992
+ content: [{ type: "text", text: lines.join("\n") }],
3993
+ structuredContent: success(
3994
+ `Workflow ${wf.id} finalized with linked entry ${summaryEntryId}.`,
3995
+ {
3996
+ entryId: summaryEntryId,
3997
+ workflowId: wf.id,
3998
+ roundId,
3999
+ isFinal: true,
4000
+ runId: checkpointResult.runId,
4001
+ linked,
4002
+ runStatus: checkpointResult.run.status
4003
+ },
4004
+ [
4005
+ {
4006
+ tool: "graph",
4007
+ description: "Discover connections",
4008
+ parameters: { action: "suggest", entryId: summaryEntryId }
4009
+ }
4010
+ ]
4011
+ )
4012
+ };
4013
+ } catch (err) {
4014
+ const msg = err instanceof Error ? err.message : String(err);
4015
+ lines.push(
4016
+ `**Finalization failed**: ${msg}`,
4017
+ "",
4018
+ `The workflow output is preserved in this conversation.`
4019
+ );
4020
+ return {
4021
+ content: [{ type: "text", text: lines.join("\n") }],
4022
+ structuredContent: failure(
4023
+ "BACKEND_ERROR",
4024
+ `Facilitated finalization failed: ${msg}.`,
4025
+ "The facilitate tool's state is preserved \u2014 retry the checkpoint or continue in conversation."
4026
+ )
4027
+ };
4028
+ }
4029
+ }
3930
4030
  function coerceWorkflowRunOutput(round, output) {
3931
4031
  if (typeof output !== "string") {
3932
4032
  assertWorkflowRunOutputMatchesRound(round, output);
@@ -8572,1118 +8672,527 @@ Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntr
8572
8672
  }
8573
8673
 
8574
8674
  // src/tools/architecture.ts
8575
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
8576
- import { resolve as resolve3, relative, dirname as dirname2, normalize } from "path";
8675
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
8676
+ import { resolve as resolve2, relative, dirname, normalize } from "path";
8577
8677
  import { z as z19 } from "zod";
8578
-
8579
- // src/resources/index.ts
8580
- import { existsSync as existsSync2 } from "fs";
8581
- import { readFile } from "fs/promises";
8582
- import { dirname, join, resolve as resolve2 } from "path";
8583
- import { fileURLToPath } from "url";
8584
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8585
- var UI_VIEWS = {
8586
- "ui://entries/entry-cards.html": "src/entry-cards/index.html",
8587
- "ui://graph/constellation.html": "src/graph-constellation/index.html"
8588
- };
8589
- var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
8590
- var MODULE_DIR = dirname(fileURLToPath(import.meta.url));
8591
- var UI_VIEW_BASE_CANDIDATES = [
8592
- resolve2(MODULE_DIR, "views"),
8593
- resolve2(MODULE_DIR, "..", "views"),
8594
- resolve2(MODULE_DIR, "..", "..", "dist", "views"),
8595
- resolve2(MODULE_DIR, "..", "..", "..", "mcp-views", "dist")
8678
+ var COLLECTION_SLUG = "architecture";
8679
+ var COLLECTION_FIELDS = [
8680
+ { key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
8681
+ { key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
8682
+ { key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
8683
+ { key: "description", label: "Description", type: "text", searchable: true },
8684
+ { key: "color", label: "Color", type: "text" },
8685
+ { key: "icon", label: "Icon", type: "text" },
8686
+ { key: "sourceNode", label: "Source Node (flows)", type: "text" },
8687
+ { key: "targetNode", label: "Target Node (flows)", type: "text" },
8688
+ { key: "filePaths", label: "File Paths", type: "text", searchable: true },
8689
+ { key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
8690
+ { key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
8691
+ { key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
8692
+ { key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
8596
8693
  ];
8597
- function resolveUiViewPath(filePath, options) {
8598
- const moduleDir = options?.moduleDir ?? MODULE_DIR;
8599
- const pathExists = options?.pathExists ?? existsSync2;
8600
- const candidateBases = options?.moduleDir ? [
8601
- resolve2(moduleDir, "views"),
8602
- resolve2(moduleDir, "..", "views"),
8603
- resolve2(moduleDir, "..", "..", "dist", "views"),
8604
- resolve2(moduleDir, "..", "..", "..", "mcp-views", "dist")
8605
- ] : UI_VIEW_BASE_CANDIDATES;
8606
- for (const candidateBase of candidateBases) {
8607
- const candidatePath = join(candidateBase, filePath);
8608
- if (pathExists(candidatePath)) {
8609
- return candidatePath;
8694
+ var ARCHITECTURE_CANONICAL_KEY = "architecture_note";
8695
+ async function ensureCollection() {
8696
+ const collections = await mcpQuery("chain.listCollections");
8697
+ const existing = collections.find((c) => c.slug === COLLECTION_SLUG);
8698
+ if (existing) {
8699
+ if (!existing.defaultCanonicalKey) {
8700
+ await mcpMutation("chain.updateCollection", {
8701
+ slug: COLLECTION_SLUG,
8702
+ defaultCanonicalKey: ARCHITECTURE_CANONICAL_KEY
8703
+ });
8610
8704
  }
8705
+ return;
8611
8706
  }
8612
- return null;
8707
+ await mcpMutation("chain.createCollection", {
8708
+ slug: COLLECTION_SLUG,
8709
+ name: "Architecture",
8710
+ icon: "\u{1F3D7}\uFE0F",
8711
+ description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
8712
+ fields: COLLECTION_FIELDS
8713
+ });
8613
8714
  }
8614
- function renderMissingUiView(uri) {
8615
- return `<!DOCTYPE html>
8616
- <html lang="en">
8617
- <head>
8618
- <meta charset="UTF-8" />
8619
- <meta name="viewport" content="width=device-width, initial-scale=1" />
8620
- <title>Product Brain View Unavailable</title>
8621
- <style>
8622
- body {
8623
- margin: 0;
8624
- padding: 20px;
8625
- font-family: system-ui, sans-serif;
8626
- background: #0f172a;
8627
- color: #e2e8f0;
8628
- }
8629
- .panel {
8630
- max-width: 720px;
8631
- margin: 0 auto;
8632
- padding: 20px;
8633
- border: 1px solid #334155;
8634
- border-radius: 12px;
8635
- background: #111827;
8636
- }
8637
- h1 {
8638
- margin: 0 0 12px;
8639
- font-size: 18px;
8640
- }
8641
- p {
8642
- margin: 0 0 10px;
8643
- line-height: 1.5;
8644
- }
8645
- code {
8646
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
8647
- color: #93c5fd;
8648
- }
8649
- </style>
8650
- </head>
8651
- <body>
8652
- <main class="panel">
8653
- <h1>Product Brain view unavailable</h1>
8654
- <p>The UI resource <code>${uri}</code> could not be loaded.</p>
8655
- <p>If this is a local checkout, rebuild the MCP views bundle. If this is an installed package, the published package is missing its bundled views.</p>
8656
- </main>
8657
- </body>
8658
- </html>`;
8715
+ async function listArchEntries() {
8716
+ return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
8659
8717
  }
8660
- function formatEntryMarkdown(entry) {
8661
- const id = entry.entryId ? `${entry.entryId}: ` : "";
8662
- const lines = [`## ${id}${entry.name} [${entry.status}]`];
8663
- if (entry.data && typeof entry.data === "object") {
8664
- for (const [key, val] of Object.entries(entry.data)) {
8665
- if (val && key !== "rawData") {
8666
- lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
8667
- }
8668
- }
8669
- }
8670
- return lines.join("\n");
8718
+ function byTag(entries, archType) {
8719
+ return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
8671
8720
  }
8672
- function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles) {
8673
- const sections = ["# Product Brain \u2014 Orientation"];
8674
- sections.push(
8675
- "## About Product Brain (PB)\nPB is a knowledge management system used by many product teams. This section describes how PB works \u2014 not your product.\n\n### How PB Organizes Its Tools\n- **Tools** = actions with side-effects or dynamic computation (capture, commit, search).\n- **Resources** = stable, read-only data (orientation, terminology, collection schemas).\n- **Prompts** = multi-step choreography (workflows, guided capture, deep dives).\n- Every tool, resource, and prompt works for ANY workspace \u2014 no bespoke logic.\n- Resource templates (URI params) serve workspace-specific data through generic patterns.\n- Compound tools with `action` enums keep the tool count minimal.\n\n### How PB Handles Your Data\n- **Draft-first**: all writes create drafts. SSOT requires explicit user confirmation.\n- **Empty-workspace safe**: every operation handles zero entries and fresh workspaces.\n- **Advisory, not blocking**: quality scores, contradiction checks, and coaching never prevent operations.\n- **Workspace-agnostic**: PB is a product for many teams \u2014 no workspace-specific logic.\n- **Self-documenting**: orient and server instructions teach agents how PB works."
8676
- );
8677
- const wsRules = [];
8678
- for (const p of (principles ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
8679
- wsRules.push({ id: p.entryId ?? "", name: p.name, severity: p.data?.severity ?? void 0, source: "principles" });
8680
- }
8681
- for (const s of (standards ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
8682
- wsRules.push({ id: s.entryId ?? "", name: s.name, severity: s.data?.severity ?? void 0, source: "standards" });
8683
- }
8684
- for (const r of (businessRules ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
8685
- wsRules.push({ id: r.entryId ?? "", name: r.name, severity: r.data?.severity ?? void 0, source: "business-rules" });
8686
- }
8687
- if (wsRules.length > 0) {
8688
- const ruleLines = wsRules.map((r) => {
8689
- const sev = r.severity ? ` [${r.severity}]` : "";
8690
- return `- **${r.id}**: ${r.name}${sev}`;
8691
- }).join("\n");
8692
- sections.push(
8693
- `## Your Workspace Principles & Rules (${wsRules.length} active)
8694
- These are principles, standards, and rules your team has committed to the Chain. Respect them during implementation.
8721
+ function formatLayerText(layer, nodes) {
8722
+ const layerNodes = nodes.filter((n) => n.data?.layerRef === layer.entryId);
8723
+ const nodeList = layerNodes.map((n) => {
8724
+ const desc = n.data?.description ? ` \u2014 ${n.data.description}` : "";
8725
+ const owner = n.data?.owner ? ` (${n.data.owner})` : "";
8726
+ return ` - ${n.data?.icon ?? "\u25FB"} **${n.name}**${desc}${owner}`;
8727
+ }).join("\n");
8728
+ return `### ${layer.name}
8729
+ ${layer.data?.description ?? ""}
8695
8730
 
8696
- ` + ruleLines + '\n\nUse `entries action=get entryId="<ID>"` to drill into any rule before making changes in that area.'
8697
- );
8698
- } else {
8699
- sections.push(
8700
- "## Your Workspace Principles & Rules\nNo active principles, standards, or business rules on the Chain yet.\nUse `capture` with collection `principles`, `standards`, or `business-rules` to add your team's guardrails.\nOnce committed, they appear here at orient time \u2014 so every agent session starts with your rules visible."
8701
- );
8731
+ ${nodeList || " _No components_"}`;
8732
+ }
8733
+ var SEED_TEMPLATE = {
8734
+ entryId: "ARCH-tpl-product-os",
8735
+ name: "Product OS Default",
8736
+ data: {
8737
+ archType: "template",
8738
+ description: "Default 4-layer architecture: Auth \u2192 Infrastructure \u2192 Core \u2192 Features, with an outward Integration layer",
8739
+ layerOrder: JSON.stringify(["auth", "infrastructure", "core", "features", "integration"])
8702
8740
  }
8703
- sections.push(
8704
- "## 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 context gathering returns richer results \u2192 AI processes have more context \u2192 quality scores improve \u2192 next commit is smarter than the last."
8705
- );
8706
- sections.push(
8707
- "## 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)."
8708
- );
8709
- if (collections) {
8710
- const collList = collections.map((c) => {
8711
- const prefix = c.icon ? `${c.icon} ` : "";
8712
- return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
8713
- }).join("\n");
8714
- sections.push(
8715
- `## Data Model (${collections.length} collections)
8716
- Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
8717
- The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
8718
- Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
8741
+ };
8742
+ var SEED_LAYERS = [
8743
+ { entryId: "ARCH-layer-auth", name: "Auth", order: 0, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#22c55e", description: "Authentication, user management, and workspace-scoped access control", icon: "\u{1F510}", dependsOn: "none", rationale: "Foundation layer. Auth depends on nothing \u2014 it is the first gate. No layer may bypass auth. All other layers depend on auth to know who the user is and which workspace they belong to." } },
8744
+ { entryId: "ARCH-layer-infra", name: "Infrastructure", order: 1, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#ec4899", description: "Real-time data, event tracking, and AI model infrastructure", icon: "\u2699\uFE0F", dependsOn: "Auth", rationale: "Infrastructure sits on top of Auth. It provides the database, analytics, and AI plumbing that Core and Features consume. Infra may import from Auth (needs workspace context) but never from Core or Features." } },
8745
+ { entryId: "ARCH-layer-core", name: "Core", order: 2, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#8b5cf6", description: "Business logic, knowledge graph, workflow engines, and MCP tooling", icon: "\u{1F9E0}", dependsOn: "Auth, Infrastructure", rationale: "Core contains business logic and engines that are UI-agnostic. It may import from Auth and Infra. Features depend on Core, but Core must never depend on Features \u2014 this is what keeps engines reusable across different UIs." } },
8746
+ { entryId: "ARCH-layer-features", name: "Features", order: 3, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#6366f1", description: "User-facing pages, components, and feature modules", icon: "\u2726", dependsOn: "Auth, Infrastructure, Core", rationale: "Features are the user-facing layer \u2014 SvelteKit routes, components, and page-level logic. Features may import from any lower layer but nothing above may import from Features. This is the outermost application layer." } },
8747
+ { entryId: "ARCH-layer-integration", name: "Integration", order: 4, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#f59e0b", description: "Outward connections to external tools, IDEs, and services", icon: "\u{1F50C}", dependsOn: "Core", rationale: "Integration is a lateral/outward layer \u2014 it connects to external systems (IDE, GitHub, Linear). It depends on Core (to expose knowledge) but sits outside the main stack. External tools call into Core via Integration, never directly into Features." } }
8748
+ ];
8749
+ var SEED_NODES = [
8750
+ // Auth layer
8751
+ { entryId: "ARCH-node-clerk", name: "Clerk", order: 0, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F511}", description: "Authentication provider \u2014 sign-in/sign-up pages, session management, organization-level access control. UserSync.svelte is cross-cutting layout glue (not mapped here).", filePaths: "src/routes/sign-in/, src/routes/sign-up/", owner: "Platform", rationale: "Auth layer because Clerk is the identity gate. Every request flows through auth first. No other layer provides identity." } },
8752
+ { entryId: "ARCH-node-workspace", name: "Workspace Scoping", order: 1, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F3E2}", description: "Multi-tenancy anchor \u2014 all data is workspace-scoped via workspaceId", filePaths: "src/lib/stores/workspace.ts, convex/workspaces.ts", owner: "Platform", rationale: "Auth layer because workspace scoping is the second gate after identity. All queries require workspaceId, making this foundational." } },
8753
+ // Infrastructure layer
8754
+ { entryId: "ARCH-node-convex", name: "Convex", order: 0, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u26A1", description: "Reactive database with real-time sync, serverless functions, and type-safe API generation. Unified Collections + Entries model", filePaths: "convex/schema.ts, convex/entries.ts, convex/http.ts", owner: "Platform", rationale: "Infrastructure because Convex is raw persistence and reactivity plumbing. It stores data but has no business logic opinions. Core and Features consume it." } },
8755
+ { entryId: "ARCH-node-posthog", name: "PostHog", order: 1, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F4CA}", description: "Product analytics \u2014 workspace-scoped events, feature flags, session replay", filePaths: "src/lib/analytics.ts, src/lib/components/PostHogWorkspaceSync.svelte", owner: "Platform", rationale: "Infrastructure because PostHog is analytics plumbing \u2014 event collection and aggregation. It has no knowledge of business domains." } },
8756
+ { entryId: "ARCH-node-openrouter", name: "OpenRouter", order: 2, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F916}", description: "AI model routing for ChainWork artifact generation \u2014 streaming responses with format-aware prompts", filePaths: "src/routes/api/chainwork/generate/+server.ts", owner: "ChainWork", rationale: "Infrastructure because OpenRouter is an AI model gateway \u2014 it routes prompts to models. The strategy logic lives in ChainWork Engine (Core); this is just the pipe." } },
8757
+ // Core layer
8758
+ { entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "~19 modular tools exposing the knowledge graph as AI-consumable operations \u2014 capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
8759
+ { entryId: "ARCH-node-knowledge-graph", name: "Knowledge Graph", order: 1, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F578}\uFE0F", description: "20 collections, 170+ entries with typed cross-collection relations. Smart capture, auto-linking, quality scoring", filePaths: "convex/mcpKnowledge.ts, convex/entries.ts", owner: "Knowledge", rationale: "Core layer because the knowledge graph IS the domain model \u2014 collections, entries, relations, versioning, quality scoring. Glossary DATA, business rules DATA, tension DATA all live here. Why not Features? Because the data model exists independently of any page. The Glossary page in Features is just one way to visualize terms that Core owns. Think: Core owns the dictionary, Features owns the dictionary app." } },
8760
+ { entryId: "ARCH-node-governance", name: "Governance Engine", order: 2, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u2696\uFE0F", description: "Circles, roles, consent-based decision-making, tension processing with IDM-inspired async workflows", filePaths: "convex/versioning.ts, src/lib/components/versioning/", owner: "Governance", rationale: "Core layer because governance logic (draft\u2192publish workflows, consent-based decisions, tension status rules) is business process that multiple UIs consume. Why not Features? Because the versioning system and proposal flow are reusable engines \u2014 the Governance Pages in Features are just one rendering of these rules." } },
8761
+ { entryId: "ARCH-node-chainwork-engine", name: "ChainWork Engine", order: 3, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u26D3", description: "Guided strategy creation through 5-step coherence chain \u2014 AI-generated artifacts with scoring and achievements", filePaths: "src/lib/components/chainwork/config.ts, src/lib/components/chainwork/scoring.ts", owner: "ChainWork", rationale: "Core layer because the coherence chain logic, scoring algorithm, and quality gates are business rules. config.ts defines chain steps, scoring.ts computes quality \u2014 these could power a CLI or API. Why not Features? Because the ChainWork UI wizard in Features is just one skin over this engine." } },
8762
+ // Features layer
8763
+ { entryId: "ARCH-node-command-center", name: "Command Center", order: 0, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u2B21", description: "Calm home screen \u2014 daily orientation, triage mode, pulse metrics, momentum tracking", filePaths: "src/routes/+page.svelte, src/lib/components/command-center/", owner: "Command Center", rationale: "Features layer because the Command Center is a SvelteKit page \u2014 PulseMetrics, NeedsAttention, DailyBriefing are UI components that assemble data from Core queries. Why not Core? Because it has no reusable business logic or engines \u2014 it is pure layout and presentation. If you deleted this page, no business rule would break." } },
8764
+ { entryId: "ARCH-node-chainwork-ui", name: "ChainWork UI", order: 1, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26D3", description: "Multi-step wizard for strategy artifact creation \u2014 setup, chain steps, quality gates, output", filePaths: "src/routes/chainwork/, src/lib/components/chainwork/", owner: "ChainWork", rationale: "Features layer because the ChainWork UI is the wizard interface \u2014 step navigation, form inputs, output rendering. Why not Core? Because the scoring logic and chain config ARE in Core (ChainWork Engine). This is the presentation skin over that engine. Delete this page and the engine still works via MCP." } },
8765
+ { entryId: "ARCH-node-glossary", name: "Glossary", order: 2, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "Aa", description: "Canonical vocabulary management \u2014 term detail, code drift detection, inline term linking", filePaths: "src/routes/glossary/, src/lib/components/glossary/", owner: "Knowledge", rationale: "Features layer \u2014 NOT Core or Infrastructure \u2014 because this is the Glossary PAGE: the SvelteKit route for browsing, editing, and viewing terms. Why not Core? Because Core owns the glossary DATA (Knowledge Graph) and term-linking logic. The MCP server also accesses glossary terms without any page. Why not Infrastructure? Because glossary is domain-specific vocabulary, not generic plumbing. The page is one consumer of the data \u2014 Core owns the dictionary, Features owns the dictionary app." } },
8766
+ { entryId: "ARCH-node-tensions", name: "Tensions", order: 3, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26A1", description: "Tension capture and processing \u2014 raise, triage, resolve through governance workflow", filePaths: "src/routes/tensions/, src/lib/components/tensions/", owner: "Governance", rationale: "Features layer because this is the Tensions PAGE \u2014 the UI for raising, listing, and viewing tensions. Why not Core? Because tension data, status rules (SOS-020), and processing logic already live in Core (Governance Engine). This page is a form + list view that reads from and writes to Core. Delete it and the governance engine still processes tensions via MCP." } },
8767
+ { entryId: "ARCH-node-strategy", name: "Strategy", order: 4, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25A3", description: "Product strategy pages \u2014 vision, ecosystem, product areas, decision frameworks, sequencing", filePaths: "src/routes/strategy/, src/routes/bridge/, src/routes/topology/", owner: "Strategy", rationale: "Features layer because Strategy is a set of SvelteKit pages (strategy, bridge, topology) that visualize strategy entries. Why not Core? Because the strategy data (vision, principles, ecosystem layers) lives in the Knowledge Graph (Core). These pages render and allow inline editing \u2014 they consume Core downward." } },
8768
+ { entryId: "ARCH-node-governance-ui", name: "Governance Pages", order: 5, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25C8", description: "Roles, circles, principles, policies, decisions, proposals, business rules", filePaths: "src/routes/roles/, src/routes/circles/, src/routes/decisions/, src/routes/proposals/", owner: "Governance", rationale: "Features layer because these are governance PAGES \u2014 list views and detail views for roles, circles, decisions, proposals. Why not Core? Because the governance ENGINE (versioning, consent, IDM) IS in Core. These pages are the user-facing window into governance data. The logic doesn't live here, only the rendering." } },
8769
+ { entryId: "ARCH-node-artifacts", name: "Artifacts", order: 6, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u{1F4C4}", description: "Strategy artifacts from ChainWork \u2014 pitches, briefs, one-pagers linked to the knowledge graph", filePaths: "src/routes/artifacts/", owner: "ChainWork", rationale: "Features layer because the Artifacts page is a list/detail view for strategy artifacts produced by ChainWork. Why not Core? Because artifact data and scoring live in Core (ChainWork Engine). This page just renders the output and links back to the knowledge graph." } },
8770
+ // Integration layer
8771
+ { entryId: "ARCH-node-cursor", name: "Cursor IDE", order: 0, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F5A5}\uFE0F", description: "AI-assisted development with MCP-powered knowledge context \u2014 smart capture, verification, context assembly in the editor", filePaths: ".cursor/mcp.json, .cursor/rules/", owner: "AI DX", rationale: "Integration layer because Cursor is an external tool that connects INTO our system via MCP. Why not Core or Features? Because Cursor itself is not our code \u2014 it's a consumer. The .cursor/ config files define how it talks to us, but Cursor lives outside our deployment boundary." } },
8772
+ { entryId: "ARCH-node-github", name: "GitHub", order: 1, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F419}", description: "Code repository, PR reviews with governance context, CI/CD", owner: "Platform", rationale: "Integration layer because GitHub is an external service. Why not Infrastructure? Because Infra is about plumbing we control (database, analytics). GitHub is a third-party that hooks into our Core (PR reviews checking governance rules) but is not part of our deployed application." } },
8773
+ { entryId: "ARCH-node-linear", name: "Linear (planned)", order: 2, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F4D0}", description: "Issue tracking, roadmap sync, tension-to-issue pipeline (planned integration)", owner: "Platform", rationale: "Integration layer because Linear is an external issue tracker. Why not Infrastructure? Because Infra is generic plumbing we run. Linear is a third-party tool we connect to. The planned pipeline bridges tensions (Core) to Linear issues \u2014 a classic outward integration pattern." } }
8774
+ ];
8775
+ var SEED_FLOWS = [
8776
+ { entryId: "ARCH-flow-smart-capture", name: "Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
8777
+ { entryId: "ARCH-flow-governance", name: "Governance Flow", order: 1, data: { archType: "flow", sourceNode: "ARCH-node-tensions", targetNode: "ARCH-node-governance", description: "Tension raised \u2192 appears in Command Center \u2192 triaged \u2192 processed via IDM \u2192 decision logged", color: "#6366f1" } },
8778
+ { entryId: "ARCH-flow-chainwork", name: "ChainWork Strategy Flow", order: 2, data: { archType: "flow", sourceNode: "ARCH-node-chainwork-ui", targetNode: "ARCH-node-artifacts", description: "Leader opens ChainWork \u2192 walks coherence chain \u2192 AI generates artifact \u2192 scored and published to knowledge graph", color: "#f59e0b" } },
8779
+ { entryId: "ARCH-flow-knowledge-trust", name: "Knowledge Trust Flow", order: 3, data: { archType: "flow", sourceNode: "ARCH-node-mcp", targetNode: "ARCH-node-glossary", description: "MCP verify tool checks entries against codebase \u2192 file existence, schema references validated \u2192 trust scores updated", color: "#22c55e" } },
8780
+ { entryId: "ARCH-flow-analytics", name: "Analytics Flow", order: 4, data: { archType: "flow", sourceNode: "ARCH-node-command-center", targetNode: "ARCH-node-posthog", description: "Feature views and actions tracked \u2192 workspace-scoped events \u2192 PostHog group analytics \u2192 Command Center metrics", color: "#ec4899" } }
8781
+ ];
8782
+ var architectureSchema = z19.object({
8783
+ action: z19.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
8784
+ template: z19.string().optional().describe("Template entry ID to filter by (for show)"),
8785
+ layer: z19.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
8786
+ flow: z19.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
8787
+ });
8788
+ var architectureAdminSchema = z19.object({
8789
+ action: z19.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
8790
+ });
8791
+ function registerArchitectureTools(server) {
8792
+ server.registerTool(
8793
+ "architecture",
8794
+ {
8795
+ title: "Architecture",
8796
+ 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",
8797
+ inputSchema: architectureSchema,
8798
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
8799
+ },
8800
+ withEnvelope(async ({ action, template, layer, flow }) => {
8801
+ await ensureCollection();
8802
+ const all = await listArchEntries();
8803
+ if (action === "show") {
8804
+ const templates = byTag(all, "template");
8805
+ const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
8806
+ const templateName = activeTemplate?.name ?? "System Architecture";
8807
+ const templateId = activeTemplate?.entryId;
8808
+ const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
8809
+ const nodes = byTag(all, "node");
8810
+ const flows = byTag(all, "flow");
8811
+ if (layers.length === 0) {
8812
+ return {
8813
+ content: [{
8814
+ type: "text",
8815
+ text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
8816
+ }],
8817
+ structuredContent: failure(
8818
+ "NOT_FOUND",
8819
+ "No architecture data found.",
8820
+ "Seed the default architecture first.",
8821
+ [{ tool: "architecture-admin", description: "Seed architecture", parameters: { action: "seed" } }]
8822
+ )
8823
+ };
8824
+ }
8825
+ const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
8826
+ const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
8827
+ (f) => `- **${f.name}**: ${f.data?.description ?? ""}`
8828
+ ).join("\n") : "";
8829
+ const text = `# ${templateName}
8719
8830
 
8720
- ` + collList + "\n\nUse `collections action=list` for field schemas, `entries action=get` for full records."
8721
- );
8722
- } else {
8723
- sections.push(
8724
- "## Data Model\nCould not load collections \u2014 use `collections action=list` to browse manually."
8725
- );
8726
- }
8727
- const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
8728
- sections.push(
8729
- `## Business Rules
8730
- Collection: \`business-rules\` (${rulesCount}).
8731
- Find rules: \`entries action=search\` for text search, \`entries action=list collection=business-rules\` to browse.
8732
- Check compliance: use the \`review-against-rules\` prompt (pass a domain).
8733
- Draft a new rule: use the \`draft-rule-from-context\` prompt.`
8734
- );
8735
- const eventsCount = trackingEvents ? `${trackingEvents.length} events` : "not loaded \u2014 collection may not exist yet";
8736
- const conventionNote = standards ? "Naming convention: `object_action` in snake_case with past-tense verbs (from standards collection)." : "Naming convention: `object_action` in snake_case with past-tense verbs.";
8737
- sections.push(
8738
- `## Analytics & Tracking
8739
- Event catalog: \`tracking-events\` collection (${eventsCount}).
8740
- ${conventionNote}
8741
- Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
8742
- Browse: \`entries action=list collection=tracking-events\`.`
8743
- );
8744
- sections.push(
8745
- "## 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- `context action=gather` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `graph action=suggest` \u2014 discover potential connections for an entry\n- `relations action=create` \u2014 create a typed link between two entries\n- `graph action=find` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `graph action=suggest` to discover and create relevant relations."
8746
- );
8747
- sections.push(
8748
- "## 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 action=check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments (status changes, field updates, deprecation)."
8749
- );
8750
- sections.push(
8751
- "## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `context action=gather` (by entry ID or task description)\n- **Discover links** \u2192 `graph action=suggest`\n- **Quality audit** \u2192 `quality action=check`\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `collections action=list`\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 `entries action=list collection=strategy status=draft`\n- **Audiences** \u2192 `productbrain://audiences/entries` resource or `entries action=list collection=audiences`\n- **Session identity** \u2192 `health action=whoami`\n- **Health check** \u2192 `health action=check`\n- **Debug MCP calls** \u2192 `health action=audit`"
8752
- );
8753
- return sections.join("\n\n---\n\n");
8754
- }
8755
- var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
8756
-
8757
- ## Core Tools
8758
- | Tool | Purpose | Key params |
8759
- |---|---|---|
8760
- | \`orient\` | Workspace context, governance, active bets | \u2014 (call at session start) |
8761
- | \`capture\` | Create entry (draft) | \`collection\`, \`name\`, \`description\`, optional \`data\` |
8762
- | \`entries\` | List / get / batch / search entries | \`action\` + \`entryId\` / \`query\` / \`collection\` |
8763
- | \`update-entry\` | Update fields on an entry | \`entryId\`, optional \`name\`, \`status\`, \`workflowStatus\`, \`data\`, \`changeNote\` |
8764
- | \`commit-entry\` | Promote draft \u2192 SSOT | \`entryId\` |
8765
- | \`graph\` | Suggest / find relations | \`action\` + \`entryId\` |
8766
- | \`relations\` | Create / batch-create / delete links | \`from\`, \`to\`, \`type\` |
8767
- | \`context\` | Gather related knowledge | \`entryId\` or \`task\` |
8768
- | \`collections\` | List / create / update collections | \`action\` |
8769
- | \`labels\` | List / create / apply / remove labels | \`action\` |
8770
- | \`quality\` | Score an entry | \`entryId\` |
8771
- | \`session\` | Start / close agent session | \`action\` |
8772
- | \`health\` | Check / audit / whoami | \`action\` |
8773
- | \`facilitate\` | Shaping workflows | \`action\`: start, respond, status |
8774
-
8775
- ## Collection Prefixes
8776
- GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
8777
- DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
8778
- BET (bets), INS (insights), ARCH (architecture), CIR (circles),
8779
- ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
8780
-
8781
- ## Valid Relation Types (21)
8782
- informs, governs, surfaces_tension_in, defines_term_for, belongs_to,
8783
- references, related_to, fills_slot, commits_to, informed_by, depends_on,
8784
- conflicts_with, confused_with, replaces, part_of, constrains,
8785
- governed_by, alternative_to, has_proposal, requests_promotion_of, resolves
8786
-
8787
- ## Lifecycle Status
8788
- All entries: \`draft\` | \`active\` | \`deprecated\` | \`archived\`
8789
-
8790
- ## Workflow Status (per collection)
8791
- - **tensions:** open \u2192 processing \u2192 decided \u2192 closed
8792
- - **decisions:** pending \u2192 decided
8793
- - **bets:** shaped \u2192 bet \u2192 building \u2192 shipped
8794
- - **business-rules:** active | conflict | review
8795
-
8796
- ## Key Patterns
8797
- - **Capture flow:** \`capture\` \u2192 \`graph action=suggest\` \u2192 \`relations action=batch-create\` \u2192 \`commit-entry\`
8798
- - Use \`workflowStatus\` (not \`status\`) for domain workflow state
8799
- - \`data\` param is merged (not replaced) \u2014 safe for partial updates
8800
- `;
8801
- function registerResources(server) {
8802
- server.resource(
8803
- "agent-cheatsheet",
8804
- "productbrain://agent-cheatsheet",
8805
- async (uri) => ({
8806
- contents: [{
8807
- uri: uri.href,
8808
- text: AGENT_CHEATSHEET,
8809
- mimeType: "text/markdown"
8810
- }]
8811
- })
8812
- );
8813
- server.resource(
8814
- "chain-orientation",
8815
- "productbrain://orientation",
8816
- async (uri) => {
8817
- const [collectionsResult, eventsResult, standardsResult, rulesResult, principlesResult] = await Promise.allSettled([
8818
- mcpQuery("chain.listCollections"),
8819
- mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
8820
- mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
8821
- mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
8822
- mcpQuery("chain.listEntries", { collectionSlug: "principles" })
8823
- ]);
8824
- const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
8825
- const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
8826
- const standards = standardsResult.status === "fulfilled" ? standardsResult.value : null;
8827
- const businessRules = rulesResult.status === "fulfilled" ? rulesResult.value : null;
8828
- const principles = principlesResult.status === "fulfilled" ? principlesResult.value : null;
8829
- return {
8830
- contents: [{
8831
- uri: uri.href,
8832
- text: buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles),
8833
- mimeType: "text/markdown"
8834
- }]
8835
- };
8836
- }
8837
- );
8838
- server.resource(
8839
- "chain-terminology",
8840
- "productbrain://terminology",
8841
- async (uri) => {
8842
- const [glossaryResult, standardsResult] = await Promise.allSettled([
8843
- mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
8844
- mcpQuery("chain.listEntries", { collectionSlug: "standards" })
8845
- ]);
8846
- const lines = ["# Product Brain \u2014 Terminology"];
8847
- if (glossaryResult.status === "fulfilled") {
8848
- const glossary = glossaryResult.value ?? [];
8849
- if (glossary.length > 0) {
8850
- const terms = glossary.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
8851
- lines.push(`## Glossary (${glossary.length} terms)
8852
-
8853
- ${terms}`);
8854
- } else {
8855
- lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
8856
- }
8857
- } else {
8858
- lines.push("## Glossary\n\nCould not load glossary \u2014 use `entries action=list collection=glossary` to browse manually.");
8831
+ ${textLayers}${textFlows}`;
8832
+ return {
8833
+ content: [{ type: "text", text }],
8834
+ structuredContent: success(
8835
+ `Showing ${templateName}: ${layers.length} layers, ${nodes.length} nodes, ${flows.length} flows.`,
8836
+ { templateName, layerCount: layers.length, nodeCount: nodes.length, flowCount: flows.length }
8837
+ )
8838
+ };
8859
8839
  }
8860
- if (standardsResult.status === "fulfilled") {
8861
- const standards = standardsResult.value ?? [];
8862
- if (standards.length > 0) {
8863
- const stds = standards.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${s.data?.description ?? ""}`).join("\n");
8864
- lines.push(`## Standards (${standards.length} entries)
8865
-
8866
- ${stds}`);
8867
- } else {
8868
- lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
8840
+ if (action === "explore") {
8841
+ if (!layer) {
8842
+ return validationResult("A `layer` is required for explore.");
8869
8843
  }
8870
- } else {
8871
- lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
8872
- }
8873
- return {
8874
- contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
8875
- };
8876
- }
8877
- );
8878
- server.resource(
8879
- "chain-collections",
8880
- "productbrain://collections",
8881
- async (uri) => {
8882
- const collections = await mcpQuery("chain.listCollections") ?? [];
8883
- if (collections.length === 0) {
8884
- return { contents: [{ uri: uri.href, text: "No collections in this workspace. Use `collections action=create` or `start` with a preset to get started.", mimeType: "text/markdown" }] };
8885
- }
8886
- const formatted = collections.map((c) => {
8887
- const fieldList = (c.fields ?? []).map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
8888
- return `## ${c.icon ?? ""} ${c.name} (\`${c.slug}\`)
8889
- ${c.description || ""}
8844
+ const layers = byTag(all, "layer");
8845
+ const target = layers.find(
8846
+ (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
8847
+ );
8848
+ if (!target) {
8849
+ const available = layers.map((l) => `\`${l.name}\``).join(", ");
8850
+ return {
8851
+ content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }],
8852
+ structuredContent: failure(
8853
+ "NOT_FOUND",
8854
+ `Layer "${layer}" not found.`,
8855
+ `Available layers: ${available}`
8856
+ )
8857
+ };
8858
+ }
8859
+ const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
8860
+ const flows = byTag(all, "flow").filter(
8861
+ (f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
8862
+ );
8863
+ const depRule = target.data?.dependsOn ? `
8864
+ **Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
8865
+ ` : "";
8866
+ const layerRationale = target.data?.rationale ? `
8867
+ > ${target.data.rationale}
8868
+ ` : "";
8869
+ const nodeDetail = nodes.map((n) => {
8870
+ const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
8871
+ if (n.data?.description) lines.push(String(n.data.description));
8872
+ if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
8873
+ if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
8874
+ if (n.data?.rationale) lines.push(`
8875
+ **Why here?** ${n.data.rationale}`);
8876
+ return lines.join("\n");
8877
+ }).join("\n\n");
8878
+ const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
8879
+ return {
8880
+ content: [{
8881
+ type: "text",
8882
+ text: `# ${target.data?.icon ?? ""} ${target.name} Layer
8890
8883
 
8891
- **Fields:**
8892
- ${fieldList}`;
8893
- }).join("\n\n---\n\n");
8894
- return {
8895
- contents: [{ uri: uri.href, text: `# Knowledge Collections (${collections.length})
8884
+ ${target.data?.description ?? ""}${depRule}${layerRationale}
8885
+ **${nodes.length} components**
8896
8886
 
8897
- ${formatted}`, mimeType: "text/markdown" }]
8898
- };
8899
- }
8900
- );
8901
- server.resource(
8902
- "chain-collection-entries",
8903
- new ResourceTemplate("productbrain://{slug}/entries", {
8904
- list: async () => {
8905
- const collections = await mcpQuery("chain.listCollections") ?? [];
8906
- return {
8907
- resources: collections.map((c) => ({
8908
- uri: `productbrain://${c.slug}/entries`,
8909
- name: `${c.icon ?? ""} ${c.name}`.trim()
8910
- }))
8887
+ ${nodeDetail}${flowLines}`
8888
+ }],
8889
+ structuredContent: success(
8890
+ `${target.name} layer: ${nodes.length} components, ${flows.length} connected flows.`,
8891
+ { layerName: target.name, entryId: target.entryId, nodeCount: nodes.length, flowCount: flows.length }
8892
+ )
8911
8893
  };
8912
8894
  }
8913
- }),
8914
- async (uri, { slug }) => {
8915
- const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug }) ?? [];
8916
- const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
8917
- return {
8918
- contents: [{
8919
- uri: uri.href,
8920
- text: formatted || "No entries in this collection.",
8921
- mimeType: "text/markdown"
8922
- }]
8923
- };
8924
- }
8925
- );
8926
- server.resource(
8927
- "chain-labels",
8928
- "productbrain://labels",
8929
- async (uri) => {
8930
- const labels = await mcpQuery("chain.listLabels") ?? [];
8931
- if (labels.length === 0) {
8932
- return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
8933
- }
8934
- const groups = labels.filter((l) => l.isGroup);
8935
- const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
8936
- const children = (parentId) => labels.filter((l) => l.parentId === parentId);
8937
- const lines = [];
8938
- for (const group of groups) {
8939
- lines.push(`## ${group.name}`);
8940
- for (const child of children(group._id)) {
8941
- lines.push(`- \`${child.slug}\` ${child.name}${child.color ? ` (${child.color})` : ""}`);
8895
+ if (action === "flow") {
8896
+ if (!flow) {
8897
+ return validationResult("A `flow` name or entry ID is required.");
8942
8898
  }
8943
- }
8944
- if (ungrouped.length > 0) {
8945
- lines.push("## Ungrouped");
8946
- for (const l of ungrouped) {
8947
- lines.push(`- \`${l.slug}\` ${l.name}${l.color ? ` (${l.color})` : ""}`);
8899
+ const flows = byTag(all, "flow");
8900
+ const target = flows.find(
8901
+ (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
8902
+ );
8903
+ if (!target) {
8904
+ const available = flows.map((f) => `\`${f.name}\``).join(", ");
8905
+ return {
8906
+ content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }],
8907
+ structuredContent: failure(
8908
+ "NOT_FOUND",
8909
+ `Flow "${flow}" not found.`,
8910
+ `Available flows: ${available}`
8911
+ )
8912
+ };
8948
8913
  }
8949
- }
8950
- return {
8951
- contents: [{ uri: uri.href, text: `# Workspace Labels (${labels.length})
8952
-
8953
- ${lines.join("\n")}`, mimeType: "text/markdown" }]
8954
- };
8955
- }
8956
- );
8957
- server.resource(
8958
- "chain-entry",
8959
- new ResourceTemplate("productbrain://entries/{entryId}", {
8960
- complete: {
8961
- entryId: async (value) => {
8962
- if (!value || value.length < 1) return [];
8963
- const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
8964
- return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
8914
+ const nodes = byTag(all, "node");
8915
+ const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
8916
+ const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
8917
+ const lines = [
8918
+ `# ${target.name}`,
8919
+ "",
8920
+ `**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
8921
+ "",
8922
+ String(target.data?.description ?? "")
8923
+ ];
8924
+ if (source) {
8925
+ lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
8926
+ if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
8965
8927
  }
8928
+ if (dest) {
8929
+ lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
8930
+ if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
8931
+ }
8932
+ return {
8933
+ content: [{ type: "text", text: lines.join("\n") }],
8934
+ structuredContent: success(
8935
+ `${target.name}: ${source?.name ?? "?"} \u2192 ${dest?.name ?? "?"}.`,
8936
+ { flowName: target.name, entryId: target.entryId, source: source?.name, target: dest?.name }
8937
+ )
8938
+ };
8966
8939
  }
8967
- }),
8968
- async (uri, { entryId }) => {
8969
- const [entry, collections] = await Promise.all([
8970
- mcpQuery("chain.getEntry", { entryId }),
8971
- mcpQuery("chain.listCollections")
8972
- ]);
8973
- if (!entry) {
8974
- return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
8975
- }
8976
- const collectionMap = new Map((collections ?? []).map((c) => [c._id, c]));
8977
- const col = collectionMap.get(entry.collectionId);
8978
- const collLabel = col?.name ?? entry.collectionName ?? entry.collectionSlug ?? "unknown";
8979
- const lines = [
8980
- `# ${entry.entryId}: ${entry.name}`,
8981
- `**Collection:** ${collLabel}`,
8982
- `**Status:** ${entry.status}`,
8983
- ""
8984
- ];
8985
- if (entry.data && typeof entry.data === "object") {
8986
- lines.push("## Data");
8987
- for (const [key, val] of Object.entries(entry.data)) {
8988
- if (val && key !== "rawData") {
8989
- const str = typeof val === "string" ? val : JSON.stringify(val, null, 2);
8990
- lines.push(`**${key}:** ${str}`);
8940
+ return unknownAction(action, ["show", "explore", "flow"]);
8941
+ })
8942
+ );
8943
+ const archAdminTool = server.registerTool(
8944
+ "architecture-admin",
8945
+ {
8946
+ title: "Architecture Admin",
8947
+ 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.",
8948
+ inputSchema: architectureAdminSchema,
8949
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
8950
+ },
8951
+ withEnvelope(async ({ action }) => {
8952
+ if (action === "seed") {
8953
+ await ensureCollection();
8954
+ const existing = await listArchEntries();
8955
+ const existingIds = new Set(existing.map((e) => e.entryId));
8956
+ let created = 0;
8957
+ let updated = 0;
8958
+ let unchanged = 0;
8959
+ const allSeeds = [
8960
+ { ...SEED_TEMPLATE, order: 0, status: "active" },
8961
+ ...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
8962
+ ...SEED_NODES.map((n) => ({ ...n, status: "active" })),
8963
+ ...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
8964
+ ];
8965
+ for (const seed of allSeeds) {
8966
+ if (existingIds.has(seed.entryId)) {
8967
+ const existingEntry = existing.find((e) => e.entryId === seed.entryId);
8968
+ const existingData = existingEntry?.data ?? {};
8969
+ const seedData = seed.data;
8970
+ const hasChanges = Object.keys(seedData).some(
8971
+ (k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
8972
+ );
8973
+ if (hasChanges) {
8974
+ const mergedData = { ...existingData, ...seedData };
8975
+ await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
8976
+ updated++;
8977
+ } else {
8978
+ unchanged++;
8979
+ }
8980
+ continue;
8991
8981
  }
8982
+ await mcpMutation("chain.createEntry", {
8983
+ collectionSlug: COLLECTION_SLUG,
8984
+ entryId: seed.entryId,
8985
+ name: seed.name,
8986
+ status: seed.status,
8987
+ data: seed.data,
8988
+ order: seed.order ?? 0
8989
+ });
8990
+ created++;
8992
8991
  }
8993
- lines.push("");
8992
+ return {
8993
+ content: [{
8994
+ type: "text",
8995
+ text: `# Architecture Seeded
8996
+
8997
+ **Created:** ${created} entries
8998
+ **Updated:** ${updated} (merged new fields)
8999
+ **Unchanged:** ${unchanged}
9000
+
9001
+ Use \`architecture action=show\` to view the map.`
9002
+ }],
9003
+ structuredContent: success(
9004
+ `Architecture seeded: ${created} created, ${updated} updated, ${unchanged} unchanged.`,
9005
+ { created, updated, unchanged },
9006
+ [{ tool: "architecture", description: "View the map", parameters: { action: "show" } }]
9007
+ )
9008
+ };
8994
9009
  }
8995
- if (entry.relations && entry.relations.length > 0) {
8996
- lines.push("## Relations");
8997
- for (const rel of entry.relations) {
8998
- const arrow = rel.direction === "outgoing" ? "\u2192" : "\u2190";
8999
- const id = rel.otherEntryId ?? "";
9000
- const name = rel.otherName ?? "(unknown)";
9001
- lines.push(`- ${arrow} **${rel.type}** ${id}: ${name}`);
9010
+ if (action === "check") {
9011
+ const projectRoot = resolveProjectRoot2();
9012
+ if (!projectRoot) {
9013
+ return {
9014
+ content: [{
9015
+ type: "text",
9016
+ text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
9017
+ }],
9018
+ structuredContent: failure(
9019
+ "VALIDATION_ERROR",
9020
+ "Cannot find project root.",
9021
+ "Set WORKSPACE_PATH env var to the absolute path of the Product OS project root."
9022
+ )
9023
+ };
9002
9024
  }
9003
- lines.push("");
9004
- }
9005
- if (entry.labels && entry.labels.length > 0) {
9006
- lines.push(`## Labels
9007
- ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
9025
+ await ensureCollection();
9026
+ const all = await listArchEntries();
9027
+ const layers = byTag(all, "layer");
9028
+ const nodes = byTag(all, "node");
9029
+ const result = scanDependencies(projectRoot, layers, nodes);
9030
+ return {
9031
+ content: [{ type: "text", text: formatScanReport(result) }],
9032
+ structuredContent: success(
9033
+ result.violations.length === 0 ? `Health check passed: 0 violations across ${result.filesScanned} files.` : `Health check found ${result.violations.length} violation(s) across ${result.filesScanned} files.`,
9034
+ {
9035
+ violations: result.violations.length,
9036
+ filesScanned: result.filesScanned,
9037
+ importsChecked: result.importsChecked,
9038
+ unmappedImports: result.unmappedImports
9039
+ }
9040
+ )
9041
+ };
9008
9042
  }
9009
- return {
9010
- contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
9011
- };
9012
- }
9043
+ return unknownAction(action, ["seed", "check"]);
9044
+ })
9013
9045
  );
9014
- server.resource(
9015
- "chain-context",
9016
- new ResourceTemplate("productbrain://context/{entryId}", {
9017
- complete: {
9018
- entryId: async (value) => {
9019
- if (!value || value.length < 1) return [];
9020
- const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
9021
- return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
9022
- }
9023
- }
9024
- }),
9025
- async (uri, { entryId }) => {
9026
- const result = await mcpQuery("chain.gatherContext", {
9027
- entryId,
9028
- maxHops: 2
9029
- });
9030
- if (!result?.root) {
9031
- return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
9032
- }
9033
- const lines = [
9034
- `# Context: ${result.root.entryId}: ${result.root.name}`,
9035
- `_${result.totalRelations} related entries (${result.hopsTraversed} hops)_`,
9036
- ""
9037
- ];
9038
- const byCollection = /* @__PURE__ */ new Map();
9039
- for (const entry of result.related ?? []) {
9040
- const key = entry.collectionName;
9041
- if (!byCollection.has(key)) byCollection.set(key, []);
9042
- byCollection.get(key).push(entry);
9043
- }
9044
- for (const [collName, entries] of byCollection) {
9045
- lines.push(`## ${collName} (${entries.length})`);
9046
- for (const e of entries) {
9047
- const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
9048
- const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
9049
- const id = e.entryId ? `${e.entryId}: ` : "";
9050
- lines.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}`);
9046
+ trackWriteTool(archAdminTool);
9047
+ }
9048
+ function resolveProjectRoot2() {
9049
+ const candidates = [
9050
+ process.env.WORKSPACE_PATH,
9051
+ process.cwd(),
9052
+ resolve2(process.cwd(), "..")
9053
+ ].filter(Boolean);
9054
+ for (const dir of candidates) {
9055
+ const resolved = resolve2(dir);
9056
+ if (existsSync2(resolve2(resolved, "convex/schema.ts"))) return resolved;
9057
+ }
9058
+ return null;
9059
+ }
9060
+ function scanDependencies(projectRoot, layers, nodes) {
9061
+ const layerMap = /* @__PURE__ */ new Map();
9062
+ for (const l of layers) layerMap.set(l.entryId, l);
9063
+ const allowedDeps = buildAllowedDeps(layers);
9064
+ const nodePathPrefixes = buildNodePrefixes(nodes);
9065
+ const violations = [];
9066
+ const nodeResults = /* @__PURE__ */ new Map();
9067
+ let totalFiles = 0;
9068
+ let totalImports = 0;
9069
+ let unmapped = 0;
9070
+ for (const node of nodes) {
9071
+ const layerRef = String(node.data?.layerRef ?? "");
9072
+ const layer = layerMap.get(layerRef);
9073
+ if (!layer) continue;
9074
+ const filePaths = parseFilePaths(node);
9075
+ const nodeViolations = [];
9076
+ let nodeFileCount = 0;
9077
+ for (const fp of filePaths) {
9078
+ const absPath = resolve2(projectRoot, fp);
9079
+ const files = collectFiles(absPath);
9080
+ for (const file of files) {
9081
+ nodeFileCount++;
9082
+ totalFiles++;
9083
+ const relFile = relative(projectRoot, file);
9084
+ const imports = parseImports(file);
9085
+ for (const imp of imports) {
9086
+ totalImports++;
9087
+ const resolved = resolveImport(imp, file, projectRoot);
9088
+ if (!resolved) {
9089
+ unmapped++;
9090
+ continue;
9091
+ }
9092
+ const targetNode = findNodeByPath(resolved, nodePathPrefixes);
9093
+ if (!targetNode) {
9094
+ unmapped++;
9095
+ continue;
9096
+ }
9097
+ const targetLayerRef = String(targetNode.data?.layerRef ?? "");
9098
+ const targetLayer = layerMap.get(targetLayerRef);
9099
+ if (!targetLayer) continue;
9100
+ if (targetLayerRef === layerRef) continue;
9101
+ const allowed = allowedDeps.get(layerRef);
9102
+ if (allowed && !allowed.has(targetLayerRef)) {
9103
+ const v = {
9104
+ sourceNode: node.name,
9105
+ sourceLayer: layer.name,
9106
+ sourceFile: relFile,
9107
+ importPath: imp,
9108
+ targetNode: targetNode.name,
9109
+ targetLayer: targetLayer.name,
9110
+ rule: `${layer.name} cannot import from ${targetLayer.name}`
9111
+ };
9112
+ violations.push(v);
9113
+ nodeViolations.push(v);
9114
+ }
9051
9115
  }
9052
- lines.push("");
9053
9116
  }
9054
- return {
9055
- contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
9056
- };
9057
9117
  }
9058
- );
9059
- server.resource(
9060
- "chain-search",
9061
- new ResourceTemplate("productbrain://search/{query}", {
9062
- complete: {
9063
- query: async (value) => {
9064
- if (!value) return ["glossary:", "business-rules:", "decisions:", "tensions:"];
9065
- return [];
9066
- }
9067
- }
9068
- }),
9069
- async (uri, { query }) => {
9070
- const results = await mcpQuery("chain.searchEntries", { query });
9071
- if (!results || results.length === 0) {
9072
- return { contents: [{ uri: uri.href, text: `No results for "${query}".`, mimeType: "text/markdown" }] };
9073
- }
9074
- const lines = [
9075
- `# Search: "${query}"`,
9076
- `_${results.length} results_`,
9077
- ""
9078
- ];
9079
- for (const entry of results) {
9080
- const id = entry.entryId ? `**${entry.entryId}:** ` : "";
9081
- const coll = entry.collectionName ?? entry.collectionSlug ?? "";
9082
- lines.push(`- ${id}${entry.name} [${coll}] (${entry.status})`);
9118
+ nodeResults.set(node.entryId, { violations: nodeViolations, filesScanned: nodeFileCount });
9119
+ }
9120
+ return { violations, filesScanned: totalFiles, importsChecked: totalImports, unmappedImports: unmapped, nodeResults };
9121
+ }
9122
+ function buildAllowedDeps(layers) {
9123
+ const nameToId = /* @__PURE__ */ new Map();
9124
+ for (const l of layers) nameToId.set(l.name.toLowerCase(), l.entryId);
9125
+ const allowed = /* @__PURE__ */ new Map();
9126
+ for (const layer of layers) {
9127
+ const deps = String(layer.data?.dependsOn ?? "none");
9128
+ const set = /* @__PURE__ */ new Set();
9129
+ if (deps !== "none") {
9130
+ for (const dep of deps.split(",").map((d) => d.trim().toLowerCase())) {
9131
+ const id = nameToId.get(dep);
9132
+ if (id) set.add(id);
9083
9133
  }
9084
- return {
9085
- contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
9086
- };
9087
9134
  }
9088
- );
9089
- for (const [uri, filePath] of Object.entries(UI_VIEWS)) {
9090
- server.resource(`ui-view-${uri.replace(/[^a-z0-9]/gi, "-")}`, uri, async (uriObj) => {
9091
- const viewPath = resolveUiViewPath(filePath);
9092
- if (viewPath) {
9093
- try {
9094
- const html = await readFile(viewPath, "utf-8");
9095
- return {
9096
- contents: [{ uri: uriObj.href, text: html, mimeType: UI_RESOURCE_MIME_TYPE }]
9097
- };
9098
- } catch {
9099
- }
9100
- }
9101
- return {
9102
- contents: [{
9103
- uri: uriObj.href,
9104
- text: renderMissingUiView(uriObj.href),
9105
- mimeType: UI_RESOURCE_MIME_TYPE
9106
- }]
9107
- };
9108
- });
9135
+ allowed.set(layer.entryId, set);
9109
9136
  }
9137
+ return allowed;
9110
9138
  }
9111
-
9112
- // src/tools/architecture.ts
9113
- var COLLECTION_SLUG = "architecture";
9114
- var COLLECTION_FIELDS = [
9115
- { key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
9116
- { key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
9117
- { key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
9118
- { key: "description", label: "Description", type: "text", searchable: true },
9119
- { key: "color", label: "Color", type: "text" },
9120
- { key: "icon", label: "Icon", type: "text" },
9121
- { key: "sourceNode", label: "Source Node (flows)", type: "text" },
9122
- { key: "targetNode", label: "Target Node (flows)", type: "text" },
9123
- { key: "filePaths", label: "File Paths", type: "text", searchable: true },
9124
- { key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
9125
- { key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
9126
- { key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
9127
- { key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
9128
- ];
9129
- var ARCHITECTURE_CANONICAL_KEY = "architecture_note";
9130
- async function ensureCollection() {
9131
- const collections = await mcpQuery("chain.listCollections");
9132
- const existing = collections.find((c) => c.slug === COLLECTION_SLUG);
9133
- if (existing) {
9134
- if (!existing.defaultCanonicalKey) {
9135
- await mcpMutation("chain.updateCollection", {
9136
- slug: COLLECTION_SLUG,
9137
- defaultCanonicalKey: ARCHITECTURE_CANONICAL_KEY
9138
- });
9139
+ function buildNodePrefixes(nodes) {
9140
+ const entries = [];
9141
+ for (const node of nodes) {
9142
+ for (const fp of parseFilePaths(node)) {
9143
+ entries.push({ prefix: normalize(fp), node });
9139
9144
  }
9140
- return;
9141
9145
  }
9142
- await mcpMutation("chain.createCollection", {
9143
- slug: COLLECTION_SLUG,
9144
- name: "Architecture",
9145
- icon: "\u{1F3D7}\uFE0F",
9146
- description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
9147
- fields: COLLECTION_FIELDS
9148
- });
9146
+ entries.sort((a, b) => b.prefix.length - a.prefix.length);
9147
+ return entries;
9149
9148
  }
9150
- async function listArchEntries() {
9151
- return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
9149
+ function parseFilePaths(node) {
9150
+ const raw = node.data?.filePaths;
9151
+ if (!raw || typeof raw !== "string") return [];
9152
+ return raw.split(",").map((p) => p.trim()).filter(Boolean);
9152
9153
  }
9153
- function byTag(entries, archType) {
9154
- return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
9154
+ function collectFiles(absPath) {
9155
+ if (!existsSync2(absPath)) return [];
9156
+ const stat = statSync(absPath);
9157
+ if (stat.isFile()) {
9158
+ return isScannableFile(absPath) ? [absPath] : [];
9159
+ }
9160
+ if (!stat.isDirectory()) return [];
9161
+ const results = [];
9162
+ const walk = (dir) => {
9163
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
9164
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
9165
+ const full = resolve2(dir, entry.name);
9166
+ if (entry.isDirectory()) walk(full);
9167
+ else if (isScannableFile(full)) results.push(full);
9168
+ }
9169
+ };
9170
+ walk(absPath);
9171
+ return results;
9155
9172
  }
9156
- function renderArchitectureHtml(layers, nodes, flows, templateName) {
9157
- const layerHtml = layers.map((layer) => {
9158
- const layerNodes = nodes.filter(
9159
- (n) => n.data?.layerRef === layer.entryId
9160
- );
9161
- const nodeCards = layerNodes.map((n) => `
9162
- <div class="node" title="${escHtml(String(n.data?.description ?? ""))}">
9163
- <span class="node-icon">${escHtml(String(n.data?.icon ?? "\u25FB"))}</span>
9164
- <span class="node-name">${escHtml(n.name)}</span>
9165
- </div>
9166
- `).join("");
9167
- return `
9168
- <div class="layer" style="--layer-color: ${escHtml(String(layer.data?.color ?? "#666"))}">
9169
- <div class="layer-label">
9170
- <span class="layer-dot"></span>
9171
- <span class="layer-name">${escHtml(layer.name)}</span>
9172
- <span class="layer-count">${layerNodes.length}</span>
9173
- </div>
9174
- <div class="layer-desc">${escHtml(String(layer.data?.description ?? ""))}</div>
9175
- <div class="nodes">${nodeCards || '<span class="empty">No components</span>'}</div>
9176
- </div>
9177
- `;
9178
- }).join("");
9179
- return `<!DOCTYPE html>
9180
- <html><head><meta charset="utf-8"><style>
9181
- *{margin:0;padding:0;box-sizing:border-box}
9182
- body{font-family:-apple-system,system-ui,sans-serif;background:#1a1a2e;color:#e0e0e0;padding:16px}
9183
- h1{font-size:14px;font-weight:600;color:#a0a0c0;margin-bottom:12px;letter-spacing:.04em}
9184
- .layer{border-left:3px solid var(--layer-color);padding:8px 12px;margin-bottom:8px;background:rgba(255,255,255,.03);border-radius:0 6px 6px 0}
9185
- .layer-label{display:flex;align-items:center;gap:8px;margin-bottom:4px}
9186
- .layer-dot{width:8px;height:8px;border-radius:50%;background:var(--layer-color)}
9187
- .layer-name{font-size:12px;font-weight:600;color:#fff;letter-spacing:.03em}
9188
- .layer-count{font-size:10px;color:var(--layer-color);background:rgba(255,255,255,.06);padding:1px 6px;border-radius:8px}
9189
- .layer-desc{font-size:11px;color:#888;margin-bottom:6px}
9190
- .nodes{display:flex;flex-wrap:wrap;gap:6px}
9191
- .node{display:flex;align-items:center;gap:4px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:4px;padding:4px 8px;font-size:11px;cursor:default;transition:border-color .15s}
9192
- .node:hover{border-color:var(--layer-color)}
9193
- .node-icon{font-size:12px}
9194
- .node-name{color:#ddd}
9195
- .empty{font-size:11px;color:#555;font-style:italic}
9196
- .flows{margin-top:12px;border-top:1px solid rgba(255,255,255,.06);padding-top:8px}
9197
- .flow{font-size:11px;color:#888;padding:2px 0}
9198
- .flow-arrow{color:#6366f1;margin:0 4px}
9199
- </style></head><body>
9200
- <h1>${escHtml(templateName)}</h1>
9201
- ${layerHtml}
9202
- ${flows.length > 0 ? `<div class="flows"><div style="font-size:10px;color:#666;margin-bottom:4px;letter-spacing:.06em">DATA FLOWS</div>${flows.map((f) => `<div class="flow">${escHtml(f.name)}<span class="flow-arrow">\u2192</span><span style="color:#aaa">${escHtml(String(f.data?.description ?? ""))}</span></div>`).join("")}</div>` : ""}
9203
- </body></html>`;
9204
- }
9205
- function escHtml(s) {
9206
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9173
+ function isScannableFile(p) {
9174
+ return /\.(ts|js|svelte)$/.test(p) && !p.endsWith(".d.ts");
9207
9175
  }
9208
- function formatLayerText(layer, nodes) {
9209
- const layerNodes = nodes.filter((n) => n.data?.layerRef === layer.entryId);
9210
- const nodeList = layerNodes.map((n) => {
9211
- const desc = n.data?.description ? ` \u2014 ${n.data.description}` : "";
9212
- const owner = n.data?.owner ? ` (${n.data.owner})` : "";
9213
- return ` - ${n.data?.icon ?? "\u25FB"} **${n.name}**${desc}${owner}`;
9214
- }).join("\n");
9215
- return `### ${layer.name}
9216
- ${layer.data?.description ?? ""}
9217
-
9218
- ${nodeList || " _No components_"}`;
9176
+ function parseImports(filePath) {
9177
+ try {
9178
+ const content = readFileSync2(filePath, "utf-8");
9179
+ const re = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
9180
+ const imports = [];
9181
+ let match;
9182
+ while ((match = re.exec(content)) !== null) {
9183
+ imports.push(match[1]);
9184
+ }
9185
+ return imports;
9186
+ } catch {
9187
+ return [];
9188
+ }
9219
9189
  }
9220
- var SEED_TEMPLATE = {
9221
- entryId: "ARCH-tpl-product-os",
9222
- name: "Product OS Default",
9223
- data: {
9224
- archType: "template",
9225
- description: "Default 4-layer architecture: Auth \u2192 Infrastructure \u2192 Core \u2192 Features, with an outward Integration layer",
9226
- layerOrder: JSON.stringify(["auth", "infrastructure", "core", "features", "integration"])
9227
- }
9228
- };
9229
- var SEED_LAYERS = [
9230
- { entryId: "ARCH-layer-auth", name: "Auth", order: 0, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#22c55e", description: "Authentication, user management, and workspace-scoped access control", icon: "\u{1F510}", dependsOn: "none", rationale: "Foundation layer. Auth depends on nothing \u2014 it is the first gate. No layer may bypass auth. All other layers depend on auth to know who the user is and which workspace they belong to." } },
9231
- { entryId: "ARCH-layer-infra", name: "Infrastructure", order: 1, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#ec4899", description: "Real-time data, event tracking, and AI model infrastructure", icon: "\u2699\uFE0F", dependsOn: "Auth", rationale: "Infrastructure sits on top of Auth. It provides the database, analytics, and AI plumbing that Core and Features consume. Infra may import from Auth (needs workspace context) but never from Core or Features." } },
9232
- { entryId: "ARCH-layer-core", name: "Core", order: 2, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#8b5cf6", description: "Business logic, knowledge graph, workflow engines, and MCP tooling", icon: "\u{1F9E0}", dependsOn: "Auth, Infrastructure", rationale: "Core contains business logic and engines that are UI-agnostic. It may import from Auth and Infra. Features depend on Core, but Core must never depend on Features \u2014 this is what keeps engines reusable across different UIs." } },
9233
- { entryId: "ARCH-layer-features", name: "Features", order: 3, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#6366f1", description: "User-facing pages, components, and feature modules", icon: "\u2726", dependsOn: "Auth, Infrastructure, Core", rationale: "Features are the user-facing layer \u2014 SvelteKit routes, components, and page-level logic. Features may import from any lower layer but nothing above may import from Features. This is the outermost application layer." } },
9234
- { entryId: "ARCH-layer-integration", name: "Integration", order: 4, data: { archType: "layer", templateRef: "ARCH-tpl-product-os", color: "#f59e0b", description: "Outward connections to external tools, IDEs, and services", icon: "\u{1F50C}", dependsOn: "Core", rationale: "Integration is a lateral/outward layer \u2014 it connects to external systems (IDE, GitHub, Linear). It depends on Core (to expose knowledge) but sits outside the main stack. External tools call into Core via Integration, never directly into Features." } }
9235
- ];
9236
- var SEED_NODES = [
9237
- // Auth layer
9238
- { entryId: "ARCH-node-clerk", name: "Clerk", order: 0, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F511}", description: "Authentication provider \u2014 sign-in/sign-up pages, session management, organization-level access control. UserSync.svelte is cross-cutting layout glue (not mapped here).", filePaths: "src/routes/sign-in/, src/routes/sign-up/", owner: "Platform", rationale: "Auth layer because Clerk is the identity gate. Every request flows through auth first. No other layer provides identity." } },
9239
- { entryId: "ARCH-node-workspace", name: "Workspace Scoping", order: 1, data: { archType: "node", layerRef: "ARCH-layer-auth", color: "#22c55e", icon: "\u{1F3E2}", description: "Multi-tenancy anchor \u2014 all data is workspace-scoped via workspaceId", filePaths: "src/lib/stores/workspace.ts, convex/workspaces.ts", owner: "Platform", rationale: "Auth layer because workspace scoping is the second gate after identity. All queries require workspaceId, making this foundational." } },
9240
- // Infrastructure layer
9241
- { entryId: "ARCH-node-convex", name: "Convex", order: 0, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u26A1", description: "Reactive database with real-time sync, serverless functions, and type-safe API generation. Unified Collections + Entries model", filePaths: "convex/schema.ts, convex/entries.ts, convex/http.ts", owner: "Platform", rationale: "Infrastructure because Convex is raw persistence and reactivity plumbing. It stores data but has no business logic opinions. Core and Features consume it." } },
9242
- { entryId: "ARCH-node-posthog", name: "PostHog", order: 1, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F4CA}", description: "Product analytics \u2014 workspace-scoped events, feature flags, session replay", filePaths: "src/lib/analytics.ts, src/lib/components/PostHogWorkspaceSync.svelte", owner: "Platform", rationale: "Infrastructure because PostHog is analytics plumbing \u2014 event collection and aggregation. It has no knowledge of business domains." } },
9243
- { entryId: "ARCH-node-openrouter", name: "OpenRouter", order: 2, data: { archType: "node", layerRef: "ARCH-layer-infra", color: "#ec4899", icon: "\u{1F916}", description: "AI model routing for ChainWork artifact generation \u2014 streaming responses with format-aware prompts", filePaths: "src/routes/api/chainwork/generate/+server.ts", owner: "ChainWork", rationale: "Infrastructure because OpenRouter is an AI model gateway \u2014 it routes prompts to models. The strategy logic lives in ChainWork Engine (Core); this is just the pipe." } },
9244
- // Core layer
9245
- { entryId: "ARCH-node-mcp", name: "MCP Server", order: 0, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F527}", description: "~19 modular tools exposing the knowledge graph as AI-consumable operations \u2014 capture, context assembly, verification, quality checks", filePaths: "packages/mcp-server/src/index.ts, packages/mcp-server/src/tools/", owner: "AI DX", rationale: "Core layer because the MCP server encodes business operations \u2014 capture with auto-linking, quality scoring, governance rules. It depends on Infra (Convex) but never touches UI routes. Why not Infrastructure? Because it has domain opinions. Why not Features? Because it has no UI \u2014 any client (Cursor, CLI, API) can call it." } },
9246
- { entryId: "ARCH-node-knowledge-graph", name: "Knowledge Graph", order: 1, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u{1F578}\uFE0F", description: "20 collections, 170+ entries with typed cross-collection relations. Smart capture, auto-linking, quality scoring", filePaths: "convex/mcpKnowledge.ts, convex/entries.ts", owner: "Knowledge", rationale: "Core layer because the knowledge graph IS the domain model \u2014 collections, entries, relations, versioning, quality scoring. Glossary DATA, business rules DATA, tension DATA all live here. Why not Features? Because the data model exists independently of any page. The Glossary page in Features is just one way to visualize terms that Core owns. Think: Core owns the dictionary, Features owns the dictionary app." } },
9247
- { entryId: "ARCH-node-governance", name: "Governance Engine", order: 2, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u2696\uFE0F", description: "Circles, roles, consent-based decision-making, tension processing with IDM-inspired async workflows", filePaths: "convex/versioning.ts, src/lib/components/versioning/", owner: "Governance", rationale: "Core layer because governance logic (draft\u2192publish workflows, consent-based decisions, tension status rules) is business process that multiple UIs consume. Why not Features? Because the versioning system and proposal flow are reusable engines \u2014 the Governance Pages in Features are just one rendering of these rules." } },
9248
- { entryId: "ARCH-node-chainwork-engine", name: "ChainWork Engine", order: 3, data: { archType: "node", layerRef: "ARCH-layer-core", color: "#8b5cf6", icon: "\u26D3", description: "Guided strategy creation through 5-step coherence chain \u2014 AI-generated artifacts with scoring and achievements", filePaths: "src/lib/components/chainwork/config.ts, src/lib/components/chainwork/scoring.ts", owner: "ChainWork", rationale: "Core layer because the coherence chain logic, scoring algorithm, and quality gates are business rules. config.ts defines chain steps, scoring.ts computes quality \u2014 these could power a CLI or API. Why not Features? Because the ChainWork UI wizard in Features is just one skin over this engine." } },
9249
- // Features layer
9250
- { entryId: "ARCH-node-command-center", name: "Command Center", order: 0, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u2B21", description: "Calm home screen \u2014 daily orientation, triage mode, pulse metrics, momentum tracking", filePaths: "src/routes/+page.svelte, src/lib/components/command-center/", owner: "Command Center", rationale: "Features layer because the Command Center is a SvelteKit page \u2014 PulseMetrics, NeedsAttention, DailyBriefing are UI components that assemble data from Core queries. Why not Core? Because it has no reusable business logic or engines \u2014 it is pure layout and presentation. If you deleted this page, no business rule would break." } },
9251
- { entryId: "ARCH-node-chainwork-ui", name: "ChainWork UI", order: 1, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26D3", description: "Multi-step wizard for strategy artifact creation \u2014 setup, chain steps, quality gates, output", filePaths: "src/routes/chainwork/, src/lib/components/chainwork/", owner: "ChainWork", rationale: "Features layer because the ChainWork UI is the wizard interface \u2014 step navigation, form inputs, output rendering. Why not Core? Because the scoring logic and chain config ARE in Core (ChainWork Engine). This is the presentation skin over that engine. Delete this page and the engine still works via MCP." } },
9252
- { entryId: "ARCH-node-glossary", name: "Glossary", order: 2, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "Aa", description: "Canonical vocabulary management \u2014 term detail, code drift detection, inline term linking", filePaths: "src/routes/glossary/, src/lib/components/glossary/", owner: "Knowledge", rationale: "Features layer \u2014 NOT Core or Infrastructure \u2014 because this is the Glossary PAGE: the SvelteKit route for browsing, editing, and viewing terms. Why not Core? Because Core owns the glossary DATA (Knowledge Graph) and term-linking logic. The MCP server also accesses glossary terms without any page. Why not Infrastructure? Because glossary is domain-specific vocabulary, not generic plumbing. The page is one consumer of the data \u2014 Core owns the dictionary, Features owns the dictionary app." } },
9253
- { entryId: "ARCH-node-tensions", name: "Tensions", order: 3, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26A1", description: "Tension capture and processing \u2014 raise, triage, resolve through governance workflow", filePaths: "src/routes/tensions/, src/lib/components/tensions/", owner: "Governance", rationale: "Features layer because this is the Tensions PAGE \u2014 the UI for raising, listing, and viewing tensions. Why not Core? Because tension data, status rules (SOS-020), and processing logic already live in Core (Governance Engine). This page is a form + list view that reads from and writes to Core. Delete it and the governance engine still processes tensions via MCP." } },
9254
- { entryId: "ARCH-node-strategy", name: "Strategy", order: 4, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25A3", description: "Product strategy pages \u2014 vision, ecosystem, product areas, decision frameworks, sequencing", filePaths: "src/routes/strategy/, src/routes/bridge/, src/routes/topology/", owner: "Strategy", rationale: "Features layer because Strategy is a set of SvelteKit pages (strategy, bridge, topology) that visualize strategy entries. Why not Core? Because the strategy data (vision, principles, ecosystem layers) lives in the Knowledge Graph (Core). These pages render and allow inline editing \u2014 they consume Core downward." } },
9255
- { entryId: "ARCH-node-governance-ui", name: "Governance Pages", order: 5, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25C8", description: "Roles, circles, principles, policies, decisions, proposals, business rules", filePaths: "src/routes/roles/, src/routes/circles/, src/routes/decisions/, src/routes/proposals/", owner: "Governance", rationale: "Features layer because these are governance PAGES \u2014 list views and detail views for roles, circles, decisions, proposals. Why not Core? Because the governance ENGINE (versioning, consent, IDM) IS in Core. These pages are the user-facing window into governance data. The logic doesn't live here, only the rendering." } },
9256
- { entryId: "ARCH-node-artifacts", name: "Artifacts", order: 6, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u{1F4C4}", description: "Strategy artifacts from ChainWork \u2014 pitches, briefs, one-pagers linked to the knowledge graph", filePaths: "src/routes/artifacts/", owner: "ChainWork", rationale: "Features layer because the Artifacts page is a list/detail view for strategy artifacts produced by ChainWork. Why not Core? Because artifact data and scoring live in Core (ChainWork Engine). This page just renders the output and links back to the knowledge graph." } },
9257
- // Integration layer
9258
- { entryId: "ARCH-node-cursor", name: "Cursor IDE", order: 0, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F5A5}\uFE0F", description: "AI-assisted development with MCP-powered knowledge context \u2014 smart capture, verification, context assembly in the editor", filePaths: ".cursor/mcp.json, .cursor/rules/", owner: "AI DX", rationale: "Integration layer because Cursor is an external tool that connects INTO our system via MCP. Why not Core or Features? Because Cursor itself is not our code \u2014 it's a consumer. The .cursor/ config files define how it talks to us, but Cursor lives outside our deployment boundary." } },
9259
- { entryId: "ARCH-node-github", name: "GitHub", order: 1, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F419}", description: "Code repository, PR reviews with governance context, CI/CD", owner: "Platform", rationale: "Integration layer because GitHub is an external service. Why not Infrastructure? Because Infra is about plumbing we control (database, analytics). GitHub is a third-party that hooks into our Core (PR reviews checking governance rules) but is not part of our deployed application." } },
9260
- { entryId: "ARCH-node-linear", name: "Linear (planned)", order: 2, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F4D0}", description: "Issue tracking, roadmap sync, tension-to-issue pipeline (planned integration)", owner: "Platform", rationale: "Integration layer because Linear is an external issue tracker. Why not Infrastructure? Because Infra is generic plumbing we run. Linear is a third-party tool we connect to. The planned pipeline bridges tensions (Core) to Linear issues \u2014 a classic outward integration pattern." } }
9261
- ];
9262
- var SEED_FLOWS = [
9263
- { entryId: "ARCH-flow-smart-capture", name: "Capture Flow", order: 0, data: { archType: "flow", sourceNode: "ARCH-node-cursor", targetNode: "ARCH-node-knowledge-graph", description: "Developer/AI calls capture via MCP \u2192 entry created with auto-linking and quality score \u2192 stored in Knowledge Graph", color: "#8b5cf6" } },
9264
- { entryId: "ARCH-flow-governance", name: "Governance Flow", order: 1, data: { archType: "flow", sourceNode: "ARCH-node-tensions", targetNode: "ARCH-node-governance", description: "Tension raised \u2192 appears in Command Center \u2192 triaged \u2192 processed via IDM \u2192 decision logged", color: "#6366f1" } },
9265
- { entryId: "ARCH-flow-chainwork", name: "ChainWork Strategy Flow", order: 2, data: { archType: "flow", sourceNode: "ARCH-node-chainwork-ui", targetNode: "ARCH-node-artifacts", description: "Leader opens ChainWork \u2192 walks coherence chain \u2192 AI generates artifact \u2192 scored and published to knowledge graph", color: "#f59e0b" } },
9266
- { entryId: "ARCH-flow-knowledge-trust", name: "Knowledge Trust Flow", order: 3, data: { archType: "flow", sourceNode: "ARCH-node-mcp", targetNode: "ARCH-node-glossary", description: "MCP verify tool checks entries against codebase \u2192 file existence, schema references validated \u2192 trust scores updated", color: "#22c55e" } },
9267
- { entryId: "ARCH-flow-analytics", name: "Analytics Flow", order: 4, data: { archType: "flow", sourceNode: "ARCH-node-command-center", targetNode: "ARCH-node-posthog", description: "Feature views and actions tracked \u2192 workspace-scoped events \u2192 PostHog group analytics \u2192 Command Center metrics", color: "#ec4899" } }
9268
- ];
9269
- var architectureSchema = z19.object({
9270
- action: z19.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
9271
- template: z19.string().optional().describe("Template entry ID to filter by (for show)"),
9272
- layer: z19.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
9273
- flow: z19.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
9274
- });
9275
- var architectureAdminSchema = z19.object({
9276
- action: z19.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
9277
- });
9278
- function registerArchitectureTools(server) {
9279
- server.registerTool(
9280
- "architecture",
9281
- {
9282
- title: "Architecture",
9283
- 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",
9284
- inputSchema: architectureSchema,
9285
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
9286
- },
9287
- withEnvelope(async ({ action, template, layer, flow }) => {
9288
- await ensureCollection();
9289
- const all = await listArchEntries();
9290
- if (action === "show") {
9291
- const templates = byTag(all, "template");
9292
- const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
9293
- const templateName = activeTemplate?.name ?? "System Architecture";
9294
- const templateId = activeTemplate?.entryId;
9295
- const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
9296
- const nodes = byTag(all, "node");
9297
- const flows = byTag(all, "flow");
9298
- if (layers.length === 0) {
9299
- return {
9300
- content: [{
9301
- type: "text",
9302
- text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
9303
- }],
9304
- structuredContent: failure(
9305
- "NOT_FOUND",
9306
- "No architecture data found.",
9307
- "Seed the default architecture first.",
9308
- [{ tool: "architecture-admin", description: "Seed architecture", parameters: { action: "seed" } }]
9309
- )
9310
- };
9311
- }
9312
- const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
9313
- const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
9314
- (f) => `- **${f.name}**: ${f.data?.description ?? ""}`
9315
- ).join("\n") : "";
9316
- const text = `# ${templateName}
9317
-
9318
- ${textLayers}${textFlows}`;
9319
- const html = renderArchitectureHtml(layers, nodes, flows, templateName);
9320
- return {
9321
- content: [
9322
- { type: "text", text },
9323
- { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: UI_RESOURCE_MIME_TYPE, text: html } }
9324
- ],
9325
- structuredContent: success(
9326
- `Showing ${templateName}: ${layers.length} layers, ${nodes.length} nodes, ${flows.length} flows.`,
9327
- { templateName, layerCount: layers.length, nodeCount: nodes.length, flowCount: flows.length }
9328
- )
9329
- };
9330
- }
9331
- if (action === "explore") {
9332
- if (!layer) {
9333
- return validationResult("A `layer` is required for explore.");
9334
- }
9335
- const layers = byTag(all, "layer");
9336
- const target = layers.find(
9337
- (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
9338
- );
9339
- if (!target) {
9340
- const available = layers.map((l) => `\`${l.name}\``).join(", ");
9341
- return {
9342
- content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }],
9343
- structuredContent: failure(
9344
- "NOT_FOUND",
9345
- `Layer "${layer}" not found.`,
9346
- `Available layers: ${available}`
9347
- )
9348
- };
9349
- }
9350
- const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
9351
- const flows = byTag(all, "flow").filter(
9352
- (f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
9353
- );
9354
- const depRule = target.data?.dependsOn ? `
9355
- **Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
9356
- ` : "";
9357
- const layerRationale = target.data?.rationale ? `
9358
- > ${target.data.rationale}
9359
- ` : "";
9360
- const nodeDetail = nodes.map((n) => {
9361
- const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
9362
- if (n.data?.description) lines.push(String(n.data.description));
9363
- if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
9364
- if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
9365
- if (n.data?.rationale) lines.push(`
9366
- **Why here?** ${n.data.rationale}`);
9367
- return lines.join("\n");
9368
- }).join("\n\n");
9369
- const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
9370
- return {
9371
- content: [{
9372
- type: "text",
9373
- text: `# ${target.data?.icon ?? ""} ${target.name} Layer
9374
-
9375
- ${target.data?.description ?? ""}${depRule}${layerRationale}
9376
- **${nodes.length} components**
9377
-
9378
- ${nodeDetail}${flowLines}`
9379
- }],
9380
- structuredContent: success(
9381
- `${target.name} layer: ${nodes.length} components, ${flows.length} connected flows.`,
9382
- { layerName: target.name, entryId: target.entryId, nodeCount: nodes.length, flowCount: flows.length }
9383
- )
9384
- };
9385
- }
9386
- if (action === "flow") {
9387
- if (!flow) {
9388
- return validationResult("A `flow` name or entry ID is required.");
9389
- }
9390
- const flows = byTag(all, "flow");
9391
- const target = flows.find(
9392
- (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
9393
- );
9394
- if (!target) {
9395
- const available = flows.map((f) => `\`${f.name}\``).join(", ");
9396
- return {
9397
- content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }],
9398
- structuredContent: failure(
9399
- "NOT_FOUND",
9400
- `Flow "${flow}" not found.`,
9401
- `Available flows: ${available}`
9402
- )
9403
- };
9404
- }
9405
- const nodes = byTag(all, "node");
9406
- const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
9407
- const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
9408
- const lines = [
9409
- `# ${target.name}`,
9410
- "",
9411
- `**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
9412
- "",
9413
- String(target.data?.description ?? "")
9414
- ];
9415
- if (source) {
9416
- lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
9417
- if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
9418
- }
9419
- if (dest) {
9420
- lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
9421
- if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
9422
- }
9423
- return {
9424
- content: [{ type: "text", text: lines.join("\n") }],
9425
- structuredContent: success(
9426
- `${target.name}: ${source?.name ?? "?"} \u2192 ${dest?.name ?? "?"}.`,
9427
- { flowName: target.name, entryId: target.entryId, source: source?.name, target: dest?.name }
9428
- )
9429
- };
9430
- }
9431
- return unknownAction(action, ["show", "explore", "flow"]);
9432
- })
9433
- );
9434
- const archAdminTool = server.registerTool(
9435
- "architecture-admin",
9436
- {
9437
- title: "Architecture Admin",
9438
- 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.",
9439
- inputSchema: architectureAdminSchema,
9440
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
9441
- },
9442
- withEnvelope(async ({ action }) => {
9443
- if (action === "seed") {
9444
- await ensureCollection();
9445
- const existing = await listArchEntries();
9446
- const existingIds = new Set(existing.map((e) => e.entryId));
9447
- let created = 0;
9448
- let updated = 0;
9449
- let unchanged = 0;
9450
- const allSeeds = [
9451
- { ...SEED_TEMPLATE, order: 0, status: "active" },
9452
- ...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
9453
- ...SEED_NODES.map((n) => ({ ...n, status: "active" })),
9454
- ...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
9455
- ];
9456
- for (const seed of allSeeds) {
9457
- if (existingIds.has(seed.entryId)) {
9458
- const existingEntry = existing.find((e) => e.entryId === seed.entryId);
9459
- const existingData = existingEntry?.data ?? {};
9460
- const seedData = seed.data;
9461
- const hasChanges = Object.keys(seedData).some(
9462
- (k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
9463
- );
9464
- if (hasChanges) {
9465
- const mergedData = { ...existingData, ...seedData };
9466
- await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
9467
- updated++;
9468
- } else {
9469
- unchanged++;
9470
- }
9471
- continue;
9472
- }
9473
- await mcpMutation("chain.createEntry", {
9474
- collectionSlug: COLLECTION_SLUG,
9475
- entryId: seed.entryId,
9476
- name: seed.name,
9477
- status: seed.status,
9478
- data: seed.data,
9479
- order: seed.order ?? 0
9480
- });
9481
- created++;
9482
- }
9483
- return {
9484
- content: [{
9485
- type: "text",
9486
- text: `# Architecture Seeded
9487
-
9488
- **Created:** ${created} entries
9489
- **Updated:** ${updated} (merged new fields)
9490
- **Unchanged:** ${unchanged}
9491
-
9492
- Use \`architecture action=show\` to view the map.`
9493
- }],
9494
- structuredContent: success(
9495
- `Architecture seeded: ${created} created, ${updated} updated, ${unchanged} unchanged.`,
9496
- { created, updated, unchanged },
9497
- [{ tool: "architecture", description: "View the map", parameters: { action: "show" } }]
9498
- )
9499
- };
9500
- }
9501
- if (action === "check") {
9502
- const projectRoot = resolveProjectRoot2();
9503
- if (!projectRoot) {
9504
- return {
9505
- content: [{
9506
- type: "text",
9507
- text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
9508
- }],
9509
- structuredContent: failure(
9510
- "VALIDATION_ERROR",
9511
- "Cannot find project root.",
9512
- "Set WORKSPACE_PATH env var to the absolute path of the Product OS project root."
9513
- )
9514
- };
9515
- }
9516
- await ensureCollection();
9517
- const all = await listArchEntries();
9518
- const layers = byTag(all, "layer");
9519
- const nodes = byTag(all, "node");
9520
- const result = scanDependencies(projectRoot, layers, nodes);
9521
- return {
9522
- content: [{ type: "text", text: formatScanReport(result) }],
9523
- structuredContent: success(
9524
- result.violations.length === 0 ? `Health check passed: 0 violations across ${result.filesScanned} files.` : `Health check found ${result.violations.length} violation(s) across ${result.filesScanned} files.`,
9525
- {
9526
- violations: result.violations.length,
9527
- filesScanned: result.filesScanned,
9528
- importsChecked: result.importsChecked,
9529
- unmappedImports: result.unmappedImports
9530
- }
9531
- )
9532
- };
9533
- }
9534
- return unknownAction(action, ["seed", "check"]);
9535
- })
9536
- );
9537
- trackWriteTool(archAdminTool);
9538
- }
9539
- function resolveProjectRoot2() {
9540
- const candidates = [
9541
- process.env.WORKSPACE_PATH,
9542
- process.cwd(),
9543
- resolve3(process.cwd(), "..")
9544
- ].filter(Boolean);
9545
- for (const dir of candidates) {
9546
- const resolved = resolve3(dir);
9547
- if (existsSync3(resolve3(resolved, "convex/schema.ts"))) return resolved;
9548
- }
9549
- return null;
9550
- }
9551
- function scanDependencies(projectRoot, layers, nodes) {
9552
- const layerMap = /* @__PURE__ */ new Map();
9553
- for (const l of layers) layerMap.set(l.entryId, l);
9554
- const allowedDeps = buildAllowedDeps(layers);
9555
- const nodePathPrefixes = buildNodePrefixes(nodes);
9556
- const violations = [];
9557
- const nodeResults = /* @__PURE__ */ new Map();
9558
- let totalFiles = 0;
9559
- let totalImports = 0;
9560
- let unmapped = 0;
9561
- for (const node of nodes) {
9562
- const layerRef = String(node.data?.layerRef ?? "");
9563
- const layer = layerMap.get(layerRef);
9564
- if (!layer) continue;
9565
- const filePaths = parseFilePaths(node);
9566
- const nodeViolations = [];
9567
- let nodeFileCount = 0;
9568
- for (const fp of filePaths) {
9569
- const absPath = resolve3(projectRoot, fp);
9570
- const files = collectFiles(absPath);
9571
- for (const file of files) {
9572
- nodeFileCount++;
9573
- totalFiles++;
9574
- const relFile = relative(projectRoot, file);
9575
- const imports = parseImports(file);
9576
- for (const imp of imports) {
9577
- totalImports++;
9578
- const resolved = resolveImport(imp, file, projectRoot);
9579
- if (!resolved) {
9580
- unmapped++;
9581
- continue;
9582
- }
9583
- const targetNode = findNodeByPath(resolved, nodePathPrefixes);
9584
- if (!targetNode) {
9585
- unmapped++;
9586
- continue;
9587
- }
9588
- const targetLayerRef = String(targetNode.data?.layerRef ?? "");
9589
- const targetLayer = layerMap.get(targetLayerRef);
9590
- if (!targetLayer) continue;
9591
- if (targetLayerRef === layerRef) continue;
9592
- const allowed = allowedDeps.get(layerRef);
9593
- if (allowed && !allowed.has(targetLayerRef)) {
9594
- const v = {
9595
- sourceNode: node.name,
9596
- sourceLayer: layer.name,
9597
- sourceFile: relFile,
9598
- importPath: imp,
9599
- targetNode: targetNode.name,
9600
- targetLayer: targetLayer.name,
9601
- rule: `${layer.name} cannot import from ${targetLayer.name}`
9602
- };
9603
- violations.push(v);
9604
- nodeViolations.push(v);
9605
- }
9606
- }
9607
- }
9608
- }
9609
- nodeResults.set(node.entryId, { violations: nodeViolations, filesScanned: nodeFileCount });
9610
- }
9611
- return { violations, filesScanned: totalFiles, importsChecked: totalImports, unmappedImports: unmapped, nodeResults };
9612
- }
9613
- function buildAllowedDeps(layers) {
9614
- const nameToId = /* @__PURE__ */ new Map();
9615
- for (const l of layers) nameToId.set(l.name.toLowerCase(), l.entryId);
9616
- const allowed = /* @__PURE__ */ new Map();
9617
- for (const layer of layers) {
9618
- const deps = String(layer.data?.dependsOn ?? "none");
9619
- const set = /* @__PURE__ */ new Set();
9620
- if (deps !== "none") {
9621
- for (const dep of deps.split(",").map((d) => d.trim().toLowerCase())) {
9622
- const id = nameToId.get(dep);
9623
- if (id) set.add(id);
9624
- }
9625
- }
9626
- allowed.set(layer.entryId, set);
9627
- }
9628
- return allowed;
9629
- }
9630
- function buildNodePrefixes(nodes) {
9631
- const entries = [];
9632
- for (const node of nodes) {
9633
- for (const fp of parseFilePaths(node)) {
9634
- entries.push({ prefix: normalize(fp), node });
9635
- }
9636
- }
9637
- entries.sort((a, b) => b.prefix.length - a.prefix.length);
9638
- return entries;
9639
- }
9640
- function parseFilePaths(node) {
9641
- const raw = node.data?.filePaths;
9642
- if (!raw || typeof raw !== "string") return [];
9643
- return raw.split(",").map((p) => p.trim()).filter(Boolean);
9644
- }
9645
- function collectFiles(absPath) {
9646
- if (!existsSync3(absPath)) return [];
9647
- const stat = statSync(absPath);
9648
- if (stat.isFile()) {
9649
- return isScannableFile(absPath) ? [absPath] : [];
9650
- }
9651
- if (!stat.isDirectory()) return [];
9652
- const results = [];
9653
- const walk = (dir) => {
9654
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
9655
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
9656
- const full = resolve3(dir, entry.name);
9657
- if (entry.isDirectory()) walk(full);
9658
- else if (isScannableFile(full)) results.push(full);
9659
- }
9660
- };
9661
- walk(absPath);
9662
- return results;
9663
- }
9664
- function isScannableFile(p) {
9665
- return /\.(ts|js|svelte)$/.test(p) && !p.endsWith(".d.ts");
9666
- }
9667
- function parseImports(filePath) {
9668
- try {
9669
- const content = readFileSync2(filePath, "utf-8");
9670
- const re = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
9671
- const imports = [];
9672
- let match;
9673
- while ((match = re.exec(content)) !== null) {
9674
- imports.push(match[1]);
9675
- }
9676
- return imports;
9677
- } catch {
9678
- return [];
9679
- }
9680
- }
9681
- var EXTENSIONS = [".ts", ".js", ".svelte", "/index.ts", "/index.js", "/index.svelte"];
9682
- function tryResolveWithExtension(absPath) {
9683
- if (existsSync3(absPath) && statSync(absPath).isFile()) return absPath;
9684
- for (const ext of EXTENSIONS) {
9685
- const withExt = absPath + ext;
9686
- if (existsSync3(withExt)) return withExt;
9190
+ var EXTENSIONS = [".ts", ".js", ".svelte", "/index.ts", "/index.js", "/index.svelte"];
9191
+ function tryResolveWithExtension(absPath) {
9192
+ if (existsSync2(absPath) && statSync(absPath).isFile()) return absPath;
9193
+ for (const ext of EXTENSIONS) {
9194
+ const withExt = absPath + ext;
9195
+ if (existsSync2(withExt)) return withExt;
9687
9196
  }
9688
9197
  return null;
9689
9198
  }
@@ -9693,12 +9202,12 @@ function resolveImport(imp, fromFile, root) {
9693
9202
  else if (imp.startsWith("$convex/")) rel = imp.replace("$convex/", "convex/");
9694
9203
  else if (imp.startsWith("$env/") || imp.startsWith("$app/")) return null;
9695
9204
  else if (imp.startsWith("./") || imp.startsWith("../")) {
9696
- const fromDir = dirname2(fromFile);
9697
- const abs2 = resolve3(fromDir, imp);
9205
+ const fromDir = dirname(fromFile);
9206
+ const abs2 = resolve2(fromDir, imp);
9698
9207
  rel = relative(root, abs2);
9699
9208
  }
9700
9209
  if (!rel) return null;
9701
- const abs = resolve3(root, rel);
9210
+ const abs = resolve2(root, rel);
9702
9211
  const actual = tryResolveWithExtension(abs);
9703
9212
  return actual ? relative(root, actual) : rel;
9704
9213
  }
@@ -10349,376 +9858,819 @@ function registerHealthTools(server) {
10349
9858
  }
10350
9859
  }
10351
9860
  return {
10352
- content: [{ type: "text", text: scanLines.join("\n") }],
9861
+ content: [{ type: "text", text: scanLines.join("\n") }],
9862
+ structuredContent: success(
9863
+ "Workspace is blank. Use the start tool for guided setup.",
9864
+ { stage: "blank", readinessScore: readiness?.score ?? 0, oriented, orientationStatus, redirectHint: "Use the `start` tool for the full guided setup experience." },
9865
+ [{ tool: "start", description: "Guided workspace setup", parameters: {} }]
9866
+ )
9867
+ };
9868
+ }
9869
+ const lines = [];
9870
+ const isLowReadiness = readiness && readiness.score < 50;
9871
+ if (wsCtx) {
9872
+ lines.push(`# ${wsCtx.workspaceName}`);
9873
+ lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
9874
+ } else {
9875
+ lines.push("# Workspace");
9876
+ lines.push("_Could not resolve workspace._");
9877
+ }
9878
+ lines.push("");
9879
+ if (mode === "brief") {
9880
+ const briefStage = readiness?.stage ?? (readiness?.score != null ? readiness.score < 50 ? "seeded" : "grounded" : "unknown");
9881
+ lines.push(`Brain stage: ${briefStage}`);
9882
+ if (orientEntries?.strategicContext) {
9883
+ const sc = orientEntries.strategicContext;
9884
+ if (sc.vision) lines.push(`Vision: ${sc.vision}`);
9885
+ if (sc.purpose) lines.push(`Purpose: ${sc.purpose}`);
9886
+ if (sc.productAreaCount != null && sc.productAreaCount > 0) {
9887
+ lines.push(`Product areas (${sc.productAreaCount}): ${(sc.productAreas ?? []).join(", ")}`);
9888
+ }
9889
+ lines.push(`${sc.activeBetCount} active bet(s), ${sc.activeTensionCount} tension(s).`);
9890
+ }
9891
+ if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
9892
+ lines.push(`Task context: ${orientEntries.taskContext.totalFound} relevant entries (${orientEntries.taskContext.confidence} confidence).`);
9893
+ }
9894
+ if (orientEntries?.activeBets?.length > 0) {
9895
+ for (const e of orientEntries.activeBets) {
9896
+ const tensions = e.linkedTensions;
9897
+ const tensionPart = tensions?.length ? ` \u2014 ${tensions.map((t) => `${t.entryId ?? t.name} (${t.severity ?? "\u2014"})`).join(", ")}` : "";
9898
+ lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
9899
+ }
9900
+ }
9901
+ if (recoveryBlock) {
9902
+ lines.push("");
9903
+ lines.push(...formatRecoveryBlock(recoveryBlock));
9904
+ } else if (priorSessions.length > 0) {
9905
+ const last = priorSessions[0];
9906
+ const date = new Date(last.startedAt).toISOString().split("T")[0];
9907
+ const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
9908
+ const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
9909
+ lines.push(`Last session (${date}): ${created} created, ${modified} modified`);
9910
+ }
9911
+ if (orientEntries) {
9912
+ const mapGovernanceEntry = (e) => ({
9913
+ entryId: e.entryId,
9914
+ name: e.name,
9915
+ description: typeof e.preview === "string" ? e.preview : void 0
9916
+ });
9917
+ lines.push("");
9918
+ lines.push(...buildOperatingProtocol({
9919
+ principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
9920
+ standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
9921
+ businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
9922
+ }, task));
9923
+ }
9924
+ if (agentSessionId) {
9925
+ try {
9926
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
9927
+ setSessionOriented(true);
9928
+ } catch {
9929
+ }
9930
+ lines.push("---");
9931
+ lines.push(`Orientation complete. Session ${agentSessionId}.`);
9932
+ } else {
9933
+ lines.push("---");
9934
+ lines.push("_No active agent session. Call `session action=start` to begin._");
9935
+ }
9936
+ if (errors.length > 0) {
9937
+ lines.push("");
9938
+ for (const err of errors) lines.push(`- ${err}`);
9939
+ }
9940
+ return {
9941
+ content: [{ type: "text", text: lines.join("\n") }],
10353
9942
  structuredContent: success(
10354
- "Workspace is blank. Use the start tool for guided setup.",
10355
- { stage: "blank", readinessScore: readiness?.score ?? 0, oriented, orientationStatus, redirectHint: "Use the `start` tool for the full guided setup experience." },
10356
- [{ tool: "start", description: "Guided workspace setup", parameters: {} }]
9943
+ `Oriented (brief). Stage: ${readiness?.stage ?? "unknown"}.`,
9944
+ { mode: "brief", stage: readiness?.stage ?? "unknown", oriented: isSessionOriented(), sessionId: agentSessionId }
10357
9945
  )
10358
9946
  };
10359
9947
  }
10360
- const lines = [];
10361
- const isLowReadiness = readiness && readiness.score < 50;
10362
- if (wsCtx) {
10363
- lines.push(`# ${wsCtx.workspaceName}`);
10364
- lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
10365
- } else {
10366
- lines.push("# Workspace");
10367
- lines.push("_Could not resolve workspace._");
9948
+ const orientStage = readiness?.stage ?? "seeded";
9949
+ if (isLowReadiness && wsCtx?.createdAt) {
9950
+ const ageDays = Math.floor((Date.now() - wsCtx.createdAt) / (1e3 * 60 * 60 * 24));
9951
+ if (ageDays >= 30) {
9952
+ lines.push(`Your workspace has been around for ${ageDays} days and is still at the **${orientStage}** stage.`);
9953
+ lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
9954
+ lines.push("");
9955
+ }
10368
9956
  }
10369
- lines.push("");
10370
- if (mode === "brief") {
10371
- const briefStage = readiness?.stage ?? (readiness?.score != null ? readiness.score < 50 ? "seeded" : "grounded" : "unknown");
10372
- lines.push(`Brain stage: ${briefStage}`);
9957
+ if (isLowReadiness) {
9958
+ lines.push(`**Brain stage: ${orientStage}.**`);
9959
+ lines.push("");
9960
+ const gaps = readiness.gaps ?? [];
9961
+ if (gaps.length > 0) {
9962
+ const gap = gaps[0];
9963
+ const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
9964
+ lines.push("## Recommended next step");
9965
+ lines.push(`**${gap.label}**`);
9966
+ lines.push("");
9967
+ lines.push(cta);
9968
+ lines.push("");
9969
+ lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
9970
+ lines.push("");
9971
+ const remainingGaps = gaps.length - 1;
9972
+ if (remainingGaps > 0 || openTensions.length > 0) {
9973
+ lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
9974
+ lines.push("");
9975
+ }
9976
+ }
9977
+ lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)?_");
9978
+ lines.push("_Use `collections action=create` to add it, or ask me to propose collections for your domain._");
9979
+ lines.push("");
9980
+ } else if (readiness) {
9981
+ lines.push(`**Brain stage: ${orientStage}.**`);
9982
+ lines.push("");
10373
9983
  if (orientEntries?.strategicContext) {
10374
9984
  const sc = orientEntries.strategicContext;
10375
- if (sc.vision) lines.push(`Vision: ${sc.vision}`);
10376
- if (sc.purpose) lines.push(`Purpose: ${sc.purpose}`);
9985
+ lines.push("## Strategic Context");
9986
+ if (sc.vision) lines.push(`**Vision:** ${sc.vision}`);
9987
+ if (sc.purpose) lines.push(`**Purpose:** ${sc.purpose}`);
10377
9988
  if (sc.productAreaCount != null && sc.productAreaCount > 0) {
10378
- lines.push(`Product areas (${sc.productAreaCount}): ${(sc.productAreas ?? []).join(", ")}`);
9989
+ lines.push(`**Product areas (${sc.productAreaCount}):** ${(sc.productAreas ?? []).join(", ")}`);
10379
9990
  }
10380
- lines.push(`${sc.activeBetCount} active bet(s), ${sc.activeTensionCount} tension(s).`);
9991
+ const betLine = sc.currentBet ? `**Current bet:** ${sc.currentBet}. ${sc.activeBetCount} active bet(s).` : "No active bets.";
9992
+ lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
9993
+ lines.push("");
9994
+ }
9995
+ if (orientEntries?.continuingFrom && orientEntries.continuingFrom.length > 0) {
9996
+ lines.push("## Continuing from");
9997
+ lines.push("_Prior-session entries most relevant to your task._");
9998
+ lines.push("");
9999
+ for (const e of orientEntries.continuingFrom) {
10000
+ const id = e.entryId ?? e.name;
10001
+ const type = e.canonicalKey ?? "generic";
10002
+ const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10003
+ lines.push(`- \`${id}\` (score ${e.score}) [${type}]${coll} \u2014 ${e.name}`);
10004
+ if (e.reasoning) lines.push(` _${e.reasoning}_`);
10005
+ }
10006
+ lines.push("");
10007
+ }
10008
+ if (orientEntries?.lastSessionTouched && orientEntries.lastSessionTouched.length > 0) {
10009
+ lines.push("## Last session touched");
10010
+ lines.push("_Entries created or modified in your most recent session._");
10011
+ lines.push("");
10012
+ for (const e of orientEntries.lastSessionTouched) {
10013
+ const id = e.entryId ?? e.name;
10014
+ const type = e.canonicalKey ?? "generic";
10015
+ const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10016
+ lines.push(`- \`${id}\` [${type}]${coll} \u2014 ${e.name}`);
10017
+ }
10018
+ lines.push("");
10381
10019
  }
10382
10020
  if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
10383
- lines.push(`Task context: ${orientEntries.taskContext.totalFound} relevant entries (${orientEntries.taskContext.confidence} confidence).`);
10021
+ const tc = orientEntries.taskContext;
10022
+ lines.push("## Task Context");
10023
+ lines.push(`_Task-scoped entries (${tc.confidence} confidence, ${tc.totalFound} relevant)`);
10024
+ lines.push("");
10025
+ for (const e of tc.context) {
10026
+ const id = e.entryId ?? e.name;
10027
+ const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10028
+ lines.push(`- \`${id}\` (score ${e.score})${coll}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
10029
+ }
10030
+ lines.push("");
10384
10031
  }
10385
- if (orientEntries?.activeBets?.length > 0) {
10386
- for (const e of orientEntries.activeBets) {
10387
- const tensions = e.linkedTensions;
10388
- const tensionPart = tensions?.length ? ` \u2014 ${tensions.map((t) => `${t.entryId ?? t.name} (${t.severity ?? "\u2014"})`).join(", ")}` : "";
10389
- lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
10032
+ if (task && orientEntries) {
10033
+ const result = runAlignmentCheck(
10034
+ task,
10035
+ orientEntries.activeBets ?? [],
10036
+ orientEntries.taskContext?.context
10037
+ );
10038
+ lines.push(...buildAlignmentCheckLines(result));
10039
+ }
10040
+ if (orientEntries) {
10041
+ const fmt = (e) => {
10042
+ const type = e.canonicalKey ?? "generic";
10043
+ const stratum = e.stratum ?? "?";
10044
+ return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
10045
+ };
10046
+ if (orientEntries.activeBets?.length > 0) {
10047
+ lines.push("## Active bets \u2014 current scope");
10048
+ lines.push("_These define what you're building now. Work outside these bets requires explicit user confirmation before designing._");
10049
+ lines.push("");
10050
+ for (const e of orientEntries.activeBets) {
10051
+ lines.push(fmt(e));
10052
+ const tensions = e.linkedTensions;
10053
+ if (tensions?.length) {
10054
+ const tensionLines = tensions.map((t) => {
10055
+ const meta = [t.severity, t.priority].filter(Boolean).join(", ");
10056
+ return `\`${t.entryId ?? t.name}\` (${t.name}${meta ? `, ${meta}` : ""})`;
10057
+ });
10058
+ lines.push(` Tensions: ${tensionLines.join("; ")}`);
10059
+ } else {
10060
+ lines.push(` Tensions: No linked tensions`);
10061
+ }
10062
+ }
10063
+ lines.push("");
10064
+ }
10065
+ if (orientEntries.activeGoals.length > 0) {
10066
+ lines.push("## Active goals");
10067
+ orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
10068
+ lines.push("");
10069
+ }
10070
+ if (orientEntries.recentDecisions.length > 0) {
10071
+ lines.push("## Recent decisions");
10072
+ orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
10073
+ lines.push("");
10074
+ }
10075
+ if (orientEntries.recentlySuperseded.length > 0) {
10076
+ lines.push("## Recently superseded");
10077
+ orientEntries.recentlySuperseded.forEach((e) => lines.push(fmt(e)));
10078
+ lines.push("");
10079
+ }
10080
+ if (orientEntries.staleEntries.length > 0) {
10081
+ lines.push("## Needs confirmation");
10082
+ lines.push(`_Domain stratum entries not confirmed in ${orientEntries.stalenessThresholdDays} days._`);
10083
+ orientEntries.staleEntries.forEach((e) => lines.push(fmt(e)));
10084
+ lines.push("");
10085
+ }
10086
+ const hasPrinciples = orientEntries.principles?.length > 0;
10087
+ const hasStandards = orientEntries.standards?.length > 0;
10088
+ const hasBusinessRules = orientEntries.businessRules.length > 0;
10089
+ if (hasPrinciples || hasStandards || hasBusinessRules) {
10090
+ lines.push("## Workspace Governance \u2014 constraints");
10091
+ lines.push("_These constrain what you build. If your proposal conflicts with any of these, stop and flag it. Do not design around them without explicit user confirmation._");
10092
+ lines.push("");
10093
+ if (hasPrinciples) {
10094
+ lines.push("**Principles** \u2014 beliefs that guide decisions:");
10095
+ orientEntries.principles.forEach((e) => lines.push(fmt(e)));
10096
+ lines.push("");
10097
+ }
10098
+ if (hasStandards) {
10099
+ lines.push("**Standards** \u2014 conventions for how work is done:");
10100
+ orientEntries.standards.forEach((e) => lines.push(fmt(e)));
10101
+ lines.push("");
10102
+ }
10103
+ if (hasBusinessRules) {
10104
+ lines.push("**Business Rules** \u2014 system constraints:");
10105
+ orientEntries.businessRules.forEach((e) => lines.push(fmt(e)));
10106
+ lines.push("");
10107
+ }
10108
+ }
10109
+ if (orientEntries.architectureNotes.length > 0) {
10110
+ lines.push("## Architecture notes");
10111
+ orientEntries.architectureNotes.forEach((e) => lines.push(fmt(e)));
10112
+ lines.push("");
10113
+ }
10114
+ const mapGovernanceEntry = (e) => ({
10115
+ entryId: e.entryId,
10116
+ name: e.name,
10117
+ description: typeof e.preview === "string" ? e.preview : void 0
10118
+ });
10119
+ lines.push(...buildOperatingProtocol({
10120
+ principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
10121
+ standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
10122
+ businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
10123
+ }, task));
10124
+ }
10125
+ let allEntries = [];
10126
+ try {
10127
+ allEntries = await mcpQuery("chain.listEntries", {}) ?? [];
10128
+ } catch {
10129
+ }
10130
+ const plannedWork = buildPlannedWork(allEntries);
10131
+ if (hasPlannedWork(plannedWork)) {
10132
+ lines.push(...buildPlannedWorkSection(plannedWork, priorSessions, recoveryBlock));
10133
+ } else {
10134
+ const briefingItems = [];
10135
+ if (priorSessions.length > 0 && !recoveryBlock) {
10136
+ const last = priorSessions[0];
10137
+ const date = new Date(last.startedAt).toISOString().split("T")[0];
10138
+ const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
10139
+ const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
10140
+ briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
10141
+ }
10142
+ if (readiness.gaps?.length > 0) {
10143
+ briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
10144
+ }
10145
+ if (briefingItems.length > 0) {
10146
+ lines.push("## Briefing");
10147
+ for (const item of briefingItems) {
10148
+ lines.push(`- ${item}`);
10149
+ }
10150
+ lines.push("");
10151
+ }
10152
+ if (recoveryBlock) {
10153
+ lines.push("");
10154
+ lines.push(...formatRecoveryBlock(recoveryBlock));
10390
10155
  }
10391
10156
  }
10392
- if (recoveryBlock) {
10157
+ const activeEntries = allEntries.filter((e) => e.status === "active");
10158
+ if (activeEntries.length > 0) {
10159
+ const orgHealth = computeOrganisationHealth(activeEntries);
10160
+ if (orgHealth.disagreements > 0) {
10161
+ lines.push("## Organisation Health");
10162
+ lines.push(...formatOrgHealthLines(orgHealth, 3));
10163
+ lines.push("");
10164
+ }
10165
+ }
10166
+ const epistemicEntries = activeEntries.filter(
10167
+ (e) => e.collectionSlug === "insights" || e.collectionSlug === "assumptions"
10168
+ );
10169
+ if (epistemicEntries.length > 0) {
10170
+ let validated = 0;
10171
+ let evidenced = 0;
10172
+ let hypotheses = 0;
10173
+ let untested = 0;
10174
+ for (const e of epistemicEntries) {
10175
+ const ws = e.workflowStatus;
10176
+ if (ws === "validated") validated++;
10177
+ else if (ws === "evidenced") evidenced++;
10178
+ else if (ws === "testing") evidenced++;
10179
+ else if (ws === "invalidated") validated++;
10180
+ else if (e.collectionSlug === "assumptions") untested++;
10181
+ else hypotheses++;
10182
+ }
10183
+ const parts = [];
10184
+ if (validated > 0) parts.push(`${validated} validated`);
10185
+ if (evidenced > 0) parts.push(`${evidenced} evidenced`);
10186
+ if (hypotheses > 0) parts.push(`${hypotheses} hypotheses`);
10187
+ if (untested > 0) parts.push(`${untested} untested`);
10188
+ lines.push(`**Epistemic health:** ${parts.join(" \xB7 ")} _(${epistemicEntries.length} claim-carrying entries)_`);
10393
10189
  lines.push("");
10394
- lines.push(...formatRecoveryBlock(recoveryBlock));
10395
- } else if (priorSessions.length > 0) {
10396
- const last = priorSessions[0];
10397
- const date = new Date(last.startedAt).toISOString().split("T")[0];
10398
- const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
10399
- const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
10400
- lines.push(`Last session (${date}): ${created} created, ${modified} modified`);
10401
10190
  }
10402
- if (orientEntries) {
10403
- const mapGovernanceEntry = (e) => ({
10404
- entryId: e.entryId,
10405
- name: e.name,
10406
- description: typeof e.preview === "string" ? e.preview : void 0
10191
+ lines.push("What would you like to work on?");
10192
+ lines.push("");
10193
+ }
10194
+ if (errors.length > 0) {
10195
+ lines.push("## Errors");
10196
+ for (const err of errors) lines.push(`- ${err}`);
10197
+ lines.push("");
10198
+ }
10199
+ if (agentSessionId) {
10200
+ try {
10201
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
10202
+ setSessionOriented(true);
10203
+ lines.push("---");
10204
+ lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
10205
+ } catch {
10206
+ lines.push("---");
10207
+ lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
10208
+ }
10209
+ try {
10210
+ await mcpMutation("chain.recordSessionSignal", {
10211
+ sessionId: agentSessionId,
10212
+ signalType: "immediate_context_load",
10213
+ metadata: { source: "orient" }
10407
10214
  });
10408
- lines.push("");
10409
- lines.push(...buildOperatingProtocol({
10410
- principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
10411
- standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
10412
- businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
10413
- }, task));
10215
+ } catch (err) {
10216
+ process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
10217
+ `);
10414
10218
  }
10415
- if (agentSessionId) {
10416
- try {
10417
- await mcpCall("agent.markOriented", { sessionId: agentSessionId });
10418
- setSessionOriented(true);
10419
- } catch {
10420
- }
10421
- lines.push("---");
10422
- lines.push(`Orientation complete. Session ${agentSessionId}.`);
10219
+ } else {
10220
+ lines.push("---");
10221
+ lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
10222
+ }
10223
+ return {
10224
+ content: [{ type: "text", text: lines.join("\n") }],
10225
+ structuredContent: success(
10226
+ `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
10227
+ { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
10228
+ )
10229
+ };
10230
+ })
10231
+ );
10232
+ }
10233
+
10234
+ // src/resources/index.ts
10235
+ import { existsSync as existsSync3 } from "fs";
10236
+ import { dirname as dirname2, join, resolve as resolve3 } from "path";
10237
+ import { fileURLToPath } from "url";
10238
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
10239
+ var MODULE_DIR = dirname2(fileURLToPath(import.meta.url));
10240
+ var UI_VIEW_BASE_CANDIDATES = [
10241
+ resolve3(MODULE_DIR, "views"),
10242
+ resolve3(MODULE_DIR, "..", "views"),
10243
+ resolve3(MODULE_DIR, "..", "..", "dist", "views"),
10244
+ resolve3(MODULE_DIR, "..", "..", "..", "mcp-views", "dist")
10245
+ ];
10246
+ function formatEntryMarkdown(entry) {
10247
+ const id = entry.entryId ? `${entry.entryId}: ` : "";
10248
+ const lines = [`## ${id}${entry.name} [${entry.status}]`];
10249
+ if (entry.data && typeof entry.data === "object") {
10250
+ for (const [key, val] of Object.entries(entry.data)) {
10251
+ if (val && key !== "rawData") {
10252
+ lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
10253
+ }
10254
+ }
10255
+ }
10256
+ return lines.join("\n");
10257
+ }
10258
+ function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles) {
10259
+ const sections = ["# Product Brain \u2014 Orientation"];
10260
+ sections.push(
10261
+ "## About Product Brain (PB)\nPB is a knowledge management system used by many product teams. This section describes how PB works \u2014 not your product.\n\n### How PB Organizes Its Tools\n- **Tools** = actions with side-effects or dynamic computation (capture, commit, search).\n- **Resources** = stable, read-only data (orientation, terminology, collection schemas).\n- **Prompts** = multi-step choreography (workflows, guided capture, deep dives).\n- Every tool, resource, and prompt works for ANY workspace \u2014 no bespoke logic.\n- Resource templates (URI params) serve workspace-specific data through generic patterns.\n- Compound tools with `action` enums keep the tool count minimal.\n\n### How PB Handles Your Data\n- **Draft-first**: all writes create drafts. SSOT requires explicit user confirmation.\n- **Empty-workspace safe**: every operation handles zero entries and fresh workspaces.\n- **Advisory, not blocking**: quality scores, contradiction checks, and coaching never prevent operations.\n- **Workspace-agnostic**: PB is a product for many teams \u2014 no workspace-specific logic.\n- **Self-documenting**: orient and server instructions teach agents how PB works."
10262
+ );
10263
+ const wsRules = [];
10264
+ for (const p of (principles ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
10265
+ wsRules.push({ id: p.entryId ?? "", name: p.name, severity: p.data?.severity ?? void 0, source: "principles" });
10266
+ }
10267
+ for (const s of (standards ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
10268
+ wsRules.push({ id: s.entryId ?? "", name: s.name, severity: s.data?.severity ?? void 0, source: "standards" });
10269
+ }
10270
+ for (const r of (businessRules ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
10271
+ wsRules.push({ id: r.entryId ?? "", name: r.name, severity: r.data?.severity ?? void 0, source: "business-rules" });
10272
+ }
10273
+ if (wsRules.length > 0) {
10274
+ const ruleLines = wsRules.map((r) => {
10275
+ const sev = r.severity ? ` [${r.severity}]` : "";
10276
+ return `- **${r.id}**: ${r.name}${sev}`;
10277
+ }).join("\n");
10278
+ sections.push(
10279
+ `## Your Workspace Principles & Rules (${wsRules.length} active)
10280
+ These are principles, standards, and rules your team has committed to the Chain. Respect them during implementation.
10281
+
10282
+ ` + ruleLines + '\n\nUse `entries action=get entryId="<ID>"` to drill into any rule before making changes in that area.'
10283
+ );
10284
+ } else {
10285
+ sections.push(
10286
+ "## Your Workspace Principles & Rules\nNo active principles, standards, or business rules on the Chain yet.\nUse `capture` with collection `principles`, `standards`, or `business-rules` to add your team's guardrails.\nOnce committed, they appear here at orient time \u2014 so every agent session starts with your rules visible."
10287
+ );
10288
+ }
10289
+ sections.push(
10290
+ "## 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 context gathering returns richer results \u2192 AI processes have more context \u2192 quality scores improve \u2192 next commit is smarter than the last."
10291
+ );
10292
+ sections.push(
10293
+ "## 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)."
10294
+ );
10295
+ if (collections) {
10296
+ const collList = collections.map((c) => {
10297
+ const prefix = c.icon ? `${c.icon} ` : "";
10298
+ return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
10299
+ }).join("\n");
10300
+ sections.push(
10301
+ `## Data Model (${collections.length} collections)
10302
+ Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
10303
+ The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
10304
+ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
10305
+
10306
+ ` + collList + "\n\nUse `collections action=list` for field schemas, `entries action=get` for full records."
10307
+ );
10308
+ } else {
10309
+ sections.push(
10310
+ "## Data Model\nCould not load collections \u2014 use `collections action=list` to browse manually."
10311
+ );
10312
+ }
10313
+ const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
10314
+ sections.push(
10315
+ `## Business Rules
10316
+ Collection: \`business-rules\` (${rulesCount}).
10317
+ Find rules: \`entries action=search\` for text search, \`entries action=list collection=business-rules\` to browse.
10318
+ Check compliance: use the \`review-against-rules\` prompt (pass a domain).
10319
+ Draft a new rule: use the \`draft-rule-from-context\` prompt.`
10320
+ );
10321
+ const eventsCount = trackingEvents ? `${trackingEvents.length} events` : "not loaded \u2014 collection may not exist yet";
10322
+ const conventionNote = standards ? "Naming convention: `object_action` in snake_case with past-tense verbs (from standards collection)." : "Naming convention: `object_action` in snake_case with past-tense verbs.";
10323
+ sections.push(
10324
+ `## Analytics & Tracking
10325
+ Event catalog: \`tracking-events\` collection (${eventsCount}).
10326
+ ${conventionNote}
10327
+ Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
10328
+ Browse: \`entries action=list collection=tracking-events\`.`
10329
+ );
10330
+ sections.push(
10331
+ "## 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- `context action=gather` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `graph action=suggest` \u2014 discover potential connections for an entry\n- `relations action=create` \u2014 create a typed link between two entries\n- `graph action=find` \u2014 list direct relations for an entry\n\n**Convention:** When creating or updating entries in governed collections, always use `graph action=suggest` to discover and create relevant relations."
10332
+ );
10333
+ sections.push(
10334
+ "## 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 action=check` to score existing entries retroactively.\nUse `update-entry` for post-creation adjustments (status changes, field updates, deprecation)."
10335
+ );
10336
+ sections.push(
10337
+ "## Where to Go Next\n- **Create entry** \u2192 `capture` tool (auto-links + quality score in one call)\n- **Full context** \u2192 `context action=gather` (by entry ID or task description)\n- **Discover links** \u2192 `graph action=suggest`\n- **Quality audit** \u2192 `quality action=check`\n- **Terminology** \u2192 `name-check` prompt or `productbrain://terminology` resource\n- **Schema details** \u2192 `productbrain://collections` resource or `collections action=list`\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 `entries action=list collection=strategy status=draft`\n- **Audiences** \u2192 `productbrain://audiences/entries` resource or `entries action=list collection=audiences`\n- **Session identity** \u2192 `health action=whoami`\n- **Health check** \u2192 `health action=check`\n- **Debug MCP calls** \u2192 `health action=audit`"
10338
+ );
10339
+ return sections.join("\n\n---\n\n");
10340
+ }
10341
+ var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
10342
+
10343
+ ## Core Tools
10344
+ | Tool | Purpose | Key params |
10345
+ |---|---|---|
10346
+ | \`orient\` | Workspace context, governance, active bets | \u2014 (call at session start) |
10347
+ | \`capture\` | Create entry (draft) | \`collection\`, \`name\`, \`description\`, optional \`data\` |
10348
+ | \`entries\` | List / get / batch / search entries | \`action\` + \`entryId\` / \`query\` / \`collection\` |
10349
+ | \`update-entry\` | Update fields on an entry | \`entryId\`, optional \`name\`, \`status\`, \`workflowStatus\`, \`data\`, \`changeNote\` |
10350
+ | \`commit-entry\` | Promote draft \u2192 SSOT | \`entryId\` |
10351
+ | \`graph\` | Suggest / find relations | \`action\` + \`entryId\` |
10352
+ | \`relations\` | Create / batch-create / delete links | \`from\`, \`to\`, \`type\` |
10353
+ | \`context\` | Gather related knowledge | \`entryId\` or \`task\` |
10354
+ | \`collections\` | List / create / update collections | \`action\` |
10355
+ | \`labels\` | List / create / apply / remove labels | \`action\` |
10356
+ | \`quality\` | Score an entry | \`entryId\` |
10357
+ | \`session\` | Start / close agent session | \`action\` |
10358
+ | \`health\` | Check / audit / whoami | \`action\` |
10359
+ | \`facilitate\` | Shaping workflows | \`action\`: start, respond, status |
10360
+
10361
+ ## Collection Prefixes
10362
+ GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
10363
+ DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
10364
+ BET (bets), INS (insights), ARCH (architecture), CIR (circles),
10365
+ ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
10366
+
10367
+ ## Valid Relation Types (21)
10368
+ informs, governs, surfaces_tension_in, defines_term_for, belongs_to,
10369
+ references, related_to, fills_slot, commits_to, informed_by, depends_on,
10370
+ conflicts_with, confused_with, replaces, part_of, constrains,
10371
+ governed_by, alternative_to, has_proposal, requests_promotion_of, resolves
10372
+
10373
+ ## Lifecycle Status
10374
+ All entries: \`draft\` | \`active\` | \`deprecated\` | \`archived\`
10375
+
10376
+ ## Workflow Status (per collection)
10377
+ - **tensions:** open \u2192 processing \u2192 decided \u2192 closed
10378
+ - **decisions:** pending \u2192 decided
10379
+ - **bets:** shaped \u2192 bet \u2192 building \u2192 shipped
10380
+ - **business-rules:** active | conflict | review
10381
+
10382
+ ## Key Patterns
10383
+ - **Capture flow:** \`capture\` \u2192 \`graph action=suggest\` \u2192 \`relations action=batch-create\` \u2192 \`commit-entry\`
10384
+ - Use \`workflowStatus\` (not \`status\`) for domain workflow state
10385
+ - \`data\` param is merged (not replaced) \u2014 safe for partial updates
10386
+ `;
10387
+ function registerResources(server) {
10388
+ server.resource(
10389
+ "agent-cheatsheet",
10390
+ "productbrain://agent-cheatsheet",
10391
+ async (uri) => ({
10392
+ contents: [{
10393
+ uri: uri.href,
10394
+ text: AGENT_CHEATSHEET,
10395
+ mimeType: "text/markdown"
10396
+ }]
10397
+ })
10398
+ );
10399
+ server.resource(
10400
+ "chain-orientation",
10401
+ "productbrain://orientation",
10402
+ async (uri) => {
10403
+ const [collectionsResult, eventsResult, standardsResult, rulesResult, principlesResult] = await Promise.allSettled([
10404
+ mcpQuery("chain.listCollections"),
10405
+ mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
10406
+ mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
10407
+ mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
10408
+ mcpQuery("chain.listEntries", { collectionSlug: "principles" })
10409
+ ]);
10410
+ const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
10411
+ const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
10412
+ const standards = standardsResult.status === "fulfilled" ? standardsResult.value : null;
10413
+ const businessRules = rulesResult.status === "fulfilled" ? rulesResult.value : null;
10414
+ const principles = principlesResult.status === "fulfilled" ? principlesResult.value : null;
10415
+ return {
10416
+ contents: [{
10417
+ uri: uri.href,
10418
+ text: buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles),
10419
+ mimeType: "text/markdown"
10420
+ }]
10421
+ };
10422
+ }
10423
+ );
10424
+ server.resource(
10425
+ "chain-terminology",
10426
+ "productbrain://terminology",
10427
+ async (uri) => {
10428
+ const [glossaryResult, standardsResult] = await Promise.allSettled([
10429
+ mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
10430
+ mcpQuery("chain.listEntries", { collectionSlug: "standards" })
10431
+ ]);
10432
+ const lines = ["# Product Brain \u2014 Terminology"];
10433
+ if (glossaryResult.status === "fulfilled") {
10434
+ const glossary = glossaryResult.value ?? [];
10435
+ if (glossary.length > 0) {
10436
+ const terms = glossary.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
10437
+ lines.push(`## Glossary (${glossary.length} terms)
10438
+
10439
+ ${terms}`);
10423
10440
  } else {
10424
- lines.push("---");
10425
- lines.push("_No active agent session. Call `session action=start` to begin._");
10426
- }
10427
- if (errors.length > 0) {
10428
- lines.push("");
10429
- for (const err of errors) lines.push(`- ${err}`);
10441
+ lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
10430
10442
  }
10431
- return {
10432
- content: [{ type: "text", text: lines.join("\n") }],
10433
- structuredContent: success(
10434
- `Oriented (brief). Stage: ${readiness?.stage ?? "unknown"}.`,
10435
- { mode: "brief", stage: readiness?.stage ?? "unknown", oriented: isSessionOriented(), sessionId: agentSessionId }
10436
- )
10437
- };
10443
+ } else {
10444
+ lines.push("## Glossary\n\nCould not load glossary \u2014 use `entries action=list collection=glossary` to browse manually.");
10438
10445
  }
10439
- const orientStage = readiness?.stage ?? "seeded";
10440
- if (isLowReadiness && wsCtx?.createdAt) {
10441
- const ageDays = Math.floor((Date.now() - wsCtx.createdAt) / (1e3 * 60 * 60 * 24));
10442
- if (ageDays >= 30) {
10443
- lines.push(`Your workspace has been around for ${ageDays} days and is still at the **${orientStage}** stage.`);
10444
- lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
10445
- lines.push("");
10446
+ if (standardsResult.status === "fulfilled") {
10447
+ const standards = standardsResult.value ?? [];
10448
+ if (standards.length > 0) {
10449
+ const stds = standards.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${s.data?.description ?? ""}`).join("\n");
10450
+ lines.push(`## Standards (${standards.length} entries)
10451
+
10452
+ ${stds}`);
10453
+ } else {
10454
+ lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
10446
10455
  }
10456
+ } else {
10457
+ lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
10447
10458
  }
10448
- if (isLowReadiness) {
10449
- lines.push(`**Brain stage: ${orientStage}.**`);
10450
- lines.push("");
10451
- const gaps = readiness.gaps ?? [];
10452
- if (gaps.length > 0) {
10453
- const gap = gaps[0];
10454
- const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
10455
- lines.push("## Recommended next step");
10456
- lines.push(`**${gap.label}**`);
10457
- lines.push("");
10458
- lines.push(cta);
10459
- lines.push("");
10460
- lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
10461
- lines.push("");
10462
- const remainingGaps = gaps.length - 1;
10463
- if (remainingGaps > 0 || openTensions.length > 0) {
10464
- lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
10465
- lines.push("");
10466
- }
10467
- }
10468
- lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)?_");
10469
- lines.push("_Use `collections action=create` to add it, or ask me to propose collections for your domain._");
10470
- lines.push("");
10471
- } else if (readiness) {
10472
- lines.push(`**Brain stage: ${orientStage}.**`);
10473
- lines.push("");
10474
- if (orientEntries?.strategicContext) {
10475
- const sc = orientEntries.strategicContext;
10476
- lines.push("## Strategic Context");
10477
- if (sc.vision) lines.push(`**Vision:** ${sc.vision}`);
10478
- if (sc.purpose) lines.push(`**Purpose:** ${sc.purpose}`);
10479
- if (sc.productAreaCount != null && sc.productAreaCount > 0) {
10480
- lines.push(`**Product areas (${sc.productAreaCount}):** ${(sc.productAreas ?? []).join(", ")}`);
10481
- }
10482
- const betLine = sc.currentBet ? `**Current bet:** ${sc.currentBet}. ${sc.activeBetCount} active bet(s).` : "No active bets.";
10483
- lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
10484
- lines.push("");
10485
- }
10486
- if (orientEntries?.continuingFrom && orientEntries.continuingFrom.length > 0) {
10487
- lines.push("## Continuing from");
10488
- lines.push("_Prior-session entries most relevant to your task._");
10489
- lines.push("");
10490
- for (const e of orientEntries.continuingFrom) {
10491
- const id = e.entryId ?? e.name;
10492
- const type = e.canonicalKey ?? "generic";
10493
- const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10494
- lines.push(`- \`${id}\` (score ${e.score}) [${type}]${coll} \u2014 ${e.name}`);
10495
- if (e.reasoning) lines.push(` _${e.reasoning}_`);
10496
- }
10497
- lines.push("");
10498
- }
10499
- if (orientEntries?.lastSessionTouched && orientEntries.lastSessionTouched.length > 0) {
10500
- lines.push("## Last session touched");
10501
- lines.push("_Entries created or modified in your most recent session._");
10502
- lines.push("");
10503
- for (const e of orientEntries.lastSessionTouched) {
10504
- const id = e.entryId ?? e.name;
10505
- const type = e.canonicalKey ?? "generic";
10506
- const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10507
- lines.push(`- \`${id}\` [${type}]${coll} \u2014 ${e.name}`);
10508
- }
10509
- lines.push("");
10510
- }
10511
- if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
10512
- const tc = orientEntries.taskContext;
10513
- lines.push("## Task Context");
10514
- lines.push(`_Task-scoped entries (${tc.confidence} confidence, ${tc.totalFound} relevant)`);
10515
- lines.push("");
10516
- for (const e of tc.context) {
10517
- const id = e.entryId ?? e.name;
10518
- const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
10519
- lines.push(`- \`${id}\` (score ${e.score})${coll}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
10520
- }
10521
- lines.push("");
10522
- }
10523
- if (task && orientEntries) {
10524
- const result = runAlignmentCheck(
10525
- task,
10526
- orientEntries.activeBets ?? [],
10527
- orientEntries.taskContext?.context
10528
- );
10529
- lines.push(...buildAlignmentCheckLines(result));
10530
- }
10531
- if (orientEntries) {
10532
- const fmt = (e) => {
10533
- const type = e.canonicalKey ?? "generic";
10534
- const stratum = e.stratum ?? "?";
10535
- return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
10536
- };
10537
- if (orientEntries.activeBets?.length > 0) {
10538
- lines.push("## Active bets \u2014 current scope");
10539
- lines.push("_These define what you're building now. Work outside these bets requires explicit user confirmation before designing._");
10540
- lines.push("");
10541
- for (const e of orientEntries.activeBets) {
10542
- lines.push(fmt(e));
10543
- const tensions = e.linkedTensions;
10544
- if (tensions?.length) {
10545
- const tensionLines = tensions.map((t) => {
10546
- const meta = [t.severity, t.priority].filter(Boolean).join(", ");
10547
- return `\`${t.entryId ?? t.name}\` (${t.name}${meta ? `, ${meta}` : ""})`;
10548
- });
10549
- lines.push(` Tensions: ${tensionLines.join("; ")}`);
10550
- } else {
10551
- lines.push(` Tensions: No linked tensions`);
10552
- }
10553
- }
10554
- lines.push("");
10555
- }
10556
- if (orientEntries.activeGoals.length > 0) {
10557
- lines.push("## Active goals");
10558
- orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
10559
- lines.push("");
10560
- }
10561
- if (orientEntries.recentDecisions.length > 0) {
10562
- lines.push("## Recent decisions");
10563
- orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
10564
- lines.push("");
10565
- }
10566
- if (orientEntries.recentlySuperseded.length > 0) {
10567
- lines.push("## Recently superseded");
10568
- orientEntries.recentlySuperseded.forEach((e) => lines.push(fmt(e)));
10569
- lines.push("");
10570
- }
10571
- if (orientEntries.staleEntries.length > 0) {
10572
- lines.push("## Needs confirmation");
10573
- lines.push(`_Domain stratum entries not confirmed in ${orientEntries.stalenessThresholdDays} days._`);
10574
- orientEntries.staleEntries.forEach((e) => lines.push(fmt(e)));
10575
- lines.push("");
10576
- }
10577
- const hasPrinciples = orientEntries.principles?.length > 0;
10578
- const hasStandards = orientEntries.standards?.length > 0;
10579
- const hasBusinessRules = orientEntries.businessRules.length > 0;
10580
- if (hasPrinciples || hasStandards || hasBusinessRules) {
10581
- lines.push("## Workspace Governance \u2014 constraints");
10582
- lines.push("_These constrain what you build. If your proposal conflicts with any of these, stop and flag it. Do not design around them without explicit user confirmation._");
10583
- lines.push("");
10584
- if (hasPrinciples) {
10585
- lines.push("**Principles** \u2014 beliefs that guide decisions:");
10586
- orientEntries.principles.forEach((e) => lines.push(fmt(e)));
10587
- lines.push("");
10588
- }
10589
- if (hasStandards) {
10590
- lines.push("**Standards** \u2014 conventions for how work is done:");
10591
- orientEntries.standards.forEach((e) => lines.push(fmt(e)));
10592
- lines.push("");
10593
- }
10594
- if (hasBusinessRules) {
10595
- lines.push("**Business Rules** \u2014 system constraints:");
10596
- orientEntries.businessRules.forEach((e) => lines.push(fmt(e)));
10597
- lines.push("");
10598
- }
10599
- }
10600
- if (orientEntries.architectureNotes.length > 0) {
10601
- lines.push("## Architecture notes");
10602
- orientEntries.architectureNotes.forEach((e) => lines.push(fmt(e)));
10603
- lines.push("");
10604
- }
10605
- const mapGovernanceEntry = (e) => ({
10606
- entryId: e.entryId,
10607
- name: e.name,
10608
- description: typeof e.preview === "string" ? e.preview : void 0
10609
- });
10610
- lines.push(...buildOperatingProtocol({
10611
- principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
10612
- standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
10613
- businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
10614
- }, task));
10615
- }
10616
- let allEntries = [];
10617
- try {
10618
- allEntries = await mcpQuery("chain.listEntries", {}) ?? [];
10619
- } catch {
10459
+ return {
10460
+ contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
10461
+ };
10462
+ }
10463
+ );
10464
+ server.resource(
10465
+ "chain-collections",
10466
+ "productbrain://collections",
10467
+ async (uri) => {
10468
+ const collections = await mcpQuery("chain.listCollections") ?? [];
10469
+ if (collections.length === 0) {
10470
+ return { contents: [{ uri: uri.href, text: "No collections in this workspace. Use `collections action=create` or `start` with a preset to get started.", mimeType: "text/markdown" }] };
10471
+ }
10472
+ const formatted = collections.map((c) => {
10473
+ const fieldList = (c.fields ?? []).map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
10474
+ return `## ${c.icon ?? ""} ${c.name} (\`${c.slug}\`)
10475
+ ${c.description || ""}
10476
+
10477
+ **Fields:**
10478
+ ${fieldList}`;
10479
+ }).join("\n\n---\n\n");
10480
+ return {
10481
+ contents: [{ uri: uri.href, text: `# Knowledge Collections (${collections.length})
10482
+
10483
+ ${formatted}`, mimeType: "text/markdown" }]
10484
+ };
10485
+ }
10486
+ );
10487
+ server.resource(
10488
+ "chain-collection-entries",
10489
+ new ResourceTemplate("productbrain://{slug}/entries", {
10490
+ list: async () => {
10491
+ const collections = await mcpQuery("chain.listCollections") ?? [];
10492
+ return {
10493
+ resources: collections.map((c) => ({
10494
+ uri: `productbrain://${c.slug}/entries`,
10495
+ name: `${c.icon ?? ""} ${c.name}`.trim()
10496
+ }))
10497
+ };
10498
+ }
10499
+ }),
10500
+ async (uri, { slug }) => {
10501
+ const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug }) ?? [];
10502
+ const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
10503
+ return {
10504
+ contents: [{
10505
+ uri: uri.href,
10506
+ text: formatted || "No entries in this collection.",
10507
+ mimeType: "text/markdown"
10508
+ }]
10509
+ };
10510
+ }
10511
+ );
10512
+ server.resource(
10513
+ "chain-labels",
10514
+ "productbrain://labels",
10515
+ async (uri) => {
10516
+ const labels = await mcpQuery("chain.listLabels") ?? [];
10517
+ if (labels.length === 0) {
10518
+ return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
10519
+ }
10520
+ const groups = labels.filter((l) => l.isGroup);
10521
+ const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
10522
+ const children = (parentId) => labels.filter((l) => l.parentId === parentId);
10523
+ const lines = [];
10524
+ for (const group of groups) {
10525
+ lines.push(`## ${group.name}`);
10526
+ for (const child of children(group._id)) {
10527
+ lines.push(`- \`${child.slug}\` ${child.name}${child.color ? ` (${child.color})` : ""}`);
10620
10528
  }
10621
- const plannedWork = buildPlannedWork(allEntries);
10622
- if (hasPlannedWork(plannedWork)) {
10623
- lines.push(...buildPlannedWorkSection(plannedWork, priorSessions, recoveryBlock));
10624
- } else {
10625
- const briefingItems = [];
10626
- if (priorSessions.length > 0 && !recoveryBlock) {
10627
- const last = priorSessions[0];
10628
- const date = new Date(last.startedAt).toISOString().split("T")[0];
10629
- const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
10630
- const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
10631
- briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
10632
- }
10633
- if (readiness.gaps?.length > 0) {
10634
- briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
10635
- }
10636
- if (briefingItems.length > 0) {
10637
- lines.push("## Briefing");
10638
- for (const item of briefingItems) {
10639
- lines.push(`- ${item}`);
10640
- }
10641
- lines.push("");
10642
- }
10643
- if (recoveryBlock) {
10644
- lines.push("");
10645
- lines.push(...formatRecoveryBlock(recoveryBlock));
10646
- }
10529
+ }
10530
+ if (ungrouped.length > 0) {
10531
+ lines.push("## Ungrouped");
10532
+ for (const l of ungrouped) {
10533
+ lines.push(`- \`${l.slug}\` ${l.name}${l.color ? ` (${l.color})` : ""}`);
10647
10534
  }
10648
- const activeEntries = allEntries.filter((e) => e.status === "active");
10649
- if (activeEntries.length > 0) {
10650
- const orgHealth = computeOrganisationHealth(activeEntries);
10651
- if (orgHealth.disagreements > 0) {
10652
- lines.push("## Organisation Health");
10653
- lines.push(...formatOrgHealthLines(orgHealth, 3));
10654
- lines.push("");
10655
- }
10535
+ }
10536
+ return {
10537
+ contents: [{ uri: uri.href, text: `# Workspace Labels (${labels.length})
10538
+
10539
+ ${lines.join("\n")}`, mimeType: "text/markdown" }]
10540
+ };
10541
+ }
10542
+ );
10543
+ server.resource(
10544
+ "chain-entry",
10545
+ new ResourceTemplate("productbrain://entries/{entryId}", {
10546
+ complete: {
10547
+ entryId: async (value) => {
10548
+ if (!value || value.length < 1) return [];
10549
+ const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
10550
+ return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
10656
10551
  }
10657
- const epistemicEntries = activeEntries.filter(
10658
- (e) => e.collectionSlug === "insights" || e.collectionSlug === "assumptions"
10659
- );
10660
- if (epistemicEntries.length > 0) {
10661
- let validated = 0;
10662
- let evidenced = 0;
10663
- let hypotheses = 0;
10664
- let untested = 0;
10665
- for (const e of epistemicEntries) {
10666
- const ws = e.workflowStatus;
10667
- if (ws === "validated") validated++;
10668
- else if (ws === "evidenced") evidenced++;
10669
- else if (ws === "testing") evidenced++;
10670
- else if (ws === "invalidated") validated++;
10671
- else if (e.collectionSlug === "assumptions") untested++;
10672
- else hypotheses++;
10552
+ }
10553
+ }),
10554
+ async (uri, { entryId }) => {
10555
+ const [entry, collections] = await Promise.all([
10556
+ mcpQuery("chain.getEntry", { entryId }),
10557
+ mcpQuery("chain.listCollections")
10558
+ ]);
10559
+ if (!entry) {
10560
+ return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
10561
+ }
10562
+ const collectionMap = new Map((collections ?? []).map((c) => [c._id, c]));
10563
+ const col = collectionMap.get(entry.collectionId);
10564
+ const collLabel = col?.name ?? entry.collectionName ?? entry.collectionSlug ?? "unknown";
10565
+ const lines = [
10566
+ `# ${entry.entryId}: ${entry.name}`,
10567
+ `**Collection:** ${collLabel}`,
10568
+ `**Status:** ${entry.status}`,
10569
+ ""
10570
+ ];
10571
+ if (entry.data && typeof entry.data === "object") {
10572
+ lines.push("## Data");
10573
+ for (const [key, val] of Object.entries(entry.data)) {
10574
+ if (val && key !== "rawData") {
10575
+ const str = typeof val === "string" ? val : JSON.stringify(val, null, 2);
10576
+ lines.push(`**${key}:** ${str}`);
10673
10577
  }
10674
- const parts = [];
10675
- if (validated > 0) parts.push(`${validated} validated`);
10676
- if (evidenced > 0) parts.push(`${evidenced} evidenced`);
10677
- if (hypotheses > 0) parts.push(`${hypotheses} hypotheses`);
10678
- if (untested > 0) parts.push(`${untested} untested`);
10679
- lines.push(`**Epistemic health:** ${parts.join(" \xB7 ")} _(${epistemicEntries.length} claim-carrying entries)_`);
10680
- lines.push("");
10681
10578
  }
10682
- lines.push("What would you like to work on?");
10683
10579
  lines.push("");
10684
10580
  }
10685
- if (errors.length > 0) {
10686
- lines.push("## Errors");
10687
- for (const err of errors) lines.push(`- ${err}`);
10581
+ if (entry.relations && entry.relations.length > 0) {
10582
+ lines.push("## Relations");
10583
+ for (const rel of entry.relations) {
10584
+ const arrow = rel.direction === "outgoing" ? "\u2192" : "\u2190";
10585
+ const id = rel.otherEntryId ?? "";
10586
+ const name = rel.otherName ?? "(unknown)";
10587
+ lines.push(`- ${arrow} **${rel.type}** ${id}: ${name}`);
10588
+ }
10688
10589
  lines.push("");
10689
10590
  }
10690
- if (agentSessionId) {
10691
- try {
10692
- await mcpCall("agent.markOriented", { sessionId: agentSessionId });
10693
- setSessionOriented(true);
10694
- lines.push("---");
10695
- lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
10696
- } catch {
10697
- lines.push("---");
10698
- lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
10591
+ if (entry.labels && entry.labels.length > 0) {
10592
+ lines.push(`## Labels
10593
+ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
10594
+ }
10595
+ return {
10596
+ contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
10597
+ };
10598
+ }
10599
+ );
10600
+ server.resource(
10601
+ "chain-context",
10602
+ new ResourceTemplate("productbrain://context/{entryId}", {
10603
+ complete: {
10604
+ entryId: async (value) => {
10605
+ if (!value || value.length < 1) return [];
10606
+ const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
10607
+ return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
10699
10608
  }
10700
- try {
10701
- await mcpMutation("chain.recordSessionSignal", {
10702
- sessionId: agentSessionId,
10703
- signalType: "immediate_context_load",
10704
- metadata: { source: "orient" }
10705
- });
10706
- } catch (err) {
10707
- process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
10708
- `);
10609
+ }
10610
+ }),
10611
+ async (uri, { entryId }) => {
10612
+ const result = await mcpQuery("chain.gatherContext", {
10613
+ entryId,
10614
+ maxHops: 2
10615
+ });
10616
+ if (!result?.root) {
10617
+ return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
10618
+ }
10619
+ const lines = [
10620
+ `# Context: ${result.root.entryId}: ${result.root.name}`,
10621
+ `_${result.totalRelations} related entries (${result.hopsTraversed} hops)_`,
10622
+ ""
10623
+ ];
10624
+ const byCollection = /* @__PURE__ */ new Map();
10625
+ for (const entry of result.related ?? []) {
10626
+ const key = entry.collectionName;
10627
+ if (!byCollection.has(key)) byCollection.set(key, []);
10628
+ byCollection.get(key).push(entry);
10629
+ }
10630
+ for (const [collName, entries] of byCollection) {
10631
+ lines.push(`## ${collName} (${entries.length})`);
10632
+ for (const e of entries) {
10633
+ const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
10634
+ const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
10635
+ const id = e.entryId ? `${e.entryId}: ` : "";
10636
+ lines.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}`);
10709
10637
  }
10710
- } else {
10711
- lines.push("---");
10712
- lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
10638
+ lines.push("");
10713
10639
  }
10714
10640
  return {
10715
- content: [{ type: "text", text: lines.join("\n") }],
10716
- structuredContent: success(
10717
- `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
10718
- { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
10719
- )
10641
+ contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
10720
10642
  };
10721
- })
10643
+ }
10644
+ );
10645
+ server.resource(
10646
+ "chain-search",
10647
+ new ResourceTemplate("productbrain://search/{query}", {
10648
+ complete: {
10649
+ query: async (value) => {
10650
+ if (!value) return ["glossary:", "business-rules:", "decisions:", "tensions:"];
10651
+ return [];
10652
+ }
10653
+ }
10654
+ }),
10655
+ async (uri, { query }) => {
10656
+ const results = await mcpQuery("chain.searchEntries", { query });
10657
+ if (!results || results.length === 0) {
10658
+ return { contents: [{ uri: uri.href, text: `No results for "${query}".`, mimeType: "text/markdown" }] };
10659
+ }
10660
+ const lines = [
10661
+ `# Search: "${query}"`,
10662
+ `_${results.length} results_`,
10663
+ ""
10664
+ ];
10665
+ for (const entry of results) {
10666
+ const id = entry.entryId ? `**${entry.entryId}:** ` : "";
10667
+ const coll = entry.collectionName ?? entry.collectionSlug ?? "";
10668
+ lines.push(`- ${id}${entry.name} [${coll}] (${entry.status})`);
10669
+ }
10670
+ return {
10671
+ contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
10672
+ };
10673
+ }
10722
10674
  );
10723
10675
  }
10724
10676
 
@@ -10943,8 +10895,11 @@ Use one of these IDs to run a workflow.`
10943
10895
  } catch {
10944
10896
  kbContext = "\n_Could not load chain context \u2014 proceed without it._";
10945
10897
  }
10946
- const roundsPlan = wf.rounds.map(
10947
- (r) => `### Round ${r.num}: ${r.label}
10898
+ const isFacilitated = descriptor?.runtime.kind === "facilitated";
10899
+ const roundsPlan = wf.rounds.map((r, index) => {
10900
+ const isLastRound = index === wf.rounds.length - 1;
10901
+ const checkpointLine = isFacilitated ? isLastRound ? `**Checkpoint**: After completing this round, persist with \`workflows action="checkpoint" workflowId="${wf.id}" roundId="${r.id}" summaryEntryId=<betEntryId>\` to finalize the durable run and link the bet.` : `**Checkpoint**: After completing this round via \`facilitate\`, also persist with \`workflows action="checkpoint" workflowId="${wf.id}" roundId="${r.id}"\`` : `**Checkpoint**: Immediately persist this round with \`workflows action="checkpoint" workflowId="${wf.id}" roundId="${r.id}"\``;
10902
+ return `### Round ${r.num}: ${r.label}
10948
10903
  **Type**: ${r.type} | **Duration**: ~${r.maxDurationHint ?? "5 min"}
10949
10904
  **Instruction**: ${r.instruction}
10950
10905
  **Facilitator guidance**: ${r.facilitatorGuidance}
@@ -10953,8 +10908,8 @@ Use one of these IDs to run a workflow.`
10953
10908
  ` + (q.options ? q.options.map((o) => ` - ${o.id}: ${o.label}`).join("\n") : " _(open response)_")
10954
10909
  ).join("\n") : "") + `
10955
10910
  **Output**: Capture to \`${r.outputSchema.field}\` (${r.outputSchema.format})
10956
- **Checkpoint**: Immediately persist this round with \`workflows action="checkpoint" workflowId="${wf.id}" roundId="${r.id}"\``
10957
- ).join("\n\n---\n\n");
10911
+ ` + checkpointLine;
10912
+ }).join("\n\n---\n\n");
10958
10913
  const contextLine = context ? `
10959
10914
  The participant provided this context: "${context}"
10960
10915
  Use it \u2014 don't make them repeat themselves.
@@ -10975,7 +10930,15 @@ Description field: ${summaryCapture.descriptionField ?? "collection default"}
10975
10930
  ` : `Primary output: ${outputDescription}
10976
10931
  Use the workflow guidance to capture the primary record directly rather than relying on final summary auto-capture.
10977
10932
  `;
10978
- const checkpointingBlock = `## Checkpointing & Resume
10933
+ const checkpointingBlock = isFacilitated ? `## Checkpointing & Resume (Facilitated Workflow)
10934
+
10935
+ This workflow uses \`${descriptor?.runtime.primaryTool ?? "facilitate"}\` as its primary tool, with durable workflow runs for tracking and observability.
10936
+ 1. Before starting, check for an existing run with \`workflows action="get-run" workflowId="${wf.id}"\`.
10937
+ 2. The \`${descriptor?.runtime.primaryTool ?? "facilitate"}\` tool drives the session. Use it for scoring, coaching, capture, and constellation management.
10938
+ 3. After completing EACH round via \`${descriptor?.runtime.primaryTool ?? "facilitate"}\`, also call \`workflows action="checkpoint" workflowId="${wf.id}" roundId="<round-id>" output=<round-output>\` to mirror progress to the durable run.
10939
+ 4. On the final round, pass \`summaryEntryId=<betEntryId>\` to link the bet as the run's summary. Do NOT use \`isFinal: true\` \u2014 the run auto-completes when all rounds are checkpointed.
10940
+ 5. If a checkpoint fails, continue the session \u2014 \`${descriptor?.runtime.primaryTool ?? "facilitate"}\` state is preserved independently.
10941
+ ` : `## Checkpointing & Resume
10979
10942
 
10980
10943
  This workflow now supports durable workflow runs.
10981
10944
  1. Before starting, check for an existing run in the current session with \`workflows action="get-run" workflowId="${wf.id}"\`.
@@ -11618,4 +11581,4 @@ export {
11618
11581
  SERVER_VERSION,
11619
11582
  createProductBrainServer
11620
11583
  };
11621
- //# sourceMappingURL=chunk-HQLEMADT.js.map
11584
+ //# sourceMappingURL=chunk-VKO3MWSQ.js.map