@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
@@ -1,4 +1,7 @@
1
+ import type { Stats } from "node:fs";
1
2
  import type { ItemMergeConflictDecision, MergePreferredSide } from "./three-way.js";
3
+ /** Return whether a receipt id is safe to expose in cleartext diagnostics. */
4
+ export declare function isSafeReceiptId(value: string): boolean;
2
5
  /** One clone-local merge receipt with recoverable branch values. */
3
6
  export interface MergeDecisionReceipt {
4
7
  /** Receipt schema version. */
@@ -70,12 +73,50 @@ export interface MergeReceiptReport {
70
73
  /** ISO timestamp for the report. */
71
74
  generated_at: string;
72
75
  }
73
- /** Internal integrity scan result used by health without exposing malformed receipt contents. */
76
+ /** Loss-aware merge receipt report for integrity gates and diagnostic adapters. */
77
+ export interface MergeReceiptEvidenceReport {
78
+ /** Whether clone-local evidence resolved and every candidate was read and validated successfully. */
79
+ ok: boolean;
80
+ /** Whether clone-local evidence resolved and every discovered JSON candidate was read and validated successfully. */
81
+ complete: boolean;
82
+ /** Number of valid receipts returned. */
83
+ count: number;
84
+ /** Number of candidates rejected by bounded-file, schema, identity, or copy-consistency validation. */
85
+ invalid_evidence_count: number;
86
+ /** Bounded privacy-safe identities and reason codes for rejected evidence. */
87
+ invalid_evidence: MergeReceiptInvalidEvidence[];
88
+ /** Whether additional rejected evidence was omitted from the bounded detail list. */
89
+ invalid_evidence_truncated: boolean;
90
+ /** Whether the clone-local Git receipt directory was resolved successfully; always emitted by current implementations and optional for structural compatibility. */
91
+ clone_local_evidence_resolved?: boolean;
92
+ /** Receipts including recoverable values from the local clone only. */
93
+ receipts: MergeDecisionReceipt[];
94
+ /** ISO timestamp for the report. */
95
+ generated_at: string;
96
+ }
97
+ /** Loss-aware receipt inspection result that never exposes malformed file contents. */
74
98
  export interface MergeReceiptEvidenceScan {
75
99
  /** Valid receipts that passed bounded-file, schema, and identity validation. */
76
100
  receipts: MergeDecisionReceipt[];
77
101
  /** Number of JSON receipt candidates that could not be validated safely. */
78
102
  invalid_evidence_count: number;
103
+ /** Bounded privacy-safe identities and reason codes for rejected evidence. */
104
+ invalid_evidence: MergeReceiptInvalidEvidence[];
105
+ /** Whether additional rejected evidence was omitted from the bounded detail list. */
106
+ invalid_evidence_truncated: boolean;
107
+ /** Whether the clone-local Git receipt directory was resolved successfully; always emitted by current implementations and optional for structural compatibility. */
108
+ clone_local_evidence_resolved?: boolean;
109
+ }
110
+ /** Stable privacy-safe classification for one rejected receipt candidate or source. */
111
+ export interface MergeReceiptInvalidEvidence {
112
+ /** Evidence store that produced the rejected candidate. */
113
+ evidence_source: "clone_local" | "durable" | "clone_local_and_durable";
114
+ /** Stable failure class suitable for remediation routing and graph analytics. */
115
+ reason: "directory_unreadable" | "candidate_not_bounded_regular_file" | "candidate_unreadable" | "candidate_invalid_json" | "schema_or_identity_invalid" | "copy_provenance_mismatch";
116
+ /** Receipt identity when the bounded filename itself is a valid receipt id. */
117
+ receipt_id?: string;
118
+ /** SHA-256 locator for an unsafe or malformed candidate filename. */
119
+ candidate_name_hash?: string;
79
120
  }
80
121
  declare function prepareReceiptSettlement(params: {
81
122
  receiptPath: string;
@@ -101,6 +142,7 @@ export declare function writeMergeReceipt(params: {
101
142
  mergedFieldHashes?: Record<string, string>;
102
143
  decisions: ItemMergeConflictDecision[];
103
144
  }): Promise<MergeDecisionReceipt | null>;
145
+ declare function receiptDirectoryFailureMeansAbsent(directory: string, error: unknown, inspectAncestor?: (ancestor: string) => Promise<Pick<Stats, "isDirectory">>): Promise<boolean>;
104
146
  /** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
105
147
  export declare function inspectMergeReceiptEvidence(cwd: string, options?: {
106
148
  /** Include receipts whose reconciliation state is already settled. */
@@ -134,8 +176,16 @@ export declare function runMergeReceiptReport(options: {
134
176
  /** Repository directory to inspect; defaults to the process working directory. */
135
177
  cwd?: string;
136
178
  }): Promise<MergeReceiptReport>;
179
+ /** Inspect every receipt candidate and retain completeness diagnostics without exposing rejected file contents. */
180
+ export declare function runMergeReceiptEvidenceReport(options: {
181
+ /** Include receipts already represented by merge history events. */
182
+ includeReconciled?: boolean;
183
+ /** Repository directory to inspect; defaults to the process working directory. */
184
+ cwd?: string;
185
+ }): Promise<MergeReceiptEvidenceReport>;
137
186
  /** Test-only seams for deterministic receipt-boundary fault coverage. */
138
187
  export declare const _testOnlyMergeReceipts: {
139
188
  prepareReceiptSettlement: typeof prepareReceiptSettlement;
189
+ receiptDirectoryFailureMeansAbsent: typeof receiptDirectoryFailureMeansAbsent;
140
190
  };
141
191
  export {};
@@ -7,13 +7,13 @@
7
7
  * value for the coordinator that performed the merge.
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]="145a6d31-5e14-5b0b-95fb-6f837d11e2f4")}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]="df7d5bd9-8381-5961-bda1-9e8ed335afc9")}catch(e){}}();
11
11
  import { execFile } from "node:child_process";
