@mutmutco/cli 4.5.58 → 4.5.60

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 +413 -157
  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";
@@ -8624,6 +8653,9 @@ async function upgradeMessage(res, body) {
8624
8653
  if (res.status !== 426) return null;
8625
8654
  return upgradeRequiredError(res, body ?? await readJsonBody(res));
8626
8655
  }
8656
+ function safeServerReason(error) {
8657
+ return typeof error === "string" && error.length > 0 && error.length <= 200 && /^[\x20-\x7e]+$/.test(error) && error.includes(" ") && !/[A-Za-z0-9+/=_-]{32,}/.test(error);
8658
+ }
8627
8659
  async function fetchSecretValue(deps, key, opts, diagnose = false) {
8628
8660
  if (!isValidSecretKey(key)) return null;
8629
8661
  const repo = await targetRepo(deps, opts);
@@ -8637,8 +8669,8 @@ async function fetchSecretValue(deps, key, opts, diagnose = false) {
8637
8669
  if (!res.ok) {
8638
8670
  if (diagnose) {
8639
8671
  const body = await readJsonBody(res);
8640
- const reason = res.status === 403 ? "authority refused" : res.status === 429 ? "rate limited" : "Hub route refused";
8641
- 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}` : ""})`);
8672
+ 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";
8673
+ throw new Error(`${reason} (HTTP ${res.status}${safeServerReason(body.error) ? `: ${body.error}` : ""})`);
8642
8674
  }
8643
8675
  return null;
8644
8676
  }
@@ -9918,6 +9950,10 @@ async function secretsVerify(deps, key, opts) {
9918
9950
  }
9919
9951
  const hasProviderVerifier = Boolean(providerForSecretKey(key));
9920
9952
  const value = await fetchSecretValue(deps, key, opts);
9953
+ if (value && value !== stripTrailingCRLF(value)) {
9954
+ 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}\``);
9955
+ return false;
9956
+ }
9921
9957
  if (!hasProviderVerifier) {
9922
9958
  if (!value) {
9923
9959
  deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
@@ -10105,6 +10141,40 @@ async function secretsDeclare(deps, key, opts) {
10105
10141
  deps.log(`declared ${key} (project tier, ${slug}) \u2014 catalog metadata only; ask master (secrets request) to wire it into requiredRuntimeSecrets`);
10106
10142
  return true;
10107
10143
  }
10144
+ async function secretsUndeclare(deps, key, opts) {
10145
+ if (!DECLARE_KEY_RE.test(key.split("@")[0] ?? "")) {
10146
+ deps.err(`secrets undeclare: invalid key ${JSON.stringify(key)} \u2014 must be an env-style name (UPPER_SNAKE convention), optionally @dev|rc|main`);
10147
+ return false;
10148
+ }
10149
+ const repo = await targetRepo(deps, opts);
10150
+ const slug = await vaultSlug(deps, opts);
10151
+ let res;
10152
+ try {
10153
+ res = await deps.fetch(`${deps.apiUrl}/projects/${slug}/secrets/undeclare`, {
10154
+ method: "POST",
10155
+ headers: await deps.headers({ "content-type": "application/json" }),
10156
+ body: JSON.stringify({ repo, key }),
10157
+ signal: AbortSignal.timeout(TIMEOUT_MS)
10158
+ });
10159
+ } catch (e) {
10160
+ deps.err(`secrets undeclare: ${e.message}`);
10161
+ return false;
10162
+ }
10163
+ const body = await readJsonBody(res);
10164
+ if (!res.ok) {
10165
+ deps.err(await upgradeMessage(res, body) ?? `secrets undeclare failed: HTTP ${res.status}${errorDetail(body)}`);
10166
+ return false;
10167
+ }
10168
+ const undeclared = body?.undeclared !== false;
10169
+ if (opts.json) {
10170
+ deps.log(JSON.stringify({ ok: true, key, undeclared }));
10171
+ return true;
10172
+ }
10173
+ deps.log(
10174
+ 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`
10175
+ );
10176
+ return true;
10177
+ }
10108
10178
  function parseRailsCredentialMapping(raw, stage) {
10109
10179
  const eq = raw.indexOf("=");
10110
10180
  if (eq <= 0 || eq === raw.length - 1) return null;
@@ -14688,7 +14758,7 @@ function extractForwardRefs(markdown) {
14688
14758
  });
14689
14759
  return refs;
14690
14760
  }
14691
- function checkPins(root, readFile9, docs) {
14761
+ function checkPins(root, readFile10, docs) {
14692
14762
  const findings = [];
14693
14763
  for (const [doc, markdown] of Object.entries(docs)) {
14694
14764
  for (const pin of extractPins(markdown)) {
@@ -14696,7 +14766,7 @@ function checkPins(root, readFile9, docs) {
14696
14766
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
14697
14767
  continue;
14698
14768
  }
14699
- const source = readFile9((0, import_node_path12.join)(root, pin.file));
14769
+ const source = readFile10((0, import_node_path12.join)(root, pin.file));
14700
14770
  if (source == null) {
14701
14771
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
14702
14772
  continue;
@@ -14913,7 +14983,7 @@ function defaultTrackedFirstSegments(root, firstSegments, exec2 = import_node_ch
14913
14983
  }
14914
14984
  }
14915
14985
  function runDocRefs(root, deps = {}) {
14916
- const readFile9 = deps.readFile ?? readFileOrNull;
14986
+ const readFile10 = deps.readFile ?? readFileOrNull;
14917
14987
  const exists = deps.exists ?? import_node_fs14.existsSync;
14918
14988
  const listDocs = deps.listDocs ?? defaultListDocs;
14919
14989
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
@@ -14922,11 +14992,11 @@ function runDocRefs(root, deps = {}) {
14922
14992
  const walked = listDocs(root);
14923
14993
  const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
14924
14994
  const docs = Object.fromEntries(
14925
- walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
14995
+ walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile10((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
14926
14996
  );
14927
14997
  const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs);
14928
14998
  const findings = [
14929
- ...checkPins(root, readFile9, docs).findings,
14999
+ ...checkPins(root, readFile10, docs).findings,
14930
15000
  ...refResult.findings
14931
15001
  ];
14932
15002
  if (commandPaths == null) {
@@ -15705,22 +15775,22 @@ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
15705
15775
  ...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
15706
15776
  ].join("\n");
15707
15777
  }
15708
- function resolveSeedContent(seed, vars, readFile9) {
15709
- if (seed.source === "self") return readFile9(seed.target);
15778
+ function resolveSeedContent(seed, vars, readFile10) {
15779
+ if (seed.source === "self") return readFile10(seed.target);
15710
15780
  if (seed.source.startsWith("seed:")) {
15711
- const tmpl = readFile9(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
15781
+ const tmpl = readFile10(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
15712
15782
  return tmpl == null ? null : renderSeed(tmpl, vars);
15713
15783
  }
15714
15784
  return null;
15715
15785
  }
15716
- function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
15786
+ function resolveSeedWriteContent(seed, vars, readFile10, remoteContent) {
15717
15787
  if (!seed.managedBlock) {
15718
- return { ok: true, content: resolveSeedContent(seed, vars, readFile9), managed: seed.source === "managed-block" };
15788
+ return { ok: true, content: resolveSeedContent(seed, vars, readFile10), managed: seed.source === "managed-block" };
15719
15789
  }
15720
- const base = remoteContent ?? resolveSeedContent(seed, vars, readFile9);
15790
+ const base = remoteContent ?? resolveSeedContent(seed, vars, readFile10);
15721
15791
  if (base == null) return { ok: true, content: null, managed: true };
15722
15792
  const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
15723
- const desired = resolveSeedContent(blockSeed, vars, readFile9);
15793
+ const desired = resolveSeedContent(blockSeed, vars, readFile10);
15724
15794
  if (desired == null) return { ok: true, content: null, managed: true };
15725
15795
  const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
15726
15796
  return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
@@ -16302,6 +16372,17 @@ function isHubControlRepo(repo) {
16302
16372
  function isJervHubRepo(repo) {
16303
16373
  return repo.toLowerCase() === JERV_HUB_REPO.toLowerCase();
16304
16374
  }
16375
+ var WINDOWS_GIT_BASH_MSYSTEM = /^(?:MINGW(?:32|64)|UCRT64|CLANG(?:64|ARM64))$/u;
16376
+ function isWindowsGitBash(msystem) {
16377
+ return WINDOWS_GIT_BASH_MSYSTEM.test(msystem ?? "");
16378
+ }
16379
+ function assertGitBashOnWindowsForJervHubRelease(repo, platform2 = process.platform, env = process.env) {
16380
+ if (platform2 !== "win32" || !isJervHubRepo(repo)) return;
16381
+ if (isWindowsGitBash(env.MSYSTEM)) return;
16382
+ throw new Error(
16383
+ `${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"`
16384
+ );
16385
+ }
16305
16386
 
16306
16387
  // src/train-apply-correlation.ts
16307
16388
  var import_node_fs18 = require("node:fs");
@@ -17070,10 +17151,10 @@ var rollout_plan_default = {
17070
17151
  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)."
17071
17152
  },
17072
17153
  baseline: {
17073
- version: "4.5.58",
17074
- tag: "v4.5.58",
17075
- commit: "05d087d1fcb7",
17076
- npm: "@mutmutco/cli@4.5.58"
17154
+ version: "4.5.60",
17155
+ tag: "v4.5.60",
17156
+ commit: "18fb882893af",
17157
+ npm: "@mutmutco/cli@4.5.60"
17077
17158
  },
17078
17159
  exitCriterion: "fleet-n-of-n",
17079
17160
  hubOnlyShortcut: "forbidden",
@@ -17090,14 +17171,14 @@ var rollout_plan_default = {
17090
17171
  repo: "mutmutco/mmi-hub",
17091
17172
  role: "canary",
17092
17173
  schedule: "train",
17093
- v3Target: "v4.5.58"
17174
+ v3Target: "v4.5.60"
17094
17175
  }
17095
17176
  ],
17096
17177
  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.",
17097
17178
  rollback: {
17098
17179
  independent: true,
17099
- mechanism: "npm dist-tag latest -> 4.5.58 and redeploy the Hub Lambda from tag v4.5.58 (05d087d1fcb7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17100
- v3Target: "v4.5.58 (@mutmutco/cli@4.5.58, tag commit 05d087d1fcb7 \u2014 last known-good release carrying the repo-index v4-only contract)"
17180
+ mechanism: "npm dist-tag latest -> 4.5.60 and redeploy the Hub Lambda from tag v4.5.60 (18fb882893af); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17181
+ v3Target: "v4.5.60 (@mutmutco/cli@4.5.60, tag commit 18fb882893af \u2014 last known-good release carrying the repo-index v4-only contract)"
17101
17182
  }
17102
17183
  },
