@mutmutco/cli 4.5.24 → 4.5.26

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 +355 -58
  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",
@@ -1043,9 +1045,21 @@ function mutating(cmd, planFn) {
1043
1045
  return cmd;
1044
1046
  }
1045
1047
  function installProcessBackstop() {
1048
+ installBrokenPipeGuard(process.stdout);
1049
+ installBrokenPipeGuard(process.stderr);
1046
1050
  process.on("unhandledRejection", (reason) => void failGraceful(reason instanceof Error ? reason.message : String(reason)));
1047
1051
  process.on("uncaughtException", (err) => void failGraceful(err instanceof Error ? err.message : String(err)));
1048
1052
  }
1053
+ function installBrokenPipeGuard(stream) {
1054
+ stream.on("error", (err) => {
1055
+ if (err?.code !== "EPIPE") throw err;
1056
+ stream.write = (_chunk, enc, cb) => {
1057
+ const done = typeof enc === "function" ? enc : cb;
1058
+ if (typeof done === "function") queueMicrotask(() => done());
1059
+ return true;
1060
+ };
1061
+ });
1062
+ }
1049
1063
  function rawFlag(flag) {
1050
1064
  return process.argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
1051
1065
  }
@@ -6537,6 +6551,10 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
6537
6551
  }
6538
6552
  let body = item.details?.body;
