@mutmutco/cli 4.6.5 → 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.
Files changed (2) hide show
  1. package/dist/main.cjs +683 -465
  2. package/package.json +1 -1
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") {
@@ -1448,7 +1467,7 @@ async function githubLogin() {
1448
1467
  }
1449
1468
  async function hubHeaders(extra = {}) {
1450
1469
  const cfg = await loadConfig();
1451
- const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
1470
+ const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken });
1452
1471
  const base = { ...clientVersionHeaders(), ...extra };
1453
1472
  return t ? { ...base, Authorization: `Bearer ${t}` } : base;
1454
1473
  }
@@ -16632,9 +16651,44 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
16632
16651
  "startup_failure"
16633
16652
  ]);
16634
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
+ }
16635
16689
  function firstTestFailureLine(log) {
16636
16690
  for (const raw of log.split(/\r?\n/)) {
16637
- 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);
16638
16692
  if (TEST_FAILURE_LINE.test(line)) {
16639
16693
  return line.replace(/\b(?:gh[pousr]_|github_pat_|AKIA|sk-)[\w-]+/g, "[redacted]").slice(0, 200);
16640
16694
  }
@@ -17216,7 +17270,9 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
17216
17270
  function commitClosingRefusalMessage(closing, context = "pr merge") {
17217
17271
  const named = closing.map((n) => `#${n}`).join(", ");
17218
17272
  const first = `#${closing[0] ?? "N"}`;
17219
- 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}`;
17220
17276
  }
17221
17277
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
17222
17278
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -21260,7 +21316,7 @@ async function repoSlug() {
21260
21316
  return "-";
21261
21317
  }
21262
21318
  function registryClientDeps(cfg) {
21263
- return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
21319
+ return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
21264
21320
  }
21265
21321
  async function isOrgRegisteredRepo(cfg, deps = {}) {
21266
21322
  const slug = await (deps.slug ?? repoSlug)();
@@ -27400,10 +27456,10 @@ var rollout_plan_default = {
27400
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)."
27401
27457
  },
27402
27458
  baseline: {
27403
- version: "4.6.5",
27404
- tag: "v4.6.5",
27405
- commit: "cd6f8a959ed5",
27406
- npm: "@mutmutco/cli@4.6.5"
27459
+ version: "4.6.6",
27460
+ tag: "v4.6.6",
27461
+ commit: "8431efc33b8c",
27462
+ npm: "@mutmutco/cli@4.6.6"
27407
27463
  },
27408
27464
  exitCriterion: "fleet-n-of-n",
27409
27465
  hubOnlyShortcut: "forbidden",
@@ -27420,14 +27476,14 @@ var rollout_plan_default = {
27420
27476
  repo: "mutmutco/mmi-hub",
27421
27477
  role: "canary",
27422
27478
  schedule: "train",
27423
- v3Target: "v4.6.5"
27479
+ v3Target: "v4.6.6"
27424
27480
  }
27425
27481
  ],
27426
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.",
27427
27483
  rollback: {
27428
27484
  independent: true,
27429
- mechanism: "npm dist-tag latest -> 4.6.5 and redeploy the Hub Lambda from tag v4.6.5 (cd6f8a959ed5); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27430
- v3Target: "v4.6.5 (@mutmutco/cli@4.6.5, tag commit cd6f8a959ed5 \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)"
27431
27487
  }
27432
27488
  },
27433
27489
  {
@@ -41001,6 +41057,403 @@ var import_node_os17 = require("node:os");
41001
41057
  var import_node_path30 = require("node:path");
41002
41058
  init_cli_shared();
41003
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
+
41004
41457
  // src/test-policy-core.ts
41005
41458
  var import_node_child_process11 = require("node:child_process");
41006
41459
  var import_node_fs32 = require("node:fs");
@@ -41539,6 +41992,9 @@ function loadPolicy(root, readFile10 = readFileOrNull2) {
41539
41992
  if (raw == null) return { mandatory: [], declared: false };
41540
41993
  return parsePolicy(raw, POLICY_FILE);
41541
41994
  }
41995
+ function parseTestPolicy(raw) {
41996
+ return parsePolicy(raw, POLICY_FILE);
41997
+ }
41542
41998
  function parsePolicy(raw, source) {
41543
41999
  try {
41544
42000
  const parsed = JSON.parse(raw);
@@ -41558,6 +42014,10 @@ function parsePolicy(raw, source) {
41558
42014
  ))) {
41559
42015
  throw new Error("protected must be an array of { path, why? } entries");
41560
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
+ }
41561
42021
  return { ...record2, declared: true };
41562
42022
  } catch (error) {
41563
42023
  throw new Error(`${source} is not valid JSON: ${error.message}`);
@@ -41975,7 +42435,7 @@ function admissionJournal(repo, pr, root) {
41975
42435
  const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
41976
42436
  const validate = (value, previous) => {
41977
42437
  const row = value;
41978
- 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)))) {
41979
42439
  throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
41980
42440
  }
41981
42441
  if (!previous) {
@@ -42013,7 +42473,17 @@ function admissionJournal(repo, pr, root) {
42013
42473
  },
42014
42474
  save(state, uuid, queueEntry, credential) {
42015
42475
  const previous = load();
42016
- 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);
42017
42487
  (0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
42018
42488
  load();
42019
42489
  }
@@ -42036,19 +42506,20 @@ function admissionResponse(output) {
42036
42506
  }
42037
42507
  return { code: Number(response[1]), body: result };
42038
42508
  }
42039
- function admissionRejected(reason) {
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).";
42040
42511
  if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
42041
42512
  return {
42042
42513
  mergeStatus: "failed",
42043
42514
  retryable: false,
42044
42515
  admissionCause: "enqueuer-not-authorized",
42045
- 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.`
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}`
42046
42517
  };
42047
42518
  }
42048
42519
  return {
42049
42520
  mergeStatus: "failed",
42050
42521
  retryable: false,
42051
- error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.`
42522
+ error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.${queueHint}`
42052
42523
  };
42053
42524
  }
42054
42525
  async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
@@ -42056,10 +42527,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42056
42527
  mergeStatus: "failed",
42057
42528
  retryable: false,
42058
42529
  outcomeUnknown: true,
42059
- 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.`
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.`
42060
42531
  });
42061
- const rejected = admissionRejected;
42062
- const reconcile = async (reason, recordEntry = false) => {
42532
+ const rejected = (reason) => admissionRejected(reason, repo, pr);
42533
+ const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
42063
42534
  try {
42064
42535
  const observed = await readPrMergeQueue(pr, repo, read);
42065
42536
  if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
@@ -42071,15 +42542,15 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42071
42542
  return { mergeStatus: "auto-merge-enqueued" };
42072
42543
  }
42073
42544
  const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
42074
- if (saved?.queueEntry && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
42075
- 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) {
42076
42547
  const events = await readArrayPages(
42077
42548
  `repos/${repo}/issues/${pr}/timeline?per_page=100`,
42078
42549
  options.timelineRead ?? read,
42079
42550
  "queue ejection timeline unreadable"
42080
42551
  );
42081
42552
  const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
42082
- const adds = times("added_to_merge_queue").filter((at) => at >= enqueuedAt);
42553
+ const adds = times("added_to_merge_queue").filter((at) => at >= boundMs);
42083
42554
  const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
42084
42555
  if (removals.length === 1) {
42085
42556
  journal.save("ejected");
@@ -42118,7 +42589,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42118
42589
  options.selection.selected = true;
42119
42590
  options.selection.intentId = receipt.intentId;
42120
42591
  }
42121
- 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);
42122
42596
  if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
42123
42597
  } else {
42124
42598
  if (resume) return rejected("the previous operation failed; rerun after reviewing its cause");
@@ -42171,31 +42645,19 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42171
42645
  const tryAutoMergeFallback = async (reason, uuid) => {
42172
42646
  const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42173
42647
  if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
42648
+ let fallback;
42174
42649
  try {
42175
- const fallback = await autoMerge(pr, repo, head);
42176
- const fallbackCredential = fallback?.credential ?? credential ?? receipt?.credential;
42177
- const observed = await readPrMergeQueue(pr, repo, read);
42178
- if (observed.state === "MERGED" && observed.headRefOid === head) {
42179
- try {
42180
- journal.save("merged", uuid, void 0, fallbackCredential);
42181
- } catch {
42182
- return reconcile("the completed auto-merge fallback could not be checkpointed");
42183
- }
42184
- return { mergeStatus: "merged" };
42185
- }
42186
- if (observed.isInMergeQueue && observed.headRefOid === head) {
42187
- const entry = observed.mergeQueueEntry;
42188
- const queueEntry = entry?.headCommit && entry.enqueuedAt ? { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt } : void 0;
42189
- try {
42190
- journal.save("enqueued", uuid, queueEntry, fallbackCredential);
42191
- } catch {
42192
- return reconcile("the enqueued auto-merge fallback could not be checkpointed");
42193
- }
42194
- return { mergeStatus: "auto-merge-enqueued" };
42195
- }
42650
+ fallback = await autoMerge(pr, repo, head);
42196
42651
  } catch {
42652
+ return void 0;
42197
42653
  }
42198
- return void 0;
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);
42199
42661
  };
42200
42662
  if (code === 400 || code === 403 || code === 404 || code === 422) {
42201
42663
  const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
@@ -42241,29 +42703,21 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42241
42703
  const reason = details?.message ?? "GitHub reported a failed operation";
42242
42704
  const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42243
42705
  if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
42706
+ let fallback = void 0;
42707
+ let fallbackSucceeded = false;
42244
42708
  try {
42245
- const fallback = await autoMerge(pr, repo, head);
42709
+ fallback = await autoMerge(pr, repo, head);
42710
+ fallbackSucceeded = true;
42711
+ } catch {
42712
+ }
42713
+ if (fallbackSucceeded) {
42246
42714
  const fallbackCredential = fallback?.credential ?? receipt?.credential;
42247
- const observed = await readPrMergeQueue(pr, repo, read);
42248
- if (observed.state === "MERGED" && observed.headRefOid === head) {
42249
- try {
42250
- journal.save("merged", receipt.uuid, void 0, fallbackCredential);
42251
- } catch {
42252
- return reconcile("the completed auto-merge fallback could not be checkpointed");
42253
- }
42254
- return { mergeStatus: "merged" };
42255
- }
42256
- if (observed.isInMergeQueue && observed.headRefOid === head) {
42257
- const entry = observed.mergeQueueEntry;
42258
- const queueEntry = entry?.headCommit && entry.enqueuedAt ? { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt } : void 0;
42259
- try {
42260
- journal.save("enqueued", receipt.uuid, queueEntry, fallbackCredential);
42261
- } catch {
42262
- return reconcile("the enqueued auto-merge fallback could not be checkpointed");
42263
- }
42264
- return { mergeStatus: "auto-merge-enqueued" };
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");
42265
42719
  }
42266
- } catch {
42720
+ return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
42267
42721
  }
42268
42722
  }
42269
42723
  }
@@ -42338,7 +42792,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42338
42792
  status: "preparation-needed",
42339
42793
  reason,
42340
42794
  issueTargets: targets.map((number) => `${repo}#${number}`),
