@mutmutco/cli 4.6.30 → 4.6.32

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 +408 -60
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -24526,19 +24526,7 @@ function formatBulkClaimSummary(results) {
24526
24526
  const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
24527
24527
  return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
24528
24528
  }
24529
- async function claimBoardIssues(options, deps = {}) {
24530
- const cfg = resolveBoardConfig(options.config);
24531
- const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24532
- const selectors = [];
24533
- const seen = /* @__PURE__ */ new Set();
24534
- for (const raw of options.selectors) {
24535
- const selector = parseIssueSelector(raw, collected.repo, options.repo);
24536
- const key = `${selector.repo.toLowerCase()}#${selector.number}`;
24537
- if (seen.has(key)) continue;
24538
- seen.add(key);
24539
- selectors.push(selector);
24540
- }
24541
- const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
24529
+ async function runBulkClaims(ctx, selectors, options) {
24542
24530
  const results = new Array(selectors.length);
24543
24531
  let next = 0;
24544
24532
  const worker = async () => {
@@ -24555,6 +24543,71 @@ async function claimBoardIssues(options, deps = {}) {
24555
24543
  }
24556
24544
  };
24557
24545
  await Promise.all(Array.from({ length: Math.min(CLAIM_CONCURRENCY, selectors.length) }, () => worker()));
24546
+ return results;
24547
+ }
24548
+ function claimArgsError(refs, all) {
24549
+ if (all && refs.length) return `--all claims the whole board; drop the explicit refs (${refs.join(" ")}) or drop --all`;
24550
+ if (!all && !refs.length) return "missing issue ref: pass one or more refs, or --all for every open claimable item on the board";
24551
+ return void 0;
24552
+ }
24553
+ async function claimAllBoardIssues(options, deps = {}) {
24554
+ const cfg = resolveBoardConfig(options.config);
24555
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24556
+ const ctx = await prepareClaimContext(options, [], deps, collected, snapshot);
24557
+ const { primary } = ctx.report;
24558
+ const assignee = options.assignee ?? "@me";
24559
+ const login = assignee === "@me" ? ctx.report.viewer : assignee.replace(/^@/, "");
24560
+ const selectors = [];
24561
+ const skipped = [];
24562
+ const seen = /* @__PURE__ */ new Set();
24563
+ for (const item of [...primary.userOwned, ...primary.claimable, ...primary.unownedInFlight, ...primary.taken]) {
24564
+ const key = item.ref.toLowerCase();
24565
+ if (seen.has(key)) continue;
24566
+ seen.add(key);
24567
+ if (item.contentType !== "Issue") {
24568
+ skipped.push({ ref: item.ref, reason: "not an issue" });
24569
+ continue;
24570
+ }
24571
+ if (item.state !== "OPEN") {
24572
+ skipped.push({ ref: item.ref, reason: `issue is ${item.state.toLowerCase()} but its card is ${item.status}` });
24573
+ continue;
24574
+ }
24575
+ if (item.status === "In Review") {
24576
+ skipped.push({ ref: item.ref, reason: "In Review \u2014 a whole-board claim does not pull review cards back to In Progress; claim it by ref to reclaim" });
24577
+ continue;
24578
+ }
24579
+ const verdict = evaluateClaim(item, login);
24580
+ if (!verdict.ok) {
24581
+ skipped.push({ ref: item.ref, reason: verdict.reason.replace(/ \(never chain separate claims.*$/, "") });
24582
+ continue;
24583
+ }
24584
+ selectors.push({ repo: item.repository, number: item.number });
24585
+ }
24586
+ ctx.selectedRefs = new Set(selectors.map((s) => `${s.repo}#${s.number}`.toLowerCase()));
24587
+ const results = await runBulkClaims(ctx, selectors, options);
24588
+ return {
24589
+ viewer: ctx.report.viewer,
24590
+ repo: ctx.report.repo,
24591
+ results,
24592
+ skipped,
24593
+ failed: results.filter((result) => !result.claimed).length,
24594
+ ...ctx.report.warnings.length ? { warnings: [...ctx.report.warnings] } : {}
24595
+ };
24596
+ }
24597
+ async function claimBoardIssues(options, deps = {}) {
24598
+ const cfg = resolveBoardConfig(options.config);
24599
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24600
+ const selectors = [];
24601
+ const seen = /* @__PURE__ */ new Set();
24602
+ for (const raw of options.selectors) {
24603
+ const selector = parseIssueSelector(raw, collected.repo, options.repo);
24604
+ const key = `${selector.repo.toLowerCase()}#${selector.number}`;
24605
+ if (seen.has(key)) continue;
24606
+ seen.add(key);
24607
+ selectors.push(selector);
24608
+ }
24609
+ const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
24610
+ const results = await runBulkClaims(ctx, selectors, options);
24558
24611
  return {
24559
24612
  viewer: ctx.report.viewer,
24560
24613
  repo: ctx.report.repo,
@@ -25980,7 +26033,7 @@ var BLESSED_RUN_WITH_BUDGET_SHA = "cb31a94c89729beac83500ad4d301132dcfe2e4c";
25980
26033
  var BLESSED_RUNNER_GATE_SHA = "7f9a25df2ce7198c67407b42d0c548f75ce3f430";
25981
26034
  var BLESSED_RUNNER_CHECK_POLICY_SHA = "67bb2012710a96d21be825c29a5d5c2b4b8001e2";
25982
26035
  var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
25983
- var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
26036
+ var BLESSED_JEV_FAILURES_SHA = "20ce7d5e0d85cdc7e14bbacbc8b6afdabee6bd41";
25984
26037
  var BLESSED_VERDICT_RECEIPT_SHA = "";
25985
26038
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
25986
26039
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -28248,10 +28301,10 @@ var rollout_plan_default = {
28248
28301
  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)."
28249
28302
  },
28250
28303
  baseline: {
28251
- version: "4.6.30",
28252
- tag: "v4.6.30",
28253
- commit: "52569fa5c01e",
28254
- npm: "@mutmutco/cli@4.6.30"
28304
+ version: "4.6.32",
28305
+ tag: "v4.6.32",
28306
+ commit: "0e477e5e2765",
28307
+ npm: "@mutmutco/cli@4.6.32"
28255
28308
  },
28256
28309
  exitCriterion: "fleet-n-of-n",
28257
28310
  hubOnlyShortcut: "forbidden",
@@ -28268,14 +28321,14 @@ var rollout_plan_default = {
28268
28321
  repo: "mutmutco/mmi-hub",
28269
28322
  role: "canary",
28270
28323
  schedule: "train",
28271
- v3Target: "v4.6.30"
28324
+ v3Target: "v4.6.32"
28272
28325
  }
28273
28326
  ],
28274
28327
  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.",
28275
28328
  rollback: {
28276
28329
  independent: true,
28277
- mechanism: "npm dist-tag latest -> 4.6.30 and redeploy the Hub Lambda from tag v4.6.30 (52569fa5c01e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28278
- v3Target: "v4.6.30 (@mutmutco/cli@4.6.30, tag commit 52569fa5c01e \u2014 last known-good release carrying the repo-index v4-only contract)"
28330
+ mechanism: "npm dist-tag latest -> 4.6.32 and redeploy the Hub Lambda from tag v4.6.32 (0e477e5e2765); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28331
+ v3Target: "v4.6.32 (@mutmutco/cli@4.6.32, tag commit 0e477e5e2765 \u2014 last known-good release carrying the repo-index v4-only contract)"
28279
28332
  }
28280
28333
  },
28281
28334
  {
@@ -42455,7 +42508,7 @@ async function runPrLandOnce(prNumber, options, deps) {
42455
42508
  }
42456
42509
  }
42457
42510
  const isConflicting = (state) => state.mergeStateStatus === "DIRTY" || state.mergeable === "CONFLICTING";
42458
- const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 merge origin/development into the branch (never rebase a pushed branch), rebuild any checked-in artifacts, then push before landing${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`;
42511
+ const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 \`git fetch origin\`, merge origin/development into the branch (never rebase a pushed branch), resolve the conflicts, regenerate any checked-in artifacts, commit and push, then re-run \`mmi-cli devops pr land ${prNumber} --repo ${repo}\` once${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`;
42459
42512
  const readPrState = async () => {
42460
42513
  const state = await deps.readPrState(prNumber, repo);
42461
42514
  if (!isConflicting(state)) return state;
@@ -42503,6 +42556,9 @@ async function runPrLandOnce(prNumber, options, deps) {
42503
42556
  base.mergeStatus = merge.mergeStatus;
42504
42557
  if (merge.credential) base.credential = merge.credential;
42505
42558
  if (merge.credentialRole) base.credentialRole = merge.credentialRole;
42559
+ if (merge.ejectedThenReenqueued) base.ejectedThenReenqueued = true;
42560
+ if (merge.reconcileNeeded) base.reconcileNeeded = true;
42561
+ if (merge.queueRemovalEvidence) base.queueRemovalEvidence = merge.queueRemovalEvidence;
42506
42562
  if (merge.mergeStatus === "queue-admission-pending") {
42507
42563
  return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
42508
42564
  }
@@ -43674,6 +43730,7 @@ var autoMergeApi = async (pr, repo, head) => {
43674
43730
  };
43675
43731
  var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
43676
43732
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
43733
+ var UNWITNESSED_EJECTION_GRACE_MS = 6e4;
43677
43734
  function admissionJournal(repo, pr, root) {
43678
43735
  const stateRoot = root ?? (process.platform === "win32" ? (0, import_node_path31.join)(process.env.LOCALAPPDATA || (0, import_node_path31.join)((0, import_node_os18.homedir)(), "AppData", "Local"), "MMI Future", "mmi-cli", "queue-admissions") : (0, import_node_path31.join)(process.env.XDG_STATE_HOME || (0, import_node_path31.join)((0, import_node_os18.homedir)(), ".local", "state"), "mmi-cli", "queue-admissions"));
43679
43736
  const directory = (0, import_node_path31.join)(stateRoot, (0, import_node_crypto12.createHash)("sha256").update(`${repo.toLowerCase()}#${pr}`).digest("hex"));
@@ -43687,12 +43744,12 @@ function admissionJournal(repo, pr, root) {
43687
43744
  const path2 = (number) => (0, import_node_path31.join)(directory, `${number}.jsonl`);
43688
43745
  const validate = (value, previous) => {
43689
43746
  const row = value;
43690
- if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
43747
+ if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected", "abandoned"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
43691
43748
  throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
43692
43749
  }
43693
43750
  if (!previous) {
43694
43751
  if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
43695
- } else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state === "ejected" && row.uuid === previous.uuid)) {
43752
+ } else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && ["ejected", "abandoned"].includes(row.state) && row.uuid === previous.uuid)) {
43696
43753
  throw new Error("queue admission receipt has an illegal transition or changed operation identity");
43697
43754
  }
43698
43755
  return row;
@@ -43775,13 +43832,23 @@ function admissionRejected(reason, repo, pr) {
43775
43832
  };
43776
43833
  }
43777
43834
  async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
43778
- const unknown = (reason) => ({
43835
+ let reenqueuedAfterEjection = false;
43836
+ let removalEvidence;
43837
+ const markReenqueued = (result) => reenqueuedAfterEjection ? {
43838
+ ...result,
43839
+ ejectedThenReenqueued: true,
43840
+ ...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
43841
+ ...result.error ? { error: `re-enqueued after ejection: ${result.error}` } : {}
43842
+ } : result;
43843
+ const unknown = (reason) => markReenqueued({
43779
43844
  mergeStatus: "failed",
43780
43845
  retryable: false,
43781
43846
  outcomeUnknown: true,
43782
- error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` and rerun \`mmi-cli devops pr land ${pr} --repo ${repo} --json\` to reconcile read-only \u2014 no new request was sent after this uncertainty.`
43847
+ reconcileNeeded: true,
43848
+ ...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
43849
+ error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\`; once the PR is verifiably OPEN at the submitted head and absent from the queue, abandon the stuck admission with \`mmi-cli devops pr land ${pr} --repo ${repo} --abandon-admission\` so a new intent needs no new head \u2014 no new request was sent after this uncertainty.`
43783
43850
  });
43784
- const rejected = (reason) => admissionRejected(reason, repo, pr);
43851
+ const rejected = (reason) => markReenqueued(admissionRejected(reason, repo, pr));
43785
43852
  const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
43786
43853
  const maxReads = options.enqueueOnly ? 3 : 1;
43787
43854
  const sleep3 = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
@@ -43789,13 +43856,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
43789
43856
  if (readAttempt) await sleep3(readAttempt * 1e3);
43790
43857
  try {
43791
43858
  const observed = await readPrMergeQueue(pr, repo, read);
43792
- if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
43859
+ if (observed.state === "MERGED" && observed.headRefOid === head) return markReenqueued({ mergeStatus: "merged" });
43793
43860
  if (observed.isInMergeQueue && observed.headRefOid === head) {
43794
43861
  const entry = observed.mergeQueueEntry;
43795
43862
  if (recordEntry && receipt?.head === head && entry?.headCommit && entry.enqueuedAt) {
43796
43863
  journal.save("enqueued", receipt?.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
43797
43864
  }
43798
- return { mergeStatus: "auto-merge-enqueued" };
43865
+ return markReenqueued({ mergeStatus: "auto-merge-enqueued" });
43799
43866
  }
43800
43867
  const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
43801
43868
  const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
@@ -43827,11 +43894,37 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
43827
43894
  let receipt = journal.receipt;
43828
43895
  let response;
43829
43896
  let superseded = false;
43830
- if (receipt && (receipt.state === "enqueued" || receipt.state === "merged") && receipt.head.toLowerCase() !== head.toLowerCase()) {
43831
- try {
43832
- const observed = await readPrMergeQueue(pr, repo, read);
43833
- superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
43834
- } catch {
43897
+ if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")) {
43898
+ if (receipt.head.toLowerCase() !== head.toLowerCase()) {
43899
+ try {
43900
+ const observed = await readPrMergeQueue(pr, repo, read);
43901
+ superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
43902
+ } catch {
43903
+ }
43904
+ } else {
43905
+ try {
43906
+ const observed = await readPrMergeQueue(pr, repo, read);
43907
+ if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid?.toLowerCase() === receipt.head.toLowerCase()) {
43908
+ const boundMs = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt + UNWITNESSED_EJECTION_GRACE_MS;
43909
+ const events = await readArrayPages(
43910
+ `repos/${repo}/issues/${pr}/timeline?per_page=100`,
43911
+ options.timelineRead ?? read,
43912
+ "queue ejection timeline unreadable"
43913
+ );
43914
+ const removals = events.filter((event) => event.event === "removed_from_merge_queue" && Number.isFinite(Date.parse(event.created_at))).map((event) => Date.parse(event.created_at)).filter((at) => at > boundMs).sort((a, b) => a - b);
43915
+ removalEvidence = {
43916
+ source: receipt.queueEntry ? "queue-entry" : "receipt-checkpoint",
43917
+ boundAt: new Date(boundMs).toISOString(),
43918
+ removalsAfterBound: removals.map((at) => new Date(at).toISOString()),
43919
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
43920
+ };
43921
+ if (removals.length > 0) {
43922
+ reenqueuedAfterEjection = true;
43923
+ superseded = true;
43924
+ }
43925
+ }
43926
+ } catch {
43927
+ }
43835
43928
  }
43836
43929
  }
43837
43930
  if (superseded && receipt?.state === "enqueued") {
@@ -43841,7 +43934,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
43841
43934
  return unknown("the superseded admission could not be checkpointed");
43842
43935
  }
43843
43936
  }
43844
- if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
43937
+ if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && receipt.state !== "abandoned" && !superseded) {
43845
43938
  if (options.selection) {
43846
43939
  options.selection.selected = true;
43847
43940
  options.selection.intentId = receipt.intentId;
@@ -43987,13 +44080,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
43987
44080
  if (receipt.head !== head) return reconcile("the previous head completed admission; inspect current head and membership before continuing");
43988
44081
  return reconcile(`operation ${receipt.uuid} reported ${status}, but matching-head membership or merged state is unconfirmed`, status === "enqueued");
43989
44082
  }
43990
- return {
44083
+ return markReenqueued({
43991
44084
  mergeStatus: "queue-admission-pending",
43992
44085
  retryable: false,
43993
44086
  outcomeUnknown: true,
43994
44087
  queueAdmission: { state: "pending", uuid: receipt.uuid, head: receipt.head },
43995
44088
  error: "GitHub accepted queue admission but has not confirmed queue membership. Rerun pr land to resume the saved operation read-only."
43996
- };
44089
+ });
43997
44090
  }
43998
44091
  function githubQueueSquashMessage(title, pr, commits) {
43999
44092
  const list = commits.map((commit) => `* ${commit.headline}
@@ -44068,13 +44161,13 @@ async function inspectQueuePreparation(pr, repo, options) {
44068
44161
  const parsed = parseRestClosingGuardInput({ state: pull.state, title: pull.title, body: pull.body }, commits, repo);
44069
44162
  parsed.closing = closing;
44070
44163
  const policy = { title: repository.squash_merge_commit_title, body: repository.squash_merge_commit_message };
44071
- const preparation = (reason, targets, detail) => ({
44164
+ const preparation = (reason, targets, detail, nextAction) => ({
44072
44165
  mergeStatus: "failed",
44073
44166
  preparation: {
44074
44167
  status: "preparation-needed",
44075
44168
  reason,
44076
44169
  issueTargets: targets.map((number) => `${repo}#${number}`),
44077
- nextAction: reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
44170
+ nextAction: nextAction ?? (reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged.")
44078
44171
  },
44079
44172
  error: `queue preparation needed: ${detail}`
44080
44173
  });
@@ -44157,7 +44250,17 @@ ${pull.body}`;
44157
44250
  }
44158
44251
  };
44159
44252
  }
44160
- return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
44253
+ const trailerKeys = (message2) => {
44254
+ const keys = [...new Set(parseTrailers(message2.trim(), process.cwd()).map((trailer) => trailer.split(":")[0].trim()))];
44255
+ return keys.length ? keys.join(", ") : "none";
44256
+ };
44257
+ const source = options.bodyText != null ? "the explicit body override" : "the body composed from the source commit messages";
44258
+ const detail = `the requested sanitized body or trailer arrangement cannot be sent to queue admission: queue merge method ${method}, squash body policy ${policy.body ?? "unknown"}; required body is ${source} (trailer keys: ${trailerKeys(`${headline}
44259
+
44260
+ ${requiredBody}`)}), but the message the queue would send carries trailer keys: ${trailerKeys(actualMessage)}`;
44261
+ const next = options.bodyText != null ? `PR author: a merge queue accepts no body override; drop --body/--squash-body-file and place the required trailer(s) where the queue's ${method}${method === "SQUASH" ? ` (${policy.body})` : ""} message reads them, then rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : method !== "SQUASH" ? `PR author: the queue's ${method} method keeps the original commit messages, so the required trailer(s) must sit in a source commit message's final trailer block; amend the commit, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : policy.body === "BLANK" ? `PR author: squash body policy BLANK sends an empty body, so no trailer from the commits or PR body reaches the queue commit; the waiver cannot survive queue admission with unchanged repository settings \u2014 remove the need for the override or ask a maintainer. Keep repository settings unchanged.` : `PR author: squash body policy ${policy.body} does not reproduce the composed body; consolidate the required trailer(s) into one source commit message's final trailer block, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.`;
44262
+ const caveat = " GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy; the combined queue candidate must pass with the shared trailer reader.";
44263
+ return { kind: "refused", result: preparation("queue-message-override", [], detail, next + caveat) };
44161
44264
  }
44162
44265
  return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
44163
44266
  }
@@ -44188,6 +44291,38 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
44188
44291
  const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
44189
44292
  return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
44190
44293
  }
44294
+ async function abandonQueueAdmission(pr, repo, options = {}) {
44295
+ if (!/^\d+$/.test(pr)) return { status: "refused", repo, pr, reason: "a bare PR number is required" };
44296
+ const refuse = (reason) => ({ status: "refused", repo, pr, reason });
44297
+ let journal;
44298
+ try {
44299
+ journal = admissionJournal(repo, pr, options.receiptDirectory);
44300
+ } catch (error) {
44301
+ return refuse(`the durable admission receipt cannot be read (${error.message})`);
44302
+ }
44303
+ const receipt = journal.receipt;
44304
+ if (!receipt) return refuse("no queue-admission receipt exists for this PR; nothing to abandon");
44305
+ if (!["pending", "enqueued"].includes(receipt.state)) {
44306
+ return refuse(`the saved admission is '${receipt.state}', not a stuck non-terminal 'pending'/'enqueued' receipt; nothing to abandon`);
44307
+ }
44308
+ let snapshot;
44309
+ try {
44310
+ snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi);
44311
+ } catch (error) {
44312
+ return refuse(`the PR and queue state could not be read (${error.message}); the admission is unchanged`);
44313
+ }
44314
+ if (snapshot.state !== "OPEN") return refuse(`the PR is '${snapshot.state}', not OPEN; the admission is unchanged`);
44315
+ if (!snapshot.headRefOid || snapshot.headRefOid.toLowerCase() !== receipt.head.toLowerCase()) {
44316
+ return refuse(`the PR head is ${snapshot.headRefOid ?? "unreadable"}, not the receipt's submitted head ${receipt.head}; the admission is unchanged`);
44317
+ }
44318
+ if (snapshot.isInMergeQueue) return refuse("the PR is currently in the merge queue, so the admission is not stuck; the admission is unchanged");
44319
+ try {
44320
+ journal.save("abandoned");
44321
+ } catch (error) {
44322
+ return refuse(`the abandoned state could not be checkpointed (${error.message}); the admission is unchanged`);
44323
+ }
44324
+ return { status: "abandoned", repo, pr, head: receipt.head, ...receipt.uuid ? { operation: receipt.uuid } : {} };
44325
+ }
44191
44326
  function closingGuardDiagnostic(message2) {
44192
44327
  if (!message2) return "";
44193
44328
  const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
@@ -46100,7 +46235,7 @@ function refuseNotClaimable(e, json) {
46100
46235
  process.exitCode = 1;
46101
46236
  return true;
46102
46237
  }
46103
- var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
46238
+ var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nNever sniff the envelope \u2014 `j.items ?? j.rows ?? j`, `Object.values(j)` or any generic \"find the list\"\nwrapper throws `items is not iterable` before any row is read (#9956). For a number|status|claim|title\nprojection across non-Done rows, read --json --flat --out .jerv/tmp/board.json and filter that array locally:\n mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
46104
46239
  function flattenBoardReport(report) {
46105
46240
  const closingByRef = /* @__PURE__ */ new Map();
46106
46241
  for (const pull of report.pullRequests ?? []) {
@@ -46223,9 +46358,41 @@ function registerBoardCommands(program3) {
46223
46358
  }
46224
46359
  });
46225
46360
  withExamples(mutating(
46226
- board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
46361
+ board.command("claim [issues...]").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs, or --all for the whole board); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n\nwhole board (#9930): --all reads the board once and claims every open item of this repo's primary\nboard that the per-ref claim may take (yours, unassigned Todo, unowned In Progress), carrying each\nitem's branch/worktree lane evidence forward like a per-ref claim. It prints one line per item, then\nthe refs it skipped and why (assigned to another login, In Review, closed, not an issue). Without\n--force a live foreign lane is skipped with its reason, never stolen; --force takes it as the\nper-ref --force would. --all with explicit refs is an error.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--all", "claim every open claimable item on the current repo's board in one pass (combine with --force/--repo; not with explicit refs) (#9930)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
46227
46362
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
46228
- ).action(async (issueRefs, o) => {
46363
+ ).action(async (issueRefs = [], o) => {
46364
+ const argsError = claimArgsError(issueRefs, o.all);
46365
+ if (argsError) return failGraceful(`oracle board claim failed: ${argsError}`);
46366
+ if (o.all) {
46367
+ try {
46368
+ const config = await loadConfigForBoardSelector2("", o.repo);
46369
+ const all = await claimAllBoardIssues({
46370
+ config,
46371
+ repo: o.repo,
46372
+ assignee: o.for,
46373
+ force: o.force,
46374
+ check: o.check,
46375
+ allowPartial: o.allowPartial
46376
+ }, { snapshot: registryClientDeps(config) });
46377
+ if (o.json) {
46378
+ console.log(JSON.stringify({ results: all.results, skipped: all.skipped }));
46379
+ printClaimWarnings(all.warnings, true);
46380
+ } else {
46381
+ for (const result of all.results) {
46382
+ console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
46383
+ }
46384
+ for (const skip of all.skipped) console.log(`Skipped ${skip.ref}: ${skip.reason}`);
46385
+ const landed = all.results.filter((r) => r.claimed).length;
46386
+ console.log(`claim --all: ${landed} of ${all.results.length} attempted item(s) claimed; ${all.skipped.length} not attempted`);
46387
+ printClaimWarnings(all.warnings);
46388
+ }
46389
+ if (all.failed > 0) process.exitCode = 1;
46390
+ } catch (e) {
46391
+ if (refuseRateLimited(e, o.json)) return;
46392
+ return failGraceful(`oracle board claim failed: ${e.message}`);
46393
+ }
46394
+ return;
46395
+ }
46229
46396
  if (issueRefs.length === 1) {
46230
46397
  const issueRef = issueRefs[0];
46231
46398
  try {
@@ -46284,11 +46451,13 @@ function registerBoardCommands(program3) {
46284
46451
  }), [
46285
46452
  "mmi-cli oracle board claim 2680",
46286
46453
  "mmi-cli oracle board claim 2680 2681 --for teammate-login",
46287
- "mmi-cli oracle board claim 2680 --check"
46454
+ "mmi-cli oracle board claim 2680 --check",
46455
+ "mmi-cli oracle board claim --all --force"
46288
46456
  ], [
46289
46457
  "Pass raw issue numbers/refs, not URLs.",
46290
46458
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
46291
46459
  "Multiple refs are handled as a batch and return per-item results.",
46460
+ "To claim the whole board, use --all (with --force to take live lanes) instead of a hand-built ref list.",
46292
46461
  "--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan.",
46293
46462
  // #5552: agents guess `oracle issue claim`; that route does not exist — board claim is the only write.
46294
46463
  "Never run `oracle issue claim` \u2014 claims are board mutations; only `oracle board claim <ref>` is valid.",
@@ -60798,23 +60967,53 @@ async function checkDocsIndexAtHead(opts, deps) {
60798
60967
  }
60799
60968
 
60800
60969
  // src/pr-create-preparation.ts
60970
+ function rejectionReason(error) {
60971
+ const text = (error instanceof Error ? error.message : String(error)).trim().split("\n")[0] || "no error detail";
60972
+ return text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
60973
+ }
60974
+ var PERMISSION_REFUSAL = /Resource not accessible|HTTP 40[13]|40[13] (?:Forbidden|Unauthorized)|Bad credentials|must have admin rights|insufficient (?:permissions?|scope)|not authorized/i;
60975
+ async function observePolicy(deps, candidate) {
60976
+ if (!candidate) return void 0;
60977
+ try {
60978
+ return await deps.policy(candidate);
60979
+ } catch (error) {
60980
+ const refusal = rejectionReason(error);
60981
+ if (!PERMISSION_REFUSAL.test(refusal)) throw error;
60982
+ await deps.fetchCandidate?.(candidate).catch(() => void 0);
60983
+ let retried;
60984
+ try {
60985
+ retried = await deps.policy(candidate);
60986
+ } catch (retryError) {
60987
+ const second = rejectionReason(retryError);
60988
+ if (!PERMISSION_REFUSAL.test(second)) throw retryError;
60989
+ return {
60990
+ check: "policy",
60991
+ state: "skipped",
60992
+ detail: `committed test policy was not observed: GitHub refused the policy read twice (${second}) \u2014 the identity running pr create lacks the permission that observation needs, repository Administration: read, which covers rulesets and the merge-queue configuration; grant it to the App installation (APP_POLL_PERMISSIONS or the installed App's permissions on ${candidate.repo}) or run with an owner GH_TOKEN. This is an unreadable guard, NOT a policy violation.`
60993
+ };
60994
+ }
60995
+ process.stderr.write(`pr create: WARNING \u2014 the committed test-policy read was refused (${refusal}); re-fetched the candidate objects and retried it once (#9946)
60996
+ `);
60997
+ return retried;
60998
+ }
60999
+ }
60801
61000
  async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60802
61001
  const observations = [];
60803
- const refs = await deps.candidate().catch(() => ({ findings: ["candidate refs unavailable; verify the repository and pushed head/base before retrying"] }));
61002
+ const refs = await deps.candidate().catch((error) => ({ findings: [`candidate refs unavailable (${rejectionReason(error)}); verify the repository and pushed head/base before retrying`] }));
60804
61003
  observations.push(...refs.findings.map((detail) => ({ check: "refs", state: "unavailable", detail })));
60805
61004
  if (refs.candidate) observations.push({ check: "refs", state: "passed", detail: `candidate ${refs.candidate.headSha} against ${refs.candidate.baseSha}` });
60806
61005
  const reads = await Promise.allSettled([
60807
61006
  deps.claims(refs.candidate),
60808
61007
  refs.candidate ? deps.docs(refs.candidate) : Promise.resolve(void 0),
60809
- refs.candidate ? deps.policy(refs.candidate) : Promise.resolve(void 0)
61008
+ observePolicy(deps, refs.candidate)
60810
61009
  ]);
60811
61010
  const [claims, docs, policy] = reads;
60812
61011
  if (claims.status === "fulfilled") {
60813
61012
  observations.push(...claims.value.findings.length ? claims.value.findings.map((detail) => ({ check: "claims", state: "failed", detail })) : [{ check: "claims", state: "passed", detail: "issue and claim evidence read" }]);
60814
- } else observations.push({ check: "claims", state: "unavailable", detail: "issue/claim evidence unavailable; retry the read" });
60815
- if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail: "committed docs observation unavailable; retry the read" });
61013
+ } else observations.push({ check: "claims", state: "unavailable", detail: `issue/claim evidence unavailable (${rejectionReason(claims.reason)}); retry the read` });
61014
+ if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail: `committed docs observation unavailable (${rejectionReason(docs.reason)}); retry the read` });
60816
61015
  else if (docs.value) observations.push({ check: "docs", state: docs.value.state, detail: [docs.value.detail, docs.value.fix].filter(Boolean).join(" \u2014 ") });
60817
- if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail: "committed test-policy observation unavailable; check policy validity and fetch full candidate history before retrying" });
61016
+ if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail: `committed test-policy observation unavailable (${rejectionReason(policy.reason)}); check policy validity and fetch full candidate history before retrying` });
60818
61017
  else if (policy.value) observations.push(policy.value);