17103
17184
  {
@@ -21284,7 +21365,7 @@ function renderTenantControl(r) {
21284
21365
 
21285
21366
  // src/train-apply-phases.ts
21286
21367
  var import_node_fs24 = require("node:fs");
21287
- var import_promises3 = require("node:fs/promises");
21368
+ var import_promises4 = require("node:fs/promises");
21288
21369
  var import_node_path24 = require("node:path");
21289
21370
 
21290
21371
  // src/actions-billing-preflight.ts
@@ -24971,6 +25052,59 @@ function formatTrainDoctor(v) {
24971
25052
  return lines2.join("\n");
24972
25053
  }
24973
25054
 
25055
+ // src/release-plain-words.ts
25056
+ var import_promises3 = require("node:fs/promises");
25057
+ var PLAIN_WORDS_HEADING = "## In plain words";
25058
+ var PLAIN_WORDS_MARKER = "<!-- mmi:big -->";
25059
+ function buildPlainWordsSection(lines2, big) {
25060
+ const [title, ...notes] = lines2;
25061
+ const body = [PLAIN_WORDS_HEADING, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
25062
+ if (big) body.push(PLAIN_WORDS_MARKER);
25063
+ return body.join("\n");
25064
+ }
25065
+ function noChangePlainWordsLine(suiteTag) {
25066
+ return `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.`;
25067
+ }
25068
+ function upsertPlainWordsSection(body, section) {
25069
+ const lines2 = body.split(/\r?\n/);
25070
+ const at = lines2.findIndex((l) => l.trim() === PLAIN_WORDS_HEADING);
25071
+ let rest = body;
25072
+ if (at >= 0) {
25073
+ let end = lines2.length;
25074
+ for (let i = at + 1; i < lines2.length; i++) {
25075
+ if (lines2[i].trim().startsWith("## ")) {
25076
+ end = i;
25077
+ break;
25078
+ }
25079
+ }
25080
+ rest = [...lines2.slice(0, at), ...lines2.slice(end)].join("\n");
25081
+ }
25082
+ const trimmedRest = rest.trim();
25083
+ return trimmedRest ? `${section}
25084
+
25085
+ ${trimmedRest}` : section;
25086
+ }
25087
+ function plainWordsSummaryLines(content) {
25088
+ return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 6);
25089
+ }
25090
+ async function writePlainWordsIfSuiteMember(deps, args) {
25091
+ if (args.meta?.suite !== "mm-work") return;
25092
+ try {
25093
+ const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag ?? args.tag)];
25094
+ const section = buildPlainWordsSection(lines2, args.big === true);
25095
+ const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
25096
+ await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
25097
+ } catch {
25098
+ }
25099
+ }
25100
+ async function writePlainWordsSection(deps, options, repo, meta, tag) {
25101
+ try {
25102
+ const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
25103
+ await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, big: options.big, suiteTag: options.suiteTag });
25104
+ } catch {
25105
+ }
25106
+ }
25107
+
24974
25108
  // src/vercel-version-env.ts
24975
25109
  var SKIPPED = { status: "skipped", note: "" };
24976
25110
  function vercelVersionEnvAheadNote(key) {
@@ -25157,6 +25291,7 @@ function enforceGateBudget(deps, repo) {
25157
25291
  }
25158
25292
  }
25159
25293
  async function preflight(deps, ctx, stage, meta, lane) {
25294
+ assertGitBashOnWindowsForJervHubRelease(ctx.repo);
25160
25295
  deps.warn?.("Release: Checking release prerequisites");
25161
25296
  if (deps.suiteCut && !authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25162
25297
  throw new Error(`${ctx.repo}: suite cut context does not match registry META; no deployment or tag was changed`);
@@ -25270,7 +25405,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
25270
25405
  const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
25271
25406
  for (; ; ) {
25272
25407
  try {
25273
- await (0, import_promises3.stat)(lockPath);
25408
+ await (0, import_promises4.stat)(lockPath);
25274
25409
  } catch (e) {
25275
25410
  if (e.code === "ENOENT") return;
25276
25411
  throw foldFailureGuidance(
@@ -25306,7 +25441,7 @@ async function clearFoldChildIndexLock(deps) {
25306
25441
  "git index lock path"
25307
25442
  );
25308
25443
  try {
25309
- await (0, import_promises3.unlink)(lockPath);
25444
+ await (0, import_promises4.unlink)(lockPath);
25310
25445
  } catch (e) {
25311
25446
  if (e.code === "ENOENT") return void 0;
25312
25447
  throw e;
@@ -25905,6 +26040,7 @@ ${recovery.note}`), recoveryInput);
25905
26040
  const autoRunSince = (deps.now ?? Date.now)();
25906
26041
  const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
25907
26042
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
26043
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
25908
26044
  await recordPhase(ledger, deps, ledgerAnchors, "githubRelease", phaseEntry({ state: "complete", sha: releaseSha, runUrl: releaseUrl, note: `verified GitHub Release ${tag} against ${releaseSha.slice(0, 12)}` }), { landed: "the immutable tag, the green required-check wall, and the origin/main fast-forward" });
25909
26045
  let alignment = alignEarly ? await alignEarly() : void 0;
25910
26046
  const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha, watch });
@@ -26464,6 +26600,7 @@ ${recovery.note}`), recoveryInput);
26464
26600
  steps.push(`created the GitHub Release for ${tag}`);
26465
26601
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
26466
26602
  steps.push("verified the Release published against the tagged commit");
26603
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
26467
26604
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, runUrl: releaseUrl, note: "verified the Release published against the tagged commit" }), { strict: ledgerMode === "strict", landed: "the immutable tag, the re-proven wall, and the origin/main push" });
26468
26605
  let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
26469
26606
  let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
@@ -27201,6 +27338,45 @@ var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflow
27201
27338
  var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
27202
27339
  var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
27203
27340
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
27341
+ var DEV_STAGE_FRESH_LABEL = "tenant dev stage runs the development tip (#7536)";
27342
+ var DEV_STAGE_STALE_TOLERANCE_MS = 30 * 60 * 1e3;
27343
+ function devStageFreshnessCheck(input) {
27344
+ const { repo, deployedTag, tipSha, tipCommittedAt, nowMs } = input;
27345
+ const label = DEV_STAGE_FRESH_LABEL;
27346
+ const cannotAudit = (why) => ({ ok: true, label, detail: `cannot audit: ${why}` });
27347
+ if (!tipSha) return cannotAudit("development tip unreadable");
27348
+ if (!deployedTag) return cannotAudit("deployed dev SHA unreadable or never stamped");
27349
+ if (deployedTag.toLowerCase() === tipSha.toLowerCase()) return { ok: true, label };
27350
+ const ageMs = nowMs - Date.parse(tipCommittedAt ?? "");
27351
+ if (!Number.isFinite(ageMs)) return cannotAudit("development tip commit time unreadable");
27352
+ const minutes = Math.max(0, Math.round(ageMs / 6e4));
27353
+ const behind = `dev runs ${deployedTag.slice(0, 7)}, development is at ${tipSha.slice(0, 7)} (${minutes} min old)`;
27354
+ if (ageMs <= DEV_STAGE_STALE_TOLERANCE_MS) return { ok: true, label, detail: `deploy pending: ${behind}` };
27355
+ return {
27356
+ ok: false,
27357
+ label,
27358
+ detail: behind,
27359
+ remediation: `mmi-cli devops runtime tenant redeploy ${repo} dev --watch \u2014 then read the Hub's "dev-auto-deploy ${repo}" log lines for why the merge did not deploy`
27360
+ };
27361
+ }
27362
+ async function auditDevStageFreshness(repo, deps) {
27363
+ const facts = await deps.getDeployFacts(slugFromRepo(repo)).catch(() => null);
27364
+ const dev = facts?.stages.dev;
27365
+ if (facts && !dev?.present) return void 0;
27366
+ let tip;
27367
+ try {
27368
+ tip = await deps.client.rest("GET", `repos/${repo}/commits/development`);
27369
+ } catch {
27370
+ tip = void 0;
27371
+ }
27372
+ return devStageFreshnessCheck({
27373
+ repo,
27374
+ deployedTag: dev?.deployedTag,
27375
+ tipSha: tip?.sha,
27376
+ tipCommittedAt: tip?.commit?.committer?.date,
27377
+ nowMs: Date.now()
27378
+ });
27379
+ }
27204
27380
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
27205
27381
  var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
27206
27382
  var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
@@ -27467,15 +27643,16 @@ async function branchPresence(deps, repo, branch) {
27467
27643
  }
27468
27644
  }
