@mutmutco/cli 4.2.5 → 4.2.7

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 (2) hide show
  1. package/dist/main.cjs +476 -115
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5844,13 +5844,20 @@ async function waitForPrChecks(deps) {
5844
5844
  failureStreak += 1;
5845
5845
  if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
5846
5846
  const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
5847
+ const diagnosticCommand = diagnosis?.failedChecks.find((check) => check.diagnosticCommand)?.diagnosticCommand ?? deps.checksDiagnosticCommand;
5848
+ const failureReceipt = {
5849
+ failedChecks: diagnosis?.failedChecks ?? [],
5850
+ ...deps.checksUrl ? { checksUrl: deps.checksUrl } : {},
5851
+ ...diagnosticCommand ? { diagnosticCommand } : {},
5852
+ ...diagnosis?.checksFailureDetail ? { checksFailureDetail: diagnosis.checksFailureDetail } : {}
5853
+ };
5847
5854
  if (diagnosis?.cause === "stale-head") {
5848
- return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head" };
5855
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head", ...failureReceipt };
5849
5856
  }
5850
5857
  if (diagnosis?.cause === "runner-infra") {
5851
- return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra" };
5858
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra", ...failureReceipt };
5852
5859
  }
5853
- return { policy, status: "failure", reason, detail: "checks-failure" };
5860
+ return { policy, status: "failure", reason, detail: "checks-failure", ...failureReceipt };
5854
5861
  }
5855
5862
  lastDetail = "confirming-failure";
5856
5863
  await deps.sleep(PR_CHECKS_POLL_MS);
@@ -9340,6 +9347,27 @@ function boardConfigFromProject(meta, floor = {}) {
9340
9347
  };
9341
9348
  }
9342
9349
 
9350
+ // src/issue-ref.ts
9351
+ var ISSUE_REF_SHAPES = "123, #123, owner/repo#123, or a GitHub issue or PR URL";
9352
+ function invalidReference(ref) {
9353
+ return new Error(`invalid reference "${ref}" \u2014 expected ${ISSUE_REF_SHAPES}`);
9354
+ }
9355
+ function parseIssueRef(ref, expectedRepo) {
9356
+ const trimmed = ref.trim();
9357
+ const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
9358
+ const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
9359
+ const bare = trimmed.match(/^#?(\d+)$/);
9360
+ const match = url ?? qualified ?? bare;
9361
+ if (!match) throw invalidReference(ref);
9362
+ const number = Number(match[match.length - 1]);
9363
+ if (!Number.isInteger(number) || number <= 0) throw invalidReference(ref);
9364
+ const repo = (url ?? qualified)?.[1];
9365
+ if (repo && expectedRepo && repo.toLowerCase() !== expectedRepo.toLowerCase()) {
9366
+ throw new Error(`reference "${ref}" names ${repo}, which does not match --repo ${expectedRepo} \u2014 expected ${ISSUE_REF_SHAPES}`);
9367
+ }
9368
+ return repo ? { repo, number } : { number };
9369
+ }
9370
+
9343
9371
  // src/repo-resolve.ts
9344
9372
  function slugOf(repoOrSlug) {
9345
9373
  return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
@@ -9349,11 +9377,11 @@ function repoFromRemoteUrl(remoteUrl) {
9349
9377
  return m ? `${m[1]}/${m[2]}` : void 0;
9350
9378
  }
9351
9379
  function repoFromSelector(selector) {
9352
- const trimmed = selector.trim();
9353
- const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/\d+$/i);
9354
- if (url) return url[1];
9355
- const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#\d+$/);
9356
- return qualified?.[1];
9380
+ try {
9381
+ return parseIssueRef(selector).repo;
9382
+ } catch {
9383
+ return void 0;
9384
+ }
9357
9385
  }
9358
9386
  async function resolveRepo(repo) {
9359
9387
  if (repo) return repo;
@@ -11802,10 +11830,10 @@ var rollout_plan_default = {
11802
11830
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
11803
11831
  },
11804
11832
  baseline: {
11805
- version: "4.2.5",
11806
- tag: "v4.2.5",
11807
- commit: "258de06d0766",
11808
- npm: "@mutmutco/cli@4.2.5"
11833
+ version: "4.2.7",
11834
+ tag: "v4.2.7",
11835
+ commit: "bb37250e186c",
11836
+ npm: "@mutmutco/cli@4.2.7"
11809
11837
  },
11810
11838
  exitCriterion: "fleet-n-of-n",
11811
11839
  hubOnlyShortcut: "forbidden",
@@ -11822,14 +11850,14 @@ var rollout_plan_default = {
11822
11850
  repo: "mutmutco/mmi-hub",
11823
11851
  role: "canary",
11824
11852
  schedule: "train",
11825
- v3Target: "v4.2.5"
11853
+ v3Target: "v4.2.7"
11826
11854
  }
11827
11855
  ],
11828
11856
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
11829
11857
  rollback: {
11830
11858
  independent: true,
11831
- mechanism: "npm dist-tag latest -> 4.2.5 and redeploy the Hub Lambda from tag v4.2.5 (258de06d0766); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11832
- v3Target: "v4.2.5 (@mutmutco/cli@4.2.5, tag commit 258de06d0766 \u2014 last known-good release carrying the repo-index v4-only contract)"
11859
+ mechanism: "npm dist-tag latest -> 4.2.7 and redeploy the Hub Lambda from tag v4.2.7 (bb37250e186c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11860
+ v3Target: "v4.2.7 (@mutmutco/cli@4.2.7, tag commit bb37250e186c \u2014 last known-good release carrying the repo-index v4-only contract)"
11833
11861
  }
11834
11862
  },
