@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,
|
|
633
|
-
|
|
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
|
|
648
|
+
text: `# Capture Timed Out
|
|
639
649
|
|
|
640
650
|
${msg}
|
|
641
651
|
|
|
642
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
676
|
-
const
|
|
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: `#
|
|
682
|
+
text: `# Already Captured: ${replay.entryId}
|
|
681
683
|
|
|
682
|
-
${
|
|
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
|
-
**
|
|
686
|
+
**Links:**
|
|
687
|
+
${linkLines.join("\n")}` : ""}
|
|
687
688
|
|
|
688
|
-
|
|
689
|
+
Read it with \`context action=get\`.`
|
|
689
690
|
}],
|
|
690
|
-
structuredContent:
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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` (
|
|
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
|
-
//
|
|
1490
|
-
|
|
1491
|
-
//
|
|
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,
|
|
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
|
|
3744
|
-
const results =
|
|
3745
|
-
const platform = platformSearchLines(
|
|
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}${
|
|
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,
|
|
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:
|
|
3814
|
-
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: {
|
|
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-
|
|
12835
|
+
//# sourceMappingURL=chunk-W3NKFJSJ.js.map
|