@mutmutco/cli 4.6.9 → 4.6.11

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 +295 -96
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -13912,6 +13912,15 @@ function issueCommentAppendOnlyRecovery(flag, canonicalPath, argv) {
13912
13912
  corrected
13913
13913
  };
13914
13914
  }
13915
+ function trainPlanRedundantFlagRecovery(flag, canonicalPath, argv) {
13916
+ if (canonicalPath !== "devops release" && canonicalPath !== "devops rcand") return void 0;
13917
+ if (flag !== "--plan" && !flag.startsWith("--plan=")) return void 0;
13918
+ const kept = argv.filter((tok) => tok !== "--plan" && !tok.startsWith("--plan="));
13919
+ return {
13920
+ message: `unknown option '--plan' \u2014 \`${canonicalPath}\` runs the dry-run plan by default when \`--apply\` is absent; planning needs no flag`,
13921
+ corrected: `mmi-cli ${kept.join(" ")}`
13922
+ };
13923
+ }
13915
13924
  var BOARD_CLAIM_OWN_FLAGS = ["--repo", "--for", "--force", "--check", "--allow-partial", "--json"];
13916
13925
  function splitInlineSelectors(argv, flags) {
13917
13926
  const out = [];
@@ -15350,6 +15359,9 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15350
15359
  // #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
15351
15360
  ["run rerun", { metadata: "read", actions: "write" }],
15352
15361
  ["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
15362
+ // #9614: closing an issue is an issue-state write — it rides the org App (mmi-github-app), so the close
15363
+ // is attributed to the App instead of the personal account. Nothing broader than issues.
15364
+ ["issue close", { metadata: "read", issues: "write" }],
15353
15365
  // #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
15354
15366
  ["issue discover-related", { metadata: "read", issues: "write" }]
15355
15367
  ]);
@@ -15738,6 +15750,13 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
15738
15750
  ].join(" ")
15739
15751
  };
15740
15752
  }
15753
+ function pendingEvidenceItems(evidence) {
15754
+ 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"})`);
15755
+ for (const run of evidence.workflows) {
15756
+ if (run.status !== "completed") items.push(`${run.name}#${run.id} (${run.status})`);
15757
+ }
15758
+ return items;
15759
+ }
15741
15760
  async function waitForPrChecks(deps) {
15742
15761
  let { policy, reason } = await deps.resolvePolicy();
15743
15762
  const baseBranch = deps.baseBranch ?? "development";
@@ -15768,10 +15787,10 @@ async function waitForPrChecks(deps) {
15768
15787
  const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
15769
15788
  const started = now();
15770
15789
  const deadline = started + timeoutMs;
15771
- const report = (state) => {
15790
+ const report = (state, detail) => {
15772
15791
  try {
15773
15792
  const elapsedMs = now() - started;
15774
- deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
15793
+ deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()), ...detail === void 0 ? {} : { detail } });
15775
15794
  } catch {
15776
15795
  }
15777
15796
  };
@@ -15781,6 +15800,10 @@ async function waitForPrChecks(deps) {
15781
15800
  let zeroRunsStreak = 0;
15782
15801
  let evidenceHeadSha;
15783
15802
  let lastEvidence;
15803
+ const pendingWaitDetail = (evidence) => {
15804
+ const items = pendingEvidenceItems(evidence);
15805
+ return items.length ? `pending (${items.join(", ")})` : void 0;
15806
+ };
15784
15807
  report("starting");
15785
15808
  while (now() < deadline) {
15786
15809
  if (deps.pollRateLimit) {
@@ -15845,7 +15868,7 @@ async function waitForPrChecks(deps) {
15845
15868
  await deps.sleep(waitMs);
15846
15869
  continue;
15847
15870
  }
15848
- report(state);
15871
+ report(state, state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0);
15849
15872
  if (state !== "success") successStreak = 0;
15850
15873
  if (state !== "failure") failureStreak = 0;
15851
15874
  if (state !== "no-checks-reported") zeroRunsStreak = 0;
@@ -15921,10 +15944,18 @@ async function waitForPrChecks(deps) {
15921
15944
  await deps.sleep(pollIntervalMs(now() - started));
15922
15945
  continue;
15923
15946
  }
15924
- lastDetail = state;
15947
+ const pendingDetail = state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0;
15948
+ lastDetail = pendingDetail ?? state;
15925
15949
  await deps.sleep(pollIntervalMs(now() - started));
15926
15950
  }
15927
- return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
15951
+ return {
15952
+ policy,
15953
+ status: "timeout",
15954
+ reason,
15955
+ detail: lastDetail,
15956
+ waitedMs: now() - started,
15957
+ ...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
15958
+ };
15928
15959
  }
15929
15960
 
15930
15961
  // src/release-auxiliary-workflows.ts
@@ -16394,16 +16425,23 @@ async function fetchHeadCheckRuns(headSha, repo, gh2) {
16394
16425
  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}"]);
16395
16426
  return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
16396
16427
  }
