@productbrain/mcp 0.0.1-beta.160 → 0.0.1-beta.161

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.
@@ -1838,7 +1838,15 @@ var captureSchema = z2.object({
1838
1838
  sourceRef: z2.string().optional().describe("URI or path of the source document backing this entry (e.g. 'meeting-2026-03-28.md', 'import://batch-5'). Stored as top-level entry field, not in data."),
1839
1839
  sourceExcerpt: z2.string().optional().describe("Verbatim excerpt from the source that backs this entry's claims. Stored as top-level entry field, not in data."),
1840
1840
  // WP-316 S3: Preview gate — dry-run mode. Returns what would happen, no DB writes.
1841
- preview: z2.boolean().optional().describe("If true, validates the capture without writing. Returns what would happen. Default false.")
1841
+ preview: z2.boolean().optional().describe("If true, validates the capture without writing. Returns what would happen. Default false."),
1842
+ // WP-318 S2: Pre-write grounding — run link suggestion before creating entry.
1843
+ suggestOnly: z2.boolean().optional().describe(
1844
+ "If true, runs pre-write grounding (suggestLinksForCapture) and returns a groundingReport WITHOUT creating any entry. Use to preview graph participation before committing. Default false."
1845
+ ),
1846
+ // WP-318 S2: Format for groundingReport in response.
1847
+ format: z2.enum(["agent", "human"]).optional().describe(
1848
+ "Response format for grounding data. 'agent' (default): full JSON groundingReport. 'human': compressed summary string appended to the capture summary."
1849
+ )
1842
1850
  });
