@mutmutco/cli 4.6.8 → 4.6.10

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.
package/dist/main.cjs CHANGED
@@ -1236,7 +1236,7 @@ var init_house_map = __esm({
1236
1236
  api: "devops",
1237
1237
  // allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
1238
1238
  run: "devops",
1239
- // #9479: one named workflow run — currently the bounded `run cancel <id>` write
1239
+ // #9479: one named workflow run — the bounded `run cancel <id>` and `run rerun <id>` writes
1240
1240
  ci: "devops",
1241
1241
  // the CI/gate audit
1242
1242
  jev: "devops",
@@ -15347,7 +15347,12 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15347
15347
  ["ci cancel-orphans", { metadata: "read", actions: "write" }],
15348
15348
  // #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
15349
15349
  ["run cancel", { metadata: "read", actions: "write" }],
15350
+ // #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
15351
+ ["run rerun", { metadata: "read", actions: "write" }],
15350
15352
  ["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
15353
+ // #9614: closing an issue is an issue-state write — it rides the org App (mmi-github-app), so the close
15354
+ // is attributed to the App instead of the personal account. Nothing broader than issues.
15355
+ ["issue close", { metadata: "read", issues: "write" }],
15351
15356
  // #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
15352
15357
  ["issue discover-related", { metadata: "read", issues: "write" }]
15353
15358
  ]);
@@ -15736,6 +15741,13 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
15736
15741
  ].join(" ")
15737
15742
  };
15738
15743
  }
15744
+ function pendingEvidenceItems(evidence) {
15745
+ const items = evidence.checks.filter((check) => check.state === "pending").map((check) => `${check.name} (${check.status ?? "unknown"}${check.conclusion ? `/${check.conclusion}` : ""}, startedAt=${check.startedAt ?? "null"}, appId=${check.appId ?? "null"})`);
15746
+ for (const run of evidence.workflows) {
15747
+ if (run.status !== "completed") items.push(`${run.name}#${run.id} (${run.status})`);
15748
+ }
15749
+ return items;
15750
+ }
15739
15751
  async function waitForPrChecks(deps) {
15740
15752
  let { policy, reason } = await deps.resolvePolicy();
15741
15753
  const baseBranch = deps.baseBranch ?? "development";
@@ -15766,10 +15778,10 @@ async function waitForPrChecks(deps) {
15766
15778
  const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
15767
15779
  const started = now();
15768
15780
  const deadline = started + timeoutMs;
15769
- const report = (state) => {
15781
+ const report = (state, detail) => {
15770
15782
  try {
15771
15783
  const elapsedMs = now() - started;
15772
- deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
15784
+ deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()), ...detail === void 0 ? {} : { detail } });
15773
15785
  } catch {
15774
15786
  }
15775
15787
  };
