@unbrained/pm-cli 2026.8.31 → 2026.9.2

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.
Files changed (108) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +15 -13
  3. package/CHANGELOG.md +33 -6
  4. package/README.md +3 -3
  5. package/dist/cli/commands/merge.d.ts +1 -1
  6. package/dist/cli/commands/merge.js +3 -3
  7. package/dist/cli/register-mutation.js +8 -5
  8. package/dist/cli-bundle/bundle-manifest.json +129 -129
  9. package/dist/cli-bundle/chunks/{chunk-52JXYSDW.js → chunk-3WICXLVT.js} +2 -2
  10. package/dist/cli-bundle/chunks/{chunk-JIFYOJF7.js → chunk-DTGWEL23.js} +2 -2
  11. package/dist/cli-bundle/chunks/{chunk-SXMHPK4V.js → chunk-FZ4Z2J4F.js} +4 -4
  12. package/dist/cli-bundle/chunks/chunk-GOACULJW.js +13 -0
  13. package/dist/cli-bundle/chunks/{chunk-NW6LV3TU.js → chunk-NOPL37X4.js} +64 -64
  14. package/dist/cli-bundle/chunks/{chunk-OUGDA7NR.js → chunk-T6H5BZCP.js} +2 -2
  15. package/dist/cli-bundle/chunks/{chunk-IEFJ3ZEB.js → chunk-TAG4VC22.js} +4 -4
  16. package/dist/cli-bundle/chunks/{chunk-MWIN6OH2.js → chunk-UDW64J2W.js} +2 -2
  17. package/dist/cli-bundle/chunks/{chunk-5DH5XCJL.js → chunk-X4RCGLTX.js} +2 -2
  18. package/dist/cli-bundle/chunks/{chunk-QZCIBZR3.js → chunk-Y7VOMSZT.js} +2 -2
  19. package/dist/cli-bundle/chunks/chunk-ZLJIFSOE.js +35 -0
  20. package/dist/cli-bundle/chunks/{register-list-query-7C7DKZ2U.js → register-list-query-XYNX4ASQ.js} +2 -2
  21. package/dist/cli-bundle/chunks/register-mutation-WFQ7MJTX.js +20 -0
  22. package/dist/cli-bundle/chunks/{register-operations-IIMA5OKO.js → register-operations-JVTI7JPP.js} +2 -2
  23. package/dist/cli-bundle/chunks/{register-setup-52H36QCB.js → register-setup-KVNC5KPL.js} +2 -2
  24. package/dist/cli-bundle/focused-chunks/{chunk-VJ7EFMFL.js → chunk-242AXYB2.js} +2 -2
  25. package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +16 -0
  26. package/dist/cli-bundle/focused-chunks/{chunk-FOMZ6NG5.js → chunk-MKHG3T76.js} +2 -2
  27. package/dist/cli-bundle/focused-chunks/{chunk-JCOOAUHK.js → chunk-O67VDLOT.js} +48 -48
  28. package/dist/cli-bundle/focused-chunks/{chunk-UN34D63M.js → chunk-PF3E6Q36.js} +2 -2
  29. package/dist/cli-bundle/focused-chunks/{chunk-APN4QN6R.js → chunk-RMRCQPWU.js} +2 -2
  30. package/dist/cli-bundle/focused-chunks/{chunk-BPWJ4QM5.js → chunk-S4U5CN5Q.js} +2 -2
  31. package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +158 -0
  32. package/dist/cli-bundle/main.js +2 -2
  33. package/dist/cli-bundle/sdk-contracts.js +1 -1
  34. package/dist/cli-bundle/sdk-core.js +25 -25
  35. package/dist/cli-bundle/sdk-governance.js +1 -1
  36. package/dist/cli-bundle/sdk-graph.js +1 -1
  37. package/dist/cli-bundle/sdk-merge.js +32 -32
  38. package/dist/cli-bundle/sdk-query.js +1 -1
  39. package/dist/cli-bundle/sdk-runtime.js +1 -1
  40. package/dist/cli-bundle/sdk-testing.js +1 -1
  41. package/dist/cli-bundle/sdk.js +10 -10
  42. package/dist/core/diagnostics/remediation.js +9 -4
  43. package/dist/core/history/drift-scan.js +6 -6
  44. package/dist/core/history/history.d.ts +6 -0
  45. package/dist/core/history/history.js +47 -5
  46. package/dist/core/history/replay.d.ts +12 -1
  47. package/dist/core/history/replay.js +60 -39
  48. package/dist/mcp/server.d.ts +2 -2
  49. package/dist/mcp/server.js +78 -15
  50. package/dist/mcp/tool-definitions.js +56 -2
  51. package/dist/sdk/agent-capability-contracts.js +4 -2
  52. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +14 -2
  53. package/dist/sdk/governance/health.js +77 -11
  54. package/dist/sdk/governance/validate.js +4 -2
  55. package/dist/sdk/history-read.js +14 -6
  56. package/dist/sdk/history-repair.js +17 -10
  57. package/dist/sdk/index.d.ts +1 -0
  58. package/dist/sdk/index.js +3 -2
  59. package/dist/sdk/init-agent-guidance.js +4 -4
  60. package/dist/sdk/lifecycle/update.js +23 -10
  61. package/dist/sdk/mcp/discovery.d.ts +152 -0
  62. package/dist/sdk/mcp/discovery.js +495 -0
  63. package/dist/sdk/merge/index.d.ts +1 -1
  64. package/dist/sdk/merge/index.js +3 -3
  65. package/dist/sdk/merge/receipts.d.ts +51 -1
  66. package/dist/sdk/merge/receipts.js +146 -35
  67. package/dist/sdk/merge/three-way.d.ts +3 -1
  68. package/dist/sdk/merge/three-way.js +9 -7
  69. package/dist/sdk/runtime-extended-actions.js +4 -4
  70. package/docs/AGENT_GUIDE.md +11 -3
  71. package/docs/ARCHITECTURE.md +5 -0
  72. package/docs/CLAUDE_CODE_PLUGIN.md +2 -2
  73. package/docs/CONTEXT_RECOVERY_PRIMITIVES.md +24 -6
  74. package/docs/MCP_2026_07_28.md +9 -5
  75. package/docs/MCP_CAPABILITY_SURFACES.md +7 -1
  76. package/docs/MCP_REMOTE_TRANSPORT_SECURITY.md +11 -1
  77. package/docs/MERGE_SAFETY.md +44 -2
  78. package/docs/OUTPUT_TOKEN_ACCOUNTING.md +5 -3
  79. package/docs/PROGRESSIVE_TOOL_DISCOVERY.md +42 -0
  80. package/docs/README.md +1 -0
  81. package/docs/agent-task-token-baseline.json +157 -3
  82. package/docs/agent-task-transcripts.json +143 -0
  83. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +4 -3
  84. package/docs/performance/cli-transport-overhead.md +8 -6
  85. package/marketplace.json +2 -2
  86. package/package.json +2 -2
  87. package/packages/pm-beads/package.json +1 -1
  88. package/packages/pm-calendar/package.json +1 -1
  89. package/packages/pm-command-kit/package.json +1 -1
  90. package/packages/pm-digital-twin/package.json +1 -1
  91. package/packages/pm-governance-audit/package.json +1 -1
  92. package/packages/pm-guide-shell/package.json +1 -1
  93. package/packages/pm-kanban/package.json +1 -1
  94. package/packages/pm-lifecycle-hooks/package.json +1 -1
  95. package/packages/pm-linked-test-adapters/package.json +1 -1
  96. package/packages/pm-search-advanced/package.json +1 -1
  97. package/packages/pm-templates/package.json +1 -1
  98. package/packages/pm-todos/package.json +1 -1
  99. package/packages/pm-vcs/package.json +1 -1
  100. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  101. package/plugins/pm-claude/README.md +4 -3
  102. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  103. package/sdk/public-surface.json +168 -2
  104. package/dist/cli-bundle/chunks/chunk-2XVV45NJ.js +0 -35
  105. package/dist/cli-bundle/chunks/chunk-3FCSFWZT.js +0 -13
  106. package/dist/cli-bundle/chunks/register-mutation-OI4BJLUX.js +0 -20
  107. package/dist/cli-bundle/focused-chunks/chunk-BMD42PUG.js +0 -158
  108. package/dist/cli-bundle/focused-chunks/chunk-EUTABA4M.js +0 -16
