@productbrain/mcp 0.0.1-beta.70 → 0.0.1-beta.72
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-TH5AUVVM.js → chunk-XLMQ7POI.js} +18 -42
- package/dist/chunk-XLMQ7POI.js.map +1 -0
- package/dist/{chunk-JOJWCU7A.js → chunk-ZPEIKRYG.js} +1328 -789
- package/dist/chunk-ZPEIKRYG.js.map +1 -0
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{smart-capture-Q64ZXK65.js → smart-capture-QFYRKMBM.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-JOJWCU7A.js.map +0 -1
- package/dist/chunk-TH5AUVVM.js.map +0 -1
- /package/dist/{smart-capture-Q64ZXK65.js.map → smart-capture-QFYRKMBM.js.map} +0 -0
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
unknownAction,
|
|
44
44
|
validationResult,
|
|
45
45
|
withEnvelope
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-XLMQ7POI.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-QFYRKMBM.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";
|
|
@@ -2363,6 +2462,7 @@ function toSnapshot(report, maxTopGaps = 10) {
|
|
|
2363
2462
|
import { readFileSync } from "fs";
|
|
2364
2463
|
import { resolve } from "path";
|
|
2365
2464
|
function resolveRegistries(projectRoot, manifest) {
|
|
2465
|
+
if (!projectRoot) return {};
|
|
2366
2466
|
const resolved = {};
|
|
2367
2467
|
for (const ref of manifest) {
|
|
2368
2468
|
try {
|
|
@@ -2374,7 +2474,10 @@ function resolveRegistries(projectRoot, manifest) {
|
|
|
2374
2474
|
if (keys.size > 0) {
|
|
2375
2475
|
resolved[ref.id] = keys;
|
|
2376
2476
|
}
|
|
2377
|
-
} catch {
|
|
2477
|
+
} catch (err) {
|
|
2478
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2479
|
+
console.debug(`[coherence] Skipping ${ref.id} (${ref.path}):`, err instanceof Error ? err.message : err);
|
|
2480
|
+
}
|
|
2378
2481
|
}
|
|
2379
2482
|
}
|
|
2380
2483
|
return resolved;
|
|
@@ -2409,14 +2512,19 @@ function extractBalanced(content, openPos, openChar, closeChar) {
|
|
|
2409
2512
|
i = skipString(content, i, ch);
|
|
2410
2513
|
continue;
|
|
2411
2514
|
}
|
|
2412
|
-
if (ch === "/" && content[i + 1] === "/") {
|
|
2515
|
+
if (ch === "/" && i + 1 < content.length && content[i + 1] === "/") {
|
|
2413
2516
|
while (i < content.length && content[i] !== "\n") i++;
|
|
2414
2517
|
continue;
|
|
2415
2518
|
}
|
|
2416
|
-
if (ch === "/" && content[i + 1] === "*") {
|
|
2519
|
+
if (ch === "/" && i + 1 < content.length && content[i + 1] === "*") {
|
|
2417
2520
|
i += 2;
|
|
2418
|
-
while (i < content.length
|
|
2419
|
-
|
|
2521
|
+
while (i < content.length) {
|
|
2522
|
+
if (content[i] === "/" && i > 0 && content[i - 1] === "*") {
|
|
2523
|
+
i++;
|
|
2524
|
+
break;
|
|
2525
|
+
}
|
|
2526
|
+
i++;
|
|
2527
|
+
}
|
|
2420
2528
|
continue;
|
|
2421
2529
|
}
|
|
2422
2530
|
if (ch === openChar) depth++;
|
|
@@ -2429,11 +2537,12 @@ function extractBalanced(content, openPos, openChar, closeChar) {
|
|
|
2429
2537
|
function skipString(content, start, quote) {
|
|
2430
2538
|
let i = start + 1;
|
|
2431
2539
|
while (i < content.length) {
|
|
2432
|
-
|
|
2540
|
+
const ch = content[i];
|
|
2541
|
+
if (ch === "\\") {
|
|
2433
2542
|
i += 2;
|
|
2434
2543
|
continue;
|
|
2435
2544
|
}
|
|
2436
|
-
if (
|
|
2545
|
+
if (ch === quote) return i + 1;
|
|
2437
2546
|
i++;
|
|
2438
2547
|
}
|
|
2439
2548
|
return i;
|
|
@@ -2454,14 +2563,19 @@ function extractObjectKeys(block) {
|
|
|
2454
2563
|
let i = 0;
|
|
2455
2564
|
while (i < block.length) {
|
|
2456
2565
|
const ch = block[i];
|
|
2457
|
-
if (ch === "/" && block[i + 1] === "/") {
|
|
2566
|
+
if (ch === "/" && i + 1 < block.length && block[i + 1] === "/") {
|
|
2458
2567
|
while (i < block.length && block[i] !== "\n") i++;
|
|
2459
2568
|
continue;
|
|
2460
2569
|
}
|
|
2461
|
-
if (ch === "/" && block[i + 1] === "*") {
|
|
2570
|
+
if (ch === "/" && i + 1 < block.length && block[i + 1] === "*") {
|
|
2462
2571
|
i += 2;
|
|
2463
|
-
while (i < block.length
|
|
2464
|
-
|
|
2572
|
+
while (i < block.length) {
|
|
2573
|
+
if (block[i] === "/" && i > 0 && block[i - 1] === "*") {
|
|
2574
|
+
i++;
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
i++;
|
|
2578
|
+
}
|
|
2465
2579
|
continue;
|
|
2466
2580
|
}
|
|
2467
2581
|
if (depth === 0) {
|
|
@@ -2515,6 +2629,7 @@ function extractSetValues(block) {
|
|
|
2515
2629
|
// src/lib/coherence/git-detection.ts
|
|
2516
2630
|
import { execSync } from "child_process";
|
|
2517
2631
|
function detectTouchedRegistries(projectRoot) {
|
|
2632
|
+
if (!projectRoot) return /* @__PURE__ */ new Set();
|
|
2518
2633
|
const manifestPaths = new Set(REGISTRY_MANIFEST.map((r) => r.path));
|
|
2519
2634
|
const touched = /* @__PURE__ */ new Set();
|
|
2520
2635
|
try {
|
|
@@ -2529,7 +2644,10 @@ function detectTouchedRegistries(projectRoot) {
|
|
|
2529
2644
|
touched.add(trimmed);
|
|
2530
2645
|
}
|
|
2531
2646
|
}
|
|
2532
|
-
} catch {
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2649
|
+
console.debug("[coherence] Git detection failed:", err instanceof Error ? err.message : err);
|
|
2650
|
+
}
|
|
2533
2651
|
}
|
|
2534
2652
|
return touched;
|
|
2535
2653
|
}
|
|
@@ -2599,13 +2717,16 @@ function computeDelta(before, after) {
|
|
|
2599
2717
|
function renderCoherenceDelta(delta) {
|
|
2600
2718
|
const lines = ["### Coherence Delta"];
|
|
2601
2719
|
const p = (n) => n === 1 ? "" : "s";
|
|
2720
|
+
const absNet = Math.abs(delta.netChange);
|
|
2721
|
+
const approx = absNet > delta.gapsFixed + delta.gapsIntroduced;
|
|
2722
|
+
const prefix = approx ? "at least " : "";
|
|
2602
2723
|
if (delta.verdict === "improved") {
|
|
2603
2724
|
lines.push(
|
|
2604
|
-
`Registry coherence **improved**: ${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
|
|
2725
|
+
`Registry coherence **improved**: ${prefix}${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
|
|
2605
2726
|
);
|
|
2606
2727
|
} else if (delta.verdict === "degraded") {
|
|
2607
2728
|
lines.push(
|
|
2608
|
-
`**Registry coherence degraded**: ${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
|
|
2729
|
+
`**Registry coherence degraded**: ${prefix}${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
|
|
2609
2730
|
);
|
|
2610
2731
|
} else {
|
|
2611
2732
|
lines.push(
|
|
@@ -2615,6 +2736,58 @@ function renderCoherenceDelta(delta) {
|
|
|
2615
2736
|
lines.push("");
|
|
2616
2737
|
return lines;
|
|
2617
2738
|
}
|
|
2739
|
+
function findPersistentGaps(delta, currentViolations) {
|
|
2740
|
+
const beforeSet = new Set(delta.before.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2741
|
+
const afterSet = new Set(delta.after.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2742
|
+
const persistentKeys = /* @__PURE__ */ new Set();
|
|
2743
|
+
for (const key of beforeSet) {
|
|
2744
|
+
if (afterSet.has(key)) persistentKeys.add(key);
|
|
2745
|
+
}
|
|
2746
|
+
if (persistentKeys.size === 0) return [];
|
|
2747
|
+
const violationMap = /* @__PURE__ */ new Map();
|
|
2748
|
+
for (const v of currentViolations) {
|
|
2749
|
+
violationMap.set(`${v.registryId}::${v.collectionSlug}`, v);
|
|
2750
|
+
}
|
|
2751
|
+
return [...persistentKeys].map((key) => {
|
|
2752
|
+
const [registry = "", slug = ""] = key.split("::", 2);
|
|
2753
|
+
return {
|
|
2754
|
+
registry,
|
|
2755
|
+
slug,
|
|
2756
|
+
severity: delta.after.topGaps.find((g) => g.registry === registry && g.slug === slug)?.severity ?? "warning",
|
|
2757
|
+
violation: violationMap.get(key) ?? null
|
|
2758
|
+
};
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
function renderPersistentGapOffers(gaps) {
|
|
2762
|
+
if (gaps.length === 0) return [];
|
|
2763
|
+
const lines = [
|
|
2764
|
+
"### Persistent Coherence Gaps",
|
|
2765
|
+
"",
|
|
2766
|
+
"These gaps existed before and after this session \u2014 they represent structural registry drift.",
|
|
2767
|
+
"Want me to capture them as tensions on the Chain?",
|
|
2768
|
+
""
|
|
2769
|
+
];
|
|
2770
|
+
const SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
|
|
2771
|
+
const errorsFirst = [...gaps].sort(
|
|
2772
|
+
(a, b) => SEVERITY_RANK2[a.severity] - SEVERITY_RANK2[b.severity]
|
|
2773
|
+
);
|
|
2774
|
+
for (const gap of errorsFirst.slice(0, 5)) {
|
|
2775
|
+
const v = gap.violation;
|
|
2776
|
+
const fixHint = v?.fix ? ` \u2014 ${v.fix}` : "";
|
|
2777
|
+
lines.push(
|
|
2778
|
+
`- **\`${gap.slug}\`** missing from \`${gap.registry}\` (${gap.severity})${fixHint}`
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
if (errorsFirst.length > 5) {
|
|
2782
|
+
lines.push(`- ...and ${errorsFirst.length - 5} more`);
|
|
2783
|
+
}
|
|
2784
|
+
lines.push("");
|
|
2785
|
+
lines.push(
|
|
2786
|
+
'_Say "capture these as tensions" to commit them to the Chain, or skip to continue._'
|
|
2787
|
+
);
|
|
2788
|
+
lines.push("");
|
|
2789
|
+
return lines;
|
|
2790
|
+
}
|
|
2618
2791
|
|
|
2619
2792
|
// src/lib/resolve-project-root.ts
|
|
2620
2793
|
import { existsSync } from "fs";
|
|
@@ -2623,6 +2796,8 @@ function resolveProjectRoot() {
|
|
|
2623
2796
|
const candidates = [
|
|
2624
2797
|
process.env.WORKSPACE_PATH,
|
|
2625
2798
|
process.cwd(),
|
|
2799
|
+
// Parent of cwd: handles monorepo subpackages (e.g. packages/mcp-server)
|
|
2800
|
+
// where the Convex schema lives one level up.
|
|
2626
2801
|
resolve2(process.cwd(), "..")
|
|
2627
2802
|
].filter(Boolean);
|
|
2628
2803
|
for (const dir of candidates) {
|
|
@@ -2652,16 +2827,24 @@ async function mapWithConcurrency(items, mapper, concurrency = 3) {
|
|
|
2652
2827
|
);
|
|
2653
2828
|
return results;
|
|
2654
2829
|
}
|
|
2830
|
+
function isValidTopGap(g) {
|
|
2831
|
+
if (typeof g !== "object" || g === null) return false;
|
|
2832
|
+
const obj = g;
|
|
2833
|
+
return typeof obj.registry === "string" && typeof obj.slug === "string" && typeof obj.severity === "string" && (obj.severity === "error" || obj.severity === "warning" || obj.severity === "info");
|
|
2834
|
+
}
|
|
2655
2835
|
async function fetchSessionCoherenceSnapshot(sessionId) {
|
|
2656
2836
|
try {
|
|
2657
2837
|
const session = await mcpCall("agent.getSession", {
|
|
2658
2838
|
sessionId
|
|
2659
2839
|
});
|
|
2660
2840
|
const raw = session?.coherenceSnapshot;
|
|
2661
|
-
if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number") {
|
|
2841
|
+
if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number" && Array.isArray(raw.topGaps) && raw.topGaps.every(isValidTopGap)) {
|
|
2662
2842
|
return raw;
|
|
2663
2843
|
}
|
|
2664
|
-
} catch {
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2846
|
+
console.debug("[coherence] Snapshot fetch failed:", err instanceof Error ? err.message : err);
|
|
2847
|
+
}
|
|
2665
2848
|
}
|
|
2666
2849
|
return null;
|
|
2667
2850
|
}
|
|
@@ -2760,6 +2943,7 @@ async function runWrapupReview() {
|
|
|
2760
2943
|
lines.push("");
|
|
2761
2944
|
}
|
|
2762
2945
|
let coherenceVerdict;
|
|
2946
|
+
let persistentGaps;
|
|
2763
2947
|
try {
|
|
2764
2948
|
const projectRoot = resolveProjectRoot();
|
|
2765
2949
|
if (projectRoot) {
|
|
@@ -2781,6 +2965,10 @@ async function runWrapupReview() {
|
|
|
2781
2965
|
);
|
|
2782
2966
|
lines.push("");
|
|
2783
2967
|
}
|
|
2968
|
+
persistentGaps = findPersistentGaps(delta, report.violations);
|
|
2969
|
+
if (persistentGaps.length > 0) {
|
|
2970
|
+
lines.push(...renderPersistentGapOffers(persistentGaps));
|
|
2971
|
+
}
|
|
2784
2972
|
} else if (touchedFiles.size > 0) {
|
|
2785
2973
|
lines.push("### Coherence Check");
|
|
2786
2974
|
lines.push(
|
|
@@ -2797,7 +2985,10 @@ async function runWrapupReview() {
|
|
|
2797
2985
|
}
|
|
2798
2986
|
}
|
|
2799
2987
|
}
|
|
2800
|
-
} catch {
|
|
2988
|
+
} catch (err) {
|
|
2989
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2990
|
+
console.debug("[coherence] Wrapup coherence check failed:", err instanceof Error ? err.message : err);
|
|
2991
|
+
}
|
|
2801
2992
|
}
|
|
2802
2993
|
if (data.drafts.length > 0) {
|
|
2803
2994
|
const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
|
|
@@ -2807,7 +2998,7 @@ async function runWrapupReview() {
|
|
|
2807
2998
|
lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
|
|
2808
2999
|
}
|
|
2809
3000
|
const gapCount = getSessionGaps().length;
|
|
2810
|
-
return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict };
|
|
3001
|
+
return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict, persistentGaps };
|
|
2811
3002
|
}
|
|
2812
3003
|
async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
2813
3004
|
requireWriteAccess();
|
|
@@ -2932,7 +3123,7 @@ function registerWrapupTools(server) {
|
|
|
2932
3123
|
}
|
|
2933
3124
|
);
|
|
2934
3125
|
}
|
|
2935
|
-
const { text, data, suggestions, failureCode, gapCount, coherenceVerdict } = await runWrapupReview();
|
|
3126
|
+
const { text, data, suggestions, failureCode, gapCount, coherenceVerdict, persistentGaps } = await runWrapupReview();
|
|
2936
3127
|
lastReviewData = data;
|
|
2937
3128
|
lastReviewSuggestions = suggestions;
|
|
2938
3129
|
lastReviewSessionId = getAgentSessionId();
|
|
@@ -2945,16 +3136,18 @@ ${text}` : text;
|
|
|
2945
3136
|
const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
|
|
2946
3137
|
const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
|
|
2947
3138
|
const coherenceSummary = coherenceVerdict ? `, coherence: ${coherenceVerdict}` : "";
|
|
3139
|
+
const persistentSummary = persistentGaps?.length ? `, ${persistentGaps.length} persistent coherence gaps` : "";
|
|
2948
3140
|
return successResult(
|
|
2949
3141
|
fullText,
|
|
2950
|
-
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}.`,
|
|
3142
|
+
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}${persistentSummary}.`,
|
|
2951
3143
|
{
|
|
2952
3144
|
drafts: data.drafts.length,
|
|
2953
3145
|
committed: data.committed.length,
|
|
2954
3146
|
uncommitted: data.summary.uncommitted,
|
|
2955
3147
|
suggestedLinks: suggestions.length,
|
|
2956
3148
|
knowledgeGaps: gapCount ?? 0,
|
|
2957
|
-
...coherenceVerdict ? { coherenceVerdict } : {}
|
|
3149
|
+
...coherenceVerdict ? { coherenceVerdict } : {},
|
|
3150
|
+
...persistentGaps?.length ? { persistentCoherenceGaps: persistentGaps.length } : {}
|
|
2958
3151
|
},
|
|
2959
3152
|
next
|
|
2960
3153
|
);
|
|
@@ -3758,7 +3951,7 @@ If any facilitate call fails:
|
|
|
3758
3951
|
description: "Confirmed problem statement and appetite",
|
|
3759
3952
|
format: "structured"
|
|
3760
3953
|
},
|
|
3761
|
-
kbCollection: "
|
|
3954
|
+
kbCollection: "chains",
|
|
3762
3955
|
maxDurationHint: "10 min"
|
|
3763
3956
|
},
|
|
3764
3957
|
{
|
|
@@ -3828,7 +4021,7 @@ If any facilitate call fails:
|
|
|
3828
4021
|
description: "Final capture summary \u2014 entries committed, relations created",
|
|
3829
4022
|
format: "structured"
|
|
3830
4023
|
},
|
|
3831
|
-
kbCollection: "
|
|
4024
|
+
kbCollection: "chains",
|
|
3832
4025
|
maxDurationHint: "5 min"
|
|
3833
4026
|
}
|
|
3834
4027
|
],
|
|
@@ -3836,7 +4029,7 @@ If any facilitate call fails:
|
|
|
3836
4029
|
primaryRecord: {
|
|
3837
4030
|
routing: {
|
|
3838
4031
|
mode: "fixed",
|
|
3839
|
-
collection: "
|
|
4032
|
+
collection: "chains"
|
|
3840
4033
|
}
|
|
3841
4034
|
},
|
|
3842
4035
|
emits: [
|
|
@@ -3862,7 +4055,7 @@ If any facilitate call fails:
|
|
|
3862
4055
|
},
|
|
3863
4056
|
{
|
|
3864
4057
|
kind: "update",
|
|
3865
|
-
collection: "
|
|
4058
|
+
collection: "chains",
|
|
3866
4059
|
description: "Bet status and no-go updates during finalize."
|
|
3867
4060
|
}
|
|
3868
4061
|
]
|
|
@@ -5729,7 +5922,8 @@ function generateBuildContract(ctx) {
|
|
|
5729
5922
|
function computeCommitBlockers(opts) {
|
|
5730
5923
|
const { betEntryId, betDocId, relations, hasStrategyLink, betData, sessionDrafts } = opts;
|
|
5731
5924
|
const blockers = [];
|
|
5732
|
-
const
|
|
5925
|
+
const links = betData.links ?? {};
|
|
5926
|
+
const str = (key) => (links[key] ?? "").trim();
|
|
5733
5927
|
if (!hasStrategyLink) {
|
|
5734
5928
|
blockers.push({
|
|
5735
5929
|
entryId: betEntryId,
|
|
@@ -6093,7 +6287,8 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
|
|
|
6093
6287
|
}
|
|
6094
6288
|
}
|
|
6095
6289
|
function buildScoringContext(userInput, betData, constellation, overlapIds, chainResults = [], activeDimension, source = "user") {
|
|
6096
|
-
const
|
|
6290
|
+
const links = betData.links ?? {};
|
|
6291
|
+
const str = (key) => links[key] ?? "";
|
|
6097
6292
|
const accParts = [
|
|
6098
6293
|
str("problem"),
|
|
6099
6294
|
str("appetite"),
|
|
@@ -6248,17 +6443,20 @@ async function processCaptures(opts) {
|
|
|
6248
6443
|
};
|
|
6249
6444
|
const FALLBACK_DEFAULTS = { dataField: "description", relationType: "related_to" };
|
|
6250
6445
|
let runningBetData = { ...betData };
|
|
6446
|
+
const runningLinks = () => ({ ...runningBetData.links ?? {} });
|
|
6251
6447
|
for (const item of captureItems) {
|
|
6252
6448
|
if (item.type === "noGo") {
|
|
6449
|
+
const curLinks = runningLinks();
|
|
6253
6450
|
const updatedNoGos = appendNoGo(
|
|
6254
|
-
|
|
6451
|
+
curLinks.noGos,
|
|
6255
6452
|
{ title: item.name, explanation: item.description }
|
|
6256
6453
|
);
|
|
6257
|
-
|
|
6454
|
+
const newLinks = { ...curLinks, noGos: updatedNoGos };
|
|
6455
|
+
runningBetData.links = newLinks;
|
|
6258
6456
|
try {
|
|
6259
6457
|
await mcpMutation("chain.updateEntry", {
|
|
6260
6458
|
entryId: betEntryId,
|
|
6261
|
-
data: {
|
|
6459
|
+
data: { links: newLinks },
|
|
6262
6460
|
changeNote: `Added no-go: ${item.name}`
|
|
6263
6461
|
});
|
|
6264
6462
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6342,15 +6540,17 @@ async function processCaptures(opts) {
|
|
|
6342
6540
|
}
|
|
6343
6541
|
}
|
|
6344
6542
|
if (item.type === "element") {
|
|
6543
|
+
const curLinks = runningLinks();
|
|
6345
6544
|
const updatedElements = appendElement(
|
|
6346
|
-
|
|
6545
|
+
curLinks.elements,
|
|
6347
6546
|
{ name: item.name, description: item.description, entryId: capturedEntryId }
|
|
6348
6547
|
);
|
|
6349
|
-
|
|
6548
|
+
const newLinks = { ...curLinks, elements: updatedElements };
|
|
6549
|
+
runningBetData.links = newLinks;
|
|
6350
6550
|
try {
|
|
6351
6551
|
await mcpMutation("chain.updateEntry", {
|
|
6352
6552
|
entryId: betEntryId,
|
|
6353
|
-
data: {
|
|
6553
|
+
data: { links: newLinks },
|
|
6354
6554
|
changeNote: `Added element: ${item.name}`
|
|
6355
6555
|
});
|
|
6356
6556
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6361,15 +6561,17 @@ async function processCaptures(opts) {
|
|
|
6361
6561
|
});
|
|
6362
6562
|
}
|
|
6363
6563
|
} else if (item.type === "risk") {
|
|
6564
|
+
const curLinks = runningLinks();
|
|
6364
6565
|
const updatedRisks = appendRabbitHole(
|
|
6365
|
-
|
|
6566
|
+
curLinks.rabbitHoles,
|
|
6366
6567
|
{ name: item.name, description: item.description, theme: item.theme, entryId: capturedEntryId }
|
|
6367
6568
|
);
|
|
6368
|
-
|
|
6569
|
+
const newLinks = { ...curLinks, rabbitHoles: updatedRisks };
|
|
6570
|
+
runningBetData.links = newLinks;
|
|
6369
6571
|
try {
|
|
6370
6572
|
await mcpMutation("chain.updateEntry", {
|
|
6371
6573
|
entryId: betEntryId,
|
|
6372
|
-
data: {
|
|
6574
|
+
data: { links: newLinks },
|
|
6373
6575
|
changeNote: `Added risk: ${item.name}`
|
|
6374
6576
|
});
|
|
6375
6577
|
await recordSessionActivity({ entryModified: betDocId });
|
|
@@ -6405,7 +6607,8 @@ async function computeAndUpdateScores(opts) {
|
|
|
6405
6607
|
const alreadyCheckedOverlap = typeof refreshedData._overlapIds === "string";
|
|
6406
6608
|
let overlap = [];
|
|
6407
6609
|
if (!alreadyCheckedOverlap && captureItems.length === 0) {
|
|
6408
|
-
const
|
|
6610
|
+
const refreshedLinks = refreshedData.links ?? {};
|
|
6611
|
+
const problemText = refreshedLinks.problem ?? userInput;
|
|
6409
6612
|
const overlapResults = await searchChain(problemText, { maxResults: 5, excludeIds: [betEntryId, ...constellationEntryIds] });
|
|
6410
6613
|
overlap = overlapResults.map((r) => ({
|
|
6411
6614
|
entryId: r.entryId,
|
|
@@ -6414,7 +6617,8 @@ async function computeAndUpdateScores(opts) {
|
|
|
6414
6617
|
}));
|
|
6415
6618
|
}
|
|
6416
6619
|
const overlapIds = overlap.map((o) => o.entryId);
|
|
6417
|
-
const
|
|
6620
|
+
const refreshedLinksForAppetite = refreshedData.links ?? {};
|
|
6621
|
+
const appetiteText = refreshedLinksForAppetite.appetite ?? "";
|
|
6418
6622
|
const isSmallBatch = detectSmallBatch(
|
|
6419
6623
|
argDimension === "appetite" ? userInput : appetiteText
|
|
6420
6624
|
);
|
|
@@ -6440,15 +6644,20 @@ async function computeAndUpdateScores(opts) {
|
|
|
6440
6644
|
const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
|
|
6441
6645
|
try {
|
|
6442
6646
|
const fieldUpdates = {};
|
|
6647
|
+
const linksUpdates = {};
|
|
6648
|
+
const currentLinks = refreshedData.links ?? {};
|
|
6443
6649
|
const persist = (dim, field, minLen) => {
|
|
6444
|
-
if (activeDimension === dim && !
|
|
6445
|
-
|
|
6650
|
+
if (activeDimension === dim && !currentLinks[field] && userInput.length > minLen) {
|
|
6651
|
+
linksUpdates[field] = userInput;
|
|
6446
6652
|
}
|
|
6447
6653
|
};
|
|
6448
6654
|
persist("problem_clarity", "problem", 50);
|
|
6449
6655
|
persist("appetite", "appetite", 20);
|
|
6450
6656
|
persist("architecture", "architecture", 50);
|
|
6451
6657
|
persist("done_when", "done_when", 30);
|
|
6658
|
+
if (Object.keys(linksUpdates).length > 0) {
|
|
6659
|
+
fieldUpdates.links = { ...currentLinks, ...linksUpdates };
|
|
6660
|
+
}
|
|
6452
6661
|
if (!refreshedData._overlapIds) {
|
|
6453
6662
|
fieldUpdates._overlapIds = overlapIds.length > 0 ? overlapIds.join(",") : "_checked";
|
|
6454
6663
|
}
|
|
@@ -6473,7 +6682,7 @@ async function computeAndUpdateScores(opts) {
|
|
|
6473
6682
|
});
|
|
6474
6683
|
}
|
|
6475
6684
|
const alignment = [];
|
|
6476
|
-
const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "
|
|
6685
|
+
const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "chains"]);
|
|
6477
6686
|
for (const result of chainSurfaced) {
|
|
6478
6687
|
if (governanceCollections.has(result.collection)) {
|
|
6479
6688
|
alignment.push({ entryId: result.entryId, relationship: `governs [${result.collection}]` });
|
|
@@ -6492,11 +6701,13 @@ async function computeAndUpdateScores(opts) {
|
|
|
6492
6701
|
relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.name }))
|
|
6493
6702
|
};
|
|
6494
6703
|
buildContract = generateBuildContract(contractCtx);
|
|
6495
|
-
|
|
6704
|
+
const currentBCLinks = refreshedData.links ?? {};
|
|
6705
|
+
if (currentBCLinks.buildContract !== buildContract) {
|
|
6496
6706
|
try {
|
|
6707
|
+
const bcLinks = { ...refreshedData.links ?? {}, buildContract };
|
|
6497
6708
|
await mcpMutation("chain.updateEntry", {
|
|
6498
6709
|
entryId: betEntryId,
|
|
6499
|
-
data: {
|
|
6710
|
+
data: { links: bcLinks },
|
|
6500
6711
|
changeNote: "Updated build contract from shaping session"
|
|
6501
6712
|
});
|
|
6502
6713
|
await recordSessionActivity({ entryModified: refreshedBet?._id ?? betEntry._id });
|
|
@@ -6573,9 +6784,10 @@ function assembleResponse(opts) {
|
|
|
6573
6784
|
} = opts;
|
|
6574
6785
|
const studioUrl = buildStudioUrl(workspaceSlug, betEntryId);
|
|
6575
6786
|
const suggested = suggestCaptures(scoringCtx, activeDimension);
|
|
6576
|
-
const
|
|
6787
|
+
const assembleLinks = refreshedData.links ?? {};
|
|
6788
|
+
const betProblem = assembleLinks.problem ?? "";
|
|
6577
6789
|
const responseBetName = refreshedData.description ?? betName;
|
|
6578
|
-
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()) ?? [];
|
|
6579
6791
|
const investigationBrief = buildInvestigationBrief(phase, responseBetName, betEntryId, betProblem, elementNames) ?? void 0;
|
|
6580
6792
|
const response = {
|
|
6581
6793
|
version: 2,
|
|
@@ -6663,20 +6875,23 @@ async function handleRespond(args) {
|
|
|
6663
6875
|
const result = await mcpMutation(
|
|
6664
6876
|
"chain.createEntry",
|
|
6665
6877
|
{
|
|
6666
|
-
collectionSlug: "
|
|
6878
|
+
collectionSlug: "chains",
|
|
6667
6879
|
name: betName,
|
|
6668
6880
|
status: "draft",
|
|
6669
6881
|
data: {
|
|
6670
|
-
|
|
6671
|
-
appetite: "",
|
|
6672
|
-
elements: "",
|
|
6673
|
-
rabbitHoles: "",
|
|
6674
|
-
noGos: "",
|
|
6675
|
-
architecture: "",
|
|
6676
|
-
buildContract: "",
|
|
6882
|
+
chainTypeId: "bet",
|
|
6677
6883
|
description: `Shaping session for: ${betName}`,
|
|
6678
6884
|
status: "shaping",
|
|
6679
|
-
shapingSessionActive: true
|
|
6885
|
+
shapingSessionActive: true,
|
|
6886
|
+
links: {
|
|
6887
|
+
problem: userInput,
|
|
6888
|
+
appetite: "",
|
|
6889
|
+
elements: "",
|
|
6890
|
+
rabbitHoles: "",
|
|
6891
|
+
noGos: "",
|
|
6892
|
+
architecture: "",
|
|
6893
|
+
buildContract: ""
|
|
6894
|
+
}
|
|
6680
6895
|
},
|
|
6681
6896
|
createdBy: agentId ? `agent:${agentId}` : "facilitate",
|
|
6682
6897
|
sessionId: agentId ?? void 0
|
|
@@ -6782,7 +6997,8 @@ async function handleScore(args) {
|
|
|
6782
6997
|
}
|
|
6783
6998
|
const constellation = await loadConstellationState(betId, betEntry._id);
|
|
6784
6999
|
const betData = betEntry.data ?? {};
|
|
6785
|
-
const
|
|
7000
|
+
const scoreLinks = betData.links ?? {};
|
|
7001
|
+
const isSmallBatch = detectSmallBatch(scoreLinks.appetite ?? "");
|
|
6786
7002
|
const scoringCtx = buildCachedScoringContext(betData, constellation);
|
|
6787
7003
|
const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
|
|
6788
7004
|
const dims = activeDimensions(isSmallBatch);
|
|
@@ -6885,7 +7101,8 @@ async function handleResume(args) {
|
|
|
6885
7101
|
}
|
|
6886
7102
|
const constellation = await loadConstellationState(betId, betEntry._id);
|
|
6887
7103
|
const betData = betEntry.data ?? {};
|
|
6888
|
-
const
|
|
7104
|
+
const resumeLinks = betData.links ?? {};
|
|
7105
|
+
const isSmallBatch = detectSmallBatch(resumeLinks.appetite ?? "");
|
|
6889
7106
|
const scoringCtx = buildCachedScoringContext(betData, constellation);
|
|
6890
7107
|
const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
|
|
6891
7108
|
const dims = activeDimensions(isSmallBatch);
|
|
@@ -7036,8 +7253,9 @@ async function handleCommitConstellation(args) {
|
|
|
7036
7253
|
}
|
|
7037
7254
|
let contradictionWarnings = [];
|
|
7038
7255
|
try {
|
|
7039
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
7040
|
-
const
|
|
7256
|
+
const { runContradictionCheck } = await import("./smart-capture-QFYRKMBM.js");
|
|
7257
|
+
const commitLinks = betData.links ?? {};
|
|
7258
|
+
const descField = commitLinks.problem ?? betData.description ?? "";
|
|
7041
7259
|
contradictionWarnings = await runContradictionCheck(
|
|
7042
7260
|
betEntry.name ?? betId,
|
|
7043
7261
|
descField
|
|
@@ -7687,22 +7905,25 @@ function buildCoherenceSection(projectRoot) {
|
|
|
7687
7905
|
try {
|
|
7688
7906
|
const root = projectRoot ?? resolveProjectRoot() ?? process.cwd();
|
|
7689
7907
|
return checkAndRender(root);
|
|
7690
|
-
} catch {
|
|
7908
|
+
} catch (err) {
|
|
7909
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
7910
|
+
console.debug("[coherence] buildCoherenceSection failed:", err instanceof Error ? err.message : err);
|
|
7911
|
+
}
|
|
7691
7912
|
return null;
|
|
7692
7913
|
}
|
|
7693
7914
|
}
|
|
7694
7915
|
function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
7695
7916
|
const betNames = activeBets.map((b) => b.name);
|
|
7696
|
-
const taskWords = task
|
|
7917
|
+
const taskWords = extractKeywords(task);
|
|
7697
7918
|
const matchingBet = activeBets.find((b) => {
|
|
7698
|
-
const
|
|
7699
|
-
return taskWords.some((
|
|
7919
|
+
const words = (b.name ?? "").toLowerCase().split(/\s+/);
|
|
7920
|
+
return taskWords.some((tw) => words.some((w) => w === tw || w.startsWith(tw + "-")));
|
|
7700
7921
|
});
|
|
7701
7922
|
if (matchingBet) {
|
|
7702
7923
|
return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
|
|
7703
7924
|
}
|
|
7704
7925
|
const betHits = (taskContextHits ?? []).filter(
|
|
7705
|
-
(e) => e.collectionSlug === "
|
|
7926
|
+
(e) => e.collectionSlug === "chains"
|
|
7706
7927
|
);
|
|
7707
7928
|
if (betHits.length > 0) {
|
|
7708
7929
|
return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
|
|
@@ -7711,11 +7932,11 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
|
7711
7932
|
}
|
|
7712
7933
|
function buildAlignmentCheckLines(result) {
|
|
7713
7934
|
const lines = ["## Alignment Check"];
|
|
7714
|
-
if (result.aligned && result.matchSource === "active_bet") {
|
|
7935
|
+
if (result.aligned && result.matchSource === "active_bet" && result.matchedBet) {
|
|
7715
7936
|
lines.push(
|
|
7716
7937
|
`Task aligns with active bet: **${result.matchedBet}**. Proceed.`
|
|
7717
7938
|
);
|
|
7718
|
-
} else if (result.aligned && result.matchSource === "task_context") {
|
|
7939
|
+
} else if (result.aligned && result.matchSource === "task_context" && result.matchedBet) {
|
|
7719
7940
|
const noActiveBets = result.betNames.length === 0;
|
|
7720
7941
|
lines.push(
|
|
7721
7942
|
`Task related to **${result.matchedBet}** via task context${noActiveBets ? " (no active bets in horizon=now)" : ""}. Proceed with caution \u2014 confirm scope with the user if uncertain.`
|
|
@@ -7745,7 +7966,15 @@ var RULE5_COMPACT = '**Validate against governance.** Before proposing or buildi
|
|
|
7745
7966
|
var MAX_ENTRIES_NO_TASK = 3;
|
|
7746
7967
|
var MAX_ENTRIES_WITH_TASK = 5;
|
|
7747
7968
|
function extractKeywords(text) {
|
|
7748
|
-
|
|
7969
|
+
const normalized = text.toLowerCase().replace(/[^\w\s-]/g, " ");
|
|
7970
|
+
const tokens = [];
|
|
7971
|
+
for (const raw of normalized.split(/\s+/)) {
|
|
7972
|
+
if (!raw) continue;
|
|
7973
|
+
const parts = raw.split(/[-_]/);
|
|
7974
|
+
if (parts.length > 1) tokens.push(raw.replace(/[-_]/g, ""));
|
|
7975
|
+
for (const p of parts) if (p) tokens.push(p);
|
|
7976
|
+
}
|
|
7977
|
+
return [...new Set(tokens.filter((w) => w.length > 2))];
|
|
7749
7978
|
}
|
|
7750
7979
|
function scoreEntry(entry, keywords) {
|
|
7751
7980
|
const text = `${entry.name} ${entry.description ?? ""}`.toLowerCase();
|
|
@@ -7846,73 +8075,16 @@ var interviewExtractionSchema = z14.object({
|
|
|
7846
8075
|
keyDecisions: z14.array(z14.string()).optional().describe("Recent significant decisions made (each as a concise statement)"),
|
|
7847
8076
|
tensions: z14.array(z14.string()).optional().describe("Pain points or friction the product is solving")
|
|
7848
8077
|
});
|
|
7849
|
-
function extractionToBatchEntries(extracted) {
|
|
7850
|
-
const entries = [];
|
|
7851
|
-
if (extracted.vision) {
|
|
7852
|
-
entries.push({
|
|
7853
|
-
collection: "strategy",
|
|
7854
|
-
name: "Product Vision",
|
|
7855
|
-
description: extracted.vision
|
|
7856
|
-
});
|
|
7857
|
-
}
|
|
7858
|
-
if (extracted.audience) {
|
|
7859
|
-
const audienceName = extracted.audience.split(/\s+/).slice(0, 6).join(" ");
|
|
7860
|
-
entries.push({
|
|
7861
|
-
collection: "audiences",
|
|
7862
|
-
name: audienceName,
|
|
7863
|
-
description: extracted.audience
|
|
7864
|
-
});
|
|
7865
|
-
}
|
|
7866
|
-
if (extracted.techStack?.length) {
|
|
7867
|
-
entries.push({
|
|
7868
|
-
collection: "architecture",
|
|
7869
|
-
name: "Tech Stack",
|
|
7870
|
-
description: `Technology choices: ${extracted.techStack.join(", ")}`
|
|
7871
|
-
});
|
|
7872
|
-
for (const tech of extracted.techStack.slice(0, 3)) {
|
|
7873
|
-
entries.push({ collection: "glossary", name: tech, description: `${tech} \u2014 part of the tech stack` });
|
|
7874
|
-
}
|
|
7875
|
-
}
|
|
7876
|
-
if (extracted.keyTerms?.length) {
|
|
7877
|
-
for (const term of extracted.keyTerms) {
|
|
7878
|
-
const alreadyAdded = entries.some(
|
|
7879
|
-
(e) => e.collection === "glossary" && e.name.toLowerCase() === term.toLowerCase()
|
|
7880
|
-
);
|
|
7881
|
-
if (!alreadyAdded) {
|
|
7882
|
-
entries.push({ collection: "glossary", name: term, description: `Core domain term: ${term}` });
|
|
7883
|
-
}
|
|
7884
|
-
}
|
|
7885
|
-
}
|
|
7886
|
-
if (extracted.keyDecisions?.length) {
|
|
7887
|
-
for (const decision of extracted.keyDecisions) {
|
|
7888
|
-
entries.push({
|
|
7889
|
-
collection: "decisions",
|
|
7890
|
-
name: decision.slice(0, 80),
|
|
7891
|
-
description: decision
|
|
7892
|
-
});
|
|
7893
|
-
}
|
|
7894
|
-
}
|
|
7895
|
-
if (extracted.tensions?.length) {
|
|
7896
|
-
for (const tension of extracted.tensions) {
|
|
7897
|
-
entries.push({
|
|
7898
|
-
collection: "tensions",
|
|
7899
|
-
name: tension.slice(0, 80),
|
|
7900
|
-
description: tension
|
|
7901
|
-
});
|
|
7902
|
-
}
|
|
7903
|
-
}
|
|
7904
|
-
return entries;
|
|
7905
|
-
}
|
|
7906
8078
|
function getInterviewInstructions(workspaceName) {
|
|
7907
8079
|
return {
|
|
7908
|
-
systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and
|
|
7909
|
-
question1: `**
|
|
7910
|
-
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.)`,
|
|
7911
8083
|
extractionGuidance: `After the user answers, extract:
|
|
7912
8084
|
- vision: the core product purpose (required)
|
|
7913
8085
|
- audience: who it's for (optional)
|
|
7914
8086
|
- techStack: technologies mentioned (optional, array)
|
|
7915
|
-
- keyTerms: domain terms \u2014 Q2 answer
|
|
8087
|
+
- keyTerms: domain terms \u2014 Q2 answer goes here (optional, array)
|
|
7916
8088
|
- keyDecisions: any decisions stated (optional, array)
|
|
7917
8089
|
- tensions: any pain points stated (optional, array)
|
|
7918
8090
|
|
|
@@ -7923,7 +8095,7 @@ Map to batch-capture entries:
|
|
|
7923
8095
|
- keyTerms \u2192 glossary (1 per term)
|
|
7924
8096
|
- keyDecisions \u2192 decisions (1 per decision)
|
|
7925
8097
|
- tensions \u2192 tensions (1 per tension)`,
|
|
7926
|
-
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.`,
|
|
7927
8099
|
qualityNote: (
|
|
7928
8100
|
// FEAT-149: Retrieval-First Proof Moment.
|
|
7929
8101
|
// The aha is friction elimination ("I'll never have to explain this again"),
|
|
@@ -7943,25 +8115,73 @@ That's context you won't have to explain again."
|
|
|
7943
8115
|
|
|
7944
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."`
|
|
7945
8117
|
),
|
|
7946
|
-
scanOffer: `After the proof moment, offer the codebase scan
|
|
7947
|
-
"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."`
|
|
7948
8120
|
};
|
|
7949
8121
|
}
|
|
7950
8122
|
function buildInterviewResponse(workspaceName) {
|
|
7951
8123
|
const instructions = getInterviewInstructions(workspaceName);
|
|
7952
8124
|
return [
|
|
7953
|
-
"## Let's
|
|
8125
|
+
"## Let's get to know your product",
|
|
7954
8126
|
"",
|
|
7955
8127
|
instructions.systemPrompt,
|
|
7956
8128
|
"",
|
|
7957
|
-
"I'll ask you
|
|
8129
|
+
"I'll ask you one or two questions. Your answers become the foundation of your Brain.",
|
|
7958
8130
|
"",
|
|
7959
|
-
|
|
8131
|
+
instructions.question1,
|
|
7960
8132
|
"",
|
|
7961
|
-
"_Take your time \u2014
|
|
8133
|
+
"_Take your time \u2014 I'll pull out the structure from whatever you say._"
|
|
7962
8134
|
].join("\n");
|
|
7963
8135
|
}
|
|
7964
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
|
+
|
|
7965
8185
|
// src/tools/start.ts
|
|
7966
8186
|
async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
|
|
7967
8187
|
if (!agentSessionId) return { oriented: false, orientationStatus: "no_session" };
|
|
@@ -8000,7 +8220,7 @@ function registerStartTools(server) {
|
|
|
8000
8220
|
try {
|
|
8001
8221
|
wsCtx = await getWorkspaceContext();
|
|
8002
8222
|
} catch (e) {
|
|
8003
|
-
errors.push(`Workspace: ${e.message}`);
|
|
8223
|
+
errors.push(`Workspace: ${e instanceof Error ? e.message : String(e)}`);
|
|
8004
8224
|
}
|
|
8005
8225
|
if (!wsCtx) {
|
|
8006
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.");
|
|
@@ -8149,7 +8369,7 @@ function buildBlankResponse(wsCtx, sessionCtx) {
|
|
|
8149
8369
|
}
|
|
8150
8370
|
async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
|
|
8151
8371
|
const stage = readiness?.stage ?? "seeded";
|
|
8152
|
-
const score = readiness?.score;
|
|
8372
|
+
const score = readiness?.score ?? null;
|
|
8153
8373
|
const gaps = readiness?.gaps ?? [];
|
|
8154
8374
|
const lines = [
|
|
8155
8375
|
`# ${wsCtx.workspaceName}`,
|
|
@@ -8157,14 +8377,8 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
|
|
|
8157
8377
|
""
|
|
8158
8378
|
];
|
|
8159
8379
|
if (gaps.length > 0) {
|
|
8160
|
-
|
|
8161
|
-
lines.push(
|
|
8162
|
-
const topGaps = gaps.slice(0, 3);
|
|
8163
|
-
for (let i = 0; i < topGaps.length; i++) {
|
|
8164
|
-
const gap = topGaps[i];
|
|
8165
|
-
const cta = gap.capabilityGuidance ?? gap.guidance ?? `Describe your ${gap.label.toLowerCase()}.`;
|
|
8166
|
-
lines.push(`${i + 1}. **${gap.label}** \u2014 ${cta}`);
|
|
8167
|
-
}
|
|
8380
|
+
const gapLines = formatGapList(gaps, 3);
|
|
8381
|
+
lines.push(...gapLines);
|
|
8168
8382
|
lines.push("");
|
|
8169
8383
|
lines.push("Pick any to start \u2014 or begin with **#1** and I'll guide you through it.");
|
|
8170
8384
|
} else {
|
|
@@ -8279,21 +8493,13 @@ function computeWorkspaceAge(createdAt) {
|
|
|
8279
8493
|
const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
|
|
8280
8494
|
return { ageDays, isNeglected: ageDays >= 30 };
|
|
8281
8495
|
}
|
|
8282
|
-
function
|
|
8283
|
-
if (
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
}
|
|
8289
|
-
if (openTensions.length > 0) {
|
|
8290
|
-
const t = openTensions[0];
|
|
8291
|
-
return {
|
|
8292
|
-
action: `Open tension: ${t.name}`,
|
|
8293
|
-
cta: "Want to discuss this tension or capture a decision about it?"
|
|
8294
|
-
};
|
|
8295
|
-
}
|
|
8296
|
-
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
|
+
};
|
|
8297
8503
|
}
|
|
8298
8504
|
async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
8299
8505
|
const wsFullCtx = await getWorkspaceContext();
|
|
@@ -8316,7 +8522,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
8316
8522
|
try {
|
|
8317
8523
|
readiness = await mcpQuery("chain.workspaceReadiness");
|
|
8318
8524
|
} catch (e) {
|
|
8319
|
-
errors.push(`Readiness: ${e.message}`);
|
|
8525
|
+
errors.push(`Readiness: ${e instanceof Error ? e.message : String(e)}`);
|
|
8320
8526
|
}
|
|
8321
8527
|
const lines = [];
|
|
8322
8528
|
const isLowReadiness = readiness !== null && readiness.score < 50;
|
|
@@ -8336,29 +8542,42 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
8336
8542
|
lines.push("Let's get your workspace active.");
|
|
8337
8543
|
lines.push("");
|
|
8338
8544
|
}
|
|
8339
|
-
if (isLowReadiness) {
|
|
8340
|
-
const
|
|
8341
|
-
if (
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
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));
|
|
8346
8555
|
lines.push("");
|
|
8347
8556
|
lines.push(captureBehaviorNote);
|
|
8348
8557
|
lines.push("");
|
|
8349
|
-
const remainingGaps =
|
|
8558
|
+
const remainingGaps = gaps.length - 1;
|
|
8350
8559
|
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
8351
|
-
|
|
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);
|
|
8352
8571
|
lines.push("");
|
|
8353
8572
|
}
|
|
8354
8573
|
}
|
|
8355
|
-
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._");
|
|
8356
8575
|
lines.push("");
|
|
8357
8576
|
} else if (isHighReadiness) {
|
|
8358
8577
|
let activeBets = [];
|
|
8359
8578
|
try {
|
|
8360
|
-
const
|
|
8361
|
-
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);
|
|
8362
8581
|
} catch {
|
|
8363
8582
|
}
|
|
8364
8583
|
if (task) {
|
|
@@ -10153,90 +10372,17 @@ function formatScanReport(result) {
|
|
|
10153
10372
|
return lines.join("\n");
|
|
10154
10373
|
}
|
|
10155
10374
|
|
|
10156
|
-
// src/tools/
|
|
10157
|
-
|
|
10158
|
-
"chain.getEntry": "read",
|
|
10159
|
-
"chain.batchGetEntries": "read",
|
|
10160
|
-
"chain.listEntries": "read",
|
|
10161
|
-
"chain.listEntryHistory": "read",
|
|
10162
|
-
"chain.listEntryRelations": "read",
|
|
10163
|
-
"chain.listEntriesByLabel": "read",
|
|
10164
|
-
"chain.searchEntries": "search",
|
|
10165
|
-
"chain.createEntry": "write",
|
|
10166
|
-
"chain.updateEntry": "write",
|
|
10167
|
-
"chain.moveToCollection": "write",
|
|
10168
|
-
"chain.createEntryRelation": "write",
|
|
10169
|
-
"chain.applyLabel": "label",
|
|
10170
|
-
"chain.removeLabel": "label",
|
|
10171
|
-
"chain.createLabel": "label",
|
|
10172
|
-
"chain.updateLabel": "label",
|
|
10173
|
-
"chain.deleteLabel": "label",
|
|
10174
|
-
"chain.createCollection": "write",
|
|
10175
|
-
"chain.updateCollection": "write",
|
|
10176
|
-
"chain.listCollections": "meta",
|
|
10177
|
-
"chain.getCollection": "meta",
|
|
10178
|
-
"chain.listLabels": "meta",
|
|
10179
|
-
"resolveWorkspace": "meta"
|
|
10180
|
-
};
|
|
10181
|
-
function categorize(fn) {
|
|
10182
|
-
return CALL_CATEGORIES[fn] ?? "meta";
|
|
10183
|
-
}
|
|
10184
|
-
function formatDuration(ms) {
|
|
10185
|
-
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
10186
|
-
const mins = Math.floor(ms / 6e4);
|
|
10187
|
-
const secs = Math.round(ms % 6e4 / 1e3);
|
|
10188
|
-
return `${mins}m ${secs}s`;
|
|
10189
|
-
}
|
|
10190
|
-
function buildSessionSummary(log) {
|
|
10191
|
-
if (log.length === 0) return "";
|
|
10192
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
10193
|
-
let errorCount = 0;
|
|
10194
|
-
let writeCreates = 0;
|
|
10195
|
-
let writeUpdates = 0;
|
|
10196
|
-
for (const entry of log) {
|
|
10197
|
-
const cat = categorize(entry.fn);
|
|
10198
|
-
if (!byCategory.has(cat)) byCategory.set(cat, /* @__PURE__ */ new Map());
|
|
10199
|
-
const fnCounts = byCategory.get(cat);
|
|
10200
|
-
fnCounts.set(entry.fn, (fnCounts.get(entry.fn) ?? 0) + 1);
|
|
10201
|
-
if (entry.status === "error") errorCount++;
|
|
10202
|
-
if (entry.fn === "chain.createEntry" && entry.status === "ok") writeCreates++;
|
|
10203
|
-
if (entry.fn === "chain.updateEntry" && entry.status === "ok") writeUpdates++;
|
|
10204
|
-
}
|
|
10205
|
-
const firstTs = new Date(log[0].ts).getTime();
|
|
10206
|
-
const lastTs = new Date(log[log.length - 1].ts).getTime();
|
|
10207
|
-
const duration = formatDuration(lastTs - firstTs);
|
|
10208
|
-
const lines = [`# Session Summary (${duration})
|
|
10209
|
-
`];
|
|
10210
|
-
const categoryLabels = [
|
|
10211
|
-
["read", "Reads"],
|
|
10212
|
-
["search", "Searches"],
|
|
10213
|
-
["write", "Writes"],
|
|
10214
|
-
["label", "Labels"],
|
|
10215
|
-
["meta", "Meta"]
|
|
10216
|
-
];
|
|
10217
|
-
for (const [cat, label] of categoryLabels) {
|
|
10218
|
-
const fnCounts = byCategory.get(cat);
|
|
10219
|
-
if (!fnCounts || fnCounts.size === 0) continue;
|
|
10220
|
-
const total = [...fnCounts.values()].reduce((a, b) => a + b, 0);
|
|
10221
|
-
const detail = [...fnCounts.entries()].sort((a, b) => b[1] - a[1]).map(([fn, count]) => `${fn.replace("chain.", "")} x${count}`).join(", ");
|
|
10222
|
-
lines.push(`- **${label}:** ${total} call${total === 1 ? "" : "s"} (${detail})`);
|
|
10223
|
-
}
|
|
10224
|
-
lines.push(`- **Errors:** ${errorCount}`);
|
|
10225
|
-
if (writeCreates > 0 || writeUpdates > 0) {
|
|
10226
|
-
lines.push("");
|
|
10227
|
-
lines.push("## Knowledge Contribution");
|
|
10228
|
-
if (writeCreates > 0) lines.push(`- ${writeCreates} entr${writeCreates === 1 ? "y" : "ies"} created`);
|
|
10229
|
-
if (writeUpdates > 0) lines.push(`- ${writeUpdates} entr${writeUpdates === 1 ? "y" : "ies"} updated`);
|
|
10230
|
-
}
|
|
10231
|
-
return lines.join("\n");
|
|
10232
|
-
}
|
|
10375
|
+
// src/tools/orient.ts
|
|
10376
|
+
import { z as z20 } from "zod";
|
|
10233
10377
|
function extractSessionEntryIds(priorSessions) {
|
|
10234
10378
|
const allSeen = /* @__PURE__ */ new Set();
|
|
10235
10379
|
const all = [];
|
|
10236
10380
|
let lastSessionOnly = [];
|
|
10237
10381
|
for (let i = 0; i < priorSessions.length; i++) {
|
|
10238
10382
|
const s = priorSessions[i];
|
|
10239
|
-
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);
|
|
10240
10386
|
if (i === 0) {
|
|
10241
10387
|
lastSessionOnly = [...new Set(ids)].slice(0, 5);
|
|
10242
10388
|
}
|
|
@@ -10251,426 +10397,11 @@ function extractSessionEntryIds(priorSessions) {
|
|
|
10251
10397
|
}
|
|
10252
10398
|
return { all, lastSessionOnly };
|
|
10253
10399
|
}
|
|
10254
|
-
function computeOrganisationHealth(entries) {
|
|
10255
|
-
let agreements = 0;
|
|
10256
|
-
let disagreements = 0;
|
|
10257
|
-
let abstentions = 0;
|
|
10258
|
-
const flagMap = /* @__PURE__ */ new Map();
|
|
10259
|
-
for (const entry of entries) {
|
|
10260
|
-
const slug = entry.collectionSlug ?? entry.collection ?? "unknown";
|
|
10261
|
-
const description = typeof entry.data?.description === "string" ? entry.data.description : "";
|
|
10262
|
-
const result = classifyCollection(entry.name, description);
|
|
10263
|
-
if (!result) {
|
|
10264
|
-
abstentions++;
|
|
10265
|
-
continue;
|
|
10266
|
-
}
|
|
10267
|
-
if (result.collection === slug) {
|
|
10268
|
-
agreements++;
|
|
10269
|
-
} else {
|
|
10270
|
-
disagreements++;
|
|
10271
|
-
if (!flagMap.has(slug)) flagMap.set(slug, /* @__PURE__ */ new Map());
|
|
10272
|
-
const suggestions = flagMap.get(slug);
|
|
10273
|
-
suggestions.set(result.collection, (suggestions.get(result.collection) ?? 0) + 1);
|
|
10274
|
-
}
|
|
10275
|
-
}
|
|
10276
|
-
const opinionated = agreements + disagreements;
|
|
10277
|
-
const agreementRate = opinionated > 0 ? Math.round(agreements / opinionated * 100) : 100;
|
|
10278
|
-
const flags = [...flagMap.entries()].map(([collection, suggestions]) => {
|
|
10279
|
-
const total = [...suggestions.values()].reduce((a, b) => a + b, 0);
|
|
10280
|
-
const topSuggested = [...suggestions.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
10281
|
-
return { collection, count: total, suggestedCollection: topSuggested?.[0] ?? "unknown" };
|
|
10282
|
-
}).sort((a, b) => b.count - a.count).slice(0, 3);
|
|
10283
|
-
return { reviewed: entries.length, agreements, disagreements, abstentions, agreementRate, flags };
|
|
10284
|
-
}
|
|
10285
|
-
function formatOrgHealthLines(orgHealth, maxFlags = 3) {
|
|
10286
|
-
const lines = [];
|
|
10287
|
-
if (orgHealth.disagreements > 0) {
|
|
10288
|
-
lines.push(
|
|
10289
|
-
`${orgHealth.disagreements} of ${orgHealth.reviewed} entries flagged for review (${orgHealth.agreementRate}% classifier agreement).`
|
|
10290
|
-
);
|
|
10291
|
-
for (const flag of orgHealth.flags.slice(0, maxFlags)) {
|
|
10292
|
-
lines.push(`- **${flag.collection}**: ${flag.count} entries may belong in \`${flag.suggestedCollection}\``);
|
|
10293
|
-
}
|
|
10294
|
-
} else if (orgHealth.reviewed > 0) {
|
|
10295
|
-
lines.push(`All ${orgHealth.reviewed - orgHealth.abstentions} classified entries agree with stored collection (${orgHealth.abstentions} without coverage).`);
|
|
10296
|
-
}
|
|
10297
|
-
return lines;
|
|
10298
|
-
}
|
|
10299
|
-
async function fetchOrganisationHealth() {
|
|
10300
|
-
try {
|
|
10301
|
-
const allEntries = await mcpQuery("chain.listEntries", { status: "active" });
|
|
10302
|
-
if (!allEntries || allEntries.length === 0) return null;
|
|
10303
|
-
return computeOrganisationHealth(allEntries);
|
|
10304
|
-
} catch (err) {
|
|
10305
|
-
process.stderr.write(`[MCP] fetchOrganisationHealth failed: ${err.message}
|
|
10306
|
-
`);
|
|
10307
|
-
return null;
|
|
10308
|
-
}
|
|
10309
|
-
}
|
|
10310
|
-
async function handleHealthCheck() {
|
|
10311
|
-
const start = Date.now();
|
|
10312
|
-
const errors = [];
|
|
10313
|
-
let workspaceId;
|
|
10314
|
-
try {
|
|
10315
|
-
workspaceId = await getWorkspaceId();
|
|
10316
|
-
} catch (e) {
|
|
10317
|
-
errors.push(`Workspace resolution failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10318
|
-
}
|
|
10319
|
-
let collections = [];
|
|
10320
|
-
try {
|
|
10321
|
-
collections = await mcpQuery("chain.listCollections");
|
|
10322
|
-
} catch (e) {
|
|
10323
|
-
errors.push(`Collection fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10324
|
-
}
|
|
10325
|
-
let totalEntries = 0;
|
|
10326
|
-
if (collections.length > 0) {
|
|
10327
|
-
try {
|
|
10328
|
-
const entries = await mcpQuery("chain.listEntries", {});
|
|
10329
|
-
totalEntries = entries.length;
|
|
10330
|
-
} catch (e) {
|
|
10331
|
-
errors.push(`Entry count failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10332
|
-
}
|
|
10333
|
-
}
|
|
10334
|
-
let wsCtx = null;
|
|
10335
|
-
try {
|
|
10336
|
-
wsCtx = await getWorkspaceContext();
|
|
10337
|
-
} catch {
|
|
10338
|
-
}
|
|
10339
|
-
const durationMs = Date.now() - start;
|
|
10340
|
-
const healthy = errors.length === 0;
|
|
10341
|
-
const lines = [
|
|
10342
|
-
`# ${healthy ? "Healthy" : "Degraded"}`,
|
|
10343
|
-
"",
|
|
10344
|
-
`**Workspace:** ${workspaceId ?? "unresolved"}`,
|
|
10345
|
-
`**Workspace Slug:** ${wsCtx?.workspaceSlug ?? "unknown"}`,
|
|
10346
|
-
`**Workspace Name:** ${wsCtx?.workspaceName ?? "unknown"}`,
|
|
10347
|
-
`**Collections:** ${collections.length}`,
|
|
10348
|
-
`**Entries:** ${totalEntries}`,
|
|
10349
|
-
`**Latency:** ${durationMs}ms`
|
|
10350
|
-
];
|
|
10351
|
-
if (errors.length > 0) {
|
|
10352
|
-
lines.push("", "## Errors");
|
|
10353
|
-
for (const err of errors) {
|
|
10354
|
-
lines.push(`- ${err}`);
|
|
10355
|
-
}
|
|
10356
|
-
}
|
|
10357
|
-
const healthData = {
|
|
10358
|
-
healthy,
|
|
10359
|
-
collections: collections.length,
|
|
10360
|
-
entries: totalEntries,
|
|
10361
|
-
latencyMs: durationMs,
|
|
10362
|
-
workspace: workspaceId ?? "unresolved"
|
|
10363
|
-
};
|
|
10364
|
-
return {
|
|
10365
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10366
|
-
structuredContent: success(
|
|
10367
|
-
healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
|
|
10368
|
-
healthData
|
|
10369
|
-
)
|
|
10370
|
-
};
|
|
10371
|
-
}
|
|
10372
|
-
async function handleWhoami() {
|
|
10373
|
-
const ctx = await getWorkspaceContext();
|
|
10374
|
-
const sessionId = getAgentSessionId();
|
|
10375
|
-
const scope = getApiKeyScope();
|
|
10376
|
-
const oriented = isSessionOriented();
|
|
10377
|
-
const lines = [
|
|
10378
|
-
`# Session Identity`,
|
|
10379
|
-
"",
|
|
10380
|
-
`**Workspace ID:** ${ctx.workspaceId}`,
|
|
10381
|
-
`**Workspace Slug:** ${ctx.workspaceSlug}`,
|
|
10382
|
-
`**Workspace Name:** ${ctx.workspaceName}`
|
|
10383
|
-
];
|
|
10384
|
-
return {
|
|
10385
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10386
|
-
structuredContent: success(
|
|
10387
|
-
`Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
|
|
10388
|
-
{ workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
|
|
10389
|
-
)
|
|
10390
|
-
};
|
|
10391
|
-
}
|
|
10392
|
-
var STAGE_LABELS = {
|
|
10393
|
-
blank: "Blank",
|
|
10394
|
-
seeded: "Seeded",
|
|
10395
|
-
grounded: "Grounded",
|
|
10396
|
-
connected: "Connected"
|
|
10397
|
-
};
|
|
10398
|
-
var STAGE_DESCRIPTIONS = {
|
|
10399
|
-
blank: "No knowledge captured yet.",
|
|
10400
|
-
seeded: "Early knowledge is in place \u2014 keep building.",
|
|
10401
|
-
grounded: "Solid foundations \u2014 a few gaps remain.",
|
|
10402
|
-
connected: "Well-connected knowledge graph \u2014 your Brain is useful."
|
|
10403
|
-
};
|
|
10404
|
-
async function handleWorkspaceStatus() {
|
|
10405
|
-
const result = await mcpQuery("chain.workspaceReadiness");
|
|
10406
|
-
const { score, totalChecks, passedChecks, checks, gaps, stats, governanceMode } = result;
|
|
10407
|
-
const scoringVersion = result.scoringVersion ?? "v1";
|
|
10408
|
-
const stage = result.stage ?? "seeded";
|
|
10409
|
-
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
|
10410
|
-
const stageDescription = STAGE_DESCRIPTIONS[stage] ?? "";
|
|
10411
|
-
const scoreBar = "\u2588".repeat(Math.round(score / 10)) + "\u2591".repeat(10 - Math.round(score / 10));
|
|
10412
|
-
const lines = [
|
|
10413
|
-
`# Brain Status: ${stageLabel}`,
|
|
10414
|
-
`_${stageDescription}_`,
|
|
10415
|
-
"",
|
|
10416
|
-
`${scoreBar} ${stageLabel} \xB7 ${score}%`,
|
|
10417
|
-
`**Governance:** ${governanceMode ?? "open"}${(governanceMode ?? "open") !== "open" ? " (commits require proposal)" : ""}`,
|
|
10418
|
-
"",
|
|
10419
|
-
"## Stats",
|
|
10420
|
-
`- **Entries:** ${stats.totalEntries} (${stats.activeCount} active, ${stats.draftCount} draft)`,
|
|
10421
|
-
`- **Relations:** ${stats.totalRelations}`,
|
|
10422
|
-
`- **Collections:** ${stats.collectionCount}`,
|
|
10423
|
-
`- **Orphaned:** ${stats.orphanedCount} committed entries with no relations`,
|
|
10424
|
-
""
|
|
10425
|
-
];
|
|
10426
|
-
if (gaps.length > 0) {
|
|
10427
|
-
lines.push("## Gaps");
|
|
10428
|
-
for (const gap of gaps) {
|
|
10429
|
-
const action = gap.capabilityGuidance ?? gap.guidance;
|
|
10430
|
-
lines.push(`- [ ] **${gap.label}**`);
|
|
10431
|
-
lines.push(` _${action}_`);
|
|
10432
|
-
}
|
|
10433
|
-
lines.push("");
|
|
10434
|
-
}
|
|
10435
|
-
const passed = checks.filter((c) => c.passed);
|
|
10436
|
-
if (passed.length > 0) {
|
|
10437
|
-
lines.push("## Passing checks");
|
|
10438
|
-
for (const check of passed) {
|
|
10439
|
-
lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
|
|
10440
|
-
}
|
|
10441
|
-
lines.push("");
|
|
10442
|
-
}
|
|
10443
|
-
const orgHealth = await fetchOrganisationHealth();
|
|
10444
|
-
if (orgHealth && orgHealth.reviewed > 0) {
|
|
10445
|
-
lines.push("## Organisation Health");
|
|
10446
|
-
lines.push(...formatOrgHealthLines(orgHealth));
|
|
10447
|
-
lines.push("");
|
|
10448
|
-
}
|
|
10449
|
-
const statusData = {
|
|
10450
|
-
stage,
|
|
10451
|
-
scoringVersion,
|
|
10452
|
-
readinessScore: score,
|
|
10453
|
-
activeEntries: stats.activeCount,
|
|
10454
|
-
totalRelations: stats.totalRelations,
|
|
10455
|
-
orphanedEntries: stats.orphanedCount,
|
|
10456
|
-
gaps: gaps.map((g) => ({
|
|
10457
|
-
id: g.id,
|
|
10458
|
-
label: g.label,
|
|
10459
|
-
guidance: g.capabilityGuidance ?? g.guidance
|
|
10460
|
-
})),
|
|
10461
|
-
...orgHealth && { organisationHealth: orgHealth }
|
|
10462
|
-
};
|
|
10463
|
-
return {
|
|
10464
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10465
|
-
structuredContent: success(
|
|
10466
|
-
`Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
|
|
10467
|
-
statusData
|
|
10468
|
-
)
|
|
10469
|
-
};
|
|
10470
|
-
}
|
|
10471
|
-
async function handleAudit(limit) {
|
|
10472
|
-
const log = getAuditLog();
|
|
10473
|
-
const recent = log.slice(-limit);
|
|
10474
|
-
if (recent.length === 0) {
|
|
10475
|
-
return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
|
|
10476
|
-
}
|
|
10477
|
-
const summary = buildSessionSummary(log);
|
|
10478
|
-
const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
|
|
10479
|
-
`];
|
|
10480
|
-
for (const entry of recent) {
|
|
10481
|
-
const icon = entry.status === "ok" ? "\u2713" : "\u2717";
|
|
10482
|
-
const errPart = entry.error ? ` \u2014 ${entry.error}` : "";
|
|
10483
|
-
const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
|
|
10484
|
-
logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
|
|
10485
|
-
}
|
|
10486
|
-
const auditData = {
|
|
10487
|
-
totalCalls: log.length,
|
|
10488
|
-
calls: recent.map((entry) => ({
|
|
10489
|
-
tool: entry.fn,
|
|
10490
|
-
...entry.toolContext?.action && { action: entry.toolContext.action },
|
|
10491
|
-
timestamp: entry.ts,
|
|
10492
|
-
...entry.durationMs != null && { durationMs: entry.durationMs }
|
|
10493
|
-
}))
|
|
10494
|
-
};
|
|
10495
|
-
return {
|
|
10496
|
-
content: [{ type: "text", text: `${summary}
|
|
10497
|
-
|
|
10498
|
-
---
|
|
10499
|
-
|
|
10500
|
-
${logLines.join("\n")}` }],
|
|
10501
|
-
structuredContent: success(
|
|
10502
|
-
`Audit: ${log.length} total calls, showing last ${recent.length}.`,
|
|
10503
|
-
auditData
|
|
10504
|
-
)
|
|
10505
|
-
};
|
|
10506
|
-
}
|
|
10507
|
-
var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
|
|
10508
|
-
var healthSchema = z20.object({
|
|
10509
|
-
action: z20.enum(HEALTH_ACTIONS).describe(
|
|
10510
|
-
"'check': connectivity and workspace stats. 'whoami': session identity. 'status': workspace readiness. 'audit': session audit log. 'self-test': validate all tool schemas."
|
|
10511
|
-
),
|
|
10512
|
-
limit: z20.number().min(1).max(50).default(20).optional().describe("For audit: how many recent calls to show (max 50)")
|
|
10513
|
-
});
|
|
10514
10400
|
var orientSchema = z20.object({
|
|
10515
10401
|
mode: z20.enum(["full", "brief"]).optional().default("full").describe("full = full context (default). brief = compact summary for mid-session re-orientation."),
|
|
10516
10402
|
task: z20.string().optional().describe("Natural-language task description for task-scoped context. When provided, orient returns scored, relevant entries for the task.")
|
|
10517
10403
|
});
|
|
10518
|
-
|
|
10519
|
-
healthy: z20.boolean(),
|
|
10520
|
-
collections: z20.number(),
|
|
10521
|
-
entries: z20.number(),
|
|
10522
|
-
latencyMs: z20.number(),
|
|
10523
|
-
workspace: z20.string()
|
|
10524
|
-
});
|
|
10525
|
-
var organisationHealthSchema = z20.object({
|
|
10526
|
-
reviewed: z20.number(),
|
|
10527
|
-
agreements: z20.number(),
|
|
10528
|
-
disagreements: z20.number(),
|
|
10529
|
-
abstentions: z20.number(),
|
|
10530
|
-
agreementRate: z20.number(),
|
|
10531
|
-
flags: z20.array(z20.object({
|
|
10532
|
-
collection: z20.string(),
|
|
10533
|
-
count: z20.number(),
|
|
10534
|
-
suggestedCollection: z20.string()
|
|
10535
|
-
}))
|
|
10536
|
-
});
|
|
10537
|
-
var healthStatusOutputSchema = z20.object({
|
|
10538
|
-
stage: z20.enum(["blank", "seeded", "grounded", "connected"]).optional().default("seeded"),
|
|
10539
|
-
scoringVersion: z20.enum(["v1", "v2"]).optional().default("v1"),
|
|
10540
|
-
readinessScore: z20.number(),
|
|
10541
|
-
activeEntries: z20.number(),
|
|
10542
|
-
totalRelations: z20.number(),
|
|
10543
|
-
orphanedEntries: z20.number(),
|
|
10544
|
-
gaps: z20.array(z20.object({ id: z20.string(), label: z20.string(), guidance: z20.string() })),
|
|
10545
|
-
organisationHealth: organisationHealthSchema.optional()
|
|
10546
|
-
});
|
|
10547
|
-
var healthAuditOutputSchema = z20.object({
|
|
10548
|
-
totalCalls: z20.number(),
|
|
10549
|
-
calls: z20.array(z20.object({
|
|
10550
|
-
tool: z20.string(),
|
|
10551
|
-
action: z20.string().optional(),
|
|
10552
|
-
timestamp: z20.string(),
|
|
10553
|
-
durationMs: z20.number().optional()
|
|
10554
|
-
}))
|
|
10555
|
-
});
|
|
10556
|
-
var healthWhoamiOutputSchema = z20.object({
|
|
10557
|
-
workspaceId: z20.string(),
|
|
10558
|
-
workspaceName: z20.string(),
|
|
10559
|
-
scope: z20.string(),
|
|
10560
|
-
sessionId: z20.union([z20.string(), z20.null()]),
|
|
10561
|
-
oriented: z20.boolean()
|
|
10562
|
-
});
|
|
10563
|
-
var ALL_TOOL_SCHEMAS = [
|
|
10564
|
-
{ name: "entries", schema: entriesSchema },
|
|
10565
|
-
{ name: "relations", schema: relationsSchema },
|
|
10566
|
-
{ name: "graph", schema: graphSchema },
|
|
10567
|
-
{ name: "context", schema: contextSchema },
|
|
10568
|
-
{ name: "collections", schema: collectionsSchema },
|
|
10569
|
-
{ name: "session", schema: sessionSchema },
|
|
10570
|
-
{ name: "health", schema: healthSchema },
|
|
10571
|
-
{ name: "orient", schema: orientSchema },
|
|
10572
|
-
{ name: "quality", schema: qualitySchema },
|
|
10573
|
-
{ name: "workflows", schema: workflowsSchema },
|
|
10574
|
-
{ name: "session-wrapup", schema: wrapupSchema },
|
|
10575
|
-
{ name: "labels", schema: labelsSchema },
|
|
10576
|
-
{ name: "verify", schema: verifySchema },
|
|
10577
|
-
{ name: "capture", schema: captureSchema },
|
|
10578
|
-
{ name: "batch-capture", schema: batchCaptureSchema },
|
|
10579
|
-
{ name: "update-entry", schema: updateEntrySchema },
|
|
10580
|
-
{ name: "get-history", schema: getHistorySchema },
|
|
10581
|
-
{ name: "commit-entry", schema: commitEntrySchema },
|
|
10582
|
-
{ name: "start", schema: startSchema },
|
|
10583
|
-
{ name: "get-usage-summary", schema: usageSummarySchema },
|
|
10584
|
-
{ name: "chain", schema: chainSchema },
|
|
10585
|
-
{ name: "chain-version", schema: chainVersionSchema },
|
|
10586
|
-
{ name: "chain-branch", schema: chainBranchSchema },
|
|
10587
|
-
{ name: "chain-review", schema: chainReviewSchema },
|
|
10588
|
-
{ name: "create-audience-map-set", schema: createAudienceMapSetSchema },
|
|
10589
|
-
{ name: "map", schema: mapSchema },
|
|
10590
|
-
{ name: "map-slot", schema: mapSlotSchema },
|
|
10591
|
-
{ name: "map-version", schema: mapVersionSchema },
|
|
10592
|
-
{ name: "map-suggest", schema: mapSuggestSchema },
|
|
10593
|
-
{ name: "architecture", schema: architectureSchema },
|
|
10594
|
-
{ name: "architecture-admin", schema: architectureAdminSchema },
|
|
10595
|
-
{ name: "facilitate", schema: facilitateSchema }
|
|
10596
|
-
];
|
|
10597
|
-
var selfTestOutputSchema = z20.object({
|
|
10598
|
-
passed: z20.number(),
|
|
10599
|
-
failed: z20.number(),
|
|
10600
|
-
total: z20.number(),
|
|
10601
|
-
results: z20.array(z20.object({
|
|
10602
|
-
tool: z20.string(),
|
|
10603
|
-
valid: z20.boolean(),
|
|
10604
|
-
error: z20.string().optional()
|
|
10605
|
-
}))
|
|
10606
|
-
});
|
|
10607
|
-
function handleSelfTest() {
|
|
10608
|
-
const results = [];
|
|
10609
|
-
for (const { name, schema } of ALL_TOOL_SCHEMAS) {
|
|
10610
|
-
try {
|
|
10611
|
-
if (!schema || typeof schema.safeParse !== "function") {
|
|
10612
|
-
results.push({ tool: name, valid: false, error: "Schema is not a valid Zod object" });
|
|
10613
|
-
continue;
|
|
10614
|
-
}
|
|
10615
|
-
const test = schema.safeParse({});
|
|
10616
|
-
if (test.success || test.error) {
|
|
10617
|
-
results.push({ tool: name, valid: true });
|
|
10618
|
-
}
|
|
10619
|
-
} catch (e) {
|
|
10620
|
-
results.push({ tool: name, valid: false, error: e instanceof Error ? e.message : String(e) });
|
|
10621
|
-
}
|
|
10622
|
-
}
|
|
10623
|
-
const passed = results.filter((r) => r.valid).length;
|
|
10624
|
-
const failed = results.filter((r) => !r.valid).length;
|
|
10625
|
-
const total = results.length;
|
|
10626
|
-
const lines = [
|
|
10627
|
-
`# Self-Test: Tool Schema Validation`,
|
|
10628
|
-
`**Result:** ${failed === 0 ? "ALL PASS" : `${failed} FAILED`}`,
|
|
10629
|
-
`**Schemas validated:** ${passed}/${total}`,
|
|
10630
|
-
""
|
|
10631
|
-
];
|
|
10632
|
-
if (failed > 0) {
|
|
10633
|
-
lines.push("## Failures");
|
|
10634
|
-
for (const r of results.filter((r2) => !r2.valid)) {
|
|
10635
|
-
lines.push(`- **${r.tool}**: ${r.error}`);
|
|
10636
|
-
}
|
|
10637
|
-
lines.push("");
|
|
10638
|
-
}
|
|
10639
|
-
lines.push("## All Tools");
|
|
10640
|
-
for (const r of results) {
|
|
10641
|
-
lines.push(`- ${r.valid ? "PASS" : "FAIL"} \`${r.tool}\``);
|
|
10642
|
-
}
|
|
10643
|
-
return {
|
|
10644
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
10645
|
-
structuredContent: success(
|
|
10646
|
-
failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
|
|
10647
|
-
{ passed, failed, total, results }
|
|
10648
|
-
)
|
|
10649
|
-
};
|
|
10650
|
-
}
|
|
10651
|
-
function registerHealthTools(server) {
|
|
10652
|
-
server.registerTool(
|
|
10653
|
-
"health",
|
|
10654
|
-
{
|
|
10655
|
-
title: "Health",
|
|
10656
|
-
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.",
|
|
10657
|
-
inputSchema: healthSchema,
|
|
10658
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
10659
|
-
},
|
|
10660
|
-
withEnvelope(async (args) => {
|
|
10661
|
-
const parsed = parseOrFail(healthSchema, args);
|
|
10662
|
-
if (!parsed.ok) return parsed.result;
|
|
10663
|
-
const { action, limit } = parsed.data;
|
|
10664
|
-
return runWithToolContext({ tool: "health", action }, async () => {
|
|
10665
|
-
if (action === "check") return handleHealthCheck();
|
|
10666
|
-
if (action === "whoami") return handleWhoami();
|
|
10667
|
-
if (action === "status") return handleWorkspaceStatus();
|
|
10668
|
-
if (action === "audit") return handleAudit(limit ?? 20);
|
|
10669
|
-
if (action === "self-test") return handleSelfTest();
|
|
10670
|
-
return unknownAction(action, HEALTH_ACTIONS);
|
|
10671
|
-
});
|
|
10672
|
-
})
|
|
10673
|
-
);
|
|
10404
|
+
function registerOrientTool(server) {
|
|
10674
10405
|
server.registerTool(
|
|
10675
10406
|
"orient",
|
|
10676
10407
|
{
|
|
@@ -10793,6 +10524,11 @@ function registerHealthTools(server) {
|
|
|
10793
10524
|
lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
|
|
10794
10525
|
}
|
|
10795
10526
|
}
|
|
10527
|
+
const briefGaps = readiness?.gaps ?? [];
|
|
10528
|
+
if (briefGaps.length > 0) {
|
|
10529
|
+
const oneLiner = formatGapOneLiner(briefGaps[0]);
|
|
10530
|
+
if (oneLiner) lines.push(oneLiner);
|
|
10531
|
+
}
|
|
10796
10532
|
if (recoveryBlock) {
|
|
10797
10533
|
lines.push("");
|
|
10798
10534
|
lines.push(...formatRecoveryBlock(recoveryBlock));
|
|
@@ -10863,29 +10599,31 @@ function registerHealthTools(server) {
|
|
|
10863
10599
|
lines.push("");
|
|
10864
10600
|
}
|
|
10865
10601
|
}
|
|
10602
|
+
let fullCoherenceSnapshot;
|
|
10866
10603
|
if (isLowReadiness) {
|
|
10867
|
-
|
|
10868
|
-
lines.push("");
|
|
10869
|
-
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._";
|
|
10870
10605
|
const gaps = readiness.gaps ?? [];
|
|
10871
10606
|
if (gaps.length > 0) {
|
|
10872
|
-
const
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
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));
|
|
10878
10614
|
lines.push("");
|
|
10879
10615
|
lines.push(captureBehaviorNote);
|
|
10880
10616
|
lines.push("");
|
|
10881
10617
|
const remainingGaps = gaps.length - 1;
|
|
10882
10618
|
if (remainingGaps > 0 || openTensions.length > 0) {
|
|
10883
|
-
|
|
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._`);
|
|
10884
10623
|
lines.push("");
|
|
10885
10624
|
}
|
|
10886
10625
|
}
|
|
10887
|
-
lines.push("_Need a collection that doesn't exist yet
|
|
10888
|
-
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._");
|
|
10889
10627
|
lines.push("");
|
|
10890
10628
|
} else if (readiness) {
|
|
10891
10629
|
const fmt = (e) => {
|
|
@@ -10893,10 +10631,9 @@ function registerHealthTools(server) {
|
|
|
10893
10631
|
const stratum = e.stratum ?? "?";
|
|
10894
10632
|
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
|
|
10895
10633
|
};
|
|
10896
|
-
let fullCoherenceSnapshot2;
|
|
10897
10634
|
const fullCoherence = buildCoherenceSection();
|
|
10898
10635
|
if (fullCoherence) {
|
|
10899
|
-
|
|
10636
|
+
fullCoherenceSnapshot = fullCoherence.snapshot;
|
|
10900
10637
|
}
|
|
10901
10638
|
if (task) {
|
|
10902
10639
|
lines.push(`**Brain stage: ${orientStage}.** Working on: **${task}**`);
|
|
@@ -10984,6 +10721,13 @@ function registerHealthTools(server) {
|
|
|
10984
10721
|
orientEntries.activeGoals.forEach((e) => lines.push(fmt(e)));
|
|
10985
10722
|
lines.push("");
|
|
10986
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
|
+
}
|
|
10987
10731
|
if (orientEntries.recentDecisions?.length > 0) {
|
|
10988
10732
|
lines.push("## Recent decisions");
|
|
10989
10733
|
orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
|
|
@@ -11063,40 +10807,535 @@ function registerHealthTools(server) {
|
|
|
11063
10807
|
for (const err of errors) lines.push(`- ${err}`);
|
|
11064
10808
|
lines.push("");
|
|
11065
10809
|
}
|
|
11066
|
-
if (agentSessionId) {
|
|
11067
|
-
try {
|
|
11068
|
-
await mcpCall("agent.markOriented", {
|
|
11069
|
-
sessionId: agentSessionId,
|
|
11070
|
-
...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
|
|
11071
|
-
});
|
|
11072
|
-
setSessionOriented(true);
|
|
11073
|
-
lines.push("---");
|
|
11074
|
-
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
11075
|
-
} catch {
|
|
11076
|
-
lines.push("---");
|
|
11077
|
-
lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
|
|
11078
|
-
}
|
|
11079
|
-
try {
|
|
11080
|
-
await mcpMutation("chain.recordSessionSignal", {
|
|
11081
|
-
sessionId: agentSessionId,
|
|
11082
|
-
signalType: "immediate_context_load",
|
|
11083
|
-
metadata: { source: "orient" }
|
|
11084
|
-
});
|
|
11085
|
-
} catch (err) {
|
|
11086
|
-
process.stderr.write(`[MCP] recordSessionSignal failed: ${err.message}
|
|
11087
|
-
`);
|
|
11088
|
-
}
|
|
11089
|
-
} else {
|
|
11090
|
-
lines.push("---");
|
|
11091
|
-
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 });
|
|
11092
11285
|
}
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
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
|
+
});
|
|
11100
11339
|
})
|
|
11101
11340
|
);
|
|
11102
11341
|
}
|
|
@@ -11545,12 +11784,197 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
|
|
|
11545
11784
|
}
|
|
11546
11785
|
|
|
11547
11786
|
// src/prompts/index.ts
|
|
11548
|
-
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
|
|
11549
11973
|
function registerPrompts(server) {
|
|
11550
11974
|
server.prompt(
|
|
11551
11975
|
"review-against-rules",
|
|
11552
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.",
|
|
11553
|
-
{ domain:
|
|
11977
|
+
{ domain: z23.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
11554
11978
|
async ({ domain }) => {
|
|
11555
11979
|
const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
11556
11980
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
@@ -11603,7 +12027,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
11603
12027
|
server.prompt(
|
|
11604
12028
|
"name-check",
|
|
11605
12029
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
11606
|
-
{ names:
|
|
12030
|
+
{ names: z23.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
11607
12031
|
async ({ names }) => {
|
|
11608
12032
|
const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
|
|
11609
12033
|
const glossaryContext = terms.map(
|
|
@@ -11639,7 +12063,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
11639
12063
|
server.prompt(
|
|
11640
12064
|
"draft-decision-record",
|
|
11641
12065
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
11642
|
-
{ context:
|
|
12066
|
+
{ context: z23.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
11643
12067
|
async ({ context }) => {
|
|
11644
12068
|
const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
|
|
11645
12069
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -11677,8 +12101,8 @@ After drafting, I can log it using the capture tool with collection "decisions".
|
|
|
11677
12101
|
"draft-rule-from-context",
|
|
11678
12102
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
11679
12103
|
{
|
|
11680
|
-
observation:
|
|
11681
|
-
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')")
|
|
11682
12106
|
},
|
|
11683
12107
|
async ({ observation, domain }) => {
|
|
11684
12108
|
const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
@@ -11719,10 +12143,10 @@ Make sure the rule is consistent with existing rules and doesn't contradict them
|
|
|
11719
12143
|
"run-workflow",
|
|
11720
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.",
|
|
11721
12145
|
{
|
|
11722
|
-
workflow:
|
|
12146
|
+
workflow: z23.string().describe(
|
|
11723
12147
|
"Workflow ID to run. Available: " + listWorkflows().map((w) => `'${w.id}' (${w.name})`).join(", ")
|
|
11724
12148
|
),
|
|
11725
|
-
context:
|
|
12149
|
+
context: z23.string().optional().describe(
|
|
11726
12150
|
"Optional context from the participant (e.g., 'retro on last sprint', 'shape the Chainwork API bet')"
|
|
11727
12151
|
)
|
|
11728
12152
|
},
|
|
@@ -11867,7 +12291,7 @@ ${wf.errorRecovery}
|
|
|
11867
12291
|
"shape-a-bet",
|
|
11868
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.",
|
|
11869
12293
|
{
|
|
11870
|
-
idea:
|
|
12294
|
+
idea: z23.string().describe("Brief description of the idea or feature to shape (e.g. 'Improve the glossary editing flow')")
|
|
11871
12295
|
},
|
|
11872
12296
|
async ({ idea }) => {
|
|
11873
12297
|
let strategicContext = "";
|
|
@@ -12028,9 +12452,9 @@ Walk away mid-session and everything captured so far exists as drafts. \`facilit
|
|
|
12028
12452
|
"capture-and-connect",
|
|
12029
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.",
|
|
12030
12454
|
{
|
|
12031
|
-
collection:
|
|
12032
|
-
name:
|
|
12033
|
-
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")
|
|
12034
12458
|
},
|
|
12035
12459
|
async ({ collection, name, description }) => {
|
|
12036
12460
|
const collections = await mcpQuery("chain.listCollections");
|
|
@@ -12084,7 +12508,7 @@ Only call \`commit-entry\` when the user explicitly confirms.
|
|
|
12084
12508
|
"deep-dive",
|
|
12085
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.",
|
|
12086
12510
|
{
|
|
12087
|
-
topic:
|
|
12511
|
+
topic: z23.string().describe("Entry ID (e.g. 'BR-001') or topic to explore (e.g. 'authentication')")
|
|
12088
12512
|
},
|
|
12089
12513
|
async ({ topic }) => {
|
|
12090
12514
|
const isEntryId = /^[A-Z]+-\d+$/i.test(topic) || /^[A-Z]+-[a-z0-9]+$/i.test(topic);
|
|
@@ -12167,7 +12591,7 @@ ${contextInstructions}`
|
|
|
12167
12591
|
"pre-commit-check",
|
|
12168
12592
|
"Run a readiness check before committing an entry to the Chain. Validates quality score, required relations, and business rule compliance.",
|
|
12169
12593
|
{
|
|
12170
|
-
entryId:
|
|
12594
|
+
entryId: z23.string().describe("Entry ID to check (e.g. 'GT-019', 'DEC-005')")
|
|
12171
12595
|
},
|
|
12172
12596
|
async ({ entryId }) => {
|
|
12173
12597
|
return {
|
|
@@ -12223,9 +12647,9 @@ If ready, ask the user to confirm. If not, suggest specific improvements.
|
|
|
12223
12647
|
);
|
|
12224
12648
|
server.prompt(
|
|
12225
12649
|
"project-scan",
|
|
12226
|
-
"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.",
|
|
12227
12651
|
{
|
|
12228
|
-
workspaceContext:
|
|
12652
|
+
workspaceContext: z23.string().optional().describe("Optional context about the workspace preset and existing collections (paste from start/orient output)")
|
|
12229
12653
|
},
|
|
12230
12654
|
async ({ workspaceContext }) => {
|
|
12231
12655
|
let collectionsContext = "";
|
|
@@ -12240,17 +12664,19 @@ ${colList}
|
|
|
12240
12664
|
}
|
|
12241
12665
|
} catch {
|
|
12242
12666
|
}
|
|
12243
|
-
const schemaFields = Object.entries(
|
|
12244
|
-
const
|
|
12245
|
-
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",
|
|
12246
12670
|
audience: "Solo developers using Cursor, Claude Code, or similar AI IDEs",
|
|
12247
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"],
|
|
12248
12676
|
keyTerms: ["Chain", "Capture", "Orientation"],
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
};
|
|
12252
|
-
const exampleEntries = extractionToBatchEntries(exampleExtraction);
|
|
12253
|
-
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);
|
|
12254
12680
|
return {
|
|
12255
12681
|
messages: [{
|
|
12256
12682
|
role: "user",
|
|
@@ -12259,84 +12685,94 @@ ${colList}
|
|
|
12259
12685
|
text: `# Project Scan \u2014 Agent-Side Knowledge Extraction
|
|
12260
12686
|
|
|
12261
12687
|
You are scanning the user's local project to extract structured knowledge for Product Brain.
|
|
12262
|
-
PB runs remotely \u2014 you
|
|
12688
|
+
PB runs remotely \u2014 you read the files and extract, then call \`batch-capture\` after confirmation.
|
|
12263
12689
|
|
|
12264
12690
|
` + (workspaceContext ? `## Workspace Context
|
|
12265
12691
|
${workspaceContext}
|
|
12266
12692
|
|
|
12267
12693
|
` : "") + collectionsContext + `## Step 1: Read Project Files
|
|
12268
12694
|
|
|
12269
|
-
Read these files in
|
|
12270
|
-
1. \`README.md\` or \`readme.md\` \u2014 product description, purpose, setup
|
|
12271
|
-
2. \`package.json\` \u2014 name, description, dependencies (framework, database, auth)
|
|
12272
|
-
3. \`tsconfig.json\` or \`tsconfig.app.json\` \u2014 compilation target, paths
|
|
12273
|
-
4. Top-level folder structure (list directories only, max 2 levels deep)
|
|
12274
|
-
5. \`.cursorrules\` or \`CLAUDE.md\` or \`AGENTS.md\` \u2014 project conventions, constraints
|
|
12275
|
-
6. \`git log --oneline -20\` \u2014 recent commit messages for decisions/tensions
|
|
12695
|
+
Read these files in order. Skip gracefully if not present.
|
|
12276
12696
|
|
|
12277
|
-
|
|
12278
|
-
|
|
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.
|
|
12279
12706
|
|
|
12280
12707
|
## Step 2: Extract Structured Data
|
|
12281
12708
|
|
|
12282
|
-
After reading, extract
|
|
12709
|
+
After reading all files, extract into this schema:
|
|
12283
12710
|
|
|
12284
12711
|
\`\`\`
|
|
12285
12712
|
${schemaFields}
|
|
12286
12713
|
\`\`\`
|
|
12287
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
|
+
|
|
12288
12726
|
**Quality rules (RH2 mitigation):**
|
|
12289
|
-
-
|
|
12290
|
-
-
|
|
12291
|
-
-
|
|
12292
|
-
-
|
|
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
|
|
12293
12731
|
|
|
12294
|
-
## Step 3: Validate Before
|
|
12732
|
+
## Step 3: Validate Before Presenting
|
|
12295
12733
|
|
|
12296
|
-
Before
|
|
12297
|
-
- vision is
|
|
12298
|
-
- each entry has a non-empty name and description
|
|
12734
|
+
Before presenting, check each entry:
|
|
12735
|
+
- vision is at least 10 characters if present
|
|
12299
12736
|
- no duplicate names within the same collection
|
|
12300
|
-
-
|
|
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")
|
|
12301
12740
|
|
|
12302
|
-
|
|
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.
|
|
12303
12742
|
|
|
12304
|
-
|
|
12305
|
-
Only after the user confirms which entries to keep should you capture the confirmed entries.
|
|
12743
|
+
## Step 4: Present for Confirmation
|
|
12306
12744
|
|
|
12307
|
-
|
|
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.
|
|
12308
12747
|
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
- \`
|
|
12313
|
-
- \`
|
|
12314
|
-
- \`
|
|
12315
|
-
- \`
|
|
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)
|
|
12316
12760
|
|
|
12317
12761
|
**Example output:**
|
|
12318
12762
|
\`\`\`json
|
|
12319
12763
|
${exampleOutput}
|
|
12320
12764
|
\`\`\`
|
|
12321
12765
|
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
After the user confirms which entries to keep, call batch-capture with only those confirmed entries.
|
|
12325
|
-
Omit \`autoCommit\` to let normal workspace governance apply.
|
|
12326
|
-
|
|
12327
|
-
\`\`\`
|
|
12328
|
-
batch-capture entries=[...your confirmed entries...]
|
|
12329
|
-
\`\`\`
|
|
12330
|
-
|
|
12331
|
-
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.
|
|
12332
12768
|
|
|
12333
12769
|
## Step 6: Connect + Prove Value
|
|
12334
12770
|
|
|
12335
12771
|
After capture:
|
|
12336
12772
|
1. Call \`graph action=suggest\` on 2\u20133 key entries (vision, architecture, a decision)
|
|
12337
|
-
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>"\`
|
|
12338
12774
|
3. Present what Product Brain now knows in that task context \u2014 this is the proof moment
|
|
12339
|
-
4.
|
|
12775
|
+
4. Show one clear result summary so the user sees exactly what was added
|
|
12340
12776
|
|
|
12341
12777
|
**Begin with Step 1 now.** Read the files, then report what you found before extracting.`
|
|
12342
12778
|
}
|
|
@@ -12344,6 +12780,108 @@ After capture:
|
|
|
12344
12780
|
};
|
|
12345
12781
|
}
|
|
12346
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
|
+
);
|
|
12347
12885
|
}
|
|
12348
12886
|
|
|
12349
12887
|
// src/server.ts
|
|
@@ -12438,6 +12976,7 @@ function createProductBrainServer() {
|
|
|
12438
12976
|
registerKnowledgeTools(server);
|
|
12439
12977
|
registerLabelTools(server);
|
|
12440
12978
|
registerHealthTools(server);
|
|
12979
|
+
registerOrientTool(server);
|
|
12441
12980
|
registerVerifyTools(server);
|
|
12442
12981
|
registerSmartCaptureTools(server);
|
|
12443
12982
|
registerQualityTools(server);
|
|
@@ -12457,4 +12996,4 @@ export {
|
|
|
12457
12996
|
SERVER_VERSION,
|
|
12458
12997
|
createProductBrainServer
|
|
12459
12998
|
};
|
|
12460
|
-
//# sourceMappingURL=chunk-
|
|
12999
|
+
//# sourceMappingURL=chunk-ZPEIKRYG.js.map
|