@mutmutco/claude-plugin 4.5.209 → 4.5.211

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
@@ -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)) {
@@ -10413,6 +10608,10 @@ async function fetchTrainAuthority(repo, deps) {
10413
10608
  }
10414
10609
  const body = await res.json();
10415
10610
  if (typeof body?.train !== "boolean" || !body.role) return { ok: false, error: "malformed train-authority response" };
10611
+ if (body.authenticatedDelegation !== void 0 && (body.authenticatedDelegation !== true || !body.train || body.repo !== repo || body.delegatedActor !== body.login || !/^[A-Za-z0-9-]{1,39}$/.test(body.delegatedActor ?? ""))) {
10612
+ return { ok: false, error: "malformed delegated train-authority response" };
10613
+ }
10614
+ if (body.delegatedActor !== void 0 && body.authenticatedDelegation !== true) return { ok: false, error: "unverified delegated actor" };
10416
10615
  return { ok: true, authority: body };
10417
10616
  } catch (e) {
10418
10617
  return { ok: false, error: e.message };
@@ -15142,6 +15341,12 @@ function renderClaimReleaseBody(release) {
15142
15341
 
15143
15342
  <!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
15144
15343
  }
15344
+ function formatClaimMarker(actor, ts = (/* @__PURE__ */ new Date()).toISOString()) {
15345
+ const marker = { v: 1, session: actor.session, surface: actor.surface, host: actor.host, ts };
15346
+ return `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.
15347
+
15348
+ <!-- mmi-claim: ${JSON.stringify(marker)} -->`;
15349
+ }
15145
15350
  function latestClaimMarker(comments) {
15146
15351
  for (let i = comments.length - 1; i >= 0; i -= 1) {
15147
15352
  const body = comments[i].body;
@@ -15466,6 +15671,7 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
15466
15671
  `repos/${repo}/branches/${encodeURIComponent(out.branch)}`
15467
15672
  );
15468
15673
  out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
15674
+ out.branchSha = head?.commit?.sha ?? void 0;
15469
15675
  } catch {
15470
15676
  }
15471
15677
  }
@@ -15532,13 +15738,27 @@ function isSeatClaimMarker(marker, current, context = {}) {
15532
15738
  const state = context.sessionState ?? probeLocalClaimSession(marker, context.now);
15533
15739
  return state === "dead";
15534
15740
  }
15741
+ function isSameBranchClaimResume(contest, repo, landing = {}) {
15742
+ const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
15743
+ const headSha = landing.headSha?.trim();
15744
+ if (!headBranch && !headSha) return false;
15745
+ if (!contest.marker || contest.unverifiable.length > 0 || contest.live.length === 0) return false;
15746
+ const branchLine = /^live branch ([^\s]+) on /;
15747
+ const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
15748
+ const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
15749
+ const liveBranch = fromContest || fromLive;
15750
+ const matches = Boolean(headBranch && liveBranch && liveBranch === headBranch) || Boolean(headSha && contest.branchSha && contest.branchSha === headSha);
15751
+ if (!matches) return false;
15752
+ const ownBranchLine = liveBranch ? `live branch ${liveBranch} ` : "live branch ";
15753
+ return contest.live.every((line) => /^claim marker from /.test(line) || line.startsWith(ownBranchLine) && line.endsWith(` on ${repo}`));
15754
+ }
15535
15755
  function laneResumeEvidence(evidence) {
15536
15756
  if (!evidence.marker || evidence.sessionState !== "dead") return void 0;
15537
15757
  if (!isHostSessionId(evidence.marker.session)) return void 0;
15538
15758
  const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
15539
15759
  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
15760
  }
15541
- async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient) {
15761
+ async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient, landing) {
15542
15762
  const livenessClient = await selectLivenessClient(client, appClient);
15543
15763
  let evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
15544
15764
  if (evidence.failed.some((channel) => channel !== "session")) {
@@ -15550,10 +15770,21 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
15550
15770
  markerBy: evidence.markerBy,
15551
15771
  sessionState: evidence.sessionState
15552
15772
  });
15553
- const resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
15773
+ let resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
15554
15774
  const live = ownSeat ? [] : liveEvidenceLines(evidence, item.repository);
15555
15775
  const unverifiable = ownSeat ? [] : evidence.failed;
15556
- const contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
15776
+ let contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
15777
+ if (contested && landing && isSameBranchClaimResume({
15778
+ contested: true,
15779
+ live,
15780
+ unverifiable,
15781
+ marker: evidence.marker,
15782
+ branch: evidence.branch,
15783
+ branchSha: evidence.branchSha
15784
+ }, item.repository, landing)) {
15785
+ contested = false;
15786
+ resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
15787
+ }
15557
15788
  const carry = staleWorkLines(evidence);
15558
15789
  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