42341
- 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."
42342
42796
  },
42343
42797
  error: `queue preparation needed: ${detail}`
42344
42798
  });
@@ -42450,6 +42904,100 @@ async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
42450
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];
42451
42905
  return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
42452
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
+ }
42453
43001
  function prMergeQueueLine(result) {
42454
43002
  const queue = result.mergeQueue;
42455
43003
  if (!queue) return void 0;
@@ -42613,31 +43161,41 @@ async function cancelWorkflowRun(repo, id, deps, force = false) {
42613
43161
  if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
42614
43162
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
42615
43163
  try {
42616
- if (force && deps.forceCancel) await deps.forceCancel(repo, runId);
42617
- else 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);
42618
43168
  } catch (error) {
42619
43169
  const detail = String(error.stderr ?? error);
42620
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}`);
42621
43171
  }
42622
- return { ok: true, repo, runId, ...force ? { force: true } : {}, status: force ? RUN_FORCE_CANCEL_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 };
42623
43174
  }
42624
43175
  function registerRunCancel(run, writeToken) {
42625
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) => {
42626
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;
42627
43184
  try {
42628
43185
  receipt = await cancelWorkflowRun(options.repo, id, {
43186
+ read,
42629
43187
  cancel: async (repo, runId) => {
42630
- const token = await writeToken();
43188
+ token ??= await writeToken();
42631
43189
  await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
42632
43190
  timeout: 2e4,
42633
- env: { ...process.env, GH_TOKEN: token }
43191
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
42634
43192
  });
42635
43193
  },
42636
43194
  forceCancel: async (repo, runId) => {
42637
- const token = await writeToken();
43195
+ token ??= await writeToken();
42638
43196
  await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
42639
43197
  timeout: 2e4,
42640
- env: { ...process.env, GH_TOKEN: token }
43198
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
42641
43199
  });
42642
43200
  }
42643
43201
  }, options.force === true);
@@ -42645,7 +43203,7 @@ function registerRunCancel(run, writeToken) {
42645
43203
  return fail(error.message);
42646
43204
  }
42647
43205
  if (options.json) console.log(JSON.stringify(receipt));
42648
- else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.force ? "force-cancel" : "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}` : ""})`);
42649
43207
  });
42650
43208
  }
42651
43209
 
@@ -45081,7 +45639,7 @@ function registerSecretsCommands(program3) {
45081
45639
  return;
45082
45640
  }
45083
45641
  const d = makeSecretsDeps(cfg);
45084
- const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
45642
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
45085
45643
  const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
45086
45644
  const repo = o.repo ?? `mutmutco/${slug}`;
45087
45645
  const meta = await fetchProjectBySlug(slug, regDeps);
@@ -45136,7 +45694,7 @@ function registerSecretsCommands(program3) {
45136
45694
  return;
45137
45695
  }
45138
45696
  const d = makeSecretsDeps(cfg);
45139
- const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
45697
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
45140
45698
  const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
45141
45699
  const repo = o.repo ?? `mutmutco/${slug}`;
45142
45700
  const meta = await fetchProjectBySlug(slug, regDeps);
@@ -52080,386 +52638,6 @@ function boardAdvanceFailureMessage(result) {
52080
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.`;
52081
52639
  }
