@mutmutco/cli 4.6.0 → 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs CHANGED
@@ -189,6 +189,7 @@ async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
189
189
  }, watchdogMs);
190
190
  timer.unref?.();
191
191
  await closeHttpPool();
192
+ await flushStdio();
192
193
  }
193
194
  function describeActiveHandles() {
194
195
  let summary;
@@ -555,6 +556,7 @@ async function githubKeyringTokenRead() {
555
556
  return cachedGhCliTokenRead;
556
557
  }
557
558
  async function githubTokenRead() {
559
+ if (process.env.MMI_GH_POOL === "app" && process.env.GH_TOKEN) return { state: "ok", token: process.env.GH_TOKEN };
558
560
  const mmiPat = process.env.MMI_GH_PAT?.trim();
559
561
  if (mmiPat) return { state: "ok", token: mmiPat };
560
562
  if (process.env.GH_TOKEN) return { state: "ok", token: process.env.GH_TOKEN };
@@ -15445,8 +15447,10 @@ async function activateAppActor(commandPath4, env, mint, note = (line) => proces
15445
15447
  }
15446
15448
  return "personal";
15447
15449
  }
15450
+ delete env.MMI_GH_PAT;
15448
15451
  throw e;
15449
15452
  }
15453
+ delete env.MMI_GH_PAT;
15450
15454
  env.GH_TOKEN = minted.token;
15451
15455
  env.MMI_GH_POOL = "app";
15452
15456
  activeQuotaScope = minted.quotaScope;
@@ -21561,7 +21565,8 @@ async function postIssueComment(client, input) {
21561
21565
  number: parsed.number,
21562
21566
  repo,
21563
21567
  url: `https://github.com/${repo}/issues/${parsed.number}`,
21564
- commentUrl: comment.html_url
21568
+ commentUrl: comment.html_url,
21569
+ ...comment.user?.login ? { author: comment.user.login } : {}
21565
21570
  };
21566
21571
  }
21567
21572
 
@@ -22079,19 +22084,19 @@ function evaluateClaim(item, login) {
22079
22084
  if (others.length && item.status === "In Review") {
22080
22085
  return {
22081
22086
  ok: false,
22082
- reason: `${item.ref} is not claimable: In Review and held by @${others.join(", @")} \u2014 ask the holder, or wait for the review to land`
22087
+ reason: `${item.ref} is not claimable: In Review and held by @${others.join(", @")} \u2014 ask the holder, or wait for the review to land (never chain separate claims with && \u2014 a refused claim aborts the chain; pass all refs in one batch call, e.g. \`mmi-cli oracle board claim <ref> <ref>\`)`
22083
22088
  };
22084
22089
  }
22085
- if (others.length) return { ok: false, reason: `${item.ref} is already assigned to @${others.join(", @")}` };
22090
+ if (others.length) return { ok: false, reason: `${item.ref} is already assigned to @${others.join(", @")} (never chain separate claims with && \u2014 a refused claim aborts the chain; pass all refs in one batch call, e.g. \`mmi-cli oracle board claim <ref> <ref>\`)` };
22086
22091
  if (item.status === "In Progress" && mine) return { ok: true, alreadyClaimed: true };
22087
22092
  if (item.status === "Done") {
22088
22093
  return {
22089
22094
  ok: false,
22090
- reason: `${item.ref} is not claimable: Status is Done \u2014 claim does not reopen terminal cards; skip completed items and pass only open issues (never chain separate claims with &&)`
22095
+ reason: `${item.ref} is not claimable: Status is Done \u2014 claim does not reopen terminal cards; skip completed items and pass only open issues (never chain separate claims with && \u2014 a refused claim aborts the chain; pass all refs in one batch call, e.g. \`mmi-cli oracle board claim <ref> <ref>\`)`
22091
22096
  };
22092
22097
  }
22093
22098
  if (item.status !== "Todo" && item.status !== "In Progress" && item.status !== "In Review") {
22094
- return { ok: false, reason: `${item.ref} is not claimable: Status is ${item.status}` };
22099
+ return { ok: false, reason: `${item.ref} is not claimable: Status is ${item.status} (never chain separate claims with && \u2014 a refused claim aborts the chain; pass all refs in one batch call, e.g. \`mmi-cli oracle board claim <ref> <ref>\`)` };
22095
22100
  }
22096
22101
  return { ok: true, alreadyClaimed: false };
22097
22102
  }
