@mutmutco/cli 4.6.4 → 4.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs CHANGED
@@ -526,6 +526,8 @@ __export(github_client_exports, {
526
526
  createGitHubClient: () => createGitHubClient,
527
527
  defaultGitHubClient: () => defaultGitHubClient,
528
528
  githubKeyringTokenRead: () => githubKeyringTokenRead,
529
+ githubPersonalToken: () => githubPersonalToken,
530
+ githubPersonalTokenRead: () => githubPersonalTokenRead,
529
531
  githubToken: () => githubToken,
530
532
  githubTokenRead: () => githubTokenRead,
531
533
  rateLimitResetNote: () => rateLimitResetNote,
@@ -563,6 +565,23 @@ async function githubTokenRead() {
563
565
  if (process.env.GITHUB_TOKEN) return { state: "ok", token: process.env.GITHUB_TOKEN };
564
566
  return githubKeyringTokenRead();
565
567
  }
568
+ async function githubPersonalTokenRead() {
569
+ const mmiPat = process.env.MMI_GH_PAT?.trim();
570
+ if (mmiPat) return { state: "ok", token: mmiPat };
571
+ if (process.env.MMI_GH_POOL !== "app" && process.env.GH_TOKEN) return { state: "ok", token: process.env.GH_TOKEN };
572
+ if (process.env.GITHUB_TOKEN) return { state: "ok", token: process.env.GITHUB_TOKEN };
573
+ return githubKeyringTokenRead();
574
+ }
575
+ async function githubPersonalToken() {
576
+ const read = await githubPersonalTokenRead();
577
+ if (read.state === "failed") {
578
+ throw new GitHubApiError(
579
+ `GitHub identity read FAILED: \`gh auth token\` ${read.detail} \u2014 this is NOT "no token", so the call is refused rather than sent anonymously (an anonymous 401/404 would read as a permission or missing-object answer). Retry, or set MMI_GH_PAT/GH_TOKEN/GITHUB_TOKEN for this process.`,
580
+ { status: 0 }
581
+ );
582
+ }
583
+ return read.state === "ok" ? read.token : void 0;
584
+ }
566
585
  async function githubToken() {
567
586
  const read = await githubTokenRead();
568
587
  if (read.state === "failed") {
@@ -1355,6 +1374,7 @@ __export(cli_shared_exports, {
1355
1374
  fail: () => fail,
1356
1375
  failGraceful: () => failGraceful,
1357
1376
  failGracefulEnvelope: () => failGracefulEnvelope,
1377
+ ghFilingAppEnv: () => ghFilingAppEnv,
1358
1378
  gitOut: () => gitOut,
1359
1379
  githubLogin: () => githubLogin,
1360
1380
  hubHeaders: () => hubHeaders,
@@ -1385,10 +1405,10 @@ function setGhReadTokenProvider(provider) {
1385
1405
  function setGhWriteTokenProvider(provider) {
1386
1406
  ghWriteTokenProvider = provider;
1387
1407
  }
1388
- async function ghFilingAppEnv(file, args) {
1389
- if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || process.env.MMI_ACTOR?.trim() === "personal") return void 0;
1408
+ async function ghFilingAppEnv(file, args, requireApp = false) {
1409
+ if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || !requireApp && process.env.MMI_ACTOR?.trim() === "personal") return void 0;
1390
1410
  if (!isAppFilingWrite(args)) return void 0;
1391
- const explicitApp = process.env.MMI_ACTOR?.trim() === "app";
1411
+ const explicitApp = requireApp || process.env.MMI_ACTOR?.trim() === "app";
1392
1412
  let token;
1393
1413
  try {
1394
1414
  token = await ghWriteTokenProvider();
@@ -1447,7 +1467,7 @@ async function githubLogin() {
1447
1467
  }
1448
1468
  async function hubHeaders(extra = {}) {
1449
1469
  const cfg = await loadConfig();
1450
- const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
1470
+ const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken });
1451
1471
  const base = { ...clientVersionHeaders(), ...extra };
1452
1472
  return t ? { ...base, Authorization: `Bearer ${t}` } : base;
1453
1473
  }
@@ -1616,7 +1636,7 @@ var init_cli_shared = __esm({
1616
1636
  const pool = appEnv ? "app" : !callerEnv ? ambientPool() : !callerEnv.GH_TOKEN ? "personal" : callerEnv.MMI_GH_POOL === "app" ? "app" : callerEnv.GH_TOKEN === process.env.GH_TOKEN ? ambientPool() : "caller";
1617
1637
  recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
1618
1638
  }
1619
- if (!appEnv) return tag(run(), "user");
1639
+ if (!appEnv) return tag(run(), options.env?.MMI_GH_POOL === "app" ? "app_installation" : "user");
1620
1640
  try {
1621
1641
  return await tag(run(appEnv), "app_installation");
1622
1642
  } catch (e) {
@@ -16631,9 +16651,44 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
16631
16651
  "startup_failure"
16632
16652
  ]);
16633
16653
  var TEST_FAILURE_LINE = /^(?:✖|not ok\b|FAIL\b|×)|\bAssertionError\b/;
16654
+ var logLine = (raw) => raw.replace(/^\uFEFF?\d{4}-\d\d-\d\dT[\d:.]+Z\s*/, "").replace(/\x1b\[[0-9;]*m/g, "").trim();
16655
+ function failingTestIds(log) {
16656
+ const ids = /* @__PURE__ */ new Set();
16657
+ let node = false, vitest = false, summary = false;
16658
+ let at;
16659
+ for (const raw of log.split(/\r?\n/)) {
16660
+ const line = logLine(raw);
16661
+ if (/^not ok\b/.test(line)) return void 0;
16662
+ if (/^\u00D7/.test(line)) vitest = true;
16663
+ const fail2 = /^FAIL\s+(\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>\s+.+)?)$/.exec(line);
16664
+ if (fail2) {
16665
+ ids.add(fail2[1].replace(/\s+\d+(?:\.\d+)?m?s$/, "").trim());
16666
+ continue;
16667
+ }
16668
+ if (/^FAIL\b/.test(line)) return void 0;
16669
+ if (/^\u2716 failing tests:?$/.test(line)) {
16670
+ summary = true;
16671
+ at = void 0;
16672
+ continue;
16673
+ }
16674
+ if (!/^\u2716/.test(line)) {
16675
+ const loc = summary ? /^test at (\S+?):(\d+):\d+$/.exec(line) : null;
16676
+ if (loc) at = `${loc[1]}:${loc[2]}`;
16677
+ continue;
16678
+ }
16679
+ node = true;
16680
+ if (!summary) continue;
16681
+ const title = /^\u2716 (.+?)(?:\s+\(\d+(?:\.\d+)?m?s\))?$/.exec(line)?.[1];
16682
+ if (!title || !at) return void 0;
16683
+ ids.add(`${at} > ${title}`);
16684
+ at = void 0;
16685
+ }
16686
+ if ((node || vitest || summary) && (!ids.size || node && !summary)) return void 0;
16687
+ return [...ids];
16688
+ }
16634
16689
  function firstTestFailureLine(log) {
16635
16690
  for (const raw of log.split(/\r?\n/)) {
16636
- const line = raw.replace(/^\uFEFF?\d{4}-\d\d-\d\dT[\d:.]+Z\s*/, "").replace(/\x1b\[[0-9;]*m/g, "").trim();
16691
+ const line = logLine(raw);
16637
16692
  if (TEST_FAILURE_LINE.test(line)) {
16638
16693
  return line.replace(/\b(?:gh[pousr]_|github_pat_|AKIA|sk-)[\w-]+/g, "[redacted]").slice(0, 200);
16639
16694
  }
@@ -17215,7 +17270,9 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
17215
17270
  function commitClosingRefusalMessage(closing, context = "pr merge") {
17216
17271
  const named = closing.map((n) => `#${n}`).join(", ");
17217
17272
  const first = `#${closing[0] ?? "N"}`;
17218
- return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
17273
+ const isQueue = /queue/i.test(context);
17274
+ const remedy = isQueue ? `GitHub's merge queue composes the squash message directly from the commits and accepts no squash-body override (--squash-body-file cannot be sent to the queue). If this PR is meant to close ${named}, register ${named} in the PR body (\`mmi-cli devops pr edit <pr> --body-file <path>\` with \`Closes ${first}\`) and rerun \`pr land\`; if ${named} must stay open, reword the commit message to use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" (never "does not close ${first}"), or re-run with --force to enqueue anyway and let ${named} close.` : `Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
17275
+ return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. ${remedy}`;
17219
17276
  }
17220
17277
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
17221
17278
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -21259,7 +21316,7 @@ async function repoSlug() {
21259
21316
  return "-";
21260
21317
  }
21261
21318
  function registryClientDeps(cfg) {
21262
- return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
21319
+ return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
21263
21320
  }
21264
21321
  async function isOrgRegisteredRepo(cfg, deps = {}) {
21265
21322
  const slug = await (deps.slug ?? repoSlug)();
@@ -23741,10 +23798,14 @@ async function claimOneBoardItem(ctx, selector, options) {
23741
23798
  return;
23742
23799
  }
23743
23800
  if (!options.force) {
23801
+ if (options.bulk) {
23802
+ const { holder: holder2, why } = laneContestParts(contest);
23803
+ 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}`);
23804
+ }
23744
23805
  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);
23745
23806
  const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
23746
23807
  const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
23747
- throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
23808
+ throw new Error(withGatedWriteChainHint(refusal));
23748
23809
  }
23749
23810
  previousHolder = displaced();
23750
23811
  };
@@ -23799,6 +23860,13 @@ async function claimBoardIssue(options, deps = {}) {
23799
23860
  const result = await claimOneBoardItem(ctx, selector, options);
23800
23861
  return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
23801
23862
  }
23863
+ function formatBulkClaimSummary(results) {
23864
+ const skipped = results.filter((r) => !r.claimed);
23865
+ if (skipped.length === 0) return void 0;
23866
+ const landed = results.filter((r) => r.claimed).map((r) => r.ref);
23867
+ const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
23868
+ return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
23869
+ }
23802
23870
  async function claimBoardIssues(options, deps = {}) {
23803
23871
  const cfg = resolveBoardConfig(options.config);
23804
23872
  const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
@@ -27388,10 +27456,10 @@ var rollout_plan_default = {
27388
27456
  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)."
27389
27457
  },
27390
27458
  baseline: {
27391
- version: "4.6.4",
27392
- tag: "v4.6.4",
27393
- commit: "db079140cdfb",
27394
- npm: "@mutmutco/cli@4.6.4"
27459
+ version: "4.6.6",
27460
+ tag: "v4.6.6",
27461
+ commit: "8431efc33b8c",
27462
+ npm: "@mutmutco/cli@4.6.6"
27395
27463
  },
27396
27464
  exitCriterion: "fleet-n-of-n",
27397
27465
  hubOnlyShortcut: "forbidden",
@@ -27408,14 +27476,14 @@ var rollout_plan_default = {
27408
27476
  repo: "mutmutco/mmi-hub",
27409
27477
  role: "canary",
27410
27478
  schedule: "train",
27411
- v3Target: "v4.6.4"
27479
+ v3Target: "v4.6.6"
27412
27480
  }
27413
27481
  ],
27414
27482
  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.",
27415
27483
  rollback: {
27416
27484
  independent: true,
27417
- mechanism: "npm dist-tag latest -> 4.6.4 and redeploy the Hub Lambda from tag v4.6.4 (db079140cdfb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27418
- v3Target: "v4.6.4 (@mutmutco/cli@4.6.4, tag commit db079140cdfb \u2014 last known-good release carrying the repo-index v4-only contract)"
27485
+ mechanism: "npm dist-tag latest -> 4.6.6 and redeploy the Hub Lambda from tag v4.6.6 (8431efc33b8c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27486
+ v3Target: "v4.6.6 (@mutmutco/cli@4.6.6, tag commit 8431efc33b8c \u2014 last known-good release carrying the repo-index v4-only contract)"
27419
27487
  }
27420
27488
  },
27421
27489
  {
@@ -40989,6 +41057,403 @@ var import_node_os17 = require("node:os");
40989
41057
  var import_node_path30 = require("node:path");
40990
41058
  init_cli_shared();
40991
41059
 
41060
+ // src/pr-land.ts
41061
+ var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
41062
+ var PR_LAND_QUEUE_BATCH_SIZE = 5;
41063
+ function prQueueReadHint(prNumber, repo) {
41064
+ return `read the queue entry with \`mmi-cli devops read pr queue ${prNumber} --repo ${repo} --json\` (no queue-admission state file is meant to be inspected directly under %LOCALAPPDATA%/MMI Future/mmi-cli)`;
41065
+ }
41066
+ function prHeadBehindBase(input) {
41067
+ if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
41068
+ return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
41069
+ }
41070
+ function prHeadUpdateRemedy(head, base) {
41071
+ return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
41072
+ }
41073
+ function prLandedBranchFinality(prNumber, base) {
41074
+ return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
41075
+ }
41076
+ var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
41077
+ function isProtectedPrHead(head) {
41078
+ return PROTECTED_PR_HEAD_BRANCHES.includes(head);
41079
+ }
41080
+ function protectedPrHeadRemedy(head, base) {
41081
+ return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
41082
+ }
41083
+ var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
41084
+ var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
41085
+ async function updatePrHeadFromBase(input) {
41086
+ const remedy = prHeadUpdateRemedy(input.head, input.base);
41087
+ if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
41088
+ if (input.localCheckedOut) {
41089
+ const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
41090
+ await input.git(["fetch", "origin", input.base]);
41091
+ try {
41092
+ await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
41093
+ } catch (e) {
41094
+ await input.git(["merge", "--abort"]).catch(() => {
41095
+ });
41096
+ throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
41097
+ }
41098
+ await input.git(["push", "origin", input.head]);
41099
+ const to = (await input.git(["rev-parse", "HEAD"])).trim();
41100
+ return { from: from2, to, base: input.base, via: "local-merge" };
41101
+ }
41102
+ const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
41103
+ const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
41104
+ const from = await readHead2();
41105
+ try {
41106
+ await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
41107
+ } catch (e) {
41108
+ throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
41109
+ }
41110
+ const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
41111
+ for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
41112
+ const to = await readHead2();
41113
+ if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
41114
+ await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
41115
+ }
41116
+ throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
41117
+ }
41118
+ var PR_LAND_POLL_MS = 6e4;
41119
+ var PR_LAND_POLL_MAX_MS = 5 * 6e4;
41120
+ function prLandPollDelayMs(attempt, waiting) {
41121
+ const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
41122
+ const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
41123
+ return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
41124
+ }
41125
+ var PR_LAND_EJECT_CONFIRM_MS = 2e4;
41126
+ var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
41127
+ var PR_LAND_STATE_READ_RETRIES = 3;
41128
+ var PR_LAND_STATE_READ_DELAY_MS = 15e3;
41129
+ var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
41130
+ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
41131
+ const now = deps.now ?? (() => Date.now());
41132
+ const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
41133
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
41134
+ const first = await deps.mergeAuto(prNumber, repo);
41135
+ let result = first;
41136
+ let transientRetried = false;
41137
+ while (result.mergeStatus === "failed") {
41138
+ if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
41139
+ const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
41140
+ if (ready.open && ready.checksPending) {
41141
+ if (now() >= deadline) break;
41142
+ await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
41143
+ if (now() >= deadline) break;
41144
+ const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
41145
+ if (checks.status !== "success" && checks.status !== "skipped") {
41146
+ return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
41147
+ registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
41148
+ }
41149
+ if (now() >= deadline) break;
41150
+ } else {
41151
+ if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
41152
+ transientRetried = true;
41153
+ if (now() >= deadline) break;
41154
+ await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
41155
+ if (now() >= deadline) break;
41156
+ }
41157
+ const retried = await deps.mergeAuto(prNumber, repo);
41158
+ result = retried.mergeStatus !== "failed" ? retried : {
41159
+ ...retried,
41160
+ error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
41161
+ retry: ${retried.error ?? "unknown error"}`
41162
+ };
41163
+ }
41164
+ return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
41165
+ required-check registration did not settle within the enqueue deadline` } : result;
41166
+ }
41167
+ var AUTO_MERGE_CONFIRM_RETRIES = 3;
41168
+ var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
41169
+ async function confirmAutoMergeEnqueued(deps, options) {
41170
+ const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
41171
+ const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
41172
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
41173
+ for (let attempt = 0; attempt < retries; attempt++) {
41174
+ if (await deps.readMerged().catch(() => false)) return "merged";
41175
+ const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
41176
+ if (stuck) return "enqueued";
41177
+ if (attempt < retries - 1) {
41178
+ await sleep2(delayMs);
41179
+ await deps.reEnqueue().catch(() => {
41180
+ });
41181
+ }
41182
+ }
41183
+ if (await deps.readMerged().catch(() => false)) return "merged";
41184
+ return "not-stuck";
41185
+ }
41186
+ async function readGhPrStateWithRetry(fetchState, options) {
41187
+ const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
41188
+ const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
41189
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
41190
+ let lastError = "empty state";
41191
+ for (let attempt = 0; attempt < retries; attempt++) {
41192
+ try {
41193
+ const state = (await fetchState()).trim();
41194
+ if (state) return { ok: true, state };
41195
+ lastError = "empty state";
41196
+ } catch (e) {
41197
+ lastError = String(e.message || "gh pr view failed");
41198
+ }
41199
+ if (attempt < retries - 1) await sleep2(delayMs);
41200
+ }
41201
+ return { ok: false, error: lastError };
41202
+ }
41203
+ async function runPrLand(prNumber, options, deps) {
41204
+ const resolved = await deps.resolveRepo(prNumber, options.repo);
41205
+ const { repo } = resolved;
41206
+ const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
41207
+ if (options.requireTrain !== false) {
41208
+ const verdict = await deps.fetchTrainAuthority(repo);
41209
+ if (!verdict.ok) {
41210
+ return { ...base, error: `train authority unavailable: ${verdict.error}` };
41211
+ }
41212
+ base.train = { role: verdict.authority.role, train: verdict.authority.train };
41213
+ if (!verdict.authority.train) {
41214
+ return {
41215
+ ...base,
41216
+ error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
41217
+ };
41218
+ }
41219
+ }
41220
+ const ciPolicy = await deps.resolveCiPolicy(repo);
41221
+ base.ciPolicy = ciPolicy;
41222
+ const pinned = options.expectHead;
41223
+ if (pinned) base.pinnedHead = { expected: pinned };
41224
+ const headDrift = async (stage) => {
41225
+ if (!pinned) return void 0;
41226
+ if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
41227
+ const observed = await deps.readHeadSha(prNumber, repo);
41228
+ base.pinnedHead = { expected: pinned, observed };
41229
+ if (observed === pinned) return void 0;
41230
+ return {
41231
+ ...base,
41232
+ pinnedHead: { ...base.pinnedHead, refused: "drift" },
41233
+ error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
41234
+ };
41235
+ };
41236
+ const drift = await headDrift("before landing");
41237
+ if (drift) return drift;
41238
+ const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
41239
+ let queueSnapshot;
41240
+ const queued = { base: resolved.base, state: "queued" };
41241
+ if (hasQueue) {
41242
+ if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
41243
+ queueSnapshot = await deps.readMergeQueue(prNumber, repo);
41244
+ if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
41245
+ base.mergeQueue = queued;
41246
+ base.mergeStatus = "auto-merge-enqueued";
41247
+ return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
41248
+ }
41249
+ }
41250
+ const checksWaitError = (checksWait) => {
41251
+ if (checksWait.status === "conflicting") {
41252
+ return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
41253
+ }
41254
+ if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
41255
+ return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
41256
+ }
41257
+ return void 0;
41258
+ };
41259
+ base.checksWait = await deps.waitForChecks(prNumber, repo);
41260
+ const waitError = checksWaitError(base.checksWait);
41261
+ if (waitError) return { ...base, error: hasQueue ? `${waitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : waitError };
41262
+ const driftAfterChecks = await headDrift("after checks");
41263
+ if (driftAfterChecks) return driftAfterChecks;
41264
+ let prState = await deps.readPrState(prNumber, repo);
41265
+ if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
41266
+ return {
41267
+ ...base,
41268
+ pinnedHead: { ...base.pinnedHead, refused: "behind" },
41269
+ error: `pr land #${prNumber}: head is BEHIND ${resolved.base} and pinned by --expect-head \u2014 refusing to update it. Update and re-check the head yourself, then land with the new SHA`
41270
+ };
41271
+ }
41272
+ if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
41273
+ try {
41274
+ base.branchUpdated = await deps.updateBranch(prNumber, repo);
41275
+ } catch (e) {
41276
+ return { ...base, error: `pr land #${prNumber}: ${e.message}` };
41277
+ }
41278
+ base.checksWait = await deps.waitForChecks(prNumber, repo);
41279
+ const reWaitError = checksWaitError(base.checksWait);
41280
+ if (reWaitError) return { ...base, error: hasQueue ? `${reWaitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : reWaitError };
41281
+ prState = await deps.readPrState(prNumber, repo);
41282
+ }
41283
+ if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
41284
+ return {
41285
+ ...base,
41286
+ error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`
41287
+ };
41288
+ }
41289
+ if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
41290
+ const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
41291
+ base.mergeStatus = merge.mergeStatus;
41292
+ if (merge.credential) base.credential = merge.credential;
41293
+ if (merge.credentialRole) base.credentialRole = merge.credentialRole;
41294
+ if (merge.mergeStatus === "queue-admission-pending") {
41295
+ return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
41296
+ }
41297
+ if (merge.mergeStatus === "failed") {
41298
+ const rawError = merge.error ?? "merge failed";
41299
+ const error = hasQueue && !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError;
41300
+ return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error };
41301
+ }
41302
+ if (hasQueue) {
41303
+ base.mergeQueue = queued;
41304
+ if (merge.mergeStatus === "merged") {
41305
+ return queueMerged(base, deps);
41306
+ }
41307
+ const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
41308
+ let confirmed;
41309
+ if (options.enqueueOnly) {
41310
+ const read = await deps.readMergeQueue(prNumber, repo);
41311
+ if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
41312
+ }
41313
+ if (!confirmed) {
41314
+ await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
41315
+ }
41316
+ return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
41317
+ }
41318
+ if (merge.mergeStatus === "merged") {
41319
+ return {
41320
+ ...base,
41321
+ status: "merged",
41322
+ branchFinality: prLandedBranchFinality(prNumber, resolved.base)
41323
+ };
41324
+ }
41325
+ const now = deps.now ?? (() => Date.now());
41326
+ const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
41327
+ if (merged) {
41328
+ return {
41329
+ ...base,
41330
+ status: "auto-merge-enqueued-then-merged",
41331
+ mergeStatus: "merged",
41332
+ branchFinality: prLandedBranchFinality(prNumber, resolved.base)
41333
+ };
41334
+ }
41335
+ return {
41336
+ ...base,
41337
+ error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
41338
+ };
41339
+ }
41340
+ function queueMerged(base, deps) {
41341
+ const result = {
41342
+ ...base,
41343
+ status: "merged-via-queue",
41344
+ mergeStatus: "merged",
41345
+ mergeQueue: { ...base.mergeQueue, state: "merged" },
41346
+ branchFinality: prLandedBranchFinality(base.pr, base.base)
41347
+ };
41348
+ deps.queueProgress?.(result);
41349
+ return result;
41350
+ }
41351
+ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41352
+ const now = deps.now ?? (() => Date.now());
41353
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
41354
+ const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
41355
+ const deadline = deadlineMs ?? now() + timeoutMs;
41356
+ let attempt = 0;
41357
+ let credentialMismatched = false;
41358
+ const observe = (read) => {
41359
+ const seen = deps.observeQueued?.(pr, base.repo, read);
41360
+ if (seen?.mismatched) {
41361
+ credentialMismatched = true;
41362
+ delete base.credential;
41363
+ }
41364
+ if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
41365
+ };
41366
+ while (now() < deadline) {
41367
+ const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
41368
+ snapshot = void 0;
41369
+ if (read.isInMergeQueue || read.state === "MERGED") observe(read);
41370
+ if (read.state === "MERGED") return queueMerged(base, deps);
41371
+ if (read.isInMergeQueue) {
41372
+ base.mergeQueue = {
41373
+ base: base.base,
41374
+ state: "queued",
41375
+ ...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
41376
+ };
41377
+ deps.queueProgress?.(base);
41378
+ if (options.enqueueOnly) return { ...base, status: "queued" };
41379
+ } else if (read.state === "OPEN" && !read.autoMergeRequest) {
41380
+ await sleep2(PR_LAND_EJECT_CONFIRM_MS);
41381
+ const again = await deps.readMergeQueue(pr, base.repo);
41382
+ if (again.state === "MERGED") {
41383
+ observe(again);
41384
+ return queueMerged(base, deps);
41385
+ }
41386
+ if (again.isInMergeQueue || again.autoMergeRequest) {
41387
+ snapshot = again;
41388
+ continue;
41389
+ }
41390
+ const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
41391
+ const result2 = {
41392
+ ...base,
41393
+ mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
41394
+ ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
41395
+ 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)}`
41396
+ };
41397
+ deps.queueProgress?.(result2);
41398
+ return result2;
41399
+ }
41400
+ await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
41401
+ }
41402
+ const result = {
41403
+ ...base,
41404
+ mergeQueue: { ...base.mergeQueue, state: "timeout" },
41405
+ 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`
41406
+ };
41407
+ deps.queueProgress?.(result);
41408
+ return result;
41409
+ }
41410
+ function prLandOutcome(result) {
41411
+ if (result.status === "queue-admission-pending") return "queue-admission-pending";
41412
+ if (result.status !== "failed" && result.status !== "queued") return "merged";
41413
+ if (result.status === "queued") return "queued";
41414
+ const state = result.mergeQueue?.state;
41415
+ return state === "ejected" || state === "timeout" ? state : "failed";
41416
+ }
41417
+ function failuresAllOnBase(prFailures, baseFailures) {
41418
+ const base = new Set(baseFailures);
41419
+ return prFailures.length > 0 && prFailures.every((f) => base.has(f));
41420
+ }
41421
+ async function landMany(prs, landOne, cleanup, hooks) {
41422
+ const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
41423
+ return Promise.all(prs.map(async (pr) => {
41424
+ let result = await safe(pr);
41425
+ if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
41426
+ const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
41427
+ if (baseRed) {
41428
+ result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
41429
+ } else {
41430
+ hooks?.onRetry?.(pr, result);
41431
+ result = { ...await safe(pr), retriedAfterEjection: true };
41432
+ }
41433
+ }
41434
+ const outcome = prLandOutcome(result);
41435
+ const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
41436
+ hooks?.onDone?.();
41437
+ return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
41438
+ }));
41439
+ }
41440
+ function renderPrLandTable(entries) {
41441
+ const rows = entries.map((e) => [
41442
+ `#${e.pr}`,
41443
+ e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
41444
+ [e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
41445
+ ]);
41446
+ const width = (i) => Math.max(...rows.map((r) => r[i].length));
41447
+ return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
41448
+ }
41449
+ function landCredentials(results) {
41450
+ const each = results.map((r) => r.credential ?? "unknown");
41451
+ return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
41452
+ }
41453
+
41454
+ // src/pr-merge-queue.ts
41455
+ var import_yaml3 = __toESM(require_dist(), 1);
41456
+
40992
41457
  // src/test-policy-core.ts
40993
41458
  var import_node_child_process11 = require("node:child_process");
40994
41459
  var import_node_fs32 = require("node:fs");
@@ -41527,6 +41992,9 @@ function loadPolicy(root, readFile10 = readFileOrNull2) {
41527
41992
  if (raw == null) return { mandatory: [], declared: false };
41528
41993
  return parsePolicy(raw, POLICY_FILE);
41529
41994
  }
41995
+ function parseTestPolicy(raw) {
41996
+ return parsePolicy(raw, POLICY_FILE);
41997
+ }
41530
41998
  function parsePolicy(raw, source) {
41531
41999
  try {
41532
42000
  const parsed = JSON.parse(raw);
@@ -41546,6 +42014,10 @@ function parsePolicy(raw, source) {
41546
42014
  ))) {
41547
42015
  throw new Error("protected must be an array of { path, why? } entries");
41548
42016
  }
42017
+ const aggregateJobs = record2.aggregateJobs;
42018
+ if (aggregateJobs !== void 0 && (!Array.isArray(aggregateJobs) || aggregateJobs.some((job) => typeof job !== "string" || !job.trim()))) {
42019
+ throw new Error("aggregateJobs must be an array of workflow job keys");
42020
+ }
41549
42021
  return { ...record2, declared: true };
41550
42022
  } catch (error) {
41551
42023
  throw new Error(`${source} is not valid JSON: ${error.message}`);
@@ -41941,6 +42413,13 @@ var writeApi = async (args) => {
41941
42413
  const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
41942
42414
  return { stdout, credential };
41943
42415
  };
42416
+ var autoMergeApi = async (pr, repo, head) => {
42417
+ const args = ["pr", "merge", pr, "-R", repo, "--squash", "--auto", "--match-head-commit", head];
42418
+ const env = await ghFilingAppEnv("gh", args, true);
42419
+ if (!env) throw new Error("app actor: filing write token unavailable for merge-queue auto-merge fallback");
42420
+ await execFileP("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024, env });
42421
+ return { credential: "app_installation" };
42422
+ };
41944
42423
  var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
41945
42424
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
41946
42425
  function admissionJournal(repo, pr, root) {
@@ -41956,7 +42435,7 @@ function admissionJournal(repo, pr, root) {
41956
42435
  const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
41957
42436
  const validate = (value, previous) => {
41958
42437
  const row = value;
41959
- if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
42438
+ if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
41960
42439
  throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
41961
42440
  }
41962
42441
  if (!previous) {
@@ -41994,7 +42473,17 @@ function admissionJournal(repo, pr, root) {
41994
42473
  },
41995
42474
  save(state, uuid, queueEntry, credential) {
41996
42475
  const previous = load();
41997
- const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {}, ...credential && previous.credential === void 0 ? { credential } : {} }, previous);
42476
+ const nextEntry = queueEntry ?? previous.queueEntry;
42477
+ const queueWitnessed = nextEntry ? true : state === "enqueued" ? previous.queueWitnessed ?? false : previous.queueWitnessed;
42478
+ const receipt = validate({
42479
+ ...previous,
42480
+ state,
42481
+ observedAt: Date.now(),
42482
+ ...uuid ? { uuid } : {},
42483
+ ...nextEntry ? { queueEntry: nextEntry } : {},
42484
+ ...queueWitnessed !== void 0 ? { queueWitnessed } : {},
42485
+ ...credential && previous.credential === void 0 ? { credential } : {}
42486
+ }, previous);
41998
42487
  (0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
41999
42488
  load();
42000
42489
  }
@@ -42017,19 +42506,31 @@ function admissionResponse(output) {
42017
42506
  }
42018
42507
  return { code: Number(response[1]), body: result };
42019
42508
  }
42509
+ function admissionRejected(reason, repo, pr) {
42510
+ const queueHint = repo && pr ? ` Read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` (no queue-admission state file is meant to be inspected or edited directly under %LOCALAPPDATA%/MMI Future/mmi-cli).` : " Read the queue entry with `mmi-cli devops read pr queue <pr> --repo <owner/repo> --json` (no queue-admission state file is meant to be inspected or edited directly under %LOCALAPPDATA%/MMI Future/mmi-cli).";
42511
+ if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
42512
+ return {
42513
+ mergeStatus: "failed",
42514
+ retryable: false,
42515
+ admissionCause: "enqueuer-not-authorized",
42516
+ error: `queue admission rejected (missing merge permission): ${reason}. The landing identity reported in \`credential\` may not merge into this base through the merge queue. Granting it (the GitHub App's merge permission, or a ruleset bypass) is a repository or organization setting only an admin can change; do not rerun pr land until that grant exists, and never fall back to a personal token.${queueHint}`
42517
+ };
42518
+ }
42519
+ return {
42520
+ mergeStatus: "failed",
42521
+ retryable: false,
42522
+ error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.${queueHint}`
42523
+ };
42524
+ }
42020
42525
  async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
42021
42526
  const unknown = (reason) => ({
42022
42527
  mergeStatus: "failed",
42023
42528
  retryable: false,
42024
42529
  outcomeUnknown: true,
42025
- error: `queue submission outcome unknown: ${reason}. Preserve the receipt and rerun pr land to reconcile read-only; no new request was sent after this uncertainty.`
42026
- });
42027
- const rejected = (reason) => ({
42028
- mergeStatus: "failed",
42029
- retryable: false,
42030
- error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.`
42530
+ error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` and rerun \`mmi-cli devops pr land ${pr} --repo ${repo} --json\` to reconcile read-only \u2014 no new request was sent after this uncertainty.`
42031
42531
  });
42032
- const reconcile = async (reason, recordEntry = false) => {
42532
+ const rejected = (reason) => admissionRejected(reason, repo, pr);
42533
+ const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
42033
42534
  try {
42034
42535
  const observed = await readPrMergeQueue(pr, repo, read);
42035
42536
  if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
@@ -42041,15 +42542,15 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42041
42542
  return { mergeStatus: "auto-merge-enqueued" };
42042
42543
  }
42043
42544
  const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
42044
- if (saved?.queueEntry && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
42045
- const enqueuedAt = Date.parse(saved.queueEntry.enqueuedAt);
42545
+ const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
42546
+ if (saved && boundMs !== void 0 && Number.isFinite(boundMs) && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
42046
42547
  const events = await readArrayPages(
42047
42548
  `repos/${repo}/issues/${pr}/timeline?per_page=100`,
42048
42549
  options.timelineRead ?? read,
42049
42550
  "queue ejection timeline unreadable"
42050
42551
  );
42051
42552
  const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
42052
- const adds = times("added_to_merge_queue").filter((at) => at >= enqueuedAt);
42553
+ const adds = times("added_to_merge_queue").filter((at) => at >= boundMs);
42053
42554
  const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
42054
42555
  if (removals.length === 1) {
42055
42556
  journal.save("ejected");
@@ -42088,7 +42589,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42088
42589
  options.selection.selected = true;
42089
42590
  options.selection.intentId = receipt.intentId;
42090
42591
  }
42091
- if (receipt.state === "enqueued" || receipt.state === "merged") return reconcile(`reconcile-needed for ${repo}#${pr}, submitted head ${receipt.head}, operation ${receipt.uuid ?? "completed immediately"}: admission completed but current membership is absent; inspect the exact queue candidate/ejection before a new intent`, receipt.state === "enqueued");
42592
+ if (options.enqueueOnly && options.force) {
42593
+ return unknown(`refusing --enqueue-only --force for ${repo}#${pr} while receipt for head ${receipt.head} is in state '${receipt.state}' (operation ${receipt.uuid ?? "unconfirmed"}): drop --enqueue-only --force and run the read-only reconcile first`);
42594
+ }
42595
+ if (receipt.state === "enqueued" || receipt.state === "merged") return reconcile(`reconcile-needed for ${repo}#${pr}, submitted head ${receipt.head}, operation ${receipt.uuid ?? "completed immediately"}: admission completed but current membership is absent; inspect the exact queue candidate/ejection before a new intent`, receipt.state === "enqueued", true);
42092
42596
  if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
42093
42597
  } else {
42094
42598
  if (resume) return rejected("the previous operation failed; rerun after reviewing its cause");
@@ -42138,13 +42642,33 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42138
42642
  }
42139
42643
  return reconcile(`GitHub reported ${body.status}, but matching-head membership or merged state is unconfirmed`, body.status === "enqueued");
42140
42644
  }
42645
+ const tryAutoMergeFallback = async (reason, uuid) => {
42646
+ const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42647
+ if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
42648
+ let fallback;
42649
+ try {
42650
+ fallback = await autoMerge(pr, repo, head);
42651
+ } catch {
42652
+ return void 0;
42653
+ }
42654
+ const fallbackCredential = fallback?.credential ?? credential ?? receipt?.credential;
42655
+ try {
42656
+ journal.save("enqueued", uuid, void 0, fallbackCredential);
42657
+ } catch {
42658
+ return reconcile("the accepted auto-merge fallback could not be checkpointed");
42659
+ }
42660
+ return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
42661
+ };
42141
42662
  if (code === 400 || code === 403 || code === 404 || code === 422) {
42663
+ const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
42664
+ const fallbackResult = await tryAutoMergeFallback(reason);
42665
+ if (fallbackResult) return fallbackResult;
42142
42666
  try {
42143
42667
  journal.save("failed", void 0, void 0, credential);
42144
42668
  } catch {
42145
42669
  return unknown("the explicit rejection could not be checkpointed");
42146
42670
  }
42147
- return rejected(body.details?.message ?? body.message ?? `HTTP ${code}`);
42671
+ return rejected(reason);
42148
42672
  }
42149
42673
  const details = body.details;
42150
42674
  if (code !== 202 && code !== 409 || body.status !== "pending" || !UUID.test(details?.uuid ?? "")) return reconcile("the send returned no recognized admission result");
@@ -42175,6 +42699,28 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42175
42699
  continue;
42176
42700
  }
42177
42701
  if (!["failed", "enqueued", "merged"].includes(status ?? "")) return reconcile("the operation returned an unrecognized state");
42702
+ if (status === "failed") {
42703
+ const reason = details?.message ?? "GitHub reported a failed operation";
42704
+ const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42705
+ if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
42706
+ let fallback = void 0;
42707
+ let fallbackSucceeded = false;
42708
+ try {
42709
+ fallback = await autoMerge(pr, repo, head);
42710
+ fallbackSucceeded = true;
42711
+ } catch {
42712
+ }
42713
+ if (fallbackSucceeded) {
42714
+ const fallbackCredential = fallback?.credential ?? receipt?.credential;
42715
+ try {
42716
+ journal.save("enqueued", receipt.uuid, void 0, fallbackCredential);
42717
+ } catch {
42718
+ return reconcile("the accepted auto-merge fallback could not be checkpointed");
42719
+ }
42720
+ return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
42721
+ }
42722
+ }
42723
+ }
42178
42724
  try {
42179
42725
  journal.save(status, receipt.uuid);
42180
42726
  } catch {
@@ -42246,7 +42792,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42246
42792
  status: "preparation-needed",
42247
42793
  reason,
42248
42794
  issueTargets: targets.map((number) => `${repo}#${number}`),
42249
- nextAction: reason === "queue-message" ? "PR author: 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."
42795
+ 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."
42250
42796
  },
42251
42797
  error: `queue preparation needed: ${detail}`
42252
42798
  });
