pm-github 2026.10.28 → 2026.10.30

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 (59) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +43 -4
  3. package/dist/closeout.d.ts.map +1 -1
  4. package/dist/closeout.js +71 -30
  5. package/dist/closeout.js.map +1 -1
  6. package/dist/discussion-schema.d.ts +110 -0
  7. package/dist/discussion-schema.d.ts.map +1 -0
  8. package/dist/discussion-schema.js +41 -0
  9. package/dist/discussion-schema.js.map +1 -0
  10. package/dist/discussions.d.ts +26 -0
  11. package/dist/discussions.d.ts.map +1 -0
  12. package/dist/discussions.js +215 -0
  13. package/dist/discussions.js.map +1 -0
  14. package/dist/gate.d.ts +2 -0
  15. package/dist/gate.d.ts.map +1 -1
  16. package/dist/gate.js +20 -9
  17. package/dist/gate.js.map +1 -1
  18. package/dist/identity.d.ts.map +1 -1
  19. package/dist/identity.js +2 -1
  20. package/dist/identity.js.map +1 -1
  21. package/dist/import-fidelity.d.ts +8 -3
  22. package/dist/import-fidelity.d.ts.map +1 -1
  23. package/dist/import-fidelity.js +39 -10
  24. package/dist/import-fidelity.js.map +1 -1
  25. package/dist/import-tags.d.ts +1 -1
  26. package/dist/import-tags.d.ts.map +1 -1
  27. package/dist/import-tags.js +8 -6
  28. package/dist/import-tags.js.map +1 -1
  29. package/dist/index.d.ts +48 -40
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +164 -69
  32. package/dist/index.js.map +1 -1
  33. package/dist/issue-forms.d.ts +1 -1
  34. package/dist/issue-forms.d.ts.map +1 -1
  35. package/dist/issue-forms.js +6 -4
  36. package/dist/issue-forms.js.map +1 -1
  37. package/dist/projects.d.ts +6 -4
  38. package/dist/projects.d.ts.map +1 -1
  39. package/dist/projects.js +21 -20
  40. package/dist/projects.js.map +1 -1
  41. package/dist/refresh.d.ts +2 -1
  42. package/dist/refresh.d.ts.map +1 -1
  43. package/dist/refresh.js +17 -8
  44. package/dist/refresh.js.map +1 -1
  45. package/dist/webhooks.d.ts.map +1 -1
  46. package/dist/webhooks.js +5 -2
  47. package/dist/webhooks.js.map +1 -1
  48. package/dist/workflow-diagnostic.d.ts +9 -0
  49. package/dist/workflow-diagnostic.d.ts.map +1 -0
  50. package/dist/workflow-diagnostic.js +62 -0
  51. package/dist/workflow-diagnostic.js.map +1 -0
  52. package/dist/workspace.d.ts +18 -0
  53. package/dist/workspace.d.ts.map +1 -0
  54. package/dist/workspace.js +39 -0
  55. package/dist/workspace.js.map +1 -0
  56. package/docs/discussion-capture-example.json +14 -0
  57. package/docs/discussion-capture.md +44 -0
  58. package/manifest.json +1 -1
  59. package/package.json +8 -4
package/dist/index.js CHANGED
@@ -1,14 +1,18 @@
1
+ import { readGithubWorkspace, canonicalGithubStatus, githubStatusKind, githubStatusIsTerminal } from "./workspace.js";
1
2
  import { ownValue } from "./own-record.js";
2
3
  import { uniqueGithubLabelNames } from "./label-names.js";
3
4
  import { githubLabelsToTags, importTagArguments, importTagOptions, importTagValues, nativeTagAdditions } from "./import-tags.js";
4
5
  export { githubLabelsToTags } from "./import-tags.js";
5
6
  import { CloseoutUsageError, compactCloseoutReport, runCloseout } from "./closeout.js";
7
+ import { readWorkflowFailure } from "./workflow-diagnostic.js";
6
8
  import { compileIssueForm, completeIssueForm, readRepositoryIssueForm, refuseIssueForm } from "./issue-forms.js";
7
9
  export { compileIssueForm, completeIssueForm, readRepositoryIssueForm } from "./issue-forms.js";
8
- import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, } from "./import-fidelity.js";
10
+ import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, normalizeImportValue, persistImportFidelity, readImportReleases, selectImportContextIndex, serializeImportTags, validateImportLifetime, } from "./import-fidelity.js";
9
11
  import { refreshChanges, runRefresh } from "./refresh.js";
10
12
  import { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.js";
11
13
  import { planGithubLabelMigration, readLabelMigrationJson } from "./labels.js";
14
+ import { captureGithubDiscussion, refuseDiscussion } from "./discussions.js";
15
+ export { captureGithubDiscussion, readGithubDiscussionComment, validateDiscussionCaptureReport } from "./discussions.js";
12
16
  export { planGithubLabelMigration, readGithubLabelCatalog, validateGithubLabelMigrationReport } from "./labels.js";
13
17
  export { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.js";
14
18
  import { acceptGithubWebhook, reconcileGithubWebhooks, WebhookRefusal } from "./webhooks.js";
@@ -329,7 +333,7 @@ async function request(method, url, token, payload, maxRetries = 4) {
329
333
  attempt++;
330
334
  continue;
331
335
  }
332
- throw new Error(`GitHub API returned HTTP ${res.status}`);
336
+ throw new Error(`GitHub API returned HTTP ${res.status} for ${method} ${new URL(url).pathname}`);
333
337
  }
334
338
  }
335
339
  /** Fetch a single GitHub REST endpoint via GET and return the decoded
@@ -958,10 +962,11 @@ export function deriveAtomicItemId(repo, issueNumber, idPrefix, normalizeItemId)
958
962
  return normalizeItemId(`github-${repoToken}-${issueNumber}`, idPrefix);
959
963
  }
960
964
  /** Map one rendered import entry to its reversible SDK mutation sequence. */
