@mutmutco/codex-plugin 4.6.3 → 4.6.4

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/cli/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",
@@ -1599,16 +1601,26 @@ var init_cli_shared = __esm({
1599
1601
  rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options, ...env ? { env } : {} })
1600
1602
  );
1601
1603
  const appEnv = options.env ? void 0 : await ghReadAppEnv(file, args) ?? await ghFilingAppEnv(file, args);
1604
+ const reportsCredential = !options.env && /(^|[\\/])gh(\.exe)?$/i.test(file) && isAppFilingWrite(args);
1605
+ const tag = async (call, credential) => {
1606
+ if (!reportsCredential) return call;
1607
+ try {
1608
+ return { ...await call, credential };
1609
+ } catch (e) {
1610
+ if (e && typeof e === "object") e.credential = credential;
1611
+ throw e;
1612
+ }
1613
+ };
1602
1614
  if (/(^|[\\/])gh(\.exe)?$/i.test(file) && args[0] !== "auth") {
1603
1615
  const callerEnv = options.env;
1604
1616
  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
1617
  recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
1606
1618
  }
1607
- if (!appEnv) return run();
1619
+ if (!appEnv) return tag(run(), "user");
1608
1620
  try {
1609
- return await run(appEnv);
1621
+ return await tag(run(appEnv), "app_installation");
1610
1622
  } catch (e) {
1611
- if (shouldRetryIntegrationRefusalOnAmbient(e)) return run();
1623
+ if (shouldRetryIntegrationRefusalOnAmbient(e)) return tag(run(), "user");
1612
1624
  throw e;
1613
1625
  }
1614
1626
  };
@@ -15263,6 +15275,8 @@ async function mintInstallationToken(deps) {
15263
15275
  }
15264
15276
  var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15265
15277
  ["ci cancel-orphans", { metadata: "read", actions: "write" }],
