@productbrain/mcp 0.0.1-beta.47 → 0.0.1-beta.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-FYFF4QKF.js → chunk-ED3KCWZE.js} +455 -72
- package/dist/chunk-ED3KCWZE.js.map +1 -0
- package/dist/{chunk-FMEZXUP5.js → chunk-IGQLZI32.js} +2232 -1941
- package/dist/chunk-IGQLZI32.js.map +1 -0
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{smart-capture-XLBFE252.js → smart-capture-2IM2565I.js} +8 -2
- package/dist/views/src/entry-cards/index.html +227 -0
- package/dist/views/src/graph-constellation/index.html +254 -0
- package/package.json +3 -2
- package/dist/chunk-FMEZXUP5.js.map +0 -1
- package/dist/chunk-FYFF4QKF.js.map +0 -1
- /package/dist/{smart-capture-XLBFE252.js.map → smart-capture-2IM2565I.js.map} +0 -0
|
@@ -2,10 +2,13 @@ import {
|
|
|
2
2
|
batchCaptureSchema,
|
|
3
3
|
captureSchema,
|
|
4
4
|
checkEntryQuality,
|
|
5
|
+
classifyCollection,
|
|
5
6
|
closeAgentSession,
|
|
7
|
+
deriveEpistemicStatus,
|
|
6
8
|
extractPreview,
|
|
7
9
|
failure,
|
|
8
10
|
failureResult,
|
|
11
|
+
formatEpistemicLine,
|
|
9
12
|
formatRubricCoaching,
|
|
10
13
|
formatRubricVerdictSection,
|
|
11
14
|
getAgentSessionId,
|
|
@@ -29,12 +32,13 @@ import {
|
|
|
29
32
|
startAgentSession,
|
|
30
33
|
success,
|
|
31
34
|
successResult,
|
|
35
|
+
toEpistemicInput,
|
|
32
36
|
trackWriteTool,
|
|
33
37
|
translateStaleToolNames,
|
|
34
38
|
unknownAction,
|
|
35
39
|
validationResult,
|
|
36
40
|
withEnvelope
|
|
37
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-ED3KCWZE.js";
|
|
38
42
|
import {
|
|
39
43
|
trackQualityCheck,
|
|
40
44
|
trackQualityVerdict
|
|
@@ -228,7 +232,7 @@ ${formatted}` }],
|
|
|
228
232
|
},
|
|
229
233
|
withEnvelope(async ({ entryId }) => {
|
|
230
234
|
requireWriteAccess();
|
|
231
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
235
|
+
const { runContradictionCheck } = await import("./smart-capture-2IM2565I.js");
|
|
232
236
|
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
233
237
|
if (!entry) {
|
|
234
238
|
return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
|
|
@@ -252,9 +256,11 @@ ${formatted}` }],
|
|
|
252
256
|
sessionId: getAgentSessionId() ?? void 0
|
|
253
257
|
});
|
|
254
258
|
const docId = result?._id ?? entry._id;
|
|
255
|
-
await recordSessionActivity({ entryModified: docId });
|
|
256
259
|
const wsCtx = await getWorkspaceContext();
|
|
257
260
|
const isProposal = result?.status === "proposal_created";
|
|
261
|
+
if (!isProposal) {
|
|
262
|
+
await recordSessionActivity({ entryModified: docId });
|
|
263
|
+
}
|
|
258
264
|
let lines;
|
|
259
265
|
if (isProposal) {
|
|
260
266
|
lines = [
|
|
@@ -308,6 +314,14 @@ ${formatted}` }],
|
|
|
308
314
|
}
|
|
309
315
|
}
|
|
310
316
|
}
|
|
317
|
+
const epistemic = deriveEpistemicStatus(toEpistemicInput(entry));
|
|
318
|
+
if (epistemic && (epistemic.level === "hypothesis" || epistemic.level === "untested")) {
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push(`\u26A0 ${formatEpistemicLine(epistemic)}`);
|
|
321
|
+
if (epistemic.action) {
|
|
322
|
+
lines.push(` \u2192 ${epistemic.action}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
311
325
|
const next = isProposal ? [
|
|
312
326
|
{ tool: "entries", description: "View entry", parameters: { action: "get", entryId } }
|
|
313
327
|
] : [
|
|
@@ -325,7 +339,8 @@ ${formatted}` }],
|
|
|
325
339
|
outcome: isProposal ? "proposal_created" : "committed",
|
|
326
340
|
qualityVerdict: coachingResult?.verdict ?? void 0,
|
|
327
341
|
source: coachingResult?.source ?? void 0,
|
|
328
|
-
contradictions: warnings.length
|
|
342
|
+
contradictions: warnings.length,
|
|
343
|
+
...epistemic ? { epistemicStatus: epistemic } : {}
|
|
329
344
|
},
|
|
330
345
|
next
|
|
331
346
|
)
|
|
@@ -337,6 +352,13 @@ ${formatted}` }],
|
|
|
337
352
|
|
|
338
353
|
// src/tools/entries.ts
|
|
339
354
|
import { z as z2 } from "zod";
|
|
355
|
+
function sanitizeEntryData(data) {
|
|
356
|
+
if (!data || typeof data !== "object") return void 0;
|
|
357
|
+
const filtered = Object.fromEntries(
|
|
358
|
+
Object.entries(data).filter(([key]) => !key.startsWith("_"))
|
|
359
|
+
);
|
|
360
|
+
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
361
|
+
}
|
|
340
362
|
var ENTRIES_ACTIONS = ["list", "get", "batch", "search"];
|
|
341
363
|
var entriesSchema = z2.object({
|
|
342
364
|
action: z2.enum(ENTRIES_ACTIONS).describe(
|
|
@@ -441,6 +463,7 @@ async function handleGet(entryId) {
|
|
|
441
463
|
return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
|
|
442
464
|
}
|
|
443
465
|
const e = entry;
|
|
466
|
+
const epistemic = deriveEpistemicStatus(toEpistemicInput(e));
|
|
444
467
|
const lines = [
|
|
445
468
|
`## Get Result`,
|
|
446
469
|
"",
|
|
@@ -449,9 +472,13 @@ async function handleGet(entryId) {
|
|
|
449
472
|
`**Status:** ${e.status}${e.workflowStatus ? ` / ${e.workflowStatus}` : ""}`,
|
|
450
473
|
`**Type:** ${e.canonicalKey ?? "untyped"}`
|
|
451
474
|
];
|
|
452
|
-
if (
|
|
475
|
+
if (epistemic) {
|
|
476
|
+
lines.push(formatEpistemicLine(epistemic));
|
|
477
|
+
}
|
|
478
|
+
const visibleData = sanitizeEntryData(e.data);
|
|
479
|
+
if (visibleData) {
|
|
453
480
|
lines.push("");
|
|
454
|
-
for (const [key, val] of Object.entries(
|
|
481
|
+
for (const [key, val] of Object.entries(visibleData)) {
|
|
455
482
|
const display = typeof val === "string" ? val : JSON.stringify(val);
|
|
456
483
|
lines.push(`**${key}:** ${display}`);
|
|
457
484
|
}
|
|
@@ -484,8 +511,9 @@ async function handleGet(entryId) {
|
|
|
484
511
|
collection: String(e.canonicalKey ?? ""),
|
|
485
512
|
status: String(e.status ?? ""),
|
|
486
513
|
...e.workflowStatus ? { workflowStatus: String(e.workflowStatus) } : {},
|
|
514
|
+
...epistemic ? { epistemicStatus: epistemic } : {},
|
|
487
515
|
entries: [{ entryId: e.entryId, name: e.name, status: e.status, ...e.workflowStatus ? { workflowStatus: e.workflowStatus } : {}, collectionName: String(e.collectionName ?? e.canonicalKey ?? "\u2014") }],
|
|
488
|
-
...
|
|
516
|
+
...visibleData ? { data: visibleData } : {},
|
|
489
517
|
...Array.isArray(e.relations) && e.relations.length > 0 ? {
|
|
490
518
|
relations: e.relations.map((r) => ({
|
|
491
519
|
...r.otherEntryId ? { entryId: r.otherEntryId } : {},
|
|
@@ -526,9 +554,10 @@ async function handleBatch(entryIds) {
|
|
|
526
554
|
lines.push("");
|
|
527
555
|
lines.push(`**Status:** ${entry.status}${entry.workflowStatus ? ` / ${entry.workflowStatus}` : ""}`);
|
|
528
556
|
lines.push(`**Type:** ${entry.canonicalKey ?? "untyped"}`);
|
|
529
|
-
|
|
557
|
+
const visibleData = sanitizeEntryData(entry.data);
|
|
558
|
+
if (visibleData) {
|
|
530
559
|
lines.push("");
|
|
531
|
-
for (const [key, val] of Object.entries(
|
|
560
|
+
for (const [key, val] of Object.entries(visibleData)) {
|
|
532
561
|
const display = typeof val === "string" ? val : JSON.stringify(val);
|
|
533
562
|
lines.push(`**${key}:** ${display}`);
|
|
534
563
|
}
|
|
@@ -565,7 +594,7 @@ async function handleBatch(entryIds) {
|
|
|
565
594
|
collection: String(entry.canonicalKey ?? ""),
|
|
566
595
|
status: String(entry.status ?? ""),
|
|
567
596
|
...entry.workflowStatus ? { workflowStatus: String(entry.workflowStatus) } : {},
|
|
568
|
-
...entry.data
|
|
597
|
+
...sanitizeEntryData(entry.data) ? { data: sanitizeEntryData(entry.data) } : {}
|
|
569
598
|
};
|
|
570
599
|
});
|
|
571
600
|
const total = structuredEntries.length;
|
|
@@ -601,7 +630,8 @@ async function handleList(collection, status, tag, label) {
|
|
|
601
630
|
const formatted = entries.map((e) => {
|
|
602
631
|
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
603
632
|
const statusDisplay = e.workflowStatus ? `${e.status} / ${e.workflowStatus}` : e.status;
|
|
604
|
-
const
|
|
633
|
+
const visibleData = sanitizeEntryData(e.data);
|
|
634
|
+
const dataPreview = visibleData ? Object.entries(visibleData).slice(0, 4).map(([k, v]) => ` ${k}: ${typeof v === "string" ? v.substring(0, 120) : JSON.stringify(v)}`).join("\n") : "";
|
|
605
635
|
return `- ${id}${e.name} \`${statusDisplay}\`${dataPreview ? `
|
|
606
636
|
${dataPreview}` : ""}`;
|
|
607
637
|
}).join("\n\n");
|
|
@@ -1009,13 +1039,14 @@ function registerRelationsTools(server) {
|
|
|
1009
1039
|
}
|
|
1010
1040
|
async function handleCreate(from, to, type, score) {
|
|
1011
1041
|
requireWriteAccess();
|
|
1042
|
+
const agentSessionId = getAgentSessionId();
|
|
1012
1043
|
const result = await mcpMutation("chain.createEntryRelation", {
|
|
1013
1044
|
fromEntryId: from,
|
|
1014
1045
|
toEntryId: to,
|
|
1015
1046
|
type,
|
|
1016
|
-
suggestionScore: score ?? void 0
|
|
1047
|
+
suggestionScore: score ?? void 0,
|
|
1048
|
+
sessionId: agentSessionId ?? void 0
|
|
1017
1049
|
});
|
|
1018
|
-
await recordSessionActivity({ relationCreated: result?.status !== "proposal_created" });
|
|
1019
1050
|
const wsCtx = await getWorkspaceContext();
|
|
1020
1051
|
if (result?.status === "proposal_created") {
|
|
1021
1052
|
const existingNote = result.existing ? " (existing proposal reused)" : "";
|
|
@@ -1035,32 +1066,49 @@ The relation was **not applied** \u2014 it will be created when the proposal is
|
|
|
1035
1066
|
}],
|
|
1036
1067
|
structuredContent: success(
|
|
1037
1068
|
`Proposal created for ${from} \u2192 ${to} (${type}). Awaiting consent.`,
|
|
1038
|
-
{ status: "proposal_created", from, to, type, proposalId: result.proposalId }
|
|
1039
|
-
[{ tool: "entries", description: "Check proposal", parameters: { action: "get", entryId: result.proposalId } }]
|
|
1069
|
+
{ status: "proposal_created", from, to, type, proposalId: result.proposalId }
|
|
1040
1070
|
)
|
|
1041
1071
|
};
|
|
1042
1072
|
}
|
|
1073
|
+
const lines = [
|
|
1074
|
+
`# Relation Created`,
|
|
1075
|
+
"",
|
|
1076
|
+
`**${from}** \u2014[${type}]\u2192 **${to}**`,
|
|
1077
|
+
`**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
|
|
1078
|
+
];
|
|
1079
|
+
const suggested = result?.suggestedType;
|
|
1080
|
+
if (suggested) {
|
|
1081
|
+
lines.push("");
|
|
1082
|
+
lines.push(`**Type suggestion:** \`${suggested.type}\` may be more precise (${suggested.confidence}% confidence).`);
|
|
1083
|
+
lines.push(`_${suggested.reasoning}_`);
|
|
1084
|
+
lines.push(`To use instead: \`relations action=create from="${from}" to="${to}" type="${suggested.type}"\``);
|
|
1085
|
+
}
|
|
1043
1086
|
return {
|
|
1044
|
-
content: [{ type: "text", text:
|
|
1045
|
-
|
|
1046
|
-
**${from}** \u2014[${type}]\u2192 **${to}**
|
|
1047
|
-
**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }],
|
|
1087
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1048
1088
|
structuredContent: success(
|
|
1049
1089
|
`Created relation ${from} \u2192 ${to} (${type}).`,
|
|
1050
|
-
{
|
|
1090
|
+
{
|
|
1091
|
+
status: "created",
|
|
1092
|
+
from,
|
|
1093
|
+
to,
|
|
1094
|
+
type,
|
|
1095
|
+
...suggested && { suggestedType: suggested }
|
|
1096
|
+
},
|
|
1051
1097
|
[{ tool: "graph", description: "See connections", parameters: { action: "find", entryId: from } }]
|
|
1052
1098
|
)
|
|
1053
1099
|
};
|
|
1054
1100
|
}
|
|
1055
1101
|
async function handleBatchCreate(relations) {
|
|
1056
1102
|
requireWriteAccess();
|
|
1103
|
+
const agentSessionId = getAgentSessionId();
|
|
1057
1104
|
const results = [];
|
|
1058
1105
|
for (const rel of relations) {
|
|
1059
1106
|
try {
|
|
1060
1107
|
const result = await mcpMutation("chain.createEntryRelation", {
|
|
1061
1108
|
fromEntryId: rel.from,
|
|
1062
1109
|
toEntryId: rel.to,
|
|
1063
|
-
type: rel.type
|
|
1110
|
+
type: rel.type,
|
|
1111
|
+
sessionId: agentSessionId ?? void 0
|
|
1064
1112
|
});
|
|
1065
1113
|
results.push({
|
|
1066
1114
|
...rel,
|
|
@@ -1075,9 +1123,6 @@ async function handleBatchCreate(relations) {
|
|
|
1075
1123
|
const created = results.filter((r) => r.ok && !r.proposalCreated);
|
|
1076
1124
|
const proposals = results.filter((r) => r.ok && r.proposalCreated);
|
|
1077
1125
|
const failed = results.filter((r) => !r.ok);
|
|
1078
|
-
for (let i = 0; i < created.length; i++) {
|
|
1079
|
-
await recordSessionActivity({ relationCreated: true });
|
|
1080
|
-
}
|
|
1081
1126
|
const lines = [`# Batch Link Results
|
|
1082
1127
|
`];
|
|
1083
1128
|
lines.push(`**${created.length}** created, **${proposals.length}** proposals, **${failed.length}** failed out of ${relations.length} total.
|
|
@@ -1141,6 +1186,12 @@ function annotateStaleNames(text) {
|
|
|
1141
1186
|
const footnote = translateStaleToolNames(text);
|
|
1142
1187
|
return footnote ? text + footnote : text;
|
|
1143
1188
|
}
|
|
1189
|
+
function epistemicCollectionHint(collectionName) {
|
|
1190
|
+
const lc = collectionName.toLowerCase();
|
|
1191
|
+
if (lc === "insights") return " \u2014 _verify confidence with `entries action=get`_";
|
|
1192
|
+
if (lc === "assumptions") return " \u2014 _check evidence strength with `entries action=get`_";
|
|
1193
|
+
return "";
|
|
1194
|
+
}
|
|
1144
1195
|
var CONTEXT_ACTIONS = ["gather", "build"];
|
|
1145
1196
|
var contextSchema = z5.object({
|
|
1146
1197
|
action: z5.enum(CONTEXT_ACTIONS).describe(
|
|
@@ -1233,7 +1284,7 @@ Use \`graph action=suggest\` to discover potential connections.`
|
|
|
1233
1284
|
""
|
|
1234
1285
|
];
|
|
1235
1286
|
for (const [collName, entries] of byCollection2) {
|
|
1236
|
-
lines2.push(`## ${collName} (${entries.length})`);
|
|
1287
|
+
lines2.push(`## ${collName} (${entries.length})${epistemicCollectionHint(collName)}`);
|
|
1237
1288
|
for (const e of entries) {
|
|
1238
1289
|
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
1239
1290
|
const id = e.entryId ? `${e.entryId}: ` : "";
|
|
@@ -1315,7 +1366,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
|
|
|
1315
1366
|
""
|
|
1316
1367
|
];
|
|
1317
1368
|
for (const [collName, entries] of byCollection2) {
|
|
1318
|
-
lines2.push(`### ${collName} (${entries.length})`);
|
|
1369
|
+
lines2.push(`### ${collName} (${entries.length})${epistemicCollectionHint(collName)}`);
|
|
1319
1370
|
for (const e of entries) {
|
|
1320
1371
|
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
1321
1372
|
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
@@ -1378,7 +1429,7 @@ Use \`graph action=suggest\` to discover potential connections, or \`relations a
|
|
|
1378
1429
|
""
|
|
1379
1430
|
];
|
|
1380
1431
|
for (const [collName, entries] of byCollection) {
|
|
1381
|
-
lines.push(`## ${collName} (${entries.length})`);
|
|
1432
|
+
lines.push(`## ${collName} (${entries.length})${epistemicCollectionHint(collName)}`);
|
|
1382
1433
|
for (const e of entries) {
|
|
1383
1434
|
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
1384
1435
|
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
@@ -1995,17 +2046,27 @@ async function runWrapupReview() {
|
|
|
1995
2046
|
async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
1996
2047
|
requireWriteAccess();
|
|
1997
2048
|
const sessionId = getAgentSessionId();
|
|
1998
|
-
if (!sessionId)
|
|
2049
|
+
if (!sessionId) {
|
|
2050
|
+
return { text: "No active session.", committed: 0, proposalsCreated: 0, linksCreated: 0, failed: 0 };
|
|
2051
|
+
}
|
|
1999
2052
|
const results = [];
|
|
2000
2053
|
let linksCreated = 0;
|
|
2054
|
+
let proposalsCreated = 0;
|
|
2001
2055
|
for (const draft of data.drafts) {
|
|
2002
2056
|
try {
|
|
2003
|
-
await mcpMutation("chain.commitEntry", {
|
|
2057
|
+
const result = await mcpMutation("chain.commitEntry", {
|
|
2004
2058
|
entryId: draft.entryId,
|
|
2005
|
-
author:
|
|
2059
|
+
author: `agent:${sessionId}`,
|
|
2060
|
+
sessionId
|
|
2006
2061
|
});
|
|
2007
|
-
|
|
2008
|
-
|
|
2062
|
+
const proposed = result?.status === "proposal_created";
|
|
2063
|
+
if (!proposed) {
|
|
2064
|
+
await recordSessionActivity({ entryModified: draft.docId });
|
|
2065
|
+
}
|
|
2066
|
+
if (proposed) {
|
|
2067
|
+
proposalsCreated++;
|
|
2068
|
+
}
|
|
2069
|
+
results.push({ entryId: draft.entryId, ok: true, proposed });
|
|
2009
2070
|
} catch (err) {
|
|
2010
2071
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2011
2072
|
results.push({ entryId: draft.entryId, ok: false, error: msg });
|
|
@@ -2016,19 +2077,25 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
2016
2077
|
(e) => (data.connectionCounts[e.entryId] ?? 0) < 2
|
|
2017
2078
|
).slice(0, 10)
|
|
2018
2079
|
);
|
|
2019
|
-
|
|
2080
|
+
const linkableSourceIds = /* @__PURE__ */ new Set([
|
|
2081
|
+
...data.committed.map((entry) => entry.entryId),
|
|
2082
|
+
...results.filter((r) => r.ok && !r.proposed).map((r) => r.entryId)
|
|
2083
|
+
]);
|
|
2084
|
+
for (const s of suggestionsToApply.filter((s2) => linkableSourceIds.has(s2.fromEntryId)).slice(0, 15)) {
|
|
2020
2085
|
try {
|
|
2021
|
-
await mcpMutation("chain.createEntryRelation", {
|
|
2086
|
+
const result = await mcpMutation("chain.createEntryRelation", {
|
|
2022
2087
|
fromEntryId: s.fromEntryId,
|
|
2023
2088
|
toEntryId: s.toEntryId,
|
|
2024
|
-
type: s.type
|
|
2089
|
+
type: s.type,
|
|
2090
|
+
sessionId
|
|
2025
2091
|
});
|
|
2026
|
-
|
|
2027
|
-
|
|
2092
|
+
if (result?.status !== "proposal_created") {
|
|
2093
|
+
linksCreated++;
|
|
2094
|
+
}
|
|
2028
2095
|
} catch {
|
|
2029
2096
|
}
|
|
2030
2097
|
}
|
|
2031
|
-
const committed = results.filter((r) => r.ok).length;
|
|
2098
|
+
const committed = results.filter((r) => r.ok && !r.proposed).length;
|
|
2032
2099
|
const failed = results.filter((r) => !r.ok);
|
|
2033
2100
|
await mcpCall("agent.recordWrapup", {
|
|
2034
2101
|
sessionId,
|
|
@@ -2039,7 +2106,7 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
2039
2106
|
const lines = [
|
|
2040
2107
|
"## Wrapup Complete",
|
|
2041
2108
|
"",
|
|
2042
|
-
`**${committed}** drafts committed, **${linksCreated}** links created.`
|
|
2109
|
+
`**${committed}** drafts committed, **${proposalsCreated}** proposals created, **${linksCreated}** links created.`
|
|
2043
2110
|
];
|
|
2044
2111
|
if (failed.length > 0) {
|
|
2045
2112
|
lines.push("");
|
|
@@ -2049,7 +2116,13 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
2049
2116
|
}
|
|
2050
2117
|
lines.push("_These remain as drafts for next session._");
|
|
2051
2118
|
}
|
|
2052
|
-
return
|
|
2119
|
+
return {
|
|
2120
|
+
text: lines.join("\n"),
|
|
2121
|
+
committed,
|
|
2122
|
+
proposalsCreated,
|
|
2123
|
+
linksCreated,
|
|
2124
|
+
failed: failed.length
|
|
2125
|
+
};
|
|
2053
2126
|
}
|
|
2054
2127
|
var WRAPUP_TOOL_DESCRIPTION = "Review your session before closing. Shows uncommitted drafts with quality scores, suggests links for low-connection entries, and offers batch commit. Run this before `session action=close` to ensure nothing is left behind. If you call `session action=close` without running wrapup first, you'll get a review-only nudge.";
|
|
2055
2128
|
var wrapupSchema = z8.object({
|
|
@@ -2060,6 +2133,7 @@ var wrapupSchema = z8.object({
|
|
|
2060
2133
|
function registerWrapupTools(server) {
|
|
2061
2134
|
let lastReviewData = null;
|
|
2062
2135
|
let lastReviewSuggestions = [];
|
|
2136
|
+
let lastReviewSessionId = null;
|
|
2063
2137
|
const wrapupHandler = async ({ action }) => {
|
|
2064
2138
|
if (action === "commit-all") {
|
|
2065
2139
|
const sessionId = getAgentSessionId();
|
|
@@ -2072,18 +2146,30 @@ function registerWrapupTools(server) {
|
|
|
2072
2146
|
[{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
|
|
2073
2147
|
);
|
|
2074
2148
|
}
|
|
2075
|
-
const
|
|
2149
|
+
const useCachedReview = lastReviewSessionId === sessionId;
|
|
2150
|
+
const data2 = useCachedReview && lastReviewData ? lastReviewData : await mcpCall("agent.getSessionWrapup", { sessionId });
|
|
2076
2151
|
if (data2.drafts.length === 0) {
|
|
2077
2152
|
return failureResult("No uncommitted drafts to commit.", "NOT_FOUND", "No uncommitted drafts to commit.", "Nothing to commit.");
|
|
2078
2153
|
}
|
|
2079
|
-
const result = await runWrapupCommitAll(data2, lastReviewSuggestions);
|
|
2154
|
+
const result = await runWrapupCommitAll(data2, useCachedReview ? lastReviewSuggestions : []);
|
|
2080
2155
|
lastReviewData = null;
|
|
2081
2156
|
lastReviewSuggestions = [];
|
|
2082
|
-
|
|
2157
|
+
lastReviewSessionId = null;
|
|
2158
|
+
return successResult(
|
|
2159
|
+
result.text,
|
|
2160
|
+
`Wrapup processed: ${result.committed} committed, ${result.proposalsCreated} proposals, ${result.linksCreated} links.`,
|
|
2161
|
+
{
|
|
2162
|
+
committed: result.committed,
|
|
2163
|
+
proposalsCreated: result.proposalsCreated,
|
|
2164
|
+
linksCreated: result.linksCreated,
|
|
2165
|
+
failed: result.failed
|
|
2166
|
+
}
|
|
2167
|
+
);
|
|
2083
2168
|
}
|
|
2084
2169
|
const { text, data, suggestions, failureCode } = await runWrapupReview();
|
|
2085
2170
|
lastReviewData = data;
|
|
2086
2171
|
lastReviewSuggestions = suggestions;
|
|
2172
|
+
lastReviewSessionId = getAgentSessionId();
|
|
2087
2173
|
const fullText = data && (data.drafts.length > 0 || data.committed.length > 0) ? `## Session Wrapup
|
|
2088
2174
|
|
|
2089
2175
|
${text}` : text;
|
|
@@ -3915,16 +4001,11 @@ function computeCommitBlockers(opts) {
|
|
|
3915
4001
|
const { betEntryId, betDocId, relations, hasStrategyLink, betData, sessionDrafts } = opts;
|
|
3916
4002
|
const blockers = [];
|
|
3917
4003
|
const str = (key) => (betData[key] ?? "").trim();
|
|
3918
|
-
|
|
3919
|
-
if (r.type !== "commits_to") return false;
|
|
3920
|
-
return r.fromId === betDocId && r.toId !== betDocId;
|
|
3921
|
-
});
|
|
3922
|
-
const strategyLinked = hasStrategyLink ?? hasDirectionalCommitsTo;
|
|
3923
|
-
if (!strategyLinked) {
|
|
4004
|
+
if (!hasStrategyLink) {
|
|
3924
4005
|
blockers.push({
|
|
3925
4006
|
entryId: betEntryId,
|
|
3926
4007
|
blocker: "Missing strategy link",
|
|
3927
|
-
fix: `relations action=create from=${betEntryId} to=<strategy> type=
|
|
4008
|
+
fix: `relations action=create from=${betEntryId} to=<strategy> type=related_to`
|
|
3928
4009
|
});
|
|
3929
4010
|
}
|
|
3930
4011
|
const MIN_FIELD_LENGTH = 20;
|
|
@@ -4028,6 +4109,21 @@ var facilitateSchema = z12.object({
|
|
|
4028
4109
|
z12.array(captureItemSchema).max(15)
|
|
4029
4110
|
]).optional().describe("Entry or entries to capture alongside the respond action. Accepts a single object or an array (max 15) for batch capture.")
|
|
4030
4111
|
});
|
|
4112
|
+
async function hasStrategyLinkForEntry(entryDocId, relations) {
|
|
4113
|
+
const relatedDocIds = /* @__PURE__ */ new Set();
|
|
4114
|
+
for (const rel of relations) {
|
|
4115
|
+
if (rel.fromId === entryDocId && rel.toId) relatedDocIds.add(rel.toId);
|
|
4116
|
+
if (rel.toId === entryDocId && rel.fromId) relatedDocIds.add(rel.fromId);
|
|
4117
|
+
}
|
|
4118
|
+
for (const docId of relatedDocIds) {
|
|
4119
|
+
try {
|
|
4120
|
+
const related = await mcpQuery("chain.getEntry", { id: docId });
|
|
4121
|
+
if (related?.collectionSlug === "strategy") return true;
|
|
4122
|
+
} catch {
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return false;
|
|
4126
|
+
}
|
|
4031
4127
|
function registerFacilitateTools(server) {
|
|
4032
4128
|
server.registerTool(
|
|
4033
4129
|
"facilitate",
|
|
@@ -4332,7 +4428,7 @@ async function searchChain(text, opts = {}) {
|
|
|
4332
4428
|
return [];
|
|
4333
4429
|
}
|
|
4334
4430
|
}
|
|
4335
|
-
async function findAndLinkExisting(name, collectionSlug, betEntryId, relationType) {
|
|
4431
|
+
async function findAndLinkExisting(name, collectionSlug, betEntryId, relationType, agentSessionId) {
|
|
4336
4432
|
try {
|
|
4337
4433
|
const results = await mcpQuery(
|
|
4338
4434
|
"chain.searchEntries",
|
|
@@ -4346,9 +4442,9 @@ async function findAndLinkExisting(name, collectionSlug, betEntryId, relationTyp
|
|
|
4346
4442
|
await mcpMutation("chain.createEntryRelation", {
|
|
4347
4443
|
fromEntryId: match.entryId,
|
|
4348
4444
|
toEntryId: betEntryId,
|
|
4349
|
-
type: relationType
|
|
4445
|
+
type: relationType,
|
|
4446
|
+
sessionId: agentSessionId ?? void 0
|
|
4350
4447
|
});
|
|
4351
|
-
await recordSessionActivity({ relationCreated: true });
|
|
4352
4448
|
return { entryId: match.entryId, linked: true };
|
|
4353
4449
|
} catch {
|
|
4354
4450
|
return { entryId: match.entryId, linked: false };
|
|
@@ -4461,7 +4557,6 @@ async function processCaptures(opts) {
|
|
|
4461
4557
|
);
|
|
4462
4558
|
capturedEntryId = result.entryId;
|
|
4463
4559
|
entriesCreated.push(result.entryId);
|
|
4464
|
-
await recordSessionActivity({ entryCreated: result.docId });
|
|
4465
4560
|
} catch (createErr) {
|
|
4466
4561
|
const msg = createErr instanceof Error ? createErr.message : String(createErr);
|
|
4467
4562
|
if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
|
|
@@ -4469,7 +4564,8 @@ async function processCaptures(opts) {
|
|
|
4469
4564
|
item.name,
|
|
4470
4565
|
mapping.slug,
|
|
4471
4566
|
betEntryId,
|
|
4472
|
-
mapping.relationType
|
|
4567
|
+
mapping.relationType,
|
|
4568
|
+
capAgentId
|
|
4473
4569
|
);
|
|
4474
4570
|
if (fallback) {
|
|
4475
4571
|
capturedEntryId = fallback.entryId;
|
|
@@ -4491,10 +4587,10 @@ async function processCaptures(opts) {
|
|
|
4491
4587
|
await mcpMutation("chain.createEntryRelation", {
|
|
4492
4588
|
fromEntryId: capturedEntryId,
|
|
4493
4589
|
toEntryId: betEntryId,
|
|
4494
|
-
type: mapping.relationType
|
|
4590
|
+
type: mapping.relationType,
|
|
4591
|
+
sessionId: capAgentId ?? void 0
|
|
4495
4592
|
});
|
|
4496
4593
|
relationsCreated++;
|
|
4497
|
-
await recordSessionActivity({ relationCreated: true });
|
|
4498
4594
|
} catch (relErr) {
|
|
4499
4595
|
const msg = relErr instanceof Error ? relErr.message : String(relErr);
|
|
4500
4596
|
if (!msg.includes("already exists") && !msg.includes("Duplicate")) {
|
|
@@ -4673,10 +4769,15 @@ async function computeAndUpdateScores(opts) {
|
|
|
4673
4769
|
}
|
|
4674
4770
|
let commitBlockers;
|
|
4675
4771
|
if (captureReady) {
|
|
4772
|
+
const hasStrategyLink = await hasStrategyLinkForEntry(
|
|
4773
|
+
refreshedBet?._id ?? betEntry._id,
|
|
4774
|
+
refreshedConstellation.relations
|
|
4775
|
+
);
|
|
4676
4776
|
commitBlockers = computeCommitBlockers({
|
|
4677
4777
|
betEntryId,
|
|
4678
4778
|
betDocId: refreshedBet?._id ?? betEntry._id,
|
|
4679
4779
|
relations: refreshedConstellation.relations,
|
|
4780
|
+
hasStrategyLink,
|
|
4680
4781
|
betData: refreshedData,
|
|
4681
4782
|
sessionDrafts
|
|
4682
4783
|
});
|
|
@@ -4841,7 +4942,6 @@ async function handleRespond(args) {
|
|
|
4841
4942
|
}
|
|
4842
4943
|
);
|
|
4843
4944
|
betId = result.entryId;
|
|
4844
|
-
await recordSessionActivity({ entryCreated: result.docId });
|
|
4845
4945
|
} catch (err) {
|
|
4846
4946
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4847
4947
|
return {
|
|
@@ -4977,10 +5077,12 @@ async function handleScore(args) {
|
|
|
4977
5077
|
lines.push("");
|
|
4978
5078
|
lines.push(`**Session drafts (${sessionDrafts.length}):** ${sessionDrafts.map((d) => `\`${d.entryId}\` ${d.name}`).join(", ")}`);
|
|
4979
5079
|
}
|
|
5080
|
+
const hasStrategyLink = await hasStrategyLinkForEntry(betEntry._id, constellation.relations);
|
|
4980
5081
|
const commitBlockers = computeCommitBlockers({
|
|
4981
5082
|
betEntryId: betId,
|
|
4982
5083
|
betDocId: betEntry._id,
|
|
4983
5084
|
relations: constellation.relations,
|
|
5085
|
+
hasStrategyLink,
|
|
4984
5086
|
betData,
|
|
4985
5087
|
sessionDrafts
|
|
4986
5088
|
});
|
|
@@ -5154,21 +5256,7 @@ async function handleCommitConstellation(args) {
|
|
|
5154
5256
|
const relations = await mcpQuery("chain.listEntryRelations", {
|
|
5155
5257
|
entryId: betId
|
|
5156
5258
|
});
|
|
5157
|
-
const hasStrategyLink = await (
|
|
5158
|
-
const relatedDocIds = /* @__PURE__ */ new Set();
|
|
5159
|
-
for (const rel of relations) {
|
|
5160
|
-
if (rel.fromId === betEntry._id && rel.toId) relatedDocIds.add(rel.toId);
|
|
5161
|
-
if (rel.toId === betEntry._id && rel.fromId) relatedDocIds.add(rel.fromId);
|
|
5162
|
-
}
|
|
5163
|
-
for (const docId of relatedDocIds) {
|
|
5164
|
-
try {
|
|
5165
|
-
const related = await mcpQuery("chain.getEntry", { id: docId });
|
|
5166
|
-
if (related?.collectionSlug === "strategy") return true;
|
|
5167
|
-
} catch {
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
return false;
|
|
5171
|
-
})();
|
|
5259
|
+
const hasStrategyLink = await hasStrategyLinkForEntry(betEntry._id, relations);
|
|
5172
5260
|
const sessionDrafts = await loadSessionDrafts(betId, betEntry._id);
|
|
5173
5261
|
const commitBlockerItems = computeCommitBlockers({
|
|
5174
5262
|
betEntryId: betId,
|
|
@@ -5207,7 +5295,7 @@ async function handleCommitConstellation(args) {
|
|
|
5207
5295
|
}
|
|
5208
5296
|
let contradictionWarnings = [];
|
|
5209
5297
|
try {
|
|
5210
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
5298
|
+
const { runContradictionCheck } = await import("./smart-capture-2IM2565I.js");
|
|
5211
5299
|
const descField = betData.problem ?? betData.description ?? "";
|
|
5212
5300
|
contradictionWarnings = await runContradictionCheck(
|
|
5213
5301
|
betEntry.name ?? betId,
|
|
@@ -5268,12 +5356,11 @@ No constellation entries were committed.`
|
|
|
5268
5356
|
const lines = [];
|
|
5269
5357
|
const hasIssues = result.failedIds.length > 0 || result.conflictIds.length > 0;
|
|
5270
5358
|
if (result.proposalCreated) {
|
|
5271
|
-
const linkedCount = result.committedIds.filter((id) => id !== betId).length;
|
|
5272
5359
|
lines.push(
|
|
5273
5360
|
hasIssues ? `# Proposal created with issues` : `# Proposal created`,
|
|
5274
5361
|
"",
|
|
5275
5362
|
`Workspace uses consent-based governance. \`${betId}\` was submitted as a proposal \u2014 it will be committed when approved.`,
|
|
5276
|
-
|
|
5363
|
+
`No linked entries were committed because the bet itself is still awaiting approval.`
|
|
5277
5364
|
);
|
|
5278
5365
|
} else {
|
|
5279
5366
|
lines.push(
|
|
@@ -5284,7 +5371,13 @@ No constellation entries were committed.`
|
|
|
5284
5371
|
);
|
|
5285
5372
|
}
|
|
5286
5373
|
if (contradictionWarnings.length > 0) {
|
|
5287
|
-
lines.push(
|
|
5374
|
+
lines.push(
|
|
5375
|
+
"",
|
|
5376
|
+
`\u26A0 **Contradiction warnings** (advisory):`,
|
|
5377
|
+
...contradictionWarnings.map(
|
|
5378
|
+
(w) => `- \`${w.entryId}\` ${w.name} [${w.collection}] (${w.governsCount} governs relation${w.governsCount === 1 ? "" : "s"})`
|
|
5379
|
+
)
|
|
5380
|
+
);
|
|
5288
5381
|
}
|
|
5289
5382
|
if (result.committedIds.length > 0) {
|
|
5290
5383
|
lines.push("", `**Committed:** ${result.committedIds.join(", ")}`);
|
|
@@ -5762,35 +5855,39 @@ function listPresets() {
|
|
|
5762
5855
|
}
|
|
5763
5856
|
|
|
5764
5857
|
// src/tools/planned-work.ts
|
|
5765
|
-
|
|
5858
|
+
function buildPlannedWork(allEntries) {
|
|
5766
5859
|
const result = {
|
|
5767
5860
|
uncommittedDrafts: [],
|
|
5768
5861
|
inProgressEntries: [],
|
|
5769
5862
|
openTensions: []
|
|
5770
5863
|
};
|
|
5864
|
+
for (const entry of allEntries) {
|
|
5865
|
+
if (entry.stratum === "system") continue;
|
|
5866
|
+
const collection = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
5867
|
+
if (entry.status === "draft") {
|
|
5868
|
+
result.uncommittedDrafts.push({ name: entry.name, collection });
|
|
5869
|
+
}
|
|
5870
|
+
if (collection === "tensions" && entry.workflowStatus === "open") {
|
|
5871
|
+
result.openTensions.push({ name: entry.name, entryId: entry.entryId ?? entry._id });
|
|
5872
|
+
}
|
|
5873
|
+
if (entry.workflowStatus === "in-progress") {
|
|
5874
|
+
result.inProgressEntries.push({
|
|
5875
|
+
name: entry.name,
|
|
5876
|
+
collection,
|
|
5877
|
+
entryId: entry.entryId ?? entry._id
|
|
5878
|
+
});
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
return result;
|
|
5882
|
+
}
|
|
5883
|
+
async function queryPlannedWork() {
|
|
5771
5884
|
try {
|
|
5772
5885
|
const allEntries = await mcpQuery("chain.listEntries", {});
|
|
5773
|
-
if (!allEntries) return
|
|
5774
|
-
|
|
5775
|
-
if (entry.stratum === "system") continue;
|
|
5776
|
-
const collection = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
5777
|
-
if (entry.status === "draft") {
|
|
5778
|
-
result.uncommittedDrafts.push({ name: entry.name, collection });
|
|
5779
|
-
}
|
|
5780
|
-
if (collection === "tensions" && entry.workflowStatus === "open") {
|
|
5781
|
-
result.openTensions.push({ name: entry.name, entryId: entry.entryId ?? entry._id });
|
|
5782
|
-
}
|
|
5783
|
-
if (entry.workflowStatus === "in-progress") {
|
|
5784
|
-
result.inProgressEntries.push({
|
|
5785
|
-
name: entry.name,
|
|
5786
|
-
collection,
|
|
5787
|
-
entryId: entry.entryId ?? entry._id
|
|
5788
|
-
});
|
|
5789
|
-
}
|
|
5790
|
-
}
|
|
5886
|
+
if (!allEntries) return { uncommittedDrafts: [], inProgressEntries: [], openTensions: [] };
|
|
5887
|
+
return buildPlannedWork(allEntries);
|
|
5791
5888
|
} catch {
|
|
5889
|
+
return { uncommittedDrafts: [], inProgressEntries: [], openTensions: [] };
|
|
5792
5890
|
}
|
|
5793
|
-
return result;
|
|
5794
5891
|
}
|
|
5795
5892
|
function hasPlannedWork(work) {
|
|
5796
5893
|
return work.uncommittedDrafts.length > 0 || work.inProgressEntries.length > 0 || work.openTensions.length > 0;
|
|
@@ -7694,1960 +7791,2154 @@ Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntr
|
|
|
7694
7791
|
}
|
|
7695
7792
|
|
|
7696
7793
|
// src/tools/architecture.ts
|
|
7697
|
-
import { existsSync as
|
|
7698
|
-
import { resolve as
|
|
7794
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
7795
|
+
import { resolve as resolve3, relative, dirname as dirname2, normalize } from "path";
|
|
7699
7796
|
import { z as z19 } from "zod";
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7797
|
+
|
|
7798
|
+
// src/resources/index.ts
|
|
7799
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7800
|
+
import { readFile } from "fs/promises";
|
|
7801
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
7802
|
+
import { fileURLToPath } from "url";
|
|
7803
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7804
|
+
var UI_VIEWS = {
|
|
7805
|
+
"ui://entries/entry-cards.html": "src/entry-cards/index.html",
|
|
7806
|
+
"ui://graph/constellation.html": "src/graph-constellation/index.html"
|
|
7807
|
+
};
|
|
7808
|
+
var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
7809
|
+
var MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7810
|
+
var UI_VIEW_BASE_CANDIDATES = [
|
|
7811
|
+
resolve2(MODULE_DIR, "views"),
|
|
7812
|
+
resolve2(MODULE_DIR, "..", "views"),
|
|
7813
|
+
resolve2(MODULE_DIR, "..", "..", "dist", "views"),
|
|
7814
|
+
resolve2(MODULE_DIR, "..", "..", "..", "mcp-views", "dist")
|
|
7715
7815
|
];
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
const
|
|
7719
|
-
const
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7816
|
+
function resolveUiViewPath(filePath, options) {
|
|
7817
|
+
const moduleDir = options?.moduleDir ?? MODULE_DIR;
|
|
7818
|
+
const pathExists = options?.pathExists ?? existsSync2;
|
|
7819
|
+
const candidateBases = options?.moduleDir ? [
|
|
7820
|
+
resolve2(moduleDir, "views"),
|
|
7821
|
+
resolve2(moduleDir, "..", "views"),
|
|
7822
|
+
resolve2(moduleDir, "..", "..", "dist", "views"),
|
|
7823
|
+
resolve2(moduleDir, "..", "..", "..", "mcp-views", "dist")
|
|
7824
|
+
] : UI_VIEW_BASE_CANDIDATES;
|
|
7825
|
+
for (const candidateBase of candidateBases) {
|
|
7826
|
+
const candidatePath = join(candidateBase, filePath);
|
|
7827
|
+
if (pathExists(candidatePath)) {
|
|
7828
|
+
return candidatePath;
|
|
7726
7829
|
}
|
|
7727
|
-
return;
|
|
7728
7830
|
}
|
|
7729
|
-
|
|
7730
|
-
slug: COLLECTION_SLUG,
|
|
7731
|
-
name: "Architecture",
|
|
7732
|
-
icon: "\u{1F3D7}\uFE0F",
|
|
7733
|
-
description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
|
|
7734
|
-
fields: COLLECTION_FIELDS
|
|
7735
|
-
});
|
|
7736
|
-
}
|
|
7737
|
-
async function listArchEntries() {
|
|
7738
|
-
return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
7739
|
-
}
|
|
7740
|
-
function byTag(entries, archType) {
|
|
7741
|
-
return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
7831
|
+
return null;
|
|
7742
7832
|
}
|
|
7743
|
-
function
|
|
7744
|
-
const layerHtml = layers.map((layer) => {
|
|
7745
|
-
const layerNodes = nodes.filter(
|
|
7746
|
-
(n) => n.data?.layerRef === layer.entryId
|
|
7747
|
-
);
|
|
7748
|
-
const nodeCards = layerNodes.map((n) => `
|
|
7749
|
-
<div class="node" title="${escHtml(String(n.data?.description ?? ""))}">
|
|
7750
|
-
<span class="node-icon">${escHtml(String(n.data?.icon ?? "\u25FB"))}</span>
|
|
7751
|
-
<span class="node-name">${escHtml(n.name)}</span>
|
|
7752
|
-
</div>
|
|
7753
|
-
`).join("");
|
|
7754
|
-
return `
|
|
7755
|
-
<div class="layer" style="--layer-color: ${escHtml(String(layer.data?.color ?? "#666"))}">
|
|
7756
|
-
<div class="layer-label">
|
|
7757
|
-
<span class="layer-dot"></span>
|
|
7758
|
-
<span class="layer-name">${escHtml(layer.name)}</span>
|
|
7759
|
-
<span class="layer-count">${layerNodes.length}</span>
|
|
7760
|
-
</div>
|
|
7761
|
-
<div class="layer-desc">${escHtml(String(layer.data?.description ?? ""))}</div>
|
|
7762
|
-
<div class="nodes">${nodeCards || '<span class="empty">No components</span>'}</div>
|
|
7763
|
-
</div>
|
|
7764
|
-
`;
|
|
7765
|
-
}).join("");
|
|
7833
|
+
function renderMissingUiView(uri) {
|
|
7766
7834
|
return `<!DOCTYPE html>
|
|
7767
|
-
<html
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
.
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7835
|
+
<html lang="en">
|
|
7836
|
+
<head>
|
|
7837
|
+
<meta charset="UTF-8" />
|
|
7838
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7839
|
+
<title>Product Brain View Unavailable</title>
|
|
7840
|
+
<style>
|
|
7841
|
+
body {
|
|
7842
|
+
margin: 0;
|
|
7843
|
+
padding: 20px;
|
|
7844
|
+
font-family: system-ui, sans-serif;
|
|
7845
|
+
background: #0f172a;
|
|
7846
|
+
color: #e2e8f0;
|
|
7847
|
+
}
|
|
7848
|
+
.panel {
|
|
7849
|
+
max-width: 720px;
|
|
7850
|
+
margin: 0 auto;
|
|
7851
|
+
padding: 20px;
|
|
7852
|
+
border: 1px solid #334155;
|
|
7853
|
+
border-radius: 12px;
|
|
7854
|
+
background: #111827;
|
|
7855
|
+
}
|
|
7856
|
+
h1 {
|
|
7857
|
+
margin: 0 0 12px;
|
|
7858
|
+
font-size: 18px;
|
|
7859
|
+
}
|
|
7860
|
+
p {
|
|
7861
|
+
margin: 0 0 10px;
|
|
7862
|
+
line-height: 1.5;
|
|
7863
|
+
}
|
|
7864
|
+
code {
|
|
7865
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
7866
|
+
color: #93c5fd;
|
|
7867
|
+
}
|
|
7868
|
+
</style>
|
|
7869
|
+
</head>
|
|
7870
|
+
<body>
|
|
7871
|
+
<main class="panel">
|
|
7872
|
+
<h1>Product Brain view unavailable</h1>
|
|
7873
|
+
<p>The UI resource <code>${uri}</code> could not be loaded.</p>
|
|
7874
|
+
<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>
|
|
7875
|
+
</main>
|
|
7876
|
+
</body>
|
|
7877
|
+
</html>`;
|
|
7791
7878
|
}
|
|
7792
|
-
function
|
|
7793
|
-
|
|
7879
|
+
function formatEntryMarkdown(entry) {
|
|
7880
|
+
const id = entry.entryId ? `${entry.entryId}: ` : "";
|
|
7881
|
+
const lines = [`## ${id}${entry.name} [${entry.status}]`];
|
|
7882
|
+
if (entry.data && typeof entry.data === "object") {
|
|
7883
|
+
for (const [key, val] of Object.entries(entry.data)) {
|
|
7884
|
+
if (val && key !== "rawData") {
|
|
7885
|
+
lines.push(`**${key}**: ${typeof val === "string" ? val : JSON.stringify(val)}`);
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
return lines.join("\n");
|
|
7794
7890
|
}
|
|
7795
|
-
function
|
|
7796
|
-
const
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7891
|
+
function buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles) {
|
|
7892
|
+
const sections = ["# Product Brain \u2014 Orientation"];
|
|
7893
|
+
sections.push(
|
|
7894
|
+
"## 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."
|
|
7895
|
+
);
|
|
7896
|
+
const wsRules = [];
|
|
7897
|
+
for (const p of (principles ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
|
|
7898
|
+
wsRules.push({ id: p.entryId ?? "", name: p.name, severity: p.data?.severity ?? void 0, source: "principles" });
|
|
7899
|
+
}
|
|
7900
|
+
for (const s of (standards ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
|
|
7901
|
+
wsRules.push({ id: s.entryId ?? "", name: s.name, severity: s.data?.severity ?? void 0, source: "standards" });
|
|
7902
|
+
}
|
|
7903
|
+
for (const r of (businessRules ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
|
|
7904
|
+
wsRules.push({ id: r.entryId ?? "", name: r.name, severity: r.data?.severity ?? void 0, source: "business-rules" });
|
|
7905
|
+
}
|
|
7906
|
+
if (wsRules.length > 0) {
|
|
7907
|
+
const ruleLines = wsRules.map((r) => {
|
|
7908
|
+
const sev = r.severity ? ` [${r.severity}]` : "";
|
|
7909
|
+
return `- **${r.id}**: ${r.name}${sev}`;
|
|
7910
|
+
}).join("\n");
|
|
7911
|
+
sections.push(
|
|
7912
|
+
`## Your Workspace Principles & Rules (${wsRules.length} active)
|
|
7913
|
+
These are principles, standards, and rules your team has committed to the Chain. Respect them during implementation.
|
|
7804
7914
|
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
archType: "template",
|
|
7812
|
-
description: "Default 4-layer architecture: Auth \u2192 Infrastructure \u2192 Core \u2192 Features, with an outward Integration layer",
|
|
7813
|
-
layerOrder: JSON.stringify(["auth", "infrastructure", "core", "features", "integration"])
|
|
7915
|
+
` + ruleLines + '\n\nUse `entries action=get entryId="<ID>"` to drill into any rule before making changes in that area.'
|
|
7916
|
+
);
|
|
7917
|
+
} else {
|
|
7918
|
+
sections.push(
|
|
7919
|
+
"## 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."
|
|
7920
|
+
);
|
|
7814
7921
|
}
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
// Core layer
|
|
7832
|
-
{ 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." } },
|
|
7833
|
-
{ 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." } },
|
|
7834
|
-
{ 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." } },
|
|
7835
|
-
{ 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." } },
|
|
7836
|
-
// Features layer
|
|
7837
|
-
{ 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." } },
|
|
7838
|
-
{ 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." } },
|
|
7839
|
-
{ 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." } },
|
|
7840
|
-
{ 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." } },
|
|
7841
|
-
{ 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." } },
|
|
7842
|
-
{ 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." } },
|
|
7843
|
-
{ 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." } },
|
|
7844
|
-
// Integration layer
|
|
7845
|
-
{ 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." } },
|
|
7846
|
-
{ 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." } },
|
|
7847
|
-
{ 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." } }
|
|
7848
|
-
];
|
|
7849
|
-
var SEED_FLOWS = [
|
|
7850
|
-
{ 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" } },
|
|
7851
|
-
{ 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" } },
|
|
7852
|
-
{ 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" } },
|
|
7853
|
-
{ 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" } },
|
|
7854
|
-
{ 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" } }
|
|
7855
|
-
];
|
|
7856
|
-
var architectureSchema = z19.object({
|
|
7857
|
-
action: z19.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
|
|
7858
|
-
template: z19.string().optional().describe("Template entry ID to filter by (for show)"),
|
|
7859
|
-
layer: z19.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
|
|
7860
|
-
flow: z19.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
|
|
7861
|
-
});
|
|
7862
|
-
var architectureAdminSchema = z19.object({
|
|
7863
|
-
action: z19.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
|
|
7864
|
-
});
|
|
7865
|
-
function registerArchitectureTools(server) {
|
|
7866
|
-
server.registerTool(
|
|
7867
|
-
"architecture",
|
|
7868
|
-
{
|
|
7869
|
-
title: "Architecture",
|
|
7870
|
-
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",
|
|
7871
|
-
inputSchema: architectureSchema,
|
|
7872
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
7873
|
-
},
|
|
7874
|
-
withEnvelope(async ({ action, template, layer, flow }) => {
|
|
7875
|
-
await ensureCollection();
|
|
7876
|
-
const all = await listArchEntries();
|
|
7877
|
-
if (action === "show") {
|
|
7878
|
-
const templates = byTag(all, "template");
|
|
7879
|
-
const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
|
|
7880
|
-
const templateName = activeTemplate?.name ?? "System Architecture";
|
|
7881
|
-
const templateId = activeTemplate?.entryId;
|
|
7882
|
-
const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
|
|
7883
|
-
const nodes = byTag(all, "node");
|
|
7884
|
-
const flows = byTag(all, "flow");
|
|
7885
|
-
if (layers.length === 0) {
|
|
7886
|
-
return {
|
|
7887
|
-
content: [{
|
|
7888
|
-
type: "text",
|
|
7889
|
-
text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
|
|
7890
|
-
}],
|
|
7891
|
-
structuredContent: failure(
|
|
7892
|
-
"NOT_FOUND",
|
|
7893
|
-
"No architecture data found.",
|
|
7894
|
-
"Seed the default architecture first.",
|
|
7895
|
-
[{ tool: "architecture-admin", description: "Seed architecture", parameters: { action: "seed" } }]
|
|
7896
|
-
)
|
|
7897
|
-
};
|
|
7898
|
-
}
|
|
7899
|
-
const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
|
|
7900
|
-
const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
|
|
7901
|
-
(f) => `- **${f.name}**: ${f.data?.description ?? ""}`
|
|
7902
|
-
).join("\n") : "";
|
|
7903
|
-
const text = `# ${templateName}
|
|
7922
|
+
sections.push(
|
|
7923
|
+
"## 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."
|
|
7924
|
+
);
|
|
7925
|
+
sections.push(
|
|
7926
|
+
"## 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)."
|
|
7927
|
+
);
|
|
7928
|
+
if (collections) {
|
|
7929
|
+
const collList = collections.map((c) => {
|
|
7930
|
+
const prefix = c.icon ? `${c.icon} ` : "";
|
|
7931
|
+
return `- ${prefix}**${c.name}** (\`${c.slug}\`) \u2014 ${c.description || "no description"}`;
|
|
7932
|
+
}).join("\n");
|
|
7933
|
+
sections.push(
|
|
7934
|
+
`## Data Model (${collections.length} collections)
|
|
7935
|
+
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
7936
|
+
The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
|
|
7937
|
+
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
|
|
7904
7938
|
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
const depRule = target.data?.dependsOn ? `
|
|
7942
|
-
**Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
|
|
7943
|
-
` : "";
|
|
7944
|
-
const layerRationale = target.data?.rationale ? `
|
|
7945
|
-
> ${target.data.rationale}
|
|
7946
|
-
` : "";
|
|
7947
|
-
const nodeDetail = nodes.map((n) => {
|
|
7948
|
-
const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
|
|
7949
|
-
if (n.data?.description) lines.push(String(n.data.description));
|
|
7950
|
-
if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
|
|
7951
|
-
if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
|
|
7952
|
-
if (n.data?.rationale) lines.push(`
|
|
7953
|
-
**Why here?** ${n.data.rationale}`);
|
|
7954
|
-
return lines.join("\n");
|
|
7955
|
-
}).join("\n\n");
|
|
7956
|
-
const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
|
|
7957
|
-
return {
|
|
7958
|
-
content: [{
|
|
7959
|
-
type: "text",
|
|
7960
|
-
text: `# ${target.data?.icon ?? ""} ${target.name} Layer
|
|
7939
|
+
` + collList + "\n\nUse `collections action=list` for field schemas, `entries action=get` for full records."
|
|
7940
|
+
);
|
|
7941
|
+
} else {
|
|
7942
|
+
sections.push(
|
|
7943
|
+
"## Data Model\nCould not load collections \u2014 use `collections action=list` to browse manually."
|
|
7944
|
+
);
|
|
7945
|
+
}
|
|
7946
|
+
const rulesCount = businessRules ? `${businessRules.length} entries` : "not loaded \u2014 collection may not exist yet";
|
|
7947
|
+
sections.push(
|
|
7948
|
+
`## Business Rules
|
|
7949
|
+
Collection: \`business-rules\` (${rulesCount}).
|
|
7950
|
+
Find rules: \`entries action=search\` for text search, \`entries action=list collection=business-rules\` to browse.
|
|
7951
|
+
Check compliance: use the \`review-against-rules\` prompt (pass a domain).
|
|
7952
|
+
Draft a new rule: use the \`draft-rule-from-context\` prompt.`
|
|
7953
|
+
);
|
|
7954
|
+
const eventsCount = trackingEvents ? `${trackingEvents.length} events` : "not loaded \u2014 collection may not exist yet";
|
|
7955
|
+
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.";
|
|
7956
|
+
sections.push(
|
|
7957
|
+
`## Analytics & Tracking
|
|
7958
|
+
Event catalog: \`tracking-events\` collection (${eventsCount}).
|
|
7959
|
+
${conventionNote}
|
|
7960
|
+
Implementation: \`src/lib/analytics.ts\`. Workspace-scoped events MUST use \`withWorkspaceGroup()\`.
|
|
7961
|
+
Browse: \`entries action=list collection=tracking-events\`.`
|
|
7962
|
+
);
|
|
7963
|
+
sections.push(
|
|
7964
|
+
"## 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."
|
|
7965
|
+
);
|
|
7966
|
+
sections.push(
|
|
7967
|
+
"## 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)."
|
|
7968
|
+
);
|
|
7969
|
+
sections.push(
|
|
7970
|
+
"## 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`"
|
|
7971
|
+
);
|
|
7972
|
+
return sections.join("\n\n---\n\n");
|
|
7973
|
+
}
|
|
7974
|
+
var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
|
|
7961
7975
|
|
|
7962
|
-
|
|
7963
|
-
|
|
7976
|
+
## Core Tools
|
|
7977
|
+
| Tool | Purpose | Key params |
|
|
7978
|
+
|---|---|---|
|
|
7979
|
+
| \`orient\` | Workspace context, governance, active bets | \u2014 (call at session start) |
|
|
7980
|
+
| \`capture\` | Create entry (draft) | \`collection\`, \`name\`, \`description\`, optional \`data\` |
|
|
7981
|
+
| \`entries\` | List / get / batch / search entries | \`action\` + \`entryId\` / \`query\` / \`collection\` |
|
|
7982
|
+
| \`update-entry\` | Update fields on an entry | \`entryId\`, optional \`name\`, \`status\`, \`workflowStatus\`, \`data\`, \`changeNote\` |
|
|
7983
|
+
| \`commit-entry\` | Promote draft \u2192 SSOT | \`entryId\` |
|
|
7984
|
+
| \`graph\` | Suggest / find relations | \`action\` + \`entryId\` |
|
|
7985
|
+
| \`relations\` | Create / batch-create / delete links | \`from\`, \`to\`, \`type\` |
|
|
7986
|
+
| \`context\` | Gather related knowledge | \`entryId\` or \`task\` |
|
|
7987
|
+
| \`collections\` | List / create / update collections | \`action\` |
|
|
7988
|
+
| \`labels\` | List / create / apply / remove labels | \`action\` |
|
|
7989
|
+
| \`quality\` | Score an entry | \`entryId\` |
|
|
7990
|
+
| \`session\` | Start / close agent session | \`action\` |
|
|
7991
|
+
| \`health\` | Check / audit / whoami | \`action\` |
|
|
7992
|
+
| \`facilitate\` | Shaping workflows | \`action\`: start, respond, status |
|
|
7964
7993
|
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
)
|
|
7971
|
-
};
|
|
7972
|
-
}
|
|
7973
|
-
if (action === "flow") {
|
|
7974
|
-
if (!flow) {
|
|
7975
|
-
return validationResult("A `flow` name or entry ID is required.");
|
|
7976
|
-
}
|
|
7977
|
-
const flows = byTag(all, "flow");
|
|
7978
|
-
const target = flows.find(
|
|
7979
|
-
(f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
|
|
7980
|
-
);
|
|
7981
|
-
if (!target) {
|
|
7982
|
-
const available = flows.map((f) => `\`${f.name}\``).join(", ");
|
|
7983
|
-
return {
|
|
7984
|
-
content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }],
|
|
7985
|
-
structuredContent: failure(
|
|
7986
|
-
"NOT_FOUND",
|
|
7987
|
-
`Flow "${flow}" not found.`,
|
|
7988
|
-
`Available flows: ${available}`
|
|
7989
|
-
)
|
|
7990
|
-
};
|
|
7991
|
-
}
|
|
7992
|
-
const nodes = byTag(all, "node");
|
|
7993
|
-
const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
|
|
7994
|
-
const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
|
|
7995
|
-
const lines = [
|
|
7996
|
-
`# ${target.name}`,
|
|
7997
|
-
"",
|
|
7998
|
-
`**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
|
|
7999
|
-
"",
|
|
8000
|
-
String(target.data?.description ?? "")
|
|
8001
|
-
];
|
|
8002
|
-
if (source) {
|
|
8003
|
-
lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
|
|
8004
|
-
if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
|
|
8005
|
-
}
|
|
8006
|
-
if (dest) {
|
|
8007
|
-
lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
|
|
8008
|
-
if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
|
|
8009
|
-
}
|
|
8010
|
-
return {
|
|
8011
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
8012
|
-
structuredContent: success(
|
|
8013
|
-
`${target.name}: ${source?.name ?? "?"} \u2192 ${dest?.name ?? "?"}.`,
|
|
8014
|
-
{ flowName: target.name, entryId: target.entryId, source: source?.name, target: dest?.name }
|
|
8015
|
-
)
|
|
8016
|
-
};
|
|
8017
|
-
}
|
|
8018
|
-
return unknownAction(action, ["show", "explore", "flow"]);
|
|
8019
|
-
})
|
|
8020
|
-
);
|
|
8021
|
-
const archAdminTool = server.registerTool(
|
|
8022
|
-
"architecture-admin",
|
|
8023
|
-
{
|
|
8024
|
-
title: "Architecture Admin",
|
|
8025
|
-
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.",
|
|
8026
|
-
inputSchema: architectureAdminSchema,
|
|
8027
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
8028
|
-
},
|
|
8029
|
-
withEnvelope(async ({ action }) => {
|
|
8030
|
-
if (action === "seed") {
|
|
8031
|
-
await ensureCollection();
|
|
8032
|
-
const existing = await listArchEntries();
|
|
8033
|
-
const existingIds = new Set(existing.map((e) => e.entryId));
|
|
8034
|
-
let created = 0;
|
|
8035
|
-
let updated = 0;
|
|
8036
|
-
let unchanged = 0;
|
|
8037
|
-
const allSeeds = [
|
|
8038
|
-
{ ...SEED_TEMPLATE, order: 0, status: "active" },
|
|
8039
|
-
...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
|
|
8040
|
-
...SEED_NODES.map((n) => ({ ...n, status: "active" })),
|
|
8041
|
-
...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
|
|
8042
|
-
];
|
|
8043
|
-
for (const seed of allSeeds) {
|
|
8044
|
-
if (existingIds.has(seed.entryId)) {
|
|
8045
|
-
const existingEntry = existing.find((e) => e.entryId === seed.entryId);
|
|
8046
|
-
const existingData = existingEntry?.data ?? {};
|
|
8047
|
-
const seedData = seed.data;
|
|
8048
|
-
const hasChanges = Object.keys(seedData).some(
|
|
8049
|
-
(k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
|
|
8050
|
-
);
|
|
8051
|
-
if (hasChanges) {
|
|
8052
|
-
const mergedData = { ...existingData, ...seedData };
|
|
8053
|
-
await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
8054
|
-
updated++;
|
|
8055
|
-
} else {
|
|
8056
|
-
unchanged++;
|
|
8057
|
-
}
|
|
8058
|
-
continue;
|
|
8059
|
-
}
|
|
8060
|
-
await mcpMutation("chain.createEntry", {
|
|
8061
|
-
collectionSlug: COLLECTION_SLUG,
|
|
8062
|
-
entryId: seed.entryId,
|
|
8063
|
-
name: seed.name,
|
|
8064
|
-
status: seed.status,
|
|
8065
|
-
data: seed.data,
|
|
8066
|
-
order: seed.order ?? 0
|
|
8067
|
-
});
|
|
8068
|
-
created++;
|
|
8069
|
-
}
|
|
8070
|
-
return {
|
|
8071
|
-
content: [{
|
|
8072
|
-
type: "text",
|
|
8073
|
-
text: `# Architecture Seeded
|
|
7994
|
+
## Collection Prefixes
|
|
7995
|
+
GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
|
|
7996
|
+
DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
|
|
7997
|
+
BET (bets), INS (insights), ARCH (architecture), CIR (circles),
|
|
7998
|
+
ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
|
|
8074
7999
|
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8000
|
+
## Valid Relation Types (21)
|
|
8001
|
+
informs, governs, surfaces_tension_in, defines_term_for, belongs_to,
|
|
8002
|
+
references, related_to, fills_slot, commits_to, informed_by, depends_on,
|
|
8003
|
+
conflicts_with, confused_with, replaces, part_of, constrains,
|
|
8004
|
+
governed_by, alternative_to, has_proposal, requests_promotion_of, resolves
|
|
8078
8005
|
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
await ensureCollection();
|
|
8104
|
-
const all = await listArchEntries();
|
|
8105
|
-
const layers = byTag(all, "layer");
|
|
8106
|
-
const nodes = byTag(all, "node");
|
|
8107
|
-
const result = scanDependencies(projectRoot, layers, nodes);
|
|
8108
|
-
return {
|
|
8109
|
-
content: [{ type: "text", text: formatScanReport(result) }],
|
|
8110
|
-
structuredContent: success(
|
|
8111
|
-
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.`,
|
|
8112
|
-
{
|
|
8113
|
-
violations: result.violations.length,
|
|
8114
|
-
filesScanned: result.filesScanned,
|
|
8115
|
-
importsChecked: result.importsChecked,
|
|
8116
|
-
unmappedImports: result.unmappedImports
|
|
8117
|
-
}
|
|
8118
|
-
)
|
|
8119
|
-
};
|
|
8120
|
-
}
|
|
8121
|
-
return unknownAction(action, ["seed", "check"]);
|
|
8006
|
+
## Lifecycle Status
|
|
8007
|
+
All entries: \`draft\` | \`active\` | \`deprecated\` | \`archived\`
|
|
8008
|
+
|
|
8009
|
+
## Workflow Status (per collection)
|
|
8010
|
+
- **tensions:** open \u2192 processing \u2192 decided \u2192 closed
|
|
8011
|
+
- **decisions:** pending \u2192 decided
|
|
8012
|
+
- **bets:** shaped \u2192 bet \u2192 building \u2192 shipped
|
|
8013
|
+
- **business-rules:** active | conflict | review
|
|
8014
|
+
|
|
8015
|
+
## Key Patterns
|
|
8016
|
+
- **Capture flow:** \`capture\` \u2192 \`graph action=suggest\` \u2192 \`relations action=batch-create\` \u2192 \`commit-entry\`
|
|
8017
|
+
- Use \`workflowStatus\` (not \`status\`) for domain workflow state
|
|
8018
|
+
- \`data\` param is merged (not replaced) \u2014 safe for partial updates
|
|
8019
|
+
`;
|
|
8020
|
+
function registerResources(server) {
|
|
8021
|
+
server.resource(
|
|
8022
|
+
"agent-cheatsheet",
|
|
8023
|
+
"productbrain://agent-cheatsheet",
|
|
8024
|
+
async (uri) => ({
|
|
8025
|
+
contents: [{
|
|
8026
|
+
uri: uri.href,
|
|
8027
|
+
text: AGENT_CHEATSHEET,
|
|
8028
|
+
mimeType: "text/markdown"
|
|
8029
|
+
}]
|
|
8122
8030
|
})
|
|
8123
8031
|
);
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
const
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
unmapped++;
|
|
8168
|
-
continue;
|
|
8169
|
-
}
|
|
8170
|
-
const targetNode = findNodeByPath(resolved, nodePathPrefixes);
|
|
8171
|
-
if (!targetNode) {
|
|
8172
|
-
unmapped++;
|
|
8173
|
-
continue;
|
|
8174
|
-
}
|
|
8175
|
-
const targetLayerRef = String(targetNode.data?.layerRef ?? "");
|
|
8176
|
-
const targetLayer = layerMap.get(targetLayerRef);
|
|
8177
|
-
if (!targetLayer) continue;
|
|
8178
|
-
if (targetLayerRef === layerRef) continue;
|
|
8179
|
-
const allowed = allowedDeps.get(layerRef);
|
|
8180
|
-
if (allowed && !allowed.has(targetLayerRef)) {
|
|
8181
|
-
const v = {
|
|
8182
|
-
sourceNode: node.name,
|
|
8183
|
-
sourceLayer: layer.name,
|
|
8184
|
-
sourceFile: relFile,
|
|
8185
|
-
importPath: imp,
|
|
8186
|
-
targetNode: targetNode.name,
|
|
8187
|
-
targetLayer: targetLayer.name,
|
|
8188
|
-
rule: `${layer.name} cannot import from ${targetLayer.name}`
|
|
8189
|
-
};
|
|
8190
|
-
violations.push(v);
|
|
8191
|
-
nodeViolations.push(v);
|
|
8192
|
-
}
|
|
8032
|
+
server.resource(
|
|
8033
|
+
"chain-orientation",
|
|
8034
|
+
"productbrain://orientation",
|
|
8035
|
+
async (uri) => {
|
|
8036
|
+
const [collectionsResult, eventsResult, standardsResult, rulesResult, principlesResult] = await Promise.allSettled([
|
|
8037
|
+
mcpQuery("chain.listCollections"),
|
|
8038
|
+
mcpQuery("chain.listEntries", { collectionSlug: "tracking-events" }),
|
|
8039
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" }),
|
|
8040
|
+
mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
|
|
8041
|
+
mcpQuery("chain.listEntries", { collectionSlug: "principles" })
|
|
8042
|
+
]);
|
|
8043
|
+
const collections = collectionsResult.status === "fulfilled" ? collectionsResult.value : null;
|
|
8044
|
+
const trackingEvents = eventsResult.status === "fulfilled" ? eventsResult.value : null;
|
|
8045
|
+
const standards = standardsResult.status === "fulfilled" ? standardsResult.value : null;
|
|
8046
|
+
const businessRules = rulesResult.status === "fulfilled" ? rulesResult.value : null;
|
|
8047
|
+
const principles = principlesResult.status === "fulfilled" ? principlesResult.value : null;
|
|
8048
|
+
return {
|
|
8049
|
+
contents: [{
|
|
8050
|
+
uri: uri.href,
|
|
8051
|
+
text: buildOrientationMarkdown(collections, trackingEvents, standards, businessRules, principles),
|
|
8052
|
+
mimeType: "text/markdown"
|
|
8053
|
+
}]
|
|
8054
|
+
};
|
|
8055
|
+
}
|
|
8056
|
+
);
|
|
8057
|
+
server.resource(
|
|
8058
|
+
"chain-terminology",
|
|
8059
|
+
"productbrain://terminology",
|
|
8060
|
+
async (uri) => {
|
|
8061
|
+
const [glossaryResult, standardsResult] = await Promise.allSettled([
|
|
8062
|
+
mcpQuery("chain.listEntries", { collectionSlug: "glossary" }),
|
|
8063
|
+
mcpQuery("chain.listEntries", { collectionSlug: "standards" })
|
|
8064
|
+
]);
|
|
8065
|
+
const lines = ["# Product Brain \u2014 Terminology"];
|
|
8066
|
+
if (glossaryResult.status === "fulfilled") {
|
|
8067
|
+
const glossary = glossaryResult.value ?? [];
|
|
8068
|
+
if (glossary.length > 0) {
|
|
8069
|
+
const terms = glossary.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
|
|
8070
|
+
lines.push(`## Glossary (${glossary.length} terms)
|
|
8071
|
+
|
|
8072
|
+
${terms}`);
|
|
8073
|
+
} else {
|
|
8074
|
+
lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
|
|
8193
8075
|
}
|
|
8076
|
+
} else {
|
|
8077
|
+
lines.push("## Glossary\n\nCould not load glossary \u2014 use `entries action=list collection=glossary` to browse manually.");
|
|
8194
8078
|
}
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
}
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
if (deps !== "none") {
|
|
8208
|
-
for (const dep of deps.split(",").map((d) => d.trim().toLowerCase())) {
|
|
8209
|
-
const id = nameToId.get(dep);
|
|
8210
|
-
if (id) set.add(id);
|
|
8079
|
+
if (standardsResult.status === "fulfilled") {
|
|
8080
|
+
const standards = standardsResult.value ?? [];
|
|
8081
|
+
if (standards.length > 0) {
|
|
8082
|
+
const stds = standards.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${s.data?.description ?? ""}`).join("\n");
|
|
8083
|
+
lines.push(`## Standards (${standards.length} entries)
|
|
8084
|
+
|
|
8085
|
+
${stds}`);
|
|
8086
|
+
} else {
|
|
8087
|
+
lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
|
|
8088
|
+
}
|
|
8089
|
+
} else {
|
|
8090
|
+
lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
|
|
8211
8091
|
}
|
|
8092
|
+
return {
|
|
8093
|
+
contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
|
|
8094
|
+
};
|
|
8212
8095
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
}
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
}
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
if (stat.isFile()) {
|
|
8236
|
-
return isScannableFile(absPath) ? [absPath] : [];
|
|
8237
|
-
}
|
|
8238
|
-
if (!stat.isDirectory()) return [];
|
|
8239
|
-
const results = [];
|
|
8240
|
-
const walk = (dir) => {
|
|
8241
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
8242
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8243
|
-
const full = resolve2(dir, entry.name);
|
|
8244
|
-
if (entry.isDirectory()) walk(full);
|
|
8245
|
-
else if (isScannableFile(full)) results.push(full);
|
|
8096
|
+
);
|
|
8097
|
+
server.resource(
|
|
8098
|
+
"chain-collections",
|
|
8099
|
+
"productbrain://collections",
|
|
8100
|
+
async (uri) => {
|
|
8101
|
+
const collections = await mcpQuery("chain.listCollections") ?? [];
|
|
8102
|
+
if (collections.length === 0) {
|
|
8103
|
+
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" }] };
|
|
8104
|
+
}
|
|
8105
|
+
const formatted = collections.map((c) => {
|
|
8106
|
+
const fieldList = (c.fields ?? []).map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
|
|
8107
|
+
return `## ${c.icon ?? ""} ${c.name} (\`${c.slug}\`)
|
|
8108
|
+
${c.description || ""}
|
|
8109
|
+
|
|
8110
|
+
**Fields:**
|
|
8111
|
+
${fieldList}`;
|
|
8112
|
+
}).join("\n\n---\n\n");
|
|
8113
|
+
return {
|
|
8114
|
+
contents: [{ uri: uri.href, text: `# Knowledge Collections (${collections.length})
|
|
8115
|
+
|
|
8116
|
+
${formatted}`, mimeType: "text/markdown" }]
|
|
8117
|
+
};
|
|
8246
8118
|
}
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
}
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8119
|
+
);
|
|
8120
|
+
server.resource(
|
|
8121
|
+
"chain-collection-entries",
|
|
8122
|
+
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
8123
|
+
list: async () => {
|
|
8124
|
+
const collections = await mcpQuery("chain.listCollections") ?? [];
|
|
8125
|
+
return {
|
|
8126
|
+
resources: collections.map((c) => ({
|
|
8127
|
+
uri: `productbrain://${c.slug}/entries`,
|
|
8128
|
+
name: `${c.icon ?? ""} ${c.name}`.trim()
|
|
8129
|
+
}))
|
|
8130
|
+
};
|
|
8131
|
+
}
|
|
8132
|
+
}),
|
|
8133
|
+
async (uri, { slug }) => {
|
|
8134
|
+
const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug }) ?? [];
|
|
8135
|
+
const formatted = entries.map(formatEntryMarkdown).join("\n\n---\n\n");
|
|
8136
|
+
return {
|
|
8137
|
+
contents: [{
|
|
8138
|
+
uri: uri.href,
|
|
8139
|
+
text: formatted || "No entries in this collection.",
|
|
8140
|
+
mimeType: "text/markdown"
|
|
8141
|
+
}]
|
|
8142
|
+
};
|
|
8262
8143
|
}
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
}
|
|
8292
|
-
|
|
8293
|
-
const normalized = normalize(filePath);
|
|
8294
|
-
for (const { prefix, node } of prefixes) {
|
|
8295
|
-
if (normalized.startsWith(prefix)) return node;
|
|
8296
|
-
}
|
|
8297
|
-
return null;
|
|
8298
|
-
}
|
|
8299
|
-
function formatScanReport(result) {
|
|
8300
|
-
const lines = [];
|
|
8301
|
-
if (result.violations.length === 0) {
|
|
8302
|
-
lines.push(
|
|
8303
|
-
`# Architecture Health Check Passed`,
|
|
8304
|
-
"",
|
|
8305
|
-
`**0 violations** across ${result.filesScanned} files (${result.importsChecked} imports checked, ${result.unmappedImports} unmapped).`,
|
|
8306
|
-
"",
|
|
8307
|
-
"All imports respect the layer dependency rules."
|
|
8308
|
-
);
|
|
8309
|
-
} else {
|
|
8310
|
-
lines.push(
|
|
8311
|
-
`# Architecture Health Check \u2014 ${result.violations.length} Violation${result.violations.length === 1 ? "" : "s"}`,
|
|
8312
|
-
"",
|
|
8313
|
-
`Scanned ${result.filesScanned} files, checked ${result.importsChecked} imports, found **${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}** (${result.unmappedImports} unmapped).`,
|
|
8314
|
-
""
|
|
8315
|
-
);
|
|
8316
|
-
const byNode = /* @__PURE__ */ new Map();
|
|
8317
|
-
for (const v of result.violations) {
|
|
8318
|
-
if (!byNode.has(v.sourceNode)) byNode.set(v.sourceNode, []);
|
|
8319
|
-
byNode.get(v.sourceNode).push(v);
|
|
8144
|
+
);
|
|
8145
|
+
server.resource(
|
|
8146
|
+
"chain-labels",
|
|
8147
|
+
"productbrain://labels",
|
|
8148
|
+
async (uri) => {
|
|
8149
|
+
const labels = await mcpQuery("chain.listLabels") ?? [];
|
|
8150
|
+
if (labels.length === 0) {
|
|
8151
|
+
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
8152
|
+
}
|
|
8153
|
+
const groups = labels.filter((l) => l.isGroup);
|
|
8154
|
+
const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
|
|
8155
|
+
const children = (parentId) => labels.filter((l) => l.parentId === parentId);
|
|
8156
|
+
const lines = [];
|
|
8157
|
+
for (const group of groups) {
|
|
8158
|
+
lines.push(`## ${group.name}`);
|
|
8159
|
+
for (const child of children(group._id)) {
|
|
8160
|
+
lines.push(`- \`${child.slug}\` ${child.name}${child.color ? ` (${child.color})` : ""}`);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
if (ungrouped.length > 0) {
|
|
8164
|
+
lines.push("## Ungrouped");
|
|
8165
|
+
for (const l of ungrouped) {
|
|
8166
|
+
lines.push(`- \`${l.slug}\` ${l.name}${l.color ? ` (${l.color})` : ""}`);
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
return {
|
|
8170
|
+
contents: [{ uri: uri.href, text: `# Workspace Labels (${labels.length})
|
|
8171
|
+
|
|
8172
|
+
${lines.join("\n")}`, mimeType: "text/markdown" }]
|
|
8173
|
+
};
|
|
8320
8174
|
}
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8175
|
+
);
|
|
8176
|
+
server.resource(
|
|
8177
|
+
"chain-entry",
|
|
8178
|
+
new ResourceTemplate("productbrain://entries/{entryId}", {
|
|
8179
|
+
complete: {
|
|
8180
|
+
entryId: async (value) => {
|
|
8181
|
+
if (!value || value.length < 1) return [];
|
|
8182
|
+
const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
|
|
8183
|
+
return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
|
|
8184
|
+
}
|
|
8325
8185
|
}
|
|
8326
|
-
|
|
8186
|
+
}),
|
|
8187
|
+
async (uri, { entryId }) => {
|
|
8188
|
+
const [entry, collections] = await Promise.all([
|
|
8189
|
+
mcpQuery("chain.getEntry", { entryId }),
|
|
8190
|
+
mcpQuery("chain.listCollections")
|
|
8191
|
+
]);
|
|
8192
|
+
if (!entry) {
|
|
8193
|
+
return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
|
|
8194
|
+
}
|
|
8195
|
+
const collectionMap = new Map((collections ?? []).map((c) => [c._id, c]));
|
|
8196
|
+
const col = collectionMap.get(entry.collectionId);
|
|
8197
|
+
const collLabel = col?.name ?? entry.collectionName ?? entry.collectionSlug ?? "unknown";
|
|
8198
|
+
const lines = [
|
|
8199
|
+
`# ${entry.entryId}: ${entry.name}`,
|
|
8200
|
+
`**Collection:** ${collLabel}`,
|
|
8201
|
+
`**Status:** ${entry.status}`,
|
|
8202
|
+
""
|
|
8203
|
+
];
|
|
8204
|
+
if (entry.data && typeof entry.data === "object") {
|
|
8205
|
+
lines.push("## Data");
|
|
8206
|
+
for (const [key, val] of Object.entries(entry.data)) {
|
|
8207
|
+
if (val && key !== "rawData") {
|
|
8208
|
+
const str = typeof val === "string" ? val : JSON.stringify(val, null, 2);
|
|
8209
|
+
lines.push(`**${key}:** ${str}`);
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
lines.push("");
|
|
8213
|
+
}
|
|
8214
|
+
if (entry.relations && entry.relations.length > 0) {
|
|
8215
|
+
lines.push("## Relations");
|
|
8216
|
+
for (const rel of entry.relations) {
|
|
8217
|
+
const arrow = rel.direction === "outgoing" ? "\u2192" : "\u2190";
|
|
8218
|
+
const id = rel.otherEntryId ?? "";
|
|
8219
|
+
const name = rel.otherName ?? "(unknown)";
|
|
8220
|
+
lines.push(`- ${arrow} **${rel.type}** ${id}: ${name}`);
|
|
8221
|
+
}
|
|
8222
|
+
lines.push("");
|
|
8223
|
+
}
|
|
8224
|
+
if (entry.labels && entry.labels.length > 0) {
|
|
8225
|
+
lines.push(`## Labels
|
|
8226
|
+
${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
|
|
8227
|
+
}
|
|
8228
|
+
return {
|
|
8229
|
+
contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
|
|
8230
|
+
};
|
|
8327
8231
|
}
|
|
8328
|
-
}
|
|
8329
|
-
lines.push("---", "");
|
|
8330
|
-
const nodeEntries = [...result.nodeResults.entries()];
|
|
8331
|
-
const cleanCount = nodeEntries.filter(([, r]) => r.violations.length === 0 && r.filesScanned > 0).length;
|
|
8332
|
-
const dirtyCount = nodeEntries.filter(([, r]) => r.violations.length > 0).length;
|
|
8333
|
-
const emptyCount = nodeEntries.filter(([, r]) => r.filesScanned === 0).length;
|
|
8334
|
-
lines.push(
|
|
8335
|
-
`**Summary:** ${cleanCount} clean nodes, ${dirtyCount} with violations, ${emptyCount} with no files.`
|
|
8336
8232
|
);
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
"chain.listEntryRelations": "read",
|
|
8347
|
-
"chain.listEntriesByLabel": "read",
|
|
8348
|
-
"chain.searchEntries": "search",
|
|
8349
|
-
"chain.createEntry": "write",
|
|
8350
|
-
"chain.updateEntry": "write",
|
|
8351
|
-
"chain.createEntryRelation": "write",
|
|
8352
|
-
"chain.applyLabel": "label",
|
|
8353
|
-
"chain.removeLabel": "label",
|
|
8354
|
-
"chain.createLabel": "label",
|
|
8355
|
-
"chain.updateLabel": "label",
|
|
8356
|
-
"chain.deleteLabel": "label",
|
|
8357
|
-
"chain.createCollection": "write",
|
|
8358
|
-
"chain.updateCollection": "write",
|
|
8359
|
-
"chain.listCollections": "meta",
|
|
8360
|
-
"chain.getCollection": "meta",
|
|
8361
|
-
"chain.listLabels": "meta",
|
|
8362
|
-
"resolveWorkspace": "meta"
|
|
8363
|
-
};
|
|
8364
|
-
function categorize(fn) {
|
|
8365
|
-
return CALL_CATEGORIES[fn] ?? "meta";
|
|
8366
|
-
}
|
|
8367
|
-
function formatDuration(ms) {
|
|
8368
|
-
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
8369
|
-
const mins = Math.floor(ms / 6e4);
|
|
8370
|
-
const secs = Math.round(ms % 6e4 / 1e3);
|
|
8371
|
-
return `${mins}m ${secs}s`;
|
|
8372
|
-
}
|
|
8373
|
-
function buildSessionSummary(log) {
|
|
8374
|
-
if (log.length === 0) return "";
|
|
8375
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
8376
|
-
let errorCount = 0;
|
|
8377
|
-
let writeCreates = 0;
|
|
8378
|
-
let writeUpdates = 0;
|
|
8379
|
-
for (const entry of log) {
|
|
8380
|
-
const cat = categorize(entry.fn);
|
|
8381
|
-
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
8382
|
-
const fnCounts = byCategory.get(cat);
|
|
8383
|
-
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
8384
|
-
if (entry.status === "error") errorCount++;
|
|
8385
|
-
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
8386
|
-
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
8387
|
-
}
|
|
8388
|
-
const firstTs = new Date(log[0].ts).getTime();
|
|
8389
|
-
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
8390
|
-
const duration = formatDuration(lastTs - firstTs);
|
|
8391
|
-
const lines = [`# Session Summary (${duration})
|
|
8392
|
-
`];
|
|
8393
|
-
const categoryLabels = [
|
|
8394
|
-
["read", "Reads"],
|
|
8395
|
-
["search", "Searches"],
|
|
8396
|
-
["write", "Writes"],
|
|
8397
|
-
["label", "Labels"],
|
|
8398
|
-
["meta", "Meta"]
|
|
8399
|
-
];
|
|
8400
|
-
for (const [cat, label] of categoryLabels) {
|
|
8401
|
-
const fnCounts = byCategory.get(cat);
|
|
8402
|
-
if (!fnCounts || fnCounts.size === 0) continue;
|
|
8403
|
-
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
8404
|
-
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
8405
|
-
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
8406
|
-
}
|
|
8407
|
-
lines.push(`- **Errors:** ${errorCount}`);
|
|
8408
|
-
if (writeCreates > 0 || writeUpdates > 0) {
|
|
8409
|
-
lines.push("");
|
|
8410
|
-
lines.push("## Knowledge Contribution");
|
|
8411
|
-
if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
|
|
8412
|
-
if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
|
|
8413
|
-
}
|
|
8414
|
-
return lines.join("\n");
|
|
8415
|
-
}
|
|
8416
|
-
function extractSessionEntryIds(priorSessions) {
|
|
8417
|
-
const allSeen = /* @__PURE__ */ new Set();
|
|
8418
|
-
const all = [];
|
|
8419
|
-
let lastSessionOnly = [];
|
|
8420
|
-
for (let i = 0; i < priorSessions.length; i++) {
|
|
8421
|
-
const s = priorSessions[i];
|
|
8422
|
-
const ids = [...s.entriesCreated ?? [], ...s.entriesModified ?? []].filter((id) => id);
|
|
8423
|
-
if (i === 0) {
|
|
8424
|
-
lastSessionOnly = [...new Set(ids)].slice(0, 5);
|
|
8425
|
-
}
|
|
8426
|
-
for (const id of ids) {
|
|
8427
|
-
if (!allSeen.has(id)) {
|
|
8428
|
-
allSeen.add(id);
|
|
8429
|
-
all.push(id);
|
|
8430
|
-
if (all.length >= 10) break;
|
|
8233
|
+
server.resource(
|
|
8234
|
+
"chain-context",
|
|
8235
|
+
new ResourceTemplate("productbrain://context/{entryId}", {
|
|
8236
|
+
complete: {
|
|
8237
|
+
entryId: async (value) => {
|
|
8238
|
+
if (!value || value.length < 1) return [];
|
|
8239
|
+
const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
|
|
8240
|
+
return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
|
|
8241
|
+
}
|
|
8431
8242
|
}
|
|
8243
|
+
}),
|
|
8244
|
+
async (uri, { entryId }) => {
|
|
8245
|
+
const result = await mcpQuery("chain.gatherContext", {
|
|
8246
|
+
entryId,
|
|
8247
|
+
maxHops: 2
|
|
8248
|
+
});
|
|
8249
|
+
if (!result?.root) {
|
|
8250
|
+
return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
|
|
8251
|
+
}
|
|
8252
|
+
const lines = [
|
|
8253
|
+
`# Context: ${result.root.entryId}: ${result.root.name}`,
|
|
8254
|
+
`_${result.totalRelations} related entries (${result.hopsTraversed} hops)_`,
|
|
8255
|
+
""
|
|
8256
|
+
];
|
|
8257
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
8258
|
+
for (const entry of result.related ?? []) {
|
|
8259
|
+
const key = entry.collectionName;
|
|
8260
|
+
if (!byCollection.has(key)) byCollection.set(key, []);
|
|
8261
|
+
byCollection.get(key).push(entry);
|
|
8262
|
+
}
|
|
8263
|
+
for (const [collName, entries] of byCollection) {
|
|
8264
|
+
lines.push(`## ${collName} (${entries.length})`);
|
|
8265
|
+
for (const e of entries) {
|
|
8266
|
+
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
8267
|
+
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
8268
|
+
const id = e.entryId ? `${e.entryId}: ` : "";
|
|
8269
|
+
lines.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}`);
|
|
8270
|
+
}
|
|
8271
|
+
lines.push("");
|
|
8272
|
+
}
|
|
8273
|
+
return {
|
|
8274
|
+
contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
|
|
8275
|
+
};
|
|
8432
8276
|
}
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
}
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
collections = await mcpQuery("chain.listCollections");
|
|
8449
|
-
} catch (e) {
|
|
8450
|
-
errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
8451
|
-
}
|
|
8452
|
-
let totalEntries = 0;
|
|
8453
|
-
if (collections.length > 0) {
|
|
8454
|
-
try {
|
|
8455
|
-
const entries = await mcpQuery("chain.listEntries", {});
|
|
8456
|
-
totalEntries = entries.length;
|
|
8457
|
-
} catch (e) {
|
|
8458
|
-
errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
8459
|
-
}
|
|
8460
|
-
}
|
|
8461
|
-
let wsCtx = null;
|
|
8462
|
-
try {
|
|
8463
|
-
wsCtx = await getWorkspaceContext();
|
|
8464
|
-
} catch {
|
|
8465
|
-
}
|
|
8466
|
-
const durationMs = Date.now() - start;
|
|
8467
|
-
const healthy = errors.length === 0;
|
|
8468
|
-
const lines = [
|
|
8469
|
-
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
8470
|
-
"",
|
|
8471
|
-
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
8472
|
-
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
8473
|
-
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
8474
|
-
`**Collections:** ${collections.length}`,
|
|
8475
|
-
`**Entries:** ${totalEntries}`,
|
|
8476
|
-
`**Latency:** ${durationMs}ms`
|
|
8477
|
-
];
|
|
8478
|
-
if (errors.length > 0) {
|
|
8479
|
-
lines.push("", "## Errors");
|
|
8480
|
-
for (const err of errors) {
|
|
8481
|
-
lines.push(`- ${err}`);
|
|
8482
|
-
}
|
|
8483
|
-
}
|
|
8484
|
-
const healthData = {
|
|
8485
|
-
healthy,
|
|
8486
|
-
collections: collections.length,
|
|
8487
|
-
entries: totalEntries,
|
|
8488
|
-
latencyMs: durationMs,
|
|
8489
|
-
workspace: workspaceId ?? "unresolved"
|
|
8490
|
-
};
|
|
8491
|
-
return {
|
|
8492
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
8493
|
-
structuredContent: success(
|
|
8494
|
-
healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
|
|
8495
|
-
healthData
|
|
8496
|
-
)
|
|
8497
|
-
};
|
|
8498
|
-
}
|
|
8499
|
-
async function handleWhoami() {
|
|
8500
|
-
const ctx = await getWorkspaceContext();
|
|
8501
|
-
const sessionId = getAgentSessionId();
|
|
8502
|
-
const scope = getApiKeyScope();
|
|
8503
|
-
const oriented = isSessionOriented();
|
|
8504
|
-
const lines = [
|
|
8505
|
-
`# Session Identity`,
|
|
8506
|
-
"",
|
|
8507
|
-
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
8508
|
-
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
8509
|
-
`**Workspace Name:** ${ctx.workspaceName}`
|
|
8510
|
-
];
|
|
8511
|
-
return {
|
|
8512
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
8513
|
-
structuredContent: success(
|
|
8514
|
-
`Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
|
|
8515
|
-
{ workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
|
|
8516
|
-
)
|
|
8517
|
-
};
|
|
8518
|
-
}
|
|
8519
|
-
var STAGE_LABELS = {
|
|
8520
|
-
blank: "Blank",
|
|
8521
|
-
seeded: "Seeded",
|
|
8522
|
-
grounded: "Grounded",
|
|
8523
|
-
connected: "Connected"
|
|
8524
|
-
};
|
|
8525
|
-
var STAGE_DESCRIPTIONS = {
|
|
8526
|
-
blank: "No knowledge captured yet.",
|
|
8527
|
-
seeded: "Early knowledge is in place \u2014 keep building.",
|
|
8528
|
-
grounded: "Solid foundations \u2014 a few gaps remain.",
|
|
8529
|
-
connected: "Well-connected knowledge graph \u2014 your Brain is useful."
|
|
8530
|
-
};
|
|
8531
|
-
async function handleWorkspaceStatus() {
|
|
8532
|
-
const result = await mcpQuery("chain.workspaceReadiness");
|
|
8533
|
-
const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
|
|
8534
|
-
const scoringVersion = result.scoringVersion ?? "v1";
|
|
8535
|
-
const stage = result.stage ?? "seeded";
|
|
8536
|
-
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
|
8537
|
-
const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
|
|
8538
|
-
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
8539
|
-
const lines = [
|
|
8540
|
-
`# Brain Status: ${stageLabel}`,
|
|
8541
|
-
`_${stageDescription}_`,
|
|
8542
|
-
"",
|
|
8543
|
-
`${scoreBar} ${stageLabel} \xB7 ${score}%`,
|
|
8544
|
-
`**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
|
|
8545
|
-
"",
|
|
8546
|
-
"## Stats",
|
|
8547
|
-
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
8548
|
-
`- **Relations:** ${stats.totalRelations}`,
|
|
8549
|
-
`- **Collections:** ${stats.collectionCount}`,
|
|
8550
|
-
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
8551
|
-
""
|
|
8552
|
-
];
|
|
8553
|
-
if (gaps.length > 0) {
|
|
8554
|
-
lines.push("## Gaps");
|
|
8555
|
-
for (const gap of gaps) {
|
|
8556
|
-
const action = gap.capabilityGuidance ?? gap.guidance;
|
|
8557
|
-
lines.push(`- [ ] **${gap.label}**`);
|
|
8558
|
-
lines.push(` _${action}_`);
|
|
8559
|
-
}
|
|
8560
|
-
lines.push("");
|
|
8561
|
-
}
|
|
8562
|
-
const passed = checks.filter((c) => c.passed);
|
|
8563
|
-
if (passed.length > 0) {
|
|
8564
|
-
lines.push("## Passing checks");
|
|
8565
|
-
for (const check of passed) {
|
|
8566
|
-
lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
|
|
8567
|
-
}
|
|
8568
|
-
}
|
|
8569
|
-
const statusData = {
|
|
8570
|
-
stage,
|
|
8571
|
-
scoringVersion,
|
|
8572
|
-
readinessScore: score,
|
|
8573
|
-
activeEntries: stats.activeCount,
|
|
8574
|
-
totalRelations: stats.totalRelations,
|
|
8575
|
-
orphanedEntries: stats.orphanedCount,
|
|
8576
|
-
gaps: gaps.map((g) => ({
|
|
8577
|
-
id: g.id,
|
|
8578
|
-
label: g.label,
|
|
8579
|
-
guidance: g.capabilityGuidance ?? g.guidance
|
|
8580
|
-
}))
|
|
8581
|
-
};
|
|
8582
|
-
return {
|
|
8583
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
8584
|
-
structuredContent: success(
|
|
8585
|
-
`Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
|
|
8586
|
-
statusData
|
|
8587
|
-
)
|
|
8588
|
-
};
|
|
8589
|
-
}
|
|
8590
|
-
async function handleAudit(limit) {
|
|
8591
|
-
const log = getAuditLog();
|
|
8592
|
-
const recent = log.slice(-limit);
|
|
8593
|
-
if (recent.length === 0) {
|
|
8594
|
-
return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
|
|
8595
|
-
}
|
|
8596
|
-
const summary = buildSessionSummary(log);
|
|
8597
|
-
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
8598
|
-
`];
|
|
8599
|
-
for (const entry of recent) {
|
|
8600
|
-
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
8601
|
-
const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
8602
|
-
const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
|
|
8603
|
-
logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
|
|
8604
|
-
}
|
|
8605
|
-
const auditData = {
|
|
8606
|
-
totalCalls: log.length,
|
|
8607
|
-
calls: recent.map((entry) => ({
|
|
8608
|
-
tool: entry.fn,
|
|
8609
|
-
...entry.toolContext?.action && { action: entry.toolContext.action },
|
|
8610
|
-
timestamp: entry.ts,
|
|
8611
|
-
...entry.durationMs != null && { durationMs: entry.durationMs }
|
|
8612
|
-
}))
|
|
8613
|
-
};
|
|
8614
|
-
return {
|
|
8615
|
-
content: [{ type: "text", text: `${summary}
|
|
8616
|
-
|
|
8617
|
-
---
|
|
8618
|
-
|
|
8619
|
-
${logLines.join("\n")}` }],
|
|
8620
|
-
structuredContent: success(
|
|
8621
|
-
`Audit: ${log.length} total calls, showing last ${recent.length}.`,
|
|
8622
|
-
auditData
|
|
8623
|
-
)
|
|
8624
|
-
};
|
|
8625
|
-
}
|
|
8626
|
-
var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
|
|
8627
|
-
var healthSchema = z20.object({
|
|
8628
|
-
action: z20.enum(HEALTH_ACTIONS).describe(
|
|
8629
|
-
"'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
|
|
8630
|
-
),
|
|
8631
|
-
limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
|
|
8632
|
-
});
|
|
8633
|
-
var orientSchema = z20.object({
|
|
8634
|
-
mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
|
|
8635
|
-
task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
|
|
8636
|
-
});
|
|
8637
|
-
var healthCheckOutputSchema = z20.object({
|
|
8638
|
-
healthy: z20.boolean(),
|
|
8639
|
-
collections: z20.number(),
|
|
8640
|
-
entries: z20.number(),
|
|
8641
|
-
latencyMs: z20.number(),
|
|
8642
|
-
workspace: z20.string()
|
|
8643
|
-
});
|
|
8644
|
-
var healthStatusOutputSchema = z20.object({
|
|
8645
|
-
// Optional with defaults — older backends that don't return these fields won't fail.
|
|
8646
|
-
stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
|
|
8647
|
-
scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
|
|
8648
|
-
readinessScore: z20.number(),
|
|
8649
|
-
activeEntries: z20.number(),
|
|
8650
|
-
totalRelations: z20.number(),
|
|
8651
|
-
orphanedEntries: z20.number(),
|
|
8652
|
-
gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() }))
|
|
8653
|
-
});
|
|
8654
|
-
var healthAuditOutputSchema = z20.object({
|
|
8655
|
-
totalCalls: z20.number(),
|
|
8656
|
-
calls: z20.array(z20.object({
|
|
8657
|
-
tool: z20.string(),
|
|
8658
|
-
action: z20.string().optional(),
|
|
8659
|
-
timestamp: z20.string(),
|
|
8660
|
-
durationMs: z20.number().optional()
|
|
8661
|
-
}))
|
|
8662
|
-
});
|
|
8663
|
-
var healthWhoamiOutputSchema = z20.object({
|
|
8664
|
-
workspaceId: z20.string(),
|
|
8665
|
-
workspaceName: z20.string(),
|
|
8666
|
-
scope: z20.string(),
|
|
8667
|
-
sessionId: z20.union([z20.string(), z20.null()]),
|
|
8668
|
-
oriented: z20.boolean()
|
|
8669
|
-
});
|
|
8670
|
-
var ALL_TOOL_SCHEMAS = [
|
|
8671
|
-
{ name: "entries", schema: entriesSchema },
|
|
8672
|
-
{ name: "relations", schema: relationsSchema },
|
|
8673
|
-
{ name: "graph", schema: graphSchema },
|
|
8674
|
-
{ name: "context", schema: contextSchema },
|
|
8675
|
-
{ name: "collections", schema: collectionsSchema },
|
|
8676
|
-
{ name: "session", schema: sessionSchema },
|
|
8677
|
-
{ name: "health", schema: healthSchema },
|
|
8678
|
-
{ name: "orient", schema: orientSchema },
|
|
8679
|
-
{ name: "quality", schema: qualitySchema },
|
|
8680
|
-
{ name: "workflows", schema: workflowsSchema },
|
|
8681
|
-
{ name: "session-wrapup", schema: wrapupSchema },
|
|
8682
|
-
{ name: "labels", schema: labelsSchema },
|
|
8683
|
-
{ name: "verify", schema: verifySchema },
|
|
8684
|
-
{ name: "capture", schema: captureSchema },
|
|
8685
|
-
{ name: "batch-capture", schema: batchCaptureSchema },
|
|
8686
|
-
{ name: "update-entry", schema: updateEntrySchema },
|
|
8687
|
-
{ name: "get-history", schema: getHistorySchema },
|
|
8688
|
-
{ name: "commit-entry", schema: commitEntrySchema },
|
|
8689
|
-
{ name: "start", schema: startSchema },
|
|
8690
|
-
{ name: "get-usage-summary", schema: usageSummarySchema },
|
|
8691
|
-
{ name: "chain", schema: chainSchema },
|
|
8692
|
-
{ name: "chain-version", schema: chainVersionSchema },
|
|
8693
|
-
{ name: "chain-branch", schema: chainBranchSchema },
|
|
8694
|
-
{ name: "chain-review", schema: chainReviewSchema },
|
|
8695
|
-
{ name: "create-audience-map-set", schema: createAudienceMapSetSchema },
|
|
8696
|
-
{ name: "map", schema: mapSchema },
|
|
8697
|
-
{ name: "map-slot", schema: mapSlotSchema },
|
|
8698
|
-
{ name: "map-version", schema: mapVersionSchema },
|
|
8699
|
-
{ name: "map-suggest", schema: mapSuggestSchema },
|
|
8700
|
-
{ name: "architecture", schema: architectureSchema },
|
|
8701
|
-
{ name: "architecture-admin", schema: architectureAdminSchema },
|
|
8702
|
-
{ name: "facilitate", schema: facilitateSchema }
|
|
8703
|
-
];
|
|
8704
|
-
var selfTestOutputSchema = z20.object({
|
|
8705
|
-
passed: z20.number(),
|
|
8706
|
-
failed: z20.number(),
|
|
8707
|
-
total: z20.number(),
|
|
8708
|
-
results: z20.array(z20.object({
|
|
8709
|
-
tool: z20.string(),
|
|
8710
|
-
valid: z20.boolean(),
|
|
8711
|
-
error: z20.string().optional()
|
|
8712
|
-
}))
|
|
8713
|
-
});
|
|
8714
|
-
function handleSelfTest() {
|
|
8715
|
-
const results = [];
|
|
8716
|
-
for (const { name, schema } of ALL_TOOL_SCHEMAS) {
|
|
8717
|
-
try {
|
|
8718
|
-
if (!schema || typeof schema.safeParse !== "function") {
|
|
8719
|
-
results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
|
|
8720
|
-
continue;
|
|
8277
|
+
);
|
|
8278
|
+
server.resource(
|
|
8279
|
+
"chain-search",
|
|
8280
|
+
new ResourceTemplate("productbrain://search/{query}", {
|
|
8281
|
+
complete: {
|
|
8282
|
+
query: async (value) => {
|
|
8283
|
+
if (!value) return ["glossary:", "business-rules:", "decisions:", "tensions:"];
|
|
8284
|
+
return [];
|
|
8285
|
+
}
|
|
8286
|
+
}
|
|
8287
|
+
}),
|
|
8288
|
+
async (uri, { query }) => {
|
|
8289
|
+
const results = await mcpQuery("chain.searchEntries", { query });
|
|
8290
|
+
if (!results || results.length === 0) {
|
|
8291
|
+
return { contents: [{ uri: uri.href, text: `No results for "${query}".`, mimeType: "text/markdown" }] };
|
|
8721
8292
|
}
|
|
8722
|
-
const
|
|
8723
|
-
|
|
8724
|
-
results.
|
|
8293
|
+
const lines = [
|
|
8294
|
+
`# Search: "${query}"`,
|
|
8295
|
+
`_${results.length} results_`,
|
|
8296
|
+
""
|
|
8297
|
+
];
|
|
8298
|
+
for (const entry of results) {
|
|
8299
|
+
const id = entry.entryId ? `**${entry.entryId}:** ` : "";
|
|
8300
|
+
const coll = entry.collectionName ?? entry.collectionSlug ?? "";
|
|
8301
|
+
lines.push(`- ${id}${entry.name} [${coll}] (${entry.status})`);
|
|
8725
8302
|
}
|
|
8726
|
-
|
|
8727
|
-
|
|
8303
|
+
return {
|
|
8304
|
+
contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
|
|
8305
|
+
};
|
|
8728
8306
|
}
|
|
8307
|
+
);
|
|
8308
|
+
for (const [uri, filePath] of Object.entries(UI_VIEWS)) {
|
|
8309
|
+
server.resource(`ui-view-${uri.replace(/[^a-z0-9]/gi, "-")}`, uri, async (uriObj) => {
|
|
8310
|
+
const viewPath = resolveUiViewPath(filePath);
|
|
8311
|
+
if (viewPath) {
|
|
8312
|
+
try {
|
|
8313
|
+
const html = await readFile(viewPath, "utf-8");
|
|
8314
|
+
return {
|
|
8315
|
+
contents: [{ uri: uriObj.href, text: html, mimeType: UI_RESOURCE_MIME_TYPE }]
|
|
8316
|
+
};
|
|
8317
|
+
} catch {
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
return {
|
|
8321
|
+
contents: [{
|
|
8322
|
+
uri: uriObj.href,
|
|
8323
|
+
text: renderMissingUiView(uriObj.href),
|
|
8324
|
+
mimeType: UI_RESOURCE_MIME_TYPE
|
|
8325
|
+
}]
|
|
8326
|
+
};
|
|
8327
|
+
});
|
|
8729
8328
|
}
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8329
|
+
}
|
|
8330
|
+
|
|
8331
|
+
// src/tools/architecture.ts
|
|
8332
|
+
var COLLECTION_SLUG = "architecture";
|
|
8333
|
+
var COLLECTION_FIELDS = [
|
|
8334
|
+
{ key: "archType", label: "Architecture Type", type: "select", required: true, options: ["template", "layer", "node", "flow"], searchable: true },
|
|
8335
|
+
{ key: "templateRef", label: "Template Entry ID", type: "text", searchable: false },
|
|
8336
|
+
{ key: "layerRef", label: "Layer Entry ID", type: "text", searchable: false },
|
|
8337
|
+
{ key: "description", label: "Description", type: "text", searchable: true },
|
|
8338
|
+
{ key: "color", label: "Color", type: "text" },
|
|
8339
|
+
{ key: "icon", label: "Icon", type: "text" },
|
|
8340
|
+
{ key: "sourceNode", label: "Source Node (flows)", type: "text" },
|
|
8341
|
+
{ key: "targetNode", label: "Target Node (flows)", type: "text" },
|
|
8342
|
+
{ key: "filePaths", label: "File Paths", type: "text", searchable: true },
|
|
8343
|
+
{ key: "owner", label: "Owner (circle/role)", type: "text", searchable: true },
|
|
8344
|
+
{ key: "layerOrder", label: "Layer Order (for templates)", type: "text" },
|
|
8345
|
+
{ key: "rationale", label: "Why Here? (placement rationale)", type: "text", searchable: true },
|
|
8346
|
+
{ key: "dependsOn", label: "Allowed Dependencies (layers this can import from)", type: "text" }
|
|
8347
|
+
];
|
|
8348
|
+
var ARCHITECTURE_CANONICAL_KEY = "architecture_note";
|
|
8349
|
+
async function ensureCollection() {
|
|
8350
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
8351
|
+
const existing = collections.find((c) => c.slug === COLLECTION_SLUG);
|
|
8352
|
+
if (existing) {
|
|
8353
|
+
if (!existing.defaultCanonicalKey) {
|
|
8354
|
+
await mcpMutation("chain.updateCollection", {
|
|
8355
|
+
slug: COLLECTION_SLUG,
|
|
8356
|
+
defaultCanonicalKey: ARCHITECTURE_CANONICAL_KEY
|
|
8357
|
+
});
|
|
8743
8358
|
}
|
|
8744
|
-
|
|
8745
|
-
}
|
|
8746
|
-
lines.push("## All Tools");
|
|
8747
|
-
for (const r of results) {
|
|
8748
|
-
lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
|
|
8359
|
+
return;
|
|
8749
8360
|
}
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
};
|
|
8361
|
+
await mcpMutation("chain.createCollection", {
|
|
8362
|
+
slug: COLLECTION_SLUG,
|
|
8363
|
+
name: "Architecture",
|
|
8364
|
+
icon: "\u{1F3D7}\uFE0F",
|
|
8365
|
+
description: "System architecture map \u2014 templates, layers, nodes, and flows. Visualized in the Architecture Explorer and via MCP architecture tools.",
|
|
8366
|
+
fields: COLLECTION_FIELDS
|
|
8367
|
+
});
|
|
8757
8368
|
}
|
|
8758
|
-
function
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8369
|
+
async function listArchEntries() {
|
|
8370
|
+
return mcpQuery("chain.listEntries", { collectionSlug: COLLECTION_SLUG });
|
|
8371
|
+
}
|
|
8372
|
+
function byTag(entries, archType) {
|
|
8373
|
+
return entries.filter((e) => e.tags?.includes(`archType:${archType}`) || e.data?.archType === archType).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
8374
|
+
}
|
|
8375
|
+
function renderArchitectureHtml(layers, nodes, flows, templateName) {
|
|
8376
|
+
const layerHtml = layers.map((layer) => {
|
|
8377
|
+
const layerNodes = nodes.filter(
|
|
8378
|
+
(n) => n.data?.layerRef === layer.entryId
|
|
8379
|
+
);
|
|
8380
|
+
const nodeCards = layerNodes.map((n) => `
|
|
8381
|
+
<div class="node" title="${escHtml(String(n.data?.description ?? ""))}">
|
|
8382
|
+
<span class="node-icon">${escHtml(String(n.data?.icon ?? "\u25FB"))}</span>
|
|
8383
|
+
<span class="node-name">${escHtml(n.name)}</span>
|
|
8384
|
+
</div>
|
|
8385
|
+
`).join("");
|
|
8386
|
+
return `
|
|
8387
|
+
<div class="layer" style="--layer-color: ${escHtml(String(layer.data?.color ?? "#666"))}">
|
|
8388
|
+
<div class="layer-label">
|
|
8389
|
+
<span class="layer-dot"></span>
|
|
8390
|
+
<span class="layer-name">${escHtml(layer.name)}</span>
|
|
8391
|
+
<span class="layer-count">${layerNodes.length}</span>
|
|
8392
|
+
</div>
|
|
8393
|
+
<div class="layer-desc">${escHtml(String(layer.data?.description ?? ""))}</div>
|
|
8394
|
+
<div class="nodes">${nodeCards || '<span class="empty">No components</span>'}</div>
|
|
8395
|
+
</div>
|
|
8396
|
+
`;
|
|
8397
|
+
}).join("");
|
|
8398
|
+
return `<!DOCTYPE html>
|
|
8399
|
+
<html><head><meta charset="utf-8"><style>
|
|
8400
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
8401
|
+
body{font-family:-apple-system,system-ui,sans-serif;background:#1a1a2e;color:#e0e0e0;padding:16px}
|
|
8402
|
+
h1{font-size:14px;font-weight:600;color:#a0a0c0;margin-bottom:12px;letter-spacing:.04em}
|
|
8403
|
+
.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}
|
|
8404
|
+
.layer-label{display:flex;align-items:center;gap:8px;margin-bottom:4px}
|
|
8405
|
+
.layer-dot{width:8px;height:8px;border-radius:50%;background:var(--layer-color)}
|
|
8406
|
+
.layer-name{font-size:12px;font-weight:600;color:#fff;letter-spacing:.03em}
|
|
8407
|
+
.layer-count{font-size:10px;color:var(--layer-color);background:rgba(255,255,255,.06);padding:1px 6px;border-radius:8px}
|
|
8408
|
+
.layer-desc{font-size:11px;color:#888;margin-bottom:6px}
|
|
8409
|
+
.nodes{display:flex;flex-wrap:wrap;gap:6px}
|
|
8410
|
+
.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}
|
|
8411
|
+
.node:hover{border-color:var(--layer-color)}
|
|
8412
|
+
.node-icon{font-size:12px}
|
|
8413
|
+
.node-name{color:#ddd}
|
|
8414
|
+
.empty{font-size:11px;color:#555;font-style:italic}
|
|
8415
|
+
.flows{margin-top:12px;border-top:1px solid rgba(255,255,255,.06);padding-top:8px}
|
|
8416
|
+
.flow{font-size:11px;color:#888;padding:2px 0}
|
|
8417
|
+
.flow-arrow{color:#6366f1;margin:0 4px}
|
|
8418
|
+
</style></head><body>
|
|
8419
|
+
<h1>${escHtml(templateName)}</h1>
|
|
8420
|
+
${layerHtml}
|
|
8421
|
+
${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>` : ""}
|
|
8422
|
+
</body></html>`;
|
|
8423
|
+
}
|
|
8424
|
+
function escHtml(s) {
|
|
8425
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8426
|
+
}
|
|
8427
|
+
function formatLayerText(layer, nodes) {
|
|
8428
|
+
const layerNodes = nodes.filter((n) => n.data?.layerRef === layer.entryId);
|
|
8429
|
+
const nodeList = layerNodes.map((n) => {
|
|
8430
|
+
const desc = n.data?.description ? ` \u2014 ${n.data.description}` : "";
|
|
8431
|
+
const owner = n.data?.owner ? ` (${n.data.owner})` : "";
|
|
8432
|
+
return ` - ${n.data?.icon ?? "\u25FB"} **${n.name}**${desc}${owner}`;
|
|
8433
|
+
}).join("\n");
|
|
8434
|
+
return `### ${layer.name}
|
|
8435
|
+
${layer.data?.description ?? ""}
|
|
8436
|
+
|
|
8437
|
+
${nodeList || " _No components_"}`;
|
|
8438
|
+
}
|
|
8439
|
+
var SEED_TEMPLATE = {
|
|
8440
|
+
entryId: "ARCH-tpl-product-os",
|
|
8441
|
+
name: "Product OS Default",
|
|
8442
|
+
data: {
|
|
8443
|
+
archType: "template",
|
|
8444
|
+
description: "Default 4-layer architecture: Auth \u2192 Infrastructure \u2192 Core \u2192 Features, with an outward Integration layer",
|
|
8445
|
+
layerOrder: JSON.stringify(["auth", "infrastructure", "core", "features", "integration"])
|
|
8446
|
+
}
|
|
8447
|
+
};
|
|
8448
|
+
var SEED_LAYERS = [
|
|
8449
|
+
{ 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." } },
|
|
8450
|
+
{ 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." } },
|
|
8451
|
+
{ 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." } },
|
|
8452
|
+
{ 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." } },
|
|
8453
|
+
{ 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." } }
|
|
8454
|
+
];
|
|
8455
|
+
var SEED_NODES = [
|
|
8456
|
+
// Auth layer
|
|
8457
|
+
{ 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." } },
|
|
8458
|
+
{ 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." } },
|
|
8459
|
+
// Infrastructure layer
|
|
8460
|
+
{ 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." } },
|
|
8461
|
+
{ 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." } },
|
|
8462
|
+
{ 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." } },
|
|
8463
|
+
// Core layer
|
|
8464
|
+
{ 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." } },
|
|
8465
|
+
{ 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." } },
|
|
8466
|
+
{ 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." } },
|
|
8467
|
+
{ 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." } },
|
|
8468
|
+
// Features layer
|
|
8469
|
+
{ 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." } },
|
|
8470
|
+
{ 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." } },
|
|
8471
|
+
{ 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." } },
|
|
8472
|
+
{ 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." } },
|
|
8473
|
+
{ 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." } },
|
|
8474
|
+
{ 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." } },
|
|
8475
|
+
{ 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." } },
|
|
8476
|
+
// Integration layer
|
|
8477
|
+
{ 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." } },
|
|
8478
|
+
{ 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." } },
|
|
8479
|
+
{ 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." } }
|
|
8480
|
+
];
|
|
8481
|
+
var SEED_FLOWS = [
|
|
8482
|
+
{ 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" } },
|
|
8483
|
+
{ 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" } },
|
|
8484
|
+
{ 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" } },
|
|
8485
|
+
{ 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" } },
|
|
8486
|
+
{ 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" } }
|
|
8487
|
+
];
|
|
8488
|
+
var architectureSchema = z19.object({
|
|
8489
|
+
action: z19.enum(["show", "explore", "flow"]).describe("Action: show full map, explore a layer, or visualize a flow"),
|
|
8490
|
+
template: z19.string().optional().describe("Template entry ID to filter by (for show)"),
|
|
8491
|
+
layer: z19.string().optional().describe("Layer name or entry ID (for explore), e.g. 'Core' or 'ARCH-layer-core'"),
|
|
8492
|
+
flow: z19.string().optional().describe("Flow name or entry ID (for flow), e.g. 'Smart Capture Flow'")
|
|
8493
|
+
});
|
|
8494
|
+
var architectureAdminSchema = z19.object({
|
|
8495
|
+
action: z19.enum(["seed", "check"]).describe("Action: seed default architecture data, or check dependency health")
|
|
8496
|
+
});
|
|
8497
|
+
function registerArchitectureTools(server) {
|
|
8781
8498
|
server.registerTool(
|
|
8782
|
-
"
|
|
8499
|
+
"architecture",
|
|
8783
8500
|
{
|
|
8784
|
-
title: "
|
|
8785
|
-
description: "
|
|
8786
|
-
inputSchema:
|
|
8501
|
+
title: "Architecture",
|
|
8502
|
+
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",
|
|
8503
|
+
inputSchema: architectureSchema,
|
|
8787
8504
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
8788
8505
|
},
|
|
8789
|
-
withEnvelope(async ({
|
|
8790
|
-
|
|
8791
|
-
const
|
|
8792
|
-
if (
|
|
8506
|
+
withEnvelope(async ({ action, template, layer, flow }) => {
|
|
8507
|
+
await ensureCollection();
|
|
8508
|
+
const all = await listArchEntries();
|
|
8509
|
+
if (action === "show") {
|
|
8510
|
+
const templates = byTag(all, "template");
|
|
8511
|
+
const activeTemplate = template ? templates.find((t) => t.entryId === template) : templates[0];
|
|
8512
|
+
const templateName = activeTemplate?.name ?? "System Architecture";
|
|
8513
|
+
const templateId = activeTemplate?.entryId;
|
|
8514
|
+
const layers = byTag(all, "layer").filter((l) => !templateId || l.data?.templateRef === templateId);
|
|
8515
|
+
const nodes = byTag(all, "node");
|
|
8516
|
+
const flows = byTag(all, "flow");
|
|
8517
|
+
if (layers.length === 0) {
|
|
8518
|
+
return {
|
|
8519
|
+
content: [{
|
|
8520
|
+
type: "text",
|
|
8521
|
+
text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
|
|
8522
|
+
}],
|
|
8523
|
+
structuredContent: failure(
|
|
8524
|
+
"NOT_FOUND",
|
|
8525
|
+
"No architecture data found.",
|
|
8526
|
+
"Seed the default architecture first.",
|
|
8527
|
+
[{ tool: "architecture-admin", description: "Seed architecture", parameters: { action: "seed" } }]
|
|
8528
|
+
)
|
|
8529
|
+
};
|
|
8530
|
+
}
|
|
8531
|
+
const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
|
|
8532
|
+
const textFlows = flows.length > 0 ? "\n\n---\n\n## Data Flows\n\n" + flows.map(
|
|
8533
|
+
(f) => `- **${f.name}**: ${f.data?.description ?? ""}`
|
|
8534
|
+
).join("\n") : "";
|
|
8535
|
+
const text = `# ${templateName}
|
|
8536
|
+
|
|
8537
|
+
${textLayers}${textFlows}`;
|
|
8538
|
+
const html = renderArchitectureHtml(layers, nodes, flows, templateName);
|
|
8793
8539
|
return {
|
|
8794
|
-
content: [
|
|
8540
|
+
content: [
|
|
8541
|
+
{ type: "text", text },
|
|
8542
|
+
{ type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: UI_RESOURCE_MIME_TYPE, text: html } }
|
|
8543
|
+
],
|
|
8795
8544
|
structuredContent: success(
|
|
8796
|
-
|
|
8797
|
-
{
|
|
8545
|
+
`Showing ${templateName}: ${layers.length} layers, ${nodes.length} nodes, ${flows.length} flows.`,
|
|
8546
|
+
{ templateName, layerCount: layers.length, nodeCount: nodes.length, flowCount: flows.length }
|
|
8798
8547
|
)
|
|
8799
8548
|
};
|
|
8800
8549
|
}
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
} catch (e) {
|
|
8805
|
-
errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
|
|
8806
|
-
}
|
|
8807
|
-
let priorSessions = [];
|
|
8808
|
-
let recoveryBlock = null;
|
|
8809
|
-
if (wsCtx) {
|
|
8810
|
-
try {
|
|
8811
|
-
const sessionsResult = await mcpQuery("agent.recentSessions", { limit: 3 });
|
|
8812
|
-
priorSessions = sessionsResult?.sessions ?? [];
|
|
8813
|
-
recoveryBlock = sessionsResult?.recoveryBlock ?? null;
|
|
8814
|
-
} catch {
|
|
8550
|
+
if (action === "explore") {
|
|
8551
|
+
if (!layer) {
|
|
8552
|
+
return validationResult("A `layer` is required for explore.");
|
|
8815
8553
|
}
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
openTensions = (tensions ?? []).filter((e) => e.workflowStatus === "open");
|
|
8831
|
-
} catch {
|
|
8832
|
-
}
|
|
8833
|
-
let readiness = null;
|
|
8834
|
-
try {
|
|
8835
|
-
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
8836
|
-
} catch (e) {
|
|
8837
|
-
errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
|
|
8838
|
-
}
|
|
8839
|
-
if (readiness?.stage === "blank") {
|
|
8840
|
-
const scanLines = [
|
|
8841
|
-
`# Welcome to ${wsCtx?.workspaceName ?? "your workspace"}`,
|
|
8842
|
-
"",
|
|
8843
|
-
"Your workspace is fresh \u2014 let's set it up.",
|
|
8844
|
-
"",
|
|
8845
|
-
"**Recommended:** call the `start` tool instead of `orient` for the best first-run experience.",
|
|
8846
|
-
"It will guide you through setting up your workspace \u2014 you can tell me about your product, scan your codebase, or use a preset.",
|
|
8847
|
-
"",
|
|
8848
|
-
"Or tell me about your product and I'll start capturing knowledge directly."
|
|
8849
|
-
];
|
|
8850
|
-
let oriented = false;
|
|
8851
|
-
let orientationStatus = "no_session";
|
|
8852
|
-
if (agentSessionId) {
|
|
8853
|
-
try {
|
|
8854
|
-
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
8855
|
-
setSessionOriented(true);
|
|
8856
|
-
oriented = true;
|
|
8857
|
-
orientationStatus = "complete";
|
|
8858
|
-
} catch {
|
|
8859
|
-
orientationStatus = "failed";
|
|
8860
|
-
}
|
|
8554
|
+
const layers = byTag(all, "layer");
|
|
8555
|
+
const target = layers.find(
|
|
8556
|
+
(l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
|
|
8557
|
+
);
|
|
8558
|
+
if (!target) {
|
|
8559
|
+
const available = layers.map((l) => `\`${l.name}\``).join(", ");
|
|
8560
|
+
return {
|
|
8561
|
+
content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }],
|
|
8562
|
+
structuredContent: failure(
|
|
8563
|
+
"NOT_FOUND",
|
|
8564
|
+
`Layer "${layer}" not found.`,
|
|
8565
|
+
`Available layers: ${available}`
|
|
8566
|
+
)
|
|
8567
|
+
};
|
|
8861
8568
|
}
|
|
8569
|
+
const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
|
|
8570
|
+
const flows = byTag(all, "flow").filter(
|
|
8571
|
+
(f) => nodes.some((n) => n.entryId === f.data?.sourceNode || n.entryId === f.data?.targetNode)
|
|
8572
|
+
);
|
|
8573
|
+
const depRule = target.data?.dependsOn ? `
|
|
8574
|
+
**Dependency rule:** May import from ${target.data.dependsOn === "none" ? "nothing (foundation layer)" : target.data.dependsOn}.
|
|
8575
|
+
` : "";
|
|
8576
|
+
const layerRationale = target.data?.rationale ? `
|
|
8577
|
+
> ${target.data.rationale}
|
|
8578
|
+
` : "";
|
|
8579
|
+
const nodeDetail = nodes.map((n) => {
|
|
8580
|
+
const lines = [`#### ${n.data?.icon ?? "\u25FB"} ${n.name}`];
|
|
8581
|
+
if (n.data?.description) lines.push(String(n.data.description));
|
|
8582
|
+
if (n.data?.owner) lines.push(`**Owner:** ${n.data.owner}`);
|
|
8583
|
+
if (n.data?.filePaths) lines.push(`**Files:** \`${n.data.filePaths}\``);
|
|
8584
|
+
if (n.data?.rationale) lines.push(`
|
|
8585
|
+
**Why here?** ${n.data.rationale}`);
|
|
8586
|
+
return lines.join("\n");
|
|
8587
|
+
}).join("\n\n");
|
|
8588
|
+
const flowLines = flows.length > 0 ? "\n\n### Connected Flows\n\n" + flows.map((f) => `- ${f.name}: ${f.data?.description ?? ""}`).join("\n") : "";
|
|
8862
8589
|
return {
|
|
8863
|
-
content: [{
|
|
8590
|
+
content: [{
|
|
8591
|
+
type: "text",
|
|
8592
|
+
text: `# ${target.data?.icon ?? ""} ${target.name} Layer
|
|
8593
|
+
|
|
8594
|
+
${target.data?.description ?? ""}${depRule}${layerRationale}
|
|
8595
|
+
**${nodes.length} components**
|
|
8596
|
+
|
|
8597
|
+
${nodeDetail}${flowLines}`
|
|
8598
|
+
}],
|
|
8864
8599
|
structuredContent: success(
|
|
8865
|
-
|
|
8866
|
-
{
|
|
8867
|
-
[{ tool: "start", description: "Guided workspace setup", parameters: {} }]
|
|
8600
|
+
`${target.name} layer: ${nodes.length} components, ${flows.length} connected flows.`,
|
|
8601
|
+
{ layerName: target.name, entryId: target.entryId, nodeCount: nodes.length, flowCount: flows.length }
|
|
8868
8602
|
)
|
|
8869
8603
|
};
|
|
8870
8604
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
lines.push(`# ${wsCtx.workspaceName}`);
|
|
8875
|
-
lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
8876
|
-
} else {
|
|
8877
|
-
lines.push("# Workspace");
|
|
8878
|
-
lines.push("_Could not resolve workspace._");
|
|
8879
|
-
}
|
|
8880
|
-
lines.push("");
|
|
8881
|
-
if (mode === "brief") {
|
|
8882
|
-
const briefStage = readiness?.stage ?? (readiness?.score != null ? readiness.score < 50 ? "seeded" : "grounded" : "unknown");
|
|
8883
|
-
lines.push(`Brain stage: ${briefStage}`);
|
|
8884
|
-
if (orientEntries?.strategicContext) {
|
|
8885
|
-
const sc = orientEntries.strategicContext;
|
|
8886
|
-
if (sc.vision) lines.push(`Vision: ${sc.vision}`);
|
|
8887
|
-
if (sc.purpose) lines.push(`Purpose: ${sc.purpose}`);
|
|
8888
|
-
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
8889
|
-
lines.push(`Product areas (${sc.productAreaCount}): ${(sc.productAreas ?? []).join(", ")}`);
|
|
8890
|
-
}
|
|
8891
|
-
lines.push(`${sc.activeBetCount} active bet(s), ${sc.activeTensionCount} tension(s).`);
|
|
8892
|
-
}
|
|
8893
|
-
if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
|
|
8894
|
-
lines.push(`Task context: ${orientEntries.taskContext.totalFound} relevant entries (${orientEntries.taskContext.confidence} confidence).`);
|
|
8895
|
-
}
|
|
8896
|
-
if (orientEntries?.activeBets?.length > 0) {
|
|
8897
|
-
for (const e of orientEntries.activeBets) {
|
|
8898
|
-
const tensions = e.linkedTensions;
|
|
8899
|
-
const tensionPart = tensions?.length ? ` \u2014 ${tensions.map((t) => `${t.entryId ?? t.name} (${t.severity ?? "\u2014"})`).join(", ")}` : "";
|
|
8900
|
-
lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
|
|
8901
|
-
}
|
|
8902
|
-
}
|
|
8903
|
-
if (recoveryBlock) {
|
|
8904
|
-
lines.push("");
|
|
8905
|
-
lines.push(...formatRecoveryBlock(recoveryBlock));
|
|
8906
|
-
} else if (priorSessions.length > 0) {
|
|
8907
|
-
const last = priorSessions[0];
|
|
8908
|
-
const date = new Date(last.startedAt).toISOString().split("T")[0];
|
|
8909
|
-
const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
|
|
8910
|
-
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
8911
|
-
lines.push(`Last session (${date}): ${created} created, ${modified} modified`);
|
|
8605
|
+
if (action === "flow") {
|
|
8606
|
+
if (!flow) {
|
|
8607
|
+
return validationResult("A `flow` name or entry ID is required.");
|
|
8912
8608
|
}
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
});
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8609
|
+
const flows = byTag(all, "flow");
|
|
8610
|
+
const target = flows.find(
|
|
8611
|
+
(f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
|
|
8612
|
+
);
|
|
8613
|
+
if (!target) {
|
|
8614
|
+
const available = flows.map((f) => `\`${f.name}\``).join(", ");
|
|
8615
|
+
return {
|
|
8616
|
+
content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }],
|
|
8617
|
+
structuredContent: failure(
|
|
8618
|
+
"NOT_FOUND",
|
|
8619
|
+
`Flow "${flow}" not found.`,
|
|
8620
|
+
`Available flows: ${available}`
|
|
8621
|
+
)
|
|
8622
|
+
};
|
|
8925
8623
|
}
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8624
|
+
const nodes = byTag(all, "node");
|
|
8625
|
+
const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
|
|
8626
|
+
const dest = nodes.find((n) => n.entryId === target.data?.targetNode);
|
|
8627
|
+
const lines = [
|
|
8628
|
+
`# ${target.name}`,
|
|
8629
|
+
"",
|
|
8630
|
+
`**${source?.data?.icon ?? "?"} ${source?.name ?? "Unknown"}** \u2192 **${dest?.data?.icon ?? "?"} ${dest?.name ?? "Unknown"}**`,
|
|
8631
|
+
"",
|
|
8632
|
+
String(target.data?.description ?? "")
|
|
8633
|
+
];
|
|
8634
|
+
if (source) {
|
|
8635
|
+
lines.push("", `### Source: ${source.name}`, String(source.data?.description ?? ""));
|
|
8636
|
+
if (source.data?.filePaths) lines.push(`Files: \`${source.data.filePaths}\``);
|
|
8937
8637
|
}
|
|
8938
|
-
if (
|
|
8939
|
-
lines.push("");
|
|
8940
|
-
|
|
8638
|
+
if (dest) {
|
|
8639
|
+
lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
|
|
8640
|
+
if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
|
|
8941
8641
|
}
|
|
8942
8642
|
return {
|
|
8943
8643
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
8944
8644
|
structuredContent: success(
|
|
8945
|
-
|
|
8946
|
-
{
|
|
8645
|
+
`${target.name}: ${source?.name ?? "?"} \u2192 ${dest?.name ?? "?"}.`,
|
|
8646
|
+
{ flowName: target.name, entryId: target.entryId, source: source?.name, target: dest?.name }
|
|
8947
8647
|
)
|
|
8948
8648
|
};
|
|
8949
8649
|
}
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
if (sc.purpose) lines.push(`**Purpose:** ${sc.purpose}`);
|
|
8990
|
-
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
8991
|
-
lines.push(`**Product areas (${sc.productAreaCount}):** ${(sc.productAreas ?? []).join(", ")}`);
|
|
8992
|
-
}
|
|
8993
|
-
const betLine = sc.currentBet ? `**Current bet:** ${sc.currentBet}. ${sc.activeBetCount} active bet(s).` : "No active bets.";
|
|
8994
|
-
lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
|
|
8995
|
-
lines.push("");
|
|
8996
|
-
}
|
|
8997
|
-
if (orientEntries?.continuingFrom && orientEntries.continuingFrom.length > 0) {
|
|
8998
|
-
lines.push("## Continuing from");
|
|
8999
|
-
lines.push("_Prior-session entries most relevant to your task._");
|
|
9000
|
-
lines.push("");
|
|
9001
|
-
for (const e of orientEntries.continuingFrom) {
|
|
9002
|
-
const id = e.entryId ?? e.name;
|
|
9003
|
-
const type = e.canonicalKey ?? "generic";
|
|
9004
|
-
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9005
|
-
lines.push(`- \`${id}\` (score ${e.score}) [${type}]${coll} \u2014 ${e.name}`);
|
|
9006
|
-
if (e.reasoning) lines.push(` _${e.reasoning}_`);
|
|
9007
|
-
}
|
|
9008
|
-
lines.push("");
|
|
9009
|
-
}
|
|
9010
|
-
if (orientEntries?.lastSessionTouched && orientEntries.lastSessionTouched.length > 0) {
|
|
9011
|
-
lines.push("## Last session touched");
|
|
9012
|
-
lines.push("_Entries created or modified in your most recent session._");
|
|
9013
|
-
lines.push("");
|
|
9014
|
-
for (const e of orientEntries.lastSessionTouched) {
|
|
9015
|
-
const id = e.entryId ?? e.name;
|
|
9016
|
-
const type = e.canonicalKey ?? "generic";
|
|
9017
|
-
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9018
|
-
lines.push(`- \`${id}\` [${type}]${coll} \u2014 ${e.name}`);
|
|
9019
|
-
}
|
|
9020
|
-
lines.push("");
|
|
9021
|
-
}
|
|
9022
|
-
if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
|
|
9023
|
-
const tc = orientEntries.taskContext;
|
|
9024
|
-
lines.push("## Task Context");
|
|
9025
|
-
lines.push(`_Task-scoped entries (${tc.confidence} confidence, ${tc.totalFound} relevant)`);
|
|
9026
|
-
lines.push("");
|
|
9027
|
-
for (const e of tc.context) {
|
|
9028
|
-
const id = e.entryId ?? e.name;
|
|
9029
|
-
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9030
|
-
lines.push(`- \`${id}\` (score ${e.score})${coll}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
|
|
9031
|
-
}
|
|
9032
|
-
lines.push("");
|
|
9033
|
-
}
|
|
9034
|
-
if (task && orientEntries) {
|
|
9035
|
-
const result = runAlignmentCheck(
|
|
9036
|
-
task,
|
|
9037
|
-
orientEntries.activeBets ?? [],
|
|
9038
|
-
orientEntries.taskContext?.context
|
|
9039
|
-
);
|
|
9040
|
-
lines.push(...buildAlignmentCheckLines(result));
|
|
9041
|
-
}
|
|
9042
|
-
if (orientEntries) {
|
|
9043
|
-
const fmt = (e) => {
|
|
9044
|
-
const type = e.canonicalKey ?? "generic";
|
|
9045
|
-
const stratum = e.stratum ?? "?";
|
|
9046
|
-
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
|
|
9047
|
-
};
|
|
9048
|
-
if (orientEntries.activeBets?.length > 0) {
|
|
9049
|
-
lines.push("## Active bets \u2014 current scope");
|
|
9050
|
-
lines.push("_These define what you're building now. Work outside these bets requires explicit user confirmation before designing._");
|
|
9051
|
-
lines.push("");
|
|
9052
|
-
for (const e of orientEntries.activeBets) {
|
|
9053
|
-
lines.push(fmt(e));
|
|
9054
|
-
const tensions = e.linkedTensions;
|
|
9055
|
-
if (tensions?.length) {
|
|
9056
|
-
const tensionLines = tensions.map((t) => {
|
|
9057
|
-
const meta = [t.severity, t.priority].filter(Boolean).join(", ");
|
|
9058
|
-
return `\`${t.entryId ?? t.name}\` (${t.name}${meta ? `, ${meta}` : ""})`;
|
|
9059
|
-
});
|
|
9060
|
-
lines.push(` Tensions: ${tensionLines.join("; ")}`);
|
|
9061
|
-
} else {
|
|
9062
|
-
lines.push(` Tensions: No linked tensions`);
|
|
9063
|
-
}
|
|
9064
|
-
}
|
|
9065
|
-
lines.push("");
|
|
9066
|
-
}
|
|
9067
|
-
if (orientEntries.activeGoals.length > 0) {
|
|
9068
|
-
lines.push("## Active goals");
|
|
9069
|
-
orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
|
|
9070
|
-
lines.push("");
|
|
9071
|
-
}
|
|
9072
|
-
if (orientEntries.recentDecisions.length > 0) {
|
|
9073
|
-
lines.push("## Recent decisions");
|
|
9074
|
-
orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
|
|
9075
|
-
lines.push("");
|
|
9076
|
-
}
|
|
9077
|
-
if (orientEntries.recentlySuperseded.length > 0) {
|
|
9078
|
-
lines.push("## Recently superseded");
|
|
9079
|
-
orientEntries.recentlySuperseded.forEach((e) => lines.push(fmt(e)));
|
|
9080
|
-
lines.push("");
|
|
9081
|
-
}
|
|
9082
|
-
if (orientEntries.staleEntries.length > 0) {
|
|
9083
|
-
lines.push("## Needs confirmation");
|
|
9084
|
-
lines.push(`_Domain stratum entries not confirmed in ${orientEntries.stalenessThresholdDays} days._`);
|
|
9085
|
-
orientEntries.staleEntries.forEach((e) => lines.push(fmt(e)));
|
|
9086
|
-
lines.push("");
|
|
9087
|
-
}
|
|
9088
|
-
const hasPrinciples = orientEntries.principles?.length > 0;
|
|
9089
|
-
const hasStandards = orientEntries.standards?.length > 0;
|
|
9090
|
-
const hasBusinessRules = orientEntries.businessRules.length > 0;
|
|
9091
|
-
if (hasPrinciples || hasStandards || hasBusinessRules) {
|
|
9092
|
-
lines.push("## Workspace Governance \u2014 constraints");
|
|
9093
|
-
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._");
|
|
9094
|
-
lines.push("");
|
|
9095
|
-
if (hasPrinciples) {
|
|
9096
|
-
lines.push("**Principles** \u2014 beliefs that guide decisions:");
|
|
9097
|
-
orientEntries.principles.forEach((e) => lines.push(fmt(e)));
|
|
9098
|
-
lines.push("");
|
|
9099
|
-
}
|
|
9100
|
-
if (hasStandards) {
|
|
9101
|
-
lines.push("**Standards** \u2014 conventions for how work is done:");
|
|
9102
|
-
orientEntries.standards.forEach((e) => lines.push(fmt(e)));
|
|
9103
|
-
lines.push("");
|
|
9104
|
-
}
|
|
9105
|
-
if (hasBusinessRules) {
|
|
9106
|
-
lines.push("**Business Rules** \u2014 system constraints:");
|
|
9107
|
-
orientEntries.businessRules.forEach((e) => lines.push(fmt(e)));
|
|
9108
|
-
lines.push("");
|
|
8650
|
+
return unknownAction(action, ["show", "explore", "flow"]);
|
|
8651
|
+
})
|
|
8652
|
+
);
|
|
8653
|
+
const archAdminTool = server.registerTool(
|
|
8654
|
+
"architecture-admin",
|
|
8655
|
+
{
|
|
8656
|
+
title: "Architecture Admin",
|
|
8657
|
+
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.",
|
|
8658
|
+
inputSchema: architectureAdminSchema,
|
|
8659
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
8660
|
+
},
|
|
8661
|
+
withEnvelope(async ({ action }) => {
|
|
8662
|
+
if (action === "seed") {
|
|
8663
|
+
await ensureCollection();
|
|
8664
|
+
const existing = await listArchEntries();
|
|
8665
|
+
const existingIds = new Set(existing.map((e) => e.entryId));
|
|
8666
|
+
let created = 0;
|
|
8667
|
+
let updated = 0;
|
|
8668
|
+
let unchanged = 0;
|
|
8669
|
+
const allSeeds = [
|
|
8670
|
+
{ ...SEED_TEMPLATE, order: 0, status: "active" },
|
|
8671
|
+
...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
|
|
8672
|
+
...SEED_NODES.map((n) => ({ ...n, status: "active" })),
|
|
8673
|
+
...SEED_FLOWS.map((f) => ({ ...f, status: "active" }))
|
|
8674
|
+
];
|
|
8675
|
+
for (const seed of allSeeds) {
|
|
8676
|
+
if (existingIds.has(seed.entryId)) {
|
|
8677
|
+
const existingEntry = existing.find((e) => e.entryId === seed.entryId);
|
|
8678
|
+
const existingData = existingEntry?.data ?? {};
|
|
8679
|
+
const seedData = seed.data;
|
|
8680
|
+
const hasChanges = Object.keys(seedData).some(
|
|
8681
|
+
(k) => seedData[k] !== void 0 && existingData[k] !== seedData[k]
|
|
8682
|
+
);
|
|
8683
|
+
if (hasChanges) {
|
|
8684
|
+
const mergedData = { ...existingData, ...seedData };
|
|
8685
|
+
await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
8686
|
+
updated++;
|
|
8687
|
+
} else {
|
|
8688
|
+
unchanged++;
|
|
9109
8689
|
}
|
|
8690
|
+
continue;
|
|
9110
8691
|
}
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
name: e.name,
|
|
9119
|
-
description: typeof e.preview === "string" ? e.preview : void 0
|
|
8692
|
+
await mcpMutation("chain.createEntry", {
|
|
8693
|
+
collectionSlug: COLLECTION_SLUG,
|
|
8694
|
+
entryId: seed.entryId,
|
|
8695
|
+
name: seed.name,
|
|
8696
|
+
status: seed.status,
|
|
8697
|
+
data: seed.data,
|
|
8698
|
+
order: seed.order ?? 0
|
|
9120
8699
|
});
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
8700
|
+
created++;
|
|
8701
|
+
}
|
|
8702
|
+
return {
|
|
8703
|
+
content: [{
|
|
8704
|
+
type: "text",
|
|
8705
|
+
text: `# Architecture Seeded
|
|
8706
|
+
|
|
8707
|
+
**Created:** ${created} entries
|
|
8708
|
+
**Updated:** ${updated} (merged new fields)
|
|
8709
|
+
**Unchanged:** ${unchanged}
|
|
8710
|
+
|
|
8711
|
+
Use \`architecture action=show\` to view the map.`
|
|
8712
|
+
}],
|
|
8713
|
+
structuredContent: success(
|
|
8714
|
+
`Architecture seeded: ${created} created, ${updated} updated, ${unchanged} unchanged.`,
|
|
8715
|
+
{ created, updated, unchanged },
|
|
8716
|
+
[{ tool: "architecture", description: "View the map", parameters: { action: "show" } }]
|
|
8717
|
+
)
|
|
8718
|
+
};
|
|
8719
|
+
}
|
|
8720
|
+
if (action === "check") {
|
|
8721
|
+
const projectRoot = resolveProjectRoot2();
|
|
8722
|
+
if (!projectRoot) {
|
|
8723
|
+
return {
|
|
8724
|
+
content: [{
|
|
8725
|
+
type: "text",
|
|
8726
|
+
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."
|
|
8727
|
+
}],
|
|
8728
|
+
structuredContent: failure(
|
|
8729
|
+
"VALIDATION_ERROR",
|
|
8730
|
+
"Cannot find project root.",
|
|
8731
|
+
"Set WORKSPACE_PATH env var to the absolute path of the Product OS project root."
|
|
8732
|
+
)
|
|
8733
|
+
};
|
|
9126
8734
|
}
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
}
|
|
9142
|
-
if (briefingItems.length > 0) {
|
|
9143
|
-
lines.push("## Briefing");
|
|
9144
|
-
for (const item of briefingItems) {
|
|
9145
|
-
lines.push(`- ${item}`);
|
|
8735
|
+
await ensureCollection();
|
|
8736
|
+
const all = await listArchEntries();
|
|
8737
|
+
const layers = byTag(all, "layer");
|
|
8738
|
+
const nodes = byTag(all, "node");
|
|
8739
|
+
const result = scanDependencies(projectRoot, layers, nodes);
|
|
8740
|
+
return {
|
|
8741
|
+
content: [{ type: "text", text: formatScanReport(result) }],
|
|
8742
|
+
structuredContent: success(
|
|
8743
|
+
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.`,
|
|
8744
|
+
{
|
|
8745
|
+
violations: result.violations.length,
|
|
8746
|
+
filesScanned: result.filesScanned,
|
|
8747
|
+
importsChecked: result.importsChecked,
|
|
8748
|
+
unmappedImports: result.unmappedImports
|
|
9146
8749
|
}
|
|
9147
|
-
|
|
8750
|
+
)
|
|
8751
|
+
};
|
|
8752
|
+
}
|
|
8753
|
+
return unknownAction(action, ["seed", "check"]);
|
|
8754
|
+
})
|
|
8755
|
+
);
|
|
8756
|
+
trackWriteTool(archAdminTool);
|
|
8757
|
+
}
|
|
8758
|
+
function resolveProjectRoot2() {
|
|
8759
|
+
const candidates = [
|
|
8760
|
+
process.env.WORKSPACE_PATH,
|
|
8761
|
+
process.cwd(),
|
|
8762
|
+
resolve3(process.cwd(), "..")
|
|
8763
|
+
].filter(Boolean);
|
|
8764
|
+
for (const dir of candidates) {
|
|
8765
|
+
const resolved = resolve3(dir);
|
|
8766
|
+
if (existsSync3(resolve3(resolved, "convex/schema.ts"))) return resolved;
|
|
8767
|
+
}
|
|
8768
|
+
return null;
|
|
8769
|
+
}
|
|
8770
|
+
function scanDependencies(projectRoot, layers, nodes) {
|
|
8771
|
+
const layerMap = /* @__PURE__ */ new Map();
|
|
8772
|
+
for (const l of layers) layerMap.set(l.entryId, l);
|
|
8773
|
+
const allowedDeps = buildAllowedDeps(layers);
|
|
8774
|
+
const nodePathPrefixes = buildNodePrefixes(nodes);
|
|
8775
|
+
const violations = [];
|
|
8776
|
+
const nodeResults = /* @__PURE__ */ new Map();
|
|
8777
|
+
let totalFiles = 0;
|
|
8778
|
+
let totalImports = 0;
|
|
8779
|
+
let unmapped = 0;
|
|
8780
|
+
for (const node of nodes) {
|
|
8781
|
+
const layerRef = String(node.data?.layerRef ?? "");
|
|
8782
|
+
const layer = layerMap.get(layerRef);
|
|
8783
|
+
if (!layer) continue;
|
|
8784
|
+
const filePaths = parseFilePaths(node);
|
|
8785
|
+
const nodeViolations = [];
|
|
8786
|
+
let nodeFileCount = 0;
|
|
8787
|
+
for (const fp of filePaths) {
|
|
8788
|
+
const absPath = resolve3(projectRoot, fp);
|
|
8789
|
+
const files = collectFiles(absPath);
|
|
8790
|
+
for (const file of files) {
|
|
8791
|
+
nodeFileCount++;
|
|
8792
|
+
totalFiles++;
|
|
8793
|
+
const relFile = relative(projectRoot, file);
|
|
8794
|
+
const imports = parseImports(file);
|
|
8795
|
+
for (const imp of imports) {
|
|
8796
|
+
totalImports++;
|
|
8797
|
+
const resolved = resolveImport(imp, file, projectRoot);
|
|
8798
|
+
if (!resolved) {
|
|
8799
|
+
unmapped++;
|
|
8800
|
+
continue;
|
|
9148
8801
|
}
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
8802
|
+
const targetNode = findNodeByPath(resolved, nodePathPrefixes);
|
|
8803
|
+
if (!targetNode) {
|
|
8804
|
+
unmapped++;
|
|
8805
|
+
continue;
|
|
8806
|
+
}
|
|
8807
|
+
const targetLayerRef = String(targetNode.data?.layerRef ?? "");
|
|
8808
|
+
const targetLayer = layerMap.get(targetLayerRef);
|
|
8809
|
+
if (!targetLayer) continue;
|
|
8810
|
+
if (targetLayerRef === layerRef) continue;
|
|
8811
|
+
const allowed = allowedDeps.get(layerRef);
|
|
8812
|
+
if (allowed && !allowed.has(targetLayerRef)) {
|
|
8813
|
+
const v = {
|
|
8814
|
+
sourceNode: node.name,
|
|
8815
|
+
sourceLayer: layer.name,
|
|
8816
|
+
sourceFile: relFile,
|
|
8817
|
+
importPath: imp,
|
|
8818
|
+
targetNode: targetNode.name,
|
|
8819
|
+
targetLayer: targetLayer.name,
|
|
8820
|
+
rule: `${layer.name} cannot import from ${targetLayer.name}`
|
|
8821
|
+
};
|
|
8822
|
+
violations.push(v);
|
|
8823
|
+
nodeViolations.push(v);
|
|
9152
8824
|
}
|
|
9153
8825
|
}
|
|
9154
|
-
lines.push("What would you like to work on?");
|
|
9155
|
-
lines.push("");
|
|
9156
8826
|
}
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
8827
|
+
}
|
|
8828
|
+
nodeResults.set(node.entryId, { violations: nodeViolations, filesScanned: nodeFileCount });
|
|
8829
|
+
}
|
|
8830
|
+
return { violations, filesScanned: totalFiles, importsChecked: totalImports, unmappedImports: unmapped, nodeResults };
|
|
8831
|
+
}
|
|
8832
|
+
function buildAllowedDeps(layers) {
|
|
8833
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
8834
|
+
for (const l of layers) nameToId.set(l.name.toLowerCase(), l.entryId);
|
|
8835
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
8836
|
+
for (const layer of layers) {
|
|
8837
|
+
const deps = String(layer.data?.dependsOn ?? "none");
|
|
8838
|
+
const set = /* @__PURE__ */ new Set();
|
|
8839
|
+
if (deps !== "none") {
|
|
8840
|
+
for (const dep of deps.split(",").map((d) => d.trim().toLowerCase())) {
|
|
8841
|
+
const id = nameToId.get(dep);
|
|
8842
|
+
if (id) set.add(id);
|
|
9161
8843
|
}
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
8844
|
+
}
|
|
8845
|
+
allowed.set(layer.entryId, set);
|
|
8846
|
+
}
|
|
8847
|
+
return allowed;
|
|
8848
|
+
}
|
|
8849
|
+
function buildNodePrefixes(nodes) {
|
|
8850
|
+
const entries = [];
|
|
8851
|
+
for (const node of nodes) {
|
|
8852
|
+
for (const fp of parseFilePaths(node)) {
|
|
8853
|
+
entries.push({ prefix: normalize(fp), node });
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
entries.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
8857
|
+
return entries;
|
|
8858
|
+
}
|
|
8859
|
+
function parseFilePaths(node) {
|
|
8860
|
+
const raw = node.data?.filePaths;
|
|
8861
|
+
if (!raw || typeof raw !== "string") return [];
|
|
8862
|
+
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
8863
|
+
}
|
|
8864
|
+
function collectFiles(absPath) {
|
|
8865
|
+
if (!existsSync3(absPath)) return [];
|
|
8866
|
+
const stat = statSync(absPath);
|
|
8867
|
+
if (stat.isFile()) {
|
|
8868
|
+
return isScannableFile(absPath) ? [absPath] : [];
|
|
8869
|
+
}
|
|
8870
|
+
if (!stat.isDirectory()) return [];
|
|
8871
|
+
const results = [];
|
|
8872
|
+
const walk = (dir) => {
|
|
8873
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
8874
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8875
|
+
const full = resolve3(dir, entry.name);
|
|
8876
|
+
if (entry.isDirectory()) walk(full);
|
|
8877
|
+
else if (isScannableFile(full)) results.push(full);
|
|
8878
|
+
}
|
|
8879
|
+
};
|
|
8880
|
+
walk(absPath);
|
|
8881
|
+
return results;
|
|
8882
|
+
}
|
|
8883
|
+
function isScannableFile(p) {
|
|
8884
|
+
return /\.(ts|js|svelte)$/.test(p) && !p.endsWith(".d.ts");
|
|
8885
|
+
}
|
|
8886
|
+
function parseImports(filePath) {
|
|
8887
|
+
try {
|
|
8888
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
8889
|
+
const re = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
8890
|
+
const imports = [];
|
|
8891
|
+
let match;
|
|
8892
|
+
while ((match = re.exec(content)) !== null) {
|
|
8893
|
+
imports.push(match[1]);
|
|
8894
|
+
}
|
|
8895
|
+
return imports;
|
|
8896
|
+
} catch {
|
|
8897
|
+
return [];
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
var EXTENSIONS = [".ts", ".js", ".svelte", "/index.ts", "/index.js", "/index.svelte"];
|
|
8901
|
+
function tryResolveWithExtension(absPath) {
|
|
8902
|
+
if (existsSync3(absPath) && statSync(absPath).isFile()) return absPath;
|
|
8903
|
+
for (const ext of EXTENSIONS) {
|
|
8904
|
+
const withExt = absPath + ext;
|
|
8905
|
+
if (existsSync3(withExt)) return withExt;
|
|
8906
|
+
}
|
|
8907
|
+
return null;
|
|
8908
|
+
}
|
|
8909
|
+
function resolveImport(imp, fromFile, root) {
|
|
8910
|
+
let rel = null;
|
|
8911
|
+
if (imp.startsWith("$lib/")) rel = imp.replace("$lib/", "src/lib/");
|
|
8912
|
+
else if (imp.startsWith("$convex/")) rel = imp.replace("$convex/", "convex/");
|
|
8913
|
+
else if (imp.startsWith("$env/") || imp.startsWith("$app/")) return null;
|
|
8914
|
+
else if (imp.startsWith("./") || imp.startsWith("../")) {
|
|
8915
|
+
const fromDir = dirname2(fromFile);
|
|
8916
|
+
const abs2 = resolve3(fromDir, imp);
|
|
8917
|
+
rel = relative(root, abs2);
|
|
8918
|
+
}
|
|
8919
|
+
if (!rel) return null;
|
|
8920
|
+
const abs = resolve3(root, rel);
|
|
8921
|
+
const actual = tryResolveWithExtension(abs);
|
|
8922
|
+
return actual ? relative(root, actual) : rel;
|
|
8923
|
+
}
|
|
8924
|
+
function findNodeByPath(filePath, prefixes) {
|
|
8925
|
+
const normalized = normalize(filePath);
|
|
8926
|
+
for (const { prefix, node } of prefixes) {
|
|
8927
|
+
if (normalized.startsWith(prefix)) return node;
|
|
8928
|
+
}
|
|
8929
|
+
return null;
|
|
8930
|
+
}
|
|
8931
|
+
function formatScanReport(result) {
|
|
8932
|
+
const lines = [];
|
|
8933
|
+
if (result.violations.length === 0) {
|
|
8934
|
+
lines.push(
|
|
8935
|
+
`# Architecture Health Check Passed`,
|
|
8936
|
+
"",
|
|
8937
|
+
`**0 violations** across ${result.filesScanned} files (${result.importsChecked} imports checked, ${result.unmappedImports} unmapped).`,
|
|
8938
|
+
"",
|
|
8939
|
+
"All imports respect the layer dependency rules."
|
|
8940
|
+
);
|
|
8941
|
+
} else {
|
|
8942
|
+
lines.push(
|
|
8943
|
+
`# Architecture Health Check \u2014 ${result.violations.length} Violation${result.violations.length === 1 ? "" : "s"}`,
|
|
8944
|
+
"",
|
|
8945
|
+
`Scanned ${result.filesScanned} files, checked ${result.importsChecked} imports, found **${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}** (${result.unmappedImports} unmapped).`,
|
|
8946
|
+
""
|
|
8947
|
+
);
|
|
8948
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
8949
|
+
for (const v of result.violations) {
|
|
8950
|
+
if (!byNode.has(v.sourceNode)) byNode.set(v.sourceNode, []);
|
|
8951
|
+
byNode.get(v.sourceNode).push(v);
|
|
8952
|
+
}
|
|
8953
|
+
for (const [nodeName, vs] of byNode) {
|
|
8954
|
+
lines.push(`## ${nodeName} (${vs[0].sourceLayer})`);
|
|
8955
|
+
for (const v of vs) {
|
|
8956
|
+
lines.push(`- \`${v.sourceFile}\` imports \`${v.importPath}\` \u2192 **${v.targetNode}** (${v.targetLayer}) \u2014 ${v.rule}`);
|
|
9185
8957
|
}
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
8958
|
+
lines.push("");
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
lines.push("---", "");
|
|
8962
|
+
const nodeEntries = [...result.nodeResults.entries()];
|
|
8963
|
+
const cleanCount = nodeEntries.filter(([, r]) => r.violations.length === 0 && r.filesScanned > 0).length;
|
|
8964
|
+
const dirtyCount = nodeEntries.filter(([, r]) => r.violations.length > 0).length;
|
|
8965
|
+
const emptyCount = nodeEntries.filter(([, r]) => r.filesScanned === 0).length;
|
|
8966
|
+
lines.push(
|
|
8967
|
+
`**Summary:** ${cleanCount} clean nodes, ${dirtyCount} with violations, ${emptyCount} with no files.`
|
|
9194
8968
|
);
|
|
8969
|
+
return lines.join("\n");
|
|
9195
8970
|
}
|
|
9196
8971
|
|
|
9197
|
-
// src/
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
"
|
|
9203
|
-
"
|
|
8972
|
+
// src/tools/health.ts
|
|
8973
|
+
var CALL_CATEGORIES = {
|
|
8974
|
+
"chain.getEntry": "read",
|
|
8975
|
+
"chain.batchGetEntries": "read",
|
|
8976
|
+
"chain.listEntries": "read",
|
|
8977
|
+
"chain.listEntryHistory": "read",
|
|
8978
|
+
"chain.listEntryRelations": "read",
|
|
8979
|
+
"chain.listEntriesByLabel": "read",
|
|
8980
|
+
"chain.searchEntries": "search",
|
|
8981
|
+
"chain.createEntry": "write",
|
|
8982
|
+
"chain.updateEntry": "write",
|
|
8983
|
+
"chain.createEntryRelation": "write",
|
|
8984
|
+
"chain.applyLabel": "label",
|
|
8985
|
+
"chain.removeLabel": "label",
|
|
8986
|
+
"chain.createLabel": "label",
|
|
8987
|
+
"chain.updateLabel": "label",
|
|
8988
|
+
"chain.deleteLabel": "label",
|
|
8989
|
+
"chain.createCollection": "write",
|
|
8990
|
+
"chain.updateCollection": "write",
|
|
8991
|
+
"chain.listCollections": "meta",
|
|
8992
|
+
"chain.getCollection": "meta",
|
|
8993
|
+
"chain.listLabels": "meta",
|
|
8994
|
+
"resolveWorkspace": "meta"
|
|
9204
8995
|
};
|
|
9205
|
-
function
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
8996
|
+
function categorize(fn) {
|
|
8997
|
+
return CALL_CATEGORIES[fn] ?? "meta";
|
|
8998
|
+
}
|
|
8999
|
+
function formatDuration(ms) {
|
|
9000
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
9001
|
+
const mins = Math.floor(ms / 6e4);
|
|
9002
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
9003
|
+
return `${mins}m ${secs}s`;
|
|
9004
|
+
}
|
|
9005
|
+
function buildSessionSummary(log) {
|
|
9006
|
+
if (log.length === 0) return "";
|
|
9007
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
9008
|
+
let errorCount = 0;
|
|
9009
|
+
let writeCreates = 0;
|
|
9010
|
+
let writeUpdates = 0;
|
|
9011
|
+
for (const entry of log) {
|
|
9012
|
+
const cat = categorize(entry.fn);
|
|
9013
|
+
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
9014
|
+
const fnCounts = byCategory.get(cat);
|
|
9015
|
+
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
9016
|
+
if (entry.status === "error") errorCount++;
|
|
9017
|
+
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
9018
|
+
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
9019
|
+
}
|
|
9020
|
+
const firstTs = new Date(log[0].ts).getTime();
|
|
9021
|
+
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
9022
|
+
const duration = formatDuration(lastTs - firstTs);
|
|
9023
|
+
const lines = [`# Session Summary (${duration})
|
|
9024
|
+
`];
|
|
9025
|
+
const categoryLabels = [
|
|
9026
|
+
["read", "Reads"],
|
|
9027
|
+
["search", "Searches"],
|
|
9028
|
+
["write", "Writes"],
|
|
9029
|
+
["label", "Labels"],
|
|
9030
|
+
["meta", "Meta"]
|
|
9031
|
+
];
|
|
9032
|
+
for (const [cat, label] of categoryLabels) {
|
|
9033
|
+
const fnCounts = byCategory.get(cat);
|
|
9034
|
+
if (!fnCounts || fnCounts.size === 0) continue;
|
|
9035
|
+
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
9036
|
+
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
9037
|
+
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
9038
|
+
}
|
|
9039
|
+
lines.push(`- **Errors:** ${errorCount}`);
|
|
9040
|
+
if (writeCreates > 0 || writeUpdates > 0) {
|
|
9041
|
+
lines.push("");
|
|
9042
|
+
lines.push("## Knowledge Contribution");
|
|
9043
|
+
if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
|
|
9044
|
+
if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
|
|
9045
|
+
}
|
|
9046
|
+
return lines.join("\n");
|
|
9047
|
+
}
|
|
9048
|
+
function extractSessionEntryIds(priorSessions) {
|
|
9049
|
+
const allSeen = /* @__PURE__ */ new Set();
|
|
9050
|
+
const all = [];
|
|
9051
|
+
let lastSessionOnly = [];
|
|
9052
|
+
for (let i = 0; i < priorSessions.length; i++) {
|
|
9053
|
+
const s = priorSessions[i];
|
|
9054
|
+
const ids = [...s.entriesCreated ?? [], ...s.entriesModified ?? []].filter((id) => id);
|
|
9055
|
+
if (i === 0) {
|
|
9056
|
+
lastSessionOnly = [...new Set(ids)].slice(0, 5);
|
|
9057
|
+
}
|
|
9058
|
+
for (const id of ids) {
|
|
9059
|
+
if (!allSeen.has(id)) {
|
|
9060
|
+
allSeen.add(id);
|
|
9061
|
+
all.push(id);
|
|
9062
|
+
if (all.length >= 10) break;
|
|
9212
9063
|
}
|
|
9213
9064
|
}
|
|
9065
|
+
if (all.length >= 10) break;
|
|
9066
|
+
}
|
|
9067
|
+
return { all, lastSessionOnly };
|
|
9068
|
+
}
|
|
9069
|
+
function computeOrganisationHealth(entries) {
|
|
9070
|
+
let agreements = 0;
|
|
9071
|
+
let disagreements = 0;
|
|
9072
|
+
let abstentions = 0;
|
|
9073
|
+
const flagMap = /* @__PURE__ */ new Map();
|
|
9074
|
+
for (const entry of entries) {
|
|
9075
|
+
const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
9076
|
+
const description = typeof entry.data?.description === "string" ? entry.data.description : "";
|
|
9077
|
+
const result = classifyCollection(entry.name, description);
|
|
9078
|
+
if (!result) {
|
|
9079
|
+
abstentions++;
|
|
9080
|
+
continue;
|
|
9081
|
+
}
|
|
9082
|
+
if (result.collection === slug) {
|
|
9083
|
+
agreements++;
|
|
9084
|
+
} else {
|
|
9085
|
+
disagreements++;
|
|
9086
|
+
if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
|
|
9087
|
+
const suggestions = flagMap.get(slug);
|
|
9088
|
+
suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
9091
|
+
const opinionated = agreements + disagreements;
|
|
9092
|
+
const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
|
|
9093
|
+
const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
|
|
9094
|
+
const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
|
|
9095
|
+
const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
9096
|
+
return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
|
|
9097
|
+
}).sort((a, b) => b.count - a.count).slice(0, 3);
|
|
9098
|
+
return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
|
|
9099
|
+
}
|
|
9100
|
+
function formatOrgHealthLines(orgHealth, maxFlags = 3) {
|
|
9101
|
+
const lines = [];
|
|
9102
|
+
if (orgHealth.disagreements > 0) {
|
|
9103
|
+
lines.push(
|
|
9104
|
+
`${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
|
|
9105
|
+
);
|
|
9106
|
+
for (const flag of orgHealth.flags.slice(0, maxFlags)) {
|
|
9107
|
+
lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
|
|
9108
|
+
}
|
|
9109
|
+
} else if (orgHealth.reviewed > 0) {
|
|
9110
|
+
lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
|
|
9111
|
+
}
|
|
9112
|
+
return lines;
|
|
9113
|
+
}
|
|
9114
|
+
async function fetchOrganisationHealth() {
|
|
9115
|
+
try {
|
|
9116
|
+
const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
|
|
9117
|
+
if (!allEntries || allEntries.length === 0) return null;
|
|
9118
|
+
return computeOrganisationHealth(allEntries);
|
|
9119
|
+
} catch (err) {
|
|
9120
|
+
process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
|
|
9121
|
+
`);
|
|
9122
|
+
return null;
|
|
9123
|
+
}
|
|
9124
|
+
}
|
|
9125
|
+
async function handleHealthCheck() {
|
|
9126
|
+
const start = Date.now();
|
|
9127
|
+
const errors = [];
|
|
9128
|
+
let workspaceId;
|
|
9129
|
+
try {
|
|
9130
|
+
workspaceId = await getWorkspaceId();
|
|
9131
|
+
} catch (e) {
|
|
9132
|
+
errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
9133
|
+
}
|
|
9134
|
+
let collections = [];
|
|
9135
|
+
try {
|
|
9136
|
+
collections = await mcpQuery("chain.listCollections");
|
|
9137
|
+
} catch (e) {
|
|
9138
|
+
errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
9139
|
+
}
|
|
9140
|
+
let totalEntries = 0;
|
|
9141
|
+
if (collections.length > 0) {
|
|
9142
|
+
try {
|
|
9143
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
9144
|
+
totalEntries = entries.length;
|
|
9145
|
+
} catch (e) {
|
|
9146
|
+
errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
9147
|
+
}
|
|
9148
|
+
}
|
|
9149
|
+
let wsCtx = null;
|
|
9150
|
+
try {
|
|
9151
|
+
wsCtx = await getWorkspaceContext();
|
|
9152
|
+
} catch {
|
|
9153
|
+
}
|
|
9154
|
+
const durationMs = Date.now() - start;
|
|
9155
|
+
const healthy = errors.length === 0;
|
|
9156
|
+
const lines = [
|
|
9157
|
+
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
9158
|
+
"",
|
|
9159
|
+
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
9160
|
+
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
9161
|
+
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
9162
|
+
`**Collections:** ${collections.length}`,
|
|
9163
|
+
`**Entries:** ${totalEntries}`,
|
|
9164
|
+
`**Latency:** ${durationMs}ms`
|
|
9165
|
+
];
|
|
9166
|
+
if (errors.length > 0) {
|
|
9167
|
+
lines.push("", "## Errors");
|
|
9168
|
+
for (const err of errors) {
|
|
9169
|
+
lines.push(`- ${err}`);
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
const healthData = {
|
|
9173
|
+
healthy,
|
|
9174
|
+
collections: collections.length,
|
|
9175
|
+
entries: totalEntries,
|
|
9176
|
+
latencyMs: durationMs,
|
|
9177
|
+
workspace: workspaceId ?? "unresolved"
|
|
9178
|
+
};
|
|
9179
|
+
return {
|
|
9180
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9181
|
+
structuredContent: success(
|
|
9182
|
+
healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
|
|
9183
|
+
healthData
|
|
9184
|
+
)
|
|
9185
|
+
};
|
|
9186
|
+
}
|
|
9187
|
+
async function handleWhoami() {
|
|
9188
|
+
const ctx = await getWorkspaceContext();
|
|
9189
|
+
const sessionId = getAgentSessionId();
|
|
9190
|
+
const scope = getApiKeyScope();
|
|
9191
|
+
const oriented = isSessionOriented();
|
|
9192
|
+
const lines = [
|
|
9193
|
+
`# Session Identity`,
|
|
9194
|
+
"",
|
|
9195
|
+
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
9196
|
+
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
9197
|
+
`**Workspace Name:** ${ctx.workspaceName}`
|
|
9198
|
+
];
|
|
9199
|
+
return {
|
|
9200
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9201
|
+
structuredContent: success(
|
|
9202
|
+
`Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
|
|
9203
|
+
{ workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
|
|
9204
|
+
)
|
|
9205
|
+
};
|
|
9206
|
+
}
|
|
9207
|
+
var STAGE_LABELS = {
|
|
9208
|
+
blank: "Blank",
|
|
9209
|
+
seeded: "Seeded",
|
|
9210
|
+
grounded: "Grounded",
|
|
9211
|
+
connected: "Connected"
|
|
9212
|
+
};
|
|
9213
|
+
var STAGE_DESCRIPTIONS = {
|
|
9214
|
+
blank: "No knowledge captured yet.",
|
|
9215
|
+
seeded: "Early knowledge is in place \u2014 keep building.",
|
|
9216
|
+
grounded: "Solid foundations \u2014 a few gaps remain.",
|
|
9217
|
+
connected: "Well-connected knowledge graph \u2014 your Brain is useful."
|
|
9218
|
+
};
|
|
9219
|
+
async function handleWorkspaceStatus() {
|
|
9220
|
+
const result = await mcpQuery("chain.workspaceReadiness");
|
|
9221
|
+
const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
|
|
9222
|
+
const scoringVersion = result.scoringVersion ?? "v1";
|
|
9223
|
+
const stage = result.stage ?? "seeded";
|
|
9224
|
+
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
|
9225
|
+
const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
|
|
9226
|
+
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
9227
|
+
const lines = [
|
|
9228
|
+
`# Brain Status: ${stageLabel}`,
|
|
9229
|
+
`_${stageDescription}_`,
|
|
9230
|
+
"",
|
|
9231
|
+
`${scoreBar} ${stageLabel} \xB7 ${score}%`,
|
|
9232
|
+
`**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
|
|
9233
|
+
"",
|
|
9234
|
+
"## Stats",
|
|
9235
|
+
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
9236
|
+
`- **Relations:** ${stats.totalRelations}`,
|
|
9237
|
+
`- **Collections:** ${stats.collectionCount}`,
|
|
9238
|
+
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
9239
|
+
""
|
|
9240
|
+
];
|
|
9241
|
+
if (gaps.length > 0) {
|
|
9242
|
+
lines.push("## Gaps");
|
|
9243
|
+
for (const gap of gaps) {
|
|
9244
|
+
const action = gap.capabilityGuidance ?? gap.guidance;
|
|
9245
|
+
lines.push(`- [ ] **${gap.label}**`);
|
|
9246
|
+
lines.push(` _${action}_`);
|
|
9247
|
+
}
|
|
9248
|
+
lines.push("");
|
|
9214
9249
|
}
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
const wsRules = [];
|
|
9223
|
-
for (const p of (principles ?? []).filter((e) => e.status === "active" || e.status === "verified")) {
|
|
9224
|
-
wsRules.push({ id: p.entryId ?? "", name: p.name, severity: p.data?.severity ?? void 0, source: "principles" });
|
|
9250
|
+
const passed = checks.filter((c) => c.passed);
|
|
9251
|
+
if (passed.length > 0) {
|
|
9252
|
+
lines.push("## Passing checks");
|
|
9253
|
+
for (const check of passed) {
|
|
9254
|
+
lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
|
|
9255
|
+
}
|
|
9256
|
+
lines.push("");
|
|
9225
9257
|
}
|
|
9226
|
-
|
|
9227
|
-
|
|
9258
|
+
const orgHealth = await fetchOrganisationHealth();
|
|
9259
|
+
if (orgHealth && orgHealth.reviewed > 0) {
|
|
9260
|
+
lines.push("## Organisation Health");
|
|
9261
|
+
lines.push(...formatOrgHealthLines(orgHealth));
|
|
9262
|
+
lines.push("");
|
|
9228
9263
|
}
|
|
9229
|
-
|
|
9230
|
-
|
|
9264
|
+
const statusData = {
|
|
9265
|
+
stage,
|
|
9266
|
+
scoringVersion,
|
|
9267
|
+
readinessScore: score,
|
|
9268
|
+
activeEntries: stats.activeCount,
|
|
9269
|
+
totalRelations: stats.totalRelations,
|
|
9270
|
+
orphanedEntries: stats.orphanedCount,
|
|
9271
|
+
gaps: gaps.map((g) => ({
|
|
9272
|
+
id: g.id,
|
|
9273
|
+
label: g.label,
|
|
9274
|
+
guidance: g.capabilityGuidance ?? g.guidance
|
|
9275
|
+
})),
|
|
9276
|
+
...orgHealth && { organisationHealth: orgHealth }
|
|
9277
|
+
};
|
|
9278
|
+
return {
|
|
9279
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9280
|
+
structuredContent: success(
|
|
9281
|
+
`Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
|
|
9282
|
+
statusData
|
|
9283
|
+
)
|
|
9284
|
+
};
|
|
9285
|
+
}
|
|
9286
|
+
async function handleAudit(limit) {
|
|
9287
|
+
const log = getAuditLog();
|
|
9288
|
+
const recent = log.slice(-limit);
|
|
9289
|
+
if (recent.length === 0) {
|
|
9290
|
+
return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
|
|
9231
9291
|
}
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
` + ruleLines + '\n\nUse `entries action=get entryId="<ID>"` to drill into any rule before making changes in that area.'
|
|
9242
|
-
);
|
|
9243
|
-
} else {
|
|
9244
|
-
sections.push(
|
|
9245
|
-
"## 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."
|
|
9246
|
-
);
|
|
9292
|
+
const summary = buildSessionSummary(log);
|
|
9293
|
+
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
9294
|
+
`];
|
|
9295
|
+
for (const entry of recent) {
|
|
9296
|
+
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
9297
|
+
const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
9298
|
+
const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
|
|
9299
|
+
logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
|
|
9247
9300
|
}
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
sections.push(
|
|
9260
|
-
`## Data Model (${collections.length} collections)
|
|
9261
|
-
Unified entries model: collections define field schemas, entries hold data in a flexible \`data\` field.
|
|
9262
|
-
The \`data\` field is polymorphic: plain fields for generic entries, \`ChainData\` (chainTypeId + links) for processes, \`MapData\` (templateId + slots) for maps.
|
|
9263
|
-
Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, versions via \`entryVersions\`, labels via \`labels\` + \`entryLabels\`.
|
|
9301
|
+
const auditData = {
|
|
9302
|
+
totalCalls: log.length,
|
|
9303
|
+
calls: recent.map((entry) => ({
|
|
9304
|
+
tool: entry.fn,
|
|
9305
|
+
...entry.toolContext?.action && { action: entry.toolContext.action },
|
|
9306
|
+
timestamp: entry.ts,
|
|
9307
|
+
...entry.durationMs != null && { durationMs: entry.durationMs }
|
|
9308
|
+
}))
|
|
9309
|
+
};
|
|
9310
|
+
return {
|
|
9311
|
+
content: [{ type: "text", text: `${summary}
|
|
9264
9312
|
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9313
|
+
---
|
|
9314
|
+
|
|
9315
|
+
${logLines.join("\n")}` }],
|
|
9316
|
+
structuredContent: success(
|
|
9317
|
+
`Audit: ${log.length} total calls, showing last ${recent.length}.`,
|
|
9318
|
+
auditData
|
|
9319
|
+
)
|
|
9320
|
+
};
|
|
9321
|
+
}
|
|
9322
|
+
var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
|
|
9323
|
+
var healthSchema = z20.object({
|
|
9324
|
+
action: z20.enum(HEALTH_ACTIONS).describe(
|
|
9325
|
+
"'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
|
|
9326
|
+
),
|
|
9327
|
+
limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
|
|
9328
|
+
});
|
|
9329
|
+
var orientSchema = z20.object({
|
|
9330
|
+
mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
|
|
9331
|
+
task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
|
|
9332
|
+
});
|
|
9333
|
+
var healthCheckOutputSchema = z20.object({
|
|
9334
|
+
healthy: z20.boolean(),
|
|
9335
|
+
collections: z20.number(),
|
|
9336
|
+
entries: z20.number(),
|
|
9337
|
+
latencyMs: z20.number(),
|
|
9338
|
+
workspace: z20.string()
|
|
9339
|
+
});
|
|
9340
|
+
var organisationHealthSchema = z20.object({
|
|
9341
|
+
reviewed: z20.number(),
|
|
9342
|
+
agreements: z20.number(),
|
|
9343
|
+
disagreements: z20.number(),
|
|
9344
|
+
abstentions: z20.number(),
|
|
9345
|
+
agreementRate: z20.number(),
|
|
9346
|
+
flags: z20.array(z20.object({
|
|
9347
|
+
collection: z20.string(),
|
|
9348
|
+
count: z20.number(),
|
|
9349
|
+
suggestedCollection: z20.string()
|
|
9350
|
+
}))
|
|
9351
|
+
});
|
|
9352
|
+
var healthStatusOutputSchema = z20.object({
|
|
9353
|
+
stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
|
|
9354
|
+
scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
|
|
9355
|
+
readinessScore: z20.number(),
|
|
9356
|
+
activeEntries: z20.number(),
|
|
9357
|
+
totalRelations: z20.number(),
|
|
9358
|
+
orphanedEntries: z20.number(),
|
|
9359
|
+
gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() })),
|
|
9360
|
+
organisationHealth: organisationHealthSchema.optional()
|
|
9361
|
+
});
|
|
9362
|
+
var healthAuditOutputSchema = z20.object({
|
|
9363
|
+
totalCalls: z20.number(),
|
|
9364
|
+
calls: z20.array(z20.object({
|
|
9365
|
+
tool: z20.string(),
|
|
9366
|
+
action: z20.string().optional(),
|
|
9367
|
+
timestamp: z20.string(),
|
|
9368
|
+
durationMs: z20.number().optional()
|
|
9369
|
+
}))
|
|
9370
|
+
});
|
|
9371
|
+
var healthWhoamiOutputSchema = z20.object({
|
|
9372
|
+
workspaceId: z20.string(),
|
|
9373
|
+
workspaceName: z20.string(),
|
|
9374
|
+
scope: z20.string(),
|
|
9375
|
+
sessionId: z20.union([z20.string(), z20.null()]),
|
|
9376
|
+
oriented: z20.boolean()
|
|
9377
|
+
});
|
|
9378
|
+
var ALL_TOOL_SCHEMAS = [
|
|
9379
|
+
{ name: "entries", schema: entriesSchema },
|
|
9380
|
+
{ name: "relations", schema: relationsSchema },
|
|
9381
|
+
{ name: "graph", schema: graphSchema },
|
|
9382
|
+
{ name: "context", schema: contextSchema },
|
|
9383
|
+
{ name: "collections", schema: collectionsSchema },
|
|
9384
|
+
{ name: "session", schema: sessionSchema },
|
|
9385
|
+
{ name: "health", schema: healthSchema },
|
|
9386
|
+
{ name: "orient", schema: orientSchema },
|
|
9387
|
+
{ name: "quality", schema: qualitySchema },
|
|
9388
|
+
{ name: "workflows", schema: workflowsSchema },
|
|
9389
|
+
{ name: "session-wrapup", schema: wrapupSchema },
|
|
9390
|
+
{ name: "labels", schema: labelsSchema },
|
|
9391
|
+
{ name: "verify", schema: verifySchema },
|
|
9392
|
+
{ name: "capture", schema: captureSchema },
|
|
9393
|
+
{ name: "batch-capture", schema: batchCaptureSchema },
|
|
9394
|
+
{ name: "update-entry", schema: updateEntrySchema },
|
|
9395
|
+
{ name: "get-history", schema: getHistorySchema },
|
|
9396
|
+
{ name: "commit-entry", schema: commitEntrySchema },
|
|
9397
|
+
{ name: "start", schema: startSchema },
|
|
9398
|
+
{ name: "get-usage-summary", schema: usageSummarySchema },
|
|
9399
|
+
{ name: "chain", schema: chainSchema },
|
|
9400
|
+
{ name: "chain-version", schema: chainVersionSchema },
|
|
9401
|
+
{ name: "chain-branch", schema: chainBranchSchema },
|
|
9402
|
+
{ name: "chain-review", schema: chainReviewSchema },
|
|
9403
|
+
{ name: "create-audience-map-set", schema: createAudienceMapSetSchema },
|
|
9404
|
+
{ name: "map", schema: mapSchema },
|
|
9405
|
+
{ name: "map-slot", schema: mapSlotSchema },
|
|
9406
|
+
{ name: "map-version", schema: mapVersionSchema },
|
|
9407
|
+
{ name: "map-suggest", schema: mapSuggestSchema },
|
|
9408
|
+
{ name: "architecture", schema: architectureSchema },
|
|
9409
|
+
{ name: "architecture-admin", schema: architectureAdminSchema },
|
|
9410
|
+
{ name: "facilitate", schema: facilitateSchema }
|
|
9411
|
+
];
|
|
9412
|
+
var selfTestOutputSchema = z20.object({
|
|
9413
|
+
passed: z20.number(),
|
|
9414
|
+
failed: z20.number(),
|
|
9415
|
+
total: z20.number(),
|
|
9416
|
+
results: z20.array(z20.object({
|
|
9417
|
+
tool: z20.string(),
|
|
9418
|
+
valid: z20.boolean(),
|
|
9419
|
+
error: z20.string().optional()
|
|
9420
|
+
}))
|
|
9421
|
+
});
|
|
9422
|
+
function handleSelfTest() {
|
|
9423
|
+
const results = [];
|
|
9424
|
+
for (const { name, schema } of ALL_TOOL_SCHEMAS) {
|
|
9425
|
+
try {
|
|
9426
|
+
if (!schema || typeof schema.safeParse !== "function") {
|
|
9427
|
+
results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
|
|
9428
|
+
continue;
|
|
9429
|
+
}
|
|
9430
|
+
const test = schema.safeParse({});
|
|
9431
|
+
if (test.success || test.error) {
|
|
9432
|
+
results.push({ tool: name, valid: true });
|
|
9433
|
+
}
|
|
9434
|
+
} catch (e) {
|
|
9435
|
+
results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
|
|
9436
|
+
}
|
|
9271
9437
|
}
|
|
9272
|
-
const
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
);
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9438
|
+
const passed = results.filter((r) => r.valid).length;
|
|
9439
|
+
const failed = results.filter((r) => !r.valid).length;
|
|
9440
|
+
const total = results.length;
|
|
9441
|
+
const lines = [
|
|
9442
|
+
`# Self-Test: Tool Schema Validation`,
|
|
9443
|
+
`**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
|
|
9444
|
+
`**Schemas validated:** ${passed}/${total}`,
|
|
9445
|
+
""
|
|
9446
|
+
];
|
|
9447
|
+
if (failed > 0) {
|
|
9448
|
+
lines.push("## Failures");
|
|
9449
|
+
for (const r of results.filter((r2) => !r2.valid)) {
|
|
9450
|
+
lines.push(`- **${r.tool}**: ${r.error}`);
|
|
9451
|
+
}
|
|
9452
|
+
lines.push("");
|
|
9453
|
+
}
|
|
9454
|
+
lines.push("## All Tools");
|
|
9455
|
+
for (const r of results) {
|
|
9456
|
+
lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
|
|
9457
|
+
}
|
|
9458
|
+
return {
|
|
9459
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9460
|
+
structuredContent: success(
|
|
9461
|
+
failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
|
|
9462
|
+
{ passed, failed, total, results }
|
|
9463
|
+
)
|
|
9464
|
+
};
|
|
9299
9465
|
}
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
|
|
9322
|
-
DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
|
|
9323
|
-
BET (bets), INS (insights), ARCH (architecture), CIR (circles),
|
|
9324
|
-
ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
|
|
9325
|
-
|
|
9326
|
-
## Valid Relation Types (21)
|
|
9327
|
-
informs, governs, surfaces_tension_in, defines_term_for, belongs_to,
|
|
9328
|
-
references, related_to, fills_slot, commits_to, informed_by, depends_on,
|
|
9329
|
-
conflicts_with, confused_with, replaces, part_of, constrains,
|
|
9330
|
-
governed_by, alternative_to, has_proposal, requests_promotion_of, resolves
|
|
9331
|
-
|
|
9332
|
-
## Lifecycle Status
|
|
9333
|
-
All entries: \`draft\` | \`active\` | \`deprecated\` | \`archived\`
|
|
9334
|
-
|
|
9335
|
-
## Workflow Status (per collection)
|
|
9336
|
-
- **tensions:** open \u2192 processing \u2192 decided \u2192 closed
|
|
9337
|
-
- **decisions:** pending \u2192 decided
|
|
9338
|
-
- **bets:** shaped \u2192 bet \u2192 building \u2192 shipped
|
|
9339
|
-
- **business-rules:** active | conflict | review
|
|
9340
|
-
|
|
9341
|
-
## Key Patterns
|
|
9342
|
-
- **Capture flow:** \`capture\` \u2192 \`graph action=suggest\` \u2192 \`relations action=batch-create\` \u2192 \`commit-entry\`
|
|
9343
|
-
- Use \`workflowStatus\` (not \`status\`) for domain workflow state
|
|
9344
|
-
- \`data\` param is merged (not replaced) \u2014 safe for partial updates
|
|
9345
|
-
`;
|
|
9346
|
-
function registerResources(server) {
|
|
9347
|
-
server.resource(
|
|
9348
|
-
"agent-cheatsheet",
|
|
9349
|
-
"productbrain://agent-cheatsheet",
|
|
9350
|
-
async (uri) => ({
|
|
9351
|
-
contents: [{
|
|
9352
|
-
uri: uri.href,
|
|
9353
|
-
text: AGENT_CHEATSHEET,
|
|
9354
|
-
mimeType: "text/markdown"
|
|
9355
|
-
}]
|
|
9466
|
+
function registerHealthTools(server) {
|
|
9467
|
+
server.registerTool(
|
|
9468
|
+
"health",
|
|
9469
|
+
{
|
|
9470
|
+
title: "Health",
|
|
9471
|
+
description: "Diagnostics and session identity. Four actions:\n\n- **check**: Verify connectivity \u2014 workspace, collections, entries, latency.\n- **whoami**: Session identity \u2014 workspace ID, slug, name.\n- **status**: Workspace readiness \u2014 score, gaps, stats (entries, relations, orphans).\n- **audit**: Session audit log \u2014 last N backend calls with summary.",
|
|
9472
|
+
inputSchema: healthSchema,
|
|
9473
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
9474
|
+
},
|
|
9475
|
+
withEnvelope(async (args) => {
|
|
9476
|
+
const parsed = parseOrFail(healthSchema, args);
|
|
9477
|
+
if (!parsed.ok) return parsed.result;
|
|
9478
|
+
const { action, limit } = parsed.data;
|
|
9479
|
+
return runWithToolContext({ tool: "health", action }, async () => {
|
|
9480
|
+
if (action === "check") return handleHealthCheck();
|
|
9481
|
+
if (action === "whoami") return handleWhoami();
|
|
9482
|
+
if (action === "status") return handleWorkspaceStatus();
|
|
9483
|
+
if (action === "audit") return handleAudit(limit ?? 20);
|
|
9484
|
+
if (action === "self-test") return handleSelfTest();
|
|
9485
|
+
return unknownAction(action, HEALTH_ACTIONS);
|
|
9486
|
+
});
|
|
9356
9487
|
})
|
|
9357
9488
|
);
|
|
9358
|
-
server.
|
|
9359
|
-
"
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9489
|
+
server.registerTool(
|
|
9490
|
+
"orient",
|
|
9491
|
+
{
|
|
9492
|
+
title: "Orient \u2014 Start Here",
|
|
9493
|
+
description: "The single entry point for starting a session. Returns workspace context with a single recommended next action for low-readiness workspaces, or a standup-style briefing for established workspaces.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.\n\n**mode:** `full` (default) returns full context. `brief` returns only vision, bet/tension counts, readiness, active bet names, and last-session summary \u2014 use for mid-session re-orientation.\n\n**task:** Optional natural-language task description. When provided, returns task-scoped context (scored, relevant entries) in addition to standard orient sections.",
|
|
9494
|
+
inputSchema: orientSchema,
|
|
9495
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
9496
|
+
},
|
|
9497
|
+
withEnvelope(async ({ mode = "full", task } = {}) => {
|
|
9498
|
+
const errors = [];
|
|
9499
|
+
const agentSessionId = getAgentSessionId();
|
|
9500
|
+
if (isSessionOriented() && mode === "brief" && !task) {
|
|
9501
|
+
return {
|
|
9502
|
+
content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
|
|
9503
|
+
structuredContent: success(
|
|
9504
|
+
"Already oriented. Session active, writes unlocked.",
|
|
9505
|
+
{ alreadyOriented: true, sessionId: agentSessionId }
|
|
9506
|
+
)
|
|
9507
|
+
};
|
|
9508
|
+
}
|
|
9509
|
+
let wsCtx = null;
|
|
9510
|
+
try {
|
|
9511
|
+
wsCtx = await getWorkspaceContext();
|
|
9512
|
+
} catch (e) {
|
|
9513
|
+
errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
|
|
9514
|
+
}
|
|
9515
|
+
let priorSessions = [];
|
|
9516
|
+
let recoveryBlock = null;
|
|
9517
|
+
if (wsCtx) {
|
|
9518
|
+
try {
|
|
9519
|
+
const sessionsResult = await mcpQuery("agent.recentSessions", { limit: 3 });
|
|
9520
|
+
priorSessions = sessionsResult?.sessions ?? [];
|
|
9521
|
+
recoveryBlock = sessionsResult?.recoveryBlock ?? null;
|
|
9522
|
+
} catch {
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
const { all: sessionEntryIds, lastSessionOnly } = extractSessionEntryIds(priorSessions);
|
|
9526
|
+
let orientEntries = null;
|
|
9527
|
+
try {
|
|
9528
|
+
const orientArgs = {};
|
|
9529
|
+
if (task) orientArgs.task = task;
|
|
9530
|
+
if (sessionEntryIds.length > 0) orientArgs.sessionEntryIds = sessionEntryIds;
|
|
9531
|
+
if (lastSessionOnly.length > 0) orientArgs.lastSessionEntryIds = lastSessionOnly;
|
|
9532
|
+
orientEntries = await mcpQuery("chain.getOrientEntries", orientArgs);
|
|
9533
|
+
} catch {
|
|
9534
|
+
}
|
|
9535
|
+
let openTensions = [];
|
|
9536
|
+
try {
|
|
9537
|
+
const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
|
|
9538
|
+
openTensions = (tensions ?? []).filter((e) => e.workflowStatus === "open");
|
|
9539
|
+
} catch {
|
|
9540
|
+
}
|
|
9541
|
+
let readiness = null;
|
|
9542
|
+
try {
|
|
9543
|
+
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
9544
|
+
} catch (e) {
|
|
9545
|
+
errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
|
|
9546
|
+
}
|
|
9547
|
+
if (readiness?.stage === "blank") {
|
|
9548
|
+
const scanLines = [
|
|
9549
|
+
`# Welcome to ${wsCtx?.workspaceName ?? "your workspace"}`,
|
|
9550
|
+
"",
|
|
9551
|
+
"Your workspace is fresh \u2014 let's set it up.",
|
|
9552
|
+
"",
|
|
9553
|
+
"**Recommended:** call the `start` tool instead of `orient` for the best first-run experience.",
|
|
9554
|
+
"It will guide you through setting up your workspace \u2014 you can tell me about your product, scan your codebase, or use a preset.",
|
|
9555
|
+
"",
|
|
9556
|
+
"Or tell me about your product and I'll start capturing knowledge directly."
|
|
9557
|
+
];
|
|
9558
|
+
let oriented = false;
|
|
9559
|
+
let orientationStatus = "no_session";
|
|
9560
|
+
if (agentSessionId) {
|
|
9561
|
+
try {
|
|
9562
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
9563
|
+
setSessionOriented(true);
|
|
9564
|
+
oriented = true;
|
|
9565
|
+
orientationStatus = "complete";
|
|
9566
|
+
} catch {
|
|
9567
|
+
orientationStatus = "failed";
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
return {
|
|
9571
|
+
content: [{ type: "text", text: scanLines.join("\n") }],
|
|
9572
|
+
structuredContent: success(
|
|
9573
|
+
"Workspace is blank. Use the start tool for guided setup.",
|
|
9574
|
+
{ stage: "blank", readinessScore: readiness?.score ?? 0, oriented, orientationStatus, redirectHint: "Use the `start` tool for the full guided setup experience." },
|
|
9575
|
+
[{ tool: "start", description: "Guided workspace setup", parameters: {} }]
|
|
9576
|
+
)
|
|
9577
|
+
};
|
|
9578
|
+
}
|
|
9579
|
+
const lines = [];
|
|
9580
|
+
const isLowReadiness = readiness && readiness.score < 50;
|
|
9581
|
+
if (wsCtx) {
|
|
9582
|
+
lines.push(`# ${wsCtx.workspaceName}`);
|
|
9583
|
+
lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
|
|
9584
|
+
} else {
|
|
9585
|
+
lines.push("# Workspace");
|
|
9586
|
+
lines.push("_Could not resolve workspace._");
|
|
9587
|
+
}
|
|
9588
|
+
lines.push("");
|
|
9589
|
+
if (mode === "brief") {
|
|
9590
|
+
const briefStage = readiness?.stage ?? (readiness?.score != null ? readiness.score < 50 ? "seeded" : "grounded" : "unknown");
|
|
9591
|
+
lines.push(`Brain stage: ${briefStage}`);
|
|
9592
|
+
if (orientEntries?.strategicContext) {
|
|
9593
|
+
const sc = orientEntries.strategicContext;
|
|
9594
|
+
if (sc.vision) lines.push(`Vision: ${sc.vision}`);
|
|
9595
|
+
if (sc.purpose) lines.push(`Purpose: ${sc.purpose}`);
|
|
9596
|
+
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
9597
|
+
lines.push(`Product areas (${sc.productAreaCount}): ${(sc.productAreas ?? []).join(", ")}`);
|
|
9598
|
+
}
|
|
9599
|
+
lines.push(`${sc.activeBetCount} active bet(s), ${sc.activeTensionCount} tension(s).`);
|
|
9401
9600
|
}
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9601
|
+
if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
|
|
9602
|
+
lines.push(`Task context: ${orientEntries.taskContext.totalFound} relevant entries (${orientEntries.taskContext.confidence} confidence).`);
|
|
9603
|
+
}
|
|
9604
|
+
if (orientEntries?.activeBets?.length > 0) {
|
|
9605
|
+
for (const e of orientEntries.activeBets) {
|
|
9606
|
+
const tensions = e.linkedTensions;
|
|
9607
|
+
const tensionPart = tensions?.length ? ` \u2014 ${tensions.map((t) => `${t.entryId ?? t.name} (${t.severity ?? "\u2014"})`).join(", ")}` : "";
|
|
9608
|
+
lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
|
|
9609
|
+
}
|
|
9610
|
+
}
|
|
9611
|
+
if (recoveryBlock) {
|
|
9612
|
+
lines.push("");
|
|
9613
|
+
lines.push(...formatRecoveryBlock(recoveryBlock));
|
|
9614
|
+
} else if (priorSessions.length > 0) {
|
|
9615
|
+
const last = priorSessions[0];
|
|
9616
|
+
const date = new Date(last.startedAt).toISOString().split("T")[0];
|
|
9617
|
+
const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
|
|
9618
|
+
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
9619
|
+
lines.push(`Last session (${date}): ${created} created, ${modified} modified`);
|
|
9620
|
+
}
|
|
9621
|
+
if (orientEntries) {
|
|
9622
|
+
const mapGovernanceEntry = (e) => ({
|
|
9623
|
+
entryId: e.entryId,
|
|
9624
|
+
name: e.name,
|
|
9625
|
+
description: typeof e.preview === "string" ? e.preview : void 0
|
|
9626
|
+
});
|
|
9627
|
+
lines.push("");
|
|
9628
|
+
lines.push(...buildOperatingProtocol({
|
|
9629
|
+
principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
|
|
9630
|
+
standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
|
|
9631
|
+
businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
|
|
9632
|
+
}, task));
|
|
9633
|
+
}
|
|
9634
|
+
if (agentSessionId) {
|
|
9635
|
+
try {
|
|
9636
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
9637
|
+
setSessionOriented(true);
|
|
9638
|
+
} catch {
|
|
9639
|
+
}
|
|
9640
|
+
lines.push("---");
|
|
9641
|
+
lines.push(`Orientation complete. Session ${agentSessionId}.`);
|
|
9412
9642
|
} else {
|
|
9413
|
-
lines.push("
|
|
9643
|
+
lines.push("---");
|
|
9644
|
+
lines.push("_No active agent session. Call `session action=start` to begin._");
|
|
9645
|
+
}
|
|
9646
|
+
if (errors.length > 0) {
|
|
9647
|
+
lines.push("");
|
|
9648
|
+
for (const err of errors) lines.push(`- ${err}`);
|
|
9414
9649
|
}
|
|
9415
|
-
} else {
|
|
9416
|
-
lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
|
|
9417
|
-
}
|
|
9418
|
-
return {
|
|
9419
|
-
contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
|
|
9420
|
-
};
|
|
9421
|
-
}
|
|
9422
|
-
);
|
|
9423
|
-
server.resource(
|
|
9424
|
-
"chain-collections",
|
|
9425
|
-
"productbrain://collections",
|
|
9426
|
-
async (uri) => {
|
|
9427
|
-
const collections = await mcpQuery("chain.listCollections") ?? [];
|
|
9428
|
-
if (collections.length === 0) {
|
|
9429
|
-
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" }] };
|
|
9430
|
-
}
|
|
9431
|
-
const formatted = collections.map((c) => {
|
|
9432
|
-
const fieldList = (c.fields ?? []).map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
|
|
9433
|
-
return `## ${c.icon ?? ""} ${c.name} (\`${c.slug}\`)
|
|
9434
|
-
${c.description || ""}
|
|
9435
|
-
|
|
9436
|
-
**Fields:**
|
|
9437
|
-
${fieldList}`;
|
|
9438
|
-
}).join("\n\n---\n\n");
|
|
9439
|
-
return {
|
|
9440
|
-
contents: [{ uri: uri.href, text: `# Knowledge Collections (${collections.length})
|
|
9441
|
-
|
|
9442
|
-
${formatted}`, mimeType: "text/markdown" }]
|
|
9443
|
-
};
|
|
9444
|
-
}
|
|
9445
|
-
);
|
|
9446
|
-
server.resource(
|
|
9447
|
-
"chain-collection-entries",
|
|
9448
|
-
new ResourceTemplate("productbrain://{slug}/entries", {
|
|
9449
|
-
list: async () => {
|
|
9450
|
-
const collections = await mcpQuery("chain.listCollections") ?? [];
|
|
9451
9650
|
return {
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9651
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9652
|
+
structuredContent: success(
|
|
9653
|
+
`Oriented (brief). Stage: ${readiness?.stage ?? "unknown"}.`,
|
|
9654
|
+
{ mode: "brief", stage: readiness?.stage ?? "unknown", oriented: isSessionOriented(), sessionId: agentSessionId }
|
|
9655
|
+
)
|
|
9456
9656
|
};
|
|
9457
9657
|
}
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
mimeType: "text/markdown"
|
|
9467
|
-
}]
|
|
9468
|
-
};
|
|
9469
|
-
}
|
|
9470
|
-
);
|
|
9471
|
-
server.resource(
|
|
9472
|
-
"chain-labels",
|
|
9473
|
-
"productbrain://labels",
|
|
9474
|
-
async (uri) => {
|
|
9475
|
-
const labels = await mcpQuery("chain.listLabels") ?? [];
|
|
9476
|
-
if (labels.length === 0) {
|
|
9477
|
-
return { contents: [{ uri: uri.href, text: "No labels in this workspace.", mimeType: "text/markdown" }] };
|
|
9658
|
+
const orientStage = readiness?.stage ?? "seeded";
|
|
9659
|
+
if (isLowReadiness && wsCtx?.createdAt) {
|
|
9660
|
+
const ageDays = Math.floor((Date.now() - wsCtx.createdAt) / (1e3 * 60 * 60 * 24));
|
|
9661
|
+
if (ageDays >= 30) {
|
|
9662
|
+
lines.push(`Your workspace has been around for ${ageDays} days and is still at the **${orientStage}** stage.`);
|
|
9663
|
+
lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
|
|
9664
|
+
lines.push("");
|
|
9665
|
+
}
|
|
9478
9666
|
}
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
lines.push(
|
|
9667
|
+
if (isLowReadiness) {
|
|
9668
|
+
lines.push(`**Brain stage: ${orientStage}.**`);
|
|
9669
|
+
lines.push("");
|
|
9670
|
+
const gaps = readiness.gaps ?? [];
|
|
9671
|
+
if (gaps.length > 0) {
|
|
9672
|
+
const gap = gaps[0];
|
|
9673
|
+
const cta = gap.capabilityGuidance ?? gap.guidance ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
|
|
9674
|
+
lines.push("## Recommended next step");
|
|
9675
|
+
lines.push(`**${gap.label}**`);
|
|
9676
|
+
lines.push("");
|
|
9677
|
+
lines.push(cta);
|
|
9678
|
+
lines.push("");
|
|
9679
|
+
lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
|
|
9680
|
+
lines.push("");
|
|
9681
|
+
const remainingGaps = gaps.length - 1;
|
|
9682
|
+
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
9683
|
+
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._`);
|
|
9684
|
+
lines.push("");
|
|
9685
|
+
}
|
|
9686
|
+
}
|
|
9687
|
+
lines.push("_Need a collection that doesn't exist yet (e.g. audiences, personas, metrics)?_");
|
|
9688
|
+
lines.push("_Use `collections action=create` to add it, or ask me to propose collections for your domain._");
|
|
9689
|
+
lines.push("");
|
|
9690
|
+
} else if (readiness) {
|
|
9691
|
+
lines.push(`**Brain stage: ${orientStage}.**`);
|
|
9692
|
+
lines.push("");
|
|
9693
|
+
if (orientEntries?.strategicContext) {
|
|
9694
|
+
const sc = orientEntries.strategicContext;
|
|
9695
|
+
lines.push("## Strategic Context");
|
|
9696
|
+
if (sc.vision) lines.push(`**Vision:** ${sc.vision}`);
|
|
9697
|
+
if (sc.purpose) lines.push(`**Purpose:** ${sc.purpose}`);
|
|
9698
|
+
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
9699
|
+
lines.push(`**Product areas (${sc.productAreaCount}):** ${(sc.productAreas ?? []).join(", ")}`);
|
|
9700
|
+
}
|
|
9701
|
+
const betLine = sc.currentBet ? `**Current bet:** ${sc.currentBet}. ${sc.activeBetCount} active bet(s).` : "No active bets.";
|
|
9702
|
+
lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
|
|
9703
|
+
lines.push("");
|
|
9704
|
+
}
|
|
9705
|
+
if (orientEntries?.continuingFrom && orientEntries.continuingFrom.length > 0) {
|
|
9706
|
+
lines.push("## Continuing from");
|
|
9707
|
+
lines.push("_Prior-session entries most relevant to your task._");
|
|
9708
|
+
lines.push("");
|
|
9709
|
+
for (const e of orientEntries.continuingFrom) {
|
|
9710
|
+
const id = e.entryId ?? e.name;
|
|
9711
|
+
const type = e.canonicalKey ?? "generic";
|
|
9712
|
+
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9713
|
+
lines.push(`- \`${id}\` (score ${e.score}) [${type}]${coll} \u2014 ${e.name}`);
|
|
9714
|
+
if (e.reasoning) lines.push(` _${e.reasoning}_`);
|
|
9715
|
+
}
|
|
9716
|
+
lines.push("");
|
|
9717
|
+
}
|
|
9718
|
+
if (orientEntries?.lastSessionTouched && orientEntries.lastSessionTouched.length > 0) {
|
|
9719
|
+
lines.push("## Last session touched");
|
|
9720
|
+
lines.push("_Entries created or modified in your most recent session._");
|
|
9721
|
+
lines.push("");
|
|
9722
|
+
for (const e of orientEntries.lastSessionTouched) {
|
|
9723
|
+
const id = e.entryId ?? e.name;
|
|
9724
|
+
const type = e.canonicalKey ?? "generic";
|
|
9725
|
+
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9726
|
+
lines.push(`- \`${id}\` [${type}]${coll} \u2014 ${e.name}`);
|
|
9727
|
+
}
|
|
9728
|
+
lines.push("");
|
|
9729
|
+
}
|
|
9730
|
+
if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
|
|
9731
|
+
const tc = orientEntries.taskContext;
|
|
9732
|
+
lines.push("## Task Context");
|
|
9733
|
+
lines.push(`_Task-scoped entries (${tc.confidence} confidence, ${tc.totalFound} relevant)`);
|
|
9734
|
+
lines.push("");
|
|
9735
|
+
for (const e of tc.context) {
|
|
9736
|
+
const id = e.entryId ?? e.name;
|
|
9737
|
+
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9738
|
+
lines.push(`- \`${id}\` (score ${e.score})${coll}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
|
|
9739
|
+
}
|
|
9740
|
+
lines.push("");
|
|
9741
|
+
}
|
|
9742
|
+
if (task && orientEntries) {
|
|
9743
|
+
const result = runAlignmentCheck(
|
|
9744
|
+
task,
|
|
9745
|
+
orientEntries.activeBets ?? [],
|
|
9746
|
+
orientEntries.taskContext?.context
|
|
9747
|
+
);
|
|
9748
|
+
lines.push(...buildAlignmentCheckLines(result));
|
|
9749
|
+
}
|
|
9750
|
+
if (orientEntries) {
|
|
9751
|
+
const fmt = (e) => {
|
|
9752
|
+
const type = e.canonicalKey ?? "generic";
|
|
9753
|
+
const stratum = e.stratum ?? "?";
|
|
9754
|
+
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
|
|
9755
|
+
};
|
|
9756
|
+
if (orientEntries.activeBets?.length > 0) {
|
|
9757
|
+
lines.push("## Active bets \u2014 current scope");
|
|
9758
|
+
lines.push("_These define what you're building now. Work outside these bets requires explicit user confirmation before designing._");
|
|
9759
|
+
lines.push("");
|
|
9760
|
+
for (const e of orientEntries.activeBets) {
|
|
9761
|
+
lines.push(fmt(e));
|
|
9762
|
+
const tensions = e.linkedTensions;
|
|
9763
|
+
if (tensions?.length) {
|
|
9764
|
+
const tensionLines = tensions.map((t) => {
|
|
9765
|
+
const meta = [t.severity, t.priority].filter(Boolean).join(", ");
|
|
9766
|
+
return `\`${t.entryId ?? t.name}\` (${t.name}${meta ? `, ${meta}` : ""})`;
|
|
9767
|
+
});
|
|
9768
|
+
lines.push(` Tensions: ${tensionLines.join("; ")}`);
|
|
9769
|
+
} else {
|
|
9770
|
+
lines.push(` Tensions: No linked tensions`);
|
|
9771
|
+
}
|
|
9772
|
+
}
|
|
9773
|
+
lines.push("");
|
|
9774
|
+
}
|
|
9775
|
+
if (orientEntries.activeGoals.length > 0) {
|
|
9776
|
+
lines.push("## Active goals");
|
|
9777
|
+
orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
|
|
9778
|
+
lines.push("");
|
|
9779
|
+
}
|
|
9780
|
+
if (orientEntries.recentDecisions.length > 0) {
|
|
9781
|
+
lines.push("## Recent decisions");
|
|
9782
|
+
orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
|
|
9783
|
+
lines.push("");
|
|
9784
|
+
}
|
|
9785
|
+
if (orientEntries.recentlySuperseded.length > 0) {
|
|
9786
|
+
lines.push("## Recently superseded");
|
|
9787
|
+
orientEntries.recentlySuperseded.forEach((e) => lines.push(fmt(e)));
|
|
9788
|
+
lines.push("");
|
|
9789
|
+
}
|
|
9790
|
+
if (orientEntries.staleEntries.length > 0) {
|
|
9791
|
+
lines.push("## Needs confirmation");
|
|
9792
|
+
lines.push(`_Domain stratum entries not confirmed in ${orientEntries.stalenessThresholdDays} days._`);
|
|
9793
|
+
orientEntries.staleEntries.forEach((e) => lines.push(fmt(e)));
|
|
9794
|
+
lines.push("");
|
|
9795
|
+
}
|
|
9796
|
+
const hasPrinciples = orientEntries.principles?.length > 0;
|
|
9797
|
+
const hasStandards = orientEntries.standards?.length > 0;
|
|
9798
|
+
const hasBusinessRules = orientEntries.businessRules.length > 0;
|
|
9799
|
+
if (hasPrinciples || hasStandards || hasBusinessRules) {
|
|
9800
|
+
lines.push("## Workspace Governance \u2014 constraints");
|
|
9801
|
+
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._");
|
|
9802
|
+
lines.push("");
|
|
9803
|
+
if (hasPrinciples) {
|
|
9804
|
+
lines.push("**Principles** \u2014 beliefs that guide decisions:");
|
|
9805
|
+
orientEntries.principles.forEach((e) => lines.push(fmt(e)));
|
|
9806
|
+
lines.push("");
|
|
9807
|
+
}
|
|
9808
|
+
if (hasStandards) {
|
|
9809
|
+
lines.push("**Standards** \u2014 conventions for how work is done:");
|
|
9810
|
+
orientEntries.standards.forEach((e) => lines.push(fmt(e)));
|
|
9811
|
+
lines.push("");
|
|
9812
|
+
}
|
|
9813
|
+
if (hasBusinessRules) {
|
|
9814
|
+
lines.push("**Business Rules** \u2014 system constraints:");
|
|
9815
|
+
orientEntries.businessRules.forEach((e) => lines.push(fmt(e)));
|
|
9816
|
+
lines.push("");
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
if (orientEntries.architectureNotes.length > 0) {
|
|
9820
|
+
lines.push("## Architecture notes");
|
|
9821
|
+
orientEntries.architectureNotes.forEach((e) => lines.push(fmt(e)));
|
|
9822
|
+
lines.push("");
|
|
9823
|
+
}
|
|
9824
|
+
const mapGovernanceEntry = (e) => ({
|
|
9825
|
+
entryId: e.entryId,
|
|
9826
|
+
name: e.name,
|
|
9827
|
+
description: typeof e.preview === "string" ? e.preview : void 0
|
|
9828
|
+
});
|
|
9829
|
+
lines.push(...buildOperatingProtocol({
|
|
9830
|
+
principles: (orientEntries.principles ?? []).map(mapGovernanceEntry),
|
|
9831
|
+
standards: (orientEntries.standards ?? []).map(mapGovernanceEntry),
|
|
9832
|
+
businessRules: (orientEntries.businessRules ?? []).map(mapGovernanceEntry)
|
|
9833
|
+
}, task));
|
|
9487
9834
|
}
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
lines.push(`- \`${l.slug}\` ${l.name}${l.color ? ` (${l.color})` : ""}`);
|
|
9835
|
+
let allEntries = [];
|
|
9836
|
+
try {
|
|
9837
|
+
allEntries = await mcpQuery("chain.listEntries", {}) ?? [];
|
|
9838
|
+
} catch {
|
|
9493
9839
|
}
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9840
|
+
const plannedWork = buildPlannedWork(allEntries);
|
|
9841
|
+
if (hasPlannedWork(plannedWork)) {
|
|
9842
|
+
lines.push(...buildPlannedWorkSection(plannedWork, priorSessions, recoveryBlock));
|
|
9843
|
+
} else {
|
|
9844
|
+
const briefingItems = [];
|
|
9845
|
+
if (priorSessions.length > 0 && !recoveryBlock) {
|
|
9846
|
+
const last = priorSessions[0];
|
|
9847
|
+
const date = new Date(last.startedAt).toISOString().split("T")[0];
|
|
9848
|
+
const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
|
|
9849
|
+
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
9850
|
+
briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
|
|
9851
|
+
}
|
|
9852
|
+
if (readiness.gaps?.length > 0) {
|
|
9853
|
+
briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
|
|
9854
|
+
}
|
|
9855
|
+
if (briefingItems.length > 0) {
|
|
9856
|
+
lines.push("## Briefing");
|
|
9857
|
+
for (const item of briefingItems) {
|
|
9858
|
+
lines.push(`- ${item}`);
|
|
9859
|
+
}
|
|
9860
|
+
lines.push("");
|
|
9861
|
+
}
|
|
9862
|
+
if (recoveryBlock) {
|
|
9863
|
+
lines.push("");
|
|
9864
|
+
lines.push(...formatRecoveryBlock(recoveryBlock));
|
|
9865
|
+
}
|
|
9510
9866
|
}
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
if (!entry) {
|
|
9519
|
-
return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
|
|
9520
|
-
}
|
|
9521
|
-
const collectionMap = new Map((collections ?? []).map((c) => [c._id, c]));
|
|
9522
|
-
const col = collectionMap.get(entry.collectionId);
|
|
9523
|
-
const collLabel = col?.name ?? entry.collectionName ?? entry.collectionSlug ?? "unknown";
|
|
9524
|
-
const lines = [
|
|
9525
|
-
`# ${entry.entryId}: ${entry.name}`,
|
|
9526
|
-
`**Collection:** ${collLabel}`,
|
|
9527
|
-
`**Status:** ${entry.status}`,
|
|
9528
|
-
""
|
|
9529
|
-
];
|
|
9530
|
-
if (entry.data && typeof entry.data === "object") {
|
|
9531
|
-
lines.push("## Data");
|
|
9532
|
-
for (const [key, val] of Object.entries(entry.data)) {
|
|
9533
|
-
if (val && key !== "rawData") {
|
|
9534
|
-
const str = typeof val === "string" ? val : JSON.stringify(val, null, 2);
|
|
9535
|
-
lines.push(`**${key}:** ${str}`);
|
|
9867
|
+
const activeEntries = allEntries.filter((e) => e.status === "active");
|
|
9868
|
+
if (activeEntries.length > 0) {
|
|
9869
|
+
const orgHealth = computeOrganisationHealth(activeEntries);
|
|
9870
|
+
if (orgHealth.disagreements > 0) {
|
|
9871
|
+
lines.push("## Organisation Health");
|
|
9872
|
+
lines.push(...formatOrgHealthLines(orgHealth, 3));
|
|
9873
|
+
lines.push("");
|
|
9536
9874
|
}
|
|
9537
9875
|
}
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9876
|
+
const epistemicEntries = activeEntries.filter(
|
|
9877
|
+
(e) => e.collectionSlug === "insights" || e.collectionSlug === "assumptions"
|
|
9878
|
+
);
|
|
9879
|
+
if (epistemicEntries.length > 0) {
|
|
9880
|
+
let validated = 0;
|
|
9881
|
+
let evidenced = 0;
|
|
9882
|
+
let hypotheses = 0;
|
|
9883
|
+
let untested = 0;
|
|
9884
|
+
for (const e of epistemicEntries) {
|
|
9885
|
+
const ws = e.workflowStatus;
|
|
9886
|
+
if (ws === "validated") validated++;
|
|
9887
|
+
else if (ws === "evidenced") evidenced++;
|
|
9888
|
+
else if (ws === "testing") evidenced++;
|
|
9889
|
+
else if (ws === "invalidated") validated++;
|
|
9890
|
+
else if (e.collectionSlug === "assumptions") untested++;
|
|
9891
|
+
else hypotheses++;
|
|
9892
|
+
}
|
|
9893
|
+
const parts = [];
|
|
9894
|
+
if (validated > 0) parts.push(`${validated} validated`);
|
|
9895
|
+
if (evidenced > 0) parts.push(`${evidenced} evidenced`);
|
|
9896
|
+
if (hypotheses > 0) parts.push(`${hypotheses} hypotheses`);
|
|
9897
|
+
if (untested > 0) parts.push(`${untested} untested`);
|
|
9898
|
+
lines.push(`**Epistemic health:** ${parts.join(" \xB7 ")} _(${epistemicEntries.length} claim-carrying entries)_`);
|
|
9899
|
+
lines.push("");
|
|
9547
9900
|
}
|
|
9901
|
+
lines.push("What would you like to work on?");
|
|
9548
9902
|
lines.push("");
|
|
9549
9903
|
}
|
|
9550
|
-
if (
|
|
9551
|
-
lines.push(
|
|
9552
|
-
|
|
9553
|
-
}
|
|
9554
|
-
return {
|
|
9555
|
-
contents: [{ uri: uri.href, text: lines.join("\n"), mimeType: "text/markdown" }]
|
|
9556
|
-
};
|
|
9557
|
-
}
|
|
9558
|
-
);
|
|
9559
|
-
server.resource(
|
|
9560
|
-
"chain-context",
|
|
9561
|
-
new ResourceTemplate("productbrain://context/{entryId}", {
|
|
9562
|
-
complete: {
|
|
9563
|
-
entryId: async (value) => {
|
|
9564
|
-
if (!value || value.length < 1) return [];
|
|
9565
|
-
const entries = await mcpQuery("chain.searchEntries", { query: value }) ?? [];
|
|
9566
|
-
return entries.slice(0, 10).map((e) => e.entryId ?? e._id);
|
|
9567
|
-
}
|
|
9568
|
-
}
|
|
9569
|
-
}),
|
|
9570
|
-
async (uri, { entryId }) => {
|
|
9571
|
-
const result = await mcpQuery("chain.gatherContext", {
|
|
9572
|
-
entryId,
|
|
9573
|
-
maxHops: 2
|
|
9574
|
-
});
|
|
9575
|
-
if (!result?.root) {
|
|
9576
|
-
return { contents: [{ uri: uri.href, text: `Entry "${entryId}" not found.`, mimeType: "text/markdown" }] };
|
|
9577
|
-
}
|
|
9578
|
-
const lines = [
|
|
9579
|
-
`# Context: ${result.root.entryId}: ${result.root.name}`,
|
|
9580
|
-
`_${result.totalRelations} related entries (${result.hopsTraversed} hops)_`,
|
|
9581
|
-
""
|
|
9582
|
-
];
|
|
9583
|
-
const byCollection = /* @__PURE__ */ new Map();
|
|
9584
|
-
for (const entry of result.related ?? []) {
|
|
9585
|
-
const key = entry.collectionName;
|
|
9586
|
-
if (!byCollection.has(key)) byCollection.set(key, []);
|
|
9587
|
-
byCollection.get(key).push(entry);
|
|
9588
|
-
}
|
|
9589
|
-
for (const [collName, entries] of byCollection) {
|
|
9590
|
-
lines.push(`## ${collName} (${entries.length})`);
|
|
9591
|
-
for (const e of entries) {
|
|
9592
|
-
const arrow = e.relationDirection === "outgoing" ? "\u2192" : "\u2190";
|
|
9593
|
-
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
9594
|
-
const id = e.entryId ? `${e.entryId}: ` : "";
|
|
9595
|
-
lines.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}`);
|
|
9596
|
-
}
|
|
9904
|
+
if (errors.length > 0) {
|
|
9905
|
+
lines.push("## Errors");
|
|
9906
|
+
for (const err of errors) lines.push(`- ${err}`);
|
|
9597
9907
|
lines.push("");
|
|
9598
9908
|
}
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
query: async (value) => {
|
|
9609
|
-
if (!value) return ["glossary:", "business-rules:", "decisions:", "tensions:"];
|
|
9610
|
-
return [];
|
|
9909
|
+
if (agentSessionId) {
|
|
9910
|
+
try {
|
|
9911
|
+
await mcpCall("agent.markOriented", { sessionId: agentSessionId });
|
|
9912
|
+
setSessionOriented(true);
|
|
9913
|
+
lines.push("---");
|
|
9914
|
+
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
9915
|
+
} catch {
|
|
9916
|
+
lines.push("---");
|
|
9917
|
+
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
9611
9918
|
}
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
const id = entry.entryId ? `**${entry.entryId}:** ` : "";
|
|
9626
|
-
const coll = entry.collectionName ?? entry.collectionSlug ?? "";
|
|
9627
|
-
lines.push(`- ${id}${entry.name} [${coll}] (${entry.status})`);
|
|
9919
|
+
try {
|
|
9920
|
+
await mcpMutation("chain.recordSessionSignal", {
|
|
9921
|
+
sessionId: agentSessionId,
|
|
9922
|
+
signalType: "immediate_context_load",
|
|
9923
|
+
metadata: { source: "orient" }
|
|
9924
|
+
});
|
|
9925
|
+
} catch (err) {
|
|
9926
|
+
process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
|
|
9927
|
+
`);
|
|
9928
|
+
}
|
|
9929
|
+
} else {
|
|
9930
|
+
lines.push("---");
|
|
9931
|
+
lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
|
|
9628
9932
|
}
|
|
9629
9933
|
return {
|
|
9630
|
-
|
|
9934
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
9935
|
+
structuredContent: success(
|
|
9936
|
+
`Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
|
|
9937
|
+
{ mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
|
|
9938
|
+
)
|
|
9631
9939
|
};
|
|
9632
|
-
}
|
|
9940
|
+
})
|
|
9633
9941
|
);
|
|
9634
|
-
const viewsBase = resolve3(process.cwd(), "packages", "mcp-views", "dist");
|
|
9635
|
-
for (const [uri, filePath] of Object.entries(UI_VIEWS)) {
|
|
9636
|
-
server.resource(`ui-view-${uri.replace(/[^a-z0-9]/gi, "-")}`, uri, async (uriObj) => {
|
|
9637
|
-
try {
|
|
9638
|
-
const html = await readFile(join(viewsBase, filePath), "utf-8");
|
|
9639
|
-
return { contents: [{ uri: uriObj.href, text: html, mimeType: "text/html" }] };
|
|
9640
|
-
} catch {
|
|
9641
|
-
return {
|
|
9642
|
-
contents: [{
|
|
9643
|
-
uri: uriObj.href,
|
|
9644
|
-
text: "View not found. Run npm run build in packages/mcp-views.",
|
|
9645
|
-
mimeType: "text/plain"
|
|
9646
|
-
}]
|
|
9647
|
-
};
|
|
9648
|
-
}
|
|
9649
|
-
});
|
|
9650
|
-
}
|
|
9651
9942
|
}
|
|
9652
9943
|
|
|
9653
9944
|
// src/prompts/index.ts
|
|
@@ -10516,4 +10807,4 @@ export {
|
|
|
10516
10807
|
SERVER_VERSION,
|
|
10517
10808
|
createProductBrainServer
|
|
10518
10809
|
};
|
|
10519
|
-
//# sourceMappingURL=chunk-
|
|
10810
|
+
//# sourceMappingURL=chunk-IGQLZI32.js.map
|