11835
11863
  {
@@ -12136,16 +12164,6 @@ function formatOrgVersions(report) {
12136
12164
  }
12137
12165
 
12138
12166
  // src/sub-issue.ts
12139
- function parseIssueRef(ref) {
12140
- const trimmed = ref.trim();
12141
- const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
12142
- if (url) return { repo: url[1], number: Number(url[2]) };
12143
- const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
12144
- if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
12145
- const bare = trimmed.match(/^#?(\d+)$/);
12146
- if (bare) return { number: Number(bare[1]) };
12147
- throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
12148
- }
12149
12167
  function buildResolveIdArgs(ref) {
12150
12168
  const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
12151
12169
  if (ref.repo) args.push("--repo", ref.repo);
@@ -14855,19 +14873,19 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
14855
14873
  }
14856
14874
  return [...contexts];
14857
14875
  }
14858
- async function findOpenAlignmentPr(deps, ctx, target) {
14859
- const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", "main", "--state", "open", "--json", "number,url"]));
14876
+ async function findAlignmentPr(deps, ctx, target) {
14877
+ const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", "main", "--state", "all", "--json", "number,url,state"]));
14860
14878
  if (!out) return void 0;
14861
14879
  const rows = JSON.parse(out);
14862
- const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
14863
- return row ? { number: row.number, url: row.url } : void 0;
14880
+ const row = rows.filter((r) => typeof r.number === "number" && typeof r.url === "string" && (r.state === "OPEN" || r.state === "MERGED")).sort((a, b) => b.number - a.number)[0];
14881
+ return row;
14864
14882
  }
14865
14883
  function parsePrNumber(url) {
14866
14884
  const n = Number.parseInt(url.split("/").pop() ?? "", 10);
14867
14885
  return Number.isFinite(n) ? n : void 0;
14868
14886
  }
14869
14887
  async function rollDevelopmentForward(deps, ctx, tag) {
14870
- await runGitRemoteRead(deps, ["fetch", "origin", "main"]);
14888
+ await runGitRemoteRead(deps, ["fetch", "origin", "main", "development"]);
14871
14889
  const required = await discoverRequiredCheckContexts(deps, ctx, "development");
14872
14890
  if (required.length === 0) {
14873
14891
  await deps.run("git", ["checkout", "development"]);
@@ -14876,13 +14894,17 @@ async function rollDevelopmentForward(deps, ctx, tag) {
14876
14894
  await deps.run("git", ["push", "origin", "development"]);
14877
14895
  return { status: "pushed", note: "development rolled forward to the released main (development has no required checks)" };
14878
14896
  }
14879
- const existing = await findOpenAlignmentPr(deps, ctx, "development");
14880
- if (existing) {
14881
- return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
14882
- }
14883
14897
  const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..origin/main"]));
14898
+ const existing = await findAlignmentPr(deps, ctx, "development");
14884
14899
  if (ahead === "0") {
14885
- return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
14900
+ return {
14901
+ status: "aligned",
14902
+ alignment: "already-merged",
14903
+ note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 development already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 development already contains the released main; nothing to roll forward"
14904
+ };
14905
+ }
14906
+ if (existing?.state === "OPEN") {
14907
+ return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
14886
14908
  }
14887
14909
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
14888
14910
 
@@ -14919,7 +14941,7 @@ async function enqueueAlignmentAutoMerge(deps, ctx, prNumber, prUrl, openedNote)
14919
14941
  return { ...base, note: manual };
14920
14942
  }
14921
14943
  async function alignRcForward(deps, ctx, tag) {
14922
- await runGitRemoteRead(deps, ["fetch", "origin", "main"]);
14944
+ await runGitRemoteRead(deps, ["fetch", "origin", "main", "rc"]);
14923
14945
  const required = await discoverRequiredCheckContexts(deps, ctx, "rc");
14924
14946
  if (required.length === 0) {
14925
14947
  const ahead2 = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
@@ -14929,13 +14951,17 @@ async function alignRcForward(deps, ctx, tag) {
14929
14951
  await runGitPush(deps, ["push", "origin", "origin/main:refs/heads/rc"]);
14930
14952
  return { status: "pushed", note: "rc aligned to the released main (rc has no required checks)" };
14931
14953
  }
14932
- const existing = await findOpenAlignmentPr(deps, ctx, "rc");
14933
- if (existing) {
14934
- return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
14935
- }
14936
14954
  const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
14955
+ const existing = await findAlignmentPr(deps, ctx, "rc");
14937
14956
  if (ahead === "0") {
14938
- return { status: "aligned", note: "rc already contains the released main; nothing to align" };
14957
+ return {
14958
+ status: "aligned",
14959
+ alignment: "already-merged",
14960
+ note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 rc already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 rc already contains the released main; nothing to align"
14961
+ };
14962
+ }
14963
+ if (existing?.state === "OPEN") {
14964
+ return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
14939
14965
  }
14940
14966
  const body = `Carries the ${tag} release from \`main\` back to \`rc\`.
14941
14967
 
@@ -15847,16 +15873,42 @@ function resolveGreenGateNpmFromGateWorkflows(files) {
15847
15873
  return pin ? bundledNpmForNodePin(pin) : void 0;
15848
15874
  }
15849
15875
  function parsePublishWorkflowNpmPin(workflowBody) {
15850
- const match = /(?:^|[^\w])npm@(\d+\.\d+\.\d+)/m.exec(workflowBody);
15876
+ const match = /(?:^|[^\w])npm@(\d+(?:\.\d+\.\d+)?)/m.exec(workflowBody);
15851
15877
  return match?.[1];
15852
15878
  }
15879
+ function parseGateNpmVersionPins(workflowBody) {
15880
+ if (!/runner-node-toolchain/.test(workflowBody)) return [];
15881
+ return [...workflowBody.matchAll(/^\s*npm-version:\s*['"]?(\d+(?:\.\d+\.\d+)?)/gm)].map((m) => m[1]);
15882
+ }
15883
+ function parseGateNpmInstallPins(workflowBody) {
15884
+ return [...workflowBody.matchAll(/npm\s+install\s+-g\s+npm@(\d+(?:\.\d+\.\d+)?)/g)].map((m) => m[1]);
15885
+ }
15886
+ function gateDeclaredNpmPins(gateFiles) {
15887
+ const files = gateFiles ?? [];
15888
+ return [
15889
+ ...files.flatMap((file) => parseGateNpmVersionPins(file.body)),
15890
+ ...files.flatMap((file) => parseGateNpmInstallPins(file.body))
15891
+ ];
15892
+ }
15893
+ function publishWorkflowNpm(files) {
15894
+ const publish = files?.find((f) => /(?:^|\/)publish\.ya?ml$/i.test(f.path));
15895
+ return publish ? parsePublishWorkflowNpmPin(publish.body) : void 0;
15896
+ }
15853
15897
  function resolveExpectedCiNpmFromWorkflows(gateFiles, allFiles) {
15898
+ const declared = gateDeclaredNpmPins(gateFiles);
15899
+ if (declared.length) return declared[0];
15854
15900
  const fromGate = resolveGreenGateNpmFromGateWorkflows(gateFiles);
15855
15901
  if (fromGate) return fromGate;
15856
- const files = allFiles ?? gateFiles;
15857
- if (!files?.length) return void 0;
15858
- const publish = files.find((f) => /(?:^|\/)publish\.ya?ml$/i.test(f.path));
15859
- return publish ? parsePublishWorkflowNpmPin(publish.body) : void 0;
15902
+ return publishWorkflowNpm(allFiles ?? gateFiles);
15903
+ }
15904
+ function detectNpmPinDisagreement(gateFiles, allFiles) {
15905
+ const gateNpm = gateDeclaredNpmPins(gateFiles)[0];
15906
+ const publishNpm = publishWorkflowNpm(allFiles ?? gateFiles);
15907
+ if (!gateNpm || !publishNpm) return null;
15908
+ const gateMajor = npmMajor(gateNpm);
15909
+ const publishMajor = npmMajor(publishNpm);
15910
+ if (gateMajor === void 0 || publishMajor === void 0) return null;
15911
+ return gateMajor === publishMajor ? null : { gateNpm, publishNpm };
15860
15912
  }
15861
15913
  function npmMajor(version) {
15862
15914
  const match = /^(\d+)/.exec(version.trim());
@@ -15871,7 +15923,7 @@ function assertNpmMajorPreflight(opts) {
15871
15923
  if (localMajor === void 0 || expectedMajor === void 0) return;
15872
15924
  if (localMajor === expectedMajor) return;
15873
15925
  throw new Error(
15874
- `${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Supported repair (preserves Node): \`npm install -g npm@${expectedNpm}\`; then verify with \`node -v && npm -v\` and rerun the same release command. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem. When gate.yml has no bundled npm pin, read the publish workflow npm pin; otherwise compare \`npm -v\` with the green gate log's toolchain-preflight line (#3446).`
15926
+ `${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Supported repair (preserves Node): \`npm install -g npm@${expectedNpm}\`; then verify with \`node -v && npm -v\` and rerun the same release command. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem. The expected npm is the gate's own declared npm \u2014 an \`npm-version:\` input or an explicit \`npm install -g npm@X\` step (#5893) \u2014 and only a gate that declares neither falls back to a bundled-npm mapping or the publish workflow's pin; otherwise compare \`npm -v\` with the green gate log's toolchain-preflight line (#3446).`
15875
15927
  );
15876
15928
  }
15877
15929
  async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
@@ -15883,6 +15935,12 @@ async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
15883
15935
  } catch {
15884
15936
  return;
15885
15937
  }
15938
+ const disagreement = detectNpmPinDisagreement(gateFiles, allFiles);
15939
+ if (disagreement) {
15940
+ (deps.warn ?? ((m) => console.warn(m)))(
15941
+ `${repo}: repo defect \u2014 the gate declares npm ${disagreement.gateNpm} while the publish workflow pins npm ${disagreement.publishNpm}. The gate's declaration wins this preflight, but the publish lane will still run npm ${disagreement.publishNpm}. Align both pins in one PR.`
15942
+ );
15943
+ }
15886
15944
  assertNpmMajorPreflight({
15887
15945
  repo,
15888
15946
  localNpm,
@@ -20683,15 +20741,9 @@ function resolveBoardConfig(cfg) {
20683
20741
  priorityOptions: cfg.priorityOptions
20684
20742
  };
20685
20743
  }
20686
- function parseIssueSelector(selector, defaultRepo) {
20687
- const trimmed = selector.trim();
20688
- const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
20689
- if (url) return { repo: url[1], number: Number(url[2]) };
20690
- const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
20691
- if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
20692
- const local = trimmed.match(/^#?(\d+)$/);
20693
- if (local) return { repo: defaultRepo, number: Number(local[1]) };
20694
- throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
20744
+ function parseIssueSelector(selector, defaultRepo, expectedRepo) {
20745
+ const parsed = parseIssueRef(selector, expectedRepo);
20746
+ return { repo: parsed.repo ?? defaultRepo, number: parsed.number };
20695
20747
  }
20696
20748
  function sameRepo(itemRepo, selectorKey) {
20697
20749
  const repo = itemRepo.toLowerCase();
@@ -21016,7 +21068,7 @@ async function moveBoardItem(options, deps = {}) {
21016
21068
  const cfg = resolveBoardConfig(options.config);
21017
21069
  const client = deps.client ?? defaultGitHubClient();
21018
21070
  const currentRepo = await resolveCurrentRepo(options, deps);
21019
- const selector = parseIssueSelector(options.selector, currentRepo);
21071
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21020
21072
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
21021
21073
  const item = lookup.item;
21022
21074
  if (!item) {
@@ -21090,7 +21142,7 @@ async function showBoardItem(options, deps = {}) {
21090
21142
  const cfg = resolveBoardConfig(options.config);
21091
21143
  const client = deps.client ?? defaultGitHubClient();
21092
21144
  const currentRepo = await resolveCurrentRepo(options, deps);
21093
- const selector = parseIssueSelector(options.selector, currentRepo);
21145
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21094
21146
  const { item } = await fetchIssueProjectItem(client, cfg, selector);
21095
21147
  if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
21096
21148
  if (item.contentType === "Issue") {
@@ -21181,6 +21233,15 @@ async function claimOneBoardItem(ctx, selector, options) {
21181
21233
  }
21182
21234
  const assignee = options.assignee ?? "@me";
21183
21235
  const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
21236
+ const holder = {
21237
+ login: assignedLogin,
21238
+ session: ctx.session.session,
21239
+ surface: ctx.session.surface,
21240
+ host: ctx.session.host
21241
+ };
21242
+ let previousHolder;
21243
+ const claimedReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
21244
+ const heldReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
21184
21245
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
21185
21246
  const pre = evaluateClaim(flatItem, assignedLogin);
21186
21247
  if (!pre.ok) throw new Error(pre.reason);
@@ -21191,20 +21252,28 @@ async function claimOneBoardItem(ctx, selector, options) {
21191
21252
  if (!verdict.ok) throw new Error(verdict.reason);
21192
21253
  item = fresh;
21193
21254
  const refuseIfContested = async () => {
21194
- if (options.force) return;
21195
21255
  const contest = await checkLaneContest(client, item, ctx.session);
21196
- if (contest.contested) throw new Error(laneContestMessage(item.ref, contest, "claim"));
21256
+ if (!contest.contested) return;
21257
+ if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
21258
+ previousHolder = {
21259
+ login: assignedLogin,
21260
+ ...contest.marker ? {
21261
+ session: contest.marker.session,
21262
+ surface: contest.marker.surface,
21263
+ host: contest.marker.host
21264
+ } : {}
21265
+ };
21197
21266
  };
21198
21267
  await refuseIfContested();
21199
21268
  if (verdict.alreadyClaimed) {
21200
21269
  if (options.check) {
21201
- return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true, checked: true };
21270
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
21202
21271
  }
21203
21272
  await postClaimMarkerComment(client, item, ctx.session);
21204
- return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true };
21273
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true };
21205
21274
  }
21206
21275
  if (options.check) {
21207
- return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, checked: true };
21276
+ return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
21208
21277
  }
21209
21278
  await refuseIfContested();
21210
21279
  try {
@@ -21218,7 +21287,7 @@ async function claimOneBoardItem(ctx, selector, options) {
21218
21287
  } catch (e) {
21219
21288
  const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
21220
21289
  if (!options.allowPartial) throw new Error(warning);
21221
- return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning };
21290
+ return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning, ...claimedReceipt() };
21222
21291
  }
21223
21292
  return {
21224
21293
  item: {
@@ -21230,13 +21299,14 @@ async function claimOneBoardItem(ctx, selector, options) {
21230
21299
  viewer: report.viewer,
21231
21300
  repo: report.repo,
21232
21301
  status: "In Progress",
21233
- partial: false
21302
+ partial: false,
21303
+ ...claimedReceipt()
21234
21304
  };
21235
21305
  }
21236
21306
  async function claimBoardIssue(options, deps = {}) {
21237
21307
  const cfg = resolveBoardConfig(options.config);
21238
21308
  const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
21239
- const selector = parseIssueSelector(options.selector, collected.repo);
21309
+ const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
21240
21310
  const ctx = await prepareClaimContext(options, [selector], deps, collected);
21241
21311
  return claimOneBoardItem(ctx, selector, options);
21242
21312
  }
@@ -21246,7 +21316,7 @@ async function claimBoardIssues(options, deps = {}) {
21246
21316
  const selectors = [];
21247
21317
  const seen = /* @__PURE__ */ new Set();
21248
21318
  for (const raw of options.selectors) {
21249
- const selector = parseIssueSelector(raw, collected.repo);
21319
+ const selector = parseIssueSelector(raw, collected.repo, options.repo);
21250
21320
  const key = `${selector.repo.toLowerCase()}#${selector.number}`;
21251
21321
  if (seen.has(key)) continue;
21252
21322
  seen.add(key);
@@ -21262,7 +21332,7 @@ async function claimBoardIssues(options, deps = {}) {
21262
21332
  const ref = `${selector.repo}#${selector.number}`;
21263
21333
  try {
21264
21334
  const result = await claimOneBoardItem(ctx, selector, options);
21265
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
21335
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
21266
21336
  } catch (e) {
21267
21337
  results[index] = { ref, claimed: false, reason: e.message };
21268
21338
  }
@@ -21283,7 +21353,7 @@ async function moveBoardIssues(options, deps = {}) {
21283
21353
  const selectors = [];
21284
21354
  const seen = /* @__PURE__ */ new Set();
21285
21355
  for (const raw of options.selectors) {
21286
- const selector = parseIssueSelector(raw, currentRepo);
21356
+ const selector = parseIssueSelector(raw, currentRepo, options.repo);
21287
21357
  const key = `${selector.repo.toLowerCase()}#${selector.number}`;
21288
21358
  if (seen.has(key)) continue;
21289
21359
  seen.add(key);
@@ -21363,7 +21433,7 @@ async function unclaimBoardIssue(options, deps = {}) {
21363
21433
  const cfg = resolveBoardConfig(options.config);
21364
21434
  const client = deps.client ?? defaultGitHubClient();
21365
21435
  const currentRepo = await resolveCurrentRepo(options, deps);
21366
- const selector = parseIssueSelector(options.selector, currentRepo);
21436
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21367
21437
  const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
21368
21438
  if (!item) {
21369
21439
  throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
@@ -22485,6 +22555,20 @@ var REQUIRED_OPTION_HELP = {
22485
22555
  optionDescription(option) {
22486
22556
  const described = Help.prototype.optionDescription.call(this, option);
22487
22557
  return option.mandatory ? `(required) ${described}` : described;
22558
+ },
22559
+ // #5874: resolveHouseShim removes the house token before Commander renders help. Its stock usage
22560
+ // therefore teaches a removed flat alias; render the manifest's canonical house-qualified path instead.
22561
+ commandUsage(command) {
22562
+ const parts = [];
22563
+ for (let node = command; node && node.parent; node = node.parent) {
22564
+ parts.unshift(node.name());
22565
+ }
22566
+ if (!parts.length) return Help.prototype.commandUsage.call(this, command);
22567
+ const flatPath = parts.join(" ");
22568
+ const canonicalPath = canonicalPathFor(flatPath) ?? flatPath;
22569
+ const alias = command.alias();
22570
+ const name = alias ? `${command.name()}|${alias}` : command.name();
22571
+ return `mmi-cli ${canonicalPath.slice(0, -command.name().length)}${name} ${command.usage()}`;
22488
22572
  }
22489
22573
  };
22490
22574
  function classifyTree(command, path2, inherited, hideFromParent) {
@@ -31286,15 +31370,81 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
31286
31370
  }
31287
31371
  return confirmEnqueueOutcome();
31288
31372
  }
31373
+ function cleanupGitArgs(cwd, args) {
31374
+ return cwd ? ["-C", cwd, ...args] : args;
31375
+ }
31289
31376
  async function remoteBranchExists2(branch, options = {}) {
31290
31377
  if (!branch) return void 0;
31291
31378
  try {
31292
- if (options.prune) await execFileP2("git", ["fetch", "origin", "--prune"], { timeout: GIT_TIMEOUT_MS });
31293
- return (await execFileP2("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
31379
+ if (options.prune) await execFileP2("git", cleanupGitArgs(options.cwd, ["fetch", "origin", "--prune"]), { timeout: GIT_TIMEOUT_MS });
31380
+ return (await execFileP2("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", "origin", branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
31294
31381
  } catch {
31295
31382
  return void 0;
31296
31383
  }
31297
31384
  }
31385
+ async function deleteMergedRemoteBranch(options) {
31386
+ const remediation = `git push origin --delete ${options.branch}`;
31387
+ if (!options.branch) {
31388
+ return {
31389
+ branch: options.branch,
31390
+ existedBefore: options.existedBefore,
31391
+ attempted: false,
31392
+ status: "failed",
31393
+ error: "missing PR head branch",
31394
+ remediation
31395
+ };
31396
+ }
31397
+ if (options.existedBefore === false) {
31398
+ const exists2 = await options.branchExists(options.branch);
31399
+ if (exists2 === false) return { branch: options.branch, existedBefore: false, attempted: false, status: "already-gone" };
31400
+ return {
31401
+ branch: options.branch,
31402
+ existedBefore: false,
31403
+ attempted: false,
31404
+ status: "failed",
31405
+ error: exists2 ? `origin reports ${options.branch} after merge` : `could not verify absence of origin/${options.branch}`,
31406
+ remediation
31407
+ };
31408
+ }
31409
+ try {
31410
+ await options.execGit(["push", "origin", "--delete", options.branch]);
31411
+ } catch (e) {
31412
+ const exists2 = await options.branchExists(options.branch);
31413
+ if (exists2 === false) {
31414
+ return {
31415
+ branch: options.branch,
31416
+ existedBefore: options.existedBefore,
31417
+ attempted: true,
31418
+ status: "deleted"
31419
+ };
31420
+ }
31421
+ return {
31422
+ branch: options.branch,
31423
+ existedBefore: options.existedBefore,
31424
+ attempted: true,
31425
+ status: "failed",
31426
+ error: e instanceof Error ? e.message : String(e),
31427
+ remediation
31428
+ };
31429
+ }
31430
+ const exists = await options.branchExists(options.branch);
31431
+ if (exists === false) {
31432
+ return {
31433
+ branch: options.branch,
31434
+ existedBefore: options.existedBefore,
31435
+ attempted: true,
31436
+ status: "deleted"
31437
+ };
31438
+ }
31439
+ return {
31440
+ branch: options.branch,
31441
+ existedBefore: options.existedBefore,
31442
+ attempted: true,
31443
+ status: "failed",
31444
+ error: exists ? `origin still reports ${options.branch} after deletion` : `could not verify deletion of origin/${options.branch}`,
31445
+ remediation
31446
+ };
31447
+ }
31298
31448
 
31299
31449
  // src/worktree-merge-cleanup.ts
31300
31450
  var import_node_fs36 = require("node:fs");
@@ -31484,6 +31634,7 @@ function removeResidueDirectory(wtPath) {
31484
31634
  } catch {
31485
31635
  }
31486
31636
  (0, import_node_fs36.rmSync)(wtPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
31637
+ if ((0, import_node_fs36.existsSync)(wtPath)) return { ok: false, error: `directory remains after removal: ${wtPath}` };
31487
31638
  return { ok: true };
31488
31639
  } catch (e) {
31489
31640
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
@@ -31776,8 +31927,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31776
31927
  if (residue.ok) {
31777
31928
  report.worktree.residue = "swept";
31778
31929
  } else {
31930
+ report.worktree.status = "failed";
31931
+ report.worktree.reason = "residue-remains";
31779
31932
  report.worktree.residue = "left";
31780
31933
  report.worktree.residueError = residue.error;
31934
+ report.worktree.remediation = `Remove-Item -LiteralPath '${wtPath.replace(/'/g, "''")}' -Recurse -Force`;
31781
31935
  }
31782
31936
  }
31783
31937
  try {
@@ -31808,7 +31962,7 @@ function renderPrMergeCleanupLines(cleanup) {
31808
31962
  lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
31809
31963
  }
31810
31964
  if (wt.residue === "left") {
31811
- lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 delete by hand: ${wt.residueError ?? wt.path}`);
31965
+ lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
31812
31966
  }
31813
31967
  if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
31814
31968
  lines.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
@@ -31863,8 +32017,24 @@ function registerBoardCommands(program3) {
31863
32017
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
31864
32018
  }
31865
32019
  }
31866
- function checkVerdict(ref, alreadyClaimed) {
31867
- return alreadyClaimed ? `Check ${ref}: claimed and In Progress, no live contest - claim would renew the lease (nothing written)` : `Check ${ref}: free - claim would proceed (nothing written)`;
32020
+ function formatClaimHolder(holder) {
32021
+ const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
32022
+ return `@${holder.login}${lane}`;
32023
+ }
32024
+ function claimVerdict(ref, result) {
32025
+ const holder = formatClaimHolder(result.holder);
32026
+ const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
32027
+ if (result.checked) {
32028
+ if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
32029
+ if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
32030
+ return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
32031
+ }
32032
+ if (result.partial) {
32033
+ return result.outcome === "took-over" ? `Partially took over ${ref} from ${previousHolder}: ${result.warning}` : `Partially claimed ${ref} for ${holder}: ${result.warning}`;
32034
+ }
32035
+ if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
32036
+ if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
32037
+ return `Claimed ${ref} for ${holder} - In Progress`;
31868
32038
  }
31869
32039
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
31870
32040
  board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
@@ -31886,7 +32056,7 @@ function registerBoardCommands(program3) {
31886
32056
  });
31887
32057
  if (!result.checked) invalidateStatuslineBoardCache();
31888
32058
  if (o.json) return console.log(JSON.stringify(result));
31889
- console.log(result.checked ? checkVerdict(result.item.ref, result.alreadyClaimed) : result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
32059
+ console.log(claimVerdict(result.item.ref, result));
31890
32060
  } catch (e) {
31891
32061
  if (refuseRateLimited(e, o.json)) return;
31892
32062
  return failGraceful(`board claim failed: ${e.message}`);
@@ -31908,7 +32078,7 @@ function registerBoardCommands(program3) {
31908
32078
  console.log(JSON.stringify(bulk.results));
31909
32079
  } else {
31910
32080
  for (const result of bulk.results) {
31911
- console.log(result.claimed ? result.checked ? checkVerdict(result.ref, result.alreadyClaimed) : result.partial ? `Partially claimed ${result.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.ref} - In Progress (no change)` : `Claimed ${result.ref} - In Progress` : `Skipped ${result.ref}: ${result.reason}`);
32081
+ console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
31912
32082
  }
31913
32083
  }
31914
32084
  if (bulk.failed > 0) process.exitCode = 1;
@@ -32004,7 +32174,7 @@ function registerBoardCommands(program3) {
32004
32174
  }
32005
32175
  try {
32006
32176
  const defaultRepo = await resolveRepo(o.repo) ?? "";
32007
- const selector = parseIssueSelector(issueRef, defaultRepo);
32177
+ const selector = parseIssueSelector(issueRef, defaultRepo, o.repo);
32008
32178
  if (!selector.repo) {
32009
32179
  return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
32010
32180
  }
@@ -32249,7 +32419,7 @@ async function fetchRestClosingGuardPayload(prNumber, repo, gh = defaultGhApi) {
32249
32419
  return { pr: pr2, commits };
32250
32420
  }
32251
32421
  async function fetchHeadCheckRuns(headSha, repo, gh) {
32252
- const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id}"]);
32422
+ const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url}"]);
32253
32423
  return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
32254
32424
  }
32255
32425
  async function fetchHeadCheckEntries(headSha, repo, gh) {
@@ -32321,6 +32491,29 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
32321
32491
  "cancelled",
32322
32492
  "startup_failure"
32323
32493
  ]);
32494
+ var BARE_CHECKS_FAILURE_MESSAGE = "GitHub reported a checks failure but exposed no per-check data.";
32495
+ function actionRunIdFromDetailsUrl(detailsUrl) {
32496
+ const match = detailsUrl?.match(/\/actions\/runs\/(\d+)(?:\/|$)/);
32497
+ if (!match) return void 0;
32498
+ const runId = Number(match[1]);
32499
+ return Number.isSafeInteger(runId) ? runId : void 0;
32500
+ }
32501
+ function failedCheckReceipt(run, repo) {
32502
+ const name = run.name ?? `check-run ${run.id ?? "unknown"}`;
32503
+ const conclusion = run.conclusion ?? "unknown";
32504
+ const detailsUrl = typeof run.details_url === "string" && run.details_url ? run.details_url : void 0;
32505
+ const runId = actionRunIdFromDetailsUrl(detailsUrl);
32506
+ return {
32507
+ name,
32508
+ conclusion,
32509
+ ...detailsUrl ? { detailsUrl } : {},
32510
+ ...runId !== void 0 ? {
32511
+ runId,
32512
+ runUrl: `https://github.com/${repo}/actions/runs/${runId}`,
32513
+ diagnosticCommand: `gh run view ${runId} --log-failed --repo ${repo}`
32514
+ } : {}
32515
+ };
32516
+ }
32324
32517
  function isErrorAnnotation(a) {
32325
32518
  const level = a.annotation_level?.toLowerCase();
32326
32519
  return level === "failure" || level === "error";
@@ -32353,6 +32546,7 @@ function classifyFailedChecks(failing) {
32353
32546
  cause: "runner-infra",
32354
32547
  infraFailures,
32355
32548
  otherFailures,
32549
+ failedChecks: [],
32356
32550
  reason: `${infraFailures.length} check(s) failed on RUNNER INFRASTRUCTURE, not your diff: ${infraFailures.join(", ")}. No test failed \u2014 the shared runner hit a wall-clock budget kill, ran out of disk, was missing a toolchain, could not acquire an mmi-live lane, or Actions returned Service Unavailable before checkout (the check annotation / conclusion names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
32357
32551
  };
32358
32552
  }
@@ -32360,6 +32554,7 @@ function classifyFailedChecks(failing) {
32360
32554
  cause: "checks-failure",
32361
32555
  infraFailures,
32362
32556
  otherFailures,
32557
+ failedChecks: [],
32363
32558
  reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
32364
32559
  };
32365
32560
  }
@@ -32380,6 +32575,7 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
32380
32575
  cause: "stale-head",
32381
32576
  infraFailures: [],
32382
32577
  otherFailures: [],
32578
+ failedChecks: [],
32383
32579
  reason: `the PR head (${snapshot.headSha.slice(0, 7)}) is BEHIND the branch tip (${tip.sha.slice(0, 7)}) \u2014 GitHub likely dropped a \`synchronize\` event, so the reported red is a superseded commit and NO run exists for your current push. This is not your diff failing. Re-push (an empty commit is enough) or close and reopen the PR to re-sync the head and trigger a fresh run.`
32384
32580
  }
32385
32581
  };
@@ -32392,15 +32588,20 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
32392
32588
  return { state: "failed", error: `check-runs read failed for ${snapshot.headSha.slice(0, 7)} on ${repo}: ${readErrorText(e)}` };
32393
32589
  }
32394
32590
  if (!failing.length) return { state: "absent", reason: "no failing check-run on the current head to diagnose" };
32591
+ const failedChecks = failing.map((run) => failedCheckReceipt(run, repo));
32395
32592
  try {
32396
32593
  const annotated = await Promise.all(failing.map(async (run) => ({
32397
32594
  name: run.name ?? `check-run ${run.id}`,
32398
32595
  conclusion: run.conclusion ?? null,
32399
32596
  annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
32400
32597
  })));
32401
- return { state: "ok", diagnosis: classifyFailedChecks(annotated) };
32598
+ return { state: "ok", diagnosis: { ...classifyFailedChecks(annotated), failedChecks } };
32402
32599
  } catch (e) {
32403
- return { state: "failed", error: `annotations read failed for a failing check-run on ${repo}: ${readErrorText(e)}` };
32600
+ return {
32601
+ state: "failed",
32602
+ error: `annotations read failed for a failing check-run on ${repo}: ${readErrorText(e)}`,
32603
+ failedChecks
32604
+ };
32404
32605
  }
32405
32606
  }
32406
32607
  function isNotFoundError2(e) {
@@ -33240,6 +33441,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33240
33441
  (opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
33241
33442
  ).action(async (ref, o) => {
33242
33443
  try {
33444
+ parseIssueRef(ref, o.repo);
33243
33445
  const defaultRepo = await resolveRepo(o.repo);
33244
33446
  const result = await editIssue(defaultGitHubClient(), {
33245
33447
  ref,
@@ -33284,6 +33486,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33284
33486
  ).action(async (ref, o) => {
33285
33487
  const parsed = parseCloseReason(o.reason);
33286
33488
  try {
33489
+ parseIssueRef(ref, o.repo);
33287
33490
  const defaultRepo = await resolveRepo(o.repo);
33288
33491
  const result = await closeIssue(defaultGitHubClient(), {
33289
33492
  ref,
@@ -33303,6 +33506,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33303
33506
  (_opts, args) => ({ command: "issue reopen", ref: args[0] })
33304
33507
  ).action(async (ref, o) => {
33305
33508
  try {
33509
+ parseIssueRef(ref, o.repo);
33306
33510
  const defaultRepo = await resolveRepo(o.repo);
33307
33511
  const result = await reopenIssue(defaultGitHubClient(), ref, defaultRepo);
33308
33512
  console.log(JSON.stringify(result));
@@ -33315,6 +33519,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33315
33519
  (_opts, args) => ({ command: "issue assign", ref: args[0], login: args[1] })
33316
33520
  ).action(async (ref, login, o) => {
33317
33521
  try {
33522
+ parseIssueRef(ref, o.repo);
33318
33523
  const defaultRepo = await resolveRepo(o.repo);
33319
33524
  const result = await assignIssue(defaultGitHubClient(), ref, login, defaultRepo);
33320
33525
  console.log(JSON.stringify(result));
@@ -33327,6 +33532,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33327
33532
  (_opts, args) => ({ command: "issue unassign", ref: args[0], login: args[1] })
33328
33533
  ).action(async (ref, login, o) => {
33329
33534
  try {
33535
+ parseIssueRef(ref, o.repo);
33330
33536
  const defaultRepo = await resolveRepo(o.repo);
33331
33537
  const result = await unassignIssue(defaultGitHubClient(), ref, login, defaultRepo);
33332
33538
  console.log(JSON.stringify(result));
@@ -33339,6 +33545,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33339
33545
  (opts, args) => ({ command: "issue relocate", ref: args[0], to: opts.to })
33340
33546
  ).action(async (ref, o) => {
33341
33547
  try {
33548
+ parseIssueRef(ref, o.repo);
33342
33549
  const defaultRepo = await resolveRepo(o.repo);
33343
33550
  const result = await relocateIssue(defaultGitHubClient(), ref, o.to, defaultRepo);
33344
33551
  console.log(JSON.stringify(result));
@@ -33350,8 +33557,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33350
33557
  issue2.command("unlink-child <parent> <child>").description("unlink a child issue from its parent (inverse of link-child) and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)"),
33351
33558
  (_opts, args) => ({ command: "issue unlink-child", parent: args[0], child: args[1] })
33352
33559
  ).action(async (parentRef, childRef, o) => {
33353
- const defaultRepo = await resolveRepo(o.repo);
33354
33560
  try {
33561
+ parseIssueRef(parentRef, o.repo);
33562
+ parseIssueRef(childRef, o.repo);
33563
+ const defaultRepo = await resolveRepo(o.repo);
33355
33564
  const result = await unlinkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
33356
33565
  console.log(JSON.stringify(result));
33357
33566
  } catch (e) {
@@ -34985,7 +35194,7 @@ function registerLearningPickupCommand(program3) {
34985
35194
  let selector;
34986
35195
  try {
34987
35196
  if (ref) {
34988
- selector = parseIssueSelector(ref, defaultRepo);
35197
+ selector = parseIssueSelector(ref, defaultRepo, o.repo);
34989
35198
  } else if (o.number) {
34990
35199
  selector = { repo: defaultRepo, number: Number.parseInt(o.number, 10) };
34991
35200
  if (!Number.isFinite(selector.number)) throw new Error("invalid --number");
@@ -39659,7 +39868,7 @@ withExamples(mutating(
39659
39868
  repo: targetRepo2,
39660
39869
  labels: extraLabels.length ? extraLabels : void 0
39661
39870
  });
39662
- if (o.parent !== void 0) parseIssueRef(o.parent);
39871
+ if (o.parent !== void 0) parseIssueRef(o.parent, o.repo);
39663
39872
  } catch (e) {
39664
39873
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
39665
39874
  }
@@ -39741,10 +39950,15 @@ async function readParentField(number, repo) {
39741
39950
  }
39742
39951
  return resolveParentField(payload);
39743
39952
  }
39744
- issue.command("view <number>").aliases(["show", "get"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show` and `get` alias view (#5483/#5706)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
39745
- const n = Number(number);
39746
- if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
39747
- const repo = await resolveRepo(o.repo);
39953
+ issue.command("view <ref>").aliases(["show", "get"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show` and `get` alias view (#5483/#5706)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
39954
+ let parsed;
39955
+ try {
39956
+ parsed = parseIssueRef(ref, o.repo);
39957
+ } catch (e) {
39958
+ return fail(`issue view: ${e.message}`);
39959
+ }
39960
+ const n = parsed.number;
39961
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
39748
39962
  if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
39749
39963
  const fields = normalizeIssueViewJsonFields(o.json);
39750
39964
  try {
@@ -39804,12 +40018,14 @@ issue.command("discover-related").description("find related issues for an existi
39804
40018
  jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
39805
40019
  const defaultRepo = await resolveRepo(o.repo);
39806
40020
  try {
40021
+ parseIssueRef(parentRef, o.repo);
40022
+ parseIssueRef(childRef, o.repo);
39807
40023
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
39808
40024
  console.log(JSON.stringify(result));
39809
40025
  } catch (e) {
39810
40026
  let conflict;
39811
40027
  try {
39812
- const child2 = parseIssueRef(childRef);
40028
+ const child2 = parseIssueRef(childRef, o.repo);
39813
40029
  const childRepo = child2.repo ?? defaultRepo;
39814
40030
  if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
39815
40031
  } catch {
@@ -39824,7 +40040,7 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
39824
40040
  jsonParity(issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
39825
40041
  let parsed;
39826
40042
  try {
39827
- parsed = parseIssueRef(ref);
40043
+ parsed = parseIssueRef(ref, o.repo);
39828
40044
  } catch (e) {
39829
40045
  return fail(`issue comment: ${e.message}`);
39830
40046
  }
@@ -39847,7 +40063,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
39847
40063
  jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2014 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
39848
40064
  let parsed;
39849
40065
  try {
39850
- parsed = parseIssueRef(ref);
40066
+ parsed = parseIssueRef(ref, o.repo);
39851
40067
  } catch (e) {
39852
40068
  return fail(`issue check: ${e.message}`);
39853
40069
  }
@@ -40034,10 +40250,15 @@ withExamples(pr.command("create").description("create a PR and print {number,url
40034
40250
  "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
40035
40251
  "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
40036
40252
  ]);
40037
- pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (number, o) => {
40038
- const n = Number(number);
40039
- if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
40040
- const repo = await resolveRepo(o.repo);
40253
+ pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (ref, o) => {
40254
+ let parsed;
40255
+ try {
40256
+ parsed = parseIssueRef(ref, o.repo);
40257
+ } catch (e) {
40258
+ return fail(`pr view: ${e.message}`);
40259
+ }
40260
+ const n = parsed.number;
40261
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
40041
40262
  if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
40042
40263
  const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
40043
40264
  const effective = resolvePrViewFields(o.json, defaultPrFields);
@@ -40146,13 +40367,44 @@ async function waitLoopCorePool(label) {
40146
40367
  async function waitLoopDiagnosis(label, prNumber, repo) {
40147
40368
  const read = await diagnoseFailedRestChecks(prNumber, repo);
40148
40369
  if (read.state === "ok") return read.diagnosis;
40370
+ if (read.state === "absent") {
40371
+ return {
40372
+ cause: "checks-failure",
40373
+ infraFailures: [],
40374
+ otherFailures: [],
40375
+ failedChecks: [],
40376
+ checksFailureDetail: BARE_CHECKS_FAILURE_MESSAGE,
40377
+ reason: BARE_CHECKS_FAILURE_MESSAGE
40378
+ };
40379
+ }
40149
40380
  if (read.state === "failed") {
40150
40381
  console.warn(
40151
40382
  `${label}: CI failure diagnosis read FAILED (${read.error}) \u2014 the red verdict stands UNDIAGNOSED; this is not evidence the failure is a real test failure. Re-read it with \`gh pr checks ${prNumber} --repo ${repo}\`.`
40152
40383
  );
40384
+ return {
40385
+ cause: "checks-failure",
40386
+ infraFailures: [],
40387
+ otherFailures: [],
40388
+ failedChecks: read.failedChecks ?? [],
40389
+ reason: "checks failure diagnosis unavailable"
40390
+ };
40153
40391
  }
40154
40392
  return null;
40155
40393
  }
40394
+ function failedChecksReceiptLines(context, wait) {
40395
+ if (wait.detail !== "checks-failure") return [];
40396
+ const lines = [];
40397
+ for (const check of wait.failedChecks ?? []) {
40398
+ lines.push(`${context}: failed check: ${check.name} (${check.conclusion})${check.detailsUrl ? ` \u2014 ${check.detailsUrl}` : ""}`);
40399
+ if (check.diagnosticCommand) lines.push(`${context}: next diagnostic: ${check.diagnosticCommand}`);
40400
+ }
40401
+ if (!wait.failedChecks?.length && wait.checksFailureDetail) lines.push(`${context}: ${wait.checksFailureDetail}`);
40402
+ if (!wait.failedChecks?.some((check) => check.diagnosticCommand) && wait.diagnosticCommand) {
40403
+ lines.push(`${context}: next diagnostic: ${wait.diagnosticCommand}`);
40404
+ }
40405
+ if (wait.checksUrl) lines.push(`${context}: PR checks: ${wait.checksUrl}`);
40406
+ return lines;
40407
+ }
40156
40408
  async function waitLoopHeadWorkflowRunCount(label, prNumber, repo) {
40157
40409
  let snapshot;
40158
40410
  try {
@@ -40206,6 +40458,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
40206
40458
  // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
40207
40459
  // reading as "your tests failed". One call per failing run, only at the verdict.
40208
40460
  diagnoseFailure: () => waitLoopDiagnosis("pr checks-wait", number, repo),
40461
+ checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
40462
+ checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
40209
40463
  // #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
40210
40464
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
40211
40465
  baseBranch,
@@ -40232,21 +40486,21 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
40232
40486
  printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
40233
40487
  } else if (result.detail === "zero-runs") {
40234
40488
  printLine(`pr checks-wait: failure (zero workflow runs delivered, NOT pending checks) \u2014 ${result.reason}`);
40235
- } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
40489
+ } else {
40490
+ printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
40491
+ for (const line of failedChecksReceiptLines("pr checks-wait", result)) printLine(line);
40492
+ }
40236
40493
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
40237
40494
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
40238
40495
  });
40239
40496
  pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").action(async (number, o) => {
40240
- const refMatch = /^(?:([^/]+\/[^/]+))?#(\d+)$/.exec(number.trim());
40241
- if (refMatch) {
40242
- number = refMatch[2];
40243
- if (refMatch[1]) {
40244
- if (o.repo && o.repo !== refMatch[1]) {
40245
- console.error(`pr land: --repo ${o.repo} conflicts with ref repo ${refMatch[1]}`);
40246
- process.exitCode = 1;
40247
- return;
40248
- }
40249
- o.repo = refMatch[1];
40497
+ if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
40498
+ try {
40499
+ const parsed = parseIssueRef(number, o.repo);
40500
+ number = String(parsed.number);
40501
+ if (parsed.repo) o.repo = parsed.repo;
40502
+ } catch (e) {
40503
+ return fail(`pr land: ${e.message}`);
40250
40504
  }
40251
40505
  }
40252
40506
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
@@ -40308,6 +40562,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
40308
40562
  // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
40309
40563
  // then read its own wall-clock kill as a broken diff.
40310
40564
  diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
40565
+ checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
40566
+ checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
40311
40567
  // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
40312
40568
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
40313
40569
  baseBranch: "development",
@@ -40490,6 +40746,8 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40490
40746
  pollMergeable: () => pollRestPrMergeable(number, repo),
40491
40747
  pollRateLimit: () => waitLoopCorePool("pr merge --wait"),
40492
40748
  diagnoseFailure: () => waitLoopDiagnosis("pr merge --wait", number, repo),
40749
+ checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
40750
+ checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
40493
40751
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
40494
40752
  baseBranch,
40495
40753
  sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
@@ -40498,7 +40756,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40498
40756
  progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
40499
40757
  });
40500
40758
  if (wait.status !== "success" && wait.status !== "skipped") {
40759
+ if (wait.detail === "checks-failure") {
40760
+ console.log(JSON.stringify({
40761
+ mergeStatus: "not-merged",
40762
+ reason: "checks-failure",
40763
+ pr: number,
40764
+ repo,
40765
+ failedChecks: wait.failedChecks ?? [],
40766
+ ...wait.checksUrl ? { checksUrl: wait.checksUrl } : {},
40767
+ ...wait.diagnosticCommand ? { diagnosticCommand: wait.diagnosticCommand } : {},
40768
+ ...wait.checksFailureDetail ? { checksFailureDetail: wait.checksFailureDetail } : {}
40769
+ }));
40770
+ }
40501
40771
  console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
40772
+ for (const line of failedChecksReceiptLines("pr merge", wait)) console.warn(line);
40502
40773
  process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
40503
40774
  return;
40504
40775
  }
@@ -40610,7 +40881,6 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40610
40881
  process.exitCode = 1;
40611
40882
  return;
40612
40883
  }
40613
- const remoteBranch = { branch: headRef, existedBefore: remoteBefore, attempted: false, reason: remoteNotAttemptedReason };
40614
40884
  const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
40615
40885
  let localCleanup;
40616
40886
  try {
@@ -40623,7 +40893,10 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40623
40893
  gcAcknowledged: o.gc,
40624
40894
  expectedHeadOid: headRefOid,
40625
40895
  pathExists: (p) => (0, import_node_fs43.existsSync)(p),
40626
- execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
40896
+ // #5899: pin cleanup git calls to the main checkout the task worktree this process may be
40897
+ // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
40898
+ // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
40899
+ execGit: async (args) => (await execFileP2("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
40627
40900
  });
40628
40901
  } catch (e) {
40629
40902
  localCleanup = {
@@ -40635,6 +40908,30 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40635
40908
  }
40636
40909
  };
40637
40910
  }
40911
+ const remoteBranch = await deleteMergedRemoteBranch({
40912
+ branch: headRef,
40913
+ existedBefore: remoteBefore,
40914
+ // #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
40915
+ // the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
40916
+ execGit: async (args) => (await execFileP2("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
40917
+ branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
40918
+ });
40919
+ const worktreePartial = localCleanup?.worktree?.path && localCleanup.worktree.status !== "removed" ? {
40920
+ kind: "worktree-directory",
40921
+ path: localCleanup.worktree.path,
40922
+ reason: localCleanup.worktree.reason ?? localCleanup.worktree.status,
40923
+ error: localCleanup.worktree.error ?? localCleanup.worktree.residueError,
40924
+ remediation: localCleanup.worktree.remediation ?? `Remove-Item -LiteralPath '${localCleanup.worktree.path?.replace(/'/g, "''")}' -Recurse -Force`
40925
+ } : void 0;
40926
+ const partialCleanup = [
40927
+ ...worktreePartial ? [worktreePartial] : [],
40928
+ ...remoteBranch.status === "failed" ? [{
40929
+ kind: "remote-branch",
40930
+ branch: remoteBranch.branch,
40931
+ error: remoteBranch.error,
40932
+ remediation: remoteBranch.remediation
40933
+ }] : []
40934
+ ];
40638
40935
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
40639
40936
  const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
40640
40937
  const recovery = repoForPostCleanup ? buildPostMergeReconRecovery({
@@ -40672,12 +40969,13 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40672
40969
  observedMethod
40673
40970
  });
40674
40971
  console.log(JSON.stringify({
40675
- mergeStatus: "merged",
40972
+ mergeStatus: partialCleanup.length ? "partial-cleanup" : "merged",
40676
40973
  merged: number,
40677
40974
  branch: headRef,
40678
40975
  ...methodField ? { method: methodField } : {},
40679
40976
  remoteBranch,
40680
40977
  housekeeping,
40978
+ ...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
40681
40979
  ...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
40682
40980
  ...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
40683
40981
  // `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
@@ -40701,7 +40999,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40701
40999
  })) {
40702
41000
  console.error(line);
40703
41001
  }
40704
- process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
41002
+ process.exitCode = (partialCleanup.length ? 1 : void 0) ?? prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
40705
41003
  });
40706
41004
  registerQueryCommands(program2);
40707
41005
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
@@ -40844,6 +41142,52 @@ function renderAlignment(label, alignment) {
40844
41142
  }
40845
41143
  return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
40846
41144
  }
41145
+ function followUpLegStatus(status) {
41146
+ return status === "failure" ? "failed" : status;
41147
+ }
41148
+ function releaseFollowUpLegs(result, projectInfoSync) {
41149
+ const legs = [
41150
+ {
41151
+ leg: "project-info",
41152
+ status: projectInfoSync && "error" in projectInfoSync ? "failed" : "success",
41153
+ ...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
41154
+ }
41155
+ ];
41156
+ const runs = result.workflowRuns ?? [];
41157
+ if (runs.length > 0) {
41158
+ legs.push(...runs.map((run) => ({
41159
+ leg: run.workflow,
41160
+ status: followUpLegStatus(run.conclusion),
41161
+ ...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" ? result.dispatch : `${run.workflow} reported failure` } : {}
41162
+ })));
41163
+ } else if (result.deployStatus !== "success") {
41164
+ legs.push({
41165
+ leg: "deploy",
41166
+ status: followUpLegStatus(result.deployStatus),
41167
+ ...result.deployStatus === "failure" ? { error: result.dispatch } : {}
41168
+ });
41169
+ }
41170
+ if (result.rcRetirement) {
41171
+ legs.push({
41172
+ leg: "rc-retirement",
41173
+ status: result.rcRetirement === "failed" ? "failed" : "success",
41174
+ ...result.rcRetirement === "failed" ? { error: result.rcRetirementNote ?? "rc retirement failed" } : {}
41175
+ });
41176
+ }
41177
+ if (result.devRollForward) {
41178
+ legs.push({
41179
+ leg: "development-alignment",
41180
+ status: result.devRollForward.status === "pr-pending" ? "pending" : "success"
41181
+ });
41182
+ }
41183
+ if (result.rcAlignment) {
41184
+ legs.push({
41185
+ leg: "rc-alignment",
41186
+ status: result.rcAlignment.status === "pr-pending" ? "pending" : "success"
41187
+ });
41188
+ }
41189
+ return legs;
41190
+ }
40847
41191
  var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
40848
41192
  async function runPostReleaseJervDoctor(repo) {
40849
41193
  if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
@@ -40884,7 +41228,11 @@ function renderTrainApply(commandName, r) {
40884
41228
  if (r.releaseVerdict) {
40885
41229
  const v = r.releaseVerdict;
40886
41230
  const verdict = v.followUpStatus === "failed" ? "PROMOTED, FOLLOW-UP FAILED" : v.followUpStatus === "pending" ? "PROMOTED, FOLLOW-UP PENDING" : "SUCCEEDED";
40887
- return `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`;
41231
+ const failedLegs = v.legs.filter((leg) => leg.status === "failed");
41232
+ return [
41233
+ `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`,
41234
+ ...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`)
41235
+ ].join("\n");
40888
41236
  }
40889
41237
  return base;
40890
41238
  }
@@ -41055,7 +41403,8 @@ for (const commandName of ["rcand", "release"]) {
41055
41403
  followUpStatus,
41056
41404
  promoted: result.promoted,
41057
41405
  tag: result.tag,
41058
- releaseUrl: result.release?.url ?? null
41406
+ releaseUrl: result.release?.url ?? null,
41407
+ legs: releaseFollowUpLegs(result, projectInfoSync)
41059
41408
  } : void 0;
41060
41409
  const reported = {
41061
41410
  ...result,
@@ -41105,7 +41454,7 @@ function renderHotfixRelease(r) {
41105
41454
  ` - ${r.tagNote}`,
41106
41455
  ` - ${r.releaseNote}`,
41107
41456
  ` - deploy: ${r.deployNote}`,
41108
- ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
41457
+ ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
41109
41458
  ` - ${r.verifyNote}`,
41110
41459
  ...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
41111
41460
  ` - fold: ${r.foldNote}`,
@@ -41116,7 +41465,7 @@ function renderHotfixStatus(r) {
41116
41465
  return [
41117
41466
  `mmi-cli devops hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
41118
41467
  ` - branch: ${r.branchExists ? "pushed" : "absent"} \u2014 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \u2014 tag: ${r.tagPushed ? "pushed" : "absent"} \u2014 Release: ${r.releaseExists ? "exists" : "absent"}`,
41119
- ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
41468
+ ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
41120
41469
  ` - npm @mutmutco/cli: ${r.npmVersion}`,
41121
41470
  ` - next: ${r.next}`,
41122
41471
  ...r.warnings.map((w) => ` - warning: ${w}`)
@@ -41127,13 +41476,25 @@ function hotfixRunOutcome(conclusion) {
41127
41476
  if (conclusion === "failure") return "failure";
41128
41477
  return "unresolved";
41129
41478
  }
41479
+ function hotfixFollowUpLegs(runs, foldPort, foldNote) {
41480
+ const legs = runs.map((run) => ({
41481
+ leg: run.workflow,
41482
+ status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
41483
+ ...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
41484
+ }));
41485
+ if (foldPort === "failure") {
41486
+ legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
41487
+ }
41488
+ return legs;
41489
+ }
41130
41490
  async function runHotfixSub(sub, body, json, render) {
41131
41491
  try {
41132
41492
  await requireFreshTrainCli("hotfix");
41133
41493
  const result = await body();
41134
- printLine(json ? JSON.stringify(result, null, 2) : render(result));
41135
41494
  const runs = result.runs;
41136
41495
  const foldPort = result.foldStatus ?? "ok";
41496
+ const legs = runs ? hotfixFollowUpLegs(runs, foldPort, result.foldNote) : void 0;
41497
+ printLine(json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result));
41137
41498
  if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
41138
41499
  projectInfo: "ok",
41139
41500
  deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.2.5",
3
+ "version": "4.2.7",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",