15278
+ // #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
15279
+ ["run cancel", { metadata: "read", actions: "write" }],
15266
15280
  ["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
15267
15281
  // #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
15268
15282
  ["issue discover-related", { metadata: "read", issues: "write" }]
@@ -15953,9 +15967,6 @@ var REST_GH_TIMEOUT_MS = 2e4;
15953
15967
  var pollTokenProvider;
15954
15968
  var pollTokenOnce;
15955
15969
  var lastPollSource;
15956
- function lastRestPollSource() {
15957
- return lastPollSource;
15958
- }
15959
15970
  function setPollTokenProvider(provider) {
15960
15971
  pollTokenProvider = provider;
15961
15972
  pollTokenOnce = void 0;
@@ -16798,6 +16809,30 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
16798
16809
  }
16799
16810
 
16800
16811
  // src/claim-marker-client.ts
16812
+ var claimWriteTokenProvider;
16813
+ var claimWriteTokenOnce;
16814
+ function setClaimWriteTokenProvider(provider) {
16815
+ claimWriteTokenProvider = provider;
16816
+ claimWriteTokenOnce = void 0;
16817
+ }
16818
+ async function selectClaimWriteClient(client, appClient) {
16819
+ if (appClient) return appClient;
16820
+ if (!claimWriteTokenProvider) return client;
16821
+ let token;
16822
+ try {
16823
+ claimWriteTokenOnce ??= claimWriteTokenProvider();
16824
+ token = await claimWriteTokenOnce;
16825
+ } catch (e) {
16826
+ claimWriteTokenOnce = void 0;
16827
+ const detail = e instanceof Error ? e.message : String(e);
16828
+ throw new Error(`${detail} \u2014 refusing personal-token fallback for claim write (#9511)`);
16829
+ }
16830
+ if (!token) {
16831
+ claimWriteTokenOnce = void 0;
16832
+ throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
16833
+ }
16834
+ return createGitHubClient({ token: async () => token });
16835
+ }
16801
16836
  async function selectLivenessClient(client, appClient) {
16802
16837
  if (appClient) return appClient;
16803
16838
  let token;
@@ -21309,7 +21344,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
21309
21344
  var SKILL_LESSON_LABEL = "skill-lesson";
21310
21345
  var SKILL_LESSON_FILE_LABELS = [SKILL_LESSON_LABEL, LEARNING_LABEL2];
21311
21346
  var SKILL_LESSON_LOOP_KIND = "lesson";
21312
- var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "rcand", "release", "secrets"];
21347
+ var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "intercom-guest", "mmi-help", "org-guide", "rcand", "release", "secrets"];
21313
21348
  function assertSkillName(name) {
21314
21349
  const match = SKILL_NAMES.find((skill) => skill === name);
21315
21350
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -21342,6 +21377,9 @@ function findDuplicateLesson(source, openLessons) {
21342
21377
  // src/board-doctor-config.ts
21343
21378
  init_github_client();
21344
21379
 
21380
+ // src/issue-comment.ts
21381
+ init_github_client();
21382
+
21345
21383
  // src/sub-issue.ts
21346
21384
  init_error_codes();
21347
21385
  init_github_quota();
@@ -21551,6 +21589,40 @@ function parentLinkFields(result, error, parentRef, childRef, reason = "failed")
21551
21589
  }
21552
21590
 
21553
21591
  // src/issue-comment.ts
21592
+ async function editIssueComment(client, input) {
21593
+ const parsed = parseIssueRef(input.ref);
21594
+ const repo = parsed.repo ?? input.defaultRepo;
21595
+ if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
21596
+ if (!input.replace.length) throw new Error("--replace must not be empty; use --edit <commentId> --replace <text> --with <text>");
21597
+ if (!/^\d+$/.test(input.commentId)) throw new Error(`--edit takes a numeric comment id, got: ${input.commentId}`);
21598
+ const path2 = `repos/${repo}/issues/comments/${input.commentId}`;
21599
+ let comment;
21600
+ try {
21601
+ comment = await client.rest("GET", path2);
21602
+ } catch (e) {
21603
+ if (e instanceof GitHubApiError && e.status === 404) {
21604
+ throw new Error(`comment ${input.commentId} does not exist on ${repo}`);
21605
+ }
21606
+ throw e;
21607
+ }
21608
+ const expectedAnchor = `/issues/${parsed.number}#issuecomment-${input.commentId}`;
21609
+ if (!comment.html_url?.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
21610
+ throw new Error(`comment ${input.commentId} is not a comment on ${repo}#${parsed.number} (it lives at ${comment.html_url ?? "unknown"})`);
21611
+ }
21612
+ const body = comment.body ?? "";
21613
+ const count = body.split(input.replace).length - 1;
21614
+ if (count !== 1) throw new Error(`--replace must match exactly once; found ${count} occurrences in comment ${input.commentId}`);
21615
+ const index = body.indexOf(input.replace);
21616
+ const newBody = body.slice(0, index) + input.with + body.slice(index + input.replace.length);
21617
+ await client.rest("PATCH", path2, { body: { body: newBody } });
21618
+ return {
21619
+ number: parsed.number,
21620
+ repo,
21621
+ url: `https://github.com/${repo}/issues/${parsed.number}`,
21622
+ commentUrl: comment.html_url,
21623
+ edited: true
21624
+ };
21625
+ }
21554
21626
  async function postIssueComment(client, input) {
21555
21627
  const parsed = parseIssueRef(input.ref);
21556
21628
  const repo = parsed.repo ?? input.defaultRepo;
@@ -22059,9 +22131,12 @@ function detailCandidates(report, options = {}) {
22059
22131
  const scopes = [report.primary, report.secondary];
22060
22132
  return scopes.flatMap((scope) => options.all ? [...scope.userOwned, ...scope.claimable, ...scope.taken, ...scope.unownedInFlight] : [...scope.userOwned, ...scope.claimable]).filter((item) => item.contentType === "Issue");
22061
22133
  }
22134
+ function boardNotFoundRemedy(board, repoExplicit) {
22135
+ 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";
22136
+ }
22062
22137
  function boardNotFoundError(ref, board, opts = {}) {
22063
22138
  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";
22139
+ const remedy = opts.remedy ?? boardNotFoundRemedy(board, opts.repoExplicit === true);
22065
22140
  return new Error(`${ref} ${verb} the ${board.owner} #${board.number} board; ${remedy}`);
22066
22141
  }
22067
22142
  var NotClaimableError = class extends Error {
@@ -22489,7 +22564,7 @@ async function showBoardItem(options, deps = {}) {
22489
22564
  const currentRepo = await resolveCurrentRepo(options, deps);
22490
22565
  const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
22491
22566
  const { item } = await fetchIssueProjectItem(client, cfg, selector);
22492
- if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
22567
+ if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
22493
22568
  if (item.contentType === "Issue") {
22494
22569
  try {
22495
22570
  item.details = await fetchIssueDetails(client, item.repository, item.number);
@@ -23319,7 +23394,7 @@ function isSameBranchClaimResume(contest, repo, landing = {}) {
23319
23394
  const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
23320
23395
  const headSha = landing.headSha?.trim();
23321
23396
  if (!headBranch && !headSha) return false;
23322
- if (!contest.marker || contest.unverifiable.length > 0 || contest.live.length === 0) return false;
23397
+ if (contest.unverifiable.length > 0 || contest.live.length === 0) return false;
23323
23398
  const branchLine = /^live branch ([^\s]+) on /;
23324
23399
  const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
23325
23400
  const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
@@ -23360,7 +23435,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
23360
23435
  branchSha: evidence.branchSha
23361
23436
  }, item.repository, landing)) {
23362
23437
  contested = false;
23363
- resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
23438
+ if (evidence.marker) {
23439
+ resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
23440
+ }
23364
23441
  }
23365
23442
  const carry = staleWorkLines(evidence);
23366
23443
  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 +23503,7 @@ async function moveBoardItem(options, deps = {}) {
23426
23503
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
23427
23504
  const item = lookup.item;
23428
23505
  if (!item) {
23429
- throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
23506
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
23430
23507
  }
23431
23508
  const optionId = cfg.statusOptions[options.status];
23432
23509
  let credential = "user";
@@ -23529,6 +23606,9 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
23529
23606
  return {
23530
23607
  cfg,
23531
23608
  client,
23609
+ appClient: deps.appClient,
23610
+ git: deps.git ?? defaultGit,
23611
+ landing: deps.landing,
23532
23612
  items: collected.items,
23533
23613
  selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
23534
23614
  writable: writableOrUnknown(writable),
@@ -23621,7 +23701,7 @@ async function claimOneBoardItem(ctx, selector, options) {
23621
23701
  if (!verdict.ok) throw new NotClaimableError(verdict.reason, { ref: fresh.ref, status: fresh.status });
23622
23702
  item = fresh;
23623
23703
  const refuseIfContested = async () => {
23624
- const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
23704
+ const contest = await checkLaneContest(client, item, ctx.session, report.viewer, ctx.appClient, ctx.landing);
23625
23705
  resumeEvidence = void 0;
23626
23706
  staleStamp = void 0;
23627
23707
  const displaced = () => ({
@@ -23632,6 +23712,24 @@ async function claimOneBoardItem(ctx, selector, options) {
23632
23712
  host: contest.marker.host
23633
23713
  } : {}
23634
23714
  });
23715
+ if (contest.contested && !ctx.landing && contest.branch) {
23716
+ const candidateBranch = contest.branch.replace(/ \(head of open PR .*\)$/, "").trim();
23717
+ const originRepo = repoFromRemoteUrl(await ctx.git(["remote", "get-url", "origin"]).catch(() => ""));
23718
+ if (originRepo && originRepo.toLowerCase() === item.repository.toLowerCase()) {
23719
+ const localSha = (await ctx.git(["rev-parse", "--verify", `refs/heads/${candidateBranch}^{commit}`]).catch(() => "")).trim();
23720
+ const currentBranch2 = (await ctx.git(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "")).trim();
23721
+ if (localSha && (!contest.branchSha || localSha === contest.branchSha) || currentBranch2 === candidateBranch) {
23722
+ const localLanding = { headBranch: candidateBranch, ...localSha ? { headSha: localSha } : {} };
23723
+ if (isSameBranchClaimResume(contest, item.repository, localLanding)) {
23724
+ if (contest.marker) {
23725
+ resumeEvidence = `same-branch resume over live branch ${contest.branch}`;
23726
+ previousHolder = displaced();
23727
+ }
23728
+ return;
23729
+ }
23730
+ }
23731
+ }
23732
+ }
23635
23733
  if (!contest.contested) {
23636
23734
  if (contest.resume && contest.marker) {
23637
23735
  resumeEvidence = contest.resume;
@@ -23655,19 +23753,21 @@ async function claimOneBoardItem(ctx, selector, options) {
23655
23753
  if (options.check) {
23656
23754
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
23657
23755
  }
23658
- const renewed = await postClaimMarkerComment(client, item, ctx.session, staleStamp);
23756
+ const appWriter2 = await selectClaimWriteClient(client, ctx.appClient);
23757
+ const renewed = await postClaimMarkerComment(appWriter2, item, ctx.session, staleStamp);
23659
23758
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
23660
23759
  }
23661
23760
  if (options.check) {
23662
23761
  return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
23663
23762
  }
23664
23763
  await refuseIfContested();
23764
+ const appWriter = await selectClaimWriteClient(client, ctx.appClient);
23665
23765
  try {
23666
- await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
23766
+ await appWriter.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
23667
23767
  } catch (e) {
23668
23768
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
23669
23769
  }
23670
- await postClaimMarkerComment(client, item, ctx.session, staleStamp);
23770
+ await postClaimMarkerComment(appWriter, item, ctx.session, staleStamp);
23671
23771
  let credential = "user";
23672
23772
  try {
23673
23773
  ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
@@ -23828,7 +23928,7 @@ async function unclaimBoardIssue(options, deps = {}) {
23828
23928
  const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
23829
23929
  const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
23830
23930
  if (!item) {
23831
- throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
23931
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
23832
23932
  }
23833
23933
  const toStatus = options.toStatus ?? "Todo";
23834
23934
  if (item.contentType === "Issue") {
@@ -23846,8 +23946,9 @@ async function unclaimBoardIssue(options, deps = {}) {
23846
23946
  }
23847
23947
  }
23848
23948
  }
23949
+ const appWriter = await selectClaimWriteClient(client, deps.appClient);
23849
23950
  try {
23850
- await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
23951
+ await appWriter.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
23851
23952
  body: { assignees: item.assignees }
23852
23953
  });
23853
23954
  } catch (e) {
@@ -23864,7 +23965,7 @@ async function unclaimBoardIssue(options, deps = {}) {
23864
23965
  host: actor.host,
23865
23966
  ts: (/* @__PURE__ */ new Date()).toISOString()
23866
23967
  };
23867
- await postIssueComment(client, { ref: item.ref, body: renderClaimReleaseBody(release) });
23968
+ await postIssueComment(appWriter, { ref: item.ref, body: renderClaimReleaseBody(release) });
23868
23969
  } catch (e) {
23869
23970
  process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
23870
23971
  `);
@@ -27287,10 +27388,10 @@ var rollout_plan_default = {
27287
27388
  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
27389
  },
27289
27390
  baseline: {
27290
- version: "4.6.3",
27291
- tag: "v4.6.3",
27292
- commit: "602adde0ce4c",
27293
- npm: "@mutmutco/cli@4.6.3"
27391
+ version: "4.6.4",
27392
+ tag: "v4.6.4",
27393
+ commit: "db079140cdfb",
27394
+ npm: "@mutmutco/cli@4.6.4"
27294
27395
  },
27295
27396
  exitCriterion: "fleet-n-of-n",
27296
27397
  hubOnlyShortcut: "forbidden",
@@ -27307,14 +27408,14 @@ var rollout_plan_default = {
27307
27408
  repo: "mutmutco/mmi-hub",
27308
27409
  role: "canary",
27309
27410
  schedule: "train",
27310
- v3Target: "v4.6.3"
27411
+ v3Target: "v4.6.4"
27311
27412
  }
27312
27413
  ],
27313
27414
  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
27415
  rollback: {
27315
27416
  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)"
27417
+ mechanism: "npm dist-tag latest -> 4.6.4 and redeploy the Hub Lambda from tag v4.6.4 (db079140cdfb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27418
+ v3Target: "v4.6.4 (@mutmutco/cli@4.6.4, tag commit db079140cdfb \u2014 last known-good release carrying the repo-index v4-only contract)"
27318
27419
  }
27319
27420
  },
27320
27421
  {
@@ -28993,16 +29094,16 @@ async function writeFleetLockstepRuleset(client, body, liveId, target, org) {
28993
29094
  await client.rest("PUT", `orgs/${org}/rulesets/${liveId}`, { body, timeoutMs: 2e4 });
28994
29095
  }
28995
29096
  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;
29097
+ const settled2 = read.kind === "ok" ? read.ruleset : void 0;
29098
+ const shapeDrift = settled2 ? diffFleetLockstepShape(settled2) : [`ruleset '${FLEET_LOCKSTEP_RULESET_NAME}' absent after apply`];
29099
+ const includeSettled = settled2 ? sameSetCi(settled2.conditions?.repository_name?.include, body.conditions.repository_name.include) : false;
29100
+ const enforcementSettled = settled2?.enforcement === body.enforcement;
29000
29101
  if (shapeDrift.length || !includeSettled || !enforcementSettled) {
29001
29102
  throw new Error(`fleet lockstep ruleset apply did not settle: ${[...shapeDrift, includeSettled ? "" : "include list differs", enforcementSettled ? "" : "enforcement differs"].filter(Boolean).join("; ")}`);
29002
29103
  }
29003
29104
  return {
29004
29105
  action: liveId == null ? "created" : "updated",
29005
- id: settled?.id,
29106
+ id: settled2?.id,
29006
29107
  target,
29007
29108
  include: body.conditions.repository_name.include,
29008
29109
  verified: true
@@ -40015,7 +40116,7 @@ var PRIMARY_GROUPS = [
40015
40116
  ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
40016
40117
  ];
40017
40118
  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"]);
40119
+ var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read", "run"]);
40019
40120
  var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
40020
40121
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
40021
40122
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
@@ -40061,6 +40162,8 @@ var COMMAND_OWNERSHIP = {
40061
40162
  api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
40062
40163
  github: { module_owner: "cli/src/github-budget.ts", consumer: "release-operator" },
40063
40164
  read: { module_owner: "cli/src/app-read-broker.ts", consumer: "agent-workflow" },
40165
+ run: { module_owner: "cli/src/merge-group-orphans.ts", consumer: "agent-workflow" },
40166
+ // #9479
40064
40167
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
40065
40168
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
40066
40169
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -41025,14 +41128,23 @@ function parseScope(value) {
41025
41128
  function trailerBlockBreak(body, line) {
41026
41129
  const lines2 = String(body ?? "").split(/\r?\n/);
41027
41130
  const at = lines2.findIndex((candidate) => candidate.trim() === line);
41028
- if (at === -1) return "";
41131
+ if (at === -1) return { after: "", before: "" };
41029
41132
  for (let i = at + 1; i < lines2.length; i += 1) {
41030
41133
  const next = lines2[i];
41031
41134
  if (!next.trim()) continue;
41032
- if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return "";
41033
- return next.trim();
41135
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
41136
+ return { after: next.trim(), before: "" };
41034
41137
  }
41035
- return "";
41138
+ let paraStart = at;
41139
+ while (paraStart > 0 && lines2[paraStart - 1].trim() !== "") {
41140
+ paraStart -= 1;
41141
+ }
41142
+ for (let i = paraStart; i < at; i += 1) {
41143
+ const prev = lines2[i];
41144
+ if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(prev) || /^[ \t]+\S/.test(prev)) continue;
41145
+ return { after: "", before: prev.trim() };
41146
+ }
41147
+ return { after: "", before: "" };
41036
41148
  }
41037
41149
  function clipParagraph(line) {
41038
41150
  const flat = String(line).trim().replace(/\s+/g, " ");
@@ -41040,6 +41152,11 @@ function clipParagraph(line) {
41040
41152
  }
41041
41153
  function invisibleTrailer(sha, line, body = "") {
41042
41154
  const broken = trailerBlockBreak(body, line);
41155
+ const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
41156
+ ${clipParagraph(broken.after)}
41157
+ ` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
41158
+ ${clipParagraph(broken.before)}
41159
+ ` : " 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
41160
  return {
41044
41161
  kind: "malformed-override-trailer",
41045
41162
  paths: [],
@@ -41055,9 +41172,7 @@ function invisibleTrailer(sha, line, body = "") {
41055
41172
  The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
41056
41173
  Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
41057
41174
  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:
41175
+ ` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
41061
41176
  git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
41062
41177
  That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
41063
41178
  };
@@ -41595,6 +41710,31 @@ ${commit.body}`.trim();
41595
41710
  if (!overrides.length) return null;
41596
41711
  return [...bodies, [...overrides, ...coauthors].join("\n")].join("\n\n");
41597
41712
  }
41713
+ function prBodyTrailerSource(prBody) {
41714
+ const lines2 = prBody.split("\n");
41715
+ const separator = lines2.findIndex((line) => GH_MESSAGE_SEPARATOR.test(line.trim()));
41716
+ return separator === -1 ? prBody : lines2.slice(0, separator).join("\n");
41717
+ }
41718
+ function prBodyPreservesCommitOverrides(commits, headline, prBody, cwd) {
41719
+ const key = `${TRAILER_KEY.toLowerCase()}:`;
41720
+ const required = [];
41721
+ for (const commit of commits) {
41722
+ for (const trailer of parseTrailers(`${commit.headline}
41723
+
41724
+ ${commit.body}`.trim(), cwd)) {
41725
+ const normalized = trailer.toLowerCase();
41726
+ if (normalized.startsWith(key) && !required.includes(normalized)) required.push(normalized);
41727
+ }
41728
+ }
41729
+ if (!required.length) return true;
41730
+ const source = prBodyTrailerSource(prBody);
41731
+ const recognized = parseTrailers(`${headline}
41732
+
41733
+ ${source}`.trim(), cwd).map((trailer) => trailer.toLowerCase()).filter((trailer) => trailer.startsWith(key));
41734
+ const textual = (source.match(/^Test-Policy-Override:\s*\S/gim) ?? []).length;
41735
+ if (textual !== recognized.length) return false;
41736
+ return required.every((trailer) => recognized.includes(trailer));
41737
+ }
41598
41738
  function resolveBase(cwd, explicit) {
41599
41739
  const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
41600
41740
  for (const ref of candidates) {
@@ -41797,7 +41937,10 @@ async function hasPrMergeQueue(repo, base, gh2 = readApi) {
41797
41937
  if (result.mergeQueue === void 0) throw new Error(`merge queue policy unreadable for ${repo}:${base}`);
41798
41938
  return result.mergeQueue !== null;
41799
41939
  }
41800
- var writeApi = async (args) => (await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 })).stdout;
41940
+ var writeApi = async (args) => {
41941
+ const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
41942
+ return { stdout, credential };
41943
+ };
41801
41944
  var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
41802
41945
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
41803
41946
  function admissionJournal(repo, pr, root) {
@@ -41813,23 +41956,31 @@ function admissionJournal(repo, pr, root) {
41813
41956
  const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
41814
41957
  const validate = (value, previous) => {
41815
41958
  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)))) {
41959
+ if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
41817
41960
  throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
41818
41961
  }
41819
41962
  if (!previous) {
41820
41963
  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)) {
41964
+ } 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
41965
  throw new Error("queue admission receipt has an illegal transition or changed operation identity");
41823
41966
  }
41824
41967
  return row;
41825
41968
  };
41826
- const load = () => {
41969
+ const load = (number = attempt) => {
41827
41970
  let previous;
41828
- for (const line of (0, import_node_fs33.readFileSync)(path2(attempt), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
41971
+ for (const line of (0, import_node_fs33.readFileSync)(path2(number), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
41829
41972
  return previous;
41830
41973
  };
41831
41974
  return {
41832
41975
  receipt: attempt ? load() : void 0,
41976
+ /** #9504: the exact attempt this invocation selected, so a later attempt recorded at the same head can never supply its credential. */
41977
+ findByIntentId(intentId) {
41978
+ for (let number = 1; number <= attempt; number++) {
41979
+ const receipt = load(number);
41980
+ if (receipt.intentId === intentId) return receipt;
41981
+ }
41982
+ return void 0;
41983
+ },
41833
41984
  create(head) {
41834
41985
  const createdAt = Date.now();
41835
41986
  const initial = { version: 1, repo: repo.toLowerCase(), pr, head, intentId: (0, import_node_crypto10.randomUUID)(), createdAt, observedAt: createdAt, state: "dispatching" };
@@ -41841,9 +41992,9 @@ function admissionJournal(repo, pr, root) {
41841
41992
  }
41842
41993
  return true;
41843
41994
  },
41844
- save(state, uuid, queueEntry) {
41995
+ save(state, uuid, queueEntry, credential) {
41845
41996
  const previous = load();
41846
- const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {} }, previous);
41997
+ const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {}, ...credential && previous.credential === void 0 ? { credential } : {} }, previous);
41847
41998
  (0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
41848
41999
  load();
41849
42000
  }
@@ -41889,6 +42040,22 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41889
42040
  }
41890
42041
  return { mergeStatus: "auto-merge-enqueued" };
41891
42042
  }
42043
+ const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
42044
+ if (saved?.queueEntry && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
42045
+ const enqueuedAt = Date.parse(saved.queueEntry.enqueuedAt);
42046
+ const events = await readArrayPages(
42047
+ `repos/${repo}/issues/${pr}/timeline?per_page=100`,
42048
+ options.timelineRead ?? read,
42049
+ "queue ejection timeline unreadable"
42050
+ );
42051
+ const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
42052
+ const adds = times("added_to_merge_queue").filter((at) => at >= enqueuedAt);
42053
+ const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
42054
+ if (removals.length === 1) {
42055
+ journal.save("ejected");
42056
+ return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
42057
+ }
42058
+ }
41892
42059
  } catch {
41893
42060
  }
41894
42061
  return unknown(reason);
@@ -41909,24 +42076,6 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41909
42076
  } catch {
41910
42077
  }
41911
42078
  }
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
42079
  if (superseded && receipt?.state === "enqueued") {
41931
42080
  try {
41932
42081
  journal.save("ejected");
@@ -41935,6 +42084,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41935
42084
  }
41936
42085
  }
41937
42086
  if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
42087
+ if (options.selection) {
42088
+ options.selection.selected = true;
42089
+ options.selection.intentId = receipt.intentId;
42090
+ }
41938
42091
  if (receipt.state === "enqueued" || receipt.state === "merged") return reconcile(`reconcile-needed for ${repo}#${pr}, submitted head ${receipt.head}, operation ${receipt.uuid ?? "completed immediately"}: admission completed but current membership is absent; inspect the exact queue candidate/ejection before a new intent`, receipt.state === "enqueued");
41939
42092
  if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
41940
42093
  } else {
@@ -41943,12 +42096,17 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41943
42096
  if (!journal.create(head)) return submitQueueAdmission(pr, repo, head, read, write, options, true);
41944
42097
  journal = admissionJournal(repo, pr, options.receiptDirectory);
41945
42098
  receipt = journal.receipt;
42099
+ if (options.selection) {
42100
+ options.selection.selected = true;
42101
+ options.selection.intentId = receipt.intentId;
42102
+ }
41946
42103
  } catch {
41947
42104
  return unknown("the pre-send checkpoint failed; no request was dispatched");
41948
42105
  }
41949
42106
  let output;
42107
+ let credential;
41950
42108
  try {
41951
- output = await write([
42109
+ const written = await write([
41952
42110
  `repos/${repo}/pulls/${pr}/merge-async`,
41953
42111
  "--method",
41954
42112
  "PUT",
@@ -41961,8 +42119,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41961
42119
  "-f",
41962
42120
  `sha=${head}`
41963
42121
  ]);
42122
+ ({ stdout: output, credential } = typeof written === "string" ? { stdout: written, credential: void 0 } : written);
41964
42123
  } catch (error) {
41965
42124
  output = error.stdout ?? "";
42125
+ credential = error.credential;
41966
42126
  }
41967
42127
  try {
41968
42128
  response = admissionResponse(output);
@@ -41972,7 +42132,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41972
42132
  const { code, body } = response;
41973
42133
  if (code === 200 && ["enqueued", "merged"].includes(body.status ?? "")) {
41974
42134
  try {
41975
- journal.save(body.status);
42135
+ journal.save(body.status, void 0, void 0, credential);
41976
42136
  } catch {
41977
42137
  return reconcile("the completed operation could not be checkpointed");
41978
42138
  }
@@ -41980,7 +42140,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41980
42140
  }
41981
42141
  if (code === 400 || code === 403 || code === 404 || code === 422) {
41982
42142
  try {
41983
- journal.save("failed");
42143
+ journal.save("failed", void 0, void 0, credential);
41984
42144
  } catch {
41985
42145
  return unknown("the explicit rejection could not be checkpointed");
41986
42146
  }
@@ -41992,7 +42152,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
41992
42152
  return unknown("an existing operation has different or missing queue/head/no-bypass options; inspect that operation before continuing");
41993
42153
  }
41994
42154
  try {
41995
- journal.save("pending", details.uuid);
42155
+ journal.save("pending", details.uuid, void 0, credential);
41996
42156
  } catch {
41997
42157
  return reconcile("the accepted operation UUID could not be checkpointed");
41998
42158
  }
@@ -42047,8 +42207,12 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
42047
42207
  if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
42048
42208
  const head = options.expectedHeadOid ?? pull.headRefOid;
42049
42209
  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" };
42210
+ const stamp = (result2, selected = false, intentId) => {
42211
+ const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
42212
+ return credential === "unknown" ? result2 : { ...result2, credential };
42213
+ };
42214
+ if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
42215
+ if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
42052
42216
  if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
42053
42217
  const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
42054
42218
  let page = pull.closingIssuesReferences.pageInfo;
@@ -42144,7 +42308,20 @@ ${pull.body}`;
42144
42308
  return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
42145
42309
  }
42146
42310
  const requiredBody = options.bodyText ?? composedBody;
42147
- if (requiredBody && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42311
+ const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
42312
+ if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
42313
+ if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
42314
+ return {
42315
+ mergeStatus: "failed",
42316
+ preparation: {
42317
+ status: "preparation-needed",
42318
+ reason: "queue-message-override",
42319
+ issueTargets: [],
42320
+ 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."
42321
+ },
42322
+ 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"
42323
+ };
42324
+ }
42148
42325
  return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
42149
42326
  }
42150
42327
  const current = await queueGraphql(
@@ -42155,8 +42332,9 @@ ${pull.body}`;
42155
42332
  if (current.pullRequest.headRefOid !== head || current.pullRequest.title !== pull.title || current.pullRequest.body !== pull.body) {
42156
42333
  return { mergeStatus: "failed", retryable: false, error: "queue source or PR text changed during preparation; rerun pr land against the new revision" };
42157
42334
  }
42158
- const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read });
42159
- return { ...result, ...verdict.message ? { warning: verdict.message } : {} };
42335
+ const selection = { selected: false };
42336
+ const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
42337
+ return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
42160
42338
  }
42161
42339
  function decideQueueMergeRoute(hasQueue, method, headRef) {
42162
42340
  if (!hasQueue) return "normal";
@@ -42194,8 +42372,50 @@ function prMergeQueueLine(result) {
42194
42372
  return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
42195
42373
  }
42196
42374
  }
42375
+ function admissionCredential(repo, pr, head, receiptDirectory, selected = false, intentId) {
42376
+ try {
42377
+ const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
42378
+ const saved = intentId ? journal.findByIntentId(intentId) : journal.receipt;
42379
+ const live = saved && ["pending", "enqueued", "merged"].includes(saved.state);
42380
+ return saved && saved.head.toLowerCase() === head.toLowerCase() && (selected || live) ? saved.credential ?? "unknown" : "unknown";
42381
+ } catch {
42382
+ return "unknown";
42383
+ }
42384
+ }
42385
+ function observeQueuedAdmission(repo, pr, snapshot, receiptDirectory) {
42386
+ try {
42387
+ const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
42388
+ const saved = journal.receipt;
42389
+ if (!saved || !["pending", "enqueued", "merged"].includes(saved.state) || snapshot.headRefOid?.toLowerCase() !== saved.head.toLowerCase()) return {};
42390
+ const entry = snapshot.mergeQueueEntry;
42391
+ if (saved.queueEntry) {
42392
+ if (entry?.enqueuedAt && entry.enqueuedAt !== saved.queueEntry.enqueuedAt) return { mismatched: true };
42393
+ } else if ((saved.state === "enqueued" || saved.state === "pending") && snapshot.isInMergeQueue && entry?.headCommit && entry.enqueuedAt) {
42394
+ try {
42395
+ journal.save("enqueued", saved.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
42396
+ } catch {
42397
+ return {};
42398
+ }
42399
+ }
42400
+ return saved.credential ? { credential: saved.credential } : {};
42401
+ } catch {
42402
+ return {};
42403
+ }
42404
+ }
42197
42405
 
42198
42406
  // src/merge-group-orphans.ts
42407
+ var SETTLE_POLLS = 6;
42408
+ var SETTLE_MS = 5e3;
42409
+ async function settled(repo, id, deps) {
42410
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
42411
+ let status = "unknown";
42412
+ for (let poll = 0; poll < SETTLE_POLLS; poll++) {
42413
+ await sleep2(SETTLE_MS);
42414
+ status = JSON.parse(await deps.read([`repos/${repo}/actions/runs/${id}`])).status ?? "unknown";
42415
+ if (status === "completed") break;
42416
+ }
42417
+ return status;
42418
+ }
42199
42419
  async function isOrphan(run, repo, deps) {
42200
42420
  const match = /^gh-readonly-queue\/(.+)\/pr-([1-9][0-9]*)-([a-f0-9]+)$/i.exec(run.head_branch);
42201
42421
  if (!match || run.event !== "merge_group") return false;
@@ -42215,7 +42435,7 @@ async function isOrphan(run, repo, deps) {
42215
42435
  }
42216
42436
  }
42217
42437
  async function cancelMergeGroupOrphans(repo, apply, deps) {
42218
- const report = { repo, apply, candidates: [], cancelled: [], errors: [] };
42438
+ const report = { repo, apply, candidates: [], cancelled: [], forceRequested: [], forceCancelled: [], errors: [] };
42219
42439
  try {
42220
42440
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("expected --repo owner/repo");
42221
42441
  const runs2 = [];
@@ -42237,7 +42457,18 @@ async function cancelMergeGroupOrphans(repo, apply, deps) {
42237
42457
  for (const run of confirmed) {
42238
42458
  if (!await isOrphan(run, repo, deps)) continue;
42239
42459
  await deps.cancel(repo, run.id);
42240
- report.cancelled.push(run.id);
42460
+ let status = await settled(repo, run.id, deps);
42461
+ let forced = false;
42462
+ if (status !== "completed" && await isOrphan(run, repo, deps)) {
42463
+ await deps.forceCancel(repo, run.id);
42464
+ report.forceRequested.push(run.id);
42465
+ forced = true;
42466
+ status = await settled(repo, run.id, deps);
42467
+ }
42468
+ if (status === "completed") {
42469
+ report.cancelled.push(run.id);
42470
+ if (forced) report.forceCancelled.push(run.id);
42471
+ } else report.errors.push(`cancel-orphans: run ${run.id} still ${status} after cancel${forced ? " and force-cancel" : ""}`);
42241
42472
  }
42242
42473
  }
42243
42474
  } catch (error) {
@@ -42263,17 +42494,59 @@ function registerCancelOrphans(ci2, writeToken) {
42263
42494
  timeout: 2e4,
42264
42495
  env: { ...process.env, GH_TOKEN: token }
42265
42496
  });
42497
+ },
42498
+ forceCancel: async (repo, id) => {
42499
+ token ??= await writeToken();
42500
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${id}/force-cancel`, "--method", "POST"], {
42501
+ timeout: 2e4,
42502
+ env: { ...process.env, GH_TOKEN: token }
42503
+ });
42266
42504
  }
42267
42505
  });
42268
42506
  if (options.json) console.log(JSON.stringify(report, null, 2));
42269
42507
  else {
42270
42508
  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)"}`);
42509
+ 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
42510
  for (const error of report.errors) console.error(error);
42273
42511
  }
42274
42512
  if (report.errors.length) process.exitCode = 1;
42275
42513
  });
42276
42514
  }
42515
+ var RUN_CANCEL_STATUS = "cancel-requested";
42516
+ async function cancelWorkflowRun(repo, id, deps) {
42517
+ const raw = String(id).trim();
42518
+ if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
42519
+ const runId = Number(raw);
42520
+ if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
42521
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
42522
+ try {
42523
+ await deps.cancel(repo, runId);
42524
+ } catch (error) {
42525
+ const detail = String(error.stderr ?? error);
42526
+ throw new Error(/\b403\b/.test(detail) ? "run cancel: GitHub App denied (HTTP 403); actions:write permission is required; no personal-token fallback" : `run cancel: ${detail}`);
42527
+ }
42528
+ return { ok: true, repo, runId, status: RUN_CANCEL_STATUS };
42529
+ }
42530
+ function registerRunCancel(run, writeToken) {
42531
+ run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--json", "machine-readable receipt").action(async (id, options) => {
42532
+ let receipt;
42533
+ try {
42534
+ receipt = await cancelWorkflowRun(options.repo, id, {
42535
+ cancel: async (repo, runId) => {
42536
+ const token = await writeToken();
42537
+ await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
42538
+ timeout: 2e4,
42539
+ env: { ...process.env, GH_TOKEN: token }
42540
+ });
42541
+ }
42542
+ });
42543
+ } catch (error) {
42544
+ return fail(error.message);
42545
+ }
42546
+ if (options.json) console.log(JSON.stringify(receipt));
42547
+ else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 cancel requested (status: ${receipt.status})`);
42548
+ });
42549
+ }
42277
42550
 
42278
42551
  // src/board-commands.ts
42279
42552
  init_cli_shared();
@@ -46353,13 +46626,14 @@ function filledDocCheck(label, text, path2) {
46353
46626
  const unfilled = unfilledDocPlaceholders(text);
46354
46627
  return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
46355
46628
  }
46356
- var NPMRC_PATHS = [".npmrc", "web/.npmrc"];
46629
+ var NPMRC_PATHS = [".npmrc", "module/.npmrc", "web/.npmrc"];
46357
46630
  var SCOPED_GITHUB_PACKAGES_RE = /^\s*@[A-Za-z0-9-]+:registry\s*=\s*https:\/\/npm\.pkg\.github\.com/m;
46358
46631
  var GITHUB_PACKAGES_AUTH_RE = /^\s*\/\/npm\.pkg\.github\.com\/:_authToken\s*=(.*)$/m;
46359
46632
  function npmrcAuthValueIsPlaceholder(value) {
46360
46633
  const trimmed = value.trim();
46361
46634
  return trimmed === "" || /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(trimmed);
46362
46635
  }
46636
+ 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
46637
  function npmrcGitHubPackagesAuthCheck(path2, text) {
46364
46638
  if (text === null || !SCOPED_GITHUB_PACKAGES_RE.test(text)) return null;
46365
46639
  const label = `${path2} authenticates GitHub Packages`;
@@ -46368,7 +46642,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
46368
46642
  return {
46369
46643
  ok: false,
46370
46644
  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`
46645
+ 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
46646
  };
46373
46647
  }
46374
46648
  if (npmrcAuthValueIsPlaceholder(match[1])) {
@@ -46376,7 +46650,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
46376
46650
  return {
46377
46651
  ok: false,
46378
46652
  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)`
46653
+ 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
46654
  };
46381
46655
  }
46382
46656
  return { ok: true, label };
@@ -46564,7 +46838,15 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
46564
46838
  });
46565
46839
  }
46566
46840
  }
46567
- for (const path2 of NPMRC_PATHS) {
46841
+ const tree = await restJson3(
46842
+ deps,
46843
+ `repos/${repo}/git/trees/${encodeURIComponent(baseBranch)}?recursive=1`,
46844
+ {}
46845
+ );
46846
+ const discoveredNpmrcPaths = (tree.tree ?? []).filter(
46847
+ (entry) => entry.type === "blob" && typeof entry.path === "string" && (entry.path === ".npmrc" || entry.path.endsWith("/.npmrc"))
46848
+ ).map((entry) => entry.path);
46849
+ for (const path2 of /* @__PURE__ */ new Set([...NPMRC_PATHS, ...discoveredNpmrcPaths])) {
46568
46850
  const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
46569
46851
  if (check) checks.push(check);
46570
46852
  }
@@ -51778,7 +52060,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
51778
52060
  if (now() >= deadline) break;
51779
52061
  const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
51780
52062
  if (checks.status !== "success" && checks.status !== "skipped") {
51781
- return { mergeStatus: "failed", error: `${result.error ?? "merge failed"}
52063
+ return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
51782
52064
  registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
51783
52065
  }
51784
52066
  if (now() >= deadline) break;
@@ -51924,6 +52206,8 @@ async function runPrLand(prNumber, options, deps) {
51924
52206
  if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
51925
52207
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
51926
52208
  base.mergeStatus = merge.mergeStatus;
52209
+ if (merge.credential) base.credential = merge.credential;
52210
+ if (merge.credentialRole) base.credentialRole = merge.credentialRole;
51927
52211
  if (merge.mergeStatus === "queue-admission-pending") {
51928
52212
  return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
51929
52213
  }
@@ -51985,9 +52269,19 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
51985
52269
  const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
51986
52270
  const deadline = deadlineMs ?? now() + timeoutMs;
51987
52271
  let attempt = 0;
52272
+ let credentialMismatched = false;
52273
+ const observe = (read) => {
52274
+ const seen = deps.observeQueued?.(pr, base.repo, read);
52275
+ if (seen?.mismatched) {
52276
+ credentialMismatched = true;
52277
+ delete base.credential;
52278
+ }
52279
+ if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
52280
+ };
51988
52281
  while (now() < deadline) {
51989
52282
  const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
51990
52283
  snapshot = void 0;
52284
+ if (read.isInMergeQueue || read.state === "MERGED") observe(read);
51991
52285
  if (read.state === "MERGED") return queueMerged(base, deps);
51992
52286
  if (read.isInMergeQueue) {
51993
52287
  base.mergeQueue = {
@@ -52000,7 +52294,10 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
52000
52294
  } else if (read.state === "OPEN" && !read.autoMergeRequest) {
52001
52295
  await sleep2(PR_LAND_EJECT_CONFIRM_MS);
52002
52296
  const again = await deps.readMergeQueue(pr, base.repo);
52003
- if (again.state === "MERGED") return queueMerged(base, deps);
52297
+ if (again.state === "MERGED") {
52298
+ observe(again);
52299
+ return queueMerged(base, deps);
52300
+ }
52004
52301
  if (again.isInMergeQueue || again.autoMergeRequest) {
52005
52302
  snapshot = again;
52006
52303
  continue;
@@ -52055,6 +52352,10 @@ function renderPrLandTable(entries) {
52055
52352
  const width = (i) => Math.max(...rows.map((r) => r[i].length));
52056
52353
  return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
52057
52354
  }
52355
+ function landCredentials(results) {
52356
+ const each = results.map((r) => r.credential ?? "unknown");
52357
+ return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
52358
+ }
52058
52359
 
52059
52360
  // src/git-worktree-observation.ts
52060
52361
  function parseGitWorktreePorcelain(text) {
@@ -52255,6 +52556,12 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).filter((line) => li
52255
52556
  var defaultGhMergeAutoIo = {
52256
52557
  gh: async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout
52257
52558
  };
52559
+ var credentialedGh = {
52560
+ gh: async (args, timeoutMs) => {
52561
+ const { stdout, credential } = await execFileP("gh", args, { timeout: timeoutMs });
52562
+ return { stdout, credential };
52563
+ }
52564
+ };
52258
52565
  function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
52259
52566
  const policy = squashBodyWithOverride(commits, cwd);
52260
52567
  const base = policy ?? defaultSquashBodyFromCommits(commits);
@@ -52316,14 +52623,34 @@ async function ghAdminTrueMergeWhenClean(prNumber, repo, opts = {}) {
52316
52623
  return { mergeStatus: "merged" };
52317
52624
  }
52318
52625
  async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
52319
- const io = extra?.io ?? defaultGhMergeAutoIo;
52626
+ let credential;
52627
+ let refusedAsMerged = false;
52628
+ const source = extra?.io ?? credentialedGh;
52629
+ const note = (args2, value) => {
52630
+ if (args2[0] === "pr" && args2[1] === "merge" && value) credential = value;
52631
+ };
52632
+ const io = {
52633
+ gh: async (ghArgs, timeoutMs) => {
52634
+ try {
52635
+ const out = await source.gh(ghArgs, timeoutMs);
52636
+ if (typeof out === "string") return out;
52637
+ note(ghArgs, out.credential);
52638
+ return out.stdout;
52639
+ } catch (e) {
52640
+ note(ghArgs, e?.credential);
52641
+ if (ghArgs[0] === "pr" && ghArgs[1] === "merge" && /already been merged/i.test(String(e?.message ?? ""))) refusedAsMerged = true;
52642
+ throw e;
52643
+ }
52644
+ }
52645
+ };
52320
52646
  const args = repo ? ["--repo", repo] : [];
52321
52647
  const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
52322
52648
  allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
52323
52649
  bodyText: method === "--squash" ? extra?.bodyText : void 0
52324
52650
  });
52325
52651
  try {
52326
- return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
52652
+ const result = await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
52653
+ return credential ? { ...result, credential, ...refusedAsMerged && result.mergeStatus === "merged" ? { credentialRole: "attempted-write" } : {} } : result;
52327
52654
  } finally {
52328
52655
  overrideBody?.cleanup();
52329
52656
  }
@@ -55727,7 +56054,7 @@ async function observePrCreateClaims(body, repoOption, deps = {}) {
55727
56054
  if (findings.length) throw new Error("pr create: claim findings must be resolved before restamping");
55728
56055
  if (attempted) throw new Error("pr create: claim restamp already attempted; observe again before retrying");
55729
56056
  attempted = true;
55730
- const writer = deps.client ?? defaultGitHubClient();
56057
+ const writer = await selectClaimWriteClient(deps.client ?? defaultGitHubClient(), deps.appClient);
55731
56058
  for (const number of restamps) {
55732
56059
  await writer.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } });
55733
56060
  }
@@ -55743,11 +56070,11 @@ function catFileBatch(root, ref, paths) {
55743
56070
  if (paths.length === 0) return Promise.resolve([]);
55744
56071
  return new Promise((resolve7) => {
55745
56072
  const chunks = [];
55746
- let settled = false;
56073
+ let settled2 = false;
55747
56074
  const child2 = (0, import_node_child_process15.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
55748
56075
  const finish = (ok = false) => {
55749
- if (settled) return;
55750
- settled = true;
56076
+ if (settled2) return;
56077
+ settled2 = true;
55751
56078
  clearTimeout(timer);
55752
56079
  resolve7(ok ? parseCatFileBatch(Buffer.concat(chunks), paths.length) : []);
55753
56080
  };
@@ -55913,6 +56240,21 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
55913
56240
  }
55914
56241
  };
55915
56242
  }
56243
+ async function retryPrCreateIdentityDrift(input, deps, notify = (line) => process.stderr.write(line)) {
56244
+ 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");
56245
+ const preparation = await preparePrCreate(input, deps);
56246
+ if (preparation.findings.length) {
56247
+ 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("; ")}`) };
56248
+ }
56249
+ try {
56250
+ await preparation.commit();
56251
+ return { preparation };
56252
+ } catch (error) {
56253
+ const message2 = error.message;
56254
+ if (!message2.includes("identity changed during preparation")) return { preparation, error };
56255
+ 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.`) };
56256
+ }
56257
+ }
55916
56258
  function createPreparationDeps(input, io = {}) {
55917
56259
  const exec3 = io.exec ?? execFileP;
55918
56260
  const git3 = async (args) => (await exec3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout.trim();
@@ -56196,10 +56538,10 @@ function healthPollIntervalMs() {
56196
56538
  }
56197
56539
  function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
56198
56540
  return new Promise((resolve7, reject2) => {
56199
- let settled = false;
56541
+ let settled2 = false;
56200
56542
  const finish = (fn) => {
56201
- if (settled) return;
56202
- settled = true;
56543
+ if (settled2) return;
56544
+ settled2 = true;
56203
56545
  clearTimeout(timer);
56204
56546
  child2.removeAllListeners("error");
56205
56547
  child2.removeAllListeners("exit");
@@ -58258,7 +58600,15 @@ function registerCollaborationCommands(program3) {
58258
58600
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
58259
58601
  }
58260
58602
  });
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) => {
58603
+ 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) => {
58604
+ const editShape = "--edit <commentId> --replace <text> --with <text> (without --body/--body-file)";
58605
+ if (o.edit !== void 0) {
58606
+ if (o.replace === void 0 || o.with === void 0 || o.body !== void 0 || o.bodyFile !== void 0 || o.replace.length === 0) {
58607
+ return fail(`issue comment: use ${editShape}; --replace must not be empty`);
58608
+ }
58609
+ } else if (o.replace !== void 0 || o.with !== void 0) {
58610
+ return fail(`issue comment: --replace/--with require ${editShape}`);
58611
+ }
58262
58612
  let parsed;
58263
58613
  try {
58264
58614
  parsed = parseIssueRef(ref, o.repo);
@@ -58267,14 +58617,14 @@ function registerCollaborationCommands(program3) {
58267
58617
  }
58268
58618
  let body;
58269
58619
  try {
58270
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
58620
+ body = o.edit !== void 0 ? "" : await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
58271
58621
  } catch (e) {
58272
58622
  return fail(`issue comment: ${e.message}`);
58273
58623
  }
58274
58624
  const repo = await resolveRepo(parsed.repo ?? o.repo);
58275
58625
  if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
58276
58626
  try {
58277
- const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
58627
+ 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
58628
  console.log(JSON.stringify(result));
58279
58629
  } catch (e) {
58280
58630
  const err = e;
@@ -58483,7 +58833,7 @@ ${list}`);
58483
58833
  title = safeTitle;
58484
58834
  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
58835
  }
58486
- const preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
58836
+ let preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
58487
58837
  if (preparation.findings.length) return fail(`pr create: preparation found ${preparation.findings.length} blocker(s):
58488
58838
  ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("\n")}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
58489
58839
  for (const observation of preparation.observations.filter((item) => item.state === "skipped")) {
@@ -58497,7 +58847,16 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
58497
58847
  try {
58498
58848
  await preparation.commit();
58499
58849
  } catch (error) {
58500
- return fail(`pr create: preparation commit failed \u2014 ${error.message}`);
58850
+ const message2 = error.message;
58851
+ if (!message2.includes("identity changed during preparation")) return fail(`pr create: preparation commit failed \u2014 ${message2}`);
58852
+ 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 }));
58853
+ 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 });
58854
+ preparation = retry.preparation;
58855
+ if (preparation.candidate?.head !== createHead) {
58856
+ const retriedHeadWarning = await prCreateMergedHeadWarning({ head: preparation.candidate?.head || void 0, repo: o.repo });
58857
+ if (retriedHeadWarning) process.stderr.write(`${retriedHeadWarning}
58858
+ `);
58859
+ }
58501
58860
  }
58502
58861
  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
58862
  if (isGhCreateRateLimited(created)) {
@@ -58859,6 +59218,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
58859
59218
  return receipt;
58860
59219
  },
58861
59220
  readMergeQueue: readPrMergeQueue,
59221
+ observeQueued: (prNumber, repo, snapshot) => observeQueuedAdmission(repo, prNumber, snapshot),
58862
59222
  findMergeGroupRun: findPrMergeGroupRun,
58863
59223
  queueProgress: (receipt) => {
58864
59224
  const line = prMergeQueueLine(receipt);
@@ -59047,7 +59407,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59047
59407
  inFlight = Math.max(1, inFlight - 1);
59048
59408
  }
59049
59409
  });
