@mutmutco/cli 4.5.149 → 4.5.151

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 +346 -215
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -16296,6 +16296,9 @@ function checkRunbookClaims(root, docs, deps) {
16296
16296
  }
16297
16297
  return findings;
16298
16298
  }
16299
+ function localRunbookBase(refExists) {
16300
+ return ["origin/development", "origin/main"].find(refExists);
16301
+ }
16299
16302
  function checkRunbookCoverage(docs, base, git3) {
16300
16303
  const changed = git3(["diff", "--name-only", `${base}...HEAD`]).split(/\r?\n/).filter(Boolean);
16301
16304
  const findings = [];
@@ -17122,9 +17125,6 @@ function suiteModuleSeeds(seeds, suite, deployModel) {
17122
17125
  "module/.npmrc": "suite-npmrc.template",
17123
17126
  "module/src/module.jsx": "suite-module.template.jsx",
17124
17127
  "module/vite.config.mjs": "suite-vite.template.mjs",
17125
- "module/screens/screens.json": "suite-screens.template.json",
17126
- "module/screens/files.json": "suite-files.template.json",
17127
- ".github/workflows/screens.yml": "suite-screens.template.yml",
17128
17128
  "docs/Guides/mm-work-module.md": "suite-guide.template.md"
17129
17129
  };
17130
17130
  return [
@@ -18666,10 +18666,10 @@ var rollout_plan_default = {
18666
18666
  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)."
18667
18667
  },
18668
18668
  baseline: {
18669
- version: "4.5.149",
18670
- tag: "v4.5.149",
18671
- commit: "b6fc6812e054",
18672
- npm: "@mutmutco/cli@4.5.149"
18669
+ version: "4.5.151",
18670
+ tag: "v4.5.151",
18671
+ commit: "c80d8f0f8b26",
18672
+ npm: "@mutmutco/cli@4.5.151"
18673
18673
  },
18674
18674
  exitCriterion: "fleet-n-of-n",
18675
18675
  hubOnlyShortcut: "forbidden",
@@ -18686,14 +18686,14 @@ var rollout_plan_default = {
18686
18686
  repo: "mutmutco/mmi-hub",
18687
18687
  role: "canary",
18688
18688
  schedule: "train",
18689
- v3Target: "v4.5.149"
18689
+ v3Target: "v4.5.151"
18690
18690
  }
18691
18691
  ],
18692
18692
  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.",
18693
18693
  rollback: {
18694
18694
  independent: true,
18695
- mechanism: "npm dist-tag latest -> 4.5.149 and redeploy the Hub Lambda from tag v4.5.149 (b6fc6812e054); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18696
- v3Target: "v4.5.149 (@mutmutco/cli@4.5.149, tag commit b6fc6812e054 \u2014 last known-good release carrying the repo-index v4-only contract)"
18695
+ mechanism: "npm dist-tag latest -> 4.5.151 and redeploy the Hub Lambda from tag v4.5.151 (c80d8f0f8b26); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18696
+ v3Target: "v4.5.151 (@mutmutco/cli@4.5.151, tag commit c80d8f0f8b26 \u2014 last known-good release carrying the repo-index v4-only contract)"
18697
18697
  }
18698
18698
  },