27469
27645
  function collectRegistryRepos(projects) {
27470
- const seen = /* @__PURE__ */ new Set();
27646
+ const seen = /* @__PURE__ */ new Map();
27471
27647
  for (const project2 of projects) {
27472
27648
  for (const raw of project2.repos ?? []) {
27473
27649
  const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
27474
- seen.add(repo);
27650
+ const key = repo.toLowerCase();
27651
+ if (!seen.has(key)) seen.set(key, repo);
27475
27652
  }
27476
27653
  }
27477
- if (![...seen].some((r) => r.toLowerCase() === HUB_REPO4.toLowerCase())) seen.add(HUB_REPO4);
27478
- return [...seen].sort((a, b) => a.localeCompare(b));
27654
+ if (!seen.has(HUB_REPO4.toLowerCase())) seen.set(HUB_REPO4.toLowerCase(), HUB_REPO4);
27655
+ return [...seen.values()].sort((a, b) => a.localeCompare(b));
27479
27656
  }
27480
27657
  async function prTriggeredWorkflowsOnRef(deps, repo, branch, repoClass) {
27481
27658
  const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, branch, PRODUCT_GATE_PATH);
@@ -27522,7 +27699,29 @@ async function filterPullRequestTriggered(deps, repo, branch, paths) {
27522
27699
  }
27523
27700
  return kept;
27524
27701
  }
27525
- async function auditRepoCi(repo, deps) {
27702
+ function trackUnauditableReads(deps) {
27703
+ let reason;
27704
+ const record = (e) => {
27705
+ if (reason) return;
27706
+ const err = e;
27707
+ if (err?.rateLimited) reason = `GitHub App rate-limited: ${err.message ?? "rate limit exceeded"}`;
27708
+ else if (err?.status === 403 || err?.status === 429) reason = `HTTP ${err.status}${err.message ? `: ${err.message}` : ""}`;
27709
+ };
27710
+ const client = {
27711
+ ...deps.client,
27712
+ rest: async (method, path2, init) => {
27713
+ try {
27714
+ return await deps.client.rest(method, path2, init);
27715
+ } catch (e) {
27716
+ record(e);
27717
+ throw e;
27718
+ }
27719
+ }
27720
+ };
27721
+ return { deps: { ...deps, client }, reason: () => reason };
27722
+ }
27723
+ async function auditRepoCi(repo, callerDeps) {
27724
+ const { deps, reason: unauditedReason } = trackUnauditableReads(callerDeps);
27526
27725
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
27527
27726
  const repoClass = classifyRepo(repo, meta);
27528
27727
  const checks = [];
@@ -27817,6 +28016,14 @@ async function auditRepoCi(repo, deps) {
27817
28016
  const workflows = await getBranchWorkflows();
27818
28017
  if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
27819
28018
  }
28019
+ if (meta?.deployModel === "tenant-container" && deps.getDeployFacts) {
28020
+ const fresh = await auditDevStageFreshness(repo, deps);
28021
+ if (fresh) checks.push(fresh);
28022
+ }
28023
+ const failure = unauditedReason();
28024
+ if (failure) {
28025
+ return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
28026
+ }
27820
28027
  const ok = checks.every((c) => c.ok);
27821
28028
  return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
27822
28029
  }
@@ -27858,8 +28065,8 @@ function renderCiAuditMarkdown(report) {
27858
28065
  "|------|-------|--------|----|---------|"
27859
28066
  ];
27860
28067
  for (const r of report.repos) {
27861
- const gap = r.checks.find((c) => !c.ok);
27862
- lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
28068
+ const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
28069
+ lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap} |`);
27863
28070
  }
27864
28071
  return lines2.join("\n");
27865
28072
  }
@@ -27869,8 +28076,13 @@ function renderCiAuditText(report) {
27869
28076
  renderCiAuditScopeLine(report)
27870
28077
  ];
27871
28078
  for (const r of report.repos) {
28079
+ const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
27872
28080
  lines2.push(`
27873
- ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
28081
+ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
28082
+ if (r.unauditedReason) {
28083
+ lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
28084
+ continue;
28085
+ }
27874
28086
  for (const c of r.checks) {
27875
28087
  lines2.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
27876
28088
  }
@@ -30258,7 +30470,7 @@ init_hub_auth();
30258
30470
  init_github_client();
30259
30471
 
30260
30472
  // src/project-set.ts
30261
- var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv"];
30473
+ var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "suiteHold"];
30262
30474
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
30263
30475
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
30264
30476
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -30758,6 +30970,7 @@ var SETTABLE_VAR_KEYS = [
30758
30970
  "branch",
30759
30971
  "vaultPath",
30760
30972
  "suite",
30973
+ "suiteHold",
30761
30974
  "repos",
30762
30975
  "oauth",
30763
30976
  "publishRequired",
@@ -30792,6 +31005,7 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
30792
31005
  var SETTABLE_VAR_HINTS = {
30793
31006
  projectNumber: "numeric",
30794
31007
  suite: "lowercase kebab slug, e.g. mm-work",
31008
+ suiteHold: 'JSON {reason, since?} \u2014 cuts the member out of the MM-Work suite train (e.g. {"reason":"frozen pending go-live proof"}); clear with --unset suiteHold (#7547)',
30795
31009
  publishRequired: "true|false",
30796
31010
  publishDir: "relative subpath, e.g. packages/ui",
30797
31011
  releaseChannel: "Slack channel id, e.g. C0BE0BRGAT0",
@@ -30870,6 +31084,20 @@ function clearSecretsConfirmationError(unsets, existing, confirmed) {
30870
31084
  if (confirmed) return null;
30871
31085
  return `org project set: --unset secrets clears the WHOLE catalog for ${existing?.slug ?? "this project"} (${count} declared entr${count === 1 ? "y" : "ies"} would be removed). To drop ONE entry use --unset-secret <KEY>; re-run with --confirm-clear-secrets to wipe all ${count}.`;
30872
31086
  }
31087
+ function parseSuiteHoldVar(raw) {
31088
+ let parsed;
31089
+ try {
31090
+ parsed = JSON.parse(raw);
31091
+ } catch {
31092
+ throw new Error('org project set: suiteHold must be a JSON object {"reason":"...","since":"..."}');
31093
+ }
31094
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error('org project set: suiteHold must be a JSON object {"reason":"...","since":"..."}');
31095
+ const hold = parsed;
31096
+ if (typeof hold.reason !== "string" || !hold.reason.trim()) throw new Error("org project set: suiteHold.reason must be a non-empty string naming the freeze");
31097
+ if (hold.since !== void 0 && (typeof hold.since !== "string" || !hold.since.trim())) throw new Error('org project set: suiteHold.since must be a non-empty string (e.g. "2026-09-28")');
31098
+ if (Object.keys(hold).some((k) => k !== "reason" && k !== "since")) throw new Error("org project set: suiteHold accepts only reason and since");
31099
+ return { reason: hold.reason, ...typeof hold.since === "string" ? { since: hold.since } : {} };
31100
+ }
30873
31101
  function buildProjectSetPatch(input) {
30874
31102
  const patch = {};
30875
31103
  if (input.class) {
@@ -30947,6 +31175,8 @@ function buildProjectSetPatch(input) {
30947
31175
  } else if (key === "suite") {
30948
31176
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(raw)) throw new Error("org project set: suite must be a lowercase kebab slug (e.g. mm-work)");
30949
31177
  patch[key] = raw;
31178
+ } else if (key === "suiteHold") {
31179
+ patch[key] = parseSuiteHoldVar(raw);
30950
31180
  } else if (key === "releaseChannel") {
30951
31181
  patch[key] = parseReleaseChannelVar(raw);
30952
31182
  } else if (key === "releaseLanguage") {
@@ -31356,7 +31586,7 @@ async function readSecretStdin() {
31356
31586
  }
31357
31587
  const chunks = [];
31358
31588
  for await (const chunk of process.stdin) chunks.push(chunk);
31359
- return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
31589
+ return stripTrailingCRLF(Buffer.concat(chunks).toString("utf8"));
31360
31590
  }
31361
31591
  function makeSecretsDeps(cfg) {
31362
31592
  return {
@@ -31508,6 +31738,10 @@ function registerSecretsCommands(program3) {
31508
31738
  });
31509
31739
  if (!ok) process.exitCode = 1;
31510
31740
  }));
31741
+ 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) => {
31742
+ const ok = await secretsUndeclare(d, key, { repo: o.repo, json: o.json });
31743
+ if (!ok) process.exitCode = 1;
31744
+ }));
31511
31745
  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) => {
31512
31746
  const ok = await secretsVerify(d, key, o);
31513
31747
  if (!ok) process.exitCode = 1;
@@ -33370,9 +33604,13 @@ function applyWaivers(checks, waivers) {
33370
33604
  const set = new Set(waivers);
33371
33605
  return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
33372
33606
  }
33607
+ var laneReasons = (doctor) => doctor.findings.filter((f) => (f.severity === "blocker" || f.code === "origin-unverified") && f.code !== SUITE_GUARD_CODE);
33608
+ function onlySuiteLeadOwned(doctor) {
33609
+ return !doctor.ready && laneReasons(doctor).length === 0 && doctor.findings.some((f) => f.code === SUITE_GUARD_CODE);
33610
+ }
33373
33611
  function laneNotEvaluatedFromThisBranch(doctor) {
33374
33612
  if (doctor.ready) return void 0;
33375
- const reasons = doctor.findings.filter((f) => f.severity === "blocker" || f.code === "origin-unverified");
33613
+ const reasons = laneReasons(doctor);
33376
33614
  const only = reasons.length === 1 ? reasons[0] : void 0;
33377
33615
  return only?.code === "branch-mismatch" ? only.title : void 0;
33378
33616
  }
@@ -33383,7 +33621,7 @@ function attachTrainDoctorVerdicts(report, trainDoctors) {
33383
33621
  });
