@mutmutco/cli 4.5.209 → 4.5.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +420 -306
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6365,21 +6365,177 @@ function splitInlineSelectors(argv, flags) {
6365
6365
  }
6366
6366
  return out;
6367
6367
  }
6368
+ var BOARD_MOVE_OWN_FLAGS = ["--repo", "--allow-partial", "--json"];
6369
+ var BOARD_SHOW_OWN_FLAGS = ["--json", "--repo", "--allow-partial", "--out"];
6370
+ var BOARD_READ_OWN_FLAGS = [
6371
+ "--json",
6372
+ "--repo",
6373
+ "--direct",
6374
+ "--snapshot-only",
6375
+ "--min-cursor",
6376
+ "--bundle-details",
6377
+ "--bodies",
6378
+ "--allow-partial",
6379
+ "--out"
6380
+ ];
6381
+ var BOARD_OTHER_OWN_FLAGS = {
6382
+ unclaim: ["--json", "--repo", "--to-status", "--force", "--holder-dead", "--allow-partial"],
6383
+ attach: ["--repo", "--json"],
6384
+ "set-priority": ["--json", "--repo"],
6385
+ "archive-done": ["--json", "--repo", "--apply"],
6386
+ doctor: ["--json", "--repo", "--fix", "--concurrency"],
6387
+ events: ["--since", "--wait", "--repo", "--json"]
6388
+ };
6389
+ var BOARD_TARGET_PLACEHOLDER = {
6390
+ unclaim: "<issue>",
6391
+ attach: "<issues>",
6392
+ "set-priority": "<issue> <priority>"
6393
+ };
6394
+ var WORKBOARD_VERB_ALIASES = {
6395
+ claim: "claim",
6396
+ move: "move",
6397
+ status: "move",
6398
+ "set-status": "move",
6399
+ show: "show",
6400
+ view: "show",
6401
+ get: "show",
6402
+ read: "read",
6403
+ list: "read",
6404
+ ls: "read",
6405
+ items: "read",
6406
+ unclaim: "unclaim",
6407
+ release: "unclaim",
6408
+ attach: "attach",
6409
+ add: "attach",
6410
+ "set-priority": "set-priority",
6411
+ priority: "set-priority",
6412
+ "archive-done": "archive-done",
6413
+ archive: "archive-done",
6414
+ doctor: "doctor",
6415
+ events: "events"
6416
+ };
6417
+ var WORKBOARD_VALUE_FLAGS = /* @__PURE__ */ new Set([
6418
+ "--status",
6419
+ "--to-status",
6420
+ "--issue",
6421
+ "--number",
6422
+ "--repo",
6423
+ "--for",
6424
+ "--out",
6425
+ "--min-cursor",
6426
+ "--since",
6427
+ "--wait",
6428
+ "--concurrency"
6429
+ ]);
6430
+ var ISSUE_TOKEN = /^(?:\d+|[\w.-]+\/[\w.-]+#\d+|#\d+)$/;
6431
+ function scanWorkboardArgs(rest) {
6432
+ const positionals = [];
6433
+ const values = /* @__PURE__ */ new Map();
6434
+ const flags = [];
6435
+ for (let i = 0; i < rest.length; i += 1) {
6436
+ const tok = rest[i];
6437
+ if (!tok.startsWith("-")) {
6438
+ positionals.push(tok);
6439
+ continue;
6440
+ }
6441
+ const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
6442
+ const name = eq > 0 ? tok.slice(0, eq) : tok;
6443
+ let value;
6444
+ if (eq > 0) value = tok.slice(eq + 1);
6445
+ else if (WORKBOARD_VALUE_FLAGS.has(name) && rest[i + 1] !== void 0 && !rest[i + 1].startsWith("-")) {
6446
+ value = rest[i + 1];
6447
+ i += 1;
6448
+ }
6449
+ if (value !== void 0) values.set(name, [...values.get(name) ?? [], value]);
6450
+ flags.push({ name, tokens: eq > 0 ? [tok] : value !== void 0 ? [name, value] : [name] });
6451
+ }
6452
+ return {
6453
+ positionals,
6454
+ values,
6455
+ flagArgs: (ownFlags) => flags.filter((f) => ownFlags.includes(f.name)).flatMap((f) => f.tokens)
6456
+ };
6457
+ }
6458
+ var quoteArg = (v) => /\s/.test(v) ? `"${v}"` : v;
6368
6459
  function workboardClaimRecovery(unknownToken, argv, paths, ownFlags = BOARD_CLAIM_OWN_FLAGS) {
6369
6460
  if (unknownToken !== "workboard") return void 0;
6370
6461
  const catalog = [...paths];
6371
- if (!catalog.includes("oracle board claim") && !catalog.includes("board claim")) return void 0;
6372
- const tail = unknownCommandTail(argv, "workboard");
6373
- if (tail[0] !== "claim") return void 0;
6374
- const canonicalPath = "oracle board claim";
6375
- const positionals = tail.slice(1);
6462
+ const inCatalog = (sub2) => catalog.includes(`oracle board ${sub2}`) || catalog.includes(`board ${sub2}`);
6463
+ let tail = unknownCommandTail(argv, "workboard");
6464
+ let skip = 0;
6465
+ if ((tail[0] === "item" || tail[0] === "items") && !(tail.length === 1 && tail[0] === "items")) {
6466
+ tail = tail.slice(1);
6467
+ skip = 1;
6468
+ }
6469
+ const verb = tail[0];
6470
+ let sub = verb === void 0 ? "read" : WORKBOARD_VERB_ALIASES[verb] ?? (inCatalog(verb) ? verb : void 0);
6471
+ if (sub === void 0) return void 0;
6472
+ if (sub === "claim") {
6473
+ if (!inCatalog("claim")) return void 0;
6474
+ const canonicalPath2 = "oracle board claim";
6475
+ const positionals = tail.slice(1);
6476
+ let corrected2;
6477
+ if (positionals.length > 0) {
6478
+ corrected2 = [`mmi-cli ${canonicalPath2}`, ...positionals, ...survivingFlagArgs(argv, "", ownFlags)].join(" ");
6479
+ } else {
6480
+ const normalized = splitInlineSelectors(argv, ["--issue", "--number"]);
6481
+ const usedFlag = normalized.includes("--issue") ? "--issue" : normalized.includes("--number") ? "--number" : "--issue";
6482
+ corrected2 = formatPositionalTarget(canonicalPath2, "issues", { flag: usedFlag, argv: normalized, ownFlags });
6483
+ }
6484
+ return { canonicalPath: canonicalPath2, corrected: corrected2, hint: `did you mean \`${corrected2}\`?` };
6485
+ }
6486
+ const verbAt = argv.indexOf("workboard") + 1 + skip;
6487
+ const scan = scanWorkboardArgs(argv.slice(verb === void 0 ? verbAt : verbAt + 1));
6488
+ const issueValues = [...scan.values.get("--issue") ?? [], ...scan.values.get("--number") ?? []];
6489
+ if (sub === "read" && verb !== void 0 && (scan.positionals.length > 0 || issueValues.length > 0)) sub = "show";
6490
+ if (!inCatalog(sub)) return void 0;
6491
+ const canonicalPath = `oracle board ${sub}`;
6492
+ const head = `mmi-cli ${canonicalPath}`;
6376
6493
  let corrected;
6377
- if (positionals.length > 0) {
6378
- corrected = [`mmi-cli ${canonicalPath}`, ...positionals, ...survivingFlagArgs(argv, "", ownFlags)].join(" ");
6494
+ if (sub === "move") {
6495
+ const flagStatus = scan.values.get("--status")?.[0] ?? scan.values.get("--to-status")?.[0];
6496
+ let status = flagStatus;
6497
+ let issues = [];
6498
+ const pos = scan.positionals;
6499
+ if (status !== void 0) {
6500
+ issues = pos;
6501
+ } else if (pos.length >= 2) {
6502
+ if (ISSUE_TOKEN.test(pos[0]) && !ISSUE_TOKEN.test(pos[pos.length - 1])) {
6503
+ status = pos[pos.length - 1];
6504
+ issues = pos.slice(0, -1);
6505
+ } else {
6506
+ status = pos[0];
6507
+ issues = pos.slice(1);
6508
+ }
6509
+ } else if (pos.length === 1) {
6510
+ if (ISSUE_TOKEN.test(pos[0])) issues = pos;
6511
+ else status = pos[0];
6512
+ }
6513
+ if (issues.length === 0) issues = issueValues;
6514
+ corrected = [
6515
+ head,
6516
+ status !== void 0 ? quoteArg(status) : "<status>",
6517
+ ...issues.length > 0 ? issues : ["<issues>"],
6518
+ ...scan.flagArgs(BOARD_MOVE_OWN_FLAGS)
6519
+ ].join(" ");
6520
+ } else if (sub === "show") {
6521
+ if (scan.positionals.length > 0) {
6522
+ corrected = [head, ...scan.positionals, ...scan.flagArgs(BOARD_SHOW_OWN_FLAGS)].join(" ");
6523
+ } else {
6524
+ const normalized = splitInlineSelectors(argv, ["--issue", "--number"]);
6525
+ const usedFlag = normalized.includes("--issue") ? "--issue" : normalized.includes("--number") ? "--number" : "--issue";
6526
+ corrected = formatPositionalTarget(canonicalPath, "issue", { flag: usedFlag, argv: normalized, ownFlags: BOARD_SHOW_OWN_FLAGS });
6527
+ }
6528
+ } else if (sub === "read") {
6529
+ corrected = [head, ...survivingFlagArgs(argv, "", BOARD_READ_OWN_FLAGS)].join(" ");
6379
6530
  } else {
6380
- const normalized = splitInlineSelectors(argv, ["--issue", "--number"]);
6381
- const usedFlag = normalized.includes("--issue") ? "--issue" : normalized.includes("--number") ? "--number" : "--issue";
6382
- corrected = formatPositionalTarget(canonicalPath, "issues", { flag: usedFlag, argv: normalized, ownFlags });
6531
+ const declared = BOARD_OTHER_OWN_FLAGS[sub] ?? ["--json", "--repo"];
6532
+ const targets = [...issueValues, ...scan.positionals];
6533
+ const placeholder = BOARD_TARGET_PLACEHOLDER[sub];
6534
+ corrected = [
6535
+ head,
6536
+ ...targets.length > 0 ? targets.map(quoteArg) : placeholder ? [placeholder] : [],
6537
+ ...scan.flagArgs(declared)
6538
+ ].join(" ");
6383
6539
  }
6384
6540
  return { canonicalPath, corrected, hint: `did you mean \`${corrected}\`?` };
6385
6541
  }
@@ -6427,6 +6583,45 @@ function normalizeBoardClaimIssueArgv(argv) {
6427
6583
  argv.splice(claimAt + 1, 0, value);
6428
6584
  return true;
6429
6585
  }
6586
+ function normalizeWorkboardClaimArgv(argv) {
6587
+ const workboardAt = argv[2] === "oracle" ? 3 : 2;
6588
+ if (argv[workboardAt] !== "workboard" || argv[workboardAt + 1] !== "claim") return false;
6589
+ const tail = argv.slice(workboardAt + 2);
6590
+ let selectorAt = -1;
6591
+ let selectorSpan = 0;
6592
+ let selected;
6593
+ let targets = 0;
6594
+ for (let i = 0; i < tail.length; i += 1) {
6595
+ const tok = tail[i];
6596
+ const eq = tok.indexOf("=");
6597
+ const name = eq > 0 ? tok.slice(0, eq) : tok;
6598
+ if (name === "--issue" || name === "--number") {
6599
+ if (selectorAt >= 0) return false;
6600
+ selectorAt = i;
6601
+ selectorSpan = eq > 0 ? 1 : 2;
6602
+ selected = eq > 0 ? tok.slice(eq + 1) : tail[++i];
6603
+ if (!selected || selected.startsWith("-")) return false;
6604
+ } else if (BOARD_CLAIM_VALUE_FLAGS.has(name)) {
6605
+ const value = eq > 0 ? tok.slice(eq + 1) : tail[++i];
6606
+ if (!value || value.startsWith("-")) return false;
6607
+ } else if (BOARD_CLAIM_BOOLEAN_FLAGS.has(tok) || tok === "--dry-run" || tok === "--validate-only") {
6608
+ continue;
6609
+ } else if (tok.startsWith("-") || !tok) {
6610
+ return false;
6611
+ } else {
6612
+ targets += 1;
6613
+ }
6614
+ }
6615
+ if (selected !== void 0) {
6616
+ if (targets > 0) return false;
6617
+ tail.splice(selectorAt, selectorSpan);
6618
+ tail.unshift(selected);
6619
+ } else if (targets === 0) {
6620
+ return false;
6621
+ }
6622
+ argv.splice(2, argv.length - 2, "oracle", "board", "claim", ...tail);
6623
+ return true;
6624
+ }
6430
6625
  function classifyParseError(plain) {
6431
6626
  if (/unknown command/i.test(plain)) return "unknown-command";
6432
6627
  if (/unknown option|missing required argument|too many arguments|argument missing|is invalid\. Allowed choices are|cannot be used with/i.test(plain)) {
@@ -15142,6 +15337,12 @@ function renderClaimReleaseBody(release) {
15142
15337
 
15143
15338
  <!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
15144
15339
  }
15340
+ function formatClaimMarker(actor, ts = (/* @__PURE__ */ new Date()).toISOString()) {
15341
+ const marker = { v: 1, session: actor.session, surface: actor.surface, host: actor.host, ts };
15342
+ return `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.
15343
+
15344
+ <!-- mmi-claim: ${JSON.stringify(marker)} -->`;
15345
+ }
15145
15346
  function latestClaimMarker(comments) {
15146
15347
  for (let i = comments.length - 1; i >= 0; i -= 1) {
15147
15348
  const body = comments[i].body;
@@ -15466,6 +15667,7 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
15466
15667
  `repos/${repo}/branches/${encodeURIComponent(out.branch)}`
15467
15668
  );
15468
15669
  out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
15670
+ out.branchSha = head?.commit?.sha ?? void 0;
15469
15671
  } catch {
15470
15672
  }
15471
15673
  }
@@ -15532,13 +15734,27 @@ function isSeatClaimMarker(marker, current, context = {}) {
15532
15734
  const state = context.sessionState ?? probeLocalClaimSession(marker, context.now);
15533
15735
  return state === "dead";
15534
15736
  }
15737
+ function isSameBranchClaimResume(contest, repo, landing = {}) {
15738
+ const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
15739
+ const headSha = landing.headSha?.trim();
15740
+ if (!headBranch && !headSha) return false;
15741
+ if (!contest.marker || contest.unverifiable.length > 0 || contest.live.length === 0) return false;
15742
+ const branchLine = /^live branch ([^\s]+) on /;
15743
+ const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
15744
+ const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
15745
+ const liveBranch = fromContest || fromLive;
15746
+ const matches = Boolean(headBranch && liveBranch && liveBranch === headBranch) || Boolean(headSha && contest.branchSha && contest.branchSha === headSha);
15747
+ if (!matches) return false;
15748
+ const ownBranchLine = liveBranch ? `live branch ${liveBranch} ` : "live branch ";
15749
+ return contest.live.every((line) => /^claim marker from /.test(line) || line.startsWith(ownBranchLine) && line.endsWith(` on ${repo}`));
15750
+ }
15535
15751
  function laneResumeEvidence(evidence) {
15536
15752
  if (!evidence.marker || evidence.sessionState !== "dead") return void 0;
15537
15753
  if (!isHostSessionId(evidence.marker.session)) return void 0;
15538
15754
  const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
15539
15755
  return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
15540
15756
  }
15541
- async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient) {
15757
+ async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient, landing) {
15542
15758
  const livenessClient = await selectLivenessClient(client, appClient);
15543
15759
  let evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
15544
15760
  if (evidence.failed.some((channel) => channel !== "session")) {
@@ -15550,10 +15766,21 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
15550
15766
  markerBy: evidence.markerBy,
15551
15767
  sessionState: evidence.sessionState
15552
15768
  });
15553
- const resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
15769
+ let resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
15554
15770
  const live = ownSeat ? [] : liveEvidenceLines(evidence, item.repository);
15555
15771
  const unverifiable = ownSeat ? [] : evidence.failed;
15556
- const contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
15772
+ let contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
15773
+ if (contested && landing && isSameBranchClaimResume({
15774
+ contested: true,
15775
+ live,
15776
+ unverifiable,
15777
+ marker: evidence.marker,
15778
+ branch: evidence.branch,
15779
+ branchSha: evidence.branchSha
15780
+ }, item.repository, landing)) {
15781
+ contested = false;
15782
+ resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
15783
+ }
15557
15784
  const carry = staleWorkLines(evidence);
15558
15785
  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;
15559
15786
  return {
@@ -15563,7 +15790,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
15563
15790
  ownership,
15564
15791
  marker: evidence.marker,
15565
15792
  resume,
15566
- ...staleStamp ? { staleStamp } : {}
15793
+ ...staleStamp ? { staleStamp } : {},
15794
+ ...evidence.branch ? { branch: evidence.branch } : {},
15795
+ ...evidence.branchSha ? { branchSha: evidence.branchSha } : {}
15567
15796
  };
15568
15797
  }
15569
15798
  function laneContestParts(contest) {
@@ -15714,6 +15943,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
15714
15943
  cfg,
15715
15944
  client,
15716
15945
  items: collected.items,
15946
+ selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
15717
15947
  writable: writableOrUnknown(writable),
15718
15948
  report,
15719
15949
  session: deps.session ?? describeSessionIdentity(),
@@ -15825,7 +16055,9 @@ async function claimOneBoardItem(ctx, selector, options) {
15825
16055
  return;
15826
16056
  }
15827
16057
  if (!options.force) {
15828
- const refusal = laneContestMessage(item.ref, contest, "claim");
16058
+ 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);
16059
+ const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
16060
+ const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
15829
16061
  throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
15830
16062
  }
15831
16063
  previousHolder = displaced();
@@ -19421,10 +19653,10 @@ var rollout_plan_default = {
19421
19653
  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)."
19422
19654
  },