15790
  return {
@@ -15563,7 +15794,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
15563
15794
  ownership,
15564
15795
  marker: evidence.marker,
15565
15796
  resume,
15566
- ...staleStamp ? { staleStamp } : {}
15797
+ ...staleStamp ? { staleStamp } : {},
15798
+ ...evidence.branch ? { branch: evidence.branch } : {},
15799
+ ...evidence.branchSha ? { branchSha: evidence.branchSha } : {}
15567
15800
  };
15568
15801
  }
15569
15802
  function laneContestParts(contest) {
@@ -15576,12 +15809,16 @@ function isUnknownHold(contest) {
15576
15809
  return !contest.marker && contest.live.length === 0 && contest.unverifiable.length > 0;
15577
15810
  }
15578
15811
  var CONTESTED_CLAIM_RECOVERY = "leave the holder's work untouched and choose another unclaimed item or coordinate a handoff (use --force only for an intentional takeover)";
15812
+ function contestedClaimRecovery(ref) {
15813
+ const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
15814
+ return `${CONTESTED_CLAIM_RECOVERY}; list free items with: mmi-cli oracle board read --json${repo ? ` --repo ${repo}` : ""} and pick from its claimable list`;
15815
+ }
15579
15816
  function laneContestMessage(ref, contest, verb) {
15580
15817
  const { holder, why } = laneContestParts(contest);
15581
15818
  if (isUnknownHold(contest)) {
15582
- return verb === "claim" ? `${ref} cannot be verified as free: ${holder} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `refusing to unclaim ${ref}: ${holder} (${why}); the liveness reads failed so the unclaim is refused fail-closed \u2014 re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
15819
+ return verb === "claim" ? `${ref} cannot be verified as free: ${holder} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${contestedClaimRecovery(ref)}` : `refusing to unclaim ${ref}: ${holder} (${why}); the liveness reads failed so the unclaim is refused fail-closed \u2014 re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
15583
15820
  }
15584
- return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
15821
+ return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); ${contestedClaimRecovery(ref)}` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
15585
15822
  }
15586
15823
  function ghError(e) {
15587
15824
  const err = e;
@@ -15714,6 +15951,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
15714
15951
  cfg,
15715
15952
  client,
15716
15953
  items: collected.items,
15954
+ selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
15717
15955
  writable: writableOrUnknown(writable),
15718
15956
  report,
15719
15957
  session: deps.session ?? describeSessionIdentity(),
@@ -15825,7 +16063,9 @@ async function claimOneBoardItem(ctx, selector, options) {
15825
16063
  return;
15826
16064
  }
15827
16065
  if (!options.force) {
15828
- const refusal = laneContestMessage(item.ref, contest, "claim");
16066
+ 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);
16067
+ const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
16068
+ const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
15829
16069
  throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
15830
16070
  }
15831
16071
  previousHolder = displaced();
@@ -19421,10 +19661,10 @@ var rollout_plan_default = {
19421
19661
  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
19662
  },
19423
19663
  baseline: {
19424
- version: "4.5.209",
19425
- tag: "v4.5.209",
19426
- commit: "2449709d554c",
19427
- npm: "@mutmutco/cli@4.5.209"
19664
+ version: "4.5.211",
19665
+ tag: "v4.5.211",
19666
+ commit: "cbbcc24fc015",
19667
+ npm: "@mutmutco/cli@4.5.211"
19428
19668
  },
19429
19669
  exitCriterion: "fleet-n-of-n",
19430
19670
  hubOnlyShortcut: "forbidden",
@@ -19441,14 +19681,14 @@ var rollout_plan_default = {
19441
19681
  repo: "mutmutco/mmi-hub",
19442
19682
  role: "canary",
19443
19683
  schedule: "train",
19444
- v3Target: "v4.5.209"
19684
+ v3Target: "v4.5.211"
19445
19685
  }
19446
19686
  ],
19447
19687
  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
19688
  rollback: {
19449
19689
  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)"
19690
+ mechanism: "npm dist-tag latest -> 4.5.211 and redeploy the Hub Lambda from tag v4.5.211 (cbbcc24fc015); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19691
+ v3Target: "v4.5.211 (@mutmutco/cli@4.5.211, tag commit cbbcc24fc015 \u2014 last known-good release carrying the repo-index v4-only contract)"
19452
19692
  }
19453
19693
  },
