@mutmutco/cli 4.2.5 → 4.2.6

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 +434 -107
  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.6",
11834
+ tag: "v4.2.6",
11835
+ commit: "608eebe09477",
11836
+ npm: "@mutmutco/cli@4.2.6"
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.6"
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.6 and redeploy the Hub Lambda from tag v4.2.6 (608eebe09477); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11860
+ v3Target: "v4.2.6 (@mutmutco/cli@4.2.6, tag commit 608eebe09477 \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,10 +15873,16 @@ 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
+ }
15853
15883
  function resolveExpectedCiNpmFromWorkflows(gateFiles, allFiles) {
15884
+ const declared = gateFiles?.flatMap((file) => parseGateNpmVersionPins(file.body)) ?? [];
15885
+ if (declared.length) return declared[0];
15854
15886
  const fromGate = resolveGreenGateNpmFromGateWorkflows(gateFiles);
15855
15887
  if (fromGate) return fromGate;
15856
15888
  const files = allFiles ?? gateFiles;
@@ -20683,15 +20715,9 @@ function resolveBoardConfig(cfg) {
20683
20715
  priorityOptions: cfg.priorityOptions
20684
20716
  };
20685
20717
  }
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`);
20718
+ function parseIssueSelector(selector, defaultRepo, expectedRepo) {
20719
+ const parsed = parseIssueRef(selector, expectedRepo);
20720
+ return { repo: parsed.repo ?? defaultRepo, number: parsed.number };
20695
20721
  }
20696
20722
  function sameRepo(itemRepo, selectorKey) {
20697
20723
  const repo = itemRepo.toLowerCase();
@@ -21016,7 +21042,7 @@ async function moveBoardItem(options, deps = {}) {
21016
21042
  const cfg = resolveBoardConfig(options.config);
21017
21043
  const client = deps.client ?? defaultGitHubClient();
21018
21044
  const currentRepo = await resolveCurrentRepo(options, deps);
21019
- const selector = parseIssueSelector(options.selector, currentRepo);
21045
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21020
21046
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
21021
21047
  const item = lookup.item;
21022
21048
  if (!item) {
@@ -21090,7 +21116,7 @@ async function showBoardItem(options, deps = {}) {
21090
21116
  const cfg = resolveBoardConfig(options.config);
21091
21117
  const client = deps.client ?? defaultGitHubClient();
21092
21118
  const currentRepo = await resolveCurrentRepo(options, deps);
21093
- const selector = parseIssueSelector(options.selector, currentRepo);
21119
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21094
21120
  const { item } = await fetchIssueProjectItem(client, cfg, selector);
21095
21121
  if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
21096
21122
  if (item.contentType === "Issue") {
@@ -21181,6 +21207,15 @@ async function claimOneBoardItem(ctx, selector, options) {
21181
21207
  }
21182
21208
  const assignee = options.assignee ?? "@me";
21183
21209
  const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
21210
+ const holder = {
21211
+ login: assignedLogin,
21212
+ session: ctx.session.session,
21213
+ surface: ctx.session.surface,
21214
+ host: ctx.session.host
21215
+ };
21216
+ let previousHolder;
21217
+ const claimedReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
21218
+ const heldReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
21184
21219
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
21185
21220
  const pre = evaluateClaim(flatItem, assignedLogin);
21186
21221
  if (!pre.ok) throw new Error(pre.reason);
@@ -21191,20 +21226,28 @@ async function claimOneBoardItem(ctx, selector, options) {
21191
21226
  if (!verdict.ok) throw new Error(verdict.reason);
21192
21227
  item = fresh;
21193
21228
  const refuseIfContested = async () => {
21194
- if (options.force) return;
21195
21229
  const contest = await checkLaneContest(client, item, ctx.session);
21196
- if (contest.contested) throw new Error(laneContestMessage(item.ref, contest, "claim"));
21230
+ if (!contest.contested) return;
21231
+ if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
21232
+ previousHolder = {
21233
+ login: assignedLogin,
21234
+ ...contest.marker ? {
21235
+ session: contest.marker.session,
21236
+ surface: contest.marker.surface,
21237
+ host: contest.marker.host
21238
+ } : {}
21239
+ };
21197
21240
  };
21198
21241
  await refuseIfContested();
21199
21242
  if (verdict.alreadyClaimed) {
21200
21243
  if (options.check) {
21201
- return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true, checked: true };
21244
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
21202
21245
  }
21203
21246
  await postClaimMarkerComment(client, item, ctx.session);
21204
- return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true };
21247
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true };
21205
21248
  }
21206
21249
  if (options.check) {
21207
- return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, checked: true };
21250
+ return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
21208
21251
  }
21209
21252
  await refuseIfContested();
21210
21253
  try {
@@ -21218,7 +21261,7 @@ async function claimOneBoardItem(ctx, selector, options) {
21218
21261
  } catch (e) {
21219
21262
  const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
21220
21263
  if (!options.allowPartial) throw new Error(warning);
21221
- return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning };
21264
+ return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning, ...claimedReceipt() };
21222
21265
  }
21223
21266
  return {
21224
21267
  item: {
@@ -21230,13 +21273,14 @@ async function claimOneBoardItem(ctx, selector, options) {
21230
21273
  viewer: report.viewer,
21231
21274
  repo: report.repo,
21232
21275
  status: "In Progress",
21233
- partial: false
21276
+ partial: false,
21277
+ ...claimedReceipt()
21234
21278
  };
21235
21279
  }
21236
21280
  async function claimBoardIssue(options, deps = {}) {
21237
21281
  const cfg = resolveBoardConfig(options.config);
21238
21282
  const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
21239
- const selector = parseIssueSelector(options.selector, collected.repo);
21283
+ const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
21240
21284
  const ctx = await prepareClaimContext(options, [selector], deps, collected);
21241
21285
  return claimOneBoardItem(ctx, selector, options);
21242
21286
  }
@@ -21246,7 +21290,7 @@ async function claimBoardIssues(options, deps = {}) {
21246
21290
  const selectors = [];
21247
21291
  const seen = /* @__PURE__ */ new Set();
21248
21292
  for (const raw of options.selectors) {
21249
- const selector = parseIssueSelector(raw, collected.repo);
21293
+ const selector = parseIssueSelector(raw, collected.repo, options.repo);
21250
21294
  const key = `${selector.repo.toLowerCase()}#${selector.number}`;