60819
61018
  const findings = observations.filter((item) => item.state === "failed" || item.state === "unavailable");
60820
61019
  let attempted = false;
@@ -60843,7 +61042,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60843
61042
  await claims.value.restamp();
60844
61043
  const beforeCreate = await deps.candidate();
60845
61044
  if (beforeCreate.findings.length || JSON.stringify(beforeCreate.candidate) !== JSON.stringify(expected)) {
60846
- throw new Error("pr create: candidate changed while restamping claims; prepare again before creating the PR");
61045
+ throw new Error("pr create: candidate identity changed during preparation while restamping claims; prepare the current head and base again");
60847
61046
  }
60848
61047
  }
60849
61048
  };
@@ -60916,6 +61115,15 @@ function createPreparationDeps(input, io = {}) {
60916
61115
  return {
60917
61116
  candidate: readCandidate,
60918
61117
  publish: (proof) => publishPreparedPrHead(proof, exec3),
61118
+ // #9946: the retry needs the exact candidate objects. Fetch each ref on its own so a fork-only head
61119
+ // that origin cannot resolve never costs the base fetch too, and never surface a fetch failure as the
61120
+ // reason the policy read stayed unreadable.
61121
+ fetchCandidate: async (candidate) => {
61122
+ if (!candidate.root) return;
61123
+ const branch = (name) => name.slice(name.lastIndexOf(":") + 1);
61124
+ const refspecs = [.../* @__PURE__ */ new Set([candidate.head, candidate.base])].map((name) => `+refs/heads/${branch(name)}:refs/remotes/origin/${branch(name)}`);
61125
+ await Promise.allSettled(refspecs.map((refspec) => exec3("git", ["-C", candidate.root, "fetch", "--no-tags", "origin", refspec], { timeout: GIT_TIMEOUT_MS })));
61126
+ },
60919
61127
  claims: (candidate) => observePrCreateClaims(input.body, candidate?.repo ?? input.repo, { head: candidate?.head, headSha: candidate?.headSha }),
60920
61128
  docs: (candidate) => checkDocsIndexAtHead({ repo: candidate.repo, head: candidate.headSha }, docs),
60921
61129
  policy: async (candidate) => {
@@ -60946,7 +61154,12 @@ function createPreparationDeps(input, io = {}) {
60946
61154
  return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
60947
61155
  }
60948
61156
  }
60949
- const policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
61157
+ let policy;
61158
+ try {
61159
+ policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
61160
+ } catch (error) {
61161
+ return result("unavailable", `committed test policy could not be evaluated: ${rejectionReason(error)} \u2014 fix test-policy.json if it is malformed, or run \`git fetch --unshallow\` if a history read failed`);
61162
+ }
60950
61163
  return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
60951
61164
  }
60952
61165
  };