16397
- async function fetchHeadCheckEntries(headSha, repo, gh2) {
16428
+ async function fetchHeadCheckEntriesWithRuns(headSha, repo, gh2) {
16398
16429
  const [runs2, statusesOut] = await Promise.all([
16399
16430
  fetchHeadCheckRuns(headSha, repo, gh2),
16400
16431
  gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
16401
16432
  ]);
16402
16433
  const statuses = parseNdjsonLines(statusesOut);
16403
- return [
16404
- ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
16405
- ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
16406
- ];
16434
+ return {
16435
+ runs: runs2,
16436
+ statuses,
16437
+ entries: [
16438
+ ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
16439
+ ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
16440
+ ]
16441
+ };
16442
+ }
16443
+ async function fetchHeadCheckEntries(headSha, repo, gh2) {
16444
+ return (await fetchHeadCheckEntriesWithRuns(headSha, repo, gh2)).entries;
16407
16445
  }
16408
16446
  async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredContexts) {
16409
16447
  const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
@@ -16411,38 +16449,68 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
16411
16449
  const { relevant } = partitionByRequired(entries, requiredContexts);
16412
16450
  return derivePollState(relevant);
16413
16451
  }
16414
- async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
16415
- const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
16416
- const [entries, rawRuns] = await Promise.all([
16417
- fetchHeadCheckEntries(snapshot.headSha, repo, gh2),
16418
- 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}"])
16452
+ async function pollRestHeadChecksWithWorkflowEvidence(headSha, repo, gh2) {
16453
+ const [{ runs: runs2, statuses }, rawRuns] = await Promise.all([
16454
+ fetchHeadCheckEntriesWithRuns(headSha, repo, gh2),
16455
+ gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
16419
16456
  ]);
16420
- const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
16421
16457
  const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
16422
- if (run.head_sha !== snapshot.headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
16458
+ if (run.head_sha !== headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
16423
16459
  if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
16424
16460
  return [{
16425
16461
  id: run.id,
16426
16462
  name: run.name,
16427
- headSha: snapshot.headSha,
16463
+ headSha,
16428
16464
  status: run.status,
16429
16465
  conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
16430
16466
  ...typeof run.html_url === "string" ? { url: run.html_url } : {}
16431
16467
  }];
16432
16468
  });
16433
- const workflowBuckets = workflows.map((run) => {
16469
+ const workflowById = new Map(workflows.map((run) => [run.id, run]));
16470
+ const isStalePendingCheckRun = (run) => {
16471
+ if (classifyCheckRun(run) !== "pending") return false;
16472
+ const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(run.details_url ?? "");
16473
+ const owning = match ? workflowById.get(Number(match[1])) : void 0;
16474
+ if (!owning) return false;
16475
+ if (owning.status === "completed") return true;
16476
+ return workflows.some((newer) => newer.id > owning.id && newer.name === owning.name && newer.status === "completed");
16477
+ };
16478
+ const checks = [
16479
+ ...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).filter((run) => !isStalePendingCheckRun(run)).map((run) => ({
16480
+ name: run.name ?? `check-run ${run.id ?? "unknown"}`,
16481
+ state: classifyCheckRun(run),
16482
+ ...run.status !== void 0 ? { status: run.status } : {},
16483
+ conclusion: run.conclusion ?? null,
16484
+ startedAt: run.started_at ?? null,
16485
+ appId: run.app_id ?? null
16486
+ })),
16487
+ ...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", state: classifyCommitStatus(s.state) }))
16488
+ ];
16489
+ const newestRunByName = /* @__PURE__ */ new Map();
16490
+ for (const run of workflows) {
16491
+ const current = newestRunByName.get(run.name);
16492
+ if (!current || run.id > current.id) newestRunByName.set(run.name, run);
16493
+ }
16494
+ const workflowBuckets = [...newestRunByName.values()].map((run) => {
16434
16495
  if (run.status !== "completed") return "pending";
16435
16496
  return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
16436
16497
  });
16437
16498
  return {
16438
16499
  state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
16439
- headSha: snapshot.headSha,
16500
+ headSha,
16440
16501
  checks,
16441
16502
  workflows
16442
16503
  };
16443
16504
  }
16505
+ async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
16506
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
16507
+ return pollRestHeadChecksWithWorkflowEvidence(snapshot.headSha, repo, gh2);
16508
+ }
16509
+ async function pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2 = defaultGhApi) {
16510
+ return pollRestHeadChecksWithWorkflowEvidence(sha, repo, gh2);
16511
+ }
16444
16512
  async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
16445
- return derivePollState(partitionByRequired(await fetchHeadCheckEntries(sha, repo, gh2), null).relevant);
16513
+ return (await pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2)).state;
16446
16514
  }
16447
16515
  var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
16448
16516
  var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
@@ -21018,8 +21086,9 @@ function resolveSpawnTarget(command, args, platform2 = process.platform) {
21018
21086
  if (platform2 !== "win32") return { cmd: command, args };
21019
21087
  for (const t of [command, ...args]) {
21020
21088
  if (/[\r\n]/.test(t)) {
21089
+ const preview = JSON.stringify(t.length > 80 ? `${t.slice(0, 77)}...` : t);
21021
21090
  throw new Error(
21022
- `cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${JSON.stringify(t)}`
21091
+ `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)`
21023
21092
  );
21024
21093
  }
21025
21094
  }
@@ -22319,8 +22388,22 @@ function renderBoardItem(item) {
22319
22388
  }
22320
22389
  return lines2.join("\n");
22321
22390
  }