@@ -15779,6 +15791,10 @@ async function waitForPrChecks(deps) {
15779
15791
  let zeroRunsStreak = 0;
15780
15792
  let evidenceHeadSha;
15781
15793
  let lastEvidence;
15794
+ const pendingWaitDetail = (evidence) => {
15795
+ const items = pendingEvidenceItems(evidence);
15796
+ return items.length ? `pending (${items.join(", ")})` : void 0;
15797
+ };
15782
15798
  report("starting");
15783
15799
  while (now() < deadline) {
15784
15800
  if (deps.pollRateLimit) {
@@ -15843,7 +15859,7 @@ async function waitForPrChecks(deps) {
15843
15859
  await deps.sleep(waitMs);
15844
15860
  continue;
15845
15861
  }
15846
- report(state);
15862
+ report(state, state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0);
15847
15863
  if (state !== "success") successStreak = 0;
15848
15864
  if (state !== "failure") failureStreak = 0;
15849
15865
  if (state !== "no-checks-reported") zeroRunsStreak = 0;
@@ -15919,10 +15935,18 @@ async function waitForPrChecks(deps) {
15919
15935
  await deps.sleep(pollIntervalMs(now() - started));
15920
15936
  continue;
15921
15937
  }
15922
- lastDetail = state;
15938
+ const pendingDetail = state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0;
15939
+ lastDetail = pendingDetail ?? state;
15923
15940
  await deps.sleep(pollIntervalMs(now() - started));
15924
15941
  }
15925
- return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
15942
+ return {
15943
+ policy,
15944
+ status: "timeout",
15945
+ reason,
15946
+ detail: lastDetail,
15947
+ waitedMs: now() - started,
15948
+ ...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
15949
+ };
15926
15950
  }
15927
15951
 
15928
15952
  // src/release-auxiliary-workflows.ts
@@ -16392,16 +16416,23 @@ async function fetchHeadCheckRuns(headSha, repo, gh2) {
16392
16416
  const runsOut = await gh2(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url, started_at, completed_at}"]);
16393
16417
  return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
16394
16418
  }
16395
- async function fetchHeadCheckEntries(headSha, repo, gh2) {
16419
+ async function fetchHeadCheckEntriesWithRuns(headSha, repo, gh2) {
16396
16420
  const [runs2, statusesOut] = await Promise.all([
16397
16421
  fetchHeadCheckRuns(headSha, repo, gh2),
16398
16422
  gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
16399
16423
  ]);
16400
16424
  const statuses = parseNdjsonLines(statusesOut);
16401
- return [
16402
- ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
16403
- ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
16404
- ];
16425
+ return {
16426
+ runs: runs2,
16427
+ statuses,
16428
+ entries: [
16429
+ ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
16430
+ ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
16431
+ ]
16432
+ };
16433
+ }
16434
+ async function fetchHeadCheckEntries(headSha, repo, gh2) {
16435
+ return (await fetchHeadCheckEntriesWithRuns(headSha, repo, gh2)).entries;
16405
16436
  }
16406
16437
  async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredContexts) {
16407
16438
  const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
@@ -16409,38 +16440,68 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
16409
16440
  const { relevant } = partitionByRequired(entries, requiredContexts);
16410
16441
  return derivePollState(relevant);
16411
16442
  }
16412
- async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
16413
- const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
16414
- const [entries, rawRuns] = await Promise.all([
16415
- fetchHeadCheckEntries(snapshot.headSha, repo, gh2),
16416
- gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(snapshot.headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
16443
+ async function pollRestHeadChecksWithWorkflowEvidence(headSha, repo, gh2) {
16444
+ const [{ runs: runs2, statuses }, rawRuns] = await Promise.all([
16445
+ fetchHeadCheckEntriesWithRuns(headSha, repo, gh2),
16446
+ gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
16417
16447
  ]);
16418
- const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
16419
16448
  const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
16420
- if (run.head_sha !== snapshot.headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
16449
+ if (run.head_sha !== headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
16421
16450
  if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
16422
16451
  return [{
16423
16452
  id: run.id,
16424
16453
  name: run.name,
16425
- headSha: snapshot.headSha,
16454
+ headSha,
16426
16455
  status: run.status,
16427
16456
  conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
16428
16457
  ...typeof run.html_url === "string" ? { url: run.html_url } : {}
16429
16458
  }];
16430
16459
  });
16431
- const workflowBuckets = workflows.map((run) => {
16460
+ const workflowById = new Map(workflows.map((run) => [run.id, run]));
16461
+ const isStalePendingCheckRun = (run) => {
16462
+ if (classifyCheckRun(run) !== "pending") return false;
16463
+ const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(run.details_url ?? "");
16464
+ const owning = match ? workflowById.get(Number(match[1])) : void 0;
16465
+ if (!owning) return false;
16466
+ if (owning.status === "completed") return true;
16467
+ return workflows.some((newer) => newer.id > owning.id && newer.name === owning.name && newer.status === "completed");
16468
+ };
16469
+ const checks = [
16470
+ ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).filter((run) => !isStalePendingCheckRun(run)).map((run) => ({
16471
+ name: run.name ?? `check-run ${run.id ?? "unknown"}`,
16472
+ state: classifyCheckRun(run),
16473
+ ...run.status !== void 0 ? { status: run.status } : {},
16474
+ conclusion: run.conclusion ?? null,
16475
+ startedAt: run.started_at ?? null,
16476
+ appId: run.app_id ?? null
16477
+ })),
16478
+ ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", state: classifyCommitStatus(s.state) }))
16479
+ ];
16480
+ const newestRunByName = /* @__PURE__ */ new Map();
16481
+ for (const run of workflows) {
16482
+ const current = newestRunByName.get(run.name);
16483
+ if (!current || run.id > current.id) newestRunByName.set(run.name, run);
16484
+ }
16485
+ const workflowBuckets = [...newestRunByName.values()].map((run) => {
16432
16486
  if (run.status !== "completed") return "pending";
16433
16487
  return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
16434
16488
  });
16435
16489
  return {
16436
16490
  state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
16437
- headSha: snapshot.headSha,
16491
+ headSha,
16438
16492
  checks,
16439
16493
  workflows
16440
16494
  };
16441
16495
  }
16496
+ async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
16497
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
16498
+ return pollRestHeadChecksWithWorkflowEvidence(snapshot.headSha, repo, gh2);
16499
+ }
16500
+ async function pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2 = defaultGhApi) {
16501
+ return pollRestHeadChecksWithWorkflowEvidence(sha, repo, gh2);
16502
+ }
16442
16503
  async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
16443
- return derivePollState(partitionByRequired(await fetchHeadCheckEntries(sha, repo, gh2), null).relevant);
16504
+ return (await pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2)).state;
16444
16505
  }
16445
16506
  var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
16446
16507
  var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
@@ -21016,8 +21077,9 @@ function resolveSpawnTarget(command, args, platform2 = process.platform) {
21016
21077
  if (platform2 !== "win32") return { cmd: command, args };
21017
21078
  for (const t of [command, ...args]) {
21018
21079
  if (/[\r\n]/.test(t)) {
21080
+ const preview = JSON.stringify(t.length > 80 ? `${t.slice(0, 77)}...` : t);
21019
21081
  throw new Error(
21020
- `cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${JSON.stringify(t)}`
21082
+ `cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${preview} \u2014 write the multiline script to a git-ignored file and run \`mmi-cli vault secrets use <KEY> -- node .jerv/tmp/<script>.mjs\` (#6048)`
21021
21083
  );
21022
21084
  }
21023
21085
  }
@@ -22317,8 +22379,22 @@ function renderBoardItem(item) {
22317
22379
  }
22318
22380
  return lines2.join("\n");
22319
22381
  }
22382
+ function renderCachedBoardSource(report) {
22383
+ if (typeof report.stale?.ageMs === "number") {
22384
+ const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
22385
+ return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
22386
+ }
22387
+ if (report.builtAt) {
22388
+ const builtMs = Date.parse(report.builtAt);
22389
+ if (Number.isFinite(builtMs)) {
22390
+ return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
22391
+ }
22392
+ return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
22393
+ }
22394
+ return "source: cache";
22395
+ }
22320
22396
  function renderBoardReport(report) {
22321
- const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? "source: cache" : renderBoardSource()];
22397
+ const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
22322
22398
  if (report.github?.rateLimit) {
22323
22399
  const { limit, remaining, cost, resetAt } = report.github.rateLimit;
22324
22400
  lines2.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
@@ -23574,7 +23650,7 @@ var contestedBoardReadCommand = (repo) => `mmi-cli oracle board read --json${rep
23574
23650
  function contestedClaimRecovery(ref) {
23575
23651
  const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
23576
23652
  const boardRead = contestedBoardReadCommand(repo);
23577
- return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force; list free items with: ${boardRead} and pick from its claimable list`;
23653
+ return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force`;
23578
23654
  }
23579
23655
  function laneContestMessage(ref, contest, verb) {
23580
23656
  const { holder, why } = laneContestParts(contest);
@@ -23592,6 +23668,7 @@ function ghError(e) {
23592
23668
  init_github_client();
23593
23669
  init_github_quota();
23594
23670
  var CLAIM_CONCURRENCY = 5;
23671
+ var VERIFIED_FREE_CANDIDATE_LIMIT = 5;
23595
23672
  function withGatedWriteChainHint(message2) {
23596
23673
  return `${message2}
23597
23674
  note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
@@ -23852,9 +23929,16 @@ async function claimOneBoardItem(ctx, selector, options) {
23852
23929
  const { holder: holder2, why } = laneContestParts(contest);
23853
23930
  throw new Error(isUnknownHold(contest) ? `${item.ref} cannot be verified as free: ${holder2} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `${item.ref} is already claimed by ${holder2} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}`);
23854
23931
  }
23855
- const candidateRefs = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).map((candidate) => candidate.ref);
23856
- const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
23857
- const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
23932
+ const candidates = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).slice(0, VERIFIED_FREE_CANDIDATE_LIMIT);
23933
+ const verifiedFreeRefs = [];
23934
+ for (const candidate of candidates) {
23935
+ const candidateContest = await checkLaneContest(client, candidate, ctx.session, report.viewer, ctx.appClient, ctx.landing).catch(() => void 0);
23936
+ if (candidateContest && !candidateContest.contested && candidateContest.unverifiable.length === 0) {
23937
+ verifiedFreeRefs.push(candidate.ref);
23938
+ }
23939
+ }
23940
+ const alternatives = verifiedFreeRefs.length ? `Next verified free item: \`mmi-cli oracle board claim ${verifiedFreeRefs[0]}\`${verifiedFreeRefs.length > 1 ? ` (also free: ${verifiedFreeRefs.slice(1).join(", ")})` : ""}` : `Verified free items in this read: none \u2014 refresh \`mmi-cli oracle board read --json --repo ${report.repo}\` or coordinate a handoff`;
23941
+ const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}`;
23858
23942
  throw new Error(withGatedWriteChainHint(refusal));
23859
23943
  }
23860
23944
  previousHolder = displaced();
@@ -25039,10 +25123,15 @@ function checkRunbookCoverage(docs, base, git3) {
25039
25123
  const heading = headings[i];
25040
25124
  const end = headings.slice(i + 1).find((h) => h.level <= heading.level)?.start ?? lines2.length + 1;
25041
25125
  const markerEnd = headings[i + 1]?.start ?? end;
25042
- const globs = lines2.slice(heading.start, markerEnd - 1).flatMap((text) => /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim()) ?? []);
25126
+ const markers = lines2.slice(heading.start, markerEnd - 1).flatMap((text, j) => {
25127
+ const found = /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim());
25128
+ return found ? [{ line: heading.start + 1 + j, globs: found }] : [];
25129
+ });
25130
+ const globs = markers.flatMap((m) => m.globs);
25043
25131
  const affected = changed.filter((path2) => globs.some((glob) => matches(glob, path2)) && !changesOnlyUses(path2));
25044
25132
  if (affected.length && !ranges.some((r) => r.count > 0 && r.start < end && r.start + r.count > heading.start)) {
25045
- findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
25133
+ const markerLine = markers.find((m) => m.globs.some((glob) => affected.some((path2) => matches(glob, path2)))).line;
25134
+ findings.push({ kind: "runbook-section-stale", doc, line: markerLine, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}, covers marker at line ${markerLine}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
25046
25135
  }
25047
25136
  }
25048
25137
  }
@@ -27506,10 +27595,10 @@ var rollout_plan_default = {
27506
27595
  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)."
27507
27596
  },
27508
27597
  baseline: {
27509
- version: "4.6.8",
27510
- tag: "v4.6.8",
27511
- commit: "0dd075a0d2c1",
27512
- npm: "@mutmutco/cli@4.6.8"
27598
+ version: "4.6.10",
27599
+ tag: "v4.6.10",
27600
+ commit: "a4bbe43232e9",
27601
+ npm: "@mutmutco/cli@4.6.10"
27513
27602
  },
27514
27603
  exitCriterion: "fleet-n-of-n",
27515
27604
  hubOnlyShortcut: "forbidden",
@@ -27526,14 +27615,14 @@ var rollout_plan_default = {
27526
27615
  repo: "mutmutco/mmi-hub",
27527
27616
  role: "canary",
27528
27617
  schedule: "train",
27529
- v3Target: "v4.6.8"
27618
+ v3Target: "v4.6.10"
27530
27619
  }
27531
27620
  ],
27532
27621
  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.",
27533
27622
  rollback: {
27534
27623
  independent: true,
27535
- mechanism: "npm dist-tag latest -> 4.6.8 and redeploy the Hub Lambda from tag v4.6.8 (0dd075a0d2c1); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27536
- v3Target: "v4.6.8 (@mutmutco/cli@4.6.8, tag commit 0dd075a0d2c1 \u2014 last known-good release carrying the repo-index v4-only contract)"
27624
+ mechanism: "npm dist-tag latest -> 4.6.10 and redeploy the Hub Lambda from tag v4.6.10 (a4bbe43232e9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27625
+ v3Target: "v4.6.10 (@mutmutco/cli@4.6.10, tag commit a4bbe43232e9 \u2014 last known-good release carrying the repo-index v4-only contract)"
27537
27626
  }
27538
27627
  },
27539
27628
  {
@@ -32106,8 +32195,11 @@ function classifyRunnerCost(selector) {
32106
32195
  }
32107
32196
  var STANDARD_HOSTED_ALLOWLIST = [
32108
32197
  ["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
32109
- ["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
32198
+ ["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"],
32199
+ // #9571 (MM-Work#8894 chooser): owned mmi-heavy first, included ubuntu-latest only on a deep backlog.
32200
+ ["mutmutco/MMI-Hub", ".github/workflows/gate.yml", "cli-part", "hosted-spill"]
32110
32201
  ];
32202
+ var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.[\w-]+\.outputs\.heavy_runs_on\s*\|\|\s*('[^']+')\)\s*\}\}$/;
32111
32203
  function standardHostedLabelFor(repo, path2, jobId) {
32112
32204
  if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
32113
32205
  return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
@@ -32122,6 +32214,8 @@ function classifyJobRunnerCost(job, allowance2) {
32122
32214
  if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
32123
32215
  const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
32124
32216
  if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
32217
+ const spill = allowance2 === "hosted-spill" ? HOSTED_SPILL_RE.exec(raw) : null;
32218
+ if (spill) return (!spill[1] || spill[1] === "ubuntu-latest") && classifyRunnerCost(`\${{ fromJSON(${spill[2]}) }}`) === "self-hosted" ? "standard-hosted" : "unknown";
32125
32219
  const direct = classifyRunnerCost(raw);
32126
32220
  const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
32127
32221
  if (!matrix || direct === "hosted") return direct;
@@ -38643,7 +38737,7 @@ function runsOnLabelSets(job) {
38643
38737
  if (plainRecord(selector)) selector = selector.labels;
38644
38738
  if (typeof selector !== "string" || !selector.includes("${{")) return [toSet(selector)].filter((s) => Boolean(s));
38645
38739
  const sets = [];
38646
- for (const m of selector.matchAll(/fromJSON\('([^']+)'\)/g)) {
38740
+ for (const m of selector.matchAll(/fromJSON\((?:needs\.[\w-]+\.outputs\.[\w-]+\s*\|\|\s*)?'([^']+)'\)/g)) {
38647
38741
  try {
38648
38742
  const parsed = toSet(JSON.parse(m[1]));
38649
38743
  if (parsed) sets.push(parsed);
@@ -41250,7 +41344,18 @@ async function readGhPrStateWithRetry(fetchState, options) {
41250
41344
  }
41251
41345
  return { ok: false, error: lastError };
41252
41346
  }
41347
+ function isNoFailedCheckQueueEjection(run) {
41348
+ return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
41349
+ }
41253
41350
  async function runPrLand(prNumber, options, deps) {
41351
+ const first = await runPrLandOnce(prNumber, options, deps);
41352
+ if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
41353
+ const queue = first.mergeQueue;
41354
+ const nothingFailed = queue?.state === "timeout" || queue?.state === "ejected" && isNoFailedCheckQueueEjection(queue.ejectedRun);
41355
+ if (!nothingFailed) return first;
41356
+ return { ...await runPrLandOnce(prNumber, options, deps), retriedAfterEjection: true };
41357
+ }
41358
+ async function runPrLandOnce(prNumber, options, deps) {
41254
41359
  const resolved = await deps.resolveRepo(prNumber, options.repo);
41255
41360
  const { repo } = resolved;
41256
41361
  const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
@@ -41296,6 +41401,18 @@ async function runPrLand(prNumber, options, deps) {
41296
41401
  base.mergeStatus = "auto-merge-enqueued";
41297
41402
  return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
41298
41403
  }
41404
+ if (deps.preflightQueue) {
41405
+ const preflight2 = await deps.preflightQueue(prNumber, repo, pinned);
41406
+ if (preflight2 && preflight2.mergeStatus === "failed") {
41407
+ const rawError = preflight2.error ?? "queue preparation needed";
41408
+ return {
41409
+ ...base,
41410
+ mergeStatus: "failed",
41411
+ ...preflight2.preparation ? { preparation: preflight2.preparation } : {},
41412
+ error: !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError
41413
+ };
41414
+ }
41415
+ }
41299
41416
  }
41300
41417
  const checksWaitError = (checksWait) => {
41301
41418
  if (checksWait.status === "conflicting") {
@@ -41456,7 +41573,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41456
41573
  ...base,
41457
41574
  mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
41458
41575
  ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
41459
- error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}`
41576
+ error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}${isNoFailedCheckQueueEjection(run) ? `; no check failed \u2014 re-run \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to re-enqueue, and if a land is refused again report the exact refusal verbatim (never fall back to a direct/admin merge or an \`MMI_ACTOR=personal\` landing workaround)` : ""}`
41460
41577
  };
41461
41578
  deps.queueProgress?.(result2);
41462
41579
  return result2;
@@ -41466,7 +41583,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41466
41583
  const result = {
41467
41584
  ...base,
41468
41585
  mergeQueue: { ...base.mergeQueue, state: "timeout" },
41469
- error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun pr land to resume polling`
41586
+ error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to resume/re-enqueue; if it still does not land, report the exact refusal verbatim (never bypass the ruleset or fall back to a direct/admin or \`MMI_ACTOR=personal\` merge)`
41470
41587
  };
41471
41588
  deps.queueProgress?.(result);
41472
41589
  return result;
@@ -41486,7 +41603,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
41486
41603
  const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
41487
41604
  return Promise.all(prs.map(async (pr) => {
41488
41605
  let result = await safe(pr);
41489
- if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
41606
+ if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
41490
41607
  const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
41491
41608
  if (baseRed) {
41492
41609
  result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
@@ -41658,10 +41775,16 @@ function trailerBlockBreak(body, line) {
41658
41775
  const lines2 = String(body ?? "").split(/\r?\n/);
41659
41776
  const at = lines2.findIndex((candidate) => candidate.trim() === line);
41660
41777
  if (at === -1) return { after: "", before: "" };
41778
+ let sawBlank = false;
41661
41779
  for (let i = at + 1; i < lines2.length; i += 1) {
41662
41780
  const next = lines2[i];
41663
- if (!next.trim()) continue;
41664
- if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
41781
+ if (!next.trim()) {
41782
+ sawBlank = true;
41783
+ continue;
41784
+ }
41785
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) {
41786
+ return sawBlank ? { after: "", before: "", splitByBlankBefore: next.trim() } : { after: "", before: "" };
41787
+ }
41665
41788
  return { after: next.trim(), before: "" };
41666
41789
  }
41667
41790
  let paraStart = at;
@@ -41681,7 +41804,10 @@ function clipParagraph(line) {
41681
41804
  }
41682
41805
  function invisibleTrailer(sha, line, body = "") {
41683
41806
  const broken = trailerBlockBreak(body, line);
41684
- const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41807
+ const brokenDiagnostic = broken.splitByBlankBefore ? ` Broken by a blank line before the final trailer paragraph, so git only read the last paragraph as the trailer block:
41808
+ ${clipParagraph(broken.splitByBlankBefore)}
41809
+ Keep every Key: value trailer (e.g. ${TRAILER_KEY} and Runbook-Unaffected) in one contiguous final paragraph with no blank line between them.
41810
+ ` : broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41685
41811
  ${clipParagraph(broken.after)}
41686
41812
  ` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
41687
41813
  ${clipParagraph(broken.before)}
@@ -41701,9 +41827,11 @@ function invisibleTrailer(sha, line, body = "") {
41701
41827
  The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
41702
41828
  Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
41703
41829
  git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
41704
- ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
41830
+ ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message. Run this
41831
+ yourself, right now, in this worktree; it is not a blocker to report and it needs no human:
41705
41832
  git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
41706
- That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
41833
+ That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.
41834
+ Re-run the policy receipt and the focused test command immediately after it.`
41707
41835
  };
41708
41836
  }
41709
41837
  function unknownScope(sha, unknown) {
@@ -42834,9 +42962,7 @@ ${list}
42834
42962
  Co-authored-by: GitHub <noreply@github.com>
42835
42963
  `;
42836
42964
  }
42837
- async function enqueuePrMergeQueue(pr, repo, options = {}) {
42838
- if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
42839
- pr = pr.replace(/^0+(?=\d)/, "");
42965
+ async function inspectQueuePreparation(pr, repo, options) {
42840
42966
  const read = options.read ?? readApi;
42841
42967
  const queueRead = options.read ?? actorReadApi;
42842
42968
  const write = options.write ?? writeApi;
@@ -42848,14 +42974,14 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42848
42974
  const pull = metadata.pullRequest;
42849
42975
  if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
42850
42976
  const head = options.expectedHeadOid ?? pull.headRefOid;
42851
- if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
42852
- const stamp = (result2, selected = false, intentId) => {
42977
+ if (head !== pull.headRefOid) return { kind: "settled", result: { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` } };
42978
+ const stamp = (result, selected = false, intentId) => {
42853
42979
  const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
42854
- return credential === "unknown" ? result2 : { ...result2, credential };
42980
+ return credential === "unknown" ? result : { ...result, credential };
42855
42981
  };
42856
- if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
42857
- if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
42858
- if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
42982
+ if (pull.state === "MERGED") return { kind: "settled", result: stamp({ mergeStatus: "merged" }) };
42983
+ if (pull.isInMergeQueue) return { kind: "settled", result: stamp({ mergeStatus: "auto-merge-enqueued" }) };
42984
+ if (pull.state !== "OPEN") return { kind: "settled", result: { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` } };
42859
42985
  const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
42860
42986
  let page = pull.closingIssuesReferences.pageInfo;
42861
42987
  while (page.hasNextPage) {
@@ -42888,7 +43014,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42888
43014
  status: "preparation-needed",
42889
43015
  reason,
42890
43016
  issueTargets: targets.map((number) => `${repo}#${number}`),
42891
- nextAction: reason === "queue-message" ? "PR author: if the PR is meant to close these issues, add `Closes #N` to the PR body (`mmi-cli devops pr edit <pr> --body-file <path>`) and rerun pr land without pushing a new head; if they must stay open, remove the closing phrases from the source commit or PR text using Part of #N / Refs #N, then re-run checks and pr land with the prepared head. A queue cannot accept --squash-body-file." : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
43017
+ nextAction: reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
42892
43018
  },
42893
43019
  error: `queue preparation needed: ${detail}`
42894
43020
  });
@@ -42906,9 +43032,9 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42906
43032
  );
42907
43033
  method = legacy.mergeQueue?.configuration?.mergeMethod;
42908
43034
  }