@@ -62566,6 +62779,46 @@ var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
62566
62779
  function canonicalWorktreeRoot(primary) {
62567
62780
  return (0, import_node_path49.join)((0, import_node_path49.dirname)(primary), "mmi-worktrees", (0, import_node_path49.basename)(primary));
62568
62781
  }
62782
+ function lastSegment(p) {
62783
+ return (0, import_node_path49.basename)(p.split(/[\\/]+/).filter(Boolean).pop() ?? p);
62784
+ }
62785
+ function readProvenance(folder, gitCommonDir, exists) {
62786
+ const found = {
62787
+ path: folder,
62788
+ gitdirPresent: false,
62789
+ registrationId: (0, import_node_path49.basename)(folder),
62790
+ registrationPresent: false
62791
+ };
62792
+ try {
62793
+ const pointer = /^\s*gitdir:\s*(.+?)\s*$/m.exec((0, import_node_fs53.readFileSync)((0, import_node_path49.join)(folder, ".git"), "utf8"));
62794
+ if (pointer) {
62795
+ const target = pointer[1];
62796
+ found.gitdir = (0, import_node_path49.isAbsolute)(target) ? target : (0, import_node_path49.join)(folder, target);
62797
+ found.gitdirPresent = exists(found.gitdir);
62798
+ found.registrationId = lastSegment(found.gitdir);
62799
+ }
62800
+ } catch {
62801
+ }
62802
+ try {
62803
+ found.registrationPresent = exists((0, import_node_path49.join)(gitCommonDir, "worktrees", found.registrationId));
62804
+ } catch {
62805
+ }
62806
+ if (found.gitdir && found.gitdirPresent) {
62807
+ try {
62808
+ const head = (0, import_node_fs53.readFileSync)((0, import_node_path49.join)(found.gitdir, "HEAD"), "utf8").trim();
62809
+ const branch = /^ref:\s*(.+?)\s*$/.exec(head);
62810
+ if (branch) found.head = branch[1].startsWith("refs/heads/") ? branch[1].slice("refs/heads/".length) : `ref ${branch[1]}`;
62811
+ else if (/^[0-9a-f]{7,64}$/i.test(head)) found.head = `detached ${head.slice(0, 12)}`;
62812
+ else if (head) found.head = `ref ${head}`;
62813
+ } catch {
62814
+ }
62815
+ }
62816
+ try {
62817
+ found.mtime = (0, import_node_fs53.statSync)(folder).mtime.toISOString();
62818
+ } catch {
62819
+ }
62820
+ return found;
62821
+ }
62569
62822
  function norm2(p) {
62570
62823
  const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
62571
62824
  return process.platform === "win32" ? s.toLowerCase() : s;
@@ -62660,7 +62913,7 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
62660
62913
  } catch {
62661
62914
  }
