@mutmutco/cli 4.5.57 → 4.5.59

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 +291 -98
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5825,11 +5825,14 @@ function waitReadFailureMessage(e) {
5825
5825
  return (stderr || err?.message || String(e)).replace(/\s+/g, " ").slice(0, 300);
5826
5826
  }
5827
5827
  var PR_CHECKS_POLL_MS = 3e4;
5828
+ function pollIntervalMs(elapsedMs) {
5829
+ return Math.min(4 * PR_CHECKS_POLL_MS, PR_CHECKS_POLL_MS * 2 ** Math.floor(Math.max(0, elapsedMs) / 3e5));
5830
+ }
5828
5831
  var PR_CHECKS_TIMEOUT_MS = 30 * 6e4;
5829
5832
  var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
5830
5833
  var PR_CHECKS_CONFIRM_MS = 5e3;
5831
5834
  var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
5832
- var PR_CHECKS_RATELIMIT_FLOOR = 50;
5835
+ var PR_CHECKS_RATELIMIT_FLOOR = 1e3;
5833
5836
  var PR_CHECKS_RATELIMIT_JITTER_MAX_MS = 5e3;
5834
5837
  var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
5835
5838
  var PR_CHECKS_ZERO_RUNS_GRACE_MS = 2 * 6e4;
@@ -5965,7 +5968,7 @@ async function waitForPrChecks(deps) {
5965
5968
  }
5966
5969
  if (state === "failing") {
5967
5970
  lastDetail = "failing (a check failed; others still running)";
5968
- await deps.sleep(PR_CHECKS_POLL_MS);
5971
+ await deps.sleep(pollIntervalMs(now() - started));
5969
5972
  continue;
5970
5973
  }
5971
5974
  if (state === "success") {
@@ -6003,11 +6006,11 @@ async function waitForPrChecks(deps) {
6003
6006
  } else {
6004
6007
  zeroRunsStreak = 0;
6005
6008
  }
6006
- await deps.sleep(PR_CHECKS_POLL_MS);
6009
+ await deps.sleep(pollIntervalMs(now() - started));
6007
6010
  continue;
6008
6011
  }
6009
6012
  lastDetail = state;
6010
- await deps.sleep(PR_CHECKS_POLL_MS);
6013
+ await deps.sleep(pollIntervalMs(now() - started));
6011
6014
  }
6012
6015
  return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
6013
6016
  }
@@ -6044,15 +6047,38 @@ async function pollToken() {
6044
6047
  pollTokenOnce ??= pollTokenProvider().catch(() => void 0);
6045
6048
  return pollTokenOnce;
6046
6049
  }
