@productbrain/mcp 0.0.1-beta.71 → 0.0.1-beta.73
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-2GMFQHAF.js → chunk-2BHEXDVG.js} +1214 -780
- package/dist/chunk-2BHEXDVG.js.map +1 -0
- package/dist/{chunk-TH5AUVVM.js → chunk-MFH4WG3T.js} +19 -42
- package/dist/chunk-MFH4WG3T.js.map +1 -0
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{smart-capture-Q64ZXK65.js → smart-capture-P4CPOIFW.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-2GMFQHAF.js.map +0 -1
- package/dist/chunk-TH5AUVVM.js.map +0 -1
- /package/dist/{smart-capture-Q64ZXK65.js.map → smart-capture-P4CPOIFW.js.map} +0 -0
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
unknownAction,
|
|
44
44
|
validationResult,
|
|
45
45
|
withEnvelope
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-MFH4WG3T.js";
|
|
47
47
|
import {
|
|
48
48
|
trackKnowledgeGap,
|
|
49
49
|
trackQualityCheck,
|
|
@@ -69,7 +69,7 @@ var WORKFLOW_STATUS_VALUES = [
|
|
|
69
69
|
"conflict",
|
|
70
70
|
"processing",
|
|
71
71
|
"closed",
|
|
72
|
-
//
|
|
72
|
+
// Chains (bet type) lifecycle
|
|
73
73
|
"shaped",
|
|
74
74
|
"bet",
|
|
75
75
|
"building",
|
|
@@ -91,7 +91,7 @@ var updateEntrySchema = z.object({
|
|
|
91
91
|
z.enum(["draft", "active", "deprecated", "archived"]),
|
|
92
92
|
z.enum(WORKFLOW_STATUS_VALUES)
|
|
93
93
|
]).optional().describe("Lifecycle status: draft | active | deprecated | archived. **Workflow values (open, pending, decided\u2026) are deprecated here \u2014 use `workflowStatus` instead. Passing a workflow value as `status` will be auto-routed with a warning until 2026-09-03, then hard-errored.**"),
|
|
94
|
-
workflowStatus: z.enum(WORKFLOW_STATUS_VALUES).optional().describe("Collection workflow state. Each collection restricts which values are valid (e.g.
|
|
94
|
+
workflowStatus: z.enum(WORKFLOW_STATUS_VALUES).optional().describe("Collection workflow state. Each collection restricts which values are valid (e.g. chains with chainTypeId='bet': 'shaped' | 'bet' | 'building' | 'shipped'; assumptions: 'untested' | 'testing' | 'validated' | 'invalidated'; decisions: 'pending' | 'decided'; tensions: 'open' | 'processing' | 'decided' | 'closed'). The backend will reject values invalid for the target collection."),
|
|
95
95
|
data: z.record(z.unknown()).optional().describe("Fields to update (merged with existing data)"),
|
|
96
96
|
order: z.number().optional().describe("New sort order"),
|
|
97
97
|
canonicalKey: z.string().optional().describe("Semantic type (e.g. 'decision', 'tension'). Only changeable on draft/uncommitted entries."),
|
|
@@ -238,7 +238,7 @@ ${formatted}` }],
|
|
|
238
238
|
},
|
|
239
239
|
withEnvelope(async ({ entryId }) => {
|
|
240
240
|
requireWriteAccess();
|
|
241
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
241
|
+
const { runContradictionCheck } = await import("./smart-capture-P4CPOIFW.js");
|
|
242
242
|
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
243
243
|
if (!entry) {
|
|
244
244
|
return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
|
|
@@ -1691,7 +1691,7 @@ async function handleBuild(server, entryId, maxHops) {
|
|
|
1691
1691
|
|
|
1692
1692
|
// src/tools/collections.ts
|
|
1693
1693
|
import { z as z6 } from "zod";
|
|
1694
|
-
var COLLECTIONS_ACTIONS = ["list", "create", "update"];
|
|
1694
|
+
var COLLECTIONS_ACTIONS = ["list", "create", "update", "describe"];
|
|
1695
1695
|
var fieldSchema = z6.object({
|
|
1696
1696
|
key: z6.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
|
|
1697
1697
|
label: z6.string().describe("Display label, e.g. 'Description', 'Severity'"),
|
|
@@ -1705,7 +1705,7 @@ var fieldSchema = z6.object({
|
|
|
1705
1705
|
});
|
|
1706
1706
|
var collectionsSchema = z6.object({
|
|
1707
1707
|
action: z6.enum(COLLECTIONS_ACTIONS).describe(
|
|
1708
|
-
"'list': browse all collections. 'create': create a new collection. 'update': update an existing collection."
|
|
1708
|
+
"'list': browse all collections. 'create': create a new collection. 'update': update an existing collection. 'describe': full documentation for one collection \u2014 fields, option guides, usage guidance, examples."
|
|
1709
1709
|
),
|
|
1710
1710
|
slug: z6.string().optional().describe("URL-safe identifier for create/update, e.g. 'glossary', 'tech-debt'"),
|
|
1711
1711
|
name: z6.string().optional().describe("Display name for create, or new name for update"),
|
|
@@ -1720,7 +1720,7 @@ function registerCollectionsTools(server) {
|
|
|
1720
1720
|
"collections",
|
|
1721
1721
|
{
|
|
1722
1722
|
title: "Collections",
|
|
1723
|
-
description: "Manage knowledge collections.
|
|
1723
|
+
description: "Manage knowledge collections. Four actions:\n\n- **list**: Browse all collections \u2014 glossary, business rules, tracking events, etc. Returns slug, name, description, and field schema. Use before capture to see what exists.\n- **describe**: Full documentation for a single collection. Returns field help text, option decision guides, usage guidance, examples, and cross-references. Use when you need to understand a collection's purpose, field semantics, or option values.\n- **create**: Create a new collection. Provide slug, name, and field schema. Use when setting up a workspace or tracking a new type of knowledge.\n- **update**: Update an existing collection's name, description, purpose, icon, navGroup, or fields. Only provide the fields you want to change.",
|
|
1724
1724
|
inputSchema: collectionsSchema,
|
|
1725
1725
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
1726
1726
|
},
|
|
@@ -1732,6 +1732,12 @@ function registerCollectionsTools(server) {
|
|
|
1732
1732
|
if (action === "list") {
|
|
1733
1733
|
return handleList2();
|
|
1734
1734
|
}
|
|
1735
|
+
if (action === "describe") {
|
|
1736
|
+
if (!slug) {
|
|
1737
|
+
return validationResult("slug is required when action is 'describe'.");
|
|
1738
|
+
}
|
|
1739
|
+
return handleDescribe(slug);
|
|
1740
|
+
}
|
|
1735
1741
|
if (action === "create") {
|
|
1736
1742
|
if (!slug || !name || !fields || fields.length === 0) {
|
|
1737
1743
|
return validationResult("slug, name, and fields are required when action is 'create'.");
|
|
@@ -1753,6 +1759,99 @@ function registerCollectionsTools(server) {
|
|
|
1753
1759
|
);
|
|
1754
1760
|
trackWriteTool(tool);
|
|
1755
1761
|
}
|
|
1762
|
+
async function handleDescribe(slug) {
|
|
1763
|
+
const col = await mcpQuery("chain.getCollection", { slug });
|
|
1764
|
+
if (!col) {
|
|
1765
|
+
return failureResult(
|
|
1766
|
+
`Collection '${slug}' not found.`,
|
|
1767
|
+
"NOT_FOUND",
|
|
1768
|
+
`No collection with slug '${slug}' in this workspace.`,
|
|
1769
|
+
"Use `collections action=list` to see available collections."
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
const sections = [];
|
|
1773
|
+
sections.push(`# ${col.name} (\`${col.slug}\`)`);
|
|
1774
|
+
if (col.description) sections.push(col.description);
|
|
1775
|
+
const meta = [
|
|
1776
|
+
col.governed ? "**Governed**" : null,
|
|
1777
|
+
col.navGroup ? `**Nav group:** ${col.navGroup}` : null,
|
|
1778
|
+
col.idPrefix ? `**Prefix:** ${col.idPrefix}-*` : null,
|
|
1779
|
+
col.stratum ? `**Stratum:** ${col.stratum}` : null
|
|
1780
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1781
|
+
if (meta) sections.push(meta);
|
|
1782
|
+
if (col.usageGuidance) {
|
|
1783
|
+
sections.push(`
|
|
1784
|
+
## When to Use
|
|
1785
|
+
|
|
1786
|
+
${col.usageGuidance}`);
|
|
1787
|
+
} else if (col.purpose) {
|
|
1788
|
+
sections.push(`
|
|
1789
|
+
## Purpose
|
|
1790
|
+
|
|
1791
|
+
${col.purpose}`);
|
|
1792
|
+
}
|
|
1793
|
+
if (col.fields.length > 0) {
|
|
1794
|
+
const fieldDocs = col.fields.map((f) => {
|
|
1795
|
+
const parts = [];
|
|
1796
|
+
const typeInfo = `${f.type}${f.required ? ", required" : ""}`;
|
|
1797
|
+
parts.push(`### \`${f.key}\` \u2014 ${f.label ?? f.key} (${typeInfo})`);
|
|
1798
|
+
if (f.helpText) parts.push(f.helpText);
|
|
1799
|
+
if (f.options && f.options.length > 0) {
|
|
1800
|
+
const optLines = f.options.map((opt) => {
|
|
1801
|
+
const desc = f.optionDescriptions?.[opt];
|
|
1802
|
+
return desc ? ` - **${opt}**: ${desc}` : ` - ${opt}`;
|
|
1803
|
+
});
|
|
1804
|
+
parts.push(`
|
|
1805
|
+
Options:
|
|
1806
|
+
${optLines.join("\n")}`);
|
|
1807
|
+
}
|
|
1808
|
+
return parts.join("\n");
|
|
1809
|
+
});
|
|
1810
|
+
sections.push(`
|
|
1811
|
+
## Fields
|
|
1812
|
+
|
|
1813
|
+
${fieldDocs.join("\n\n")}`);
|
|
1814
|
+
}
|
|
1815
|
+
if (col.examples && col.examples.length > 0) {
|
|
1816
|
+
const exList = col.examples.map((ex) => `- **${ex.name}** \u2014 ${ex.description}`).join("\n");
|
|
1817
|
+
sections.push(`
|
|
1818
|
+
## Examples
|
|
1819
|
+
|
|
1820
|
+
${exList}`);
|
|
1821
|
+
}
|
|
1822
|
+
if (col.crossReferences && col.crossReferences.length > 0) {
|
|
1823
|
+
const refList = col.crossReferences.map(
|
|
1824
|
+
(r) => `- **${r.slug}** (${r.relationship}): ${r.guidance}`
|
|
1825
|
+
).join("\n");
|
|
1826
|
+
sections.push(`
|
|
1827
|
+
## Related Collections
|
|
1828
|
+
|
|
1829
|
+
${refList}`);
|
|
1830
|
+
}
|
|
1831
|
+
return {
|
|
1832
|
+
content: [{ type: "text", text: sections.join("\n\n") }],
|
|
1833
|
+
structuredContent: success(
|
|
1834
|
+
`Documentation for collection '${col.name}' (${col.slug}).`,
|
|
1835
|
+
{
|
|
1836
|
+
slug: col.slug,
|
|
1837
|
+
name: col.name,
|
|
1838
|
+
description: col.description,
|
|
1839
|
+
usageGuidance: col.usageGuidance ?? null,
|
|
1840
|
+
fieldCount: col.fields.length,
|
|
1841
|
+
fields: col.fields.map((f) => ({
|
|
1842
|
+
key: f.key,
|
|
1843
|
+
type: f.type,
|
|
1844
|
+
required: f.required,
|
|
1845
|
+
helpText: f.helpText ?? null,
|
|
1846
|
+
options: f.options ?? null,
|
|
1847
|
+
optionDescriptions: f.optionDescriptions ?? null
|
|
1848
|
+
})),
|
|
1849
|
+
examples: col.examples ?? [],
|
|
1850
|
+
crossReferences: col.crossReferences ?? []
|
|
1851
|
+
}
|
|
1852
|
+
)
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1756
1855
|
async function handleList2() {
|
|
1757
1856
|
const collections = await mcpQuery("chain.listCollections");
|
|
1758
1857
|
if (collections.length === 0) {
|
|
@@ -1997,7 +2096,7 @@ function registerLabelTools(server) {
|
|
|
1997
2096
|
}
|
|
1998
2097
|
|
|
1999
2098
|
// src/tools/health.ts
|
|
2000
|
-
import { z as
|
|
2099
|
+
import { z as z21 } from "zod";
|
|
2001
2100
|
|
|
2002
2101
|
// src/tools/session.ts
|
|
2003
2102
|
import { z as z9 } from "zod";
|
|
@@ -3852,7 +3951,7 @@ If any facilitate call fails:
|
|
|
3852
3951
|
description: "Confirmed problem statement and appetite",
|
|
3853
3952
|
format: "structured"
|
|
3854
3953
|
},
|
|
3855
|
-
kbCollection: "
|
|
3954
|
+
kbCollection: "chains",
|
|
3856
3955
|
maxDurationHint: "10 min"
|
|
3857
3956
|
},
|
|
3858
3957
|
{
|
|
@@ -3922,7 +4021,7 @@ If any facilitate call fails:
|
|
|
3922
4021
|
description: "Final capture summary \u2014 entries committed, relations created",
|
|
3923
4022
|
format: "structured"
|
|
3924
4023
|
},
|
|
3925
|
-
kbCollection: "
|
|
4024
|
+
kbCollection: "chains",
|
|
3926
4025
|
maxDurationHint: "5 min"
|
|
3927
4026
|
}
|
|
3928
4027
|
],
|
|
@@ -3930,7 +4029,7 @@ If any facilitate call fails:
|
|
|
3930
4029
|
primaryRecord: {
|
|
3931
4030
|
routing: {
|
|
3932
4031
|
mode: "fixed",
|
|
3933
|
-
collection: "
|
|
4032
|
+
collection: "chains"
|
|
3934
4033
|
}
|
|
3935
4034
|
},
|
|
3936
4035
|
emits: [
|
|
@@ -3956,7 +4055,7 @@ If any facilitate call fails:
|
|
|
3956
4055
|
},
|
|
3957
4056
|
{
|
|
3958
4057
|
kind: "update",
|
|
3959
|
-
collection: "
|
|
4058
|
+
collection: "chains",
|
|
3960
4059
|
description: "Bet status and no-go updates during finalize."
|
|
3961
4060
|
}
|
|
3962
4061
|
]
|
|
@@ -5823,7 +5922,8 @@ function generateBuildContract(ctx) {
|
|
|
5823
5922
|
function computeCommitBlockers(opts) {
|
|
5824
5923
|
const { betEntryId, betDocId, relations, hasStrategyLink, betData, sessionDrafts } = opts;
|
|
5825
5924
|
const blockers = [];
|
|
5826
|
-
const
|
|
5925
|
+
const links = betData.links ?? {};
|
|
5926
|
+
const str = (key) => (links[key] ?? "").trim();
|
|
5827
5927
|
if (!hasStrategyLink) {
|
|
5828
5928
|
blockers.push({
|
|
5829
5929
|
entryId: betEntryId,
|
|
@@ -6187,7 +6287,8 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
|
|
|
6187
6287
|
}
|
|
6188
6288
|
}
|
|
6189
6289
|
function buildScoringContext(userInput, betData, constellation, overlapIds, chainResults = [], activeDimension, source = "user") {
|
|
6190
|
-
const
|
|
6290
|
+
const links = betData.links ?? {};
|
|
6291
|
+
const str = (key) => links[key] ?? "";
|
|
6191
6292
|
const accParts = [
|
|
6192
6293
|
str("problem"),
|
|
6193
6294
|
str("appetite"),
|
|
@@ -6342,17 +6443,20 @@ async function processCaptures(opts) {
|
|
|
6342
6443
|
};
|
|
6343
6444
|
const FALLBACK_DEFAULTS = { dataField: "description", relationType: "related_to" };
|
|
6344
6445
|
let runningBetData = { ...betData };
|
|
6446
|
+
const runningLinks = () => ({ ...runningBetData.links ?? {} });
|
|
6345
6447
|
for (const item of captureItems) {
|
|
6346
6448
|
if (item.type === "noGo") {
|
|
6449
|
+
const curLinks = runningLinks();
|
|
6347
6450
|
const updatedNoGos = appendNoGo(
|
|
6348
|
-
|
|
6451
|
+
curLinks.noGos,
|
|
6349
6452
|
{ title: item.name, explanation: item.description }
|
|
6350
6453
|
);
|
|
6351
|
-
|
|
6454
|
+
const newLinks = { ...curLinks, noGos: updatedNoGos };
|
|
6455
|
+
runningBetData.links = newLinks;
|
|
6352
6456
|
try {
|
|
6353
6457
|
await mcpMutation("chain.updateEntry", {
|
|
6354
6458
|
entryId: betEntryId,
|
|
6355
|
-
data: {
|
|
6459
|
+
data: { links: newLinks },
|
|
6356
6460
|
changeNote: `Added no-go: ${item.name}`
|
|
6357
6461
|
});
|
|
6358
6462
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6436,15 +6540,17 @@ async function processCaptures(opts) {
|
|
|
6436
6540
|
}
|
|
6437
6541
|
}
|
|
6438
6542
|
if (item.type === "element") {
|
|
6543
|
+
const curLinks = runningLinks();
|
|
6439
6544
|
const updatedElements = appendElement(
|
|
6440
|
-
|
|
6545
|
+
curLinks.elements,
|
|
6441
6546
|
{ name: item.name, description: item.description, entryId: capturedEntryId }
|
|
6442
6547
|
);
|
|
6443
|
-
|
|
6548
|
+
const newLinks = { ...curLinks, elements: updatedElements };
|
|
6549
|
+
runningBetData.links = newLinks;
|
|
6444
6550
|
try {
|
|
6445
6551
|
await mcpMutation("chain.updateEntry", {
|
|
6446
6552
|
entryId: betEntryId,
|
|
6447
|
-
data: {
|
|
6553
|
+
data: { links: newLinks },
|
|
6448
6554
|
changeNote: `Added element: ${item.name}`
|
|
6449
6555
|
});
|
|
6450
6556
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6455,15 +6561,17 @@ async function processCaptures(opts) {
|
|
|
6455
6561
|
});
|
|
6456
6562
|
}
|
|
6457
6563
|
} else if (item.type === "risk") {
|
|
6564
|
+
const curLinks = runningLinks();
|
|
6458
6565
|
const updatedRisks = appendRabbitHole(
|
|
6459
|
-
|
|
6566
|
+
curLinks.rabbitHoles,
|
|
6460
6567
|
{ name: item.name, description: item.description, theme: item.theme, entryId: capturedEntryId }
|
|
6461
6568
|
);
|
|
6462
|
-
|
|
6569
|
+
const newLinks = { ...curLinks, rabbitHoles: updatedRisks };
|
|
6570
|
+
runningBetData.links = newLinks;
|
|
6463
6571
|
try {
|
|
6464
6572
|
await mcpMutation("chain.updateEntry", {
|
|
6465
6573
|
entryId: betEntryId,
|
|
6466
|
-
data: {
|
|
6574
|
+
data: { links: newLinks },
|
|
6467
6575
|
changeNote: `Added risk: ${item.name}`
|
|
6468
6576
|
});
|
|
6469
6577
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6499,7 +6607,8 @@ async function computeAndUpdateScores(opts) {
|
|
|
6499
6607
|
const alreadyCheckedOverlap = typeof refreshedData._overlapIds === "string";
|
|
6500
6608
|
let overlap = [];
|
|
6501
6609
|
if (!alreadyCheckedOverlap && captureItems.length === 0) {
|
|
6502
|
-
const
|
|
6610
|
+
const refreshedLinks = refreshedData.links ?? {};
|
|
6611
|
+
const problemText = refreshedLinks.problem ?? userInput;
|
|
6503
6612
|
const overlapResults = await searchChain(problemText, { maxResults: 5, excludeIds: [betEntryId, ...constellationEntryIds] });
|
|
6504
6613
|
overlap = overlapResults.map((r) => ({
|
|
6505
6614
|
entryId: r.entryId,
|
|
@@ -6508,7 +6617,8 @@ async function computeAndUpdateScores(opts) {
|
|
|
6508
6617
|
}));
|
|
6509
6618
|
}
|
|
6510
6619
|
const overlapIds = overlap.map((o) => o.entryId);
|
|
6511
|
-
const
|
|
6620
|
+
const refreshedLinksForAppetite = refreshedData.links ?? {};
|
|
6621
|
+
const appetiteText = refreshedLinksForAppetite.appetite ?? "";
|
|
6512
6622
|
const isSmallBatch = detectSmallBatch(
|
|
6513
6623
|
argDimension === "appetite" ? userInput : appetiteText
|
|
6514
6624
|
);
|
|
@@ -6534,15 +6644,20 @@ async function computeAndUpdateScores(opts) {
|
|
|
6534
6644
|
const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
|
|
6535
6645
|
try {
|
|
6536
6646
|
const fieldUpdates = {};
|
|
6647
|
+
const linksUpdates = {};
|
|
6648
|
+
const currentLinks = refreshedData.links ?? {};
|
|
6537
6649
|
const persist = (dim, field, minLen) => {
|
|
6538
|
-
if (activeDimension === dim && !
|
|
6539
|
-
|
|
6650
|
+
if (activeDimension === dim && !currentLinks[field] && userInput.length > minLen) {
|
|
6651
|
+
linksUpdates[field] = userInput;
|
|
6540
6652
|
}
|
|
6541
6653
|
};
|
|
6542
6654
|
persist("problem_clarity", "problem", 50);
|
|
6543
6655
|
persist("appetite", "appetite", 20);
|
|
6544
6656
|
persist("architecture", "architecture", 50);
|
|
6545
6657
|
persist("done_when", "done_when", 30);
|
|
6658
|
+
if (Object.keys(linksUpdates).length > 0) {
|
|
6659
|
+
fieldUpdates.links = { ...currentLinks, ...linksUpdates };
|
|
6660
|
+
}
|
|
6546
6661
|
if (!refreshedData._overlapIds) {
|
|
6547
6662
|
fieldUpdates._overlapIds = overlapIds.length > 0 ? overlapIds.join(",") : "_checked";
|
|
6548
6663
|
}
|
|
@@ -6567,7 +6682,7 @@ async function computeAndUpdateScores(opts) {
|
|
|
6567
6682
|
});
|
|
6568
6683
|
}
|
|
6569
6684
|
const alignment = [];
|
|
6570
|
-
const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "
|
|
6685
|
+
const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "chains"]);
|
|
6571
6686
|
for (const result of chainSurfaced) {
|
|
6572
6687
|
if (governanceCollections.has(result.collection)) {
|
|
6573
6688
|
alignment.push({ entryId: result.entryId, relationship: `governs [${result.collection}]` });
|
|
@@ -6586,11 +6701,13 @@ async function computeAndUpdateScores(opts) {
|
|
|
6586
6701
|
relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.name }))
|
|
6587
6702
|
};
|
|
6588
6703
|
buildContract = generateBuildContract(contractCtx);
|
|
6589
|
-
|
|
6704
|
+
const currentBCLinks = refreshedData.links ?? {};
|
|
6705
|
+
if (currentBCLinks.buildContract !== buildContract) {
|
|
6590
6706
|
try {
|
|
6707
|
+
const bcLinks = { ...refreshedData.links ?? {}, buildContract };
|
|
6591
6708
|
await mcpMutation("chain.updateEntry", {
|
|
6592
6709
|
entryId: betEntryId,
|
|
6593
|
-
data: {
|
|
6710
|
+
data: { links: bcLinks },
|
|
6594
6711
|
changeNote: "Updated build contract from shaping session"
|
|
6595
6712
|
});
|
|
6596
6713
|
await recordSessionActivity({ entryModified: refreshedBet?._id ?? betEntry._id });
|
|
@@ -6667,9 +6784,10 @@ function assembleResponse(opts) {
|
|
|
6667
6784
|
} = opts;
|
|
6668
6785
|
const studioUrl = buildStudioUrl(workspaceSlug, betEntryId);
|
|
6669
6786
|
const suggested = suggestCaptures(scoringCtx, activeDimension);
|
|
6670
|
-
const
|
|
6787
|
+
const assembleLinks = refreshedData.links ?? {};
|
|
6788
|
+
const betProblem = assembleLinks.problem ?? "";
|
|
6671
6789
|
const responseBetName = refreshedData.description ?? betName;
|
|
6672
|
-
const elementNames = (
|
|
6790
|
+
const elementNames = (assembleLinks.elements ?? "").match(/###\s*Element\s*\d+:\s*(.+)/gi)?.map((h) => h.replace(/###\s*Element\s*\d+:\s*/i, "").trim()) ?? [];
|
|
6673
6791
|
const investigationBrief = buildInvestigationBrief(phase, responseBetName, betEntryId, betProblem, elementNames) ?? void 0;
|
|
6674
6792
|
const response = {
|
|
6675
6793
|
version: 2,
|
|
@@ -6757,20 +6875,23 @@ async function handleRespond(args) {
|
|
|
6757
6875
|
const result = await mcpMutation(
|
|
6758
6876
|
"chain.createEntry",
|
|
6759
6877
|
{
|
|
6760
|
-
collectionSlug: "
|
|
6878
|
+
collectionSlug: "chains",
|
|
6761
6879
|
name: betName,
|
|
6762
6880
|
status: "draft",
|
|
6763
6881
|
data: {
|
|
6764
|
-
|
|
6765
|
-
appetite: "",
|
|
6766
|
-
elements: "",
|
|
6767
|
-
rabbitHoles: "",
|
|
6768
|
-
noGos: "",
|
|
6769
|
-
architecture: "",
|
|
6770
|
-
buildContract: "",
|
|
6882
|
+
chainTypeId: "bet",
|
|
6771
6883
|
description: `Shaping session for: ${betName}`,
|
|
6772
6884
|
status: "shaping",
|
|
6773
|
-
shapingSessionActive: true
|
|
6885
|
+
shapingSessionActive: true,
|
|
6886
|
+
links: {
|
|
6887
|
+
problem: userInput,
|
|
6888
|
+
appetite: "",
|
|
6889
|
+
elements: "",
|
|
6890
|
+
rabbitHoles: "",
|
|
6891
|
+
noGos: "",
|
|
6892
|
+
architecture: "",
|
|
6893
|
+
buildContract: ""
|
|
6894
|
+
}
|
|
6774
6895
|
},
|
|
6775
6896
|
createdBy: agentId ? `agent:${agentId}` : "facilitate",
|
|
6776
6897
|
sessionId: agentId ?? void 0
|
|
@@ -6876,7 +6997,8 @@ async function handleScore(args) {
|
|
|
6876
6997
|
}
|
|
6877
6998
|
const constellation = await loadConstellationState(betId, betEntry._id);
|
|
6878
6999
|
const betData = betEntry.data ?? {};
|
|
6879
|
-
const
|
|
7000
|
+
const scoreLinks = betData.links ?? {};
|
|
7001
|
+
const isSmallBatch = detectSmallBatch(scoreLinks.appetite ?? "");
|
|
6880
7002
|
const scoringCtx = buildCachedScoringContext(betData, constellation);
|
|
6881
7003
|
const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
|
|
6882
7004
|
const dims = activeDimensions(isSmallBatch);
|
|
@@ -6979,7 +7101,8 @@ async function handleResume(args) {
|
|
|
6979
7101
|
}
|
|
6980
7102
|
const constellation = await loadConstellationState(betId, betEntry._id);
|
|
6981
7103
|
const betData = betEntry.data ?? {};
|
|
6982
|
-
const
|
|
7104
|
+
const resumeLinks = betData.links ?? {};
|
|
7105
|
+
const isSmallBatch = detectSmallBatch(resumeLinks.appetite ?? "");
|
|
6983
7106
|
const scoringCtx = buildCachedScoringContext(betData, constellation);
|
|
6984
7107
|
const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
|
|
6985
7108
|
const dims = activeDimensions(isSmallBatch);
|
|
@@ -7130,8 +7253,9 @@ async function handleCommitConstellation(args) {
|
|
|
7130
7253
|
}
|
|
7131
7254
|
let contradictionWarnings = [];
|
|
7132
7255
|
try {
|
|
7133
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
7134
|
-
const
|
|
7256
|
+
const { runContradictionCheck } = await import("./smart-capture-P4CPOIFW.js");
|
|
7257
|
+
const commitLinks = betData.links ?? {};
|
|
7258
|
+
const descField = commitLinks.problem ?? betData.description ?? "";
|
|
7135
7259
|
contradictionWarnings = await runContradictionCheck(
|
|
7136
7260
|
betEntry.name ?? betId,
|
|
7137
7261
|
descField
|
|
@@ -7799,7 +7923,7 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
|
7799
7923
|
return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
|
|
7800
7924
|
}
|
|
7801
7925
|
const betHits = (taskContextHits ?? []).filter(
|
|
7802
|
-
(e) => e.collectionSlug === "
|
|
7926
|
+
(e) => e.collectionSlug === "chains"
|
|
7803
7927
|
);
|
|
7804
7928
|
if (betHits.length > 0) {
|
|
7805
7929
|
return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
|
|
@@ -7951,73 +8075,16 @@ var interviewExtractionSchema = z14.object({
|
|
|
7951
8075
|
keyDecisions: z14.array(z14.string()).optional().describe("Recent significant decisions made (each as a concise statement)"),
|
|
7952
8076
|
tensions: z14.array(z14.string()).optional().describe("Pain points or friction the product is solving")
|
|
7953
8077
|
});
|
|
7954
|
-
function extractionToBatchEntries(extracted) {
|
|
7955
|
-
const entries = [];
|
|
7956
|
-
if (extracted.vision) {
|
|
7957
|
-
entries.push({
|
|
7958
|
-
collection: "strategy",
|
|
7959
|
-
name: "Product Vision",
|
|
7960
|
-
description: extracted.vision
|
|
7961
|
-
});
|
|
7962
|
-
}
|
|
7963
|
-
if (extracted.audience) {
|
|
7964
|
-
const audienceName = extracted.audience.split(/\s+/).slice(0, 6).join(" ");
|
|
7965
|
-
entries.push({
|
|
7966
|
-
collection: "audiences",
|
|
7967
|
-
name: audienceName,
|
|
7968
|
-
description: extracted.audience
|
|
7969
|
-
});
|
|
7970
|
-
}
|
|
7971
|
-
if (extracted.techStack?.length) {
|
|
7972
|
-
entries.push({
|
|
7973
|
-
collection: "architecture",
|
|
7974
|
-
name: "Tech Stack",
|
|
7975
|
-
description: `Technology choices: ${extracted.techStack.join(", ")}`
|
|
7976
|
-
});
|
|
7977
|
-
for (const tech of extracted.techStack.slice(0, 3)) {
|
|
7978
|
-
entries.push({ collection: "glossary", name: tech, description: `${tech} \u2014 part of the tech stack` });
|
|
7979
|
-
}
|
|
7980
|
-
}
|
|
7981
|
-
if (extracted.keyTerms?.length) {
|
|
7982
|
-
for (const term of extracted.keyTerms) {
|
|
7983
|
-
const alreadyAdded = entries.some(
|
|
7984
|
-
(e) => e.collection === "glossary" && e.name.toLowerCase() === term.toLowerCase()
|
|
7985
|
-
);
|
|
7986
|
-
if (!alreadyAdded) {
|
|
7987
|
-
entries.push({ collection: "glossary", name: term, description: `Core domain term: ${term}` });
|
|
7988
|
-
}
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
|
-
if (extracted.keyDecisions?.length) {
|
|
7992
|
-
for (const decision of extracted.keyDecisions) {
|
|
7993
|
-
entries.push({
|
|
7994
|
-
collection: "decisions",
|
|
7995
|
-
name: decision.slice(0, 80),
|
|
7996
|
-
description: decision
|
|
7997
|
-
});
|
|
7998
|
-
}
|
|
7999
|
-
}
|
|
8000
|
-
if (extracted.tensions?.length) {
|
|
8001
|
-
for (const tension of extracted.tensions) {
|
|
8002
|
-
entries.push({
|
|
8003
|
-
collection: "tensions",
|
|
8004
|
-
name: tension.slice(0, 80),
|
|
8005
|
-
description: tension
|
|
8006
|
-
});
|
|
8007
|
-
}
|
|
8008
|
-
}
|
|
8009
|
-
return entries;
|
|
8010
|
-
}
|
|
8011
8078
|
function getInterviewInstructions(workspaceName) {
|
|
8012
8079
|
return {
|
|
8013
|
-
systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and
|
|
8014
|
-
question1: `**
|
|
8015
|
-
question2: `**
|
|
8080
|
+
systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and batch-capture it. Let the user talk naturally \u2014 you do the structuring. In Open governance mode, entries commit automatically. In consensus/role mode, they stay as drafts for review.`,
|
|
8081
|
+
question1: `**What are you building, and who is it for?** A sentence or two is plenty \u2014 I'll pull out the structure.`,
|
|
8082
|
+
question2: `**What's one word or phrase that would trip someone up if they didn't know your world?** (That becomes your first glossary term \u2014 the one that saves explanations later.)`,
|
|
8016
8083
|
extractionGuidance: `After the user answers, extract:
|
|
8017
8084
|
- vision: the core product purpose (required)
|
|
8018
8085
|
- audience: who it's for (optional)
|
|
8019
8086
|
- techStack: technologies mentioned (optional, array)
|
|
8020
|
-
- keyTerms: domain terms \u2014 Q2 answer
|
|
8087
|
+
- keyTerms: domain terms \u2014 Q2 answer goes here (optional, array)
|
|
8021
8088
|
- keyDecisions: any decisions stated (optional, array)
|
|
8022
8089
|
- tensions: any pain points stated (optional, array)
|
|
8023
8090
|
|
|
@@ -8028,7 +8095,7 @@ Map to batch-capture entries:
|
|
|
8028
8095
|
- keyTerms \u2192 glossary (1 per term)
|
|
8029
8096
|
- keyDecisions \u2192 decisions (1 per decision)
|
|
8030
8097
|
- tensions \u2192 tensions (1 per tension)`,
|
|
8031
|
-
captureInstructions: `Call batch-capture with the extracted entries. Omit \`autoCommit\` to follow workspace governance automatically, or pass \`autoCommit: false\` if the user wants
|
|
8098
|
+
captureInstructions: `Call batch-capture with the extracted entries. Omit \`autoCommit\` to follow workspace governance automatically, or pass \`autoCommit: false\` if the user wants review-first. Keep descriptions concise (1\u20132 sentences each). Prefer 8 good entries over 15 mediocre ones \u2014 quality over volume. If batch-capture returns failedEntries, tell the user and retry individually.`,
|
|
8032
8099
|
qualityNote: (
|
|
8033
8100
|
// FEAT-149: Retrieval-First Proof Moment.
|
|
8034
8101
|
// The aha is friction elimination ("I'll never have to explain this again"),
|
|
@@ -8048,25 +8115,73 @@ That's context you won't have to explain again."
|
|
|
8048
8115
|
|
|
8049
8116
|
End with: "**Try it right now** \u2014 ask me something you'd normally have to re-explain first. I'll answer like I've known your product for months."`
|
|
8050
8117
|
),
|
|
8051
|
-
scanOffer: `After the proof moment, offer the codebase scan
|
|
8052
|
-
"Want me to learn
|
|
8118
|
+
scanOffer: `After the proof moment, offer the codebase scan:
|
|
8119
|
+
"Want me to learn more? I can read your project files \u2014 README, package.json, source structure \u2014 and pick up technical decisions, conventions, and architecture I missed. Takes about two minutes."`
|
|
8053
8120
|
};
|
|
8054
8121
|
}
|
|
8055
8122
|
function buildInterviewResponse(workspaceName) {
|
|
8056
8123
|
const instructions = getInterviewInstructions(workspaceName);
|
|
8057
8124
|
return [
|
|
8058
|
-
"## Let's
|
|
8125
|
+
"## Let's get to know your product",
|
|
8059
8126
|
"",
|
|
8060
8127
|
instructions.systemPrompt,
|
|
8061
8128
|
"",
|
|
8062
|
-
"I'll ask you
|
|
8129
|
+
"I'll ask you one or two questions. Your answers become the foundation of your Brain.",
|
|
8063
8130
|
"",
|
|
8064
|
-
|
|
8131
|
+
instructions.question1,
|
|
8065
8132
|
"",
|
|
8066
|
-
"_Take your time \u2014
|
|
8133
|
+
"_Take your time \u2014 I'll pull out the structure from whatever you say._"
|
|
8067
8134
|
].join("\n");
|
|
8068
8135
|
}
|
|
8069
8136
|
|
|
8137
|
+
// src/lib/gapToPrompt.ts
|
|
8138
|
+
function cleanLabel(label) {
|
|
8139
|
+
return label.replace(/ has entries$/i, "").replace(/ coverage$/i, "").replace(/^Strategy — /i, "");
|
|
8140
|
+
}
|
|
8141
|
+
function progressFrame(ctx) {
|
|
8142
|
+
if (ctx.passedChecks === 0) {
|
|
8143
|
+
return "Your Brain is just getting started.";
|
|
8144
|
+
}
|
|
8145
|
+
if (ctx.passedChecks === 1) {
|
|
8146
|
+
return "Your Brain has one area covered so far.";
|
|
8147
|
+
}
|
|
8148
|
+
if (ctx.score >= 70) {
|
|
8149
|
+
return `Your Brain covers ${ctx.passedChecks} areas \u2014 almost there.`;
|
|
8150
|
+
}
|
|
8151
|
+
return `Your Brain covers ${ctx.passedChecks} of ${ctx.totalChecks} areas.`;
|
|
8152
|
+
}
|
|
8153
|
+
function formatTopGapPrompt(gap, remaining, ctx) {
|
|
8154
|
+
const prompt = gap.capabilityGuidance ?? gap.guidance;
|
|
8155
|
+
const label = cleanLabel(gap.label);
|
|
8156
|
+
const lines = [
|
|
8157
|
+
progressFrame(ctx),
|
|
8158
|
+
remaining > 0 ? `The biggest gap right now: **${label}**.` : `One area would strengthen it: **${label}**.`,
|
|
8159
|
+
"",
|
|
8160
|
+
prompt
|
|
8161
|
+
];
|
|
8162
|
+
return lines.join("\n");
|
|
8163
|
+
}
|
|
8164
|
+
function formatGapList(gaps, limit = 3) {
|
|
8165
|
+
const topGaps = gaps.slice(0, limit);
|
|
8166
|
+
if (topGaps.length === 0) return [];
|
|
8167
|
+
const lines = [
|
|
8168
|
+
"Here's where your Brain would benefit most:",
|
|
8169
|
+
""
|
|
8170
|
+
];
|
|
8171
|
+
for (let i = 0; i < topGaps.length; i++) {
|
|
8172
|
+
const gap = topGaps[i];
|
|
8173
|
+
const prompt = gap.capabilityGuidance ?? gap.guidance;
|
|
8174
|
+
const label = cleanLabel(gap.label);
|
|
8175
|
+
lines.push(`${i + 1}. **${label}** \u2014 ${prompt}`);
|
|
8176
|
+
}
|
|
8177
|
+
return lines;
|
|
8178
|
+
}
|
|
8179
|
+
function formatGapOneLiner(gap) {
|
|
8180
|
+
if (!gap) return null;
|
|
8181
|
+
const prompt = gap.capabilityGuidance ?? gap.guidance;
|
|
8182
|
+
return `Next gap: ${prompt}`;
|
|
8183
|
+
}
|
|
8184
|
+
|
|
8070
8185
|
// src/tools/start.ts
|
|
8071
8186
|
async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
|
|
8072
8187
|
if (!agentSessionId) return { oriented: false, orientationStatus: "no_session" };
|
|
@@ -8105,7 +8220,7 @@ function registerStartTools(server) {
|
|
|
8105
8220
|
try {
|
|
8106
8221
|
wsCtx = await getWorkspaceContext();
|
|
8107
8222
|
} catch (e) {
|
|
8108
|
-
errors.push(`Workspace: ${e.message}`);
|
|
8223
|
+
errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
|
|
8109
8224
|
}
|
|
8110
8225
|
if (!wsCtx) {
|
|
8111
8226
|
const text = "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.");
|
|
@@ -8254,7 +8369,7 @@ function buildBlankResponse(wsCtx, sessionCtx) {
|
|
|
8254
8369
|
}
|
|
8255
8370
|
async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
|
|
8256
8371
|
const stage = readiness?.stage ?? "seeded";
|
|
8257
|
-
const score = readiness?.score;
|
|
8372
|
+
const score = readiness?.score ?? null;
|
|
8258
8373
|
const gaps = readiness?.gaps ?? [];
|
|
8259
8374
|
const lines = [
|
|
8260
8375
|
`# ${wsCtx.workspaceName}`,
|
|
@@ -8262,14 +8377,8 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
|
|
|
8262
8377
|
""
|
|
8263
8378
|
];
|
|
8264
8379
|
if (gaps.length > 0) {
|
|
8265
|
-
|
|
8266
|
-
lines.push(
|
|
8267
|
-
const topGaps = gaps.slice(0, 3);
|
|
8268
|
-
for (let i = 0; i < topGaps.length; i++) {
|
|
8269
|
-
const gap = topGaps[i];
|
|
8270
|
-
const cta = gap.capabilityGuidance ?? gap.guidance ?? `Describe your ${gap.label.toLowerCase()}.`;
|
|
8271
|
-
lines.push(`${i + 1}. **${gap.label}** \u2014 ${cta}`);
|
|
8272
|
-
}
|
|
8380
|
+
const gapLines = formatGapList(gaps, 3);
|
|
8381
|
+
lines.push(...gapLines);
|
|
8273
8382
|
lines.push("");
|
|
8274
8383
|
lines.push("Pick any to start \u2014 or begin with **#1** and I'll guide you through it.");
|
|
8275
8384
|
} else {
|
|
@@ -8384,21 +8493,13 @@ function computeWorkspaceAge(createdAt) {
|
|
|
8384
8493
|
const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
|
|
8385
8494
|
return { ageDays, isNeglected: ageDays >= 30 };
|
|
8386
8495
|
}
|
|
8387
|
-
function
|
|
8388
|
-
if (
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
}
|
|
8394
|
-
if (openTensions.length > 0) {
|
|
8395
|
-
const t = openTensions[0];
|
|
8396
|
-
return {
|
|
8397
|
-
action: `Open tension: ${t.name}`,
|
|
8398
|
-
cta: "Want to discuss this tension or capture a decision about it?"
|
|
8399
|
-
};
|
|
8400
|
-
}
|
|
8401
|
-
return null;
|
|
8496
|
+
function pickNextTensionPrompt(openTensions) {
|
|
8497
|
+
if (openTensions.length === 0) return null;
|
|
8498
|
+
const t = openTensions[0];
|
|
8499
|
+
return {
|
|
8500
|
+
action: `Open tension: ${t.name}`,
|
|
8501
|
+
cta: "Want to discuss this tension or capture a decision about it?"
|
|
8502
|
+
};
|
|
8402
8503
|
}
|
|
8403
8504
|
async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
8404
8505
|
const wsFullCtx = await getWorkspaceContext();
|
|
@@ -8421,7 +8522,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
8421
8522
|
try {
|
|
8422
8523
|
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
8423
8524
|
} catch (e) {
|
|
8424
|
-
errors.push(`Readiness: ${e.message}`);
|
|
8525
|
+
errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
|
|
8425
8526
|
}
|
|
8426
8527
|
const lines = [];
|
|
8427
8528
|
const isLowReadiness = readiness !== null && readiness.score < 50;
|
|
@@ -8441,29 +8542,42 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
8441
8542
|
lines.push("Let's get your workspace active.");
|
|
8442
8543
|
lines.push("");
|
|
8443
8544
|
}
|
|
8444
|
-
if (isLowReadiness) {
|
|
8445
|
-
const
|
|
8446
|
-
if (
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8545
|
+
if (isLowReadiness && readiness) {
|
|
8546
|
+
const gaps = readiness.gaps ?? [];
|
|
8547
|
+
if (gaps.length > 0) {
|
|
8548
|
+
const gapCtx = {
|
|
8549
|
+
stage: stage ?? "seeded",
|
|
8550
|
+
passedChecks: readiness.passedChecks ?? 0,
|
|
8551
|
+
totalChecks: readiness.totalChecks ?? 0,
|
|
8552
|
+
score: readiness.score ?? 0
|
|
8553
|
+
};
|
|
8554
|
+
lines.push(formatTopGapPrompt(gaps[0], gaps.length - 1, gapCtx));
|
|
8451
8555
|
lines.push("");
|
|
8452
8556
|
lines.push(captureBehaviorNote);
|
|
8453
8557
|
lines.push("");
|
|
8454
|
-
const remainingGaps =
|
|
8558
|
+
const remainingGaps = gaps.length - 1;
|
|
8455
8559
|
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
8456
|
-
|
|
8560
|
+
const parts = [];
|
|
8561
|
+
if (remainingGaps > 0) parts.push(`${remainingGaps} more area${remainingGaps === 1 ? "" : "s"} to cover`);
|
|
8562
|
+
if (openTensions.length > 0) parts.push(`${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}`);
|
|
8563
|
+
lines.push(`_${parts.join(" and ")} \u2014 ask for full status to see everything._`);
|
|
8564
|
+
lines.push("");
|
|
8565
|
+
}
|
|
8566
|
+
} else {
|
|
8567
|
+
const tensionPrompt = pickNextTensionPrompt(openTensions);
|
|
8568
|
+
if (tensionPrompt) {
|
|
8569
|
+
lines.push(`**${tensionPrompt.action}**`);
|
|
8570
|
+
lines.push(tensionPrompt.cta);
|
|
8457
8571
|
lines.push("");
|
|
8458
8572
|
}
|
|
8459
8573
|
}
|
|
8460
|
-
lines.push("_Need a collection that doesn't exist yet
|
|
8574
|
+
lines.push("_Need a collection that doesn't exist yet? Just ask \u2014 I'll set it up._");
|
|
8461
8575
|
lines.push("");
|
|
8462
8576
|
} else if (isHighReadiness) {
|
|
8463
8577
|
let activeBets = [];
|
|
8464
8578
|
try {
|
|
8465
|
-
const
|
|
8466
|
-
activeBets = (
|
|
8579
|
+
const chainEntries = await mcpQuery("chain.listEntries", { collectionSlug: "chains" });
|
|
8580
|
+
activeBets = (chainEntries ?? []).filter((e) => e.data?.chainTypeId === "bet" && e.status === "active" && e.data?.horizon === "now").slice(0, 8);
|
|
8467
8581
|
} catch {
|
|
8468
8582
|
}
|
|
8469
8583
|
if (task) {
|
|
@@ -10258,90 +10372,17 @@ function formatScanReport(result) {
|
|
|
10258
10372
|
return lines.join("\n");
|
|
10259
10373
|
}
|
|
10260
10374
|
|
|
10261
|
-
// src/tools/
|
|
10262
|
-
|
|
10263
|
-
"chain.getEntry": "read",
|
|
10264
|
-
"chain.batchGetEntries": "read",
|
|
10265
|
-
"chain.listEntries": "read",
|
|
10266
|
-
"chain.listEntryHistory": "read",
|
|
10267
|
-
"chain.listEntryRelations": "read",
|
|
10268
|
-
"chain.listEntriesByLabel": "read",
|
|
10269
|
-
"chain.searchEntries": "search",
|
|
10270
|
-
"chain.createEntry": "write",
|
|
10271
|
-
"chain.updateEntry": "write",
|
|
10272
|
-
"chain.moveToCollection": "write",
|
|
10273
|
-
"chain.createEntryRelation": "write",
|
|
10274
|
-
"chain.applyLabel": "label",
|
|
10275
|
-
"chain.removeLabel": "label",
|
|
10276
|
-
"chain.createLabel": "label",
|
|
10277
|
-
"chain.updateLabel": "label",
|
|
10278
|
-
"chain.deleteLabel": "label",
|
|
10279
|
-
"chain.createCollection": "write",
|
|
10280
|
-
"chain.updateCollection": "write",
|
|
10281
|
-
"chain.listCollections": "meta",
|
|
10282
|
-
"chain.getCollection": "meta",
|
|
10283
|
-
"chain.listLabels": "meta",
|
|
10284
|
-
"resolveWorkspace": "meta"
|
|
10285
|
-
};
|
|
10286
|
-
function categorize(fn) {
|
|
10287
|
-
return CALL_CATEGORIES[fn] ?? "meta";
|
|
10288
|
-
}
|
|
10289
|
-
function formatDuration(ms) {
|
|
10290
|
-
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
10291
|
-
const mins = Math.floor(ms / 6e4);
|
|
10292
|
-
const secs = Math.round(ms % 6e4 / 1e3);
|
|
10293
|
-
return `${mins}m ${secs}s`;
|
|
10294
|
-
}
|
|
10295
|
-
function buildSessionSummary(log) {
|
|
10296
|
-
if (log.length === 0) return "";
|
|
10297
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
10298
|
-
let errorCount = 0;
|
|
10299
|
-
let writeCreates = 0;
|
|
10300
|
-
let writeUpdates = 0;
|
|
10301
|
-
for (const entry of log) {
|
|
10302
|
-
const cat = categorize(entry.fn);
|
|
10303
|
-
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
10304
|
-
const fnCounts = byCategory.get(cat);
|
|
10305
|
-
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
10306
|
-
if (entry.status === "error") errorCount++;
|
|
10307
|
-
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
10308
|
-
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
10309
|
-
}
|
|
10310
|
-
const firstTs = new Date(log[0].ts).getTime();
|
|
10311
|
-
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
10312
|
-
const duration = formatDuration(lastTs - firstTs);
|
|
10313
|
-
const lines = [`# Session Summary (${duration})
|
|
10314
|
-
`];
|
|
10315
|
-
const categoryLabels = [
|
|
10316
|
-
["read", "Reads"],
|
|
10317
|
-
["search", "Searches"],
|
|
10318
|
-
["write", "Writes"],
|
|
10319
|
-
["label", "Labels"],
|
|
10320
|
-
["meta", "Meta"]
|
|
10321
|
-
];
|
|
10322
|
-
for (const [cat, label] of categoryLabels) {
|
|
10323
|
-
const fnCounts = byCategory.get(cat);
|
|
10324
|
-
if (!fnCounts || fnCounts.size === 0) continue;
|
|
10325
|
-
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
10326
|
-
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
10327
|
-
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
10328
|
-
}
|
|
10329
|
-
lines.push(`- **Errors:** ${errorCount}`);
|
|
10330
|
-
if (writeCreates > 0 || writeUpdates > 0) {
|
|
10331
|
-
lines.push("");
|
|
10332
|
-
lines.push("## Knowledge Contribution");
|
|
10333
|
-
if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
|
|
10334
|
-
if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
|
|
10335
|
-
}
|
|
10336
|
-
return lines.join("\n");
|
|
10337
|
-
}
|
|
10375
|
+
// src/tools/orient.ts
|
|
10376
|
+
import { z as z20 } from "zod";
|
|
10338
10377
|
function extractSessionEntryIds(priorSessions) {
|
|
10339
10378
|
const allSeen = /* @__PURE__ */ new Set();
|
|
10340
10379
|
const all = [];
|
|
10341
10380
|
let lastSessionOnly = [];
|
|
10342
10381
|
for (let i = 0; i < priorSessions.length; i++) {
|
|
10343
10382
|
const s = priorSessions[i];
|
|
10344
|
-
const
|
|
10383
|
+
const created = Array.isArray(s.entriesCreated) ? s.entriesCreated : [];
|
|
10384
|
+
const modified = Array.isArray(s.entriesModified) ? s.entriesModified : [];
|
|
10385
|
+
const ids = [...created, ...modified].filter((id) => typeof id === "string" && id.length > 0);
|
|
10345
10386
|
if (i === 0) {
|
|
10346
10387
|
lastSessionOnly = [...new Set(ids)].slice(0, 5);
|
|
10347
10388
|
}
|
|
@@ -10356,445 +10397,30 @@ function extractSessionEntryIds(priorSessions) {
|
|
|
10356
10397
|
}
|
|
10357
10398
|
return { all, lastSessionOnly };
|
|
10358
10399
|
}
|
|
10359
|
-
function computeOrganisationHealth(entries) {
|
|
10360
|
-
let agreements = 0;
|
|
10361
|
-
let disagreements = 0;
|
|
10362
|
-
let abstentions = 0;
|
|
10363
|
-
const flagMap = /* @__PURE__ */ new Map();
|
|
10364
|
-
for (const entry of entries) {
|
|
10365
|
-
const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
10366
|
-
const description = typeof entry.data?.description === "string" ? entry.data.description : "";
|
|
10367
|
-
const result = classifyCollection(entry.name, description);
|
|
10368
|
-
if (!result) {
|
|
10369
|
-
abstentions++;
|
|
10370
|
-
continue;
|
|
10371
|
-
}
|
|
10372
|
-
if (result.collection === slug) {
|
|
10373
|
-
agreements++;
|
|
10374
|
-
} else {
|
|
10375
|
-
disagreements++;
|
|
10376
|
-
if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
|
|
10377
|
-
const suggestions = flagMap.get(slug);
|
|
10378
|
-
suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
|
|
10379
|
-
}
|
|
10380
|
-
}
|
|
10381
|
-
const opinionated = agreements + disagreements;
|
|
10382
|
-
const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
|
|
10383
|
-
const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
|
|
10384
|
-
const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
|
|
10385
|
-
const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
10386
|
-
return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
|
|
10387
|
-
}).sort((a, b) => b.count - a.count).slice(0, 3);
|
|
10388
|
-
return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
|
|
10389
|
-
}
|
|
10390
|
-
function formatOrgHealthLines(orgHealth, maxFlags = 3) {
|
|
10391
|
-
const lines = [];
|
|
10392
|
-
if (orgHealth.disagreements > 0) {
|
|
10393
|
-
lines.push(
|
|
10394
|
-
`${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
|
|
10395
|
-
);
|
|
10396
|
-
for (const flag of orgHealth.flags.slice(0, maxFlags)) {
|
|
10397
|
-
lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
|
|
10398
|
-
}
|
|
10399
|
-
} else if (orgHealth.reviewed > 0) {
|
|
10400
|
-
lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
|
|
10401
|
-
}
|
|
10402
|
-
return lines;
|
|
10403
|
-
}
|
|
10404
|
-
async function fetchOrganisationHealth() {
|
|
10405
|
-
try {
|
|
10406
|
-
const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
|
|
10407
|
-
if (!allEntries || allEntries.length === 0) return null;
|
|
10408
|
-
return computeOrganisationHealth(allEntries);
|
|
10409
|
-
} catch (err) {
|
|
10410
|
-
process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
|
|
10411
|
-
`);
|
|
10412
|
-
return null;
|
|
10413
|
-
}
|
|
10414
|
-
}
|
|
10415
|
-
async function handleHealthCheck() {
|
|
10416
|
-
const start = Date.now();
|
|
10417
|
-
const errors = [];
|
|
10418
|
-
let workspaceId;
|
|
10419
|
-
try {
|
|
10420
|
-
workspaceId = await getWorkspaceId();
|
|
10421
|
-
} catch (e) {
|
|
10422
|
-
errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10423
|
-
}
|
|
10424
|
-
let collections = [];
|
|
10425
|
-
try {
|
|
10426
|
-
collections = await mcpQuery("chain.listCollections");
|
|
10427
|
-
} catch (e) {
|
|
10428
|
-
errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10429
|
-
}
|
|
10430
|
-
let totalEntries = 0;
|
|
10431
|
-
if (collections.length > 0) {
|
|
10432
|
-
try {
|
|
10433
|
-
const entries = await mcpQuery("chain.listEntries", {});
|
|
10434
|
-
totalEntries = entries.length;
|
|
10435
|
-
} catch (e) {
|
|
10436
|
-
errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10437
|
-
}
|
|
10438
|
-
}
|
|
10439
|
-
let wsCtx = null;
|
|
10440
|
-
try {
|
|
10441
|
-
wsCtx = await getWorkspaceContext();
|
|
10442
|
-
} catch {
|
|
10443
|
-
}
|
|
10444
|
-
const durationMs = Date.now() - start;
|
|
10445
|
-
const healthy = errors.length === 0;
|
|
10446
|
-
const lines = [
|
|
10447
|
-
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
10448
|
-
"",
|
|
10449
|
-
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
10450
|
-
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
10451
|
-
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
10452
|
-
`**Collections:** ${collections.length}`,
|
|
10453
|
-
`**Entries:** ${totalEntries}`,
|
|
10454
|
-
`**Latency:** ${durationMs}ms`
|
|
10455
|
-
];
|
|
10456
|
-
if (errors.length > 0) {
|
|
10457
|
-
lines.push("", "## Errors");
|
|
10458
|
-
for (const err of errors) {
|
|
10459
|
-
lines.push(`- ${err}`);
|
|
10460
|
-
}
|
|
10461
|
-
}
|
|
10462
|
-
const healthData = {
|
|
10463
|
-
healthy,
|
|
10464
|
-
collections: collections.length,
|
|
10465
|
-
entries: totalEntries,
|
|
10466
|
-
latencyMs: durationMs,
|
|
10467
|
-
workspace: workspaceId ?? "unresolved"
|
|
10468
|
-
};
|
|
10469
|
-
return {
|
|
10470
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10471
|
-
structuredContent: success(
|
|
10472
|
-
healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
|
|
10473
|
-
healthData
|
|
10474
|
-
)
|
|
10475
|
-
};
|
|
10476
|
-
}
|
|
10477
|
-
async function handleWhoami() {
|
|
10478
|
-
const ctx = await getWorkspaceContext();
|
|
10479
|
-
const sessionId = getAgentSessionId();
|
|
10480
|
-
const scope = getApiKeyScope();
|
|
10481
|
-
const oriented = isSessionOriented();
|
|
10482
|
-
const lines = [
|
|
10483
|
-
`# Session Identity`,
|
|
10484
|
-
"",
|
|
10485
|
-
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
10486
|
-
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
10487
|
-
`**Workspace Name:** ${ctx.workspaceName}`
|
|
10488
|
-
];
|
|
10489
|
-
return {
|
|
10490
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10491
|
-
structuredContent: success(
|
|
10492
|
-
`Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
|
|
10493
|
-
{ workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
|
|
10494
|
-
)
|
|
10495
|
-
};
|
|
10496
|
-
}
|
|
10497
|
-
var STAGE_LABELS = {
|
|
10498
|
-
blank: "Blank",
|
|
10499
|
-
seeded: "Seeded",
|
|
10500
|
-
grounded: "Grounded",
|
|
10501
|
-
connected: "Connected"
|
|
10502
|
-
};
|
|
10503
|
-
var STAGE_DESCRIPTIONS = {
|
|
10504
|
-
blank: "No knowledge captured yet.",
|
|
10505
|
-
seeded: "Early knowledge is in place \u2014 keep building.",
|
|
10506
|
-
grounded: "Solid foundations \u2014 a few gaps remain.",
|
|
10507
|
-
connected: "Well-connected knowledge graph \u2014 your Brain is useful."
|
|
10508
|
-
};
|
|
10509
|
-
async function handleWorkspaceStatus() {
|
|
10510
|
-
const result = await mcpQuery("chain.workspaceReadiness");
|
|
10511
|
-
const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
|
|
10512
|
-
const scoringVersion = result.scoringVersion ?? "v1";
|
|
10513
|
-
const stage = result.stage ?? "seeded";
|
|
10514
|
-
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
|
10515
|
-
const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
|
|
10516
|
-
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
10517
|
-
const lines = [
|
|
10518
|
-
`# Brain Status: ${stageLabel}`,
|
|
10519
|
-
`_${stageDescription}_`,
|
|
10520
|
-
"",
|
|
10521
|
-
`${scoreBar} ${stageLabel} \xB7 ${score}%`,
|
|
10522
|
-
`**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
|
|
10523
|
-
"",
|
|
10524
|
-
"## Stats",
|
|
10525
|
-
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
10526
|
-
`- **Relations:** ${stats.totalRelations}`,
|
|
10527
|
-
`- **Collections:** ${stats.collectionCount}`,
|
|
10528
|
-
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
10529
|
-
""
|
|
10530
|
-
];
|
|
10531
|
-
if (gaps.length > 0) {
|
|
10532
|
-
lines.push("## Gaps");
|
|
10533
|
-
for (const gap of gaps) {
|
|
10534
|
-
const action = gap.capabilityGuidance ?? gap.guidance;
|
|
10535
|
-
lines.push(`- [ ] **${gap.label}**`);
|
|
10536
|
-
lines.push(` _${action}_`);
|
|
10537
|
-
}
|
|
10538
|
-
lines.push("");
|
|
10539
|
-
}
|
|
10540
|
-
const passed = checks.filter((c) => c.passed);
|
|
10541
|
-
if (passed.length > 0) {
|
|
10542
|
-
lines.push("## Passing checks");
|
|
10543
|
-
for (const check of passed) {
|
|
10544
|
-
lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
|
|
10545
|
-
}
|
|
10546
|
-
lines.push("");
|
|
10547
|
-
}
|
|
10548
|
-
const orgHealth = await fetchOrganisationHealth();
|
|
10549
|
-
if (orgHealth && orgHealth.reviewed > 0) {
|
|
10550
|
-
lines.push("## Organisation Health");
|
|
10551
|
-
lines.push(...formatOrgHealthLines(orgHealth));
|
|
10552
|
-
lines.push("");
|
|
10553
|
-
}
|
|
10554
|
-
const statusData = {
|
|
10555
|
-
stage,
|
|
10556
|
-
scoringVersion,
|
|
10557
|
-
readinessScore: score,
|
|
10558
|
-
activeEntries: stats.activeCount,
|
|
10559
|
-
totalRelations: stats.totalRelations,
|
|
10560
|
-
orphanedEntries: stats.orphanedCount,
|
|
10561
|
-
gaps: gaps.map((g) => ({
|
|
10562
|
-
id: g.id,
|
|
10563
|
-
label: g.label,
|
|
10564
|
-
guidance: g.capabilityGuidance ?? g.guidance
|
|
10565
|
-
})),
|
|
10566
|
-
...orgHealth && { organisationHealth: orgHealth }
|
|
10567
|
-
};
|
|
10568
|
-
return {
|
|
10569
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10570
|
-
structuredContent: success(
|
|
10571
|
-
`Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
|
|
10572
|
-
statusData
|
|
10573
|
-
)
|
|
10574
|
-
};
|
|
10575
|
-
}
|
|
10576
|
-
async function handleAudit(limit) {
|
|
10577
|
-
const log = getAuditLog();
|
|
10578
|
-
const recent = log.slice(-limit);
|
|
10579
|
-
if (recent.length === 0) {
|
|
10580
|
-
return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
|
|
10581
|
-
}
|
|
10582
|
-
const summary = buildSessionSummary(log);
|
|
10583
|
-
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
10584
|
-
`];
|
|
10585
|
-
for (const entry of recent) {
|
|
10586
|
-
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
10587
|
-
const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
10588
|
-
const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
|
|
10589
|
-
logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
|
|
10590
|
-
}
|
|
10591
|
-
const auditData = {
|
|
10592
|
-
totalCalls: log.length,
|
|
10593
|
-
calls: recent.map((entry) => ({
|
|
10594
|
-
tool: entry.fn,
|
|
10595
|
-
...entry.toolContext?.action && { action: entry.toolContext.action },
|
|
10596
|
-
timestamp: entry.ts,
|
|
10597
|
-
...entry.durationMs != null && { durationMs: entry.durationMs }
|
|
10598
|
-
}))
|
|
10599
|
-
};
|
|
10600
|
-
return {
|
|
10601
|
-
content: [{ type: "text", text: `${summary}
|
|
10602
|
-
|
|
10603
|
-
---
|
|
10604
|
-
|
|
10605
|
-
${logLines.join("\n")}` }],
|
|
10606
|
-
structuredContent: success(
|
|
10607
|
-
`Audit: ${log.length} total calls, showing last ${recent.length}.`,
|
|
10608
|
-
auditData
|
|
10609
|
-
)
|
|
10610
|
-
};
|
|
10611
|
-
}
|
|
10612
|
-
var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
|
|
10613
|
-
var healthSchema = z20.object({
|
|
10614
|
-
action: z20.enum(HEALTH_ACTIONS).describe(
|
|
10615
|
-
"'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
|
|
10616
|
-
),
|
|
10617
|
-
limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
|
|
10618
|
-
});
|
|
10619
10400
|
var orientSchema = z20.object({
|
|
10620
10401
|
mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
|
|
10621
10402
|
task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
|
|
10622
10403
|
});
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
|
|
10644
|
-
scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
|
|
10645
|
-
readinessScore: z20.number(),
|
|
10646
|
-
activeEntries: z20.number(),
|
|
10647
|
-
totalRelations: z20.number(),
|
|
10648
|
-
orphanedEntries: z20.number(),
|
|
10649
|
-
gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() })),
|
|
10650
|
-
organisationHealth: organisationHealthSchema.optional()
|
|
10651
|
-
});
|
|
10652
|
-
var healthAuditOutputSchema = z20.object({
|
|
10653
|
-
totalCalls: z20.number(),
|
|
10654
|
-
calls: z20.array(z20.object({
|
|
10655
|
-
tool: z20.string(),
|
|
10656
|
-
action: z20.string().optional(),
|
|
10657
|
-
timestamp: z20.string(),
|
|
10658
|
-
durationMs: z20.number().optional()
|
|
10659
|
-
}))
|
|
10660
|
-
});
|
|
10661
|
-
var healthWhoamiOutputSchema = z20.object({
|
|
10662
|
-
workspaceId: z20.string(),
|
|
10663
|
-
workspaceName: z20.string(),
|
|
10664
|
-
scope: z20.string(),
|
|
10665
|
-
sessionId: z20.union([z20.string(), z20.null()]),
|
|
10666
|
-
oriented: z20.boolean()
|
|
10667
|
-
});
|
|
10668
|
-
var ALL_TOOL_SCHEMAS = [
|
|
10669
|
-
{ name: "entries", schema: entriesSchema },
|
|
10670
|
-
{ name: "relations", schema: relationsSchema },
|
|
10671
|
-
{ name: "graph", schema: graphSchema },
|
|
10672
|
-
{ name: "context", schema: contextSchema },
|
|
10673
|
-
{ name: "collections", schema: collectionsSchema },
|
|
10674
|
-
{ name: "session", schema: sessionSchema },
|
|
10675
|
-
{ name: "health", schema: healthSchema },
|
|
10676
|
-
{ name: "orient", schema: orientSchema },
|
|
10677
|
-
{ name: "quality", schema: qualitySchema },
|
|
10678
|
-
{ name: "workflows", schema: workflowsSchema },
|
|
10679
|
-
{ name: "session-wrapup", schema: wrapupSchema },
|
|
10680
|
-
{ name: "labels", schema: labelsSchema },
|
|
10681
|
-
{ name: "verify", schema: verifySchema },
|
|
10682
|
-
{ name: "capture", schema: captureSchema },
|
|
10683
|
-
{ name: "batch-capture", schema: batchCaptureSchema },
|
|
10684
|
-
{ name: "update-entry", schema: updateEntrySchema },
|
|
10685
|
-
{ name: "get-history", schema: getHistorySchema },
|
|
10686
|
-
{ name: "commit-entry", schema: commitEntrySchema },
|
|
10687
|
-
{ name: "start", schema: startSchema },
|
|
10688
|
-
{ name: "get-usage-summary", schema: usageSummarySchema },
|
|
10689
|
-
{ name: "chain", schema: chainSchema },
|
|
10690
|
-
{ name: "chain-version", schema: chainVersionSchema },
|
|
10691
|
-
{ name: "chain-branch", schema: chainBranchSchema },
|
|
10692
|
-
{ name: "chain-review", schema: chainReviewSchema },
|
|
10693
|
-
{ name: "create-audience-map-set", schema: createAudienceMapSetSchema },
|
|
10694
|
-
{ name: "map", schema: mapSchema },
|
|
10695
|
-
{ name: "map-slot", schema: mapSlotSchema },
|
|
10696
|
-
{ name: "map-version", schema: mapVersionSchema },
|
|
10697
|
-
{ name: "map-suggest", schema: mapSuggestSchema },
|
|
10698
|
-
{ name: "architecture", schema: architectureSchema },
|
|
10699
|
-
{ name: "architecture-admin", schema: architectureAdminSchema },
|
|
10700
|
-
{ name: "facilitate", schema: facilitateSchema }
|
|
10701
|
-
];
|
|
10702
|
-
var selfTestOutputSchema = z20.object({
|
|
10703
|
-
passed: z20.number(),
|
|
10704
|
-
failed: z20.number(),
|
|
10705
|
-
total: z20.number(),
|
|
10706
|
-
results: z20.array(z20.object({
|
|
10707
|
-
tool: z20.string(),
|
|
10708
|
-
valid: z20.boolean(),
|
|
10709
|
-
error: z20.string().optional()
|
|
10710
|
-
}))
|
|
10711
|
-
});
|
|
10712
|
-
function handleSelfTest() {
|
|
10713
|
-
const results = [];
|
|
10714
|
-
for (const { name, schema } of ALL_TOOL_SCHEMAS) {
|
|
10715
|
-
try {
|
|
10716
|
-
if (!schema || typeof schema.safeParse !== "function") {
|
|
10717
|
-
results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
|
|
10718
|
-
continue;
|
|
10719
|
-
}
|
|
10720
|
-
const test = schema.safeParse({});
|
|
10721
|
-
if (test.success || test.error) {
|
|
10722
|
-
results.push({ tool: name, valid: true });
|
|
10723
|
-
}
|
|
10724
|
-
} catch (e) {
|
|
10725
|
-
results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
|
|
10726
|
-
}
|
|
10727
|
-
}
|
|
10728
|
-
const passed = results.filter((r) => r.valid).length;
|
|
10729
|
-
const failed = results.filter((r) => !r.valid).length;
|
|
10730
|
-
const total = results.length;
|
|
10731
|
-
const lines = [
|
|
10732
|
-
`# Self-Test: Tool Schema Validation`,
|
|
10733
|
-
`**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
|
|
10734
|
-
`**Schemas validated:** ${passed}/${total}`,
|
|
10735
|
-
""
|
|
10736
|
-
];
|
|
10737
|
-
if (failed > 0) {
|
|
10738
|
-
lines.push("## Failures");
|
|
10739
|
-
for (const r of results.filter((r2) => !r2.valid)) {
|
|
10740
|
-
lines.push(`- **${r.tool}**: ${r.error}`);
|
|
10741
|
-
}
|
|
10742
|
-
lines.push("");
|
|
10743
|
-
}
|
|
10744
|
-
lines.push("## All Tools");
|
|
10745
|
-
for (const r of results) {
|
|
10746
|
-
lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
|
|
10747
|
-
}
|
|
10748
|
-
return {
|
|
10749
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10750
|
-
structuredContent: success(
|
|
10751
|
-
failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
|
|
10752
|
-
{ passed, failed, total, results }
|
|
10753
|
-
)
|
|
10754
|
-
};
|
|
10755
|
-
}
|
|
10756
|
-
function registerHealthTools(server) {
|
|
10757
|
-
server.registerTool(
|
|
10758
|
-
"health",
|
|
10759
|
-
{
|
|
10760
|
-
title: "Health",
|
|
10761
|
-
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.",
|
|
10762
|
-
inputSchema: healthSchema,
|
|
10763
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
10764
|
-
},
|
|
10765
|
-
withEnvelope(async (args) => {
|
|
10766
|
-
const parsed = parseOrFail(healthSchema, args);
|
|
10767
|
-
if (!parsed.ok) return parsed.result;
|
|
10768
|
-
const { action, limit } = parsed.data;
|
|
10769
|
-
return runWithToolContext({ tool: "health", action }, async () => {
|
|
10770
|
-
if (action === "check") return handleHealthCheck();
|
|
10771
|
-
if (action === "whoami") return handleWhoami();
|
|
10772
|
-
if (action === "status") return handleWorkspaceStatus();
|
|
10773
|
-
if (action === "audit") return handleAudit(limit ?? 20);
|
|
10774
|
-
if (action === "self-test") return handleSelfTest();
|
|
10775
|
-
return unknownAction(action, HEALTH_ACTIONS);
|
|
10776
|
-
});
|
|
10777
|
-
})
|
|
10778
|
-
);
|
|
10779
|
-
server.registerTool(
|
|
10780
|
-
"orient",
|
|
10781
|
-
{
|
|
10782
|
-
title: "Orient \u2014 Start Here",
|
|
10783
|
-
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.",
|
|
10784
|
-
inputSchema: orientSchema,
|
|
10785
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
10786
|
-
},
|
|
10787
|
-
withEnvelope(async ({ mode = "full", task } = {}) => {
|
|
10788
|
-
const errors = [];
|
|
10789
|
-
const agentSessionId = getAgentSessionId();
|
|
10790
|
-
if (isSessionOriented() && mode === "brief" && !task) {
|
|
10791
|
-
return {
|
|
10792
|
-
content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
|
|
10793
|
-
structuredContent: success(
|
|
10794
|
-
"Already oriented. Session active, writes unlocked.",
|
|
10795
|
-
{ alreadyOriented: true, sessionId: agentSessionId }
|
|
10796
|
-
)
|
|
10797
|
-
};
|
|
10404
|
+
function registerOrientTool(server) {
|
|
10405
|
+
server.registerTool(
|
|
10406
|
+
"orient",
|
|
10407
|
+
{
|
|
10408
|
+
title: "Orient \u2014 Start Here",
|
|
10409
|
+
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.",
|
|
10410
|
+
inputSchema: orientSchema,
|
|
10411
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
10412
|
+
},
|
|
10413
|
+
withEnvelope(async ({ mode = "full", task } = {}) => {
|
|
10414
|
+
const errors = [];
|
|
10415
|
+
const agentSessionId = getAgentSessionId();
|
|
10416
|
+
if (isSessionOriented() && mode === "brief" && !task) {
|
|
10417
|
+
return {
|
|
10418
|
+
content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
|
|
10419
|
+
structuredContent: success(
|
|
10420
|
+
"Already oriented. Session active, writes unlocked.",
|
|
10421
|
+
{ alreadyOriented: true, sessionId: agentSessionId }
|
|
10422
|
+
)
|
|
10423
|
+
};
|
|
10798
10424
|
}
|
|
10799
10425
|
let wsCtx = null;
|
|
10800
10426
|
try {
|
|
@@ -10898,6 +10524,11 @@ function registerHealthTools(server) {
|
|
|
10898
10524
|
lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
|
|
10899
10525
|
}
|
|
10900
10526
|
}
|
|
10527
|
+
const briefGaps = readiness?.gaps ?? [];
|
|
10528
|
+
if (briefGaps.length > 0) {
|
|
10529
|
+
const oneLiner = formatGapOneLiner(briefGaps[0]);
|
|
10530
|
+
if (oneLiner) lines.push(oneLiner);
|
|
10531
|
+
}
|
|
10901
10532
|
if (recoveryBlock) {
|
|
10902
10533
|
lines.push("");
|
|
10903
10534
|
lines.push(...formatRecoveryBlock(recoveryBlock));
|
|
@@ -10968,29 +10599,31 @@ function registerHealthTools(server) {
|
|
|
10968
10599
|
lines.push("");
|
|
10969
10600
|
}
|
|
10970
10601
|
}
|
|
10602
|
+
let fullCoherenceSnapshot;
|
|
10971
10603
|
if (isLowReadiness) {
|
|
10972
|
-
|
|
10973
|
-
lines.push("");
|
|
10974
|
-
const captureBehaviorNote = (readiness?.governanceMode ?? wsCtx?.governanceMode ?? "open") === "open" ? "_In Open mode, user-authored captures commit immediately unless you ask me to keep them as drafts._" : '_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._';
|
|
10604
|
+
const captureBehaviorNote = (readiness?.governanceMode ?? wsCtx?.governanceMode ?? "open") === "open" ? "_In Open mode, captures commit automatically unless you ask me to keep them as drafts._" : "_Everything stays as a draft until you confirm._";
|
|
10975
10605
|
const gaps = readiness.gaps ?? [];
|
|
10976
10606
|
if (gaps.length > 0) {
|
|
10977
|
-
const
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10607
|
+
const gapCtx = {
|
|
10608
|
+
stage: orientStage,
|
|
10609
|
+
passedChecks: readiness.passedChecks ?? 0,
|
|
10610
|
+
totalChecks: readiness.totalChecks ?? 0,
|
|
10611
|
+
score: readiness.score ?? 0
|
|
10612
|
+
};
|
|
10613
|
+
lines.push(formatTopGapPrompt(gaps[0], gaps.length - 1, gapCtx));
|
|
10983
10614
|
lines.push("");
|
|
10984
10615
|
lines.push(captureBehaviorNote);
|
|
10985
10616
|
lines.push("");
|
|
10986
10617
|
const remainingGaps = gaps.length - 1;
|
|
10987
10618
|
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
10988
|
-
|
|
10619
|
+
const parts = [];
|
|
10620
|
+
if (remainingGaps > 0) parts.push(`${remainingGaps} more area${remainingGaps === 1 ? "" : "s"} to cover`);
|
|
10621
|
+
if (openTensions.length > 0) parts.push(`${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}`);
|
|
10622
|
+
lines.push(`_${parts.join(" and ")} \u2014 ask for full status to see everything._`);
|
|
10989
10623
|
lines.push("");
|
|
10990
10624
|
}
|
|
10991
10625
|
}
|
|
10992
|
-
lines.push("_Need a collection that doesn't exist yet
|
|
10993
|
-
lines.push("_Use `collections action=create` to add it, or ask me to propose collections for your domain._");
|
|
10626
|
+
lines.push("_Need a collection that doesn't exist yet? Just ask \u2014 I'll set it up._");
|
|
10994
10627
|
lines.push("");
|
|
10995
10628
|
} else if (readiness) {
|
|
10996
10629
|
const fmt = (e) => {
|
|
@@ -10998,10 +10631,9 @@ function registerHealthTools(server) {
|
|
|
10998
10631
|
const stratum = e.stratum ?? "?";
|
|
10999
10632
|
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
|
|
11000
10633
|
};
|
|
11001
|
-
let fullCoherenceSnapshot2;
|
|
11002
10634
|
const fullCoherence = buildCoherenceSection();
|
|
11003
10635
|
if (fullCoherence) {
|
|
11004
|
-
|
|
10636
|
+
fullCoherenceSnapshot = fullCoherence.snapshot;
|
|
11005
10637
|
}
|
|
11006
10638
|
if (task) {
|
|
11007
10639
|
lines.push(`**Brain stage: ${orientStage}.** Working on: **${task}**`);
|
|
@@ -11089,6 +10721,13 @@ function registerHealthTools(server) {
|
|
|
11089
10721
|
orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
|
|
11090
10722
|
lines.push("");
|
|
11091
10723
|
}
|
|
10724
|
+
if (orientEntries.strategyHighlights?.length > 0) {
|
|
10725
|
+
lines.push("## Strategy highlights");
|
|
10726
|
+
lines.push("_One-sentence strategy, positioning, moat, business model, GTM \u2014 high-level strategic context._");
|
|
10727
|
+
lines.push("");
|
|
10728
|
+
orientEntries.strategyHighlights.forEach((e) => lines.push(fmt(e)));
|
|
10729
|
+
lines.push("");
|
|
10730
|
+
}
|
|
11092
10731
|
if (orientEntries.recentDecisions?.length > 0) {
|
|
11093
10732
|
lines.push("## Recent decisions");
|
|
11094
10733
|
orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
|
|
@@ -11168,40 +10807,535 @@ function registerHealthTools(server) {
|
|
|
11168
10807
|
for (const err of errors) lines.push(`- ${err}`);
|
|
11169
10808
|
lines.push("");
|
|
11170
10809
|
}
|
|
11171
|
-
if (agentSessionId) {
|
|
11172
|
-
try {
|
|
11173
|
-
await mcpCall("agent.markOriented", {
|
|
11174
|
-
sessionId: agentSessionId,
|
|
11175
|
-
...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
|
|
11176
|
-
});
|
|
11177
|
-
setSessionOriented(true);
|
|
11178
|
-
lines.push("---");
|
|
11179
|
-
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
11180
|
-
} catch {
|
|
11181
|
-
lines.push("---");
|
|
11182
|
-
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
11183
|
-
}
|
|
11184
|
-
try {
|
|
11185
|
-
await mcpMutation("chain.recordSessionSignal", {
|
|
11186
|
-
sessionId: agentSessionId,
|
|
11187
|
-
signalType: "immediate_context_load",
|
|
11188
|
-
metadata: { source: "orient" }
|
|
11189
|
-
});
|
|
11190
|
-
} catch (err) {
|
|
11191
|
-
process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
|
|
11192
|
-
`);
|
|
11193
|
-
}
|
|
11194
|
-
} else {
|
|
11195
|
-
lines.push("---");
|
|
11196
|
-
lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
|
|
10810
|
+
if (agentSessionId) {
|
|
10811
|
+
try {
|
|
10812
|
+
await mcpCall("agent.markOriented", {
|
|
10813
|
+
sessionId: agentSessionId,
|
|
10814
|
+
...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
|
|
10815
|
+
});
|
|
10816
|
+
setSessionOriented(true);
|
|
10817
|
+
lines.push("---");
|
|
10818
|
+
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
10819
|
+
} catch {
|
|
10820
|
+
lines.push("---");
|
|
10821
|
+
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
10822
|
+
}
|
|
10823
|
+
try {
|
|
10824
|
+
await mcpMutation("chain.recordSessionSignal", {
|
|
10825
|
+
sessionId: agentSessionId,
|
|
10826
|
+
signalType: "immediate_context_load",
|
|
10827
|
+
metadata: { source: "orient" }
|
|
10828
|
+
});
|
|
10829
|
+
} catch (err) {
|
|
10830
|
+
process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
|
|
10831
|
+
`);
|
|
10832
|
+
}
|
|
10833
|
+
} else {
|
|
10834
|
+
lines.push("---");
|
|
10835
|
+
lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
|
|
10836
|
+
}
|
|
10837
|
+
return {
|
|
10838
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10839
|
+
structuredContent: success(
|
|
10840
|
+
`Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
|
|
10841
|
+
{ mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
|
|
10842
|
+
)
|
|
10843
|
+
};
|
|
10844
|
+
})
|
|
10845
|
+
);
|
|
10846
|
+
}
|
|
10847
|
+
|
|
10848
|
+
// src/tools/health.ts
|
|
10849
|
+
var CALL_CATEGORIES = {
|
|
10850
|
+
"chain.getEntry": "read",
|
|
10851
|
+
"chain.batchGetEntries": "read",
|
|
10852
|
+
"chain.listEntries": "read",
|
|
10853
|
+
"chain.listEntryHistory": "read",
|
|
10854
|
+
"chain.listEntryRelations": "read",
|
|
10855
|
+
"chain.listEntriesByLabel": "read",
|
|
10856
|
+
"chain.searchEntries": "search",
|
|
10857
|
+
"chain.createEntry": "write",
|
|
10858
|
+
"chain.updateEntry": "write",
|
|
10859
|
+
"chain.moveToCollection": "write",
|
|
10860
|
+
"chain.createEntryRelation": "write",
|
|
10861
|
+
"chain.applyLabel": "label",
|
|
10862
|
+
"chain.removeLabel": "label",
|
|
10863
|
+
"chain.createLabel": "label",
|
|
10864
|
+
"chain.updateLabel": "label",
|
|
10865
|
+
"chain.deleteLabel": "label",
|
|
10866
|
+
"chain.createCollection": "write",
|
|
10867
|
+
"chain.updateCollection": "write",
|
|
10868
|
+
"chain.listCollections": "meta",
|
|
10869
|
+
"chain.getCollection": "meta",
|
|
10870
|
+
"chain.listLabels": "meta",
|
|
10871
|
+
"resolveWorkspace": "meta"
|
|
10872
|
+
};
|
|
10873
|
+
function categorize(fn) {
|
|
10874
|
+
return CALL_CATEGORIES[fn] ?? "meta";
|
|
10875
|
+
}
|
|
10876
|
+
function formatDuration(ms) {
|
|
10877
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
10878
|
+
const mins = Math.floor(ms / 6e4);
|
|
10879
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
10880
|
+
return `${mins}m ${secs}s`;
|
|
10881
|
+
}
|
|
10882
|
+
function buildSessionSummary(log) {
|
|
10883
|
+
if (log.length === 0) return "";
|
|
10884
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
10885
|
+
let errorCount = 0;
|
|
10886
|
+
let writeCreates = 0;
|
|
10887
|
+
let writeUpdates = 0;
|
|
10888
|
+
for (const entry of log) {
|
|
10889
|
+
const cat = categorize(entry.fn);
|
|
10890
|
+
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
10891
|
+
const fnCounts = byCategory.get(cat);
|
|
10892
|
+
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
10893
|
+
if (entry.status === "error") errorCount++;
|
|
10894
|
+
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
10895
|
+
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
10896
|
+
}
|
|
10897
|
+
const firstTs = new Date(log[0].ts).getTime();
|
|
10898
|
+
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
10899
|
+
const duration = formatDuration(lastTs - firstTs);
|
|
10900
|
+
const lines = [`# Session Summary (${duration})
|
|
10901
|
+
`];
|
|
10902
|
+
const categoryLabels = [
|
|
10903
|
+
["read", "Reads"],
|
|
10904
|
+
["search", "Searches"],
|
|
10905
|
+
["write", "Writes"],
|
|
10906
|
+
["label", "Labels"],
|
|
10907
|
+
["meta", "Meta"]
|
|
10908
|
+
];
|
|
10909
|
+
for (const [cat, label] of categoryLabels) {
|
|
10910
|
+
const fnCounts = byCategory.get(cat);
|
|
10911
|
+
if (!fnCounts || fnCounts.size === 0) continue;
|
|
10912
|
+
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
10913
|
+
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
10914
|
+
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
10915
|
+
}
|
|
10916
|
+
lines.push(`- **Errors:** ${errorCount}`);
|
|
10917
|
+
if (writeCreates > 0 || writeUpdates > 0) {
|
|
10918
|
+
lines.push("");
|
|
10919
|
+
lines.push("## Knowledge Contribution");
|
|
10920
|
+
if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
|
|
10921
|
+
if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
|
|
10922
|
+
}
|
|
10923
|
+
return lines.join("\n");
|
|
10924
|
+
}
|
|
10925
|
+
function computeOrganisationHealth(entries) {
|
|
10926
|
+
let agreements = 0;
|
|
10927
|
+
let disagreements = 0;
|
|
10928
|
+
let abstentions = 0;
|
|
10929
|
+
const flagMap = /* @__PURE__ */ new Map();
|
|
10930
|
+
for (const entry of entries) {
|
|
10931
|
+
const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
10932
|
+
const description = typeof entry.data?.description === "string" ? entry.data.description : "";
|
|
10933
|
+
const result = classifyCollection(entry.name, description);
|
|
10934
|
+
if (!result) {
|
|
10935
|
+
abstentions++;
|
|
10936
|
+
continue;
|
|
10937
|
+
}
|
|
10938
|
+
if (result.collection === slug) {
|
|
10939
|
+
agreements++;
|
|
10940
|
+
} else {
|
|
10941
|
+
disagreements++;
|
|
10942
|
+
if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
|
|
10943
|
+
const suggestions = flagMap.get(slug);
|
|
10944
|
+
suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
const opinionated = agreements + disagreements;
|
|
10948
|
+
const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
|
|
10949
|
+
const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
|
|
10950
|
+
const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
|
|
10951
|
+
const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
10952
|
+
return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
|
|
10953
|
+
}).sort((a, b) => b.count - a.count).slice(0, 3);
|
|
10954
|
+
return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
|
|
10955
|
+
}
|
|
10956
|
+
function formatOrgHealthLines(orgHealth, maxFlags = 3) {
|
|
10957
|
+
const lines = [];
|
|
10958
|
+
if (orgHealth.disagreements > 0) {
|
|
10959
|
+
lines.push(
|
|
10960
|
+
`${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
|
|
10961
|
+
);
|
|
10962
|
+
for (const flag of orgHealth.flags.slice(0, maxFlags)) {
|
|
10963
|
+
lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
|
|
10964
|
+
}
|
|
10965
|
+
} else if (orgHealth.reviewed > 0) {
|
|
10966
|
+
lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
|
|
10967
|
+
}
|
|
10968
|
+
return lines;
|
|
10969
|
+
}
|
|
10970
|
+
async function fetchOrganisationHealth() {
|
|
10971
|
+
try {
|
|
10972
|
+
const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
|
|
10973
|
+
if (!allEntries || allEntries.length === 0) return null;
|
|
10974
|
+
return computeOrganisationHealth(allEntries);
|
|
10975
|
+
} catch (err) {
|
|
10976
|
+
process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
|
|
10977
|
+
`);
|
|
10978
|
+
return null;
|
|
10979
|
+
}
|
|
10980
|
+
}
|
|
10981
|
+
async function handleHealthCheck() {
|
|
10982
|
+
const start = Date.now();
|
|
10983
|
+
const errors = [];
|
|
10984
|
+
let workspaceId;
|
|
10985
|
+
try {
|
|
10986
|
+
workspaceId = await getWorkspaceId();
|
|
10987
|
+
} catch (e) {
|
|
10988
|
+
errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10989
|
+
}
|
|
10990
|
+
let collections = [];
|
|
10991
|
+
try {
|
|
10992
|
+
collections = await mcpQuery("chain.listCollections");
|
|
10993
|
+
} catch (e) {
|
|
10994
|
+
errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10995
|
+
}
|
|
10996
|
+
let totalEntries = 0;
|
|
10997
|
+
if (collections.length > 0) {
|
|
10998
|
+
try {
|
|
10999
|
+
const entries = await mcpQuery("chain.listEntries", {});
|
|
11000
|
+
totalEntries = entries.length;
|
|
11001
|
+
} catch (e) {
|
|
11002
|
+
errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
let wsCtx = null;
|
|
11006
|
+
try {
|
|
11007
|
+
wsCtx = await getWorkspaceContext();
|
|
11008
|
+
} catch {
|
|
11009
|
+
}
|
|
11010
|
+
const durationMs = Date.now() - start;
|
|
11011
|
+
const healthy = errors.length === 0;
|
|
11012
|
+
const lines = [
|
|
11013
|
+
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
11014
|
+
"",
|
|
11015
|
+
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
11016
|
+
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
11017
|
+
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
11018
|
+
`**Collections:** ${collections.length}`,
|
|
11019
|
+
`**Entries:** ${totalEntries}`,
|
|
11020
|
+
`**Latency:** ${durationMs}ms`
|
|
11021
|
+
];
|
|
11022
|
+
if (errors.length > 0) {
|
|
11023
|
+
lines.push("", "## Errors");
|
|
11024
|
+
for (const err of errors) {
|
|
11025
|
+
lines.push(`- ${err}`);
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
const healthData = {
|
|
11029
|
+
healthy,
|
|
11030
|
+
collections: collections.length,
|
|
11031
|
+
entries: totalEntries,
|
|
11032
|
+
latencyMs: durationMs,
|
|
11033
|
+
workspace: workspaceId ?? "unresolved"
|
|
11034
|
+
};
|
|
11035
|
+
return {
|
|
11036
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11037
|
+
structuredContent: success(
|
|
11038
|
+
healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
|
|
11039
|
+
healthData
|
|
11040
|
+
)
|
|
11041
|
+
};
|
|
11042
|
+
}
|
|
11043
|
+
async function handleWhoami() {
|
|
11044
|
+
const ctx = await getWorkspaceContext();
|
|
11045
|
+
const sessionId = getAgentSessionId();
|
|
11046
|
+
const scope = getApiKeyScope();
|
|
11047
|
+
const oriented = isSessionOriented();
|
|
11048
|
+
const lines = [
|
|
11049
|
+
`# Session Identity`,
|
|
11050
|
+
"",
|
|
11051
|
+
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
11052
|
+
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
11053
|
+
`**Workspace Name:** ${ctx.workspaceName}`
|
|
11054
|
+
];
|
|
11055
|
+
return {
|
|
11056
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11057
|
+
structuredContent: success(
|
|
11058
|
+
`Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
|
|
11059
|
+
{ workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
|
|
11060
|
+
)
|
|
11061
|
+
};
|
|
11062
|
+
}
|
|
11063
|
+
var STAGE_LABELS = {
|
|
11064
|
+
blank: "Blank",
|
|
11065
|
+
seeded: "Seeded",
|
|
11066
|
+
grounded: "Grounded",
|
|
11067
|
+
connected: "Connected"
|
|
11068
|
+
};
|
|
11069
|
+
var STAGE_DESCRIPTIONS = {
|
|
11070
|
+
blank: "No knowledge captured yet.",
|
|
11071
|
+
seeded: "Early knowledge is in place \u2014 keep building.",
|
|
11072
|
+
grounded: "Solid foundations \u2014 a few gaps remain.",
|
|
11073
|
+
connected: "Well-connected knowledge graph \u2014 your Brain is useful."
|
|
11074
|
+
};
|
|
11075
|
+
async function handleWorkspaceStatus() {
|
|
11076
|
+
const result = await mcpQuery("chain.workspaceReadiness");
|
|
11077
|
+
const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
|
|
11078
|
+
const scoringVersion = result.scoringVersion ?? "v1";
|
|
11079
|
+
const stage = result.stage ?? "seeded";
|
|
11080
|
+
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
|
11081
|
+
const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
|
|
11082
|
+
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
11083
|
+
const lines = [
|
|
11084
|
+
`# Brain Status: ${stageLabel}`,
|
|
11085
|
+
`_${stageDescription}_`,
|
|
11086
|
+
"",
|
|
11087
|
+
`${scoreBar} ${stageLabel} \xB7 ${score}%`,
|
|
11088
|
+
`**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
|
|
11089
|
+
"",
|
|
11090
|
+
"## Stats",
|
|
11091
|
+
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
11092
|
+
`- **Relations:** ${stats.totalRelations}`,
|
|
11093
|
+
`- **Collections:** ${stats.collectionCount}`,
|
|
11094
|
+
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
11095
|
+
""
|
|
11096
|
+
];
|
|
11097
|
+
if (gaps.length > 0) {
|
|
11098
|
+
lines.push("## Gaps");
|
|
11099
|
+
for (const gap of gaps) {
|
|
11100
|
+
const action = gap.capabilityGuidance ?? gap.guidance;
|
|
11101
|
+
lines.push(`- [ ] **${gap.label}**`);
|
|
11102
|
+
lines.push(` _${action}_`);
|
|
11103
|
+
}
|
|
11104
|
+
lines.push("");
|
|
11105
|
+
}
|
|
11106
|
+
const passed = checks.filter((c) => c.passed);
|
|
11107
|
+
if (passed.length > 0) {
|
|
11108
|
+
lines.push("## Passing checks");
|
|
11109
|
+
for (const check of passed) {
|
|
11110
|
+
lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
|
|
11111
|
+
}
|
|
11112
|
+
lines.push("");
|
|
11113
|
+
}
|
|
11114
|
+
const orgHealth = await fetchOrganisationHealth();
|
|
11115
|
+
if (orgHealth && orgHealth.reviewed > 0) {
|
|
11116
|
+
lines.push("## Organisation Health");
|
|
11117
|
+
lines.push(...formatOrgHealthLines(orgHealth));
|
|
11118
|
+
lines.push("");
|
|
11119
|
+
}
|
|
11120
|
+
const statusData = {
|
|
11121
|
+
stage,
|
|
11122
|
+
scoringVersion,
|
|
11123
|
+
readinessScore: score,
|
|
11124
|
+
activeEntries: stats.activeCount,
|
|
11125
|
+
totalRelations: stats.totalRelations,
|
|
11126
|
+
orphanedEntries: stats.orphanedCount,
|
|
11127
|
+
gaps: gaps.map((g) => ({
|
|
11128
|
+
id: g.id,
|
|
11129
|
+
label: g.label,
|
|
11130
|
+
guidance: g.capabilityGuidance ?? g.guidance
|
|
11131
|
+
})),
|
|
11132
|
+
...orgHealth && { organisationHealth: orgHealth }
|
|
11133
|
+
};
|
|
11134
|
+
return {
|
|
11135
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11136
|
+
structuredContent: success(
|
|
11137
|
+
`Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
|
|
11138
|
+
statusData
|
|
11139
|
+
)
|
|
11140
|
+
};
|
|
11141
|
+
}
|
|
11142
|
+
async function handleAudit(limit) {
|
|
11143
|
+
const log = getAuditLog();
|
|
11144
|
+
const recent = log.slice(-limit);
|
|
11145
|
+
if (recent.length === 0) {
|
|
11146
|
+
return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
|
|
11147
|
+
}
|
|
11148
|
+
const summary = buildSessionSummary(log);
|
|
11149
|
+
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
11150
|
+
`];
|
|
11151
|
+
for (const entry of recent) {
|
|
11152
|
+
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
11153
|
+
const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
11154
|
+
const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
|
|
11155
|
+
logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
|
|
11156
|
+
}
|
|
11157
|
+
const auditData = {
|
|
11158
|
+
totalCalls: log.length,
|
|
11159
|
+
calls: recent.map((entry) => ({
|
|
11160
|
+
tool: entry.fn,
|
|
11161
|
+
...entry.toolContext?.action && { action: entry.toolContext.action },
|
|
11162
|
+
timestamp: entry.ts,
|
|
11163
|
+
...entry.durationMs != null && { durationMs: entry.durationMs }
|
|
11164
|
+
}))
|
|
11165
|
+
};
|
|
11166
|
+
return {
|
|
11167
|
+
content: [{ type: "text", text: `${summary}
|
|
11168
|
+
|
|
11169
|
+
---
|
|
11170
|
+
|
|
11171
|
+
${logLines.join("\n")}` }],
|
|
11172
|
+
structuredContent: success(
|
|
11173
|
+
`Audit: ${log.length} total calls, showing last ${recent.length}.`,
|
|
11174
|
+
auditData
|
|
11175
|
+
)
|
|
11176
|
+
};
|
|
11177
|
+
}
|
|
11178
|
+
var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
|
|
11179
|
+
var healthSchema = z21.object({
|
|
11180
|
+
action: z21.enum(HEALTH_ACTIONS).describe(
|
|
11181
|
+
"'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
|
|
11182
|
+
),
|
|
11183
|
+
limit: z21.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
|
|
11184
|
+
});
|
|
11185
|
+
var healthCheckOutputSchema = z21.object({
|
|
11186
|
+
healthy: z21.boolean(),
|
|
11187
|
+
collections: z21.number(),
|
|
11188
|
+
entries: z21.number(),
|
|
11189
|
+
latencyMs: z21.number(),
|
|
11190
|
+
workspace: z21.string()
|
|
11191
|
+
});
|
|
11192
|
+
var organisationHealthSchema = z21.object({
|
|
11193
|
+
reviewed: z21.number(),
|
|
11194
|
+
agreements: z21.number(),
|
|
11195
|
+
disagreements: z21.number(),
|
|
11196
|
+
abstentions: z21.number(),
|
|
11197
|
+
agreementRate: z21.number(),
|
|
11198
|
+
flags: z21.array(z21.object({
|
|
11199
|
+
collection: z21.string(),
|
|
11200
|
+
count: z21.number(),
|
|
11201
|
+
suggestedCollection: z21.string()
|
|
11202
|
+
}))
|
|
11203
|
+
});
|
|
11204
|
+
var healthStatusOutputSchema = z21.object({
|
|
11205
|
+
stage: z21.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
|
|
11206
|
+
scoringVersion: z21.enum(["v1", "v2"]).optional().default("v1"),
|
|
11207
|
+
readinessScore: z21.number(),
|
|
11208
|
+
activeEntries: z21.number(),
|
|
11209
|
+
totalRelations: z21.number(),
|
|
11210
|
+
orphanedEntries: z21.number(),
|
|
11211
|
+
gaps: z21.array(z21.object({ id: z21.string(), label: z21.string(), guidance: z21.string() })),
|
|
11212
|
+
organisationHealth: organisationHealthSchema.optional()
|
|
11213
|
+
});
|
|
11214
|
+
var healthAuditOutputSchema = z21.object({
|
|
11215
|
+
totalCalls: z21.number(),
|
|
11216
|
+
calls: z21.array(z21.object({
|
|
11217
|
+
tool: z21.string(),
|
|
11218
|
+
action: z21.string().optional(),
|
|
11219
|
+
timestamp: z21.string(),
|
|
11220
|
+
durationMs: z21.number().optional()
|
|
11221
|
+
}))
|
|
11222
|
+
});
|
|
11223
|
+
var healthWhoamiOutputSchema = z21.object({
|
|
11224
|
+
workspaceId: z21.string(),
|
|
11225
|
+
workspaceName: z21.string(),
|
|
11226
|
+
scope: z21.string(),
|
|
11227
|
+
sessionId: z21.union([z21.string(), z21.null()]),
|
|
11228
|
+
oriented: z21.boolean()
|
|
11229
|
+
});
|
|
11230
|
+
var ALL_TOOL_SCHEMAS = [
|
|
11231
|
+
{ name: "entries", schema: entriesSchema },
|
|
11232
|
+
{ name: "relations", schema: relationsSchema },
|
|
11233
|
+
{ name: "graph", schema: graphSchema },
|
|
11234
|
+
{ name: "context", schema: contextSchema },
|
|
11235
|
+
{ name: "collections", schema: collectionsSchema },
|
|
11236
|
+
{ name: "session", schema: sessionSchema },
|
|
11237
|
+
{ name: "health", schema: healthSchema },
|
|
11238
|
+
{ name: "orient", schema: orientSchema },
|
|
11239
|
+
{ name: "quality", schema: qualitySchema },
|
|
11240
|
+
{ name: "workflows", schema: workflowsSchema },
|
|
11241
|
+
{ name: "session-wrapup", schema: wrapupSchema },
|
|
11242
|
+
{ name: "labels", schema: labelsSchema },
|
|
11243
|
+
{ name: "verify", schema: verifySchema },
|
|
11244
|
+
{ name: "capture", schema: captureSchema },
|
|
11245
|
+
{ name: "batch-capture", schema: batchCaptureSchema },
|
|
11246
|
+
{ name: "update-entry", schema: updateEntrySchema },
|
|
11247
|
+
{ name: "get-history", schema: getHistorySchema },
|
|
11248
|
+
{ name: "commit-entry", schema: commitEntrySchema },
|
|
11249
|
+
{ name: "start", schema: startSchema },
|
|
11250
|
+
{ name: "get-usage-summary", schema: usageSummarySchema },
|
|
11251
|
+
{ name: "chain", schema: chainSchema },
|
|
11252
|
+
{ name: "chain-version", schema: chainVersionSchema },
|
|
11253
|
+
{ name: "chain-branch", schema: chainBranchSchema },
|
|
11254
|
+
{ name: "chain-review", schema: chainReviewSchema },
|
|
11255
|
+
{ name: "create-audience-map-set", schema: createAudienceMapSetSchema },
|
|
11256
|
+
{ name: "map", schema: mapSchema },
|
|
11257
|
+
{ name: "map-slot", schema: mapSlotSchema },
|
|
11258
|
+
{ name: "map-version", schema: mapVersionSchema },
|
|
11259
|
+
{ name: "map-suggest", schema: mapSuggestSchema },
|
|
11260
|
+
{ name: "architecture", schema: architectureSchema },
|
|
11261
|
+
{ name: "architecture-admin", schema: architectureAdminSchema },
|
|
11262
|
+
{ name: "facilitate", schema: facilitateSchema }
|
|
11263
|
+
];
|
|
11264
|
+
var selfTestOutputSchema = z21.object({
|
|
11265
|
+
passed: z21.number(),
|
|
11266
|
+
failed: z21.number(),
|
|
11267
|
+
total: z21.number(),
|
|
11268
|
+
results: z21.array(z21.object({
|
|
11269
|
+
tool: z21.string(),
|
|
11270
|
+
valid: z21.boolean(),
|
|
11271
|
+
error: z21.string().optional()
|
|
11272
|
+
}))
|
|
11273
|
+
});
|
|
11274
|
+
function handleSelfTest() {
|
|
11275
|
+
const results = [];
|
|
11276
|
+
for (const { name, schema } of ALL_TOOL_SCHEMAS) {
|
|
11277
|
+
try {
|
|
11278
|
+
if (!schema || typeof schema.safeParse !== "function") {
|
|
11279
|
+
results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
|
|
11280
|
+
continue;
|
|
11281
|
+
}
|
|
11282
|
+
const test = schema.safeParse({});
|
|
11283
|
+
if (test.success || test.error) {
|
|
11284
|
+
results.push({ tool: name, valid: true });
|
|
11197
11285
|
}
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11286
|
+
} catch (e) {
|
|
11287
|
+
results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11290
|
+
const passed = results.filter((r) => r.valid).length;
|
|
11291
|
+
const failed = results.filter((r) => !r.valid).length;
|
|
11292
|
+
const total = results.length;
|
|
11293
|
+
const lines = [
|
|
11294
|
+
`# Self-Test: Tool Schema Validation`,
|
|
11295
|
+
`**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
|
|
11296
|
+
`**Schemas validated:** ${passed}/${total}`,
|
|
11297
|
+
""
|
|
11298
|
+
];
|
|
11299
|
+
if (failed > 0) {
|
|
11300
|
+
lines.push("## Failures");
|
|
11301
|
+
for (const r of results.filter((r2) => !r2.valid)) {
|
|
11302
|
+
lines.push(`- **${r.tool}**: ${r.error}`);
|
|
11303
|
+
}
|
|
11304
|
+
lines.push("");
|
|
11305
|
+
}
|
|
11306
|
+
lines.push("## All Tools");
|
|
11307
|
+
for (const r of results) {
|
|
11308
|
+
lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
|
|
11309
|
+
}
|
|
11310
|
+
return {
|
|
11311
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11312
|
+
structuredContent: success(
|
|
11313
|
+
failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
|
|
11314
|
+
{ passed, failed, total, results }
|
|
11315
|
+
)
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
function registerHealthTools(server) {
|
|
11319
|
+
server.registerTool(
|
|
11320
|
+
"health",
|
|
11321
|
+
{
|
|
11322
|
+
title: "Health",
|
|
11323
|
+
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.",
|
|
11324
|
+
inputSchema: healthSchema,
|
|
11325
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
11326
|
+
},
|
|
11327
|
+
withEnvelope(async (args) => {
|
|
11328
|
+
const parsed = parseOrFail(healthSchema, args);
|
|
11329
|
+
if (!parsed.ok) return parsed.result;
|
|
11330
|
+
const { action, limit } = parsed.data;
|
|
11331
|
+
return runWithToolContext({ tool: "health", action }, async () => {
|
|
11332
|
+
if (action === "check") return handleHealthCheck();
|
|
11333
|
+
if (action === "whoami") return handleWhoami();
|
|
11334
|
+
if (action === "status") return handleWorkspaceStatus();
|
|
11335
|
+
if (action === "audit") return handleAudit(limit ?? 20);
|
|
11336
|
+
if (action === "self-test") return handleSelfTest();
|
|
11337
|
+
return unknownAction(action, HEALTH_ACTIONS);
|
|
11338
|
+
});
|
|
11205
11339
|
})
|
|
11206
11340
|
);
|
|
11207
11341
|
}
|
|
@@ -11650,12 +11784,197 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
|
|
|
11650
11784
|
}
|
|
11651
11785
|
|
|
11652
11786
|
// src/prompts/index.ts
|
|
11653
|
-
import { z as
|
|
11787
|
+
import { z as z23 } from "zod";
|
|
11788
|
+
|
|
11789
|
+
// src/tools/project-scan.ts
|
|
11790
|
+
import { z as z22 } from "zod";
|
|
11791
|
+
var scanExtractionSchema = z22.object({
|
|
11792
|
+
vision: z22.string().min(10).optional().describe("Product purpose from README"),
|
|
11793
|
+
audience: z22.string().optional().describe("Primary user or customer segment"),
|
|
11794
|
+
techStack: z22.array(z22.string()).optional().describe("Key technologies from package.json"),
|
|
11795
|
+
modules: z22.array(z22.object({ name: z22.string(), purpose: z22.string() })).max(8).optional().describe("Top-level modules from folder structure"),
|
|
11796
|
+
businessRules: z22.array(z22.string()).max(10).optional().describe("System constraints from .cursorrules/AGENTS.md/CLAUDE.md"),
|
|
11797
|
+
conventions: z22.array(z22.string()).max(8).optional().describe("Coding or process standards from .cursorrules"),
|
|
11798
|
+
keyDecisions: z22.array(z22.string()).optional().describe("Significant architectural decisions from git log or README"),
|
|
11799
|
+
tensions: z22.array(z22.string()).optional().describe("Friction points or known trade-offs"),
|
|
11800
|
+
keyTerms: z22.array(z22.string()).optional().describe("Domain-specific vocabulary found in files")
|
|
11801
|
+
});
|
|
11802
|
+
var COLLECTION_PRIORITY = {
|
|
11803
|
+
strategy: 0,
|
|
11804
|
+
audiences: 1,
|
|
11805
|
+
architecture: 2,
|
|
11806
|
+
decisions: 3,
|
|
11807
|
+
"business-rules": 4,
|
|
11808
|
+
standards: 5,
|
|
11809
|
+
glossary: 6,
|
|
11810
|
+
tensions: 7
|
|
11811
|
+
};
|
|
11812
|
+
var GENERIC_MODULE_NAMES = /* @__PURE__ */ new Set(["src", "lib", "utils"]);
|
|
11813
|
+
function truncate(str, maxLen) {
|
|
11814
|
+
return str.length <= maxLen ? str : str.slice(0, maxLen);
|
|
11815
|
+
}
|
|
11816
|
+
function firstNWords(str, n) {
|
|
11817
|
+
return str.split(/\s+/).slice(0, n).join(" ");
|
|
11818
|
+
}
|
|
11819
|
+
function scanToBatchEntries(extracted) {
|
|
11820
|
+
const entries = [];
|
|
11821
|
+
const techStackGlossaryNames = /* @__PURE__ */ new Set();
|
|
11822
|
+
if (extracted.vision) {
|
|
11823
|
+
entries.push({
|
|
11824
|
+
collection: "strategy",
|
|
11825
|
+
name: "Product Vision",
|
|
11826
|
+
description: extracted.vision
|
|
11827
|
+
});
|
|
11828
|
+
}
|
|
11829
|
+
if (extracted.audience) {
|
|
11830
|
+
entries.push({
|
|
11831
|
+
collection: "audiences",
|
|
11832
|
+
name: firstNWords(extracted.audience, 6),
|
|
11833
|
+
description: extracted.audience
|
|
11834
|
+
});
|
|
11835
|
+
}
|
|
11836
|
+
if (extracted.techStack && extracted.techStack.length > 0) {
|
|
11837
|
+
entries.push({
|
|
11838
|
+
collection: "architecture",
|
|
11839
|
+
name: "Tech Stack",
|
|
11840
|
+
description: `Technology choices: ${extracted.techStack.join(", ")}`
|
|
11841
|
+
});
|
|
11842
|
+
for (const tech of extracted.techStack.slice(0, 3)) {
|
|
11843
|
+
const name = tech;
|
|
11844
|
+
techStackGlossaryNames.add(name.toLowerCase());
|
|
11845
|
+
entries.push({
|
|
11846
|
+
collection: "glossary",
|
|
11847
|
+
name,
|
|
11848
|
+
description: `${tech} \u2014 part of the tech stack`
|
|
11849
|
+
});
|
|
11850
|
+
}
|
|
11851
|
+
}
|
|
11852
|
+
if (extracted.modules) {
|
|
11853
|
+
let count = 0;
|
|
11854
|
+
for (const mod of extracted.modules) {
|
|
11855
|
+
if (count >= 5) break;
|
|
11856
|
+
if (GENERIC_MODULE_NAMES.has(mod.name.toLowerCase())) continue;
|
|
11857
|
+
entries.push({
|
|
11858
|
+
collection: "architecture",
|
|
11859
|
+
name: mod.name,
|
|
11860
|
+
description: mod.purpose
|
|
11861
|
+
});
|
|
11862
|
+
count++;
|
|
11863
|
+
}
|
|
11864
|
+
}
|
|
11865
|
+
for (const rule of extracted.businessRules ?? []) {
|
|
11866
|
+
entries.push({
|
|
11867
|
+
collection: "business-rules",
|
|
11868
|
+
name: truncate(rule, 70),
|
|
11869
|
+
description: rule
|
|
11870
|
+
});
|
|
11871
|
+
}
|
|
11872
|
+
for (const convention of extracted.conventions ?? []) {
|
|
11873
|
+
entries.push({
|
|
11874
|
+
collection: "standards",
|
|
11875
|
+
name: truncate(convention, 70),
|
|
11876
|
+
description: convention
|
|
11877
|
+
});
|
|
11878
|
+
}
|
|
11879
|
+
for (const decision of extracted.keyDecisions ?? []) {
|
|
11880
|
+
entries.push({
|
|
11881
|
+
collection: "decisions",
|
|
11882
|
+
name: truncate(decision, 80),
|
|
11883
|
+
description: decision
|
|
11884
|
+
});
|
|
11885
|
+
}
|
|
11886
|
+
for (const tension of extracted.tensions ?? []) {
|
|
11887
|
+
entries.push({
|
|
11888
|
+
collection: "tensions",
|
|
11889
|
+
name: truncate(tension, 80),
|
|
11890
|
+
description: tension
|
|
11891
|
+
});
|
|
11892
|
+
}
|
|
11893
|
+
for (const term of extracted.keyTerms ?? []) {
|
|
11894
|
+
if (techStackGlossaryNames.has(term.toLowerCase())) continue;
|
|
11895
|
+
entries.push({
|
|
11896
|
+
collection: "glossary",
|
|
11897
|
+
name: term,
|
|
11898
|
+
description: `Core domain term: ${term}`
|
|
11899
|
+
});
|
|
11900
|
+
}
|
|
11901
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11902
|
+
const deduped = entries.filter((e) => {
|
|
11903
|
+
const key = `${e.collection}:${e.name.toLowerCase()}`;
|
|
11904
|
+
if (seen.has(key)) return false;
|
|
11905
|
+
seen.add(key);
|
|
11906
|
+
return true;
|
|
11907
|
+
});
|
|
11908
|
+
if (deduped.length <= 30) return deduped;
|
|
11909
|
+
return deduped.sort((a, b) => {
|
|
11910
|
+
const pa = COLLECTION_PRIORITY[a.collection] ?? 99;
|
|
11911
|
+
const pb = COLLECTION_PRIORITY[b.collection] ?? 99;
|
|
11912
|
+
return pa - pb;
|
|
11913
|
+
}).slice(0, 30);
|
|
11914
|
+
}
|
|
11915
|
+
|
|
11916
|
+
// src/tools/draft-review.ts
|
|
11917
|
+
var REVIEW_COLLECTION_ORDER = [
|
|
11918
|
+
"strategy",
|
|
11919
|
+
"chains",
|
|
11920
|
+
"audiences",
|
|
11921
|
+
"architecture",
|
|
11922
|
+
"decisions",
|
|
11923
|
+
"business-rules",
|
|
11924
|
+
"standards",
|
|
11925
|
+
"glossary",
|
|
11926
|
+
"tensions"
|
|
11927
|
+
];
|
|
11928
|
+
function groupDraftsByCollection(drafts) {
|
|
11929
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
11930
|
+
for (const draft of drafts) {
|
|
11931
|
+
const existing = buckets.get(draft.collection);
|
|
11932
|
+
if (existing) {
|
|
11933
|
+
existing.push(draft);
|
|
11934
|
+
} else {
|
|
11935
|
+
buckets.set(draft.collection, [draft]);
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
const knownKeys = REVIEW_COLLECTION_ORDER.filter((k) => buckets.has(k));
|
|
11939
|
+
const unknownKeys = [...buckets.keys()].filter((k) => !REVIEW_COLLECTION_ORDER.includes(k)).sort();
|
|
11940
|
+
const ordered = /* @__PURE__ */ new Map();
|
|
11941
|
+
for (const key of [...knownKeys, ...unknownKeys]) {
|
|
11942
|
+
ordered.set(key, buckets.get(key));
|
|
11943
|
+
}
|
|
11944
|
+
return ordered;
|
|
11945
|
+
}
|
|
11946
|
+
var COLLECTION_DISPLAY_NAMES = {
|
|
11947
|
+
"business-rules": "Business Rules",
|
|
11948
|
+
"tracking-events": "Tracking Events"
|
|
11949
|
+
};
|
|
11950
|
+
function collectionLabel(slug) {
|
|
11951
|
+
return COLLECTION_DISPLAY_NAMES[slug] ?? slug.charAt(0).toUpperCase() + slug.slice(1);
|
|
11952
|
+
}
|
|
11953
|
+
function formatDraftGroups(grouped, total) {
|
|
11954
|
+
if (total === 0) {
|
|
11955
|
+
return "No drafts to review \u2014 your workspace is fully committed.";
|
|
11956
|
+
}
|
|
11957
|
+
const lines = [`## Drafts Ready for Review (${total} total)`];
|
|
11958
|
+
let counter = 1;
|
|
11959
|
+
for (const [collection, entries] of grouped) {
|
|
11960
|
+
const label = collectionLabel(collection);
|
|
11961
|
+
const count = entries.length;
|
|
11962
|
+
lines.push(`
|
|
11963
|
+
### ${label} (${count} ${count === 1 ? "entry" : "entries"})`);
|
|
11964
|
+
for (const entry of entries) {
|
|
11965
|
+
lines.push(`${counter}. ${entry.name} [${entry.entryId}]`);
|
|
11966
|
+
counter++;
|
|
11967
|
+
}
|
|
11968
|
+
}
|
|
11969
|
+
return lines.join("\n");
|
|
11970
|
+
}
|
|
11971
|
+
|
|
11972
|
+
// src/prompts/index.ts
|
|
11654
11973
|
function registerPrompts(server) {
|
|
11655
11974
|
server.prompt(
|
|
11656
11975
|
"review-against-rules",
|
|
11657
11976
|
"Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
|
|
11658
|
-
{ domain:
|
|
11977
|
+
{ domain: z23.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
11659
11978
|
async ({ domain }) => {
|
|
11660
11979
|
const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
11661
11980
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
@@ -11708,7 +12027,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
11708
12027
|
server.prompt(
|
|
11709
12028
|
"name-check",
|
|
11710
12029
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
11711
|
-
{ names:
|
|
12030
|
+
{ names: z23.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
11712
12031
|
async ({ names }) => {
|
|
11713
12032
|
const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
|
|
11714
12033
|
const glossaryContext = terms.map(
|
|
@@ -11744,7 +12063,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
11744
12063
|
server.prompt(
|
|
11745
12064
|
"draft-decision-record",
|
|
11746
12065
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
11747
|
-
{ context:
|
|
12066
|
+
{ context: z23.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
11748
12067
|
async ({ context }) => {
|
|
11749
12068
|
const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
|
|
11750
12069
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -11782,8 +12101,8 @@ After drafting, I can log it using the capture tool with collection "decisions".
|
|
|
11782
12101
|
"draft-rule-from-context",
|
|
11783
12102
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
11784
12103
|
{
|
|
11785
|
-
observation:
|
|
11786
|
-
domain:
|
|
12104
|
+
observation: z23.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
|
|
12105
|
+
domain: z23.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
|
|
11787
12106
|
},
|
|
11788
12107
|
async ({ observation, domain }) => {
|
|
11789
12108
|
const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
@@ -11824,10 +12143,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
|
|
|
11824
12143
|
"run-workflow",
|
|
11825
12144
|
"Launch any registered Chainwork workflow in Facilitator Mode. Returns the full workflow definition, facilitation instructions, and round structure. The agent enters Facilitator Mode and guides the participant through each round.",
|
|
11826
12145
|
{
|
|
11827
|
-
workflow:
|
|
12146
|
+
workflow: z23.string().describe(
|
|
11828
12147
|
"Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
|
|
11829
12148
|
),
|
|
11830
|
-
context:
|
|
12149
|
+
context: z23.string().optional().describe(
|
|
11831
12150
|
"Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
|
|
11832
12151
|
)
|
|
11833
12152
|
},
|
|
@@ -11972,7 +12291,7 @@ ${wf.errorRecovery}
|
|
|
11972
12291
|
"shape-a-bet",
|
|
11973
12292
|
"Launch a coached shaping session powered by the facilitate tool. Dynamically loads workspace context, governance constraints, and coaching rubrics. Use when the user wants to shape a bet, define a pitch, or scope work.",
|
|
11974
12293
|
{
|
|
11975
|
-
idea:
|
|
12294
|
+
idea: z23.string().describe("Brief description of the idea or feature to shape (e.g. 'Improve the glossary editing flow')")
|
|
11976
12295
|
},
|
|
11977
12296
|
async ({ idea }) => {
|
|
11978
12297
|
let strategicContext = "";
|
|
@@ -12133,9 +12452,9 @@ Walk away mid-session and everything captured so far exists as drafts. \`facilit
|
|
|
12133
12452
|
"capture-and-connect",
|
|
12134
12453
|
"Guided workflow: capture a knowledge entry, discover graph connections, create relations, and prepare for commit. Encodes the capture \u2192 suggest \u2192 batch-create \u2192 commit choreography as a step-by-step guide.",
|
|
12135
12454
|
{
|
|
12136
|
-
collection:
|
|
12137
|
-
name:
|
|
12138
|
-
description:
|
|
12455
|
+
collection: z23.string().describe("Collection slug (e.g. 'glossary', 'business-rules', 'decisions', 'tensions')"),
|
|
12456
|
+
name: z23.string().describe("Entry name"),
|
|
12457
|
+
description: z23.string().describe("Entry description")
|
|
12139
12458
|
},
|
|
12140
12459
|
async ({ collection, name, description }) => {
|
|
12141
12460
|
const collections = await mcpQuery("chain.listCollections");
|
|
@@ -12189,7 +12508,7 @@ Only call \`commit-entry\` when the user explicitly confirms.
|
|
|
12189
12508
|
"deep-dive",
|
|
12190
12509
|
"Explore everything the Chain knows about a topic or entry. Assembles entry details, graph context, related business rules, and glossary terms into a comprehensive briefing.",
|
|
12191
12510
|
{
|
|
12192
|
-
topic:
|
|
12511
|
+
topic: z23.string().describe("Entry ID (e.g. 'BR-001') or topic to explore (e.g. 'authentication')")
|
|
12193
12512
|
},
|
|
12194
12513
|
async ({ topic }) => {
|
|
12195
12514
|
const isEntryId = /^[A-Z]+-\d+$/i.test(topic) || /^[A-Z]+-[a-z0-9]+$/i.test(topic);
|
|
@@ -12272,7 +12591,7 @@ ${contextInstructions}`
|
|
|
12272
12591
|
"pre-commit-check",
|
|
12273
12592
|
"Run a readiness check before committing an entry to the Chain. Validates quality score, required relations, and business rule compliance.",
|
|
12274
12593
|
{
|
|
12275
|
-
entryId:
|
|
12594
|
+
entryId: z23.string().describe("Entry ID to check (e.g. 'GT-019', 'DEC-005')")
|
|
12276
12595
|
},
|
|
12277
12596
|
async ({ entryId }) => {
|
|
12278
12597
|
return {
|
|
@@ -12328,9 +12647,9 @@ If ready, ask the user to confirm. If not, suggest specific improvements.
|
|
|
12328
12647
|
);
|
|
12329
12648
|
server.prompt(
|
|
12330
12649
|
"project-scan",
|
|
12331
|
-
"Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack,
|
|
12650
|
+
"Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack, modules, business rules, conventions, decisions, and domain terms into batch-capture entries. The trust boundary is confirmation before capture: only capture entries the user explicitly keeps, then let normal workspace governance apply.",
|
|
12332
12651
|
{
|
|
12333
|
-
workspaceContext:
|
|
12652
|
+
workspaceContext: z23.string().optional().describe("Optional context about the workspace preset and existing collections (paste from start/orient output)")
|
|
12334
12653
|
},
|
|
12335
12654
|
async ({ workspaceContext }) => {
|
|
12336
12655
|
let collectionsContext = "";
|
|
@@ -12345,17 +12664,19 @@ ${colList}
|
|
|
12345
12664
|
}
|
|
12346
12665
|
} catch {
|
|
12347
12666
|
}
|
|
12348
|
-
const schemaFields = Object.entries(
|
|
12349
|
-
const
|
|
12350
|
-
vision: "A
|
|
12667
|
+
const schemaFields = Object.entries(scanExtractionSchema.shape).map(([key, field]) => `- \`${key}\`: ${field.description ?? ""}`).join("\n");
|
|
12668
|
+
const exampleEntries = scanToBatchEntries({
|
|
12669
|
+
vision: "A knowledge management tool for solo developers working with AI coding assistants",
|
|
12351
12670
|
audience: "Solo developers using Cursor, Claude Code, or similar AI IDEs",
|
|
12352
12671
|
techStack: ["SvelteKit", "Convex", "TypeScript"],
|
|
12672
|
+
modules: [{ name: "Product Brain MCP", purpose: "MCP server that exposes knowledge capture tools to the IDE agent" }],
|
|
12673
|
+
businessRules: ["All public functions must validate arguments with Zod before processing"],
|
|
12674
|
+
conventions: ["Use batch-capture for multi-entry writes; never call capture in a loop"],
|
|
12675
|
+
keyDecisions: ["Use Convex for real-time backend \u2014 avoids managing infrastructure"],
|
|
12353
12676
|
keyTerms: ["Chain", "Capture", "Orientation"],
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
};
|
|
12357
|
-
const exampleEntries = extractionToBatchEntries(exampleExtraction);
|
|
12358
|
-
const exampleOutput = JSON.stringify({ entries: exampleEntries.slice(0, 4) }, null, 2);
|
|
12677
|
+
tensions: ["Activation requires skill \u2014 users need to know what to capture before value compounds"]
|
|
12678
|
+
});
|
|
12679
|
+
const exampleOutput = JSON.stringify({ entries: exampleEntries.slice(0, 5) }, null, 2);
|
|
12359
12680
|
return {
|
|
12360
12681
|
messages: [{
|
|
12361
12682
|
role: "user",
|
|
@@ -12364,84 +12685,94 @@ ${colList}
|
|
|
12364
12685
|
text: `# Project Scan \u2014 Agent-Side Knowledge Extraction
|
|
12365
12686
|
|
|
12366
12687
|
You are scanning the user's local project to extract structured knowledge for Product Brain.
|
|
12367
|
-
PB runs remotely \u2014 you
|
|
12688
|
+
PB runs remotely \u2014 you read the files and extract, then call \`batch-capture\` after confirmation.
|
|
12368
12689
|
|
|
12369
12690
|
` + (workspaceContext ? `## Workspace Context
|
|
12370
12691
|
${workspaceContext}
|
|
12371
12692
|
|
|
12372
12693
|
` : "") + collectionsContext + `## Step 1: Read Project Files
|
|
12373
12694
|
|
|
12374
|
-
Read these files in
|
|
12375
|
-
1. \`README.md\` or \`readme.md\` \u2014 product description, purpose, setup
|
|
12376
|
-
2. \`package.json\` \u2014 name, description, dependencies (framework, database, auth)
|
|
12377
|
-
3. \`tsconfig.json\` or \`tsconfig.app.json\` \u2014 compilation target, paths
|
|
12378
|
-
4. Top-level folder structure (list directories only, max 2 levels deep)
|
|
12379
|
-
5. \`.cursorrules\` or \`CLAUDE.md\` or \`AGENTS.md\` \u2014 project conventions, constraints
|
|
12380
|
-
6. \`git log --oneline -20\` \u2014 recent commit messages for decisions/tensions
|
|
12695
|
+
Read these files in order. Skip gracefully if not present.
|
|
12381
12696
|
|
|
12382
|
-
|
|
12383
|
-
|
|
12697
|
+
1. **\`README.md\`** \u2014 Look for: product purpose (vision), who it's for (audience), key concepts, architecture notes, known issues
|
|
12698
|
+
2. **\`package.json\`** \u2014 Look for: \`name\`, \`description\`, and **dependencies** \u2014 identify the framework, database, auth library, UI library (these become tech stack + glossary). Ignore dev tools and test libraries.
|
|
12699
|
+
3. **\`tsconfig.json\` / \`tsconfig.app.json\`** \u2014 Look for: path aliases (e.g. \`@/\`) that hint at module structure
|
|
12700
|
+
4. **Top-level folder structure** (list directories only, max 2 levels) \u2014 Look for: named modules, feature folders, service boundaries. Note names that are specific enough to be meaningful.
|
|
12701
|
+
5. **\`.cursorrules\` / \`CLAUDE.md\` / \`AGENTS.md\`** \u2014 Look for: **business rules** (constraints the system enforces), **conventions** (how work is done here), no-gos, domain vocabulary
|
|
12702
|
+
6. **\`git log --oneline -20\`** \u2014 Look for: commits that imply decisions ("migrate from X to Y", "switch to Z for reason"), repeated friction ("fix broken", "revert", "hotfix same area twice")
|
|
12703
|
+
|
|
12704
|
+
**ICP stack only (V1):** README, package.json, tsconfig, folder structure, .cursorrules/CLAUDE.md/AGENTS.md, git log.
|
|
12705
|
+
Do not attempt Python, Rust, Go, or mobile-specific files in this pass.
|
|
12384
12706
|
|
|
12385
12707
|
## Step 2: Extract Structured Data
|
|
12386
12708
|
|
|
12387
|
-
After reading, extract
|
|
12709
|
+
After reading all files, extract into this schema:
|
|
12388
12710
|
|
|
12389
12711
|
\`\`\`
|
|
12390
12712
|
${schemaFields}
|
|
12391
12713
|
\`\`\`
|
|
12392
12714
|
|
|
12715
|
+
**Field-by-field guidance:**
|
|
12716
|
+
- \`vision\`: one sentence product purpose. Source: README intro or package.json description. Required if README exists.
|
|
12717
|
+
- \`audience\`: who it's for. Source: README target-user section or "built for" language.
|
|
12718
|
+
- \`techStack\`: main runtime dependencies only \u2014 framework, database, auth, UI. Max 8. Not dev tools, test libs, or linters.
|
|
12719
|
+
- \`modules\`: named subsystems with clear purposes. Source: folder structure + tsconfig path aliases. Skip generic names (src, lib, utils alone). Max 5.
|
|
12720
|
+
- \`businessRules\`: constraints the system enforces. Source: .cursorrules/AGENTS.md. Write as: "All X must Y" or "The system must Z". Keep as written.
|
|
12721
|
+
- \`conventions\`: how work is done here. Source: .cursorrules standards sections. Write as: "Use X not Y" or "Always do Z".
|
|
12722
|
+
- \`keyDecisions\`: architectural choices made, preferably with rationale. Source: git log patterns, README architecture notes. Each as a declarative statement ("Use Convex for real-time backend").
|
|
12723
|
+
- \`tensions\`: known friction or trade-offs. Source: git log hotfixes/reverts, README "known issues".
|
|
12724
|
+
- \`keyTerms\`: domain vocabulary specific to this project \u2014 not tech names, but product/business terms. Source: README, .cursorrules.
|
|
12725
|
+
|
|
12393
12726
|
**Quality rules (RH2 mitigation):**
|
|
12394
|
-
-
|
|
12395
|
-
-
|
|
12396
|
-
-
|
|
12397
|
-
-
|
|
12727
|
+
- Only include items with direct evidence from the files \u2014 no inference, no hallucination
|
|
12728
|
+
- Prefer 8 precise entries over 20 vague ones
|
|
12729
|
+
- Business rules and conventions must be actual constraints, not generic advice
|
|
12730
|
+
- Empty arrays beat fabricated data
|
|
12398
12731
|
|
|
12399
|
-
## Step 3: Validate Before
|
|
12732
|
+
## Step 3: Validate Before Presenting
|
|
12400
12733
|
|
|
12401
|
-
Before
|
|
12402
|
-
- vision is
|
|
12403
|
-
- each entry has a non-empty name and description
|
|
12734
|
+
Before presenting, check each entry:
|
|
12735
|
+
- vision is at least 10 characters if present
|
|
12404
12736
|
- no duplicate names within the same collection
|
|
12405
|
-
-
|
|
12737
|
+
- each entry has a non-empty name and description
|
|
12738
|
+
- techStack items are actual technologies, not version strings or config flags
|
|
12739
|
+
- module names are specific (not just "src", "lib", "utils")
|
|
12406
12740
|
|
|
12407
|
-
|
|
12741
|
+
Drop any entry that fails. If no vision AND no techStack exist, the scan is too sparse \u2014 tell the user which files were missing and stop.
|
|
12408
12742
|
|
|
12409
|
-
|
|
12410
|
-
Only after the user confirms which entries to keep should you capture the confirmed entries.
|
|
12743
|
+
## Step 4: Present for Confirmation
|
|
12411
12744
|
|
|
12412
|
-
|
|
12745
|
+
Present inferred entries as a numbered list grouped by collection. Ask which to keep.
|
|
12746
|
+
**Do NOT call batch-capture yet.** Only after the user confirms.
|
|
12413
12747
|
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
- \`
|
|
12418
|
-
- \`
|
|
12419
|
-
- \`
|
|
12420
|
-
- \`
|
|
12748
|
+
## Step 5: Map to batch-capture
|
|
12749
|
+
|
|
12750
|
+
After confirmation, map entries using these rules:
|
|
12751
|
+
- \`vision\` \u2192 strategy, name: "Product Vision"
|
|
12752
|
+
- \`audience\` \u2192 audiences
|
|
12753
|
+
- \`techStack\` \u2192 architecture ("Tech Stack") + top 3 items individually \u2192 glossary
|
|
12754
|
+
- \`modules\` \u2192 architecture (1 per module, max 5)
|
|
12755
|
+
- \`businessRules\` \u2192 business-rules (1 per rule)
|
|
12756
|
+
- \`conventions\` \u2192 standards (1 per convention)
|
|
12757
|
+
- \`keyDecisions\` \u2192 decisions (1 per decision)
|
|
12758
|
+
- \`tensions\` \u2192 tensions (1 per tension)
|
|
12759
|
+
- \`keyTerms\` \u2192 glossary (skip names already added from techStack)
|
|
12421
12760
|
|
|
12422
12761
|
**Example output:**
|
|
12423
12762
|
\`\`\`json
|
|
12424
12763
|
${exampleOutput}
|
|
12425
12764
|
\`\`\`
|
|
12426
12765
|
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
After the user confirms which entries to keep, call batch-capture with only those confirmed entries.
|
|
12430
|
-
Omit \`autoCommit\` to let normal workspace governance apply.
|
|
12431
|
-
|
|
12432
|
-
\`\`\`
|
|
12433
|
-
batch-capture entries=[...your confirmed entries...]
|
|
12434
|
-
\`\`\`
|
|
12435
|
-
|
|
12436
|
-
If \`failed > 0\` in the response, inspect \`failedEntries\` and retry those individually.
|
|
12766
|
+
Call batch-capture with confirmed entries only. Omit \`autoCommit\` to follow workspace governance.
|
|
12767
|
+
If \`failed > 0\`, inspect \`failedEntries\` and retry those individually.
|
|
12437
12768
|
|
|
12438
12769
|
## Step 6: Connect + Prove Value
|
|
12439
12770
|
|
|
12440
12771
|
After capture:
|
|
12441
12772
|
1. Call \`graph action=suggest\` on 2\u20133 key entries (vision, architecture, a decision)
|
|
12442
|
-
2. Invent a plausible next task and call \`context action=gather task="<that task>"\`
|
|
12773
|
+
2. Invent a plausible next task the user might actually do and call \`context action=gather task="<that task>"\`
|
|
12443
12774
|
3. Present what Product Brain now knows in that task context \u2014 this is the proof moment
|
|
12444
|
-
4.
|
|
12775
|
+
4. Show one clear result summary so the user sees exactly what was added
|
|
12445
12776
|
|
|
12446
12777
|
**Begin with Step 1 now.** Read the files, then report what you found before extracting.`
|
|
12447
12778
|
}
|
|
@@ -12449,6 +12780,108 @@ After capture:
|
|
|
12449
12780
|
};
|
|
12450
12781
|
}
|
|
12451
12782
|
);
|
|
12783
|
+
server.prompt(
|
|
12784
|
+
"review-drafts",
|
|
12785
|
+
"Review all uncommitted workspace drafts grouped by collection and commit them in batches. After each committed batch, surfaces the most non-obvious graph connection found as the WOW moment \u2014 the signal that Product Brain has started compounding.",
|
|
12786
|
+
{
|
|
12787
|
+
focus: z23.string().optional().describe("Optional: a specific collection to review first (e.g. 'glossary', 'decisions')")
|
|
12788
|
+
},
|
|
12789
|
+
async ({ focus }) => {
|
|
12790
|
+
let allDrafts = [];
|
|
12791
|
+
try {
|
|
12792
|
+
const raw = await mcpQuery("chain.listEntries", { status: "draft" });
|
|
12793
|
+
if (Array.isArray(raw)) {
|
|
12794
|
+
allDrafts = raw.filter(
|
|
12795
|
+
(e) => typeof e === "object" && e !== null && typeof e.entryId === "string" && typeof e.name === "string" && typeof e.collection === "string"
|
|
12796
|
+
);
|
|
12797
|
+
}
|
|
12798
|
+
} catch {
|
|
12799
|
+
}
|
|
12800
|
+
if (focus) {
|
|
12801
|
+
allDrafts.sort((a, b) => {
|
|
12802
|
+
const aMatch = a.collection === focus ? -1 : 0;
|
|
12803
|
+
const bMatch = b.collection === focus ? -1 : 0;
|
|
12804
|
+
return aMatch - bMatch;
|
|
12805
|
+
});
|
|
12806
|
+
}
|
|
12807
|
+
const grouped = groupDraftsByCollection(allDrafts);
|
|
12808
|
+
const draftList = formatDraftGroups(grouped, allDrafts.length);
|
|
12809
|
+
const hasDrafts = allDrafts.length > 0;
|
|
12810
|
+
return {
|
|
12811
|
+
messages: [{
|
|
12812
|
+
role: "user",
|
|
12813
|
+
content: {
|
|
12814
|
+
type: "text",
|
|
12815
|
+
text: `# Review Drafts \u2014 Batch Commit Queue
|
|
12816
|
+
|
|
12817
|
+
` + (hasDrafts ? draftList + "\n\n" : "No drafts found in this workspace. Everything is already committed.\n\n") + (hasDrafts ? `---
|
|
12818
|
+
|
|
12819
|
+
## How to proceed
|
|
12820
|
+
|
|
12821
|
+
Work through each collection group with the user. The goal is: every entry the user wants to keep gets committed.
|
|
12822
|
+
|
|
12823
|
+
**Step 1 \u2014 Offer a starting point**
|
|
12824
|
+
|
|
12825
|
+
Ask the user which group to tackle first, or suggest the most important one. A good default: start with Strategy and Decisions \u2014 these are high-signal and there are usually few of them. Glossary and Architecture entries can be batched quickly afterward.
|
|
12826
|
+
|
|
12827
|
+
**Step 2 \u2014 Confirm, then commit**
|
|
12828
|
+
|
|
12829
|
+
For each group the user confirms, call \`commit-entry\` once per entry. These must be sequential \u2014 commit each before moving to the next.
|
|
12830
|
+
|
|
12831
|
+
\`\`\`
|
|
12832
|
+
commit-entry entryId="[ID]"
|
|
12833
|
+
\`\`\`
|
|
12834
|
+
|
|
12835
|
+
Report any failures and skip rather than block. If commit returns \`proposal_created\`, tell the user \u2014 this workspace uses governed commits and proposals need approval.
|
|
12836
|
+
|
|
12837
|
+
**Step 3 \u2014 Surface the WOW connection (once per committed batch)**
|
|
12838
|
+
|
|
12839
|
+
After finishing a collection group, call \`graph action=suggest\` on up to 2 of the most important entries you just committed:
|
|
12840
|
+
- First choice: any strategy entry
|
|
12841
|
+
- Second choice: architecture or decisions entries
|
|
12842
|
+
- Maximum: 2 \`graph action=suggest\` calls per batch \u2014 not one per entry
|
|
12843
|
+
|
|
12844
|
+
From the suggestions returned, pick the **most non-obvious connection** \u2014 the one that crosses collection boundaries (e.g. a vision entry connecting to a decision, not just two glossary terms). Prefer suggestions with specific reasoning over generic ones. Ignore suggestions with no reasoning.
|
|
12845
|
+
|
|
12846
|
+
If a strong connection exists, present it exactly like this:
|
|
12847
|
+
|
|
12848
|
+
---
|
|
12849
|
+
**The graph noticed something.**
|
|
12850
|
+
|
|
12851
|
+
Your **[Entry A]** connects to **[Entry B]** \u2014 [reasoning from the graph, in one sentence].
|
|
12852
|
+
|
|
12853
|
+
You didn't explain this relationship. Product Brain inferred it from what you've built so far. This is the compounding starting.
|
|
12854
|
+
|
|
12855
|
+
Want to add this link?
|
|
12856
|
+
\`\`\`
|
|
12857
|
+
relations action=create from=[A] to=[B] type=[relationType]
|
|
12858
|
+
\`\`\`
|
|
12859
|
+
---
|
|
12860
|
+
|
|
12861
|
+
If there are no specific suggestions (or all reasoning is generic), skip this step silently. Do not fabricate a connection.
|
|
12862
|
+
|
|
12863
|
+
**Step 4 \u2014 The offer**
|
|
12864
|
+
|
|
12865
|
+
After the first batch is committed (especially if it includes a strategy or decisions entry), offer:
|
|
12866
|
+
|
|
12867
|
+
> "Ask me anything about your product \u2014 I'll answer using everything you just committed."
|
|
12868
|
+
|
|
12869
|
+
This is not a formality. If the user asks a question, actually answer it using \`context action=gather task="<their question>"\`. Show them what the Brain now knows, not just that it captured the data.
|
|
12870
|
+
|
|
12871
|
+
**Step 5 \u2014 Continue or wrap**
|
|
12872
|
+
|
|
12873
|
+
After each batch, ask if the user wants to continue with the next group. Once all groups are done, close with a summary:
|
|
12874
|
+
|
|
12875
|
+
- How many entries were committed this session
|
|
12876
|
+
- Which collections are now populated
|
|
12877
|
+
- One sentence: what Product Brain now knows about their product
|
|
12878
|
+
|
|
12879
|
+
End with: **"Your Brain is ready."**` : `Your workspace has no uncommitted entries. If you recently captured new knowledge, check that \`batch-capture\` completed successfully and try \`entries action=list status=draft\`.`)
|
|
12880
|
+
}
|
|
12881
|
+
}]
|
|
12882
|
+
};
|
|
12883
|
+
}
|
|
12884
|
+
);
|
|
12452
12885
|
}
|
|
12453
12886
|
|
|
12454
12887
|
// src/server.ts
|
|
@@ -12543,6 +12976,7 @@ function createProductBrainServer() {
|
|
|
12543
12976
|
registerKnowledgeTools(server);
|
|
12544
12977
|
registerLabelTools(server);
|
|
12545
12978
|
registerHealthTools(server);
|
|
12979
|
+
registerOrientTool(server);
|
|
12546
12980
|
registerVerifyTools(server);
|
|
12547
12981
|
registerSmartCaptureTools(server);
|
|
12548
12982
|
registerQualityTools(server);
|
|
@@ -12562,4 +12996,4 @@ export {
|
|
|
12562
12996
|
SERVER_VERSION,
|
|
12563
12997
|
createProductBrainServer
|
|
12564
12998
|
};
|
|
12565
|
-
//# sourceMappingURL=chunk-
|
|
12999
|
+
//# sourceMappingURL=chunk-2BHEXDVG.js.map
|