42909
- if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred");
43035
+ if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return { kind: "refused", result: preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred") };
42910
43036
  if (method === "SQUASH" && (!["PR_TITLE", "COMMIT_OR_PR_TITLE"].includes(policy.title ?? "") || !["PR_BODY", "COMMIT_MESSAGES", "BLANK"].includes(policy.body ?? ""))) {
42911
- return preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred");
43037
+ return { kind: "refused", result: preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred") };
42912
43038
  }
42913
43039
  const messages = parsed.commits;
42914
43040
  const headline = policy.title === "COMMIT_OR_PR_TITLE" && messages.length === 1 ? messages[0].headline : pull.title;
@@ -42925,7 +43051,7 @@ ${body}`;
42925
43051
  ${message2.body}`).join("\n\n");
42926
43052
  if (method === "MERGE") {
42927
43053
  if (!["PR_TITLE", "MERGE_MESSAGE"].includes(repository.merge_commit_title ?? "") || !["PR_TITLE", "PR_BODY", "BLANK"].includes(repository.merge_commit_message ?? "")) {
42928
- return preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred");
43054
+ return { kind: "refused", result: preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred") };
42929
43055
  }
42930
43056
  if (repository.merge_commit_title === "PR_TITLE" || repository.merge_commit_message === "PR_TITLE") actualMessage += `
42931
43057
 
@@ -42947,19 +43073,22 @@ ${pull.body}`;
42947
43073
  ...findNegatedClosings(input.text, closing),
42948
43074
  ...findCrossRepoAmbiguousClosings(input.text, closing, repo)
42949
43075
  ])];