22391
+ function renderCachedBoardSource(report) {
22392
+ if (typeof report.stale?.ageMs === "number") {
22393
+ const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
22394
+ return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
22395
+ }
22396
+ if (report.builtAt) {
22397
+ const builtMs = Date.parse(report.builtAt);
22398
+ if (Number.isFinite(builtMs)) {
22399
+ return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
22400
+ }
22401
+ return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
22402
+ }
22403
+ return "source: cache";
22404
+ }
22322
22405
  function renderBoardReport(report) {
22323
- const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? "source: cache" : renderBoardSource()];
22406
+ const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
22324
22407
  if (report.github?.rateLimit) {
22325
22408
  const { limit, remaining, cost, resetAt } = report.github.rateLimit;
22326
22409
  lines2.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
@@ -23576,7 +23659,7 @@ var contestedBoardReadCommand = (repo) => `mmi-cli oracle board read --json${rep
23576
23659
  function contestedClaimRecovery(ref) {
23577
23660
  const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
23578
23661
  const boardRead = contestedBoardReadCommand(repo);
23579
- 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`;
23662
+ 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`;
23580
23663
  }
23581
23664
  function laneContestMessage(ref, contest, verb) {
23582
23665
  const { holder, why } = laneContestParts(contest);
@@ -23594,6 +23677,7 @@ function ghError(e) {
23594
23677
  init_github_client();
23595
23678
  init_github_quota();
23596
23679
  var CLAIM_CONCURRENCY = 5;
23680
+ var VERIFIED_FREE_CANDIDATE_LIMIT = 5;
23597
23681
  function withGatedWriteChainHint(message2) {
23598
23682
  return `${message2}
23599
23683
  note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
@@ -23854,9 +23938,16 @@ async function claimOneBoardItem(ctx, selector, options) {
23854
23938
  const { holder: holder2, why } = laneContestParts(contest);
23855
23939
  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}`);
23856
23940
  }
23857
- 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);
23858
- const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
23859
- const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
23941
+ 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);
23942
+ const verifiedFreeRefs = [];
23943
+ for (const candidate of candidates) {
23944
+ const candidateContest = await checkLaneContest(client, candidate, ctx.session, report.viewer, ctx.appClient, ctx.landing).catch(() => void 0);
23945
+ if (candidateContest && !candidateContest.contested && candidateContest.unverifiable.length === 0) {
23946
+ verifiedFreeRefs.push(candidate.ref);
23947
+ }
23948
+ }
23949
+ 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`;
23950
+ const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}`;
23860
23951
  throw new Error(withGatedWriteChainHint(refusal));
23861
23952
  }
23862
23953
  previousHolder = displaced();
@@ -25352,6 +25443,7 @@ var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
25352
25443
  var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
25353
25444
  var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
25354
25445
  var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
25446
+ var BLESSED_RUNNER_CHOICE_SHA = "";
25355
25447
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
25356
25448
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
25357
25449
  var LOCAL_USE = "./.github/actions/run-with-budget";
@@ -27513,10 +27605,10 @@ var rollout_plan_default = {
27513
27605
  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)."
27514
27606
  },
27515
27607
  baseline: {
27516
- version: "4.6.9",
27517
- tag: "v4.6.9",
27518
- commit: "7cbf84926132",
27519
- npm: "@mutmutco/cli@4.6.9"
27608
+ version: "4.6.11",
27609
+ tag: "v4.6.11",
27610
+ commit: "cb31a94c8972",
27611
+ npm: "@mutmutco/cli@4.6.11"
27520
27612
  },
27521
27613
  exitCriterion: "fleet-n-of-n",
27522
27614
  hubOnlyShortcut: "forbidden",
@@ -27533,14 +27625,14 @@ var rollout_plan_default = {
27533
27625
  repo: "mutmutco/mmi-hub",
27534
27626
  role: "canary",
27535
27627
  schedule: "train",
27536
- v3Target: "v4.6.9"
27628
+ v3Target: "v4.6.11"
27537
27629
  }
27538
27630
  ],
27539
27631
  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.",
27540
27632
  rollback: {
27541
27633
  independent: true,
27542
- mechanism: "npm dist-tag latest -> 4.6.9 and redeploy the Hub Lambda from tag v4.6.9 (7cbf84926132); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27543
- v3Target: "v4.6.9 (@mutmutco/cli@4.6.9, tag commit 7cbf84926132 \u2014 last known-good release carrying the repo-index v4-only contract)"
27634
+ mechanism: "npm dist-tag latest -> 4.6.11 and redeploy the Hub Lambda from tag v4.6.11 (cb31a94c8972); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27635
+ v3Target: "v4.6.11 (@mutmutco/cli@4.6.11, tag commit cb31a94c8972 \u2014 last known-good release carrying the repo-index v4-only contract)"
27544
27636
  }
27545
27637
  },
27546
27638
  {
@@ -32113,17 +32205,30 @@ function classifyRunnerCost(selector) {
32113
32205
  }
32114
32206
  var STANDARD_HOSTED_ALLOWLIST = [
32115
32207
  ["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
32116
- ["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"],
32117
- // #9571 (MM-Work#8894 chooser): owned mmi-heavy first, included ubuntu-latest only on a deep backlog.
32118
- ["mutmutco/MMI-Hub", ".github/workflows/gate.yml", "cli-part", "hosted-spill"]
32208
+ ["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
32119
32209
  ];
32120
- var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.[\w-]+\.outputs\.heavy_runs_on\s*\|\|\s*('[^']+')\)\s*\}\}$/;
32210
+ var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.([\w-]+)\.outputs\.(\w+_runs_on)\s*\|\|\s*('[^']+')\)\s*\}\}$/;
32211
+ var RUNNER_CHOICE_ACTION = "mutmutco/MMI-Hub/.github/actions/runner-choice";
32212
+ function runnerChoiceProduces(job, decider, output, source) {
32213
+ const needs = job.needs;
32214
+ if (!(needs === decider || Array.isArray(needs) && needs.includes(decider))) return false;
32215
+ const producer = source.jobs.get(decider);
32216
+ if (!record(producer) || !record(producer.outputs) || !Array.isArray(producer.steps)) return false;
32217
+ const step = /^\$\{\{\s*steps\.([\w-]+)\.outputs\.runs-on\s*\}\}$/.exec(String(producer.outputs[output] ?? ""))?.[1];
32218
+ const steps = producer.steps.filter((s) => record(s) && s.id === step);
32219
+ if (steps.length !== 1 || !record(steps[0])) return false;
32220
+ const { uses } = steps[0], inputs = steps[0].with ?? {};
32221
+ const pinned = /^[0-9a-f]{40}$/.test(source.blessedSha) && uses === `${RUNNER_CHOICE_ACTION}@${source.blessedSha}`;
32222
+ if (!pinned && !(source.repo === "mutmutco/MMI-Hub" && uses === "./.github/actions/runner-choice")) return false;
32223
+ const hosted = record(inputs) ? inputs.hosted ?? "ubuntu-latest" : void 0;
32224
+ return typeof hosted === "string" && STANDARD_HOSTED_LABEL_RE.test(hosted);
32225
+ }
32121
32226
  function standardHostedLabelFor(repo, path2, jobId) {
32122
32227
  if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
32123
32228
  return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
32124
32229
  }
32125
32230
  var record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
32126
- function classifyJobRunnerCost(job, allowance2) {
32231
+ function classifyJobRunnerCost(job, allowance2, source) {
32127
32232
  if (!record(job) || !Object.hasOwn(job, "runs-on")) return "unknown";
32128
32233
  const selector = job["runs-on"];
32129
32234
  const raw = typeof selector === "string" ? selector.trim() : Array.isArray(selector) && selector.length && selector.every((label) => typeof label === "string") ? JSON.stringify(selector) : "";
@@ -32132,8 +32237,8 @@ function classifyJobRunnerCost(job, allowance2) {
32132
32237
  if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
32133
32238
  const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
32134
32239
  if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
32135
- const spill = allowance2 === "hosted-spill" ? HOSTED_SPILL_RE.exec(raw) : null;
32136
- if (spill) return (!spill[1] || spill[1] === "ubuntu-latest") && classifyRunnerCost(`\${{ fromJSON(${spill[2]}) }}`) === "self-hosted" ? "standard-hosted" : "unknown";
32240
+ const spill = HOSTED_SPILL_RE.exec(raw);
32241
+ if (spill && source && (!spill[1] || STANDARD_HOSTED_LABEL_RE.test(spill[1])) && classifyRunnerCost(`\${{ fromJSON(${spill[4]}) }}`) === "self-hosted" && runnerChoiceProduces(job, spill[2], spill[3], source)) return "standard-hosted";
32137
32242
  const direct = classifyRunnerCost(raw);
32138
32243
  const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
32139
32244
  if (!matrix || direct === "hosted") return direct;
@@ -32188,16 +32293,24 @@ function parseRunnerJobs(source) {
32188
32293
  for (const { key, value: job } of jobs.items) {
32189
32294
  if (!(0, import_yaml.isScalar)(key) || typeof key.value !== "string" || !literalMap(job)) return void 0;
32190
32295
  const projected = /* @__PURE__ */ Object.create(null);
32191
- for (const field of ["runs-on", "uses", "strategy"]) {
32296
+ for (const field of ["runs-on", "uses", "strategy", "needs", "outputs", "steps"]) {
32192
32297
  if (!job.has(field)) continue;
32298
+ const optional = field === "needs" || field === "outputs" || field === "steps";
32193
32299
  const value = job.get(field, true);
32194
- if (value !== null && !(0, import_yaml.isNode)(value)) return void 0;
32300
+ if (value !== null && !(0, import_yaml.isNode)(value)) {
32301
+ if (optional) continue;
32302
+ return void 0;
32303
+ }
32195
32304
  (0, import_yaml.visit)(value, (_key, node) => {
32196
32305
  if (!literal(node) || (0, import_yaml.isPair)(node) && (0, import_yaml.isScalar)(node.key) && node.key.value === "<<") {
32197
32306
  invalid = true;
32198
32307
  return import_yaml.visit.BREAK;
32199
32308
  }
32200
32309
  });
32310
+ if (invalid && optional) {
32311
+ invalid = false;
32312
+ continue;
32313
+ }
32201
32314
  if (invalid) return void 0;
32202
32315
  projected[field] = value === null ? null : value.toJS(doc, { maxAliasCount: 0 });
32203
32316
  }
@@ -32334,7 +32447,7 @@ function classifyRunnerWorkflows(files) {
32334
32447
  const uses = hasCallee && typeof job.uses === "string" && !Object.hasOwn(job, "runs-on") ? job.uses : void 0;
32335
32448
  const target = uses && resolveCallee(file, uses);
32336
32449
  if (target && known.has(locationOf(target)) && resolved(file, /* @__PURE__ */ new Set())) return [];
32337
- const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id));
32450
+ const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id), { repo: file.repo, jobs, blessedSha: BLESSED_RUNNER_CHOICE_SHA });
32338
32451
  return [{ repo: file.repo, path: file.path, job: id, cost, callee: hasCallee }];
32339
32452
  });
32340
32453
  });
@@ -41262,7 +41375,18 @@ async function readGhPrStateWithRetry(fetchState, options) {
41262
41375
  }
41263
41376
  return { ok: false, error: lastError };
41264
41377
  }
41378
+ function isNoFailedCheckQueueEjection(run) {
41379
+ return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
41380
+ }
41265
41381
  async function runPrLand(prNumber, options, deps) {
41382
+ const first = await runPrLandOnce(prNumber, options, deps);
41383
+ if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
41384
+ const queue = first.mergeQueue;
41385
+ const nothingFailed = queue?.state === "timeout" || queue?.state === "ejected" && isNoFailedCheckQueueEjection(queue.ejectedRun);
41386
+ if (!nothingFailed) return first;
41387
+ return { ...await runPrLandOnce(prNumber, options, deps), retriedAfterEjection: true };
41388
+ }
41389
+ async function runPrLandOnce(prNumber, options, deps) {
41266
41390
  const resolved = await deps.resolveRepo(prNumber, options.repo);
41267
41391
  const { repo } = resolved;
41268
41392
  const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
@@ -41308,6 +41432,18 @@ async function runPrLand(prNumber, options, deps) {
41308
41432
  base.mergeStatus = "auto-merge-enqueued";
41309
41433
  return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
41310
41434
  }
41435
+ if (deps.preflightQueue) {
41436
+ const preflight2 = await deps.preflightQueue(prNumber, repo, pinned);
41437
+ if (preflight2 && preflight2.mergeStatus === "failed") {
41438
+ const rawError = preflight2.error ?? "queue preparation needed";
41439
+ return {
41440
+ ...base,
41441
+ mergeStatus: "failed",
41442
+ ...preflight2.preparation ? { preparation: preflight2.preparation } : {},
41443
+ error: !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError
41444
+ };
41445
+ }
41446
+ }
41311
41447
  }
41312
41448
  const checksWaitError = (checksWait) => {
41313
41449
  if (checksWait.status === "conflicting") {
@@ -41468,7 +41604,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41468
41604
  ...base,
41469
41605
  mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
41470
41606
  ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
41471
- 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)}`
41607
+ 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)` : ""}`
41472
41608
  };