59050
- const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
59410
+ const { each: credentials, overall: credential } = landCredentials(entries.map((e) => e.result));
59051
59411
  if (numbers.length === 1) {
59052
59412
  const { result, cleanup } = entries[0];
59053
59413
  if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
@@ -59058,7 +59418,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
59058
59418
  if (cleanup) printLine(`pr land: ${cleanup}`);
59059
59419
  }
59060
59420
  } 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 }));
59421
+ 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
59422
  } else {
59063
59423
  printLine(renderPrLandTable(entries));
59064
59424
  printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
@@ -64808,8 +65168,7 @@ function parseBrokerPrState(raw) {
64808
65168
  }
64809
65169
  function buildPrListSpec(o) {
64810
65170
  const repo = parseBrokerRepo(o.repo, "--repo");
64811
- const fields = parseBrokerJsonFields(o.json);
64812
- rejectPrViewQueueFields(fields, "<number>", repo, "list");
65171
+ const fields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), "<number>", repo, "list");
64813
65172
  const args = withJson(withRepo(["pr", "list"], repo), fields, o.json);
64814
65173
  const state = parseBrokerPrState(o.state);
64815
65174
  if (state) args.push("--state", state);
@@ -64876,18 +65235,24 @@ function buildRunWatchSpec(id, o) {
64876
65235
  }