@@ -42358,6 +42904,100 @@ async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
42358
42904
  const run = pages.flatMap((page) => page.workflow_runs).filter((run2) => run2.head_branch?.startsWith(prefix)).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id - a.id)[0];
42359
42905
  return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
42360
42906
  }
42907
+ function workflowNeeds(text) {
42908
+ const doc = (0, import_yaml3.parse)(text);
42909
+ const out = /* @__PURE__ */ new Map();
42910
+ for (const [key, job] of Object.entries(doc?.jobs ?? {})) {
42911
+ const raw = job?.needs;
42912
+ const needs = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw.filter((n) => typeof n === "string") : [];
42913
+ const name = typeof job?.name === "string" ? job.name : key;
42914
+ out.set(key, { name, needs, matrix: job?.strategy?.matrix != null, dynamicName: name.includes("${{") });
42915
+ }
42916
+ return out;
42917
+ }
42918
+ function jobKey(name, jobs) {
42919
+ const leg = name.replace(/ \([^()]*\)$/, "");
42920
+ const matches = [...jobs].filter(([, job]) => job.name === name || job.matrix && leg !== name && job.name === leg).map(([key]) => key);
42921
+ if (matches.length !== 1) return void 0;
42922
+ const collides = [...jobs.keys()].some((key) => key !== matches[0] && (key === name || key === leg));
42923
+ return collides ? void 0 : matches[0];
42924
+ }
42925
+ function aggregateKey(name, jobs, declared) {
42926
+ const matches = [...jobs].filter(([, job]) => job.name === name).map(([key2]) => key2);
42927
+ if (matches.length !== 1) return void 0;
42928
+ const key = matches[0];
42929
+ if ([...jobs.keys()].some((other) => other !== key && other === name)) return void 0;
42930
+ return declared.has(key) && !jobs.get(key).matrix ? key : void 0;
42931
+ }
42932
+ var JOB_PAGES = 10;
42933
+ var JOBS_PER_PAGE = 100;
42934
+ async function attemptJobs(repo, run, gh2) {
42935
+ if (!Number.isSafeInteger(run.run_attempt) || run.run_attempt < 1) return void 0;
42936
+ const jobs = [];
42937
+ let total;
42938
+ for (let page = 1; page <= JOB_PAGES; page++) {
42939
+ const body = JSON.parse(await gh2([`repos/${repo}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=${JOBS_PER_PAGE}&page=${page}`]));
42940
+ if (!Number.isSafeInteger(body.total_count) || !Array.isArray(body.jobs)) return void 0;
42941
+ total ??= body.total_count;
42942
+ if (body.total_count !== total) return void 0;
42943
+ jobs.push(...body.jobs);
42944
+ if (jobs.length >= total) return jobs.length === total && new Set(jobs.map((j) => j.id)).size === jobs.length ? jobs : void 0;
42945
+ if (body.jobs.length !== JOBS_PER_PAGE) return void 0;
42946
+ }
42947
+ return void 0;
42948
+ }
42949
+ async function runFailingTests(repo, run, gh2, declared) {
42950
+ const jobs = await attemptJobs(repo, run, gh2);
42951
+ if (!jobs) return void 0;
42952
+ const bad = jobs.filter((j) => !["success", "skipped", "neutral"].includes(j.conclusion ?? ""));
42953
+ if (!bad.length || bad.some((j) => j.conclusion !== "failure")) return void 0;
42954
+ const names = [];
42955
+ const markerless = [];
42956
+ for (const job of bad) {
42957
+ const found = failingTestIds(await gh2([`repos/${repo}/actions/jobs/${job.id}/logs`]));
42958
+ if (!found) return void 0;
42959
+ if (found.length) names.push(...found);
42960
+ else markerless.push(job);
42961
+ }
42962
+ if (!names.length) return void 0;
42963
+ if (!markerless.length) return names;
42964
+ const aggregates = await declared();
42965
+ if (!aggregates.size || !run.path || !/^[a-f0-9]{40}$/i.test(run.head_sha ?? "")) return void 0;
42966
+ const workflow = workflowNeeds(await gh2([`repos/${repo}/contents/${run.path}?ref=${run.head_sha}`, "-H", "Accept: application/vnd.github.raw"]));
42967
+ if ([...workflow.values()].some((job) => job.dynamicName)) return void 0;
42968
+ const failedKeys = new Set(bad.map((j) => jobKey(j.name ?? "", workflow)).filter((k) => !!k));
42969
+ for (const job of markerless) {
42970
+ const key = aggregateKey(job.name ?? "", workflow, aggregates);
42971
+ if (!key || !workflow.get(key).needs.some((n) => failedKeys.has(n))) return void 0;
42972
+ }
42973
+ return names;
42974
+ }
42975
+ async function declaredAggregates(repo, sha, gh2) {
42976
+ if (!/^[a-f0-9]{40}$/i.test(sha ?? "")) return /* @__PURE__ */ new Set();
42977
+ try {
42978
+ return new Set(parseTestPolicy(await gh2([`repos/${repo}/contents/test-policy.json?ref=${sha}`, "-H", "Accept: application/vnd.github.raw"])).aggregateJobs ?? []);
42979
+ } catch {
42980
+ return /* @__PURE__ */ new Set();
42981
+ }
42982
+ }
42983
+ var appOnlyReadApi = async (args) => {
42984
+ const env = await ghReadEnv();
42985
+ if (env?.MMI_GH_POOL !== "app") throw new Error("base-red: no App read environment; evidence unread");
42986
+ return (await execFileP("gh", ["api", ...args], { timeout: 2e4, maxBuffer: 16 * 1024 * 1024, env })).stdout;
42987
+ };
42988
+ async function readBaseRedVerdict(result, gh2 = appOnlyReadApi) {
42989
+ const run = result.mergeQueue?.ejectedRun;
42990
+ if (run?.conclusion !== "failure") return void 0;
42991
+ const ejected = JSON.parse(await gh2([`repos/${result.repo}/actions/runs/${run.id}`]));
42992
+ const { workflow_runs } = JSON.parse(await gh2([`repos/${result.repo}/actions/workflows/${ejected.workflow_id}/runs?branch=${encodeURIComponent(result.base)}&event=push&per_page=1`]));
42993
+ const baseRun = workflow_runs?.[0];
42994
+ if (baseRun?.status !== "completed" || baseRun.conclusion !== "failure") return void 0;
42995
+ let declared;
42996
+ const aggregates = () => declared ??= declaredAggregates(result.repo, baseRun.head_sha, gh2);
42997
+ const mine = await runFailingTests(result.repo, { ...ejected, id: run.id }, gh2, aggregates);
42998
+ const base = mine && await runFailingTests(result.repo, baseRun, gh2, aggregates);
42999
+ return mine && base && failuresAllOnBase(mine, base) ? { baseRunId: String(baseRun.id) } : void 0;
43000
+ }
42361
43001
  function prMergeQueueLine(result) {
42362
43002
  const queue = result.mergeQueue;
42363
43003
  if (!queue) return void 0;
@@ -42513,38 +43153,57 @@ function registerCancelOrphans(ci2, writeToken) {
42513
43153
  });