41473
41609
  deps.queueProgress?.(result2);
41474
41610
  return result2;
@@ -41478,7 +41614,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41478
41614
  const result = {
41479
41615
  ...base,
41480
41616
  mergeQueue: { ...base.mergeQueue, state: "timeout" },
41481
- 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`
41617
+ 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)`
41482
41618
  };
41483
41619
  deps.queueProgress?.(result);
41484
41620
  return result;
@@ -41498,7 +41634,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
41498
41634
  const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
41499
41635
  return Promise.all(prs.map(async (pr) => {
41500
41636
  let result = await safe(pr);
41501
- if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
41637
+ if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
41502
41638
  const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
41503
41639
  if (baseRed) {
41504
41640
  result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
@@ -41670,10 +41806,16 @@ function trailerBlockBreak(body, line) {
41670
41806
  const lines2 = String(body ?? "").split(/\r?\n/);
41671
41807
  const at = lines2.findIndex((candidate) => candidate.trim() === line);
41672
41808
  if (at === -1) return { after: "", before: "" };
41809
+ let sawBlank = false;
41673
41810
  for (let i = at + 1; i < lines2.length; i += 1) {
41674
41811
  const next = lines2[i];
41675
- if (!next.trim()) continue;
41676
- if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
41812
+ if (!next.trim()) {
41813
+ sawBlank = true;
41814
+ continue;
41815
+ }
41816
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) {
41817
+ return sawBlank ? { after: "", before: "", splitByBlankBefore: next.trim() } : { after: "", before: "" };
41818
+ }
41677
41819
  return { after: next.trim(), before: "" };
41678
41820
  }
41679
41821
  let paraStart = at;
@@ -41693,7 +41835,10 @@ function clipParagraph(line) {
41693
41835
  }
41694
41836
  function invisibleTrailer(sha, line, body = "") {
41695
41837
  const broken = trailerBlockBreak(body, line);
41696
- const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41838
+ 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:
41839
+ ${clipParagraph(broken.splitByBlankBefore)}
41840
+ Keep every Key: value trailer (e.g. ${TRAILER_KEY} and Runbook-Unaffected) in one contiguous final paragraph with no blank line between them.
41841
+ ` : broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41697
41842
  ${clipParagraph(broken.after)}
41698
41843
  ` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
41699
41844
  ${clipParagraph(broken.before)}
@@ -41713,9 +41858,11 @@ function invisibleTrailer(sha, line, body = "") {
41713
41858
  The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
41714
41859
  Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
41715
41860
  git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
41716
- ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
41861
+ ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message. Run this
41862
+ yourself, right now, in this worktree; it is not a blocker to report and it needs no human:
41717
41863
  git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
41718
- That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
41864
+ That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.
41865
+ Re-run the policy receipt and the focused test command immediately after it.`
41719
41866
  };
41720
41867
  }
41721
41868
  function unknownScope(sha, unknown) {
@@ -42846,9 +42993,7 @@ ${list}
42846
42993
  Co-authored-by: GitHub <noreply@github.com>
42847
42994
  `;
42848
42995
  }
42849
- async function enqueuePrMergeQueue(pr, repo, options = {}) {
42850
- if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
42851
- pr = pr.replace(/^0+(?=\d)/, "");
42996
+ async function inspectQueuePreparation(pr, repo, options) {
42852
42997
  const read = options.read ?? readApi;
42853
42998
  const queueRead = options.read ?? actorReadApi;
42854
42999
  const write = options.write ?? writeApi;
@@ -42860,14 +43005,14 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42860
43005
  const pull = metadata.pullRequest;
42861
43006
  if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
42862
43007
  const head = options.expectedHeadOid ?? pull.headRefOid;
42863
- if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
42864
- const stamp = (result2, selected = false, intentId) => {
43008
+ if (head !== pull.headRefOid) return { kind: "settled", result: { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` } };
43009
+ const stamp = (result, selected = false, intentId) => {
42865
43010
  const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
42866
- return credential === "unknown" ? result2 : { ...result2, credential };
43011
+ return credential === "unknown" ? result : { ...result, credential };
42867
43012
  };
42868
- if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
42869
- if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
42870
- if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
43013
+ if (pull.state === "MERGED") return { kind: "settled", result: stamp({ mergeStatus: "merged" }) };
43014
+ if (pull.isInMergeQueue) return { kind: "settled", result: stamp({ mergeStatus: "auto-merge-enqueued" }) };
43015
+ if (pull.state !== "OPEN") return { kind: "settled", result: { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` } };
42871
43016
  const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
42872
43017
  let page = pull.closingIssuesReferences.pageInfo;
42873
43018
  while (page.hasNextPage) {
@@ -42900,7 +43045,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42900
43045
  status: "preparation-needed",
42901
43046
  reason,
42902
43047
  issueTargets: targets.map((number) => `${repo}#${number}`),
42903
- 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."
43048
+ 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."
42904
43049
  },
42905
43050
  error: `queue preparation needed: ${detail}`
42906
43051
  });
@@ -42918,9 +43063,9 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42918
43063
  );
42919
43064
  method = legacy.mergeQueue?.configuration?.mergeMethod;
42920
43065
  }
42921
- if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred");
43066
+ if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return { kind: "refused", result: preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred") };
42922
43067
  if (method === "SQUASH" && (!["PR_TITLE", "COMMIT_OR_PR_TITLE"].includes(policy.title ?? "") || !["PR_BODY", "COMMIT_MESSAGES", "BLANK"].includes(policy.body ?? ""))) {
42923
- return preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred");
43068
+ return { kind: "refused", result: preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred") };
42924
43069
  }
42925
43070
  const messages = parsed.commits;
42926
43071
  const headline = policy.title === "COMMIT_OR_PR_TITLE" && messages.length === 1 ? messages[0].headline : pull.title;
@@ -42937,7 +43082,7 @@ ${body}`;
42937
43082
  ${message2.body}`).join("\n\n");
42938
43083
  if (method === "MERGE") {
42939
43084
  if (!["PR_TITLE", "MERGE_MESSAGE"].includes(repository.merge_commit_title ?? "") || !["PR_TITLE", "PR_BODY", "BLANK"].includes(repository.merge_commit_message ?? "")) {
42940
- return preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred");
43085
+ return { kind: "refused", result: preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred") };
42941
43086
  }
42942
43087
  if (repository.merge_commit_title === "PR_TITLE" || repository.merge_commit_message === "PR_TITLE") actualMessage += `
42943
43088
 
@@ -42959,19 +43104,22 @@ ${pull.body}`;
42959
43104
  ...findNegatedClosings(input.text, closing),
42960
43105
  ...findCrossRepoAmbiguousClosings(input.text, closing, repo)
42961
43106
  ])];
42962
- return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
43107
+ 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.`) };
42963
43108
  }
42964
43109
  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}
42965
43110
  ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
42966
43111
  return {
42967
- mergeStatus: "failed",
42968
- preparation: {
42969
- status: "preparation-needed",
42970
- reason: "queue-message-override",
42971
- issueTargets: [],
42972
- 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."
42973
- },
42974
- 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"
43112
+ kind: "refused",
43113
+ result: {
43114
+ mergeStatus: "failed",
43115
+ preparation: {
43116
+ status: "preparation-needed",
43117
+ reason: "queue-message-override",
43118
+ issueTargets: [],
43119
+ 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."
43120
+ },
43121
+ 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"
43122
+ }
42975
43123
  };
42976
43124
  }
42977
43125
  const requiredBody = options.bodyText ?? composedBody;
@@ -42979,18 +43127,38 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
42979
43127
  if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42980
43128
  if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
42981
43129
  return {
42982
- mergeStatus: "failed",
42983
- preparation: {
42984
- status: "preparation-needed",
42985
- reason: "queue-message-override",
42986
- issueTargets: [],
42987
- 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."
42988
- },
42989
- 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"
43130
+ kind: "refused",
43131
+ result: {
43132
+ mergeStatus: "failed",
43133
+ preparation: {
43134
+ status: "preparation-needed",
43135
+ reason: "queue-message-override",
43136
+ issueTargets: [],
43137
+ 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.`
43138
+ },
43139
+ 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"
43140
+ }
42990
43141
  };
42991
43142
  }