21251
21295
  if (seen.has(key)) continue;
21252
21296
  seen.add(key);
@@ -21262,7 +21306,7 @@ async function claimBoardIssues(options, deps = {}) {
21262
21306
  const ref = `${selector.repo}#${selector.number}`;
21263
21307
  try {
21264
21308
  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 };
21309
+ 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
21310
  } catch (e) {
21267
21311
  results[index] = { ref, claimed: false, reason: e.message };
21268
21312
  }
@@ -21283,7 +21327,7 @@ async function moveBoardIssues(options, deps = {}) {
21283
21327
  const selectors = [];
21284
21328
  const seen = /* @__PURE__ */ new Set();
21285
21329
  for (const raw of options.selectors) {
21286
- const selector = parseIssueSelector(raw, currentRepo);
21330
+ const selector = parseIssueSelector(raw, currentRepo, options.repo);
21287
21331
  const key = `${selector.repo.toLowerCase()}#${selector.number}`;
21288
21332
  if (seen.has(key)) continue;
21289
21333
  seen.add(key);
@@ -21363,7 +21407,7 @@ async function unclaimBoardIssue(options, deps = {}) {
21363
21407
  const cfg = resolveBoardConfig(options.config);
21364
21408
  const client = deps.client ?? defaultGitHubClient();
21365
21409
  const currentRepo = await resolveCurrentRepo(options, deps);
21366
- const selector = parseIssueSelector(options.selector, currentRepo);
21410
+ const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
21367
21411
  const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
21368
21412
  if (!item) {
21369
21413
  throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
@@ -22485,6 +22529,20 @@ var REQUIRED_OPTION_HELP = {
22485
22529
  optionDescription(option) {
22486
22530
  const described = Help.prototype.optionDescription.call(this, option);
22487
22531
  return option.mandatory ? `(required) ${described}` : described;
22532
+ },
22533
+ // #5874: resolveHouseShim removes the house token before Commander renders help. Its stock usage
22534
+ // therefore teaches a removed flat alias; render the manifest's canonical house-qualified path instead.
22535
+ commandUsage(command) {
22536
+ const parts = [];
22537
+ for (let node = command; node && node.parent; node = node.parent) {
22538
+ parts.unshift(node.name());
22539
+ }
22540
+ if (!parts.length) return Help.prototype.commandUsage.call(this, command);
22541
+ const flatPath = parts.join(" ");
22542
+ const canonicalPath = canonicalPathFor(flatPath) ?? flatPath;
22543
+ const alias = command.alias();
22544
+ const name = alias ? `${command.name()}|${alias}` : command.name();
22545
+ return `mmi-cli ${canonicalPath.slice(0, -command.name().length)}${name} ${command.usage()}`;
22488
22546
  }
22489
22547
  };
22490
22548
  function classifyTree(command, path2, inherited, hideFromParent) {
@@ -31295,6 +31353,69 @@ async function remoteBranchExists2(branch, options = {}) {
31295
31353
  return void 0;
31296
31354
  }
31297
31355
  }
31356
+ async function deleteMergedRemoteBranch(options) {
31357
+ const remediation = `git push origin --delete ${options.branch}`;
31358
+ if (!options.branch) {
31359
+ return {
31360
+ branch: options.branch,
31361
+ existedBefore: options.existedBefore,
31362
+ attempted: false,
31363
+ status: "failed",
31364
+ error: "missing PR head branch",
31365
+ remediation
31366
+ };
31367
+ }
31368
+ if (options.existedBefore === false) {
31369
+ const exists2 = await options.branchExists(options.branch);
31370
+ if (exists2 === false) return { branch: options.branch, existedBefore: false, attempted: false, status: "already-gone" };
31371
+ return {
31372
+ branch: options.branch,
31373
+ existedBefore: false,
31374
+ attempted: false,
31375
+ status: "failed",
31376
+ error: exists2 ? `origin reports ${options.branch} after merge` : `could not verify absence of origin/${options.branch}`,
31377
+ remediation
31378
+ };
31379
+ }
31380
+ try {
31381
+ await options.execGit(["push", "origin", "--delete", options.branch]);
31382
+ } catch (e) {
31383
+ const exists2 = await options.branchExists(options.branch);
31384
+ if (exists2 === false) {
31385
+ return {
31386
+ branch: options.branch,
31387
+ existedBefore: options.existedBefore,
31388
+ attempted: true,
31389
+ status: "deleted"
31390
+ };
31391
+ }
31392
+ return {
31393
+ branch: options.branch,
31394
+ existedBefore: options.existedBefore,
31395
+ attempted: true,
31396
+ status: "failed",
31397
+ error: e instanceof Error ? e.message : String(e),
31398
+ remediation
31399
+ };
31400
+ }
31401
+ const exists = await options.branchExists(options.branch);
31402
+ if (exists === false) {
31403
+ return {
31404
+ branch: options.branch,
31405
+ existedBefore: options.existedBefore,
31406
+ attempted: true,
31407
+ status: "deleted"
31408
+ };
31409
+ }
31410
+ return {
31411
+ branch: options.branch,
31412
+ existedBefore: options.existedBefore,
31413
+ attempted: true,
31414
+ status: "failed",
31415
+ error: exists ? `origin still reports ${options.branch} after deletion` : `could not verify deletion of origin/${options.branch}`,
31416
+ remediation
31417
+ };
31418
+ }
31298
31419
 
31299
31420
  // src/worktree-merge-cleanup.ts
31300
31421
  var import_node_fs36 = require("node:fs");
@@ -31484,6 +31605,7 @@ function removeResidueDirectory(wtPath) {
31484
31605
  } catch {
31485
31606
  }
31486
31607
  (0, import_node_fs36.rmSync)(wtPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
31608
+ if ((0, import_node_fs36.existsSync)(wtPath)) return { ok: false, error: `directory remains after removal: ${wtPath}` };
31487
31609
  return { ok: true };
31488
31610
  } catch (e) {
31489
31611
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
@@ -31776,8 +31898,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31776
31898
  if (residue.ok) {
31777
31899
  report.worktree.residue = "swept";
31778
31900
  } else {
31901
+ report.worktree.status = "failed";
31902
+ report.worktree.reason = "residue-remains";
31779
31903
  report.worktree.residue = "left";
31780
31904
  report.worktree.residueError = residue.error;
31905
+ report.worktree.remediation = `Remove-Item -LiteralPath '${wtPath.replace(/'/g, "''")}' -Recurse -Force`;
31781
31906
  }
31782
31907
  }
31783
31908
  try {
@@ -31808,7 +31933,7 @@ function renderPrMergeCleanupLines(cleanup) {
31808
31933
  lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
31809
31934
  }
31810
31935
  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}`);
31936
+ lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
31812
31937
  }
31813
31938
  if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
31814
31939
  lines.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
@@ -31863,8 +31988,24 @@ function registerBoardCommands(program3) {
31863
31988
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
31864
31989
  }
31865
31990
  }
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)`;
31991
+ function formatClaimHolder(holder) {
31992
+ const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
31993
+ return `@${holder.login}${lane}`;
31994
+ }
31995
+ function claimVerdict(ref, result) {
31996
+ const holder = formatClaimHolder(result.holder);
31997
+ const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
31998
+ if (result.checked) {
31999
+ if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
32000
+ if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
32001
+ return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
32002
+ }
32003
+ if (result.partial) {
32004
+ return result.outcome === "took-over" ? `Partially took over ${ref} from ${previousHolder}: ${result.warning}` : `Partially claimed ${ref} for ${holder}: ${result.warning}`;
32005
+ }
32006
+ if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
32007
+ if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
32008
+ return `Claimed ${ref} for ${holder} - In Progress`;
31868
32009
  }
31869
32010
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
31870
32011
  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 +32027,7 @@ function registerBoardCommands(program3) {
31886
32027
  });
31887
32028
  if (!result.checked) invalidateStatuslineBoardCache();
31888
32029
  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`);
32030
+ console.log(claimVerdict(result.item.ref, result));
31890
32031
  } catch (e) {
31891
32032
  if (refuseRateLimited(e, o.json)) return;
31892
32033
  return failGraceful(`board claim failed: ${e.message}`);
@@ -31908,7 +32049,7 @@ function registerBoardCommands(program3) {
31908
32049
  console.log(JSON.stringify(bulk.results));
31909
32050
  } else {
31910
32051
  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}`);
32052
+ console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
31912
32053
  }
31913
32054
  }