19423
19655
  baseline: {
19424
- version: "4.5.209",
19425
- tag: "v4.5.209",
19426
- commit: "2449709d554c",
19427
- npm: "@mutmutco/cli@4.5.209"
19656
+ version: "4.5.210",
19657
+ tag: "v4.5.210",
19658
+ commit: "a9e5d838bc19",
19659
+ npm: "@mutmutco/cli@4.5.210"
19428
19660
  },
19429
19661
  exitCriterion: "fleet-n-of-n",
19430
19662
  hubOnlyShortcut: "forbidden",
@@ -19441,14 +19673,14 @@ var rollout_plan_default = {
19441
19673
  repo: "mutmutco/mmi-hub",
19442
19674
  role: "canary",
19443
19675
  schedule: "train",
19444
- v3Target: "v4.5.209"
19676
+ v3Target: "v4.5.210"
19445
19677
  }
19446
19678
  ],
19447
19679
  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.",
19448
19680
  rollback: {
19449
19681
  independent: true,
19450
- mechanism: "npm dist-tag latest -> 4.5.209 and redeploy the Hub Lambda from tag v4.5.209 (2449709d554c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19451
- v3Target: "v4.5.209 (@mutmutco/cli@4.5.209, tag commit 2449709d554c \u2014 last known-good release carrying the repo-index v4-only contract)"
19682
+ mechanism: "npm dist-tag latest -> 4.5.210 and redeploy the Hub Lambda from tag v4.5.210 (a9e5d838bc19); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19683
+ v3Target: "v4.5.210 (@mutmutco/cli@4.5.210, tag commit a9e5d838bc19 \u2014 last known-good release carrying the repo-index v4-only contract)"
19452
19684
  }