42950
- return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
43076
+ return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\` remedy.`) };
42951
43077
  }
42952
43078
  if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && messages.length > 1 && options.bodyText == null && messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
42953
43079
  ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
42954
43080
  return {
42955
- mergeStatus: "failed",
42956
- preparation: {
42957
- status: "preparation-needed",
42958
- reason: "queue-message-override",
42959
- issueTargets: [],
42960
- nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
42961
- },
42962
- error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
43081
+ kind: "refused",
43082
+ result: {
43083
+ mergeStatus: "failed",
43084
+ preparation: {
43085
+ status: "preparation-needed",
43086
+ reason: "queue-message-override",
43087
+ issueTargets: [],
43088
+ nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
43089
+ },
43090
+ error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
43091
+ }
42963
43092
  };
42964
43093
  }
42965
43094
  const requiredBody = options.bodyText ?? composedBody;
@@ -42967,18 +43096,38 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
42967
43096
  if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42968
43097
  if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
42969
43098
  return {
42970
- mergeStatus: "failed",
42971
- preparation: {
42972
- status: "preparation-needed",
42973
- reason: "queue-message-override",
42974
- issueTargets: [],
42975
- nextAction: "PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. mmi-cli devops pr edit <n> --body-file <path>) without a body override, then rerun pr land. Keep repository settings unchanged."
42976
- },
42977
- error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
43099
+ kind: "refused",
43100
+ result: {
43101
+ mergeStatus: "failed",
43102
+ preparation: {
43103
+ status: "preparation-needed",
43104
+ reason: "queue-message-override",
43105
+ issueTargets: [],
43106
+ nextAction: `PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) without a body override, then rerun \`pr land\`. Keep repository settings unchanged.`
43107
+ },
43108
+ error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
43109
+ }
42978
43110
  };