31914
32055
  if (bulk.failed > 0) process.exitCode = 1;
@@ -32004,7 +32145,7 @@ function registerBoardCommands(program3) {
32004
32145
  }
32005
32146
  try {
32006
32147
  const defaultRepo = await resolveRepo(o.repo) ?? "";
32007
- const selector = parseIssueSelector(issueRef, defaultRepo);
32148
+ const selector = parseIssueSelector(issueRef, defaultRepo, o.repo);
32008
32149
  if (!selector.repo) {
32009
32150
  return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
32010
32151
  }
@@ -32249,7 +32390,7 @@ async function fetchRestClosingGuardPayload(prNumber, repo, gh = defaultGhApi) {
32249
32390
  return { pr: pr2, commits };
32250
32391
  }
32251
32392
  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}"]);
32393
+ 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
32394
  return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
32254
32395
  }
32255
32396
  async function fetchHeadCheckEntries(headSha, repo, gh) {
@@ -32321,6 +32462,29 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
32321
32462
  "cancelled",
32322
32463
  "startup_failure"
32323
32464
  ]);
32465
+ var BARE_CHECKS_FAILURE_MESSAGE = "GitHub reported a checks failure but exposed no per-check data.";
32466
+ function actionRunIdFromDetailsUrl(detailsUrl) {
32467
+ const match = detailsUrl?.match(/\/actions\/runs\/(\d+)(?:\/|$)/);
32468
+ if (!match) return void 0;
32469
+ const runId = Number(match[1]);
32470
+ return Number.isSafeInteger(runId) ? runId : void 0;
32471
+ }
32472
+ function failedCheckReceipt(run, repo) {
32473
+ const name = run.name ?? `check-run ${run.id ?? "unknown"}`;
32474
+ const conclusion = run.conclusion ?? "unknown";
32475
+ const detailsUrl = typeof run.details_url === "string" && run.details_url ? run.details_url : void 0;
32476
+ const runId = actionRunIdFromDetailsUrl(detailsUrl);
32477
+ return {
32478
+ name,
32479
+ conclusion,
32480
+ ...detailsUrl ? { detailsUrl } : {},
32481
+ ...runId !== void 0 ? {
32482
+ runId,
32483
+ runUrl: `https://github.com/${repo}/actions/runs/${runId}`,
32484
+ diagnosticCommand: `gh run view ${runId} --log-failed --repo ${repo}`
32485
+ } : {}
32486
+ };
32487
+ }
32324
32488
  function isErrorAnnotation(a) {
32325
32489
  const level = a.annotation_level?.toLowerCase();
32326
32490
  return level === "failure" || level === "error";
@@ -32353,6 +32517,7 @@ function classifyFailedChecks(failing) {
32353
32517
  cause: "runner-infra",
32354
32518
  infraFailures,
32355
32519
  otherFailures,
32520
+ failedChecks: [],
32356
32521
  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
32522
  };
32358
32523
  }