19454
19694
  {
@@ -21592,9 +21832,12 @@ async function buildTrainApplyContext(deps) {
21592
21832
  const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
21593
21833
  const [owner, name] = repo.split("/");
21594
21834
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
21595
- const login = await readTrainLogin(deps);
21596
21835
  const verdict = await deps.trainAuthority(repo);
21597
21836
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
21837
+ if (verdict.authenticatedDelegation === true && (!verdict.train || !/^[A-Za-z0-9-]{1,39}$/.test(verdict.delegatedActor ?? ""))) {
21838
+ throw new Error("invalid delegated train authority");
21839
+ }
21840
+ const login = verdict.authenticatedDelegation === true ? verdict.delegatedActor : await readTrainLogin(deps);
21598
21841
  if (!verdict.train) {
21599
21842
  throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
21600
21843
  }
@@ -23952,46 +24195,51 @@ var import_node_fs30 = require("node:fs");
23952
24195
  var import_promises4 = require("node:fs/promises");
23953
24196
  var import_node_path28 = require("node:path");
23954
24197
 
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";
24198
+ // ../shared/runner-fallback.ts
24199
+ var HOSTED_RUNNER_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/i;
24200
+ function classifyRunnerCost(selector) {
24201
+ const raw = selector.replace(/\s+#.*$/gm, "").trim();
24202
+ if (HOSTED_RUNNER_LABEL_RE.test(raw)) return "hosted";
24203
+ const json = /^\$\{\{\s*fromJSON\('([^']+)'\)\s*\}\}$/.exec(raw);
24204
+ if (json) {
24205
+ try {
24206
+ const labels2 = JSON.parse(json[1]);
24207
+ 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";
24208
+ } catch {
24209
+ return "unknown";
24210
+ }
23978
24211
  }
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
- );
24212
+ if (raw.includes("${{")) return "unknown";
24213
+ 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")];
24214
+ return labels.every((l) => /^[\w-]+$/.test(l)) && labels.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
23989
24215
  }
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
- );
24216
+ var STANDARD_HOSTED_ALLOWLIST = [
24217
+ ["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"]
24218
+ ];
24219
+ function standardHostedLabelFor(repo, path2, jobId) {
24220
+ return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
24221
+ }
24222
+ function classifyJobRunnerCost(selector, jobBody, standardHostedLabel) {
24223
+ const direct = classifyRunnerCost(selector);
24224
+ const matrix = /^\$\{\{\s*matrix\.([\w-]+)\s*\}\}$/.exec(selector.trim());
24225
+ if (!matrix || direct === "hosted") return direct;
24226
+ const strategy = /^ strategy:\s*\n((?: {6,}[^\n]*\n?|\s*\n)*)/m.exec(jobBody)?.[1];
24227
+ const matrixBody = strategy && /^ matrix:[^\S\n]*\n((?: {8,}[^\n]*\n?|\s*\n)*)/m.exec(strategy)?.[1];
24228
+ const include = matrixBody && /^ include:[^\S\n]*\n((?: {10,}[^\n]*\n?|\s*\n)*)$/.exec(matrixBody)?.[1];
24229
+ if (!include) return "unknown";
24230
+ const rows = include.split(/^ {10}- /m).slice(1);
24231
+ if (!rows.length) return "unknown";
24232
+ const costs = rows.map((row) => {
24233
+ const match = new RegExp(`^(?: {12})?${matrix[1]}:\\s*(.+)$`, "m").exec(row);
24234
+ if (standardHostedLabel && match?.[1].trim() === standardHostedLabel) return "standard-hosted";
24235
+ return match ? classifyRunnerCost(match[1]) : "unknown";
24236
+ });
24237
+ if (costs.includes("hosted")) return "hosted";
24238
+ if (costs.includes("unknown")) return "unknown";
24239
+ return costs.includes("standard-hosted") ? "standard-hosted" : "self-hosted";
23994
24240
  }
24241
+
24242
+ // src/actions-billing-preflight.ts
23995
24243
  function parseJson(raw, label) {
23996
24244
  try {
23997
24245
  return JSON.parse(raw);
@@ -23999,153 +24247,31 @@ function parseJson(raw, label) {
23999
24247
  throw new Error(`${label} was not valid JSON`);
24000
24248
  }
24001
24249
  }
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));
24250
+ function workflowNeedsHostedRunner(yaml, localWorkflows = /* @__PURE__ */ new Set(), where) {
24251
+ const jobs = parseWorkflowJobs(yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n"));
24252
+ return !jobs.length || jobs.some((job) => {
24253
+ const callee = /^ uses:\s*\.\/(.+)$/m.exec(job.body)?.[1];
24254
+ if (callee && localWorkflows.has(callee)) return false;
24255
+ const section = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
24256
+ const selector = section.split("\n").map((line) => line.trim()).join("\n");
24257
+ const cost = classifyJobRunnerCost(selector, job.body, where && standardHostedLabelFor(where.repo, where.path, job.id));
24258
+ return cost !== "self-hosted" && cost !== "standard-hosted";
24259
+ });
24136
24260
  }
24137
24261
  async function hostedWorkflowsOf(deps, repo) {
24138
24262
  const list = parseJson(
24139
24263
  await deps.run("gh", ["api", `repos/${repo}/contents/.github/workflows`]),
24140
24264
  `${repo} workflow listing`
24141
24265
  );
24142
- const hosted = [];
24266
+ if (!Array.isArray(list) || !list.some((f) => f.type === "file" && /\.ya?ml$/.test(f.path ?? ""))) throw new Error("no workflow inventory");
24267
+ const workflows = [];
24143
24268
  for (const f of list) {
24144
- if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path) || f.name === CANARY_WORKFLOW) continue;
24269
+ if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)) continue;
24145
24270
  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);