42979
43111
  }
42980
- return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
43112
+ return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
42981
43113
  }
43114
+ return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
43115
+ }
43116
+ async function preflightPrMergeQueue(prNumber, repo, options = {}) {
43117
+ if (!/^\d+$/.test(prNumber)) return void 0;
43118
+ const inspected = await inspectQueuePreparation(prNumber.replace(/^0+(?=\d)/, ""), repo, options);
43119
+ return inspected.kind === "refused" ? inspected.result : void 0;
43120
+ }
43121
+ async function enqueuePrMergeQueue(pr, repo, options = {}) {
43122
+ if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
43123
+ pr = pr.replace(/^0+(?=\d)/, "");
43124
+ const inspected = await inspectQueuePreparation(pr, repo, options);
43125
+ if (inspected.kind !== "ready") return inspected.result;
43126
+ const { head, pull, read, queueRead, write } = inspected;
43127
+ const stamp = (result2, selected = false, intentId) => {
43128
+ const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
43129
+ return credential === "unknown" ? result2 : { ...result2, credential };
43130
+ };
42982
43131
  const current = await queueGraphql(
42983
43132
  repo,
42984
43133
  `pullRequest(number:${pr}) { headRefOid title body }`,
@@ -42989,7 +43138,12 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
42989
43138
  }
42990
43139
  const selection = { selected: false };
42991
43140
  const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
42992
- return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
43141
+ return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
43142
+ }
43143
+ function closingGuardDiagnostic(message2) {
43144
+ if (!message2) return "";
43145
+ const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
43146
+ return (remedy === -1 ? message2 : message2.slice(0, remedy)).trim();
42993
43147
  }