@@ -23600,10 +23605,11 @@ async function claimOneBoardItem(ctx, selector, options) {
23600
23605
  let staleStamp;
23601
23606
  const tookOver = () => ({ outcome: "took-over", holder, previousHolder, ...staleStamp ? { staleStamp } : {} });
23602
23607
  const claimedReceipt = () => ({
23608
+ held: true,
23603
23609
  ...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
23604
23610
  ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "claimed", holder }
23605
23611
  });
23606
- const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "held", holder };
23612
+ const heldReceipt = () => ({ held: true, ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "held", holder } });
23607
23613
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
23608
23614
  const pre = evaluateClaim(flatItem, assignedLogin);
23609
23615
  if (!pre.ok) throw new NotClaimableError(pre.reason, { ref: flatItem.ref, status: flatItem.status });
@@ -23714,7 +23720,7 @@ async function claimBoardIssues(options, deps = {}) {
23714
23720
  const ref = `${selector.repo}#${selector.number}`;
23715
23721
  try {
23716
23722
  const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
23717
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, staleStamp: result.staleStamp, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
23723
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, held: result.held, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, staleStamp: result.staleStamp, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
23718
23724
  } catch (e) {
23719
23725
  results[index] = { ref, claimed: false, reason: e.message };
23720
23726
  }
@@ -24816,7 +24822,7 @@ function checkRunbookCoverage(docs, base, git3) {
24816
24822
  const globs = lines2.slice(heading.start, markerEnd - 1).flatMap((text) => /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim()) ?? []);
24817
24823
  const affected = changed.filter((path2) => globs.some((glob) => matches(glob, path2)) && !changesOnlyUses(path2));
24818
24824
  if (affected.length && !ranges.some((r) => r.count > 0 && r.start < end && r.start + r.count > heading.start)) {
24819
- findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title}: changed ${affected.join(", ")}; update this section's text or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
24825
+ findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
24820
24826
  }
24821
24827
  }
24822
24828
  }
@@ -25117,7 +25123,7 @@ function createDocsIndexDeps(repoRoot2) {
25117
25123
  // src/gate-budget.ts
25118
25124
  var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
25119
25125
  var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
25120
- var BLESSED_KIT_AGENT_FILES_SHA = "a8b273ed418e703c0aa501a53ea87228ba28b1f0";
25126
+ var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
25121
25127
  var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
25122
25128
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
25123
25129
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -27280,10 +27286,10 @@ var rollout_plan_default = {
27280
27286
  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)."
27281
27287
  },
27282
27288
  baseline: {
27283
- version: "4.6.0",
27284
- tag: "v4.6.0",
27285
- commit: "72a2b5df3188",
27286
- npm: "@mutmutco/cli@4.6.0"
27289
+ version: "4.6.2",
27290
+ tag: "v4.6.2",
27291
+ commit: "1708e0fdc55b",
27292
+ npm: "@mutmutco/cli@4.6.2"
27287
27293
  },
27288
27294
  exitCriterion: "fleet-n-of-n",
27289
27295
  hubOnlyShortcut: "forbidden",
@@ -27300,14 +27306,14 @@ var rollout_plan_default = {
27300
27306
  repo: "mutmutco/mmi-hub",
27301
27307
  role: "canary",
27302
27308
  schedule: "train",
27303
- v3Target: "v4.6.0"
27309
+ v3Target: "v4.6.2"
27304
27310
  }
27305
27311
  ],
27306
27312
  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.",
27307
27313
  rollback: {
27308
27314
  independent: true,
27309
- mechanism: "npm dist-tag latest -> 4.6.0 and redeploy the Hub Lambda from tag v4.6.0 (72a2b5df3188); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27310
- v3Target: "v4.6.0 (@mutmutco/cli@4.6.0, tag commit 72a2b5df3188 \u2014 last known-good release carrying the repo-index v4-only contract)"
27315
+ mechanism: "npm dist-tag latest -> 4.6.2 and redeploy the Hub Lambda from tag v4.6.2 (1708e0fdc55b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27316
+ v3Target: "v4.6.2 (@mutmutco/cli@4.6.2, tag commit 1708e0fdc55b \u2014 last known-good release carrying the repo-index v4-only contract)"
27311
27317
  }
27312
27318
  },
27313
27319
  {
@@ -58054,10 +58060,11 @@ function registerCollaborationCommands(program3) {
58054
58060
  return;
58055
58061
  } catch (reRead) {
58056
58062
  const reErr = reRead;
58057
- return fail(`issue view: ${repo}#${n} was transferred to ${moved.repo}#${moved.number} (${moved.url}), but re-reading it there failed: ${(reErr.stderr || reErr.message || String(reRead)).trim()}`);
58063
+ return await failGraceful(`issue view: ${repo}#${n} was transferred to ${moved.repo}#${moved.number} (${moved.url}), but re-reading it there failed: ${(reErr.stderr || reErr.message || String(reRead)).trim()}`);
58058
58064
  }
58059
58065
  }