24271
+ workflows.push({ path: f.path, body: Buffer.from(raw.content ?? "", "base64").toString("utf8") });
24147
24272
  }
24148
- return hosted;
24273
+ const local = new Set(workflows.map((file) => file.path));
24274
+ return workflows.filter((file) => workflowNeedsHostedRunner(file.body, local, { repo, path: file.path })).map((file) => file.path);
24149
24275
  }
24150
24276
  async function assertActionsJobsCanStart(deps, targetRepo2) {
24151
24277
  let hosted;
@@ -24155,50 +24281,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
24155
24281
  hosted = void 0;
24156
24282
  }
24157
24283
  if (hosted && hosted.length === 0) {
24158
- process.stderr.write(`actions preflight: ${targetRepo2} has no automatically triggered GitHub-hosted job; hosted canary skipped (#7721)
24284
+ process.stderr.write(`actions preflight: ${targetRepo2} source selectors are self-hosted only; no canary dispatch; access/isolation/execution unproven (#8911)
24159
24285
  `);
24160
24286
  return;
24161
24287
  }
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
- );
24288
+ 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
24289
  }
24203
24290
 
24204
24291
  // src/train-apply-recovery.ts
@@ -25734,6 +25821,9 @@ function failedRunIdentity(run, repo) {
25734
25821
  function manualRunAction(run, repo) {
25735
25822
  return `safe next action: diagnose with gh run view ${run.runId} --repo ${repo} --log-failed; rerun by hand only when the cause is fixed: gh run rerun ${run.runId} --repo ${repo} --failed (never re-tag)`;
25736
25823
  }
25824
+ function runsOnShaAction(repo, sha) {
25825
+ return `safe next action: mmi-cli devops read run list --repo ${repo} --commit ${sha} --json databaseId,workflowName,event,status,conclusion to identify the run`;
25826
+ }
25737
25827
  async function readFailedRunsOnSha(deps, repo, sha) {
25738
25828
  try {
25739
25829
  const rows = JSON.parse(await deps.run("gh", [
@@ -25809,7 +25899,7 @@ async function correlateCandidateRunsToRequiredContexts(deps, repo, sha, candida
25809
25899
  if (contexts.length === 0) {
25810
25900
  return {
25811
25901
  unique: false,
25812
- note: `the failure names no failed required context \u2014 rerunning the first failed run could rerun an unrelated workflow; manual intervention required; safe next action: gh run list --repo ${repo} --commit ${sha.slice(0, 12)} to identify the run`
25902
+ note: `the failure names no failed required context \u2014 rerunning the first failed run could rerun an unrelated workflow; manual intervention required; ${runsOnShaAction(repo, sha)}`
25813
25903
  };
25814
25904
  }
25815
25905
  const failedChecks = await readFailedCheckRunsOnSha(deps, repo, sha);
@@ -25862,7 +25952,7 @@ async function transientRecoveryForFailedPhase(deps, input) {
25862
25952
  };
25863
25953
  }
25864
25954
  if (candidates.length === 0) {
25865
- return { reran: false, recovered: false, note: `no failed run with an id could be classified on ${sha.slice(0, 12)} \u2014 manual intervention required (no marker proof, no automatic rerun); safe next action: gh run list --repo ${input.repo} --commit ${sha.slice(0, 12)} to identify the run` };
25955
+ return { reran: false, recovered: false, note: `no failed run with an id could be classified on ${sha.slice(0, 12)} \u2014 manual intervention required (no marker proof, no automatic rerun); ${runsOnShaAction(input.repo, sha)}` };
25866
25956
  }
25867
25957
  if (enumerated && phase === "ordinaryCi") {
25868
25958
  const correlation = await correlateCandidateRunsToRequiredContexts(deps, input.repo, sha, candidates, extractFailedRequiredContexts(failure));
@@ -30301,33 +30391,6 @@ function registryRepoKey(repo) {
30301
30391
  return repo.toLowerCase();
30302
30392
  }
30303
30393
 
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
30394
  // src/ci-audit.ts
30332
30395
  var HUB_REPO4 = "mutmutco/MMI-Hub";
30333
30396
  var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
@@ -30392,20 +30455,18 @@ function auditRunnerFallbacks(repo, files) {
30392
30455
  for (const { path: path2, body } of files) {
30393
30456
  const jobs = parseWorkflowJobs(body.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n"));
30394
30457
  for (const job of jobs) {
30395
- const runsOn = /^ runs-on:\s*(.+)$/m.exec(job.body)?.[1] ?? "";
30396
30458
  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;
30459
+ const callee = /^ uses:\s*(.+)$/m.exec(job.body)?.[1];
30460
+ if (callee?.startsWith("./") && files.some((file) => file.path === callee.slice(2))) continue;
30461
+ if (callee && /^mutmutco\/MMI-Hub\/\.github\/workflows\/[\w.-]+\.ya?ml@[\w./-]+$/.test(callee)) continue;
30462
+ const standardHostedLabel = standardHostedLabelFor(repo, path2, job.id);
30463
+ const cost = classifyJobRunnerCost(runnerSection.split("\n").map((line) => line.trim()).join("\n"), job.body, standardHostedLabel);
30464
+ const ok = !callee && (cost === "self-hosted" || cost === "standard-hosted");
30404
30465
  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.`
30466
+ ok,
30467
+ label: cost === "standard-hosted" ? `runner uses exact standard-hosted exception (#8912): ${path2}:${job.id}` : `runner is self-hosted only (#8911): ${path2}:${job.id}`,
30468
+ 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",
30469
+ 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
30470
  });
30410
30471
  }