@@ -32360,6 +32525,7 @@ function classifyFailedChecks(failing) {
32360
32525
  cause: "checks-failure",
32361
32526
  infraFailures,
32362
32527
  otherFailures,
32528
+ failedChecks: [],
32363
32529
  reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
32364
32530
  };
32365
32531
  }
@@ -32380,6 +32546,7 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
32380
32546
  cause: "stale-head",
32381
32547
  infraFailures: [],
32382
32548
  otherFailures: [],
32549
+ failedChecks: [],
32383
32550
  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
32551
  }
32385
32552
  };
@@ -32392,15 +32559,20 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
32392
32559
  return { state: "failed", error: `check-runs read failed for ${snapshot.headSha.slice(0, 7)} on ${repo}: ${readErrorText(e)}` };
32393
32560
  }
32394
32561
  if (!failing.length) return { state: "absent", reason: "no failing check-run on the current head to diagnose" };
32562
+ const failedChecks = failing.map((run) => failedCheckReceipt(run, repo));
32395
32563
  try {
32396
32564
  const annotated = await Promise.all(failing.map(async (run) => ({
32397
32565
  name: run.name ?? `check-run ${run.id}`,
32398
32566
  conclusion: run.conclusion ?? null,
32399
32567
  annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
32400
32568
  })));
32401
- return { state: "ok", diagnosis: classifyFailedChecks(annotated) };
32569
+ return { state: "ok", diagnosis: { ...classifyFailedChecks(annotated), failedChecks } };
32402
32570
  } catch (e) {
32403
- return { state: "failed", error: `annotations read failed for a failing check-run on ${repo}: ${readErrorText(e)}` };
32571
+ return {
32572
+ state: "failed",
32573
+ error: `annotations read failed for a failing check-run on ${repo}: ${readErrorText(e)}`,
32574
+ failedChecks
32575
+ };
32404
32576
  }
32405
32577
  }