33384
33622
  return {
33385
33623
  ...report,
33386
- ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0),
33624
+ ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0 || onlySuiteLeadOwned(doctor)),
33387
33625
  ...marked.length ? { trainDoctors: marked } : {}
33388
33626
  };
33389
33627
  }
@@ -33397,6 +33635,8 @@ function renderBootstrapVerifyReport(report) {
33397
33635
  lines2.push("", formatTrainDoctor(doctor));
33398
33636
  if (doctor.notEvaluatedFromThisBranch) {
33399
33637
  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.`);
33638
+ } else if (onlySuiteLeadOwned(doctor)) {
33639
+ 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).");
33400
33640
  }
33401
33641
  }
33402
33642
  return lines2.join("\n");
@@ -33863,7 +34103,7 @@ function registerBootstrapCommands(program3) {
33863
34103
  }
33864
34104
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
33865
34105
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
33866
- const readFile9 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
34106
+ const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
33867
34107
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
33868
34108
  const putSeed = async (target, content, ref, sha) => {
33869
34109
  const tmp = (0, import_node_path28.join)((0, import_node_os17.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
@@ -34019,7 +34259,7 @@ function registerBootstrapCommands(program3) {
34019
34259
  registryMeta?.requiredCheckBranches,
34020
34260
  registryMeta?.requiredChecks
34021
34261
  );
34022
- const rendered = resolveSeedContent(gateSeed, gateVars, readFile9);
34262
+ const rendered = resolveSeedContent(gateSeed, gateVars, readFile10);
34023
34263
  if (rendered === null || missingPlaceholders(rendered).length) return failGraceful("bootstrap apply: registry gate template is unreadable or has unfilled placeholders");
34024
34264
  const base = (await readTargetedSeedFiles(repo, scopedPlan, onlyTarget, scopedPlan.baseSha, controlClient)).base?.content ?? null;
34025
34265
  configGateAction = planGateFromConfig(base, rendered);
@@ -34123,7 +34363,7 @@ function registerBootstrapCommands(program3) {
34123
34363
  ...vars,
34124
34364
  ...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
34125
34365
  ...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
34126
- } : vars, readFile9, preservedContent);
34366
+ } : vars, readFile10, preservedContent);
34127
34367
  if (!writeContent.ok) {
34128
34368
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
34129
34369
  }
@@ -34278,7 +34518,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34278
34518
  }
34279
34519
  const rulesetSeed = repo.toLowerCase() === "mutmutco/mmi-hub" ? void 0 : manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
34280
34520
  if (rulesetSeed) {
34281
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile9);
34521
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile10);
34282
34522
  if (rulesetContent) {
34283
34523
  try {
34284
34524
  const client = controlClient;
@@ -35139,6 +35379,16 @@ function pathOf(url) {
35139
35379
  }
35140
35380
  }
35141
35381
  var bundlesOf = (members) => Object.fromEntries(members.map((m) => [m.id, m.bundles]));
35382
+ async function holdSplit(deps, modules) {
35383
+ const pairs = await Promise.all(modules.map(async (m) => [m, await deps.hold(m)]));
35384
+ return {
35385
+ active: pairs.filter(([, hold]) => !hold).map(([m]) => m),
35386
+ held: pairs.flatMap(([m, hold]) => hold ? [{ id: m.id, repo: m.repo, reason: hold.reason, ...hold.since ? { since: hold.since } : {} }] : [])
35387
+ };
35388
+ }
35389
+ function heldMatches(recorded, now) {
35390
+ return recorded.length === now.length && now.every((h) => recorded.some((r) => r.id === h.id && r.repo === h.repo));
35391
+ }
35142
35392
  function suiteReleaseTag(rc) {
35143
35393
  if (!RC_TAG.test(rc)) throw new Error(`invalid rc tag ${rc}`);
35144
35394
  return rc.replace(/-rc\.\d+$/, "");
@@ -35157,7 +35407,7 @@ function readSuiteCut(path2) {
35157
35407
  } catch {
35158
35408
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
35159
35409
  }
35160
- 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)))
35410
+ 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.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || 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)))
35161
35411
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
35162
35412
  return record;
35163
35413
  }
@@ -35212,8 +35462,10 @@ async function suiteRcand(deps, opts) {
35212
35462
  if (!opts.ownerGo) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
35213
35463
  const members = suiteMembers(await deps.registry());
35214
35464
  const modules = members.filter((m) => m.repo !== SUITE_REPO);
35465
+ const { active, held } = await holdSplit(deps, modules);
35466
+ const heldById = new Map(held.map((h) => [h.id, h]));
35215
35467
  const plan = [
35216
- ...modules.map((m) => `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
35468
+ ...modules.map((m) => heldById.has(m.id) ? `${m.repo}: HELD: ${heldById.get(m.id).reason}` : `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
35217
35469
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35218
35470
  `${SUITE_REPO}: rcand`
35219
35471
  ];
@@ -35233,7 +35485,7 @@ async function suiteRcand(deps, opts) {
35233
35485
  const [leadDevelopment, leadPreviousRc, snapshots] = await Promise.all([
35234
35486
  deps.head(SUITE_REPO, "development"),
35235
35487
  deps.head(SUITE_REPO, "rc"),
35236
- Promise.all(modules.map(async (m) => {
35488
+ Promise.all(active.map(async (m) => {
35237
35489
  const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
35238
35490
  const previousRc = rcHead ?? await deps.head(m.repo, "main");
35239
35491
  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`);
@@ -35241,12 +35493,14 @@ async function suiteRcand(deps, opts) {
35241
35493
  }))
35242
35494
  ]);
35243
35495
  if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
35244
- const created = { schema: SCHEMA, lead: SUITE_REPO, leadDevelopment, leadPreviousRc, members: snapshots };
35496
+ const created = { schema: SCHEMA, lead: SUITE_REPO, leadDevelopment, leadPreviousRc, members: snapshots, ...held.length ? { heldMembers: held } : {} };
35245
35497
  writeSuiteCut(deps.recordPath, created);
35246
35498
  record = created;
35247
35499
  }
35248
35500
  if (!record) throw new Error("suite cut record unavailable after initialization");