12
12
  import { randomUUID } from "node:crypto";
13
- import { readdir } from "node:fs/promises";
13
+ import { readdir, stat } from "node:fs/promises";
14
14
  import path from "node:path";
15
15
  import { promisify } from "node:util";
16
- import { ensureDir, isFileAbsentError, pathExists, writeFileAtomic, } from "../../core/fs/fs-utils.js";
16
+ import { ensureDir, isFileAbsentError, isFileMissingError, pathExists, writeFileAtomic, } from "../../core/fs/fs-utils.js";
17
17
  import { sha256Hex, stableStringify } from "../../core/shared/serialization.js";
18
18
  import { isRfc3339DateTime, nowIso } from "../../core/shared/time.js";
19
19
  import { readBoundedRegularFile } from "./receipt-file-boundary.js";
@@ -24,6 +24,7 @@ const RECEIPT_FIELD_MAX = 2_048;
24
24
  const RECEIPT_FILE_MAX_BYTES = 16 * 1024 * 1024;
25
25
  const RECEIPT_VALUE_MAX_DEPTH = 64;
26
26
  const RECEIPT_VALUE_MAX_NODES = 100_000;
27
+ const RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT = 100;
27
28
  const RECEIPT_KEYS = new Set([
28
29
  "version",
29
30
  "id",
@@ -42,6 +43,10 @@ const RECEIPT_KEYS = new Set([
42
43
  "value_availability",
43
44
  "evidence_source",
44
45
  ]);
46
+ /** Return whether a receipt id is safe to expose in cleartext diagnostics. */
47
+ export function isSafeReceiptId(value) {
48
+ return RECEIPT_ID_PATTERN.test(value);
49
+ }
45
50
  const RECEIPT_DECISION_KEYS = new Set([
46
51
  "field",
47
52
  "base",
@@ -163,7 +168,7 @@ function hasValidReceiptIdentity(value) {
163
168
  return !hasOnlyKeys(value, RECEIPT_KEYS) || value.version !== 1
164
169
  ? false
165
170
  : typeof value.id === "string" &&
166
- RECEIPT_ID_PATTERN.test(value.id) &&
171
+ isSafeReceiptId(value.id) &&
167
172
  typeof value.item_id === "string" &&
168
173
  RECEIPT_ITEM_ID_PATTERN.test(value.item_id) &&
169
174
  isSafeReceiptItemPath(value.item_path, value.item_id);
@@ -367,59 +372,120 @@ export async function writeMergeReceipt(params) {
367
372
  return receipt;
368
373
  }
369
374
  async function readReceiptsFromDirectory(directory, evidenceSource) {
370
- if (!(await pathExists(directory))) {
371
- return { receipts: [], invalid_evidence_count: 0 };
372
- }
373
375
  let names;
374
376
  try {
375
377
  names = await readdir(directory);
376
378
  }
377
- catch {
378
- return { receipts: [], invalid_evidence_count: 1 };
379
+ catch (error) {
380
+ const absent = await receiptDirectoryFailureMeansAbsent(directory, error);
381
+ return {
382
+ receipts: [],
383
+ invalid_evidence_count: absent ? 0 : 1,
384
+ invalid_evidence: absent
385
+ ? []
386
+ : [{ evidence_source: evidenceSource, reason: "directory_unreadable" }],
387
+ invalid_evidence_truncated: false,
388
+ };
379
389
  }
380
390
  const receipts = [];
381
391
  let invalidEvidenceCount = 0;
392
+ const invalidEvidence = [];
393
+ const recordInvalidEvidence = (name, reason) => {
394
+ invalidEvidenceCount += 1;
395
+ if (invalidEvidence.length >= RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT)
396
+ return;
397
+ const receiptId = name.slice(0, -".json".length);
398
+ invalidEvidence.push({
399
+ evidence_source: evidenceSource,
400
+ reason,
401
+ ...(isSafeReceiptId(receiptId)
402
+ ? { receipt_id: receiptId }
403
+ : { candidate_name_hash: sha256Hex(name) }),
404
+ });
405
+ };
382
406
  for (const name of names.sort((left, right) => left.localeCompare(right))) {
383
407
  if (!name.endsWith(".json"))
384
408
  continue;
409
+ const candidate = await inspectReceiptCandidate(directory, name, evidenceSource);
410
+ if (candidate.receipt !== undefined) {
411
+ receipts.push(candidate.receipt);
412
+ }
413
+ else {
414
+ recordInvalidEvidence(name, candidate.reason);
415
+ }
416
+ }
417
+ return {
418
+ receipts,
419
+ invalid_evidence_count: invalidEvidenceCount,
420
+ invalid_evidence: invalidEvidence,
421
+ invalid_evidence_truncated: invalidEvidenceCount > invalidEvidence.length,
422
+ };
423
+ }
424
+ async function receiptDirectoryFailureMeansAbsent(directory, error, inspectAncestor = stat) {
425
+ if (!isFileMissingError(error))
426
+ return false;
427
+ let ancestor = directory;
428
+ while (true) {
385
429
  try {
386
- const receiptPath = path.join(directory, name);
387
- const raw = await readBoundedRegularReceiptFile(receiptPath);
388
- if (raw === null) {
389
- invalidEvidenceCount += 1;
390
- continue;
391
- }
392
- const parsed = JSON.parse(raw);
393
- if (!isMergeDecisionReceipt(parsed, evidenceSource) ||
394
- name !== receiptFileName(parsed.id)) {
395
- invalidEvidenceCount += 1;
396
- continue;
397
- }
398
- const { preferred: legacyPreference, evidence_source: _serializedEvidenceSource, ...receiptWithoutRuntimeKeys } = parsed;
399
- const normalizedReceipt = {
400
- ...receiptWithoutRuntimeKeys,
401
- requested_preference: parsed.requested_preference ?? legacyPreference ?? "ours",
402
- conflict_resolution: parsed.conflict_resolution ?? "preferred_side",
403
- evidence_source: evidenceSource,
404
- };
405
- receipts.push(normalizedReceipt);
430
+ return (await inspectAncestor(ancestor)).isDirectory();
406
431
  }
407
- catch {
408
- invalidEvidenceCount += 1;
432
+ catch (ancestorError) {
433
+ if (!isFileMissingError(ancestorError))
434
+ return false;
435
+ const parent = path.dirname(ancestor);
436
+ if (parent === ancestor)
437
+ return true;
438
+ ancestor = parent;
409
439
  }
410
440
  }
411
- return { receipts, invalid_evidence_count: invalidEvidenceCount };
441
+ }
442
+ async function inspectReceiptCandidate(directory, name, evidenceSource) {
443
+ let raw;
444
+ try {
445
+ raw = await readBoundedRegularReceiptFile(path.join(directory, name));
446
+ }
447
+ catch {
448
+ return { reason: "candidate_unreadable" };
449
+ }
450
+ if (raw === null)
451
+ return { reason: "candidate_not_bounded_regular_file" };
452
+ let parsed;
453
+ try {
454
+ parsed = JSON.parse(raw);
455
+ }
456
+ catch {
457
+ return { reason: "candidate_invalid_json" };
458
+ }
459
+ if (!isMergeDecisionReceipt(parsed, evidenceSource) ||
460
+ name !== receiptFileName(parsed.id)) {
461
+ return { reason: "schema_or_identity_invalid" };
462
+ }
463
+ const { preferred: legacyPreference, evidence_source: _serializedEvidenceSource, ...receiptWithoutRuntimeKeys } = parsed;
464
+ return {
465
+ receipt: {
466
+ ...receiptWithoutRuntimeKeys,
467
+ requested_preference: parsed.requested_preference ?? legacyPreference ?? "ours",
468
+ conflict_resolution: parsed.conflict_resolution ?? "preferred_side",
469
+ evidence_source: evidenceSource,
470
+ },
471
+ };
412
472
  }
413
473
  /** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
414
474
  export async function inspectMergeReceiptEvidence(cwd, options = {}) {
415
475
  const directory = await resolveReceiptDirectory(cwd);
416
476
  const local = directory === null
417
- ? { receipts: [], invalid_evidence_count: 0 }
477
+ ? {
478
+ receipts: [],
479
+ invalid_evidence_count: 0,
480
+ invalid_evidence: [],
481
+ invalid_evidence_truncated: false,
482
+ }
418
483
  : await readReceiptsFromDirectory(directory, "clone_local");
419
484
  const trackerRoot = options.pmRoot ?? path.join(cwd, ".agents", "pm");
420
485
  const durable = await readReceiptsFromDirectory(durableReceiptDirectory(trackerRoot), "durable");
421
486
  const receipts = new Map(durable.receipts.map((receipt) => [receipt.id, receipt]));
422
487
  let divergentCopyCount = 0;
488
+ const divergentCopyEvidence = [];
423
489
  for (const receipt of local.receipts) {
424
490
  const durableCopy = receipts.get(receipt.id);
425
491
  if (durableCopy !== undefined &&
@@ -427,6 +493,16 @@ export async function inspectMergeReceiptEvidence(cwd, options = {}) {
427
493
  receiptProvenanceFingerprint(receipt)) {
428
494
  receipts.delete(receipt.id);
429
495
  divergentCopyCount += 1;
496
+ if (local.invalid_evidence.length +
497
+ durable.invalid_evidence.length +
498
+ divergentCopyEvidence.length <
499
+ RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT) {
500
+ divergentCopyEvidence.push({
501
+ evidence_source: "clone_local_and_durable",
502
+ reason: "copy_provenance_mismatch",
503
+ receipt_id: receipt.id,
504
+ });
505
+ }
430
506
  continue;
431
507
  }
432
508
  receipts.set(receipt.id, durableCopy === undefined
@@ -441,6 +517,18 @@ export async function inspectMergeReceiptEvidence(cwd, options = {}) {
441
517
  invalid_evidence_count: local.invalid_evidence_count +
442
518
  durable.invalid_evidence_count +
443
519
  divergentCopyCount,
520
+ invalid_evidence: [
521
+ ...local.invalid_evidence,
522
+ ...durable.invalid_evidence,
523
+ ...divergentCopyEvidence,
524
+ ].slice(0, RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT),
525
+ invalid_evidence_truncated: local.invalid_evidence_truncated ||
526
+ durable.invalid_evidence_truncated ||
527
+ local.invalid_evidence_count +
528
+ durable.invalid_evidence_count +
529
+ divergentCopyCount >
530
+ RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT,
531
+ clone_local_evidence_resolved: directory !== null,
444
532
  };
445
533
  }
446
534
  /** Read clone-local receipts with explicit reconciled/lossless classification controls. */
@@ -524,7 +612,30 @@ export async function runMergeReceiptReport(options) {
524
612
  generated_at: nowIso(),
525
613
  };
526
614
  }
615
+ /** Inspect every receipt candidate and retain completeness diagnostics without exposing rejected file contents. */
616
+ export async function runMergeReceiptEvidenceReport(options) {
617
+ const evidence = await inspectMergeReceiptEvidence(options.cwd ?? process.cwd(), {
618
+ ...options,
619
+ includeLossless: true,
620
+ });
621
+ const complete = evidence.clone_local_evidence_resolved === true &&
622
+ evidence.invalid_evidence_count === 0;
623
+ return {
624
+ ok: complete,
625
+ complete,
626
+ count: evidence.receipts.length,
627
+ invalid_evidence_count: evidence.invalid_evidence_count,
628
+ invalid_evidence: evidence.invalid_evidence,
629
+ invalid_evidence_truncated: evidence.invalid_evidence_truncated,
630
+ clone_local_evidence_resolved: evidence.clone_local_evidence_resolved,
631
+ receipts: evidence.receipts,
632
+ generated_at: nowIso(),
633
+ };
634
+ }
527
635
  /** Test-only seams for deterministic receipt-boundary fault coverage. */
528
- export const _testOnlyMergeReceipts = { prepareReceiptSettlement };
636
+ export const _testOnlyMergeReceipts = {
637
+ prepareReceiptSettlement,
638
+ receiptDirectoryFailureMeansAbsent,
639
+ };
529
640
  //# sourceMappingURL=receipts.js.map
530
- //# debugId=145a6d31-5e14-5b0b-95fb-6f837d11e2f4
641
+ //# debugId=df7d5bd9-8381-5961-bda1-9e8ed335afc9
@@ -28,7 +28,9 @@ export interface HistoryMergeResult {
28
28
  * hash re-anchored so the resulting chain verifies again. No side's events are
29
29
  * ever discarded: content divergence is preserved for the post-merge
30
30
  * reconciliation pass (`pm validate` + `pm history-repair`) instead of being
31
- * silently resolved by last-writer-wins.
31
+ * silently resolved by last-writer-wins. If either suffix cannot apply after
32
+ * deterministic ordering, the merge fails closed instead of publishing a
33
+ * hash-valid stream that omitted a branch effect.
32
34
  */
33
35
  export declare function mergeHistoryStreams(baseRaw: string, oursRaw: string, theirsRaw: string): HistoryMergeResult;
34
36
  /** Documents the relationship event stream merge result payload exchanged by command, SDK, and package integrations. */
@@ -9,7 +9,7 @@
9
9
  * definition for multi-branch agent workflows.
10
10
  */
11
11
 
12
- !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]="393f6460-bf8b-5479-8e3e-cac6f2680823")}catch(e){}}();
12
+ !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]="7e8fcddf-09fe-5979-b23a-78915bf7f896")}catch(e){}}();
13
13
  import { historyEntriesToRaw, reanchorHistoryEntries, } from "../../core/history/replay.js";
