@mutmutco/cli 4.0.16 → 4.0.17

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 +143 -16
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5035,16 +5035,26 @@ function isAgentScratchPath(path2) {
5035
5035
  return /^tmp_[^/]+$/.test(normalized);
5036
5036
  }
5037
5037
  var PUSH_WALL_AGENT_CONFIG_DIRS = [".claude/", ".cursor/rules/", ".codex/", ".agents/"];
5038
+ var LIVE_SESSION_DIRS = [".jerv/", ".pi/"];
5039
+ function pathUnderDirPrefix(path2, dir) {
5040
+ return path2 === dir || path2 === dir.slice(0, -1) || path2.startsWith(dir) || path2.includes(`/${dir}`);
5041
+ }
5038
5042
  function isUncommittableAgentConfigPath(path2) {
5039
5043
  const normalized = path2.replace(/\\/g, "/").trim();
5040
- return PUSH_WALL_AGENT_CONFIG_DIRS.some((dir) => normalized === dir || normalized === dir.slice(0, -1) || normalized.startsWith(dir) || normalized.includes(`/${dir}`));
5044
+ return PUSH_WALL_AGENT_CONFIG_DIRS.some((dir) => pathUnderDirPrefix(normalized, dir));
5045
+ }
5046
+ function isLiveSessionPath(path2) {
5047
+ const normalized = path2.replace(/\\/g, "/").trim();
5048
+ return LIVE_SESSION_DIRS.some((dir) => pathUnderDirPrefix(normalized, dir));
5041
5049
  }
5042
5050
  function splitPorcelainLine(line) {
5043
5051
  return { status: line.slice(0, 2), path: line.slice(3).split(" -> ")[0]?.trim() ?? "" };
5044
5052
  }
5045
5053
  function isExemptPorcelainLine(status, path2) {
5046
5054
  if (isAgentScratchPath(path2)) return true;
5047
- return status === "??" && isUncommittableAgentConfigPath(path2);
5055
+ if (status !== "??") return false;
5056
+ if (isUncommittableAgentConfigPath(path2)) return true;
5057
+ return isLiveSessionPath(path2);
5048
5058
  }