42992
- return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
43143
+ return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
42993
43144
  }
43145
+ return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
43146
+ }
43147
+ async function preflightPrMergeQueue(prNumber, repo, options = {}) {
43148
+ if (!/^\d+$/.test(prNumber)) return void 0;
43149
+ const inspected = await inspectQueuePreparation(prNumber.replace(/^0+(?=\d)/, ""), repo, options);
43150
+ return inspected.kind === "refused" ? inspected.result : void 0;
43151
+ }
43152
+ async function enqueuePrMergeQueue(pr, repo, options = {}) {
43153
+ if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
43154
+ pr = pr.replace(/^0+(?=\d)/, "");
43155
+ const inspected = await inspectQueuePreparation(pr, repo, options);
43156
+ if (inspected.kind !== "ready") return inspected.result;
43157
+ const { head, pull, read, queueRead, write } = inspected;
43158
+ const stamp = (result2, selected = false, intentId) => {
43159
+ const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
43160
+ return credential === "unknown" ? result2 : { ...result2, credential };
43161
+ };
42994
43162
  const current = await queueGraphql(
42995
43163
  repo,
42996
43164
  `pullRequest(number:${pr}) { headRefOid title body }`,
@@ -43001,7 +43169,12 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
43001
43169
  }
43002
43170
  const selection = { selected: false };
43003
43171
  const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
43004
- return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
43172
+ return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
43173
+ }
43174
+ function closingGuardDiagnostic(message2) {
43175
+ if (!message2) return "";
43176
+ const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
43177
+ return (remedy === -1 ? message2 : message2.slice(0, remedy)).trim();
43005
43178
  }