42514
43154
  }
42515
43155
  var RUN_CANCEL_STATUS = "cancel-requested";
42516
- async function cancelWorkflowRun(repo, id, deps) {
43156
+ var RUN_FORCE_CANCEL_STATUS = "force-cancel-requested";
43157
+ async function cancelWorkflowRun(repo, id, deps, force = false) {
42517
43158
  const raw = String(id).trim();
42518
43159
  if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
42519
43160
  const runId = Number(raw);
42520
43161
  if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
42521
43162
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
42522
43163
  try {
42523
- await deps.cancel(repo, runId);
43164
+ if (force) {
43165
+ if (!deps.forceCancel) throw new Error("force-cancel adapter unavailable");
43166
+ await deps.forceCancel(repo, runId);
43167
+ } else await deps.cancel(repo, runId);
42524
43168
  } catch (error) {
42525
43169
  const detail = String(error.stderr ?? error);
42526
43170
  throw new Error(/\b403\b/.test(detail) ? "run cancel: GitHub App denied (HTTP 403); actions:write permission is required; no personal-token fallback" : `run cancel: ${detail}`);
42527
43171
  }
42528
- return { ok: true, repo, runId, status: RUN_CANCEL_STATUS };
43172
+ const settledInfo = force && deps.read ? await settled(repo, runId, { read: deps.read, sleep: deps.sleep }).then((runStatus) => ({ settled: runStatus === "completed", runStatus })) : void 0;
43173
+ return { ok: true, repo, runId, ...force ? { force: true } : {}, status: force ? RUN_FORCE_CANCEL_STATUS : RUN_CANCEL_STATUS, ...settledInfo };
42529
43174
  }
42530
43175
  function registerRunCancel(run, writeToken) {
42531
- run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--json", "machine-readable receipt").action(async (id, options) => {
43176
+ run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--force", "force-cancel the run so always() jobs stop too (#9512)").option("--json", "machine-readable receipt").action(async (id, options) => {
42532
43177
  let receipt;
43178
+ let token;
43179
+ const read = async (args) => (await execFileP("gh", ["api", ...args], {
43180
+ timeout: 2e4,
43181
+ maxBuffer: 16 * 1024 * 1024,
43182
+ env: await ghReadEnv()
43183
+ })).stdout;
42533
43184
  try {
42534
43185
  receipt = await cancelWorkflowRun(options.repo, id, {
43186
+ read,
42535
43187
  cancel: async (repo, runId) => {
42536
- const token = await writeToken();
43188
+ token ??= await writeToken();
42537
43189
  await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
42538
43190
  timeout: 2e4,
42539
- env: { ...process.env, GH_TOKEN: token }
43191
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
43192
+ });
43193
+ },
43194
+ forceCancel: async (repo, runId) => {
43195
+ token ??= await writeToken();
43196
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
43197
+ timeout: 2e4,
43198
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
42540
43199
  });
42541
43200
  }
42542
- });
43201
+ }, options.force === true);
42543
43202
  } catch (error) {
42544
43203
  return fail(error.message);
42545
43204
  }
