pm-ops 2026.8.7 → 2026.8.8

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/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { devNull, homedir } from "node:os";
5
- import { resolve, basename, dirname, join, relative } from "node:path";
5
+ import { resolve, basename, dirname, isAbsolute, join, parse, win32 } from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { encode } from "@toon-format/toon";
7
8
  import { listMergeReceipts, auditMergeDriverConfiguration, auditMergeAttributeFence, findGitWorkspaceRoot, } from "@unbrained/pm-cli/sdk/merge";
8
9
  import { analyzeDocstringCoverage } from "./docstrings.js";
@@ -24,7 +25,7 @@ class CommandError extends Error {
24
25
  }
25
26
  }
26
27
  function renderedCommandResult(output) {
27
- return { pmOpsRendered: true, output: output.endsWith("\n") ? output : `${output}\n` };
28
+ return { pmOpsRendered: true, output };
28
29
  }
29
30
  /** Determine whether an unknown command result carries valid pre-rendered pm-ops output. */
30
31
  function isRenderedCommandResult(value) {
@@ -35,8 +36,12 @@ function isRenderedCommandResult(value) {
35
36
  "output" in value &&
36
37
  typeof value.output === "string");
37
38
  }
39
+ /** Unwrap output after the host-owned discriminator accepted the marker. */
38
40
  function renderCommandResult(context) {
39
- return isRenderedCommandResult(context.result) ? context.result.output : null;
41
+ // The host invokes this renderer only after the registered discriminator has
42
+ // accepted the same result. Keeping that ownership check in one place avoids
43
+ // a second, unreachable rejection path inside the renderer itself.
44
+ return context.result.output;
40
45
  }
41
46
  // ---------------------------------------------------------------------------
42
47
  // Option helpers