@@ -0,0 +1,152 @@
1
+ import { type PmCommandCapabilityFamily, type PmCommandVisibilityTier, type PmMcpToolProfile } from "../agent-capability-contracts.js";
2
+ /** Namespaced MCP extension that opts a client into progressive tool discovery. */
3
+ export declare const PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION = "dev.unbrained.pm/progressive-tool-discovery";
4
+ /** Versioned server capability advertised for progressive tool discovery. */
5
+ export declare const PM_MCP_PROGRESSIVE_DISCOVERY_SERVER_CAPABILITY: Readonly<{
6
+ version: 1;
7
+ discoveryTool: "pm_discover";
8
+ canonicalResult: "structuredContent.result";
9
+ compatibilityText: "pointer";
10
+ }>;
11
+ /** Small stable tool catalog returned to clients that negotiate the extension. */
12
+ export declare const PM_MCP_ENTRY_TOOL_NAMES: readonly string[];
13
+ /** Ranking signals composed by the public discovery contract. */
14
+ export type PmToolDiscoverySignalName = "lexical" | "semantic" | "graph" | "permission" | "freshness" | "usage";
15
+ /** Optional host-owned signals supplied for one tool. */
16
+ export interface PmToolDiscoverySignalInput {
17
+ /** Semantic relevance normalized to the inclusive range zero through one. */
18
+ semantic?: number;
19
+ /** Graph proximity normalized to the inclusive range zero through one. */
20
+ graph?: number;
21
+ /** Definition freshness normalized to the inclusive range zero through one. */
22
+ freshness?: number;
23
+ /** Historical selection usefulness normalized to the inclusive range zero through one. */
24
+ usage?: number;
25
+ }
26
+ /** One tool definition accepted by the SDK discovery engine. */
27
+ export interface PmToolDiscoveryCandidate {
28
+ /** Stable callable tool name. */
29
+ name: string;
30
+ /** Agent-facing tool description. */
31
+ description: string;
32
+ /** JSON Schema supplied when callers request schema expansion. */
33
+ inputSchema: Record<string, unknown>;
34
+ /** Whether the current caller is authorized to invoke the tool. */
35
+ authorized?: boolean;
36
+ /** Optional host-owned ranking signals. */
37
+ signals?: PmToolDiscoverySignalInput;
38
+ }
39
+ /** Options controlling one progressive discovery page. */
40
+ export interface PmToolDiscoveryOptions {
41
+ /** Free-text capability intent. Empty text lists by deterministic policy order. */
42
+ query?: string;
43
+ /** Restrict results to one stable capability family. */
44
+ family?: PmCommandCapabilityFamily;
45
+ /** Restrict results to tools visible at or below this tier. */
46
+ tier?: Exclude<PmCommandVisibilityTier, "internal">;
47
+ /** Maximum result rows, from one through one hundred. */
48
+ limit?: number;
49
+ /** Opaque continuation cursor from an equivalent discovery request. */
50
+ cursor?: string;
51
+ /** Include complete input schemas instead of the default compact projection. */
52
+ includeSchema?: boolean;
53
+ /** Estimated-token ceiling, or unbounded for the complete selected page. */
54
+ outputBudget?: number | "unbounded";
55
+ /** MCP profile that produced the authorized candidate catalog. */
56
+ profile?: PmMcpToolProfile;
57
+ /** Host-owned HMAC key of at least 32 bytes for restart-stable cursors. */
58
+ cursorIntegrityKey?: Uint8Array;
59
+ }
60
+ /** Applied score for one signal with explicit provenance. */
61
+ export interface PmToolDiscoveryAppliedSignal {
62
+ /** Normalized signal value. */
63
+ value: number;
64
+ /** Public policy weight. */
65
+ weight: number;
66
+ /** Whether the signal was computed or supplied rather than defaulted. */
67
+ available: boolean;
68
+ /** Transparent source of the normalized value. */
69
+ source: "computed" | "host" | "authorization" | "unavailable";
70
+ }
71
+ /** One ranked discovery result. */
72
+ export interface PmToolDiscoveryResultRow {
73
+ /** Stable callable tool name. */
74
+ name: string;
75
+ /** Agent-facing description. */
76
+ description: string;
77
+ /** Canonical CLI command backing the tool. */
78
+ command: string;
79
+ /** Minimum visibility tier inherited from the command contract. */
80
+ tier: PmCommandVisibilityTier;
81
+ /** Stable capability family inherited from the command contract. */
82
+ family: PmCommandCapabilityFamily;
83
+ /** Weighted aggregate score used for ordering. */
84
+ score: number;
85
+ /** Complete per-signal score explanation. */
86
+ signals: Readonly<Record<PmToolDiscoverySignalName, PmToolDiscoveryAppliedSignal>>;
87
+ /** Complete JSON Schema when explicitly requested. */
88
+ input_schema?: Record<string, unknown>;
89
+ }
90
+ /** Explicit receipt for information omitted from a discovery page. */
91
+ export interface PmToolDiscoveryOmissionReceipt {
92
+ /** Whether any result rows or schemas were omitted. */
93
+ has_omissions: boolean;
94
+ /** Stable omitted field groups. */
95
+ omitted: Array<{
96
+ name: "input_schema" | "tools";
97
+ reason: "compact_projection" | "limit" | "token_budget";
98
+ restore_with: string;
99
+ }>;
100
+ }
101
+ /** Deterministic progressive tool discovery response. */
102
+ export interface PmToolDiscoveryResult {
103
+ /** Stable result discriminator for model and host routing. */
104
+ result_type: "pm_tool_discovery";
105
+ /** Public contract revision. */
106
+ contract_version: 1;
107
+ /** Normalized query used by ranking and cursor binding. */
108
+ query: string;
109
+ /** MCP profile that produced the candidate catalog. */
110
+ profile: PmMcpToolProfile;
111
+ /** Ranked page of authorized tools. */
112
+ tools: PmToolDiscoveryResultRow[];
113
+ /** Total authorized rows after filters and before pagination. */
114
+ total: number;
115
+ /** Rows returned on this page. */
116
+ returned: number;
117
+ /** Whether a later ranked row remains. */
118
+ has_more: boolean;
119
+ /** Opaque continuation cursor bound to query, filters, and catalog. */
120
+ next_cursor?: string;
121
+ /** Exact estimated serialized result cost. */
122
+ token_cost: {
123
+ estimated_tokens: number;
124
+ budget: number | "unbounded";
125
+ within_budget: boolean;
126
+ };
127
+ /** Public ranking formula and weights. */
128
+ ranking_policy: {
129
+ version: 1;
130
+ formula: "weighted_sum_then_name";
131
+ weights: Readonly<Record<PmToolDiscoverySignalName, number>>;
132
+ };
133
+ /** Cache identity and invalidation contract for deterministic pages. */
134
+ cache: {
135
+ key: string;
136
+ ttl_ms: number;
137
+ scope: "private";
138
+ invalidates_on: readonly string[];
139
+ };
140
+ /** Explicit recovery for compacted fields and rows. */
141
+ omission_receipt: PmToolDiscoveryOmissionReceipt;
142
+ }
143
+ /** Parse untrusted adapter input without discarding malformed discovery values. */
144
+ export declare function parsePmToolDiscoveryOptions(input: Record<string, unknown>): PmToolDiscoveryOptions;
145
+ /**
146
+ * Rank and page an authorized MCP tool catalog under an explicit token budget.
147
+ *
148
+ * Cursors fail closed when the query, filters, schemas, authorization-filtered
149
+ * catalog, or ranking inputs change. Host signals override documented local
150
+ * fallbacks without changing the public formula or hiding signal provenance.
151
+ */
152
+ export declare function discoverPmTools(candidates: readonly PmToolDiscoveryCandidate[], options?: PmToolDiscoveryOptions): PmToolDiscoveryResult;
@@ -0,0 +1,495 @@
1
+ /**
2
+ * @module sdk/mcp/discovery
3
+ *
4
+ * Provides the deterministic, token-bounded progressive tool discovery
5
+ * contract shared by MCP servers, embedded hosts, and agent integrations.
6
+ */
7
+
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4853ebbf-f28a-55f1-b306-aa75cb9355ae")}catch(e){}}();
9
+ import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
10
+ import { PM_COMMAND_CAPABILITY_CONTRACTS, PM_MCP_TOOL_COMMAND_CONTRACTS, resolvePmCommandCapabilityFamily, resolvePmCommandVisibilityTier, } from "../agent-capability-contracts.js";
11
+ import { PmCliError } from "../runtime-primitives.js";
12
+ /** Namespaced MCP extension that opts a client into progressive tool discovery. */
13
+ export const PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION = "dev.unbrained.pm/progressive-tool-discovery";
14
+ /** Versioned server capability advertised for progressive tool discovery. */
15
+ export const PM_MCP_PROGRESSIVE_DISCOVERY_SERVER_CAPABILITY = Object.freeze({
16
+ version: 1,
17
+ discoveryTool: "pm_discover",
18
+ canonicalResult: "structuredContent.result",
19
+ compatibilityText: "pointer",
20
+ });
21
+ /** Small stable tool catalog returned to clients that negotiate the extension. */
22
+ export const PM_MCP_ENTRY_TOOL_NAMES = Object.freeze([
23
+ "pm_discover",
24
+ "pm_next",
25
+ "pm_context",
26
+ "pm_search",
27
+ "pm_get",
28
+ ]);
29
+ const DISCOVERY_WEIGHTS = Object.freeze({
30
+ lexical: 0.4,
31
+ semantic: 0.2,
32
+ graph: 0.1,
33
+ permission: 0.15,
34
+ freshness: 0.05,
35
+ usage: 0.1,
36
+ });
37
+ const DISCOVERY_CACHE_INVALIDATIONS = Object.freeze([
38
+ "profile_changed",
39
+ "tool_contract_changed",
40
+ "workspace_extension_changed",
41
+ "authorization_changed",
42
+ "ranking_signal_changed",
43
+ "cursor_integrity_key_rotated",
44
+ ]);
45
+ const TIER_RANK = {
46
+ core: 0,
47
+ standard: 1,
48
+ full: 2,
49
+ internal: 3,
50
+ };
51
+ const SELECTABLE_TIERS = Object.freeze([
52
+ "core",
53
+ "standard",
54
+ "full",
55
+ ]);
56
+ const MCP_TOOL_PROFILES = new Set([
57
+ "core",
58
+ "standard",
59
+ "full",
60
+ "custom",
61
+ ]);
62
+ const CAPABILITY_FAMILIES = new Set(PM_COMMAND_CAPABILITY_CONTRACTS.map(({ family }) => family));
63
+ const PROCESS_CURSOR_INTEGRITY_KEY = randomBytes(32);
64
+ /** Validate a discovery string with the Unicode code-point semantics used by JSON Schema. */
65
+ function boundedDiscoveryString(value, field) {
66
+ if (typeof value !== "string") {
67
+ throw new PmCliError(`pm tool discovery ${field} must be a string of at most 4096 Unicode characters.`, 64);
68
+ }
69
+ const iterator = value[Symbol.iterator]();
70
+ let count = 0;
71
+ for (let next = iterator.next(); !next.done; next = iterator.next()) {
72
+ count += 1;
73
+ if (count > 4_096) {
74
+ throw new PmCliError(`pm tool discovery ${field} must be a string of at most 4096 Unicode characters.`, 64);
75
+ }
76
+ }
77
+ return value;
78
+ }
79
+ function discoveryLimit(value) {
80
+ if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 100) {
81
+ throw new PmCliError("pm tool discovery limit must be from 1 through 100.", 64);
82
+ }
83
+ return Number(value);
84
+ }
85
+ function discoveryOutputBudget(value) {
86
+ if (value !== "unbounded" &&
87
+ (!Number.isInteger(value) || Number(value) < 128)) {
88
+ throw new PmCliError("pm tool discovery outputBudget must be unbounded or an integer of at least 128.", 64);
89
+ }
90
+ return value === "unbounded" ? value : Number(value);
91
+ }
92
+ function discoveryIncludeSchema(value) {
93
+ if (value !== undefined && typeof value !== "boolean") {
94
+ throw new PmCliError("pm tool discovery includeSchema must be a boolean.", 64);
95
+ }
96
+ return value === true;
97
+ }
98
+ function validateDiscoveryOptionsContainer(value) {
99
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
100
+ throw new PmCliError("pm tool discovery options must be an object.", 64);
101
+ }
102
+ }
103
+ /** Reject malformed candidate catalogs supplied by untyped SDK consumers. */
104
+ function validateDiscoveryCandidates(candidates) {
105
+ if (!Array.isArray(candidates)) {
106
+ throw new PmCliError("pm tool discovery candidates must be an array.", 64);
107
+ }
108
+ for (const [index, candidate] of candidates.entries()) {
109
+ if (candidate === null ||
110
+ typeof candidate !== "object" ||
111
+ Array.isArray(candidate)) {
112
+ throw new PmCliError(`pm tool discovery candidate ${index} must be an object.`, 64);
113
+ }
114
+ if (typeof candidate.name !== "string" ||
115
+ candidate.name.trim().length === 0) {
116
+ throw new PmCliError(`pm tool discovery candidate ${index} name must be a non-empty string.`, 64);
117
+ }
118
+ if (typeof candidate.description !== "string" ||
119
+ candidate.description.trim().length === 0) {
120
+ throw new PmCliError(`pm tool discovery candidate ${index} description must be a non-empty string.`, 64);
121
+ }
122
+ if (candidate.inputSchema === null ||
123
+ typeof candidate.inputSchema !== "object" ||
124
+ Array.isArray(candidate.inputSchema)) {
125
+ throw new PmCliError(`pm tool discovery candidate ${index} inputSchema must be an object.`, 64);
126
+ }
127
+ if (candidate.authorized !== undefined &&
128
+ typeof candidate.authorized !== "boolean") {
129
+ throw new PmCliError(`pm tool discovery candidate ${index} authorized must be a boolean.`, 64);
130
+ }
131
+ }
132
+ }
133
+ /** Parse untrusted adapter input without discarding malformed discovery values. */
134
+ export function parsePmToolDiscoveryOptions(input) {
135
+ validateDiscoveryOptionsContainer(input);
136
+ if (input.family !== undefined &&
137
+ (typeof input.family !== "string" || !CAPABILITY_FAMILIES.has(input.family))) {
138
+ throw new PmCliError("pm tool discovery family must be a declared capability family.", 64);
139
+ }
140
+ if (input.tier !== undefined &&
141
+ (typeof input.tier !== "string" || !SELECTABLE_TIERS.includes(input.tier))) {
142
+ throw new PmCliError("pm tool discovery tier must be core, standard, or full.", 64);
143
+ }
144
+ return {
145
+ ...(input.query !== undefined
146
+ ? { query: boundedDiscoveryString(input.query, "query") }
147
+ : {}),
148
+ ...(input.family !== undefined
149
+ ? { family: input.family }
150
+ : {}),
151
+ ...(input.tier !== undefined
152
+ ? {
153
+ tier: input.tier,
154
+ }
155
+ : {}),
156
+ ...(input.limit !== undefined
157
+ ? { limit: discoveryLimit(input.limit) }
158
+ : {}),
159
+ ...(input.cursor !== undefined
160
+ ? { cursor: boundedDiscoveryString(input.cursor, "cursor") }
161
+ : {}),
162
+ ...(input.includeSchema !== undefined
163
+ ? { includeSchema: discoveryIncludeSchema(input.includeSchema) }
164
+ : {}),
165
+ ...(input.outputBudget !== undefined
166
+ ? { outputBudget: discoveryOutputBudget(input.outputBudget) }
167
+ : {}),
168
+ };
169
+ }
170
+ function normalizedUnitInterval(value) {
171
+ if (value === undefined || !Number.isFinite(value))
172
+ return 0;
173
+ return Math.min(1, Math.max(0, value));
174
+ }
175
+ function normalizedSearchTokens(value) {
176
+ return [...new Set(value.toLowerCase().match(/[a-z0-9]+/gu) ?? [])].sort();
177
+ }
178
+ function lexicalRelevance(query, text) {
179
+ const queryTokens = normalizedSearchTokens(query);
180
+ if (queryTokens.length === 0)
181
+ return 0;
182
+ const targetTokens = new Set(normalizedSearchTokens(text));
183
+ const matched = queryTokens.filter((token) => targetTokens.has(token)).length;
184
+ const exactBonus = text.toLowerCase().includes(query.toLowerCase())
185
+ ? 0.25
186
+ : 0;
187
+ return Math.min(1, matched / queryTokens.length + exactBonus);
188
+ }
189
+ function semanticRelevance(query, text) {
190
+ const queryTokens = normalizedSearchTokens(query);
191
+ if (queryTokens.length === 0)
192
+ return 0;
193
+ const target = text.toLowerCase();
194
+ const targetTokens = normalizedSearchTokens(text);
195
+ const related = queryTokens.filter((queryToken) => targetTokens.some((targetToken) => targetToken.startsWith(queryToken) ||
196
+ queryToken.startsWith(targetToken))).length;
197
+ const phraseBonus = target.includes(query.toLowerCase()) ? 0.2 : 0;
198
+ return Math.min(1, related / queryTokens.length + phraseBonus);
199
+ }
200
+ function hashDiscoveryValue(value) {
201
+ return createHash("sha256")
202
+ .update(JSON.stringify(value))
203
+ .digest("hex")
204
+ .slice(0, 24);
205
+ }
206
+ function cursorSignature(fingerprint, offset, integrityKey) {
207
+ return createHmac("sha256", integrityKey)
208
+ .update(JSON.stringify({ version: 1, fingerprint, offset }))
209
+ .digest("base64url");
210
+ }
211
+ function encodeDiscoveryCursor(fingerprint, offset, integrityKey) {
212
+ const cursor = {
213
+ version: 1,
214
+ offset,
215
+ signature: cursorSignature(fingerprint, offset, integrityKey),
216
+ };
217
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
218
+ }
219
+ function decodeDiscoveryCursor(value, fingerprint, total, integrityKey) {
220
+ if (value === undefined)
221
+ return 0;
222
+ try {
223
+ const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
224
+ if (decoded.version !== 1 ||
225
+ !Number.isInteger(decoded.offset) ||
226
+ (decoded.offset ?? -1) < 0 ||
227
+ Number(decoded.offset) > total ||
228
+ typeof decoded.signature !== "string") {
229
+ throw new Error("cursor fields do not match the request");
230
+ }
231
+ const expectedSignature = Buffer.from(cursorSignature(fingerprint, decoded.offset, integrityKey), "base64url");
232
+ const suppliedSignature = Buffer.from(decoded.signature, "base64url");
233
+ if (suppliedSignature.length !== expectedSignature.length ||
234
+ !timingSafeEqual(suppliedSignature, expectedSignature)) {
235
+ throw new Error("cursor signature does not match the request");
236
+ }
237
+ return decoded.offset;
238
+ }
239
+ catch (error) {
240
+ throw new PmCliError("Invalid or stale pm tool discovery cursor; restart discovery without cursor.", 64, { reason: String(error) });
241
+ }
242
+ }
243
+ function appliedSignal(value, weight, source) {
244
+ return {
245
+ value: normalizedUnitInterval(value),
246
+ weight,
247
+ available: source !== "unavailable",
248
+ source,
249
+ };
250
+ }
251
+ function hostOrComputedSignal(hostValue, computedValue, weight) {
252
+ return appliedSignal(hostValue ?? computedValue, weight, hostValue === undefined ? "computed" : "host");
253
+ }
254
+ function buildRankedRow(candidate, query, includeSchema) {
255
+ const command = Object.hasOwn(PM_MCP_TOOL_COMMAND_CONTRACTS, candidate.name)
256
+ ? PM_MCP_TOOL_COMMAND_CONTRACTS[candidate.name]
257
+ : "help";
258
+ const family = resolvePmCommandCapabilityFamily(command);
259
+ const searchableText = `${candidate.name} ${candidate.description} ${command} ${family}`;
260
+ const lexical = lexicalRelevance(query, searchableText);
261
+ const queryTokens = new Set(normalizedSearchTokens(query));
262
+ const signals = {
263
+ lexical: appliedSignal(lexical, DISCOVERY_WEIGHTS.lexical, "computed"),
264
+ semantic: hostOrComputedSignal(candidate.signals?.semantic, semanticRelevance(query, searchableText), DISCOVERY_WEIGHTS.semantic),
265
+ graph: hostOrComputedSignal(candidate.signals?.graph, queryTokens.has(family) || queryTokens.has(command) ? 1 : 0, DISCOVERY_WEIGHTS.graph),
266
+ permission: appliedSignal(1, DISCOVERY_WEIGHTS.permission, "authorization"),
267
+ freshness: hostOrComputedSignal(candidate.signals?.freshness, 1, DISCOVERY_WEIGHTS.freshness),
268
+ usage: hostOrComputedSignal(candidate.signals?.usage, PM_MCP_ENTRY_TOOL_NAMES.includes(candidate.name) ? 1 : 0, DISCOVERY_WEIGHTS.usage),
269
+ };
270
+ const score = Object.values(signals).reduce((total, signal) => total + signal.value * signal.weight, 0);
271
+ return {
272
+ name: candidate.name,
273
+ description: candidate.description,
274
+ command,
275
+ tier: resolvePmCommandVisibilityTier(command),
276
+ family,
277
+ score: Number(score.toFixed(6)),
278
+ signals,
279
+ ...(includeSchema
280
+ ? { input_schema: structuredClone(candidate.inputSchema) }
281
+ : {}),
282
+ };
283
+ }
284
+ /** Reject runtime filter values that bypass the public TypeScript unions. */
285
+ function validateDiscoveryFilters(options) {
286
+ if (options.cursorIntegrityKey !== undefined &&
287
+ (!(options.cursorIntegrityKey instanceof Uint8Array) ||
288
+ options.cursorIntegrityKey.byteLength < 32)) {
289
+ throw new PmCliError("pm tool discovery cursorIntegrityKey must contain at least 32 bytes.", 64);
290
+ }
291
+ if (options.tier !== undefined &&
292
+ (typeof options.tier !== "string" ||
293
+ !SELECTABLE_TIERS.includes(options.tier))) {
294
+ throw new PmCliError("pm tool discovery tier must be core, standard, or full.", 64);
295
+ }
296
+ if (options.family !== undefined &&
297
+ (typeof options.family !== "string" ||
298
+ !CAPABILITY_FAMILIES.has(options.family))) {
299
+ throw new PmCliError("pm tool discovery family must be a declared capability family.", 64);
300
+ }
301
+ if (options.profile !== undefined &&
302
+ (typeof options.profile !== "string" ||
303
+ !MCP_TOOL_PROFILES.has(options.profile))) {
304
+ throw new PmCliError("pm tool discovery profile must be core, standard, full, or custom.", 64);
305
+ }
306
+ }
307
+ function resolveDiscoveryRequest(options) {
308
+ const rawQuery = boundedDiscoveryString(options.query === undefined ? "" : options.query, "query");
309
+ const query = rawQuery.trim().replace(/\s+/gu, " ");
310
+ boundedDiscoveryString(options.cursor === undefined ? "" : options.cursor, "cursor");
311
+ const limit = discoveryLimit(options.limit === undefined ? 10 : options.limit);
312
+ const budget = discoveryOutputBudget(options.outputBudget === undefined ? 1_200 : options.outputBudget);
313
+ validateDiscoveryFilters(options);
314
+ return {
315
+ query,
316
+ includeSchema: discoveryIncludeSchema(options.includeSchema),
317
+ limit,
318
+ budget,
319
+ maximumTier: TIER_RANK[options.tier ?? "full"],
320
+ profile: options.profile === undefined ? "core" : options.profile,
321
+ };
322
+ }
323
+ /**
324
+ * Measure one result exactly while allowing callers to supply pre-serialized
325
+ * array contents, so budget selection stays linear in the returned row bytes.
326
+ */
327
+ function estimateDiscoveryResultTokens(resultWithoutCost, budget, serializedToolRowsBytes) {
328
+ const toolRowsBytes = serializedToolRowsBytes ??
329
+ resultWithoutCost.tools.reduce((total, row, index) => total +
330
+ (index === 0 ? 0 : 1) +
331
+ Buffer.byteLength(JSON.stringify(row), "utf8"), 0);
332
+ const emptyToolsResult = { ...resultWithoutCost, tools: [] };
333
+ let estimatedTokens = 0;
334
+ for (let iteration = 0; iteration < 4; iteration += 1) {
335
+ const envelopeBytes = Buffer.byteLength(JSON.stringify({
336
+ ...emptyToolsResult,
337
+ token_cost: {
338
+ estimated_tokens: estimatedTokens,
339
+ budget,
340
+ within_budget: true,
341
+ },
342
+ }), "utf8");
343
+ const nextEstimate = Math.ceil((envelopeBytes + toolRowsBytes) / 4);
344
+ if (nextEstimate === estimatedTokens)
345
+ break;
346
+ estimatedTokens = nextEstimate;
347
+ }
348
+ return estimatedTokens;
349
+ }
350
+ function rankedDiscoveryRows(candidates, options, request) {
351
+ return candidates
352
+ .filter((candidate) => candidate.authorized !== false)
353
+ .map((candidate) => buildRankedRow(candidate, request.query, request.includeSchema))
354
+ .filter((row) => row.tier !== "internal" &&
355
+ TIER_RANK[row.tier] <= request.maximumTier &&
356
+ (options.family === undefined || row.family === options.family))
357
+ .sort((left, right) => right.score - left.score ||
358
+ (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
359
+ }
360
+ function rowsWithinDiscoveryBudget(candidates, budget, buildResultWithoutCost) {
361
+ if (budget === "unbounded")
362
+ return [...candidates];
363
+ const selected = [];
364
+ let serializedToolRowsBytes = 0;
365
+ for (const [index, row] of candidates.entries()) {
366
+ const candidateRowsBytes = serializedToolRowsBytes +
367
+ (index === 0 ? 0 : 1) +
368
+ Buffer.byteLength(JSON.stringify(row), "utf8");
369
+ const estimatedTokens = estimateDiscoveryResultTokens(buildResultWithoutCost(index + 1), budget, candidateRowsBytes);
370
+ if (estimatedTokens > budget)
371
+ break;
372
+ selected.push(row);
373
+ serializedToolRowsBytes = candidateRowsBytes;
374
+ }
375
+ return selected;
376
+ }
377
+ /**
378
+ * Rank and page an authorized MCP tool catalog under an explicit token budget.
379
+ *
380
+ * Cursors fail closed when the query, filters, schemas, authorization-filtered
381
+ * catalog, or ranking inputs change. Host signals override documented local
382
+ * fallbacks without changing the public formula or hiding signal provenance.
383
+ */
384
+ export function discoverPmTools(candidates, options = {}) {
385
+ validateDiscoveryCandidates(candidates);
386
+ validateDiscoveryOptionsContainer(options);
387
+ const request = resolveDiscoveryRequest(options);
388
+ const cursorIntegrityKey = options.cursorIntegrityKey ?? PROCESS_CURSOR_INTEGRITY_KEY;
389
+ const ranked = rankedDiscoveryRows(candidates, options, request);
390
+ const fingerprint = hashDiscoveryValue({
391
+ query: request.query,
392
+ family: options.family ?? null,
393
+ tier: options.tier ?? "full",
394
+ includeSchema: request.includeSchema,
395
+ profile: request.profile,
396
+ ranked,
397
+ });
398
+ const offset = decodeDiscoveryCursor(options.cursor, fingerprint, ranked.length, cursorIntegrityKey);
399
+ const pageCandidates = ranked.slice(offset, offset + request.limit);
400
+ const omitted = [];
401
+ if (!request.includeSchema) {
402
+ omitted.push({
403
+ name: "input_schema",
404
+ reason: "compact_projection",
405
+ restore_with: "Set includeSchema=true.",
406
+ });
407
+ }
408
+ if (offset + request.limit < ranked.length) {
409
+ omitted.push({
410
+ name: "tools",
411
+ reason: "limit",
412
+ restore_with: "Continue with next_cursor or increase limit.",
413
+ });
414
+ }
415
+ const base = {
416
+ result_type: "pm_tool_discovery",
417
+ contract_version: 1,
418
+ query: request.query,
419
+ profile: request.profile,
420
+ total: ranked.length,
421
+ ranking_policy: {
422
+ version: 1,
423
+ formula: "weighted_sum_then_name",
424
+ weights: DISCOVERY_WEIGHTS,
425
+ },
426
+ cache: {
427
+ key: hashDiscoveryValue({
428
+ fingerprint,
429
+ cursorIntegrityKey: createHash("sha256")
430
+ .update(cursorIntegrityKey)
431
+ .digest("hex"),
432
+ limit: request.limit,
433
+ offset,
434
+ budget: request.budget,
435
+ }),
436
+ ttl_ms: 30_000,
437
+ scope: "private",
438
+ invalidates_on: DISCOVERY_CACHE_INVALIDATIONS,
439
+ },
440
+ };
441
+ const buildResultWithoutCost = (selected, selectedCount = selected.length) => {
442
+ const endOffset = offset + selectedCount;
443
+ const hasMore = endOffset < ranked.length;
444
+ const resultOmissions = [...omitted];
445
+ if (selectedCount < pageCandidates.length) {
446
+ resultOmissions.push({
447
+ name: "tools",
448
+ reason: "token_budget",
449
+ restore_with: "Continue with next_cursor or increase outputBudget.",
450
+ });
451
+ }
452
+ const resultWithoutCost = {
453
+ ...base,
454
+ tools: [...selected],
455
+ returned: selectedCount,
456
+ has_more: hasMore,
457
+ ...(hasMore
458
+ ? {
459
+ next_cursor: encodeDiscoveryCursor(fingerprint, endOffset, cursorIntegrityKey),
460
+ }
461
+ : {}),
462
+ omission_receipt: {
463
+ has_omissions: resultOmissions.length > 0,
464
+ omitted: resultOmissions,
465
+ },
466
+ };
467
+ return resultWithoutCost;
468
+ };
469
+ const buildResult = (selected) => {
470
+ const resultWithoutCost = buildResultWithoutCost(selected);
471
+ const estimatedTokens = estimateDiscoveryResultTokens(resultWithoutCost, request.budget);
472
+ return {
473
+ ...resultWithoutCost,
474
+ token_cost: {
475
+ estimated_tokens: estimatedTokens,
476
+ budget: request.budget,
477
+ within_budget: request.budget === "unbounded" || estimatedTokens <= request.budget,
478
+ },
479
+ };
480
+ };
481
+ const selected = rowsWithinDiscoveryBudget(pageCandidates, request.budget, (selectedCount) => buildResultWithoutCost([], selectedCount));
482
+ const result = buildResult(selected);
483
+ if (pageCandidates.length > 0 && selected.length === 0) {
484
+ const oneRowCost = buildResult([pageCandidates[0]]).token_cost
485
+ .estimated_tokens;
486
+ throw new PmCliError(`pm tool discovery outputBudget is too small to return a tool; increase it to at least ${oneRowCost}.`, 64, { required: String(oneRowCost) });
487
+ }
488
+ if (request.budget !== "unbounded" &&
489
+ result.token_cost.estimated_tokens > request.budget) {
490
+ throw new PmCliError(`pm tool discovery outputBudget is too small; at least ${result.token_cost.estimated_tokens} estimated tokens are required for this page.`, 64, { required: String(result.token_cost.estimated_tokens) });
491
+ }
492
+ return result;
493
+ }
494
+ //# sourceMappingURL=discovery.js.map
495
+ //# debugId=4853ebbf-f28a-55f1-b306-aa75cb9355ae
@@ -7,7 +7,7 @@
7
7
  * this module so packages and hosts can reuse the same merge behavior.
8
8
  */
9
9
  export { ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, type HistoryMergeResult, type HistoryMergeStrategy, type ItemDocumentMergeResult, type JsonDocumentMergeResult, type ItemMergeConflictDecision, type MergePreferredSide, type RelationshipStreamMergeResult, } from "./three-way.js";
10
- export { listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptReport, summarizeMergeReceipt, type MergeDecisionReceipt, type MergeDecisionReceiptSummary, type MergeReceiptReport, } from "./receipts.js";
10
+ export { inspectMergeReceiptEvidence, listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptEvidenceReport, runMergeReceiptReport, summarizeMergeReceipt, type MergeDecisionReceipt, type MergeDecisionReceiptSummary, type MergeReceiptEvidenceReport, type MergeReceiptEvidenceScan, type MergeReceiptInvalidEvidence, type MergeReceiptReport, } from "./receipts.js";
11
11
  export { MERGE_DRIVER_ARTIFACT_VALUES, runMergeDriver, type MergeDriverArtifact, type MergeDriverOptions, type MergeDriverResult, } from "./driver.js";
12
12
  export { auditMergeAttributeFence, auditMergeDriverConfiguration, buildMergeAttributePatterns, findGitWorkspaceRoot, PM_GITATTRIBUTES_END, PM_GITATTRIBUTES_START, PM_GITATTRIBUTES_V2_END, PM_GITATTRIBUTES_V2_START, refreshMergeAttributeFenceIfInstalled, resolveMergeInstallContext, runMergeInstall, type MergeFenceAuditResult, type MergeDriverConfigurationAuditResult, type MergeFenceRefreshOutcome, type MergeInstallOptions, type MergeInstallContextResolution, type MergeInstallResult, } from "./install.js";
13
13
  export { runMergeReconcile, type MergeReconcileOptions, type MergeReconcileResult, } from "./reconcile.js";
@@ -7,11 +7,11 @@
7
7
  * this module so packages and hosts can reuse the same merge behavior.
8
8
  */
9
9
 
10
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="687855bf-7432-5691-a809-1f0406b5d8a8")}catch(e){}}();
10
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0a17e896-d54a-5796-8be3-535179c4d6e0")}catch(e){}}();
11
11
  export { ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, } from "./three-way.js";
12
- export { listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptReport, summarizeMergeReceipt, } from "./receipts.js";
12
+ export { inspectMergeReceiptEvidence, listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptEvidenceReport, runMergeReceiptReport, summarizeMergeReceipt, } from "./receipts.js";
13
13
  export { MERGE_DRIVER_ARTIFACT_VALUES, runMergeDriver, } from "./driver.js";
14
14
  export { auditMergeAttributeFence, auditMergeDriverConfiguration, buildMergeAttributePatterns, findGitWorkspaceRoot, PM_GITATTRIBUTES_END, PM_GITATTRIBUTES_START, PM_GITATTRIBUTES_V2_END, PM_GITATTRIBUTES_V2_START, refreshMergeAttributeFenceIfInstalled, resolveMergeInstallContext, runMergeInstall, } from "./install.js";
15
15
  export { runMergeReconcile, } from "./reconcile.js";
16
16
  //# sourceMappingURL=index.js.map
17
- //# debugId=687855bf-7432-5691-a809-1f0406b5d8a8
17
+ //# debugId=0a17e896-d54a-5796-8be3-535179c4d6e0