19453
19685
  },
19454
19686
  {
@@ -23952,46 +24184,45 @@ var import_node_fs30 = require("node:fs");
23952
24184
  var import_promises4 = require("node:fs/promises");
23953
24185
  var import_node_path28 = require("node:path");
23954
24186
 
23955
- // src/actions-billing-preflight.ts
23956
- var CANARY_WORKFLOW = "actions-job-start-canary.yml";
23957
- var CANARY_REPO = "mutmutco/MMI-Hub";
23958
- var ACTIONS_BILLING_BLOCK_RE = /job was not started because recent account payments have failed|account payments have failed|spending limit needs to be increased|Billing & plans/i;
23959
- var CANARY_POLLS = 20;
23960
- var CANARY_POLL_MS = 1e3;
23961
- function isActionsBillingBlockText(text) {
23962
- return ACTIONS_BILLING_BLOCK_RE.test(text);
23963
- }
23964
- function interpretActionsJobStart(input) {
23965
- const text = input.text ?? "";
23966
- if (isActionsBillingBlockText(text)) return "billing-blocked";
23967
- const jobs = input.jobs ?? [];
23968
- if (jobs.some((job) => isActionsBillingBlockText(JSON.stringify(job)))) return "billing-blocked";
23969
- if (jobs.some((job) => Array.isArray(job.steps) && job.steps.length > 0 || Boolean(job.startedAt))) {
23970
- return "started";
23971
- }
23972
- if (jobs.length > 0 && jobs.every((job) => {
23973
- const finished = job.conclusion === "failure" || job.status === "completed";
23974
- const empty = !Array.isArray(job.steps) || job.steps.length === 0;
23975
- return finished && empty && !job.startedAt;
23976
- })) {
23977
- return "never-started";
24187
+ // ../shared/runner-fallback.ts
24188
+ var HOSTED_RUNNER_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/i;
24189
+ function classifyRunnerCost(selector) {
24190
+ const raw = selector.replace(/\s+#.*$/gm, "").trim();
24191
+ if (HOSTED_RUNNER_LABEL_RE.test(raw)) return "hosted";
24192
+ const json = /^\$\{\{\s*fromJSON\('([^']+)'\)\s*\}\}$/.exec(raw);
24193
+ if (json) {
24194
+ try {
24195
+ const labels2 = JSON.parse(json[1]);
24196
+ return Array.isArray(labels2) && labels2.length > 0 && labels2.every((l) => typeof l === "string" && /^[\w-]+$/.test(l)) && labels2.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
24197
+ } catch {
24198
+ return "unknown";
24199
+ }
23978
24200
  }
23979
- return "pending";
23980
- }
23981
- var CANARY_ACCESS_BLOCK_RE = /HTTP 404|HTTP 403|Not Found|Resource not accessible|must have admin rights|not authorized|no identity|401/i;
23982
- function isCanaryAccessBlockText(text) {
23983
- return !isActionsBillingBlockText(text) && CANARY_ACCESS_BLOCK_RE.test(text);
23984
- }
23985
- function actionsCanaryUndispatchedRefusal(detail) {
23986
- return new Error(
23987
- `release refused: the hosted-job canary (#5604) was not dispatched, so a hosted job start is UNPROVEN \u2014 this is NOT a billing verdict. ${detail} The Hub dispatches ${CANARY_WORKFLOW} with its App token for a train-authorized caller (#6319): refresh the Hub session (\`gh auth login\`) and retry, and confirm \`mmi-cli oracle org access role <owner/repo>\` reports train authority. Nothing was tagged or pushed \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
23988
- );
24201
+ if (raw.includes("${{")) return "unknown";
24202
+ const labels = /^\[[^\[\]]+\]$/.test(raw) ? raw.slice(1, -1).split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")) : /^(?:-\s*[^\n]+\s*)+$/.test(raw) ? raw.split("\n").map((s) => s.trim().replace(/^-\s*/, "").replace(/^(['"])(.*)\1$/, "$2")) : [raw.replace(/^(['"])(.*)\1$/, "$2")];
24203
+ return labels.every((l) => /^[\w-]+$/.test(l)) && labels.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
23989
24204
  }
23990
- function actionsBillingRefusal(detail) {
23991
- return new Error(
23992
- `release refused: GitHub Actions cannot start a hosted job (billing/spending). ${detail} Fix Billing & plans / the org spending limit, then rerun. Do not mint a new tag. If a tag is already on origin, use \`mmi-cli devops release --retry-publish <run-id> --apply\` on that exact run \u2014 never recut.`
23993
- );
24205
+ function classifyJobRunnerCost(selector, jobBody, standardHostedLabel) {
24206
+ const direct = classifyRunnerCost(selector);
24207
+ const matrix = /^\$\{\{\s*matrix\.([\w-]+)\s*\}\}$/.exec(selector.trim());
24208
+ if (!matrix || direct === "hosted") return direct;
24209
+ const strategy = /^ strategy:\s*\n((?: {6,}[^\n]*\n?|\s*\n)*)/m.exec(jobBody)?.[1];
24210
+ const matrixBody = strategy && /^ matrix:[^\S\n]*\n((?: {8,}[^\n]*\n?|\s*\n)*)/m.exec(strategy)?.[1];
24211
+ const include = matrixBody && /^ include:[^\S\n]*\n((?: {10,}[^\n]*\n?|\s*\n)*)$/.exec(matrixBody)?.[1];
24212
+ if (!include) return "unknown";
24213
+ const rows = include.split(/^ {10}- /m).slice(1);
24214
+ if (!rows.length) return "unknown";
24215
+ const costs = rows.map((row) => {
24216
+ const match = new RegExp(`^(?: {12})?${matrix[1]}:\\s*(.+)$`, "m").exec(row);
24217
+ if (standardHostedLabel && match?.[1].trim() === standardHostedLabel) return "standard-hosted";
24218
+ return match ? classifyRunnerCost(match[1]) : "unknown";
24219
+ });
24220
+ if (costs.includes("hosted")) return "hosted";
24221
+ if (costs.includes("unknown")) return "unknown";
24222
+ return costs.includes("standard-hosted") ? "standard-hosted" : "self-hosted";
23994
24223
  }
24224
+
24225
+ // src/actions-billing-preflight.ts
23995
24226
  function parseJson(raw, label) {
23996
24227
  try {
23997
24228
  return JSON.parse(raw);
@@ -23999,153 +24230,30 @@ function parseJson(raw, label) {
23999
24230
  throw new Error(`${label} was not valid JSON`);
24000
24231
  }
24001
24232
  }
24002
- async function scanRepoForBillingBlock(deps, repo) {
24003
- let raw;
24004
- try {
24005
- raw = await deps.run("gh", [
24006
- "run",
24007
- "list",
24008
- "--repo",
24009
- repo,
24010
- "--limit",
24011
- "8",
24012
- "--json",
24013
- "databaseId,conclusion,status,displayTitle,url"
24014
- ]);
24015
- } catch {
24016
- return void 0;
24017
- }
24018
- const rows = parseJson(raw, `gh run list --repo ${repo}`);
24019
- if (!Array.isArray(rows)) return void 0;
24020
- for (const row of rows) {
24021
- const blob = `${row.displayTitle ?? ""} ${row.conclusion ?? ""} ${row.url ?? ""}`;
24022
- if (isActionsBillingBlockText(blob)) {
24023
- return `${repo} run ${row.databaseId ?? row.url ?? "(unknown)"} already names a billing/spending block`;
24024
- }
24025
- if (row.conclusion !== "failure" && row.status !== "completed") continue;
24026
- if (typeof row.databaseId !== "number") continue;
24027
- try {
24028
- const view = await deps.run("gh", [
24029
- "run",
24030
- "view",
24031
- String(row.databaseId),
24032
- "--repo",
24033
- repo,
24034
- "--json",
24035
- "jobs,conclusion,displayTitle,url"
24036
- ]);
24037
- const parsed = parseJson(view, `gh run view ${row.databaseId}`);
24038
- const verdict = interpretActionsJobStart({
24039
- jobs: parsed.jobs,
24040
- text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
24041
- });
24042
- if (verdict === "billing-blocked" || verdict === "never-started") {
24043
- return `${repo} run ${row.databaseId} (${parsed.url ?? row.url ?? "no url"}): hosted job never started` + (verdict === "billing-blocked" ? " (billing/spending refusal)" : " (empty steps)");
24044
- }
24045
- } catch {
24046
- }
24047
- }
24048
- return void 0;
24049
- }
24050
- async function correlateCanaryRun(deps, nonce) {
24051
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
24052
- let lastError = "no matching canary run";
24053
- for (let attempt = 0; attempt < 8; attempt++) {
24054
- if (attempt > 0) await sleep2(CANARY_POLL_MS);
24055
- let raw;
24056
- try {
24057
- raw = await deps.run("gh", [
24058
- "run",
24059
- "list",
24060
- "--repo",
24061
- CANARY_REPO,
24062
- "--workflow",
24063
- CANARY_WORKFLOW,
24064
- "--limit",
24065
- "10",
24066
- "--json",
24067
- "databaseId,displayTitle,status,createdAt"
24068
- ]);
24069
- } catch (e) {
24070
- lastError = e instanceof Error ? e.message : String(e);
24071
- continue;
24072
- }
24073
- const rows = parseJson(
24074
- raw,
24075
- "canary gh run list"
24076
- );
24077
- const match = rows.find((row) => (row.displayTitle ?? "").includes(nonce) && typeof row.databaseId === "number");
24078
- if (match?.databaseId) return match.databaseId;
24079
- }
24080
- throw new Error(
24081
- `could not correlate ${CANARY_WORKFLOW} on ${CANARY_REPO} (nonce ${nonce}): ${lastError}`
24082
- );
24083
- }
24084
- async function dispatchCanary(deps, targetRepo2, nonce) {
24085
- const attempts = [];
24086
- if (deps.dispatchActionsCanary) {
24087
- try {
24088
- await deps.dispatchActionsCanary({ repo: targetRepo2, nonce });
24089
- return;
24090
- } catch (e) {
24091
- attempts.push(`Hub App dispatch: ${e instanceof Error ? e.message : String(e)}`);
24092
- }
24093
- }
24094
- try {
24095
- await deps.run("gh", [
24096
- "workflow",
24097
- "run",
24098
- CANARY_WORKFLOW,
24099
- "--repo",
24100
- CANARY_REPO,
24101
- "-f",
24102
- `nonce=${nonce}`
24103
- ]);
24104
- return;
24105
- } catch (e) {
24106
- attempts.push(`gh workflow run --repo ${CANARY_REPO}: ${e instanceof Error ? e.message : String(e)}`);
24107
- }
24108
- const detail = `could not dispatch ${CANARY_WORKFLOW} on ${CANARY_REPO} \u2014 ${attempts.join(" | ")}.`;
24109
- if (attempts.some((a) => isActionsBillingBlockText(a))) {
24110
- throw actionsBillingRefusal(`${detail} A hosted publish job may not start (the v1.54.2 class).`);
24111
- }
24112
- throw actionsCanaryUndispatchedRefusal(
24113
- attempts.every((a) => isCanaryAccessBlockText(a)) ? `${detail} Every door answered with an access refusal \u2014 the caller may read MMI-Hub but not start its workflows.` : detail
24114
- );
24115
- }
24116
- var HOSTED_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/;
24117
- var FALLBACK_EXPR_GLOBAL_RE = /\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)'\s*\|\|\s*fromJSON\('\[[^\n']+\]'\)\s*\}\}/g;
24118
- var AUTOMATIC_TRIGGERS = /* @__PURE__ */ new Set(["push", "pull_request", "pull_request_target", "release", "schedule", "workflow_run", "merge_group", "create", "deployment", "repository_dispatch", "check_run", "check_suite", "issues", "issue_comment"]);
24119
- function workflowTriggers(yaml) {
24120
- const lines2 = yaml.split(/\r?\n/);
24121
- const i = lines2.findIndex((l) => /^(?:on|"on"|'on'):(?:\s|$)/.test(l));
24122
- if (i < 0) return [];
24123
- const inline = lines2[i].replace(/^[^:]+:\s*/, "").replace(/\s+#.*$/, "").trim();
24124
- if (inline) return inline.replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
24125
- const out = [];
24126
- for (const line of lines2.slice(i + 1)) {
24127
- if (/^\S/.test(line)) break;
24128
- const m = /^ {2}([A-Za-z_]+):/.exec(line) ?? /^ {2}- ([A-Za-z_]+)\s*$/.exec(line);
24129
- if (m) out.push(m[1]);
24130
- }
24131
- return out;
24132
- }
24133
- function workflowNeedsHostedRunner(yaml) {
24134
- const body = yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n").replace(FALLBACK_EXPR_GLOBAL_RE, "");
24135
- return HOSTED_LABEL_RE.test(body) && workflowTriggers(yaml).some((t) => AUTOMATIC_TRIGGERS.has(t));
24233
+ function workflowNeedsHostedRunner(yaml, localWorkflows = /* @__PURE__ */ new Set()) {
24234
+ const jobs = parseWorkflowJobs(yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n"));
24235
+ return !jobs.length || jobs.some((job) => {
24236
+ const callee = /^ uses:\s*\.\/(.+)$/m.exec(job.body)?.[1];
24237
+ if (callee && localWorkflows.has(callee)) return false;
24238
+ const section = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
24239
+ const selector = section.split("\n").map((line) => line.trim()).join("\n");
24240
+ return classifyJobRunnerCost(selector, job.body) !== "self-hosted";
24241
+ });
24136
24242
  }
24137
24243
  async function hostedWorkflowsOf(deps, repo) {
24138
24244
  const list = parseJson(
24139
24245
  await deps.run("gh", ["api", `repos/${repo}/contents/.github/workflows`]),
24140
24246
  `${repo} workflow listing`
24141
24247
  );
24142
- const hosted = [];
24248
+ if (!Array.isArray(list) || !list.some((f) => f.type === "file" && /\.ya?ml$/.test(f.path ?? ""))) throw new Error("no workflow inventory");
24249
+ const workflows = [];
24143
24250
  for (const f of list) {
24144
- if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path) || f.name === CANARY_WORKFLOW) continue;
24251
+ if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)) continue;
24145
24252
  const raw = parseJson(await deps.run("gh", ["api", `repos/${repo}/contents/${f.path}`]), `${repo} ${f.path}`);
24146
- if (workflowNeedsHostedRunner(Buffer.from(raw.content ?? "", "base64").toString("utf8"))) hosted.push(f.path);
24253
+ workflows.push({ path: f.path, body: Buffer.from(raw.content ?? "", "base64").toString("utf8") });
24147
24254
  }
24148
- return hosted;
24255
+ const local = new Set(workflows.map((file) => file.path));
24256
+ return workflows.filter((file) => workflowNeedsHostedRunner(file.body, local)).map((file) => file.path);
24149
24257
  }
24150
24258
  async function assertActionsJobsCanStart(deps, targetRepo2) {
24151
24259
  let hosted;
@@ -24155,50 +24263,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
24155
24263
  hosted = void 0;
24156
24264
  }
24157
24265
  if (hosted && hosted.length === 0) {
24158
- process.stderr.write(`actions preflight: ${targetRepo2} has no automatically triggered GitHub-hosted job; hosted canary skipped (#7721)
24266
+ process.stderr.write(`actions preflight: ${targetRepo2} source selectors are self-hosted only; no canary dispatch; access/isolation/execution unproven (#8911)
24159
24267
  `);
24160
24268
  return;
24161
24269
  }
24162
- const scanned = await scanRepoForBillingBlock(deps, targetRepo2);
24163
- if (scanned) throw actionsBillingRefusal(scanned);
24164
- if (targetRepo2.toLowerCase() !== CANARY_REPO.toLowerCase()) {
24165
- const hubScan = await scanRepoForBillingBlock(deps, CANARY_REPO);
24166
- if (hubScan) throw actionsBillingRefusal(hubScan);
24167
- }
24168
- const nonce = `5604-${(deps.now ?? Date.now)().toString(36)}`;
24169
- await dispatchCanary(deps, targetRepo2, nonce);
24170
- const runId = await correlateCanaryRun(deps, nonce);
24171
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
24172
- let last = "pending";
24173
- let url = `https://github.com/${CANARY_REPO}/actions/runs/${runId}`;
24174
- for (let attempt = 0; attempt < CANARY_POLLS; attempt++) {
24175
- if (attempt > 0) await sleep2(CANARY_POLL_MS);
24176
- const view = await deps.run("gh", [
24177
- "run",
24178
- "view",
24179
- String(runId),
24180
- "--repo",
24181
- CANARY_REPO,
24182
- "--json",
24183
- "jobs,status,conclusion,url,displayTitle"
24184
- ]);
24185
- const parsed = parseJson(view, `canary gh run view ${runId}`);
24186
- if (parsed.url) url = parsed.url;
24187
- last = interpretActionsJobStart({
24188
- jobs: parsed.jobs,
24189
- text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
24190
- });
24191
- if (last === "started") {
24192
- await deps.run("gh", ["run", "cancel", String(runId), "--repo", CANARY_REPO]).catch(() => "");
24193
- return;
24194
- }
24195
- if (last === "billing-blocked" || last === "never-started") {
24196
- throw actionsBillingRefusal(`${url}: hosted canary ${last} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`);
24197
- }
24198
- }
24199
- throw actionsBillingRefusal(
24200
- `${url}: hosted canary stayed ${last} after ${CANARY_POLLS} polls \u2014 cannot prove a hosted job can start \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
24201
- );
24270
+ throw new Error(`release refused (#8911): ${hosted ? `paid or unresolved runner path(s): ${hosted.join(", ")}` : "runner inventory unreadable; self-hosted-only selection unproven"}. Remove paid selectors/fallbacks and verify existing self-hosted access and isolation before tagging.`);
24202
24271
  }
24203
24272
 
24204
24273
  // src/train-apply-recovery.ts
@@ -30301,33 +30370,6 @@ function registryRepoKey(repo) {
30301
30370
  return repo.toLowerCase();
30302
30371
  }
30303
30372
 
30304
- // ../shared/runner-fallback.ts
30305
- var RUNNER_FALLBACK_EXPR_RE = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'((?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*))'\s*\|\|\s*fromJSON\('(\[[^\n']+\])'\)\s*\}\}$/;
30306
- var FLEET_ONLY_RUNNER_JOBS = {
30307
- "backup-health.yml:judge": "SSH backup inventory; hosted source-IP access unverified",
30308
- "browser-pass.yml:browser-pass": "owner-bounded two-lane browser capacity",
30309
- "app-publish.yml:publish": "SSH tenant publish; hosted source-IP access unverified",
30310
- "get-publish.yml:publish": "SSH installer publish; hosted source-IP access unverified",
30311
- "get-publish-desktop.yml:publish": "SSH installer publish; hosted source-IP access unverified",
30312
- "hetzner-knownhosts-pin.yml:pin": "SSH host-key probe; hosted source-IP access unverified",
30313
- "module-rc-publish.yml:publish": "SSH live-container snapshot and tenant persistent state",
30314
- "tenant-control.yml:control": "SSH tenant control; hosted source-IP access unverified",
30315
- "tenant-deploy.yml:deploy": "SSH tenant deploy; hosted source-IP access unverified",
30316
- "tenant-reconcile.yml:reconcile": "SSH tenant reconciliation; hosted source-IP access unverified"
30317
- };
30318
- function parseRunnerFallbackExpression(runsOn) {
30319
- const match = RUNNER_FALLBACK_EXPR_RE.exec(runsOn.trim());
30320
- if (!match) return void 0;
30321
- try {
30322
- const parsed = JSON.parse(match[2]);
30323
- if (!Array.isArray(parsed) || !parsed.length || !parsed.every((item) => typeof item === "string")) return void 0;
30324
- if (!parsed.some((item) => item.toLowerCase() === "self-hosted")) return void 0;
30325
- return { hosted: match[1], labels: parsed };
30326
- } catch {
30327
- return void 0;
30328
- }
30329
- }
30330
-
30331
30373
  // src/ci-audit.ts
30332
30374
  var HUB_REPO4 = "mutmutco/MMI-Hub";
30333
30375
  var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
@@ -30387,25 +30429,28 @@ var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
30387
30429
  var SHARED_ACTION_PINS = [
30388
30430
  { path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
30389
30431
  ];
30432
+ var STANDARD_HOSTED_ALLOWLIST = [
30433
+ ["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"]
30434
+ ];
30390
30435
  function auditRunnerFallbacks(repo, files) {
30391
30436
  const checks = [];
30392
30437
  for (const { path: path2, body } of files) {
30393
30438
  const jobs = parseWorkflowJobs(body.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n"));
30394
30439
  for (const job of jobs) {
30395
- const runsOn = /^ runs-on:\s*(.+)$/m.exec(job.body)?.[1] ?? "";
30396
30440
  const runnerSection = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
30397
- const fallback = parseRunnerFallbackExpression(runsOn);
30398
- const selfHosted = !!fallback || /self-hosted/i.test(runnerSection) || /matrix\./.test(runsOn) && /self-hosted/i.test(job.body);
30399
- if (!selfHosted) continue;
30400
- const key = `${path2.split("/").pop()}:${job.id}`;
30401
- const marked = /#\s*mmi-runner:\s*fleet-only\s*[—–-]+\s*(\S.*?)\s*$/.exec(runsOn)?.[1];
30402
- const exemption = (repo === HUB_REPO4 ? FLEET_ONLY_RUNNER_JOBS[key] : void 0) ?? marked;
30403
- const safe = !!fallback;
30441
+ const callee = /^ uses:\s*(.+)$/m.exec(job.body)?.[1];
30442
+ if (callee?.startsWith("./") && files.some((file) => file.path === callee.slice(2))) continue;
30443
+ if (callee && /^mutmutco\/MMI-Hub\/\.github\/workflows\/[\w.-]+\.ya?ml@[\w./-]+$/.test(callee)) continue;
30444
+ const standardHostedLabel = STANDARD_HOSTED_ALLOWLIST.find(
30445
+ ([allowedRepo, allowedPath, allowedJob]) => repo === allowedRepo && path2 === allowedPath && job.id === allowedJob
30446
+ )?.[3];
30447
+ const cost = classifyJobRunnerCost(runnerSection.split("\n").map((line) => line.trim()).join("\n"), job.body, standardHostedLabel);
30448
+ const ok = !callee && (cost === "self-hosted" || cost === "standard-hosted");
30404
30449
  checks.push({
30405
- ok: !!exemption || safe,
30406
- label: `runner has hosted backup (#8671): ${path2}:${job.id}`,
30407
- detail: exemption ? `fleet-only: ${exemption}` : safe ? void 0 : "self-hosted runner does not use the vars.MMI_RUNNER_FALLBACK backup expression",
30408
- remediation: exemption || safe ? void 0 : `Use runs-on: \${{ vars.MMI_RUNNER_FALLBACK == 'hosted' && 'ubuntu-latest' || fromJSON('["self-hosted","linux","x64","mmi-live"]') }} (keeping the job's original self-hosted tuple). See gh-runner-runbook.md.`
30450
+ ok,
30451
+ label: cost === "standard-hosted" ? `runner uses exact standard-hosted exception (#8912): ${path2}:${job.id}` : `runner is self-hosted only (#8911): ${path2}:${job.id}`,
30452
+ detail: callee ? `unknown callee runner policy: ${callee}` : cost === "hosted" ? "paid GitHub-hosted runner selection is forbidden" : cost === "unknown" ? "unknown runner selector; not proven self-hosted" : cost === "standard-hosted" ? "exact Hub launcher windows-latest matrix leg permitted within included Enterprise minutes; runner access and isolation not established" : "self-hosted selection only; runner access and isolation not established",
30453
+ remediation: ok ? void 0 : `Use only a verified accessible self-hosted tuple, with no hosted fallback; resolve unknown selectors/callees. Preserve checks and artifacts. See gh-runner-runbook.md (${repo}).`
30409
30454
  });
30410
30455
  }
30411
30456
  }
@@ -30630,7 +30675,7 @@ function rulesetCoversReleaseBranches(payload, track) {
30630
30675
  function releaseBranchRefsCovered(conditions, track) {
30631
30676
  const include = conditions?.ref_name?.include;
30632
30677
  if (!Array.isArray(include)) return [];
30633
- const releaseRefs = track === "full" ? ["refs/heads/rc", "refs/heads/main"] : ["refs/heads/main"];
30678
+ const releaseRefs = branchesForTrack(track).filter((branch) => branch !== "development").map((branch) => `refs/heads/${branch}`);
30634
30679
  return releaseRefs.filter((ref) => include.some((entry) => typeof entry === "string" && refPatternCovers(entry, ref)));
30635
30680
  }
30636
30681
  function unreachableReleaseContexts(input) {
@@ -30859,6 +30904,18 @@ async function auditRepoCi(repo, callerDeps) {
30859
30904
  const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
30860
30905
  const deployableGated = repoClass === "deployable" && !explicitNoCi;
30861
30906
  const info = await restJson2(deps, `repos/${repo}`, {});
30907
+ if (info.archived === true) {
30908
+ return {
30909
+ repo,
30910
+ class: repoClass,
30911
+ mergePolicy: "wait-for-checks",
30912
+ ok: false,
30913
+ checks: [],
30914
+ explicitNoCi,
30915
+ excludedReason: "GitHub archived repository (read-only)"
30916
+ };
30917
+ }
30918
+ let releaseReadFailure;
30862
30919
  const baseBranch = repoClass === "content" ? "main" : "development";
30863
30920
  checks.push({
30864
30921
  ok: info.allow_auto_merge === true,
@@ -31061,14 +31118,29 @@ async function auditRepoCi(repo, callerDeps) {
31061
31118
  const releaseBranches = releaseBranchRefsCovered(productRuleset.conditions, resolveReleaseTrack(meta, void 0, repo));
31062
31119
  if (releaseBranches.length > 0) {
31063
31120
  const emittedByBranch = {};
31121
+ const absent = [];
31122
+ const unreadable = [];
31064
31123
  for (const ref of releaseBranches) {
31065
- emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
31124
+ const branch = ref.replace("refs/heads/", "");
31125
+ const present = await branchPresence(deps, repo, branch);
31126
+ if (present === false) absent.push(branch);
31127
+ else if (present === void 0) unreadable.push(branch);
31128
+ else {
31129
+ emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, branch);
31130
+ if (emittedByBranch[ref] === void 0) unreadable.push(branch);
31131
+ }
31066
31132
  }
31133
+ if (unreadable.length) releaseReadFailure = `release branch reachability unreadable: [${unreadable.join(", ")}] \u2014 no emitter conclusion`;
31134
+ const presenceNote = [
31135
+ absent.length ? `absent: [${absent.join(", ")}] \u2014 no emitter requirement audited` : "",
31136
+ unreadable.length ? `unreadable: [${unreadable.join(", ")}] \u2014 cannot audit emitters` : ""
31137
+ ].filter(Boolean).join("; ");
31067
31138
  const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
31068
31139
  checks.push({
31069
31140
  ok: unreachable.length === 0,
31070
31141
  label: RELEASE_BRANCH_REACHABLE_LABEL,
31071
- detail: unreachable.length === 0 ? void 0 : unreachable.map((u) => `${u.branch.replace("refs/heads/", "")} requires [${u.unreachable.join(", ")}] but its own workflows emit none of them`).join("; ") + " \u2014 a base-" + unreachable.map((u) => u.branch.replace("refs/heads/", "")).join("/") + " PR (e.g. a hotfix) can never satisfy the gate and stays permanently BLOCKED",
31142
+ severity: unreadable.length ? "warn" : absent.length ? "info" : void 0,
31143
+ detail: unreachable.length === 0 ? presenceNote || void 0 : unreachable.map((u) => `${u.branch.replace("refs/heads/", "")} requires [${u.unreachable.join(", ")}] but its own workflows emit none of them`).join("; ") + " \u2014 a base-" + unreachable.map((u) => u.branch.replace("refs/heads/", "")).join("/") + " PR (e.g. a hotfix) can never satisfy the gate and stays permanently BLOCKED" + (presenceNote ? `; ${presenceNote}` : ""),
31072
31144
  remediation: unreachable.length === 0 ? void 0 : `Remove the unreachable context(s) from those branches in the ${PRODUCT_RULESET_NAME} ruleset conditions and ${PRODUCT_RULESET_REF}, or add the emitting workflow to the branch`
31073
31145
  });
31074
31146
  }
@@ -31124,7 +31196,7 @@ async function auditRepoCi(repo, callerDeps) {
31124
31196
  const body = await fetchFileContent(deps, repo, baseBranch, path2);
31125
31197
  if (body == null) continue;
31126
31198
  if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
31127
- if (!/^\s*concurrency:/m.test(body) || !/^\s*cancel-in-progress:[ \t]*(?:true|\$\{\{\s*github\.event_name\s*==\s*(['"])pull_request\1\s*\}\})[ \t]*(?:#.*)?$/m.test(body)) {
31199
+ if (!/^\s*concurrency:/m.test(body) || !/^\s*cancel-in-progress:[ \t]*(?:true|\$\{\{\s*github\.event_name\s*(?:==\s*(['"])pull_request\1|!=\s*(['"])merge_group\2)\s*\}\})[ \t]*(?:#.*)?$/m.test(body)) {
31128
31200
  if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
31129
31201
  else missing.push(path2);
31130
31202
  }
@@ -31183,34 +31255,45 @@ async function auditRepoCi(repo, callerDeps) {
31183
31255
  if (failure) {
31184
31256
  return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
31185
31257
  }
31186
- const ok = checks.every((c) => c.ok);
31187
- return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
31258
+ const ok = !releaseReadFailure && checks.every((c) => c.ok);
31259
+ return {
31260
+ repo,
31261
+ class: repoClass,
31262
+ mergePolicy: policy,
31263
+ ok,
31264
+ checks,
31265
+ explicitNoCi,
31266
+ ...releaseReadFailure ? { unauditedReason: releaseReadFailure } : {}
31267
+ };
31188
31268
  }
31189
31269
  var REGISTRY_UNREADABLE_DETAIL = `the registry roster read returned nothing \u2014 the fleet was NOT audited. 0 repos read from the registry; the number left unaudited is unknown because the roster is what names them. This is "cannot audit the fleet", never "the fleet is green". Under GitHub Actions the CLI needs a CI Hub session (MMI_HUB_TOKEN, minted from the job's OIDC claim via POST /auth/session-ci) \u2014 an App installation token has no /user identity and can never pass /auth/session (#3215).`;
31190
31270
  async function auditOrgCi(deps, repoFilter) {
31191
31271
  if (repoFilter) {
31192
31272
  const report = await auditRepoCi(repoFilter, deps);
31193
- return { ok: report.ok, scope: "single-repo", repos: [report] };
31273
+ return { ok: report.ok, scope: "single-repo", repos: [report], excludedCount: report.excludedReason ? 1 : 0 };
31194
31274
  }
31195
31275
  const projects = await deps.listProjects();
31196
31276
  if (!projects || projects.length === 0) {
31197
- return { ok: false, scope: "registry-unreadable", repos: [], scopeDetail: REGISTRY_UNREADABLE_DETAIL };
31277
+ return { ok: false, scope: "registry-unreadable", repos: [], excludedCount: 0, scopeDetail: REGISTRY_UNREADABLE_DETAIL };
31198
31278
  }
31199
31279
  const targets = collectRegistryRepos(projects);
31200
31280
  const repos = [];
31201
31281
  for (const repo of targets) {
31202
31282
  repos.push(await auditRepoCi(repo, deps));
31203
31283
  }
31204
- return { ok: repos.every((r) => r.ok), scope: "fleet", repos };
31284
+ const audited = repos.filter((r) => !r.excludedReason);
31285
+ return { ok: audited.length > 0 && audited.every((r) => r.ok), scope: "fleet", repos, excludedCount: repos.length - audited.length };
31205
31286
  }
31206
31287
  function renderCiAuditScopeLine(report) {
31207
31288
  if (report.scope === "registry-unreadable") {
31208
31289
  return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
31209
31290
  }
31291
+ const excluded = report.repos.filter((r) => r.excludedReason).length;
31292
+ const count = `repos audited: ${report.repos.length - excluded}${excluded ? ` \u2014 repos excluded: ${excluded}` : ""}`;
31210
31293
  if (report.scope === "single-repo") {
31211
- return `scope: single-repo (explicit --repo) \u2014 repos audited: ${report.repos.length} \u2014 this is NOT a fleet verdict`;
31294
+ return `scope: single-repo (explicit --repo) \u2014 ${count} \u2014 this is NOT a fleet verdict`;
31212
31295
  }
31213
- return `scope: fleet (registry roster) \u2014 repos audited: ${report.repos.length}`;
31296
+ return `scope: fleet (registry roster) \u2014 ${count}`;
31214
31297
  }
31215
31298
  function renderCiAuditMarkdown(report) {
31216
31299
  const lines2 = [
@@ -31218,16 +31301,16 @@ function renderCiAuditMarkdown(report) {
31218
31301
  "",
31219
31302
  renderCiAuditScopeLine(report),
31220
31303
  "",
31221
- `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
31304
+ `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok && !r.excludedReason).length}/${report.repos.filter((r) => !r.excludedReason).length} repos ready)`,
31222
31305
  "",
31223
31306
  "| Repo | Class | Policy | OK | Top gap |",
31224
31307
  "|------|-------|--------|----|---------|"
31225
31308
  ];
31226
31309
  for (const r of report.repos) {
31227
- const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
31228
- lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap} |`);
31310
+ const gap = r.excludedReason ? `EXCLUDED \u2014 ${r.excludedReason}` : r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
31311
+ lines2.push(`| ${r.repo} | ${r.class} | ${r.excludedReason ? "\u2014" : r.mergePolicy} | ${r.excludedReason ? "excluded" : r.ok ? "yes" : "no"} | ${gap} |`);
31229
31312
  }
31230
- const advisories = report.repos.flatMap((r) => r.checks.filter((c) => c.severity && c.label === PENDING_LANDING_LABEL).map((c) => `- ${c.severity === "warn" ? "WARN" : "INFO"} ${r.repo}: ${c.label} \u2014 ${c.detail ?? ""}`));
31313
+ const advisories = report.repos.flatMap((r) => r.checks.filter((c) => c.severity).map((c) => `- ${c.severity === "warn" ? "WARN" : "INFO"} ${r.repo}: ${c.label} \u2014 ${c.detail ?? ""}`));
31231
31314
  if (advisories.length) lines2.push("", ...advisories);
31232
31315
  return lines2.join("\n");
31233
31316
  }
@@ -31237,12 +31320,15 @@ function renderCiAuditText(report) {
31237
31320
  renderCiAuditScopeLine(report)
31238
31321
  ];
31239
31322
  for (const r of report.repos) {
31240
- const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
31323
+ const verdict = r.excludedReason ? "EXCLUDED" : r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
31241
31324
  lines2.push(`
31242
- ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
31325
+ ${r.repo} (${r.class}, policy=${r.excludedReason ? "not audited" : r.mergePolicy}) ${verdict}`);
31326
+ if (r.excludedReason) {
31327
+ lines2.push(` EXCLUDED \u2014 ${r.excludedReason}`);
31328
+ continue;
31329
+ }
31243
31330
  if (r.unauditedReason) {
31244
31331
  lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
31245
- continue;
31246
31332
  }
31247
31333
  for (const c of r.checks) {
31248
31334
  lines2.push(` ${!c.ok ? "FAIL" : c.severity === "warn" ? "WARN" : c.severity === "info" ? "INFO" : "OK"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
@@ -31261,6 +31347,10 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
31261
31347
  errors.push(unauditedReconcileError(report.unauditedReason));
31262
31348
  return { repo, applied, skipped, errors };
31263
31349
  }
31350
+ if (report.excludedReason) {
31351
+ skipped.push(`excluded \u2014 ${report.excludedReason}; no reconcile change made`);
31352
+ return { repo, applied, skipped, errors };
31353
+ }
31264
31354
  const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
31265
31355
  const needsPatch = mergeChecks.some((c) => !c.ok);
31266
31356
  if (!needsPatch) {
@@ -31564,6 +31654,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
31564
31654
  return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
31565
31655
  }
31566
31656
  const report = await auditRepoCi(repo, deps);
31657
+ if (report.excludedReason) {
31658
+ return { repo, applied: [], skipped: [`excluded \u2014 ${report.excludedReason}; no reconcile change made`], errors: [] };
31659
+ }
31567
31660
  if (report.unauditedReason) {
31568
31661
  return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
31569
31662
  }
@@ -45976,7 +46069,7 @@ async function checkDocsIndexAtHead(opts, deps) {
45976
46069
  return {
45977
46070
  ok: false,
45978
46071
  detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there (checked ${ref} over ${relDocs.length} indexed doc${relDocs.length === 1 ? "" : "s"})`,
45979
- fix: "run `mmi-cli oracle docs index --write`, commit docs/index.md, and push again before retrying `pr create`"
46072
+ fix: "run `mmi-cli oracle docs index --write`, commit docs/index.md, and push again before retrying `pr create`; freshness is checked at the pushed commit regardless of merge-queue state, so regenerate and commit docs/index.md BEFORE `pr create`, never defer it to after merge"
45980
46073
  };
45981
46074
  }
45982
46075
 
@@ -46066,6 +46159,10 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
46066
46159
  if (state?.toLowerCase() === "closed") continue;
46067
46160
  const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin, deps.appClient);
46068
46161
  if (!contest.contested) continue;
46162
+ if (isSameBranchClaimResume(contest, repo, { headBranch: deps.head, headSha: deps.headSha })) {
46163
+ await client.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } }).catch(() => void 0);
46164
+ continue;
46165
+ }
46069
46166
  const ref = `${repo}#${number}`;
46070
46167
  const commentsPath = `repos/${repo}/issues/${number}/comments`;
46071
46168
  const now = deps.now ?? Date.now;
@@ -48588,9 +48685,14 @@ ${list}`);
48588
48685
  if (docsCheck && !docsCheck.ok) {
48589
48686
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
48590
48687
  }
48591
- const claimRefusal = await prCreateClaimRefusal(body, o.repo);
48592
- if (claimRefusal) return fail(claimRefusal);
48593
48688
  const createHead = o.head ?? await gitOut(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "");
48689
+ const shaRef = o.head ? `refs/heads/${o.head.replace(/^[^/:]+:/, "")}` : "HEAD";
48690
+ const createHeadSha = await gitOut(["rev-parse", "--verify", "--quiet", shaRef]).catch(() => "");
48691
+ const claimRefusal = await prCreateClaimRefusal(body, o.repo, {
48692
+ head: createHead || void 0,
48693
+ headSha: createHeadSha || void 0
48694
+ });
48695
+ if (claimRefusal) return fail(claimRefusal);
48594
48696
  const mergedHeadWarning = await prCreateMergedHeadWarning({ head: createHead || void 0, repo: o.repo });
48595
48697
  if (mergedHeadWarning) process.stderr.write(`${mergedHeadWarning}
48596
48698
  `);
@@ -54737,9 +54839,13 @@ function parseBrokerAssignee(raw) {
54737
54839
  var SINGLE_SHOT_TIMEOUT_MS = 6e4;
54738
54840
  var BROKER_MAX_BUFFER = 64 * 1024 * 1024;
54739
54841
  var PR_QUEUE_QUERY = "query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){mergeQueueEntry{state position enqueuedAt estimatedTimeToMerge headCommit{oid}}}}}";
54842
+ function missingBrokerPrRepoMessage(subcommand, number) {
54843
+ const example = subcommand === "list" ? "mmi-cli devops read pr list --repo <owner/repo> --state open --json number,title,headRefName" : subcommand === "diff" ? `mmi-cli devops read pr diff ${number ?? "<number>"} --repo <owner/repo> --name-only` : `mmi-cli devops read pr queue ${number ?? "<number>"} --repo <owner/repo> --json`;
54844
+ return `read pr ${subcommand}: required option '--repo <owner/repo>' not specified \u2014 broker PR ${subcommand} reads do not infer the repo from a worktree's git origin; pass --repo <owner/repo> explicitly (e.g. \`${example}\`)`;
54845
+ }
54740
54846
  function buildPrQueueSpec(number, o) {
54741
54847
  const repo = parseBrokerRepo(o.repo, "--repo");
54742
- if (!repo) throw new Error("read pr queue: --repo is required");
54848
+ if (!repo) throw new Error(missingBrokerPrRepoMessage("queue", number));
54743
54849
  const [owner, name] = repo.split("/");
54744
54850
  return { program: "gh", args: ["api", "graphql", "-f", `query=${PR_QUEUE_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${parseBrokerId(number, "PR number")}`], timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
54745
54851
  }
@@ -54895,7 +55001,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
54895
55001
  return 0;
54896
55002
  } catch (e) {
54897
55003
  const err = e;
54898
- if (typeof err.stdout === "string" && err.stdout) write(redactPrivateCredential(err.stdout, env));
55004
+ if (typeof err.stdout === "string" && err.stdout) process.stderr.write(redactPrivateCredential(err.stdout, env));
54899
55005
  const code = ghExitCodeOf(e);
54900
55006
  if (code !== null) {
54901
55007
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
@@ -55021,13 +55127,16 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
55021
55127
  await dispatchGh(spec, "read run watch");
55022
55128
  });
55023
55129
  const pr = read.command("pr").description("pull-request reads \u2014 view/list/diff ride gh; queue uses fixed GraphQL; checks is REST-backed (deterministic closed-custody adapter; native was EMPTY only on the historical #3035 fixture)");
55024
- pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").requiredOption("--repo <owner/repo>", "target repo", repoOpt).option("--json [fields]", "native gh JSON fields", jsonOpt).addOption(new Option("--state <state>", "open|closed|merged|all").choices(["open", "closed", "merged", "all"])).option("--base <branch>", "base branch", branchOpt).option("--head <branch>", "head branch", branchOpt).option("--label <label>", "filter by label", labelOpt).option("--search <query>", "viewer-free search", (v) => parseBrokerPrSearch(v)).option("--assignee <login>", "explicit assignee (viewer refused)", assigneeOpt).option("--limit <n>", "maximum PRs (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
55130
+ pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json [fields]", "native gh JSON fields", jsonOpt).addOption(new Option("--state <state>", "open|closed|merged|all").choices(["open", "closed", "merged", "all"])).option("--base <branch>", "base branch", branchOpt).option("--head <branch>", "head branch", branchOpt).option("--label <label>", "filter by label", labelOpt).option("--search <query>", "viewer-free search", (v) => parseBrokerPrSearch(v)).option("--assignee <login>", "explicit assignee (viewer refused)", assigneeOpt).option("--limit <n>", "maximum PRs (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
55131
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("list"));
55025
55132
  await dispatchGh(buildPrListSpec(o), "read pr list");
55026
55133
  });
55027
- pr.command("diff").description("read a bounded PR diff on the App pool").requiredOption("--repo <owner/repo>", "target repo", repoOpt).option("--name-only", "only changed file names").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
55134
+ pr.command("diff").description("read a bounded PR diff on the App pool").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--name-only", "only changed file names").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
55135
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("diff", number));
55028
55136
  await dispatchGh(buildPrDiffSpec(number, o), "read pr diff");
55029
55137
  });
55030
- pr.command("queue").description("read one PR mergeQueueEntry (fixed GraphQL query, no permission widening)").requiredOption("--repo <owner/repo>", "target repo", repoOpt).option("--json", "machine-readable mergeQueueEntry (null when not queued)").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
55138
+ pr.command("queue").description("read one PR mergeQueueEntry (fixed GraphQL query, no permission widening)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json", "machine-readable mergeQueueEntry (null when not queued)").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
55139
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("queue", number));
55031
55140
  const spec = buildPrQueueSpec(number, o);
55032
55141
  requireBrokerApp("read pr queue");
55033
55142
  try {
@@ -55160,9 +55269,13 @@ var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
55160
55269
  var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
55161
55270
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
55162
55271
  var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
55272
+ var API_GET_RECOVERY = " No other GET read exists \u2014 do not retry path variants; use a dedicated mmi-cli subcommand or ask a human token holder. This refusal exits 1 on stderr with nothing on stdout: check the exit code and never merge stderr into stdout (2>&1) before JSON-parsing.";
55163
55273
  function parseApiGetPath(rawPath) {
55164
55274
  const path2 = rawPath.trim();
55165
- const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
55275
+ const refuse = () => {
55276
+ 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." : "";
55277
+ return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${hint}`);
55278
+ };
55166
55279
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
55167
55280
  const q = path2.indexOf("?");
55168
55281
  const base = q === -1 ? path2 : path2.slice(0, q);
@@ -55183,7 +55296,7 @@ function parseApiGetPath(rawPath) {
55183
55296
  if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
55184
55297
  return { kind: "contents", path: path2, paginate: false };
55185
55298
  }
55186
- throw new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
55299
+ throw refuse();
55187
55300
  }
55188
55301
  var ZIP_TIMEOUT_MS = 12e4;
55189
55302
  var ZIP_MAX_BUFFER = 512 * 1024 * 1024;
@@ -59180,7 +59293,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
59180
59293
  }
59181
59294
  const deps = ciAuditDeps2();
59182
59295
  const audit = await auditOrgCi(deps, o.repo);
59183
- const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : o.parkRuleset ? await Promise.all(audit.repos.map((r) => parkProductRuleset(r.repo, deps))) : [];
59296
+ const applyResults = o.apply ? await Promise.all(audit.repos.filter((r) => !r.excludedReason).map((r) => applyCiReconcileRepo(r.repo, deps))) : o.parkRuleset ? await Promise.all(audit.repos.filter((r) => !r.excludedReason).map((r) => parkProductRuleset(r.repo, deps))) : [];
59184
59297
  const payload = { audit, apply: applyResults };
59185
59298
  if (o.json) console.log(JSON.stringify(payload, null, 2));
59186
59299
  else {
@@ -59511,6 +59624,7 @@ function printHouseRootHelp(house) {
59511
59624
  consoleIo.log(lines2.join("\n"));
59512
59625
  }
59513
59626
  resolveVerbAliasShim(process.argv);
59627
+ normalizeWorkboardClaimArgv(process.argv);
59514
59628
  resolveHouseShim(process.argv);
59515
59629
  normalizeBoardClaimIssueArgv(process.argv);
59516
59630
  program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.209",
3
+ "version": "4.5.210",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",