42994
43148
  function decideQueueMergeRoute(hasQueue, method, headRef) {
42995
43149
  if (!hasQueue) return "normal";
@@ -43166,8 +43320,10 @@ function prMergeQueueLine(result) {
43166
43320
  return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
43167
43321
  case "merged":
43168
43322
  return `pr land: PR #${result.pr} merged via merge queue`;
43169
- case "ejected":
43170
- return `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
43323
+ case "ejected": {
43324
+ const line = `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
43325
+ return isNoFailedCheckQueueEjection(queue.ejectedRun) ? `${line} \u2014 no check failed; re-run \`mmi-cli devops pr land ${result.pr}${result.repo ? ` --repo ${result.repo}` : ""}\` once to re-enqueue, and report any further refusal verbatim (never fall back to a direct/admin merge or \`MMI_ACTOR=personal\` workaround)` : line;
43326
+ }
43171
43327
  case "timeout":
43172
43328
  return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
43173
43329
  }
@@ -43366,6 +43522,57 @@ function registerRunCancel(run, writeToken) {
43366
43522
  else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.force ? "force-cancel" : "cancel"} requested (status: ${receipt.status}${receipt.runStatus ? `, runStatus: ${receipt.runStatus}` : ""})`);
43367
43523
  });
43368
43524
  }
43525
+ var RUN_RERUN_STATUS = "rerun-requested";
43526
+ async function rerunWorkflowRun(repo, id, deps, failed2 = false) {
43527
+ const raw = String(id).trim();
43528
+ if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run rerun: <id> must be a positive workflow run id (received '${String(id)}')`);
43529
+ const runId = Number(raw);
43530
+ if (!Number.isSafeInteger(runId)) throw new Error(`run rerun: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
43531
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run rerun: --repo must be owner/repo (received '${repo}')`);
43532
+ const projects = await deps.projects();
43533
+ if (!projects) throw new Error("run rerun: the Hub project registry is unreadable; refusing to rerun on a repo this command cannot prove registered");
43534
+ const key = registryRepoKey(repo);
43535
+ if (!projects.some((p) => (p.repos ?? []).some((r) => typeof r === "string" && registryRepoKey(qualifyRegistryRepo(r)) === key))) {
43536
+ throw new Error(`run rerun: ${repo} is not a registered org repo; App writes are limited to repos in the Hub registry`);
43537
+ }
43538
+ const mode = failed2 ? "failed" : "all";
43539
+ let httpStatus;
43540
+ try {
43541
+ httpStatus = await deps.post(`repos/${repo}/actions/runs/${runId}/${failed2 ? "rerun-failed-jobs" : "rerun"}`);
43542
+ } catch (error) {
43543
+ const e = error;
43544
+ const detail = String(e.stderr ?? error);
43545
+ const code = /\bHTTP(?:\/[\d.]+)? (\d{3})\b/.exec(`${detail} ${e.stdout ?? ""}`)?.[1];
43546
+ if (code === "403") throw new Error(`run rerun: GitHub App denied (HTTP 403) on ${repo}; actions:write permission is required; no personal-token fallback`);
43547
+ if (code === "404") throw new Error(`run rerun: run ${runId} not found on ${repo} (HTTP 404), or the App cannot see that repo`);
43548
+ if (code === "409" || code === "422") throw new Error(`run rerun: run ${runId} is not rerunnable (HTTP ${code}); it may still be in progress, or be past GitHub's rerun window`);
43549
+ throw new Error(`run rerun: ${detail}`);
43550
+ }
43551
+ return { ok: true, repo, runId, mode, httpStatus, credential: "app_installation", status: RUN_RERUN_STATUS };
43552
+ }
43553
+ function appRunPost(writeToken, exec3 = execFileP) {
43554
+ let token;
43555
+ return async (path2) => {
43556
+ token ??= await writeToken();
43557
+ const { stdout } = await exec3("gh", ["api", "--include", "--method", "POST", path2], {
43558
+ timeout: 2e4,
43559
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
43560
+ });
43561
+ return Number(/^HTTP\/[\d.]+ (\d{3})/.exec(stdout)?.[1] ?? 0);
43562
+ };
43563
+ }
43564
+ function registerRunRerun(run, writeToken, projects) {
43565
+ run.command("rerun <id>").description("rerun one workflow run on the org App (actions:write), registered repos only; --failed reruns only the failed jobs; prints a receipt").requiredOption("--repo <owner/repo>", "registered repository that owns the run").option("--failed", "rerun only the failed jobs (POST .../rerun-failed-jobs)").option("--json", "machine-readable receipt").action(async (id, options) => {
43566
+ let receipt;
43567
+ try {
43568
+ receipt = await rerunWorkflowRun(options.repo, id, { projects, post: appRunPost(writeToken) }, options.failed === true);
43569
+ } catch (error) {
43570
+ return fail(error.message);
43571
+ }
43572
+ if (options.json) console.log(JSON.stringify(receipt));
43573
+ else console.log(`run rerun: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.mode === "failed" ? "failed jobs" : "all jobs"} rerun requested (HTTP ${receipt.httpStatus}, credential: ${receipt.credential})`);
43574
+ });
43575
+ }
43369
43576
 