30411
30472
  }
@@ -30630,7 +30691,7 @@ function rulesetCoversReleaseBranches(payload, track) {
30630
30691
  function releaseBranchRefsCovered(conditions, track) {
30631
30692
  const include = conditions?.ref_name?.include;
30632
30693
  if (!Array.isArray(include)) return [];
30633
- const releaseRefs = track === "full" ? ["refs/heads/rc", "refs/heads/main"] : ["refs/heads/main"];
30694
+ const releaseRefs = branchesForTrack(track).filter((branch) => branch !== "development").map((branch) => `refs/heads/${branch}`);
30634
30695
  return releaseRefs.filter((ref) => include.some((entry) => typeof entry === "string" && refPatternCovers(entry, ref)));
30635
30696
  }
30636
30697
  function unreachableReleaseContexts(input) {
@@ -30859,6 +30920,18 @@ async function auditRepoCi(repo, callerDeps) {
30859
30920
  const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
30860
30921
  const deployableGated = repoClass === "deployable" && !explicitNoCi;
30861
30922
  const info = await restJson2(deps, `repos/${repo}`, {});
30923
+ if (info.archived === true) {
30924
+ return {
30925
+ repo,
30926
+ class: repoClass,
30927
+ mergePolicy: "wait-for-checks",
30928
+ ok: false,
30929
+ checks: [],
30930
+ explicitNoCi,
30931
+ excludedReason: "GitHub archived repository (read-only)"
30932
+ };
30933
+ }
30934
+ let releaseReadFailure;
30862
30935
  const baseBranch = repoClass === "content" ? "main" : "development";
30863
30936
  checks.push({
30864
30937
  ok: info.allow_auto_merge === true,
@@ -31061,14 +31134,29 @@ async function auditRepoCi(repo, callerDeps) {
31061
31134
  const releaseBranches = releaseBranchRefsCovered(productRuleset.conditions, resolveReleaseTrack(meta, void 0, repo));
31062
31135
  if (releaseBranches.length > 0) {
31063
31136
  const emittedByBranch = {};
31137
+ const absent = [];
31138
+ const unreadable = [];
31064
31139
  for (const ref of releaseBranches) {
31065
- emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
31140
+ const branch = ref.replace("refs/heads/", "");
31141
+ const present = await branchPresence(deps, repo, branch);
31142
+ if (present === false) absent.push(branch);
31143
+ else if (present === void 0) unreadable.push(branch);
31144
+ else {
31145
+ emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, branch);
31146
+ if (emittedByBranch[ref] === void 0) unreadable.push(branch);
31147
+ }
31066
31148
  }
31149
+ if (unreadable.length) releaseReadFailure = `release branch reachability unreadable: [${unreadable.join(", ")}] \u2014 no emitter conclusion`;
31150
+ const presenceNote = [
31151
+ absent.length ? `absent: [${absent.join(", ")}] \u2014 no emitter requirement audited` : "",
31152
+ unreadable.length ? `unreadable: [${unreadable.join(", ")}] \u2014 cannot audit emitters` : ""
31153
+ ].filter(Boolean).join("; ");
31067
31154
  const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
31068
31155
  checks.push({
31069
31156
  ok: unreachable.length === 0,
31070
31157
  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",
31158
+ severity: unreadable.length ? "warn" : absent.length ? "info" : void 0,
31159
+ 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
31160
  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
31161
  });
31074
31162
  }
@@ -31124,7 +31212,7 @@ async function auditRepoCi(repo, callerDeps) {
31124
31212
  const body = await fetchFileContent(deps, repo, baseBranch, path2);
31125
31213
  if (body == null) continue;
31126
31214
  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)) {
31215
+ 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
31216
  if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
31129
31217
  else missing.push(path2);
31130
31218
  }
@@ -31183,34 +31271,45 @@ async function auditRepoCi(repo, callerDeps) {
31183
31271
  if (failure) {
31184
31272
  return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
31185
31273
  }
31186
- const ok = checks.every((c) => c.ok);
31187
- return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
31274
+ const ok = !releaseReadFailure && checks.every((c) => c.ok);
31275
+ return {
31276
+ repo,
31277
+ class: repoClass,
31278
+ mergePolicy: policy,
31279
+ ok,
31280
+ checks,
31281
+ explicitNoCi,
31282
+ ...releaseReadFailure ? { unauditedReason: releaseReadFailure } : {}
31283
+ };
31188
31284
  }
31189
31285
  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
31286
  async function auditOrgCi(deps, repoFilter) {
31191
31287
  if (repoFilter) {
31192
31288
  const report = await auditRepoCi(repoFilter, deps);
31193
- return { ok: report.ok, scope: "single-repo", repos: [report] };
31289
+ return { ok: report.ok, scope: "single-repo", repos: [report], excludedCount: report.excludedReason ? 1 : 0 };
31194
31290
  }
31195
31291
  const projects = await deps.listProjects();
31196
31292
  if (!projects || projects.length === 0) {
31197
- return { ok: false, scope: "registry-unreadable", repos: [], scopeDetail: REGISTRY_UNREADABLE_DETAIL };
31293
+ return { ok: false, scope: "registry-unreadable", repos: [], excludedCount: 0, scopeDetail: REGISTRY_UNREADABLE_DETAIL };
31198
31294
  }
31199
31295
  const targets = collectRegistryRepos(projects);
31200
31296
  const repos = [];
31201
31297
  for (const repo of targets) {
31202
31298
  repos.push(await auditRepoCi(repo, deps));
31203
31299
  }
31204
- return { ok: repos.every((r) => r.ok), scope: "fleet", repos };
31300
+ const audited = repos.filter((r) => !r.excludedReason);
31301
+ return { ok: audited.length > 0 && audited.every((r) => r.ok), scope: "fleet", repos, excludedCount: repos.length - audited.length };
31205
31302
  }
31206
31303
  function renderCiAuditScopeLine(report) {
31207
31304
  if (report.scope === "registry-unreadable") {
31208
31305
  return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
31209
31306
  }
31307
+ const excluded = report.repos.filter((r) => r.excludedReason).length;
31308
+ const count = `repos audited: ${report.repos.length - excluded}${excluded ? ` \u2014 repos excluded: ${excluded}` : ""}`;
31210
31309
  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`;
31310
+ return `scope: single-repo (explicit --repo) \u2014 ${count} \u2014 this is NOT a fleet verdict`;
31212
31311
  }
31213
- return `scope: fleet (registry roster) \u2014 repos audited: ${report.repos.length}`;
31312
+ return `scope: fleet (registry roster) \u2014 ${count}`;
31214
31313
  }
31215
31314
  function renderCiAuditMarkdown(report) {
31216
31315
  const lines2 = [
@@ -31218,16 +31317,16 @@ function renderCiAuditMarkdown(report) {
31218
31317
  "",
31219
31318
  renderCiAuditScopeLine(report),
31220
31319
  "",
31221
- `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
31320
+ `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok && !r.excludedReason).length}/${report.repos.filter((r) => !r.excludedReason).length} repos ready)`,
31222
31321
  "",
31223
31322
  "| Repo | Class | Policy | OK | Top gap |",
31224
31323
  "|------|-------|--------|----|---------|"
31225
31324
  ];
31226
31325
  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} |`);
31326
+ const gap = r.excludedReason ? `EXCLUDED \u2014 ${r.excludedReason}` : r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
31327
+ lines2.push(`| ${r.repo} | ${r.class} | ${r.excludedReason ? "\u2014" : r.mergePolicy} | ${r.excludedReason ? "excluded" : r.ok ? "yes" : "no"} | ${gap} |`);
31229
31328
  }
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 ?? ""}`));
31329
+ 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
31330
  if (advisories.length) lines2.push("", ...advisories);
31232
31331
  return lines2.join("\n");
31233
31332
  }