6047
- async function defaultGhApi(args) {
6048
- const token = await pollToken();
6049
- const { stdout } = await execFileP("gh", ["api", ...args], {
6050
- timeout: REST_GH_TIMEOUT_MS,
6051
- // Only when a token was actually minted: passing `env` at all would otherwise replace the
6052
- // inherited environment wholesale, and `gh` needs the rest of it (PATH, GH_HOST, config home).
6053
- ...token ? { env: { ...process.env, GH_TOKEN: token } } : {}
6050
+ var INCLUDED_HEADER_BLOCK = /^(?:\r?\n)?HTTP\/\d(?:\.\d)? (\d{3})[^\n]*\n(?:[^\n]+\n)*?\r?\n/gm;
6051
+ function parseIncludedOutput(out) {
6052
+ let status;
6053
+ let etag;
6054
+ let pages = 0;
6055
+ const body = out.replace(INCLUDED_HEADER_BLOCK, (block, code) => {
6056
+ if (pages++ === 0) {
6057
+ status = Number(code);
6058
+ etag = /^etag:\s*(.+?)\s*$/im.exec(block)?.[1];
6059
+ }
6060
+ return "";
6054
6061
  });
6055
- return stdout;
6062
+ return { body, status, etag, pages };
6063
+ }
6064
+ var etagCache = /* @__PURE__ */ new Map();
6065
+ async function defaultGhApi(args) {
6066
+ const key = args.join("\0");
6067
+ const cached = etagCache.get(key);
6068
+ let stdout;
6069
+ try {
6070
+ ({ stdout } = await execFileP("gh", ["api", "--include", ...cached ? ["-H", `If-None-Match: ${cached.etag}`] : [], ...args], {
6071
+ timeout: REST_GH_TIMEOUT_MS
6072
+ }));
6073
+ } catch (e) {
6074
+ const out = e.stdout;
6075
+ if (cached && typeof out === "string" && parseIncludedOutput(out).status === 304) return cached.body;
6076
+ throw e;
6077
+ }
6078
+ const { body, etag, pages } = parseIncludedOutput(stdout);
6079
+ if (etag && pages === 1) etagCache.set(key, { etag, body });
6080
+ else etagCache.delete(key);
6081
+ return body;
6056
6082
  }
6057
6083
  function classifyCheckRun(run) {
6058
6084
  if (run.status !== "completed") return "pending";
@@ -8475,6 +8501,9 @@ function isValidSecretKey(key) {
8475
8501
  if (key.includes("..") || key.startsWith("/") || key.includes("*")) return false;
8476
8502
  return KEY_RE.test(key);
8477
8503
  }
8504
+ function stripTrailingCRLF(value) {
8505
+ return value.replace(/\r\n$|\r$|\n$/, "");
8506
+ }
8478
8507
  function classifyTier(_slug, key) {
8479
8508
  const slash2 = key.indexOf("/");
8480
8509
  if (slash2 === -1) return "project";
@@ -8637,7 +8666,7 @@ async function fetchSecretValue(deps, key, opts, diagnose = false) {
8637
8666
  if (!res.ok) {
8638
8667
  if (diagnose) {
8639
8668
  const body = await readJsonBody(res);
8640
- const reason = res.status === 403 ? "authority refused" : res.status === 429 ? "rate limited" : "Hub route refused";
8669
+ const reason = res.status === 403 ? "authority refused" : res.status === 429 ? "rate limited" : body.code === "github-rate-limited" ? "Hub GitHub App allowance used up, not a denial" : "Hub route refused";
8641
8670
  throw new Error(`${reason} (HTTP ${res.status}${typeof body.error === "string" && /^(forbidden|unauthorized|rate limit|too many requests|master-admin only)$/i.test(body.error) ? `: ${body.error}` : ""})`);
8642
8671
  }
8643
8672
  return null;
@@ -9918,6 +9947,10 @@ async function secretsVerify(deps, key, opts) {
9918
9947
  }
9919
9948
  const hasProviderVerifier = Boolean(providerForSecretKey(key));
9920
9949
  const value = await fetchSecretValue(deps, key, opts);
9950
+ if (value && value !== stripTrailingCRLF(value)) {
9951
+ deps.err(`${key}: stored value ends in a CR or LF character \u2014 likely corrupted by a piped write; re-set it with \`mmi-cli vault secrets set ${key}\``);
9952
+ return false;
9953
+ }
9921
9954
  if (!hasProviderVerifier) {
9922
9955
  if (!value) {
9923
9956
  deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
@@ -10105,6 +10138,40 @@ async function secretsDeclare(deps, key, opts) {
10105
10138
  deps.log(`declared ${key} (project tier, ${slug}) \u2014 catalog metadata only; ask master (secrets request) to wire it into requiredRuntimeSecrets`);
10106
10139
  return true;
10107
10140
  }
10141
+ async function secretsUndeclare(deps, key, opts) {
10142
+ if (!DECLARE_KEY_RE.test(key.split("@")[0] ?? "")) {
10143
+ deps.err(`secrets undeclare: invalid key ${JSON.stringify(key)} \u2014 must be an env-style name (UPPER_SNAKE convention), optionally @dev|rc|main`);
10144
+ return false;
10145
+ }
10146
+ const repo = await targetRepo(deps, opts);
10147
+ const slug = await vaultSlug(deps, opts);
10148
+ let res;
10149
+ try {
10150
+ res = await deps.fetch(`${deps.apiUrl}/projects/${slug}/secrets/undeclare`, {
10151
+ method: "POST",
10152
+ headers: await deps.headers({ "content-type": "application/json" }),
10153
+ body: JSON.stringify({ repo, key }),
10154
+ signal: AbortSignal.timeout(TIMEOUT_MS)
10155
+ });
10156
+ } catch (e) {
10157
+ deps.err(`secrets undeclare: ${e.message}`);
10158
+ return false;
10159
+ }
10160
+ const body = await readJsonBody(res);
10161
+ if (!res.ok) {
10162
+ deps.err(await upgradeMessage(res, body) ?? `secrets undeclare failed: HTTP ${res.status}${errorDetail(body)}`);
10163
+ return false;
10164
+ }
10165
+ const undeclared = body?.undeclared !== false;
10166
+ if (opts.json) {
10167
+ deps.log(JSON.stringify({ ok: true, key, undeclared }));
10168
+ return true;
10169
+ }
10170
+ deps.log(
10171
+ undeclared ? `undeclared ${key} (project tier, ${slug}) \u2014 catalog entry removed; the vault value was already absent` : `${key} was not declared in ${slug}'s catalog \u2014 nothing to undeclare`
10172
+ );
10173
+ return true;
10174
+ }
10108
10175
  function parseRailsCredentialMapping(raw, stage) {
10109
10176
  const eq = raw.indexOf("=");
10110
10177
  if (eq <= 0 || eq === raw.length - 1) return null;
@@ -15697,11 +15764,12 @@ function planBootstrapLabels(labels, live) {
15697
15764
  ...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
15698
15765
  ];
15699
15766
  }
15700
- function renderBootstrapAncillaryPlan(labels, environments) {
15767
+ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
15701
15768
  return [
15702
15769
  "bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
15703
15770
  ...labels.map(({ action, name }) => ` ${action} label ${name}`),
15704
- ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
15771
+ ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`),
15772
+ ...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
15705
15773
  ].join("\n");
15706
15774
  }
15707
15775
  function resolveSeedContent(seed, vars, readFile9) {
@@ -16301,6 +16369,17 @@ function isHubControlRepo(repo) {
16301
16369
  function isJervHubRepo(repo) {
16302
16370
  return repo.toLowerCase() === JERV_HUB_REPO.toLowerCase();
16303
16371
  }
16372
+ var WINDOWS_GIT_BASH_MSYSTEM = /^(?:MINGW(?:32|64)|UCRT64|CLANG(?:64|ARM64))$/u;
16373
+ function isWindowsGitBash(msystem) {
16374
+ return WINDOWS_GIT_BASH_MSYSTEM.test(msystem ?? "");
16375
+ }
16376
+ function assertGitBashOnWindowsForJervHubRelease(repo, platform2 = process.platform, env = process.env) {
16377
+ if (platform2 !== "win32" || !isJervHubRepo(repo)) return;
16378
+ if (isWindowsGitBash(env.MSYSTEM)) return;
16379
+ throw new Error(
16380
+ `${repo}: Windows Git Bash required \u2014 the release train's Jerv Gateway deploy leg refuses outside it, and the current shell is not Git Bash (MSYSTEM=${env.MSYSTEM ?? "(unset)"}). Run the release from Git Bash instead, e.g.: "C:/Program Files/Git/bin/bash.exe" -lc "mmi-cli devops release --apply"`
16381
+ );
16382
+ }
16304
16383
 
16305
16384
  // src/train-apply-correlation.ts
16306
16385
  var import_node_fs18 = require("node:fs");
@@ -17069,10 +17148,10 @@ var rollout_plan_default = {
17069
17148
  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)."
17070
17149
  },
17071
17150
  baseline: {
17072
- version: "4.5.57",
17073
- tag: "v4.5.57",
17074
- commit: "d8aff7ba4689",
17075
- npm: "@mutmutco/cli@4.5.57"
17151
+ version: "4.5.59",
17152
+ tag: "v4.5.59",
17153
+ commit: "ea079366d96e",
17154
+ npm: "@mutmutco/cli@4.5.59"
17076
17155
  },
17077
17156
  exitCriterion: "fleet-n-of-n",
17078
17157
  hubOnlyShortcut: "forbidden",
@@ -17089,14 +17168,14 @@ var rollout_plan_default = {
17089
17168
  repo: "mutmutco/mmi-hub",
17090
17169
  role: "canary",
17091
17170
  schedule: "train",
17092
- v3Target: "v4.5.57"
17171
+ v3Target: "v4.5.59"
17093
17172
  }
17094
17173
  ],
17095
17174
  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.",
17096
17175
  rollback: {
17097
17176
  independent: true,
17098
- mechanism: "npm dist-tag latest -> 4.5.57 and redeploy the Hub Lambda from tag v4.5.57 (d8aff7ba4689); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17099
- v3Target: "v4.5.57 (@mutmutco/cli@4.5.57, tag commit d8aff7ba4689 \u2014 last known-good release carrying the repo-index v4-only contract)"
17177
+ mechanism: "npm dist-tag latest -> 4.5.59 and redeploy the Hub Lambda from tag v4.5.59 (ea079366d96e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17178
+ v3Target: "v4.5.59 (@mutmutco/cli@4.5.59, tag commit ea079366d96e \u2014 last known-good release carrying the repo-index v4-only contract)"
17100
17179
  }
17101
17180
  },
17102
17181
  {
@@ -20094,9 +20173,14 @@ function validSuiteCutContext(repo, context) {
20094
20173
  function authorisedSuiteCut(repo, meta, context) {
20095
20174
  return validSuiteCutContext(repo, context) && !!meta?.suite && context.lead.toLowerCase() === suiteLeadRepo(repo, meta.suite).toLowerCase();
20096
20175
  }
20176
+ var BUNDLE_ONLY_RC_EXCEPTION = "suite member: rc is bundle-only (no rc runtime)";
20177
+ function isBundleOnlySuiteMember(repo, suite) {
20178
+ if (!suite) return false;
20179
+ return repo.split("/")[1]?.toLowerCase() !== suite.toLowerCase();
20180
+ }
20097
20181
  function suiteMemberRefusal(repo, meta, lane, solo = false, cut) {
20098
20182
  const suite = meta?.suite;
20099
- if (!suite || repo.split("/")[1]?.toLowerCase() === suite.toLowerCase()) return null;
20183
+ if (!suite || !isBundleOnlySuiteMember(repo, suite)) return null;
20100
20184
  if (authorisedSuiteCut(repo, meta, cut)) return null;
20101
20185
  if (lane !== "rcand" && solo) return null;
20102
20186
  return `${repo} is part of the ${suite} suite; run the suite cut/release from ${suiteLeadRepo(repo, suite)} (mmi-cli devops suite rcand|release). A solo backend release needs --suite-solo --owner-go.`;
@@ -20701,7 +20785,7 @@ async function appendServedChannelLegs(deps, input, dispatch) {
20701
20785
  const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
20702
20786
  const prepared = await (input.prepared ?? prepareServedChannel(deps, input));
20703
20787
  if (!isHubControlRepo(input.repo)) {
20704
- const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut) && input.repo.split("/")[1]?.toLowerCase() !== "mmi-katip";
20788
+ const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut);
20705
20789
  const [home2, proof2] = await Promise.all([
20706
20790
  appendInstallHomePublish(deps, { repo: input.repo, installer: input.installer, publishStatus, watch: input.watch }, dispatch),
20707
20791
  bundleOnly && servedChannelFor(input.repo, input.installer) ? Promise.resolve({ ...dispatch, note: `${dispatch.note}; served channel not probed: a suite bundle-only release does not deploy the backend that serves it (#7468)` }) : appendServedChannelProof(deps, { repo: input.repo, tag: input.tag, shipped: shipLegsGreen(input.deploy, input.publish), installer: input.installer }, dispatch)
@@ -20971,7 +21055,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
20971
21055
  return extra;
20972
21056
  }
20973
21057
  async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
20974
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" && stage !== "dev") {
21058
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut) && stage !== "dev") {
20975
21059
  return { note: "suite bundle-only: live backend not deployed", deployStatus: "success" };
20976
21060
  }
20977
21061
  if (isCentralDispatchModel(model)) {
@@ -21098,7 +21182,7 @@ async function attemptRetire(deps, ctx) {
21098
21182
  return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
21099
21183
  }
21100
21184
  async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21101
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip")
21185
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut))
21102
21186
  return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
21103
21187
  if (model !== "tenant-container") {
21104
21188
  return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
@@ -24718,7 +24802,7 @@ async function runTrainDoctor(input) {
24718
24802
  }
24719
24803
  }
24720
24804
  let preflightTimeouts = 0;
24721
- if (!(repo.split("/")[1]?.toLowerCase() !== "mmi-katip" && authorisedSuiteCut(repo, meta, input.suiteCut))) for (; ; ) {
24805
+ if (!authorisedSuiteCut(repo, meta, input.suiteCut)) for (; ; ) {
24722
24806
  try {
24723
24807
  await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
24724
24808
  break;
@@ -25151,6 +25235,7 @@ function enforceGateBudget(deps, repo) {
25151
25235
  }
25152
25236
  }
25153
25237
  async function preflight(deps, ctx, stage, meta, lane) {
25238
+ assertGitBashOnWindowsForJervHubRelease(ctx.repo);
25154
25239
  deps.warn?.("Release: Checking release prerequisites");
25155
25240
  if (deps.suiteCut && !authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25156
25241
  throw new Error(`${ctx.repo}: suite cut context does not match registry META; no deployment or tag was changed`);
@@ -25161,7 +25246,7 @@ async function preflight(deps, ctx, stage, meta, lane) {
25161
25246
  if (model === "none") {
25162
25247
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
25163
25248
  }
25164
- if (!(ctx.slug !== "mmi-katip" && authorisedSuiteCut(ctx.repo, meta, deps.suiteCut)))
25249
+ if (!authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25165
25250
  await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
25166
25251
  await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
25167
25252
  await assertActionsJobsCanStart(deps, ctx.repo);
@@ -25760,7 +25845,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
25760
25845
  );
25761
25846
  const dispatch = recovered.deployStatus === "failure" && !recovered.note.includes(RCAND_RETRY_DEPLOY_COMMAND) ? { ...recovered, note: `once the cause is fixed, retry this rc deploy with \`${RCAND_RETRY_DEPLOY_COMMAND}\` (same tag and SHA, never re-cut): ${recovered.note}` } : recovered;
25762
25847
  const unverifiable = !isCentralDispatchModel(deployModel) && dispatch.deployStatus === "pending" && dispatch.runId == null && !dispatch.workflowRuns?.length;
25763
- const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut) && anchors.repo.split("/")[1]?.toLowerCase() !== "mmi-katip" ? { state: "skipped", sha, note: "suite bundle-only: rc backend not deployed (#7262)" } : unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
25848
+ const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut) ? { state: "skipped", sha, note: "suite bundle-only: rc backend not deployed (#7262)" } : unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
25764
25849
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployEntry), { ...opts, landed: "the immutable rc tag, the green wall, and origin/rc" });
25765
25850
  await recordPhase(ledger, deps, anchors, "publish", phaseEntry({ state: "skipped", sha, note: "rcand publishes nothing \u2014 the publish leg rides the release" }), { ...opts, landed });
25766
25851
  await recordPhase(ledger, deps, anchors, "alignment", phaseEntry({ state: "skipped", sha, note: "rcand aligns no branch \u2014 alignment rides the release" }), { ...opts, landed });
@@ -25923,7 +26008,7 @@ ${recovery.note}`), recoveryInput);
25923
26008
  }
25924
26009
  );
25925
26010
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
25926
- const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
26011
+ const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
25927
26012
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
25928
26013
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
25929
26014
  });
@@ -26346,7 +26431,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
26346
26431
  dispatch2.deployStatus = "success";
26347
26432
  dispatch2.note = `historical ${deployModel} deploy/publish status: success (re-verified by the recorded run)`;
26348
26433
  }
26349
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip") {
26434
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut)) {
26350
26435
  phaseInputs.deploy = { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" };
26351
26436
  if (dispatch2.deployStatus !== "failure" && (phaseInputs.publish.state === "complete" || phaseInputs.publish.state === "skipped")) {
26352
26437
  dispatch2.deployStatus = "success";
@@ -26481,7 +26566,7 @@ ${recovery.note}`), recoveryInput);
26481
26566
  );
26482
26567
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
26483
26568
  steps.push(`dispatched the ${deployModel} deploy path`);
26484
- const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
26569
+ const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
26485
26570
  if (persisted?.phases.deploy.superseded !== true) {
26486
26571
  await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
26487
26572
  strict: ledgerMode === "strict",
@@ -27461,15 +27546,16 @@ async function branchPresence(deps, repo, branch) {
27461
27546
  }
27462
27547
  }
27463
27548
  function collectRegistryRepos(projects) {
27464
- const seen = /* @__PURE__ */ new Set();
27549
+ const seen = /* @__PURE__ */ new Map();
27465
27550
  for (const project2 of projects) {
27466
27551
  for (const raw of project2.repos ?? []) {
27467
27552
  const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
27468
- seen.add(repo);
27553
+ const key = repo.toLowerCase();
27554
+ if (!seen.has(key)) seen.set(key, repo);
27469
27555
  }
27470
27556
  }
27471
- if (![...seen].some((r) => r.toLowerCase() === HUB_REPO4.toLowerCase())) seen.add(HUB_REPO4);
27472
- return [...seen].sort((a, b) => a.localeCompare(b));
27557
+ if (!seen.has(HUB_REPO4.toLowerCase())) seen.set(HUB_REPO4.toLowerCase(), HUB_REPO4);
27558
+ return [...seen.values()].sort((a, b) => a.localeCompare(b));
27473
27559
  }
27474
27560
  async function prTriggeredWorkflowsOnRef(deps, repo, branch, repoClass) {
27475
27561
  const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, branch, PRODUCT_GATE_PATH);
@@ -27516,7 +27602,29 @@ async function filterPullRequestTriggered(deps, repo, branch, paths) {
27516
27602
  }
27517
27603
  return kept;
27518
27604
  }
27519
- async function auditRepoCi(repo, deps) {
27605
+ function trackUnauditableReads(deps) {
27606
+ let reason;
27607
+ const record = (e) => {
27608
+ if (reason) return;
27609
+ const err = e;
27610
+ if (err?.rateLimited) reason = `GitHub App rate-limited: ${err.message ?? "rate limit exceeded"}`;
27611
+ else if (err?.status === 403 || err?.status === 429) reason = `HTTP ${err.status}${err.message ? `: ${err.message}` : ""}`;
27612
+ };
27613
+ const client = {
27614
+ ...deps.client,
27615
+ rest: async (method, path2, init) => {
27616
+ try {
27617
+ return await deps.client.rest(method, path2, init);
27618
+ } catch (e) {
27619
+ record(e);
27620
+ throw e;
27621
+ }
27622
+ }
27623
+ };
27624
+ return { deps: { ...deps, client }, reason: () => reason };
27625
+ }
27626
+ async function auditRepoCi(repo, callerDeps) {
27627
+ const { deps, reason: unauditedReason } = trackUnauditableReads(callerDeps);
27520
27628
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
27521
27629
  const repoClass = classifyRepo(repo, meta);
27522
27630
  const checks = [];
@@ -27811,6 +27919,10 @@ async function auditRepoCi(repo, deps) {
27811
27919
  const workflows = await getBranchWorkflows();
27812
27920
  if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
27813
27921
  }
27922
+ const failure = unauditedReason();
27923
+ if (failure) {
27924
+ return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
27925
+ }
27814
27926
  const ok = checks.every((c) => c.ok);
27815
27927
  return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
27816
27928
  }
@@ -27852,8 +27964,8 @@ function renderCiAuditMarkdown(report) {
27852
27964
  "|------|-------|--------|----|---------|"
27853
27965
  ];
27854
27966
  for (const r of report.repos) {
27855
- const gap = r.checks.find((c) => !c.ok);
27856
- lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
27967
+ const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
27968
+ lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap} |`);
27857
27969
  }
27858
27970
  return lines2.join("\n");
27859
27971
  }
@@ -27863,8 +27975,13 @@ function renderCiAuditText(report) {
27863
27975
  renderCiAuditScopeLine(report)
27864
27976
  ];
27865
27977
  for (const r of report.repos) {
27978
+ const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
27866
27979
  lines2.push(`
27867
- ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
27980
+ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
27981
+ if (r.unauditedReason) {
27982
+ lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
27983
+ continue;
27984
+ }
27868
27985
  for (const c of r.checks) {
27869
27986
  lines2.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
27870
27987
  }
@@ -31350,7 +31467,7 @@ async function readSecretStdin() {
31350
31467
  }
31351
31468
  const chunks = [];
31352
31469
  for await (const chunk of process.stdin) chunks.push(chunk);
31353
- return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
31470
+ return stripTrailingCRLF(Buffer.concat(chunks).toString("utf8"));
31354
31471
  }
31355
31472
  function makeSecretsDeps(cfg) {
31356
31473
  return {
@@ -31502,6 +31619,10 @@ function registerSecretsCommands(program3) {
31502
31619
  });
31503
31620
  if (!ok) process.exitCode = 1;
31504
31621
  }));
31622
+ secrets.command("undeclare <key>").description("project-admin self-undeclare: retire a secrets-catalog entry for your own repo (catalog metadata only \u2014 never a value); declare's exact inverse (MMI-Hub#7524). Refuses (409) while a live vault value still exists for the key at any declared stage \u2014 delete the value first (`secrets rm`), then undeclare. A key that is not declared is a no-op, not an error.").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
31623
+ const ok = await secretsUndeclare(d, key, { repo: o.repo, json: o.json });
31624
+ if (!ok) process.exitCode = 1;
31625
+ }));
31505
31626
  secrets.command("verify <key>").description("validate a known provider secret without printing its value; a key with no configured provider verifier falls back to a value-free PRESENT/ABSENT reachability check (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").action((key, o) => withSecrets(async (d) => {
31506
31627
  const ok = await secretsVerify(d, key, o);
31507
31628
  if (!ok) process.exitCode = 1;
@@ -32356,11 +32477,12 @@ function environmentNamesForTrack(track) {
32356
32477
  const branches = new Set(branchesForTrack(track));
32357
32478
  return REPO_ENVIRONMENTS.filter((environment) => branches.has(ENVIRONMENT_BRANCHES[environment]));
32358
32479
  }
32359
- function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32480
+ function repoEnvironmentSpecs(repo, releaseTrack = "full", suite) {
32360
32481
  const included = new Set(environmentNamesForTrack(releaseTrack));
32361
32482
  const trackExceptions = releaseTrack === "full" ? void 0 : TRACK_ENVIRONMENT_EXCEPTIONS[releaseTrack];
32483
+ const bundleOnlyRc = isBundleOnlySuiteMember(repo, suite);
32362
32484
  return REPO_ENVIRONMENTS.map((name) => {
32363
- const exception = name in included ? void 0 : trackExceptions?.[name];
32485
+ const exception = !included.has(name) ? trackExceptions?.[name] : name === "rc" && bundleOnlyRc ? BUNDLE_ONLY_RC_EXCEPTION : void 0;
32364
32486
  return {
32365
32487
  name,
32366
32488
  branch: ENVIRONMENT_BRANCHES[name],
@@ -32371,8 +32493,8 @@ function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32371
32493
  };
32372
32494
  });
32373
32495
  }
32374
- function provisionedEnvironmentSpecs(repo, releaseTrack = "full") {
32375
- return repoEnvironmentSpecs(repo, releaseTrack).filter((spec) => spec.exception == null);
32496
+ function provisionedEnvironmentSpecs(repo, releaseTrack = "full", suite) {
32497
+ return repoEnvironmentSpecs(repo, releaseTrack, suite).filter((spec) => spec.exception == null);
32376
32498
  }
32377
32499
  function asEnvironmentList(value) {
32378
32500
  if (Array.isArray(value)) return value;
@@ -32412,10 +32534,10 @@ var PLAN_GATED_PROTECTION_RE = /billing plan/i;
32412
32534
  function isPlanGatedProtectionError(error) {
32413
32535
  return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
32414
32536
  }
32415
- async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32537
+ async function reconcileRepoEnvironments(repo, releaseTrack, client, suite) {
32416
32538
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32417
32539
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32418
- const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
32540
+ const planned = provisionedEnvironmentSpecs(repo, releaseTrack, suite);
32419
32541
  const created = [];
32420
32542
  const updated = [];
32421
32543
  const skipped = [];
@@ -32448,10 +32570,10 @@ async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32448
32570
  }
32449
32571
  return { repo, planned, created, updated, skipped, planGated };
32450
32572
  }
32451
- async function verifyRepoEnvironments(repo, releaseTrack, client) {
32573
+ async function verifyRepoEnvironments(repo, releaseTrack, client, suite) {
32452
32574
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32453
32575
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32454
- return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
32576
+ return provisionedEnvironmentSpecs(repo, releaseTrack, suite).map((spec) => {
32455
32577
  const drift = environmentDrift(live.get(spec.name), spec);
32456
32578
  return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
32457
32579
  });
@@ -32569,7 +32691,17 @@ function planBootstrapControlPlane(input) {
32569
32691
  }
32570
32692
  }
32571
32693
  const deployStages = deployStagesForBootstrap(input.deployModel, input.releaseTrack);
32694
+ const bundleOnlyRc = isBundleOnlySuiteMember(input.repo, input.suite);
32572
32695
  for (const stage of deployStages) {
32696
+ if (stage === "rc" && bundleOnlyRc) {
32697
+ rows.push({
32698
+ kind: "deploy-row",
32699
+ target: "DEPLOY#rc",
32700
+ action: "skip",
32701
+ detail: BUNDLE_ONLY_RC_EXCEPTION
32702
+ });
32703
+ continue;
32704
+ }
32573
32705
  const present = input.deployFacts?.stages?.[stage]?.present === true;
32574
32706
  const command = bootstrapSetDeployCommand(input.repo, stage, input.deployModel);
32575
32707
  rows.push(present && stage === "main" ? {
@@ -32959,10 +33091,13 @@ function centralContainerPortRangeCheck(deployModel, portRange, repo) {
32959
33091
  detail: ok ? void 0 : `${deployModel} needs PROJECT# META.portRange for mmi-cli stage \u2014 assign with: mmi-cli stage port-range ${repo}`
32960
33092
  };
32961
33093
  }
32962
- function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead) {
33094
+ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suite) {
32963
33095
  const stages = deployStagesForBootstrap(deployModel, releaseTrack);
32964
33096
  if (!stages.length) return [];
32965
33097
  return stages.map((stage) => {
33098
+ if (stage === "rc" && isBundleOnlySuiteMember(repo, suite)) {
33099
+ return { ok: true, label: "DEPLOY#rc row exception", detail: BUNDLE_ONLY_RC_EXCEPTION };
33100
+ }
32966
33101
  const ok = factsRead && facts?.stages?.[stage]?.present === true;
32967
33102
  return {
32968
33103
  ok,
@@ -33023,7 +33158,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33023
33158
  }
33024
33159
  let environmentDrift2 = [];
33025
33160
  try {
33026
- environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client);
33161
+ environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
33027
33162
  } catch (e) {
33028
33163
  checks.push({
33029
33164
  ok: false,
@@ -33038,7 +33173,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33038
33173
  detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
33039
33174
  });
33040
33175
  }
33041
- for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full")).filter((entry) => entry.exception)) {
33176
+ for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full"), deps.projectMeta?.suite).filter((entry) => entry.exception)) {
33042
33177
  checks.push({ ok: true, label: `repository environment exception: ${spec.name}`, detail: spec.exception });
33043
33178
  }
33044
33179
  const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
@@ -33112,7 +33247,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33112
33247
  deps.deployModel,
33113
33248
  effectiveTrack,
33114
33249
  deps.deployFacts,
33115
- deps.deployFactsRead === true
33250
+ deps.deployFactsRead === true,
33251
+ deps.projectMeta?.suite
33116
33252
  ));
33117
33253
  const readme = await contentText(deps, repo, baseBranch, "README.md");
33118
33254
  checks.push({
@@ -33349,9 +33485,13 @@ function applyWaivers(checks, waivers) {
33349
33485
  const set = new Set(waivers);
33350
33486
  return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
33351
33487
  }
33488
+ var laneReasons = (doctor) => doctor.findings.filter((f) => (f.severity === "blocker" || f.code === "origin-unverified") && f.code !== SUITE_GUARD_CODE);
33489
+ function onlySuiteLeadOwned(doctor) {
33490
+ return !doctor.ready && laneReasons(doctor).length === 0 && doctor.findings.some((f) => f.code === SUITE_GUARD_CODE);
33491
+ }
33352
33492
  function laneNotEvaluatedFromThisBranch(doctor) {
33353
33493
  if (doctor.ready) return void 0;
33354
- const reasons = doctor.findings.filter((f) => f.severity === "blocker" || f.code === "origin-unverified");
33494
+ const reasons = laneReasons(doctor);
33355
33495
  const only = reasons.length === 1 ? reasons[0] : void 0;
33356
33496
  return only?.code === "branch-mismatch" ? only.title : void 0;
33357
33497
  }
@@ -33362,7 +33502,7 @@ function attachTrainDoctorVerdicts(report, trainDoctors) {
33362
33502
  });
33363
33503
  return {
33364
33504
  ...report,
33365
- ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0),
33505
+ ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0 || onlySuiteLeadOwned(doctor)),
33366
33506
  ...marked.length ? { trainDoctors: marked } : {}
33367
33507
  };
33368
33508
  }
@@ -33376,6 +33516,8 @@ function renderBootstrapVerifyReport(report) {
33376
33516
  lines2.push("", formatTrainDoctor(doctor));
33377
33517
  if (doctor.notEvaluatedFromThisBranch) {
33378
33518
  lines2.push(` NOT EVALUATED from this branch \u2014 ${doctor.notEvaluatedFromThisBranch}; one checkout stands on one branch, so this lane does not fail the verify (#6677). Run \`mmi-cli devops train doctor --lane ${doctor.lane}\` from that branch for its own verdict.`);
33519
+ } else if (onlySuiteLeadOwned(doctor)) {
33520
+ lines2.push(" SUITE MEMBER: this lane is run from the suite lead (mmi-cli devops suite rcand|release), so suite-member-solo does not fail the verify (#7511).");
33379
33521
  }
33380
33522
  }
33381
33523
  return lines2.join("\n");
@@ -33707,6 +33849,7 @@ function registerBootstrapCommands(program3) {
33707
33849
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(meta, cls), repo),
33708
33850
  requiredChecks: meta?.requiredChecks ?? void 0,
33709
33851
  deployModel: meta?.deployModel,
33852
+ suite: meta?.suite,
33710
33853
  owners: controlOwners,
33711
33854
  github,
33712
33855
  deployFacts,
@@ -33773,31 +33916,39 @@ function registerBootstrapCommands(program3) {
33773
33916
  return fail(`bootstrap apply: ${e.message}`);
33774
33917
  }
33775
33918
  let effectiveTrack = bootstrapReleaseTrack;
33919
+ let suiteSlug;
33776
33920
  if (!o.releaseTrack) {
33777
33921
  const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33778
33922
  if (read.ok) {
33779
33923
  effectiveTrack = resolveReleaseTrack(metaWithDeclaredClass(read.project, o.class), void 0, repo);
33924
+ suiteSlug = read.project?.suite;
33780
33925
  } else if (o.execute) {
33781
33926
  return failGraceful(`bootstrap apply: registry META unreadable (${read.error}) and no --release-track given \u2014 refusing to assume the ${bootstrapReleaseTrack} track for live writes`);
33782
33927
  } else {
33783
33928
  console.error(`bootstrap apply: registry META unreadable (${read.error}) \u2014 release track ${bootstrapReleaseTrack} is ASSUMED from the class default for this dry-run; --execute will refuse`);
33784
33929
  }
33930
+ } else {
33931
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33932
+ if (read.ok) suiteSlug = read.project?.suite;
33785
33933
  }
33786
33934
  if (o.only.trim() === "environments") {
33787
33935
  const client = defaultGitHubClient();
33788
33936
  const envJson = { repo, track: effectiveTrack };
33789
33937
  if (!o.execute) {
33790
- const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
33938
+ const drift = await verifyRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33791
33939
  const classified = drift.map(classifyEnvironmentDrift);
33792
33940
  envJson.plan = classified;
33793
33941
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33794
33942
  else {
33795
33943
  console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
33796
33944
  for (const c of classified) console.log(` ${c.action} ${c.environment} \u2014 ${c.reason}`);
33945
+ for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
33946
+ if (spec.exception) console.log(` OK environment ${spec.name} (${spec.exception})`);
33947
+ }
33797
33948
  console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
33798
33949
  }
33799
33950
  } else {
33800
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
33951
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33801
33952
  const lines2 = [];
33802
33953
  if (envResult.created.length) lines2.push(`environments created: ${envResult.created.join(", ")}`);
33803
33954
  if (envResult.updated.length) lines2.push(`environments updated: ${envResult.updated.join(", ")}`);
@@ -33805,6 +33956,9 @@ function registerBootstrapCommands(program3) {
33805
33956
  for (const gated of envResult.planGated) {
33806
33957
  lines2.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
33807
33958
  }
33959
+ for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
33960
+ if (spec.exception) lines2.push(`OK environment ${spec.name} (${spec.exception})`);
33961
+ }
33808
33962
  envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
33809
33963
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33810
33964
  else {
@@ -33926,6 +34080,7 @@ function registerBootstrapCommands(program3) {
33926
34080
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
33927
34081
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
33928
34082
  deployModel: applyDeployModel,
34083
+ suite: suiteSlug,
33929
34084
  owners: controlOwners,
33930
34085
  github,
33931
34086
  deployFacts: controlDeployFacts,
@@ -34270,7 +34425,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34270
34425
  }
34271
34426
  if (o.execute && !onlyTarget) {
34272
34427
  try {
34273
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
34428
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient(), suiteSlug);
34274
34429
  if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
34275
34430
  if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
34276
34431
  if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
@@ -34367,6 +34522,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34367
34522
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
34368
34523
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
34369
34524
  deployModel: applyDeployModel,
34525
+ suite: suiteSlug,
34370
34526
  owners: controlOwners,
34371
34527
  github,
34372
34528
  deployFacts: controlDeployFacts,
@@ -34410,7 +34566,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34410
34566
  const liveLabels = JSON.parse((await gh2(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
34411
34567
  if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
34412
34568
  ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
34413
- environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
34569
+ environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient, suiteSlug)).map(classifyEnvironmentDrift);
34414
34570
  } catch (e) {
34415
34571
  return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
34416
34572
  }
@@ -34421,7 +34577,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34421
34577
  if (controlRows.length) console.log(`
34422
34578
  ${renderBootstrapControlPlan(controlRows)}`);
34423
34579
  if (ancillaryPlan && environmentPlan) console.log(`
34424
- ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
34580
+ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug).filter((spec) => spec.exception).map((spec) => ({ name: spec.name, detail: spec.exception })))}`);
34425
34581
  if (o.execute) console.log(`
34426
34582
  LIVE apply to ${repo}:
34427
34583
  ${applied.join("\n ")}`);
@@ -35086,13 +35242,15 @@ var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
35086
35242
  function suitePins(record, bundles) {
35087
35243
  return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
35088
35244
  module: m.id,
35089
- rc: `https://${m.id === "katip" ? m.rcHost ?? m.host : m.host}/${m.id === "katip" ? "" : "rc/"}module.js?v=${m.tag}`,
35245
+ rc: `https://${m.host}/rc/module.js?v=${m.tag}`,
35090
35246
  main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
35091
35247
  });
35092
35248
  }
35093
35249
  function mainPinFor(host, bundles, tag) {
35094
- const path2 = [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35095
- return `https://${host}${path2}?v=${tag}`;
35250
+ return `https://${host}${mainBundlePath(bundles)}?v=${tag}`;
35251
+ }
35252
+ function mainBundlePath(bundles) {
35253
+ return [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35096
35254
  }
35097
35255
  function pathOf(url) {
35098
35256
  try {
@@ -35120,7 +35278,7 @@ function readSuiteCut(path2) {
35120
35278
  } catch {
35121
35279
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
35122
35280
  }
35123
- if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
35281
+ if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
35124
35282
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
35125
35283
  return record;
35126
35284
  }
@@ -35176,13 +35334,13 @@ async function suiteRcand(deps, opts) {
35176
35334
  const members = suiteMembers(await deps.registry());
35177
35335
  const modules = members.filter((m) => m.repo !== SUITE_REPO);
35178
35336
  const plan = [
35179
- ...modules.map((m) => `${m.repo}: normal rcand${m.id === "katip" ? " + rc server" : " + module-rc-publish.yml"} (once)`),
35337
+ ...modules.map((m) => `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
35180
35338
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35181
35339
  `${SUITE_REPO}: rcand`
35182
35340
  ];
35183
35341
  if (opts.dryRun) return { plan };
35184
35342
  let record = readSuiteCut(deps.recordPath);
35185
- if (record?.announcement === "sent" && record.leadReleased) {
35343
+ if ((record?.announcement === "sent" || record?.announcement === "skipped") && record.leadReleased) {
35186
35344
  const archive = `${deps.recordPath}.${record.leadRcTag}.json`;
35187
35345
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
35188
35346
  (0, import_node_fs29.renameSync)(deps.recordPath, archive);
@@ -35200,7 +35358,7 @@ async function suiteRcand(deps, opts) {
35200
35358
  const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
35201
35359
  const previousRc = rcHead ?? await deps.head(m.repo, "main");
35202
35360
  if (!backend.version || !backend.host || !development || !SHA2.test(development) || !previousRc || !SHA2.test(previousRc)) throw new Error(`${m.repo}: live backend or branch snapshot is unproven; no cut started`);
35203
- return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host, rcHost: backend.rcHost, development, previousRc, ...rcHead === null ? { rcWasAbsent: true } : {} };
35361
+ return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host, development, previousRc, ...rcHead === null ? { rcWasAbsent: true } : {} };
35204
35362
  }))
35205
35363
  ]);
35206
35364
  if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
@@ -35233,7 +35391,7 @@ async function suiteRcand(deps, opts) {
35233
35391
  }
35234
35392
  const refusals = [];
35235
35393
  for (const member of record.members) {
35236
- if (member.published || member.id === "katip") continue;
35394
+ if (member.published) continue;
35237
35395
  try {
35238
35396
  await deps.preflight(member.repo);
35239
35397
  } catch (e) {
@@ -35251,7 +35409,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35251
35409
  if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
35252
35410
  throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
35253
35411
  if (member.carried) {
35254
- if (member.id !== "katip") await deps.publish(member.repo, pinVersion(member.rcPin));
35412
+ await deps.publish(member.repo, pinVersion(member.rcPin));
35255
35413
  continue;
35256
35414
  }
35257
35415
  if (!member.tag) {
@@ -35264,7 +35422,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35264
35422
  }
35265
35423
  if ((await deps.tagSha(member.repo, member.tag))?.toLowerCase() !== member.sha.toLowerCase()) throw new Error(`${member.repo}: rc tag ${member.tag} moved or is unreadable; stop`);
35266
35424
  if (!member.published) {
35267
- if (member.id !== "katip") await deps.publish(member.repo, member.tag);
35425
+ await deps.publish(member.repo, member.tag);
35268
35426
  member.published = true;
35269
35427
  writeSuiteCut(deps.recordPath, record);
35270
35428
  }
@@ -35323,43 +35481,59 @@ async function suiteRelease(deps, ownerGo) {
35323
35481
  if (hotfix) member.hotfixedTo = hotfix;
35324
35482
  }
35325
35483
  if (member.hotfixedTo && !member.released) continue;
35326
- const katipReleaseSha = member.id === "katip" ? await deps.tagSha(member.repo, planned) : void 0;
35327
- const expectedBackend = member.id === "katip" && member.released ? member.backendAfterRelease ?? katipReleaseSha : member.backend;
35328
- if (live.version !== expectedBackend && !(member.id === "katip" && !member.released && katipReleaseSha && live.version === katipReleaseSha))
35329
- throw new Error(`${member.repo}: live backend changed after the cut (${expectedBackend} \u2192 ${live.version}); release refused`);
35484
+ if (live.version !== member.backend && !(member.hotfixedTo && await deps.hotfixOver(member.repo, planned, live.version) === member.hotfixedTo))
35485
+ throw new Error(`${member.repo}: live backend changed after the cut (${member.backend} \u2192 ${live.version}); release refused`);
35330
35486
  }
35331
35487
  if ((await deps.tagSha(SUITE_REPO, record.leadRcTag))?.toLowerCase() !== record.leadRcSha?.toLowerCase()) throw new Error("MM-Work rc tag changed after the cut; release refused");
35488
+ const publishesMain = !record.leadReleased;
35489
+ const refusals = [];
35490
+ for (const member of record.members) {
35491
+ if (!publishesMain || member.carried || member.released) continue;
35492
+ try {
35493
+ await deps.preflightMain(member.repo, mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
35494
+ } catch (e) {
35495
+ refusals.push(`${member.repo}: ${e.message}`);
35496
+ }
35497
+ }
35498
+ if (refusals.length) throw new Error(`module main serving is not ready; no member was released:
35499
+ ${refusals.join("\n")}
35500
+ Each member's live release must serve <data>/main/module.js at its main pin path when <data>/main exists.`);
35332
35501
  for (const member of record.members) {
35333
- if (member.released || member.carried) continue;
35334
- if (member.hotfixedTo) {
35335
- console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35336
- member.backendAfterRelease = (await deps.backend({ repo: member.repo, id: member.id, bundles: {} })).version;
35502
+ if (member.carried) continue;
35503
+ if (!member.released) {
35504
+ if (member.hotfixedTo) {
35505
+ console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35506
+ } else {
35507
+ if (member.releasedAs && member.releasedAs !== suiteReleaseTag(member.tag)) console.warn(`${member.repo}: its rc already shipped as ${member.releasedAs}, not ${suiteReleaseTag(member.tag)}; finishing that release (#7464)`);
35508
+ await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35509
+ }
35337
35510
  member.released = true;
35338
35511
  writeSuiteCut(deps.recordPath, record);
35339
- continue;
35340
35512
  }
35341
- if (member.releasedAs && member.releasedAs !== suiteReleaseTag(member.tag)) console.warn(`${member.repo}: its rc already shipped as ${member.releasedAs}, not ${suiteReleaseTag(member.tag)}; finishing that release (#7464)`);
35342
- await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35343
- if (member.id === "katip") {
35344
- const expected = await deps.tagSha(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag));
35345
- const live = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
35346
- if (!expected || live.version !== expected) throw new Error(`${member.repo}: normal main deploy not stamped at released tag SHA; resume after verifying live backend`);
35347
- member.backendAfterRelease = live.version;
35513
+ if (publishesMain && !member.mainPublished) {
35514
+ await deps.publishMain(member.repo, member.hotfixedTo ?? member.releasedAs ?? suiteReleaseTag(member.tag), mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
35515
+ member.mainPublished = true;
35516
+ writeSuiteCut(deps.recordPath, record);
35348
35517
  }
35349
- member.released = true;
35350
- writeSuiteCut(deps.recordPath, record);
35351
35518
  }
35352
35519
  if (!record.leadReleased) {
35353
35520
  await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
35354
35521
  record.leadReleased = true;
35355
35522
  writeSuiteCut(deps.recordPath, record);
35356
35523
  }
35357
- if (record.announcement === "pending") throw new Error("suite announcement outcome ambiguous; verify it before retrying (never post twice)");
35524
+ if (record.announcement === "pending") {
35525
+ const reason = await deps.announceSkipReason(record);
35526
+ if (!reason) throw new Error("suite announcement outcome ambiguous; verify it before retrying (never post twice)");
35527
+ record.announcement = "skipped";
35528
+ record.announcementNote = reason;
35529
+ writeSuiteCut(deps.recordPath, record);
35530
+ }
35358
35531
  if (!record.announcement) {
35359
35532
  record.announcement = "pending";
35360
35533
  writeSuiteCut(deps.recordPath, record);
35361
- await deps.announce(record);
35362
- record.announcement = "sent";
35534
+ const { skipped } = await deps.announce(record);
35535
+ record.announcement = skipped ? "skipped" : "sent";
35536
+ if (skipped) record.announcementNote = skipped;
35363
35537
  writeSuiteCut(deps.recordPath, record);
35364
35538
  }
35365
35539
  return record;
@@ -42059,6 +42233,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
42059
42233
  try {
42060
42234
  entries = (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true });
42061
42235
  } catch (e) {
42236
+ if (e.code === "ENOENT") continue;
42062
42237
  return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
42063
42238
  }
42064
42239
  for (const entry of entries) {
@@ -43877,7 +44052,7 @@ ${list}`);
43877
44052
  console.error(line);
43878
44053
  }
43879
44054
  }
43880
- const credential = await pollToken().catch(() => void 0) ? "app_installation" : "user";
44055
+ const credential = "user";
43881
44056
  if (o.json) printLine(JSON.stringify({ ...result, credential }));
43882
44057
  else printLine(
43883
44058
  `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
@@ -44392,9 +44567,8 @@ function trainApplyDeps() {
44392
44567
  trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
44393
44568
  run: async (file, args) => {
44394
44569
  const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && /jerv-gateway-release-deploy\.sh$/.test(args[0] ?? "") ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file === "git" && args[0] === "commit" ? GIT_COMMIT_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
44395
- const watchToken = file === "gh" && args[0] === "run" && args[1] === "watch" ? await pollToken() : void 0;
44396
44570
  try {
44397
- return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, file === "git" ? trainGitArgs(args) : args, { timeout, ...watchToken ? { env: { ...process.env, GH_TOKEN: watchToken } } : {} })).stdout;
44571
+ return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, file === "git" ? trainGitArgs(args) : args, { timeout })).stdout;
44398
44572
  } catch (e) {
44399
44573
  throw surfaceTrainSubprocessFailure(e, file, args);
44400
44574
  }
@@ -45331,10 +45505,7 @@ function suiteTrainIo(root, summaryFile) {
45331
45505
  throw new Error(`${member.repo}: healthy live backend stamp and edgeDomains.main must agree with DEPLOY#main; stop`);
45332
45506
  const health = await probePublicHealth(url);
45333
45507
  if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
45334
- const rcHost = edgeDomains?.rc;
45335
- if (member.id === "katip" && (!rcHost || facts?.stages.rc?.domain !== rcHost))
45336
- throw new Error(`${member.repo}: normal Katip rc host must agree with DEPLOY#rc; stop`);
45337
- return { version: live.deployedTag, host, ...rcHost ? { rcHost } : {} };
45508
+ return { version: live.deployedTag, host };
45338
45509
  };
45339
45510
  const tagSha = async (repo, tag) => {
45340
45511
  try {
@@ -45449,10 +45620,22 @@ function suiteTrainIo(root, summaryFile) {
45449
45620
  const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
45450
45621
  if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
45451
45622
  };
45623
+ const publishMain = async (repo, tag, path2) => {
45624
+ const title = `module-main-publish ${repo} @ ${tag}`;
45625
+ const existing = (await rcRuns()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
45626
+ if (existing[0]?.conclusion === "success") return;
45627
+ if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-main-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
45628
+ const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`, "channel=main", `path=${path2}`], existing[0]);
45629
+ if (run.conclusion !== "success") throw new Error(`${repo}: module main publish run ${run.id} not green${await rcRunCause(run.id)}`);
45630
+ };
45452
45631
  const preflight2 = async (repo) => {
45453
45632
  const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
45454
45633
  if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
45455
45634
  };
45635
+ const preflightMain = async (repo, path2) => {
45636
+ const run = await watchRcRun(`module-main-preflight ${repo}`, [`repo=${repo}`, "channel=main", `path=${path2}`, "preflight=true"], void 0);
45637
+ if (run.conclusion !== "success") throw new Error(`main serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
45638
+ };
45456
45639
  const rcRunCause = async (id) => {
45457
45640
  try {
45458
45641
  return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
@@ -45493,6 +45676,8 @@ function suiteTrainIo(root, summaryFile) {
45493
45676
  rcand: runRcand,
45494
45677
  publish,
45495
45678
  preflight: preflight2,
45679
+ publishMain,
45680
+ preflightMain,
45496
45681
  pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
45497
45682
  verifyLeadCut: async (sha) => {
45498
45683
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -45516,7 +45701,15 @@ function suiteTrainIo(root, summaryFile) {
45516
45701
  announce: async (record) => {
45517
45702
  if (!summaryFile) throw new Error("suite announcement requires --announce-summary-file; outcome was not posted");
45518
45703
  const result = await trainApplyDeps().announce?.({ repo: SUITE_REPO, tag: record.leadRcTag.replace(/-rc\.\d+$/, ""), summaryFile });
45704
+ if (result?.status === "skipped") return { skipped: result.note };
45519
45705
  if (result?.status !== "announced") throw new Error(`suite announcement unverified: ${result?.note ?? "no announcement seam"}`);
45706
+ return {};
45707
+ },
45708
+ announceSkipReason: async () => {
45709
+ const slug = slugOf(SUITE_REPO);
45710
+ const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
45711
+ if (!read.ok) throw new Error(`suite announcement: registry read failed for ${slug} (${read.error}); outcome stays ambiguous`);
45712
+ return read.project?.releaseChannel?.trim() ? null : `no releaseChannel configured for ${slug}; nothing could have been posted`;
45520
45713
  }
45521
45714
  };
45522
45715
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.57",
3
+ "version": "4.5.59",
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",