@productbrain/mcp 0.0.1-beta.62 → 0.0.1-beta.63

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.
@@ -38,11 +38,12 @@ import {
38
38
  unknownAction,
39
39
  validationResult,
40
40
  withEnvelope
41
- } from "./chunk-4M3SUZHX.js";
41
+ } from "./chunk-DM5DZC7B.js";
42
42
  import {
43
+ trackKnowledgeGap,
43
44
  trackQualityCheck,
44
45
  trackQualityVerdict
45
- } from "./chunk-MRIO53BY.js";
46
+ } from "./chunk-RQXM3TCI.js";
46
47
 
47
48
  // src/server.ts
48
49
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -232,7 +233,7 @@ ${formatted}` }],
232
233
  },
233
234
  withEnvelope(async ({ entryId }) => {
234
235
  requireWriteAccess();
235
- const { runContradictionCheck } = await import("./smart-capture-NXLUISUD.js");
236
+ const { runContradictionCheck } = await import("./smart-capture-NYOKGAFS.js");
236
237
  const entry = await mcpQuery("chain.getEntry", { entryId });
237
238
  if (!entry) {
238
239
  return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
@@ -352,6 +353,43 @@ ${formatted}` }],
352
353
 
353
354
  // src/tools/entries.ts
354
355
  import { z as z2 } from "zod";