42546
43205
  if (options.json) console.log(JSON.stringify(receipt));
42547
- else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 cancel requested (status: ${receipt.status})`);
43206
+ 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}` : ""})`);
42548
43207
  });
42549
43208
  }
42550
43209
 
@@ -42963,6 +43622,8 @@ function registerBoardCommands(program3) {
42963
43622
  console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
42964
43623
  if (result.claimed) for (const line of formatDecisionLines(result.ref, ruled[i].decisions ?? [])) console.log(line);
42965
43624
  });
43625
+ const summary = formatBulkClaimSummary(bulk.results);
43626
+ if (summary) console.log(summary);
42966
43627
  printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings]);
42967
43628
  }
42968
43629
  if (bulk.failed > 0) process.exitCode = 1;
@@ -44978,7 +45639,7 @@ function registerSecretsCommands(program3) {
44978
45639
  return;
44979
45640
  }
44980
45641
  const d = makeSecretsDeps(cfg);
44981
- const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
45642
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
44982
45643
  const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
44983
45644
  const repo = o.repo ?? `mutmutco/${slug}`;
44984
45645
  const meta = await fetchProjectBySlug(slug, regDeps);
@@ -45033,7 +45694,7 @@ function registerSecretsCommands(program3) {
45033
45694
  return;
45034
45695
  }
45035
45696
  const d = makeSecretsDeps(cfg);
45036
- const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
45697
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
45037
45698
  const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
45038
45699
  const repo = o.repo ?? `mutmutco/${slug}`;
45039
45700
  const meta = await fetchProjectBySlug(slug, regDeps);
@@ -51977,386 +52638,6 @@ function boardAdvanceFailureMessage(result) {
51977
52638
  return `pr merge: board advance incomplete for ${named} \u2014 the PR MERGED, but ${failures.length === 1 ? "this issue" : "these issues"} did not reach Done on the board.`;
51978
52639
  }
51979
52640
 
51980
- // src/pr-land.ts
51981
- var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
51982
- var PR_LAND_QUEUE_BATCH_SIZE = 5;
51983
- function prHeadBehindBase(input) {
51984
- if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
51985
- return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
51986
- }
51987
- function prHeadUpdateRemedy(head, base) {
51988
- return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
51989
- }
51990
- function prLandedBranchFinality(prNumber, base) {
51991
- return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
51992
- }
51993
- var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
51994
- function isProtectedPrHead(head) {
51995
- return PROTECTED_PR_HEAD_BRANCHES.includes(head);
51996
- }
51997
- function protectedPrHeadRemedy(head, base) {
51998
- return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
51999
- }
52000
- var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
52001
- var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
52002
- async function updatePrHeadFromBase(input) {
52003
- const remedy = prHeadUpdateRemedy(input.head, input.base);
52004
- if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
52005
- if (input.localCheckedOut) {
52006
- const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
52007
- await input.git(["fetch", "origin", input.base]);
52008
- try {
52009
- await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
52010
- } catch (e) {
52011
- await input.git(["merge", "--abort"]).catch(() => {
52012
- });
52013
- throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
52014
- }
52015
- await input.git(["push", "origin", input.head]);
52016
- const to = (await input.git(["rev-parse", "HEAD"])).trim();
52017
- return { from: from2, to, base: input.base, via: "local-merge" };
52018
- }
52019
- const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
52020
- const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
52021
- const from = await readHead2();
52022
- try {
52023
- await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
52024
- } catch (e) {
52025
- throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
52026
- }
52027
- const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52028
- for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
52029
- const to = await readHead2();
52030
- if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
52031
- await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
52032
- }
52033
- throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
52034
- }
52035
- var PR_LAND_POLL_MS = 6e4;
52036
- var PR_LAND_POLL_MAX_MS = 5 * 6e4;
52037
- function prLandPollDelayMs(attempt, waiting) {
52038
- const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
52039
- const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
52040
- return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
52041
- }
52042
- var PR_LAND_EJECT_CONFIRM_MS = 2e4;
52043
- var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
52044
- var PR_LAND_STATE_READ_RETRIES = 3;
52045
- var PR_LAND_STATE_READ_DELAY_MS = 15e3;
52046
- var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
52047
- async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
52048
- const now = deps.now ?? (() => Date.now());
52049
- const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
52050
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52051
- const first = await deps.mergeAuto(prNumber, repo);
52052
- let result = first;
52053
- let transientRetried = false;
52054
- while (result.mergeStatus === "failed") {
52055
- if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
52056
- const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
52057
- if (ready.open && ready.checksPending) {
52058
- if (now() >= deadline) break;
52059
- await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
52060
- if (now() >= deadline) break;
52061
- const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
52062
- if (checks.status !== "success" && checks.status !== "skipped") {
52063
- return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
52064
- registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
52065
- }
52066
- if (now() >= deadline) break;
52067
- } else {
52068
- if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
52069
- transientRetried = true;
52070
- if (now() >= deadline) break;
52071
- await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
52072
- if (now() >= deadline) break;
52073
- }
52074
- const retried = await deps.mergeAuto(prNumber, repo);
52075
- result = retried.mergeStatus !== "failed" ? retried : {
52076
- ...retried,
52077
- error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
52078
- retry: ${retried.error ?? "unknown error"}`
52079
- };
52080
- }
52081
- return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
52082
- required-check registration did not settle within the enqueue deadline` } : result;
52083
- }
52084
- var AUTO_MERGE_CONFIRM_RETRIES = 3;
52085
- var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
52086
- async function confirmAutoMergeEnqueued(deps, options) {
52087
- const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
52088
- const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
52089
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52090
- for (let attempt = 0; attempt < retries; attempt++) {
52091
- if (await deps.readMerged().catch(() => false)) return "merged";
52092
- const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
52093
- if (stuck) return "enqueued";
52094
- if (attempt < retries - 1) {
52095
- await sleep2(delayMs);
52096
- await deps.reEnqueue().catch(() => {
52097
- });
52098
- }
52099
- }
52100
- if (await deps.readMerged().catch(() => false)) return "merged";
52101
- return "not-stuck";
52102
- }
52103
- async function readGhPrStateWithRetry(fetchState, options) {
52104
- const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
52105
- const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
52106
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52107
- let lastError = "empty state";
52108
- for (let attempt = 0; attempt < retries; attempt++) {
52109
- try {
52110
- const state = (await fetchState()).trim();
52111
- if (state) return { ok: true, state };
52112
- lastError = "empty state";
52113
- } catch (e) {
52114
- lastError = String(e.message || "gh pr view failed");
52115
- }
52116
- if (attempt < retries - 1) await sleep2(delayMs);
52117
- }
52118
- return { ok: false, error: lastError };
52119
- }
52120
- async function runPrLand(prNumber, options, deps) {
52121
- const resolved = await deps.resolveRepo(prNumber, options.repo);
52122
- const { repo } = resolved;
52123
- const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
52124
- if (options.requireTrain !== false) {
52125
- const verdict = await deps.fetchTrainAuthority(repo);
52126
- if (!verdict.ok) {
52127
- return { ...base, error: `train authority unavailable: ${verdict.error}` };
52128
- }
52129
- base.train = { role: verdict.authority.role, train: verdict.authority.train };
52130
- if (!verdict.authority.train) {
52131
- return {
52132
- ...base,
52133
- error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
52134
- };
52135
- }
52136
- }
52137
- const ciPolicy = await deps.resolveCiPolicy(repo);
52138
- base.ciPolicy = ciPolicy;
52139
- const pinned = options.expectHead;
52140
- if (pinned) base.pinnedHead = { expected: pinned };
52141
- const headDrift = async (stage) => {
52142
- if (!pinned) return void 0;
52143
- if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
52144
- const observed = await deps.readHeadSha(prNumber, repo);
52145
- base.pinnedHead = { expected: pinned, observed };
52146
- if (observed === pinned) return void 0;
52147
- return {
52148
- ...base,
52149
- pinnedHead: { ...base.pinnedHead, refused: "drift" },
52150
- error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
52151
- };
52152
- };
52153
- const drift = await headDrift("before landing");
52154
- if (drift) return drift;
52155
- const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
52156
- let queueSnapshot;
52157
- const queued = { base: resolved.base, state: "queued" };
52158
- if (hasQueue) {
52159
- if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
52160
- queueSnapshot = await deps.readMergeQueue(prNumber, repo);
52161
- if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
52162
- base.mergeQueue = queued;
52163
- base.mergeStatus = "auto-merge-enqueued";
52164
- return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
52165
- }
52166
- }
52167
- const checksWaitError = (checksWait) => {
52168
- if (checksWait.status === "conflicting") {
52169
- return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
52170
- }
52171
- if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
52172
- return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
52173
- }
52174
- return void 0;
52175
- };
52176
- base.checksWait = await deps.waitForChecks(prNumber, repo);
52177
- const waitError = checksWaitError(base.checksWait);
52178
- if (waitError) return { ...base, error: waitError };
52179
- const driftAfterChecks = await headDrift("after checks");
52180
- if (driftAfterChecks) return driftAfterChecks;
52181
- let prState = await deps.readPrState(prNumber, repo);
52182
- if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
52183
- return {
52184
- ...base,
52185
- pinnedHead: { ...base.pinnedHead, refused: "behind" },
52186
- error: `pr land #${prNumber}: head is BEHIND ${resolved.base} and pinned by --expect-head \u2014 refusing to update it. Update and re-check the head yourself, then land with the new SHA`
52187
- };
52188
- }
52189
- if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
52190
- try {
52191
- base.branchUpdated = await deps.updateBranch(prNumber, repo);
52192
- } catch (e) {
52193
- return { ...base, error: `pr land #${prNumber}: ${e.message}` };
52194
- }
52195
- base.checksWait = await deps.waitForChecks(prNumber, repo);
52196
- const reWaitError = checksWaitError(base.checksWait);
52197
- if (reWaitError) return { ...base, error: reWaitError };
52198
- prState = await deps.readPrState(prNumber, repo);
52199
- }
52200
- if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
52201
- return {
52202
- ...base,
52203
- error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
52204
- };
52205
- }
52206
- if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
52207
- const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
52208
- base.mergeStatus = merge.mergeStatus;
52209
- if (merge.credential) base.credential = merge.credential;
52210
- if (merge.credentialRole) base.credentialRole = merge.credentialRole;
52211
- if (merge.mergeStatus === "queue-admission-pending") {
52212
- return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
52213
- }
52214
- if (merge.mergeStatus === "failed") {
52215
- return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error: merge.error ?? "merge failed" };
52216
- }
52217
- if (hasQueue) {
52218
- base.mergeQueue = queued;
52219
- if (merge.mergeStatus === "merged") {
52220
- return queueMerged(base, deps);
52221
- }
52222
- const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
52223
- let confirmed;
52224
- if (options.enqueueOnly) {
52225
- const read = await deps.readMergeQueue(prNumber, repo);
52226
- if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
52227
- }
52228
- if (!confirmed) {
52229
- await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
52230
- }
52231
- return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
52232
- }
52233
- if (merge.mergeStatus === "merged") {
52234
- return {
52235
- ...base,
52236
- status: "merged",
52237
- branchFinality: prLandedBranchFinality(prNumber, resolved.base)
52238
- };
52239
- }
52240
- const now = deps.now ?? (() => Date.now());
52241
- const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
52242
- if (merged) {
52243
- return {
52244
- ...base,
52245
- status: "auto-merge-enqueued-then-merged",
52246
- mergeStatus: "merged",
52247
- branchFinality: prLandedBranchFinality(prNumber, resolved.base)
52248
- };
52249
- }
52250
- return {
52251
- ...base,
52252
- error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
52253
- };
52254
- }
52255
- function queueMerged(base, deps) {
52256
- const result = {
52257
- ...base,
52258
- status: "merged-via-queue",
52259
- mergeStatus: "merged",
52260
- mergeQueue: { ...base.mergeQueue, state: "merged" },
52261
- branchFinality: prLandedBranchFinality(base.pr, base.base)
52262
- };
52263
- deps.queueProgress?.(result);
52264
- return result;
52265
- }
52266
- async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
52267
- const now = deps.now ?? (() => Date.now());
52268
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52269
- const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
52270
- const deadline = deadlineMs ?? now() + timeoutMs;
52271
- let attempt = 0;
52272
- let credentialMismatched = false;
52273
- const observe = (read) => {
52274
- const seen = deps.observeQueued?.(pr, base.repo, read);
52275
- if (seen?.mismatched) {
52276
- credentialMismatched = true;
52277
- delete base.credential;
52278
- }
52279
- if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
52280
- };
52281
- while (now() < deadline) {
52282
- const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
52283
- snapshot = void 0;
52284
- if (read.isInMergeQueue || read.state === "MERGED") observe(read);
52285
- if (read.state === "MERGED") return queueMerged(base, deps);
52286
- if (read.isInMergeQueue) {
52287
- base.mergeQueue = {
52288
- base: base.base,
52289
- state: "queued",
52290
- ...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
52291
- };
52292
- deps.queueProgress?.(base);
52293
- if (options.enqueueOnly) return { ...base, status: "queued" };
52294
- } else if (read.state === "OPEN" && !read.autoMergeRequest) {
52295
- await sleep2(PR_LAND_EJECT_CONFIRM_MS);
52296
- const again = await deps.readMergeQueue(pr, base.repo);
52297
- if (again.state === "MERGED") {
52298
- observe(again);
52299
- return queueMerged(base, deps);
52300
- }
52301
- if (again.isInMergeQueue || again.autoMergeRequest) {
52302
- snapshot = again;
52303
- continue;
52304
- }
52305
- const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
52306
- const result2 = {
52307
- ...base,
52308
- mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
52309
- ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
52310
- error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
52311
- };
52312
- deps.queueProgress?.(result2);
52313
- return result2;
52314
- }
52315
- await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
52316
- }
52317
- const result = {
52318
- ...base,
52319
- mergeQueue: { ...base.mergeQueue, state: "timeout" },
52320
- error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
52321
- };
52322
- deps.queueProgress?.(result);
52323
- return result;
52324
- }
52325
- function prLandOutcome(result) {
52326
- if (result.status === "queue-admission-pending") return "queue-admission-pending";
52327
- if (result.status !== "failed" && result.status !== "queued") return "merged";
52328
- if (result.status === "queued") return "queued";
52329
- const state = result.mergeQueue?.state;
52330
- return state === "ejected" || state === "timeout" ? state : "failed";
52331
- }
52332
- async function landMany(prs, landOne, cleanup, hooks) {
52333
- const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
52334
- return Promise.all(prs.map(async (pr) => {
52335
- let result = await safe(pr);
52336
- if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
52337
- hooks?.onRetry?.(pr, result);
52338
- result = { ...await safe(pr), retriedAfterEjection: true };
52339
- }
52340
- const outcome = prLandOutcome(result);
52341
- const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
52342
- hooks?.onDone?.();
52343
- return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
52344
- }));
52345
- }
52346
- function renderPrLandTable(entries) {
52347
- const rows = entries.map((e) => [
52348
- `#${e.pr}`,
52349
- e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
52350
- [e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
52351
- ]);
52352
- const width = (i) => Math.max(...rows.map((r) => r[i].length));
52353
- return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
52354
- }
52355
- function landCredentials(results) {
52356
- const each = results.map((r) => r.credential ?? "unknown");
52357
- return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
52358
- }
52359
-
52360
52641
  // src/git-worktree-observation.ts
52361
52642
  function parseGitWorktreePorcelain(text) {
52362
52643
  const rows = [];
@@ -59167,7 +59448,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59167
59448
  }
59168
59449
  class PrHeadBehindBaseError extends Error {
59169
59450
  }
59170
- pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 the queue's own check timeout is 60)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
59451
+ pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
59171
59452
  const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
59172
59453
  if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
59173
59454
  if (opts.expectHead !== void 0) {
@@ -59213,7 +59494,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59213
59494
  pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
59214
59495
  hasMergeQueue: hasPrMergeQueue,
59215
59496
  enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
59216
- const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force });
59497
+ const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
59217
59498
  if (receipt.warning) console.warn(receipt.warning);
