@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
@@ -19,7 +19,7 @@
19
19
  */
20
20
  /** Registry of every non-extension `pm health` / `pm validate` warning code and its remediation. Extension findings keep their richer, contextual `details.triage.remediation` produced by the extension health triage and are intentionally excluded here (see pm-0hnu: "all non-extension checks"). */
21
21
 
22
- !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]="bb094b44-42b2-5a1c-9fc2-7a0eeaf9fbfd")}catch(e){}}();
22
+ !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]="24a69725-363a-59e1-96c6-1e1ca079a311")}catch(e){}}();
23
23
  export const REMEDIATION_REGISTRY = Object.freeze([
24
24
  // --- pm health: directories ---
25
25
  {
@@ -200,8 +200,13 @@ export const REMEDIATION_REGISTRY = Object.freeze([
200
200
  },
201
201
  {
202
202
  code: "merge_receipt_evidence_invalid",
203
- command: "pm health --check-only --full",
204
- summary: "One or more receipt files failed bounded-file, schema, or identity validation; use the full health count to inspect the clone-local and durable receipt directories, then repair or remove only confirmed invalid evidence.",
203
+ command: "pm merge report --include-reconciled --json",
204
+ summary: "One or more receipt candidates failed bounded-file, schema, identity, or copy-consistency validation; inspect the report's privacy-safe source, receipt id, and reason before repairing only the named evidence.",
205
+ },
206
+ {
207
+ code: "merge_receipt_history_reference_missing",
208
+ command: "pm health --check-only --full --json",
209
+ summary: "One or more append-only history coordinates reference receipt evidence that is no longer present; inspect the bounded item, line, and privacy-safe receipt locator, then recover the named evidence from an authoritative clone or backup without rewriting history.",
205
210
  },
206
211
  {
207
212
  code: "validate_merge_decisions_unreviewed",
@@ -499,4 +504,4 @@ export function buildRemediationCommands(warnings) {
499
504
  return Object.values(buildRemediationMap(warnings));
500
505
  }
501
506
  //# sourceMappingURL=remediation.js.map
502
- //# debugId=bb094b44-42b2-5a1c-9fc2-7a0eeaf9fbfd
507
+ //# debugId=24a69725-363a-59e1-96c6-1e1ca079a311
@@ -1,14 +1,14 @@
1
1
 
2
- !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]="65c0fed6-315a-5930-a23f-83996d3f5081")}catch(e){}}();
2
+ !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]="1c6c65ef-115b-50e2-815e-84f6e91158cd")}catch(e){}}();
3
3
  import fs from "node:fs/promises";
4
4
  import { createHash } from "node:crypto";
5
5
  import path from "node:path";
6
6
  import { getHistoryPath } from "../store/paths.js";
7
7
  import { isFileMissingError, writeFileAtomic } from "../fs/fs-utils.js";
8
- import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentForVersion, } from "./history.js";
8
+ import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, } from "./history.js";
9
9
  import { verifyHistoryChainWithVersion } from "./replay.js";
10
10
  import { getWorkspaceHistoryPath, inspectWorkspaceHistoryState, WORKSPACE_HISTORY_ID, } from "./workspace-history.js";
11
- const DRIFT_CACHE_VERSION = 7;
11
+ const DRIFT_CACHE_VERSION = 9;
12
12
  const DRIFT_CACHE_FILENAME = "history-drift-cache.json";
13
13
  function getDriftCachePath(pmRoot) {
14
14
  return path.join(pmRoot, "runtime", DRIFT_CACHE_FILENAME);
@@ -237,8 +237,8 @@ async function scanItemHistory(pmRoot, item, cache, accumulator) {
237
237
  const { body, ...itemMetadata } = item;
238
238
  if (!resolved.verification.latestHashComparable)
239
239
  return resolved.cacheDirty;
240
- const currentHash = hashDocumentForVersion({ metadata: itemMetadata, body }, resolved.verification.itemHashVersion);
241
- if (currentHash !== resolved.verification.latestAfterHash)
240
+ const currentHashes = hashDocumentVerificationCandidates({ metadata: itemMetadata, body }, resolved.verification.itemHashVersion);
241
+ if (!currentHashes.includes(resolved.verification.latestAfterHash))
242
242
  accumulator.hashMismatches.push(item.id);
243
243
  return resolved.cacheDirty;
244
244
  }
@@ -330,4 +330,4 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
330
330
  return { ...accumulator, driftedItems };
331
331
  }
332
332
  //# sourceMappingURL=drift-scan.js.map
333
- //# debugId=65c0fed6-315a-5930-a23f-83996d3f5081
333
+ //# debugId=1c6c65ef-115b-50e2-815e-84f6e91158cd
@@ -12,6 +12,12 @@ declare function isDefinedPointerPath(document: unknown, path: string): boolean;
12
12
  export declare function hashDocument(document: ItemDocument): string;
13
13
  /** Hash an item with an explicit canonicalization epoch for replay compatibility. */