14
14
  import { canonicalDocument, parseItemDocument, serializeItemDocument, } from "../../core/item/item-format.js";
15
15
  import { EXIT_CODE } from "../../core/shared/constants.js";
@@ -69,7 +69,9 @@ function commonHistoryPrefixLength(ours, theirs) {
69
69
  * hash re-anchored so the resulting chain verifies again. No side's events are
70
70
  * ever discarded: content divergence is preserved for the post-merge
71
71
  * reconciliation pass (`pm validate` + `pm history-repair`) instead of being
72
- * silently resolved by last-writer-wins.
72
+ * silently resolved by last-writer-wins. If either suffix cannot apply after
73
+ * deterministic ordering, the merge fails closed instead of publishing a
74
+ * hash-valid stream that omitted a branch effect.
73
75
  */
74
76
  export function mergeHistoryStreams(baseRaw, oursRaw, theirsRaw) {
75
77
  parseHistoryJsonl(baseRaw, "base");
@@ -126,10 +128,10 @@ export function mergeHistoryStreams(baseRaw, oursRaw, theirsRaw) {
126
128
  }
127
129
  return historyEntryIdentity(left).localeCompare(historyEntryIdentity(right));
128
130
  });
129
- const reanchored = reanchorHistoryEntries([
130
- ...ours.slice(0, shared),
131
- ...mergedSuffix,
132
- ]);
131
+ const reanchored = reanchorHistoryEntries([...ours.slice(0, shared), ...mergedSuffix], undefined, { continuousHashSurface: true });
132
+ if (reanchored.skippedOps > 0) {
133
+ throw new PmCliError(`History union cannot preserve ${reanchored.skippedOps} patch operation(s) after deterministic suffix ordering; leave the file unresolved for manual recovery.`, EXIT_CODE.GENERIC_FAILURE);
134
+ }
133
135
  return {
134
136
  merged: historyEntriesToRaw(reanchored.entries),
135
137
  strategy: "union_reanchor",
@@ -704,4 +706,4 @@ export function mergeJsonDocuments(baseRaw, oursRaw, theirsRaw, options = {}) {
704
706
  };
705
707
  }