@@ -31237,12 +31336,15 @@ function renderCiAuditText(report) {
31237
31336
  renderCiAuditScopeLine(report)
31238
31337
  ];
31239
31338
  for (const r of report.repos) {
31240
- const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
31339
+ const verdict = r.excludedReason ? "EXCLUDED" : r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
31241
31340
  lines2.push(`
31242
- ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
31341
+ ${r.repo} (${r.class}, policy=${r.excludedReason ? "not audited" : r.mergePolicy}) ${verdict}`);
31342
+ if (r.excludedReason) {
31343
+ lines2.push(` EXCLUDED \u2014 ${r.excludedReason}`);
31344
+ continue;
31345
+ }
31243
31346
  if (r.unauditedReason) {
31244
31347
  lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
31245
- continue;
31246
31348
  }
31247
31349
  for (const c of r.checks) {
31248
31350
  lines2.push(` ${!c.ok ? "FAIL" : c.severity === "warn" ? "WARN" : c.severity === "info" ? "INFO" : "OK"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
@@ -31261,6 +31363,10 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
31261
31363
  errors.push(unauditedReconcileError(report.unauditedReason));
31262
31364
  return { repo, applied, skipped, errors };
31263
31365
  }
31366
+ if (report.excludedReason) {
31367
+ skipped.push(`excluded \u2014 ${report.excludedReason}; no reconcile change made`);
31368
+ return { repo, applied, skipped, errors };
31369
+ }
31264
31370
  const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
31265
31371
  const needsPatch = mergeChecks.some((c) => !c.ok);
31266
31372
  if (!needsPatch) {
@@ -31564,6 +31670,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
31564
31670
  return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
31565
31671
  }
31566
31672
  const report = await auditRepoCi(repo, deps);
31673
+ if (report.excludedReason) {
31674
+ return { repo, applied: [], skipped: [`excluded \u2014 ${report.excludedReason}; no reconcile change made`], errors: [] };
31675
+ }
31567
31676
  if (report.unauditedReason) {
31568
31677
  return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
31569
31678
  }
@@ -45976,7 +46085,7 @@ async function checkDocsIndexAtHead(opts, deps) {
45976
46085
  return {
45977
46086
  ok: false,
45978
46087
  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`"
46088
+ 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
46089
  };
45981
46090
  }
45982
46091
 
@@ -46066,6 +46175,10 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
46066
46175
  if (state?.toLowerCase() === "closed") continue;
46067
46176
  const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin, deps.appClient);