58060
- return fail(`issue view: ${raw}${argvNote}`);
58066
+ const repoHint = /Could not resolve to an issue/i.test(raw) ? ` (if #${n} is in another repository, pass --repo <owner/repo>)` : "";
58067
+ return await failGraceful(`issue view: ${raw}${repoHint}${argvNote}`);
58061
58068
  }
58062
58069
  });
58063
58070
  issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").option("--fail-soft", "suppress errors (used by the background auto-caller; direct runs fail loudly)").action(async (o) => {
@@ -58723,7 +58730,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
58723
58730
  readMergeQueue: readPrMergeQueue,
58724
58731
  findMergeGroupRun: findPrMergeGroupRun,
58725
58732
  queueProgress: (receipt) => {
58726
- if (!o.json) printLine(prMergeQueueLine(receipt));
58733
+ const line = prMergeQueueLine(receipt);
58734
+ if (line) (o.json ? console.warn : printLine)(line);
58727
58735
  },
58728
58736
  resolveRepo: async (prNumber, repoOpt) => {
58729
58737
  const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
@@ -64567,6 +64575,7 @@ function parseBrokerJsonFields(raw, allowed) {
64567
64575
  return value;
64568
64576
  }
64569
64577
  var BROKER_JSON_FLAGS_NOTE = "gh formatter flags are refused (--template can read child credentials; --web opens a browser) \u2014 use --json with field selection; --jq filters the returned JSON locally with the system jq, never inside gh";
64578
+ var JQ_HELP = "filter the returned JSON locally with the system jq (gh --jq habit; JSON stdout only; jq never sees a credential) (#8966)";
64570
64579
  var JQ_MAX_LENGTH = 4e3;
64571
64580
  function parseBrokerJq(raw) {
64572
64581
  if (!raw.trim() || raw.length > JQ_MAX_LENGTH) {
@@ -64611,6 +64620,14 @@ function parseBrokerIssueState(raw) {
64611
64620
  return v;
64612
64621
  }
64613
64622
  var RUN_LIST_STATUSES = ["queued", "completed", "in_progress", "requested", "waiting", "pending", "action_required", "cancelled", "failure", "neutral", "skipped", "stale", "startup_failure", "success", "timed_out"];
64623
+ function parseBrokerRunEvent(raw) {
64624
+ if (raw === void 0) return void 0;
64625
+ const event = raw.trim();
64626
+ if (!/^[A-Za-z0-9_]{1,64}$/.test(event)) {
64627
+ throw new Error(`read: --event must be a workflow trigger event name like pull_request, merge_group, push, workflow_dispatch (received '${raw}')`);
64628
+ }
64629
+ return event;
64630
+ }
64614
64631
  function parseBrokerRunStatus(raw) {
64615
64632
  if (raw === void 0) return void 0;
64616
64633
  const v = raw.trim().toLowerCase();
@@ -64706,6 +64723,8 @@ function buildRunListSpec(o) {
64706
64723
  if (branch) args.push("--branch", branch);
64707
64724
  const commit = parseBrokerCommit(o.commit);
64708
64725
  if (commit) args.push("--commit", commit);
64726
+ const event = parseBrokerRunEvent(o.event);
64727
+ if (event) args.push("--event", event);
64709
64728
  const status = parseBrokerRunStatus(o.status);
64710
64729
  if (status) args.push("--status", status);
64711
64730
  const created = parseBrokerCreated(o.created);
@@ -64721,17 +64740,21 @@ function buildRunWatchSpec(id, o) {
64721
64740
  const timeoutMin = o.timeoutMin !== void 0 ? parseBrokerInt(o.timeoutMin, "--timeout", 1, 120) : 30;
64722
64741
  return { program: "gh", args, timeoutMs: timeoutMin * 6e4 };
64723
64742
  }
64724
- function rejectPrViewQueueFields(fields, number) {
64725
- if (!fields || !fields.split(",").includes("mergeQueueEntry")) return;
64743
+ function rejectPrViewQueueFields(fields, number, repo) {
64744
+ const badField = fields?.split(",").find((field) => field === "mergeQueueEntry" || field === "isInMergeQueue");
64745
+ if (!badField) return;
64746
+ const targetRepo2 = repo ?? "<owner/repo>";
64747
+ const remaining = fields?.split(",").filter((f) => f && f !== "mergeQueueEntry" && f !== "isInMergeQueue").join(",") ?? "";
64726
64748
  throw new Error(
64727
- `read pr view: 'mergeQueueEntry' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo <owner/repo> --json\` and drop it from --json`
64749
+ `read pr view: '${badField}' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\` and drop it from --json${remaining ? `; for the remaining fields run \`mmi-cli devops read pr view ${number}${repo ? ` --repo ${repo}` : ""} --json ${remaining}\`` : ""}`
64728
64750
  );
64729
64751
  }
64730
64752
  function buildPrViewSpec(ref, o) {
64731
64753
  const number = parseBrokerId(ref, "PR number");
64732
64754
  const jsonFields = parseBrokerJsonFields(o.json);
64733
- rejectPrViewQueueFields(jsonFields, number);
64734
- const args = withJson(withRepo(["pr", "view", number], parseBrokerRepo(o.repo, "--repo")), jsonFields, o.json);
64755
+ const repo = parseBrokerRepo(o.repo, "--repo");
64756
+ rejectPrViewQueueFields(jsonFields, number, repo);
64757
+ const args = withJson(withRepo(["pr", "view", number], repo), jsonFields, o.json);
64735
64758
  if (o.comments) args.push("--comments");
64736
64759
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
64737
64760
  }
@@ -64807,8 +64830,8 @@ function ghExitCodeOf(error) {
64807
64830
  function brokerGhFailureHint(failureText, args) {
64808
64831
  const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
64809
64832
  const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
64810
- if (unknownField === "mergeQueueEntry") {
64811
- return `'mergeQueueEntry' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${target ?? "<number>"} --repo <owner/repo> --json\` and drop it from --json; repeating this read cannot succeed`;
64833
+ if (unknownField === "mergeQueueEntry" || unknownField === "isInMergeQueue") {
64834
+ return `'${unknownField}' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${target ?? "<number>"} --repo <owner/repo> --json\` and drop it from --json; repeating this read cannot succeed`;
64812
64835
  }
64813
64836
  if (unknownField) {
64814
64837
  return `gh rejected --json field '${unknownField}' \u2014 drop it or use the typed read whose fields cover it (the "Available fields" list gh printed is its own vocabulary)`;
@@ -64950,7 +64973,6 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
64950
64973
  const runStatusOpt = (v) => parseBrokerRunStatus(v);
64951
64974
  const assigneeOpt = (v) => parseBrokerAssignee(v);
64952
64975
  const jqOpt = (v) => parseBrokerJq(v);
64953
- const JQ_HELP = "filter the returned JSON locally with the system jq (gh --jq habit; needs --json; jq never sees a credential) (#8966)";
64954
64976
  const read = program3.command("read").description(`closed typed GitHub reads on the App pool (${BROKER_JSON_FLAGS_NOTE})`);
64955
64977
  const run = read.command("run").description("workflow-run reads (view/list/watch pass gh through; log is a bounded App REST read)");
64956
64978
  registerRunLogRead(run, overrides?.apiRunner);
@@ -64964,7 +64986,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
64964
64986
  }
64965
64987
  await dispatchGh(spec, "read run view", o.jq);
64966
64988
  });
64967
- 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; larger values clamp to 100)", limitOpt).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
64989
+ run.command("list").description("list workflow runs (workflow/branch/commit/status/event/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("-e, --event <event>", "filter by workflow trigger event (e.g. pull_request, merge_group, push, workflow_dispatch)", (v) => parseBrokerRunEvent(v)).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; larger values clamp to 100)", limitOpt).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
64968
64990
  let spec;
64969
64991
  try {
64970
64992
  requireJqJson(o.jq, o.json);
@@ -65173,11 +65195,18 @@ function orgScopedHint(path2) {
65173
65195
  const surface = runners ? `Runner/lane state has no App-token read: a human token holder answers it with \`gh api orgs/${org}/actions/runners\` (docs/Guides/gh-runner-runbook.md).` : "It needs a human token holder with an org-scoped token, or a dedicated mmi-cli verb when one exists for this resource.";
65174
65196
  return ` This route is ORG-SCOPED (\`orgs/${org}/...\`) and is deliberately outside the allowlist \u2014 no App-token read exists for org-level resources, so retrying path variants cannot work and no \`repos/{owner}/{repo}/...\` respelling of it will be accepted. ${surface} A refusal writes nothing to stdout: check the exit code and never merge stderr into the JSON pipe (2>&1).`;
65175
65197
  }
65198
+ function tagsReadHint(path2) {
65199
+ const match = new RegExp(`^repos/(${NAME})/(${NAME})/(?:tags|git/(?:refs|matching-refs|tags|commits))(?:[/?#]|$)`).exec(path2);
65200
+ if (!match) return "";
65201
+ const slug = `${match[1]}/${match[2]}`;
65202
+ return ` No App-token REST read exists for git tags or refs \u2014 read git tags directly with \`git ls-remote --tags origin 'refs/tags/*'\` (no token needed; \`git ls-remote origin refs/tags/<tag>\` for one tag), or read tag-backed GitHub Releases with \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/releases?per_page=20"\`.`;
65203
+ }
65176
65204
  function parseApiGetPath(rawPath) {
65177
65205
  const path2 = rawPath.trim();
65178
65206
  const refuse = () => {
65179
65207
  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." : "";
65180
- return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${hint}`);
65208
+ const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${hint}`;
65209
+ return new Error(`api get: refusing '${rawPath}' \u2014${routeHints}${API_GET_RECOVERY} Only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.`);
65181
65210
  };
65182
65211
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
65183
65212
  const q = path2.indexOf("?");
@@ -65225,11 +65254,13 @@ async function readAppApi(path2, runner = apiReadRunner) {
65225
65254
  }
65226
65255
  function registerApiReadCommands(program3, overrides) {
65227
65256
  const spawn7 = overrides?.runner ?? apiReadRunner;
65257
+ const jqRun = overrides?.jqRunner ?? systemJqRunner;
65228
65258
  const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)");
65229
65259
  api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
65230
65260
  acceptHeaderFor(v);
65231
65261
  return v.trim().toLowerCase();
65232
- }, "json").option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)").argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o) => {
65262
+ }, "json").option("--jq <expr>", JQ_HELP, (v) => parseBrokerJq(v)).option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)").argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o) => {
65263
+ if (o.jq !== void 0 && (o.accept === "raw" || o.out !== void 0)) return fail("api get: --jq is only valid with JSON stdout reads (not --accept raw or --out)");
65233
65264
  const zip = req.kind === "actions-artifact-zip" || req.kind === "actions-job-logs" || req.kind === "release-asset";
