@mutmutco/cli 4.6.3 → 4.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs CHANGED
@@ -400,7 +400,7 @@ function isFilingWriteApi(rest) {
400
400
  }
401
401
  }
402
402
  if (method === "POST" && path2 && /^repos\/[^/]+\/[^/]+\/(issues|pulls)$/.test(path2)) return true;
403
- if (method === "PUT" && path2 && /^repos\/[^/]+\/[^/]+\/pulls\/\d+\/merge$/.test(path2)) return true;
403
+ if (method === "PUT" && path2 && /^repos\/[^/]+\/[^/]+\/pulls\/\d+\/merge(-async)?$/.test(path2)) return true;
404
404
  return false;
405
405
  }
406
406
  function isViewerFreeGhRead(args) {
@@ -1216,6 +1216,8 @@ var init_house_map = __esm({
1216
1216
  // personal and App allowance visibility
1217
1217
  api: "devops",
1218
1218
  // allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
1219
+ run: "devops",
1220
+ // #9479: one named workflow run — currently the bounded `run cancel <id>` write
1219
1221
  ci: "devops",
1220
1222
  // the CI/gate audit
1221
1223
  jev: "devops",
@@ -1353,6 +1355,7 @@ __export(cli_shared_exports, {
1353
1355
  fail: () => fail,
1354
1356
  failGraceful: () => failGraceful,
1355
1357
  failGracefulEnvelope: () => failGracefulEnvelope,
1358
+ ghFilingAppEnv: () => ghFilingAppEnv,
1356
1359
  gitOut: () => gitOut,
1357
1360
  githubLogin: () => githubLogin,
1358
1361
  hubHeaders: () => hubHeaders,
@@ -1383,10 +1386,10 @@ function setGhReadTokenProvider(provider) {
1383
1386
  function setGhWriteTokenProvider(provider) {
1384
1387
  ghWriteTokenProvider = provider;
1385
1388
  }
1386
- async function ghFilingAppEnv(file, args) {
1387
- if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || process.env.MMI_ACTOR?.trim() === "personal") return void 0;
1389
+ async function ghFilingAppEnv(file, args, requireApp = false) {
1390
+ if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || !requireApp && process.env.MMI_ACTOR?.trim() === "personal") return void 0;
1388
1391
  if (!isAppFilingWrite(args)) return void 0;
1389
- const explicitApp = process.env.MMI_ACTOR?.trim() === "app";
1392
+ const explicitApp = requireApp || process.env.MMI_ACTOR?.trim() === "app";
1390
1393
  let token;
1391
1394
  try {
1392
1395
  token = await ghWriteTokenProvider();
@@ -1599,16 +1602,26 @@ var init_cli_shared = __esm({
1599
1602
  rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options, ...env ? { env } : {} })
1600
1603
  );
1601
1604
  const appEnv = options.env ? void 0 : await ghReadAppEnv(file, args) ?? await ghFilingAppEnv(file, args);
1605
+ const reportsCredential = !options.env && /(^|[\\/])gh(\.exe)?$/i.test(file) && isAppFilingWrite(args);
1606
+ const tag = async (call, credential) => {
1607
+ if (!reportsCredential) return call;
1608
+ try {
1609
+ return { ...await call, credential };
1610
+ } catch (e) {
1611
+ if (e && typeof e === "object") e.credential = credential;
1612
+ throw e;
1613
+ }
1614
+ };
1602
1615
  if (/(^|[\\/])gh(\.exe)?$/i.test(file) && args[0] !== "auth") {
1603
1616
  const callerEnv = options.env;
1604
1617
  const pool = appEnv ? "app" : !callerEnv ? ambientPool() : !callerEnv.GH_TOKEN ? "personal" : callerEnv.MMI_GH_POOL === "app" ? "app" : callerEnv.GH_TOKEN === process.env.GH_TOKEN ? ambientPool() : "caller";
1605
1618
  recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
1606
1619
  }
1607
- if (!appEnv) return run();
1620
+ if (!appEnv) return tag(run(), options.env?.MMI_GH_POOL === "app" ? "app_installation" : "user");
1608
1621
  try {
1609
- return await run(appEnv);
1622
+ return await tag(run(appEnv), "app_installation");
1610
1623
  } catch (e) {
1611
- if (shouldRetryIntegrationRefusalOnAmbient(e)) return run();
1624
+ if (shouldRetryIntegrationRefusalOnAmbient(e)) return tag(run(), "user");
1612
1625
  throw e;
1613
1626
  }
1614
1627
  };
@@ -15263,6 +15276,8 @@ async function mintInstallationToken(deps) {
15263
15276
  }
15264
15277
  var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15265
15278
  ["ci cancel-orphans", { metadata: "read", actions: "write" }],
15279
+ // #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
15280
+ ["run cancel", { metadata: "read", actions: "write" }],
15266
15281
  ["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
15267
15282
  // #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
15268
15283
  ["issue discover-related", { metadata: "read", issues: "write" }]
@@ -15953,9 +15968,6 @@ var REST_GH_TIMEOUT_MS = 2e4;
15953
15968
  var pollTokenProvider;
15954
15969
  var pollTokenOnce;
15955
15970
  var lastPollSource;
15956
- function lastRestPollSource() {
15957
- return lastPollSource;
15958
- }
15959
15971
  function setPollTokenProvider(provider) {
15960
15972
  pollTokenProvider = provider;
15961
15973
  pollTokenOnce = void 0;
@@ -16798,6 +16810,30 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
16798
16810
  }
16799
16811
 
16800
16812
  // src/claim-marker-client.ts
16813
+ var claimWriteTokenProvider;
16814
+ var claimWriteTokenOnce;
16815
+ function setClaimWriteTokenProvider(provider) {
16816
+ claimWriteTokenProvider = provider;
16817
+ claimWriteTokenOnce = void 0;
16818
+ }
16819
+ async function selectClaimWriteClient(client, appClient) {
16820
+ if (appClient) return appClient;
16821
+ if (!claimWriteTokenProvider) return client;
16822
+ let token;
16823
+ try {
16824
+ claimWriteTokenOnce ??= claimWriteTokenProvider();
16825
+ token = await claimWriteTokenOnce;
16826
+ } catch (e) {
16827
+ claimWriteTokenOnce = void 0;
16828
+ const detail = e instanceof Error ? e.message : String(e);
16829
+ throw new Error(`${detail} \u2014 refusing personal-token fallback for claim write (#9511)`);
16830
+ }
16831
+ if (!token) {
16832
+ claimWriteTokenOnce = void 0;
16833
+ throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
16834
+ }
16835
+ return createGitHubClient({ token: async () => token });
16836
+ }
16801
16837
  async function selectLivenessClient(client, appClient) {
16802
16838
  if (appClient) return appClient;
16803
16839
  let token;
@@ -21309,7 +21345,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
21309
21345
  var SKILL_LESSON_LABEL = "skill-lesson";
21310
21346
  var SKILL_LESSON_FILE_LABELS = [SKILL_LESSON_LABEL, LEARNING_LABEL2];
21311
21347
  var SKILL_LESSON_LOOP_KIND = "lesson";
21312
- var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "rcand", "release", "secrets"];
21348
+ var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "intercom-guest", "mmi-help", "org-guide", "rcand", "release", "secrets"];
21313
21349
  function assertSkillName(name) {
21314
21350
  const match = SKILL_NAMES.find((skill) => skill === name);
21315
21351
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -21342,6 +21378,9 @@ function findDuplicateLesson(source, openLessons) {
21342
21378
  // src/board-doctor-config.ts
21343
21379
  init_github_client();
21344
21380
 
21381
+ // src/issue-comment.ts
21382
+ init_github_client();
21383
+
21345
21384
  // src/sub-issue.ts
21346
21385
  init_error_codes();
21347
21386
  init_github_quota();
@@ -21551,6 +21590,40 @@ function parentLinkFields(result, error, parentRef, childRef, reason = "failed")
21551
21590
  }
21552
21591
 
21553
21592
  // src/issue-comment.ts
21593
+ async function editIssueComment(client, input) {
21594
+ const parsed = parseIssueRef(input.ref);
21595
+ const repo = parsed.repo ?? input.defaultRepo;
21596
+ if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
21597
+ if (!input.replace.length) throw new Error("--replace must not be empty; use --edit <commentId> --replace <text> --with <text>");
21598
+ if (!/^\d+$/.test(input.commentId)) throw new Error(`--edit takes a numeric comment id, got: ${input.commentId}`);
21599
+ const path2 = `repos/${repo}/issues/comments/${input.commentId}`;
21600
+ let comment;
21601
+ try {
21602
+ comment = await client.rest("GET", path2);
21603
+ } catch (e) {
21604
+ if (e instanceof GitHubApiError && e.status === 404) {
21605
+ throw new Error(`comment ${input.commentId} does not exist on ${repo}`);
21606
+ }
21607
+ throw e;
21608
+ }
21609
+ const expectedAnchor = `/issues/${parsed.number}#issuecomment-${input.commentId}`;
21610
+ if (!comment.html_url?.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
21611
+ throw new Error(`comment ${input.commentId} is not a comment on ${repo}#${parsed.number} (it lives at ${comment.html_url ?? "unknown"})`);
21612
+ }
21613
+ const body = comment.body ?? "";
21614
+ const count = body.split(input.replace).length - 1;
21615
+ if (count !== 1) throw new Error(`--replace must match exactly once; found ${count} occurrences in comment ${input.commentId}`);
21616
+ const index = body.indexOf(input.replace);
21617
+ const newBody = body.slice(0, index) + input.with + body.slice(index + input.replace.length);
21618
+ await client.rest("PATCH", path2, { body: { body: newBody } });
21619
+ return {
21620
+ number: parsed.number,
21621
+ repo,
21622
+ url: `https://github.com/${repo}/issues/${parsed.number}`,
21623
+ commentUrl: comment.html_url,
21624
+ edited: true
21625
+ };
21626
+ }
21554
21627
  async function postIssueComment(client, input) {
21555
21628
  const parsed = parseIssueRef(input.ref);
21556
21629
  const repo = parsed.repo ?? input.defaultRepo;
@@ -22059,9 +22132,12 @@ function detailCandidates(report, options = {}) {
22059
22132
  const scopes = [report.primary, report.secondary];
22060
22133
  return scopes.flatMap((scope) => options.all ? [...scope.userOwned, ...scope.claimable, ...scope.taken, ...scope.unownedInFlight] : [...scope.userOwned, ...scope.claimable]).filter((item) => item.contentType === "Issue");
22061
22134
  }
22135
+ function boardNotFoundRemedy(board, repoExplicit) {
22136
+ return repoExplicit ? `--repo already names the repo, so inspect the repo's registered board coordinates with \`mmi-cli oracle org project get <owner/repo>\` (resolved board: ${board.owner} #${board.number})` : "if it lives on a different board, pass --repo <owner/repo> for the repo that owns it";
22137
+ }
22062
22138
  function boardNotFoundError(ref, board, opts = {}) {
22063
22139
  const verb = opts.verb ?? "is not on";
22064
- const remedy = opts.remedy ?? "if it lives on a different board, pass --repo <owner/repo> for the repo that owns it";
22140
+ const remedy = opts.remedy ?? boardNotFoundRemedy(board, opts.repoExplicit === true);
22065
22141
  return new Error(`${ref} ${verb} the ${board.owner} #${board.number} board; ${remedy}`);
22066
22142
  }
22067
22143
  var NotClaimableError = class extends Error {
@@ -22489,7 +22565,7 @@ async function showBoardItem(options, deps = {}) {
22489
22565
  const currentRepo = await resolveCurrentRepo(options, deps);
22490
22566
  const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
22491
22567
  const { item } = await fetchIssueProjectItem(client, cfg, selector);
22492
- if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
22568
+ if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
22493
22569
  if (item.contentType === "Issue") {
22494
22570
  try {
22495
22571
  item.details = await fetchIssueDetails(client, item.repository, item.number);
@@ -23319,7 +23395,7 @@ function isSameBranchClaimResume(contest, repo, landing = {}) {
23319
23395
  const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
23320
23396
  const headSha = landing.headSha?.trim();
23321
23397
  if (!headBranch && !headSha) return false;
23322
- if (!contest.marker || contest.unverifiable.length > 0 || contest.live.length === 0) return false;
23398
+ if (contest.unverifiable.length > 0 || contest.live.length === 0) return false;
23323
23399
  const branchLine = /^live branch ([^\s]+) on /;
23324
23400
  const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
23325
23401
  const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
@@ -23360,7 +23436,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
23360
23436
  branchSha: evidence.branchSha
23361
23437
  }, item.repository, landing)) {
23362
23438
  contested = false;
23363
- resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
23439
+ if (evidence.marker) {
23440
+ resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
23441
+ }
23364
23442
  }
23365
23443
  const carry = staleWorkLines(evidence);
23366
23444
  const staleStamp = !contested && !resume && !ownSeat && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
@@ -23426,7 +23504,7 @@ async function moveBoardItem(options, deps = {}) {
23426
23504
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
23427
23505
  const item = lookup.item;
23428
23506
  if (!item) {
23429
- throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
23507
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
23430
23508
  }
23431
23509
  const optionId = cfg.statusOptions[options.status];
23432
23510
  let credential = "user";
@@ -23529,6 +23607,9 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
23529
23607
  return {
23530
23608
  cfg,
23531
23609
  client,
23610
+ appClient: deps.appClient,
23611
+ git: deps.git ?? defaultGit,
23612
+ landing: deps.landing,
23532
23613
  items: collected.items,
23533
23614
  selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
23534
23615
  writable: writableOrUnknown(writable),
@@ -23621,7 +23702,7 @@ async function claimOneBoardItem(ctx, selector, options) {
23621
23702
  if (!verdict.ok) throw new NotClaimableError(verdict.reason, { ref: fresh.ref, status: fresh.status });
23622
23703
  item = fresh;
23623
23704
  const refuseIfContested = async () => {
23624
- const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
23705
+ const contest = await checkLaneContest(client, item, ctx.session, report.viewer, ctx.appClient, ctx.landing);
23625
23706
  resumeEvidence = void 0;
23626
23707
  staleStamp = void 0;
23627
23708
  const displaced = () => ({
@@ -23632,6 +23713,24 @@ async function claimOneBoardItem(ctx, selector, options) {
23632
23713
  host: contest.marker.host
23633
23714
  } : {}
23634
23715
  });
23716
+ if (contest.contested && !ctx.landing && contest.branch) {
23717
+ const candidateBranch = contest.branch.replace(/ \(head of open PR .*\)$/, "").trim();
23718
+ const originRepo = repoFromRemoteUrl(await ctx.git(["remote", "get-url", "origin"]).catch(() => ""));
23719
+ if (originRepo && originRepo.toLowerCase() === item.repository.toLowerCase()) {
23720
+ const localSha = (await ctx.git(["rev-parse", "--verify", `refs/heads/${candidateBranch}^{commit}`]).catch(() => "")).trim();
23721
+ const currentBranch2 = (await ctx.git(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "")).trim();
23722
+ if (localSha && (!contest.branchSha || localSha === contest.branchSha) || currentBranch2 === candidateBranch) {
23723
+ const localLanding = { headBranch: candidateBranch, ...localSha ? { headSha: localSha } : {} };
23724
+ if (isSameBranchClaimResume(contest, item.repository, localLanding)) {
23725
+ if (contest.marker) {
23726
+ resumeEvidence = `same-branch resume over live branch ${contest.branch}`;
23727
+ previousHolder = displaced();
23728
+ }
23729
+ return;
23730
+ }
23731
+ }
23732
+ }
23733
+ }
23635
23734
  if (!contest.contested) {
23636
23735
  if (contest.resume && contest.marker) {
23637
23736
  resumeEvidence = contest.resume;
@@ -23643,10 +23742,14 @@ async function claimOneBoardItem(ctx, selector, options) {
23643
23742
  return;
23644
23743
  }
23645
23744
  if (!options.force) {
23745
+ if (options.bulk) {
23746
+ const { holder: holder2, why } = laneContestParts(contest);
23747
+ throw new Error(isUnknownHold(contest) ? `${item.ref} cannot be verified as free: ${holder2} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `${item.ref} is already claimed by ${holder2} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}`);
23748
+ }
23646
23749
  const candidateRefs = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).map((candidate) => candidate.ref);
23647
23750
  const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
23648
23751
  const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
23649
- throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
23752
+ throw new Error(withGatedWriteChainHint(refusal));
23650
23753
  }
23651
23754
  previousHolder = displaced();
23652
23755
  };
@@ -23655,19 +23758,21 @@ async function claimOneBoardItem(ctx, selector, options) {
23655
23758
  if (options.check) {
23656
23759
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
23657
23760
  }
23658
- const renewed = await postClaimMarkerComment(client, item, ctx.session, staleStamp);
23761
+ const appWriter2 = await selectClaimWriteClient(client, ctx.appClient);
23762
+ const renewed = await postClaimMarkerComment(appWriter2, item, ctx.session, staleStamp);
23659
23763
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
23660
23764
  }
23661
23765
  if (options.check) {
23662
23766
  return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
23663
23767
  }
23664
23768
  await refuseIfContested();
23769
+ const appWriter = await selectClaimWriteClient(client, ctx.appClient);
23665
23770
  try {
23666
- await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
23771
+ await appWriter.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
23667
23772
  } catch (e) {
23668
23773
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
23669
23774
  }
23670
- await postClaimMarkerComment(client, item, ctx.session, staleStamp);
23775
+ await postClaimMarkerComment(appWriter, item, ctx.session, staleStamp);
23671
23776
  let credential = "user";
23672
23777
  try {
23673
23778
  ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
@@ -23699,6 +23804,13 @@ async function claimBoardIssue(options, deps = {}) {
23699
23804
  const result = await claimOneBoardItem(ctx, selector, options);
23700
23805
  return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
23701
23806
  }
23807
+ function formatBulkClaimSummary(results) {
23808
+ const skipped = results.filter((r) => !r.claimed);
23809
+ if (skipped.length === 0) return void 0;
23810
+ const landed = results.filter((r) => r.claimed).map((r) => r.ref);
23811
+ const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
23812
+ return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
23813
+ }
23702
23814
  async function claimBoardIssues(options, deps = {}) {
23703
23815
  const cfg = resolveBoardConfig(options.config);
23704
23816
  const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
@@ -23828,7 +23940,7 @@ async function unclaimBoardIssue(options, deps = {}) {
23828
23940
  const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
23829
23941
  const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
23830
23942
  if (!item) {
23831
- throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
23943
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
23832
23944
  }
23833
23945
  const toStatus = options.toStatus ?? "Todo";
23834
23946
  if (item.contentType === "Issue") {
@@ -23846,8 +23958,9 @@ async function unclaimBoardIssue(options, deps = {}) {
23846
23958
  }
23847
23959
  }
23848
23960
  }
23961
+ const appWriter = await selectClaimWriteClient(client, deps.appClient);
23849
23962
  try {
23850
- await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
23963
+ await appWriter.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
23851
23964
  body: { assignees: item.assignees }
23852
23965
  });
23853
23966
  } catch (e) {
@@ -23864,7 +23977,7 @@ async function unclaimBoardIssue(options, deps = {}) {
23864
23977
  host: actor.host,
23865
23978
  ts: (/* @__PURE__ */ new Date()).toISOString()
23866
23979
  };
23867
- await postIssueComment(client, { ref: item.ref, body: renderClaimReleaseBody(release) });
23980
+ await postIssueComment(appWriter, { ref: item.ref, body: renderClaimReleaseBody(release) });
23868
23981
  } catch (e) {
23869
23982
  process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
23870
23983
  `);
@@ -27287,10 +27400,10 @@ var rollout_plan_default = {
27287
27400
  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)."
27288
27401
  },
27289
27402
  baseline: {
27290
- version: "4.6.3",
27291
- tag: "v4.6.3",
27292
- commit: "602adde0ce4c",
27293
- npm: "@mutmutco/cli@4.6.3"
27403
+ version: "4.6.5",
27404
+ tag: "v4.6.5",
27405
+ commit: "cd6f8a959ed5",
27406
+ npm: "@mutmutco/cli@4.6.5"
27294
27407
  },
27295
27408
  exitCriterion: "fleet-n-of-n",
27296
27409
  hubOnlyShortcut: "forbidden",
@@ -27307,14 +27420,14 @@ var rollout_plan_default = {
27307
27420
  repo: "mutmutco/mmi-hub",
27308
27421
  role: "canary",
27309
27422
  schedule: "train",
27310
- v3Target: "v4.6.3"
27423
+ v3Target: "v4.6.5"
27311
27424
  }
27312
27425
  ],
27313
27426
  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.",
27314
27427
  rollback: {
27315
27428
  independent: true,
27316
- mechanism: "npm dist-tag latest -> 4.6.3 and redeploy the Hub Lambda from tag v4.6.3 (602adde0ce4c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27317
- v3Target: "v4.6.3 (@mutmutco/cli@4.6.3, tag commit 602adde0ce4c \u2014 last known-good release carrying the repo-index v4-only contract)"
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)"
27318
27431
  }
27319
27432
  },
27320
27433
  {
@@ -28993,16 +29106,16 @@ async function writeFleetLockstepRuleset(client, body, liveId, target, org) {
28993
29106
  await client.rest("PUT", `orgs/${org}/rulesets/${liveId}`, { body, timeoutMs: 2e4 });
28994
29107
  }
28995
29108
  const read = await fetchFleetLockstepRuleset(client, org);
28996
- const settled = read.kind === "ok" ? read.ruleset : void 0;
28997
- const shapeDrift = settled ? diffFleetLockstepShape(settled) : [`ruleset '${FLEET_LOCKSTEP_RULESET_NAME}' absent after apply`];
28998
- const includeSettled = settled ? sameSetCi(settled.conditions?.repository_name?.include, body.conditions.repository_name.include) : false;
28999
- const enforcementSettled = settled?.enforcement === body.enforcement;
29109
+ const settled2 = read.kind === "ok" ? read.ruleset : void 0;
29110
+ const shapeDrift = settled2 ? diffFleetLockstepShape(settled2) : [`ruleset '${FLEET_LOCKSTEP_RULESET_NAME}' absent after apply`];
29111
+ const includeSettled = settled2 ? sameSetCi(settled2.conditions?.repository_name?.include, body.conditions.repository_name.include) : false;
29112
+ const enforcementSettled = settled2?.enforcement === body.enforcement;
29000
29113
  if (shapeDrift.length || !includeSettled || !enforcementSettled) {
29001
29114
  throw new Error(`fleet lockstep ruleset apply did not settle: ${[...shapeDrift, includeSettled ? "" : "include list differs", enforcementSettled ? "" : "enforcement differs"].filter(Boolean).join("; ")}`);
29002
29115
  }
29003
29116
  return {
29004
29117
  action: liveId == null ? "created" : "updated",
29005
- id: settled?.id,
29118
+ id: settled2?.id,
29006
29119
  target,
29007
29120
  include: body.conditions.repository_name.include,
29008
29121
  verified: true
@@ -40015,7 +40128,7 @@ var PRIMARY_GROUPS = [
40015
40128
  ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
40016
40129
  ];
40017
40130
  var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["oauth", "org", "runtime", "plugin", "packages", "github", "archive-prune", "intercom", "jev"]);
40018
- var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read"]);
40131
+ var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read", "run"]);
40019
40132
  var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
40020
40133
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
40021
40134
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
@@ -40061,6 +40174,8 @@ var COMMAND_OWNERSHIP = {
40061
40174
  api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
40062
40175
  github: { module_owner: "cli/src/github-budget.ts", consumer: "release-operator" },
40063
40176
  read: { module_owner: "cli/src/app-read-broker.ts", consumer: "agent-workflow" },
40177
+ run: { module_owner: "cli/src/merge-group-orphans.ts", consumer: "agent-workflow" },
40178
+ // #9479
40064
40179
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
40065
40180
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
40066
40181
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -41025,14 +41140,23 @@ function parseScope(value) {
41025
41140
  function trailerBlockBreak(body, line) {
41026
41141
  const lines2 = String(body ?? "").split(/\r?\n/);
41027
41142
  const at = lines2.findIndex((candidate) => candidate.trim() === line);
41028
- if (at === -1) return "";
41143
+ if (at === -1) return { after: "", before: "" };
41029
41144
  for (let i = at + 1; i < lines2.length; i += 1) {
41030
41145
  const next = lines2[i];
41031
41146
  if (!next.trim()) continue;
41032
- if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return "";
41033
- return next.trim();
41147
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
41148
+ return { after: next.trim(), before: "" };
41034
41149
  }
41035
- return "";
41150
+ let paraStart = at;
41151
+ while (paraStart > 0 && lines2[paraStart - 1].trim() !== "") {
41152
+ paraStart -= 1;
41153
+ }
41154
+ for (let i = paraStart; i < at; i += 1) {
41155
+ const prev = lines2[i];
41156
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(prev) || /^[ \t]+\S/.test(prev)) continue;
41157
+ return { after: "", before: prev.trim() };
41158
+ }
41159
+ return { after: "", before: "" };
41036
41160
  }
41037
41161
  function clipParagraph(line) {
41038
41162
  const flat = String(line).trim().replace(/\s+/g, " ");
@@ -41040,6 +41164,11 @@ function clipParagraph(line) {
41040
41164
  }
41041
41165
  function invisibleTrailer(sha, line, body = "") {
41042
41166
  const broken = trailerBlockBreak(body, line);
41167
+ const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41168
+ ${clipParagraph(broken.after)}
41169
+ ` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
41170
+ ${clipParagraph(broken.before)}
41171
+ ` : " The trailer-shaped line is not in the message's FINAL `Key: value` block \u2014 prose or an\n unindented wrap either before or after it ends that block.\n";
41043
41172
  return {
41044
41173
  kind: "malformed-override-trailer",
41045
41174
  paths: [],
@@ -41055,9 +41184,7 @@ function invisibleTrailer(sha, line, body = "") {
41055
41184
  The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
41056
41185
  Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
41057
41186
  git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
41058
- ` + (broken ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41059
- ${clipParagraph(broken)}
41060
- ` : " The trailer-shaped line is not in the message's FINAL `Key: value` block \u2014 prose or an\n unindented wrap either before or after it ends that block.\n") + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
41187
+ ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
41061
41188
  git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
41062
41189
  That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
41063
41190
  };
@@ -41595,6 +41722,31 @@ ${commit.body}`.trim();
41595
41722
  if (!overrides.length) return null;
41596
41723
  return [...bodies, [...overrides, ...coauthors].join("\n")].join("\n\n");
41597
41724
  }
41725
+ function prBodyTrailerSource(prBody) {
41726
+ const lines2 = prBody.split("\n");
41727
+ const separator = lines2.findIndex((line) => GH_MESSAGE_SEPARATOR.test(line.trim()));
41728
+ return separator === -1 ? prBody : lines2.slice(0, separator).join("\n");
41729
+ }
41730
+ function prBodyPreservesCommitOverrides(commits, headline, prBody, cwd) {
41731
+ const key = `${TRAILER_KEY.toLowerCase()}:`;
41732
+ const required = [];
41733
+ for (const commit of commits) {
41734
+ for (const trailer of parseTrailers(`${commit.headline}
41735
+
41736
+ ${commit.body}`.trim(), cwd)) {
41737
+ const normalized = trailer.toLowerCase();
41738
+ if (normalized.startsWith(key) && !required.includes(normalized)) required.push(normalized);
41739
+ }
41740
+ }
41741
+ if (!required.length) return true;
41742
+ const source = prBodyTrailerSource(prBody);
41743
+ const recognized = parseTrailers(`${headline}
41744
+
41745
+ ${source}`.trim(), cwd).map((trailer) => trailer.toLowerCase()).filter((trailer) => trailer.startsWith(key));
41746
+ const textual = (source.match(/^Test-Policy-Override:\s*\S/gim) ?? []).length;
41747
+ if (textual !== recognized.length) return false;
41748
+ return required.every((trailer) => recognized.includes(trailer));
41749
+ }
41598
41750
  function resolveBase(cwd, explicit) {
41599
41751
  const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
41600
41752
  for (const ref of candidates) {
@@ -41797,7 +41949,17 @@ async function hasPrMergeQueue(repo, base, gh2 = readApi) {
41797
41949
  if (result.mergeQueue === void 0) throw new Error(`merge queue policy unreadable for ${repo}:${base}`);
41798
41950
  return result.mergeQueue !== null;
41799
41951
  }
41800
- var writeApi = async (args) => (await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 })).stdout;
41952
+ var writeApi = async (args) => {
41953
+ const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
41954
+ return { stdout, credential };
41955
+ };
41956
+ var autoMergeApi = async (pr, repo, head) => {
41957
+ const args = ["pr", "merge", pr, "-R", repo, "--squash", "--auto", "--match-head-commit", head];
41958
+ const env = await ghFilingAppEnv("gh", args, true);
41959
+ if (!env) throw new Error("app actor: filing write token unavailable for merge-queue auto-merge fallback");
41960
+ await execFileP("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024, env });
41961
+ return { credential: "app_installation" };
41962
+ };
41801
41963
  var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
41802
41964
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
41803
41965
  function admissionJournal(repo, pr, root) {
@@ -41813,23 +41975,31 @@ function admissionJournal(repo, pr, root) {
41813
41975
  const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
41814
41976
  const validate = (value, previous) => {
41815
41977
  const row = value;
41816
- 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.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)))) {
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)))) {
41817
41979
  throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
41818
41980
  }
41819
41981
  if (!previous) {
41820
41982
  if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
41821
- } else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state === "ejected" && row.uuid === previous.uuid)) {
41983
+ } else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state === "ejected" && row.uuid === previous.uuid)) {
41822
41984
  throw new Error("queue admission receipt has an illegal transition or changed operation identity");
41823
41985
  }
41824
41986
  return row;
41825
41987
  };
41826
- const load = () => {
41988
+ const load = (number = attempt) => {
41827
41989
  let previous;
41828
- for (const line of (0, import_node_fs33.readFileSync)(path2(attempt), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
41990
+ for (const line of (0, import_node_fs33.readFileSync)(path2(number), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
41829
41991
  return previous;
41830
41992
  };
41831
41993
  return {
41832
41994
  receipt: attempt ? load() : void 0,
41995
+ /** #9504: the exact attempt this invocation selected, so a later attempt recorded at the same head can never supply its credential. */
41996
+ findByIntentId(intentId) {
41997
+ for (let number = 1; number <= attempt; number++) {
41998
+ const receipt = load(number);
41999
+ if (receipt.intentId === intentId) return receipt;
42000
+ }
42001
+ return void 0;
42002
+ },
41833
42003
  create(head) {
41834
42004
  const createdAt = Date.now();
41835
42005
  const initial = { version: 1, repo: repo.toLowerCase(), pr, head, intentId: (0, import_node_crypto10.randomUUID)(), createdAt, observedAt: createdAt, state: "dispatching" };
@@ -41841,9 +42011,9 @@ function admissionJournal(repo, pr, root) {
41841
42011
  }
41842
42012
  return true;
41843
42013
  },
41844
- save(state, uuid, queueEntry) {
42014
+ save(state, uuid, queueEntry, credential) {
41845
42015
  const previous = load();
41846
- const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {} }, previous);
42016
+ const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {}, ...credential && previous.credential === void 0 ? { credential } : {} }, previous);
41847
42017
  (0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
41848
42018
  load();
41849
42019
  }
@@ -41866,6 +42036,21 @@ function admissionResponse(output) {
41866
42036
  }
41867
42037
  return { code: Number(response[1]), body: result };
41868
42038
  }
42039
+ function admissionRejected(reason) {
42040
+ if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
42041
+ return {
42042
+ mergeStatus: "failed",
42043
+ retryable: false,
42044
+ 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.`
42046
+ };
42047
+ }
42048
+ return {
42049
+ mergeStatus: "failed",
42050
+ 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.`
42052
+ };
42053
+ }
41869
42054
  async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
41870
42055
  const unknown = (reason) => ({
41871
42056
  mergeStatus: "failed",
@@ -41873,11 +42058,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41873
42058
  outcomeUnknown: true,
41874
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.`
41875
42060
  });
41876
- const rejected = (reason) => ({
41877
- mergeStatus: "failed",
41878
- retryable: false,
41879
- error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.`
41880
- });
42061
+ const rejected = admissionRejected;
41881
42062
  const reconcile = async (reason, recordEntry = false) => {
41882
42063
  try {
41883
42064
  const observed = await readPrMergeQueue(pr, repo, read);
@@ -41889,6 +42070,22 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41889
42070
  }
41890
42071
  return { mergeStatus: "auto-merge-enqueued" };
41891
42072
  }
42073
+ 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);
42076
+ const events = await readArrayPages(
42077
+ `repos/${repo}/issues/${pr}/timeline?per_page=100`,
42078
+ options.timelineRead ?? read,
42079
+ "queue ejection timeline unreadable"
42080
+ );
42081
+ 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);
42083
+ const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
42084
+ if (removals.length === 1) {
42085
+ journal.save("ejected");
42086
+ return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
42087
+ }
42088
+ }
41892
42089
  } catch {
41893
42090
  }
41894
42091
  return unknown(reason);
@@ -41909,24 +42106,6 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41909
42106
  } catch {
41910
42107
  }
41911
42108
  }
41912
- if (receipt?.state === "enqueued" && !superseded) {
41913
- try {
41914
- const observed = await readPrMergeQueue(pr, repo, read);
41915
- if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === receipt.head) {
41916
- const events = await readArrayPages(
41917
- `repos/${repo}/issues/${pr}/timeline?per_page=100`,
41918
- options.timelineRead ?? read,
41919
- "queue ejection timeline unreadable"
41920
- );
41921
- const admittedAt = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt;
41922
- if (events.some((event) => event.event === "removed_from_merge_queue" && Date.parse(event.created_at) > admittedAt)) {
41923
- journal.save("ejected");
41924
- return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
41925
- }
41926
- }
41927
- } catch {
41928
- }
41929
- }
41930
42109
  if (superseded && receipt?.state === "enqueued") {
41931
42110
  try {
41932
42111
  journal.save("ejected");
@@ -41935,6 +42114,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41935
42114
  }
41936
42115
  }
41937
42116
  if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
42117
+ if (options.selection) {
42118
+ options.selection.selected = true;
42119
+ options.selection.intentId = receipt.intentId;
42120
+ }
41938
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");
41939
42122
  if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
41940
42123
  } else {
@@ -41943,12 +42126,17 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41943
42126
  if (!journal.create(head)) return submitQueueAdmission(pr, repo, head, read, write, options, true);
41944
42127
  journal = admissionJournal(repo, pr, options.receiptDirectory);
41945
42128
  receipt = journal.receipt;
42129
+ if (options.selection) {
42130
+ options.selection.selected = true;
42131
+ options.selection.intentId = receipt.intentId;
42132
+ }
41946
42133
  } catch {
41947
42134
  return unknown("the pre-send checkpoint failed; no request was dispatched");
41948
42135
  }
41949
42136
  let output;
42137
+ let credential;
41950
42138
  try {
41951
- output = await write([
42139
+ const written = await write([
41952
42140
  `repos/${repo}/pulls/${pr}/merge-async`,
41953
42141
  "--method",
41954
42142
  "PUT",
@@ -41961,8 +42149,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41961
42149
  "-f",
41962
42150
  `sha=${head}`
41963
42151
  ]);
42152
+ ({ stdout: output, credential } = typeof written === "string" ? { stdout: written, credential: void 0 } : written);
41964
42153
  } catch (error) {
41965
42154
  output = error.stdout ?? "";
42155
+ credential = error.credential;
41966
42156
  }
41967
42157
  try {
41968
42158
  response = admissionResponse(output);
@@ -41972,19 +42162,51 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41972
42162
  const { code, body } = response;
41973
42163
  if (code === 200 && ["enqueued", "merged"].includes(body.status ?? "")) {
41974
42164
  try {
41975
- journal.save(body.status);
42165
+ journal.save(body.status, void 0, void 0, credential);
41976
42166
  } catch {
41977
42167
  return reconcile("the completed operation could not be checkpointed");
41978
42168
  }
41979
42169
  return reconcile(`GitHub reported ${body.status}, but matching-head membership or merged state is unconfirmed`, body.status === "enqueued");
41980
42170
  }
42171
+ const tryAutoMergeFallback = async (reason, uuid) => {
42172
+ const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42173
+ if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
42174
+ 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
+ }
42196
+ } catch {
42197
+ }
42198
+ return void 0;
42199
+ };
41981
42200
  if (code === 400 || code === 403 || code === 404 || code === 422) {
42201
+ const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
42202
+ const fallbackResult = await tryAutoMergeFallback(reason);
42203
+ if (fallbackResult) return fallbackResult;
41982
42204
  try {
41983
- journal.save("failed");
42205
+ journal.save("failed", void 0, void 0, credential);
41984
42206
  } catch {
41985
42207
  return unknown("the explicit rejection could not be checkpointed");
41986
42208
  }
41987
- return rejected(body.details?.message ?? body.message ?? `HTTP ${code}`);
42209
+ return rejected(reason);
41988
42210
  }
41989
42211
  const details = body.details;
41990
42212
  if (code !== 202 && code !== 409 || body.status !== "pending" || !UUID.test(details?.uuid ?? "")) return reconcile("the send returned no recognized admission result");
@@ -41992,7 +42214,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41992
42214
  return unknown("an existing operation has different or missing queue/head/no-bypass options; inspect that operation before continuing");
41993
42215
  }
41994
42216
  try {
41995
- journal.save("pending", details.uuid);
42217
+ journal.save("pending", details.uuid, void 0, credential);
41996
42218
  } catch {
41997
42219
  return reconcile("the accepted operation UUID could not be checkpointed");
41998
42220
  }
@@ -42015,6 +42237,36 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
42015
42237
  continue;
42016
42238
  }
42017
42239
  if (!["failed", "enqueued", "merged"].includes(status ?? "")) return reconcile("the operation returned an unrecognized state");
42240
+ if (status === "failed") {
42241
+ const reason = details?.message ?? "GitHub reported a failed operation";
42242
+ const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
42243
+ if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
42244
+ try {
42245
+ const fallback = await autoMerge(pr, repo, head);
42246
+ 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" };
42265
+ }
42266
+ } catch {
42267
+ }
42268
+ }
42269
+ }
42018
42270
  try {
42019
42271
  journal.save(status, receipt.uuid);
42020
42272
  } catch {
@@ -42047,8 +42299,12 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42047
42299
  if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
42048
42300
  const head = options.expectedHeadOid ?? pull.headRefOid;
42049
42301
  if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
42050
- if (pull.state === "MERGED") return { mergeStatus: "merged" };
42051
- if (pull.isInMergeQueue) return { mergeStatus: "auto-merge-enqueued" };
42302
+ const stamp = (result2, selected = false, intentId) => {
42303
+ const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
42304
+ return credential === "unknown" ? result2 : { ...result2, credential };
42305
+ };
42306
+ if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
42307
+ if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
42052
42308
  if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
42053
42309
  const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
42054
42310
  let page = pull.closingIssuesReferences.pageInfo;
@@ -42144,7 +42400,20 @@ ${pull.body}`;
42144
42400
  return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
42145
42401
  }
42146
42402
  const requiredBody = options.bodyText ?? composedBody;
42147
- if (requiredBody && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42403
+ const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
42404
+ if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42405
+ if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
42406
+ return {
42407
+ mergeStatus: "failed",
42408
+ preparation: {
42409
+ status: "preparation-needed",
42410
+ reason: "queue-message-override",
42411
+ issueTargets: [],
42412
+ nextAction: "PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. mmi-cli devops pr edit <n> --body-file <path>) without a body override, then rerun pr land. Keep repository settings unchanged."
42413
+ },
42414
+ error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
42415
+ };
42416
+ }
42148
42417
  return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
42149
42418
  }
42150
42419
  const current = await queueGraphql(
@@ -42155,8 +42424,9 @@ ${pull.body}`;
42155
42424
  if (current.pullRequest.headRefOid !== head || current.pullRequest.title !== pull.title || current.pullRequest.body !== pull.body) {
42156
42425
  return { mergeStatus: "failed", retryable: false, error: "queue source or PR text changed during preparation; rerun pr land against the new revision" };
42157
42426
  }
42158
- const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read });
42159
- return { ...result, ...verdict.message ? { warning: verdict.message } : {} };
42427
+ const selection = { selected: false };
42428
+ const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
42429
+ return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
42160
42430
  }
42161
42431
  function decideQueueMergeRoute(hasQueue, method, headRef) {
42162
42432
  if (!hasQueue) return "normal";
@@ -42194,8 +42464,50 @@ function prMergeQueueLine(result) {
42194
42464
  return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
42195
42465
  }
42196
42466
  }
42467
+ function admissionCredential(repo, pr, head, receiptDirectory, selected = false, intentId) {
42468
+ try {
42469
+ const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
42470
+ const saved = intentId ? journal.findByIntentId(intentId) : journal.receipt;
42471
+ const live = saved && ["pending", "enqueued", "merged"].includes(saved.state);
42472
+ return saved && saved.head.toLowerCase() === head.toLowerCase() && (selected || live) ? saved.credential ?? "unknown" : "unknown";
42473
+ } catch {
42474
+ return "unknown";
42475
+ }
42476
+ }
42477
+ function observeQueuedAdmission(repo, pr, snapshot, receiptDirectory) {
42478
+ try {
42479
+ const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
42480
+ const saved = journal.receipt;
42481
+ if (!saved || !["pending", "enqueued", "merged"].includes(saved.state) || snapshot.headRefOid?.toLowerCase() !== saved.head.toLowerCase()) return {};
42482
+ const entry = snapshot.mergeQueueEntry;
42483
+ if (saved.queueEntry) {
42484
+ if (entry?.enqueuedAt && entry.enqueuedAt !== saved.queueEntry.enqueuedAt) return { mismatched: true };
42485
+ } else if ((saved.state === "enqueued" || saved.state === "pending") && snapshot.isInMergeQueue && entry?.headCommit && entry.enqueuedAt) {
42486
+ try {
42487
+ journal.save("enqueued", saved.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
42488
+ } catch {
42489
+ return {};
42490
+ }
42491
+ }
42492
+ return saved.credential ? { credential: saved.credential } : {};
42493
+ } catch {
42494
+ return {};
42495
+ }
42496
+ }
42197
42497
 
42198
42498
  // src/merge-group-orphans.ts
42499
+ var SETTLE_POLLS = 6;
42500
+ var SETTLE_MS = 5e3;
42501
+ async function settled(repo, id, deps) {
42502
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
42503
+ let status = "unknown";
42504
+ for (let poll = 0; poll < SETTLE_POLLS; poll++) {
42505
+ await sleep2(SETTLE_MS);
42506
+ status = JSON.parse(await deps.read([`repos/${repo}/actions/runs/${id}`])).status ?? "unknown";
42507
+ if (status === "completed") break;
42508
+ }
42509
+ return status;
42510
+ }
42199
42511
  async function isOrphan(run, repo, deps) {
42200
42512
  const match = /^gh-readonly-queue\/(.+)\/pr-([1-9][0-9]*)-([a-f0-9]+)$/i.exec(run.head_branch);
42201
42513
  if (!match || run.event !== "merge_group") return false;
@@ -42215,7 +42527,7 @@ async function isOrphan(run, repo, deps) {
42215
42527
  }
42216
42528
  }
42217
42529
  async function cancelMergeGroupOrphans(repo, apply, deps) {
42218
- const report = { repo, apply, candidates: [], cancelled: [], errors: [] };
42530
+ const report = { repo, apply, candidates: [], cancelled: [], forceRequested: [], forceCancelled: [], errors: [] };
42219
42531
  try {
42220
42532
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("expected --repo owner/repo");
42221
42533
  const runs2 = [];
@@ -42237,7 +42549,18 @@ async function cancelMergeGroupOrphans(repo, apply, deps) {
42237
42549
  for (const run of confirmed) {
42238
42550
  if (!await isOrphan(run, repo, deps)) continue;
42239
42551
  await deps.cancel(repo, run.id);
42240
- report.cancelled.push(run.id);
42552
+ let status = await settled(repo, run.id, deps);
42553
+ let forced = false;
42554
+ if (status !== "completed" && await isOrphan(run, repo, deps)) {
42555
+ await deps.forceCancel(repo, run.id);
42556
+ report.forceRequested.push(run.id);
42557
+ forced = true;
42558
+ status = await settled(repo, run.id, deps);
42559
+ }
42560
+ if (status === "completed") {
42561
+ report.cancelled.push(run.id);
42562
+ if (forced) report.forceCancelled.push(run.id);
42563
+ } else report.errors.push(`cancel-orphans: run ${run.id} still ${status} after cancel${forced ? " and force-cancel" : ""}`);
42241
42564
  }
42242
42565
  }
42243
42566
  } catch (error) {
@@ -42263,17 +42586,68 @@ function registerCancelOrphans(ci2, writeToken) {
42263
42586
  timeout: 2e4,
42264
42587
  env: { ...process.env, GH_TOKEN: token }
42265
42588
  });
42589
+ },
42590
+ forceCancel: async (repo, id) => {
42591
+ token ??= await writeToken();
42592
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${id}/force-cancel`, "--method", "POST"], {
42593
+ timeout: 2e4,
42594
+ env: { ...process.env, GH_TOKEN: token }
42595
+ });
42266
42596
  }
42267
42597
  });
42268
42598
  if (options.json) console.log(JSON.stringify(report, null, 2));
42269
42599
  else {
42270
42600
  for (const run of report.candidates) console.log(`${run.id} ${run.head_branch} ${run.html_url}`);
42271
- console.log(`cancel-orphans: ${report.candidates.length} candidates; ${report.cancelled.length} cancelled${options.apply ? "" : " (report only; use --apply)"}`);
42601
+ console.log(`cancel-orphans: ${report.candidates.length} candidates; ${report.cancelled.length} cancelled (verified completed; ${report.forceCancelled.length} of them after force-cancel; ${report.forceRequested.length} force-cancel request(s))${options.apply ? "" : " (report only; use --apply)"}`);
42272
42602
  for (const error of report.errors) console.error(error);
42273
42603
  }
42274
42604
  if (report.errors.length) process.exitCode = 1;
42275
42605
  });
42276
42606
  }
42607
+ var RUN_CANCEL_STATUS = "cancel-requested";
42608
+ var RUN_FORCE_CANCEL_STATUS = "force-cancel-requested";
42609
+ async function cancelWorkflowRun(repo, id, deps, force = false) {
42610
+ const raw = String(id).trim();
42611
+ if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
42612
+ const runId = Number(raw);
42613
+ if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
42614
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
42615
+ try {
42616
+ if (force && deps.forceCancel) await deps.forceCancel(repo, runId);
42617
+ else await deps.cancel(repo, runId);
42618
+ } catch (error) {
42619
+ const detail = String(error.stderr ?? error);
42620
+ 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
+ }
42622
+ return { ok: true, repo, runId, ...force ? { force: true } : {}, status: force ? RUN_FORCE_CANCEL_STATUS : RUN_CANCEL_STATUS };
42623
+ }
42624
+ function registerRunCancel(run, writeToken) {
42625
+ 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
+ let receipt;
42627
+ try {
42628
+ receipt = await cancelWorkflowRun(options.repo, id, {
42629
+ cancel: async (repo, runId) => {
42630
+ const token = await writeToken();
42631
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
42632
+ timeout: 2e4,
42633
+ env: { ...process.env, GH_TOKEN: token }
42634
+ });
42635
+ },
42636
+ forceCancel: async (repo, runId) => {
42637
+ const token = await writeToken();
42638
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
42639
+ timeout: 2e4,
42640
+ env: { ...process.env, GH_TOKEN: token }
42641
+ });
42642
+ }
42643
+ }, options.force === true);
42644
+ } catch (error) {
42645
+ return fail(error.message);
42646
+ }
42647
+ 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})`);
42649
+ });
42650
+ }
42277
42651
 
42278
42652
  // src/board-commands.ts
42279
42653
  init_cli_shared();
@@ -42690,6 +43064,8 @@ function registerBoardCommands(program3) {
42690
43064
  console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
42691
43065
  if (result.claimed) for (const line of formatDecisionLines(result.ref, ruled[i].decisions ?? [])) console.log(line);
42692
43066
  });
43067
+ const summary = formatBulkClaimSummary(bulk.results);
43068
+ if (summary) console.log(summary);
42693
43069
  printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings]);
42694
43070
  }
42695
43071
  if (bulk.failed > 0) process.exitCode = 1;
@@ -46353,13 +46729,14 @@ function filledDocCheck(label, text, path2) {
46353
46729
  const unfilled = unfilledDocPlaceholders(text);
46354
46730
  return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
46355
46731
  }
46356
- var NPMRC_PATHS = [".npmrc", "web/.npmrc"];
46732
+ var NPMRC_PATHS = [".npmrc", "module/.npmrc", "web/.npmrc"];
46357
46733
  var SCOPED_GITHUB_PACKAGES_RE = /^\s*@[A-Za-z0-9-]+:registry\s*=\s*https:\/\/npm\.pkg\.github\.com/m;
46358
46734
  var GITHUB_PACKAGES_AUTH_RE = /^\s*\/\/npm\.pkg\.github\.com\/:_authToken\s*=(.*)$/m;
46359
46735
  function npmrcAuthValueIsPlaceholder(value) {
46360
46736
  const trimmed = value.trim();
46361
46737
  return trimmed === "" || /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(trimmed);
46362
46738
  }
46739
+ var GH_PACKAGES_SCOPE_NOTE = ". Note that `gh auth token` only works as a Packages credential when that login carries the read:packages scope \u2014 without it the same install fails as `E403 ... Permission permission_denied: The token ... does not match expected scopes` (a scope refusal, not a 401/E401). Recoveries: widen the login with `gh auth refresh -h github.com -s read:packages`, or keep the token keyless in the Jerv vault key `GH_PACKAGES_READ_TOKEN` and supply it with `jerv-cli vault use`";
46363
46740
  function npmrcGitHubPackagesAuthCheck(path2, text) {
46364
46741
  if (text === null || !SCOPED_GITHUB_PACKAGES_RE.test(text)) return null;
46365
46742
  const label = `${path2} authenticates GitHub Packages`;
@@ -46368,7 +46745,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
46368
46745
  return {
46369
46746
  ok: false,
46370
46747
  label,
46371
- detail: `${path2} points a scope at npm.pkg.github.com with no token line \u2014 add \`//npm.pkg.github.com/:_authToken=<token>\` with a real value, or keep the line as \`\${NODE_AUTH_TOKEN}\` and supply the token in the shell for every install (NODE_AUTH_TOKEN=$(gh auth token) npm ci) \u2014 otherwise npm ci 401s outside a setup-node CI job`
46748
+ detail: `${path2} points a scope at npm.pkg.github.com with no token line \u2014 add \`//npm.pkg.github.com/:_authToken=<token>\` with a real value, or keep the line as \`\${NODE_AUTH_TOKEN}\` and supply the token in the shell for every install (NODE_AUTH_TOKEN=$(gh auth token) npm ci) \u2014 otherwise npm ci 401s outside a setup-node CI job` + GH_PACKAGES_SCOPE_NOTE
46372
46749
  };