64877
65236
  function rejectPrViewQueueFields(fields, number, repo, verb = "view") {
64878
65237
  const badField = fields?.split(",").find((field) => field === "mergeQueueEntry" || field === "isInMergeQueue");
64879
- if (!badField) return;
65238
+ if (!badField) return fields;
64880
65239
  const targetRepo2 = repo ?? "<owner/repo>";
64881
65240
  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}\`` : ""}`
65241
+ if (!remaining) {
65242
+ throw new Error(
65243
+ `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`
65244
+ );
65245
+ }
65246
+ process.stderr.write(
65247
+ `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\`
65248
+ `
64884
65249
  );
65250
+ return remaining;
64885
65251
  }
64886
65252
  function buildPrViewSpec(ref, o) {
64887
65253
  const number = parseBrokerId(ref, "PR number");
64888
- const jsonFields = parseBrokerJsonFields(o.json);
64889
65254
  const repo = parseBrokerRepo(o.repo, "--repo");
64890
- rejectPrViewQueueFields(jsonFields, number, repo);
65255
+ const jsonFields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), number, repo);
64891
65256
  const args = withJson(withRepo(["pr", "view", number], repo), jsonFields, o.json);
64892
65257
  if (o.comments) args.push("--comments");
64893
65258
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
@@ -64961,7 +65326,34 @@ function ghExitCodeOf(error) {
64961
65326
  const code = error.code;
64962
65327
  return typeof code === "number" ? code : null;
64963
65328
  }