961
- export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, updateOnly = false) {
965
+ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, updateOnly = false, statuses) {
962
966
  if (updateOnly && !entry.match?.id)
963
967
  throw new CommandError("Refresh refuses an unlinked target; no item may be created.");
964
- const changed = updateOnly ? new Set(refreshChanges(entry).map(change => change.field)) : undefined;
968
+ const closing = canonicalGithubStatus(entry.status, statuses) === canonicalGithubStatus("closed", statuses);
969
+ const changed = updateOnly ? new Set(refreshChanges(entry, statuses).map(change => change.field)) : undefined;
965
970
  const audit = updateOnly ? { comment: [`Refreshed linked GitHub issue ${repo.toLowerCase()}#${entry.issueNumber}`] } : {};
966
971
  if (entry.linkOnly && entry.match?.id) {
967
972
  return { itemId: entry.match.id, mutations: [{ op: "update", id: entry.match.id, options: { ...(!changed || changed.has("tags") ? importTagOptions(entry) : {}), ...audit } }] };
@@ -974,13 +979,13 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
974
979
  ...importTagOptions(entry),
975
980
  ...(entry.assignee ? { assignee: entry.assignee } : {}),
976
981
  ...(entry.milestone ? { sprint: entry.milestone } : {}),
977
- ...(entry.status === "canceled" ? { closeReason: entry.fidelity?.github_close_reason ?? `GitHub issue #${entry.issueNumber} closed` } : {}),
982
+ ...((!closing && githubStatusIsTerminal(entry.status, statuses)) ? { closeReason: entry.fidelity?.github_close_reason ?? `GitHub issue #${entry.issueNumber} closed` } : {}),
978
983
  };
979
984
  // Explicit terminal reconciliation also handles canceled -> completed. The
980
985
  // supported force option permits that source-authoritative repeat closure;
981
986
  // finalization restores the real dates before the transaction commits.
982
- const statusOptions = entry.status === "closed"
983
- ? entry.fidelity ? { status: "closed", closeReason: entry.fidelity.github_close_reason, force: true } : {}
987
+ const statusOptions = closing
988
+ ? entry.fidelity ? { status: entry.status, closeReason: entry.fidelity.github_close_reason, force: true } : {}
984
989
  : { status: entry.status };
985
990
  const managedItemId = deriveAtomicItemId(repo, entry.issueNumber, idPrefix, normalizeItemId);
986
991
  // A missing match and a match at our deterministic external-key id use the
@@ -990,7 +995,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
990
995
  // create step treats an existing stable id as already applied; update then
991
996
  // makes later content-bearing transactions refresh the item normally.
992
997
  if (!updateOnly && (!entry.match?.id || entry.match.id === managedItemId)) {
993
- const createStatus = entry.status === "closed" ? "open" : entry.status;
998
+ const createStatus = closing ? "open" : entry.status;
994
999
  const mutations = [{
995
1000
  op: "create",
996
1001
  id: managedItemId,
@@ -1003,7 +1008,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
1003
1008
  ...statusOptions,
1004
1009
  },
1005
1010
  }];
1006
- if (entry.status === "closed" && !entry.fidelity) {
1011
+ if (closing && !entry.fidelity) {
1007
1012
  mutations.push({
1008
1013
  op: "close",
1009
1014
  id: managedItemId,
@@ -1030,7 +1035,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
1030
1035
  id: itemId,
1031
1036
  options: updateOptions,
1032
1037
  }];
1033
- if (entry.status === "closed" && !entry.fidelity) {
1038
+ if (closing && !entry.fidelity) {
1034
1039
  mutations.push({
1035
1040
  op: "close",
1036
1041
  id: itemId,
@@ -1046,13 +1051,13 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
1046
1051
  * The importer's desired-state and compensation writes leave retries stable;
1047
1052
  * local edits and later state cycles invalidate completed journals.
1048
1053
  */
1049
- async function importContextVersions(pmRoot, entries) {
1054
+ async function importContextVersions(pmRoot, entries, statuses) {
1050
1055
  const versions = [];
1051
1056
  for (const entry of entries) {
1052
1057
  if (!entry.fidelity || !entry.match?.id)
1053
1058
  continue;
1054
1059
  const snapshot = await sdkReadHistorySnapshot(sdkGetHistoryPath(pmRoot, entry.match.id), entry.match.id);
1055
- const context = snapshot.entries[selectImportContextIndex(snapshot.entries, entry.status)];
1060
+ const context = snapshot.entries[selectImportContextIndex(snapshot.entries, entry.status, statuses)];
1056
1061
  if (context)
1057
1062
  versions.push(`${entry.match.id}:${crypto.createHash("sha256").update(JSON.stringify(context)).digest("hex")}`);
1058
1063
  }
@@ -1197,6 +1202,7 @@ async function commitGithubImportTransaction(config) {
1197
1202
  export async function importGithubAtomic(pmRoot, repo, entries, opts = {}) {
1198
1203
  pmRoot = sdkResolvePmRoot(process.cwd(), pmRoot);
1199
1204
  const { commitItemMutations: commit, normalizeItemId, readSettings, } = await resolveAtomicSdkFunctions(opts);
1205
+ const workspace = await readGithubWorkspace(pmRoot);
1200
1206
  let idPrefix = "pm-";
1201
1207
  try {
1202
1208
  const settings = await readSettings(pmRoot);
@@ -1213,11 +1219,11 @@ export async function importGithubAtomic(pmRoot, repo, entries, opts = {}) {
1213
1219
  // by the stable GitHub issue number so a retry whose API page/order changed
1214
1220
  // supplies the exact same plan as well as the same transaction id.
1215
1221
  for (const entry of [...entries].sort((a, b) => a.issueNumber - b.issueNumber)) {
1216
- const planned = buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, opts.updateOnly);
1222
+ const planned = buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, opts.updateOnly, workspace.statuses);
1217
1223
  itemIds.set(entry.issueNumber, planned.itemId);
1218
1224
  mutations.push(...planned.mutations);
1219
1225
  }
1220
- const transactionId = deriveAtomicTransactionId(repo, entries, mutations, await importContextVersions(pmRoot, entries));
1226
+ const transactionId = deriveAtomicTransactionId(repo, entries, mutations, await importContextVersions(pmRoot, entries, workspace.statuses));
1221
1227
  // Update-only transactions derive their steps from the CURRENT item state,
1222
1228
  // so a retry after a partial apply would fingerprint a different plan and
1223
1229
  // orphan the journal behind a fresh transaction id. Persist the exact plan
@@ -2405,7 +2411,7 @@ export async function prepareGithubImport(issue, repo, opts, token, match, fetch
2405
2411
  const timeline = await fidelityContext.timeline(issue, repo, token);
2406
2412
  const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline);
2407
2413
  const targetStatus = mapState(issue.state, issue.state_reason);
2408
- const conflict = match?.status && match.status !== targetStatus && (["closed", "canceled"].includes(match.status) || issue.state === "closed")
2414
+ const conflict = match?.status && canonicalGithubStatus(match.status, fidelityContext.statuses) !== canonicalGithubStatus(targetStatus, fidelityContext.statuses) && (githubStatusIsTerminal(match.status, fidelityContext.statuses) || issue.state === "closed")
2409
2415
  ? { local: match.status, github: issue.state, rule: identity.linkOnly ? "preserve-provider-owner" : "github-source-wins" } : undefined;
2410
2416
  const kind = issue.pull_request ? "PR" : "issue";
2411
2417
  const labels = issue.labels.map((label) => label.name).filter(Boolean);
@@ -2451,6 +2457,16 @@ export async function prepareGithubImport(issue, repo, opts, token, match, fetch
2451
2457
  match,
2452
2458
  };
2453
2459
  }
2460
+ /** Recognize a complete source projection without treating the SDK mutation clock as content. */
2461
+ export function importEntryUnchanged(entry, types, statuses) {
2462
+ if (!entry.match?.id || !entry.fidelity || refreshChanges(entry, statuses).length !== 0)
2463
+ return false;
2464
+ if (entry.linkOnly)
2465
+ return true;
2466
+ const fields = { type: entry.itemType, description: entry.description, body: entry.body,
2467
+ ...(entry.assignee ? { assignee: entry.assignee } : {}), ...(entry.milestone ? { sprint: entry.milestone } : {}) };
2468
+ return Object.entries(fields).every(([field, value]) => normalizeImportValue(field, ownValue(entry.match, field) ?? "", types, statuses) === normalizeImportValue(field, value, types, statuses));
2469
+ }
2454
2470
  /**
2455
2471
  * Verify that a gated import's plan accounts for every fetched issue exactly once.
2456
2472
  *
@@ -2622,11 +2638,11 @@ function gateImportWrites(pmRoot) {
2622
2638
  * throws {@link CommandError} (with a semantic exit code) on failure.
2623
2639
  *
2624
2640
  * @param repoArg - The `owner/repo` to import from.
2625
- * @param pmRoot - Workspace root or pm data dir.
2641
+ * @param pmPath - Workspace root or pm data dir.
2626
2642
  * @param opts - Normalized import options.
2627
2643
  * @param deps - Injectable collaborators (token resolver, fetchers, etc.) for tests.
2628
2644
  */
2629
- export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2645
+ export async function runImport(repoArg, pmPath, opts, dependencies = {}) {
2630
2646
  if (!repoArg || !repoArg.includes("/")) {
2631
2647
  throw new CommandError("Usage: pm github import <owner/repo> [--all|--state open|closed|all] " +
2632
2648
  "[--labels bug,enhancement] [--since <iso>] [--assignee <login>] " +
@@ -2668,7 +2684,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2668
2684
  console.error("No issues found.");
2669
2685
  // Even a no-op gated import must prove the workspace is gateable (a Git work
2670
2686
  // tree the scanner can read), so the empty-change gate scan runs here too.
2671
- const emptyGateReceipt = opts.gate && !opts.dryRun ? gateImportWrites(pmRoot) : undefined;
2687
+ const emptyGateReceipt = opts.gate && !opts.dryRun ? gateImportWrites(pmPath) : undefined;
2672
2688
  if (opts.dryRun && (opts.atomic || missingIssues.length > 0)) {
2673
2689
  return {
2674
2690
  dryRun: true,
@@ -2688,6 +2704,9 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2688
2704
  // reported "would import N, skip 0" where the real run performs updates for
2689
2705
  // already-linked issues. A preview that overstates creates reads as "this will
2690
2706
  // duplicate my whole tracker" and is the one thing --dry-run exists to rule out.
2707
+ const workspace = await readGithubWorkspace(pmPath);
2708
+ const pmRoot = workspace.pmRoot;
2709
+ const typeNames = new Map(Object.entries(workspace.types.alias_to_type));
2691
2710
  /** Read the complete raw corpus in-process, retaining all provider metadata and bodies. */
2692
2711
  const readImportItems = async () => {
2693
2712
  if (dependencies.readItems)
@@ -2697,7 +2716,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2697
2716
  };
2698
2717
  const existingItems = await readImportItems();
2699
2718
  const existing = indexByProvenance(existingItems);
2700
- const fidelityContext = { items: existingItems, releases: readImportReleases(pmRoot),
2719
+ const fidelityContext = { statuses: workspace.statuses, items: existingItems, releases: readImportReleases(pmRoot),
2701
2720
  timeline: dependencies.fetchIssueTimeline ?? (dependencies.fetchIssues ? async () => [] : fetchImportTimeline) };
2702
2721
  let imported = 0;
2703
2722
  let updated = 0;
@@ -2763,8 +2782,8 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2763
2782
  gatePlan = planned;
2764
2783
  gatePlanByNumber = new Map(planned.map((entry) => [entry.issueNumber, entry]));
2765
2784
  skipped = skippedNumbers.length;
2766
- if (opts.gate && !opts.dryRun) {
2767
- const preview = runCommandTrackerGate({ pmRoot, plannedItems: planned.map(entry => ({
2785
+ if (opts.gate) {
2786
+ const preview = runCommandTrackerGate({ pmRoot, planOnly: opts.dryRun, plannedItems: planned.map(entry => ({
2768
2787
  itemId: entry.match?.id ?? `github-issue-${entry.issueNumber}`,
2769
2788
  fields: { ...entry.fidelity, title: entry.title, description: entry.description, body: entry.body, tags: entry.tags,
2770
2789
  assignee: entry.assignee, sprint: entry.milestone,
@@ -2784,7 +2803,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2784
2803
  const source = entry.fidelity;
2785
2804
  const reason = source.github_close_reason.split(" (", 1)[0];
2786
2805
  return { issue: entry.issueNumber, item: entry.match?.id,
2787
- action: entry.identity?.via === "citation" ? "adopt" : entry.linkOnly ? "link" : entry.match?.id ? "update" : "create", status: entry.status,
2806
+ action: importEntryUnchanged(entry, typeNames, workspace.statuses) ? "unchanged" : entry.identity?.via === "citation" ? "adopt" : entry.linkOnly ? "link" : entry.match?.id ? "update" : "create", status: entry.status,
2788
2807
  ...source, github_close_reason: reason,
2789
2808
  created_at: entry.linkOnly ? entry.match.created_at : source.github_created_at,
2790
2809
  updated_at_policy: "sdk-mutation-clock; source in github_updated_at",
@@ -2796,7 +2815,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2796
2815
  });
2797
2816
  const adoptionCount = gatePlan.filter(entry => entry.identity?.via === "citation").length;
2798
2817
  const planFields = { ...(opts.gate || adoptionCount > 0 || citationCandidates.length > 0 ? { plan: fidelityPlan() } : {}), ...citationReceipt };
2799
- const gatePlanFields = { ...planFields, ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: "post-write" } } : {}) };
2818
+ const gatePlanFields = { ...planFields, ...(opts.gate ? { gate: { completeness: gateCompleteness, idempotency: gateIdempotency, scan: opts.dryRun ? "plan" : "post-write" } } : {}) };
2800
2819
  if (opts.atomic) {
2801
2820
  // The gated pipeline already prepared every entry (and counted its skips)
2802
2821
  // during the pre-mutation plan phase; reuse that plan verbatim so the write
@@ -2817,11 +2836,16 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2817
2836
  }
2818
2837
  throw new CommandError(`Imported 0 issue(s); ${skipped} failed.`, EXIT_CODE.GENERIC_FAILURE);
2819
2838
  }
2839
+ // Preserve the complete ordered plan while a transaction is resumable.
2840
+ // Otherwise omit each normalized no-op, including in mixed batches.
2841
+ const changed = (await listUnfinishedGithubImports(pmRoot)).length > 0 ? prepared
2842
+ : prepared.filter(entry => !importEntryUnchanged(entry, typeNames, workspace.statuses));
2843
+ const changedAdoptions = changed.filter(entry => entry.identity?.via === "citation").length;
2820
2844
  if (opts.dryRun) {
2821
- imported = prepared.filter((entry) => !entry.match?.id).length;
2822
- updated = prepared.length - imported - adoptionCount;
2845
+ imported = changed.filter((entry) => !entry.match?.id).length;
2846
+ updated = changed.length - imported - changedAdoptions;
2823
2847
  for (const entry of prepared) {
2824
- const action = entry.identity?.via === "citation" ? "adopt" : entry.match?.id ? "update" : "import";
2848
+ const action = !changed.includes(entry) ? "unchanged" : entry.identity?.via === "citation" ? "adopt" : entry.match?.id ? "update" : "import";
2825
2849
  console.error(` [dry-run][atomic] #${entry.issueNumber} ${action}: ${opts.gate ? "(gated title)" : entry.title} (${entry.status})`);
2826
2850
  }
2827
2851
  console.error(`[dry-run] Atomic plan would import ${imported}, update ${updated}, skip ${skipped}. Adopt ${adoptionCount}.`);
@@ -2841,7 +2865,16 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2841
2865
  ...fetchFields,
2842
2866
  };
2843
2867
  }
2844
- const result = await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, prepared);
2868
+ // Mixed create/update batches can select a fresh journal after the first
2869
+ // apply. Replaying an otherwise identical SDK UPDATE advances updated_at.
2870
+ // Never hide an interrupted journal behind the unchanged-state shortcut.
2871
+ const unchanged = changed.length === 0;
2872
+ const result = unchanged ? { transactionId: undefined, recovered: false, recoveredItems: undefined, imported: 0, updated: 0,
2873
+ itemIds: new Map(prepared.map(entry => [entry.issueNumber, entry.match.id])) }
2874
+ : await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, changed);
2875
+ for (const entry of prepared)
2876
+ if (!changed.includes(entry))
2877
+ result.itemIds.set(entry.issueNumber, entry.match.id);
2845
2878
  if (opts.gate)
2846
2879
  verifyImportedProvenance(await readImportItems(), prepared, repo);
2847
2880
  // Planned native comments are part of the verified plan: a contended lock,
@@ -2861,7 +2894,9 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2861
2894
  }
2862
2895
  }
2863
2896
  failClosedOnPlanDivergence(opts.gate, skipped, skipped, commentWriteFailures);
2864
- if (result.recovered) {
2897
+ if (unchanged)
2898
+ console.error("Atomic import unchanged; no item transaction was needed.");
2899
+ else if (result.recovered) {
2865
2900
  console.error(`Atomic import recovered transaction ${result.transactionId} covering ${result.recoveredItems ?? prepared.length} item(s).`);
2866
2901
  }
2867
2902
  else {
@@ -2878,10 +2913,11 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2878
2913
  const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined;
2879
2914
  return {
2880
2915
  transactionId: result.transactionId,
2916
+ ...(unchanged ? { unchanged: true } : {}),
2881
2917
  recovered: result.recovered,
2882
2918
  imported: result.imported,
2883
- updated: result.updated - (result.recovered ? 0 : adoptionCount),
2884
- adopted: result.recovered ? 0 : adoptionCount,
2919
+ updated: result.updated - (result.recovered ? 0 : changedAdoptions),
2920
+ adopted: result.recovered ? 0 : changedAdoptions,
2885
2921
  ...(result.recoveredItems !== undefined ? { recoveredItems: result.recoveredItems } : {}),
2886
2922
  skipped,
2887
2923
  atomic: true,
@@ -2913,6 +2949,14 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2913
2949
  const args = ["--path", pmRoot, "close", id, "--reason", prepared.fidelity.github_close_reason];
2914
2950
  return args;
2915
2951
  };
2952
+ // Annotation sync still runs when only comments changed; native content
2953
+ // mutations must never advance the SDK clock for an identical projection.
2954
+ if (importEntryUnchanged(prepared, typeNames, workspace.statuses)) {
2955
+ if (!opts.dryRun && syncAnnotations) {
2956
+ commentWriteFailures += (await syncGithubCommentsToAnnotations(match.id, comments, pmRoot, issue.number)).failed;
2957
+ }
2958
+ continue;
2959
+ }
2916
2960
  if (opts.dryRun) {
2917
2961
  const action = prepared.identity?.via === "citation" ? "adopt" : match?.id ? "update" : "import";
2918
2962
  const metadata = !opts.gate && labels.length > 0 ? `${status}, ${labels.join(",")}` : status;
@@ -2962,22 +3006,22 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2962
3006
  continue;
2963
3007
  }
2964
3008
  // Reconcile status separately.
2965
- if (status === "canceled" && match.status !== "canceled") {
3009
+ if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("canceled", workspace.statuses)) {
2966
3010
  const cancel = pmRun(["--path", pmRoot, "update", match.id, "--status", "canceled", "--close-reason", prepared.fidelity.github_close_reason]);
2967
3011
  if (!cancel.ok)
2968
3012
  throw new CommandError(`Could not cancel GitHub issue #${issue.number}.`);
2969
3013
  }
2970
- if (status === "closed" && match.status !== "closed") {
3014
+ if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("closed", workspace.statuses)) {
2971
3015
  // Close through `pm close` so the reason is real provenance; pass the
2972
3016
  // source completion time as --completed-at when GitHub recorded one.
2973
- const close = pmRun([...githubCloseArgs(match.id), ...(match.status === "canceled" ? ["--force"] : [])]);
3017
+ const close = pmRun([...githubCloseArgs(match.id), ...(githubStatusIsTerminal(match.status, workspace.statuses) ? ["--force"] : [])]);
2974
3018
  if (!close.ok) {
2975
3019
  console.error(`#${issue.number}: close reconciliation failed — ${close.stderr}`);
2976
3020
  skipped++;
2977
3021
  continue;
2978
3022
  }
2979
3023
  }
2980
- else if (status === "open" && (match.status === "closed" || match.status === "canceled")) {
3024
+ else if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("open", workspace.statuses) && githubStatusIsTerminal(match.status, workspace.statuses)) {
2981
3025
  const reopen = pmRun(["--path", pmRoot, "update", match.id, "--status", "open", "--message", `GitHub issue #${issue.number} reopened`]);
2982
3026
  if (!reopen.ok) {
2983
3027
  console.error(`#${issue.number}: reopen reconciliation failed — ${reopen.stderr}`);
@@ -2999,8 +3043,8 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
2999
3043
  // `require_close_reason` rejects `pm create --status closed`. Create the
3000
3044
  // item open, then close it through `pm close` with the source's real
3001
3045
  // completion timestamp (`closed_at`) as --completed-at provenance.
3002
- const createStatus = status === "closed" ? "open" : status;
3003
- const mustClose = status === "closed";
3046
+ const createStatus = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) ? "open" : status;
3047
+ const mustClose = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses);
3004
3048
  const createArgs = [
3005
3049
  "--path", pmRoot, "create",
3006
3050
  "--title", title,
@@ -3011,7 +3055,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
3011
3055
  "--tags", importTagOptions(prepared).tags,
3012
3056
  "--message", `Imported from GitHub #${issue.number}`,
3013
3057
  ];
3014
- if (status === "canceled")
3058
+ if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses))
3015
3059
  createArgs.push("--close-reason", prepared.fidelity.github_close_reason);
3016
3060
  if (assignee)
3017
3061
  createArgs.push("--assignee", assignee);
@@ -3163,8 +3207,8 @@ function failClosedOnPlanDivergence(gate, plannedSkipped, actualSkipped, comment
3163
3207
  // ---------------------------------------------------------------------------
3164
3208
  // Sync core — push pm status changes back to GitHub (close / reopen)
3165
3209
  // ---------------------------------------------------------------------------
3166
- function pmStatusToGithubState(status) {
3167
- return status === "closed" || status === "canceled" ? "closed" : "open";
3210
+ function pmStatusToGithubState(status, statuses) {
3211
+ return githubStatusIsTerminal(status, statuses) ? "closed" : "open";
3168
3212
  }
3169
3213
  /**
3170
3214
  * Require every requested item before a remote sync or export can begin.
@@ -3182,7 +3226,7 @@ function requireRequestedItems(items, requested) {
3182
3226
  }
3183
3227
  // Build the pm → GitHub issue sync plan: for each pm item linked to `repo`, emit
3184
3228
  // a create-or-update entry keyed by the issue's provenance tag.
3185
- export function planSync(items, repo) {
3229
+ export function planSync(items, repo, statuses) {
3186
3230
  const plan = [];
3187
3231
  const repoLc = repo.toLowerCase();
3188
3232
  for (const item of items) {
@@ -3192,7 +3236,7 @@ export function planSync(items, repo) {
3192
3236
  const p = parseProvenanceTag(tag);
3193
3237
  if (!p || p.repo !== repoLc)
3194
3238
  continue;
3195
- const desired = pmStatusToGithubState(item.status);
3239
+ const desired = pmStatusToGithubState(item.status, statuses);
3196
3240
  plan.push({
3197
3241
  id: item.id,
3198
3242
  number: p.number,
@@ -3227,7 +3271,8 @@ async function runSync(ctx) {
3227
3271
  "(set GITHUB_TOKEN/GH_TOKEN or run `gh auth login`). Use --dry-run to preview without a token.", EXIT_CODE.USAGE);
3228
3272
  }
3229
3273
  const selected = requireRequestedItems(readPmItems(ctx.pm_root), scopedIds);
3230
- const plan = planSync(selected, repo);
3274
+ const workspace = await readGithubWorkspace(ctx.pm_root);
3275
+ const plan = planSync(selected, repo, workspace.statuses);
3231
3276
  if (plan.length === 0) {
3232
3277
  const scopeNote = scopedIds.length > 0 ? ` from --ids (${scopedIds.join(", ")})` : "";
3233
3278
  console.error(`No pm items${scopeNote} linked to ${repo} ` +
@@ -3289,7 +3334,7 @@ async function runSync(ctx) {
3289
3334
  }
3290
3335
  // Convert a pm item into the GitHub issue create/update payload, dropping
3291
3336
  // internal provenance tags from labels and applying the optional label map.
3292
- function itemToGithubPayload(item, labelMap) {
3337
+ function itemToGithubPayload(item, labelMap, statuses) {
3293
3338
  // Drop our internal provenance tags from exported labels, then apply any
3294
3339
  // user-supplied label mapping (pm tag → GitHub label). Both issue provenance
3295
3340
  // (`gh:owner/repo#N`) and project provenance (`gh-project:owner/number#itemId`)
@@ -3300,18 +3345,18 @@ function itemToGithubPayload(item, labelMap) {
3300
3345
  title: item.title ?? "(untitled)",
3301
3346
  body: item.body || item.description || "",
3302
3347
  labels: applyLabelMap(labels, labelMap),
3303
- state: item.status === "closed" || item.status === "canceled" ? "closed" : "open",
3348
+ state: pmStatusToGithubState(item.status, statuses),
3304
3349
  };
3305
3350
  }
3306
3351
  // Build the create/update plan. `repo` (lowercased) decides which provenance
3307
3352
  // tags count as an "already exported to THIS repo" link → update; everything
3308
3353
  // else is a create. Pure + side-effect free so it can be unit-tested and
3309
3354
  // printed verbatim in --dry-run.
3310
- export function buildExportPlan(items, repo, labelMap) {
3355
+ export function buildExportPlan(items, repo, labelMap, statuses) {
3311
3356
  const repoLc = repo?.toLowerCase();
3312
3357
  const plan = [];
3313
3358
  for (const item of items) {
3314
- const payload = itemToGithubPayload(item, labelMap);
3359
+ const payload = itemToGithubPayload(item, labelMap, statuses);
3315
3360
  let number;
3316
3361
  if (repoLc) {
3317
3362
  for (const tag of item.tags ?? []) {
@@ -3473,7 +3518,8 @@ async function runExport(ctx) {
3473
3518
  else if (["form-ref", "formRef", "form-answers", "formAnswers", "form-bindings", "formBindings"].some(flag => optionProvided(options, flag))) {
3474
3519
  refuseIssueForm("invalid_form_input", [{ field: "form", reason: "selection", remedy: "Pass --form with --repo and --form-ref to enforce an issue-form contract." }]);
3475
3520
  }
3476
- const plan = buildExportPlan(selected, repo, labelMap);
3521
+ const workspace = await readGithubWorkspace(ctx.pm_root);
3522
+ const plan = buildExportPlan(selected, repo, labelMap, workspace.statuses);
3477
3523
  for (const [index, completion] of completions.entries()) {
3478
3524
  plan[index].payload.body = completion.body;
3479
3525
  plan[index].payload.labels = uniqueGithubLabelNames([...plan[index].payload.labels, ...completion.report.contract.labels]);
@@ -3736,11 +3782,11 @@ function isGithubBlockerDependency(target) {
3736
3782
  * @param items - The complete runtime-validated item corpus.
3737
3783
  * @returns The candidate items, in corpus order.
3738
3784
  */
3739
- export function selectBlockerCandidates(items) {
3785
+ export function selectBlockerCandidates(items, statuses) {
3740
3786
  return items.filter((item) => {
3741
- if (item.status === "blocked")
3787
+ if (githubStatusKind(item.status, statuses) === "blocked")
3742
3788
  return true;
3743
- if (BLOCKER_TERMINAL_STATUSES.has(item.status))
3789
+ if (githubStatusIsTerminal(item.status, statuses))
3744
3790
  return false;
3745
3791
  return (item.dependencies.some((dep) => dep.kind === "blocked_by" && isGithubBlockerDependency(dep.id))
3746
3792
  || (item.blockedBy !== undefined && isGithubBlockerDependency(item.blockedBy)));
@@ -4199,7 +4245,8 @@ async function runBlockers(ctx) {
4199
4245
  }
4200
4246
  const corpus = await readBlockerPmItems(ctx.pm_root);
4201
4247
  const selected = requireRequestedItems(corpus, scopedIds);
4202
- const candidates = selectBlockerCandidates(selected);
4248
+ const workspace = await readGithubWorkspace(ctx.pm_root);
4249
+ const candidates = selectBlockerCandidates(selected, workspace.statuses);
4203
4250
  const perItem = candidates.map((item) => ({ item, references: collectBlockerReferences(item, fallbackRepo) }));
4204
4251
  const unique = new Map();
4205
4252
  for (const entry of perItem) {
@@ -4845,7 +4892,8 @@ async function runProjectImport(ctx) {
4845
4892
  // Reading the local store is non-mutating and keeps dry-run faithful: linked
4846
4893
  // project items must preview as updates, not misleading duplicate creates.
4847
4894
  const pmItems = readPmItems(ctx.pm_root);
4848
- const plan = buildProjectImportPlan(projectItems, ref, pmItems, statusMap);
4895
+ const workspace = await readGithubWorkspace(ctx.pm_root);
4896
+ const plan = buildProjectImportPlan(projectItems, ref, pmItems, statusMap, workspace.statuses);
4849
4897
  let imported = 0;
4850
4898
  let updated = 0;
4851
4899
  let skipped = 0;
@@ -4878,9 +4926,12 @@ async function runProjectImport(ctx) {
4878
4926
  // A `closed` mapping cannot go through `pm update --status closed`
4879
4927
  // (governance require_close_reason rejects it since pm-cli 2026.8.3);
4880
4928
  // the update omits --status and a separate `pm close` records the reason.
4881
- const closeAfterUpdate = entry.mappedStatus === "closed";
4882
- if (entry.mappedStatus && !closeAfterUpdate)
4929
+ const closeAfterUpdate = entry.mappedStatus !== undefined && canonicalGithubStatus(entry.mappedStatus, workspace.statuses) === workspace.statuses.close_status;
4930
+ if (entry.mappedStatus && !closeAfterUpdate) {
4883
4931
  updArgs.push("--status", entry.mappedStatus);
4932
+ if (githubStatusIsTerminal(entry.mappedStatus, workspace.statuses))
4933
+ updArgs.push("--close-reason", projectImportCloseReason(ref, entry.content));
4934
+ }
4884
4935
  const upd = pmRun(updArgs);
4885
4936
  if (!upd.ok) {
4886
4937
  console.error(` ${entry.title}: update failed — ${upd.stderr}`);
@@ -4902,19 +4953,21 @@ async function runProjectImport(ctx) {
4902
4953
  // `require_close_reason` rejects `pm create --status closed` (pm-cli
4903
4954
  // 2026.8.3+). Create open, then close through `pm close` with factual
4904
4955
  // provenance. `--json` is needed to read the assigned id for the close.
4905
- const createStatus = entry.status === "closed" ? "open" : entry.status;
4906
- const mustClose = entry.status === "closed";
4956
+ const mustClose = canonicalGithubStatus(entry.status, workspace.statuses) === workspace.statuses.close_status;
4957
+ const createStatus = mustClose ? workspace.statuses.open_status : entry.status;
4907
4958
  const createArgs = [
4908
4959
  "--path", ctx.pm_root, "create",
4909
4960
  "--title", entry.title,
4910
4961
  "--type", itemType,
4911
4962
  "--status", createStatus,
4912
4963
  "--description", description,
4913
- "--tags", entry.tags.join(","),
4964
+ "--tags", serializeImportTags(entry.tags),
4914
4965
  "--message", `Imported from GitHub project ${ref.owner}/${ref.number}`,
4915
4966
  ];
4916
4967
  if (entry.body)
4917
4968
  createArgs.push("--body", entry.body);
4969
+ if (!mustClose && githubStatusIsTerminal(entry.status, workspace.statuses))
4970
+ createArgs.push("--close-reason", projectImportCloseReason(ref, entry.content));
4918
4971
  if (mustClose)
4919
4972
  createArgs.push("--json");
4920
4973
  const created = pmRun(createArgs);
@@ -5035,22 +5088,22 @@ async function applyPushEntry(entry, meta, ref, pmById, pmRoot, token) {
5035
5088
  * @param pmRoot - Workspace root for the `pm` invocation.
5036
5089
  * @returns The argv array.
5037
5090
  */
5038
- export function buildPullEntryArgs(entry, pmRoot) {
5091
+ export function buildPullEntryArgs(entry, pmRoot, statuses) {
5039
5092
  const reason = `GitHub project status → ${entry.toStatus}`;
5040
- if (entry.toStatus === "closed") {
5093
+ if (canonicalGithubStatus(entry.toStatus, statuses) === (statuses?.close_status ?? "closed")) {
5041
5094
  return ["--path", pmRoot, "close", entry.pmId, "--reason", reason];
5042
5095
  }
5043
- if (entry.toStatus === "canceled") {
5096
+ if (githubStatusIsTerminal(entry.toStatus, statuses)) {
5044
5097
  return [
5045
5098
  "--path", pmRoot, "update", entry.pmId,
5046
- "--status", "canceled",
5099
+ "--status", canonicalGithubStatus(entry.toStatus, statuses),
5047
5100
  "--close-reason", reason,
5048
5101
  "--message", reason,
5049
5102
  ];
5050
5103
  }
5051
5104
  return [
5052
5105
  "--path", pmRoot, "update", entry.pmId,
5053
- "--status", entry.toStatus,
5106
+ "--status", canonicalGithubStatus(entry.toStatus, statuses),
5054
5107
  "--message", reason,
5055
5108
  ];
5056
5109
  }
@@ -5058,8 +5111,8 @@ export function buildPullEntryArgs(entry, pmRoot) {
5058
5111
  // (records closed_at + close_reason); `canceled` keeps its distinct terminal
5059
5112
  // state via `pm update` while recording `--close-reason`; everything else is a
5060
5113
  // plain `pm update --status`.
5061
- function applyPullEntry(entry, pmRoot) {
5062
- const res = pmRun(buildPullEntryArgs(entry, pmRoot));
5114
+ function applyPullEntry(entry, pmRoot, statuses) {
5115
+ const res = pmRun(buildPullEntryArgs(entry, pmRoot, statuses));
5063
5116
  return res.ok ? { changed: true } : { changed: false, error: res.stderr };
5064
5117
  }
5065
5118
  /**
@@ -5108,11 +5161,12 @@ async function runProjectSync(ctx) {
5108
5161
  for (const it of scoped.selected)
5109
5162
  if (it.id)
5110
5163
  pmById.set(it.id, it);
5164
+ const workspace = await readGithubWorkspace(ctx.pm_root);
5111
5165
  const pushPlan = doPush
5112
- ? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap })
5166
+ ? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap, statuses: workspace.statuses })
5113
5167
  : undefined;
5114
5168
  const pullPlan = doPull
5115
- ? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap)
5169
+ ? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap, workspace.statuses)
5116
5170
  : undefined;
5117
5171
  // Conflict resolution when applying BOTH directions: a linked item can appear
5118
5172
  // in both plans. `--prefer pm` (default) lets push win (skip its pull entry);
@@ -5179,7 +5233,7 @@ async function runProjectSync(ctx) {
5179
5233
  for (const e of pullActionable) {
5180
5234
  if (prefer === "pm" && pushItemIds.has(e.itemId))
5181
5235
  continue; // push wins
5182
- const r = applyPullEntry(e, ctx.pm_root);
5236
+ const r = applyPullEntry(e, ctx.pm_root, workspace.statuses);
5183
5237
  if (!r.changed) {
5184
5238
  console.error(` pull ${e.pmId} "${e.title}": ${r.error}`);
5185
5239
  pullFailed++;
@@ -5315,7 +5369,7 @@ const PROJECT_SYNC_FLAGS = [
5315
5369
  const defineExtension = (module) => module;
5316
5370
  export default defineExtension({
5317
5371
  name: "pm-github",
5318
- version: "2026.10.28",
5372
+ version: "2026.10.30",
5319
5373
  activate(api) {
5320
5374
  // -----------------------------------------------------------------------
5321
5375
  // schema — declare the GitHub provenance fields so the workspace knows them
@@ -5575,6 +5629,30 @@ export default defineExtension({
5575
5629
  });
5576
5630
  },
5577
5631
  });
5632
+ api.registerCommand({
5633
+ name: "github discussion capture",
5634
+ description: "Preview explicitly marked issue-comment spans as native Decisions. Apply only the reviewed digest; source instructions remain quoted data.",
5635
+ flags: [
5636
+ { long: "--input", value_name: "file", description: "Strict JSON selection: repo, issue, item, selections with comment/start/end/quote/marked:true" },
5637
+ { long: "--approve", value_name: "digest", description: "Apply the exact reviewed preview, re-reading every source and native target" },
5638
+ { long: "--max-output-bytes", value_name: "N", description: "Complete JSON receipt budget, integer 1024..131072 (default 65536)" },
5639
+ ],
5640
+ async run(ctx) {
5641
+ const { input, approve, maxOutputBytes } = ctx.options;
5642
+ if (typeof input !== "string" || approve !== undefined && typeof approve !== "string")
5643
+ refuseDiscussion("invalid_selection", "Supply --input with a strict JSON selection document and an optional full --approve digest.");
5644
+ let selection;
5645
+ try {
5646
+ selection = readLabelMigrationJson(input);
5647
+ }
5648
+ catch {
5649
+ refuseDiscussion("invalid_selection", "Supply a readable bounded regular JSON selection file; see pm github discussion capture --help.");
5650
+ }
5651
+ return captureGithubDiscussion(ctx.pm_root, selection, ctx.sdk, {
5652
+ approval: approve, maxOutputBytes: maxOutputBytes === undefined ? undefined : Number(maxOutputBytes),
5653
+ });
5654
+ },
5655
+ });
5578
5656
  api.registerCommand({
5579
5657
  name: "github labels plan",
5580
5658
  description: "Preview exact provider label rename/consolidation impacts on stored tags and declared filters/mappings. Read-only; --approve revalidates a saved digest against current state.",
@@ -5763,6 +5841,19 @@ export default defineExtension({
5763
5841
  return report;
5764
5842
  },
5765
5843
  });
5844
+ // Receipts stay private until the workflow scans the selected diagnostic.
5845
+ api.registerCommand({
5846
+ name: "github workflow-diagnostic",
5847
+ description: "Select one actionable refusal from private workflow receipts. Privacy-scan before publishing the result.",
5848
+ flags: [
5849
+ { long: "--report", value_name: "file", description: "Command JSON stdout receipt" },
5850
+ { long: "--error-file", value_name: "file", description: "Command stderr receipt" },
5851
+ { long: "--diff", value_name: "file", description: "Repeat-import tracker diff" },
5852
+ ],
5853
+ async run(ctx) {
5854
+ return readWorkflowFailure(optionString(ctx.options, "report"), optionString(ctx.options, "error-file", "errorFile"), optionString(ctx.options, "diff"));
5855
+ },
5856
+ });
5766
5857
  // Closeout uses the existing repository resolution and REST transport.
5767
5858
  api.registerCommand({
5768
5859
  name: "github closeout",
@@ -5785,8 +5876,12 @@ export default defineExtension({
5785
5876
  const report = await runCloseout(ctx.pm_root, resolveSearchRepo(ctx.options), ctx.options, {
5786
5877
  base: githubApiBase(), get: url => fetchJSON(url, token), next: parseNextLink,
5787
5878
  });
5788
- if (report.failed > 0)
5879
+ if (report.failed > 0) {
5880
+ // Preserve the full private receipt before the host formats or budgets its error message.
5881
+ if (ctx.global.json === true)
5882
+ process.stdout.write(`${JSON.stringify(report)}\n`);
5789
5883
  throw new CommandError(JSON.stringify({ ...report, error: "pm github closeout: some closes failed" }), EXIT_CODE.GENERIC_FAILURE);
5884
+ }
5790
5885
  return ctx.global.json === true || ctx.options.full === true ? report : compactCloseoutReport(report);
5791
5886
  }
5792
5887
  catch (error) {