14
14
  export declare function hashDocumentForVersion(document: ItemDocument, version: HistoryItemHashVersion): string;
15
+ /**
16
+ * Return the ordered hash candidates accepted when verifying one immutable
17
+ * history epoch. Epoch 2 has both its established expanded hash first and the
18
+ * earlier field-frozen writer hash second; the other epochs have one form.
19
+ */
20
+ export declare function hashDocumentVerificationCandidates(document: ItemDocument, version: HistoryItemHashVersion): [string, ...string[]];
15
21
  /** Implements hash empty document for the public runtime surface of this module. */
16
22
  export declare function hashEmptyDocument(): string;
17
23
  /** Implements create history entry for the public runtime surface of this module. */
@@ -4,7 +4,7 @@
4
4
  * Implements append-only history and replay behavior for History.
5
5
  */
6
6
 
7
- !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]="8581413f-e74e-5db3-86bf-5e39290de741")}catch(e){}}();
7
+ !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]="60d6ec0e-a8ed-5a5d-b5bd-d29da8532a79")}catch(e){}}();
8
8
  import jsonPatch from "fast-json-patch";
9
9
  import { ITEM_METADATA_KEY_ORDER } from "../shared/constants.js";
10
10
  import { runActiveServiceOverride } from "../extensions/index.js";
@@ -117,7 +117,7 @@ function normalizeHistoryPatchOps(beforeDocument, patch) {
117
117
  }
118
118
  return normalized;
119
119
  }
120
- function canonicalHashDocument(document, version) {
120
+ function canonicalHashDocument(document, version, legacyV2Fields = false) {
121
121
  const hasMetadata = document.metadata && Object.keys(document.metadata).length > 0;
122
122
  if (!hasMetadata) {
123
123
  return {
@@ -126,12 +126,40 @@ function canonicalHashDocument(document, version) {
126
126
  };
127
127
  }
128
128
  const canonical = canonicalDocument(document);
129
- const metadata = version === 1 && canonical.metadata.tests
129
+ // Epoch 1 and the earliest epoch-2 writer predate linked-test provenance,
130
+ // workspace isolation, per-execution receipts, and case-preserving
131
+ // dependency ids. Later epoch-2 writers included those fields without
132
+ // advancing the marker, so verification must retain both immutable forms.
133
+ const usesLegacyFields = version === 1 || (version === 2 && legacyV2Fields);
134
+ const epochMetadata = usesLegacyFields
130
135
  ? {
131
136
  ...canonical.metadata,
132
- tests: [...canonical.metadata.tests].sort(compareLegacyLinkedTests),
137
+ ...(canonical.metadata.dependencies === undefined
138
+ ? {}
139
+ : {
140
+ dependencies: canonical.metadata.dependencies.map((dependency) => ({
141
+ ...dependency,
142
+ id: dependency.id.toLowerCase(),
143
+ })),
144
+ }),
145
+ ...(canonical.metadata.tests === undefined
146
+ ? {}
147
+ : {
148
+ tests: canonical.metadata.tests.map(({ workspace_context_mode: _workspaceContextMode, provenance: _provenance, provenance_invalid: _provenanceInvalid, ...test }) => test),
149
+ }),
150
+ ...(canonical.metadata.test_runs === undefined
151
+ ? {}
152
+ : {
153
+ test_runs: canonical.metadata.test_runs.map(({ executions: _executions, ...testRun }) => testRun),
154
+ }),
133
155
  }
134
156
  : canonical.metadata;
157
+ const metadata = version === 1 && epochMetadata.tests
158
+ ? {
159
+ ...epochMetadata,
160
+ tests: [...epochMetadata.tests].sort(compareLegacyLinkedTests),
161
+ }
162
+ : epochMetadata;
135
163
  const orderedMetadata = orderObject(toItemRecord(metadata), ITEM_METADATA_KEY_ORDER);
136
164
  return {
137
165
  front_matter: orderedMetadata,
@@ -164,6 +192,20 @@ export function hashDocumentForVersion(document, version) {
164
192
  }
165
193
  return sha256Hex(stableStringify(canonicalHashDocument(document, version)));
166
194
  }
195
+ /**
196
+ * Return the ordered hash candidates accepted when verifying one immutable
197
+ * history epoch. Epoch 2 has both its established expanded hash first and the
198
+ * earlier field-frozen writer hash second; the other epochs have one form.
199
+ */
200
+ export function hashDocumentVerificationCandidates(document, version) {
201
+ const canonicalHash = hashDocumentForVersion(document, version);
202
+ return version === 2
203
+ ? [
204
+ canonicalHash,
205
+ sha256Hex(stableStringify(canonicalHashDocument(document, version, true))),
206
+ ]
207
+ : [canonicalHash];
208
+ }
167
209
  /** Implements hash empty document for the public runtime surface of this module. */
168
210
  export function hashEmptyDocument() {
169
211
  return sha256Hex(stableStringify(EMPTY_LEGACY_HASH_DOCUMENT));
@@ -292,4 +334,4 @@ export const _testOnly = {
292
334
  isDefinedPointerPath,
293
335
  };
294
336
  //# sourceMappingURL=history.js.map
295
- //# debugId=8581413f-e74e-5db3-86bf-5e39290de741
337
+ //# debugId=60d6ec0e-a8ed-5a5d-b5bd-d29da8532a79
@@ -21,6 +21,12 @@ export declare const EMPTY_REPLAY_DOCUMENT: ReplayDocument;
21
21
  export declare function cloneEmptyReplayDocument(): ReplayDocument;
22
22
  /** Implements replay hash for the public runtime surface of this module. */
23
23
  export declare function replayHash(document: ReplayDocument, version?: HistoryItemHashVersion): string;
24
+ /**
25
+ * Return hash candidates that preserve the semantic variant within an epoch.
26
+ * Duplicate values are intentional: before and after candidates use the same
27
+ * index when a patch introduces a field that distinguishes legacy epoch 2.
28
+ */
29
+ export declare function replayHashVerificationCandidates(document: ReplayDocument, version: HistoryItemHashVersion): [string, ...string[]];
24
30
  /** Implements replay to item document for the public runtime surface of this module. */
25
31
  export declare function replayToItemDocument(document: ReplayDocument): ItemDocument;
26
32
  /** Converts a materialized replay document into a canonical item document. Use this when callers have already rejected the empty/deleted replay state and need restored metadata validated through the normal item-metadata rules. */
@@ -98,6 +104,11 @@ export interface ReanchorResult {
98
104
  /** Whether rewritten entries explicitly carry the hash epoch field. */
99
105
  explicitItemHashVersion: boolean;
100
106
  }
107
+ /** Controls whether re-anchoring preserves historical writer transitions or emits one exact hash surface. */
108
+ export interface ReanchorHistoryOptions {
109
+ /** Keep one semantic hash candidate for the complete rewritten stream so adjacent stored endpoints are identical. */
110
+ continuousHashSurface?: boolean;
111
+ }
101
112
  /**
102
113
  * Resolve the hash epoch a repair must retain.
103
114
  *
@@ -108,6 +119,6 @@ export interface ReanchorResult {
108
119
  */
109
120
  export declare function resolveHistoryRepairItemHashVersion(entries: HistoryEntry[]): HistoryItemHashVersion;
110
121
  /** Re-anchor a drifted history chain: replay every entry from empty, recompute the before/after hashes, and only rewrite a patch when the original op set no longer strictly applies (legacy drift). Clean entries keep their patch verbatim so the on-disk diff stays minimal. The returned chain verifies via verifyHistoryChain. */
111
- export declare function reanchorHistoryEntries(entries: HistoryEntry[], itemHashVersion?: 1 | 2 | 3): ReanchorResult;
122
+ export declare function reanchorHistoryEntries(entries: HistoryEntry[], itemHashVersion?: 1 | 2 | 3, options?: ReanchorHistoryOptions): ReanchorResult;
112
123
  /** Implements history entries to raw for the public runtime surface of this module. */
113
124
  export declare function historyEntriesToRaw(entries: HistoryEntry[]): string;
@@ -4,13 +4,13 @@
4
4
  * Implements append-only history and replay behavior for Replay.
5
5
  */
6
6
 
7
- !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]="c823af51-0cfd-536b-8f85-1f3e72db1c86")}catch(e){}}();
7
+ !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]="c9488ed4-8228-5e7c-9717-69dd7a3275c4")}catch(e){}}();
8
8
  import jsonPatch from "fast-json-patch";