706
708
  //# sourceMappingURL=three-way.js.map
707
- //# debugId=393f6460-bf8b-5479-8e3e-cac6f2680823
709
+ //# debugId=7e8fcddf-09fe-5979-b23a-78915bf7f896
@@ -1,5 +1,5 @@
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]="95e9def9-3f48-5108-8a17-0747fcf64f2d")}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]="41227e35-6354-5208-997b-e8a1cf696bf9")}catch(e){}}();
3
3
  import { EXIT_CODE } from "../core/shared/constants.js";
4
4
  import { PmCliError } from "../core/shared/errors.js";
5
5
  import { transferMutationStdinTokenPolicy } from "./runtime-primitives.js";
@@ -9,7 +9,7 @@ import { runEval } from "./eval.js";
9
9
  import { runMergeDriver } from "./merge/driver.js";
10
10
  import { runMergeInstall } from "./merge/install.js";
11
11
  import { runMergeReconcile } from "./merge/reconcile.js";
12
- import { runMergeReceiptReport } from "./merge/receipts.js";
12
+ import { runMergeReceiptEvidenceReport } from "./merge/receipts.js";
13
13
  import { listMutationEvents } from "./mutation-events.js";
14
14
  import { parseRuntimeInteger, readRuntimeString, readRuntimeStringArray, } from "./runtime-input.js";