32406
32578
  function isNotFoundError2(e) {
@@ -33240,6 +33412,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33240
33412
  (opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
33241
33413
  ).action(async (ref, o) => {
33242
33414
  try {
33415
+ parseIssueRef(ref, o.repo);
33243
33416
  const defaultRepo = await resolveRepo(o.repo);
33244
33417
  const result = await editIssue(defaultGitHubClient(), {
33245
33418
  ref,
@@ -33284,6 +33457,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33284
33457
  ).action(async (ref, o) => {
33285
33458
  const parsed = parseCloseReason(o.reason);
33286
33459
  try {
33460
+ parseIssueRef(ref, o.repo);
33287
33461
  const defaultRepo = await resolveRepo(o.repo);
33288
33462
  const result = await closeIssue(defaultGitHubClient(), {
33289
33463
  ref,
@@ -33303,6 +33477,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33303
33477
  (_opts, args) => ({ command: "issue reopen", ref: args[0] })
33304
33478
  ).action(async (ref, o) => {
33305
33479
  try {
33480
+ parseIssueRef(ref, o.repo);
33306
33481
  const defaultRepo = await resolveRepo(o.repo);
33307
33482
  const result = await reopenIssue(defaultGitHubClient(), ref, defaultRepo);
33308
33483
  console.log(JSON.stringify(result));
@@ -33315,6 +33490,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33315
33490
  (_opts, args) => ({ command: "issue assign", ref: args[0], login: args[1] })
33316
33491
  ).action(async (ref, login, o) => {
33317
33492
  try {
33493
+ parseIssueRef(ref, o.repo);
33318
33494
  const defaultRepo = await resolveRepo(o.repo);
33319
33495
  const result = await assignIssue(defaultGitHubClient(), ref, login, defaultRepo);
33320
33496
  console.log(JSON.stringify(result));
@@ -33327,6 +33503,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33327
33503
  (_opts, args) => ({ command: "issue unassign", ref: args[0], login: args[1] })
33328
33504
  ).action(async (ref, login, o) => {
33329
33505
  try {
33506
+ parseIssueRef(ref, o.repo);
33330
33507
  const defaultRepo = await resolveRepo(o.repo);
33331
33508
  const result = await unassignIssue(defaultGitHubClient(), ref, login, defaultRepo);
33332
33509
  console.log(JSON.stringify(result));
@@ -33339,6 +33516,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33339
33516
  (opts, args) => ({ command: "issue relocate", ref: args[0], to: opts.to })
33340
33517
  ).action(async (ref, o) => {
33341
33518
  try {
33519
+ parseIssueRef(ref, o.repo);
33342
33520
  const defaultRepo = await resolveRepo(o.repo);
33343
33521
  const result = await relocateIssue(defaultGitHubClient(), ref, o.to, defaultRepo);
33344
33522
  console.log(JSON.stringify(result));
@@ -33350,8 +33528,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
33350
33528
  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
33529
  (_opts, args) => ({ command: "issue unlink-child", parent: args[0], child: args[1] })
33352
33530
  ).action(async (parentRef, childRef, o) => {
33353
- const defaultRepo = await resolveRepo(o.repo);
33354
33531
  try {
33532
+ parseIssueRef(parentRef, o.repo);
33533
+ parseIssueRef(childRef, o.repo);
33534
+ const defaultRepo = await resolveRepo(o.repo);
33355
33535
  const result = await unlinkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
33356
33536
  console.log(JSON.stringify(result));
33357
33537
  } catch (e) {
@@ -34985,7 +35165,7 @@ function registerLearningPickupCommand(program3) {
34985
35165
  let selector;
34986
35166
  try {
34987
35167
  if (ref) {
34988
- selector = parseIssueSelector(ref, defaultRepo);
35168
+ selector = parseIssueSelector(ref, defaultRepo, o.repo);
34989
35169
  } else if (o.number) {
34990
35170
  selector = { repo: defaultRepo, number: Number.parseInt(o.number, 10) };
34991
35171
  if (!Number.isFinite(selector.number)) throw new Error("invalid --number");
@@ -39659,7 +39839,7 @@ withExamples(mutating(
39659
39839
  repo: targetRepo2,
39660
39840
  labels: extraLabels.length ? extraLabels : void 0
39661
39841
  });
39662
- if (o.parent !== void 0) parseIssueRef(o.parent);
39842
+ if (o.parent !== void 0) parseIssueRef(o.parent, o.repo);
39663
39843
  } catch (e) {
39664
39844
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
39665
39845
  }
@@ -39741,10 +39921,15 @@ async function readParentField(number, repo) {
39741
39921
  }
39742
39922
  return resolveParentField(payload);
39743
39923
  }
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);
39924
+ 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) => {
39925
+ let parsed;
39926
+ try {
39927
+ parsed = parseIssueRef(ref, o.repo);
39928
+ } catch (e) {
39929
+ return fail(`issue view: ${e.message}`);
39930
+ }
39931
+ const n = parsed.number;
39932
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
39748
39933
  if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
39749
39934
  const fields = normalizeIssueViewJsonFields(o.json);
39750
39935
  try {
@@ -39804,12 +39989,14 @@ issue.command("discover-related").description("find related issues for an existi
39804
39989
  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
39990
  const defaultRepo = await resolveRepo(o.repo);
39806
39991
  try {
39992
+ parseIssueRef(parentRef, o.repo);
39993
+ parseIssueRef(childRef, o.repo);
39807
39994
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
39808
39995
  console.log(JSON.stringify(result));
39809
39996
  } catch (e) {
39810
39997
  let conflict;
39811
39998
  try {
39812
- const child2 = parseIssueRef(childRef);
39999
+ const child2 = parseIssueRef(childRef, o.repo);
39813
40000
  const childRepo = child2.repo ?? defaultRepo;
39814
40001
  if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
39815
40002
  } catch {
@@ -39824,7 +40011,7 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
39824
40011
  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
40012
  let parsed;
39826
40013
  try {
39827
- parsed = parseIssueRef(ref);
40014
+ parsed = parseIssueRef(ref, o.repo);
39828
40015
  } catch (e) {
39829
40016
  return fail(`issue comment: ${e.message}`);
39830
40017
  }
@@ -39847,7 +40034,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
39847
40034
  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
40035
  let parsed;
39849
40036
  try {
39850
- parsed = parseIssueRef(ref);
40037
+ parsed = parseIssueRef(ref, o.repo);
39851
40038
  } catch (e) {
39852
40039
  return fail(`issue check: ${e.message}`);
39853
40040
  }
@@ -40034,10 +40221,15 @@ withExamples(pr.command("create").description("create a PR and print {number,url
40034
40221
  "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
40035
40222
  "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
40036
40223
  ]);
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);
40224
+ 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) => {
40225
+ let parsed;
40226
+ try {
40227
+ parsed = parseIssueRef(ref, o.repo);
40228
+ } catch (e) {
40229
+ return fail(`pr view: ${e.message}`);
40230
+ }
40231
+ const n = parsed.number;
40232
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
40041
40233
  if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
40042
40234
  const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
40043
40235
  const effective = resolvePrViewFields(o.json, defaultPrFields);
@@ -40146,13 +40338,44 @@ async function waitLoopCorePool(label) {
40146
40338
  async function waitLoopDiagnosis(label, prNumber, repo) {
40147
40339
  const read = await diagnoseFailedRestChecks(prNumber, repo);
40148
40340
  if (read.state === "ok") return read.diagnosis;
40341
+ if (read.state === "absent") {
40342
+ return {
40343
+ cause: "checks-failure",
40344
+ infraFailures: [],
40345
+ otherFailures: [],
40346
+ failedChecks: [],
40347
+ checksFailureDetail: BARE_CHECKS_FAILURE_MESSAGE,
40348
+ reason: BARE_CHECKS_FAILURE_MESSAGE
40349
+ };
40350
+ }
40149
40351
  if (read.state === "failed") {
40150
40352
  console.warn(
40151
40353
  `${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
40354
  );
40355
+ return {
40356
+ cause: "checks-failure",
40357
+ infraFailures: [],
40358
+ otherFailures: [],
40359
+ failedChecks: read.failedChecks ?? [],
40360
+ reason: "checks failure diagnosis unavailable"
40361
+ };
40153
40362
  }
40154
40363
  return null;
40155
40364
  }
40365
+ function failedChecksReceiptLines(context, wait) {
40366
+ if (wait.detail !== "checks-failure") return [];
40367
+ const lines = [];
40368
+ for (const check of wait.failedChecks ?? []) {
40369
+ lines.push(`${context}: failed check: ${check.name} (${check.conclusion})${check.detailsUrl ? ` \u2014 ${check.detailsUrl}` : ""}`);
40370
+ if (check.diagnosticCommand) lines.push(`${context}: next diagnostic: ${check.diagnosticCommand}`);
40371
+ }
40372
+ if (!wait.failedChecks?.length && wait.checksFailureDetail) lines.push(`${context}: ${wait.checksFailureDetail}`);
40373
+ if (!wait.failedChecks?.some((check) => check.diagnosticCommand) && wait.diagnosticCommand) {
40374
+ lines.push(`${context}: next diagnostic: ${wait.diagnosticCommand}`);
40375
+ }
40376
+ if (wait.checksUrl) lines.push(`${context}: PR checks: ${wait.checksUrl}`);
40377
+ return lines;
40378
+ }
40156
40379
  async function waitLoopHeadWorkflowRunCount(label, prNumber, repo) {
40157
40380
  let snapshot;
40158
40381
  try {
@@ -40206,6 +40429,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
40206
40429
  // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
40207
40430
  // reading as "your tests failed". One call per failing run, only at the verdict.
40208
40431
  diagnoseFailure: () => waitLoopDiagnosis("pr checks-wait", number, repo),
40432
+ checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
40433
+ checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
40209
40434
  // #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
40210
40435
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
40211
40436
  baseBranch,
@@ -40232,21 +40457,21 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
40232
40457
  printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
40233
40458
  } else if (result.detail === "zero-runs") {
40234
40459
  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})` : ""}`);
40460
+ } else {
40461
+ printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
40462
+ for (const line of failedChecksReceiptLines("pr checks-wait", result)) printLine(line);
40463
+ }
40236
40464
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
40237
40465
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
40238
40466
  });
40239
40467
  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];
40468
+ if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
40469
+ try {
40470
+ const parsed = parseIssueRef(number, o.repo);
40471
+ number = String(parsed.number);
40472
+ if (parsed.repo) o.repo = parsed.repo;
40473
+ } catch (e) {
40474
+ return fail(`pr land: ${e.message}`);
40250
40475
  }
40251
40476
  }
40252
40477
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
@@ -40308,6 +40533,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
40308
40533
  // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
40309
40534
  // then read its own wall-clock kill as a broken diff.
40310
40535
  diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
40536
+ checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
40537
+ checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
40311
40538
  // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
40312
40539
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
40313
40540
  baseBranch: "development",
@@ -40490,6 +40717,8 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40490
40717
  pollMergeable: () => pollRestPrMergeable(number, repo),
40491
40718
  pollRateLimit: () => waitLoopCorePool("pr merge --wait"),
40492
40719
  diagnoseFailure: () => waitLoopDiagnosis("pr merge --wait", number, repo),
40720
+ checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
40721
+ checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
40493
40722
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
40494
40723
  baseBranch,
40495
40724
  sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
@@ -40498,7 +40727,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40498
40727
  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
40728
  });
40500
40729
  if (wait.status !== "success" && wait.status !== "skipped") {
40730
+ if (wait.detail === "checks-failure") {
40731
+ console.log(JSON.stringify({
40732
+ mergeStatus: "not-merged",
40733
+ reason: "checks-failure",
40734
+ pr: number,
40735
+ repo,
40736
+ failedChecks: wait.failedChecks ?? [],
40737
+ ...wait.checksUrl ? { checksUrl: wait.checksUrl } : {},
40738
+ ...wait.diagnosticCommand ? { diagnosticCommand: wait.diagnosticCommand } : {},
40739
+ ...wait.checksFailureDetail ? { checksFailureDetail: wait.checksFailureDetail } : {}
40740
+ }));
40741
+ }
40501
40742
  console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
40743
+ for (const line of failedChecksReceiptLines("pr merge", wait)) console.warn(line);
40502
40744
  process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
40503
40745
  return;
40504
40746
  }
@@ -40610,7 +40852,6 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40610
40852
  process.exitCode = 1;
40611
40853
  return;
40612
40854
  }
40613
- const remoteBranch = { branch: headRef, existedBefore: remoteBefore, attempted: false, reason: remoteNotAttemptedReason };
40614
40855
  const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
40615
40856
  let localCleanup;
40616
40857
  try {
@@ -40635,6 +40876,28 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40635
40876
  }
40636
40877
  };
40637
40878
  }
40879
+ const remoteBranch = await deleteMergedRemoteBranch({
40880
+ branch: headRef,
40881
+ existedBefore: remoteBefore,
40882
+ execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
40883
+ branchExists: remoteBranchExists2
40884
+ });
40885
+ const worktreePartial = localCleanup?.worktree?.path && localCleanup.worktree.status !== "removed" ? {
40886
+ kind: "worktree-directory",
40887
+ path: localCleanup.worktree.path,
40888
+ reason: localCleanup.worktree.reason ?? localCleanup.worktree.status,
40889
+ error: localCleanup.worktree.error ?? localCleanup.worktree.residueError,
40890
+ remediation: localCleanup.worktree.remediation ?? `Remove-Item -LiteralPath '${localCleanup.worktree.path?.replace(/'/g, "''")}' -Recurse -Force`
40891
+ } : void 0;
40892
+ const partialCleanup = [
40893
+ ...worktreePartial ? [worktreePartial] : [],
40894
+ ...remoteBranch.status === "failed" ? [{
40895
+ kind: "remote-branch",
40896
+ branch: remoteBranch.branch,
40897
+ error: remoteBranch.error,
40898
+ remediation: remoteBranch.remediation
40899
+ }] : []
40900
+ ];
40638
40901
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
40639
40902
  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
40903
  const recovery = repoForPostCleanup ? buildPostMergeReconRecovery({
@@ -40672,12 +40935,13 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40672
40935
  observedMethod
40673
40936
  });
40674
40937
  console.log(JSON.stringify({
40675
- mergeStatus: "merged",
40938
+ mergeStatus: partialCleanup.length ? "partial-cleanup" : "merged",
40676
40939
  merged: number,
40677
40940
  branch: headRef,
40678
40941
  ...methodField ? { method: methodField } : {},
40679
40942
  remoteBranch,
40680
40943
  housekeeping,
40944
+ ...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
40681
40945
  ...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
40682
40946
  ...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
40683
40947
  // `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
@@ -40701,7 +40965,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40701
40965
  })) {
40702
40966
  console.error(line);
40703
40967
  }
40704
- process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
40968
+ process.exitCode = (partialCleanup.length ? 1 : void 0) ?? prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
40705
40969
  });
40706
40970
  registerQueryCommands(program2);
40707
40971
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
@@ -40844,6 +41108,52 @@ function renderAlignment(label, alignment) {
40844
41108
  }
40845
41109
  return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
40846
41110
  }
41111
+ function followUpLegStatus(status) {
41112
+ return status === "failure" ? "failed" : status;
41113
+ }
41114
+ function releaseFollowUpLegs(result, projectInfoSync) {
41115
+ const legs = [
41116
+ {
41117
+ leg: "project-info",
41118
+ status: projectInfoSync && "error" in projectInfoSync ? "failed" : "success",
41119
+ ...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
41120
+ }
41121
+ ];
41122
+ const runs = result.workflowRuns ?? [];
41123
+ if (runs.length > 0) {
41124
+ legs.push(...runs.map((run) => ({
41125
+ leg: run.workflow,
41126
+ status: followUpLegStatus(run.conclusion),
41127
+ ...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" ? result.dispatch : `${run.workflow} reported failure` } : {}
41128
+ })));
41129
+ } else if (result.deployStatus !== "success") {
41130
+ legs.push({
41131
+ leg: "deploy",
41132
+ status: followUpLegStatus(result.deployStatus),
41133
+ ...result.deployStatus === "failure" ? { error: result.dispatch } : {}
41134
+ });
41135
+ }
41136
+ if (result.rcRetirement) {
41137
+ legs.push({
41138
+ leg: "rc-retirement",
41139
+ status: result.rcRetirement === "failed" ? "failed" : "success",
41140
+ ...result.rcRetirement === "failed" ? { error: result.rcRetirementNote ?? "rc retirement failed" } : {}
41141
+ });
41142
+ }
41143
+ if (result.devRollForward) {
41144
+ legs.push({
41145
+ leg: "development-alignment",
41146
+ status: result.devRollForward.status === "pr-pending" ? "pending" : "success"
41147
+ });
41148
+ }
41149
+ if (result.rcAlignment) {
41150
+ legs.push({
41151
+ leg: "rc-alignment",
41152
+ status: result.rcAlignment.status === "pr-pending" ? "pending" : "success"
41153
+ });
41154
+ }
41155
+ return legs;
41156
+ }
40847
41157
  var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
40848
41158
  async function runPostReleaseJervDoctor(repo) {
40849
41159
  if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
@@ -40884,7 +41194,11 @@ function renderTrainApply(commandName, r) {
40884
41194
  if (r.releaseVerdict) {
40885
41195
  const v = r.releaseVerdict;
40886
41196
  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()}`;
41197
+ const failedLegs = v.legs.filter((leg) => leg.status === "failed");
41198
+ return [
41199
+ `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`,
41200
+ ...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`)
41201
+ ].join("\n");
40888
41202
  }
40889
41203
  return base;
40890
41204
  }
@@ -41055,7 +41369,8 @@ for (const commandName of ["rcand", "release"]) {
41055
41369
  followUpStatus,
41056
41370
  promoted: result.promoted,
41057
41371
  tag: result.tag,
41058
- releaseUrl: result.release?.url ?? null
41372
+ releaseUrl: result.release?.url ?? null,
41373
+ legs: releaseFollowUpLegs(result, projectInfoSync)
41059
41374
  } : void 0;
41060
41375
  const reported = {
41061
41376
  ...result,
@@ -41105,7 +41420,7 @@ function renderHotfixRelease(r) {
41105
41420
  ` - ${r.tagNote}`,
41106
41421
  ` - ${r.releaseNote}`,
41107
41422
  ` - deploy: ${r.deployNote}`,
41108
- ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
41423
+ ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
41109
41424
  ` - ${r.verifyNote}`,
41110
41425
  ...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
41111
41426
  ` - fold: ${r.foldNote}`,
@@ -41116,7 +41431,7 @@ function renderHotfixStatus(r) {
41116
41431
  return [
41117
41432
  `mmi-cli devops hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
41118
41433
  ` - 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})` : ""}`),
41434
+ ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
41120
41435
  ` - npm @mutmutco/cli: ${r.npmVersion}`,
41121
41436
  ` - next: ${r.next}`,
41122
41437
  ...r.warnings.map((w) => ` - warning: ${w}`)
@@ -41127,13 +41442,25 @@ function hotfixRunOutcome(conclusion) {
41127
41442
  if (conclusion === "failure") return "failure";
41128
41443
  return "unresolved";
41129
41444
  }
41445
+ function hotfixFollowUpLegs(runs, foldPort, foldNote) {
41446
+ const legs = runs.map((run) => ({
41447
+ leg: run.workflow,
41448
+ status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
41449
+ ...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
41450
+ }));
41451
+ if (foldPort === "failure") {
41452
+ legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
41453
+ }
41454
+ return legs;
41455
+ }
41130
41456
  async function runHotfixSub(sub, body, json, render) {
41131
41457
  try {
41132
41458
  await requireFreshTrainCli("hotfix");
41133
41459
  const result = await body();
41134
- printLine(json ? JSON.stringify(result, null, 2) : render(result));
41135
41460
  const runs = result.runs;
41136
41461
  const foldPort = result.foldStatus ?? "ok";
41462
+ const legs = runs ? hotfixFollowUpLegs(runs, foldPort, result.foldNote) : void 0;
41463
+ printLine(json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result));
41137
41464
  if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
41138
41465
  projectInfo: "ok",
41139
41466
  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.6",
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",