18699
18699
  {
@@ -35652,8 +35652,8 @@ function suiteModulePackageAccessCheck(suite, deployModel) {
35652
35652
  if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
35653
35653
  return {
35654
35654
  ok: true,
35655
- label: "mmwork-ds + mmwork-screens package access (manual org-admin step; unverified)",
35656
- detail: `An org admin must add this repository with Read under BOTH @mutmutco/mmwork-ds and @mutmutco/mmwork-screens \u2192 Package settings \u2192 Manage Actions access (#7984). No GitHub REST API can verify or grant this; without it the image build or the screens job fails with 403 read_package. Once screens is green, require it: mmi-cli oracle org project set <owner/repo> --var 'requiredChecks=["gate","screens / screens"]'.`
35655
+ label: "mmwork-ds package access (manual org-admin step; unverified)",
35656
+ detail: "An org admin must add this repository with Read under @mutmutco/mmwork-ds \u2192 Package settings \u2192 Manage Actions access (#7984). No GitHub REST API can verify or grant this; without it the image build fails with 403 read_package."
35657
35657
  };
35658
35658
  }
35659
35659
  function attachTrainDoctorVerdicts(report, trainDoctors) {
@@ -40299,7 +40299,8 @@ function boardAdvanceFailureMessage(result) {
40299
40299
  }
40300
40300
 
40301
40301
  // src/pr-land.ts
40302
- var PR_LAND_QUEUE_TIMEOUT_MS = 45 * 6e4;
40302
+ var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
40303
+ var PR_LAND_QUEUE_BATCH_SIZE = 5;
40303
40304
  function prHeadBehindBase(input) {
40304
40305
  if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
40305
40306
  return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
@@ -40353,6 +40354,12 @@ async function updatePrHeadFromBase(input) {
40353
40354
  throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
40354
40355
  }
40355
40356
  var PR_LAND_POLL_MS = 6e4;
40357
+ var PR_LAND_POLL_MAX_MS = 5 * 6e4;
40358
+ function prLandPollDelayMs(attempt, waiting) {
40359
+ const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
40360
+ const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
40361
+ return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
40362
+ }
40356
40363
  var PR_LAND_EJECT_CONFIRM_MS = 2e4;
40357
40364
  var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
40358
40365
  var PR_LAND_STATE_READ_RETRIES = 3;
@@ -40474,8 +40481,8 @@ async function runPrLand(prNumber, options, deps) {
40474
40481
  if (merge.mergeStatus === "merged") {
40475
40482
  return queueMerged(base, deps);
40476
40483
  }
40477
- const deadline = (deps.now ?? (() => Date.now()))() + PR_LAND_QUEUE_TIMEOUT_MS;
40478
- await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(PR_LAND_POLL_MS);
40484
+ const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
40485
+ await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
40479
40486
  return pollPrMergeQueue(prNumber, options, deps, base, void 0, deadline);
40480
40487
  }
40481
40488
  if (merge.mergeStatus === "merged") {
@@ -40514,7 +40521,9 @@ function queueMerged(base, deps) {
40514
40521
  async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
40515
40522
  const now = deps.now ?? (() => Date.now());
40516
40523
  const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
40517
- const deadline = deadlineMs ?? now() + PR_LAND_QUEUE_TIMEOUT_MS;
40524
+ const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
40525
+ const deadline = deadlineMs ?? now() + timeoutMs;
40526
+ let attempt = 0;
40518
40527
  while (now() < deadline) {
40519
40528
  const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
40520
40529
  snapshot = void 0;
@@ -40544,16 +40553,45 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
40544
40553
  deps.queueProgress?.(result2);
40545
40554
  return result2;
40546
40555
  }
40547
- await sleep2(PR_LAND_POLL_MS);
40556
+ await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
40548
40557
  }
40549
40558
  const result = {
40550
40559
  ...base,
40551
40560
  mergeQueue: { ...base.mergeQueue, state: "timeout" },
40552
- error: "merge queue did not reach MERGED within 45 minutes \u2014 rerun pr land to resume polling"
40561
+ error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
40553
40562
  };
40554
40563
  deps.queueProgress?.(result);
40555
40564
  return result;
40556
40565
  }
40566
+ function prLandOutcome(result) {
40567
+ if (result.status !== "failed" && result.status !== "queued") return "merged";
40568
+ if (result.status === "queued") return "queued";
40569
+ const state = result.mergeQueue?.state;
40570
+ return state === "ejected" || state === "timeout" ? state : "failed";
40571
+ }
40572
+ async function landMany(prs, landOne, cleanup, hooks) {
40573
+ const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
40574
+ return Promise.all(prs.map(async (pr) => {
40575
+ let result = await safe(pr);
40576
+ if (prLandOutcome(result) === "ejected") {
40577
+ hooks?.onRetry?.(pr, result);
40578
+ result = { ...await safe(pr), retriedAfterEjection: true };
40579
+ }
40580
+ const outcome = prLandOutcome(result);
40581
+ const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
40582
+ hooks?.onDone?.();
40583
+ return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
40584
+ }));
40585
+ }
40586
+ function renderPrLandTable(entries) {
40587
+ const rows = entries.map((e) => [
40588
+ `#${e.pr}`,
40589
+ e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
40590
+ [e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
40591
+ ]);
40592
+ const width = (i) => Math.max(...rows.map((r) => r[i].length));
40593
+ return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
40594
+ }
40557
40595
 
40558
40596
  // src/test-policy-core.ts
40559
40597
  var import_node_child_process13 = require("node:child_process");
@@ -40567,6 +40605,7 @@ var TRAILER_KEY = "Test-Policy-Override";
40567
40605
  var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
40568
40606
  var REC = "";
40569
40607
  var FLD = "";
40608
+ var GH_SQUASH_COAUTHORS_RE = /\r?\n(?:[ \t]*\r?\n)+---------\r?\n(?:[ \t]*\r?\n)+(?:Co-authored-by:[ \t]+\S[^\r\n]*(?:\r?\n|$))+[ \t\r\n]*$/i;
40570
40609
  var WAIVABLE_KINDS = [
40571
40610
  "mandatory-zone-untested",
40572
40611
  "unrequested-test-file",
@@ -40724,6 +40763,21 @@ function isShallowRepository(cwd) {
40724
40763
  return true;
40725
40764
  }
40726
40765
  }
40766
+ function readOverrideTrailer(trailer, body, cwd) {
40767
+ const direct = (trailer ?? "").trim();
40768
+ if (direct) return direct;
40769
+ const raw = body ?? "";
40770
+ const stripped = raw.replace(GH_SQUASH_COAUTHORS_RE, "\n");
40771
+ if (stripped === raw) return "";
40772
+ const prefix = `${TRAILER_KEY.toLowerCase()}:`;
40773
+ return (0, import_node_child_process12.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
40774
+ windowsHide: true,
40775
+ cwd,
40776
+ input: stripped,
40777
+ encoding: "utf8",
40778
+ maxBuffer: 32 * 1024 * 1024
40779
+ }).split(/\r?\n/).filter((line) => line.trim().toLowerCase().startsWith(prefix)).map((line) => line.trim().slice(prefix.length).trim()).join("\n").trim();
40780
+ }
40727
40781
  function readOverride(base, cwd) {
40728
40782
  const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
40729
40783
  const refusals = [];
@@ -40732,7 +40786,7 @@ function readOverride(base, cwd) {
40732
40786
  const [sha, trailer, body] = record.replace(/^\s+/, "").split(FLD);
40733
40787
  if (!sha) continue;
40734
40788
  if (override) break;
40735
- const value = (trailer ?? "").trim();
40789
+ const value = readOverrideTrailer(trailer, body, cwd);
40736
40790
  if (!value) {
40737
40791
  const shaped = OVERRIDE_RE.exec(body ?? "");
40738
40792
  if (shaped) refusals.push(invisibleTrailer(sha, shaped[0].trim()));
@@ -47257,205 +47311,246 @@ ${list}`);
47257
47311
  }
47258
47312
  class PrHeadBehindBaseError extends Error {
47259
47313
  }
47260
- 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("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").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").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
47261
- if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
47262
- try {
47263
- const parsed = parseIssueRef(number, o.repo);
47264
- number = String(parsed.number);
47265
- if (parsed.repo) o.repo = parsed.repo;
47266
- } catch (e) {
47267
- return fail(`pr land: ${e.message}`);
47268
- }
47269
- }
47270
- const repoArgs = o.repo ? ["--repo", o.repo] : [];
47271
- const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
47272
- assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
47273
- const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47274
- const landClosingGuardInput = await withAlreadyClosedCommitTargets(
47275
- await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
47276
- async (n) => {
47277
- if (!landRepoForGuard) return void 0;
47278
- const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
47279
- return typeof viewed.state === "string" ? viewed.state : void 0;
47280
- }
47281
- );
47282
- const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
47283
- warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
47284
- const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
47285
- force: o.force,
47286
- context: "pr land",
47287
- squashBodyText: landSquashBody
47288
- });
47289
- if (landClosingGuardVerdict.blocked) {
47290
- console.error(landClosingGuardVerdict.message);
47291
- process.exitCode = 1;
47292
- return;
47293
- }
47294
- if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
47295
- let mergeQueueEnabled = false;
47296
- const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly }, {
47297
- hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
47298
- readMergeQueue: readPrMergeQueue,
47299
- findMergeGroupRun: findPrMergeGroupRun,
47300
- queueProgress: (receipt) => {
47301
- if (!o.json) printLine(prMergeQueueLine(receipt));
47302
- },
47303
- resolveRepo: async (prNumber, repoOpt) => {
47304
- const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47305
- let repoFromGh = "";
47306
- let base = "";
47307
- if (knownRepo) {
47308
- const rested = await fetchRestPrSnapshot(prNumber, knownRepo).catch(() => void 0);
47309
- if (rested) {
47310
- console.warn("pr land: PR repo/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
47311
- repoFromGh = knownRepo;
47312
- base = rested.baseRef;
47313
- }
47314
- }
47315
- if (!repoFromGh || !base) {
47316
- const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
47317
- const readEnv = await ghReadEnv();
47318
- const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout.trim();
47319
- [repoFromGh, base] = viewed.split(/\s+/);
47320
- }
47321
- const repo = repoOpt ?? repoFromGh;
47322
- if (!repo) throw new Error("pr land: could not resolve PR repo");
47323
- let track;
47314
+ pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").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").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 the queue's own check timeout is 60)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").action(async (numbers, opts) => {
47315
+ const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
47316
+ if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
47317
+ const queueTimeoutMs = timeoutMinutes !== void 0 ? Math.round(timeoutMinutes * 6e4) : PR_LAND_QUEUE_TIMEOUT_MS * Math.ceil(numbers.length / PR_LAND_QUEUE_BATCH_SIZE);
47318
+ let inFlight = numbers.length;
47319
+ const landOne = async (numberArg) => {
47320
+ const o = { ...opts };
47321
+ let number = numberArg;
47322
+ const refused = (error) => ({ status: "failed", repo: o.repo ?? "", pr: number, base: "", error });
47323
+ if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
47324
47324
  try {
47325
- const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
47326
- if (meta) track = resolveReleaseTrack(meta, void 0, repo);
47327
- } catch {
47325
+ const parsed = parseIssueRef(number, o.repo);
47326
+ number = String(parsed.number);
47327
+ if (parsed.repo) o.repo = parsed.repo;
47328
+ } catch (e) {
47329
+ return refused(e.message);
47328
47330
  }
47329
- if (isPromotionBase(base, track)) {
47330
- const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
47331
- throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
47331
+ }
47332
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
47333
+ const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
47334
+ assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
47335
+ const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47336
+ const landClosingGuardInput = await withAlreadyClosedCommitTargets(
47337
+ await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
47338
+ async (n) => {
47339
+ if (!landRepoForGuard) return void 0;
47340
+ const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
47341
+ return typeof viewed.state === "string" ? viewed.state : void 0;
47332
47342
  }
47333
- return { repo, base };
47334
- },
47335
- fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
47336
- resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47337
- waitForChecks: async (prNumber, repo) => {
47338
- const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
47339
- return waitForPrChecks({
47340
- resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47341
- // #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
47342
- pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
47343
- // #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
47344
- // other base), so the fast-fail message's base branch is always 'development' here.
47345
- pollMergeable: () => pollRestPrMergeable(prNumber, repo),
47346
- pollRateLimit: () => waitLoopCorePool("pr land"),
47347
- // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
47348
- // then read its own wall-clock kill as a broken diff.
47349
- diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
47350
- checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
47351
- checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
47352
- // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
47353
- pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
47354
- baseBranch: "development",
47355
- sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
47356
- log: (message2) => console.warn(message2),
47357
- // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
47358
- // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
47359
- progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
47360
- });
47361
- },
47362
- // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
47363
- // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
47364
- // #4396: same required-status scoping as the wait loop above — a non-required red must not read
47365
- // as "not ready to retry" any more than it should have blocked the wait itself.
47366
- probeMergeReady: async (prNumber, repo) => {
47367
- const [snapshot, requiredContexts] = await Promise.all([
47368
- fetchRestPrSnapshot(prNumber, repo).catch(() => null),
47369
- // io-census-allow: a failed re-probe reads as open:false/mergeable:false below, the conservative 'not ready to retry' direction per the comment above
47370
- fetchRequiredCheckContexts(repo, "development").catch(() => null)
47371
- // io-census-allow: null is the established failed-state for this read (see pr-checks-rest.ts's partitionByRequired contract) — it makes every check required, the conservative direction
47372
- ]);
47373
- const checks = await pollRestPrChecks(prNumber, repo, void 0, requiredContexts).catch(() => "error");
47374
- return {
47375
- open: snapshot?.state === "open",
47376
- mergeable: snapshot?.mergeable === "MERGEABLE",
47377
- checksPassing: checks === "success" || checks === "no-checks-reported"
47378
- };
47379
- },
47380
- readPrState: async (prNumber, repo) => {
47381
- const args = repo ? ["--repo", repo] : [];
47382
- const readEnv = await ghReadEnv();
47383
- const result2 = await readGhPrStateWithRetry(async () => (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout);
47384
- if (!result2.ok) {
47385
- if (isGitHubRateLimitError(result2.error)) return fetchRestPrLandState(prNumber, repo);
47386
- throw new Error(`could not read PR state: ${result2.error}`);
47387
- }
47388
- return JSON.parse(result2.state);
47389
- },
47390
- mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
47391
- allowedClosing: landClosingGuardInput?.closing,
47392
- bodyText: landSquashBody,
47393
- mergeQueue: mergeQueueEnabled
47394
- }),
47395
- updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
47396
- pollMerged: async (prNumber, repo, deadlineMs) => {
47397
- let lastFailure;
47398
- while (Date.now() < deadlineMs) {
47399
- const read = await pollRestPrMerged(prNumber, repo);
47400
- if (read.state === "merged") return true;
47401
- if (read.state === "failed") {
47402
- lastFailure = read.error;
47403
- console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
47404
- } else {
47405
- lastFailure = void 0;
47343
+ );
47344
+ const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
47345
+ warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
47346
+ const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
47347
+ force: o.force,
47348
+ context: "pr land",
47349
+ squashBodyText: landSquashBody
47350
+ });
47351
+ if (landClosingGuardVerdict.blocked) {
47352
+ console.error(landClosingGuardVerdict.message);
47353
+ return refused("refused by the closing-keyword guard (see the message above)");
47354
+ }
47355
+ if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
47356
+ let mergeQueueEnabled = false;
47357
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly, queueTimeoutMs }, {
47358
+ pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
47359
+ hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
47360
+ readMergeQueue: readPrMergeQueue,
47361
+ findMergeGroupRun: findPrMergeGroupRun,
47362
+ queueProgress: (receipt) => {
47363
+ if (!o.json) printLine(prMergeQueueLine(receipt));
47364
+ },
47365
+ resolveRepo: async (prNumber, repoOpt) => {
47366
+ const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47367
+ let repoFromGh = "";
47368
+ let base = "";
47369
+ if (knownRepo) {
47370
+ const rested = await fetchRestPrSnapshot(prNumber, knownRepo).catch(() => void 0);
47371
+ if (rested) {
47372
+ console.warn("pr land: PR repo/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
47373
+ repoFromGh = knownRepo;
47374
+ base = rested.baseRef;
47375
+ }
47376
+ }
47377
+ if (!repoFromGh || !base) {
47378
+ const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
47379
+ const readEnv = await ghReadEnv();
47380
+ const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout.trim();
47381
+ [repoFromGh, base] = viewed.split(/\s+/);
47382
+ }
47383
+ const repo = repoOpt ?? repoFromGh;
47384
+ if (!repo) throw new Error("pr land: could not resolve PR repo");
47385
+ let track;
47386
+ try {
47387
+ const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
47388
+ if (meta) track = resolveReleaseTrack(meta, void 0, repo);
47389
+ } catch {
47406
47390
  }
47407
- await new Promise((resolve8) => setTimeout(resolve8, PR_LAND_POLL_MS));
47391
+ if (isPromotionBase(base, track)) {
47392
+ const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
47393
+ throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
47394
+ }
47395
+ return { repo, base };
47396
+ },
47397
+ fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
47398
+ resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47399
+ waitForChecks: async (prNumber, repo) => {
47400
+ const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
47401
+ return waitForPrChecks({
47402
+ resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47403
+ // #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
47404
+ pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
47405
+ // #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
47406
+ // other base), so the fast-fail message's base branch is always 'development' here.
47407
+ pollMergeable: () => pollRestPrMergeable(prNumber, repo),
47408
+ pollRateLimit: () => waitLoopCorePool("pr land"),
47409
+ // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
47410
+ // then read its own wall-clock kill as a broken diff.
47411
+ diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
47412
+ checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
47413
+ checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
47414
+ // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
47415
+ pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
47416
+ baseBranch: "development",
47417
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
47418
+ log: (message2) => console.warn(message2),
47419
+ // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
47420
+ // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
47421
+ progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
47422
+ });
47423
+ },
47424
+ // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
47425
+ // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
47426
+ // #4396: same required-status scoping as the wait loop above — a non-required red must not read
47427
+ // as "not ready to retry" any more than it should have blocked the wait itself.
47428
+ probeMergeReady: async (prNumber, repo) => {
47429
+ const [snapshot, requiredContexts] = await Promise.all([
47430
+ fetchRestPrSnapshot(prNumber, repo).catch(() => null),
47431
+ // io-census-allow: a failed re-probe reads as open:false/mergeable:false below, the conservative 'not ready to retry' direction per the comment above
47432
+ fetchRequiredCheckContexts(repo, "development").catch(() => null)
47433
+ // io-census-allow: null is the established failed-state for this read (see pr-checks-rest.ts's partitionByRequired contract) — it makes every check required, the conservative direction
47434
+ ]);
47435
+ const checks = await pollRestPrChecks(prNumber, repo, void 0, requiredContexts).catch(() => "error");
47436
+ return {
47437
+ open: snapshot?.state === "open",
47438
+ mergeable: snapshot?.mergeable === "MERGEABLE",
47439
+ checksPassing: checks === "success" || checks === "no-checks-reported"
47440
+ };
47441
+ },
47442
+ readPrState: async (prNumber, repo) => {
47443
+ const args = repo ? ["--repo", repo] : [];
47444
+ const readEnv = await ghReadEnv();
47445
+ const result2 = await readGhPrStateWithRetry(async () => (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout);
47446
+ if (!result2.ok) {
47447
+ if (isGitHubRateLimitError(result2.error)) return fetchRestPrLandState(prNumber, repo);
47448
+ throw new Error(`could not read PR state: ${result2.error}`);
47449
+ }
47450
+ return JSON.parse(result2.state);
47451
+ },
47452
+ mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
47453
+ allowedClosing: landClosingGuardInput?.closing,
47454
+ bodyText: landSquashBody,
47455
+ mergeQueue: mergeQueueEnabled
47456
+ }),
47457
+ updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
47458
+ pollMerged: async (prNumber, repo, deadlineMs) => {
47459
+ let lastFailure;
47460
+ while (Date.now() < deadlineMs) {
47461
+ const read = await pollRestPrMerged(prNumber, repo);
47462
+ if (read.state === "merged") return true;
47463
+ if (read.state === "failed") {
47464
+ lastFailure = read.error;
47465
+ console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
47466
+ } else {
47467
+ lastFailure = void 0;
47468
+ }
47469
+ await new Promise((resolve8) => setTimeout(resolve8, PR_LAND_POLL_MS));
47470
+ }
47471
+ if (lastFailure) {
47472
+ throw new Error(
47473
+ `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
47474
+ );
47475
+ }
47476
+ return false;
47408
47477
  }
47409
- if (lastFailure) {
47410
- throw new Error(
47411
- `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
47412
- );
47478
+ });
47479
+ if (result.status !== "failed" && result.status !== "queued") {
47480
+ const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
47481
+ const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
47482
+ const recovery = result.repo ? buildPostMergeReconRecovery({
47483
+ repo: result.repo,
47484
+ pr: number,
47485
+ boardAdvance,
47486
+ crossRepoFilingIssue
47487
+ }) : void 0;
47488
+ let recoveryPath;
47489
+ if (recovery) {
47490
+ try {
47491
+ recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
47492
+ } catch {
47493
+ }
47494
+ } else if (result.repo) {
47495
+ clearPostMergeReconRecovery(process.cwd(), result.repo, number);
47496
+ }
47497
+ Object.assign(result, {
47498
+ ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
47499
+ boardAdvanceStatus: boardAdvance.status,
47500
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
47501
+ ...recovery ? { postMergeReconRecovery: recovery } : {}
47502
+ });
47503
+ for (const line of postMergeReconWarnings({
47504
+ context: "pr land",
47505
+ boardAdvance,
47506
+ crossRepoFilingIssue,
47507
+ recovery,
47508
+ recoveryPath
47509
+ })) {
47510
+ console.error(line);
47413
47511
  }
47414
- return false;
47415
47512
  }
47416
- });
47417
- if (result.status !== "failed" && result.status !== "queued") {
47418
- const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
47419
- const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
47420
- const recovery = result.repo ? buildPostMergeReconRecovery({
47421
- repo: result.repo,
47422
- pr: number,
47423
- boardAdvance,
47424
- crossRepoFilingIssue
47425
- }) : void 0;
47426
- let recoveryPath;
47427
- if (recovery) {
47428
- try {
47429
- recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
47430
- } catch {
47431
- }
47432
- } else if (result.repo) {
47433
- clearPostMergeReconRecovery(process.cwd(), result.repo, number);
47513
+ return result;
47514
+ };
47515
+ const cwd0 = process.cwd();
47516
+ const primaryRoot0 = (await execFileP("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.match(/^worktree (.+)$/m)?.[1];
47517
+ const cleanupMerged = async (r) => {
47518
+ if (opts.preserveWorktree) return "cleanup skipped (--preserve-worktree)";
47519
+ const ran = await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(["pr", "merge", r.pr, "--repo", r.repo])], {
47520
+ cwd: (0, import_node_fs45.existsSync)(cwd0) ? cwd0 : primaryRoot0,
47521
+ timeout: GH_MUTATION_TIMEOUT_MS * 3
47522
+ }).then((x) => ({ stdout: x.stdout, stderr: x.stderr }), (e) => ({ stdout: e.stdout ?? "", stderr: e.stderr ?? "" }));
47523
+ const json = ran.stdout.split("\n").reverse().find((l) => l.startsWith("{"));
47524
+ if (!json) return `cleanup failed: ${ran.stderr.trim().split("\n").pop() || "no receipt"}`;
47525
+ const c = JSON.parse(json);
47526
+ const part = (x) => x ? `${x.status}${x.reason ? ` (${x.reason})` : ""}` : "n/a";
47527
+ return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
47528
+ };
47529
+ const entries = await landMany(numbers, landOne, cleanupMerged, {
47530
+ onRetry: (pr2, r) => console.warn(`pr land: queue ejected PR #${pr2} (${r.error}) \u2014 landing it once more (head updated from base if behind)`),
47531
+ onDone: () => {
47532
+ inFlight = Math.max(1, inFlight - 1);
47434
47533
  }
47435
- Object.assign(result, {
47436
- ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
47437
- boardAdvanceStatus: boardAdvance.status,
47438
- ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
47439
- ...recovery ? { postMergeReconRecovery: recovery } : {}
47440
- });
47441
- for (const line of postMergeReconWarnings({
47442
- context: "pr land",
47443
- boardAdvance,
47444
- crossRepoFilingIssue,
47445
- recovery,
47446
- recoveryPath
47447
- })) {
47448
- console.error(line);
47534
+ });
47535
+ const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
47536
+ if (numbers.length === 1) {
47537
+ const { result, cleanup } = entries[0];
47538
+ if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
47539
+ else {
47540
+ printLine(
47541
+ `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
47542
+ );
47543
+ if (cleanup) printLine(`pr land: ${cleanup}`);
47449
47544
  }
47545
+ } else if (opts.json) {
47546
+ printLine(JSON.stringify({ results: entries.map((e) => ({ ...e.result, outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
47547
+ } else {
47548
+ printLine(renderPrLandTable(entries));
47549
+ printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
47450
47550
  }
47451
- const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
47452
- if (o.json) printLine(JSON.stringify({ ...result, credential }));
47453
- else printLine(
47454
- `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
47455
- );
47456
- if (result.status === "failed") process.exitCode = 1;
47551
+ if (entries.some((e) => e.outcome !== "merged" && e.outcome !== "queued")) process.exitCode = 1;
47457
47552
  });
47458
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge 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").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
47553
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679); on a merge-queue base use `pr land <n> [<n>...]` instead (#8322) \u2014 this verb is refused there except for train PRs").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge 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").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
47459
47554
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
47460
47555
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
47461
47556
  if (o.disableAuto) {
@@ -47697,7 +47792,8 @@ ${list}`);
47697
47792
  });
47698
47793
  try {
47699
47794
  try {
47700
- await mergeOnce();
47795
+ if (prMeta.state === "MERGED") remoteNotAttemptedReason = "pr-already-merged";
47796
+ else await mergeOnce();
47701
47797
  } catch (e) {
47702
47798
  if (!(e instanceof PrHeadBehindBaseError)) throw e;
47703
47799
  const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
@@ -53317,27 +53413,51 @@ var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FR
53317
53413
  var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
53318
53414
  var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
53319
53415
  var ACTIONS_RUNS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs$`);
53416
+ var RUNS_QUERY_VALUE = {
53417
+ per_page: /^(?:[1-9]\d?|100)$/,
53418
+ page: /^[1-9]\d{0,5}$/,
53419
+ status: /^[A-Za-z_]+$/,
53420
+ branch: /^[A-Za-z0-9_.\/-]+$/,
53421
+ event: /^[A-Za-z_]+$/,
53422
+ head_sha: /^[0-9a-fA-F]{40}$/,
53423
+ created: /^[0-9:TZ.<>=-]+$/,
53424
+ actor: /^[A-Za-z0-9_.\[\]-]+$/
53425
+ };
53426
+ function validRunsQuery(query) {
53427
+ const seen = /* @__PURE__ */ new Set();
53428
+ for (const pair of query.split("&")) {
53429
+ const eq = pair.indexOf("=");
53430
+ if (eq < 1) return false;
53431
+ const key = pair.slice(0, eq);
53432
+ const value = pair.slice(eq + 1);
53433
+ const re = Object.prototype.hasOwnProperty.call(RUNS_QUERY_VALUE, key) ? RUNS_QUERY_VALUE[key] : void 0;
53434
+ if (!re || seen.has(key) || !re.test(value)) return false;
53435
+ seen.add(key);
53436
+ }
53437
+ return true;
53438
+ }
53439
+ var JSON_MAX_BUFFER = 64 * 1024 * 1024;
53320
53440
  var ACTIONS_RUN_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs/\\d+$`);
53321
53441
  var ACTIONS_JOBS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs/\\d+(?:/attempts/\\d+)?/jobs$`);
53322
53442
  var ACTIONS_ARTIFACTS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs/\\d+/artifacts$`);
53323
53443
  var ACTIONS_ARTIFACT_ZIP_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/artifacts/\\d+/zip$`);
53324
53444
  var ACTIONS_JOB_LOGS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/jobs/\\d+/logs$`);
53325
- var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs, repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>)";
53445
+ var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>)";
53326
53446
  function parseApiGetPath(rawPath) {
53327
53447
  const path2 = rawPath.trim();
53328
53448
  const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
53329
53449
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
53450
+ const q = path2.indexOf("?");
53451
+ const base = q === -1 ? path2 : path2.slice(0, q);
53452
+ const query = q === -1 ? "" : path2.slice(q + 1);
53330
53453
  if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
53331
53454
  if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
53332
- if (ACTIONS_RUNS_RE.test(path2)) return { kind: "actions-runs", path: path2, paginate: true };
53455
+ if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
53333
53456
  if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
53334
53457
  if (ACTIONS_JOBS_RE.test(path2)) return { kind: "actions-jobs", path: path2, paginate: true };
53335
53458
  if (ACTIONS_ARTIFACTS_RE.test(path2)) return { kind: "actions-artifacts", path: path2, paginate: true };
53336
53459
  if (ACTIONS_ARTIFACT_ZIP_RE.test(path2)) return { kind: "actions-artifact-zip", path: path2, paginate: false };
53337
53460
  if (ACTIONS_JOB_LOGS_RE.test(path2)) return { kind: "actions-job-logs", path: path2, paginate: false };
53338
- const q = path2.indexOf("?");
53339
- const base = q === -1 ? path2 : path2.slice(0, q);
53340
- const query = q === -1 ? "" : path2.slice(q + 1);
53341
53461
  if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
53342
53462
  return { kind: "contents", path: path2, paginate: false };
53343
53463
  }
@@ -53356,7 +53476,7 @@ function buildApiGetArgs(path2, accept) {
53356
53476
  async function runApiGet(run, path2, accept) {
53357
53477
  return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
53358
53478
  }
53359
- var apiReadRunner = async (args, timeoutMs, env, binary) => (await execFileP("gh", args, { timeout: timeoutMs, ...binary ? { encoding: "latin1", maxBuffer: ZIP_MAX_BUFFER } : {}, ...env ? { env } : {} })).stdout;
53479
+ var apiReadRunner = async (args, timeoutMs, env, binary) => (await execFileP("gh", args, { timeout: timeoutMs, ...binary ? { encoding: "latin1", maxBuffer: ZIP_MAX_BUFFER } : { maxBuffer: JSON_MAX_BUFFER }, ...env ? { env } : {} })).stdout;
53360
53480
  async function readAppApi(path2, runner = apiReadRunner) {
53361
53481
  const req = parseApiGetPath(path2);
53362
53482
  requireBrokerApp("read run log");
@@ -55803,8 +55923,19 @@ function registerDeveloperCommands(program3) {
55803
55923
  await failGraceful(e.message);
55804
55924
  }
55805
55925
  });
55806
- docs.command("refs").description("deterministic doc reference gate: repo paths, markdown links, mmi-cli command refs, and pins across docs, org-guide, mmi-help, README and architecture must resolve, or exit 1").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).option("--base <ref>", "require covered runbook sections to be touched against this merge base (#8126)").option("--runbooks-only", "check only operator runbooks (#8126)").option("--live", "check operator runbooks against registry tasks, vault names and HTTPS endpoints (implies --runbooks-only)").action(async (o) => {
55926
+ docs.command("refs").description("deterministic doc reference gate: repo paths, markdown links, mmi-cli command refs, and pins across docs, org-guide, mmi-help, README and architecture must resolve, or exit 1").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).option("--base <ref>", "require covered runbook sections to be touched against this merge base (#8126)").option("--runbooks-only", "check only operator runbooks (#8126)").option("--live", "check operator runbooks against registry tasks, vault names and HTTPS endpoints (implies --runbooks-only)").option("--local", "pre-push mode (#8323): the CI runbook guard against origin/development (else origin/main) \u2014 implies --runbooks-only, offline, same verdict as the gate step").action(async (o) => {
55807
55927
  try {
55928
+ if (o.local) {
55929
+ o.runbooksOnly = true;
55930
+ if (!o.base) {
55931
+ const exists = /* @__PURE__ */ new Set();
55932
+ for (const ref of ["origin/development", "origin/main"]) {
55933
+ if (await execFileP("git", ["rev-parse", "--verify", "--quiet", ref], { timeout: GIT_TIMEOUT_MS }).then(() => true, () => false)) exists.add(ref);
55934
+ }
55935
+ o.base = localRunbookBase((ref) => exists.has(ref));
55936
+ if (!o.base) return failGraceful("docs refs --local: neither origin/development nor origin/main exists \u2014 run `git fetch origin` or pass --base <ref>");
55937
+ }
55938
+ }
55808
55939
  if (o.repo) {
55809
55940
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
55810
55941
  if (!guard.ok) return failGraceful(guard.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.149",
3
+ "version": "4.5.151",
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",