62662
62915
  } else {
62663
- report.unrecorded.push(child2);
62916
+ report.unrecorded.push(readProvenance(child2, gitCommonDir, exists));
62664
62917
  }
62665
62918
  }
62666
62919
  }
@@ -62674,12 +62927,19 @@ function renderResidueSweepLines(report) {
62674
62927
  if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
62675
62928
  for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
62676
62929
  if (report.unrecorded.length) {
62677
- const shown = report.unrecorded.slice(0, 5).join(", ");
62930
+ const listed = report.unrecorded.slice(0, 5);
62678
62931
  const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
62679
- lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${shown}${more}`);
62932
+ lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${listed.map((u) => u.path).join(", ")}${more}`);
62933
+ for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
62680
62934
  }
62681
62935
  return lines2;
62682
62936
  }
62937
+ function provenanceText(u) {
62938
+ const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
62939
+ const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
62940
+ const head = u.head ? `HEAD ${u.head}` : "HEAD unknown";
62941
+ return `${pointer}; ${registration}; ${head}; ${u.mtime ? `modified ${u.mtime}` : "mtime unknown"}`;
62942
+ }
62683
62943
 
62684
62944
  // src/command-register-collaboration.ts
62685
62945
  var DEFAULT_PRIORITY = "medium";
@@ -63829,9 +64089,19 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
63829
64089
  }