52082
52640
 
52083
- // src/pr-land.ts
52084
- var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
52085
- var PR_LAND_QUEUE_BATCH_SIZE = 5;
52086
- function prHeadBehindBase(input) {
52087
- if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
52088
- return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
52089
- }
52090
- function prHeadUpdateRemedy(head, base) {
52091
- 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`;
52092
- }
52093
- function prLandedBranchFinality(prNumber, base) {
52094
- 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`;
52095
- }
52096
- var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
52097
- function isProtectedPrHead(head) {
52098
- return PROTECTED_PR_HEAD_BRANCHES.includes(head);
52099
- }
52100
- function protectedPrHeadRemedy(head, base) {
52101
- 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`;
52102
- }
52103
- var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
52104
- var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
52105
- async function updatePrHeadFromBase(input) {
52106
- const remedy = prHeadUpdateRemedy(input.head, input.base);
52107
- if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
52108
- if (input.localCheckedOut) {
52109
- const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
52110
- await input.git(["fetch", "origin", input.base]);
52111
- try {
52112
- await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
52113
- } catch (e) {
52114
- await input.git(["merge", "--abort"]).catch(() => {
52115
- });
52116
- throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
52117
- }
52118
- await input.git(["push", "origin", input.head]);
52119
- const to = (await input.git(["rev-parse", "HEAD"])).trim();
52120
- return { from: from2, to, base: input.base, via: "local-merge" };
52121
- }
52122
- const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
52123
- const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
52124
- const from = await readHead2();
52125
- try {
52126
- await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
52127
- } catch (e) {
52128
- throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
52129
- }
52130
- const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52131
- for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
52132
- const to = await readHead2();
52133
- if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
52134
- await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
52135
- }
52136
- 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`);
52137
- }
52138
- var PR_LAND_POLL_MS = 6e4;
52139
- var PR_LAND_POLL_MAX_MS = 5 * 6e4;
52140
- function prLandPollDelayMs(attempt, waiting) {
52141
- const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
52142
- const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
52143
- return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
52144
- }
52145
- var PR_LAND_EJECT_CONFIRM_MS = 2e4;
52146
- var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
52147
- var PR_LAND_STATE_READ_RETRIES = 3;
52148
- var PR_LAND_STATE_READ_DELAY_MS = 15e3;
52149
- var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
52150
- async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
52151
- const now = deps.now ?? (() => Date.now());
52152
- const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
52153
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52154
- const first = await deps.mergeAuto(prNumber, repo);
52155
- let result = first;
52156
- let transientRetried = false;
52157
- while (result.mergeStatus === "failed") {
52158
- if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
52159
- const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
52160
- if (ready.open && ready.checksPending) {
52161
- if (now() >= deadline) break;
52162
- await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
52163
- if (now() >= deadline) break;
52164
- const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
52165
- if (checks.status !== "success" && checks.status !== "skipped") {
52166
- return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
52167
- registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
52168
- }
52169
- if (now() >= deadline) break;
52170
- } else {
52171
- if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
52172
- transientRetried = true;
52173
- if (now() >= deadline) break;
52174
- await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
52175
- if (now() >= deadline) break;
52176
- }
52177
- const retried = await deps.mergeAuto(prNumber, repo);
52178
- result = retried.mergeStatus !== "failed" ? retried : {
52179
- ...retried,
52180
- error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
52181
- retry: ${retried.error ?? "unknown error"}`
52182
- };
52183
- }
52184
- return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
52185
- required-check registration did not settle within the enqueue deadline` } : result;
52186
- }
52187
- var AUTO_MERGE_CONFIRM_RETRIES = 3;
52188
- var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
52189
- async function confirmAutoMergeEnqueued(deps, options) {
52190
- const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
52191
- const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
52192
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52193
- for (let attempt = 0; attempt < retries; attempt++) {
52194
- if (await deps.readMerged().catch(() => false)) return "merged";
52195
- const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
52196
- if (stuck) return "enqueued";
52197
- if (attempt < retries - 1) {
52198
- await sleep2(delayMs);
52199
- await deps.reEnqueue().catch(() => {
52200
- });
52201
- }
52202
- }
52203
- if (await deps.readMerged().catch(() => false)) return "merged";
52204
- return "not-stuck";
52205
- }
52206
- async function readGhPrStateWithRetry(fetchState, options) {
52207
- const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
52208
- const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
52209
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52210
- let lastError = "empty state";
52211
- for (let attempt = 0; attempt < retries; attempt++) {
52212
- try {
52213
- const state = (await fetchState()).trim();
52214
- if (state) return { ok: true, state };
52215
- lastError = "empty state";
52216
- } catch (e) {
52217
- lastError = String(e.message || "gh pr view failed");
52218
- }
52219
- if (attempt < retries - 1) await sleep2(delayMs);
52220
- }
52221
- return { ok: false, error: lastError };
52222
- }
52223
- async function runPrLand(prNumber, options, deps) {
52224
- const resolved = await deps.resolveRepo(prNumber, options.repo);
52225
- const { repo } = resolved;
52226
- const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
52227
- if (options.requireTrain !== false) {
52228
- const verdict = await deps.fetchTrainAuthority(repo);
52229
- if (!verdict.ok) {
52230
- return { ...base, error: `train authority unavailable: ${verdict.error}` };
52231
- }
52232
- base.train = { role: verdict.authority.role, train: verdict.authority.train };
52233
- if (!verdict.authority.train) {
52234
- return {
52235
- ...base,
52236
- error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
52237
- };
52238
- }
52239
- }
52240
- const ciPolicy = await deps.resolveCiPolicy(repo);
52241
- base.ciPolicy = ciPolicy;
52242
- const pinned = options.expectHead;
52243
- if (pinned) base.pinnedHead = { expected: pinned };
52244
- const headDrift = async (stage) => {
52245
- if (!pinned) return void 0;
52246
- if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
52247
- const observed = await deps.readHeadSha(prNumber, repo);
52248
- base.pinnedHead = { expected: pinned, observed };
52249
- if (observed === pinned) return void 0;
52250
- return {
52251
- ...base,
52252
- pinnedHead: { ...base.pinnedHead, refused: "drift" },
52253
- error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
52254
- };
52255
- };
52256
- const drift = await headDrift("before landing");
52257
- if (drift) return drift;
52258
- const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
52259
- let queueSnapshot;
52260
- const queued = { base: resolved.base, state: "queued" };
52261
- if (hasQueue) {
52262
- if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
52263
- queueSnapshot = await deps.readMergeQueue(prNumber, repo);
52264
- if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
52265
- base.mergeQueue = queued;
52266
- base.mergeStatus = "auto-merge-enqueued";
52267
- return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
52268
- }
52269
- }
52270
- const checksWaitError = (checksWait) => {
52271
- if (checksWait.status === "conflicting") {
52272
- return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
52273
- }
52274
- if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
52275
- return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
52276
- }
52277
- return void 0;
52278
- };
52279
- base.checksWait = await deps.waitForChecks(prNumber, repo);
52280
- const waitError = checksWaitError(base.checksWait);
52281
- if (waitError) return { ...base, error: waitError };
52282
- const driftAfterChecks = await headDrift("after checks");
52283
- if (driftAfterChecks) return driftAfterChecks;
52284
- let prState = await deps.readPrState(prNumber, repo);
52285
- if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
52286
- return {
52287
- ...base,
52288
- pinnedHead: { ...base.pinnedHead, refused: "behind" },
52289
- 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`
52290
- };
52291
- }
52292
- if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
52293
- try {
52294
- base.branchUpdated = await deps.updateBranch(prNumber, repo);
52295
- } catch (e) {
52296
- return { ...base, error: `pr land #${prNumber}: ${e.message}` };
52297
- }
52298
- base.checksWait = await deps.waitForChecks(prNumber, repo);
52299
- const reWaitError = checksWaitError(base.checksWait);
52300
- if (reWaitError) return { ...base, error: reWaitError };
52301
- prState = await deps.readPrState(prNumber, repo);
52302
- }
52303
- if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
52304
- return {
52305
- ...base,
52306
- error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
52307
- };
52308
- }
52309
- if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
52310
- const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
52311
- base.mergeStatus = merge.mergeStatus;
52312
- if (merge.credential) base.credential = merge.credential;
52313
- if (merge.credentialRole) base.credentialRole = merge.credentialRole;
52314
- if (merge.mergeStatus === "queue-admission-pending") {
52315
- return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
52316
- }
52317
- if (merge.mergeStatus === "failed") {
52318
- return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error: merge.error ?? "merge failed" };
52319
- }
52320
- if (hasQueue) {
52321
- base.mergeQueue = queued;
52322
- if (merge.mergeStatus === "merged") {
52323
- return queueMerged(base, deps);
52324
- }
52325
- const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
52326
- let confirmed;
52327
- if (options.enqueueOnly) {
52328
- const read = await deps.readMergeQueue(prNumber, repo);
52329
- if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
52330
- }
52331
- if (!confirmed) {
52332
- await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
52333
- }
52334
- return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
52335
- }
52336
- if (merge.mergeStatus === "merged") {
52337
- return {
52338
- ...base,
52339
- status: "merged",
52340
- branchFinality: prLandedBranchFinality(prNumber, resolved.base)
52341
- };
52342
- }
52343
- const now = deps.now ?? (() => Date.now());
52344
- const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
52345
- if (merged) {
52346
- return {
52347
- ...base,
52348
- status: "auto-merge-enqueued-then-merged",
52349
- mergeStatus: "merged",
52350
- branchFinality: prLandedBranchFinality(prNumber, resolved.base)
52351
- };
52352
- }
52353
- return {
52354
- ...base,
52355
- error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
52356
- };
52357
- }
52358
- function queueMerged(base, deps) {
52359
- const result = {
52360
- ...base,
52361
- status: "merged-via-queue",
52362
- mergeStatus: "merged",
52363
- mergeQueue: { ...base.mergeQueue, state: "merged" },
52364
- branchFinality: prLandedBranchFinality(base.pr, base.base)
52365
- };
52366
- deps.queueProgress?.(result);
52367
- return result;
52368
- }
52369
- async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
52370
- const now = deps.now ?? (() => Date.now());
52371
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
52372
- const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
52373
- const deadline = deadlineMs ?? now() + timeoutMs;
52374
- let attempt = 0;
52375
- let credentialMismatched = false;
52376
- const observe = (read) => {
52377
- const seen = deps.observeQueued?.(pr, base.repo, read);
52378
- if (seen?.mismatched) {
52379
- credentialMismatched = true;
52380
- delete base.credential;
52381
- }
52382
- if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
52383
- };
52384
- while (now() < deadline) {
52385
- const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
52386
- snapshot = void 0;
52387
- if (read.isInMergeQueue || read.state === "MERGED") observe(read);
52388
- if (read.state === "MERGED") return queueMerged(base, deps);
52389
- if (read.isInMergeQueue) {
52390
- base.mergeQueue = {
52391
- base: base.base,
52392
- state: "queued",
52393
- ...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
52394
- };
52395
- deps.queueProgress?.(base);
52396
- if (options.enqueueOnly) return { ...base, status: "queued" };
52397
- } else if (read.state === "OPEN" && !read.autoMergeRequest) {
52398
- await sleep2(PR_LAND_EJECT_CONFIRM_MS);
52399
- const again = await deps.readMergeQueue(pr, base.repo);
52400
- if (again.state === "MERGED") {
52401
- observe(again);
52402
- return queueMerged(base, deps);
52403
- }
52404
- if (again.isInMergeQueue || again.autoMergeRequest) {
52405
- snapshot = again;
52406
- continue;
52407
- }
52408
- const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
52409
- const result2 = {
52410
- ...base,
52411
- mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
52412
- ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
52413
- error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
52414
- };
52415
- deps.queueProgress?.(result2);
52416
- return result2;
52417
- }
52418
- await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
52419
- }
52420
- const result = {
52421
- ...base,
52422
- mergeQueue: { ...base.mergeQueue, state: "timeout" },
52423
- error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
52424
- };
52425
- deps.queueProgress?.(result);
52426
- return result;
52427
- }
52428
- function prLandOutcome(result) {
52429
- if (result.status === "queue-admission-pending") return "queue-admission-pending";
52430
- if (result.status !== "failed" && result.status !== "queued") return "merged";
52431
- if (result.status === "queued") return "queued";
52432
- const state = result.mergeQueue?.state;
52433
- return state === "ejected" || state === "timeout" ? state : "failed";
52434
- }
52435
- async function landMany(prs, landOne, cleanup, hooks) {
52436
- const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
52437
- return Promise.all(prs.map(async (pr) => {
52438
- let result = await safe(pr);
52439
- if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
52440
- hooks?.onRetry?.(pr, result);
52441
- result = { ...await safe(pr), retriedAfterEjection: true };
52442
- }
52443
- const outcome = prLandOutcome(result);
52444
- const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
52445
- hooks?.onDone?.();
52446
- return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
52447
- }));
52448
- }
52449
- function renderPrLandTable(entries) {
52450
- const rows = entries.map((e) => [
52451
- `#${e.pr}`,
52452
- e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
52453
- [e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
52454
- ]);
52455
- const width = (i) => Math.max(...rows.map((r) => r[i].length));
52456
- return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
52457
- }
52458
- function landCredentials(results) {
52459
- const each = results.map((r) => r.credential ?? "unknown");
52460
- return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
52461
- }
52462
-
52463
52641
  // src/git-worktree-observation.ts
52464
52642
  function parseGitWorktreePorcelain(text) {
52465
52643
  const rows = [];
@@ -59270,7 +59448,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59270
59448
  }
59271
59449
  class PrHeadBehindBaseError extends Error {
59272
59450
  }
59273
- 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) => {
59274
59452
  const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
59275
59453
  if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
59276
59454
  if (opts.expectHead !== void 0) {
@@ -59316,7 +59494,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59316
59494
  pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
59317
59495
  hasMergeQueue: hasPrMergeQueue,
59318
59496
  enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
59319
- const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force });
59497
+ const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
59320
59498
  if (receipt.warning) console.warn(receipt.warning);