43006
43179
  function decideQueueMergeRoute(hasQueue, method, headRef) {
43007
43180
  if (!hasQueue) return "normal";
@@ -43178,8 +43351,10 @@ function prMergeQueueLine(result) {
43178
43351
  return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
43179
43352
  case "merged":
43180
43353
  return `pr land: PR #${result.pr} merged via merge queue`;
43181
- case "ejected":
43182
- 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"}`;
43354
+ case "ejected": {
43355
+ 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"}`;
43356
+ 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;
43357
+ }
43183
43358
  case "timeout":
43184
43359
  return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
43185
43360
  }
@@ -43697,9 +43872,27 @@ function refuseNotClaimable(e, json) {
43697
43872
  process.exitCode = 1;
43698
43873
  return true;
43699
43874
  }
43700
- 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";
43875
+ 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";
43701
43876
  function flattenBoardReport(report) {
43702
- 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 }))));
43877
+ const closingByRef = /* @__PURE__ */ new Map();
43878
+ for (const pull of report.pullRequests ?? []) {
43879
+ for (const closes of pull.closesIssues) {
43880
+ const rows = closingByRef.get(closes) ?? [];
43881
+ rows.push({ number: pull.number, url: pull.url });
43882
+ closingByRef.set(closes, rows);
43883
+ }
43884
+ }
43885
+ return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, number, title, status, assignees, labels, claim }) => ({
43886
+ ref,
43887
+ number,
43888
+ title,
43889
+ status,
43890
+ assignees,
43891
+ labels,
43892
+ bucket,
43893
+ claim: claim ?? null,
43894
+ openPullRequests: closingByRef.get(ref) ?? []
43895
+ }))));
43703
43896
  }
