@unbrained/pm-cli 2026.8.31 → 2026.9.1
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +15 -13
- package/CHANGELOG.md +23 -6
- package/README.md +3 -3
- package/dist/cli/commands/merge.d.ts +1 -1
- package/dist/cli/commands/merge.js +3 -3
- package/dist/cli/register-mutation.js +8 -5
- package/dist/cli-bundle/bundle-manifest.json +125 -125
- package/dist/cli-bundle/chunks/{chunk-SXMHPK4V.js → chunk-2MC2LCXZ.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-52JXYSDW.js → chunk-3WICXLVT.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-OUGDA7NR.js → chunk-6XIIGE7U.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-ALKKAUFN.js +35 -0
- package/dist/cli-bundle/chunks/{chunk-JIFYOJF7.js → chunk-DTGWEL23.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-GOACULJW.js +13 -0
- package/dist/cli-bundle/chunks/{chunk-NW6LV3TU.js → chunk-NOPL37X4.js} +64 -64
- package/dist/cli-bundle/chunks/{chunk-IEFJ3ZEB.js → chunk-TAG4VC22.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-MWIN6OH2.js → chunk-U45JXJCC.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-5DH5XCJL.js → chunk-X4RCGLTX.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-QZCIBZR3.js → chunk-Y7VOMSZT.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-7C7DKZ2U.js → register-list-query-XYNX4ASQ.js} +2 -2
- package/dist/cli-bundle/chunks/register-mutation-2MVLRS3J.js +20 -0
- package/dist/cli-bundle/chunks/{register-operations-IIMA5OKO.js → register-operations-WCIDXBU3.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-52H36QCB.js → register-setup-KVNC5KPL.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +16 -0
- package/dist/cli-bundle/focused-chunks/{chunk-VJ7EFMFL.js → chunk-FWPLDEXF.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-FOMZ6NG5.js → chunk-MKHG3T76.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-UN34D63M.js → chunk-PF3E6Q36.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-APN4QN6R.js → chunk-RMRCQPWU.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-BPWJ4QM5.js → chunk-S4U5CN5Q.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +158 -0
- package/dist/cli-bundle/focused-chunks/{chunk-JCOOAUHK.js → chunk-Y6JJIX6I.js} +30 -30
- package/dist/cli-bundle/main.js +2 -2
- package/dist/cli-bundle/sdk-contracts.js +1 -1
- package/dist/cli-bundle/sdk-core.js +25 -25
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +31 -31
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +10 -10
- package/dist/core/history/drift-scan.js +6 -6
- package/dist/core/history/history.d.ts +6 -0
- package/dist/core/history/history.js +47 -5
- package/dist/core/history/replay.d.ts +12 -1
- package/dist/core/history/replay.js +60 -39
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +78 -15
- package/dist/mcp/tool-definitions.js +56 -2
- package/dist/sdk/agent-capability-contracts.js +4 -2
- package/dist/sdk/history-read.js +14 -6
- package/dist/sdk/history-repair.js +17 -10
- package/dist/sdk/index.d.ts +1 -0
- package/dist/sdk/index.js +3 -2
- package/dist/sdk/init-agent-guidance.js +4 -4
- package/dist/sdk/lifecycle/update.js +23 -10
- package/dist/sdk/mcp/discovery.d.ts +152 -0
- package/dist/sdk/mcp/discovery.js +495 -0
- package/dist/sdk/merge/index.d.ts +1 -1
- package/dist/sdk/merge/index.js +3 -3
- package/dist/sdk/merge/receipts.d.ts +27 -1
- package/dist/sdk/merge/receipts.js +27 -8
- package/dist/sdk/merge/three-way.d.ts +3 -1
- package/dist/sdk/merge/three-way.js +9 -7
- package/dist/sdk/runtime-extended-actions.js +4 -4
- package/docs/AGENT_GUIDE.md +11 -3
- package/docs/ARCHITECTURE.md +5 -0
- package/docs/CLAUDE_CODE_PLUGIN.md +2 -2
- package/docs/CONTEXT_RECOVERY_PRIMITIVES.md +14 -5
- package/docs/MCP_2026_07_28.md +9 -5
- package/docs/MCP_CAPABILITY_SURFACES.md +7 -1
- package/docs/MCP_REMOTE_TRANSPORT_SECURITY.md +11 -1
- package/docs/MERGE_SAFETY.md +30 -2
- package/docs/OUTPUT_TOKEN_ACCOUNTING.md +5 -3
- package/docs/PROGRESSIVE_TOOL_DISCOVERY.md +42 -0
- package/docs/README.md +1 -0
- package/docs/agent-task-token-baseline.json +157 -3
- package/docs/agent-task-transcripts.json +143 -0
- package/docs/performance/cli-transport-overhead.md +8 -6
- package/marketplace.json +2 -2
- package/package.json +2 -2
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-claude/README.md +4 -3
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/sdk/public-surface.json +145 -2
- package/dist/cli-bundle/chunks/chunk-2XVV45NJ.js +0 -35
- package/dist/cli-bundle/chunks/chunk-3FCSFWZT.js +0 -13
- package/dist/cli-bundle/chunks/register-mutation-OI4BJLUX.js +0 -20
- package/dist/cli-bundle/focused-chunks/chunk-BMD42PUG.js +0 -158
- package/dist/cli-bundle/focused-chunks/chunk-EUTABA4M.js +0 -16
|
@@ -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]="
|
|
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,
|
|
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 =
|
|
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
|
|
241
|
-
if (
|
|
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=
|
|
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]="
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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]="
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
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:
|
|
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
|
|
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=
|
|
406
|
+
//# debugId=c9488ed4-8228-5e7c-9717-69dd7a3275c4
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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]="
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
732
|
-
: deterministicMcpTools(
|
|
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=
|
|
1228
|
+
//# debugId=71cfdc6b-c990-51b6-a9ae-ed9bf98136f7
|