@productbrain/mcp 0.0.1-beta.10769 → 0.0.1-beta.10778

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.
@@ -625,98 +625,80 @@ async function foldWhyIntoData(slug, why, userData) {
625
625
  // src/tools/smart-capture.ts
626
626
  import { z as z4 } from "zod/v3";
627
627
 
628
+ // src/lib/statusCategories.ts
629
+ var STATUS_CATEGORIES = ["draft", "current", "past", "discarded"];
630
+ var STATUS_CATEGORY_SET = new Set(STATUS_CATEGORIES);
631
+ var TRUST_ACCEPTED_WORDS = ["accepted", "active", "current"];
632
+ function isTrustAccepted(status2) {
633
+ return TRUST_ACCEPTED_WORDS.includes(status2);
634
+ }
635
+ var TRUST_ARCHIVED_WORDS = ["archived", "deprecated", "past", "discarded"];
636
+ var ENTRY_STATUS_READ_WORDS = ["draft", ...TRUST_ACCEPTED_WORDS, ...TRUST_ARCHIVED_WORDS];
637
+ var ENTRY_STATUS_WRITE_WORDS = ["draft", "archived"];
638
+
628
639
  // src/lib/captureTimeoutOutcome.ts
629
640
  function isGatewayTimeout(error) {
630
641
  return error?.name === "GatewayTimeoutError";
631
642
  }
632
- function buildCaptureOutcomeUnknownResult(error, name, msg) {
633
- if (!error.mayHaveLanded) return buildDryRunTimeoutResult(error, msg);
634
- const recovery = `Search for "${name}" before retrying \u2014 a retry will duplicate it if the write landed. If found, read back its commit status and coaching output before assuming either is missing.`;
643
+ function buildCaptureOutcomeUnknownResult(error, msg) {
644
+ const recovery = error.mayHaveLanded ? "Re-send the same capture; the kernel replays it if the first one landed, so it never writes twice." : "Re-run the preview; nothing was written.";
635
645
  return {
636
646
  content: [{
637
647
  type: "text",
638
- text: `# Capture Outcome Unknown \u2014 Not Failed
648
+ text: `# Capture Timed Out
639
649
 
640
650
  ${msg}
641
651
 
642
- **What is known:** the request reached the server and this client gave up waiting after ${error.elapsedMs}ms. The entry **may or may not** have been written.
643
-
644
- **What definitely did NOT run:** relation linking \u2014 a separate mutation issued after the create returns, so it is never reached when create itself times out.
645
-
646
- **What may also have run \u2014 outcome unknown, same reason as the entry itself:** auto-commit and quality coaching. Both execute INSIDE the create call, before it returns, so if the server finished after this client stopped waiting, they ran too. Do not assume either is absent \u2014 read the entry back to find out.
647
-
648
- **Do this next:** ${recovery}`
652
+ This client stopped waiting after ${error.elapsedMs}ms. ${recovery}`
649
653
  }],
650
- structuredContent: failure(
651
- "CAPTURE_OUTCOME_UNKNOWN",
652
- msg,
653
- recovery,
654
- [{ tool: "context", description: "Search for the entry before retrying", parameters: { action: "search", query: name } }],
655
- {
656
- // Structured so a caller can branch on this without parsing prose — the
657
- // reconciliation signal TEN-1126 says the capture path never had.
658
- partialSuccess: true,
659
- mayHaveLanded: error.mayHaveLanded,
660
- budgetMs: error.budgetMs,
661
- elapsedMs: error.elapsedMs,
662
- completedBeforeTimeout: [],
663
- // WP-575 review (P1): genuinely definite — relation linking is a separate mutation
664
- // never reached when create throws. See the doc comment above for why `commit` and
665
- // `coaching` were removed from this list.
666
- didNotRun: ["relations"],
667
- // WP-575 review (P1): distinct from `didNotRun` on purpose — these ran INSIDE the
668
- // same server call as the create, so their outcome is exactly as unknown as the
669
- // entry's. A caller must read the entry back, never assume either ran or didn't.
670
- mayHaveRun: ["commit", "coaching"]
671
- }
672
- )
654
+ structuredContent: failure("CAPTURE_OUTCOME_UNKNOWN", msg, recovery, [], {
655
+ mayHaveLanded: error.mayHaveLanded,
656
+ budgetMs: error.budgetMs,
657
+ elapsedMs: error.elapsedMs
658
+ })
673
659
  };
674
660
  }
675
- function buildDryRunTimeoutResult(error, msg) {
676
- const recovery = "Re-run the preview \u2014 nothing was written, so there is nothing to search for and nothing to duplicate.";
661
+ async function resumeReplayedCapture(replay, name, links, sessionId) {
662
+ const requested = (links ?? []).slice(0, 25);
663
+ let replayedLinks = [];
664
+ if (requested.length > 0) {
665
+ const batch = await kernelMutation("chain.createEntryRelations", {
666
+ relations: requested.map((l) => ({ fromEntryId: replay.entryId, toEntryId: l.to, type: l.type, proposedBy: "user" })),
667
+ sessionId
668
+ }).catch((err) => ({ results: requested.map((l) => ({ toEntryId: l.to, type: l.type, ok: false, error: err instanceof Error ? err.message : "unknown error" })) }));
669
+ replayedLinks = requested.map((l) => {
670
+ const row = batch.results.find((r) => r.toEntryId === l.to && r.type === l.type);
671
+ if (row?.ok) return { to: l.to, type: l.type, outcome: "written" };
672
+ const error = row?.error ?? "no answer for this link";
673
+ return /^Duplicate relation/.test(error) ? { to: l.to, type: l.type, outcome: "existing" } : { to: l.to, type: l.type, outcome: "failed", error };
674
+ });
675
+ }
676
+ const status2 = isTrustAccepted(replay.status) ? "committed" : "draft";
677
+ const summary = `Already captured as ${replay.entryId}; this re-send returned that entry and wrote no second one.`;
678
+ const linkLines = replayedLinks.map((l) => `- ${l.type} ${l.to}: ${l.outcome === "written" ? "written now" : l.outcome === "existing" ? "already there" : `not written (${l.error})`}`);
677
679
  return {
678
680
  content: [{
679
681
  type: "text",
680
- text: `# Preview Outcome Unknown \u2014 Nothing Was Written
682
+ text: `# Already Captured: ${replay.entryId}
681
683
 
682
- ${msg}
683
-
684
- **What is known:** the request reached the server and this client gave up waiting after ${error.elapsedMs}ms. This was a **preview**, and the server returns its validation result before performing any write \u2014 so no entry, auto-commit, quality coaching, or relation linking happened.
684
+ ${summary}${linkLines.length ? `
685
685
 
686
- **What is unknown:** the validation verdict this preview was asked for.
686
+ **Links:**
687
+ ${linkLines.join("\n")}` : ""}
687
688
 
688
- **Do this next:** ${recovery}`
689
+ Read it with \`context action=get\`.`
689
690
  }],
690
- structuredContent: failure(
691
- "CAPTURE_OUTCOME_UNKNOWN",
692
- msg,
693
- recovery,
694
- [],
695
- {
696
- // No partial success to reconcile: a dry run has no server-side state to converge on.
697
- partialSuccess: false,
698
- mayHaveLanded: false,
699
- budgetMs: error.budgetMs,
700
- elapsedMs: error.elapsedMs,
701
- completedBeforeTimeout: [],
702
- didNotRun: ["entry", "relations", "commit", "coaching"],
703
- mayHaveRun: []
704
- }
705
- )
691
+ structuredContent: success(summary, {
692
+ entryId: replay.entryId,
693
+ collection: replay.collection,
694
+ name,
695
+ status: status2,
696
+ replayed: true,
697
+ ...replayedLinks.length ? { replayedLinks } : {}
698
+ })
706
699
  };
707
700
  }
708
701
 
709
- // src/lib/statusCategories.ts
710
- var STATUS_CATEGORIES = ["draft", "current", "past", "discarded"];
711
- var STATUS_CATEGORY_SET = new Set(STATUS_CATEGORIES);
712
- var TRUST_ACCEPTED_WORDS = ["accepted", "active", "current"];
713
- function isTrustAccepted(status2) {
714
- return TRUST_ACCEPTED_WORDS.includes(status2);
715
- }
716
- var TRUST_ARCHIVED_WORDS = ["archived", "deprecated", "past", "discarded"];
717
- var ENTRY_STATUS_READ_WORDS = ["draft", ...TRUST_ACCEPTED_WORDS, ...TRUST_ARCHIVED_WORDS];
718
- var ENTRY_STATUS_WRITE_WORDS = ["draft", "archived"];
719
-
720
702
  // src/lib/scopedCache.ts
721
703
  var ScopedCache = class {
722
704
  constructor(ttlMs, maxScopes = 100) {
@@ -1466,8 +1448,7 @@ var captureSuccessOutputSchema = z4.object({
1466
1448
  collection: z4.string(),
1467
1449
  name: z4.string(),
1468
1450
  status: z4.enum(["draft", "committed", "proposed", "draft_on_failure"]),
1469
- // WP-480 S1: `qualityScore` (required, client N/10) deleted — a named, accepted
1470
- // breaking output-contract change. The server verdict stays as `qualityVerdict`.
1451
+ // WP-480 S1: `qualityScore` deleted (accepted breaking output change); the server verdict stays as `qualityVerdict`.
1471
1452
  qualityVerdict: z4.record(z4.unknown()).optional(),
1472
1453
  qualityHints: z4.array(z4.record(z4.unknown())).optional(),
1473
1454
  groundingReport: captureSinglePreviewOutputSchema.shape.groundingReport.optional(),
@@ -1485,10 +1466,11 @@ var captureSuccessOutputSchema = z4.object({
1485
1466
  })).optional(),
1486
1467
  // TASK-160: the server's birth-floor verdict must survive the strict output contract.
1487
1468
  spineFloor: z4.object({ relationType: z4.string(), parentKind: z4.string(), exempt: z4.boolean() }).optional(),
1488
- // WP-751 M3a-1b (TASK-189, DEC-1700): `coherencyRefusal` and `contradictionAdvisory` are
1489
- // deleted — both were only ever populated by the client-side auto-commit attempt (surfacing
1490
- // its own gate refusal, or the commit response's contradiction advisory), and that attempt is
1491
- // deleted entirely. Neither field is set anywhere in this file anymore.
1469
+ // WP-751 M3a-1b (TASK-189, DEC-1700): `coherencyRefusal` and `contradictionAdvisory` are deleted with the
1470
+ // client-side auto-commit attempt that alone populated them (its gate refusal, the commit's advisory).
1471
+ replayed: z4.literal(true).optional(),
1472
+ // TASK-898: a re-sent capture answered with the first capture's entry
1473
+ replayedLinks: z4.array(z4.object({ to: z4.string(), type: z4.string(), outcome: z4.enum(["written", "existing", "failed"]), error: z4.string().optional() })).optional(),
1492
1474
  fallbackApplied: z4.boolean().optional()
1493
1475
  // round 2 (PRRT_kwDORS4ils6cTNbF): classifier field above still names the original (pre-substitution) prediction
1494
1476
  }).strict();
@@ -1726,6 +1708,7 @@ async function handleCapture(server, { collection, name, description, context, c
1726
1708
  // accepted shape as the CLI dying between create and re-eval).
1727
1709
  ...links && links.length > 0 ? { deferLLMSchedule: true } : {}
1728
1710
  });
1711
+ if (result2 && "replayed" in result2) return resumeReplayedCapture(result2, name, links, agentId ?? void 0);
1729
1712
  if (result2 && isPreviewCreateEntryResult(result2)) {
1730
1713
  const preflight = await resolvePreviewPreflight(result2, links);
1731
1714
  const dupCount = groundingReport.duplicates.length;
@@ -1800,7 +1783,7 @@ No DB writes \u2014 call without \`preview:true\` to capture for real.${result2.
1800
1783
  spineFloor = created.spineFloor;
1801
1784
  } catch (error) {
1802
1785
  const msg = error instanceof Error ? error.message : String(error);
1803
- if (isGatewayTimeout(error)) return buildCaptureOutcomeUnknownResult(error, name, msg);
1786
+ if (isGatewayTimeout(error)) return buildCaptureOutcomeUnknownResult(error, msg);
1804
1787
  if (msg.includes("Duplicate") || msg.includes("already exists")) {
1805
1788
  return {
1806
1789
  content: [{
@@ -3740,9 +3723,9 @@ async function handleSearch(server, query, collection, status2) {
3740
3723
  data: `Searching${scope} for "${query}"...`,
3741
3724
  logger: "product-os"
3742
3725
  });
3743
- const answer = await kernelQuery("chain.search", { query, collectionSlug: collection, status: status2 });
3744
- const results = answer.rows;
3745
- const platform = platformSearchLines(answer.platform);
3726
+ const answer2 = await kernelQuery("chain.search", { query, collectionSlug: collection, status: status2 });
3727
+ const results = answer2.rows;
3728
+ const platform = platformSearchLines(answer2.platform);
3746
3729
  const matches = results.length + platform.count;
3747
3730
  if (matches === 0) {
3748
3731
  const recorded = recordGap({ query, tool: "context", action: "search", gapType: "search_zero", collectionScope: collection });
@@ -3780,7 +3763,7 @@ async function handleSearch(server, query, collection, status2) {
3780
3763
  }).join("\n");
3781
3764
  const header = `## Search Result
3782
3765
 
3783
- # Search Results for "${query}"${scope} (${matches}${answer.truncated ? "+" : ""} match${matches === 1 ? "" : "es"})
3766
+ # Search Results for "${query}"${scope} (${matches}${answer2.truncated ? "+" : ""} match${matches === 1 ? "" : "es"})
3784
3767
 
3785
3768
  **By collection:** ${collSummary}`;
3786
3769
  const footer = `_Tip: Use \`collection\` param to scope search. Use context action=get with an entry ID for full details._`;
@@ -3802,7 +3785,7 @@ async function handleSearch(server, query, collection, status2) {
3802
3785
  return {
3803
3786
  content: [{ type: "text", text: `${header}
3804
3787
 
3805
- ${[...platform.lines, formatted, answer.cutLine].filter(Boolean).join("\n")}
3788
+ ${[...platform.lines, formatted, answer2.cutLine].filter(Boolean).join("\n")}
3806
3789
 
3807
3790
  ${footer}` }],
3808
3791
  structuredContent: success(`Found ${total} entries${platform.count ? ` and ${platform.count} platform glossary terms` : ""} matching '${query}'.`, {
@@ -3810,8 +3793,8 @@ ${footer}` }],
3810
3793
  results: structuredResults,
3811
3794
  entries: entriesForView,
3812
3795
  total,
3813
- truncated: answer.truncated,
3814
- cutLine: answer.cutLine,
3796
+ truncated: answer2.truncated,
3797
+ cutLine: answer2.cutLine,
3815
3798
  query
3816
3799
  })
3817
3800
  };
@@ -11669,16 +11652,20 @@ var PRODUCT_FEEDBACK_STATUSES = ["new", "triaged", "planned", "resolved", "dismi
11669
11652
  var VENDOR_SETTABLE_STATUSES = PRODUCT_FEEDBACK_STATUSES.filter(
11670
11653
  (status2) => status2 !== "screening"
11671
11654
  );
11655
+ var FEEDBACK_TENSION_ID_PATTERN = /^TEN-[1-9][0-9]*$/;
11656
+ var MAX_FEEDBACK_ANSWER_LENGTH = 240;
11672
11657
 
11673
11658
  // src/tools/feedback.ts
11674
11659
  var actions = ["submit", "list", "queue", "note", "group", "status"];
11675
11660
  var category = z30.enum(PRODUCT_FEEDBACK_CATEGORIES);
11676
11661
  var status = z30.enum(PRODUCT_FEEDBACK_STATUSES);
11677
11662
  var vendorStatus = z30.enum(VENDOR_SETTABLE_STATUSES);
11663
+ var tensionId = z30.string().regex(FEEDBACK_TENSION_ID_PATTERN);
11664
+ var answer = z30.string().trim().min(1).max(MAX_FEEDBACK_ANSWER_LENGTH);
11678
11665
  var MESSAGE_DISPLAY_LIMIT = 1e3;
11679
11666
  var FULL_MESSAGE_MAX_LIMIT = 5;
11680
11667
  var feedbackSchema = z30.object({
11681
- action: z30.enum(actions).describe("submit requires message; list returns your own workspace's feedback (all statuses); queue is the vendor triage queue (system admins only) and accepts filters; note requires feedbackId+note; group requires feedbackIds+groupId (null clears); status requires feedbackId+status."),
11668
+ action: z30.enum(actions).describe("submit requires message; list returns your own workspace's feedback (all statuses); queue is the vendor triage queue (system admins only) and accepts filters; note requires feedbackId+note; group requires feedbackIds+groupId (null clears); status requires feedbackId+status and may carry tensionId and answer."),
11682
11669
  message: z30.string().max(1e4).optional().describe("Required for submit: the product feedback text (max 10,000 characters)."),
11683
11670
  category: category.optional().describe("Submit/queue category; submit defaults to other."),
11684
11671
  command: z30.string().max(1e3).optional().describe("Optional submit command context; argument values are scrubbed server-side."),
@@ -11688,6 +11675,8 @@ var feedbackSchema = z30.object({
11688
11675
  feedbackIds: z30.array(z30.string().max(200)).max(100).optional().describe("Required for group: 1\u2013100 target feedback IDs."),
11689
11676
  note: z30.string().max(4e3).optional().describe("Required for note: replacement triage note (max 4,000 characters)."),
11690
11677
  status: status.optional().describe("list/queue filter (any status, including 'screening'); the status action's required target value excludes 'screening' (system-only)."),
11678
+ tensionId: tensionId.optional().describe("status only: the tension the report joined, its display id TEN- and a number (vendor-only, never shown to the reporter)."),
11679
+ answer: answer.optional().describe(`status only: the one sentence the reporter reads under their report, dated (max ${MAX_FEEDBACK_ANSWER_LENGTH} characters).`),
11691
11680
  groupId: z30.string().min(1).max(200).nullable().optional().describe("Group filter, or required group destination; null explicitly clears grouping."),
11692
11681
  workspaceId: z30.string().max(200).optional().describe("Queue-only filter (vendor cross-workspace triage); omitted means all workspaces. Has no effect on list \u2014 your own workspace is always injected server-side."),
11693
11682
  since: z30.number().optional().describe("Queue-only lower createdAt window bound (inclusive) in epoch milliseconds."),
@@ -11702,7 +11691,7 @@ var union = z30.discriminatedUnion("action", [
11702
11691
  z30.object({ action: z30.literal("queue"), status: status.optional(), category: category.optional(), groupId: z30.string().min(1).max(200).optional(), workspaceId: z30.string().max(200).optional(), since: z30.number().optional(), before: z30.number().optional(), limit: z30.number().int().min(1).max(100).optional(), cursor: z30.string().max(2e3).optional(), full: z30.boolean().optional() }),
11703
11692
  z30.object({ action: z30.literal("note"), feedbackId: z30.string().min(1).max(200), note: z30.string().max(4e3) }),
11704
11693
  z30.object({ action: z30.literal("group"), feedbackIds: z30.array(z30.string().min(1).max(200)).min(1).max(100), groupId: z30.string().min(1).max(200).nullable() }),
11705
- z30.object({ action: z30.literal("status"), feedbackId: z30.string().min(1).max(200), status: vendorStatus })
11694
+ z30.object({ action: z30.literal("status"), feedbackId: z30.string().min(1).max(200), status: vendorStatus, tensionId: tensionId.optional(), answer: answer.optional() })
11706
11695
  ]);
11707
11696
  var specs = {
11708
11697
  submit: { params: ["message", "category", "command", "route", "client"], description: "message is required; category defaults to other." },
@@ -11711,7 +11700,10 @@ var specs = {
11711
11700
  queue: { params: ["status", "category", "groupId", "workspaceId", "since", "before", "limit", "cursor", "full"], description: "Vendor cross-workspace triage queue (system admins only); filters are optional, status defaults to new; cursor pages, full lifts message truncation for limit <= 5." },
11712
11701
  note: { params: ["feedbackId", "note"], description: "feedbackId and note are required." },
11713
11702
  group: { params: ["feedbackIds", "groupId"], description: "feedbackIds and groupId are required; null clears a group." },
11714
- status: { params: ["feedbackId", "status"], description: "feedbackId and status are required; status cannot be set to 'screening' (system-only)." }
11703
+ status: {
11704
+ params: ["feedbackId", "status", "tensionId", "answer"],
11705
+ description: "feedbackId and status are required; status cannot be set to 'screening' (system-only); optional tensionId and answer are written in the same call."
11706
+ }
11715
11707
  };
11716
11708
  function result(action, data) {
11717
11709
  const summary = action === "submit" ? "Feedback submitted." : `Feedback ${action} complete.`;
@@ -12840,4 +12832,4 @@ export {
12840
12832
  createProductBrainServer,
12841
12833
  initFeatureFlags
12842
12834
  };
12843
- //# sourceMappingURL=chunk-UNDEFOTU.js.map
12835
+ //# sourceMappingURL=chunk-W3NKFJSJ.js.map