59218
59499
  return receipt;
59219
59500
  },
@@ -59402,6 +59683,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59402
59683
  return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
59403
59684
  };
59404
59685
  const entries = await landMany(numbers, landOne, cleanupMerged, {
59686
+ baseRedCheck: (_pr, r) => readBaseRedVerdict(r),
59405
59687
  onRetry: (pr2, r) => console.warn(`pr land: queue ejected PR #${pr2} (${r.error}) \u2014 landing it once more (head updated from base if behind)`),
59406
59688
  onDone: () => {
59407
59689
  inFlight = Math.max(1, inFlight - 1);
@@ -62442,8 +62724,47 @@ function adoptExportProdDeps() {
62442
62724
  }
62443
62725
  };
62444
62726
  }
62727
+ var PACKAGE_ORG = "mutmutco";
62728
+ var PACKAGE_REGISTRY = "https://npm.pkg.github.com";
62729
+ function normalizeOrgPackageName(raw) {
62730
+ const trimmed = raw.trim().replace(/^@mutmutco\//, "");
62731
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(trimmed)) {
62732
+ throw new Error(`--package must be a @mutmutco/<name> or <name> package slug (received '${raw}')`);
62733
+ }
62734
+ return { short: trimmed, full: `@${PACKAGE_ORG}/${trimmed}` };
62735
+ }
62736
+ async function listPackageVersions(opts, deps) {
62737
+ const { short: short3, full } = normalizeOrgPackageName(opts.package);
62738
+ const limit = opts.limit ?? 5;
62739
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
62740
+ throw new Error(`--limit must be an integer from 1 to 100 (received '${String(opts.limit)}')`);
62741
+ }
62742
+ const raw = (await deps.ghApi(`orgs/${PACKAGE_ORG}/packages/npm/${encodeURIComponent(short3)}/versions?per_page=${limit}`)).toString("utf8");
62743
+ const parsed = JSON.parse(raw);
62744
+ if (!Array.isArray(parsed)) {
62745
+ throw new Error(`unexpected GitHub Packages response for ${full}`);
62746
+ }
62747
+ const versions = parsed.slice(0, limit).map((entry) => ({
62748
+ version: String(entry?.name ?? ""),
62749
+ publishedAt: typeof entry?.created_at === "string" ? entry.created_at : typeof entry?.updated_at === "string" ? entry.updated_at : null
62750
+ }));
62751
+ return { package: full, registry: PACKAGE_REGISTRY, versions };
62752
+ }
62445
62753
  function registerPackagesCommands(program3) {
62446
- const packages = program3.command("packages").description("consumer adoption of verified package exports (#7833)");
62754
+ const packages = program3.command("packages").description("GitHub Packages version lookup and consumer adoption of verified package exports (#7833, #9542)");
62755
+ packages.command("versions").description("list newest-first published versions of a @mutmutco/* npm package from GitHub Packages (orgs/mutmutco/packages/npm/<pkg>/versions; never bare npm view)").option("--package <pkg>", "@mutmutco/<pkg> or <pkg> name on GitHub Packages", "mmwork-ds").option("--limit <n>", "maximum number of versions to return (1-100)", "5").option("--json", "machine-readable output").action(async (opts) => {
62756
+ const limit = /^[1-9]\d*$/.test(opts.limit.trim()) ? Number(opts.limit.trim()) : NaN;
62757
+ try {
62758
+ const res = await listPackageVersions({ package: opts.package, limit }, adoptExportProdDeps());
62759
+ if (opts.json) {
62760
+ console.log(JSON.stringify(res, null, 2));
62761
+ } else {
62762
+ for (const v of res.versions) console.log(`${v.version} ${v.publishedAt ?? ""}`);
62763
+ }
62764
+ } catch (error) {
62765
+ fail(`packages versions refused: ${error.message}`);
62766
+ }
62767
+ });
62447
62768
  packages.command("adopt-export").description("verify a package-export artifact and install its exact kit pair offline, keeping registry URLs and SRI in the lock").requiredOption("--run <id>", "package-export workflow run id").requiredOption("--artifact <id>", "artifact id from the run summary").requiredOption("--digest <sha256:hex>", "artifact digest from the run summary").requiredOption("--ds <version>", "exact @mutmutco/mmwork-ds version").requiredOption("--screens <version>", "exact @mutmutco/mmwork-screens version").option("--dir <path>", "npm project holding package.json and package-lock.json", ".").action(async (opts) => {
62448
62769
  const id = (value, flag) => {
62449
62770
  if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
@@ -65689,7 +66010,7 @@ var RELEASE_LIST_QUERY_VALUE = { per_page: RUNS_QUERY_VALUE.per_page, page: RUNS
65689
66010
  var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
65690
66011
  var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
65691
66012
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
65692
- var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
66013
+ var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/artifacts[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
65693
66014
  var API_GET_RECOVERY = " No other GET read exists \u2014 do not retry path variants; use a dedicated mmi-cli subcommand or ask a human token holder. This refusal exits 1 on stderr with nothing on stdout: check the exit code and never merge stderr into stdout (2>&1) before JSON-parsing.";
65694
66015
  var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
65695
66016
  function typedReadHint(path2) {
@@ -65726,7 +66047,7 @@ function orgScopedHint(path2) {
65726
66047
  if (!match) return "";
65727
66048
  const org = match[1];
65728
66049
  const runners = /^orgs\/[^/?#]+\/actions\/runners(?:\/|$)/.test(path2.split("?")[0]);
65729
- const surface = runners ? `Runner/lane state has no App-token read: a human token holder answers it with \`gh api orgs/${org}/actions/runners\` (docs/Guides/gh-runner-runbook.md).` : "It needs a human token holder with an org-scoped token, or a dedicated mmi-cli verb when one exists for this resource.";
66050
+ const surface = runners ? `For which lane is busy in a repo, use the App-token reads \`mmi-cli devops read run list --repo <owner>/<repo> --status in_progress --json databaseId,workflowName\` followed by \`mmi-cli devops read run view <id> --repo <owner>/<repo> --json jobs\` (runner-name/status per job); fleet-wide runner online/offline state has no App-token read and a human token holder answers it with \`gh api orgs/${org}/actions/runners\` (docs/Guides/gh-runner-runbook.md).` : "It needs a human token holder with an org-scoped token, or a dedicated mmi-cli verb when one exists for this resource.";
65730
66051
  return ` This route is ORG-SCOPED (\`orgs/${org}/...\`) and is deliberately outside the allowlist \u2014 no App-token read exists for org-level resources, so retrying path variants cannot work and no \`repos/{owner}/{repo}/...\` respelling of it will be accepted. ${surface} A refusal writes nothing to stdout: check the exit code and never merge stderr into the JSON pipe (2>&1).`;
65731
66052
  }
65732
66053
  function tagsReadHint(path2) {
@@ -65735,11 +66056,17 @@ function tagsReadHint(path2) {
65735
66056
  const slug = `${match[1]}/${match[2]}`;
65736
66057
  return ` No App-token REST read exists for git tags or refs \u2014 read git tags directly with \`git ls-remote --tags origin 'refs/tags/*'\` (no token needed; \`git ls-remote origin refs/tags/<tag>\` for one tag), or read tag-backed GitHub Releases with \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/releases?per_page=20"\`.`;
65737
66058
  }
66059
+ function branchRulesReadHint(path2) {
66060
+ const match = new RegExp(`^repos/(${NAME})/(${NAME})/(?:rules/branches/[^/?#]+|branches/[^/?#]+/protection(?:/|$)|rulesets(?:/|$))`).exec(path2.split("?")[0]);
66061
+ if (!match) return "";
66062
+ const slug = `${match[1]}/${match[2]}`;
66063
+ return ` No raw rules, rulesets or branch-protection read is on the App allowlist, so retrying path or query variants cannot work \u2014 inspect a PR's required status contexts with \`MMI_ACTOR=app mmi-cli devops pr checks-required <pr> --repo ${slug} --json\`, or verify repository ruleset and branch-protection health with \`mmi-cli devops bootstrap verify --repo ${slug} --json\` and \`mmi-cli devops ci audit --repo ${slug} --json\`.`;
66064
+ }
65738
66065
  function parseApiGetPath(rawPath) {
65739
66066
  const path2 = rawPath.trim();
65740
66067
  const refuse = () => {
65741
66068
  const hint = /^[A-Za-z]:[/\\]/.test(path2) ? " Git Bash/MSYS may have rewritten a leading-slash argument; retry with MSYS_NO_PATHCONV=1 and the slash-free repos/... path. The endpoint must still be allowlisted." : "";
65742
- const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${hint}`;
66069
+ const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${branchRulesReadHint(path2)}${hint}`;
65743
66070
  return new Error(`api get: refusing '${rawPath}' \u2014${routeHints}${API_GET_RECOVERY} Only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.`);
65744
66071
  };
65745
66072
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
@@ -65752,8 +66079,8 @@ function parseApiGetPath(rawPath) {
65752
66079
  if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
65753
66080
  if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
65754
66081
  if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
65755
- if (ACTIONS_JOBS_RE.test(path2)) return { kind: "actions-jobs", path: path2, paginate: true };
65756
- if (ACTIONS_ARTIFACTS_RE.test(path2)) return { kind: "actions-artifacts", path: path2, paginate: true };
66082
+ if (ACTIONS_JOBS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-jobs", path: path2, paginate: q === -1 };
66083
+ if (ACTIONS_ARTIFACTS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-artifacts", path: path2, paginate: q === -1 };
65757
66084
  if (ACTIONS_ARTIFACT_ZIP_RE.test(path2)) return { kind: "actions-artifact-zip", path: path2, paginate: false };
65758
66085
  if (ACTIONS_JOB_LOGS_RE.test(path2)) return { kind: "actions-job-logs", path: path2, paginate: false };
65759
66086
  if (RELEASE_LIST_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "release-list", path: path2, paginate: false };
@@ -68607,7 +68934,7 @@ function registerDeveloperCommands(program3) {
68607
68934
  async function runWhoami(io = consoleIo) {
68608
68935
  const cfg = await loadConfig();
68609
68936
  const report = await resolveWhoami({
68610
- hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
68937
+ hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken }),
68611
68938
  ghLogin: githubLogin
68612
68939
  });
68613
68940
  io.log(JSON.stringify(report));
@@ -68618,7 +68945,7 @@ function registerDeveloperCommands(program3) {
68618
68945
  });
68619
68946
  whoami.command("diagnose-hub").description("bounded cache-free Hub issuance diagnostic").action(async () => {
68620
68947
  const cfg = await loadConfig();
68621
- const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubToken, fetch });
68948
+ const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubPersonalToken, fetch });
68622
68949
  consoleIo.log(JSON.stringify(report));
68623
68950
  if (report.category !== "issued") process.exitCode = 1;
68624
68951
  });
@@ -69457,7 +69784,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
69457
69784
  appActorCommand = commandPath4;
69458
69785
  setAuditCommand(commandPath4);
69459
69786
  if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
69460
- if (commandPath4 === "ci cancel-orphans") return;
69787
+ if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
69461
69788
  try {
69462
69789
  await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
69463
69790
  } catch (e) {
@@ -70294,7 +70621,7 @@ registerCiChecksWait(ci);
70294
70621
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
70295
70622
  registerRunCancel(
70296
70623
  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"),
70297
- async () => (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
70624
+ async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
70298
70625
  );
70299
70626
  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) => {
70300
70627
  if (o.apply && o.parkRuleset) {