59321
59499
  return receipt;
59322
59500
  },
@@ -59505,6 +59683,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59505
59683
  return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
59506
59684
  };
59507
59685
  const entries = await landMany(numbers, landOne, cleanupMerged, {
59686
+ baseRedCheck: (_pr, r) => readBaseRedVerdict(r),
59508
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)`),
59509
59688
  onDone: () => {
59510
59689
  inFlight = Math.max(1, inFlight - 1);
@@ -62545,8 +62724,47 @@ function adoptExportProdDeps() {
62545
62724
  }
62546
62725
  };
62547
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
+ }
62548
62753
  function registerPackagesCommands(program3) {
62549
- 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
+ });
62550
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) => {
62551
62769
  const id = (value, flag) => {
62552
62770
  if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
@@ -65792,7 +66010,7 @@ var RELEASE_LIST_QUERY_VALUE = { per_page: RUNS_QUERY_VALUE.per_page, page: RUNS
65792
66010
  var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
65793
66011
  var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
65794
66012
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
65795
- 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>)";
65796
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.";
65797
66015
  var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
65798
66016
  function typedReadHint(path2) {
@@ -65829,7 +66047,7 @@ function orgScopedHint(path2) {
65829
66047
  if (!match) return "";
65830
66048
  const org = match[1];
65831
66049
  const runners = /^orgs\/[^/?#]+\/actions\/runners(?:\/|$)/.test(path2.split("?")[0]);
65832
- 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.";
65833
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).`;
65834
66052
  }