46373
46750
  }
46374
46751
  if (npmrcAuthValueIsPlaceholder(match[1])) {
@@ -46376,7 +46753,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
46376
46753
  return {
46377
46754
  ok: false,
46378
46755
  label,
46379
- detail: `${path2} carries only the placeholder //npm.pkg.github.com/:_authToken=${carried} \u2014 declared, not proven: GitHub Packages reads that line only while NODE_AUTH_TOKEN is exported in the same shell, so a lane, laptop or container with it unset 401s before anything names the cause. Two recoveries: put a real token value in the line, or supply the token from the shell (NODE_AUTH_TOKEN=$(gh auth token) npm ci)`
46756
+ detail: `${path2} carries only the placeholder //npm.pkg.github.com/:_authToken=${carried} \u2014 declared, not proven: GitHub Packages reads that line only while NODE_AUTH_TOKEN is exported in the same shell, so a lane, laptop or container with it unset 401s before anything names the cause. Two recoveries: put a real token value in the line, or supply the token from the shell (NODE_AUTH_TOKEN=$(gh auth token) npm ci)` + GH_PACKAGES_SCOPE_NOTE
46380
46757
  };
46381
46758
  }
46382
46759
  return { ok: true, label };
@@ -46564,7 +46941,15 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
46564
46941
  });