9
9
  import { ITEM_METADATA_KEY_ORDER } from "../shared/constants.js";
10
10
  import { canonicalDocument } from "../item/item-format.js";
11
11
  import { toItemRecord } from "../item/item-record.js";
12
12
  import { orderObject, sha256Hex, stableStringify, } from "../shared/serialization.js";
13
- import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentForVersion, } from "./history.js";
13
+ import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, } from "./history.js";
14
14
  /** Public contract for empty replay document, shared by SDK and presentation-layer consumers. */
15
15
  export const EMPTY_REPLAY_DOCUMENT = {
16
16
  metadata: {},
@@ -22,23 +22,42 @@ export function cloneEmptyReplayDocument() {
22
22
  }
23
23
  /** Implements replay hash for the public runtime surface of this module. */
24
24
  export function replayHash(document, version = CURRENT_HISTORY_ITEM_HASH_VERSION) {
25
+ return replayHashVerificationCandidates(document, version)[0];
26
+ }
27
+ /**
28
+ * Return hash candidates that preserve the semantic variant within an epoch.
29
+ * Duplicate values are intentional: before and after candidates use the same
30
+ * index when a patch introduces a field that distinguishes legacy epoch 2.
31
+ */
32
+ export function replayHashVerificationCandidates(document, version) {
25
33
  if (Object.keys(document.metadata).length === 0 ||
26
34
  Array.isArray(document.metadata.tags)) {
27
35
  try {
28
- return hashDocumentForVersion(replayToItemDocument(document), version);
36
+ return hashDocumentVerificationCandidates(replayToItemDocument(document), version);
29
37
  }
30
38
  catch {
31
39
  // Fall through when another malformed legacy field cannot be canonicalized.
32
40
  }
33
41
  }
34
- // Preserve the structural hashes recorded before missing tags became safe to
35
- // normalize. Pre-create/legacy replay states and other malformed metadata cannot
36
- // switch hash algorithms without invalidating immutable history chains.
37
- return sha256Hex(stableStringify({
42
+ const fallback = sha256Hex(stableStringify({
38
43
  replay_fallback: true,
39
44
  metadata: document.metadata,
40
45
  body: document.body,
41
46
  }));
47
+ return version === 2 ? [fallback, fallback] : [fallback];
48
+ }
49
+ /** Match one recorded entry against corresponding before/after hash variants. */
50
+ function matchReplayHashCandidates(before, after, version, entry) {
51
+ const beforeHashes = replayHashVerificationCandidates(before, version);
52
+ const afterHashes = replayHashVerificationCandidates(after, version);
53
+ return {
54
+ beforeHashes,
55
+ afterHashes,
56
+ pairIndex: beforeHashes.findIndex((beforeHash, hashIndex) => beforeHash === entry.before_hash &&
57
+ afterHashes[hashIndex] === entry.after_hash),
58
+ beforeIndex: beforeHashes.indexOf(entry.before_hash),
59
+ afterIndex: afterHashes.indexOf(entry.after_hash),
60
+ };
42
61
  }
43
62
  /** Implements replay to item document for the public runtime surface of this module. */
44
63
  export function replayToItemDocument(document) {
@@ -156,22 +175,9 @@ export function verifyHistoryChainWithVersion(entries) {
156
175
  for (let index = 0; index < entries.length; index += 1) {
157
176
  const entry = entries[index];
158
177
  const explicitVersion = entry.item_hash_version;
159
- if (isUnsupportedExplicitHistoryItemHashVersion(explicitVersion)) {
178
+ const unsupportedVersion = isUnsupportedExplicitHistoryItemHashVersion(explicitVersion);
179
+ if (unsupportedVersion) {
160
180
  errors.push(`verify_failed:unsupported_item_hash_version:${String(explicitVersion)}:entry_${index + 1}`);
161
- const applied = tryApplyReplayPatch(replay, entry.patch);
162
- if (!applied.ok) {
163
- return {
164
- ok: false,
165
- errors: [
166
- ...errors,
167
- `verify_failed:patch_apply_failed:entry_${index + 1}`,
168
- ],
169
- };
170
- }
171
- replay = applied.document;
172
- detectedVersion = undefined;
173
- authoritativeExplicitVersion = undefined;
174
- continue;
175
181
  }
176
182
  const applied = tryApplyReplayPatch(replay, entry.patch);
177
183
  if (!applied.ok) {
@@ -183,30 +189,32 @@ export function verifyHistoryChainWithVersion(entries) {
183
189
  ],
184
190
  };
185
191
  }
192
+ if (unsupportedVersion) {
193
+ replay = applied.document;
194
+ detectedVersion = undefined;
195
+ authoritativeExplicitVersion = undefined;
196
+ continue;
197
+ }
186
198
  // Prefer legacy epoch 1 when an unversioned entry is valid under both
187
199
  // algorithms, but retain compatibility with transitional epoch-2 writers
188
200
  // that shipped before item_hash_version became explicit.
189
201
  const candidates = historyEntryHashCandidates(explicitVersion, authoritativeExplicitVersion);
190
- const beforeMatches = candidates.filter((version) => replayHash(replay, version) === entry.before_hash);
191
- if (beforeMatches.length === 0) {
202
+ const candidateMatches = candidates.map((version) => ({
203
+ version,
204
+ match: matchReplayHashCandidates(replay, applied.document, version, entry),
205
+ }));
206
+ const matchingVersion = candidateMatches.find(({ match }) => match.pairIndex >= 0);
207
+ if (matchingVersion === undefined) {
208
+ const beforeMatches = candidateMatches.some(({ match }) => match.beforeIndex >= 0);
192
209
  return {
193
210
  ok: false,
194
211
  errors: [
195
212
  ...errors,
196
- `verify_failed:before_hash_mismatch:entry_${index + 1}`,
197
- ],
198
- };
199
- }
200
- const version = beforeMatches.find((candidate) => replayHash(applied.document, candidate) === entry.after_hash);
201
- if (version === undefined) {
202
- return {
203
- ok: false,
204
- errors: [
205
- ...errors,
206
- `verify_failed:after_hash_mismatch:entry_${index + 1}`,
213
+ `verify_failed:${beforeMatches ? "after" : "before"}_hash_mismatch:entry_${index + 1}`,
207
214
  ],
208
215
  };
209
216
  }
217
+ const version = matchingVersion.version;
210
218
  replay = applied.document;
211
219
  detectedVersion = version;
212
220
  authoritativeExplicitVersion =
@@ -297,7 +305,7 @@ export function resolveHistoryRepairItemHashVersion(entries) {
297
305
  return CURRENT_HISTORY_ITEM_HASH_VERSION;
298
306
  }
299
307
  /** Re-anchor a drifted history chain: replay every entry from empty, recompute the before/after hashes, and only rewrite a patch when the original op set no longer strictly applies (legacy drift). Clean entries keep their patch verbatim so the on-disk diff stays minimal. The returned chain verifies via verifyHistoryChain. */
300
- export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistoryRepairItemHashVersion(entries)) {
308
+ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistoryRepairItemHashVersion(entries), options = {}) {
301
309
  const unsupportedIndex = entries.findIndex((entry) => isUnsupportedExplicitHistoryItemHashVersion(entry.item_hash_version));
302
310
  if (unsupportedIndex >= 0) {
303
311
  throw new TypeError(`unsupported_item_hash_version:${String(entries[unsupportedIndex]?.item_hash_version)}:entry_${unsupportedIndex + 1}`);
@@ -309,11 +317,11 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
309
317
  let entriesPatchRepaired = 0;
310
318
  let convertedReplaceToAdd = 0;
311
319
  let skippedOps = 0;
320
+ let semanticIndex;
312
321
  const explicitItemHashVersion = itemHashVersion !== 1 ||
313
322
  entries.some((entry) => entry.item_hash_version !== undefined);
314
323
  for (let index = 0; index < entries.length; index += 1) {
315
324
  const entry = entries[index];
316
- const beforeHash = replayHash(replay, itemHashVersion);
317
325
  const strict = tryApplyReplayPatch(replay, entry.patch);
318
326
  let next;
319
327
  let outPatch;
@@ -335,7 +343,20 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
335
343
  skippedOps += entrySkipped;
336
344
  entriesPatchRepaired += 1;
337
345
  }
338
- const afterHash = replayHash(next, itemHashVersion);
346
+ const hashMatch = matchReplayHashCandidates(replay, next, itemHashVersion, entry);
347
+ if (semanticIndex === undefined || options.continuousHashSurface !== true) {
348
+ semanticIndex = [
349
+ hashMatch.pairIndex,
350
+ hashMatch.beforeIndex,
351
+ hashMatch.afterIndex,
352
+ Math.min(semanticIndex ?? 0, hashMatch.beforeHashes.length - 1),
353
+ ].find((candidate) => candidate >= 0);
354
+ }
355
+ else {
356
+ semanticIndex = Math.min(semanticIndex, hashMatch.beforeHashes.length - 1);
357
+ }
358
+ const beforeHash = hashMatch.beforeHashes[semanticIndex];
359
+ const afterHash = hashMatch.afterHashes[semanticIndex];
339
360
  const rehashed = beforeHash !== entry.before_hash || afterHash !== entry.after_hash;
340
361
  if (rehashed) {
341
362
  entriesRehashed += 1;
@@ -382,4 +403,4 @@ export function historyEntriesToRaw(entries) {
382
403
  return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
383
404
  }
384
405
  //# sourceMappingURL=replay.js.map
385
- //# debugId=c823af51-0cfd-536b-8f85-1f3e72db1c86
406
+ //# debugId=c9488ed4-8228-5e7c-9717-69dd7a3275c4
@@ -21,8 +21,8 @@ declare function nearestDeclaredKey(unexpected: string, declared: string[]): str
21
21
  declare function detectUnexpectedTopLevelKeys(toolName: string, args: Record<string, unknown>, toolDeclaredKeys?: Map<string, string[]>): string[];
22
22
  declare function collectMutationGuardWarnings(toolName: string, action: string | undefined, args: Record<string, unknown>): Promise<string[]>;
23
23
  declare function detectUnexpectedOptionKeys(toolName: string, action: string | undefined, args: Record<string, unknown>): string[];
24
- declare function resultContent(result: unknown, warnings?: string[], tokenAccounting?: boolean): Record<string, unknown>;
25
- declare function errorContent(error: unknown): Record<string, unknown>;
24
+ declare function resultContent(result: unknown, warnings?: string[], tokenAccounting?: boolean, canonicalStructuredResult?: boolean): Record<string, unknown>;
25
+ declare function errorContent(error: unknown, canonicalStructuredResult?: boolean): Record<string, unknown>;
26
26
  /** Retain at most 32 own, valid provenance values without cloning unbounded input. */
27
27
  declare function boundMcpClientProvenance(value: unknown): Record<string, string>;
28
28
  declare function emitMcpChangeNotifications(action: string | undefined): Promise<void>;
@@ -5,7 +5,7 @@
5
5
  * Runs the MCP server adapter that exposes pm actions and contracts to external agents.
6
6
  */
7
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]="77067047-62fc-54e8-9047-ad30580d6125")}catch(e){}}();
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]="71cfdc6b-c990-51b6-a9ae-ed9bf98136f7")}catch(e){}}();
9
9
  import { realpathSync } from "node:fs";
10
10
  import { readFile } from "node:fs/promises";
11
11
  import path from "node:path";
@@ -17,6 +17,7 @@ import { readRequiredString, runAction, runWithActiveExtensions, } from "../sdk/
17
17
  import { NARROW_TOOL_ACTIONS, TOOLS } from "./tool-definitions.js";
18
18
  import { normalizeWorkspaceToolArguments, resolveMcpToolAccess, resolveMcpToolSurface, } from "./runtime-capabilities.js";
19
19
  import { PM_MCP_PROMPT_CONTRACTS, PM_MCP_RESOURCE_CONTRACTS, } from "../sdk/agent-capability-contracts.js";
20
+ import { PM_MCP_ENTRY_TOOL_NAMES, PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION, PM_MCP_PROGRESSIVE_DISCOVERY_SERVER_CAPABILITY, discoverPmTools, parsePmToolDiscoveryOptions, } from "../sdk/mcp/discovery.js";
20
21
  import { commitItemMutations } from "../sdk/item-transaction.js";
21
22
  import { isRuntimeRecord, markMcpMutationTransportInput, } from "../sdk/runtime-input.js";
22
23
  import { attachOutputTokenAccounting } from "../sdk/output-token-accounting.js";
@@ -59,6 +60,7 @@ const PM_MCP_SERVER_CAPABILITIES = {
59
60
  [PM_MCP_TASKS_EXTENSION]: {},
60
61
  [PM_MCP_APPS_EXTENSION]: PM_MCP_APPS_SERVER_CAPABILITY,
61
62
  [PM_MCP_SKILLS_EXTENSION]: PM_MCP_SKILLS_SERVER_CAPABILITY,
63
+ [PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION]: PM_MCP_PROGRESSIVE_DISCOVERY_SERVER_CAPABILITY,
62
64
  },
63
65
  };
64
66
  const PM_MCP_SUBSCRIPTIONS = new Map();
@@ -99,6 +101,7 @@ const PM_MCP_SKILL_METHODS = new Set([
99
101
  "resources/directory/read",
100
102
  ]);
101
103
  const PM_MCP_INSTRUCTIONS = "You have access to native pm CLI tools for git-based project management. " +
104
+ "When progressive discovery is negotiated, use pm_discover to expand the small entry catalog by intent and request schemas only when needed. " +
102
105
  "Use pm_next to pick the next actionable item, or pm_context or pm_search before creating new work. " +
103
106
  "Prefer narrow tools (pm_next, pm_context, pm_list, pm_get, pm_search, pm_events, pm_create, pm_mutate, pm_copy, pm_focus, pm_update, pm_append, pm_claim, pm_release, pm_close, pm_comments, pm_files, pm_docs, pm_notes, pm_learnings, pm_deps, pm_graph, pm_test, pm_validate, pm_health, pm_contracts, pm_schema, pm_profile, pm_config, pm_plan) over pm_run when they cover the operation. " +
104
107
  "Use pm_plan for agent harness Plan workflows: it provides Codex/Claude/Cursor-style planning with durable steps, dependencies, decisions, discoveries, validation, and materialization. " +
@@ -145,6 +148,17 @@ function nearestDeclaredKey(unexpected, declared) {
145
148
  // keys there are by-design rather than typos and must not be flagged.
146
149
  const UNEXPECTED_KEY_WARNING_EXEMPT_TOOLS = new Set(["pm_run"]);
147
150
  const MCP_RETRY_ARGUMENT_KEYS = new Set(["inputResponses", "requestState"]);
151
+ /** Resolve the optional shared cursor key without exposing it to tool input. */
152
+ function resolveMcpDiscoveryCursorIntegrityKey() {
153
+ const configuredKey = process.env.PM_MCP_DISCOVERY_CURSOR_KEY;
154
+ if (configuredKey === undefined)
155
+ return undefined;
156
+ const key = Buffer.from(configuredKey, "utf8");
157
+ if (key.byteLength < 32) {
158
+ throw new PmCliError("PM_MCP_DISCOVERY_CURSOR_KEY must contain at least 32 UTF-8 bytes.", 64);
159
+ }
160
+ return key;
161
+ }
148
162
  function detectUnexpectedTopLevelKeys(toolName, args, toolDeclaredKeys = declaredToolKeys()) {
149
163
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
150
164
  return [];
@@ -171,6 +185,29 @@ function detectUnexpectedTopLevelKeys(toolName, args, toolDeclaredKeys = declare
171
185
  }
172
186
  const HANDLERS = {
173
187
  pm_run: (args) => runMcpAction(args),
188
+ pm_discover: async (args) => {
189
+ const surface = await resolveMcpToolSurface(TOOLS, args);
190
+ const cursorIntegrityKey = resolveMcpDiscoveryCursorIntegrityKey();
191
+ const discoveryOptions = parsePmToolDiscoveryOptions(args);
192
+ return discoverPmTools(surface.tools.map((tool) => ({
193
+ name: tool.name,
194
+ description: tool.description,
195
+ inputSchema: tool.inputSchema,
196
+ authorized: true,
197
+ signals: {
198
+ freshness: 1,
199
+ usage: PM_MCP_ENTRY_TOOL_NAMES.includes(tool.name) ? 1 : 0,
200
+ },
201
+ })), {
202
+ ...discoveryOptions,
203
+ profile: surface.profile,
204
+ ...(cursorIntegrityKey === undefined
205
+ ? {}
206
+ : {
207
+ cursorIntegrityKey,
208
+ }),
209
+ });
210
+ },
174
211
  pm_mutate: async (args) => {
175
212
  const transactionId = readRequiredString(args, "transactionId");
176
213
  const controls = parseAtomicMutationControls(args);
@@ -279,7 +316,7 @@ function detectUnexpectedOptionKeys(toolName, action, args) {
279
316
  }
280
317
  return warnings;
281
318
  }
282
- function resultContent(result, warnings, tokenAccounting = false) {
319
+ function resultContent(result, warnings, tokenAccounting = false, canonicalStructuredResult = false) {
283
320
  const effectiveResult = tokenAccounting
284
321
  ? attachOutputTokenAccounting(result, (value) => JSON.stringify(value, null, 2))
285
322
  : result;
@@ -292,13 +329,15 @@ function resultContent(result, warnings, tokenAccounting = false) {
292
329
  content: [
293
330
  {
294
331
  type: "text",
295
- text: JSON.stringify(effectiveResult, null, 2),
332
+ text: canonicalStructuredResult
333
+ ? "Canonical result: structuredContent.result"
334
+ : JSON.stringify(effectiveResult, null, 2),
296
335
  },
297
336
  ],
298
337
  structuredContent,
299
338
  };
300
339
  }
301
- function errorContent(error) {
340
+ function errorContent(error, canonicalStructuredResult = false) {
302
341
  const code = error instanceof PmCliError ? error.exitCode : 1;
303
342
  const message = error instanceof Error ? error.message : String(error);
304
343
  const details = error instanceof PmCliError ? error.context : undefined;
@@ -307,7 +346,9 @@ function errorContent(error) {
307
346
  content: [
308
347
  {
309
348
  type: "text",
310
- text: JSON.stringify({ error: message, code, details }, null, 2),
349
+ text: canonicalStructuredResult
350
+ ? "Canonical error: structuredContent"
351
+ : JSON.stringify({ error: message, code, details }, null, 2),
311
352
  },
312
353
  ],
313
354
  // Keep `result` present on the error envelope so consumers can read
@@ -369,7 +410,7 @@ async function emitMcpChangeNotifications(action) {
369
410
  }
370
411
  pruneClosedMcpSubscriptionRegistries();
371
412
  }
372
- async function handleToolCall(paramsInput, clientInfo) {
413
+ async function handleToolCall(paramsInput, clientInfo, progressiveDiscoveryNegotiated = false) {
373
414
  return runWithHarnessDetectionSignals({
374
415
  env: process.env,
375
416
  argv: process.argv,
@@ -386,6 +427,7 @@ async function handleToolCall(paramsInput, clientInfo) {
386
427
  const inputResponses = params.inputResponses === undefined
387
428
  ? undefined
388
429
  : parseMcpInputResponses(params.inputResponses);
430
+ validateMcpToolArguments(params.arguments);
389
431
  const requestedArgs = {
390
432
  ...decodeHtmlEntitiesInOptions(asRecordClone(params.arguments)),
391
433
  ...(inputResponses ? { inputResponses } : {}),
@@ -394,7 +436,10 @@ async function handleToolCall(paramsInput, clientInfo) {
394
436
  : {}),
395
437
  };
396
438
  const access = await resolveMcpToolAccess(TOOLS, name, requestedArgs);
397
- if (!access.available) {
439
+ const requiredCustomDiscoveryEntry = progressiveDiscoveryNegotiated &&
440
+ access.profile === "custom" &&
441
+ name === "pm_discover";
442
+ if (!access.available && !requiredCustomDiscoveryEntry) {
398
443
  throw new PmCliError(`pm MCP tool "${name}" is unavailable in the ${access.profile} profile.`, 64);
399
444
  }
400
445
  // pm-ydkl: defensive HTML-entity decode for free-text fields. Claude / the
@@ -426,7 +471,7 @@ async function handleToolCall(paramsInput, clientInfo) {
426
471
  // so the chdir/restore is exclusive per request and cannot race a concurrent caller.
427
472
  const result = await handler(args);
428
473
  void emitMcpChangeNotifications(action);
429
- return resultContent(result, warnings, args.tokenAccounting === true);
474
+ return resultContent(result, warnings, args.tokenAccounting === true, progressiveDiscoveryNegotiated);
430
475
  }, { probesEnabled: workspaceIdentity?.probes_enabled });
431
476
  });
432
477
  }
@@ -484,7 +529,8 @@ export function buildMcpToolCallErrorResult(request, error) {
484
529
  if (request.method !== "tools/call" || error instanceof PmMcpProtocolError) {
485
530
  return undefined;
486
531
  }
487
- const content = errorContent(error);
532
+ const content = errorContent(error, hasMcpProtocolVersionMetadata(request) &&
533
+ hasMcpClientExtension(resolveMcpRequestContext(request.params), PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION));
488
534
  return hasMcpProtocolVersionMetadata(request)
489
535
  ? buildMcpCompleteResult(content, PM_MCP_SERVER_INFO)
490
536
  : content;
@@ -607,6 +653,12 @@ function mcpTaskStore() {
607
653
  owner: "pm-mcp-task-provider",
608
654
  });
609
655
  }
656
+ /** Rejects malformed MCP tool argument containers before dispatch. */
657
+ function validateMcpToolArguments(value) {
658
+ if (value !== undefined && !isMcpRecord(value)) {
659
+ throw new PmCliError("pm MCP tool arguments must be an object.", 64);
660
+ }
661
+ }
610
662
  function shouldCreateMcpTask(params, requestContext) {
611
663
  if (!hasMcpClientExtension(requestContext, PM_MCP_TASKS_EXTENSION)) {
612
664
  return false;
@@ -633,7 +685,7 @@ async function executeMcpTask(execution, inputResponses) {
633
685
  ...(execution.requestState ? { requestState: execution.requestState } : {}),
634
686
  };
635
687
  try {
636
- const result = await handleToolCall(params, execution.context.clientInfo);
688
+ const result = await handleToolCall(params, execution.context.clientInfo, hasMcpClientExtension(execution.context, PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION));
637
689
  await execution.store.complete(execution.taskId, execution.principal, buildMcpCompleteResult(result, PM_MCP_SERVER_INFO));
638
690
  PENDING_MCP_TASKS.delete(execution.taskId);
639
691
  }
@@ -657,7 +709,7 @@ async function executeMcpTask(execution, inputResponses) {
657
709
  });
658
710
  }
659
711
  else {
660
- await execution.store.complete(execution.taskId, execution.principal, buildMcpCompleteResult(errorContent(error), PM_MCP_SERVER_INFO));
712
+ await execution.store.complete(execution.taskId, execution.principal, buildMcpCompleteResult(errorContent(error, hasMcpClientExtension(execution.context, PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION)), PM_MCP_SERVER_INFO));
661
713
  }
662
714
  PENDING_MCP_TASKS.delete(execution.taskId);
663
715
  }
@@ -726,17 +778,28 @@ async function dispatchMcpTaskMethod(request, requestContext) {
726
778
  async function dispatchModernToolMethod(request, requestContext, clientInfo) {
727
779
  if (request.method === "tools/list") {
728
780
  const surface = await resolveMcpToolSurface(TOOLS, requestWorkspaceArgs(request.params));
781
+ const progressiveDiscovery = hasMcpClientExtension(requestContext, PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION);
782
+ const listedTools = progressiveDiscovery
783
+ ? [
784
+ ...surface.tools.filter((tool) => PM_MCP_ENTRY_TOOL_NAMES.includes(tool.name)),
785
+ ...(surface.profile === "custom" &&
786
+ !surface.tools.some((tool) => tool.name === "pm_discover")
787
+ ? TOOLS.filter((tool) => tool.name === "pm_discover")
788
+ : []),
789
+ ]
790
+ : surface.tools;
729
791
  return buildMcpCompleteResult(withMcpCachePolicy({
730
792
  tools: hasCompatiblePmMcpAppsCapability(requestContext)
731
- ? decoratePmMcpToolsWithApps(deterministicMcpTools(surface.tools))
732
- : deterministicMcpTools(surface.tools),
793
+ ? decoratePmMcpToolsWithApps(deterministicMcpTools(listedTools))
794
+ : deterministicMcpTools(listedTools),
733
795
  }, PM_MCP_CACHE_POLICIES.tools), PM_MCP_SERVER_INFO);
734
796
  }
797
+ validateMcpToolArguments(request.params?.arguments);
735
798
  if (shouldCreateMcpTask(request.params, requestContext)) {
736
799
  return createMcpTaskForToolCall(request.params, requestContext);
737
800
  }
738
801
  try {
739
- return buildMcpCompleteResult(await handleToolCall(request.params, clientInfo), PM_MCP_SERVER_INFO);
802
+ return buildMcpCompleteResult(await handleToolCall(request.params, clientInfo, hasMcpClientExtension(requestContext, PM_MCP_PROGRESSIVE_DISCOVERY_EXTENSION)), PM_MCP_SERVER_INFO);
740
803
  }
741
804
  catch (error) {
742
805
  if (!(error instanceof PmMcpInputRequiredError))
@@ -1162,4 +1225,4 @@ if (isInvokedAsMcpMainModule(process.argv[1], import.meta.url)) {
1162
1225
  }
1163
1226
  /* c8 ignore stop */
1164
1227
  //# sourceMappingURL=server.js.map
1165
- //# debugId=77067047-62fc-54e8-9047-ad30580d6125
1228
+ //# debugId=71cfdc6b-c990-51b6-a9ae-ed9bf98136f7