43370
43577
  // src/board-commands.ts
43371
43578
  init_cli_shared();
@@ -43634,9 +43841,27 @@ function refuseNotClaimable(e, json) {
43634
43841
  process.exitCode = 1;
43635
43842
  return true;
43636
43843
  }
43637
- var BOARD_READ_HELP = "\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\nWhen using Windows PowerShell 5.1, avoid shell redirects: 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\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,title,status,assignees,labels,bucket}.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
43844
+ var BOARD_READ_HELP = "\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\nWhen using Windows PowerShell 5.1, avoid shell redirects: 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\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
43638
43845
  function flattenBoardReport(report) {
43639
- return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, title, status, assignees, labels }) => ({ ref, title, status, assignees, labels, bucket }))));
43846
+ const closingByRef = /* @__PURE__ */ new Map();
43847
+ for (const pull of report.pullRequests ?? []) {
43848
+ for (const closes of pull.closesIssues) {
43849
+ const rows = closingByRef.get(closes) ?? [];
43850
+ rows.push({ number: pull.number, url: pull.url });
43851
+ closingByRef.set(closes, rows);
43852
+ }
43853
+ }
43854
+ return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, number, title, status, assignees, labels, claim }) => ({
43855
+ ref,
43856
+ number,
43857
+ title,
43858
+ status,
43859
+ assignees,
43860
+ labels,
43861
+ bucket,
43862
+ claim: claim ?? null,
43863
+ openPullRequests: closingByRef.get(ref) ?? []
43864
+ }))));
43640
43865
  }
43641
43866
  function registerBoardCommands(program3) {
43642
43867
  function withDiscoverMissDetail(message2) {
@@ -54891,23 +55116,24 @@ async function closeIssue(client, options, deps = {}) {
54891
55116
  if (options.evidence !== void 0) {
54892
55117
  await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
54893
55118
  }
55119
+ const appWriter = await selectClaimWriteClient(client, deps.appClient);
54894
55120
  if (reason === "completed" && options.evidence !== void 0) {
54895
55121
  const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
54896
55122
  await ensureLabels([CLOSED_MERGED_LOOP_STATE_LABEL], repo);
54897
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
55123
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
54898
55124
  body: { labels: [CLOSED_MERGED_LOOP_STATE_LABEL] }
54899
55125
  });
54900
55126
  }
54901
- await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
55127
+ await appWriter.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
54902
55128
  body: { state: "closed", state_reason: stateReason }
54903
55129
  });