43704
43897
  function registerBoardCommands(program3) {
43705
43898
  function withDiscoverMissDetail(message2) {
@@ -54954,23 +55147,24 @@ async function closeIssue(client, options, deps = {}) {
54954
55147
  if (options.evidence !== void 0) {
54955
55148
  await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
54956
55149
  }
55150
+ const appWriter = await selectClaimWriteClient(client, deps.appClient);
54957
55151
  if (reason === "completed" && options.evidence !== void 0) {
54958
55152
  const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
54959
55153
  await ensureLabels([CLOSED_MERGED_LOOP_STATE_LABEL], repo);
54960
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
55154
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
54961
55155
  body: { labels: [CLOSED_MERGED_LOOP_STATE_LABEL] }
54962
55156
  });
54963
55157
  }
54964
- await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
55158
+ await appWriter.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
54965
55159
  body: { state: "closed", state_reason: stateReason }
54966
55160
  });
54967
55161
  if (reason === "duplicate-of" && options.duplicateOf) {
54968
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
55162
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
54969
55163
  body: { body: `Duplicate of #${options.duplicateOf}` }
54970
55164
  });
54971
55165
  }
54972
55166
  if (options.comment) {
54973
- await client.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
55167
+ await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
54974
55168
  body: { body: options.comment }
54975
55169
  });