5049
5059
  function porcelainBlockingPaths(porcelain) {
5050
5060
  const blocking = [];
@@ -5059,12 +5069,21 @@ function porcelainHasBlockingChanges(porcelain) {
5059
5069
  return porcelainBlockingPaths(porcelain).length > 0;
5060
5070
  }
5061
5071
  var BLOCKING_PATH_PREVIEW_LIMIT = 10;
5072
+ function porcelainHasBlockingUntracked(porcelain) {
5073
+ for (const line of porcelain.split("\n")) {
5074
+ if (!line.trim()) continue;
5075
+ const { status, path: path2 } = splitPorcelainLine(line);
5076
+ if (path2 !== "" && status === "??" && !isExemptPorcelainLine(status, path2)) return true;
5077
+ }
5078
+ return false;
5079
+ }
5062
5080
  function describeBlockingPaths(porcelain) {
5063
5081
  const paths = porcelainBlockingPaths(porcelain);
5064
5082
  if (paths.length === 0) return "";
5065
5083
  const shown = paths.slice(0, BLOCKING_PATH_PREVIEW_LIMIT);
5066
5084
  const rest = paths.length - shown.length;
5067
- return ` \u2014 blocking: ${shown.join(", ")}${rest > 0 ? `, \u2026 and ${rest} more` : ""}`;
5085
+ const hint = porcelainHasBlockingUntracked(porcelain) ? " (untracked: gitignore it, or add it to .git/info/exclude)" : "";
5086
+ return ` \u2014 blocking: ${shown.join(", ")}${rest > 0 ? `, \u2026 and ${rest} more` : ""}${hint}`;
5068
5087
  }
5069
5088
 
5070
5089
  // src/local-train-sync.ts
@@ -5614,6 +5633,12 @@ var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
5614
5633
  var PR_CHECKS_RATELIMIT_FLOOR = 50;
5615
5634
  var PR_CHECKS_RATELIMIT_JITTER_MAX_MS = 5e3;
5616
5635
  var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
5636
+ var PR_CHECKS_ZERO_RUNS_GRACE_MS = 2 * 6e4;
5637
+ var PR_CHECKS_ZERO_RUNS_CONFIRMATIONS = 2;
5638
+ function zeroRunsDeliveryMessage(headSha) {
5639
+ const tip = headSha ? ` (${headSha.slice(0, 7)})` : "";
5640
+ return `GitHub delivered ZERO workflow runs for this PR head${tip} \u2014 not "checks still pending". Sibling PRs on the same repo can still be healthy while this head is silent. Close/reopen is insufficient. Recourse: push an empty commit on the same branch (\`git commit --allow-empty -m "ci: re-trigger gates" && git push\`) or re-branch onto a new head and open a fresh PR. See docs/Guides/gh-runner-runbook.md \xA7 Zero workflow runs on a PR head.`;
5641
+ }
5617
5642
  function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
5618
5643
  if (checks === "no-checks-reported") return { action: "proceed" };
5619
5644
  const staleNote = `merge CI policy says no-ci (${policyReason}), but live PR checks exist`;
@@ -5661,6 +5686,7 @@ async function waitForPrChecks(deps) {
5661
5686
  let lastDetail = "pending";
5662
5687
  let successStreak = 0;
5663
5688
  let failureStreak = 0;
5689
+ let zeroRunsStreak = 0;
5664
5690
  report("starting");
5665
5691
  while (now() < deadline) {
5666
5692
  if (deps.pollRateLimit) {
@@ -5694,6 +5720,7 @@ async function waitForPrChecks(deps) {
5694
5720
  report(state);
5695
5721
  if (state !== "success") successStreak = 0;
5696
5722
  if (state !== "failure") failureStreak = 0;
5723
+ if (state !== "no-checks-reported") zeroRunsStreak = 0;
5697
5724
  if (state === "failure") {
5698
5725
  failureStreak += 1;
5699
5726
  if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
@@ -5726,6 +5753,30 @@ async function waitForPrChecks(deps) {
5726
5753
  }
5727
5754
  if (state === "no-checks-reported") {
5728
5755
  lastDetail = "no-checks-reported (waiting for workflow to queue)";
5756
+ const elapsed = now() - started;
5757
+ if (deps.pollHeadWorkflowRunCount && elapsed >= PR_CHECKS_ZERO_RUNS_GRACE_MS) {
5758
+ const count = await deps.pollHeadWorkflowRunCount().catch(() => null);
5759
+ if (count === 0) {
5760
+ zeroRunsStreak += 1;
5761
+ if (zeroRunsStreak >= PR_CHECKS_ZERO_RUNS_CONFIRMATIONS) {
5762
+ return {
5763
+ policy,
5764
+ status: "failure",
5765
+ reason: zeroRunsDeliveryMessage(),
5766
+ detail: "zero-runs",
5767
+ waitedMs: elapsed
5768
+ };
5769
+ }
5770
+ lastDetail = "no-checks-reported (confirming zero workflow runs on head)";
5771
+ } else {
5772
+ zeroRunsStreak = 0;
5773
+ if (count !== null && count > 0) {
5774
+ lastDetail = `no-checks-reported (${count} workflow run(s) on head; waiting for check-runs)`;
5775
+ }
5776
+ }
5777
+ } else {
5778
+ zeroRunsStreak = 0;
5779
+ }
5729
5780
  await deps.sleep(PR_CHECKS_POLL_MS);
5730
5781
  continue;
5731
5782
  }
@@ -10832,10 +10883,10 @@ var rollout_plan_default = {
10832
10883
  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)."
10833
10884
  },
10834
10885
  baseline: {
10835
- version: "4.0.16",
10836
- tag: "v4.0.16",
10837
- commit: "ac03c4999cb4",
10838
- npm: "@mutmutco/cli@4.0.16"
10886
+ version: "4.0.17",
10887
+ tag: "v4.0.17",
10888
+ commit: "df1e866df4f8",
10889
+ npm: "@mutmutco/cli@4.0.17"
10839
10890
  },
10840
10891
  exitCriterion: "fleet-n-of-n",
10841
10892
  hubOnlyShortcut: "forbidden",
@@ -10852,14 +10903,14 @@ var rollout_plan_default = {
10852
10903
  repo: "mutmutco/mmi-hub",
10853
10904
  role: "canary",
10854
10905
  schedule: "train",
10855
- v3Target: "v4.0.16"
10906
+ v3Target: "v4.0.17"
10856
10907
  }
10857
10908
  ],
10858
10909
  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.",
10859
10910
  rollback: {
10860
10911
  independent: true,
10861
- mechanism: "npm dist-tag latest -> 4.0.16 and redeploy the Hub Lambda from tag v4.0.16 (ac03c4999cb4); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10862
- v3Target: "v4.0.16 (@mutmutco/cli@4.0.16, tag commit ac03c4999cb4 \u2014 last known-good release carrying the repo-index v4-only contract)"
10912
+ mechanism: "npm dist-tag latest -> 4.0.17 and redeploy the Hub Lambda from tag v4.0.17 (df1e866df4f8); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10913
+ v3Target: "v4.0.17 (@mutmutco/cli@4.0.17, tag commit df1e866df4f8 \u2014 last known-good release carrying the repo-index v4-only contract)"
10863
10914
  }
10864
10915
  },
10865
10916
  {
@@ -14659,7 +14710,11 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
14659
14710
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
14660
14711
  }
14661
14712
  if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
14662
- await runGitPush(deps, ["push", "origin", "main"]);
14713
+ try {
14714
+ await runGitPush(deps, ["push", "origin", "main"]);
14715
+ } catch (e) {
14716
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
14717
+ }
14663
14718
  const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
14664
14719
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
14665
14720
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
@@ -15045,7 +15100,11 @@ async function runTrainApplyPipeline(mode, input) {
15045
15100
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
15046
15101
  }
15047
15102
  const autoRunSince = (deps.now ?? Date.now)();
15048
- await runGitPush(deps, ["push", "origin", "rc"]);
15103
+ try {
15104
+ await runGitPush(deps, ["push", "origin", "rc"]);
15105
+ } catch (e) {
15106
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
15107
+ }
15049
15108
  const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
15050
15109
  return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, bumpIntent, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
15051
15110
  }
@@ -29772,6 +29831,14 @@ function ghPrMergeLocalBranchDeleteWarning(message) {
29772
29831
  function mergeAutoRejectedPrAlreadyClean(message) {
29773
29832
  return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
29774
29833
  }
29834
+ function mergeMethodFromParentCount(parentCount) {
29835
+ if (parentCount >= 2) return "merge";
29836
+ return void 0;
29837
+ }
29838
+ function reportedMergeMethod(input) {
29839
+ if (!input.alreadyMerged) return input.requestedMethod;
29840
+ return input.observedMethod;
29841
+ }
29775
29842
 
29776
29843
  // src/merge-cleanup.ts
29777
29844
  var GC_GH_TIMEOUT_MS = 2e4;
@@ -30410,6 +30477,25 @@ async function pollRestPrMergeable(prNumber, repo, gh = defaultGhApi) {
30410
30477
  return "UNKNOWN";
30411
30478
  }
30412
30479
  }
30480
+ async function countHeadWorkflowRuns(headSha, repo, gh = defaultGhApi) {
30481
+ if (!headSha) return { state: "failed", error: "no head SHA" };
30482
+ let raw;
30483
+ try {
30484
+ raw = await gh([`repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=1`]);
30485
+ } catch (e) {
30486
+ return { state: "failed", error: `actions/runs read failed for ${headSha.slice(0, 7)} on ${repo}: ${readErrorText(e)}` };
30487
+ }
30488
+ let count;
30489
+ try {
30490
+ count = JSON.parse(raw).total_count;
30491
+ } catch (e) {
30492
+ return { state: "failed", error: `actions/runs payload for ${headSha.slice(0, 7)} was not JSON: ${readErrorText(e)}` };
30493
+ }
30494
+ if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
30495
+ return { state: "failed", error: `actions/runs payload for ${headSha.slice(0, 7)} carried no usable total_count` };
30496
+ }
30497
+ return { state: "ok", count };
30498
+ }
30413
30499
  async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
30414
30500
  try {
30415
30501
  return (await fetchRestPrSnapshot(prNumber, repo, gh)).merged ? { state: "merged" } : { state: "open" };
@@ -38057,12 +38143,29 @@ async function waitLoopDiagnosis(label, prNumber, repo) {
38057
38143
  }
38058
38144
  return null;
38059
38145
  }
38146
+ async function waitLoopHeadWorkflowRunCount(label, prNumber, repo) {
38147
+ let snapshot;
38148
+ try {
38149
+ snapshot = await fetchRestPrSnapshot(prNumber, repo);
38150
+ } catch (e) {
38151
+ console.warn(
38152
+ `${label}: zero-runs probe FAILED (pulls read: ${e?.message ?? e}) \u2014 not treating as delivery failure this cycle`
38153
+ );
38154
+ return null;
38155
+ }
38156
+ const read = await countHeadWorkflowRuns(snapshot.headSha, repo);
38157
+ if (read.state === "ok") return read.count;
38158
+ console.warn(
38159
+ `${label}: zero-runs probe FAILED (${read.error}) \u2014 not treating as delivery failure this cycle`
38160
+ );
38161
+ return null;
38162
+ }
38060
38163
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
38061
38164
  const result = await resolveMergeCiPolicyForCheckout(o.repo);
38062
38165
  if (o.json) return printLine(JSON.stringify(result));
38063
38166
  printLine(`merge CI policy: ${result.policy} (${result.reason})`);
38064
38167
  });
