@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.
@@ -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-DHBJFEAT.js";
47
+ } from "./chunk-G4WJIWXX.js";
47
48
  import {
49
+ trackChainEntryCommitted,
48
50
  trackKnowledgeGap,
49
51
  trackQualityCheck,
50
52
  trackQualityVerdict
51
- } from "./chunk-RQXM3TCI.js";
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("Short human-readable summary for history (e.g. 'Updated description to F1-themed copy'). If omitted, a friendly default is generated from fields changed.")
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-ATJ5F7R2.js");
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(3).default(2).describe("Relation traversal depth (1=direct only, 2=default, 3=wide net)"),
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. Three 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.",
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.taskAwareGatherContext", {
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:** ${result2.seeds?.map((s) => s.entryId ?? s.name).join(", ") ?? "none"}`,
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
- console.warn("[wrapup] batch relation creation failed \u2014 links from this session may be missing:", err);
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. **Match the energy.** Be warm but structured. This is a ceremony, not a checklist.
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: "bets",
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: "bets",
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 bets collection (DEC-245).
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: "bets"
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: "bets",
4162
- description: "Bet status and no-go updates during finalize."
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-ATJ5F7R2.js");
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: report }],
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 === "bets"
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: "bets" });
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, circles, principles, policies, decisions, proposals, business rules", filePaths: "src/routes/roles/, src/routes/circles/, src/routes/decisions/, src/routes/proposals/", owner: "Governance", rationale: "Features layer because these are governance PAGES \u2014 list views and detail views for roles, circles, decisions, proposals. Why not Core? Because the governance ENGINE (versioning, consent, IDM) IS in Core. These pages are the user-facing window into governance data. The logic doesn't live here, only the rendering." } },
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
- return {
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
- lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
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
- return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
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
- lines.push(`- \`${id}\` (score ${e.score})${coll}${e.name !== id ? ` \u2014 ${e.name}` : ""}`);
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), CIR (circles),
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 z22 } from "zod";
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: z22.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
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: z22.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
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: z22.string().describe("Description of the decision (e.g. 'We decided to use MRSL v3.1 as the conformance baseline because...')") },
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: z22.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
10486
- domain: z22.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
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-O337N4MC.js.map
11531
+ //# sourceMappingURL=chunk-ZFODPVNH.js.map