35249
- if (record.members.length !== modules.length || modules.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id)))
35501
+ if (!heldMatches(record.heldMembers ?? [], held))
35502
+ throw new Error("suite hold changed since cut began; finish or abandon this cut, then re-cut with the new holds");
35503
+ if (record.members.length !== active.length || active.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id)))
35250
35504
  throw new Error("suite roster changed since cut began; refuse to omit or add a member");
35251
35505
  const quiet = [];
35252
35506
  for (const member of record.members) {
@@ -35316,7 +35570,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35316
35570
  }
35317
35571
  const pins = suitePins(record, bundlesOf(modules));
35318
35572
  await deps.verifyLeadCut(record.leadDevelopment);
35319
- const pr = await deps.pin({ cut: record.leadDevelopment, pins });
35573
+ const pr = await deps.pin({ cut: record.leadDevelopment, pins, heldIds: record.heldMembers?.map((h) => h.id) });
35320
35574
  await deps.verifyLeadCut(record.leadDevelopment);
35321
35575
  if (record.pinPr && record.pinPr !== pr.pr) throw new Error(`pin PR changed from ${record.pinPr} to ${pr.pr}; stop`);
35322
35576
  record.pinPr = pr.pr;
@@ -35333,12 +35587,14 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35333
35587
  }
35334
35588
  return { plan, record };
35335
35589
  }
35336
- async function suiteRelease(deps, ownerGo) {
35590
+ async function suiteRelease(deps, ownerGo, big = false) {
35337
35591
  if (!ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
35338
35592
  const record = readSuiteCut(deps.recordPath);
35339
35593
  if (!record?.pinMerged || !record.leadRcTag) throw new Error("no completed suite rc cut and merged pin PR; release refused");
35340
35594
  const members = suiteMembers(await deps.registry()).filter((m) => m.repo !== SUITE_REPO);
35341
- if (members.length !== record.members.length || members.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id))) throw new Error("suite roster changed after the cut; release refused");
35595
+ const { active, held } = await holdSplit(deps, members);
35596
+ if (!heldMatches(record.heldMembers ?? [], held)) throw new Error("suite hold changed after the cut; release refused");
35597
+ if (active.length !== record.members.length || active.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id))) throw new Error("suite roster changed after the cut; release refused");
35342
35598
  await deps.verifyPins(suitePins(record, bundlesOf(members)), record.leadRcTag);
35343
35599
  for (const member of record.members) {
35344
35600
  if (member.carried) {
@@ -35384,7 +35640,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
35384
35640
  console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35385
35641
  } else {
35386
35642
  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)`);
35387
- await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35643
+ await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha, { suiteTag: suiteReleaseTag(record.leadRcTag) });
35388
35644
  }
35389
35645
  member.released = true;
35390
35646
  writeSuiteCut(deps.recordPath, record);
@@ -35396,7 +35652,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
35396
35652
  }
35397
35653
  }
35398
35654
  if (!record.leadReleased) {
35399
- await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
35655
+ await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha, { big });
35400
35656
  record.leadReleased = true;
35401
35657
  writeSuiteCut(deps.recordPath, record);
35402
35658
  }
@@ -35424,14 +35680,14 @@ var import_node_path43 = require("node:path");
35424
35680
  init_cli_shared();
35425
35681
 
35426
35682
  // src/command-register-train-operations.ts
35427
- var import_promises8 = require("node:fs/promises");
35683
+ var import_promises9 = require("node:fs/promises");
35428
35684
  init_clean_exit();
35429
35685
  init_cli_shared();
35430
35686
  init_client_version();
35431
35687
  init_house_map();
35432
35688
 
35433
35689
  // src/hotfix-apply.ts
35434
- var import_promises4 = require("node:fs/promises");
35690
+ var import_promises5 = require("node:fs/promises");
35435
35691
 
35436
35692
  // src/release-announce.ts
35437
35693
  var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
@@ -36400,7 +36656,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
36400
36656
  }
36401
36657
  let lines2;
36402
36658
  try {
36403
- lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises4.readFile)(p, "utf8")))(options.announceSummaryFile));
36659
+ lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises5.readFile)(p, "utf8")))(options.announceSummaryFile));
36404
36660
  } catch (e) {
36405
36661
  throw new Error(`could not read --announce-summary-file ${options.announceSummaryFile}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`);
36406
36662
  }
@@ -36451,6 +36707,7 @@ ${decision.note}`);
36451
36707
  await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", "main", "--generate-notes", "--latest"]);
36452
36708
  releaseNote = `Release ${tag} created (target main)`;
36453
36709
  }
36710
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
36454
36711
  await recordHotfixPhase(ledger, anchors, "githubRelease", { state: "complete", sha: mergedSha, note: releaseNote }, "the immutable tag, the green wall, and origin/main");
36455
36712
  const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha: mergedSha, watch: true });
36456
36713
  const runs = [];