1843
1851
  var batchCaptureSchema = z2.object({
1844
1852
  entries: z2.array(z2.object({
@@ -2266,7 +2274,7 @@ function registerSmartCaptureTools(server) {
2266
2274
  inputSchema: captureSchema.shape,
2267
2275
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2268
2276
  },
2269
- withEnvelope(async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit, sourceRef, sourceExcerpt, preview }) => {
2277
+ withEnvelope(async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit, sourceRef, sourceExcerpt, preview, suggestOnly, format }) => {
2270
2278
  requireWriteAccess();
2271
2279
  const timingStart = Date.now();
2272
2280
  const wsCtx = await getWorkspaceContext();
@@ -2341,6 +2349,72 @@ Or use \`collections action=list\` to see available collections.`
2341
2349
  )
2342
2350
  };
2343
2351
  }
2352
+ const skipAutoDiscoveryEarly = links && links.length > 0;
2353
+ const groundingPromise = !skipAutoDiscoveryEarly && (name || description) ? mcpQuery("chain.suggestLinksForCapture", {
2354
+ entries: [{ name, description: description ?? "", collectionHint: resolvedCollection }],
2355
+ limit: 5,
2356
+ threshold: 2
2357
+ }).catch(() => null) : Promise.resolve(null);
2358
+ const groundingRaw = await groundingPromise;
2359
+ const groundingEntry = groundingRaw?.[0] ?? null;
2360
+ const groundingRelated = groundingEntry?.related ?? [];
2361
+ const groundingDuplicates = groundingEntry?.duplicates ?? [];
2362
+ const groundingGovernanceCandidates = [...groundingRelated, ...groundingDuplicates];
2363
+ const groundingGovernance = [];
2364
+ for (const c of groundingGovernanceCandidates) {
2365
+ if (groundingGovernance.some((g) => g.entryId === c.entryId)) continue;
2366
+ if (await isGoverned(c.collectionSlug)) {
2367
+ groundingGovernance.push({ entryId: c.entryId, name: c.name, collectionSlug: c.collectionSlug });
2368
+ }
2369
+ }
2370
+ const groundingReport = {
2371
+ related: groundingRelated.map((r) => ({
2372
+ entryId: r.entryId,
2373
+ name: r.name,
2374
+ collectionSlug: r.collectionSlug,
2375
+ overlapRatio: r.overlapRatio,
2376
+ recommendedRelationType: r.recommendedRelationType,
2377
+ reasoning: r.reasoning
2378
+ })),
2379
+ duplicates: groundingDuplicates.map((d) => ({
2380
+ entryId: d.entryId,
2381
+ name: d.name,
2382
+ collectionSlug: d.collectionSlug,
2383
+ matchType: d.matchType,
2384
+ overlapRatio: d.overlapRatio
2385
+ })),
2386
+ governance: groundingGovernance
2387
+ };
2388
+ if (suggestOnly) {
2389
+ const hasSuggestions = groundingReport.related.length > 0 || groundingReport.duplicates.length > 0;
2390
+ const summaryText = hasSuggestions ? `Grounding preview for "${name}": ${groundingReport.related.length} related, ${groundingReport.duplicates.length} possible duplicates. No entry created.` : `Grounding preview for "${name}": no similar entries found. Safe to capture.`;
2391
+ return {
2392
+ content: [{
2393
+ type: "text",
2394
+ text: `# Grounding Preview \u2014 No Entry Created
2395
+
2396
+ ${summaryText}
2397
+
2398
+ ` + (groundingReport.related.length > 0 ? `## Related entries
2399
+ ${groundingReport.related.map((r) => `- **${r.entryId}** ${r.name} [${r.collectionSlug}] \u2014 ${r.reasoning}`).join("\n")}
2400
+
2401
+ ` : "") + (groundingReport.duplicates.length > 0 ? `## Possible duplicates
2402
+ ${groundingReport.duplicates.map((d) => `- **${d.entryId}** ${d.name} [${d.collectionSlug}] (${d.matchType}, overlap: ${d.overlapRatio})`).join("\n")}
2403
+
2404
+ ` : "") + (groundingReport.governance.length > 0 ? `## Governance entries to review
2405
+ ${groundingReport.governance.map((g) => `- **${g.entryId}** ${g.name} [${g.collectionSlug}]`).join("\n")}
2406
+
2407
+ ` : "") + `_Call \`capture\` without \`suggestOnly\` to proceed with entry creation._`
2408
+ }],
2409
+ structuredContent: {
2410
+ ...success(
2411
+ summaryText,
2412
+ { groundingReport, outcome: "suggest_only" },
2413
+ [{ tool: "capture", description: "Capture for real", parameters: { collection: resolvedCollection, name, description } }]
2414
+ )
2415
+ }
2416
+ };
2417
+ }
2344
2418
  const data = buildDataFromFields(col.fields ?? [], profile.descriptionField, description);
2345
2419
  for (const def of profile.defaults) {
2346
2420
  if (def.value !== "infer" && def.value !== "today") {
@@ -2429,6 +2503,14 @@ Or use \`collections action=list\` to see available collections.`
2429
2503
  ...preview ? { preview: true } : {}
2430
2504
  });
2431
2505
  if (result?.preview) {
2506
+ const hasGrounding = groundingReport.related.length > 0 || groundingReport.duplicates.length > 0 || groundingReport.governance.length > 0;
2507
+ const groundingSummary = hasGrounding ? `
2508
+
2509
+ **Grounding:** ${[
2510
+ groundingReport.duplicates.length > 0 && `${groundingReport.duplicates.length} possible duplicate${groundingReport.duplicates.length > 1 ? "s" : ""}`,
2511
+ groundingReport.related.length > 0 && `${groundingReport.related.length} related entr${groundingReport.related.length > 1 ? "ies" : "y"}`,
2512
+ groundingReport.governance.length > 0 && `${groundingReport.governance[0]?.entryId} may govern this`
2513
+ ].filter(Boolean).join(", ")}.` : "";
2432
2514
  const previewEnvelope = success(
2433
2515
  `Preview: would capture "${name}" \u2014 no DB writes`,
2434
2516
  {
@@ -2436,7 +2518,8 @@ Or use \`collections action=list\` to see available collections.`
2436
2518
  name,
2437
2519
  collection: resolvedCollection,
2438
2520
  outcome: "preview",
2439
- warnings: result.warnings ?? []
2521
+ warnings: result.warnings ?? [],
2522
+ groundingReport
2440
2523
  },
2441
2524
  [{ tool: "capture", description: "Capture for real", parameters: { collection: resolvedCollection, name, description } }]
2442
2525
  );
@@ -2448,7 +2531,7 @@ Or use \`collections action=list\` to see available collections.`
2448
2531
 
2449
2532
  No DB writes \u2014 call without \`preview:true\` to capture for real.${result.warnings?.length ? `
2450
2533
 
2451
- **Warnings:** ${result.warnings.join("; ")}` : ""}` }],
2534
+ **Warnings:** ${result.warnings.join("; ")}` : ""}${groundingSummary}` }],
2452
2535
  structuredContent: previewEnvelope
2453
2536
  };
2454
2537
  }
