@productbrain/mcp 0.0.1-beta.111 → 0.0.1-beta.122
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-RQXM3TCI.js → chunk-2ZWGQ6PK.js} +18 -1
- package/dist/chunk-2ZWGQ6PK.js.map +1 -0
- package/dist/{chunk-DHBJFEAT.js → chunk-G4WJIWXX.js} +55 -24
- package/dist/chunk-G4WJIWXX.js.map +1 -0
- package/dist/{chunk-O337N4MC.js → chunk-ZFODPVNH.js} +950 -56
- package/dist/chunk-ZFODPVNH.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +8 -5
- package/dist/http.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{setup-GQ3LQS2L.js → setup-2JWIZ3QO.js} +3 -3
- package/dist/setup-2JWIZ3QO.js.map +1 -0
- package/dist/{smart-capture-ATJ5F7R2.js → smart-capture-CVZ5PLUZ.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-DHBJFEAT.js.map +0 -1
- package/dist/chunk-O337N4MC.js.map +0 -1
- package/dist/chunk-RQXM3TCI.js.map +0 -1
- package/dist/setup-GQ3LQS2L.js.map +0 -1
- /package/dist/{smart-capture-ATJ5F7R2.js.map → smart-capture-CVZ5PLUZ.js.map} +0 -0
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getWorkspaceContext,
|
|
21
21
|
getWorkspaceId,
|
|
22
22
|
initToolSurface,
|
|
23
|
+
isFeatureEnabled,
|
|
23
24
|
isSessionOriented,
|
|
24
25
|
mcpCall,
|
|
25
26
|
mcpMutation,
|
|
@@ -43,12 +44,13 @@ import {
|
|
|
43
44
|
unknownAction,
|
|
44
45
|
validationResult,
|
|
45
46
|
withEnvelope
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-G4WJIWXX.js";
|
|
47
48
|
import {
|
|
49
|
+
trackChainEntryCommitted,
|
|
48
50
|
trackKnowledgeGap,
|
|
49
51
|
trackQualityCheck,
|
|
50
52
|
trackQualityVerdict
|
|
51
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-2ZWGQ6PK.js";
|
|
52
54
|
|
|
53
55
|
// src/server.ts
|
|
54
56
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -96,7 +98,7 @@ var updateEntrySchema = z.object({
|
|
|
96
98
|
order: z.number().optional().describe("New sort order"),
|
|
97
99
|
canonicalKey: z.string().optional().describe("Semantic type (e.g. 'decision', 'tension'). Only changeable on draft/uncommitted entries."),
|
|
98
100
|
autoPublish: z.boolean().optional().default(false).describe("Only true when user explicitly asks to publish. Default false = draft. Never auto-publish without user confirmation."),
|
|
99
|
-
changeNote: z.string().optional().describe("
|
|
101
|
+
changeNote: z.string().optional().describe("Strongly recommended: short human-readable rationale for WHY this change was made (e.g. 'Aligned description with F1-themed copy per BET-238'). Surfaces in activity feed and pb get. If omitted, falls back to session purpose or auto-generated field summary.")
|
|
100
102
|
});
|
|
101
103
|
var getHistorySchema = z.object({
|
|
102
104
|
entryId: z.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001'")
|
|
@@ -271,7 +273,7 @@ ${formatted}` }],
|
|
|
271
273
|
},
|
|
272
274
|
withEnvelope(async ({ entryId }) => {
|
|
273
275
|
requireWriteAccess();
|
|
274
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
276
|
+
const { runContradictionCheck } = await import("./smart-capture-CVZ5PLUZ.js");
|
|
275
277
|
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
276
278
|
if (!entry) {
|
|
277
279
|
return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
|
|
@@ -299,6 +301,13 @@ ${formatted}` }],
|
|
|
299
301
|
const isProposal = result?.status === "proposal_created";
|
|
300
302
|
if (!isProposal) {
|
|
301
303
|
await recordSessionActivity({ entryModified: docId });
|
|
304
|
+
const coll = entry.collectionSlug ?? entry.collection?.slug;
|
|
305
|
+
trackChainEntryCommitted(wsCtx.workspaceId, {
|
|
306
|
+
entry_id: entry.entryId ?? entryId,
|
|
307
|
+
collection: coll,
|
|
308
|
+
commit_method: "manual",
|
|
309
|
+
surface: "mcp_commit_tool"
|
|
310
|
+
});
|
|
302
311
|
}
|
|
303
312
|
let lines;
|
|
304
313
|
if (isProposal) {
|
|
@@ -516,6 +525,9 @@ async function handleGet(entryId) {
|
|
|
516
525
|
`**Status:** ${e.status}${e.workflowStatus ? ` / ${e.workflowStatus}` : ""}`,
|
|
517
526
|
`**Type:** ${e.canonicalKey ?? "untyped"}`
|
|
518
527
|
];
|
|
528
|
+
if (e.origin) lines.push(`**Origin:** ${e.origin}`);
|
|
529
|
+
if (e.verificationStatus) lines.push(`**Verification:** ${e.verificationStatus}`);
|
|
530
|
+
if (e.sourceRef) lines.push(`**Source ref:** ${e.sourceRef}`);
|
|
519
531
|
if (epistemic) {
|
|
520
532
|
lines.push(formatEpistemicLine(epistemic));
|
|
521
533
|
}
|
|
@@ -556,6 +568,10 @@ async function handleGet(entryId) {
|
|
|
556
568
|
status: String(e.status ?? ""),
|
|
557
569
|
...e.workflowStatus ? { workflowStatus: String(e.workflowStatus) } : {},
|
|
558
570
|
...epistemic ? { epistemicStatus: epistemic } : {},
|
|
571
|
+
// BET-240 S6: trust/provenance fields
|
|
572
|
+
...e.origin ? { origin: String(e.origin) } : {},
|
|
573
|
+
...e.verificationStatus ? { verificationStatus: String(e.verificationStatus) } : {},
|
|
574
|
+
...e.sourceRef ? { sourceRef: String(e.sourceRef) } : {},
|
|
559
575
|
entries: [{ entryId: e.entryId, name: e.name, status: e.status, ...e.workflowStatus ? { workflowStatus: e.workflowStatus } : {}, collectionName: String(e.collectionName ?? e.canonicalKey ?? "\u2014") }],
|
|
560
576
|
...visibleData ? { data: visibleData } : {},
|
|
561
577
|
...Array.isArray(e.relations) && e.relations.length > 0 ? {
|
|
@@ -1317,39 +1333,53 @@ function epistemicCollectionHint(collectionName) {
|
|
|
1317
1333
|
if (lc === "assumptions") return " \u2014 _check evidence strength with `entries action=get`_";
|
|
1318
1334
|
return "";
|
|
1319
1335
|
}
|
|
1320
|
-
var CONTEXT_ACTIONS = ["gather", "build", "neighborhood"];
|
|
1336
|
+
var CONTEXT_ACTIONS = ["gather", "build", "neighborhood", "changes", "chain", "cross-cut", "incremental", "brief"];
|
|
1321
1337
|
var contextSchema = z5.object({
|
|
1322
1338
|
action: z5.enum(CONTEXT_ACTIONS).describe(
|
|
1323
|
-
"'gather': assemble knowledge context (entry graph, task auto-load, journey mode, or graph mode). 'build': structured build spec for an entry. 'neighborhood': typed graph neighborhood for an entry \u2014 blocking chain, dependencies, parent context, tensions, staleness (BET-142)."
|
|
1339
|
+
"'gather': assemble knowledge context (entry graph, task auto-load, journey mode, or graph mode). 'build': structured build spec for an entry. 'neighborhood': typed graph neighborhood for an entry \u2014 blocking chain, dependencies, parent context, tensions, staleness (BET-142). 'changes': entries modified and relations created since a timestamp (BET-239). Requires 'since' parameter. 'chain': directed traversal along one relation type to depth 4 (BET-239). Requires entryId. Optional: direction, relationType, maxHops (1-4). 'cross-cut': structural aggregation \u2014 all relations of a given type grouped by source collection (BET-239). Requires 'relationType' parameter. 'incremental': delta since last brief run for a skill (BET-239 E4). Requires 'skill' parameter. Returns only entries changed since the skill's last brief. 'brief': compound intelligence query (BET-239 E6). Requires 'briefType' parameter: 'steering' (changes + structure + delta + readiness), 'confidence' (changes + active bets + tensions), or 'delta' (changes + relations since timestamp). Optional 'since' for delta type."
|
|
1324
1340
|
),
|
|
1325
1341
|
entryId: z5.string().optional().describe("Entry ID for graph traversal or build, e.g. 'FEAT-001', 'GT-019'"),
|
|
1326
1342
|
mapEntryId: z5.string().optional().describe(
|
|
1327
1343
|
"Journey map entry ID for journey-aware context (gather only). Returns context organised by journey stage. Takes precedence over entryId when both are supplied. Example: 'MAP-1'."
|
|
1328
1344
|
),
|
|
1329
1345
|
task: z5.string().optional().describe("Natural-language task description for auto-loading relevant context (gather only)"),
|
|
1346
|
+
since: z5.string().optional().describe(
|
|
1347
|
+
"ISO 8601 timestamp for 'changes' action \u2014 returns entries/relations modified since this time. Example: '2026-03-24T00:00:00Z'."
|
|
1348
|
+
),
|
|
1349
|
+
direction: z5.enum(["outgoing", "incoming"]).default("outgoing").optional().describe("For 'chain' action: traversal direction. 'outgoing' follows relations from source, 'incoming' follows relations to source. Default: outgoing."),
|
|
1350
|
+
relationType: z5.string().optional().describe(
|
|
1351
|
+
"Relation type filter. For 'chain': optional filter to traverse only this relation type. For 'cross-cut': required \u2014 scans all relations of this type across the workspace. Examples: 'part_of', 'informs', 'governs', 'blocks', 'depends_on'."
|
|
1352
|
+
),
|
|
1330
1353
|
mode: z5.enum(["search", "graph"]).default("search").optional().describe("For gather: 'search' (default) or 'graph' (enhanced with provenance paths). Ignored when mapEntryId is provided."),
|
|
1331
|
-
maxHops: z5.number().min(1).max(
|
|
1332
|
-
maxResults: z5.number().min(1).max(25).default(10).optional().describe("Max entries to return in gather task mode (default 10)")
|
|
1354
|
+
maxHops: z5.number().min(1).max(4).default(2).describe("Relation traversal depth (1=direct only, 2=default, 3=wide net, 4=deep chain walk)"),
|
|
1355
|
+
maxResults: z5.number().min(1).max(25).default(10).optional().describe("Max entries to return in gather task mode (default 10)"),
|
|
1356
|
+
strategy: z5.enum(["hybrid", "keyword"]).default("hybrid").optional().describe("Seed strategy for task-based gather: 'hybrid' (vector + FTS, default) or 'keyword' (FTS only). Only affects task mode."),
|
|
1357
|
+
skill: z5.string().optional().describe(
|
|
1358
|
+
"Skill name for 'incremental' action \u2014 identifies which skill's brief history to compare against. Examples: 'preflight', 'shaping', 'review'. Required when action is 'incremental'."
|
|
1359
|
+
),
|
|
1360
|
+
briefType: z5.enum(["steering", "confidence", "delta"]).optional().describe(
|
|
1361
|
+
"Compound query type for 'brief' action. 'steering': 7d changes + structural aggregation (part_of, depends_on, constrains) + incremental delta + workspace readiness. 'confidence': 7d changes + active bets summary + active tensions breakdown. 'delta': changes + relations since a custom timestamp (use 'since' param). Required when action is 'brief'."
|
|
1362
|
+
)
|
|
1333
1363
|
});
|
|
1334
1364
|
function registerContextTools(server) {
|
|
1335
1365
|
server.registerTool(
|
|
1336
1366
|
"context",
|
|
1337
1367
|
{
|
|
1338
1368
|
title: "Context",
|
|
1339
|
-
description: "Assemble knowledge context in one call.
|
|
1369
|
+
description: "Assemble knowledge context in one call. Six actions:\n\n- **gather**: Three modes \u2014 (1) By entry: traverse the graph around a specific entry. (2) By task: auto-load relevant domain knowledge for a natural-language task. (3) Graph mode (entryId + mode='graph'): enhanced traversal with provenance paths.\n- **build**: Structured build spec for any entry \u2014 data, related entries, business rules, glossary terms, chain refs. Use when starting a build. Requires active session.\n- **neighborhood**: Typed graph neighborhood \u2014 blocking chain (what this blocks, 3 hops), dependency chain (what blocks this), parent context (sibling count), related tensions, staleness signal, isolation signal. Fast indexed lookups. Requires entryId.\n- **changes**: Detect entries modified and relations created since a timestamp. Use for incremental updates instead of full traversal. Requires 'since' (ISO 8601). Returns grouped counts by collection. Cap: 200 entries. (BET-239)\n- **chain**: Directed deep traversal along one relation type to depth 4 (BET-239). Use for dependency analysis, governance chains, strategic alignment. Requires entryId. Optional: direction (outgoing/incoming), relationType, maxHops (1-4). Cycle detection, fan-out cap 10/hop, total cap 100 nodes. Returns tree + edges.\n- **cross-cut**: Structural aggregation \u2014 all relations of a given type across the workspace, grouped by source collection. Use to answer 'show me everything connected by part_of'. Requires 'relationType'. Cap: 500 entries. (BET-239)\n- **incremental**: Delta since last brief run for a skill. Returns only entries changed since the skill's last brief, filtering out previously surfaced entries. Requires 'skill' parameter. Use for steering briefs that need incremental awareness. (BET-239 E4)\n- **brief**: Compound intelligence query composing multiple primitives. Requires 'briefType': 'steering' (7d changes + structure + delta + readiness), 'confidence' (7d changes + active bets + tensions), or 'delta' (changes since custom timestamp). Auto-records brief run for steering. (BET-239 E6)",
|
|
1340
1370
|
inputSchema: contextSchema,
|
|
1341
1371
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
1342
1372
|
},
|
|
1343
1373
|
withEnvelope(async (args) => {
|
|
1344
1374
|
const parsed = parseOrFail(contextSchema, args);
|
|
1345
1375
|
if (!parsed.ok) return parsed.result;
|
|
1346
|
-
const { action, entryId, mapEntryId, task, mode, maxHops, maxResults } = parsed.data;
|
|
1376
|
+
const { action, entryId, mapEntryId, task, since, direction, relationType, mode, maxHops, maxResults, strategy, skill, briefType } = parsed.data;
|
|
1347
1377
|
return runWithToolContext({ tool: "context", action }, async () => {
|
|
1348
1378
|
if (action === "gather") {
|
|
1349
1379
|
if (mapEntryId) {
|
|
1350
1380
|
return handleJourneyGather(server, mapEntryId, maxHops ?? 2, maxResults ?? 20);
|
|
1351
1381
|
}
|
|
1352
|
-
return handleGather(server, entryId, task, mode ?? "search", maxHops ?? 2, maxResults ?? 10);
|
|
1382
|
+
return handleGather(server, entryId, task, mode ?? "search", maxHops ?? 2, maxResults ?? 10, strategy ?? "keyword");
|
|
1353
1383
|
}
|
|
1354
1384
|
if (action === "build") {
|
|
1355
1385
|
if (!entryId) {
|
|
@@ -1363,12 +1393,46 @@ function registerContextTools(server) {
|
|
|
1363
1393
|
}
|
|
1364
1394
|
return handleNeighborhood(entryId);
|
|
1365
1395
|
}
|
|
1396
|
+
if (action === "changes") {
|
|
1397
|
+
if (!since) {
|
|
1398
|
+
return validationResult("'since' (ISO 8601 timestamp) is required when action is 'changes'.");
|
|
1399
|
+
}
|
|
1400
|
+
return handleChanges(since);
|
|
1401
|
+
}
|
|
1402
|
+
if (action === "chain") {
|
|
1403
|
+
if (!entryId) {
|
|
1404
|
+
return validationResult("entryId is required when action is 'chain'.");
|
|
1405
|
+
}
|
|
1406
|
+
return handleChainWalk(entryId, direction ?? "outgoing", maxHops ?? 2, relationType);
|
|
1407
|
+
}
|
|
1408
|
+
if (action === "cross-cut") {
|
|
1409
|
+
if (!relationType) {
|
|
1410
|
+
return validationResult("'relationType' is required when action is 'cross-cut'. Examples: 'part_of', 'informs', 'governs'.");
|
|
1411
|
+
}
|
|
1412
|
+
return handleCrossCut(relationType);
|
|
1413
|
+
}
|
|
1414
|
+
if (action === "incremental") {
|
|
1415
|
+
if (!skill) {
|
|
1416
|
+
return validationResult("'skill' is required when action is 'incremental'. Examples: 'preflight', 'shaping', 'review'.");
|
|
1417
|
+
}
|
|
1418
|
+
return handleIncremental(skill);
|
|
1419
|
+
}
|
|
1420
|
+
if (action === "brief") {
|
|
1421
|
+
if (!briefType) {
|
|
1422
|
+
return validationResult("'briefType' is required when action is 'brief'. Options: 'steering', 'confidence', 'delta'.");
|
|
1423
|
+
}
|
|
1424
|
+
const sinceMs = briefType === "delta" && since ? Date.parse(since) : void 0;
|
|
1425
|
+
if (briefType === "delta" && since && (sinceMs === void 0 || isNaN(sinceMs))) {
|
|
1426
|
+
return validationResult(`Invalid 'since' timestamp: '${since}'. Use ISO 8601 format (e.g. '2026-03-24T00:00:00Z').`);
|
|
1427
|
+
}
|
|
1428
|
+
return handleBrief(briefType, sinceMs);
|
|
1429
|
+
}
|
|
1366
1430
|
return unknownAction(action, CONTEXT_ACTIONS);
|
|
1367
1431
|
});
|
|
1368
1432
|
})
|
|
1369
1433
|
);
|
|
1370
1434
|
}
|
|
1371
|
-
async function handleGather(server, entryId, task, mode, maxHops, maxResults) {
|
|
1435
|
+
async function handleGather(server, entryId, task, mode, maxHops, maxResults, strategy = "hybrid") {
|
|
1372
1436
|
if (!entryId && !task) {
|
|
1373
1437
|
return validationResult("Provide either entryId (graph traversal) or task (auto-load context).");
|
|
1374
1438
|
}
|
|
@@ -1426,11 +1490,12 @@ Use \`graph action=suggest\` to discover potential connections.`
|
|
|
1426
1490
|
const hopLabel = e.hop > 1 ? ` (hop ${e.hop})` : "";
|
|
1427
1491
|
const scoreLabel = typeof e.score === "number" ? ` ${e.score}` : "";
|
|
1428
1492
|
const flagsLabel = e.scoreFlags?.length ? ` [${e.scoreFlags.join(", ")}]` : "";
|
|
1493
|
+
const originLabel = e.origin ? ` (origin: ${e.origin})` : "";
|
|
1429
1494
|
const provenancePath = e.provenance && e.provenance.length > 1 ? `
|
|
1430
1495
|
_Path: ${e.provenance.map((p) => `${p.entryId ?? p.name} [${p.relationType}]`).join(" \u2192 ")}_` : "";
|
|
1431
1496
|
const preview = e.preview ? `
|
|
1432
1497
|
${e.preview.substring(0, 120)}` : "";
|
|
1433
|
-
lines2.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}${scoreLabel}${flagsLabel}${provenancePath}${preview}`);
|
|
1498
|
+
lines2.push(`- ${arrow} **${e.relationType}** ${id}${e.name}${hopLabel}${scoreLabel}${flagsLabel}${originLabel}${provenancePath}${preview}`);
|
|
1434
1499
|
}
|
|
1435
1500
|
lines2.push("");
|
|
1436
1501
|
}
|
|
@@ -1457,12 +1522,17 @@ Use \`graph action=suggest\` to discover potential connections.`
|
|
|
1457
1522
|
};
|
|
1458
1523
|
}
|
|
1459
1524
|
if (task && !entryId) {
|
|
1525
|
+
const strategyLabel = strategy === "hybrid" ? "hybrid (vector + FTS)" : "keyword (FTS only)";
|
|
1460
1526
|
await server.sendLoggingMessage({
|
|
1461
1527
|
level: "info",
|
|
1462
|
-
data: `Loading context for task: "${task.substring(0, 80)}..."`,
|
|
1528
|
+
data: `Loading context for task: "${task.substring(0, 80)}..." [strategy: ${strategyLabel}]`,
|
|
1463
1529
|
logger: "product-brain"
|
|
1464
1530
|
});
|
|
1465
|
-
const result2 = await mcpQuery("chain.
|
|
1531
|
+
const result2 = strategy === "hybrid" ? await mcpQuery("chain.taskAwareHybridGatherContext", {
|
|
1532
|
+
task,
|
|
1533
|
+
maxHops,
|
|
1534
|
+
maxResults
|
|
1535
|
+
}) : await mcpQuery("chain.taskAwareGatherContext", {
|
|
1466
1536
|
task,
|
|
1467
1537
|
maxHops,
|
|
1468
1538
|
maxResults
|
|
@@ -1499,11 +1569,16 @@ _This helps future sessions start with better context._`
|
|
|
1499
1569
|
if (!byCollection2.has(key)) byCollection2.set(key, []);
|
|
1500
1570
|
byCollection2.get(key).push(entry);
|
|
1501
1571
|
}
|
|
1572
|
+
const seedDisplay = result2.seeds?.map((s) => {
|
|
1573
|
+
const id = s.entryId ?? s.name;
|
|
1574
|
+
return s.source ? `${id} [${s.source}]` : id;
|
|
1575
|
+
}).join(", ") ?? "none";
|
|
1502
1576
|
const lines2 = [
|
|
1503
1577
|
`# Context Loaded`,
|
|
1504
1578
|
`**Confidence:** ${result2.confidence.charAt(0).toUpperCase() + result2.confidence.slice(1)}`,
|
|
1579
|
+
`**Strategy:** ${strategyLabel}`,
|
|
1505
1580
|
`**Matched:** ${result2.totalFound} entries across ${byCollection2.size} collection${byCollection2.size === 1 ? "" : "s"}`,
|
|
1506
|
-
`**Seeds:** ${
|
|
1581
|
+
`**Seeds:** ${seedDisplay}`,
|
|
1507
1582
|
""
|
|
1508
1583
|
];
|
|
1509
1584
|
for (const [collName, entries] of byCollection2) {
|
|
@@ -1513,8 +1588,9 @@ _This helps future sessions start with better context._`
|
|
|
1513
1588
|
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
1514
1589
|
const scoreLabel = typeof e.score === "number" ? ` ${e.score}` : "";
|
|
1515
1590
|
const flagsLabel = e.scoreFlags?.length ? ` [${e.scoreFlags.join(", ")}]` : "";
|
|
1591
|
+
const originLabel = e.origin ? ` (origin: ${e.origin})` : "";
|
|
1516
1592
|
const hopLabel = e.hop > 0 ? ` _(hop ${e.hop}, ${arrow} ${e.relationType})_` : "";
|
|
1517
|
-
lines2.push(`- ${id}${e.name}${scoreLabel}${flagsLabel}${hopLabel}`);
|
|
1593
|
+
lines2.push(`- ${id}${e.name}${scoreLabel}${flagsLabel}${originLabel}${hopLabel}`);
|
|
1518
1594
|
}
|
|
1519
1595
|
lines2.push("");
|
|
1520
1596
|
}
|
|
@@ -1527,8 +1603,8 @@ _This helps future sessions start with better context._`
|
|
|
1527
1603
|
return {
|
|
1528
1604
|
content: [{ type: "text", text: lines2.join("\n") }],
|
|
1529
1605
|
structuredContent: success(
|
|
1530
|
-
`Loaded ${result2.totalFound} entries for task (${result2.confidence} confidence).`,
|
|
1531
|
-
{ entries: taskEntries, totalFound: result2.totalFound, confidence: result2.confidence }
|
|
1606
|
+
`Loaded ${result2.totalFound} entries for task (${result2.confidence} confidence, ${strategy} strategy).`,
|
|
1607
|
+
{ entries: taskEntries, totalFound: result2.totalFound, confidence: result2.confidence, strategy }
|
|
1532
1608
|
)
|
|
1533
1609
|
};
|
|
1534
1610
|
}
|
|
@@ -1827,6 +1903,408 @@ async function handleBuild(server, entryId, maxHops) {
|
|
|
1827
1903
|
)
|
|
1828
1904
|
};
|
|
1829
1905
|
}
|
|
1906
|
+
async function handleCrossCut(relationType) {
|
|
1907
|
+
const result = await mcpQuery("chain.structuralAggregation", {
|
|
1908
|
+
relationType
|
|
1909
|
+
});
|
|
1910
|
+
const groupKeys = Object.keys(result.groups);
|
|
1911
|
+
if (result.totalCount === 0) {
|
|
1912
|
+
return {
|
|
1913
|
+
content: [{
|
|
1914
|
+
type: "text",
|
|
1915
|
+
text: `# Cross-Cut: ${relationType}
|
|
1916
|
+
|
|
1917
|
+
No relations of type '${relationType}' found in this workspace.`
|
|
1918
|
+
}],
|
|
1919
|
+
structuredContent: success(
|
|
1920
|
+
`No '${relationType}' relations found.`,
|
|
1921
|
+
{ groups: {}, totalCount: 0, truncated: false }
|
|
1922
|
+
)
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
const lines = [
|
|
1926
|
+
`# Cross-Cut: ${relationType}`,
|
|
1927
|
+
`_${result.totalCount} relations across ${groupKeys.length} source collection${groupKeys.length === 1 ? "" : "s"}${result.truncated ? " (TRUNCATED \u2014 more relations exist)" : ""}_`,
|
|
1928
|
+
""
|
|
1929
|
+
];
|
|
1930
|
+
for (const [sourceSlug, items] of Object.entries(result.groups)) {
|
|
1931
|
+
lines.push(`## ${sourceSlug} (${items.length})`);
|
|
1932
|
+
for (const item of items) {
|
|
1933
|
+
const fromId = item.entryId ? `**${item.entryId}:** ` : "";
|
|
1934
|
+
const toId = item.relatedEntryId ?? item.relatedName;
|
|
1935
|
+
lines.push(`- ${fromId}${item.name} \u2192 ${toId} [${item.relatedCollectionSlug}]`);
|
|
1936
|
+
}
|
|
1937
|
+
lines.push("");
|
|
1938
|
+
}
|
|
1939
|
+
if (result.truncated) {
|
|
1940
|
+
lines.push("_Results truncated at 500 relations. Filter by collection or use a more specific relation type._");
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1944
|
+
structuredContent: success(
|
|
1945
|
+
`${result.totalCount} '${relationType}' relations across ${groupKeys.length} collections.${result.truncated ? " Truncated." : ""}`,
|
|
1946
|
+
result
|
|
1947
|
+
)
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
async function handleChainWalk(entryId, direction, maxHops, relationType) {
|
|
1951
|
+
const clampedHops = Math.max(1, Math.min(4, maxHops));
|
|
1952
|
+
const result = await mcpQuery("chain.deepChainWalk", {
|
|
1953
|
+
entryId,
|
|
1954
|
+
direction,
|
|
1955
|
+
maxHops: clampedHops,
|
|
1956
|
+
...relationType ? { relationType } : {}
|
|
1957
|
+
});
|
|
1958
|
+
if (result.error) {
|
|
1959
|
+
return notFoundResult(entryId, result.error);
|
|
1960
|
+
}
|
|
1961
|
+
if (result.totalNodes <= 1) {
|
|
1962
|
+
return {
|
|
1963
|
+
content: [{
|
|
1964
|
+
type: "text",
|
|
1965
|
+
text: `# Chain Walk: ${entryId}
|
|
1966
|
+
|
|
1967
|
+
_No ${direction} relations found${relationType ? ` of type '${relationType}'` : ""}._
|
|
1968
|
+
|
|
1969
|
+
Try \`context action=chain entryId="${entryId}" direction="${direction === "outgoing" ? "incoming" : "outgoing"}"\` or remove the relationType filter.`
|
|
1970
|
+
}],
|
|
1971
|
+
structuredContent: success(
|
|
1972
|
+
`No ${direction} relations found for ${entryId}.`,
|
|
1973
|
+
result
|
|
1974
|
+
)
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
const lines = [
|
|
1978
|
+
`# Chain Walk: ${entryId}`,
|
|
1979
|
+
`**Direction:** ${direction} | **Depth:** ${clampedHops}${relationType ? ` | **Type filter:** ${relationType}` : ""}`,
|
|
1980
|
+
`**Nodes:** ${result.totalNodes}${result.truncated ? " (TRUNCATED at 100)" : ""} | **Edges:** ${result.edges.length} | **Cycles:** ${result.cycles.length}`,
|
|
1981
|
+
""
|
|
1982
|
+
];
|
|
1983
|
+
const maxDepth = Math.max(...result.nodes.map((n) => n.depth));
|
|
1984
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
1985
|
+
const atDepth = result.nodes.filter((n) => n.depth === d);
|
|
1986
|
+
if (atDepth.length === 0) continue;
|
|
1987
|
+
const label = d === 0 ? "Root" : `Depth ${d}`;
|
|
1988
|
+
lines.push(`## ${label} (${atDepth.length})`);
|
|
1989
|
+
for (const node of atDepth) {
|
|
1990
|
+
const indent = " ".repeat(d);
|
|
1991
|
+
const id = node.entryId ? `**${node.entryId}:** ` : "";
|
|
1992
|
+
const collection = node.collectionSlug ? ` [${node.collectionSlug}]` : "";
|
|
1993
|
+
const parent = node.parentEntryId ? ` (via ${node.parentEntryId})` : "";
|
|
1994
|
+
lines.push(`${indent}- ${id}${node.name}${collection}${parent}`);
|
|
1995
|
+
}
|
|
1996
|
+
lines.push("");
|
|
1997
|
+
}
|
|
1998
|
+
if (result.cycles.length > 0) {
|
|
1999
|
+
lines.push("## Cycles Detected");
|
|
2000
|
+
for (const cycle of result.cycles) {
|
|
2001
|
+
lines.push(`- ${cycle.path.join(" -> ")}`);
|
|
2002
|
+
}
|
|
2003
|
+
lines.push("");
|
|
2004
|
+
}
|
|
2005
|
+
if (result.snapshotWarning) {
|
|
2006
|
+
lines.push("_Note: This traversal spans multiple reads (Convex action). Data consistency is not guaranteed across hops (TEN-1029)._");
|
|
2007
|
+
}
|
|
2008
|
+
if (result.truncated) {
|
|
2009
|
+
lines.push("_Results truncated at 100 nodes. Narrow with relationType filter or reduce maxHops._");
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2013
|
+
structuredContent: success(
|
|
2014
|
+
`Chain walk for ${entryId}: ${result.totalNodes} nodes, ${result.edges.length} edges, ${result.cycles.length} cycles (${direction}, depth ${clampedHops}).`,
|
|
2015
|
+
result
|
|
2016
|
+
)
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
async function handleChanges(since) {
|
|
2020
|
+
const sinceMs = Date.parse(since);
|
|
2021
|
+
if (isNaN(sinceMs)) {
|
|
2022
|
+
return validationResult(`Invalid 'since' timestamp: '${since}'. Use ISO 8601 format (e.g. '2026-03-24T00:00:00Z').`);
|
|
2023
|
+
}
|
|
2024
|
+
const result = await mcpQuery("chain.changeDetection", { since: sinceMs });
|
|
2025
|
+
const totalEntries = result.entries.length;
|
|
2026
|
+
const totalRelations = result.relations.length;
|
|
2027
|
+
if (totalEntries === 0 && totalRelations === 0) {
|
|
2028
|
+
return {
|
|
2029
|
+
content: [{
|
|
2030
|
+
type: "text",
|
|
2031
|
+
text: `# Changes since ${since}
|
|
2032
|
+
|
|
2033
|
+
No entries modified or relations created since this timestamp.`
|
|
2034
|
+
}],
|
|
2035
|
+
structuredContent: success(
|
|
2036
|
+
`No changes detected since ${since}.`,
|
|
2037
|
+
{ entries: [], relations: [], counts: {}, truncated: false }
|
|
2038
|
+
)
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
const lines = [
|
|
2042
|
+
`# Changes since ${since}`,
|
|
2043
|
+
`_${totalEntries} entries modified, ${totalRelations} relations created${result.truncated ? " (TRUNCATED \u2014 more changes exist)" : ""}_`,
|
|
2044
|
+
""
|
|
2045
|
+
];
|
|
2046
|
+
if (totalEntries > 0) {
|
|
2047
|
+
lines.push("## Modified Entries");
|
|
2048
|
+
const sortedCounts = Object.entries(result.counts).sort((a, b) => b[1] - a[1]);
|
|
2049
|
+
for (const [slug, count] of sortedCounts) {
|
|
2050
|
+
lines.push(`### ${slug} (${count})`);
|
|
2051
|
+
const slugEntries = result.entries.filter((e) => e.collectionSlug === slug);
|
|
2052
|
+
for (const e of slugEntries) {
|
|
2053
|
+
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
2054
|
+
const ago = formatTimeAgo(e.updatedAt);
|
|
2055
|
+
lines.push(`- ${id}${e.name} _(${ago})_`);
|
|
2056
|
+
}
|
|
2057
|
+
lines.push("");
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (totalRelations > 0) {
|
|
2061
|
+
lines.push("## New Relations");
|
|
2062
|
+
for (const r of result.relations) {
|
|
2063
|
+
const from = r.fromEntryId ?? "?";
|
|
2064
|
+
const to = r.toEntryId ?? "?";
|
|
2065
|
+
const ago = formatTimeAgo(r.createdAt);
|
|
2066
|
+
lines.push(`- ${from} **${r.type}** ${to} _(${ago})_`);
|
|
2067
|
+
}
|
|
2068
|
+
lines.push("");
|
|
2069
|
+
}
|
|
2070
|
+
if (result.truncated) {
|
|
2071
|
+
lines.push("_Results truncated at 200 entries. Use a more recent timestamp to narrow the window._");
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2075
|
+
structuredContent: success(
|
|
2076
|
+
`${totalEntries} entries modified, ${totalRelations} relations created since ${since}.${result.truncated ? " Truncated." : ""}`,
|
|
2077
|
+
result
|
|
2078
|
+
)
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
async function handleIncremental(skill) {
|
|
2082
|
+
const result = await mcpQuery("chain.incrementalChanges", { skill });
|
|
2083
|
+
const totalEntries = result.newEntries.length;
|
|
2084
|
+
const surfacedIds = result.newEntries.map((e) => e.entryId).filter((id) => id !== null);
|
|
2085
|
+
mcpMutation("chain.recordBriefRun", {
|
|
2086
|
+
skill,
|
|
2087
|
+
entriesSurfaced: surfacedIds,
|
|
2088
|
+
timestamp: Date.now()
|
|
2089
|
+
}).catch(() => {
|
|
2090
|
+
});
|
|
2091
|
+
if (totalEntries === 0 && !result.isFirstRun) {
|
|
2092
|
+
return {
|
|
2093
|
+
content: [{
|
|
2094
|
+
type: "text",
|
|
2095
|
+
text: `# Incremental Brief \u2014 ${skill}
|
|
2096
|
+
|
|
2097
|
+
No new entries since last brief run (${formatTimeAgo(result.previousRunTimestamp)}).`
|
|
2098
|
+
}],
|
|
2099
|
+
structuredContent: success(
|
|
2100
|
+
`No new entries for skill '${skill}' since last brief.`,
|
|
2101
|
+
result
|
|
2102
|
+
)
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
const lines = [
|
|
2106
|
+
`# Incremental Brief \u2014 ${skill}`
|
|
2107
|
+
];
|
|
2108
|
+
if (result.isFirstRun) {
|
|
2109
|
+
lines.push("_First run \u2014 showing all recent entries._");
|
|
2110
|
+
} else {
|
|
2111
|
+
lines.push(`_Delta since last brief (${formatTimeAgo(result.previousRunTimestamp)})_`);
|
|
2112
|
+
}
|
|
2113
|
+
lines.push(`_${totalEntries} new entries${result.truncated ? " (TRUNCATED)" : ""}_`);
|
|
2114
|
+
lines.push("");
|
|
2115
|
+
if (totalEntries > 0) {
|
|
2116
|
+
lines.push("## New Entries");
|
|
2117
|
+
const byCollection = {};
|
|
2118
|
+
for (const e of result.newEntries) {
|
|
2119
|
+
const slug = e.collectionSlug;
|
|
2120
|
+
if (!byCollection[slug]) byCollection[slug] = [];
|
|
2121
|
+
byCollection[slug].push(e);
|
|
2122
|
+
}
|
|
2123
|
+
const sortedSlugs = Object.entries(byCollection).sort((a, b) => b[1].length - a[1].length).map(([slug]) => slug);
|
|
2124
|
+
for (const slug of sortedSlugs) {
|
|
2125
|
+
const entries = byCollection[slug];
|
|
2126
|
+
lines.push(`### ${slug} (${entries.length})`);
|
|
2127
|
+
for (const e of entries) {
|
|
2128
|
+
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
2129
|
+
const ago = formatTimeAgo(e.updatedAt);
|
|
2130
|
+
lines.push(`- ${id}${e.name} _(${ago})_`);
|
|
2131
|
+
}
|
|
2132
|
+
lines.push("");
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (result.truncated) {
|
|
2136
|
+
lines.push("_Results truncated at 200 entries. Run more frequently to stay within the cap._");
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2140
|
+
structuredContent: success(
|
|
2141
|
+
`${totalEntries} new entries for skill '${skill}'${result.isFirstRun ? " (first run)" : ""}.${result.truncated ? " Truncated." : ""}`,
|
|
2142
|
+
result
|
|
2143
|
+
)
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
async function handleBrief(briefType, sinceMs) {
|
|
2147
|
+
const args = { type: briefType };
|
|
2148
|
+
if (sinceMs !== void 0) args.since = sinceMs;
|
|
2149
|
+
const result = await mcpQuery("chain.compoundQuery", args);
|
|
2150
|
+
if (briefType === "steering") {
|
|
2151
|
+
return formatSteeringBrief(result);
|
|
2152
|
+
}
|
|
2153
|
+
if (briefType === "confidence") {
|
|
2154
|
+
return formatConfidencePass(result);
|
|
2155
|
+
}
|
|
2156
|
+
if (briefType === "delta") {
|
|
2157
|
+
return formatDeltaSync(result);
|
|
2158
|
+
}
|
|
2159
|
+
return validationResult(`Unknown briefType: ${briefType}`);
|
|
2160
|
+
}
|
|
2161
|
+
function formatSteeringBrief(result) {
|
|
2162
|
+
const changes = result.changes;
|
|
2163
|
+
const delta = result.delta;
|
|
2164
|
+
const readiness = result.readiness;
|
|
2165
|
+
const lines = [
|
|
2166
|
+
"# Steering Brief",
|
|
2167
|
+
"",
|
|
2168
|
+
`## Changes (last 7 days)`,
|
|
2169
|
+
`_${changes.entries.length} entries modified, ${changes.relations.length} relations created${changes.truncated ? " (TRUNCATED)" : ""}_`,
|
|
2170
|
+
""
|
|
2171
|
+
];
|
|
2172
|
+
if (Object.keys(changes.counts).length > 0) {
|
|
2173
|
+
const sortedCounts = Object.entries(changes.counts).sort((a, b) => b[1] - a[1]);
|
|
2174
|
+
for (const [slug, count] of sortedCounts) {
|
|
2175
|
+
lines.push(`- **${slug}:** ${count}`);
|
|
2176
|
+
}
|
|
2177
|
+
lines.push("");
|
|
2178
|
+
}
|
|
2179
|
+
lines.push("## Structure");
|
|
2180
|
+
for (const [relType, agg] of Object.entries(result.structure)) {
|
|
2181
|
+
const groupCount = Object.keys(agg.groups).length;
|
|
2182
|
+
lines.push(`- **${relType}:** ${agg.totalCount} relations across ${groupCount} collection groups${agg.truncated ? " (TRUNCATED)" : ""}`);
|
|
2183
|
+
}
|
|
2184
|
+
lines.push("");
|
|
2185
|
+
lines.push("## Incremental Delta (steering skill)");
|
|
2186
|
+
if (delta.isFirstRun) {
|
|
2187
|
+
lines.push("_First run \u2014 all recent entries included._");
|
|
2188
|
+
}
|
|
2189
|
+
lines.push(`${delta.newEntries.length} new entries since last steering brief${delta.truncated ? " (TRUNCATED)" : ""}`);
|
|
2190
|
+
lines.push("");
|
|
2191
|
+
lines.push("## Workspace Readiness");
|
|
2192
|
+
lines.push(`Score: **${readiness.score}%** (${readiness.passedChecks}/${readiness.totalChecks} checks passed)`);
|
|
2193
|
+
if (readiness.gaps.length > 0) {
|
|
2194
|
+
lines.push("");
|
|
2195
|
+
lines.push("**Gaps:**");
|
|
2196
|
+
for (const gap of readiness.gaps.slice(0, 5)) {
|
|
2197
|
+
lines.push(`- ${gap.label}: ${gap.current}/${gap.required}${gap.guidance ? ` \u2014 ${gap.guidance}` : ""}`);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
lines.push("");
|
|
2201
|
+
lines.push("## Trust");
|
|
2202
|
+
lines.push("_Scoring kernel applies trust weighting: human=1.0, agent+verified=0.7, agent+unverified=0.56, external+unverified=0.64. Scores in context/gather reflect this. Use `orient -b` for workspace trust metrics._");
|
|
2203
|
+
lines.push("");
|
|
2204
|
+
if (result.snapshotWarning) {
|
|
2205
|
+
lines.push("_Note: Compound query spans multiple reads. Data consistency is not guaranteed across components (TEN-1029)._");
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2209
|
+
structuredContent: success(
|
|
2210
|
+
`Steering brief: ${changes.entries.length} changes, ${delta.newEntries.length} new entries, ${readiness.score}% readiness.`,
|
|
2211
|
+
{ ...result, trustFilterActive: true }
|
|
2212
|
+
)
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function formatConfidencePass(result) {
|
|
2216
|
+
const changes = result.changes;
|
|
2217
|
+
const lines = [
|
|
2218
|
+
"# Confidence Pass",
|
|
2219
|
+
"",
|
|
2220
|
+
`## Changes (last 7 days)`,
|
|
2221
|
+
`_${changes.entries.length} entries modified${changes.truncated ? " (TRUNCATED)" : ""}_`,
|
|
2222
|
+
""
|
|
2223
|
+
];
|
|
2224
|
+
lines.push(`## Active Bets (${result.betCount})`);
|
|
2225
|
+
if (result.activeBets.length > 0) {
|
|
2226
|
+
for (const bet of result.activeBets) {
|
|
2227
|
+
const id = bet.entryId ? `**${bet.entryId}:** ` : "";
|
|
2228
|
+
lines.push(`- ${id}${bet.name} [${bet.status}]`);
|
|
2229
|
+
}
|
|
2230
|
+
} else {
|
|
2231
|
+
lines.push("_No active bets._");
|
|
2232
|
+
}
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
lines.push(`## Active Tensions (${result.tensionCount})`);
|
|
2235
|
+
if (result.activeTensions.length > 0) {
|
|
2236
|
+
for (const tension of result.activeTensions) {
|
|
2237
|
+
const id = tension.entryId ? `**${tension.entryId}:** ` : "";
|
|
2238
|
+
lines.push(`- ${id}${tension.name}`);
|
|
2239
|
+
}
|
|
2240
|
+
} else {
|
|
2241
|
+
lines.push("_No active tensions._");
|
|
2242
|
+
}
|
|
2243
|
+
lines.push("");
|
|
2244
|
+
lines.push("## Trust");
|
|
2245
|
+
lines.push("_Trust-weighted scoring active: agent-origin and unverified entries receive lower confidence scores (human=1.0, agent+verified=0.7, agent+unverified=0.56). Use `entries action=get` to inspect individual entry origin/verification. Use `orient -b` for workspace trust metrics._");
|
|
2246
|
+
lines.push("");
|
|
2247
|
+
if (result.snapshotWarning) {
|
|
2248
|
+
lines.push("_Note: Compound query spans multiple reads. Data consistency is not guaranteed across components (TEN-1029)._");
|
|
2249
|
+
}
|
|
2250
|
+
return {
|
|
2251
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2252
|
+
structuredContent: success(
|
|
2253
|
+
`Confidence pass: ${changes.entries.length} changes, ${result.betCount} active bets, ${result.tensionCount} active tensions.`,
|
|
2254
|
+
{ ...result, trustFilterActive: true }
|
|
2255
|
+
)
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function formatDeltaSync(result) {
|
|
2259
|
+
const changes = result.changes;
|
|
2260
|
+
const sinceDate = new Date(result.since).toISOString();
|
|
2261
|
+
const lines = [
|
|
2262
|
+
`# Delta Sync since ${sinceDate}`,
|
|
2263
|
+
`_${changes.entries.length} entries modified, ${changes.relations.length} relations created${changes.truncated ? " (TRUNCATED)" : ""}_`,
|
|
2264
|
+
""
|
|
2265
|
+
];
|
|
2266
|
+
if (changes.entries.length > 0) {
|
|
2267
|
+
lines.push("## Modified Entries");
|
|
2268
|
+
const sortedCounts = Object.entries(changes.counts).sort((a, b) => b[1] - a[1]);
|
|
2269
|
+
for (const [slug, count] of sortedCounts) {
|
|
2270
|
+
lines.push(`### ${slug} (${count})`);
|
|
2271
|
+
const slugEntries = changes.entries.filter((e) => e.collectionSlug === slug);
|
|
2272
|
+
for (const e of slugEntries) {
|
|
2273
|
+
const id = e.entryId ? `**${e.entryId}:** ` : "";
|
|
2274
|
+
const ago = formatTimeAgo(e.updatedAt);
|
|
2275
|
+
lines.push(`- ${id}${e.name} _(${ago})_`);
|
|
2276
|
+
}
|
|
2277
|
+
lines.push("");
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (changes.relations.length > 0) {
|
|
2281
|
+
lines.push("## New Relations");
|
|
2282
|
+
for (const r of changes.relations) {
|
|
2283
|
+
const from = r.fromEntryId ?? "?";
|
|
2284
|
+
const to = r.toEntryId ?? "?";
|
|
2285
|
+
const ago = formatTimeAgo(r.createdAt);
|
|
2286
|
+
lines.push(`- ${from} **${r.type}** ${to} _(${ago})_`);
|
|
2287
|
+
}
|
|
2288
|
+
lines.push("");
|
|
2289
|
+
}
|
|
2290
|
+
if (result.snapshotWarning) {
|
|
2291
|
+
lines.push("_Note: Compound query spans multiple reads. Data consistency is not guaranteed across components (TEN-1029)._");
|
|
2292
|
+
}
|
|
2293
|
+
return {
|
|
2294
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2295
|
+
structuredContent: success(
|
|
2296
|
+
`Delta sync since ${sinceDate}: ${changes.entries.length} entries, ${changes.relations.length} relations.${changes.truncated ? " Truncated." : ""}`,
|
|
2297
|
+
result
|
|
2298
|
+
)
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
function formatTimeAgo(ms) {
|
|
2302
|
+
const diff = Date.now() - ms;
|
|
2303
|
+
if (diff < 6e4) return "just now";
|
|
2304
|
+
if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
|
|
2305
|
+
if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
|
|
2306
|
+
return `${Math.floor(diff / 864e5)}d ago`;
|
|
2307
|
+
}
|
|
1830
2308
|
|
|
1831
2309
|
// src/tools/collections.ts
|
|
1832
2310
|
import { z as z6 } from "zod";
|
|
@@ -1850,7 +2328,9 @@ var fieldSchema = z6.object({
|
|
|
1850
2328
|
iconMap: z6.record(z6.string(), z6.string()).optional().describe("ENT-61: maps field values to icons (emoji/symbol), prepended to badge text"),
|
|
1851
2329
|
helpText: z6.string().optional().describe("Help text shown in editors and describe output"),
|
|
1852
2330
|
optionDescriptions: z6.record(z6.string()).optional().describe("Per-option guidance for select fields"),
|
|
1853
|
-
semanticRole: z6.enum(["problem", "appetite", "elements", "architecture", "done_when", "risks", "exclusions"]).optional().describe("BET-136: Semantic role for schema-driven consumers \u2014 enables field-key-independent validation and rendering")
|
|
2331
|
+
semanticRole: z6.enum(["problem", "appetite", "elements", "architecture", "done_when", "risks", "exclusions"]).optional().describe("BET-136: Semantic role for schema-driven consumers \u2014 enables field-key-independent validation and rendering"),
|
|
2332
|
+
maxLength: z6.number().optional().describe("BET-196: Maximum character length for field values. Three-tier resolution: explicit > displayHint > type fallback."),
|
|
2333
|
+
minLength: z6.number().optional().describe("BET-196: Minimum character length for field values. Only explicit \u2014 no defaults.")
|
|
1854
2334
|
});
|
|
1855
2335
|
var collectionsSchema = z6.object({
|
|
1856
2336
|
action: z6.enum(COLLECTIONS_ACTIONS).describe(
|
|
@@ -3138,10 +3618,13 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3138
3618
|
requireWriteAccess();
|
|
3139
3619
|
const sessionId = getAgentSessionId();
|
|
3140
3620
|
if (!sessionId) {
|
|
3141
|
-
return { text: "No active session.", committed: 0, proposalsCreated: 0, linksCreated: 0, failed: 0 };
|
|
3621
|
+
return { text: "No active session.", committed: 0, proposalsCreated: 0, linksCreated: 0, linksFailed: 0, failed: 0 };
|
|
3142
3622
|
}
|
|
3623
|
+
const wsCtx = await getWorkspaceContext();
|
|
3143
3624
|
const results = [];
|
|
3144
3625
|
let linksCreated = 0;
|
|
3626
|
+
let linksFailed = 0;
|
|
3627
|
+
const linkFailureDetails = [];
|
|
3145
3628
|
let proposalsCreated = 0;
|
|
3146
3629
|
for (const draft of data.drafts) {
|
|
3147
3630
|
try {
|
|
@@ -3153,6 +3636,11 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3153
3636
|
const proposed = result?.status === "proposal_created";
|
|
3154
3637
|
if (!proposed) {
|
|
3155
3638
|
await recordSessionActivity({ entryModified: draft.docId });
|
|
3639
|
+
trackChainEntryCommitted(wsCtx.workspaceId, {
|
|
3640
|
+
entry_id: draft.entryId,
|
|
3641
|
+
commit_method: "manual",
|
|
3642
|
+
surface: "mcp_wrapup"
|
|
3643
|
+
});
|
|
3156
3644
|
}
|
|
3157
3645
|
if (proposed) {
|
|
3158
3646
|
proposalsCreated++;
|
|
@@ -3186,8 +3674,22 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3186
3674
|
sessionId
|
|
3187
3675
|
});
|
|
3188
3676
|
linksCreated += batchResult?.created ?? 0;
|
|
3677
|
+
if (batchResult && batchResult.failed > 0) {
|
|
3678
|
+
linksFailed += batchResult.failed;
|
|
3679
|
+
const failedResults = batchResult.results.map((r, i) => ({ ...r, index: i })).filter((r) => !r.ok);
|
|
3680
|
+
for (const fr of failedResults) {
|
|
3681
|
+
const rel = batchRels[fr.index];
|
|
3682
|
+
if (rel) {
|
|
3683
|
+
linkFailureDetails.push(
|
|
3684
|
+
`${rel.fromEntryId} \u2192 ${rel.toEntryId} [${rel.type}]: ${fr.error ?? "unknown error"}`
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3189
3689
|
} catch (err) {
|
|
3190
|
-
|
|
3690
|
+
linksFailed += batchRels.length;
|
|
3691
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3692
|
+
linkFailureDetails.push(`Batch creation failed (${batchRels.length} relations): ${msg}`);
|
|
3191
3693
|
}
|
|
3192
3694
|
}
|
|
3193
3695
|
for (const s of governsRels) {
|
|
@@ -3201,7 +3703,10 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3201
3703
|
if (result?.status !== "proposal_created") {
|
|
3202
3704
|
linksCreated++;
|
|
3203
3705
|
}
|
|
3204
|
-
} catch {
|
|
3706
|
+
} catch (err) {
|
|
3707
|
+
linksFailed++;
|
|
3708
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3709
|
+
linkFailureDetails.push(`${s.fromEntryId} \u2192 ${s.toEntryId} [${s.type}]: ${msg}`);
|
|
3205
3710
|
}
|
|
3206
3711
|
}
|
|
3207
3712
|
const committed = results.filter((r) => r.ok && !r.proposed).length;
|
|
@@ -3217,6 +3722,9 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3217
3722
|
"",
|
|
3218
3723
|
`**${committed}** drafts committed, **${proposalsCreated}** proposals created, **${linksCreated}** links created.`
|
|
3219
3724
|
];
|
|
3725
|
+
if (linksFailed > 0) {
|
|
3726
|
+
lines.push(`**${linksFailed}** link${linksFailed === 1 ? "" : "s"} failed to create.`);
|
|
3727
|
+
}
|
|
3220
3728
|
if (failed.length > 0) {
|
|
3221
3729
|
lines.push("");
|
|
3222
3730
|
lines.push("### Failed to commit");
|
|
@@ -3225,11 +3733,20 @@ async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
|
3225
3733
|
}
|
|
3226
3734
|
lines.push("_These remain as drafts for next session._");
|
|
3227
3735
|
}
|
|
3736
|
+
if (linkFailureDetails.length > 0) {
|
|
3737
|
+
lines.push("");
|
|
3738
|
+
lines.push("### Failed to create links");
|
|
3739
|
+
for (const detail of linkFailureDetails) {
|
|
3740
|
+
lines.push(`- ${detail}`);
|
|
3741
|
+
}
|
|
3742
|
+
lines.push("_You can create these links manually with `entries action=create-relation`._");
|
|
3743
|
+
}
|
|
3228
3744
|
return {
|
|
3229
3745
|
text: lines.join("\n"),
|
|
3230
3746
|
committed,
|
|
3231
3747
|
proposalsCreated,
|
|
3232
3748
|
linksCreated,
|
|
3749
|
+
linksFailed,
|
|
3233
3750
|
failed: failed.length
|
|
3234
3751
|
};
|
|
3235
3752
|
}
|
|
@@ -3264,13 +3781,15 @@ function registerWrapupTools(server) {
|
|
|
3264
3781
|
lastReviewData = null;
|
|
3265
3782
|
lastReviewSuggestions = [];
|
|
3266
3783
|
lastReviewSessionId = null;
|
|
3784
|
+
const linkWarnSuffix = result.linksFailed > 0 ? `, ${result.linksFailed} link(s) failed` : "";
|
|
3267
3785
|
return successResult(
|
|
3268
3786
|
result.text,
|
|
3269
|
-
`Wrapup processed: ${result.committed} committed, ${result.proposalsCreated} proposals, ${result.linksCreated} links.`,
|
|
3787
|
+
`Wrapup processed: ${result.committed} committed, ${result.proposalsCreated} proposals, ${result.linksCreated} links${linkWarnSuffix}.`,
|
|
3270
3788
|
{
|
|
3271
3789
|
committed: result.committed,
|
|
3272
3790
|
proposalsCreated: result.proposalsCreated,
|
|
3273
3791
|
linksCreated: result.linksCreated,
|
|
3792
|
+
linksFailed: result.linksFailed,
|
|
3274
3793
|
failed: result.failed
|
|
3275
3794
|
}
|
|
3276
3795
|
);
|
|
@@ -3835,7 +4354,9 @@ var RETRO_WORKFLOW_DESCRIPTOR = {
|
|
|
3835
4354
|
5. **Synthesize between rounds.** After collecting input, reflect back what you heard before moving on.
|
|
3836
4355
|
6. **Never go silent.** If something fails, say what happened and what to do next.
|
|
3837
4356
|
7. **Finalize through the workflow substrate.** Complete the terminal round with \`workflows action=checkpoint ... isFinal=true\` so the durable run creates the retro draft record.
|
|
3838
|
-
8. **
|
|
4357
|
+
8. **Session before checkpoint (default).** Before the **first** \`workflows action=checkpoint\` in this run, ensure an **active** agent write session: MCP \`session action=start\` or \`pb session start\` (DEC-9, STD-135). Inactive or superseded sessions commonly return **400** on checkpoint \u2014 durable progress is lost unless the chat record or a **handoff block** preserves it (see handoff skill: Facilitated workflow handoff).
|
|
4358
|
+
9. **Hand off mid-flight.** If the user switches agents or checkpoint keeps failing, emit the **Facilitated workflow handoff** package from \`.productbrain/skills/handoff.md\` (copy-paste block). Another agent resumes from that block + \`workflows get-run\` or \`start\` with \`restart=true\` \u2014 see workflow \`errorRecovery\`.
|
|
4359
|
+
10. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
|
|
3839
4360
|
|
|
3840
4361
|
## Communication Style
|
|
3841
4362
|
|
|
@@ -3991,6 +4512,8 @@ Create a Cursor Plan with these rounds as tasks. Mark each in_progress as you en
|
|
|
3991
4512
|
3. **Plan creation fails**: Continue without the Plan. The conversation IS the record.
|
|
3992
4513
|
4. **Participant goes off-topic**: Gently redirect: "That's valuable \u2014 let's capture it. For now, let's stay with [current round]."
|
|
3993
4514
|
5. **Participant wants to stop**: Respect it. Summarize what you have so far. Offer to commit partial results to the Chain.
|
|
4515
|
+
6. **Checkpoint failed (e.g. 400, superseded session)**: Say so in one line. **Start or refresh session** (\`session action=start\` / \`pb session start\`), then \`workflows action=get-run\` with the \`runId\` you were given. If the run is **incomplete**, retry \`checkpoint\` with that \`runId\`. If the run is **complete** or **missing**, treat server state as unreliable: run \`workflows action=start workflowId=retro restart=true\` and **paste the Facilitated workflow handoff block** from the prior chat into round 1 so scope and completed rounds are explicit \u2014 do not assume partial server progress exists.
|
|
4516
|
+
7. **Cross-agent resume (default expectation)**: Receiving agent reads the handoff block first. Prefer \`get-run\` to verify status; otherwise **restart** and continue from the **next round** named in the handoff. The conversation + handoff block are valid SSOT when ChainWork did not persist checkpoints (TEN-917).
|
|
3994
4517
|
|
|
3995
4518
|
The retro must never fail silently. Always communicate state.`
|
|
3996
4519
|
};
|
|
@@ -4053,7 +4576,7 @@ Use \`workflows action=checkpoint\` after each round to persist progress to the
|
|
|
4053
4576
|
description: "Confirmed problem statement and appetite",
|
|
4054
4577
|
format: "structured"
|
|
4055
4578
|
},
|
|
4056
|
-
kbCollection: "
|
|
4579
|
+
kbCollection: "work-packages",
|
|
4057
4580
|
maxDurationHint: "10 min"
|
|
4058
4581
|
},
|
|
4059
4582
|
{
|
|
@@ -4123,16 +4646,16 @@ Use \`workflows action=checkpoint\` after each round to persist progress to the
|
|
|
4123
4646
|
description: "Final capture summary \u2014 entries committed, relations created",
|
|
4124
4647
|
format: "structured"
|
|
4125
4648
|
},
|
|
4126
|
-
kbCollection: "
|
|
4649
|
+
kbCollection: "work-packages",
|
|
4127
4650
|
maxDurationHint: "5 min"
|
|
4128
4651
|
}
|
|
4129
4652
|
],
|
|
4130
4653
|
output: {
|
|
4131
4654
|
primaryRecord: {
|
|
4132
4655
|
routing: {
|
|
4133
|
-
// BET-145 E6 / FEAT-468: shape workflow outputs route to
|
|
4656
|
+
// BET-145 E6 / FEAT-468: shape workflow outputs route to work-packages collection (DEC-245, BET-230 S5).
|
|
4134
4657
|
mode: "fixed",
|
|
4135
|
-
collection: "
|
|
4658
|
+
collection: "work-packages"
|
|
4136
4659
|
}
|
|
4137
4660
|
},
|
|
4138
4661
|
emits: [
|
|
@@ -4158,8 +4681,8 @@ Use \`workflows action=checkpoint\` after each round to persist progress to the
|
|
|
4158
4681
|
},
|
|
4159
4682
|
{
|
|
4160
4683
|
kind: "update",
|
|
4161
|
-
collection: "
|
|
4162
|
-
description: "
|
|
4684
|
+
collection: "work-packages",
|
|
4685
|
+
description: "Work package status and no-go updates during finalize."
|
|
4163
4686
|
}
|
|
4164
4687
|
]
|
|
4165
4688
|
},
|
|
@@ -5316,7 +5839,7 @@ var facilitateSchema = z12.object({
|
|
|
5316
5839
|
operationId: z12.string().optional().describe("Optional idempotency key for commit-constellation retries.")
|
|
5317
5840
|
});
|
|
5318
5841
|
function buildStudioUrl(workspaceSlug, entryId) {
|
|
5319
|
-
const appUrl = process.env.PRODUCTBRAIN_APP_URL ?? "https://productbrain.io";
|
|
5842
|
+
const appUrl = process.env.PRODUCTBRAIN_APP_URL ?? "https://work.productbrain.io";
|
|
5320
5843
|
return `${appUrl}/${workspaceSlug}/legacy/entries/${entryId}`;
|
|
5321
5844
|
}
|
|
5322
5845
|
async function loadBetEntry(entryId) {
|
|
@@ -5524,7 +6047,7 @@ async function handleCommitConstellation(args) {
|
|
|
5524
6047
|
}
|
|
5525
6048
|
let contradictionWarnings = [];
|
|
5526
6049
|
try {
|
|
5527
|
-
const { runContradictionCheck } = await import("./smart-capture-
|
|
6050
|
+
const { runContradictionCheck } = await import("./smart-capture-CVZ5PLUZ.js");
|
|
5528
6051
|
const links = betData["links"];
|
|
5529
6052
|
const descField = betData.problem ?? links?.problem ?? betData.description ?? "";
|
|
5530
6053
|
contradictionWarnings = await runContradictionCheck(
|
|
@@ -5884,6 +6407,7 @@ function registerVerifyTools(server) {
|
|
|
5884
6407
|
logger: "product-brain"
|
|
5885
6408
|
});
|
|
5886
6409
|
const fixes = [];
|
|
6410
|
+
const fixUpdateWarnings = [];
|
|
5887
6411
|
if (mode === "fix") {
|
|
5888
6412
|
const driftedByEntry = /* @__PURE__ */ new Map();
|
|
5889
6413
|
for (const mc of mappingChecks) {
|
|
@@ -5898,10 +6422,17 @@ function registerVerifyTools(server) {
|
|
|
5898
6422
|
const updated = (entry.data?.codeMapping ?? []).map(
|
|
5899
6423
|
(cm) => cm.status === "aligned" && driftedFields.has(cm.field) ? { ...cm, status: "drifted" } : cm
|
|
5900
6424
|
);
|
|
5901
|
-
await mcpMutation("chain.updateEntry", {
|
|
6425
|
+
const updateResult = await mcpMutation("chain.updateEntry", {
|
|
5902
6426
|
entryId: entry.entryId,
|
|
5903
6427
|
data: { codeMapping: updated }
|
|
5904
6428
|
});
|
|
6429
|
+
if (updateResult.normalizationWarnings.length > 0 || updateResult.validationWarnings.length > 0) {
|
|
6430
|
+
fixUpdateWarnings.push({
|
|
6431
|
+
entryId: entry.entryId,
|
|
6432
|
+
normalizationWarnings: updateResult.normalizationWarnings,
|
|
6433
|
+
validationWarnings: updateResult.validationWarnings
|
|
6434
|
+
});
|
|
6435
|
+
}
|
|
5905
6436
|
fixes.push(entry.entryId);
|
|
5906
6437
|
}
|
|
5907
6438
|
}
|
|
@@ -5923,8 +6454,22 @@ function registerVerifyTools(server) {
|
|
|
5923
6454
|
schema.size,
|
|
5924
6455
|
projectRoot
|
|
5925
6456
|
);
|
|
6457
|
+
let reportText = report;
|
|
6458
|
+
if (fixUpdateWarnings.length > 0) {
|
|
6459
|
+
reportText += "\n\n---\n\n\u26A0\uFE0F **Fix update warnings:**";
|
|
6460
|
+
for (const w of fixUpdateWarnings) {
|
|
6461
|
+
for (const nw of w.normalizationWarnings) {
|
|
6462
|
+
reportText += `
|
|
6463
|
+
- **${w.entryId}** (normalization): ${nw}`;
|
|
6464
|
+
}
|
|
6465
|
+
for (const vw of w.validationWarnings) {
|
|
6466
|
+
reportText += `
|
|
6467
|
+
- **${w.entryId}** (validation): ${vw}`;
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
5926
6471
|
return {
|
|
5927
|
-
content: [{ type: "text", text:
|
|
6472
|
+
content: [{ type: "text", text: reportText }],
|
|
5928
6473
|
structuredContent: success(
|
|
5929
6474
|
`Trust report for ${collection}: ${totalPassed}/${totalChecks} checks passed (${trustScore}%).`,
|
|
5930
6475
|
{
|
|
@@ -6294,7 +6839,7 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
|
6294
6839
|
return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
|
|
6295
6840
|
}
|
|
6296
6841
|
const betHits = (taskContextHits ?? []).filter(
|
|
6297
|
-
(e) => e.collectionSlug === "chains" || e.collectionSlug === "
|
|
6842
|
+
(e) => e.collectionSlug === "chains" || e.collectionSlug === "work-packages"
|
|
6298
6843
|
);
|
|
6299
6844
|
if (betHits.length > 0) {
|
|
6300
6845
|
return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
|
|
@@ -7046,7 +7591,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
7046
7591
|
} else if (isHighReadiness) {
|
|
7047
7592
|
let activeBets = [];
|
|
7048
7593
|
try {
|
|
7049
|
-
const betsEntries = await mcpQuery("chain.listEntries", { collectionSlug: "
|
|
7594
|
+
const betsEntries = await mcpQuery("chain.listEntries", { collectionSlug: "work-packages" });
|
|
7050
7595
|
activeBets = (betsEntries ?? []).filter((e) => isActiveNowBet(e)).map((e) => ({ name: e.name, entryId: e.entryId ?? null })).slice(0, 8);
|
|
7051
7596
|
} catch {
|
|
7052
7597
|
}
|
|
@@ -8380,7 +8925,7 @@ var SEED_NODES = [
|
|
|
8380
8925
|
{ entryId: "ARCH-node-glossary", name: "Glossary", order: 2, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "Aa", description: "Canonical vocabulary management \u2014 term detail, code drift detection, inline term linking", filePaths: "src/routes/glossary/, src/lib/components/glossary/", owner: "Knowledge", rationale: "Features layer \u2014 NOT Core or Infrastructure \u2014 because this is the Glossary PAGE: the SvelteKit route for browsing, editing, and viewing terms. Why not Core? Because Core owns the glossary DATA (Knowledge Graph) and term-linking logic. The MCP server also accesses glossary terms without any page. Why not Infrastructure? Because glossary is domain-specific vocabulary, not generic plumbing. The page is one consumer of the data \u2014 Core owns the dictionary, Features owns the dictionary app." } },
|
|
8381
8926
|
{ entryId: "ARCH-node-tensions", name: "Tensions", order: 3, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u26A1", description: "Tension capture and processing \u2014 raise, triage, resolve through governance workflow", filePaths: "src/routes/tensions/, src/lib/components/tensions/", owner: "Governance", rationale: "Features layer because this is the Tensions PAGE \u2014 the UI for raising, listing, and viewing tensions. Why not Core? Because tension data, status rules (SOS-020), and processing logic already live in Core (Governance Engine). This page is a form + list view that reads from and writes to Core. Delete it and the governance engine still processes tensions via MCP." } },
|
|
8382
8927
|
{ entryId: "ARCH-node-strategy", name: "Strategy", order: 4, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25A3", description: "Product strategy pages \u2014 vision, ecosystem, product areas, decision frameworks, sequencing", filePaths: "src/routes/strategy/, src/routes/bridge/, src/routes/topology/", owner: "Strategy", rationale: "Features layer because Strategy is a set of SvelteKit pages (strategy, bridge, topology) that visualize strategy entries. Why not Core? Because the strategy data (vision, principles, ecosystem layers) lives in the Knowledge Graph (Core). These pages render and allow inline editing \u2014 they consume Core downward." } },
|
|
8383
|
-
{ entryId: "ARCH-node-governance-ui", name: "Governance Pages", order: 5, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25C8", description: "Roles,
|
|
8928
|
+
{ entryId: "ARCH-node-governance-ui", name: "Governance Pages", order: 5, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u25C8", description: "Roles, teams, principles, policies, decisions, proposals, business rules", filePaths: "src/routes/roles/, src/routes/teams/, src/routes/decisions/, src/routes/proposals/", owner: "Governance", rationale: "Features layer because these are governance PAGES \u2014 list views and detail views for roles, teams, decisions, proposals. Why not Core? Because the governance ENGINE (versioning, consent, IDM) IS in Core. These pages are the user-facing window into governance data. The logic doesn't live here, only the rendering." } },
|
|
8384
8929
|
{ entryId: "ARCH-node-artifacts", name: "Artifacts", order: 6, data: { archType: "node", layerRef: "ARCH-layer-features", color: "#6366f1", icon: "\u{1F4C4}", description: "Strategy artifacts from ChainWork \u2014 pitches, briefs, one-pagers linked to the knowledge graph", filePaths: "src/routes/artifacts/", owner: "ChainWork", rationale: "Features layer because the Artifacts page is a list/detail view for strategy artifacts produced by ChainWork. Why not Core? Because artifact data and scoring live in Core (ChainWork Engine). This page just renders the output and links back to the knowledge graph." } },
|
|
8385
8930
|
// Integration layer
|
|
8386
8931
|
{ entryId: "ARCH-node-cursor", name: "Cursor IDE", order: 0, data: { archType: "node", layerRef: "ARCH-layer-integration", color: "#f59e0b", icon: "\u{1F5A5}\uFE0F", description: "AI-assisted development with MCP-powered knowledge context \u2014 smart capture, verification, context assembly in the editor", filePaths: ".cursor/mcp.json, .cursor/rules/", owner: "AI DX", rationale: "Integration layer because Cursor is an external tool that connects INTO our system via MCP. Why not Core or Features? Because Cursor itself is not our code \u2014 it's a consumer. The .cursor/ config files define how it talks to us, but Cursor lives outside our deployment boundary." } },
|
|
@@ -8571,6 +9116,7 @@ ${nodeDetail}${flowLines}`
|
|
|
8571
9116
|
let created = 0;
|
|
8572
9117
|
let updated = 0;
|
|
8573
9118
|
let unchanged = 0;
|
|
9119
|
+
const allWarnings = [];
|
|
8574
9120
|
const allSeeds = [
|
|
8575
9121
|
{ ...SEED_TEMPLATE, order: 0, status: "active" },
|
|
8576
9122
|
...SEED_LAYERS.map((l) => ({ ...l, status: "active" })),
|
|
@@ -8587,14 +9133,21 @@ ${nodeDetail}${flowLines}`
|
|
|
8587
9133
|
);
|
|
8588
9134
|
if (hasChanges) {
|
|
8589
9135
|
const mergedData = { ...existingData, ...seedData };
|
|
8590
|
-
await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
9136
|
+
const updateResult = await mcpMutation("chain.updateEntry", { entryId: seed.entryId, data: mergedData });
|
|
9137
|
+
if (updateResult.normalizationWarnings.length > 0 || updateResult.validationWarnings.length > 0) {
|
|
9138
|
+
allWarnings.push({
|
|
9139
|
+
entryId: seed.entryId,
|
|
9140
|
+
normalizationWarnings: updateResult.normalizationWarnings,
|
|
9141
|
+
validationWarnings: updateResult.validationWarnings
|
|
9142
|
+
});
|
|
9143
|
+
}
|
|
8591
9144
|
updated++;
|
|
8592
9145
|
} else {
|
|
8593
9146
|
unchanged++;
|
|
8594
9147
|
}
|
|
8595
9148
|
continue;
|
|
8596
9149
|
}
|
|
8597
|
-
await mcpMutation("chain.createEntry", {
|
|
9150
|
+
const createResult = await mcpMutation("chain.createEntry", {
|
|
8598
9151
|
collectionSlug: COLLECTION_SLUG,
|
|
8599
9152
|
entryId: seed.entryId,
|
|
8600
9153
|
name: seed.name,
|
|
@@ -8602,18 +9155,39 @@ ${nodeDetail}${flowLines}`
|
|
|
8602
9155
|
data: seed.data,
|
|
8603
9156
|
order: seed.order ?? 0
|
|
8604
9157
|
});
|
|
9158
|
+
if (createResult.normalizationWarnings.length > 0 || createResult.validationWarnings.length > 0) {
|
|
9159
|
+
allWarnings.push({
|
|
9160
|
+
entryId: seed.entryId,
|
|
9161
|
+
normalizationWarnings: createResult.normalizationWarnings,
|
|
9162
|
+
validationWarnings: createResult.validationWarnings
|
|
9163
|
+
});
|
|
9164
|
+
}
|
|
8605
9165
|
created++;
|
|
8606
9166
|
}
|
|
8607
|
-
|
|
8608
|
-
content: [{
|
|
8609
|
-
type: "text",
|
|
8610
|
-
text: `# Architecture Seeded
|
|
9167
|
+
let responseText = `# Architecture Seeded
|
|
8611
9168
|
|
|
8612
9169
|
**Created:** ${created} entries
|
|
8613
9170
|
**Updated:** ${updated} (merged new fields)
|
|
8614
9171
|
**Unchanged:** ${unchanged}
|
|
8615
9172
|
|
|
8616
|
-
Use \`architecture action=show\` to view the map
|
|
9173
|
+
Use \`architecture action=show\` to view the map.`;
|
|
9174
|
+
if (allWarnings.length > 0) {
|
|
9175
|
+
responseText += "\n\n---\n\n\u26A0\uFE0F **Seed warnings:**";
|
|
9176
|
+
for (const w of allWarnings) {
|
|
9177
|
+
for (const nw of w.normalizationWarnings) {
|
|
9178
|
+
responseText += `
|
|
9179
|
+
- **${w.entryId}** (normalization): ${nw}`;
|
|
9180
|
+
}
|
|
9181
|
+
for (const vw of w.validationWarnings) {
|
|
9182
|
+
responseText += `
|
|
9183
|
+
- **${w.entryId}** (validation): ${vw}`;
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
return {
|
|
9188
|
+
content: [{
|
|
9189
|
+
type: "text",
|
|
9190
|
+
text: responseText
|
|
8617
9191
|
}],
|
|
8618
9192
|
structuredContent: success(
|
|
8619
9193
|
`Architecture seeded: ${created} created, ${updated} updated, ${unchanged} unchanged.`,
|
|
@@ -9008,6 +9582,9 @@ function registerOrientTool(server) {
|
|
|
9008
9582
|
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
9009
9583
|
lines.push(`Product areas (${sc.productAreaCount}): ${(sc.productAreas ?? []).join(", ")}`);
|
|
9010
9584
|
}
|
|
9585
|
+
if (sc.playingFieldCount != null && sc.playingFieldCount > 0) {
|
|
9586
|
+
lines.push(`Playing field (${sc.playingFieldCount}): ${(sc.playingField ?? []).join(", ")}`);
|
|
9587
|
+
}
|
|
9011
9588
|
lines.push(`${sc.activeBetCount} active bet(s), ${sc.activeTensionCount} tension(s).`);
|
|
9012
9589
|
}
|
|
9013
9590
|
if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
|
|
@@ -9017,7 +9594,15 @@ function registerOrientTool(server) {
|
|
|
9017
9594
|
for (const e of orientEntries.activeBets) {
|
|
9018
9595
|
const tensions = e.linkedTensions;
|
|
9019
9596
|
const tensionPart = tensions?.length ? ` \u2014 ${tensions.map((t) => `${t.entryId ?? t.name} (${t.severity ?? "\u2014"})`).join(", ")}` : "";
|
|
9020
|
-
|
|
9597
|
+
const originPart = e.origin ? ` (origin: ${e.origin})` : "";
|
|
9598
|
+
lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${originPart}${tensionPart}`);
|
|
9599
|
+
}
|
|
9600
|
+
}
|
|
9601
|
+
if (orientEntries?.trustMetrics) {
|
|
9602
|
+
const tm = orientEntries.trustMetrics;
|
|
9603
|
+
if (tm.unverified > 0 || tm.verified > 0) {
|
|
9604
|
+
const capNote = tm.scannedCap ? "+" : "";
|
|
9605
|
+
lines.push(`Trust: ${tm.verified} verified, ${tm.unverified} unverified, ${tm.noStatus} no-status (${tm.total}${capNote} scanned).`);
|
|
9021
9606
|
}
|
|
9022
9607
|
}
|
|
9023
9608
|
const briefWna = formatWhatNeedsAttentionBrief(orientEntries?.whatNeedsAttention);
|
|
@@ -9140,7 +9725,11 @@ function registerOrientTool(server) {
|
|
|
9140
9725
|
const fmt = (e) => {
|
|
9141
9726
|
const type = e.canonicalKey ?? "generic";
|
|
9142
9727
|
const stratum = e.stratum ?? "?";
|
|
9143
|
-
|
|
9728
|
+
const confidencePart = e.confidence ? ` [confidence: ${e.confidence}]` : "";
|
|
9729
|
+
const reviewOverdue = e.reviewAt && new Date(e.reviewAt).getTime() < Date.now();
|
|
9730
|
+
const overduePart = reviewOverdue ? " [review overdue]" : "";
|
|
9731
|
+
const originPart = e.origin ? ` (origin: ${e.origin})` : "";
|
|
9732
|
+
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}${originPart}${confidencePart}${overduePart}`;
|
|
9144
9733
|
};
|
|
9145
9734
|
const fullCoherence = buildCoherenceSection();
|
|
9146
9735
|
if (fullCoherence) {
|
|
@@ -9157,6 +9746,9 @@ function registerOrientTool(server) {
|
|
|
9157
9746
|
if (sc.productAreaCount != null && sc.productAreaCount > 0) {
|
|
9158
9747
|
lines.push(`**Product areas (${sc.productAreaCount}):** ${(sc.productAreas ?? []).join(", ")}`);
|
|
9159
9748
|
}
|
|
9749
|
+
if (sc.playingFieldCount != null && sc.playingFieldCount > 0) {
|
|
9750
|
+
lines.push(`**Playing field (${sc.playingFieldCount}):** ${(sc.playingField ?? []).join(", ")}`);
|
|
9751
|
+
}
|
|
9160
9752
|
const betLine = sc.currentBet ? `**Current bet:** ${sc.currentBet}. ${sc.activeBetCount} active bet(s).` : "No active bets.";
|
|
9161
9753
|
lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
|
|
9162
9754
|
lines.push("");
|
|
@@ -9214,7 +9806,8 @@ function registerOrientTool(server) {
|
|
|
9214
9806
|
for (const e of tc.context) {
|
|
9215
9807
|
const id = e.entryId ?? e.name;
|
|
9216
9808
|
const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
|
|
9217
|
-
|
|
9809
|
+
const originTag = e.origin ? ` (origin: ${e.origin})` : "";
|
|
9810
|
+
lines.push(`- \`${id}\` (score ${e.score})${coll}${originTag}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
|
|
9218
9811
|
if (e.preview) lines.push(` _${e.preview}_`);
|
|
9219
9812
|
}
|
|
9220
9813
|
lines.push("");
|
|
@@ -9235,6 +9828,20 @@ function registerOrientTool(server) {
|
|
|
9235
9828
|
lines.push(...formatInitiativeStatusLines(orientEntries.initiativeStatus));
|
|
9236
9829
|
lines.push(...formatWorkstreamHealthLines(orientEntries.workstreamHealth));
|
|
9237
9830
|
lines.push(...formatWhatNeedsAttentionLines(orientEntries.whatNeedsAttention));
|
|
9831
|
+
if (orientEntries.trustMetrics) {
|
|
9832
|
+
const tm = orientEntries.trustMetrics;
|
|
9833
|
+
if (tm.verified > 0 || tm.unverified > 0) {
|
|
9834
|
+
const capNote = tm.scannedCap ? " (capped at 500)" : "";
|
|
9835
|
+
lines.push("## Trust metrics");
|
|
9836
|
+
lines.push(`_Entry verification status across workspace${capNote}._`);
|
|
9837
|
+
lines.push("");
|
|
9838
|
+
lines.push(`- **Verified:** ${tm.verified}`);
|
|
9839
|
+
lines.push(`- **Unverified:** ${tm.unverified}`);
|
|
9840
|
+
lines.push(`- **No status (pre-BET-240):** ${tm.noStatus}`);
|
|
9841
|
+
lines.push(`- **Total scanned:** ${tm.total}`);
|
|
9842
|
+
lines.push("");
|
|
9843
|
+
}
|
|
9844
|
+
}
|
|
9238
9845
|
}
|
|
9239
9846
|
if (fullCoherence) {
|
|
9240
9847
|
lines.push(...fullCoherence.lines);
|
|
@@ -9275,6 +9882,13 @@ function registerOrientTool(server) {
|
|
|
9275
9882
|
orientEntries.strategyHighlights.forEach((e) => lines.push(fmt(e)));
|
|
9276
9883
|
lines.push("");
|
|
9277
9884
|
}
|
|
9885
|
+
if (orientEntries.playingField?.length > 0) {
|
|
9886
|
+
lines.push("## Playing field");
|
|
9887
|
+
lines.push("_Products and opportunities in the strategic landscape._");
|
|
9888
|
+
lines.push("");
|
|
9889
|
+
orientEntries.playingField.forEach((e) => lines.push(fmt(e)));
|
|
9890
|
+
lines.push("");
|
|
9891
|
+
}
|
|
9278
9892
|
if (orientEntries.recentDecisions?.length > 0) {
|
|
9279
9893
|
lines.push("## Recent decisions");
|
|
9280
9894
|
orientEntries.recentDecisions.forEach((e) => lines.push(fmt(e)));
|
|
@@ -9333,6 +9947,20 @@ function registerOrientTool(server) {
|
|
|
9333
9947
|
lines.push(...formatInitiativeStatusLines(orientEntries?.initiativeStatus));
|
|
9334
9948
|
lines.push(...formatWorkstreamHealthLines(orientEntries?.workstreamHealth));
|
|
9335
9949
|
lines.push(...formatWhatNeedsAttentionLines(orientEntries?.whatNeedsAttention));
|
|
9950
|
+
if (orientEntries?.trustMetrics) {
|
|
9951
|
+
const tm = orientEntries.trustMetrics;
|
|
9952
|
+
if (tm.verified > 0 || tm.unverified > 0) {
|
|
9953
|
+
const capNote = tm.scannedCap ? " (capped at 500)" : "";
|
|
9954
|
+
lines.push("## Trust metrics");
|
|
9955
|
+
lines.push(`_Entry verification status across workspace${capNote}._`);
|
|
9956
|
+
lines.push("");
|
|
9957
|
+
lines.push(`- **Verified:** ${tm.verified}`);
|
|
9958
|
+
lines.push(`- **Unverified:** ${tm.unverified}`);
|
|
9959
|
+
lines.push(`- **No status (pre-BET-240):** ${tm.noStatus}`);
|
|
9960
|
+
lines.push(`- **Total scanned:** ${tm.total}`);
|
|
9961
|
+
lines.push("");
|
|
9962
|
+
}
|
|
9963
|
+
}
|
|
9336
9964
|
lines.push(...buildOperatingProtocol());
|
|
9337
9965
|
if (fullCoherence) {
|
|
9338
9966
|
lines.push(...fullCoherence.lines);
|
|
@@ -9906,6 +10534,270 @@ function registerHealthTools(server) {
|
|
|
9906
10534
|
);
|
|
9907
10535
|
}
|
|
9908
10536
|
|
|
10537
|
+
// src/tools/audit.ts
|
|
10538
|
+
import { z as z22 } from "zod";
|
|
10539
|
+
var AUDIT_ACTIONS = ["run"];
|
|
10540
|
+
var auditSchema = z22.object({
|
|
10541
|
+
action: z22.enum(AUDIT_ACTIONS).describe(
|
|
10542
|
+
"'run': run the STD-113 hygiene audit for a bet entry."
|
|
10543
|
+
),
|
|
10544
|
+
entryId: z22.string().describe("Bet entry ID to audit, e.g. 'BET-182'"),
|
|
10545
|
+
phase: z22.enum(["shaping", "handoff"]).default("shaping").optional().describe(
|
|
10546
|
+
"'shaping': check shaping-phase fields only. 'handoff': check all required fields including buildContract/buildSequence/exclusions/risks. Default: shaping."
|
|
10547
|
+
)
|
|
10548
|
+
});
|
|
10549
|
+
function registerAuditTools(server) {
|
|
10550
|
+
server.registerTool(
|
|
10551
|
+
"audit",
|
|
10552
|
+
{
|
|
10553
|
+
title: "Audit",
|
|
10554
|
+
description: "Run STD-113 hygiene audit on a bet entry. Checks 13 gates covering:\n\n- **Relations**: intentional links, no auto-link noise, strategic anchoring\n- **Risk/Tension**: risks in correct collection, tensions linked\n- **Field completeness**: mandatory fields populated for current phase\n- **Elements**: correct collection, committed status\n- **Acceptance criteria**: doneWhen populated and verifiable\n- **Structural**: no circular relations, appetite consistency\n\nUse `phase=shaping` (default) before starting implementation. Use `phase=handoff` before handing off to AI Engineer.",
|
|
10555
|
+
inputSchema: auditSchema,
|
|
10556
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
10557
|
+
},
|
|
10558
|
+
withEnvelope(async (args) => {
|
|
10559
|
+
const parsed = parseOrFail(auditSchema, args);
|
|
10560
|
+
if (!parsed.ok) return parsed.result;
|
|
10561
|
+
const { entryId, phase = "shaping" } = parsed.data;
|
|
10562
|
+
return runWithToolContext({ tool: "audit", action: "run" }, async () => {
|
|
10563
|
+
return handleAuditRun(entryId, phase ?? "shaping");
|
|
10564
|
+
});
|
|
10565
|
+
})
|
|
10566
|
+
);
|
|
10567
|
+
}
|
|
10568
|
+
async function handleAuditRun(entryId, phase) {
|
|
10569
|
+
let result;
|
|
10570
|
+
try {
|
|
10571
|
+
result = await mcpQuery("chain.auditBet", { entryId, phase });
|
|
10572
|
+
} catch (err) {
|
|
10573
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10574
|
+
if (msg.includes("not found") || msg.includes("NOT_FOUND")) {
|
|
10575
|
+
return failureResult(
|
|
10576
|
+
`Entry '${entryId}' not found.`,
|
|
10577
|
+
"NOT_FOUND",
|
|
10578
|
+
`Entry '${entryId}' not found.`,
|
|
10579
|
+
"Use entries action=search to find the correct BET-ID.",
|
|
10580
|
+
[{ tool: "entries", description: "Search entries", parameters: { action: "search", query: entryId } }]
|
|
10581
|
+
);
|
|
10582
|
+
}
|
|
10583
|
+
throw err;
|
|
10584
|
+
}
|
|
10585
|
+
const { verdict, gates, summary, entryName, elapsed_ms } = result;
|
|
10586
|
+
const verdictIcon = verdict === "pass" ? "\u2713" : verdict === "fail" ? "\u2717" : "\u26A0";
|
|
10587
|
+
const verdictLabel = verdict.toUpperCase();
|
|
10588
|
+
const lines = [
|
|
10589
|
+
`## Audit: ${entryId} \u2014 ${entryName}`,
|
|
10590
|
+
`Phase: ${phase} | Verdict: ${verdictIcon} ${verdictLabel} | ${elapsed_ms}ms`,
|
|
10591
|
+
`Gates: ${summary.passed}/${summary.total} passed \xB7 ${summary.failed} failed \xB7 ${summary.warned} warned`,
|
|
10592
|
+
""
|
|
10593
|
+
];
|
|
10594
|
+
const failed = gates.filter((g) => g.status === "fail");
|
|
10595
|
+
const warned = gates.filter((g) => g.status === "warn");
|
|
10596
|
+
const passed = gates.filter((g) => g.status === "pass");
|
|
10597
|
+
if (failed.length > 0) {
|
|
10598
|
+
lines.push("### \u2717 Failed Gates (blocking)");
|
|
10599
|
+
for (const gate of failed) {
|
|
10600
|
+
lines.push(`**${gate.gate}**: ${gate.detail}`);
|
|
10601
|
+
if (gate.fix) lines.push(` \u2192 Fix: ${gate.fix}`);
|
|
10602
|
+
if (gate.hint) lines.push(` \u2192 Note: ${gate.hint}`);
|
|
10603
|
+
}
|
|
10604
|
+
lines.push("");
|
|
10605
|
+
}
|
|
10606
|
+
if (warned.length > 0) {
|
|
10607
|
+
lines.push("### \u26A0 Warnings (non-blocking)");
|
|
10608
|
+
for (const gate of warned) {
|
|
10609
|
+
lines.push(`**${gate.gate}**: ${gate.detail}`);
|
|
10610
|
+
if (gate.fix) lines.push(` \u2192 Fix: ${gate.fix}`);
|
|
10611
|
+
if (gate.hint) lines.push(` \u2192 Note: ${gate.hint}`);
|
|
10612
|
+
}
|
|
10613
|
+
lines.push("");
|
|
10614
|
+
}
|
|
10615
|
+
if (passed.length > 0 && verdict === "pass") {
|
|
10616
|
+
lines.push("### \u2713 All Gates Passed");
|
|
10617
|
+
for (const gate of passed) {
|
|
10618
|
+
lines.push(` ${gate.gate}: ${gate.detail}`);
|
|
10619
|
+
}
|
|
10620
|
+
} else if (passed.length > 0) {
|
|
10621
|
+
lines.push(`### \u2713 Passed (${passed.length}): ${passed.map((g) => g.gate).join(", ")}`);
|
|
10622
|
+
}
|
|
10623
|
+
const nextActions = [];
|
|
10624
|
+
if (verdict !== "pass") {
|
|
10625
|
+
nextActions.push({
|
|
10626
|
+
tool: "entries",
|
|
10627
|
+
description: "View full entry details",
|
|
10628
|
+
parameters: { action: "get", entryId }
|
|
10629
|
+
});
|
|
10630
|
+
nextActions.push({
|
|
10631
|
+
tool: "relations",
|
|
10632
|
+
description: "Manage entry relations",
|
|
10633
|
+
parameters: { action: "list", entryId }
|
|
10634
|
+
});
|
|
10635
|
+
}
|
|
10636
|
+
return {
|
|
10637
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10638
|
+
structuredContent: success(
|
|
10639
|
+
`Audit ${entryId} (${phase}): ${verdictLabel} \u2014 ${summary.passed}/${summary.total} gates passed.`,
|
|
10640
|
+
{
|
|
10641
|
+
entryId: result.entryId,
|
|
10642
|
+
entryName: result.entryName,
|
|
10643
|
+
phase: result.phase,
|
|
10644
|
+
clean: result.clean,
|
|
10645
|
+
verdict: result.verdict,
|
|
10646
|
+
gates: result.gates,
|
|
10647
|
+
summary: result.summary,
|
|
10648
|
+
elapsed_ms: result.elapsed_ms
|
|
10649
|
+
},
|
|
10650
|
+
nextActions.length > 0 ? nextActions : void 0
|
|
10651
|
+
)
|
|
10652
|
+
};
|
|
10653
|
+
}
|
|
10654
|
+
|
|
10655
|
+
// src/tools/governance.ts
|
|
10656
|
+
import { z as z23 } from "zod";
|
|
10657
|
+
var governanceSchema = z23.object({
|
|
10658
|
+
action: z23.enum(["list", "respond", "count"]).describe("Action: list open proposals, respond to a proposal, or count open proposals"),
|
|
10659
|
+
proposalId: z23.string().optional().describe("Proposal ID (required for respond action)"),
|
|
10660
|
+
verdict: z23.enum(["approve", "reject"]).optional().describe("Verdict for respond action: approve or reject"),
|
|
10661
|
+
reason: z23.string().optional().describe("Reason for the verdict (required when rejecting)"),
|
|
10662
|
+
status: z23.enum(["open", "approved", "objected", "expired"]).optional().describe("Filter proposals by status (default: open). Only used with list action.")
|
|
10663
|
+
});
|
|
10664
|
+
function registerGovernanceTools(server) {
|
|
10665
|
+
const tool = server.registerTool(
|
|
10666
|
+
"governance-proposals",
|
|
10667
|
+
{
|
|
10668
|
+
title: "Governance Proposals",
|
|
10669
|
+
description: "Manage consent proposals \u2014 list open proposals awaiting review, respond with approve/reject, or count open proposals. Consent proposals are created when a governs relation is proposed and require explicit approval before the relation is created. Use list to see what needs attention, respond to act on proposals, and count for badge display.",
|
|
10670
|
+
inputSchema: governanceSchema,
|
|
10671
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
10672
|
+
},
|
|
10673
|
+
withEnvelope(async ({ action, proposalId, verdict, reason, status }) => {
|
|
10674
|
+
const wsCtx = await getWorkspaceContext();
|
|
10675
|
+
const enabled = await isFeatureEnabled(
|
|
10676
|
+
"bet-221-governance-at-scale",
|
|
10677
|
+
wsCtx.workspaceId,
|
|
10678
|
+
wsCtx.workspaceSlug
|
|
10679
|
+
);
|
|
10680
|
+
if (!enabled) {
|
|
10681
|
+
return failureResult(
|
|
10682
|
+
"Governance proposals are not yet enabled for this workspace.",
|
|
10683
|
+
"FEATURE_DISABLED",
|
|
10684
|
+
"The bet-221-governance-at-scale flag is OFF.",
|
|
10685
|
+
"Ask your workspace admin to enable the governance-at-scale feature flag."
|
|
10686
|
+
);
|
|
10687
|
+
}
|
|
10688
|
+
if (action === "list") {
|
|
10689
|
+
const proposals = await mcpQuery("governance.listProposals", {
|
|
10690
|
+
...status ? { status } : {}
|
|
10691
|
+
});
|
|
10692
|
+
if (proposals.length === 0) {
|
|
10693
|
+
const statusLabel = status ?? "open";
|
|
10694
|
+
return successResult(
|
|
10695
|
+
`No ${statusLabel} consent proposals in this workspace.`,
|
|
10696
|
+
`No ${statusLabel} consent proposals found.`,
|
|
10697
|
+
{ proposals: [], count: 0 }
|
|
10698
|
+
);
|
|
10699
|
+
}
|
|
10700
|
+
const lines = ["# Consent Proposals", ""];
|
|
10701
|
+
for (const p of proposals) {
|
|
10702
|
+
const urgency = p.isExpired ? "EXPIRED" : p.hoursRemaining < 1 ? `< 1h remaining` : `${p.hoursRemaining}h remaining`;
|
|
10703
|
+
lines.push(`## ${p.targetEntryName} \u2192 ${p.toEntryName ?? "unknown"}`);
|
|
10704
|
+
lines.push(`- **ID:** \`${p._id}\``);
|
|
10705
|
+
lines.push(`- **Status:** ${p.status} (${urgency})`);
|
|
10706
|
+
lines.push(`- **Operation:** ${p.proposedOperation}`);
|
|
10707
|
+
lines.push(`- **Reasoning:** ${p.reasoning}`);
|
|
10708
|
+
lines.push(`- **Proposed by:** ${p.proposedBy}`);
|
|
10709
|
+
if (p.objectionReason) {
|
|
10710
|
+
lines.push(`- **Objection:** ${p.objectionReason}`);
|
|
10711
|
+
}
|
|
10712
|
+
lines.push("");
|
|
10713
|
+
}
|
|
10714
|
+
return successResult(
|
|
10715
|
+
lines.join("\n"),
|
|
10716
|
+
`Found ${proposals.length} consent proposal(s).`,
|
|
10717
|
+
{ proposals, count: proposals.length },
|
|
10718
|
+
[
|
|
10719
|
+
{
|
|
10720
|
+
tool: "governance-proposals",
|
|
10721
|
+
description: "Respond to a proposal",
|
|
10722
|
+
parameters: { action: "respond" }
|
|
10723
|
+
}
|
|
10724
|
+
]
|
|
10725
|
+
);
|
|
10726
|
+
}
|
|
10727
|
+
if (action === "count") {
|
|
10728
|
+
const result = await mcpQuery("governance.countOpenProposals");
|
|
10729
|
+
return successResult(
|
|
10730
|
+
result.count === 0 ? "No open consent proposals." : `${result.count} open consent proposal(s) awaiting review.`,
|
|
10731
|
+
`${result.count} open consent proposal(s).`,
|
|
10732
|
+
result,
|
|
10733
|
+
result.count > 0 ? [{
|
|
10734
|
+
tool: "governance-proposals",
|
|
10735
|
+
description: "List open proposals",
|
|
10736
|
+
parameters: { action: "list" }
|
|
10737
|
+
}] : void 0
|
|
10738
|
+
);
|
|
10739
|
+
}
|
|
10740
|
+
if (action === "respond") {
|
|
10741
|
+
requireWriteAccess();
|
|
10742
|
+
if (!proposalId) {
|
|
10743
|
+
return validationResult(
|
|
10744
|
+
"A proposalId is required for the respond action. Use list action to find proposal IDs."
|
|
10745
|
+
);
|
|
10746
|
+
}
|
|
10747
|
+
if (!verdict) {
|
|
10748
|
+
return validationResult(
|
|
10749
|
+
"A verdict (approve or reject) is required for the respond action."
|
|
10750
|
+
);
|
|
10751
|
+
}
|
|
10752
|
+
if (verdict === "reject" && !reason) {
|
|
10753
|
+
return validationResult(
|
|
10754
|
+
"A reason is required when rejecting a proposal. Explain what harm this change would cause."
|
|
10755
|
+
);
|
|
10756
|
+
}
|
|
10757
|
+
try {
|
|
10758
|
+
const result = await mcpMutation(
|
|
10759
|
+
"governance.respondToProposal",
|
|
10760
|
+
{
|
|
10761
|
+
proposalId,
|
|
10762
|
+
verdict,
|
|
10763
|
+
...reason ? { reason } : {}
|
|
10764
|
+
}
|
|
10765
|
+
);
|
|
10766
|
+
return successResult(
|
|
10767
|
+
result.message,
|
|
10768
|
+
result.message,
|
|
10769
|
+
result,
|
|
10770
|
+
[
|
|
10771
|
+
{
|
|
10772
|
+
tool: "governance-proposals",
|
|
10773
|
+
description: "List remaining proposals",
|
|
10774
|
+
parameters: { action: "list" }
|
|
10775
|
+
}
|
|
10776
|
+
]
|
|
10777
|
+
);
|
|
10778
|
+
} catch (err) {
|
|
10779
|
+
const msg = err?.message ?? String(err);
|
|
10780
|
+
return failureResult(
|
|
10781
|
+
msg,
|
|
10782
|
+
"PROPOSAL_ERROR",
|
|
10783
|
+
msg,
|
|
10784
|
+
"Use governance-proposals action=list to check proposal status.",
|
|
10785
|
+
[
|
|
10786
|
+
{
|
|
10787
|
+
tool: "governance-proposals",
|
|
10788
|
+
description: "List proposals",
|
|
10789
|
+
parameters: { action: "list" }
|
|
10790
|
+
}
|
|
10791
|
+
]
|
|
10792
|
+
);
|
|
10793
|
+
}
|
|
10794
|
+
}
|
|
10795
|
+
return validationResult("Unknown action. Valid actions: list, respond, count.");
|
|
10796
|
+
})
|
|
10797
|
+
);
|
|
10798
|
+
trackWriteTool(tool);
|
|
10799
|
+
}
|
|
10800
|
+
|
|
9909
10801
|
// src/resources/index.ts
|
|
9910
10802
|
import { existsSync as existsSync4 } from "fs";
|
|
9911
10803
|
import { dirname as dirname2, join, resolve as resolve5 } from "path";
|
|
@@ -10036,7 +10928,7 @@ var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
|
|
|
10036
10928
|
## Collection Prefixes
|
|
10037
10929
|
GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
|
|
10038
10930
|
DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
|
|
10039
|
-
BET (bets), INS (insights), ARCH (architecture),
|
|
10931
|
+
BET (bets), INS (insights), ARCH (architecture), TEAM (teams),
|
|
10040
10932
|
ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
|
|
10041
10933
|
|
|
10042
10934
|
## Valid Relation Types (21)
|
|
@@ -10350,12 +11242,12 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
|
|
|
10350
11242
|
}
|
|
10351
11243
|
|
|
10352
11244
|
// src/prompts/index.ts
|
|
10353
|
-
import { z as
|
|
11245
|
+
import { z as z24 } from "zod";
|
|
10354
11246
|
function registerPrompts(server) {
|
|
10355
11247
|
server.prompt(
|
|
10356
11248
|
"review-against-rules",
|
|
10357
11249
|
"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.",
|
|
10358
|
-
{ domain:
|
|
11250
|
+
{ domain: z24.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
|
|
10359
11251
|
async ({ domain }) => {
|
|
10360
11252
|
const entries = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
10361
11253
|
const rules = entries.filter((e) => e.data?.domain === domain);
|
|
@@ -10408,7 +11300,7 @@ Provide a structured review with a compliance status for each rule (COMPLIANT /
|
|
|
10408
11300
|
server.prompt(
|
|
10409
11301
|
"name-check",
|
|
10410
11302
|
"Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
|
|
10411
|
-
{ names:
|
|
11303
|
+
{ names: z24.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
|
|
10412
11304
|
async ({ names }) => {
|
|
10413
11305
|
const terms = await mcpQuery("chain.listEntries", { collectionSlug: "glossary" });
|
|
10414
11306
|
const glossaryContext = terms.map(
|
|
@@ -10444,7 +11336,7 @@ Format as a table: Name | Status | Canonical Form | Action Needed`
|
|
|
10444
11336
|
server.prompt(
|
|
10445
11337
|
"draft-decision-record",
|
|
10446
11338
|
"Draft a structured decision record from a description of what was decided. Includes context from recent decisions and relevant rules.",
|
|
10447
|
-
{ context:
|
|
11339
|
+
{ context: z24.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
|
|
10448
11340
|
async ({ context }) => {
|
|
10449
11341
|
const recentDecisions = await mcpQuery("chain.listEntries", { collectionSlug: "decisions" });
|
|
10450
11342
|
const sorted = [...recentDecisions].sort((a, b) => (b.data?.date ?? "") > (a.data?.date ?? "") ? 1 : -1).slice(0, 5);
|
|
@@ -10482,8 +11374,8 @@ After drafting, I can log it using the capture tool with collection "decisions".
|
|
|
10482
11374
|
"draft-rule-from-context",
|
|
10483
11375
|
"Draft a new business rule from an observation or discovery made while coding. Fetches existing rules for the domain to ensure consistency.",
|
|
10484
11376
|
{
|
|
10485
|
-
observation:
|
|
10486
|
-
domain:
|
|
11377
|
+
observation: z24.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
|
|
11378
|
+
domain: z24.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
|
|
10487
11379
|
},
|
|
10488
11380
|
async ({ observation, domain }) => {
|
|
10489
11381
|
const allRules = await mcpQuery("chain.listEntries", { collectionSlug: "business-rules" });
|
|
@@ -10625,6 +11517,8 @@ function createProductBrainServer() {
|
|
|
10625
11517
|
registerStartTools(server);
|
|
10626
11518
|
registerUsageTools(server);
|
|
10627
11519
|
registerFacilitateTools(server);
|
|
11520
|
+
registerAuditTools(server);
|
|
11521
|
+
registerGovernanceTools(server);
|
|
10628
11522
|
registerResources(server);
|
|
10629
11523
|
registerPrompts(server);
|
|
10630
11524
|
return server;
|
|
@@ -10634,4 +11528,4 @@ export {
|
|
|
10634
11528
|
SERVER_VERSION,
|
|
10635
11529
|
createProductBrainServer
|
|
10636
11530
|
};
|
|
10637
|
-
//# sourceMappingURL=chunk-
|
|
11531
|
+
//# sourceMappingURL=chunk-ZFODPVNH.js.map
|