38065
- pr.command("checks-wait <number>").description(`bounded wait for ALL checks on the PR head, required or not (#5336); skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR \u2014 GitHub never queues checks for one (#2970). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED or the PR is CONFLICTING, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired or the API pool ran dry (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
38168
+ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on the PR head, required or not (#5336); skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR \u2014 GitHub never queues checks for one (#2970), and fails early on a confirmed zero-workflow-runs head instead of burning the full budget as pending (#5400). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED, the PR is CONFLICTING, or GitHub delivered zero runs; exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired or the API pool ran dry (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
38066
38169
  let timeoutMs;
38067
38170
  if (o.timeout !== void 0) {
38068
38171
  const minutes = Number(o.timeout);
@@ -38088,6 +38191,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
38088
38191
  // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
38089
38192
  // reading as "your tests failed". One call per failing run, only at the verdict.
38090
38193
  diagnoseFailure: () => waitLoopDiagnosis("pr checks-wait", number, repo),
38194
+ // #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
38195
+ pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
38091
38196
  baseBranch,
38092
38197
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
38093
38198
  log: (message) => console.warn(message),
@@ -38102,7 +38207,7 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
38102
38207
  } else if (result.status === "timeout") {
38103
38208
  const stuckQueuing = /no-checks-reported/i.test(result.detail ?? "");
38104
38209
  printLine(
38105
- `pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.` + (stuckQueuing ? " Tip: jobs never appeared \u2014 mmi-live may be saturated or Actions may be failing before checkout; check runner health (docs/Guides/gh-runner-runbook.md) and re-run the workflow." : "")
38210
+ `pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.` + (stuckQueuing ? " Tip: check-runs never appeared \u2014 if `gh api repos/<owner>/<repo>/actions/runs?head_sha=<sha>` shows total_count=0, see docs/Guides/gh-runner-runbook.md \xA7 Zero workflow runs; otherwise mmi-live may be saturated or Actions may be failing before checkout (docs/Guides/gh-runner-runbook.md)." : "")
38106
38211
  );
38107
38212
  } else if (result.status === "rate-limited") {
38108
38213
  printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
@@ -38110,6 +38215,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
38110
38215
  printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
38111
38216
  } else if (result.detail === "stale-head") {
38112
38217
  printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
38218
+ } else if (result.detail === "zero-runs") {
38219
+ printLine(`pr checks-wait: failure (zero workflow runs delivered, NOT pending checks) \u2014 ${result.reason}`);
38113
38220
  } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
38114
38221
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
38115
38222
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
@@ -38180,6 +38287,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
38180
38287
  // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
38181
38288
  // then read its own wall-clock kill as a broken diff.
38182
38289
  diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
38290
+ // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
38291
+ pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
38183
38292
  baseBranch: "development",
38184
38293
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
38185
38294
  log: (message) => console.warn(message),
@@ -38302,6 +38411,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38302
38411
  pollMergeable: () => pollRestPrMergeable(number, repo),
38303
38412
  pollRateLimit: () => waitLoopCorePool("pr merge --wait"),
38304
38413
  diagnoseFailure: () => waitLoopDiagnosis("pr merge --wait", number, repo),
38414
+ pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
38305
38415
  baseBranch,
38306
38416
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
38307
38417
  log: (message) => console.warn(message),
@@ -38421,11 +38531,27 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38421
38531
  invalidateStatuslineBoardCache();
38422
38532
  const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
38423
38533
  const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
38534
+ let observedMethod;
38535
+ if (remoteNotAttemptedReason === "pr-already-merged" && repoForPostCleanup) {
38536
+ try {
38537
+ const mergeSha = (await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}/pulls/${number}`)).merge_commit_sha;
38538
+ if (mergeSha) {
38539
+ const commit = await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}/commits/${mergeSha}`);
38540
+ observedMethod = mergeMethodFromParentCount(Array.isArray(commit.parents) ? commit.parents.length : 0);
38541
+ }
38542
+ } catch {
38543
+ }
38544
+ }
38545
+ const methodField = reportedMergeMethod({
38546
+ requestedMethod: method.slice(2),
38547
+ alreadyMerged: remoteNotAttemptedReason === "pr-already-merged",
38548
+ observedMethod
38549
+ });
38424
38550
  console.log(JSON.stringify({
38425
38551
  mergeStatus: "merged",
38426
38552
  merged: number,
38427
38553
  branch: headRef,
38428
- method: method.slice(2),
38554
+ ...methodField ? { method: methodField } : {},
38429
38555
  remoteBranch,
38430
38556
  housekeeping,
38431
38557
  // `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
@@ -38807,7 +38933,8 @@ for (const commandName of ["rcand", "release"]) {
38807
38933
  applyTrainFollowUpExit(followUpStatus);
38808
38934
  return;
38809
38935
  } catch (e) {
38810
- return failGraceful(`${commandName}: ${e.message}`);
38936
+ process.exitCode = 1;
38937
+ return await failGraceful(`${commandName}: ${e.message}`);
38811
38938
  }
38812
38939
  }
38813
38940
  const repo = o.repo ?? await resolveRepo();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.0.16",
3
+ "version": "4.0.17",
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",