63830
64090
  class PrHeadBehindBaseError extends Error {
63831
64091
  }
63832
- pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
64092
+ pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--abandon-admission", "one stuck-receipt recovery, one PR per call: journal the saved non-terminal (pending/enqueued) queue-admission receipt terminal ONLY after positively observing the PR OPEN at the receipt head and absent from the queue; any unprovable observation refuses and changes nothing (#9951). The next pr land then submits a new intent with no new head").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
63833
64093
  const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
63834
64094
  if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
64095
+ if (opts.abandonAdmission) {
64096
+ if (numbers.length !== 1) return fail("pr land: --abandon-admission abandons one PR per call");
64097
+ const repo = await resolveRepo(opts.repo).catch(() => void 0) ?? opts.repo;
64098
+ if (!repo) return fail("pr land: --abandon-admission needs --repo <owner/repo>; the PR repo could not be resolved");
64099
+ const outcome = await abandonQueueAdmission(numbers[0], repo);
64100
+ if (opts.json) printLine(JSON.stringify(outcome));
64101
+ else printLine(outcome.status === "abandoned" ? `pr land: abandoned the stuck queue admission of PR #${outcome.pr} on ${repo}${outcome.operation ? ` (operation ${outcome.operation})` : ""} at head ${outcome.head} \u2014 the next pr land submits a new intent with no new head` : `pr land: refused to abandon the queue admission of PR #${outcome.pr} on ${repo} \u2014 ${outcome.reason}`);
64102
+ if (outcome.status !== "abandoned") process.exitCode = 1;
64103
+ return;
64104
+ }
63835
64105
  if (opts.expectHead !== void 0) {
63836
64106
  if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
63837
64107
  if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
@@ -64976,9 +65246,13 @@ function projectInfoOutcome(projectInfoSync) {
64976
65246
  }
64977
65247
  function buildReleaseVerdict(commandName, result, projectInfoSync) {
64978
65248
  const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
65249
+ const deployOutcome = reduceFollowUpOutcomes([
65250
+ deployFollowUpOutcome(result.deployStatus),
65251
+ deployFollowUpOutcome(aggregateWorkflowRuns(result.workflowRuns ?? []))
65252
+ ]);
64979
65253
  const followUpStatus = deriveTrainFollowUpStatus({
64980
65254
  projectInfo: projectInfoOutcome(projectInfoSync),
64981
- deploy: deployFollowUpOutcome(result.deployStatus),
65255
+ deploy: deployOutcome,
64982
65256
  rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
64983
65257
  alignment: alignmentPending ? "unresolved" : "ok",
64984
65258
  foldPort: "ok"
@@ -64998,9 +65272,14 @@ function buildReleaseVerdict(commandName, result, projectInfoSync) {
64998
65272
  }
64999
65273
  function buildResumeReleaseVerdict(result, projectInfoSync) {
65000
65274
  const resumeStatus = resumeFollowUpOf(result.state);
65275
+ const deployOutcome = reduceFollowUpOutcomes([
65276
+ // No dispatch (e.g. an alignment-only resume) keeps the old 'ok' deploy dimension.
65277
+ deployFollowUpOutcome(result.dispatch?.deployStatus ?? "success"),
65278
+ deployFollowUpOutcome(aggregateWorkflowRuns(result.dispatch?.workflowRuns ?? []))
65279
+ ]);
65001
65280
  const followUpStatus = deriveTrainFollowUpStatus({
65002
65281
  projectInfo: projectInfoOutcome(projectInfoSync),
65003
- deploy: "ok",
65282
+ deploy: deployOutcome,
65004
65283
  rcRetirement: "ok",
65005
65284
  alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
65006
65285
  foldPort: "ok"
@@ -71440,6 +71719,54 @@ function renderTenantLogs(r) {
71440
71719
  return out.join("\n");
71441
71720
  }
71442
71721
 
71722
+ // src/tenant-status-wait.ts
71723
+ var TENANT_STATUS_WAIT_INTERVAL_MS = 12e3;
71724
+ var TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S = 660;
71725
+ var TENANT_STATUS_WAIT_TIMEOUT_MAX_S = 1800;
71726
+ var TENANT_STATUS_WAIT_MIN_SHA_LENGTH = 7;
71727
+ function parseWaitForSha(raw) {
71728
+ const sha = raw.trim().toLowerCase();
71729
+ if (!new RegExp(`^[0-9a-f]{${TENANT_STATUS_WAIT_MIN_SHA_LENGTH},40}$`).test(sha)) {
71730
+ return { ok: false, reason: `--wait-for must be a commit SHA (${TENANT_STATUS_WAIT_MIN_SHA_LENGTH}-40 hex characters), got "${raw.trim()}"` };
71731
+ }
71732
+ return { ok: true, sha };
71733
+ }
71734
+ function parseWaitTimeoutSeconds(raw) {
71735
+ if (raw === void 0) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S, clamped: false };
71736
+ const seconds = Number(raw.trim());
71737
+ if (!Number.isInteger(seconds) || seconds <= 0) {
71738
+ return { ok: false, reason: `--timeout must be a positive whole number of seconds, got "${raw.trim()}"` };
71739
+ }
71740
+ if (seconds > TENANT_STATUS_WAIT_TIMEOUT_MAX_S) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_MAX_S, clamped: true };
71741
+ return { ok: true, seconds, clamped: false };
71742
+ }
71743
+ function deployedTagReaches(tag, sha) {
71744
+ const stamped = (tag ?? "").trim().toLowerCase();
71745
+ const needle = sha.trim().toLowerCase();
71746
+ if (!stamped || !needle) return false;
71747
+ if (stamped === needle) return true;
71748
+ const [shorter, longer] = stamped.length <= needle.length ? [stamped, needle] : [needle, stamped];
71749
+ return shorter.length >= TENANT_STATUS_WAIT_MIN_SHA_LENGTH && longer.startsWith(shorter);
71750
+ }
71751
+ async function waitForDeployedTag(deps) {
71752
+ const intervalMs = deps.intervalMs ?? TENANT_STATUS_WAIT_INTERVAL_MS;
71753
+ const now = deps.now ?? Date.now;
71754
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
71755
+ const startedAt = now();
71756
+ const deadline = startedAt + deps.timeoutMs;
71757
+ let polls = 0;
71758
+ for (; ; ) {
71759
+ const status = await deps.readStatus();
71760
+ polls += 1;
71761
+ if (deployedTagReaches(deps.deployedTagOf(status), deps.sha)) {
71762
+ return { matched: true, status, polls, elapsedMs: now() - startedAt };
71763
+ }
71764
+ const remaining = deadline - now();
71765
+ if (remaining <= 0) return { matched: false, status, polls, elapsedMs: now() - startedAt };
71766
+ await sleep2(Math.min(intervalMs, remaining));
71767
+ }
71768
+ }
71769
+
71443
71770
  // src/tenant-sweep.ts
71444
71771
  function isRcBearingTenant(p) {
71445
71772
  if ((p.deployModel ?? "") !== "tenant-container") return false;
@@ -73498,12 +73825,33 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
73498
73825
  return failGraceful(`runtime tenant reconcile: ${e.message}`);
73499
73826
  }
73500
73827
  });
73501
- tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
73828
+ tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137); `--wait-for <sha>` polls the same read to a deadline (#9942)").option("--wait-for <sha>", "poll until DEPLOY#<stage> deployedTag reaches this commit SHA (7-40 hex, short form ok), then print the final status and exit 0; exits 1 on timeout \u2014 use this instead of a shell sleep loop (#9942)").option("--timeout <secs>", `with --wait-for: give up after this many seconds (default ${TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S}, max ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}, polled every ${TENANT_STATUS_WAIT_INTERVAL_MS / 1e3}s)`).action(async (repo, stage, o) => {
73502
73829
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
73830
+ let wait;
73831
+ if (o.waitFor !== void 0) {
73832
+ const sha = parseWaitForSha(o.waitFor);
73833
+ if (!sha.ok) return fail(`runtime tenant status: ${sha.reason}`);
73834
+ const timeout = parseWaitTimeoutSeconds(o.timeout);
73835
+ if (!timeout.ok) return fail(`runtime tenant status: ${timeout.reason}`);
73836
+ wait = { sha: sha.sha, seconds: timeout.seconds, clamped: timeout.clamped };
73837
+ } else if (o.timeout !== void 0) {
73838
+ return fail("runtime tenant status: --timeout only applies with --wait-for <sha>");
73839
+ }
73503
73840
  const cfg = await loadConfig();
73504
- const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
73841
+ const readStatus = () => buildTenantRuntimeStatusFor(repo, stage, cfg);
73842
+ let result;
73843
+ let waitMatched;
73844
+ if (!wait) result = await readStatus();
73845
+ else {
73846
+ console.error(`runtime tenant status: waiting up to ${wait.seconds}s for ${repo} ${stage} deployedTag to reach ${wait.sha}${wait.clamped ? ` (--timeout clamped to the ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}s max)` : ""}`);
73847
+ const waited = await waitForDeployedTag({ sha: wait.sha, timeoutMs: wait.seconds * 1e3, readStatus, deployedTagOf: (status) => status.deploy?.deployedTag });
73848
+ result = waited.status;
73849
+ waitMatched = waited.matched;
73850
+ if (!waited.matched) console.error(`runtime tenant status: timed out after ${Math.round(waited.elapsedMs / 1e3)}s (limit ${wait.seconds}s, ${waited.polls} read(s)); deployedTag is ${waited.status.deploy?.deployedTag ?? "unset"}, not ${wait.sha}`);
73851
+ }
73505
73852
  console.log(JSON.stringify(result, null, 2));
73506
- if (result.publicProbe?.ok === false || result.edgeCertificate?.missing) process.exitCode = 1;
73853
+ if (waitMatched !== void 0) process.exitCode = waitMatched ? 0 : 1;
73854
+ else if (result.publicProbe?.ok === false || result.edgeCertificate?.missing) process.exitCode = 1;
73507
73855
  });
73508
73856
  tenant.command("logs <owner/repo> <stage>").description("READ-ONLY: read a tenant stage's application journal (journald, so a deploy-recreated container still answers) \u2014 the unit namespace comes from the tenant's DEPLOY coords and the blue/green slot is chosen on the box, never from a flag; bounded and redacted (#9230)").option("--tail <n>", `lines to return (1-${TENANT_LOG_TAIL_MAX}, default ${TENANT_LOG_TAIL_DEFAULT}); the applied bound is printed`).option("--since <window>", "window start: a relative window like 15m, 6h or 2d, or a UTC time like 2026-10-07T13:10Z (default 1h)").option("--until <time>", "window end: same forms as --since (default: now) \u2014 the box returns the newest N lines OF the window, so end it earlier to reach older lines on a busy tenant (#9878)").option("--grep <needle>", "keep only returned lines containing this literal text (applied to the bounded tail, not the whole journal)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
73509
73857
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.30",
3
+ "version": "4.6.32",
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",