@@ -2770,6 +2853,19 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2770
2853
  lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview2}`);
2771
2854
  }
2772
2855
  }
2856
+ if (format === "human") {
2857
+ const hasDuplicates = groundingReport.duplicates.length > 0;
2858
+ const hasRelated = groundingReport.related.length > 0;
2859
+ const hasGov = groundingReport.governance.length > 0;
2860
+ if (hasDuplicates || hasRelated || hasGov) {
2861
+ lines.push("");
2862
+ const parts = [];
2863
+ if (hasDuplicates) parts.push(`${groundingReport.duplicates.length} possible duplicate${groundingReport.duplicates.length > 1 ? "s" : ""}`);
2864
+ if (hasRelated) parts.push(`${groundingReport.related.length} related entr${groundingReport.related.length > 1 ? "ies" : "y"}`);
2865
+ if (hasGov) parts.push(`${groundingReport.governance[0]?.entryId} may govern this`);
2866
+ lines.push(`_Grounding: ${parts.join(", ")}. Review and link if relevant._`);
2867
+ }
2868
+ }
2773
2869
  lines.push("");
2774
2870
  lines.push(formatQualityReport(quality));
2775
2871
  const failedChecks = quality.checks.filter((c) => !c.passed);
@@ -2946,7 +3042,9 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2946
3042
  // BET-272 S3: Advisory quality hints — always included (empty array when all criteria pass)
2947
3043
  qualityHints: formativeHints,
2948
3044
  // BET-272 S5: Advisory relation suggestions from graph (BR-144, STD-147)
2949
- relationSuggestions: relationSuggestionsFromCreate
3045
+ relationSuggestions: relationSuggestionsFromCreate,
3046
+ // WP-318 S2: Pre-write grounding report — always included (empty arrays when no suggestions)
3047
+ groundingReport
2950
3048
  },
2951
3049
  next
2952
3050
  ),
@@ -11114,7 +11212,16 @@ async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
11114
11212
  setSessionOriented(true);
11115
11213
  return { oriented: true, orientationStatus: "complete" };
11116
11214
  } catch {
11117
- return { oriented: false, orientationStatus: "failed" };
11215
+ if (!coherenceSnapshot) {
11216
+ return { oriented: false, orientationStatus: "failed" };
11217
+ }
11218
+ try {
11219
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
11220
+ setSessionOriented(true);
11221
+ return { oriented: true, orientationStatus: "complete" };
11222
+ } catch {
11223
+ return { oriented: false, orientationStatus: "failed" };
11224
+ }
11118
11225
  }
11119
11226
  }
11120
11227
  var startSchema = z17.object({
@@ -13361,6 +13468,25 @@ function formatScanReport(result) {
13361
13468
  // src/tools/orient.ts
13362
13469
  import { z as z22 } from "zod";
13363
13470
  var PURPOSE_GAP_PREFIX = "purpose-gap-";
13471
+ async function markOrientedWithSnapshotFallback(agentSessionId, coherenceSnapshot) {
13472
+ try {
13473
+ await mcpCall("agent.markOriented", {
13474
+ sessionId: agentSessionId,
13475
+ ...coherenceSnapshot ? { coherenceSnapshot } : {}
13476
+ });
13477
+ setSessionOriented(true);
13478
+ return true;
13479
+ } catch {
13480
+ if (!coherenceSnapshot) return false;
13481
+ try {
13482
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
13483
+ setSessionOriented(true);
13484
+ return true;
13485
+ } catch {
13486
+ return false;
13487
+ }
13488
+ }
13489
+ }
13364
13490
  function extractSessionEntryIds(priorSessions) {
13365
13491
  const allSeen = /* @__PURE__ */ new Set();
13366
13492
  const all = [];
@@ -13648,16 +13774,12 @@ function registerOrientTool(server) {
13648
13774
  }
13649
13775
  let orientationStatus2 = "no_session";
13650
13776
  if (agentSessionId && hasTaskGrounding) {
13651
- try {
13652
- await mcpCall("agent.markOriented", {
13653
- sessionId: agentSessionId,
13654
- ...coherenceSnapshot ? { coherenceSnapshot } : {}
13655
- });
13656
- setSessionOriented(true);
13777
+ const orientedOk = await markOrientedWithSnapshotFallback(agentSessionId, coherenceSnapshot);
13778
+ if (orientedOk) {
13657
13779
  orientationStatus2 = "complete";
13658
13780
  lines.push("---");
13659
13781
  lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
13660
- } catch {
13782
+ } else {
13661
13783
  orientationStatus2 = "failed";
13662
13784
  lines.push("---");
13663
13785
  lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
@@ -14019,16 +14141,12 @@ function registerOrientTool(server) {
14019
14141
  }
14020
14142
  let orientationStatus = "no_session";
14021
14143
  if (agentSessionId && hasTaskGrounding) {
14022
- try {
14023
- await mcpCall("agent.markOriented", {
14024
- sessionId: agentSessionId,
14025
- ...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
14026
- });
14027
- setSessionOriented(true);
14144
+ const orientedOk = await markOrientedWithSnapshotFallback(agentSessionId, fullCoherenceSnapshot);
14145
+ if (orientedOk) {
14028
14146
  orientationStatus = "complete";
14029
14147
  lines.push("---");
14030
14148
  lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
14031
- } catch {
14149
+ } else {
14032
14150
  orientationStatus = "failed";
14033
14151
  lines.push("---");
14034
14152
  lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
@@ -15629,7 +15747,6 @@ export {
15629
15747
  hashKey,
15630
15748
  runWithAuth,
15631
15749
  getAgentSessionId,
15632
- startAgentSession,
15633
15750
  orphanAgentSession,
15634
15751
  bootstrap,
15635
15752
  bootstrapHttp,
@@ -15639,4 +15756,4 @@ export {
15639
15756
  SERVER_VERSION,
15640
15757
  createProductBrainServer
15641
15758
  };
15642
- //# sourceMappingURL=chunk-LKUFRQUO.js.map
15759
+ //# sourceMappingURL=chunk-6E6HZFTX.js.map