64964
- function brokerGhFailureHint(failureText, args) {
65329
+ var RUN_LIST_RECOVERY_FLAGS = ["--repo", "--branch", "--commit", "--event", "--workflow", "--status", "--created", "--limit"];
65330
+ function shellWord(value) {
65331
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
65332
+ }
65333
+ function formatBrokerRunListRecovery(spec, jq) {
65334
+ const tokens2 = spec.args.slice(2);
65335
+ const valueOf = (flag) => {
65336
+ const index = tokens2.indexOf(flag);
65337
+ if (index === -1) return void 0;
65338
+ const next = tokens2[index + 1];
65339
+ return next === void 0 || next.startsWith("-") ? void 0 : next;
65340
+ };
65341
+ const words = ["MMI_ACTOR=app", "mmi-cli", "devops", "read", "run", "list"];
65342
+ const repo = valueOf("--repo");
65343
+ words.push("--repo", repo === void 0 ? "<owner/repo>" : shellWord(repo));
65344
+ for (const flag of RUN_LIST_RECOVERY_FLAGS.slice(1)) {
65345
+ const value = valueOf(flag);
65346
+ if (value !== void 0) words.push(flag, shellWord(value));
65347
+ }
65348
+ if (tokens2.includes("--json")) {
65349
+ const fields = valueOf("--json");
65350
+ words.push("--json");
65351
+ if (fields !== void 0) words.push(shellWord(fields));
65352
+ }
65353
+ if (jq !== void 0) words.push("--jq", `'${jq.replace(/'/g, `'\\''`)}'`);
65354
+ 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`;
65355
+ }
65356
+ function brokerGhFailureHint(failureText, args, jq) {
64965
65357
  const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
64966
65358
  const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
64967
65359
  if (unknownField === "mergeQueueEntry" || unknownField === "isInMergeQueue") {
@@ -64973,9 +65365,12 @@ function brokerGhFailureHint(failureText, args) {
64973
65365
  if (/Could not resolve to a PullRequest/i.test(failureText)) {
64974
65366
  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
65367
  }
65368
+ if (args[0] === "run" && args[1] === "list") {
65369
+ return formatBrokerRunListRecovery({ program: "gh", args: [...args], timeoutMs: SINGLE_SHOT_TIMEOUT_MS }, jq);
65370
+ }
64976
65371
  return void 0;
64977
65372
  }
64978
- async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
65373
+ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk), jq) {
64979
65374
  const env = brokerChildEnv();
64980
65375
  let output;
64981
65376
  try {
@@ -64987,7 +65382,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
64987
65382
  if (code !== null) {
64988
65383
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
64989
65384
  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";
65385
+ const recovery = brokerGhFailureHint(failure, spec.args, jq) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
64991
65386
  process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 ${recovery}
64992
65387
  `);
64993
65388
  return code;
@@ -64998,7 +65393,8 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
64998
65393
  try {
64999
65394
  JSON.parse(output);
65000
65395
  } 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
65396
+ 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";
65397
+ process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 ${recovery}
65002
65398
  `);
65003
65399
  return 1;
65004
65400
  }
@@ -65080,13 +65476,13 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
65080
65476
  requireBrokerApp(label);
65081
65477
  try {
65082
65478
  if (jq === void 0) {
65083
- process.exitCode = await runBrokerGh(ghRun, spec);
65479
+ process.exitCode = await runBrokerGh(ghRun, spec, void 0, void 0);
65084
65480
  return;
65085
65481
  }
65086
65482
  let out = "";
65087
65483
  const code = await runBrokerGh(ghRun, spec, (chunk) => {
65088
65484
  out += chunk;
65089
- });
65485
+ }, jq);
65090
65486
  if (code === 0) await emitJson(out, jq);
65091
65487
  process.exitCode = code;
65092
65488
  } catch (e) {
@@ -65309,7 +65705,7 @@ function typedReadHint(path2) {
65309
65705
  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
65706
  }
65311
65707
  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");
65708
+ 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
65709
  }
65314
65710
  return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json title,body,state,labels,comments`);
65315
65711
  }
@@ -65395,14 +65791,16 @@ async function readAppApi(path2, runner = apiReadRunner) {
65395
65791
  function registerApiReadCommands(program3, overrides) {
65396
65792
  const spawn7 = overrides?.runner ?? apiReadRunner;
65397
65793
  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) => {
65794
+ const runGet = async (req, o) => {
65403
65795
  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
65796
  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}`);