46565
46942
  }
46566
46943
  }
46567
- for (const path2 of NPMRC_PATHS) {
46944
+ const tree = await restJson3(
46945
+ deps,
46946
+ `repos/${repo}/git/trees/${encodeURIComponent(baseBranch)}?recursive=1`,
46947
+ {}
46948
+ );
46949
+ const discoveredNpmrcPaths = (tree.tree ?? []).filter(
46950
+ (entry) => entry.type === "blob" && typeof entry.path === "string" && (entry.path === ".npmrc" || entry.path.endsWith("/.npmrc"))
46951
+ ).map((entry) => entry.path);
46952
+ for (const path2 of /* @__PURE__ */ new Set([...NPMRC_PATHS, ...discoveredNpmrcPaths])) {
46568
46953
  const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
46569
46954
  if (check) checks.push(check);
46570
46955
  }
@@ -51778,7 +52163,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
51778
52163
  if (now() >= deadline) break;
51779
52164
  const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
51780
52165
  if (checks.status !== "success" && checks.status !== "skipped") {
51781
- return { mergeStatus: "failed", error: `${result.error ?? "merge failed"}
52166
+ return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
51782
52167
  registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
51783
52168
  }
51784
52169
  if (now() >= deadline) break;
@@ -51924,6 +52309,8 @@ async function runPrLand(prNumber, options, deps) {
51924
52309
  if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
51925
52310
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
51926
52311
  base.mergeStatus = merge.mergeStatus;
52312
+ if (merge.credential) base.credential = merge.credential;
52313
+ if (merge.credentialRole) base.credentialRole = merge.credentialRole;
51927
52314
  if (merge.mergeStatus === "queue-admission-pending") {
51928
52315
  return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
51929
52316
  }
@@ -51985,9 +52372,19 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
51985
52372
  const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
51986
52373
  const deadline = deadlineMs ?? now() + timeoutMs;
51987
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
+ };
51988
52384
  while (now() < deadline) {
51989
52385
  const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
51990
52386
  snapshot = void 0;
52387
+ if (read.isInMergeQueue || read.state === "MERGED") observe(read);
51991
52388
  if (read.state === "MERGED") return queueMerged(base, deps);
51992
52389
  if (read.isInMergeQueue) {
51993
52390
  base.mergeQueue = {
@@ -52000,7 +52397,10 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
52000
52397
  } else if (read.state === "OPEN" && !read.autoMergeRequest) {
52001
52398
  await sleep2(PR_LAND_EJECT_CONFIRM_MS);
52002
52399
  const again = await deps.readMergeQueue(pr, base.repo);
52003
- if (again.state === "MERGED") return queueMerged(base, deps);
52400
+ if (again.state === "MERGED") {
52401
+ observe(again);
52402
+ return queueMerged(base, deps);
52403
+ }
52004
52404
  if (again.isInMergeQueue || again.autoMergeRequest) {
52005
52405
  snapshot = again;
52006
52406
  continue;
@@ -52055,6 +52455,10 @@ function renderPrLandTable(entries) {
52055
52455
  const width = (i) => Math.max(...rows.map((r) => r[i].length));
52056
52456
  return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
52057
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
+ }
52058
52462
 
52059
52463
  // src/git-worktree-observation.ts
52060
52464
  function parseGitWorktreePorcelain(text) {
@@ -52255,6 +52659,12 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).filter((line) => li
52255
52659
  var defaultGhMergeAutoIo = {
52256
52660
  gh: async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout
52257
52661
  };
52662
+ var credentialedGh = {
52663
+ gh: async (args, timeoutMs) => {
52664
+ const { stdout, credential } = await execFileP("gh", args, { timeout: timeoutMs });
52665
+ return { stdout, credential };
52666
+ }
52667
+ };
52258
52668
  function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
52259
52669
  const policy = squashBodyWithOverride(commits, cwd);
52260
52670
  const base = policy ?? defaultSquashBodyFromCommits(commits);
@@ -52316,14 +52726,34 @@ async function ghAdminTrueMergeWhenClean(prNumber, repo, opts = {}) {
52316
52726
  return { mergeStatus: "merged" };
52317
52727
  }
52318
52728
  async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
52319
- const io = extra?.io ?? defaultGhMergeAutoIo;
52729
+ let credential;
52730
+ let refusedAsMerged = false;
52731
+ const source = extra?.io ?? credentialedGh;
52732
+ const note = (args2, value) => {
52733
+ if (args2[0] === "pr" && args2[1] === "merge" && value) credential = value;
52734
+ };
52735
+ const io = {
52736
+ gh: async (ghArgs, timeoutMs) => {
52737
+ try {
52738
+ const out = await source.gh(ghArgs, timeoutMs);
52739
+ if (typeof out === "string") return out;
52740
+ note(ghArgs, out.credential);
52741
+ return out.stdout;
52742
+ } catch (e) {
52743
+ note(ghArgs, e?.credential);
52744
+ if (ghArgs[0] === "pr" && ghArgs[1] === "merge" && /already been merged/i.test(String(e?.message ?? ""))) refusedAsMerged = true;
52745
+ throw e;
52746
+ }
52747
+ }
52748
+ };
52320
52749
  const args = repo ? ["--repo", repo] : [];
52321
52750
  const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
52322
52751
  allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
52323
52752
  bodyText: method === "--squash" ? extra?.bodyText : void 0
52324
52753
  });
52325
52754
  try {
52326
- return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
52755
+ const result = await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
52756
+ return credential ? { ...result, credential, ...refusedAsMerged && result.mergeStatus === "merged" ? { credentialRole: "attempted-write" } : {} } : result;
52327
52757
  } finally {
52328
52758
  overrideBody?.cleanup();
52329
52759
  }
@@ -55727,7 +56157,7 @@ async function observePrCreateClaims(body, repoOption, deps = {}) {
55727
56157
  if (findings.length) throw new Error("pr create: claim findings must be resolved before restamping");
55728
56158
  if (attempted) throw new Error("pr create: claim restamp already attempted; observe again before retrying");
55729
56159
  attempted = true;
55730
- const writer = deps.client ?? defaultGitHubClient();
56160
+ const writer = await selectClaimWriteClient(deps.client ?? defaultGitHubClient(), deps.appClient);
55731
56161
  for (const number of restamps) {
55732
56162
  await writer.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } });
55733
56163
  }
@@ -55743,11 +56173,11 @@ function catFileBatch(root, ref, paths) {
55743
56173
  if (paths.length === 0) return Promise.resolve([]);
55744
56174
  return new Promise((resolve7) => {
55745
56175
  const chunks = [];
55746
- let settled = false;
56176
+ let settled2 = false;
55747
56177
  const child2 = (0, import_node_child_process15.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
55748
56178
  const finish = (ok = false) => {
55749
- if (settled) return;
55750
- settled = true;
56179
+ if (settled2) return;
56180
+ settled2 = true;
55751
56181
  clearTimeout(timer);
55752
56182
  resolve7(ok ? parseCatFileBatch(Buffer.concat(chunks), paths.length) : []);
55753
56183
  };
@@ -55913,6 +56343,21 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
55913
56343
  }
55914
56344
  };
55915
56345
  }
56346
+ async function retryPrCreateIdentityDrift(input, deps, notify = (line) => process.stderr.write(line)) {
56347
+ notify("pr create: WARNING \u2014 candidate or policy/config identity changed during preparation; re-reading the head, base and policy/config identity and preparing once more (#9470)\n");
56348
+ const preparation = await preparePrCreate(input, deps);
56349
+ if (preparation.findings.length) {
56350
+ return { preparation, error: new Error(`preparation found ${preparation.findings.length} blocker(s) after re-reading the current identity: ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("; ")}`) };
56351
+ }
56352
+ try {
56353
+ await preparation.commit();
56354
+ return { preparation };
56355
+ } catch (error) {
56356
+ const message2 = error.message;
56357
+ if (!message2.includes("identity changed during preparation")) return { preparation, error };
56358
+ return { preparation, error: new Error(`${message2}. Automatic re-preparation ran once and the identity drifted again \u2014 this is retryable: re-run \`mmi-cli devops pr create\` (same title/body/head/base) against the current head and base; a concurrent push, rebase or policy/config commit caused it, and re-preparing loses nothing.`) };
56359
+ }
56360
+ }
55916
56361
  function createPreparationDeps(input, io = {}) {
55917
56362
  const exec3 = io.exec ?? execFileP;
55918
56363
  const git3 = async (args) => (await exec3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout.trim();
@@ -56196,10 +56641,10 @@ function healthPollIntervalMs() {
56196
56641
  }
56197
56642
  function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
56198
56643
  return new Promise((resolve7, reject2) => {
56199
- let settled = false;
56644
+ let settled2 = false;
56200
56645
  const finish = (fn) => {
56201
- if (settled) return;
56202
- settled = true;
56646
+ if (settled2) return;
56647
+ settled2 = true;
56203
56648
  clearTimeout(timer);
56204
56649
  child2.removeAllListeners("error");
56205
56650
  child2.removeAllListeners("exit");
@@ -58258,7 +58703,15 @@ function registerCollaborationCommands(program3) {
58258
58703
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
58259
58704
  }
58260
58705
  });
58261
- jsonParity(issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
58706
+ jsonParity(issue.command("comment <ref>").description("post or exact-text edit a Markdown issue comment and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).option("--edit <commentId>", "edit an existing comment on this issue").option("--replace <text>", "exact text to replace (must occur exactly once)").option("--with <text>", "replacement text (may be empty)").action(async (ref, o) => {
58707
+ const editShape = "--edit <commentId> --replace <text> --with <text> (without --body/--body-file)";
58708
+ if (o.edit !== void 0) {
58709
+ if (o.replace === void 0 || o.with === void 0 || o.body !== void 0 || o.bodyFile !== void 0 || o.replace.length === 0) {
58710
+ return fail(`issue comment: use ${editShape}; --replace must not be empty`);
58711
+ }
58712
+ } else if (o.replace !== void 0 || o.with !== void 0) {
58713
+ return fail(`issue comment: --replace/--with require ${editShape}`);
58714
+ }
58262
58715
  let parsed;
58263
58716
  try {
58264
58717
  parsed = parseIssueRef(ref, o.repo);
@@ -58267,14 +58720,14 @@ function registerCollaborationCommands(program3) {
58267
58720
  }
58268
58721
  let body;
58269
58722
  try {
58270
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
58723
+ body = o.edit !== void 0 ? "" : await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
58271
58724
  } catch (e) {
58272
58725
  return fail(`issue comment: ${e.message}`);
58273
58726
  }
58274
58727
  const repo = await resolveRepo(parsed.repo ?? o.repo);
58275
58728
  if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
58276
58729
  try {
58277
- const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
58730
+ const result = o.edit !== void 0 ? await editIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, commentId: o.edit, replace: o.replace, with: o.with }) : await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
58278
58731
  console.log(JSON.stringify(result));
58279
58732
  } catch (e) {
58280
58733
  const err = e;
@@ -58483,7 +58936,7 @@ ${list}`);
58483
58936
  title = safeTitle;
58484
58937
  process.stderr.write('pr create: WARNING \u2014 rewrote close-keyword text in the title to "part of #N": the squash commit subject would close it (#7688). Put closing keywords on a "Closes #N" body line.\n');
58485
58938
  }
58486
- const preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
58939
+ let preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
58487
58940
  if (preparation.findings.length) return fail(`pr create: preparation found ${preparation.findings.length} blocker(s):
58488
58941
  ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("\n")}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
58489
58942
  for (const observation of preparation.observations.filter((item) => item.state === "skipped")) {
@@ -58497,7 +58950,16 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
58497
58950
  try {
58498
58951
  await preparation.commit();
58499
58952
  } catch (error) {
58500
- return fail(`pr create: preparation commit failed \u2014 ${error.message}`);
58953
+ const message2 = error.message;
58954
+ if (!message2.includes("identity changed during preparation")) return fail(`pr create: preparation commit failed \u2014 ${message2}`);
58955
+ const retry = await retryPrCreateIdentityDrift({ body, repo: o.repo, head: o.head, base: o.base }, createPreparationDeps({ body, repo: o.repo, head: o.head, base: o.base }));
58956
+ if (retry.error) return fail(`pr create: preparation commit failed after one automatic re-preparation \u2014 ${retry.error.message}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
58957
+ preparation = retry.preparation;
58958
+ if (preparation.candidate?.head !== createHead) {
58959
+ const retriedHeadWarning = await prCreateMergedHeadWarning({ head: preparation.candidate?.head || void 0, repo: o.repo });
58960
+ if (retriedHeadWarning) process.stderr.write(`${retriedHeadWarning}
58961
+ `);
58962
+ }
58501
58963
  }
58502
58964
  const created = await ghCreate(buildPrArgs({ title, body, base: preparation.candidate.base, head: preparation.candidate.head, repo: preparation.candidate.repo, draft: o.draft }), { allowHeadPublication: false });
58503
58965
  if (isGhCreateRateLimited(created)) {
@@ -58859,6 +59321,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
58859
59321
  return receipt;
58860
59322
  },
58861
59323
  readMergeQueue: readPrMergeQueue,
59324
+ observeQueued: (prNumber, repo, snapshot) => observeQueuedAdmission(repo, prNumber, snapshot),
58862
59325
  findMergeGroupRun: findPrMergeGroupRun,
58863
59326
  queueProgress: (receipt) => {
58864
59327
  const line = prMergeQueueLine(receipt);
@@ -59047,7 +59510,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59047
59510
  inFlight = Math.max(1, inFlight - 1);
59048
59511
  }
59049
59512
  });
59050
- const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
59513
+ const { each: credentials, overall: credential } = landCredentials(entries.map((e) => e.result));
59051
59514
  if (numbers.length === 1) {
59052
59515
  const { result, cleanup } = entries[0];
59053
59516
  if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
@@ -59058,7 +59521,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59058
59521
  if (cleanup) printLine(`pr land: ${cleanup}`);
59059
59522
  }
59060
59523
  } else if (opts.json) {
59061
- printLine(JSON.stringify({ results: entries.map((e) => ({ ...e.result, outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "queue-admission-pending", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
59524
+ printLine(JSON.stringify({ results: entries.map((e, i) => ({ ...e.result, credential: credentials[i], outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "queue-admission-pending", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
59062
59525
  } else {
59063
59526
  printLine(renderPrLandTable(entries));
59064
59527
  printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
@@ -64808,8 +65271,7 @@ function parseBrokerPrState(raw) {
64808
65271
  }
64809
65272
  function buildPrListSpec(o) {
64810
65273
  const repo = parseBrokerRepo(o.repo, "--repo");
64811
- const fields = parseBrokerJsonFields(o.json);
64812
- rejectPrViewQueueFields(fields, "<number>", repo, "list");
65274
+ const fields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), "<number>", repo, "list");
64813
65275
  const args = withJson(withRepo(["pr", "list"], repo), fields, o.json);
64814
65276
  const state = parseBrokerPrState(o.state);
64815
65277
  if (state) args.push("--state", state);
@@ -64876,18 +65338,24 @@ function buildRunWatchSpec(id, o) {
64876
65338
  }
64877
65339
  function rejectPrViewQueueFields(fields, number, repo, verb = "view") {
64878
65340
  const badField = fields?.split(",").find((field) => field === "mergeQueueEntry" || field === "isInMergeQueue");
64879
- if (!badField) return;
65341
+ if (!badField) return fields;
64880
65342
  const targetRepo2 = repo ?? "<owner/repo>";
64881
65343
  const remaining = fields?.split(",").filter((f) => f && f !== "mergeQueueEntry" && f !== "isInMergeQueue").join(",") ?? "";
64882
- throw new Error(
64883
- `read pr ${verb}: '${badField}' is not a gh pr ${verb} --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\` and drop it from --json${remaining ? `; for the remaining fields run \`mmi-cli devops read pr ${verb} ${number}${repo ? ` --repo ${repo}` : ""} --json ${remaining}\`` : ""}`
65344
+ if (!remaining) {
65345
+ throw new Error(
65346
+ `read pr ${verb}: '${badField}' is not a gh pr ${verb} --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\` and drop it from --json`
65347
+ );
65348
+ }
65349
+ process.stderr.write(
65350
+ `read pr ${verb}: dropped '${badField}' (not a gh pr ${verb} --json field) \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\`
65351
+ `
64884
65352
  );
65353
+ return remaining;
64885
65354
  }
64886
65355
  function buildPrViewSpec(ref, o) {
64887
65356
  const number = parseBrokerId(ref, "PR number");
64888
- const jsonFields = parseBrokerJsonFields(o.json);
64889
65357
  const repo = parseBrokerRepo(o.repo, "--repo");
64890
- rejectPrViewQueueFields(jsonFields, number, repo);
65358
+ const jsonFields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), number, repo);
64891
65359
  const args = withJson(withRepo(["pr", "view", number], repo), jsonFields, o.json);
64892
65360
  if (o.comments) args.push("--comments");
64893
65361
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
@@ -64961,7 +65429,34 @@ function ghExitCodeOf(error) {
64961
65429
  const code = error.code;
64962
65430
  return typeof code === "number" ? code : null;
64963
65431
  }
64964
- function brokerGhFailureHint(failureText, args) {
65432
+ var RUN_LIST_RECOVERY_FLAGS = ["--repo", "--branch", "--commit", "--event", "--workflow", "--status", "--created", "--limit"];
65433
+ function shellWord(value) {
65434
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
65435
+ }
65436
+ function formatBrokerRunListRecovery(spec, jq) {
65437
+ const tokens2 = spec.args.slice(2);
65438
+ const valueOf = (flag) => {
65439
+ const index = tokens2.indexOf(flag);
65440
+ if (index === -1) return void 0;
65441
+ const next = tokens2[index + 1];
65442
+ return next === void 0 || next.startsWith("-") ? void 0 : next;
65443
+ };
65444
+ const words = ["MMI_ACTOR=app", "mmi-cli", "devops", "read", "run", "list"];
65445
+ const repo = valueOf("--repo");
65446
+ words.push("--repo", repo === void 0 ? "<owner/repo>" : shellWord(repo));
65447
+ for (const flag of RUN_LIST_RECOVERY_FLAGS.slice(1)) {
65448
+ const value = valueOf(flag);
65449
+ if (value !== void 0) words.push(flag, shellWord(value));
65450
+ }
65451
+ if (tokens2.includes("--json")) {
65452
+ const fields = valueOf("--json");
65453
+ words.push("--json");
65454
+ if (fields !== void 0) words.push(shellWord(fields));
65455
+ }
65456
+ if (jq !== void 0) words.push("--jq", `'${jq.replace(/'/g, `'\\''`)}'`);
65457
+ return `re-run exactly this read: \`${words.join(" ")}\` \u2014 it carries every filter gh was given, because a reconstructed list that drops them answers a different question`;
65458
+ }
65459
+ function brokerGhFailureHint(failureText, args, jq) {
64965
65460
  const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
64966
65461
  const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
64967
65462
  if (unknownField === "mergeQueueEntry" || unknownField === "isInMergeQueue") {
@@ -64973,9 +65468,12 @@ function brokerGhFailureHint(failureText, args) {
64973
65468
  if (/Could not resolve to a PullRequest/i.test(failureText)) {
64974
65469
  return `GitHub could not resolve the PR${target ? ` #${target}` : ""} \u2014 verify the number and that --repo names the repo that owns it before retrying; an identical re-run fails the same way`;
64975
65470
  }
65471
+ if (args[0] === "run" && args[1] === "list") {
65472
+ return formatBrokerRunListRecovery({ program: "gh", args: [...args], timeoutMs: SINGLE_SHOT_TIMEOUT_MS }, jq);
65473
+ }
64976
65474
  return void 0;
64977
65475
  }
64978
- async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
65476
+ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk), jq) {
64979
65477
  const env = brokerChildEnv();
64980
65478
  let output;
64981
65479
  try {
@@ -64987,7 +65485,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
64987
65485
  if (code !== null) {
64988
65486
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
64989
65487
  const failure = `${typeof err.stdout === "string" ? err.stdout : ""}${typeof err.stderr === "string" ? err.stderr : ""}`;
64990
- const recovery = brokerGhFailureHint(failure, spec.args) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
65488
+ const recovery = brokerGhFailureHint(failure, spec.args, jq) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
64991
65489
  process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 ${recovery}
64992
65490
  `);
64993
65491
  return code;
@@ -64998,7 +65496,8 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
64998
65496
  try {
64999
65497
  JSON.parse(output);
65000
65498
  } catch {
65001
- process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 retry the same typed read; only parse stdout after success
65499
+ const recovery = spec.args[0] === "run" && spec.args[1] === "list" ? `${formatBrokerRunListRecovery(spec, jq)}; only parse stdout after success` : "retry the same typed read; only parse stdout after success";
65500
+ process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 ${recovery}
65002
65501
  `);
65003
65502
  return 1;
65004
65503
  }
@@ -65080,13 +65579,13 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
65080
65579
  requireBrokerApp(label);
65081
65580
  try {
65082
65581
  if (jq === void 0) {
65083
- process.exitCode = await runBrokerGh(ghRun, spec);
65582
+ process.exitCode = await runBrokerGh(ghRun, spec, void 0, void 0);
65084
65583
  return;
65085
65584
  }
65086
65585
  let out = "";
65087
65586
  const code = await runBrokerGh(ghRun, spec, (chunk) => {
65088
65587
  out += chunk;
65089
- });
65588
+ }, jq);
65090
65589
  if (code === 0) await emitJson(out, jq);
65091
65590
  process.exitCode = code;
65092
65591
  } catch (e) {
@@ -65309,7 +65808,7 @@ function typedReadHint(path2) {
65309
65808
  return say(`mmi-cli devops read issue view <issue> --repo ${slug} --json comments`, `; a comment is read through its issue, so match the comment whose url ends in issuecomment-${id}`);
65310
65809
  }
65311
65810
  if (sub === "events" || sub === "timeline") {
65312
- return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json state,stateReason,closedAt,labels,assignees,comments`, "; the raw event/timeline history itself has no read");
65811
+ return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json state,stateReason,closedAt,closedBy,labels,assignees,comments`, "; closedBy carries the merge evidence (merged, mergedAt, mergeCommit.oid), while the raw event/timeline history itself has no read");
65313
65812
  }
65314
65813
  return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json title,body,state,labels,comments`);
65315
65814
  }
@@ -65339,11 +65838,17 @@ function tagsReadHint(path2) {
65339
65838
  const slug = `${match[1]}/${match[2]}`;
65340
65839
  return ` No App-token REST read exists for git tags or refs \u2014 read git tags directly with \`git ls-remote --tags origin 'refs/tags/*'\` (no token needed; \`git ls-remote origin refs/tags/<tag>\` for one tag), or read tag-backed GitHub Releases with \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/releases?per_page=20"\`.`;
65341
65840
  }
65841
+ function branchRulesReadHint(path2) {
65842
+ const match = new RegExp(`^repos/(${NAME})/(${NAME})/(?:rules/branches/[^/?#]+|branches/[^/?#]+/protection(?:/|$)|rulesets(?:/|$))`).exec(path2.split("?")[0]);
65843
+ if (!match) return "";
65844
+ const slug = `${match[1]}/${match[2]}`;
65845
+ return ` No raw rules, rulesets or branch-protection read is on the App allowlist, so retrying path or query variants cannot work \u2014 inspect a PR's required status contexts with \`MMI_ACTOR=app mmi-cli devops pr checks-required <pr> --repo ${slug} --json\`, or verify repository ruleset and branch-protection health with \`mmi-cli devops bootstrap verify --repo ${slug} --json\` and \`mmi-cli devops ci audit --repo ${slug} --json\`.`;
65846
+ }
65342
65847
  function parseApiGetPath(rawPath) {
65343
65848
  const path2 = rawPath.trim();
65344
65849
  const refuse = () => {
65345
65850
  const hint = /^[A-Za-z]:[/\\]/.test(path2) ? " Git Bash/MSYS may have rewritten a leading-slash argument; retry with MSYS_NO_PATHCONV=1 and the slash-free repos/... path. The endpoint must still be allowlisted." : "";
65346
- const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${hint}`;
65851
+ const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${branchRulesReadHint(path2)}${hint}`;
65347
65852
  return new Error(`api get: refusing '${rawPath}' \u2014${routeHints}${API_GET_RECOVERY} Only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.`);
65348
65853
  };
65349
65854
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
@@ -65395,14 +65900,16 @@ async function readAppApi(path2, runner = apiReadRunner) {
65395
65900
  function registerApiReadCommands(program3, overrides) {
65396
65901
  const spawn7 = overrides?.runner ?? apiReadRunner;
65397
65902
  const jqRun = overrides?.jqRunner ?? systemJqRunner;
65398
- const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)");
65399
- api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
65400
- acceptHeaderFor(v);
65401
- return v.trim().toLowerCase();
65402
- }, "json").option("--jq <expr>", JQ_HELP, (v) => parseBrokerJq(v)).option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)").argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o) => {
65903
+ const runGet = async (req, o) => {
65403
65904
  if (o.jq !== void 0 && (o.accept === "raw" || o.out !== void 0)) return fail("api get: --jq is only valid with JSON stdout reads (not --accept raw or --out)");
65404
65905
  const zip = req.kind === "actions-artifact-zip" || req.kind === "actions-job-logs" || req.kind === "release-asset";
65405
- if (zip !== (o.out !== void 0)) return fail(`api get: --out <file> is required for, and only valid with, repos/{owner}/{repo}/actions/artifacts/{id}/zip and repos/{owner}/{repo}/actions/jobs/{id}/logs and repos/{owner}/{repo}/releases/assets/{id}`);
65906
+ if (zip !== (o.out !== void 0)) {
65907
+ const shapes = "api get: --out <file> is required for, and only valid with, repos/{owner}/{repo}/actions/artifacts/{id}/zip and repos/{owner}/{repo}/actions/jobs/{id}/logs and repos/{owner}/{repo}/releases/assets/{id}";
65908
+ if (!zip && o.out !== void 0) {
65909
+ return fail(`${shapes} \u2014 drop --out and let the JSON body go to stdout (pipe it into node/jq, or use --jq <expr>), or use the typed read (e.g. \`mmi-cli devops read run list --repo <owner/repo> --json databaseId,workflowName,event,status,conclusion\`). This refusal exits 1 on stderr with nothing written: check the exit code and never discard stderr (2>&1 / >/dev/null) before parsing.`);
65910
+ }
65911
+ return fail(shapes);
65912
+ }
65406
65913
  const appRoute = req.kind.startsWith("actions-") || req.kind.startsWith("release-");
65407
65914
  if (appRoute) requireBrokerApp("api get");
65408
65915
  const env = appRoute || process.env.MMI_ACTOR?.trim() !== "personal" ? brokerChildEnv() : void 0;
@@ -65423,6 +65930,28 @@ function registerApiReadCommands(program3, overrides) {
65423
65930
  return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
65424
65931
  }
65425
65932
  process.stdout.write(body);
65933
+ };
65934
+ const withApiGetOptions = (cmd) => cmd.option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
65935
+ acceptHeaderFor(v);
65936
+ return v.trim().toLowerCase();
65937
+ }, "json").option("--jq <expr>", JQ_HELP, (v) => parseBrokerJq(v)).option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)");
65938
+ const api = withApiGetOptions(
65939
+ program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)")
65940
+ ).argument("[path]", "allowlisted GET path \u2014 shorthand for `api get <path>` (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, _o, cmd) => {
65941
+ if (req === void 0) {
65942
+ return fail(`api: name an allowlisted GET path \u2014 \`mmi-cli devops api <path>\` or \`mmi-cli devops api get <path>\`. Only these GET reads are allowed: ${API_GET_ALLOWED_SHAPES}.`);
65943
+ }
65944
+ return runGet(req, cmd.opts());
65945
+ });
65946
+ withApiGetOptions(
65947
+ api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist")
65948
+ ).argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o, cmd) => {
65949
+ const parentOpts = cmd.parent?.opts() ?? {};
65950
+ return runGet(req, {
65951
+ accept: o.accept && o.accept !== "json" ? o.accept : parentOpts.accept ?? o.accept ?? "json",
65952
+ jq: o.jq ?? parentOpts.jq,
65953
+ out: o.out ?? parentOpts.out
65954
+ });
65426
65955
  });