65234
65265
  if (zip !== (o.out !== void 0)) return fail(`api get: --out <file> is required for, and only valid with, repos/{owner}/{repo}/actions/artifacts/{id}/zip and repos/{owner}/{repo}/actions/jobs/{id}/logs and repos/{owner}/{repo}/releases/assets/{id}`);
65235
65266
  const appRoute = req.kind.startsWith("actions-") || req.kind.startsWith("release-");
@@ -65242,6 +65273,7 @@ function registerApiReadCommands(program3, overrides) {
65242
65273
  req.path,
65243
65274
  o.accept
65244
65275
  );
65276
+ if (o.jq !== void 0) body = await jqRun(o.jq, body, localJqEnv());
65245
65277
  if (zip) {
65246
65278
  (0, import_node_fs56.writeFileSync)(o.out, body, "latin1");
65247
65279
  return;
@@ -68188,7 +68220,9 @@ function registerDeveloperCommands(program3) {
68188
68220
  );
68189
68221
  return;
68190
68222
  }
68191
- console.error("doc refs: unresolved references");
68223
+ const hasStale = result.findings.some((f) => f.kind === "runbook-section-stale" || f.kind === "seed-runbook-unreviewed");
68224
+ const hasRefs = result.findings.some((f) => f.kind !== "runbook-section-stale" && f.kind !== "seed-runbook-unreviewed");
68225
+ console.error(hasStale && !hasRefs ? "doc refs: stale runbook section(s)" : hasStale && hasRefs ? "doc refs: unresolved references and stale runbook section(s)" : "doc refs: unresolved references");
68192
68226
  for (const f of result.findings) console.error(`- ${f.doc}:${f.line} [${f.kind}] ${f.detail}`);
68193
68227
  process.exitCode = 1;
68194
68228
  } catch (e) {
@@ -8217,7 +8217,7 @@ function checkRunbookCoverage(docs, base, git) {
8217
8217
  const globs = lines.slice(heading.start, markerEnd - 1).flatMap((text) => /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim()) ?? []);
8218
8218
  const affected = changed.filter((path2) => globs.some((glob) => matches(glob, path2)) && !changesOnlyUses(path2));
8219
8219
  if (affected.length && !ranges.some((r) => r.count > 0 && r.start < end && r.start + r.count > heading.start)) {
8220
- findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title}: changed ${affected.join(", ")}; update this section's text or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
8220
+ findings.push({ kind: "runbook-section-stale", doc, line: heading.start, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
8221
8221
  }
8222
8222
  }
8223
8223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.0",
3
+ "version": "4.6.2",
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",