65797
+ if (zip !== (o.out !== void 0)) {
65798
+ 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}";
65799
+ if (!zip && o.out !== void 0) {
65800
+ 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.`);
65801
+ }
65802
+ return fail(shapes);
65803
+ }
65406
65804
  const appRoute = req.kind.startsWith("actions-") || req.kind.startsWith("release-");
65407
65805
  if (appRoute) requireBrokerApp("api get");
65408
65806
  const env = appRoute || process.env.MMI_ACTOR?.trim() !== "personal" ? brokerChildEnv() : void 0;
@@ -65423,6 +65821,28 @@ function registerApiReadCommands(program3, overrides) {
65423
65821
  return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
65424
65822
  }
65425
65823
  process.stdout.write(body);
65824
+ };
65825
+ const withApiGetOptions = (cmd) => cmd.option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
65826
+ acceptHeaderFor(v);
65827
+ return v.trim().toLowerCase();
65828
+ }, "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)");
65829
+ const api = withApiGetOptions(
65830
+ program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)")
65831
+ ).argument("[path]", "allowlisted GET path \u2014 shorthand for `api get <path>` (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, _o, cmd) => {
65832
+ if (req === void 0) {
65833
+ 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}.`);
65834
+ }
65835
+ return runGet(req, cmd.opts());
65836
+ });
65837
+ withApiGetOptions(
65838
+ 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")
65839
+ ).argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o, cmd) => {
65840
+ const parentOpts = cmd.parent?.opts() ?? {};
65841
+ return runGet(req, {
65842
+ accept: o.accept && o.accept !== "json" ? o.accept : parentOpts.accept ?? o.accept ?? "json",
65843
+ jq: o.jq ?? parentOpts.jq,
65844
+ out: o.out ?? parentOpts.out
65845
+ });
65426
65846
  });
65427
65847
  }
65428
65848
 
@@ -69029,6 +69449,7 @@ setGhWriteTokenProvider(() => {
69029
69449
  }
69030
69450
  return filingToken ??= mintFilingWriteToken(appActorDeps()).then((t) => t.token, () => void 0);
69031
69451
  });
69452
+ setClaimWriteTokenProvider(() => mintFilingWriteToken(appActorDeps()).then((t) => t.token));
69032
69453
  setStatusWriteTokenProvider(() => mintStatusWriteToken(appActorDeps()));
69033
69454
  var registerPreAction = Reflect.get(program2, ["h", "ook"].join(""));
69034
69455
  registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand) => {
@@ -69871,6 +70292,10 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
69871
70292
  });
69872
70293
  registerCiChecksWait(ci);
69873
70294
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
70295
+ registerRunCancel(
70296
+ program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
70297
+ async () => (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
70298
+ );
69874
70299
  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
70300
  if (o.apply && o.parkRuleset) {
69876
70301
  return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");