54976
55170
  }
@@ -59609,7 +59803,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59609
59803
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
59610
59804
  // Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
59611
59805
  // an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
59612
- progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
59806
+ 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`)
59613
59807
  });
59614
59808
  printChecksWaitResult("pr checks-wait", result, o.json);
59615
59809
  });
@@ -59716,6 +59910,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59716
59910
  readHeadSha: (prNumber, repo) => readPrLandPinnedHeadSha(prNumber, repo),
59717
59911
  pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
59718
59912
  hasMergeQueue: hasPrMergeQueue,
59913
+ // #9611: read-only preparation verdict BEFORE the check wait; the enqueue then re-reads and submits.
59914
+ preflightQueue: (prNumber, repo, expectedHeadOid) => preflightPrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly }),
59719
59915
  enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
59720
59916
  const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
59721
59917
  if (receipt.warning) console.warn(receipt.warning);
@@ -60381,6 +60577,9 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
60381
60577
  const result = await waitForPrChecks({
60382
60578
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
60383
60579
  pollChecks: () => pollRestCommitChecks(sha, repo),
60580
+ // #9589: the evidence-bearing read lets the wait name the rows holding it open and carry the
60581
+ // full check/workflow evidence on the timeout receipt.
60582
+ pollChecksWithEvidence: () => pollRestCommitChecksWithWorkflowEvidence(sha, repo),
60384
60583
  pollRateLimit: () => waitLoopCorePool("ci checks-wait"),
60385
60584
  checksUrl: `https://github.com/${repo}/commit/${sha}/checks`,
60386
60585
  checksDiagnosticCommand: `gh api repos/${repo}/commits/${sha}/check-runs`,
@@ -60393,7 +60592,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
60393
60592
  sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
60394
60593
  log: (message2) => console.warn(message2),
60395
60594
  timeoutMs: timeoutMs ?? PR_CHECKS_TIMEOUT_MS,
60396
- progress: ({ state, elapsedMs, remainingMs }) => console.warn(`ci checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
60595
+ 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`)
60397
60596
  });
60398
60597
  printChecksWaitResult("ci checks-wait", result, o.json);
60399
60598
  });
@@ -66245,7 +66444,7 @@ function typedReadHint(path2) {
66245
66444
  if (!match) return "";
66246
66445
  const [, owner, repo, kind, item, sub] = match;
66247
66446
  const slug = `${owner}/${repo}`;
66248
- const say = (command, note = "") => ` Typed read for this route: \`${command}\`${note}.`;
66447
+ const say = (command, note = "") => ` Typed read for this route: \`MMI_ACTOR=app ${command}\`${note}.`;
66249
66448
  if (kind === "issues") {
66250
66449
  if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
66251
66450
  if (item.startsWith("comments/")) {
@@ -69939,7 +70138,7 @@ function envelopeAwareWriteErr(str2) {
69939
70138
  const invoked = resolveCommandFromArgv(program2, argv);
69940
70139
  const invokedPath = invoked ? canonicalPathFor(commandPath3(invoked)) ?? commandPath3(invoked) : void 0;
69941
70140
  const canonicalArgv = canonicalArgvFor(argv);
69942
- const claimStatus = invoked ? boardClaimStatusFlagRecovery(flag, invokedPath, canonicalArgv) ?? issueCommentAppendOnlyRecovery(flag, invokedPath, canonicalArgv) : void 0;
70141
+ const claimStatus = invoked ? boardClaimStatusFlagRecovery(flag, invokedPath, canonicalArgv) ?? issueCommentAppendOnlyRecovery(flag, invokedPath, canonicalArgv) ?? trainPlanRedundantFlagRecovery(flag, invokedPath, canonicalArgv) : void 0;
69943
70142
  if (claimStatus) {
69944
70143
  if (argvWantsJson3()) {
69945
70144
  process.stderr.write(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.9",
3
+ "version": "4.6.11",
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",