15
15
  import { runEvent, runMeet, runRemind, } from "./scheduling-shortcuts.js";
@@ -87,7 +87,7 @@ export function runRuntimeMergeAction(context) {
87
87
  }, context.global);
88
88
  }
89
89
  if (subcommand === "report") {
90
- return runMergeReceiptReport({
90
+ return runMergeReceiptEvidenceReport({
91
91
  includeReconciled: input.includeReconciled === true,
92
92
  cwd: readRuntimeString(input, "cwd"),
93
93
  });
@@ -175,4 +175,4 @@ export function runRuntimeSchedulingAction(context) {
175
175
  return runRemind(title, input, context.global);
176
176
  }
177
177
  //# sourceMappingURL=runtime-extended-actions.js.map
178
- //# debugId=95e9def9-3f48-5108-8a17-0747fcf64f2d
178
+ //# debugId=41227e35-6354-5208-997b-e8a1cf696bf9
@@ -4,18 +4,25 @@ This guide is optimized for coding agents that need to understand and mutate rep
4
4
 
5
5
  ## Agent Quick Context
6
6
 
7
- Run this before heavy work:
7
+ Run this before heavy work. The first call is the canonical cold start. Then run the request-specific duplicate and ownership checks before item mutation:
8
8
 
9
9
  ```bash
10
- pm context --limit 10
10
+ pm context --limit 10 --for orient
11
11
  pm search "<request keywords>" --limit 10
12
12
  pm list --status open --limit 20
13
13
  pm list --status in_progress --limit 20
14
+ ```
15
+
16
+ Run the optional guidance setup commands only when needed:
17
+
18
+ ```bash
14
19
  pm init --agent-guidance status
15
20
  pm package install guide-shell --project
16
21
  pm guide workflows
17
22
  ```
18
23
 
24
+ The completed-task replay gate selects that one-call protocol from equivalent fixed-corpus candidates: 1,055 estimated tokens versus 2,293 for the historical four-read sequence and 4,798 for `contracts --summary` plus `next`. The reviewed numbers and per-step receipts live in [the published token baseline](agent-task-token-baseline.json).
25
+
19
26
  If a relevant item exists, reuse it. If not, create a parent lineage, then create and claim the child implementation item.
20
27
  When AGENTS/CLAUDE guidance is missing, use `pm init --agent-guidance add` to inject compact workflow guardrails, or `pm init --agent-guidance skip` to persist an explicit decline.
21
28
 
@@ -26,7 +33,7 @@ Tracked documentation work: [pm-u9d0](../.agents/pm/epics/pm-u9d0.toon).
26
33
  1. **Orient**
27
34
 
28
35
  ```bash
29
- pm context --limit 10
36
+ pm context --limit 10 --for orient
30
37
  pm search "<keywords>" --limit 10
31
38
  pm list --status open --limit 20
32
39
  pm list --status in_progress --limit 20
@@ -108,6 +115,7 @@ pm release <item-id>
108
115
  | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
109
116
  | The single next action + why | `pm next` (concrete ready leaves first; completed-container closeout rows surface only when no leaf work is ready; `--ready-only` for the tightest output) |
110
117
  | Next ready work in one epic | `pm next --parent <id>` |
118
+ | Canonical cold-start orientation | `pm context --limit 10 --for orient` (1,055 estimated tokens on the fixed 101-item replay corpus) |
111
119
  | Next work and agenda | `pm context --limit 10` |
112
120
  | Comprehensive whole-tracker snapshot | `pm context --depth full` (every section, no per-section row cap) |
113
121
  | Status of one epic/subtree | `pm context --parent <id> --depth deep` |
@@ -56,9 +56,12 @@ src/
56
56
  shared/
57
57
  mcp/
58
58
  server.ts
59
+ tool-definitions.ts
59
60
  sdk/
60
61
  cli-contracts.ts
61
62
  index.ts
63
+ mcp/
64
+ discovery.ts
62
65
  types/
63
66
  tests/
64
67
  unit/
@@ -287,6 +290,8 @@ allowance file:
287
290
  Do not add new CLI/MCP behavior by deep-importing `src/core`. Add or extend an
288
291
  SDK primitive first, then consume that SDK surface from the presentation layer.
289
292
 
293
+ Progressive tool discovery follows this boundary directly: `src/sdk/mcp/discovery.ts` owns ranking, authorization filtering, budgets, cursors, omissions, cache identity, and the canonical result contract. `src/mcp/server.ts` only negotiates the extension, selects the entry catalog, and adapts compatibility text. See [Progressive Tool Discovery](PROGRESSIVE_TOOL_DISCOVERY.md).
294
+
290
295
  ## Telemetry Schema Negotiation
291
296
 
292
297
  Telemetry preserves wire compatibility through an explicit client/server negotiation split:
@@ -55,7 +55,7 @@ This clones the repo, reads `.claude-plugin/marketplace.json` at the root, insta
55
55
  claude mcp add --transport stdio pm-mcp -- npx -y --package=@unbrained/pm-cli@latest pm-mcp
56
56
  ```
57
57
 
58
- Gives you the 31 MCP tools without skills or slash commands.
58
+ Gives you the 32 MCP tools without skills or slash commands.
59
59
 
60
60
  ### 3. Direct project `.mcp.json` (project-scoped MCP only)
61
61
 
@@ -117,7 +117,7 @@ pnpm smoke:claude-plugin
117
117
  ```
118
118
 
119
119
  Verifies: plugin file structure, manifest name consistency, stateless MCP
120
- discovery for `2026-07-28`, 31 tools present, full workflow (init → create →
120
+ discovery for `2026-07-28`, 32 tools present, full workflow (init → create →
121
121
  claim → update → link files/docs/tests → get → context → search → validate →
122
122
  health), and session-start hook.
123
123
 
@@ -1,6 +1,6 @@
1
1
  # Context Integrity and Recovery Primitives
2
2
 
3
- Tracker: [pm-hu92i3](../.agents/pm/issues/pm-hu92i3.toon), [pm-nc94mk](../.agents/pm/issues/pm-nc94mk.toon), [pm-xrjy8o](../.agents/pm/issues/pm-xrjy8o.toon), [pm-f60039](../.agents/pm/issues/pm-f60039.toon), [pm-2qahia](../.agents/pm/issues/pm-2qahia.toon), [pm-z1z96w](../.agents/pm/issues/pm-z1z96w.toon), [pm-yhle2e](../.agents/pm/issues/pm-yhle2e.toon), [pm-t7wl00](../.agents/pm/issues/pm-t7wl00.toon), [pm-hiqlkh](../.agents/pm/issues/pm-hiqlkh.toon), [pm-ntnv4k](../.agents/pm/issues/pm-ntnv4k.toon), [pm-sxg7wl](../.agents/pm/issues/pm-sxg7wl.toon), [pm-xspd](../.agents/pm/chores/pm-xspd.toon)
3
+ Tracker: [pm-dn8rwl](../.agents/pm/issues/pm-dn8rwl.toon), [pm-r97901](../.agents/pm/issues/pm-r97901.toon), [pm-hu92i3](../.agents/pm/issues/pm-hu92i3.toon), [pm-nc94mk](../.agents/pm/issues/pm-nc94mk.toon), [pm-xrjy8o](../.agents/pm/issues/pm-xrjy8o.toon), [pm-f60039](../.agents/pm/issues/pm-f60039.toon), [pm-2qahia](../.agents/pm/issues/pm-2qahia.toon), [pm-z1z96w](../.agents/pm/issues/pm-z1z96w.toon), [pm-yhle2e](../.agents/pm/issues/pm-yhle2e.toon), [pm-t7wl00](../.agents/pm/issues/pm-t7wl00.toon), [pm-hiqlkh](../.agents/pm/issues/pm-hiqlkh.toon), [pm-ntnv4k](../.agents/pm/issues/pm-ntnv4k.toon), [pm-sxg7wl](../.agents/pm/issues/pm-sxg7wl.toon), [pm-xspd](../.agents/pm/chores/pm-xspd.toon)
4
4
 
5
5
  `project management = context management`: a successful command is useful only when the context it records and later returns is truthful, lossless, and actionable. These SDK-owned primitives are shared by the CLI and package integrations.
6
6
 
@@ -46,11 +46,29 @@ When multiple entries exist, the SDK selects the requested package by key or `na
46
46
 
47
47
  ## History and merge recovery
48
48
 
49
- History hash capability 3 distinguishes the current canonical writer surface from older epoch-2 writers while retaining readers for epochs 1 and 2. An unsupported epoch is version skew, not permission to reinterpret or silently normalize history.
50
-
51
- Health keeps the metadata-only cache path for clean history streams. A cached hash mismatch, chain mismatch, or writer-version skew is only a candidate: health rereads canonical item sources and verifies stream content hashes before reporting corruption. The `history_drift` details expose `cache_confirmation` candidate, confirmed, and resolved-false-positive item sets, while `cache_hit_verification: metadata_then_content_hash` identifies the authoritative fallback. Drift-cache envelopes also carry the current item-hash capability, so a runtime with incompatible hash semantics rebuilds the cache instead of trusting it.
52
-
53
- Merge reconciliation may consume a durable hash-only receipt without `--force` only when its canonical item path, complete declared-field set, and every merged-value hash exactly match the current item snapshot. Raw discarded values remain clone-local. Any incomplete or mismatched proof fails closed.
49
+ History hash capability 3 distinguishes the current canonical writer surface
50
+ from older writers while retaining frozen readers for epochs 1 and 2. Two
51
+ writer surfaces were historically emitted under epoch 2: its earlier form
52
+ excluded linked-test workspace/provenance fields and test-run execution
53
+ receipts while normalizing dependency ids; its later form included those
54
+ fields. Verification recognizes both immutable forms and requires each entry's
55
+ before/after hashes to use one consistent form. Current-document verification
56
+ selects the candidate that matches the verified chain head. An unsupported
57
+ epoch is version skew, not permission to reinterpret or silently normalize
58
+ history.
59
+
60
+ Health keeps the metadata-only cache path for clean history streams. A cached hash mismatch, chain mismatch, or writer-version skew is only a candidate: health rereads canonical item sources and verifies stream content hashes before reporting corruption. The `history_drift` details expose `cache_confirmation` candidate, confirmed, and resolved-false-positive item sets, while `cache_hit_verification: metadata_then_content_hash` identifies the authoritative fallback. Drift-cache envelopes carry both their schema version and the current item-hash capability, so changing legacy canonicalization invalidates prior verdicts and a runtime with incompatible hash semantics rebuilds the cache instead of trusting it.
61
+
62
+ Merge reconciliation may consume a durable hash-only receipt without `--force` only when its canonical item path, complete declared-field set, and every merged-value hash exactly match the current item snapshot. Raw discarded values remain clone-local. Any incomplete or mismatched proof fails closed. SDK gates use `inspectMergeReceiptEvidence` or `runMergeReceiptEvidenceReport` to retain the distinction between no evidence, rejected evidence, and clone-local evidence whose Git directory could not be resolved; `clone_local_evidence_resolved=false` makes the loss-aware report incomplete. Rejected evidence carries bounded privacy-safe source, reason, and receipt-id-or-hash locators, while the exact count and truncation receipt preserve completeness under an agent token budget. Cross-platform ancestor inspection distinguishes an absent receipt store from an unreadable or non-directory root. The list-only and legacy report compatibility projections intentionally return valid receipts only. Diverged history unions also fail closed when deterministic suffix ordering would make any patch operation inapplicable, instead of publishing a rehashed stream with a skipped branch effect.
63
+
64
+ History receipt summaries are durable references, not cleanup hints. Health
65
+ checks each `context.merge.receipts[].receipt_id` against valid pending and
66
+ reconciled authoritative evidence. A missing reference is returned with its
67
+ item id and one-based history line; unsafe identifiers are SHA-256 locators
68
+ rather than echoed content. Detail is capped at 100 while the exact count and a
69
+ truncation receipt preserve loss awareness. Recovery means restoring the named
70
+ receipt from an authoritative clone or backup. Rewriting append-only history or
71
+ deleting sibling evidence is never an automatic remediation.
54
72
 
55
73
  ## Strict-create recovery
56
74
 
@@ -12,7 +12,9 @@ by [pm-v7e337](../.agents/pm/features/pm-v7e337.toon),
12
12
  [pm-3zh9s4](../.agents/pm/features/pm-3zh9s4.toon), and
13
13
  [pm-vzcisw](../.agents/pm/chores/pm-vzcisw.toon). Skills and Apps are tracked
14
14
  by [pm-8nzivt](../.agents/pm/features/pm-8nzivt.toon) and
15
- [pm-pznhee](../.agents/pm/features/pm-pznhee.toon).
15
+ [pm-pznhee](../.agents/pm/features/pm-pznhee.toon). Progressive tool discovery
16
+ and canonical model-facing results are tracked by
17
+ [pm-3g3f8z](../.agents/pm/features/pm-3g3f8z.toon).
16
18
 
17
19
  Status: accepted. MCP `2026-07-28` is pm's canonical protocol revision.
18
20
 
@@ -39,16 +41,18 @@ capabilities, public cache policy, server identity, and bounded instructions.
39
41
  No modern request reads identity, capabilities, or version from a previous
40
42
  request.
41
43
 
44
+ The optional `dev.unbrained.pm/progressive-tool-discovery` extension is also request-local. Negotiated clients receive a stable five-tool entry catalog and expand it with `pm_discover`; unnegotiated clients retain the complete profile-selected list. Negotiated tool results designate `structuredContent.result` as canonical and use text content only as a pointer, while the isolated compatibility adapter retains duplicated JSON text for existing clients. The public SDK owns ranking, cursor binding, token and omission receipts, and cache invalidation. See [Progressive Tool Discovery](PROGRESSIVE_TOOL_DISCOVERY.md).
45
+
42
46
  ## Legacy boundary
43
47
 
44
48
  Legacy means every revision that establishes a session with an `initialize`
45
49
  handshake, which the canonical revision defines as `2025-11-25` and earlier.
46
50
  The stdio adapter therefore accepts the full initialize-era set, newest first:
47
51
 
48
- | Revision | Accepted at `initialize` | Notes |
49
- |----------|--------------------------|-------|
50
- | `2025-11-25` | yes | Final legacy revision; offered when a client omits `protocolVersion` |
51
- | `2025-06-18` | yes | Earlier legacy revision retained for existing consumers |
52
+ | Revision | Accepted at `initialize` | Notes |
53
+ | ------------ | ------------------------ | -------------------------------------------------------------------- |
54
+ | `2025-11-25` | yes | Final legacy revision; offered when a client omits `protocolVersion` |
55
+ | `2025-06-18` | yes | Earlier legacy revision retained for existing consumers |
52
56
 
53
57
  `initialize` answers with the revision the client requested, because a legacy
54
58
  client has no fall-forward mechanism and treats the answered version as the
@@ -1,6 +1,6 @@
1
1
  # Runtime capability surfaces
2
2
 
3
- Tracker: [pm-xwah](../.agents/pm/chores/pm-xwah.toon), [pm-kxci8x](../.agents/pm/tasks/pm-kxci8x.toon), [pm-mu8m](../.agents/pm/tasks/pm-mu8m.toon), [pm-9k90](../.agents/pm/features/pm-9k90.toon), [pm-m4ikkz](../.agents/pm/features/pm-m4ikkz.toon), [pm-yf07b7](../.agents/pm/features/pm-yf07b7.toon)
3
+ Tracker: [pm-xwah](../.agents/pm/chores/pm-xwah.toon), [pm-kxci8x](../.agents/pm/tasks/pm-kxci8x.toon), [pm-mu8m](../.agents/pm/tasks/pm-mu8m.toon), [pm-9k90](../.agents/pm/features/pm-9k90.toon), [pm-m4ikkz](../.agents/pm/features/pm-m4ikkz.toon), [pm-yf07b7](../.agents/pm/features/pm-yf07b7.toon), and [pm-3g3f8z](../.agents/pm/features/pm-3g3f8z.toon).
4
4
 
5
5
  The SDK exports one agent capability contract for command visibility, MCP tool profiles, resources, and workflow prompts. CLI-facing generators and MCP hosts should project from these contracts instead of maintaining independent allowlists.
6
6
 
@@ -15,6 +15,12 @@ Set `PM_MCP_PROFILE` for the `pm-mcp` process:
15
15
 
16
16
  An activated extension command makes `pm_run` discoverable outside a custom profile. Its action is added to the live `pm_run` action enumeration. Custom profiles remain exact allowlists.
17
17
 
18
+ ## Progressive discovery
19
+
20
+ Modern clients may negotiate `dev.unbrained.pm/progressive-tool-discovery`. Their initial `tools/list` response is the stable five-tool entry catalog, and `pm_discover` expands authorized tools by intent, family, and tier under explicit row and token ceilings. Unnegotiated and initialize-era clients retain the full selected profile.
21
+
22
+ The SDK ranks with visible lexical, semantic, graph, permission, freshness, and usage signals, binds cursors to all ranking inputs, and emits omission recovery plus cache invalidation metadata. Negotiated tool calls make `structuredContent.result` canonical and replace duplicated JSON text with a pointer; compatibility clients retain the old representation. See [Progressive Tool Discovery](PROGRESSIVE_TOOL_DISCOVERY.md).
23
+
18
24
  Package authors can declare `tier: "core" | "standard" | "full" | "internal"` on `registerCommand()` definitions. The default is `standard`; `internal` commands remain callable by native dispatch but are not advertised by normal MCP profiles.
19
25
 
20
26
  ## Workspace schema projection