@@ -37775,8 +38032,8 @@ function annotateChangeMeaning(changed, policy, read) {
37775
38032
  function isMeaningfulRow(file) {
37776
38033
  return file.meaningful !== false;
37777
38034
  }
37778
- function loadPolicy(root, readFile9 = readFileOrNull2) {
37779
- const raw = readFile9((0, import_node_path30.join)(root, POLICY_FILE));
38035
+ function loadPolicy(root, readFile10 = readFileOrNull2) {
38036
+ const raw = readFile10((0, import_node_path30.join)(root, POLICY_FILE));
37780
38037
  if (raw == null) return { mandatory: [], declared: false };
37781
38038
  return parsePolicy(raw, POLICY_FILE);
37782
38039
  }
@@ -38724,12 +38981,16 @@ function releaseProgressVerdict(status, subject = "Release") {
38724
38981
  }
38725
38982
 
38726
38983
  // src/suite-pin.ts
38727
- var import_promises5 = require("node:fs/promises");
38984
+ var import_promises6 = require("node:fs/promises");
38728
38985
  var import_node_path34 = require("node:path");
38729
38986
  async function openSuiteCutPinPr(deps, input) {
38730
38987
  if (!/^[a-f0-9]{40}$/.test(input.cut) || !input.pins.length) throw new Error("suite pin PR needs an anchored cut and at least one module");
38731
38988
  const ids = input.pins.map((p) => p.module);
38732
38989
  if (new Set(ids).size !== ids.length) throw new Error("suite pin PR contains a duplicate module");
38990
+ const heldIds = input.heldIds ?? [];
38991
+ if (new Set(heldIds).size !== heldIds.length) throw new Error("suite pin PR contains a duplicate held member");
38992
+ const covered = [...ids, ...heldIds];
38993
+ if (new Set(covered).size !== covered.length) throw new Error("suite pin PR holds a member it also pins");
38733
38994
  const branch = `suite-pin/cut-${input.cut.slice(0, 16)}`;
38734
38995
  const registry2 = async (ref = "development") => {
38735
38996
  const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`]));
@@ -38740,7 +39001,7 @@ async function openSuiteCutPinPr(deps, input) {
38740
39001
  try {
38741
39002
  const rows = await registry2();
38742
39003
  const members = suiteMembers(rows).filter((m) => m.repo !== SUITE_REPO);
38743
- if (members.length !== ids.length || members.some((m) => !ids.includes(m.id))) throw new Error("cut pins do not match the complete MM-Work roster");
39004
+ if (members.length !== covered.length || members.some((m) => !covered.includes(m.id))) throw new Error("cut pins do not match the complete MM-Work roster");
38744
39005
  for (const pin of input.pins) {
38745
39006
  for (const url of [pin.rc, pin.main]) {
38746
39007
  const parsed = new URL(url);
@@ -38762,8 +39023,8 @@ async function openSuiteCutPinPr(deps, input) {
38762
39023
  });
38763
39024
  if (prs.length && matches(await registry2(branch))) return { status: "pending", pr: prs[0].url };
38764
39025
  if (!prs.length && matches(rows)) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
38765
- await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
38766
- scratch = await (0, import_promises5.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
39026
+ await (0, import_promises6.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
39027
+ scratch = await (0, import_promises6.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
38767
39028
  const run = (file, args) => deps.run(file, args, scratch);
38768
39029
  await run("git", ["clone", "--quiet", "--branch", prs.length ? branch : "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38769
39030
  if (prs.length) {
@@ -38776,7 +39037,7 @@ async function openSuiteCutPinPr(deps, input) {
38776
39037
  }
38777
39038
  for (const stage of ["rc", "main"]) {
38778
39039
  const path2 = (0, import_node_path34.join)(scratch, `${stage}-pins.json`);
38779
- await (0, import_promises5.writeFile)(path2, JSON.stringify(input.pins.map((p) => ({ module: p.module, url: p[stage] }))));
39040
+ await (0, import_promises6.writeFile)(path2, JSON.stringify(input.pins.map((p) => ({ module: p.module, url: p[stage] }))));
38780
39041
  await run("node", ["scripts/pin-modules.mjs", "--stage", stage, "--pins", path2]);
38781
39042
  }
38782
39043
  await run("git", ["add", "seed/modules.json"]);
@@ -38806,7 +39067,7 @@ async function openSuiteCutPinPr(deps, input) {
38806
39067
  } catch (e) {
38807
39068
  throw new Error(`suite cut pin PR FAILED: ${e.message}; merge/verify the single ${SUITE_REPO} development pin PR, then rerun the cut (never recut member tags)`);
38808
39069
  } finally {
38809
- if (scratch) await (0, import_promises5.rm)(scratch, { recursive: true, force: true });
39070
+ if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
38810
39071
  }
38811
39072
  }
38812
39073
  async function openSuitePinPr(deps, input) {
@@ -38825,8 +39086,8 @@ async function openSuitePinPr(deps, input) {
38825
39086
  url.searchParams.set("v", tag);
38826
39087
  const pin = url.toString();
38827
39088
  const branch = `suite-pin/${stage}-${member.id}-${tag}`;
38828
- await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
38829
- scratch = await (0, import_promises5.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
39089
+ await (0, import_promises6.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
39090
+ scratch = await (0, import_promises6.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
38830
39091
  const run = (file, args) => deps.run(file, args, scratch);
38831
39092
  await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38832
39093
  const existing = JSON.parse(await run("gh", ["pr", "list", "--repo", SUITE_REPO, "--head", branch, "--state", "all", "--json", "url,state"]));
@@ -38864,14 +39125,14 @@ async function openSuitePinPr(deps, input) {
38864
39125
  } catch (e) {
38865
39126
  throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
38866
39127
  } finally {
38867
- if (scratch) await (0, import_promises5.rm)(scratch, { recursive: true, force: true });
39128
+ if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
38868
39129
  }
38869
39130
  }
38870
39131
 
38871
39132
  // src/command-register-collaboration.ts
38872
39133
  var import_node_child_process17 = require("node:child_process");
38873
39134
  var import_node_fs42 = require("node:fs");
38874
- var import_promises7 = require("node:fs/promises");
39135
+ var import_promises8 = require("node:fs/promises");
38875
39136
  init_clean_exit();
38876
39137
  init_cli_shared();
38877
39138
  init_error_codes();
@@ -40144,7 +40405,7 @@ function postMergeReconWarnings(input) {
40144
40405
  }
40145
40406
 
40146
40407
  // src/pr-commands.ts
40147
- var import_promises6 = require("node:fs/promises");
40408
+ var import_promises7 = require("node:fs/promises");
40148
40409
  init_cli_shared();
40149
40410
  init_error_codes();
40150
40411
  var GC_GH_TIMEOUT_MS2 = 2e4;
@@ -40298,10 +40559,10 @@ function registerPrLifecycleCommands(program3) {
40298
40559
  let body;
40299
40560
  try {
40300
40561
  if (o.title || o.titleFile) {
40301
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
40562
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
40302
40563
  }
40303
40564
  if (o.body || o.bodyFile) {
40304
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
40565
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
40305
40566
  }
40306
40567
  } catch (e) {
40307
40568
  return fail(`pr edit: ${e.message}`);
@@ -40331,7 +40592,7 @@ function registerPrLifecycleCommands(program3) {
40331
40592
  }
40332
40593
  let body;
40333
40594
  try {
40334
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
40595
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
40335
40596
  } catch (e) {
40336
40597
  return fail(`pr comment: ${e.message}`);
40337
40598
  }
@@ -42112,6 +42373,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
42112
42373
  try {
42113
42374
  entries = (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true });
42114
42375
  } catch (e) {
42376
+ if (e.code === "ENOENT") continue;
42115
42377
  return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
42116
42378
  }
42117
42379
  for (const entry of entries) {
@@ -42691,34 +42953,6 @@ function renderResidueSweepLines(report) {
42691
42953
  return lines2;
42692
42954
  }
42693
42955
 
42694
- // src/pr-deploy-dev.ts
42695
- function slugOfRepo(repo) {
42696
- return repo.split("/")[1]?.toLowerCase() ?? "";
42697
- }
42698
- async function planDevDeployOnDevelopmentMerge(repo, baseRef, deps) {
42699
- if (baseRef !== "development") return { applicable: false, reason: "not-development-base" };
42700
- const slug = slugOfRepo(repo);
42701
- if (!slug) return { applicable: false, reason: "unreadable" };
42702
- let project2;
42703
- try {
42704
- project2 = await fetchProjectBySlug(slug, deps);
42705
- } catch {
42706
- return { applicable: false, reason: "unreadable" };
42707
- }
42708
- const model = project2?.deployModel;
42709
- if (!model || !isCentralDispatchModel(model)) return { applicable: false, reason: "not-tenant" };
42710
- return {
42711
- applicable: true,
42712
- // The issue's fallback ask, verbatim-shaped: the gap must be visible and actionable at the moment it opens.
42713
- manualPointer: `dev not redeployed \u2014 run \`mmi-cli devops runtime tenant redeploy ${repo} dev\``
42714
- };
42715
- }
42716
- async function dispatchDevDeploy(repo, deps) {
42717
- const res = await tenantDeploy({ repo, stage: "dev" }, deps);
42718
- if (res.ok) return { ok: true };
42719
- return { ok: false, detail: res.body?.error ?? res.error ?? `HTTP ${res.status}` };
42720
- }
42721
-
42722
42956
  // src/command-register-collaboration.ts
42723
42957
  var DEFAULT_PRIORITY = "medium";
42724
42958
  function argvWantsJson2() {
@@ -42916,7 +43150,7 @@ function registerCollaborationCommands(program3) {
42916
43150
  try {
42917
43151
  title = await resolveIssueTitle(
42918
43152
  { title: opts.title, titleFile: opts.titleFile },
42919
- { readFile: import_promises7.readFile, readStdin }
43153
+ { readFile: import_promises8.readFile, readStdin }
42920
43154
  );
42921
43155
  } catch (e) {
42922
43156
  return fail(
@@ -42970,8 +43204,8 @@ function registerCollaborationCommands(program3) {
42970
43204
  let surfaceInferred;
42971
43205
  try {
42972
43206
  issueType = resolveCreateType(o.type, "issue create", o.label);
42973
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
42974
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43207
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
43208
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
42975
43209
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
42976
43210
  priority = resolveCreatePriority(o.priority, "issue create");
42977
43211
  extraLabels = [...o.label ?? []];
@@ -43247,7 +43481,7 @@ function registerCollaborationCommands(program3) {
43247
43481
  }
43248
43482
  let body;
43249
43483
  try {
43250
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43484
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
43251
43485
  } catch (e) {
43252
43486
  return fail(`issue comment: ${e.message}`);
43253
43487
  }
@@ -43307,8 +43541,8 @@ ${list}`);
43307
43541
  let title;
43308
43542
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
43309
43543
  try {
43310
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43311
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43544
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
43545
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
43312
43546
  priority = resolveCreatePriority(o.priority, "report");
43313
43547
  if (!ISSUE_TYPES.includes(o.type)) {
43314
43548
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -43362,8 +43596,8 @@ ${list}`);
43362
43596
  let args;
43363
43597
  try {
43364
43598
  skill = assertSkillName(o.skill);
43365
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43366
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43599
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
43600
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
43367
43601
  title = buildSkillLessonTitle(skill, rawTitle);
43368
43602
  priority = resolveCreatePriority(o.priority, "skill-lesson");
43369
43603
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -43424,8 +43658,8 @@ ${list}`);
43424
43658
  let body;
43425
43659
  let title;
43426
43660
  try {
43427
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43428
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43661
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
43662
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
43429
43663
  } catch (e) {
43430
43664
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
43431
43665
  }
@@ -43930,7 +44164,7 @@ ${list}`);
43930
44164
  console.error(line);
43931
44165
  }
43932
44166
  }
43933
- const credential = await pollToken().catch(() => void 0) ? "app_installation" : "user";
44167
+ const credential = "user";
43934
44168
  if (o.json) printLine(JSON.stringify({ ...result, credential }));
43935
44169
  else printLine(
43936
44170
  `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
@@ -43960,8 +44194,6 @@ ${list}`);
43960
44194
  const baseRef = prMeta.base;
43961
44195
  const headRefOid = (prMeta.oid ?? "").trim() || void 0;
43962
44196
  if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
43963
- const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
43964
- const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
43965
44197
  const closingGuardInput = await withAlreadyClosedCommitTargets(
43966
44198
  await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
43967
44199
  async (n) => {
@@ -44191,7 +44423,6 @@ ${list}`);
44191
44423
  const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
44192
44424
  console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
44193
44425
  console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
44194
- if (devDeployPlan.applicable) console.warn(`pr merge: ${devDeployPlan.manualPointer} once the merge lands (auto-merge does not redeploy the dev stage).`);
44195
44426
  if (upgradedToAuto && !o.auto) {
44196
44427
  console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
44197
44428
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
@@ -44301,8 +44532,6 @@ ${list}`);
44301
44532
  } else if (repoForPostCleanup) {
44302
44533
  clearPostMergeReconRecovery(process.cwd(), repoForPostCleanup, number);
44303
44534
  }
44304
- const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
44305
- const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
44306
44535
  let observedMethod;
44307
44536
  if (remoteNotAttemptedReason === "pr-already-merged" && repoForPostCleanup) {
44308
44537
  try {
@@ -44337,14 +44566,10 @@ ${list}`);
44337
44566
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
44338
44567
  boardAdvanceStatus: boardAdvance.status,
44339
44568
  ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
44340
- ...recovery ? { postMergeReconRecovery: recovery } : {},
44341
- ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
44569
+ ...recovery ? { postMergeReconRecovery: recovery } : {}
44342
44570
  }));
44343
44571
  for (const line of renderPrMergeCleanupLines(localCleanup)) console.warn(line);
44344
44572
  for (const line of renderResidueSweepLines(residueSweep)) console.warn(line);
44345
- if (devDeploy && !devDeploy.ok) {
44346
- console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
44347
- }
44348
44573
  for (const line of postMergeReconWarnings({
44349
44574
  context: "pr merge",
44350
44575
  boardAdvance,
@@ -44408,7 +44633,7 @@ function trainGitArgs(args) {
44408
44633
  async function announceSummaryRefusal(path2, isHub = false) {
44409
44634
  let lines2;
44410
44635
  try {
44411
- lines2 = summaryFileLines(await (0, import_promises8.readFile)(path2, "utf8"));
44636
+ lines2 = summaryFileLines(await (0, import_promises9.readFile)(path2, "utf8"));
44412
44637
  } catch (e) {
44413
44638
  return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
44414
44639
  }
@@ -44445,9 +44670,8 @@ function trainApplyDeps() {
44445
44670
  trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
44446
44671
  run: async (file, args) => {
44447
44672
  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;
44448
- const watchToken = file === "gh" && args[0] === "run" && args[1] === "watch" ? await pollToken() : void 0;
44449
44673
  try {
44450
- 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;
44674
+ return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, file === "git" ? trainGitArgs(args) : args, { timeout })).stdout;
44451
44675
  } catch (e) {
44452
44676
  throw surfaceTrainSubprocessFailure(e, file, args);
44453
44677
  }
@@ -44517,8 +44741,8 @@ function trainApplyDeps() {
44517
44741
  }
44518
44742
  return body.status;
44519
44743
  },
44520
- readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
44521
- removeFile: (path2) => (0, import_promises8.unlink)(path2),
44744
+ readFile: (path2) => (0, import_promises9.readFile)(path2, "utf8"),
44745
+ removeFile: (path2) => (0, import_promises9.unlink)(path2),
44522
44746
  // #6521/#6523: the project's own release channel AND releaseLanguage, read from registry META in ONE
44523
44747
  // checked read. Same shape as fetchEdgeDomains below: a FAILED read refuses by name so the announce
44524
44748
  // note reports a failed read, never a project that configured nothing.
@@ -44846,7 +45070,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44846
45070
  { flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
44847
45071
  { flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553); combined with --supersede-publish it closes BOTH content-caused legs in one approved closure (#6752)" },
44848
45072
  { flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742); combined with --supersede-deploy it closes BOTH content-caused legs in one approved closure (#6752)" },
44849
- { flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
45073
+ { flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" },
45074
+ { flags: "--big", description: "marks this release big for the mm-work suite `## In plain words` section even though its patch tag alone would not read as one (#7510); automatic for a minor/major bump \u2014 this is only for a patch a project-admin wants surfaced as big" }
44850
45075
  ];
44851
45076
  for (const f of RELEASE_ONLY_FLAGS) {
44852
45077
  if (commandName === "release") {
@@ -44874,6 +45099,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44874
45099
  if (o.announceSummaryFile && commandName !== "release") {
44875
45100
  return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no release announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
44876
45101
  }
45102
+ if (o.big && commandName !== "release") {
45103
+ return fail(`${commandName}: --big applies only to release \u2014 it marks the mm-work suite plain-words section, which only a Release carries. Run: mmi-cli devops release --big`);
45104
+ }
44877
45105
  if (o.abort && commandName !== "release") {
44878
45106
  return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished release tag. Run: mmi-cli devops release --abort --apply`);
44879
45107
  }
@@ -44916,7 +45144,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44916
45144
  if (o.abort) return fail("release: --supersede-deploy --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
44917
45145
  if (o.retryPublish) return fail("release: --supersede-deploy --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
44918
45146
  if (!o.apply) return fail("release: --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
44919
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45147
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
44920
45148
  return fail("release: --supersede-deploy --supersede-publish accepts only --apply, --repo, --json and --out; it closes ledger legs and dispatches nothing");
44921
45149
  }
44922
45150
  if (o.repo) {
@@ -44933,7 +45161,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44933
45161
  if (o.retryPublish) {
44934
45162
  if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
44935
45163
  if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
44936
- if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo, --json and --out");
45164
+ if (o.announceSummaryFile || o.ack || o.dev || o.big) return fail("release: --retry-publish accepts only --apply, --watch, --repo, --json and --out");
44937
45165
  if (o.repo) {
44938
45166
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
44939
45167
  if (!guard.ok) return fail(`release: ${guard.message}`);
@@ -44957,7 +45185,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44957
45185
  if (o.abort) {
44958
45186
  if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
44959
45187
  if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
44960
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45188
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
44961
45189
  return fail("release: --abort accepts only --apply, --repo, --json and --out; promotion flags cannot be combined with rollback");
44962
45190
  }
44963
45191
  if (o.repo) {
@@ -44976,7 +45204,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44976
45204
  if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
44977
45205
  if (o.retryPublish) return fail("release: --supersede-deploy cannot be combined with --retry-publish");
44978
45206
  if (!o.apply) return fail("release: --supersede-deploy requires --apply after explicit master approval; nothing was written");
44979
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45207
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
44980
45208
  return fail("release: --supersede-deploy accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
44981
45209
  }
44982
45210
  if (o.repo) {
@@ -44995,7 +45223,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44995
45223
  if (o.abort) return fail("release: --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PROMOTED release, abort only ever deletes a proven unpublished candidate");
44996
45224
  if (o.retryPublish) return fail("release: --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
44997
45225
  if (!o.apply) return fail("release: --supersede-publish requires --apply after explicit master approval; nothing was written");
44998
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45226
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
44999
45227
  return fail("release: --supersede-publish accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
45000
45228
  }
45001
45229
  if (o.repo) {
@@ -45023,7 +45251,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45023
45251
  const raw = await withReleaseProgress(
45024
45252
  trainApplyDeps(),
45025
45253
  "Checking the existing release",
45026
- (deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo })
45254
+ (deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo, big: o.big })
45027
45255
  );
45028
45256
  const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
45029
45257
  const projectInfoSync = await withReleaseProgress(
@@ -45087,7 +45315,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45087
45315
  if (o.apply) {
45088
45316
  try {
45089
45317
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
45090
- const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo });
45318
+ const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
45091
45319
  const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
45092
45320
  const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
45093
45321
  let projectInfoSync;
@@ -45338,7 +45566,10 @@ function rcRunCauseFromLog(log) {
45338
45566
  const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
45339
45567
  return causes.length ? `: ${causes.join("; ")}` : "";
45340
45568
  }
45341
- function suiteTrainIo(root, summaryFile) {
45569
+ function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
45570
+ return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
45571
+ }
45572
+ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
45342
45573
  const recordPath = repoRuntimeStatePath(root, "suite", "cut.json");
45343
45574
  const base = (0, import_node_path43.join)(repoRuntimeStatePath(root, "suite"), "checkouts");
45344
45575
  const checkout = async (repo) => {
@@ -45386,6 +45617,17 @@ function suiteTrainIo(root, summaryFile) {
45386
45617
  if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
45387
45618
  return { version: live.deployedTag, host };
45388
45619
  };
45620
+ const hold = async (member) => {
45621
+ const slug = member.repo.split("/")[1].toLowerCase();
45622
+ const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
45623
+ if (!read.ok) throw new Error(`${member.repo}: registry META unreadable for suiteHold (${read.error}); stop`);
45624
+ const value = read.project?.suiteHold;
45625
+ if (value === void 0 || value === null) return null;
45626
+ const raw = value;
45627
+ if (typeof raw.reason !== "string" || !raw.reason.trim())
45628
+ throw new Error(`${member.repo}: registry META suiteHold is malformed; set it as {"reason":"...","since":"..."} or clear it with --unset suiteHold; stop`);
45629
+ return { reason: raw.reason, ...typeof raw.since === "string" && raw.since ? { since: raw.since } : {} };
45630
+ };
45389
45631
  const tagSha = async (repo, tag) => {
45390
45632
  try {
45391
45633
  return await gh(["api", `repos/${repo}/commits/${encodeURIComponent(tag)}`, "--jq", ".sha"]);
@@ -45432,8 +45674,9 @@ function suiteTrainIo(root, summaryFile) {
45432
45674
  return { tag: result.tag, sha: result.tagSha };
45433
45675
  }
45434
45676
  });
45435
- const release = async (repo, expected, rcSha) => inTrain(repo, async (deps) => {
45677
+ const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
45436
45678
  await prepareRcCheckout((args) => deps.run("git", args), repo);
45679
+ const plainWords = { announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles), big: opts?.big, suiteTag: opts?.suiteTag };
45437
45680
  const current = await tagSha(repo, expected);
45438
45681
  if (current) {
45439
45682
  await deps.run("git", ["fetch", "origin", "--tags"]);
@@ -45442,10 +45685,10 @@ function suiteTrainIo(root, summaryFile) {
45442
45685
  } catch {
45443
45686
  throw new Error(`${repo}: existing ${expected} does not carry this suite's rc SHA ${rcSha}; refuse stale release`);
45444
45687
  }
45445
- const result = await runReleaseResume(deps, { watch: true });
45688
+ const result = await runReleaseResume(deps, { watch: true, ...plainWords });
45446
45689
  if (result.tag !== expected || result.state !== "aligned") throw new Error(`${repo}: release resume not complete at ${expected} (${result.tag}/${result.state})`);
45447
45690
  } else {
45448
- const result = await withEnv("MMI_RELEASE_VERSION", expected.replace(/^v/, ""), () => runTrainApply("release", deps, { watch: true }));
45691
+ const result = await withEnv("MMI_RELEASE_VERSION", expected.replace(/^v/, ""), () => runTrainApply("release", deps, { watch: true, ...plainWords }));
45449
45692
  if (result.tag !== expected || result.deployStatus !== "success") throw new Error(`${repo}: release is not green at expected ${expected} (${result.tag}/${result.deployStatus})`);
45450
45693
  }
45451
45694
  });
@@ -45533,6 +45776,7 @@ function suiteTrainIo(root, summaryFile) {
45533
45776
  recordPath,
45534
45777
  registry: registry2,
45535
45778
  backend,
45779
+ hold,
45536
45780
  head: async (repo, branch) => {
45537
45781
  try {
45538
45782
  return await gh(["api", `repos/${repo}/git/ref/heads/${branch}`, "--jq", ".object.sha"]);
@@ -45596,6 +45840,15 @@ function suiteTrainIo(root, summaryFile) {
45596
45840
  // src/suite-commands.ts
45597
45841
  init_cli_shared();
45598
45842
  var import_node_fs44 = require("node:fs");
45843
+ function parseMemberSummaryFiles(entries) {
45844
+ const memberSummaryFiles = {};
45845
+ for (const entry of entries) {
45846
+ const eq = entry.indexOf("=");
45847
+ if (eq < 0) throw new Error(`--member-summary-file expects <owner/repo>=<path>, got ${entry}`);
45848
+ memberSummaryFiles[entry.slice(0, eq)] = entry.slice(eq + 1);
45849
+ }
45850
+ return memberSummaryFiles;
45851
+ }
45599
45852
  function registerSuiteCommands(program3) {
45600
45853
  const suite = program3.command("suite").description("MM-Work suite status, candidate cut and release (#7262/#7263)");
45601
45854
  const leadRoot = async () => {
@@ -45643,23 +45896,25 @@ function registerSuiteCommands(program3) {
45643
45896
  return fail(`suite abandon: ${e.message}`);
45644
45897
  }
45645
45898
  });
45646
- suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train").option("--json", "structured receipt").action(async (o) => {
45899
+ suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: a member's OWN plain-words summary (#7510) \u2014 never the suite lead's summary reused; a member with no entry still ships, with the coordinator-ruled no-change line", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
45647
45900
  try {
45648
45901
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
45649
45902
  if (!o.announceSummaryFile) throw new Error("suite release requires --announce-summary-file before promoting any member");
45650
45903
  const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
45651
45904
  if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
45905
+ const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
45652
45906
  const root = await leadRoot();
45653
45907
  const unlock = await acquireTrainWriterLock(root, "suite release");
45654
45908
  let result;
45655
45909
  try {
45656
- result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile), true);
45910
+ result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
45657
45911
  } finally {
45658
45912
  await unlock();
45659
45913
  }
45660
45914
  console.log(o.json ? JSON.stringify(result, null, 2) : [
45661
45915
  `suite release: ${result.leadRcTag} \u2192 main`,
45662
45916
  ...result.members.map((m) => m.carried ? ` - ${m.repo}: carried, untouched` : ` - ${m.repo}: ${m.tag} \u2192 main ${m.released ? "released" : "pending"}`),
45917
+ ...result.heldMembers?.length ? ["held (frozen live release, untouched):", ...result.heldMembers.map((h) => ` - ${h.repo}: HELD: ${h.reason}`)] : [],
45663
45918
  ` - MM-Work: ${result.leadReleased ? "released" : "pending"}; announcement: ${result.announcement}`
45664
45919
  ].join("\n"));
45665
45920
  } catch (e) {
@@ -48691,7 +48946,7 @@ function renderTrackToDirectResult(result) {
48691
48946
  }
48692
48947
 
48693
48948
  // src/schedules-commands.ts
48694
- var import_promises9 = require("node:fs/promises");
48949
+ var import_promises10 = require("node:fs/promises");
48695
48950
  init_clean_exit();
48696
48951
  init_github_client();
48697
48952
  init_cli_shared();
@@ -48885,9 +49140,9 @@ function registerSchedulesCommands(program3) {
48885
49140
  await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
48886
49141
  return;
48887
49142
  }
48888
- const docText = await (0, import_promises9.readFile)(o.doc, "utf8");
49143
+ const docText = await (0, import_promises10.readFile)(o.doc, "utf8");
48889
49144
  const spliced = spliceDoc(docText, renderDocSection(entries, now.toISOString()));
48890
- await (0, import_promises9.writeFile)(o.doc, spliced, "utf8");
49145
+ await (0, import_promises10.writeFile)(o.doc, spliced, "utf8");
48891
49146
  reportParked(parkedLines);
48892
49147
  reportDrift(drift);
48893
49148
  console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
@@ -49625,7 +49880,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
49625
49880
  // src/tenant-artifact.ts
49626
49881
  var import_node_crypto15 = require("node:crypto");
49627
49882
  var import_node_fs48 = require("node:fs");
49628
- var import_promises10 = require("node:fs/promises");
49883
+ var import_promises11 = require("node:fs/promises");
49629
49884
  var import_node_path46 = require("node:path");
49630
49885
  var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
49631
49886
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
@@ -49637,7 +49892,7 @@ async function sha256File(path2) {
49637
49892
  async function putTenantArtifact(repo, stage, inputPath, deps) {
49638
49893
  if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
49639
49894
  const path2 = (0, import_node_path46.resolve)(inputPath);
49640
- const info = await (0, import_promises10.stat)(path2);
49895
+ const info = await (0, import_promises11.stat)(path2);
49641
49896
  if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
49642
49897
  if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES} bytes`);
49643
49898
  const sha2562 = await sha256File(path2);
@@ -50119,7 +50374,7 @@ init_github_client();
50119
50374
  init_hub_auth();
50120
50375
 
50121
50376
  // src/schedules-lift-command.ts
50122
- var import_promises11 = require("node:fs/promises");
50377
+ var import_promises12 = require("node:fs/promises");
50123
50378
  var import_node_path49 = require("node:path");
50124
50379
  init_clean_exit();
50125
50380
  init_cli_shared();
@@ -50142,14 +50397,14 @@ var RegistryUnreachableError = class extends Error {
50142
50397
  async function readWorkflowFiles(dir) {
50143
50398
  let names;
50144
50399
  try {
50145
- names = await (0, import_promises11.readdir)(dir);
50400
+ names = await (0, import_promises12.readdir)(dir);
50146
50401
  } catch {
50147
50402
  return [];
50148
50403
  }
50149
50404
  const files = [];
50150
50405
  for (const name of names.sort()) {
50151
50406
  if (!/\.ya?ml$/.test(name)) continue;
50152
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises11.readFile)((0, import_node_path49.join)(dir, name), "utf8") });
50407
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path49.join)(dir, name), "utf8") });
50153
50408
  }
50154
50409
  return files;
50155
50410
  }
@@ -51876,6 +52131,7 @@ function ciAuditDeps2() {
51876
52131
  client: defaultGitHubClient(),
51877
52132
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
51878
52133
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
52134
+ getDeployFacts: async (slug) => fetchDeployFactsBySlug(slug, registryClientDeps(await cfgPromise)),
51879
52135
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
51880
52136
  // Resolve from the installed package root so the reconcile works from any org repo checkout; when the
51881
52137
  // Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the