46068
46177
  if (!contest.contested) continue;
46178
+ if (isSameBranchClaimResume(contest, repo, { headBranch: deps.head, headSha: deps.headSha })) {
46179
+ await client.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } }).catch(() => void 0);
46180
+ continue;
46181
+ }
46069
46182
  const ref = `${repo}#${number}`;
46070
46183
  const commentsPath = `repos/${repo}/issues/${number}/comments`;
46071
46184
  const now = deps.now ?? Date.now;
@@ -48588,9 +48701,14 @@ ${list}`);
48588
48701
  if (docsCheck && !docsCheck.ok) {
48589
48702
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
48590
48703
  }
48591
- const claimRefusal = await prCreateClaimRefusal(body, o.repo);
48592
- if (claimRefusal) return fail(claimRefusal);
48593
48704
  const createHead = o.head ?? await gitOut(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "");
48705
+ const shaRef = o.head ? `refs/heads/${o.head.replace(/^[^/:]+:/, "")}` : "HEAD";
48706
+ const createHeadSha = await gitOut(["rev-parse", "--verify", "--quiet", shaRef]).catch(() => "");
48707
+ const claimRefusal = await prCreateClaimRefusal(body, o.repo, {
48708
+ head: createHead || void 0,
48709
+ headSha: createHeadSha || void 0
48710
+ });
48711
+ if (claimRefusal) return fail(claimRefusal);
48594
48712
  const mergedHeadWarning = await prCreateMergedHeadWarning({ head: createHead || void 0, repo: o.repo });
48595
48713
  if (mergedHeadWarning) process.stderr.write(`${mergedHeadWarning}
48596
48714
  `);
@@ -49743,7 +49861,12 @@ function trainApplyDeps() {
49743
49861
  runSelf: async (args) => (await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(args)], { timeout: 3e4 })).stdout,
49744
49862
  trainAuthority: async (repo) => {
49745
49863
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
49746
- return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
49864
+ return verdict.ok ? {
49865
+ ok: true,
49866
+ role: verdict.authority.role,
49867
+ train: verdict.authority.train,
49868
+ ...verdict.authority.authenticatedDelegation === true ? { authenticatedDelegation: true, delegatedActor: verdict.authority.delegatedActor } : {}
49869
+ } : verdict;
49747
49870
  },
49748
49871
  // Hub-App-authority dispatch of the central tenant deploy (#953) — the Hub fires the
49749
49872
  // workflow_dispatch with its App token, so the caller needs no MMI-Hub Actions write.
@@ -54737,9 +54860,13 @@ function parseBrokerAssignee(raw) {
54737
54860
  var SINGLE_SHOT_TIMEOUT_MS = 6e4;
54738
54861
  var BROKER_MAX_BUFFER = 64 * 1024 * 1024;
54739
54862
  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}}}}}";
54863
+ function missingBrokerPrRepoMessage(subcommand, number) {
54864
+ 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`;
54865
+ 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}\`)`;
54866
+ }
54740
54867
  function buildPrQueueSpec(number, o) {
54741
54868
  const repo = parseBrokerRepo(o.repo, "--repo");
54742
- if (!repo) throw new Error("read pr queue: --repo is required");
54869
+ if (!repo) throw new Error(missingBrokerPrRepoMessage("queue", number));
54743
54870
  const [owner, name] = repo.split("/");
54744
54871
  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
54872
  }
@@ -54793,12 +54920,19 @@ function parseBrokerCreated(v) {
54793
54920
  if (!CREATED_RE.test(v)) throw new Error(`--created must be a GitHub created filter like ">=2026-10-11T00:30:00Z" (got ${JSON.stringify(v)})`);
54794
54921
  return v;
54795
54922
  }