54904
55130
  if (reason === "duplicate-of" && options.duplicateOf) {
54905
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
55131
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
54906
55132
  body: { body: `Duplicate of #${options.duplicateOf}` }
54907
55133
  });
54908
55134
  }
54909
55135
  if (options.comment) {
54910
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
55136
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
54911
55137
  body: { body: options.comment }
54912
55138
  });
54913
55139
  }
@@ -59546,7 +59772,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59546
59772
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
59547
59773
  // Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
59548
59774
  // an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
59549
- progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
59775
+ progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`pr checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
59550
59776
  });
59551
59777
  printChecksWaitResult("pr checks-wait", result, o.json);
59552
59778
  });
@@ -59653,6 +59879,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59653
59879
  readHeadSha: (prNumber, repo) => readPrLandPinnedHeadSha(prNumber, repo),
59654
59880
  pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
59655
59881
  hasMergeQueue: hasPrMergeQueue,
59882
+ // #9611: read-only preparation verdict BEFORE the check wait; the enqueue then re-reads and submits.
59883
+ preflightQueue: (prNumber, repo, expectedHeadOid) => preflightPrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly }),
59656
59884
  enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
59657
59885
  const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
59658
59886
  if (receipt.warning) console.warn(receipt.warning);
@@ -60318,6 +60546,9 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
60318
60546
  const result = await waitForPrChecks({
60319
60547
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
60320
60548
  pollChecks: () => pollRestCommitChecks(sha, repo),
60549
+ // #9589: the evidence-bearing read lets the wait name the rows holding it open and carry the
60550
+ // full check/workflow evidence on the timeout receipt.
60551
+ pollChecksWithEvidence: () => pollRestCommitChecksWithWorkflowEvidence(sha, repo),
60321
60552
  pollRateLimit: () => waitLoopCorePool("ci checks-wait"),
60322
60553
  checksUrl: `https://github.com/${repo}/commit/${sha}/checks`,
60323
60554
  checksDiagnosticCommand: `gh api repos/${repo}/commits/${sha}/check-runs`,
@@ -60330,7 +60561,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
60330
60561
  sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
60331
60562
  log: (message2) => console.warn(message2),
60332
60563
  timeoutMs: timeoutMs ?? PR_CHECKS_TIMEOUT_MS,
60333
- progress: ({ state, elapsedMs, remainingMs }) => console.warn(`ci checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
60564
+ progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`ci checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
60334
60565
  });
60335
60566
  printChecksWaitResult("ci checks-wait", result, o.json);
60336
60567
  });
@@ -66182,7 +66413,7 @@ function typedReadHint(path2) {
66182
66413
  if (!match) return "";
66183
66414
  const [, owner, repo, kind, item, sub] = match;
66184
66415
  const slug = `${owner}/${repo}`;
66185
- const say = (command, note = "") => ` Typed read for this route: \`${command}\`${note}.`;
66416
+ const say = (command, note = "") => ` Typed read for this route: \`MMI_ACTOR=app ${command}\`${note}.`;
66186
66417
  if (kind === "issues") {
66187
66418
  if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
66188
66419
  if (item.startsWith("comments/")) {
@@ -69965,7 +70196,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
69965
70196
  appActorCommand = commandPath4;
69966
70197
  setAuditCommand(commandPath4);
69967
70198
  if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
69968
- if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
70199
+ if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel" || commandPath4 === "run rerun") return;
69969
70200
  try {
69970
70201
  await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
69971
70202
  } catch (e) {
@@ -70800,9 +71031,12 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
70800
71031
  });
70801
71032
  registerCiChecksWait(ci);
70802
71033
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
70803
- registerRunCancel(
70804
- program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
70805
- async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
71034
+ var runRoot = program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` and `run rerun <id> [--failed]`, both on the org App actions:write profile");
71035
+ registerRunCancel(runRoot, async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token);
71036
+ registerRunRerun(
71037
+ runRoot,
71038
+ async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run rerun")).token,
71039
+ async () => fetchProjectsList(registryClientDeps(await loadConfig()))
70806
71040
  );
70807
71041
  ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
70808
71042
  if (o.apply && o.parkRuleset) {
@@ -8226,10 +8226,15 @@ function checkRunbookCoverage(docs, base, git) {
8226
8226
  const heading = headings[i];
8227
8227
  const end = headings.slice(i + 1).find((h) => h.level <= heading.level)?.start ?? lines.length + 1;
8228
8228
  const markerEnd = headings[i + 1]?.start ?? end;
8229
- const globs = lines.slice(heading.start, markerEnd - 1).flatMap((text) => /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim()) ?? []);
8229
+ const markers = lines.slice(heading.start, markerEnd - 1).flatMap((text, j) => {
8230
+ const found = /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim());
8231
+ return found ? [{ line: heading.start + 1 + j, globs: found }] : [];
8232
+ });
8233
+ const globs = markers.flatMap((m) => m.globs);
8230
8234
  const affected = changed.filter((path2) => globs.some((glob) => matches(glob, path2)) && !changesOnlyUses(path2));
8231
8235
  if (affected.length && !ranges.some((r) => r.count > 0 && r.start < end && r.start + r.count > heading.start)) {
8232
- findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
8236
+ const markerLine = markers.find((m) => m.globs.some((glob) => affected.some((path2) => matches(glob, path2)))).line;
8237
+ findings.push({ kind: "runbook-section-stale", doc, line: markerLine, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}, covers marker at line ${markerLine}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
8233
8238
  }
8234
8239
  }
8235
8240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.8",
3
+ "version": "4.6.10",
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",