@mutmutco/cli 4.5.24 → 4.5.25

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 +331 -50
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -791,6 +791,8 @@ var init_house_map = __esm({
791
791
  // register/run/park schedule rows under the one lane contract
792
792
  // --- devops — shipping --------------------------------------------------------------------------
793
793
  pr: "devops",
794
+ api: "devops",
795
+ // allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
794
796
  ci: "devops",
795
797
  // the CI/gate audit
796
798
  rcand: "devops",
@@ -6537,6 +6539,10 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
6537
6539
  }
6538
6540
  let body = item.details?.body;
6539
6541
  if (body === void 0) {
6542
+ if (item.detailsUnavailable) {
6543
+ results[index] = { bucket: "claimable", item, warnings: [] };
6544
+ continue;
6545
+ }
6540
6546
  if (opts.requireBundledBody) {
6541
6547
  results[index] = { bucket: "claimable", item, warnings: [] };
6542
6548
  continue;
@@ -6544,7 +6550,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
6544
6550
  try {
6545
6551
  const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
6546
6552
  body = data?.body ?? "";
6547
- item.details = { body, comments: item.details?.comments ?? [] };
6553
+ item.details = { body, comments: item.details?.comments ?? [], ...item.details?.comments === void 0 ? { commentsUnread: true } : {} };
6548
6554
  } catch (e) {
6549
6555
  results[index] = {
6550
6556
  bucket: "claimable",
@@ -8026,7 +8032,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
8026
8032
  }
8027
8033
  async function probePublicHealth(publicUrl) {
8028
8034
  const root = await probeHttpBounded(publicUrl);
8029
- if (root.status !== 401 && root.status !== 403) return root;
8035
+ if (root.status !== 401 && root.status !== 403 && root.status !== 404) return root;
8030
8036
  return probeHttpBounded(`${publicUrl.replace(/\/$/, "")}/health`);
8031
8037
  }
8032
8038
  var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
@@ -8048,7 +8054,7 @@ function tenantRuntimeHints(stage, fact, probe, edgeCertificate) {
8048
8054
  if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
8049
8055
  if (!fact.domain) hints.push(`DEPLOY#${stage} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
8050
8056
  if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
8051
- if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root is auth-walled (401/403) and ${probe.url} answered ${probe.status ?? probe.error}; expose a 2xx /health route or fix the app \u2014 the edge is fine.`);
8057
+ if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root answered 401/403/404 and ${probe.url} answered ${probe.status ?? probe.error}; the app is up and the edge is fine \u2014 expose a 2xx /health route to make this probe green.`);
8052
8058
  else if (edgeCertificate?.missing && edgeCertificate.hint) hints.push(edgeCertificate.hint);
8053
8059
  else if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
8054
8060
  if (stage === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
@@ -12447,6 +12453,7 @@ function renderBoardSource() {
12447
12453
  return "source: live";
12448
12454
  }
12449
12455
  async function collectBoardPreferringSnapshot(cfg, options, deps) {
12456
+ if (options.snapshotOnly && !deps.snapshot) throw new Error("--snapshot-only needs the Hub snapshot transport (not with --direct)");
12450
12457
  const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
12451
12458
  {
12452
12459
  owner: cfg.projectOwner,
@@ -12486,6 +12493,9 @@ async function collectBoardPreferringSnapshot(cfg, options, deps) {
12486
12493
  snapshot
12487
12494
  };
12488
12495
  }
12496
+ if (options.snapshotOnly && attempt?.state === "unavailable") {
12497
+ throw new Error(`Hub board snapshot unavailable (${attempt.reason}) \u2014 --snapshot-only refuses the direct user-auth fallback`);
12498
+ }
12489
12499
  const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
12490
12500
  if (attempt?.state === "unavailable") {
12491
12501
  collected.warnings.push(`Hub board snapshot unavailable (${attempt.reason}) \u2014 served by the direct user-auth read (emergency fallback)`);
@@ -12768,7 +12778,13 @@ function nodeToItem(node, cfg) {
12768
12778
  priorityOptionId: fields.priority?.optionId,
12769
12779
  assignees,
12770
12780
  labels,
12771
- type: labels.find((label) => TYPE_LABELS.includes(label))
12781
+ type: labels.find((label) => TYPE_LABELS.includes(label)),
12782
+ ...content.__typename === "Issue" && content.claimEvidenceUnavailable ? { detailsUnavailable: "App-token claim evidence unread" } : {},
12783
+ ...content.__typename === "Issue" && typeof content.body === "string" && Array.isArray(content.comments?.nodes) && typeof content.comments.totalCount === "number" && Number.isInteger(content.comments.totalCount) && content.comments.totalCount >= content.comments.nodes.length ? { details: {
12784
+ body: content.body,
12785
+ comments: content.comments.nodes.map((comment) => ({ author: comment.author?.login ?? "", body: comment.body ?? "" })),
12786
+ ...content.comments.totalCount > content.comments.nodes.length ? { omittedComments: content.comments.totalCount - content.comments.nodes.length } : {}
12787
+ } } : {}
12772
12788
  })
12773
12789
  };
12774
12790
  }
@@ -12783,7 +12799,8 @@ async function attachBundleDetails(report, client, allowPartial, options = {}) {
12783
12799
  while (next < candidates.length) {
12784
12800
  const item = candidates[next++];
12785
12801
  try {
12786
- item.details = await fetchIssueDetails(client, item.repository, item.number);
12802
+ if (!item.details && !item.detailsUnavailable) item.details = await fetchIssueDetails(client, item.repository, item.number);
12803
+ if (!item.details) continue;
12787
12804
  const claim = boardItemClaim(item);
12788
12805
  if (claim) item.claim = claim;
12789
12806
  } catch (e) {
@@ -13017,7 +13034,8 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13017
13034
  const warnings = [];
13018
13035
  let partial = false;
13019
13036
  const fetchComments = deps.fetchComments ?? (async (item) => {
13020
- if (item.details) return item.details.comments;
13037
+ if (item.detailsUnavailable) return void 0;
13038
+ if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13021
13039
  const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13022
13040
  return comments.map((comment) => ({ body: comment.body ?? "" }));
13023
13041
  });
@@ -13600,11 +13618,6 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
13600
13618
  warnings: collected.warnings,
13601
13619
  partial: collected.partial
13602
13620
  };
13603
- for (const scope of ["primary", "secondary"]) {
13604
- const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
13605
- report[scope].claimable = filtered.claimable;
13606
- report.warnings.push(...filtered.warnings);
13607
- }
13608
13621
  return {
13609
13622
  cfg,
13610
13623
  client,
@@ -14061,7 +14074,7 @@ init_github_client();
14061
14074
  var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
14062
14075
  var PRODUCT_GATE_CONTEXT = "gate";
14063
14076
  var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
14064
- function reseedProductRulesetStrictness(current, seed) {
14077
+ function reseedProductRulesetStrictness(current, seed, declared = {}) {
14065
14078
  const parse = (raw) => {
14066
14079
  const policy2 = JSON.parse(raw);
14067
14080
  if (!policy2 || typeof policy2 !== "object" || Array.isArray(policy2) || policy2.name !== PRODUCT_RULESET_NAME || policy2.target !== "branch" || !Array.isArray(policy2.rules) || policy2.rules.some((r) => !r || typeof r !== "object" || Array.isArray(r) || typeof r.type !== "string")) {
@@ -14074,14 +14087,28 @@ function reseedProductRulesetStrictness(current, seed) {
14074
14087
  }
14075
14088
  return { policy: policy2, parameters: parameters2 };
14076
14089
  };
14077
- const strict = parse(seed).parameters.strict_required_status_checks_policy;
14090
+ const canonical = parse(seed);
14091
+ const strict = canonical.parameters.strict_required_status_checks_policy;
14078
14092
  if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
14079
14093
  if (current === null) return seed;
14080
14094
  const { policy, parameters } = parse(current);
14081
- if (parameters.strict_required_status_checks_policy === strict) return current;
14095
+ let changed = parameters.strict_required_status_checks_policy !== strict;
14082
14096
  parameters.strict_required_status_checks_policy = strict;
14083
- return `${JSON.stringify(policy, null, 2)}
14084
- `;
14097
+ if (declared.requiredChecks) {
14098
+ const checks = canonical.parameters.required_status_checks.map((check) => parameters.required_status_checks.find((old) => old.context === check.context) ?? check);
14099
+ if (JSON.stringify(parameters.required_status_checks) !== JSON.stringify(checks)) changed = true;
14100
+ parameters.required_status_checks = checks;
14101
+ }
14102
+ if (declared.requiredCheckBranches) {
14103
+ const refs = canonical.policy.conditions?.ref_name?.include;
14104
+ if (!Array.isArray(refs) || !refs.length) throw new Error("canonical product ruleset must declare branch includes");
14105
+ const conditions = policy.conditions;
14106
+ const refName = conditions?.ref_name;
14107
+ if (JSON.stringify(refName?.include) !== JSON.stringify(refs)) changed = true;
14108
+ policy.conditions = { ...conditions, ref_name: { ...refName, include: refs } };
14109
+ }
14110
+ return changed ? `${JSON.stringify(policy, null, 2)}
14111
+ ` : current;
14085
14112
  }
14086
14113
  function stripRulesetComment(raw) {
14087
14114
  const parsed = JSON.parse(raw);
@@ -15398,6 +15425,20 @@ function labelsToPrune(orgLabelNames) {
15398
15425
  const org = new Set(orgLabelNames);
15399
15426
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
15400
15427
  }
15428
+ function planBootstrapLabels(labels, live) {
15429
+ const existing = new Set(live.map((label) => label.name));
15430
+ return [
15431
+ ...labels.map((label) => ({ name: label.name, action: existing.has(label.name) ? "UPDATE" : "CREATE" })),
15432
+ ...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
15433
+ ];
15434
+ }
15435
+ function renderBootstrapAncillaryPlan(labels, environments) {
15436
+ return [
15437
+ "bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
15438
+ ...labels.map(({ action, name }) => ` ${action} label ${name}`),
15439
+ ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
15440
+ ].join("\n");
15441
+ }
15401
15442
  function resolveSeedContent(seed, vars, readFile9) {
15402
15443
  if (seed.source === "self") return readFile9(seed.target);
15403
15444
  if (seed.source.startsWith("seed:")) {
@@ -15418,6 +15459,11 @@ function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
15418
15459
  const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
15419
15460
  return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
15420
15461
  }
15462
+ function edgeCertificateStagesForBootstrap(track, domains) {
15463
+ if (!domains || typeof domains !== "object" || Array.isArray(domains)) return [];
15464
+ const stages = branchesForTrack(track);
15465
+ return ["dev", "rc"].filter((stage) => stages.includes(stage === "dev" ? "development" : stage) && Boolean(domains[stage]));
15466
+ }
15421
15467
  function buildRegisterPayload(repo, cls, vars, options = {}) {
15422
15468
  const parsedRepo = parseOwnerRepo(repo);
15423
15469
  const slug = parsedRepo.slug;
@@ -15442,6 +15488,12 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
15442
15488
  );
15443
15489
  }
15444
15490
  const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
15491
+ const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
15492
+ const inferredDomains = inferEdgeDomainsForWrite(slug);
15493
+ const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).map((branch) => {
15494
+ const stage = branch === "development" ? "dev" : branch;
15495
+ return [stage, inferredDomains[stage]];
15496
+ }));
15445
15497
  const num = (v) => {
15446
15498
  if (v == null || v === "") return void 0;
15447
15499
  const n = Number(v);
@@ -15471,10 +15523,9 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
15471
15523
  deployModel,
15472
15524
  // Persist the canonical stage hosts at the same write-time boundary as deployModel. Read paths never
15473
15525
  // infer from an absent edgeDomains field.
15474
- edgeDomains: options.existing?.edgeDomains ?? inferEdgeDomainsForWrite(slug),
15526
+ edgeDomains: options.existing?.edgeDomains ?? derivedDomains,
15475
15527
  // #1359: always persist an explicit track so release tooling never guesses from absence alone.
15476
- // #7134: an explicit flag wins, then the registered track, then the class default (first bootstrap).
15477
- releaseTrack: options.releaseTrack ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0),
15528
+ releaseTrack,
15478
15529
  // Board coords (from GraphQL at bootstrap, passed as --var by the skill).
15479
15530
  projectOwner: vars.PROJECT_OWNER || void 0,
15480
15531
  projectNumber: num(vars.PROJECT_NUMBER),
@@ -16753,10 +16804,10 @@ var rollout_plan_default = {
16753
16804
  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)."
16754
16805
  },
16755
16806
  baseline: {
16756
- version: "4.5.24",
16757
- tag: "v4.5.24",
16758
- commit: "4d303073492a",
16759
- npm: "@mutmutco/cli@4.5.24"
16807
+ version: "4.5.25",
16808
+ tag: "v4.5.25",
16809
+ commit: "d7c572c38021",
16810
+ npm: "@mutmutco/cli@4.5.25"
16760
16811
  },
16761
16812
  exitCriterion: "fleet-n-of-n",
16762
16813
  hubOnlyShortcut: "forbidden",
@@ -16773,14 +16824,14 @@ var rollout_plan_default = {
16773
16824
  repo: "mutmutco/mmi-hub",
16774
16825
  role: "canary",
16775
16826
  schedule: "train",
16776
- v3Target: "v4.5.24"
16827
+ v3Target: "v4.5.25"
16777
16828
  }
16778
16829
  ],
16779
16830
  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.",
16780
16831
  rollback: {
16781
16832
  independent: true,
16782
- mechanism: "npm dist-tag latest -> 4.5.24 and redeploy the Hub Lambda from tag v4.5.24 (4d303073492a); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16783
- v3Target: "v4.5.24 (@mutmutco/cli@4.5.24, tag commit 4d303073492a \u2014 last known-good release carrying the repo-index v4-only contract)"
16833
+ mechanism: "npm dist-tag latest -> 4.5.25 and redeploy the Hub Lambda from tag v4.5.25 (d7c572c38021); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16834
+ v3Target: "v4.5.25 (@mutmutco/cli@4.5.25, tag commit d7c572c38021 \u2014 last known-good release carrying the repo-index v4-only contract)"
16784
16835
  }
16785
16836
  },
16786
16837
  {
@@ -17108,7 +17159,7 @@ function ensureField(fields, field) {
17108
17159
  return list.join(",");
17109
17160
  }
17110
17161
  function resolveIssueViewGhFields(normalized) {
17111
- const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent");
17162
+ const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent" && f !== "closedBy");
17112
17163
  if (requested.length === 0) return { ghFields: "number", numberIsTransport: true };
17113
17164
  return { ghFields: requested.join(","), numberIsTransport: false };
17114
17165
  }
@@ -17597,6 +17648,45 @@ async function runPrForIssue(deps, issueRef, opts) {
17597
17648
  }
17598
17649
  return extractPrForIssueResponse(resp);
17599
17650
  }
17651
+ function closedByGraphqlArgs(owner, name, number) {
17652
+ const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issue(number:" + number + "){number closedByPullRequestsReferences(first:100){nodes{number merged mergedAt mergeCommit{oid} repository{owner{login} name}}}}}}";
17653
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
17654
+ }
17655
+ function extractClosedByResponse(resp) {
17656
+ const r = resp ?? {};
17657
+ if ((r.errors ?? []).some((e) => e?.type === "NOT_FOUND") || !r.data?.repository?.issue) {
17658
+ throw new QueryReadError("NOT_FOUND", "issue not found");
17659
+ }
17660
+ const out = [];
17661
+ for (const node of r.data.repository.issue.closedByPullRequestsReferences?.nodes ?? []) {
17662
+ const pr = node;
17663
+ if (typeof pr.number !== "number") continue;
17664
+ out.push({
17665
+ number: pr.number,
17666
+ merged: pr.merged === true,
17667
+ mergedAt: typeof pr.mergedAt === "string" ? pr.mergedAt : null,
17668
+ mergeCommit: pr.mergeCommit && typeof pr.mergeCommit.oid === "string" ? { oid: pr.mergeCommit.oid } : null,
17669
+ repository: {
17670
+ owner: { login: String(pr.repository?.owner?.login ?? "") },
17671
+ name: String(pr.repository?.name ?? "")
17672
+ }
17673
+ });
17674
+ }
17675
+ return out;
17676
+ }
17677
+ async function runClosedBy(deps, opts) {
17678
+ const { owner, name } = splitRepo(opts.repo);
17679
+ let resp;
17680
+ try {
17681
+ resp = await deps.ghJson(closedByGraphqlArgs(owner, name, opts.number), GH_LIST_TIMEOUT_MS);
17682
+ } catch (e) {
17683
+ if (isGhNotFound(e)) {
17684
+ throw new QueryReadError("NOT_FOUND", `#${opts.number} not found in ${opts.repo}`);
17685
+ }
17686
+ throw e;
17687
+ }
17688
+ return extractClosedByResponse(resp);
17689
+ }
17600
17690
  async function runIssueViewContext(deps, opts) {
17601
17691
  const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
17602
17692
  const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
@@ -26463,6 +26553,10 @@ var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressa
26463
26553
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
26464
26554
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
26465
26555
  var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
26556
+ var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
26557
+ var SHARED_ACTION_PINS = [
26558
+ { path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
26559
+ ];
26466
26560
  function slugFromRepo(repo) {
26467
26561
  return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
26468
26562
  }
@@ -26486,6 +26580,43 @@ async function restJson2(deps, path2, fallback) {
26486
26580
  return fallback;
26487
26581
  }
26488
26582
  }
26583
+ var sharedPinComparisons = /* @__PURE__ */ new WeakMap();
26584
+ function pinContainsFloor(deps, floor, pin) {
26585
+ let comparisons = sharedPinComparisons.get(deps);
26586
+ if (!comparisons) {
26587
+ comparisons = /* @__PURE__ */ new Map();
26588
+ sharedPinComparisons.set(deps, comparisons);
26589
+ }
26590
+ const key = `${floor}...${pin}`;
26591
+ let comparison = comparisons.get(key);
26592
+ if (!comparison) {
26593
+ comparison = deps.client.rest("GET", `repos/${HUB_REPO4}/compare/${key}`).then((result) => result?.status === "ahead" || result?.status === "identical" ? true : result?.status === "behind" || result?.status === "diverged" ? false : void 0).catch(() => void 0);
26594
+ comparisons.set(key, comparison);
26595
+ }
26596
+ return comparison;
26597
+ }
26598
+ async function auditSharedActionPins(files, deps) {
26599
+ const checks = [];
26600
+ for (const { path: path2, body } of files) {
26601
+ for (const [index, line] of body.split(/\r?\n/).entries()) {
26602
+ const match = /^\s*-?\s*uses:\s*['"]?mutmutco\/MMI-Hub\/\.github\/actions\/([^@\s'"]+)@([0-9a-f]{40})['"]?(?:\s*(?:#.*)?)$/.exec(line);
26603
+ if (!match) continue;
26604
+ const action = SHARED_ACTION_PINS.find((item) => item.path === match[1]);
26605
+ if (!action || match[2] === action.floor) continue;
26606
+ const safe = await pinContainsFloor(deps, action.floor, match[2]);
26607
+ if (safe === true) continue;
26608
+ const location = `${path2}:${index + 1}`;
26609
+ const replacement = `mutmutco/MMI-Hub/.github/actions/${action.path}@${action.floor}`;
26610
+ checks.push({
26611
+ ok: safe !== false,
26612
+ label: `${action.path} shared action pin is current`,
26613
+ detail: `${location}: ${action.path} pinned to ${match[2]} \u2014 ` + (safe === false ? `lacks the minimum safe floor ${action.floor}` : "could not verify ancestry against the safe floor; refresh the pin"),
26614
+ remediation: safe === false ? `Replace the uses: pin at ${location} with ${replacement}` : void 0
26615
+ });
26616
+ }
26617
+ }
26618
+ return checks;
26619
+ }
26489
26620
  async function rulesetDetails(deps, repo, list) {
26490
26621
  const details = [];
26491
26622
  for (const ruleset of list) {
@@ -27022,6 +27153,10 @@ async function auditRepoCi(repo, deps) {
27022
27153
  }
27023
27154
  }
27024
27155
  }
27156
+ if (repoClass !== "hub") {
27157
+ const workflows = await getBranchWorkflows();
27158
+ if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
27159
+ }
27025
27160
  const ok = checks.every((c) => c.ok);
27026
27161
  return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
27027
27162
  }
@@ -27585,7 +27720,7 @@ var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata"
27585
27720
  var PRIMARY_GROUPS = [
27586
27721
  ["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
27587
27722
  ["Plan and work", ["board", "issue", "stage"]],
27588
- ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
27723
+ ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train", "api"]],
27589
27724
  // `tests` sits beside `docs` deliberately: both are deterministic, repo-local gates a workflow
27590
27725
  // step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
27591
27726
  // joins them on the same footing (#3979); `dist status` does too (#5576) — the checkout's own
@@ -27633,6 +27768,7 @@ var COMMAND_OWNERSHIP = {
27633
27768
  issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
27634
27769
  stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
27635
27770
  pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
27771
+ api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
27636
27772
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
27637
27773
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
27638
27774
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -28372,7 +28508,10 @@ var APP_VAULT_REPO = "mutmutco/MMI-Hub";
28372
28508
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
28373
28509
  var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
28374
28510
  "pr checks-wait",
28375
- "pr view"
28511
+ "pr view",
28512
+ "issue list",
28513
+ "issue view",
28514
+ "api get"
28376
28515
  ]);
28377
28516
  var APP_POLL_PERMISSIONS = {
28378
28517
  metadata: "read",
@@ -28602,8 +28741,8 @@ async function attachBoardIssues(selectors, repoOption, deps = {}) {
28602
28741
  ref = `${parsed.repo}#${parsed.number}`;
28603
28742
  const cfg = resolveBoardConfig(await (deps.config ?? loadConfigForBoardSelector2)(selector, repoOption));
28604
28743
  const issue = await client.rest("GET", `repos/${parsed.repo}/issues/${parsed.number}`);
28605
- if (!issue.id) throw new Error("issue node id could not be read");
28606
- const projectItemId = await (deps.attach ?? attachViaHubApp)(cfg, issue.id);
28744
+ if (!issue.node_id) throw new Error("issue node id could not be read");
28745
+ const projectItemId = await (deps.attach ?? attachViaHubApp)(cfg, issue.node_id);
28607
28746
  return { ref, attached: true, projectItemId, credential: "app_installation" };
28608
28747
  } catch (e) {
28609
28748
  return { ref, attached: false, reason: e.message };
@@ -28652,7 +28791,8 @@ function registerBoardCommands(program3) {
28652
28791
  repo: o.repo,
28653
28792
  includeBundleDetails: o.bundleDetails,
28654
28793
  includeAllBodies: o.bodies,
28655
- allowPartial: o.allowPartial
28794
+ allowPartial: o.allowPartial,
28795
+ snapshotOnly: o.snapshotOnly
28656
28796
  }, o.direct ? {} : { snapshot: registryClientDeps(config) });
28657
28797
  const output = o.json ? JSON.stringify(report) : renderBoardReport(report);
28658
28798
  if (!o.out) console.log(output);
@@ -28695,7 +28835,7 @@ function registerBoardCommands(program3) {
28695
28835
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
28696
28836
  }
28697
28837
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
28698
- board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\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").action((o) => runBoardRead(o));
28838
+ board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--snapshot-only", "fail instead of falling back to your personal GraphQL pool when the Hub snapshot is unavailable \u2014 for ambient readers such as sidebars (#7203)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\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").action((o) => runBoardRead(o));
28699
28839
  withExamples(mutating(
28700
28840
  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"),
28701
28841
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
@@ -29618,6 +29758,43 @@ function parseGateVar(raw) {
29618
29758
  }
29619
29759
  return out;
29620
29760
  }
29761
+ var SECRET_OUTPUT_PATH_RE = /^\/[A-Za-z0-9._/-]+$/;
29762
+ var SECRET_OUTPUT_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
29763
+ var SECRET_OUTPUT_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
29764
+ function parseTenantTaskSecretOutput(raw, label) {
29765
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
29766
+ throw new Error(`org project set: ${label} must be an object {path,vault:{repo,key},overwrite?}`);
29767
+ }
29768
+ const value = raw;
29769
+ if (Object.keys(value).some((key) => !["path", "vault", "overwrite"].includes(key))) {
29770
+ throw new Error(`org project set: ${label} has an unknown field`);
29771
+ }
29772
+ if (typeof value.path !== "string" || value.path.length < 2 || value.path.length > 256 || !SECRET_OUTPUT_PATH_RE.test(value.path) || value.path.split("/").includes("..")) {
29773
+ throw new Error(`org project set: ${label}.path must be an absolute container path (no ".." segment)`);
29774
+ }
29775
+ const vault = value.vault;
29776
+ if (!vault || typeof vault !== "object" || Array.isArray(vault)) {
29777
+ throw new Error(`org project set: ${label}.vault must be an object {repo,key}`);
29778
+ }
29779
+ const vaultMap = vault;
29780
+ if (Object.keys(vaultMap).some((key) => !["repo", "key"].includes(key))) {
29781
+ throw new Error(`org project set: ${label}.vault has an unknown field`);
29782
+ }
29783
+ if (typeof vaultMap.repo !== "string" || !SECRET_OUTPUT_REPO_RE.test(vaultMap.repo)) {
29784
+ throw new Error(`org project set: ${label}.vault.repo must be owner/name`);
29785
+ }
29786
+ if (typeof vaultMap.key !== "string" || !SECRET_OUTPUT_KEY_RE.test(vaultMap.key)) {
29787
+ throw new Error(`org project set: ${label}.vault.key must be UPPER_SNAKE`);
29788
+ }
29789
+ if (value.overwrite !== void 0 && typeof value.overwrite !== "boolean") {
29790
+ throw new Error(`org project set: ${label}.overwrite must be true or false`);
29791
+ }
29792
+ return {
29793
+ path: value.path,
29794
+ vault: { repo: vaultMap.repo, key: vaultMap.key },
29795
+ ...value.overwrite !== void 0 ? { overwrite: value.overwrite } : {}
29796
+ };
29797
+ }
29621
29798
  function parseTenantTasksVar(raw) {
29622
29799
  let parsed;
29623
29800
  try {
@@ -29636,7 +29813,7 @@ function parseTenantTasksVar(raw) {
29636
29813
  if (!nameRe.test(name)) throw new Error(`org project set: tenantTasks name ${JSON.stringify(name)} must be lowercase kebab-case`);
29637
29814
  if (!rawTask || typeof rawTask !== "object" || Array.isArray(rawTask)) throw new Error(`org project set: tenantTasks.${name} must be an object`);
29638
29815
  const task = rawTask;
29639
- if (Object.keys(task).some((key) => !["service", "command", "stages", "timeoutSeconds", "artifact"].includes(key))) throw new Error(`org project set: tenantTasks.${name} has an unknown field`);
29816
+ if (Object.keys(task).some((key) => !["service", "command", "stages", "timeoutSeconds", "artifact", "secretOutput"].includes(key))) throw new Error(`org project set: tenantTasks.${name} has an unknown field`);
29640
29817
  if (typeof task.service !== "string" || !serviceRe.test(task.service)) throw new Error(`org project set: tenantTasks.${name}.service has an unsafe shape`);
29641
29818
  if (!Array.isArray(task.command) || task.command.length < 1 || task.command.length > 32 || task.command.some((arg) => typeof arg !== "string" || arg.length < 1 || arg.length > 256 || /[\u0000\r\n]/.test(arg))) throw new Error(`org project set: tenantTasks.${name}.command must be 1-32 bounded argv strings`);
29642
29819
  if (!Array.isArray(task.stages) || task.stages.length < 1 || task.stages.length > 3 || task.stages.some((stage) => typeof stage !== "string" || !validStages.has(stage)) || new Set(task.stages).size !== task.stages.length) throw new Error(`org project set: tenantTasks.${name}.stages must be unique dev/rc/main values`);
@@ -29650,7 +29827,8 @@ function parseTenantTasksVar(raw) {
29650
29827
  command: task.command,
29651
29828
  stages: task.stages,
29652
29829
  ...task.timeoutSeconds !== void 0 ? { timeoutSeconds: task.timeoutSeconds } : {},
29653
- ...artifact !== "none" ? { artifact } : {}
29830
+ ...artifact !== "none" ? { artifact } : {},
29831
+ ...task.secretOutput !== void 0 ? { secretOutput: parseTenantTaskSecretOutput(task.secretOutput, `tenantTasks.${name}.secretOutput`) } : {}
29654
29832
  };
29655
29833
  }
29656
29834
  return out;
@@ -29708,7 +29886,7 @@ var SETTABLE_VAR_HINTS = {
29708
29886
  requiredGcpApis: "comma-string",
29709
29887
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
29710
29888
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
29711
- tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required"}}; use {artifact} in argv when required',
29889
+ tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}}; use {artifact} in argv when required; a secretOutput is master-only',
29712
29890
  secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
29713
29891
  edgeDomains: "JSON {dev,rc,main} domain map",
29714
29892
  statusOptions: "JSON name\u2192id map",
@@ -32321,9 +32499,7 @@ function registerBootstrapCommands(program3) {
32321
32499
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
32322
32500
  deployFacts,
32323
32501
  deployFactsRead: deployFacts !== null,
32324
- edgeCertificates: await Promise.all(["dev", "rc"].map(async (stage) => {
32325
- const domain = meta?.edgeDomains?.[stage];
32326
- if (!domain) return null;
32502
+ edgeCertificates: await Promise.all(edgeCertificateStagesForBootstrap(effectiveTrack, meta?.edgeDomains).map(async (stage) => {
32327
32503
  try {
32328
32504
  return { stage, status: await edgeCertificateForStage(slug, stage, reg) };
32329
32505
  } catch (e) {
@@ -32823,7 +32999,18 @@ function registerBootstrapCommands(program3) {
32823
32999
  if (isLegacyBlock) {
32824
33000
  content = upsertManagedGitignoreBlock(preservedContent).content;
32825
33001
  } else {
32826
- const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, preservedContent);
33002
+ const rulesetVars = preserveRuleset ? gateSeedVars(
33003
+ o.class,
33004
+ effectiveTrack,
33005
+ "node",
33006
+ registryMeta?.requiredCheckBranches,
33007
+ registryMeta?.requiredChecks
33008
+ ) : null;
33009
+ const writeContent = resolveSeedWriteContent(resolved, preserveRuleset && rulesetVars ? {
33010
+ ...vars,
33011
+ ...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
33012
+ ...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
33013
+ } : vars, readFile9, preservedContent);
32827
33014
  if (!writeContent.ok) {
32828
33015
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
32829
33016
  }
@@ -32831,7 +33018,10 @@ function registerBootstrapCommands(program3) {
32831
33018
  if (preserveRuleset) {
32832
33019
  if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
32833
33020
  try {
32834
- content = reseedProductRulesetStrictness(baseContent, content);
33021
+ content = reseedProductRulesetStrictness(baseContent, content, {
33022
+ requiredChecks: !!registryMeta?.requiredChecks?.length,
33023
+ requiredCheckBranches: !!registryMeta?.requiredCheckBranches?.length
33024
+ });
32835
33025
  } catch (e) {
32836
33026
  return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
32837
33027
  }
@@ -33054,8 +33244,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
33054
33244
  if (res.ok) {
33055
33245
  ddbWrites.push({ slug: registerPayload.slug, action: "register", record: registerPayload });
33056
33246
  applied.push(`ddb register ${registerPayload.slug}`);
33057
- for (const stage of ["dev", "rc"]) {
33058
- if (!registerPayload.edgeDomains?.[stage]) continue;
33247
+ for (const stage of edgeCertificateStagesForBootstrap(registerPayload.releaseTrack, registerPayload.edgeDomains)) {
33059
33248
  try {
33060
33249
  const cert = await edgeCertificateForStage(slug, stage, reg, true);
33061
33250
  if (cert.required) applied.push(`edge certificate ${stage}: ${cert.action ?? "checked"}`);
@@ -33135,11 +33324,25 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
33135
33324
  });
33136
33325
  }
33137
33326
  }
33138
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, applied, ddbWrites }, null, 2));
33327
+ let ancillaryPlan;
33328
+ let environmentPlan;
33329
+ if (!o.execute && !onlyTarget) {
33330
+ try {
33331
+ const liveLabels = JSON.parse((await gh(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
33332
+ if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
33333
+ ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
33334
+ environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
33335
+ } catch (e) {
33336
+ return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
33337
+ }
33338
+ }
33339
+ if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, labels: ancillaryPlan, environments: environmentPlan, applied, ddbWrites }, null, 2));
33139
33340
  else {
33140
33341
  console.log(renderSeedPlan(actions));
33141
33342
  if (controlRows.length) console.log(`
33142
33343
  ${renderBootstrapControlPlan(controlRows)}`);
33344
+ if (ancillaryPlan && environmentPlan) console.log(`
33345
+ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
33143
33346
  if (o.execute) console.log(`
33144
33347
  LIVE apply to ${repo}:
33145
33348
  ${applied.join("\n ")}`);
@@ -33649,7 +33852,7 @@ function formatDeployStatus(r) {
33649
33852
  var STAGES2 = ["dev", "rc", "main"];
33650
33853
  function registerDeployCommands(program3) {
33651
33854
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
33652
- deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
33855
+ deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
33653
33856
  if (!STAGES2.includes(stage)) {
33654
33857
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
33655
33858
  }
@@ -38035,6 +38238,70 @@ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
38035
38238
  });
38036
38239
  }
38037
38240
 
38241
+ // src/api-read.ts
38242
+ init_cli_shared();
38243
+ var API_ACCEPT_HEADERS = {
38244
+ json: "application/vnd.github+json",
38245
+ raw: "application/vnd.github.raw"
38246
+ };
38247
+ function acceptHeaderFor(accept) {
38248
+ const v = (accept ?? "json").trim().toLowerCase();
38249
+ const header = API_ACCEPT_HEADERS[v];
38250
+ if (!header) {
38251
+ throw new Error(`api get: --accept must be one of: json, raw (received '${accept}')`);
38252
+ }
38253
+ return header;
38254
+ }
38255
+ var NAME = "[A-Za-z0-9_.-]+";
38256
+ var FREE = "[^?#%\\\\\\s]+";
38257
+ var PULL_FILES_RE = new RegExp(`^repos/${NAME}/${NAME}/pulls/\\d+/files$`);
38258
+ var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FREE}$`);
38259
+ var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
38260
+ var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
38261
+ var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>]";
38262
+ function parseApiGetPath(rawPath) {
38263
+ const path2 = rawPath.trim();
38264
+ const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
38265
+ if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
38266
+ if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
38267
+ if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
38268
+ const q = path2.indexOf("?");
38269
+ const base = q === -1 ? path2 : path2.slice(0, q);
38270
+ const query = q === -1 ? "" : path2.slice(q + 1);
38271
+ if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
38272
+ return { kind: "contents", path: path2, paginate: false };
38273
+ }
38274
+ throw new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
38275
+ }
38276
+ function buildApiGetArgs(path2, accept) {
38277
+ const req = parseApiGetPath(path2);
38278
+ const args = ["api", req.path];
38279
+ if (req.paginate) args.push("--paginate");
38280
+ const header = acceptHeaderFor(accept);
38281
+ if (header !== API_ACCEPT_HEADERS.json) args.push("-H", `Accept: ${header}`);
38282
+ return args;
38283
+ }
38284
+ async function runApiGet(run, path2, accept) {
38285
+ return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
38286
+ }
38287
+ function registerApiReadCommands(program3) {
38288
+ const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents)");
38289
+ api.command("get <path>").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)", "json").action(async (path2, o) => {
38290
+ let body;
38291
+ try {
38292
+ body = await runApiGet(
38293
+ async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout,
38294
+ path2,
38295
+ o.accept
38296
+ );
38297
+ } catch (e) {
38298
+ const err = e;
38299
+ return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
38300
+ }
38301
+ process.stdout.write(body);
38302
+ });
38303
+ }
38304
+
38038
38305
  // src/pr-commands.ts
38039
38306
  var import_promises4 = require("node:fs/promises");
38040
38307
  init_cli_shared();
@@ -43515,6 +43782,14 @@ function registerCollaborationCommands(program3) {
43515
43782
  }
43516
43783
  return resolveParentField(payload);
43517
43784
  }
43785
+ async function readClosedByField(number, repo) {
43786
+ try {
43787
+ return { closedBy: await runClosedBy(queryDeps(), { number, repo }) };
43788
+ } catch (e) {
43789
+ const err = e;
43790
+ return { closedByReadError: (err.stderr || err.message || String(e)).trim() };
43791
+ }
43792
+ }
43518
43793
  function withoutTransportNumber(data) {
43519
43794
  const rest = { ...data };
43520
43795
  delete rest.number;
@@ -43533,7 +43808,7 @@ function registerCollaborationCommands(program3) {
43533
43808
  return void 0;
43534
43809
  }
43535
43810
  }
43536
- issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
43811
+ issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). `closedBy` is synthetic too: the closing PRs with merged/mergedAt/mergeCommit.oid, composed via one fixed GraphQL read (#7219). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
43537
43812
  let parsed;
43538
43813
  try {
43539
43814
  parsed = parseIssueRef(ref, o.repo);
@@ -43544,6 +43819,7 @@ function registerCollaborationCommands(program3) {
43544
43819
  const repo = await resolveRepo(parsed.repo ?? o.repo);
43545
43820
  if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
43546
43821
  const fields = normalizeIssueViewJsonFields(o.json);
43822
+ const wantsClosedBy = fields.split(",").map((f) => f.trim()).includes("closedBy");
43547
43823
  const gh = resolveIssueViewGhFields(fields);
43548
43824
  const emit = (data, parent) => console.log(JSON.stringify({ ...gh.numberIsTransport ? withoutTransportNumber(data) : data, ...parent }));
43549
43825
  const readIssue = async (targetRepo2, targetNumber) => {
@@ -43555,9 +43831,10 @@ function registerCollaborationCommands(program3) {
43555
43831
  const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh.ghFields]);
43556
43832
  return { data, parent: await readParentField(targetNumber, targetRepo2) };
43557
43833
  };
43834
+ const readClosedBy = async (targetRepo2, targetNumber) => wantsClosedBy ? readClosedByField(targetNumber, targetRepo2) : {};
43558
43835
  try {
43559
43836
  const { data, parent } = await readIssue(repo, n);
43560
- emit(data, parent);
43837
+ emit(data, { ...parent, ...await readClosedBy(repo, n) });
43561
43838
  } catch (e) {
43562
43839
  const err = e;
43563
43840
  const raw = (err.stderr || err.message || String(e)).trim();
@@ -43566,7 +43843,7 @@ function registerCollaborationCommands(program3) {
43566
43843
  if (moved) {
43567
43844
  try {
43568
43845
  const { data, parent } = await readIssue(moved.repo, moved.number);
43569
- emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, parent);
43846
+ emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, { ...parent, ...await readClosedBy(moved.repo, moved.number) });
43570
43847
  return;
43571
43848
  } catch (reRead) {
43572
43849
  const reErr = reRead;
@@ -48711,7 +48988,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
48711
48988
  return failGraceful(`runtime tenant reconcile: ${e.message}`);
48712
48989
  }
48713
48990
  });
48714
- 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; an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
48991
+ 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) => {
48715
48992
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
48716
48993
  const cfg = await loadConfig();
48717
48994
  const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
@@ -49195,7 +49472,7 @@ ${filelessTransitionGuide(target, o.stage)}`
49195
49472
  return reportWrite("org project set-deploy", res);
49196
49473
  });
49197
49474
  var projectTenantTasks = project.command("tenant-tasks").description("project-admin self-declare of tenant tasks for every stage of your own repo \u2014 new-task-only; editing an existing task stays master-only (`org project set --var tenantTasks=...`)");
49198
- projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", "task command as argv (repeatable; required unless --var carries it)").option("--stages <list>", "comma-separated dev,rc,main stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
49475
+ projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", "task command as argv (repeatable; required unless --var carries it)").option("--stages <list>", "comma-separated dev,rc,main stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags. A task carrying secretOutput:{path,vault:{repo,key},overwrite?} is master-only; declare that via `org project set --var tenantTasks=...`").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
49199
49476
  const cfg = await loadConfig();
49200
49477
  let target;
49201
49478
  try {
@@ -49223,6 +49500,9 @@ projectTenantTasks.command("declare <name>").description("register a NEW tenant
49223
49500
  ...o.artifact !== void 0 ? { artifact: o.artifact } : {}
49224
49501
  };
49225
49502
  }
49503
+ if (task && typeof task === "object" && task.secretOutput !== void 0) {
49504
+ return fail("org project tenant-tasks declare: a task with secretOutput names a vault destination and is master-only (#7213) \u2014 declare it via `mmi-cli oracle org project set <owner/repo> --var 'tenantTasks=...'`");
49505
+ }
49226
49506
  const res = await postJson(`/projects/${encodeURIComponent(slug)}/tenant-tasks`, { repo: target, tenantTasks: { [name]: task } }, registryClientDeps(cfg), "POST", { noRetry: true });
49227
49507
  if (o.json) {
49228
49508
  console.log(JSON.stringify(res, null, 2));
@@ -49370,6 +49650,7 @@ registerDeployCommands(program2);
49370
49650
  registerDiscoveryCommands(program2);
49371
49651
  registerExplainCommand(program2);
49372
49652
  registerPrLifecycleCommands(program2);
49653
+ registerApiReadCommands(program2);
49373
49654
  registerSessionReport(program2);
49374
49655
  registerLearningClosureRateCommand(program2);
49375
49656
  registerLearningPickupCommand(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.24",
3
+ "version": "4.5.25",
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",