54923
+ function parseBrokerCommit(v) {
54924
+ if (v === void 0) return void 0;
54925
+ if (!/^[0-9a-f]{40}$/i.test(v)) throw new Error(`read: --commit must be a full 40-character commit SHA (received '${v}')`);
54926
+ return v;
54927
+ }
54796
54928
  function buildRunListSpec(o) {
54797
54929
  const args = withJson(withRepo(["run", "list"], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
54798
54930
  const workflow = parseBrokerWorkflow(o.workflow);
54799
54931
  if (workflow) args.push("--workflow", workflow);
54800
54932
  const branch = parseBrokerBranch(o.branch);
54801
54933
  if (branch) args.push("--branch", branch);
54934
+ const commit = parseBrokerCommit(o.commit);
54935
+ if (commit) args.push("--commit", commit);
54802
54936
  const status = parseBrokerRunStatus(o.status);
54803
54937
  if (status) args.push("--status", status);
54804
54938
  const created = parseBrokerCreated(o.created);
@@ -54895,7 +55029,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
54895
55029
  return 0;
54896
55030
  } catch (e) {
54897
55031
  const err = e;
54898
- if (typeof err.stdout === "string" && err.stdout) write(redactPrivateCredential(err.stdout, env));
55032
+ if (typeof err.stdout === "string" && err.stdout) process.stderr.write(redactPrivateCredential(err.stdout, env));
54899
55033
  const code = ghExitCodeOf(e);
54900
55034
  if (code !== null) {
54901
55035
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
@@ -55002,7 +55136,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
55002
55136
  }
55003
55137
  await dispatchGh(spec, "read run view");
55004
55138
  });
55005
- run.command("list").description("list workflow runs (workflow/branch/status/created/limit/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--workflow <name>", "filter by workflow name or file", workflowOpt).option("--branch <name>", "filter by branch", branchOpt).option("--status <status>", "filter by run status", runStatusOpt).option("--created <filter>", 'only runs created in this window, e.g. ">=2026-10-11T00:30:00Z" (#8096)', (v) => parseBrokerCreated(v)).option("--limit <n>", "maximum runs to fetch (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
55139
+ run.command("list").description("list workflow runs (workflow/branch/commit/status/created/limit/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--workflow <name>", "filter by workflow name or file", workflowOpt).option("--branch <name>", "filter by branch", branchOpt).option("--commit <sha>", "filter by the full 40-character head commit SHA (#8937)", (v) => parseBrokerCommit(v)).option("--status <status>", "filter by run status", runStatusOpt).option("--created <filter>", 'only runs created in this window, e.g. ">=2026-10-11T00:30:00Z" (#8096)', (v) => parseBrokerCreated(v)).option("--limit <n>", "maximum runs to fetch (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
55006
55140
  let spec;
55007
55141
  try {
55008
55142
  spec = buildRunListSpec(o);
@@ -55021,13 +55155,16 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
55021
55155
  await dispatchGh(spec, "read run watch");
55022
55156
  });
55023
55157
  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) => {
55158
+ 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) => {
55159
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("list"));
55025
55160
  await dispatchGh(buildPrListSpec(o), "read pr list");
55026
55161
  });
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) => {
55162
+ 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) => {
55163
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("diff", number));
55028
55164
  await dispatchGh(buildPrDiffSpec(number, o), "read pr diff");
55029
55165
  });
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) => {
55166
+ 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) => {
55167
+ if (!o.repo) return fail(missingBrokerPrRepoMessage("queue", number));
55031
55168
  const spec = buildPrQueueSpec(number, o);
55032
55169
  requireBrokerApp("read pr queue");
55033
55170
  try {
@@ -55160,9 +55297,13 @@ var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
55160
55297
  var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
55161
55298
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
55162
55299
  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>)";
55300
+ 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
55301
  function parseApiGetPath(rawPath) {
55164
55302
  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}`);
55303
+ const refuse = () => {
55304
+ 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." : "";
55305
+ return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${hint}`);
55306
+ };
55166
55307
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
55167
55308
  const q = path2.indexOf("?");
55168
55309
  const base = q === -1 ? path2 : path2.slice(0, q);
@@ -55183,7 +55324,7 @@ function parseApiGetPath(rawPath) {
55183
55324
  if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
55184
55325
  return { kind: "contents", path: path2, paginate: false };
55185
55326
  }
55186
- throw new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
55327
+ throw refuse();
55187
55328
  }
55188
55329
  var ZIP_TIMEOUT_MS = 12e4;
55189
55330
  var ZIP_MAX_BUFFER = 512 * 1024 * 1024;
@@ -59180,7 +59321,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
59180
59321
  }
59181
59322
  const deps = ciAuditDeps2();
59182
59323
  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))) : [];
59324
+ 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
59325
  const payload = { audit, apply: applyResults };
59185
59326
  if (o.json) console.log(JSON.stringify(payload, null, 2));
59186
59327
  else {
@@ -59511,6 +59652,7 @@ function printHouseRootHelp(house) {
59511
59652
  consoleIo.log(lines2.join("\n"));
59512
59653
  }
59513
59654
  resolveVerbAliasShim(process.argv);
59655
+ normalizeWorkboardClaimArgv(process.argv);
59514
59656
  resolveHouseShim(process.argv);
59515
59657
  normalizeBoardClaimIssueArgv(process.argv);
59516
59658
  program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));