356
+
357
+ // src/gap-store.ts
358
+ var SESSION_GAPS = [];
359
+ var DEDUP_WINDOW_MS = 6e4;
360
+ function isDuplicate(gap) {
361
+ const cutoff = gap.timestamp - DEDUP_WINDOW_MS;
362
+ return SESSION_GAPS.some(
363
+ (existing) => existing.query === gap.query && existing.tool === gap.tool && existing.action === gap.action && existing.timestamp >= cutoff
364
+ );
365
+ }
366
+ function recordGap(gap) {
367
+ const timestamped = { ...gap, timestamp: Date.now() };
368
+ if (isDuplicate(timestamped)) return false;
369
+ SESSION_GAPS.push(timestamped);
370
+ return true;
371
+ }
372
+ function getSessionGaps() {
373
+ return SESSION_GAPS;
374
+ }
375
+ function getTopGaps(limit = 5) {
376
+ const counts = /* @__PURE__ */ new Map();
377
+ for (const gap of SESSION_GAPS) {
378
+ const key = gap.query.toLowerCase().trim();
379
+ const existing = counts.get(key);
380
+ if (existing) {
381
+ existing.count++;
382
+ } else {
383
+ counts.set(key, { count: 1, gapType: gap.gapType });
384
+ }
385
+ }
386
+ return [...counts.entries()].map(([query, { count, gapType }]) => ({ query, count, gapType })).sort((a, b) => b.count - a.count).slice(0, limit);
387
+ }
388
+ function clearSessionGaps() {
389
+ SESSION_GAPS.length = 0;
390
+ }
391
+
392
+ // src/tools/entries.ts
355
393
  function sanitizeEntryData(data) {
356
394
  if (!data || typeof data !== "object") return void 0;
357
395
  const filtered = Object.fromEntries(
@@ -664,15 +702,23 @@ async function handleSearch(server, query, collection, status) {
664
702
  mcpQuery("chain.listCollections")
665
703
  ]);
666
704
  if (results.length === 0) {
705
+ const recorded = recordGap({ query, tool: "entries", action: "search", gapType: "search_zero", collectionScope: collection });
706
+ if (recorded) {
707
+ getWorkspaceId().then((wsId) => trackKnowledgeGap(wsId, { query, tool: "entries", action: "search", gap_type: "search_zero", collection_scope: collection })).catch(() => {
708
+ });
709
+ }
667
710
  return {
668
711
  content: [{
669
712
  type: "text",
670
- text: `No results for "${query}"${scope}. Try a broader search or use \`collections action=list\` to see available data.`
713
+ text: `"${query}" is not yet on the Chain${scope}. Try a broader search or use \`collections action=list\` to browse available knowledge.`
671
714
  }],
672
715
  structuredContent: success(
673
- `No results for '${query}'${scope}.`,
716
+ `'${query}' is not yet on the Chain${scope}.`,
674
717
  { entries: [], total: 0, query },
675
- [{ tool: "collections", description: "List collections", parameters: { action: "list" } }]
718
+ [
719
+ { tool: "entries", description: "Try a broader search", parameters: { action: "search", query: query.split(" ")[0] } },
720
+ { tool: "collections", description: "Browse available collections", parameters: { action: "list" } }
721
+ ]
676
722
  )
677
723
  };
678
724
  }
@@ -1253,6 +1299,11 @@ async function handleGather(server, entryId, task, mode, maxHops, maxResults) {
1253
1299
  return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
1254
1300
  }
1255
1301
  if (result2.context.length === 0) {
1302
+ const recorded = recordGap({ query: result2.root.entryId, tool: "context", action: "gather", gapType: "context_graph_empty" });
1303
+ if (recorded) {
1304
+ getWorkspaceId().then((wsId) => trackKnowledgeGap(wsId, { query: result2.root.entryId, tool: "context", action: "gather", gap_type: "context_graph_empty" })).catch(() => {
1305
+ });
1306
+ }
1256
1307
  return {
1257
1308
  content: [{
1258
1309
  type: "text",
@@ -1330,6 +1381,12 @@ Use \`graph action=suggest\` to discover potential connections.`
1330
1381
  maxResults
1331
1382
  });
1332
1383
  if (!result2 || result2.totalFound === 0) {
1384
+ const taskQuery = task.slice(0, 200);
1385
+ const recorded = recordGap({ query: taskQuery, tool: "context", action: "gather", gapType: "context_task_empty" });
1386
+ if (recorded) {
1387
+ getWorkspaceId().then((wsId) => trackKnowledgeGap(wsId, { query: taskQuery, tool: "context", action: "gather", gap_type: "context_task_empty" })).catch(() => {
1388
+ });
1389
+ }
1333
1390
  return {
1334
1391
  content: [{
1335
1392
  type: "text",
@@ -1337,15 +1394,15 @@ Use \`graph action=suggest\` to discover potential connections.`
1337
1394
 
1338
1395
  **Confidence:** None
1339
1396
 
1340
- No context found for this task. The chain may not cover this area yet.
1397
+ The Chain doesn't cover this area yet. If you discover relevant knowledge during this task, use \`capture\` with a name and description to add it.
1341
1398
 
1342
- _Consider capturing domain knowledge discovered during this task via \`capture\`._`
1399
+ _This helps future sessions start with better context._`
1343
1400
  }],
1344
1401
  structuredContent: failure(
1345
1402
  "NOT_FOUND",
1346
- "No context found for this task.",
1347
- "The chain may not cover this area. Capture domain knowledge via capture.",
1348
- [{ tool: "capture", description: "Capture knowledge", parameters: {} }]
1403
+ "The Chain doesn't cover this area yet.",
1404
+ "If you discover relevant knowledge during this task, capture it with a name and description.",
1405
+ [{ tool: "entries", description: "Search for related terms", parameters: { action: "search", query: task.split(" ").slice(0, 3).join(" ") } }]
1349
1406
  )
1350
1407
  };
1351
1408
  }
@@ -1398,6 +1455,11 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
1398
1455
  return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
1399
1456
  }
1400
1457
  if (result.related.length === 0) {
1458
+ const recorded = recordGap({ query: result.root.entryId, tool: "context", action: "gather", gapType: "context_entry_isolated" });
1459
+ if (recorded) {
1460
+ getWorkspaceId().then((wsId) => trackKnowledgeGap(wsId, { query: result.root.entryId, tool: "context", action: "gather", gap_type: "context_entry_isolated" })).catch(() => {
1461
+ });
1462
+ }
1401
1463
  return {
1402
1464
  content: [{
1403
1465
  type: "text",
@@ -2031,6 +2093,18 @@ async function runWrapupReview() {
2031
2093
  }
2032
2094
  lines.push("");
2033
2095
  }
2096
+ const topGaps = getTopGaps(5);
2097
+ if (topGaps.length > 0) {
2098
+ lines.push("### Learning Opportunities", "");
2099
+ lines.push(`This session, agents looked for ${topGaps.length} topic${topGaps.length === 1 ? "" : "s"} not yet on the Chain:`, "");
2100
+ for (const gap of topGaps) {
2101
+ const freq = gap.count > 1 ? ` _(${gap.count}x)_` : "";
2102
+ lines.push(`- **${gap.query}** \u2014 not yet captured${freq}`);
2103
+ }
2104
+ lines.push("");
2105
+ lines.push("_Capturing these will make future sessions more effective._");
2106
+ lines.push("");
2107
+ }
2034
2108
  if (data.drafts.length > 0) {
2035
2109
  const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
2036
2110
  lines.push("### Actions", "");
@@ -2038,7 +2112,8 @@ async function runWrapupReview() {
2038
2112
  lines.push("- **Commit all:** call `session-wrapup` with action `commit-all`");
2039
2113
  lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
2040
2114
  }
2041
- return { text: lines.join("\n"), data, suggestions };
2115
+ const gapCount = getSessionGaps().length;
2116
+ return { text: lines.join("\n"), data, suggestions, gapCount };
2042
2117
  }
2043
2118
  async function runWrapupCommitAll(data, cachedSuggestions) {
2044
2119
  requireWriteAccess();
@@ -2163,7 +2238,7 @@ function registerWrapupTools(server) {
2163
2238
  }
2164
2239
  );
2165
2240
  }
2166
- const { text, data, suggestions, failureCode } = await runWrapupReview();
2241
+ const { text, data, suggestions, failureCode, gapCount } = await runWrapupReview();
2167
2242
  lastReviewData = data;
2168
2243
  lastReviewSuggestions = suggestions;
2169
2244
  lastReviewSessionId = getAgentSessionId();
@@ -2174,14 +2249,16 @@ ${text}` : text;
2174
2249
  return failureResult(fullText, failureCode, text);
2175
2250
  }
2176
2251
  const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
2252
+ const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
2177
2253
  return successResult(
2178
2254
  fullText,
2179
- `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions.`,
2255
+ `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}.`,
2180
2256
  {
2181
2257
  drafts: data.drafts.length,
2182
2258
  committed: data.committed.length,
2183
2259
  uncommitted: data.summary.uncommitted,
2184
- suggestedLinks: suggestions.length
2260
+ suggestedLinks: suggestions.length,
2261
+ knowledgeGaps: gapCount ?? 0
2185
2262
  },
2186
2263
  next
2187
2264
  );
@@ -2296,6 +2373,7 @@ async function handleClose() {
2296
2373
  }
2297
2374
  }
2298
2375
  await closeAgentSession();
2376
+ clearSessionGaps();
2299
2377
  const lines = [
2300
2378
  `Session ${sessionId} closed.`,
2301
2379
  ""
@@ -3469,7 +3547,7 @@ var workflowsSchema = z11.object({
3469
3547
  "'list': browse available workflows. 'checkpoint': record round output or final summary. 'get-run': inspect a persisted workflow run."
3470
3548
  ),
3471
3549
  workflowId: z11.string().optional().describe("Workflow ID for checkpoint, e.g. 'retro'"),
3472
- runId: z11.string().optional().describe("Workflow run ID to inspect. Optional for action='get-run' when workflowId is provided."),
3550
+ runId: z11.string().optional().describe("Workflow run ID: for get-run, which run to load; for checkpoint, target this run (avoids session drift \u2014 pass runId from get-run)."),
3473
3551
  roundId: z11.string().optional().describe("Round ID for checkpoint, e.g. 'what-went-well'"),
3474
3552
  output: z11.union([z11.string(), workflowRunOutputInputSchema]).optional().describe(
3475
3553
  "The round's output \u2014 either legacy synthesized text or a typed workflow run payload."
@@ -3504,6 +3582,38 @@ function validateFinalWorkflowCheckpoint(workflow, roundId) {
3504
3582
  }
3505
3583
  return null;
3506
3584
  }
3585
+ function levenshtein(a, b) {
3586
+ const m = a.length;
3587
+ const n = b.length;
3588
+ const dp = Array.from(
3589
+ { length: m + 1 },
3590
+ (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
3591
+ );
3592
+ for (let i = 1; i <= m; i++) {
3593
+ for (let j = 1; j <= n; j++) {
3594
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
3595
+ }
3596
+ }
3597
+ return dp[m][n];
3598
+ }
3599
+ function findClosestRound(input, rounds) {
3600
+ if (rounds.length === 0) return null;
3601
+ const substringMatch = rounds.find(
3602
+ (r) => r.id.includes(input) || input.includes(r.id)
3603
+ );
3604
+ if (substringMatch) return substringMatch;
3605
+ let best = rounds[0];
3606
+ let bestDist = levenshtein(input, best.id);
3607
+ for (const r of rounds.slice(1)) {
3608
+ const dist = levenshtein(input, r.id);
3609
+ if (dist < bestDist) {
3610
+ bestDist = dist;
3611
+ best = r;
3612
+ }
3613
+ }
3614
+ const threshold = Math.ceil(Math.max(input.length, best.id.length) / 2);
3615
+ return bestDist <= threshold ? best : null;
3616
+ }
3507
3617
  function registerWorkflowTools(server) {
3508
3618
  const workflowsTool = server.registerTool(
3509
3619
  "workflows",
@@ -3554,7 +3664,8 @@ function registerWorkflowTools(server) {
3554
3664
  restart,
3555
3665
  summaryName,
3556
3666
  summaryDescription,
3557
- summaryEntryId
3667
+ summaryEntryId,
3668
+ runId
3558
3669
  );
3559
3670
  }
3560
3671
  return unknownAction(action, WORKFLOWS_ACTIONS);
@@ -3662,7 +3773,7 @@ async function handleGetRun(runId, workflowId) {
3662
3773
  )
3663
3774
  };
3664
3775
  }
3665
- async function handleCheckpoint(workflowId, roundId, output, isFinal, restart, summaryName, summaryDescription, summaryEntryId) {
3776
+ async function handleCheckpoint(workflowId, roundId, output, isFinal, restart, summaryName, summaryDescription, summaryEntryId, runId) {
3666
3777
  const wf = getWorkflow(workflowId);
3667
3778
  if (!wf) {
3668
3779
  return {
@@ -3683,18 +3794,33 @@ This checkpoint was NOT saved. Continue the conversation \u2014 the facilitator
3683
3794
  const round = wf.rounds.find((r) => r.id === roundId);
3684
3795
  if (!round) {
3685
3796
  const availableRounds = wf.rounds.map((r) => r.id).join(", ");
3797
+ const closest = findClosestRound(roundId, wf.rounds);
3798
+ const correctionActions = closest ? [
3799
+ {
3800
+ tool: "workflows",
3801
+ description: `Retry with corrected round ID '${closest.id}'`,
3802
+ parameters: { action: "checkpoint", workflowId, roundId: closest.id }
3803
+ }
3804
+ ] : [
3805
+ {
3806
+ tool: "workflows",
3807
+ description: "List workflows to inspect round IDs",
3808
+ parameters: { action: "list" }
3809
+ }
3810
+ ];
3811
+ const suggestionHint = closest ? ` Did you mean '${closest.id}'?` : "";
3686
3812
  return {
3687
3813
  content: [{
3688
3814
  type: "text",
3689
- text: `Round "${roundId}" not found in workflow "${workflowId}". Available rounds: ${availableRounds}.
3815
+ text: `Round "${roundId}" not found in workflow "${workflowId}".${suggestionHint} Available rounds: ${availableRounds}.
3690
3816
 
3691
3817
  This checkpoint was NOT saved. The conversation context is preserved \u2014 continue facilitating.`
3692
3818
  }],
3693
3819
  structuredContent: failure(
3694
3820
  "NOT_FOUND",
3695
3821
  `Round '${roundId}' not found in workflow '${workflowId}'.`,
3696
- `Available rounds: ${availableRounds}`,
3697
- [{ tool: "workflows", description: "List workflows", parameters: { action: "list" } }]
3822
+ closest ? `Did you mean '${closest.id}'? Available rounds: ${availableRounds}` : `Available rounds: ${availableRounds}`,
3823
+ correctionActions
3698
3824
  )
3699
3825
  };
3700
3826
  }
@@ -3755,7 +3881,8 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3755
3881
  normalizedOutput,
3756
3882
  agentSessionId,
3757
3883
  summaryEntryId,
3758
- lines
3884
+ lines,
3885
+ runId
3759
3886
  );
3760
3887
  }
3761
3888
  const existingRun = await mcpQuery(
@@ -3888,6 +4015,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3888
4015
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
3889
4016
  },
3890
4017
  restart,
4018
+ ...runId ? { runId } : {},
3891
4019
  ...summaryEntryId ? { summaryEntryId } : {}
3892
4020
  });
3893
4021
  lines.push(
@@ -3952,7 +4080,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
3952
4080
  }
3953
4081
  }
3954
4082
  }
3955
- async function handleFacilitatedFinalization(wf, descriptor, roundId, normalizedOutput, agentSessionId, summaryEntryId, lines) {
4083
+ async function handleFacilitatedFinalization(wf, descriptor, roundId, normalizedOutput, agentSessionId, summaryEntryId, lines, runId) {
3956
4084
  const template = descriptor ? getWorkflowTemplateMetadata(descriptor) : { slug: wf.id, name: wf.name };
3957
4085
  try {
3958
4086
  const checkpointResult = await mcpMutation("chainwork.recordWorkflowCheckpoint", {
@@ -3974,7 +4102,8 @@ async function handleFacilitatedFinalization(wf, descriptor, roundId, normalized
3974
4102
  output: normalizedOutput,
3975
4103
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
3976
4104
  },
3977
- summaryEntryId
4105
+ summaryEntryId,
4106
+ ...runId ? { runId } : {}
3978
4107
  });
3979
4108
  const linked = checkpointResult.summary?.linked ?? false;
3980
4109
  lines.push(
@@ -6176,7 +6305,7 @@ async function handleCommitConstellation(args) {
6176
6305
  }
6177
6306
  let contradictionWarnings = [];
6178
6307
  try {
6179
- const { runContradictionCheck } = await import("./smart-capture-NXLUISUD.js");
6308
+ const { runContradictionCheck } = await import("./smart-capture-NYOKGAFS.js");
6180
6309
  const descField = betData.problem ?? betData.description ?? "";
6181
6310
  contradictionWarnings = await runContradictionCheck(
6182
6311
  betEntry.name ?? betId,
@@ -7024,7 +7153,7 @@ function extractionToBatchEntries(extracted) {
7024
7153
  }
7025
7154
  function getInterviewInstructions(workspaceName) {
7026
7155
  return {
7027
- systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and feed it to batch-capture. Everything stays pending \u2014 the user confirms before anything is committed.`,
7156
+ systemPrompt: `You are activating the **${workspaceName}** Product Brain. Ask 1\u20132 focused questions, then extract structured knowledge and feed it to batch-capture. In Open governance mode, entries are committed automatically. In consensus/role mode, they stay as drafts for review.`,
7028
7157
  question1: `**Q1 \u2014 What are you building and for whom?** Describe your product in 1\u20132 sentences and who it's for. (This becomes your Product Vision and primary Audience.)`,
7029
7158
  question2: `**Q2 (optional) \u2014 What's your tech stack or key domain terms?** Name a few core technologies or terms your team uses. (These become Architecture + Glossary entries.)`,
7030
7159
  extractionGuidance: `After the user answers, extract:
@@ -7042,9 +7171,25 @@ Map to batch-capture entries:
7042
7171
  - keyTerms \u2192 glossary (1 per term)
7043
7172
  - keyDecisions \u2192 decisions (1 per decision)
7044
7173
  - tensions \u2192 tensions (1 per tension)`,
7045
- captureInstructions: `Call batch-capture with the extracted entries. Keep descriptions concise (1\u20132 sentences each). Prefer 8 good entries over 15 mediocre ones \u2014 quality over volume. If batch-capture returns failedEntries, tell the user and retry those individually.`,
7046
- qualityNote: `After capturing, call graph action=suggest on 2\u20133 key entries to build the graph. Present the drafts grouped by collection. Ask: "Commit all, or review first?" Only call commit-entry when the user confirms.`,
7047
- scanOffer: `After the interview capture, offer the Project Scan: "I can also scan your project files (README.md, package.json, .cursorrules) to extract more knowledge automatically. Want me to? It takes about 2 minutes and stays as drafts."`
7174
+ captureInstructions: `Call batch-capture with the extracted entries. Omit \`autoCommit\` to follow workspace governance automatically, or pass \`autoCommit: false\` if the user wants a review-first pass. Keep descriptions concise (1\u20132 sentences each). Prefer 8 good entries over 15 mediocre ones \u2014 quality over volume. If batch-capture returns failedEntries, tell the user and retry those individually.`,
7175
+ qualityNote: (
7176
+ // FEAT-149: Retrieval-First Proof Moment.
7177
+ // After capture, demonstrate retrieval instead of offering commit ceremony.
7178
+ // The aha moment is the AI using the user's knowledge, not the user entering it.
7179
+ `After capturing, silently call graph action=suggest on the strategy or architecture entry to build connections.
7180
+
7181
+ **Then demonstrate the proof moment \u2014 this is the most important step:**
7182
+ Based on what the user told you, invent a specific, plausible development task they might actually do next (e.g. "add user authentication" for a SaaS app, "build the recommendation engine" for a marketplace, "set up the API layer" for a developer tool). Call \`context action=gather task="<your invented task>"\` to load their captured knowledge.
7183
+
7184
+ Present the results conversationally:
7185
+ "Here's what I already know about your product that I didn't know 5 minutes ago \u2014 if you asked me to **<task>**, I'd bring this context:"
7186
+
7187
+ Then list the key entries that context returned: their product vision, audience, tech decisions, glossary terms. Keep it concrete \u2014 entry names and one-line summaries, not abstract descriptions.
7188
+
7189
+ End with: "**Try it \u2014 ask me to help with something on your product.** I'll use everything I just learned."`
7190
+ ),
7191
+ scanOffer: `After the proof moment, offer the codebase scan as a way to deepen the knowledge:
7192
+ "Want me to learn even more? I can scan your project files (README, package.json, source structure) and pick up technical decisions, conventions, and architecture that I missed. Takes about 2 minutes."`
7048
7193
  };
7049
7194
  }
7050
7195
  function buildInterviewResponse(workspaceName) {
@@ -7054,15 +7199,11 @@ function buildInterviewResponse(workspaceName) {
7054
7199
  "",
7055
7200
  instructions.systemPrompt,
7056
7201
  "",
7057
- "I'll ask you 1\u20132 questions. Your answers become the first entries in your knowledge graph \u2014 all pending until you confirm.",
7202
+ "I'll ask you 1\u20132 questions. Your answers become the foundation of your product knowledge.",
7058
7203
  "",
7059
7204
  `**${instructions.question1}**`,
7060
7205
  "",
7061
- "_Take your time \u2014 a sentence or two is enough. I'll extract the structure from your answer._",
7062
- "",
7063
- `> ${instructions.scanOffer}`,
7064
- "",
7065
- "_Everything stays pending until you confirm._"
7206
+ "_Take your time \u2014 a sentence or two is enough. I'll extract the structure from your answer._"
7066
7207
  ].join("\n");
7067
7208
  }
7068
7209
 
@@ -7087,7 +7228,7 @@ function registerStartTools(server) {
7087
7228
  "start",
7088
7229
  {
7089
7230
  title: "Start Product Brain",
7090
- description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace (blank)**: scans your codebase and seeds knowledge from what it finds.\n- **Early workspace (seeded)**: picks up where you left off with the top gaps to fill.\n- **Active workspace (grounded/connected)**: standup briefing with recent activity and open items.\n\nUse this as your first call. Replaces the need to call orient or health separately.",
7231
+ description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace (blank)**: asks what you're building, captures the essentials, and can scan your codebase if useful.\n- **Early workspace (seeded)**: picks up where you left off with the top gaps to fill.\n- **Active workspace (grounded/connected)**: standup briefing with recent activity and open items.\n\nUse this as your first call. Replaces the need to call orient or health separately.",
7091
7232
  inputSchema: startSchema,
7092
7233
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
7093
7234
  },
@@ -7192,14 +7333,15 @@ function buildBlankResponse(wsCtx, sessionCtx) {
7192
7333
  instructions.extractionGuidance,
7193
7334
  "",
7194
7335
  instructions.captureInstructions,
7195
- "Omit `autoCommit` \u2014 the capture tool auto-commits in Open mode and creates drafts in consensus/role mode.",
7336
+ "Omit `autoCommit` \u2014 capture tools auto-commit in Open mode and create drafts in consensus/role mode.",
7337
+ "",
7338
+ instructions.qualityNote,
7196
7339
  "",
7197
- "**Then offer the codebase scan with context:**",
7198
- `"Now that I know what you're building, I can scan your project files and find technical decisions, features, and conventions that are relevant to **your** product. Want me to?"`,
7340
+ instructions.scanOffer,
7199
7341
  "",
7200
7342
  "If they say yes, scan README.md, package.json/pyproject.toml/Cargo.toml, and top-level source dirs.",
7201
7343
  "Infer entries WITH the product context you now have. Present as a numbered list for confirmation.",
7202
- "Capture confirmed entries (auto-committed in Open mode).",
7344
+ "Do not capture anything until the user confirms what to keep. Then capture only the confirmed entries and let normal workspace governance apply.",
7203
7345
  "",
7204
7346
  "## Path 2: Scan first",
7205
7347
  "",
@@ -7209,17 +7351,18 @@ function buildBlankResponse(wsCtx, sessionCtx) {
7209
7351
  "3. Top-level source directories and their purpose",
7210
7352
  "",
7211
7353
  "Infer 5\u20138 product knowledge entries (product name, tech decisions, features, conventions).",
7212
- "Present as a numbered list. Ask which to keep. Capture confirmed entries (auto-committed in Open mode).",
7354
+ "Present as a numbered list. Ask which to keep. Only after confirmation, capture the confirmed entries and let normal workspace governance apply.",
7213
7355
  "",
7214
7356
  "**If the codebase is sparse** (no README, empty project): fall back to Path 1 (ask the user about their product).",
7215
7357
  "",
7216
7358
  "_Prefer 5 specific entries over 8 generic ones. Skip anything that would apply to any codebase._",
7217
7359
  "",
7218
- "## Path 3: Preset",
7360
+ "**After scan capture, run the proof moment (same as Path 1):**",
7361
+ instructions.qualityNote,
7219
7362
  "",
7220
- "If the user picks a preset, call `start` again with `preset: '<chosen-preset>'`.",
7363
+ "## Path 3: Preset",
7221
7364
  "",
7222
- instructions.qualityNote
7365
+ "If the user picks a preset, call `start` again with `preset: '<chosen-preset>'`."
7223
7366
  ].join("\n");
7224
7367
  return {
7225
7368
  text,
@@ -7256,6 +7399,19 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
7256
7399
  } else {
7257
7400
  lines.push("No gaps detected \u2014 your workspace is filling up nicely. What would you like to work on?");
7258
7401
  }
7402
+ const knowledgeGaps = getTopGaps(3);
7403
+ if (knowledgeGaps.length > 0) {
7404
+ lines.push("");
7405
+ lines.push("### Learning Opportunities");
7406
+ lines.push("_Topics agents recently looked for but aren't on the Chain yet:_");
7407
+ lines.push("");
7408
+ for (const gap of knowledgeGaps) {
7409
+ const freq = gap.count > 1 ? ` _(${gap.count}x)_` : "";
7410
+ lines.push(`- **${gap.query}**${freq}`);
7411
+ }
7412
+ lines.push("");
7413
+ lines.push("_Capturing these will make future sessions more effective._");
7414
+ }
7259
7415
  const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId);
7260
7416
  const next = gaps.length > 0 ? [{ tool: "capture", description: `Fill gap: ${gaps[0].label}`, parameters: {} }] : [];
7261
7417
  return {
@@ -7395,6 +7551,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
7395
7551
  const isLowReadiness = readiness !== null && readiness.score < 50;
7396
7552
  const isHighReadiness = readiness !== null && readiness.score >= 50;
7397
7553
  const stage = readiness?.stage ?? null;
7554
+ const captureBehaviorNote = wsFullCtx.governanceMode === "open" ? "_In Open mode, user-authored captures commit immediately unless you ask me to keep them as drafts._" : "_Everything I capture stays pending until you confirm._";
7398
7555
  lines.push(`# ${wsCtx.workspaceName}`);
7399
7556
  lines.push("_Product Brain is ready._");
7400
7557
  lines.push("");
@@ -7415,7 +7572,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
7415
7572
  lines.push("");
7416
7573
  lines.push(nextAction.cta);
7417
7574
  lines.push("");
7418
- lines.push("_Everything I capture stays pending until you confirm._");
7575
+ lines.push(captureBehaviorNote);
7419
7576
  lines.push("");
7420
7577
  const remainingGaps = (readiness.gaps ?? []).length - 1;
7421
7578
  if (remainingGaps > 0 || openTensions.length > 0) {
@@ -9957,6 +10114,7 @@ function registerHealthTools(server) {
9957
10114
  if (isLowReadiness) {
9958
10115
  lines.push(`**Brain stage: ${orientStage}.**`);
9959
10116
  lines.push("");
10117
+ const captureBehaviorNote = (readiness?.governanceMode ?? wsCtx?.governanceMode ?? "open") === "open" ? "_In Open mode, user-authored captures commit immediately unless you ask me to keep them as drafts._" : '_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._';
9960
10118
  const gaps = readiness.gaps ?? [];
9961
10119
  if (gaps.length > 0) {
9962
10120
  const gap = gaps[0];
@@ -9966,7 +10124,7 @@ function registerHealthTools(server) {
9966
10124
  lines.push("");
9967
10125
  lines.push(cta);
9968
10126
  lines.push("");
9969
- lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
10127
+ lines.push(captureBehaviorNote);
9970
10128
  lines.push("");
9971
10129
  const remainingGaps = gaps.length - 1;
9972
10130
  if (remainingGaps > 0 || openTensions.length > 0) {
@@ -11353,7 +11511,7 @@ If ready, ask the user to confirm. If not, suggest specific improvements.
11353
11511
  );
11354
11512
  server.prompt(
11355
11513
  "project-scan",
11356
- "Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack, key terms, and decisions into batch-capture entries. All entries land as drafts \u2014 the user reviews and commits.",
11514
+ "Scan local project files to extract structured knowledge for Product Brain. The IDE agent reads README.md, package.json, .cursorrules, folder structure, and recent git history, then extracts vision, tech stack, key terms, and decisions into batch-capture entries. The trust boundary is confirmation before capture: only capture entries the user explicitly keeps, then let normal workspace governance apply.",
11357
11515
  {
11358
11516
  workspaceContext: z21.string().optional().describe("Optional context about the workspace preset and existing collections (paste from start/orient output)")
11359
11517
  },
@@ -11431,6 +11589,9 @@ Before calling batch-capture, check your extraction:
11431
11589
 
11432
11590
  If validation fails for any entry, drop it rather than sending malformed data.
11433
11591
 
11592
+ Then present the inferred entries as a numbered list. Ask which to keep, and do NOT call batch-capture yet.
11593
+ Only after the user confirms which entries to keep should you capture the confirmed entries.
11594
+
11434
11595
  ## Step 4: Map to batch-capture
11435
11596
 
11436
11597
  Map your extracted data to entries using these rules:
@@ -11448,19 +11609,22 @@ ${exampleOutput}
11448
11609
 
11449
11610
  ## Step 5: Call batch-capture
11450
11611
 
11612
+ After the user confirms which entries to keep, call batch-capture with only those confirmed entries.
11613
+ Omit \`autoCommit\` to let normal workspace governance apply.
11614
+
11451
11615
  \`\`\`
11452
- batch-capture entries=[...your mapped entries...]
11616
+ batch-capture entries=[...your confirmed entries...]
11453
11617
  \`\`\`
11454
11618
 
11455
11619
  If \`failed > 0\` in the response, inspect \`failedEntries\` and retry those individually.
11456
11620
 
11457
- ## Step 6: Connect + Review
11621
+ ## Step 6: Connect + Prove Value
11458
11622
 
11459
11623
  After capture:
11460
11624
  1. Call \`graph action=suggest\` on 2\u20133 key entries (vision, architecture, a decision)
11461
- 2. Present entries grouped by collection
11462
- 3. Ask: "Commit all, or would you like to review first?"
11463
- 4. Only call \`commit-entry\` when the user confirms
11625
+ 2. Invent a plausible next task and call \`context action=gather task="<that task>"\`
11626
+ 3. Present what Product Brain now knows in that task context \u2014 this is the proof moment
11627
+ 4. Then show one clear result summary so the user sees exactly what was added
11464
11628
 
11465
11629
  **Begin with Step 1 now.** Read the files, then report what you found before extracting.`
11466
11630
  }
@@ -11581,4 +11745,4 @@ export {
11581
11745
  SERVER_VERSION,
11582
11746
  createProductBrainServer
11583
11747
  };
11584
- //# sourceMappingURL=chunk-MH5MYP76.js.map
11748
+ //# sourceMappingURL=chunk-GKMXGRJW.js.map