6539
6553
  if (body === void 0) {
6554
+ if (item.detailsUnavailable) {
6555
+ results[index] = { bucket: "claimable", item, warnings: [] };
6556
+ continue;
6557
+ }
6540
6558
  if (opts.requireBundledBody) {
6541
6559
  results[index] = { bucket: "claimable", item, warnings: [] };
6542
6560
  continue;
@@ -6544,7 +6562,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
6544
6562
  try {
6545
6563
  const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
6546
6564
  body = data?.body ?? "";
6547
- item.details = { body, comments: item.details?.comments ?? [] };
6565
+ item.details = { body, comments: item.details?.comments ?? [], ...item.details?.comments === void 0 ? { commentsUnread: true } : {} };
6548
6566
  } catch (e) {
6549
6567
  results[index] = {
6550
6568
  bucket: "claimable",
@@ -8026,7 +8044,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
8026
8044
  }
8027
8045
  async function probePublicHealth(publicUrl) {
8028
8046
  const root = await probeHttpBounded(publicUrl);
8029
- if (root.status !== 401 && root.status !== 403) return root;
8047
+ if (root.status !== 401 && root.status !== 403 && root.status !== 404) return root;
8030
8048
  return probeHttpBounded(`${publicUrl.replace(/\/$/, "")}/health`);
8031
8049
  }
8032
8050
  var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
@@ -8048,7 +8066,7 @@ function tenantRuntimeHints(stage, fact, probe, edgeCertificate) {
8048
8066
  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
8067
  if (!fact.domain) hints.push(`DEPLOY#${stage} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
8050
8068
  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.`);
8069
+ 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
8070
  else if (edgeCertificate?.missing && edgeCertificate.hint) hints.push(edgeCertificate.hint);
8053
8071
  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
8072
  if (stage === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
@@ -12447,6 +12465,7 @@ function renderBoardSource() {
12447
12465
  return "source: live";
12448
12466
  }
12449
12467
  async function collectBoardPreferringSnapshot(cfg, options, deps) {
12468
+ if (options.snapshotOnly && !deps.snapshot) throw new Error("--snapshot-only needs the Hub snapshot transport (not with --direct)");
12450
12469
  const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
12451
12470
  {
12452
12471
  owner: cfg.projectOwner,
@@ -12486,6 +12505,9 @@ async function collectBoardPreferringSnapshot(cfg, options, deps) {
12486
12505
  snapshot
12487
12506
  };
12488
12507
  }
12508
+ if (options.snapshotOnly && attempt?.state === "unavailable") {
12509
+ throw new Error(`Hub board snapshot unavailable (${attempt.reason}) \u2014 --snapshot-only refuses the direct user-auth fallback`);
12510
+ }
12489
12511
  const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
12490
12512
  if (attempt?.state === "unavailable") {
12491
12513
  collected.warnings.push(`Hub board snapshot unavailable (${attempt.reason}) \u2014 served by the direct user-auth read (emergency fallback)`);
@@ -12768,7 +12790,13 @@ function nodeToItem(node, cfg) {
12768
12790
  priorityOptionId: fields.priority?.optionId,
12769
12791
  assignees,
12770
12792
  labels,
12771
- type: labels.find((label) => TYPE_LABELS.includes(label))
12793
+ type: labels.find((label) => TYPE_LABELS.includes(label)),
12794
+ ...content.__typename === "Issue" && content.claimEvidenceUnavailable ? { detailsUnavailable: "App-token claim evidence unread" } : {},
12795
+ ...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: {
12796
+ body: content.body,
12797
+ comments: content.comments.nodes.map((comment) => ({ author: comment.author?.login ?? "", body: comment.body ?? "" })),
12798
+ ...content.comments.totalCount > content.comments.nodes.length ? { omittedComments: content.comments.totalCount - content.comments.nodes.length } : {}
12799
+ } } : {}
12772
12800
  })
12773
12801
  };
12774
12802
  }
@@ -12783,7 +12811,8 @@ async function attachBundleDetails(report, client, allowPartial, options = {}) {
12783
12811
  while (next < candidates.length) {
12784
12812
  const item = candidates[next++];
12785
12813
  try {
12786
- item.details = await fetchIssueDetails(client, item.repository, item.number);
12814
+ if (!item.details && !item.detailsUnavailable) item.details = await fetchIssueDetails(client, item.repository, item.number);
12815
+ if (!item.details) continue;
12787
12816
  const claim = boardItemClaim(item);
12788
12817
  if (claim) item.claim = claim;
12789
12818
  } catch (e) {
@@ -13017,7 +13046,8 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13017
13046
  const warnings = [];
13018
13047
  let partial = false;
13019
13048
  const fetchComments = deps.fetchComments ?? (async (item) => {
13020
- if (item.details) return item.details.comments;
13049
+ if (item.detailsUnavailable) return void 0;
13050
+ if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13021
13051
  const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13022
13052
  return comments.map((comment) => ({ body: comment.body ?? "" }));
13023
13053
  });
@@ -13600,11 +13630,6 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
13600
13630
  warnings: collected.warnings,
13601
13631
  partial: collected.partial
13602
13632
  };
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
13633
  return {
13609
13634
  cfg,
13610
13635
  client,
@@ -14061,7 +14086,7 @@ init_github_client();
14061
14086
  var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
14062
14087
  var PRODUCT_GATE_CONTEXT = "gate";
14063
14088
  var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
14064
- function reseedProductRulesetStrictness(current, seed) {
14089
+ function reseedProductRulesetStrictness(current, seed, declared = {}) {
14065
14090
  const parse = (raw) => {
14066
14091
  const policy2 = JSON.parse(raw);
14067
14092
  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 +14099,28 @@ function reseedProductRulesetStrictness(current, seed) {
14074
14099
  }
14075
14100
  return { policy: policy2, parameters: parameters2 };
14076
14101
  };
14077
- const strict = parse(seed).parameters.strict_required_status_checks_policy;
14102
+ const canonical = parse(seed);
14103
+ const strict = canonical.parameters.strict_required_status_checks_policy;
14078
14104
  if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
14079
14105
  if (current === null) return seed;
14080
14106
  const { policy, parameters } = parse(current);
14081
- if (parameters.strict_required_status_checks_policy === strict) return current;
14107
+ let changed = parameters.strict_required_status_checks_policy !== strict;
14082
14108
  parameters.strict_required_status_checks_policy = strict;
14083
- return `${JSON.stringify(policy, null, 2)}
14084
- `;
14109
+ if (declared.requiredChecks) {
14110
+ const checks = canonical.parameters.required_status_checks.map((check) => parameters.required_status_checks.find((old) => old.context === check.context) ?? check);
14111
+ if (JSON.stringify(parameters.required_status_checks) !== JSON.stringify(checks)) changed = true;
14112
+ parameters.required_status_checks = checks;
14113
+ }
14114
+ if (declared.requiredCheckBranches) {
14115
+ const refs = canonical.policy.conditions?.ref_name?.include;
14116
+ if (!Array.isArray(refs) || !refs.length) throw new Error("canonical product ruleset must declare branch includes");
14117
+ const conditions = policy.conditions;
14118
+ const refName = conditions?.ref_name;
14119
+ if (JSON.stringify(refName?.include) !== JSON.stringify(refs)) changed = true;
14120
+ policy.conditions = { ...conditions, ref_name: { ...refName, include: refs } };
14121
+ }
14122
+ return changed ? `${JSON.stringify(policy, null, 2)}
14123
+ ` : current;
14085
14124
  }
14086
14125
  function stripRulesetComment(raw) {
14087
14126
  const parsed = JSON.parse(raw);
@@ -15398,6 +15437,20 @@ function labelsToPrune(orgLabelNames) {
15398
15437
  const org = new Set(orgLabelNames);
15399
15438
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
15400
15439
  }
15440
+ function planBootstrapLabels(labels, live) {
15441
+ const existing = new Set(live.map((label) => label.name));
15442
+ return [
15443
+ ...labels.map((label) => ({ name: label.name, action: existing.has(label.name) ? "UPDATE" : "CREATE" })),
15444
+ ...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
15445
+ ];
15446
+ }
15447
+ function renderBootstrapAncillaryPlan(labels, environments) {
15448
+ return [
15449
+ "bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
15450
+ ...labels.map(({ action, name }) => ` ${action} label ${name}`),
15451
+ ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
15452
+ ].join("\n");
15453
+ }
15401
15454
  function resolveSeedContent(seed, vars, readFile9) {
15402
15455
  if (seed.source === "self") return readFile9(seed.target);
15403
15456
  if (seed.source.startsWith("seed:")) {
@@ -15418,6 +15471,11 @@ function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
15418
15471
  const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
15419
15472
  return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
15420
15473
  }
15474
+ function edgeCertificateStagesForBootstrap(track, domains) {
15475
+ if (!domains || typeof domains !== "object" || Array.isArray(domains)) return [];
15476
+ const stages = branchesForTrack(track);
15477
+ return ["dev", "rc"].filter((stage) => stages.includes(stage === "dev" ? "development" : stage) && Boolean(domains[stage]));
15478
+ }
15421
15479
  function buildRegisterPayload(repo, cls, vars, options = {}) {
15422
15480
  const parsedRepo = parseOwnerRepo(repo);
15423
15481
  const slug = parsedRepo.slug;
@@ -15442,6 +15500,12 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
15442
15500
  );
15443
15501
  }
15444
15502
  const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
15503
+ const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
15504
+ const inferredDomains = inferEdgeDomainsForWrite(slug);
15505
+ const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).map((branch) => {
15506
+ const stage = branch === "development" ? "dev" : branch;
15507
+ return [stage, inferredDomains[stage]];
15508
+ }));
15445
15509
  const num = (v) => {
15446
15510
  if (v == null || v === "") return void 0;
15447
15511
  const n = Number(v);
@@ -15471,10 +15535,9 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
15471
15535
  deployModel,
15472
15536
  // Persist the canonical stage hosts at the same write-time boundary as deployModel. Read paths never
15473
15537
  // infer from an absent edgeDomains field.
15474
- edgeDomains: options.existing?.edgeDomains ?? inferEdgeDomainsForWrite(slug),
15538
+ edgeDomains: options.existing?.edgeDomains ?? derivedDomains,
15475
15539
  // #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),
15540
+ releaseTrack,
15478
15541
  // Board coords (from GraphQL at bootstrap, passed as --var by the skill).
15479
15542
  projectOwner: vars.PROJECT_OWNER || void 0,
15480
15543
  projectNumber: num(vars.PROJECT_NUMBER),
@@ -16753,10 +16816,10 @@ var rollout_plan_default = {
16753
16816
  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
16817
  },
16755
16818
  baseline: {
16756
- version: "4.5.24",
16757
- tag: "v4.5.24",
16758
- commit: "4d303073492a",
16759
- npm: "@mutmutco/cli@4.5.24"
16819
+ version: "4.5.26",
16820
+ tag: "v4.5.26",
16821
+ commit: "f5ca4e782979",
16822
+ npm: "@mutmutco/cli@4.5.26"
16760
16823
  },
16761
16824
  exitCriterion: "fleet-n-of-n",
16762
16825
  hubOnlyShortcut: "forbidden",
@@ -16773,14 +16836,14 @@ var rollout_plan_default = {
16773
16836
  repo: "mutmutco/mmi-hub",
16774
16837
  role: "canary",
16775
16838
  schedule: "train",
16776
- v3Target: "v4.5.24"
16839
+ v3Target: "v4.5.26"
16777
16840
  }
16778
16841
  ],
16779
16842
  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
16843
  rollback: {
16781
16844
  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)"
16845
+ mechanism: "npm dist-tag latest -> 4.5.26 and redeploy the Hub Lambda from tag v4.5.26 (f5ca4e782979); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16846
+ v3Target: "v4.5.26 (@mutmutco/cli@4.5.26, tag commit f5ca4e782979 \u2014 last known-good release carrying the repo-index v4-only contract)"
16784
16847
  }
16785
16848
  },
16786
16849
  {
@@ -17108,7 +17171,7 @@ function ensureField(fields, field) {
17108
17171
  return list.join(",");
17109
17172
  }
17110
17173
  function resolveIssueViewGhFields(normalized) {
17111
- const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent");
17174
+ const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent" && f !== "closedBy");
17112
17175
  if (requested.length === 0) return { ghFields: "number", numberIsTransport: true };
17113
17176
  return { ghFields: requested.join(","), numberIsTransport: false };
17114
17177
  }
@@ -17144,11 +17207,11 @@ var QueryReadError = class extends Error {
17144
17207
  this.code = code;
17145
17208
  }
17146
17209
  };
17147
- function clampLimit(n, dflt) {
17210
+ function clampLimit(n, dflt, max = MAX_LIMIT) {
17148
17211
  if (n === void 0 || !Number.isFinite(n)) return dflt;
17149
17212
  const v = Math.floor(n);
17150
17213
  if (v < 1) return 1;
17151
- if (v > MAX_LIMIT) return MAX_LIMIT;
17214
+ if (v > max) return max;
17152
17215
  return v;
17153
17216
  }
17154
17217
  function isGhNotFound(e) {
@@ -17175,10 +17238,12 @@ function loginsOf(v) {
17175
17238
  function namesOf(v) {
17176
17239
  return Array.isArray(v) ? v.map((a) => String(a?.name ?? "")).filter(Boolean) : [];
17177
17240
  }
17241
+ var ISSUE_LIST_MAX_LIMIT = 1e3;
17178
17242
  function buildIssueListArgs(opts, repo) {
17179
- const limit = clampLimit(opts.limit, DEFAULT_LIMIT);
17243
+ const limit = clampLimit(opts.limit, DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT);
17180
17244
  const state = opts.state ?? "open";
17181
- const args = ["issue", "list", "--repo", repo, "--state", state, "--limit", String(limit), "--json", ISSUE_LIST_FIELDS];
17245
+ const fields = opts.withBody ? `${ISSUE_LIST_FIELDS},body` : ISSUE_LIST_FIELDS;
17246
+ const args = ["issue", "list", "--repo", repo, "--state", state, "--limit", String(limit), "--json", fields];
17182
17247
  if (opts.label) args.push("--label", opts.label);
17183
17248
  if (opts.assignee) args.push("--assignee", opts.assignee);
17184
17249
  if (opts.search) args.push("--search", opts.search);
@@ -17194,7 +17259,8 @@ function shapeIssueList(rows) {
17194
17259
  state: String(row.state ?? ""),
17195
17260
  url: String(row.url ?? ""),
17196
17261
  assignees: loginsOf(row.assignees),
17197
- labels: namesOf(row.labels)
17262
+ labels: namesOf(row.labels),
17263
+ ...typeof row.body === "string" ? { body: row.body } : {}
17198
17264
  };
17199
17265
  });
17200
17266
  }
@@ -17203,7 +17269,8 @@ async function runIssueList(deps, opts) {
17203
17269
  if (!repo) {
17204
17270
  throw new QueryReadError("BAD_INPUT", `could not resolve repo (pass --repo <owner/repo>)`);
17205
17271
  }
17206
- const rows = await deps.ghJson(buildIssueListArgs(opts, repo), GH_LIST_TIMEOUT_MS);
17272
+ const timeout = (opts.limit ?? 0) > MAX_LIMIT ? GH_LIST_TIMEOUT_MS * 4 : GH_LIST_TIMEOUT_MS;
17273
+ const rows = await deps.ghJson(buildIssueListArgs(opts, repo), timeout);
17207
17274
  return shapeIssueList(rows);
17208
17275
  }
17209
17276
  function childrenGraphqlArgs(owner, name, number) {
@@ -17597,6 +17664,45 @@ async function runPrForIssue(deps, issueRef, opts) {
17597
17664
  }
17598
17665
  return extractPrForIssueResponse(resp);
17599
17666
  }
17667
+ function closedByGraphqlArgs(owner, name, number) {
17668
+ 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}}}}}}";
17669
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
17670
+ }
17671
+ function extractClosedByResponse(resp) {
17672
+ const r = resp ?? {};
17673
+ if ((r.errors ?? []).some((e) => e?.type === "NOT_FOUND") || !r.data?.repository?.issue) {
17674
+ throw new QueryReadError("NOT_FOUND", "issue not found");
17675
+ }
17676
+ const out = [];
17677
+ for (const node of r.data.repository.issue.closedByPullRequestsReferences?.nodes ?? []) {
17678
+ const pr = node;
17679
+ if (typeof pr.number !== "number") continue;
17680
+ out.push({
17681
+ number: pr.number,
17682
+ merged: pr.merged === true,
17683
+ mergedAt: typeof pr.mergedAt === "string" ? pr.mergedAt : null,
17684
+ mergeCommit: pr.mergeCommit && typeof pr.mergeCommit.oid === "string" ? { oid: pr.mergeCommit.oid } : null,
17685
+ repository: {
17686
+ owner: { login: String(pr.repository?.owner?.login ?? "") },
17687
+ name: String(pr.repository?.name ?? "")
17688
+ }
17689
+ });
17690
+ }
17691
+ return out;
17692
+ }
17693
+ async function runClosedBy(deps, opts) {
17694
+ const { owner, name } = splitRepo(opts.repo);
17695
+ let resp;
17696
+ try {
17697
+ resp = await deps.ghJson(closedByGraphqlArgs(owner, name, opts.number), GH_LIST_TIMEOUT_MS);
17698
+ } catch (e) {
17699
+ if (isGhNotFound(e)) {
17700
+ throw new QueryReadError("NOT_FOUND", `#${opts.number} not found in ${opts.repo}`);
17701
+ }
17702
+ throw e;
17703
+ }
17704
+ return extractClosedByResponse(resp);
17705
+ }
17600
17706
  async function runIssueViewContext(deps, opts) {
17601
17707
  const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
17602
17708
  const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
@@ -17772,10 +17878,10 @@ function registerQueryCommands(program3) {
17772
17878
  const project2 = program3.commands.find((c) => c.name() === "project");
17773
17879
  if (!issue || !pr) return;
17774
17880
  const deps = queryDeps();
17775
- issue.command("list").description("bounded issue read \u2014 filter by text, label, state, or assignee; always prints JSON").option("--label <label>", "filter by label").option("--search <query>", "free-text issue search").addOption(caseInsensitiveChoices(new Option("--state <state>", "open | closed | all").default("open"), ISSUE_STATES)).option("--assignee <login>", "filter by assignee login").option("--limit <n>", "max issues to return (capped at 100)", (v) => Number(v), DEFAULT_LIMIT).option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output (already the default \u2014 accepted for contract uniformity)").action(async (o) => {
17881
+ issue.command("list").description("bounded issue read \u2014 filter by text, label, state, or assignee; always prints JSON").option("--label <label>", "filter by label").option("--search <query>", "free-text issue search").addOption(caseInsensitiveChoices(new Option("--state <state>", "open | closed | all").default("open"), ISSUE_STATES)).option("--assignee <login>", "filter by assignee login").option("--limit <n>", "max issues to return (capped at 1000; gh pages past 100)", (v) => Number(v), DEFAULT_LIMIT).option("--with-body", "include each issue's markdown body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output (already the default \u2014 accepted for contract uniformity)").action(async (o) => {
17776
17882
  try {
17777
17883
  const state = validateEnum("--state", ISSUE_STATES, o.state, "issue list");
17778
- const rows = await runIssueList(deps, { label: o.label, search: o.search, state, assignee: o.assignee, limit: o.limit, repo: o.repo });
17884
+ const rows = await runIssueList(deps, { label: o.label, search: o.search, state, assignee: o.assignee, limit: o.limit, repo: o.repo, withBody: o.withBody });
17779
17885
  console.log(JSON.stringify(rows));
17780
17886
  } catch (e) {
17781
17887
  queryFail("issue list", e);
@@ -26463,6 +26569,10 @@ var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressa
26463
26569
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
26464
26570
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
26465
26571
  var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
26572
+ var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
26573
+ var SHARED_ACTION_PINS = [
26574
+ { path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
26575
+ ];
26466
26576
  function slugFromRepo(repo) {
26467
26577
  return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
26468
26578
  }
@@ -26486,6 +26596,43 @@ async function restJson2(deps, path2, fallback) {
26486
26596
  return fallback;
26487
26597
  }
26488
26598
  }
26599
+ var sharedPinComparisons = /* @__PURE__ */ new WeakMap();
26600
+ function pinContainsFloor(deps, floor, pin) {
26601
+ let comparisons = sharedPinComparisons.get(deps);
26602
+ if (!comparisons) {
26603
+ comparisons = /* @__PURE__ */ new Map();
26604
+ sharedPinComparisons.set(deps, comparisons);
26605
+ }
26606
+ const key = `${floor}...${pin}`;
26607
+ let comparison = comparisons.get(key);
26608
+ if (!comparison) {
26609
+ 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);
26610
+ comparisons.set(key, comparison);
26611
+ }
26612
+ return comparison;
26613
+ }
26614
+ async function auditSharedActionPins(files, deps) {
26615
+ const checks = [];
26616
+ for (const { path: path2, body } of files) {
26617
+ for (const [index, line] of body.split(/\r?\n/).entries()) {
26618
+ const match = /^\s*-?\s*uses:\s*['"]?mutmutco\/MMI-Hub\/\.github\/actions\/([^@\s'"]+)@([0-9a-f]{40})['"]?(?:\s*(?:#.*)?)$/.exec(line);
26619
+ if (!match) continue;
26620
+ const action = SHARED_ACTION_PINS.find((item) => item.path === match[1]);
26621
+ if (!action || match[2] === action.floor) continue;
26622
+ const safe = await pinContainsFloor(deps, action.floor, match[2]);
26623
+ if (safe === true) continue;
26624
+ const location = `${path2}:${index + 1}`;
26625
+ const replacement = `mutmutco/MMI-Hub/.github/actions/${action.path}@${action.floor}`;
26626
+ checks.push({
26627
+ ok: safe !== false,
26628
+ label: `${action.path} shared action pin is current`,
26629
+ 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"),
26630
+ remediation: safe === false ? `Replace the uses: pin at ${location} with ${replacement}` : void 0
26631
+ });
26632
+ }
26633
+ }
26634
+ return checks;
26635
+ }
26489
26636
  async function rulesetDetails(deps, repo, list) {
26490
26637
  const details = [];
26491
26638
  for (const ruleset of list) {
@@ -27022,6 +27169,10 @@ async function auditRepoCi(repo, deps) {
27022
27169
  }
27023
27170
  }
27024
27171
  }
27172
+ if (repoClass !== "hub") {
27173
+ const workflows = await getBranchWorkflows();
27174
+ if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
27175
+ }
27025
27176
  const ok = checks.every((c) => c.ok);
27026
27177
  return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
27027
27178
  }
@@ -27585,7 +27736,7 @@ var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata"
27585
27736
  var PRIMARY_GROUPS = [
27586
27737
  ["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
27587
27738
  ["Plan and work", ["board", "issue", "stage"]],
27588
- ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
27739
+ ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train", "api"]],
27589
27740
  // `tests` sits beside `docs` deliberately: both are deterministic, repo-local gates a workflow
27590
27741
  // step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
27591
27742
  // joins them on the same footing (#3979); `dist status` does too (#5576) — the checkout's own
@@ -27633,6 +27784,7 @@ var COMMAND_OWNERSHIP = {
27633
27784
  issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
27634
27785
  stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
27635
27786
  pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
27787
+ api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
27636
27788
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
27637
27789
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
27638
27790
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -28372,7 +28524,10 @@ var APP_VAULT_REPO = "mutmutco/MMI-Hub";
28372
28524
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
28373
28525
  var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
28374
28526
  "pr checks-wait",
28375
- "pr view"
28527
+ "pr view",
28528
+ "issue list",
28529
+ "issue view",
28530
+ "api get"
28376
28531
  ]);
28377
28532
  var APP_POLL_PERMISSIONS = {
28378
28533
  metadata: "read",
@@ -28602,8 +28757,8 @@ async function attachBoardIssues(selectors, repoOption, deps = {}) {
28602
28757
  ref = `${parsed.repo}#${parsed.number}`;
28603
28758
  const cfg = resolveBoardConfig(await (deps.config ?? loadConfigForBoardSelector2)(selector, repoOption));
28604
28759
  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);
28760
+ if (!issue.node_id) throw new Error("issue node id could not be read");
28761
+ const projectItemId = await (deps.attach ?? attachViaHubApp)(cfg, issue.node_id);
28607
28762
  return { ref, attached: true, projectItemId, credential: "app_installation" };
28608
28763
  } catch (e) {
28609
28764
  return { ref, attached: false, reason: e.message };
@@ -28652,7 +28807,8 @@ function registerBoardCommands(program3) {
28652
28807
  repo: o.repo,
28653
28808
  includeBundleDetails: o.bundleDetails,
28654
28809
  includeAllBodies: o.bodies,
28655
- allowPartial: o.allowPartial
28810
+ allowPartial: o.allowPartial,
28811
+ snapshotOnly: o.snapshotOnly
28656
28812
  }, o.direct ? {} : { snapshot: registryClientDeps(config) });
28657
28813
  const output = o.json ? JSON.stringify(report) : renderBoardReport(report);
28658
28814
  if (!o.out) console.log(output);
@@ -28695,7 +28851,7 @@ function registerBoardCommands(program3) {
28695
28851
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
28696
28852
  }
28697
28853
  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));
28854
+ 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
28855
  withExamples(mutating(
28700
28856
  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
28857
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
@@ -29618,6 +29774,43 @@ function parseGateVar(raw) {
29618
29774
  }
29619
29775
  return out;
29620
29776
  }
29777
+ var SECRET_OUTPUT_PATH_RE = /^\/[A-Za-z0-9._/-]+$/;
29778
+ var SECRET_OUTPUT_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
29779
+ var SECRET_OUTPUT_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
29780
+ function parseTenantTaskSecretOutput(raw, label) {
29781
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
29782
+ throw new Error(`org project set: ${label} must be an object {path,vault:{repo,key},overwrite?}`);
29783
+ }
29784
+ const value = raw;
29785
+ if (Object.keys(value).some((key) => !["path", "vault", "overwrite"].includes(key))) {
29786
+ throw new Error(`org project set: ${label} has an unknown field`);
29787
+ }
29788
+ if (typeof value.path !== "string" || value.path.length < 2 || value.path.length > 256 || !SECRET_OUTPUT_PATH_RE.test(value.path) || value.path.split("/").includes("..")) {
29789
+ throw new Error(`org project set: ${label}.path must be an absolute container path (no ".." segment)`);
29790
+ }
29791
+ const vault = value.vault;
29792
+ if (!vault || typeof vault !== "object" || Array.isArray(vault)) {
29793
+ throw new Error(`org project set: ${label}.vault must be an object {repo,key}`);
29794
+ }
29795
+ const vaultMap = vault;
29796
+ if (Object.keys(vaultMap).some((key) => !["repo", "key"].includes(key))) {
29797
+ throw new Error(`org project set: ${label}.vault has an unknown field`);
29798
+ }
29799
+ if (typeof vaultMap.repo !== "string" || !SECRET_OUTPUT_REPO_RE.test(vaultMap.repo)) {
29800
+ throw new Error(`org project set: ${label}.vault.repo must be owner/name`);
29801
+ }
29802
+ if (typeof vaultMap.key !== "string" || !SECRET_OUTPUT_KEY_RE.test(vaultMap.key)) {
29803
+ throw new Error(`org project set: ${label}.vault.key must be UPPER_SNAKE`);
29804
+ }
29805
+ if (value.overwrite !== void 0 && typeof value.overwrite !== "boolean") {
29806
+ throw new Error(`org project set: ${label}.overwrite must be true or false`);
29807
+ }
29808
+ return {
29809
+ path: value.path,
29810
+ vault: { repo: vaultMap.repo, key: vaultMap.key },
29811
+ ...value.overwrite !== void 0 ? { overwrite: value.overwrite } : {}
29812
+ };
29813
+ }
29621
29814
  function parseTenantTasksVar(raw) {
29622
29815
  let parsed;
29623
29816
  try {
@@ -29636,7 +29829,7 @@ function parseTenantTasksVar(raw) {
29636
29829
  if (!nameRe.test(name)) throw new Error(`org project set: tenantTasks name ${JSON.stringify(name)} must be lowercase kebab-case`);
29637
29830
  if (!rawTask || typeof rawTask !== "object" || Array.isArray(rawTask)) throw new Error(`org project set: tenantTasks.${name} must be an object`);
29638
29831
  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`);
29832
+ 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
29833
  if (typeof task.service !== "string" || !serviceRe.test(task.service)) throw new Error(`org project set: tenantTasks.${name}.service has an unsafe shape`);
29641
29834
  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
29835
  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 +29843,8 @@ function parseTenantTasksVar(raw) {
29650
29843
  command: task.command,
29651
29844
  stages: task.stages,
29652
29845
  ...task.timeoutSeconds !== void 0 ? { timeoutSeconds: task.timeoutSeconds } : {},
29653
- ...artifact !== "none" ? { artifact } : {}
29846
+ ...artifact !== "none" ? { artifact } : {},
29847
+ ...task.secretOutput !== void 0 ? { secretOutput: parseTenantTaskSecretOutput(task.secretOutput, `tenantTasks.${name}.secretOutput`) } : {}
29654
29848
  };
29655
29849
  }
29656
29850
  return out;
@@ -29708,7 +29902,7 @@ var SETTABLE_VAR_HINTS = {
29708
29902
  requiredGcpApis: "comma-string",
29709
29903
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
29710
29904
  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',
29905
+ 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
29906
  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
29907
  edgeDomains: "JSON {dev,rc,main} domain map",
29714
29908
  statusOptions: "JSON name\u2192id map",
@@ -32321,9 +32515,7 @@ function registerBootstrapCommands(program3) {
32321
32515
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
32322
32516
  deployFacts,
32323
32517
  deployFactsRead: deployFacts !== null,
32324
- edgeCertificates: await Promise.all(["dev", "rc"].map(async (stage) => {
32325
- const domain = meta?.edgeDomains?.[stage];
32326
- if (!domain) return null;
32518
+ edgeCertificates: await Promise.all(edgeCertificateStagesForBootstrap(effectiveTrack, meta?.edgeDomains).map(async (stage) => {
32327
32519
  try {
32328
32520
  return { stage, status: await edgeCertificateForStage(slug, stage, reg) };
32329
32521
  } catch (e) {
@@ -32823,7 +33015,18 @@ function registerBootstrapCommands(program3) {
32823
33015
  if (isLegacyBlock) {
32824
33016
  content = upsertManagedGitignoreBlock(preservedContent).content;
32825
33017
  } else {
32826
- const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, preservedContent);
33018
+ const rulesetVars = preserveRuleset ? gateSeedVars(
33019
+ o.class,
33020
+ effectiveTrack,
33021
+ "node",
33022
+ registryMeta?.requiredCheckBranches,
33023
+ registryMeta?.requiredChecks
33024
+ ) : null;
33025
+ const writeContent = resolveSeedWriteContent(resolved, preserveRuleset && rulesetVars ? {
33026
+ ...vars,
33027
+ ...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
33028
+ ...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
33029
+ } : vars, readFile9, preservedContent);
32827
33030
  if (!writeContent.ok) {
32828
33031
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
32829
33032
  }
@@ -32831,7 +33034,10 @@ function registerBootstrapCommands(program3) {
32831
33034
  if (preserveRuleset) {
32832
33035
  if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
32833
33036
  try {
32834
- content = reseedProductRulesetStrictness(baseContent, content);
33037
+ content = reseedProductRulesetStrictness(baseContent, content, {
33038
+ requiredChecks: !!registryMeta?.requiredChecks?.length,
33039
+ requiredCheckBranches: !!registryMeta?.requiredCheckBranches?.length
33040
+ });
32835
33041
  } catch (e) {
32836
33042
  return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
32837
33043
  }
@@ -33054,8 +33260,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
33054
33260
  if (res.ok) {
33055
33261
  ddbWrites.push({ slug: registerPayload.slug, action: "register", record: registerPayload });
33056
33262
  applied.push(`ddb register ${registerPayload.slug}`);
33057
- for (const stage of ["dev", "rc"]) {
33058
- if (!registerPayload.edgeDomains?.[stage]) continue;
33263
+ for (const stage of edgeCertificateStagesForBootstrap(registerPayload.releaseTrack, registerPayload.edgeDomains)) {
33059
33264
  try {
33060
33265
  const cert = await edgeCertificateForStage(slug, stage, reg, true);
33061
33266
  if (cert.required) applied.push(`edge certificate ${stage}: ${cert.action ?? "checked"}`);
@@ -33135,11 +33340,25 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
33135
33340
  });
33136
33341
  }
33137
33342
  }
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));
33343
+ let ancillaryPlan;
33344
+ let environmentPlan;
33345
+ if (!o.execute && !onlyTarget) {
33346
+ try {
33347
+ const liveLabels = JSON.parse((await gh(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
33348
+ if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
33349
+ ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
33350
+ environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
33351
+ } catch (e) {
33352
+ return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
33353
+ }
33354
+ }
33355
+ 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
33356
  else {
33140
33357
  console.log(renderSeedPlan(actions));
33141
33358
  if (controlRows.length) console.log(`
33142
33359
  ${renderBootstrapControlPlan(controlRows)}`);
33360
+ if (ancillaryPlan && environmentPlan) console.log(`
33361
+ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
33143
33362
  if (o.execute) console.log(`
33144
33363
  LIVE apply to ${repo}:
33145
33364
  ${applied.join("\n ")}`);
@@ -33649,7 +33868,7 @@ function formatDeployStatus(r) {
33649
33868
  var STAGES2 = ["dev", "rc", "main"];
33650
33869
  function registerDeployCommands(program3) {
33651
33870
  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) => {
33871
+ 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
33872
  if (!STAGES2.includes(stage)) {
33654
33873
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
33655
33874
  }
@@ -38035,6 +38254,70 @@ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
38035
38254
  });
38036
38255
  }
38037
38256
 
38257
+ // src/api-read.ts
38258
+ init_cli_shared();
38259
+ var API_ACCEPT_HEADERS = {
38260
+ json: "application/vnd.github+json",
38261
+ raw: "application/vnd.github.raw"
38262
+ };
38263
+ function acceptHeaderFor(accept) {
38264
+ const v = (accept ?? "json").trim().toLowerCase();
38265
+ const header = API_ACCEPT_HEADERS[v];
38266
+ if (!header) {
38267
+ throw new Error(`api get: --accept must be one of: json, raw (received '${accept}')`);
38268
+ }
38269
+ return header;
38270
+ }
38271
+ var NAME = "[A-Za-z0-9_.-]+";
38272
+ var FREE = "[^?#%\\\\\\s]+";
38273
+ var PULL_FILES_RE = new RegExp(`^repos/${NAME}/${NAME}/pulls/\\d+/files$`);
38274
+ var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FREE}$`);
38275
+ var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
38276
+ var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
38277
+ var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>]";
38278
+ function parseApiGetPath(rawPath) {
38279
+ const path2 = rawPath.trim();
38280
+ const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
38281
+ if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
38282
+ if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
38283
+ if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
38284
+ const q = path2.indexOf("?");
38285
+ const base = q === -1 ? path2 : path2.slice(0, q);
38286
+ const query = q === -1 ? "" : path2.slice(q + 1);
38287
+ if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
38288
+ return { kind: "contents", path: path2, paginate: false };
38289
+ }
38290
+ throw new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
38291
+ }
38292
+ function buildApiGetArgs(path2, accept) {
38293
+ const req = parseApiGetPath(path2);
38294
+ const args = ["api", req.path];
38295
+ if (req.paginate) args.push("--paginate");
38296
+ const header = acceptHeaderFor(accept);
38297
+ if (header !== API_ACCEPT_HEADERS.json) args.push("-H", `Accept: ${header}`);
38298
+ return args;
38299
+ }
38300
+ async function runApiGet(run, path2, accept) {
38301
+ return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
38302
+ }
38303
+ function registerApiReadCommands(program3) {
38304
+ const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents)");
38305
+ 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) => {
38306
+ let body;
38307
+ try {
38308
+ body = await runApiGet(
38309
+ async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout,
38310
+ path2,
38311
+ o.accept
38312
+ );
38313
+ } catch (e) {
38314
+ const err = e;
38315
+ return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
38316
+ }
38317
+ process.stdout.write(body);
38318
+ });
38319
+ }
38320
+
38038
38321
  // src/pr-commands.ts
38039
38322
  var import_promises4 = require("node:fs/promises");
38040
38323
  init_cli_shared();
@@ -43515,6 +43798,14 @@ function registerCollaborationCommands(program3) {
43515
43798
  }
43516
43799
  return resolveParentField(payload);
43517
43800
  }
43801
+ async function readClosedByField(number, repo) {
43802
+ try {
43803
+ return { closedBy: await runClosedBy(queryDeps(), { number, repo }) };
43804
+ } catch (e) {
43805
+ const err = e;
43806
+ return { closedByReadError: (err.stderr || err.message || String(e)).trim() };
43807
+ }
43808
+ }
43518
43809
  function withoutTransportNumber(data) {
43519
43810
  const rest = { ...data };
43520
43811
  delete rest.number;
@@ -43533,7 +43824,7 @@ function registerCollaborationCommands(program3) {
43533
43824
  return void 0;
43534
43825
  }
43535
43826
  }
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) => {
43827
+ 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
43828
  let parsed;
43538
43829
  try {
43539
43830
  parsed = parseIssueRef(ref, o.repo);
@@ -43544,6 +43835,7 @@ function registerCollaborationCommands(program3) {
43544
43835
  const repo = await resolveRepo(parsed.repo ?? o.repo);
43545
43836
  if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
43546
43837
  const fields = normalizeIssueViewJsonFields(o.json);
43838
+ const wantsClosedBy = fields.split(",").map((f) => f.trim()).includes("closedBy");
43547
43839
  const gh = resolveIssueViewGhFields(fields);
43548
43840
  const emit = (data, parent) => console.log(JSON.stringify({ ...gh.numberIsTransport ? withoutTransportNumber(data) : data, ...parent }));
43549
43841
  const readIssue = async (targetRepo2, targetNumber) => {
@@ -43555,9 +43847,10 @@ function registerCollaborationCommands(program3) {
43555
43847
  const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh.ghFields]);
43556
43848
  return { data, parent: await readParentField(targetNumber, targetRepo2) };
43557
43849
  };
43850
+ const readClosedBy = async (targetRepo2, targetNumber) => wantsClosedBy ? readClosedByField(targetNumber, targetRepo2) : {};
43558
43851
  try {
43559
43852
  const { data, parent } = await readIssue(repo, n);
43560
- emit(data, parent);
43853
+ emit(data, { ...parent, ...await readClosedBy(repo, n) });
43561
43854
  } catch (e) {
43562
43855
  const err = e;
43563
43856
  const raw = (err.stderr || err.message || String(e)).trim();
@@ -43566,7 +43859,7 @@ function registerCollaborationCommands(program3) {
43566
43859
  if (moved) {
43567
43860
  try {
43568
43861
  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);
43862
+ emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, { ...parent, ...await readClosedBy(moved.repo, moved.number) });
43570
43863
  return;
43571
43864
  } catch (reRead) {
43572
43865
  const reErr = reRead;
@@ -48711,7 +49004,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
48711
49004
  return failGraceful(`runtime tenant reconcile: ${e.message}`);
48712
49005
  }
48713
49006
  });
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) => {
49007
+ 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
49008
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
48716
49009
  const cfg = await loadConfig();
48717
49010
  const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
@@ -49195,7 +49488,7 @@ ${filelessTransitionGuide(target, o.stage)}`
49195
49488
  return reportWrite("org project set-deploy", res);
49196
49489
  });
49197
49490
  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) => {
49491
+ 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
49492
  const cfg = await loadConfig();
49200
49493
  let target;
49201
49494
  try {
@@ -49223,6 +49516,9 @@ projectTenantTasks.command("declare <name>").description("register a NEW tenant
49223
49516
  ...o.artifact !== void 0 ? { artifact: o.artifact } : {}
49224
49517
  };
49225
49518
  }
49519
+ if (task && typeof task === "object" && task.secretOutput !== void 0) {
49520
+ 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=...'`");
49521
+ }
49226
49522
  const res = await postJson(`/projects/${encodeURIComponent(slug)}/tenant-tasks`, { repo: target, tenantTasks: { [name]: task } }, registryClientDeps(cfg), "POST", { noRetry: true });
49227
49523
  if (o.json) {
49228
49524
  console.log(JSON.stringify(res, null, 2));
@@ -49370,6 +49666,7 @@ registerDeployCommands(program2);
49370
49666
  registerDiscoveryCommands(program2);
49371
49667
  registerExplainCommand(program2);
49372
49668
  registerPrLifecycleCommands(program2);
49669
+ registerApiReadCommands(program2);
49373
49670
  registerSessionReport(program2);
49374
49671
  registerLearningClosureRateCommand(program2);
49375
49672
  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.26",
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",