65835
66053
  function tagsReadHint(path2) {
@@ -65861,8 +66079,8 @@ function parseApiGetPath(rawPath) {
65861
66079
  if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
65862
66080
  if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
65863
66081
  if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
65864
- if (ACTIONS_JOBS_RE.test(path2)) return { kind: "actions-jobs", path: path2, paginate: true };
65865
- 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 };
65866
66084
  if (ACTIONS_ARTIFACT_ZIP_RE.test(path2)) return { kind: "actions-artifact-zip", path: path2, paginate: false };
65867
66085
  if (ACTIONS_JOB_LOGS_RE.test(path2)) return { kind: "actions-job-logs", path: path2, paginate: false };
65868
66086
  if (RELEASE_LIST_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "release-list", path: path2, paginate: false };
@@ -68716,7 +68934,7 @@ function registerDeveloperCommands(program3) {
68716
68934
  async function runWhoami(io = consoleIo) {
68717
68935
  const cfg = await loadConfig();
68718
68936
  const report = await resolveWhoami({
68719
- hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
68937
+ hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken }),
68720
68938
  ghLogin: githubLogin
68721
68939
  });
68722
68940
  io.log(JSON.stringify(report));
@@ -68727,7 +68945,7 @@ function registerDeveloperCommands(program3) {
68727
68945
  });
68728
68946
  whoami.command("diagnose-hub").description("bounded cache-free Hub issuance diagnostic").action(async () => {
68729
68947
  const cfg = await loadConfig();
68730
- const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubToken, fetch });
68948
+ const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubPersonalToken, fetch });
68731
68949
  consoleIo.log(JSON.stringify(report));
68732
68950
  if (report.category !== "issued") process.exitCode = 1;
68733
68951
  });