65427
65956
  }
65428
65957
 
@@ -69029,6 +69558,7 @@ setGhWriteTokenProvider(() => {
69029
69558
  }
69030
69559
  return filingToken ??= mintFilingWriteToken(appActorDeps()).then((t) => t.token, () => void 0);
69031
69560
  });
69561
+ setClaimWriteTokenProvider(() => mintFilingWriteToken(appActorDeps()).then((t) => t.token));
69032
69562
  setStatusWriteTokenProvider(() => mintStatusWriteToken(appActorDeps()));
69033
69563
  var registerPreAction = Reflect.get(program2, ["h", "ook"].join(""));
69034
69564
  registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand) => {
@@ -69036,7 +69566,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
69036
69566
  appActorCommand = commandPath4;
69037
69567
  setAuditCommand(commandPath4);
69038
69568
  if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
69039
- if (commandPath4 === "ci cancel-orphans") return;
69569
+ if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
69040
69570
  try {
69041
69571
  await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
69042
69572
  } catch (e) {
@@ -69871,6 +70401,10 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
69871
70401
  });
69872
70402
  registerCiChecksWait(ci);
69873
70403
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
70404
+ registerRunCancel(
70405
+ program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
70406
+ async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
70407
+ );
69874
70408
  ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
69875
70409
  if (o.apply && o.parkRuleset) {
69876
70410
  return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");