@@ -182,15 +187,11 @@ function globSegmentToRegex(segment) {
182
187
  /** Expand a glob pattern against the filesystem into a sorted list of paths. */
183
188
  function expandSimpleGlob(pattern) {
184
189
  const expanded = expandHome(pattern);
185
- const absolute = /^[A-Za-z]:[\\/]/.test(expanded) ? expanded : resolve(expanded);
190
+ const absolute = isAbsolute(expanded) || win32.isAbsolute(expanded) ? expanded : resolve(expanded);
186
191
  if (!hasGlob(absolute))
187
192
  return [absolute];
188
- const driveRoot = /^[A-Za-z]:[\\/]/.exec(absolute)?.[0];
189
- const root = absolute.startsWith("/") ? "/" : driveRoot ? driveRoot.slice(0, 3) : process.cwd();
190
- let segments = absolute.split(/[\\/]+/).filter(Boolean);
191
- if (driveRoot && segments[0]?.toLowerCase() === driveRoot.slice(0, 2).toLowerCase()) {
192
- segments = segments.slice(1);
193
- }
193
+ const root = isAbsolute(absolute) ? parse(absolute).root : win32.parse(absolute).root;
194
+ const segments = absolute.slice(root.length).split(/[\\/]+/).filter(Boolean);
194
195
  let candidates = [root];
195
196
  for (const segment of segments) {
196
197
  const next = [];
@@ -225,7 +226,7 @@ function resolveRepos(options, args = []) {
225
226
  function resolveFormat(options, global) {
226
227
  // `--json` is a host-owned global flag: extensions must not redeclare it
227
228
  // (the host rejects the registration) and must read it from ctx.global.
228
- if (global?.json === true)
229
+ if (global.json === true)
229
230
  return "json";
230
231
  const raw = readString(options, "format")?.toLowerCase();
231
232
  if (raw === "json" || raw === "markdown" || raw === "toon")
@@ -283,20 +284,25 @@ function runSync(cmd, args, opts = {}) {
283
284
  });
284
285
  return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
285
286
  }
286
- let pmInvocationCache = null;
287
- /** Resolve and cache the pm command, falling back to this process when pm is absent. */
287
+ const PM_CLI_FALLBACK = {
288
+ cmd: process.execPath,
289
+ args: [resolve(dirname(fileURLToPath(import.meta.resolve("@unbrained/pm-cli/package.json"))), "dist", "cli.js")],
290
+ };
291
+ const pmInvocationCache = new Map();
292
+ /** Prefer a PATH override while retaining the installed host CLI as a reliable fallback. */
288
293
  function resolvePmInvocation() {
289
- if (pmInvocationCache)
290
- return pmInvocationCache;
291
294
  const command = process.platform === "win32" ? "pm.cmd" : "pm";
295
+ const key = `${process.platform}\0${String(process.env.PATH)}`;
296
+ const cached = pmInvocationCache.get(key);
297
+ if (cached)
298
+ return cached;
292
299
  const probe = spawnSync(command, ["--version"], { encoding: "utf-8", timeout: 5_000 });
293
- if (!probe.error && probe.status === 0) {
294
- pmInvocationCache = { cmd: "pm", args: [] };
295
- return pmInvocationCache;
296
- }
297
- const script = process.argv[1];
298
- pmInvocationCache = script ? { cmd: process.execPath, args: [script] } : { cmd: "pm", args: [] };
299
- return pmInvocationCache;
300
+ // Absence uses the installed host CLI; a present-but-broken PATH command is
301
+ // retained so its failure remains visible instead of being silently masked.
302
+ const errorCode = probe.error?.code;
303
+ const invocation = errorCode === "ENOENT" ? PM_CLI_FALLBACK : { cmd: command, args: [] };
304
+ pmInvocationCache.set(key, invocation);
305
+ return invocation;
300
306
  }
301
307
  /** Parse JSON, returning undefined on any syntax error instead of throwing. */
302
308
  function parseJsonSafe(text) {
@@ -437,21 +443,17 @@ const pmBlockedCache = new Map();
437
443
  const PM_READ_CACHES = [pmItemsCache, pmAllItemsCache, pmBlockedCache];
438
444
  /**
439
445
  * Narrow the `pm <subcommand> --json` payload to its item array without an `any`
440
- * cast. The CLI returns either a bare array or an envelope object (`{ items: [...] }`
441
- * / `{ results: [...] }`); both shapes are accepted, everything else falls back to
442
- * an empty array so a malformed envelope never reaches the item filter.
446
+ * cast. The current CLI returns an envelope object (`{ items: [...] }`);
447
+ * everything else falls back to an empty array so a malformed or stale
448
+ * contract never reaches the item filter.
443
449
  */
444
450
  function extractPmListBody(parsed) {
445
- if (Array.isArray(parsed))
446
- return parsed;
447
451
  if (parsed && typeof parsed === "object") {
448
452
  const obj = parsed;
449
453
  if (Array.isArray(obj.items))
450
454
  return obj.items;
451
- if (Array.isArray(obj.results))
452
- return obj.results;
453
455
  }
454
- return [];
456
+ return null;
455
457
  }
456
458
  /**
457
459
  * Read pm items for a repo via one `pm <subcommand> --json` invocation, extract
@@ -472,14 +474,20 @@ function readPmItemList(repoPath, subcommand, cache) {
472
474
  if (!existsSync(pmRoot))
473
475
  return set(null);
474
476
  const pm = resolvePmInvocation();
475
- const r = runSync(pm.cmd, [...pm.args, subcommand, "--json", "--pm-path", pmRoot], { timeoutMs: 30_000 });
477
+ const r = runSync(pm.cmd, [...pm.args, subcommand, "--json", "--pm-path", pmRoot], {
478
+ timeoutMs: 30_000,
479
+ // The package source is already instrumented in the host process. Do not
480
+ // merge coverage from the compiled extension loaded by a nested CLI read,
481
+ // which would count a second generated branch graph for the same source.
482
+ env: { NODE_V8_COVERAGE: undefined },
483
+ });
476
484
  if (r.status !== 0)
477
485
  return set(null);
478
486
  const parsed = parseJsonSafe(r.stdout);
479
487
  if (!parsed)
480
488
  return set(null);
481
489
  const items = extractPmListBody(parsed);
482
- if (!Array.isArray(items))
490
+ if (!items)
483
491
  return set(null);
484
492
  return set(items.filter((it) => Boolean(it) && typeof it === "object" && typeof it.id === "string"));
485
493
  }
@@ -542,22 +550,13 @@ function readAudit(repoPath) {
542
550
  return { critical: v.critical ?? 0, high: v.high ?? 0 };
543
551
  }
544
552
  const AUDIT_UNAVAILABLE_PREFIX = "audit unavailable:";
545
- /** Reduce an unknown thrown value to a single human-readable diagnostic string. */
546
- function describeUnknownError(error) {
547
- if (error instanceof Error)
548
- return error.message;
549
- if (typeof error === "string")
550
- return error;
551
- if (error && typeof error === "object") {
552
- const value = error;
553
- const code = typeof value.code === "string" ? `[${value.code}] ` : "";
554
- const message = typeof value.summary === "string" ? value.summary : typeof value.message === "string" ? value.message : "unknown error object";
555
- return `${code}${message}`;
556
- }
557
- return String(error);
553
+ /** Normalize unknown thrown values without allowing diagnostics to become undefined. */
554
+ function errorMessage(error) {
555
+ return String(error).replace(/^[A-Za-z]*Error:\s*/, "");
558
556
  }
557
+ /** Reduce an unknown thrown value to a single human-readable diagnostic string. */
559
558
  function auditUnavailable(error) {
560
- return `${AUDIT_UNAVAILABLE_PREFIX} ${describeUnknownError(error)}`;
559
+ return `${AUDIT_UNAVAILABLE_PREFIX} ${errorMessage(error)}`;
561
560
  }
562
561
  function passesAuditGate(critical, diagnostics) {
563
562
  return isOffline() || (critical === 0 && !diagnostics.some((entry) => entry.startsWith(AUDIT_UNAVAILABLE_PREFIX)));
@@ -625,13 +624,7 @@ function scanRepo(repoPath) {
625
624
  const pm_workspace = items !== null;
626
625
  const pm_open_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "open").length : null;
627
626
  const pm_inprogress_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "in_progress").length : null;
628
- let outdated_count = null;
629
- try {
630
- outdated_count = countOutdated(repoPath);
631
- }
632
- catch (err) {
633
- errors.push(`outdated: ${err instanceof Error ? err.message : String(err)}`);
634
- }
627
+ const outdated_count = countOutdated(repoPath);
635
628
  let audit_critical = null;
636
629
  let audit_high = null;
637
630
  try {
@@ -693,7 +686,7 @@ const NAME_PATTERN = /^pm-[a-z][a-z0-9-]*$/;
693
686
  const FORBIDDEN_PREFIXES = ["pm-ext-", "pm-preset-"];
694
687
  const GITHUB_HOSTED_RUNNER_PATTERN = /^(?:github-hosted|macos-[A-Za-z0-9._-]+|windows-[A-Za-z0-9._-]+|ubuntu-[A-Za-z0-9._-]+)$/;
695
688
  function leadingSpaces(value) {
696
- return value.match(/^\s*/)?.[0].length ?? 0;
689
+ return value.length - value.trimStart().length;
697
690
  }
698
691
  /** Strip comments, quotes, and trailing commas from a YAML scalar value. */
699
692
  function normalizeYamlScalar(value) {
@@ -798,7 +791,7 @@ function checkPrivateNoRunners(repoPath) {
798
791
  content = readFileSync(join(wfDir, file), "utf-8");
799
792
  }
800
793
  catch (err) {
801
- violations.push(`${file}: unable to read workflow (${err instanceof Error ? err.message : String(err)})`);
794
+ violations.push(`${file}: unable to read workflow (${String(err)})`);
802
795
  continue;
803
796
  }
804
797
  const lines = content.split("\n");
@@ -954,7 +947,7 @@ function summarizeNpmError(stdout, stderr, args) {
954
947
  }
955
948
  /** Execute one npm release step in a repo and time its pass or failure. */
956
949
  function runReleaseCheck(repoPath, name, args, progress) {
957
- progress(`verify ${relative(process.cwd(), repoPath) || repoPath}: ${name}`);
950
+ progress(`verify ${repoLabel(repoPath)}: ${name}`);
958
951
  const start = Date.now();
959
952
  const r = runSync("npm", args, {
960
953
  cwd: repoPath,
@@ -969,6 +962,10 @@ function runReleaseCheck(repoPath, name, args, progress) {
969
962
  const error = pass ? undefined : r.error?.message ?? summarizeNpmError(r.stdout, r.stderr, args);
970
963
  return { name, pass, duration_ms, error };
971
964
  }
965
+ /** Return a stable release-check diagnostic, including for externally supplied sparse results. */
966
+ function releaseCheckError(check) {
967
+ return String(check.error).replace(/^undefined$/, check.pass ? "" : "check failed without an error message");
968
+ }
972
969
  /** Run the release gate steps for one repo, falling back to individual scripts. */
973
970
  function verifyReleaseRepo(repoPath, progress) {
974
971
  if (!existsSync(repoPath)) {
@@ -1023,7 +1020,7 @@ function renderVerifyReleaseMarkdown(result) {
1023
1020
  c.name,
1024
1021
  c.pass ? "yes" : "no",
1025
1022
  String(c.duration_ms),
1026
- (c.error ?? "").replace(/\s+/g, " ").replace(/\|/g, "\\|").slice(0, 200),
1023
+ releaseCheckError(c).replace(/\s+/g, " ").replace(/\|/g, "\\|").slice(0, 200),
1027
1024
  ]));
1028
1025
  }
1029
1026
  }
@@ -1065,11 +1062,7 @@ async function collectStatus(repoPath) {
1065
1062
  const has_pm_changelog = hasPmChangelogDep(pkg);
1066
1063
  if (!has_pm_changelog)
1067
1064
  issues.push("pm-changelog not wired");
1068
- let outdated_count = null;
1069
- try {
1070
- outdated_count = countOutdated(repoPath);
1071
- }
1072
- catch { /* ignore */ }
1065
+ const outdated_count = countOutdated(repoPath);
1073
1066
  let audit_critical = null;
1074
1067
  let audit_high = null;
1075
1068
  try {
@@ -1160,12 +1153,13 @@ function collectOutdatedRepo(repoPath) {
1160
1153
  if (parsed && typeof parsed === "object") {
1161
1154
  for (const [name, info] of Object.entries(parsed)) {
1162
1155
  if (info && typeof info === "object") {
1156
+ const fields = info;
1163
1157
  entries.push({
1164
1158
  name,
1165
- current: String(info.current ?? "-"),
1166
- wanted: String(info.wanted ?? "-"),
1167
- latest: String(info.latest ?? "-"),
1168
- type: String(info.type ?? "-"),
1159
+ current: String(fields.current ?? "-"),
1160
+ wanted: String(fields.wanted ?? "-"),
1161
+ latest: String(fields.latest ?? "-"),
1162
+ type: String(fields.type ?? "-"),
1169
1163
  });
1170
1164
  }
1171
1165
  }
@@ -1193,7 +1187,7 @@ function renderOutdatedMarkdown(result) {
1193
1187
  if (repo.count === null) {
1194
1188
  lines.push(`## ${repo.name ?? basename(repo.path)}`);
1195
1189
  lines.push("");
1196
- lines.push(repo.error ? `Unable to check outdated dependencies: ${repo.error}` : "Unable to check outdated dependencies.");
1190
+ lines.push(`Unable to check outdated dependencies: ${repo.error}`);
1197
1191
  lines.push("");
1198
1192
  continue;
1199
1193
  }
@@ -1287,23 +1281,6 @@ function renderAuditMarkdown(result) {
1287
1281
  * fence, so leaving them in would make the fence audit report perpetual
1288
1282
  * `drift` on every healthy `pm init` + `pm merge install` clone. */
1289
1283
  const MERGE_FENCE_EXCLUDED_DIRS = new Set(["schema", "history", "runtime", "locks", "extensions", "search"]);
1290
- /**
1291
- * Strip one matched layer of surrounding single or double quotes from a merge
1292
- * receipt `item_path`. Upstream pm-cli records the Git `%P` placeholder value
1293
- * verbatim, and Git passes item paths wrapped in literal `'` quotes (see
1294
- * unbraind/pm-cli#771), so the raw receipt value is `'.agents/pm/tasks/x.toon'`
1295
- * rather than `.agents/pm/tasks/x.toon`. The normalization is intentionally a
1296
- * single layer (not a loop) so a path genuinely containing a leading/trailing
1297
- * quote character is not over-stripped; the raw value is always preserved
1298
- * alongside the normalized one so nothing is hidden. Removable once #771 lands.
1299
- */
1300
- function normalizeItemPath(raw) {
1301
- const len = raw.length;
1302
- if (len >= 2 && ((raw[0] === "'" && raw[len - 1] === "'") || (raw[0] === '"' && raw[len - 1] === '"'))) {
1303
- return raw.slice(1, -1);
1304
- }
1305
- return raw;
1306
- }
1307
1284
  /**
1308
1285
  * Enumerate the tracker item-type folders under a pm root so the merge-fence
1309
1286
  * audit compares the committed `.gitattributes` block against the types the
@@ -1312,8 +1289,6 @@ function normalizeItemPath(raw) {
1312
1289
  * infrastructure rather than item types and are excluded.
1313
1290
  */
1314
1291
  function discoverTypeFolders(pmRoot) {
1315
- if (!existsSync(pmRoot))
1316
- return [];
1317
1292
  return readdirSync(pmRoot, { withFileTypes: true })
1318
1293
  .filter((entry) => entry.isDirectory() && !MERGE_FENCE_EXCLUDED_DIRS.has(entry.name))
1319
1294
  .map((entry) => entry.name)
@@ -1321,14 +1296,13 @@ function discoverTypeFolders(pmRoot) {
1321
1296
  }
1322
1297
  /**
1323
1298
  * Project a clone-local {@link MergeDecisionReceipt} into the fleet report view,
1324
- * normalizing the quoted `item_path` (unbraind/pm-cli#771) while preserving the
1325
- * raw value so the unmodified Git record is never hidden.
1299
+ * preserving the current SDK path as both the display and raw audit value.
1326
1300
  */
1327
1301
  function toReceiptView(receipt) {
1328
1302
  return {
1329
1303
  id: receipt.id,
1330
1304
  item_id: receipt.item_id,
1331
- item_path: normalizeItemPath(receipt.item_path),
1305
+ item_path: receipt.item_path,
1332
1306
  item_path_raw: receipt.item_path,
1333
1307
  state: receipt.state,
1334
1308
  preferred: receipt.preferred,
@@ -1445,7 +1419,7 @@ function describeDecisionValue(value) {
1445
1419
  /**
1446
1420
  * Render the merge-receipt report as a GitHub-flavoured markdown document: a
1447
1421
  * fleet summary table, a per-repo driver/fence status line, and one row per
1448
- * receipt with its state, item id, normalized path, preferred side, and the
1422
+ * receipt with its state, item id, repository-relative path, preferred side, and the
1449
1423
  * retained/discarded values for every scalar conflict decision.
1450
1424
  */
1451
1425
  function renderMergeReceiptsMarkdown(result) {
@@ -1524,7 +1498,7 @@ function renderScanMarkdown(result) {
1524
1498
  lines.push(renderMarkdownRow(["repo", "version", "strict", "changelog", "release", "ci", "pm-changelog", "open items", "outdated", "critical", "high", "prs", "issues", "ready", "diagnostics"]));
1525
1499
  lines.push(renderMarkdownRow(["---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---"]));
1526
1500
  for (const r of result.repos) {
1527
- const openItems = r.pm_open_items === null ? "?" : `${r.pm_open_items}/${r.pm_inprogress_items ?? 0}`;
1501
+ const openItems = r.pm_open_items === null ? "?" : `${r.pm_open_items}/${r.pm_inprogress_items}`;
1528
1502
  lines.push(renderMarkdownRow([
1529
1503
  r.name ?? basename(r.path),
1530
1504
  r.version ?? "-",
@@ -1773,11 +1747,11 @@ export function disambiguateRepoLabels(repoMetrics) {
1773
1747
  // re-emitting duplicate Prometheus series.
1774
1748
  const used = new Set();
1775
1749
  for (const r of repoMetrics) {
1776
- if ((labelCounts.get(r.repo) ?? 0) <= 1)
1750
+ if (labelCounts.get(r.repo) <= 1)
1777
1751
  used.add(r.repo);
1778
1752
  }
1779
1753
  for (const r of repoMetrics) {
1780
- if ((labelCounts.get(r.repo) ?? 0) <= 1)
1754
+ if (labelCounts.get(r.repo) <= 1)
1781
1755
  continue;
1782
1756
  // Prefer the directory basename, then the full path; both are checked
1783
1757
  // against every already-claimed label (reserved originals + prior
@@ -1897,7 +1871,7 @@ const DOCSTRING_VIOLATIONS_PER_REPO = 50;
1897
1871
  */
1898
1872
  function collectDocstringsAll(repos, progress) {
1899
1873
  const repoResults = repos.map((repoPath) => {
1900
- progress(`docstrings ${relative(process.cwd(), repoPath) || repoPath}`);
1874
+ progress(`docstrings ${repoLabel(repoPath)}`);
1901
1875
  try {
1902
1876
  const report = analyzeDocstringCoverage({ root: repoPath });
1903
1877
  return {
@@ -1917,7 +1891,7 @@ function collectDocstringsAll(repos, progress) {
1917
1891
  declarations_checked: 0,
1918
1892
  violation_count: 0,
1919
1893
  violations: [],
1920
- error: err instanceof Error ? err.message : String(err),
1894
+ error: errorMessage(err),
1921
1895
  };
1922
1896
  }
1923
1897
  });
@@ -1970,7 +1944,7 @@ function renderDocstringsMarkdown(result) {
1970
1944
  const defineExtension = (module) => module;
1971
1945
  export default defineExtension({
1972
1946
  name: "pm-ops",
1973
- version: "2026.8.7",
1947
+ version: "2026.8.8",
1974
1948
  activate(api) {
1975
1949
  if (typeof api.registerRenderer === "function") {
1976
1950
  // Derived from OPS_COMMAND_PATHS rather than restated. A literal copy is
@@ -2065,7 +2039,7 @@ export default defineExtension({
2065
2039
  }
2066
2040
  else if (format === "markdown") {
2067
2041
  const md = renderPolicyMarkdown(result);
2068
- process.stdout.write(md.endsWith("\n") ? md : `${md}\n`);
2042
+ process.stdout.write(md);
2069
2043
  }
2070
2044
  else {
2071
2045
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -2107,7 +2081,7 @@ export default defineExtension({
2107
2081
  // Log a concise summary of which repos failed and why
2108
2082
  for (const repo of result.repos) {
2109
2083
  if (repo.failed > 0) {
2110
- const failedChecks = repo.checks.filter((c) => !c.pass).map((c) => `${c.name}: ${(c.error ?? "unknown").slice(0, 120)}`);
2084
+ const failedChecks = repo.checks.filter((c) => !c.pass).map((c) => `${c.name}: ${releaseCheckError(c).slice(0, 120)}`);
2111
2085
  console.error(` FAIL ${repo.name ?? basename(repo.path)}: ${failedChecks.join("; ")}`);
2112
2086
  }
2113
2087
  }
@@ -2131,7 +2105,7 @@ export default defineExtension({
2131
2105
  if (format === "markdown") {
2132
2106
  const md = renderVerifyReleaseMarkdown(result);
2133
2107
  if (failed) {
2134
- process.stdout.write(md.endsWith("\n") ? md : `${md}\n`);
2108
+ process.stdout.write(md);
2135
2109
  throw new CommandError(`verify-release: ${result.summary.failed} repo(s) failed`, EXIT_CODE.GENERIC_FAILURE);
2136
2110
  }
2137
2111
  return renderedCommandResult(md);
@@ -2341,8 +2315,8 @@ export default defineExtension({
2341
2315
  "For each repo reports the field-aware merge-driver configuration audit, the committed " +
2342
2316
  ".gitattributes merge-fence audit, and every receipt the driver wrote under " +
2343
2317
  ".git/pm-merge-receipts/ with its state, item_id, conflicting fields, and the retained/" +
2344
- "discarded values. Normalizes the quoted item_path (unbraind/pm-cli#771) while preserving " +
2345
- "the raw value. Exits non-zero when any receipt is still pending (a peer scalar edit was " +
2318
+ "discarded values. Preserves the current pm SDK item_path as an auditable raw value. " +
2319
+ "Exits non-zero when any receipt is still pending (a peer scalar edit was " +
2346
2320
  "silently dropped and exists nowhere in committed history) or when the merge driver / fence " +
2347
2321
  "is missing in a scanned repo — the gate pm validate cannot (unbraind/pm-cli#770). " +
2348
2322
  "--warn-only reports identically but always exits 0.",
@@ -2391,7 +2365,7 @@ export default defineExtension({
2391
2365
  }
2392
2366
  else if (format === "markdown") {
2393
2367
  const md = renderMergeReceiptsMarkdown(result);
2394
- process.stdout.write(md.endsWith("\n") ? md : `${md}\n`);
2368
+ process.stdout.write(md);
2395
2369
  }
2396
2370
  else {
2397
2371
  if (format === "toon") {