@mutmutco/cli 4.5.59 → 4.5.61

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 +265 -130
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -8653,6 +8653,9 @@ async function upgradeMessage(res, body) {
8653
8653
  if (res.status !== 426) return null;
8654
8654
  return upgradeRequiredError(res, body ?? await readJsonBody(res));
8655
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
+ }
8656
8659
  async function fetchSecretValue(deps, key, opts, diagnose = false) {
8657
8660
  if (!isValidSecretKey(key)) return null;
8658
8661
  const repo = await targetRepo(deps, opts);
@@ -8667,7 +8670,7 @@ async function fetchSecretValue(deps, key, opts, diagnose = false) {
8667
8670
  if (diagnose) {
8668
8671
  const body = await readJsonBody(res);
8669
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";
8670
- throw new Error(`${reason} (HTTP ${res.status}${typeof body.error === "string" && /^(forbidden|unauthorized|rate limit|too many requests|master-admin only)$/i.test(body.error) ? `: ${body.error}` : ""})`);
8673
+ throw new Error(`${reason} (HTTP ${res.status}${safeServerReason(body.error) ? `: ${body.error}` : ""})`);
8671
8674
  }
8672
8675
  return null;
8673
8676
  }
@@ -14755,7 +14758,7 @@ function extractForwardRefs(markdown) {
14755
14758
  });
14756
14759
  return refs;
14757
14760
  }
14758
- function checkPins(root, readFile9, docs) {
14761
+ function checkPins(root, readFile10, docs) {
14759
14762
  const findings = [];
14760
14763
  for (const [doc, markdown] of Object.entries(docs)) {
14761
14764
  for (const pin of extractPins(markdown)) {
@@ -14763,7 +14766,7 @@ function checkPins(root, readFile9, docs) {
14763
14766
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
14764
14767
  continue;
14765
14768
  }
14766
- const source = readFile9((0, import_node_path12.join)(root, pin.file));
14769
+ const source = readFile10((0, import_node_path12.join)(root, pin.file));
14767
14770
  if (source == null) {
14768
14771
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
14769
14772
  continue;
@@ -14980,7 +14983,7 @@ function defaultTrackedFirstSegments(root, firstSegments, exec2 = import_node_ch
14980
14983
  }
14981
14984
  }
14982
14985
  function runDocRefs(root, deps = {}) {
14983
- const readFile9 = deps.readFile ?? readFileOrNull;
14986
+ const readFile10 = deps.readFile ?? readFileOrNull;
14984
14987
  const exists = deps.exists ?? import_node_fs14.existsSync;
14985
14988
  const listDocs = deps.listDocs ?? defaultListDocs;
14986
14989
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
@@ -14989,11 +14992,11 @@ function runDocRefs(root, deps = {}) {
14989
14992
  const walked = listDocs(root);
14990
14993
  const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
14991
14994
  const docs = Object.fromEntries(
14992
- 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)
14993
14996
  );
14994
14997
  const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs);
14995
14998
  const findings = [
14996
- ...checkPins(root, readFile9, docs).findings,
14999
+ ...checkPins(root, readFile10, docs).findings,
14997
15000
  ...refResult.findings
14998
15001
  ];
14999
15002
  if (commandPaths == null) {
@@ -15772,22 +15775,22 @@ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
15772
15775
  ...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
15773
15776
  ].join("\n");
15774
15777
  }
15775
- function resolveSeedContent(seed, vars, readFile9) {
15776
- if (seed.source === "self") return readFile9(seed.target);
15778
+ function resolveSeedContent(seed, vars, readFile10) {
15779
+ if (seed.source === "self") return readFile10(seed.target);
15777
15780
  if (seed.source.startsWith("seed:")) {
15778
- const tmpl = readFile9(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
15781
+ const tmpl = readFile10(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
15779
15782
  return tmpl == null ? null : renderSeed(tmpl, vars);
15780
15783
  }
15781
15784
  return null;
15782
15785
  }
15783
- function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
15786
+ function resolveSeedWriteContent(seed, vars, readFile10, remoteContent) {
15784
15787
  if (!seed.managedBlock) {
15785
- 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" };
15786
15789
  }
15787
- const base = remoteContent ?? resolveSeedContent(seed, vars, readFile9);
15790
+ const base = remoteContent ?? resolveSeedContent(seed, vars, readFile10);
15788
15791
  if (base == null) return { ok: true, content: null, managed: true };
15789
15792
  const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
15790
- const desired = resolveSeedContent(blockSeed, vars, readFile9);
15793
+ const desired = resolveSeedContent(blockSeed, vars, readFile10);
15791
15794
  if (desired == null) return { ok: true, content: null, managed: true };
15792
15795
  const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
15793
15796
  return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
@@ -17148,10 +17151,10 @@ var rollout_plan_default = {
17148
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)."
17149
17152
  },
17150
17153
  baseline: {
17151
- version: "4.5.59",
17152
- tag: "v4.5.59",
17153
- commit: "ea079366d96e",
17154
- npm: "@mutmutco/cli@4.5.59"
17154
+ version: "4.5.61",
17155
+ tag: "v4.5.61",
17156
+ commit: "72f52b1d5bbb",
17157
+ npm: "@mutmutco/cli@4.5.61"
17155
17158
  },
17156
17159
  exitCriterion: "fleet-n-of-n",
17157
17160
  hubOnlyShortcut: "forbidden",
@@ -17168,14 +17171,14 @@ var rollout_plan_default = {
17168
17171
  repo: "mutmutco/mmi-hub",
17169
17172
  role: "canary",
17170
17173
  schedule: "train",
17171
- v3Target: "v4.5.59"
17174
+ v3Target: "v4.5.61"
17172
17175
  }
17173
17176
  ],
17174
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.",
17175
17178
  rollback: {
17176
17179
  independent: true,
17177
- mechanism: "npm dist-tag latest -> 4.5.59 and redeploy the Hub Lambda from tag v4.5.59 (ea079366d96e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17178
- v3Target: "v4.5.59 (@mutmutco/cli@4.5.59, tag commit ea079366d96e \u2014 last known-good release carrying the repo-index v4-only contract)"
17180
+ mechanism: "npm dist-tag latest -> 4.5.61 and redeploy the Hub Lambda from tag v4.5.61 (72f52b1d5bbb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17181
+ v3Target: "v4.5.61 (@mutmutco/cli@4.5.61, tag commit 72f52b1d5bbb \u2014 last known-good release carrying the repo-index v4-only contract)"
17179
17182
  }
17180
17183
  },
17181
17184
  {
@@ -21362,7 +21365,7 @@ function renderTenantControl(r) {
21362
21365
 
21363
21366
  // src/train-apply-phases.ts
21364
21367
  var import_node_fs24 = require("node:fs");
21365
- var import_promises3 = require("node:fs/promises");
21368
+ var import_promises4 = require("node:fs/promises");
21366
21369
  var import_node_path24 = require("node:path");
21367
21370
 
21368
21371
  // src/actions-billing-preflight.ts
@@ -25049,6 +25052,59 @@ function formatTrainDoctor(v) {
25049
25052
  return lines2.join("\n");
25050
25053
  }
25051
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
+
25052
25108
  // src/vercel-version-env.ts
25053
25109
  var SKIPPED = { status: "skipped", note: "" };
25054
25110
  function vercelVersionEnvAheadNote(key) {
@@ -25349,7 +25405,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
25349
25405
  const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
25350
25406
  for (; ; ) {
25351
25407
  try {
25352
- await (0, import_promises3.stat)(lockPath);
25408
+ await (0, import_promises4.stat)(lockPath);
25353
25409
  } catch (e) {
25354
25410
  if (e.code === "ENOENT") return;
25355
25411
  throw foldFailureGuidance(
@@ -25385,7 +25441,7 @@ async function clearFoldChildIndexLock(deps) {
25385
25441
  "git index lock path"
25386
25442
  );
25387
25443
  try {
25388
- await (0, import_promises3.unlink)(lockPath);
25444
+ await (0, import_promises4.unlink)(lockPath);
25389
25445
  } catch (e) {
25390
25446
  if (e.code === "ENOENT") return void 0;
25391
25447
  throw e;
@@ -25984,6 +26040,7 @@ ${recovery.note}`), recoveryInput);
25984
26040
  const autoRunSince = (deps.now ?? Date.now)();
25985
26041
  const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
25986
26042
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
26043
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
25987
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" });
25988
26045
  let alignment = alignEarly ? await alignEarly() : void 0;
25989
26046
  const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha, watch });
@@ -26543,6 +26600,7 @@ ${recovery.note}`), recoveryInput);
26543
26600
  steps.push(`created the GitHub Release for ${tag}`);
26544
26601
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
26545
26602
  steps.push("verified the Release published against the tagged commit");
26603
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
26546
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" });
26547
26605
  let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
26548
26606
  let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
@@ -27280,6 +27338,45 @@ var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflow
27280
27338
  var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
27281
27339
  var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
27282
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
+ }
27283
27380
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
27284
27381
  var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
27285
27382
  var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
@@ -27919,6 +28016,10 @@ async function auditRepoCi(repo, callerDeps) {
27919
28016
  const workflows = await getBranchWorkflows();
27920
28017
  if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
27921
28018
  }
28019
+ if (meta?.deployModel === "tenant-container" && deps.getDeployFacts) {
28020
+ const fresh = await auditDevStageFreshness(repo, deps);
28021
+ if (fresh) checks.push(fresh);
28022
+ }
27922
28023
  const failure = unauditedReason();
27923
28024
  if (failure) {
27924
28025
  return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
@@ -30369,7 +30470,7 @@ init_hub_auth();
30369
30470
  init_github_client();
30370
30471
 
30371
30472
  // src/project-set.ts
30372
- 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"];
30373
30474
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
30374
30475
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
30375
30476
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -30869,6 +30970,7 @@ var SETTABLE_VAR_KEYS = [
30869
30970
  "branch",
30870
30971
  "vaultPath",
30871
30972
  "suite",
30973
+ "suiteHold",
30872
30974
  "repos",
30873
30975
  "oauth",
30874
30976
  "publishRequired",
@@ -30903,6 +31005,7 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
30903
31005
  var SETTABLE_VAR_HINTS = {
30904
31006
  projectNumber: "numeric",
30905
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)',
30906
31009
  publishRequired: "true|false",
30907
31010
  publishDir: "relative subpath, e.g. packages/ui",
30908
31011
  releaseChannel: "Slack channel id, e.g. C0BE0BRGAT0",
@@ -30981,6 +31084,20 @@ function clearSecretsConfirmationError(unsets, existing, confirmed) {
30981
31084
  if (confirmed) return null;
30982
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}.`;
30983
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
+ }
30984
31101
  function buildProjectSetPatch(input) {
30985
31102
  const patch = {};
30986
31103
  if (input.class) {
@@ -31058,6 +31175,8 @@ function buildProjectSetPatch(input) {
31058
31175
  } else if (key === "suite") {
31059
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)");
31060
31177
  patch[key] = raw;
31178
+ } else if (key === "suiteHold") {
31179
+ patch[key] = parseSuiteHoldVar(raw);
31061
31180
  } else if (key === "releaseChannel") {
31062
31181
  patch[key] = parseReleaseChannelVar(raw);
31063
31182
  } else if (key === "releaseLanguage") {
@@ -33984,7 +34103,7 @@ function registerBootstrapCommands(program3) {
33984
34103
  }
33985
34104
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
33986
34105
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
33987
- 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;
33988
34107
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
33989
34108
  const putSeed = async (target, content, ref, sha) => {
33990
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`);
@@ -34140,7 +34259,7 @@ function registerBootstrapCommands(program3) {
34140
34259
  registryMeta?.requiredCheckBranches,
34141
34260
  registryMeta?.requiredChecks
34142
34261
  );
34143
- const rendered = resolveSeedContent(gateSeed, gateVars, readFile9);
34262
+ const rendered = resolveSeedContent(gateSeed, gateVars, readFile10);
34144
34263
  if (rendered === null || missingPlaceholders(rendered).length) return failGraceful("bootstrap apply: registry gate template is unreadable or has unfilled placeholders");
34145
34264
  const base = (await readTargetedSeedFiles(repo, scopedPlan, onlyTarget, scopedPlan.baseSha, controlClient)).base?.content ?? null;
34146
34265
  configGateAction = planGateFromConfig(base, rendered);
@@ -34244,7 +34363,7 @@ function registerBootstrapCommands(program3) {
34244
34363
  ...vars,
34245
34364
  ...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
34246
34365
  ...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
34247
- } : vars, readFile9, preservedContent);
34366
+ } : vars, readFile10, preservedContent);
34248
34367
  if (!writeContent.ok) {
34249
34368
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
34250
34369
  }
@@ -34399,7 +34518,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34399
34518
  }
34400
34519
  const rulesetSeed = repo.toLowerCase() === "mutmutco/mmi-hub" ? void 0 : manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
34401
34520
  if (rulesetSeed) {
34402
- 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);
34403
34522
  if (rulesetContent) {
34404
34523
  try {
34405
34524
  const client = controlClient;
@@ -35260,6 +35379,16 @@ function pathOf(url) {
35260
35379
  }
35261
35380
  }
35262
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
+ }
35263
35392
  function suiteReleaseTag(rc) {
35264
35393
  if (!RC_TAG.test(rc)) throw new Error(`invalid rc tag ${rc}`);
35265
35394
  return rc.replace(/-rc\.\d+$/, "");
@@ -35278,7 +35407,7 @@ function readSuiteCut(path2) {
35278
35407
  } catch {
35279
35408
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
35280
35409
  }
35281
- if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
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)))
35282
35411
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
35283
35412
  return record;
35284
35413
  }
@@ -35333,8 +35462,10 @@ async function suiteRcand(deps, opts) {
35333
35462
  if (!opts.ownerGo) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
35334
35463
  const members = suiteMembers(await deps.registry());
35335
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]));
35336
35467
  const plan = [
35337
- ...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)`),
35338
35469
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35339
35470
  `${SUITE_REPO}: rcand`
35340
35471
  ];
@@ -35354,7 +35485,7 @@ async function suiteRcand(deps, opts) {
35354
35485
  const [leadDevelopment, leadPreviousRc, snapshots] = await Promise.all([
35355
35486
  deps.head(SUITE_REPO, "development"),
35356
35487
  deps.head(SUITE_REPO, "rc"),
35357
- Promise.all(modules.map(async (m) => {
35488
+ Promise.all(active.map(async (m) => {
35358
35489
  const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
35359
35490
  const previousRc = rcHead ?? await deps.head(m.repo, "main");
35360
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`);
@@ -35362,12 +35493,14 @@ async function suiteRcand(deps, opts) {
35362
35493
  }))
35363
35494
  ]);
35364
35495
  if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
35365
- 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 } : {} };
35366
35497
  writeSuiteCut(deps.recordPath, created);
35367
35498
  record = created;
35368
35499
  }
35369
35500
  if (!record) throw new Error("suite cut record unavailable after initialization");
35370
- 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)))
35371
35504
  throw new Error("suite roster changed since cut began; refuse to omit or add a member");
35372
35505
  const quiet = [];
35373
35506
  for (const member of record.members) {
@@ -35437,7 +35570,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35437
35570
  }
35438
35571
  const pins = suitePins(record, bundlesOf(modules));
35439
35572
  await deps.verifyLeadCut(record.leadDevelopment);
35440
- 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) });
35441
35574
  await deps.verifyLeadCut(record.leadDevelopment);
35442
35575
  if (record.pinPr && record.pinPr !== pr.pr) throw new Error(`pin PR changed from ${record.pinPr} to ${pr.pr}; stop`);
35443
35576
  record.pinPr = pr.pr;
@@ -35454,12 +35587,14 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35454
35587
  }
35455
35588
  return { plan, record };
35456
35589
  }
35457
- async function suiteRelease(deps, ownerGo) {
35590
+ async function suiteRelease(deps, ownerGo, big = false) {
35458
35591
  if (!ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
35459
35592
  const record = readSuiteCut(deps.recordPath);
35460
35593
  if (!record?.pinMerged || !record.leadRcTag) throw new Error("no completed suite rc cut and merged pin PR; release refused");
35461
35594
  const members = suiteMembers(await deps.registry()).filter((m) => m.repo !== SUITE_REPO);
35462
- 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");
35463
35598
  await deps.verifyPins(suitePins(record, bundlesOf(members)), record.leadRcTag);
35464
35599
  for (const member of record.members) {
35465
35600
  if (member.carried) {
@@ -35505,7 +35640,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
35505
35640
  console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35506
35641
  } else {
35507
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)`);
35508
- 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) });
35509
35644
  }
35510
35645
  member.released = true;
35511
35646
  writeSuiteCut(deps.recordPath, record);
@@ -35517,7 +35652,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
35517
35652
  }
35518
35653
  }
35519
35654
  if (!record.leadReleased) {
35520
- await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
35655
+ await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha, { big });
35521
35656
  record.leadReleased = true;
35522
35657
  writeSuiteCut(deps.recordPath, record);
35523
35658
  }
@@ -35545,14 +35680,14 @@ var import_node_path43 = require("node:path");
35545
35680
  init_cli_shared();
35546
35681
 
35547
35682
  // src/command-register-train-operations.ts
35548
- var import_promises8 = require("node:fs/promises");
35683
+ var import_promises9 = require("node:fs/promises");
35549
35684
  init_clean_exit();
35550
35685
  init_cli_shared();
35551
35686
  init_client_version();
35552
35687
  init_house_map();
35553
35688
 
35554
35689
  // src/hotfix-apply.ts
35555
- var import_promises4 = require("node:fs/promises");
35690
+ var import_promises5 = require("node:fs/promises");
35556
35691
 
35557
35692
  // src/release-announce.ts
35558
35693
  var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
@@ -36521,7 +36656,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
36521
36656
  }
36522
36657
  let lines2;
36523
36658
  try {
36524
- 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));
36525
36660
  } catch (e) {
36526
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`);
36527
36662
  }
@@ -36572,6 +36707,7 @@ ${decision.note}`);
36572
36707
  await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", "main", "--generate-notes", "--latest"]);
36573
36708
  releaseNote = `Release ${tag} created (target main)`;
36574
36709
  }
36710
+ await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
36575
36711
  await recordHotfixPhase(ledger, anchors, "githubRelease", { state: "complete", sha: mergedSha, note: releaseNote }, "the immutable tag, the green wall, and origin/main");
36576
36712
  const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha: mergedSha, watch: true });
36577
36713
  const runs = [];
@@ -37896,8 +38032,8 @@ function annotateChangeMeaning(changed, policy, read) {
37896
38032
  function isMeaningfulRow(file) {
37897
38033
  return file.meaningful !== false;
37898
38034
  }
37899
- function loadPolicy(root, readFile9 = readFileOrNull2) {
37900
- 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));
37901
38037
  if (raw == null) return { mandatory: [], declared: false };
37902
38038
  return parsePolicy(raw, POLICY_FILE);
37903
38039
  }
@@ -38845,12 +38981,16 @@ function releaseProgressVerdict(status, subject = "Release") {
38845
38981
  }
38846
38982
 
38847
38983
  // src/suite-pin.ts
38848
- var import_promises5 = require("node:fs/promises");
38984
+ var import_promises6 = require("node:fs/promises");
38849
38985
  var import_node_path34 = require("node:path");
38850
38986
  async function openSuiteCutPinPr(deps, input) {
38851
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");
38852
38988
  const ids = input.pins.map((p) => p.module);
38853
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");
38854
38994
  const branch = `suite-pin/cut-${input.cut.slice(0, 16)}`;
38855
38995
  const registry2 = async (ref = "development") => {
38856
38996
  const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`]));
@@ -38861,7 +39001,7 @@ async function openSuiteCutPinPr(deps, input) {
38861
39001
  try {
38862
39002
  const rows = await registry2();
38863
39003
  const members = suiteMembers(rows).filter((m) => m.repo !== SUITE_REPO);
38864
- 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");
38865
39005
  for (const pin of input.pins) {
38866
39006
  for (const url of [pin.rc, pin.main]) {
38867
39007
  const parsed = new URL(url);
@@ -38883,8 +39023,8 @@ async function openSuiteCutPinPr(deps, input) {
38883
39023
  });
38884
39024
  if (prs.length && matches(await registry2(branch))) return { status: "pending", pr: prs[0].url };
38885
39025
  if (!prs.length && matches(rows)) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
38886
- await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
38887
- 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-"));
38888
39028
  const run = (file, args) => deps.run(file, args, scratch);
38889
39029
  await run("git", ["clone", "--quiet", "--branch", prs.length ? branch : "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38890
39030
  if (prs.length) {
@@ -38897,7 +39037,7 @@ async function openSuiteCutPinPr(deps, input) {
38897
39037
  }
38898
39038
  for (const stage of ["rc", "main"]) {
38899
39039
  const path2 = (0, import_node_path34.join)(scratch, `${stage}-pins.json`);
38900
- 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] }))));
38901
39041
  await run("node", ["scripts/pin-modules.mjs", "--stage", stage, "--pins", path2]);
38902
39042
  }
38903
39043
  await run("git", ["add", "seed/modules.json"]);
@@ -38927,7 +39067,7 @@ async function openSuiteCutPinPr(deps, input) {
38927
39067
  } catch (e) {
38928
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)`);
38929
39069
  } finally {
38930
- 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 });
38931
39071
  }
38932
39072
  }
38933
39073
  async function openSuitePinPr(deps, input) {
@@ -38946,8 +39086,8 @@ async function openSuitePinPr(deps, input) {
38946
39086
  url.searchParams.set("v", tag);
38947
39087
  const pin = url.toString();
38948
39088
  const branch = `suite-pin/${stage}-${member.id}-${tag}`;
38949
- await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
38950
- 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-"));
38951
39091
  const run = (file, args) => deps.run(file, args, scratch);
38952
39092
  await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38953
39093
  const existing = JSON.parse(await run("gh", ["pr", "list", "--repo", SUITE_REPO, "--head", branch, "--state", "all", "--json", "url,state"]));
@@ -38985,14 +39125,14 @@ async function openSuitePinPr(deps, input) {
38985
39125
  } catch (e) {
38986
39126
  throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
38987
39127
  } finally {
38988
- 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 });
38989
39129
  }
38990
39130
  }
38991
39131
 
38992
39132
  // src/command-register-collaboration.ts
38993
39133
  var import_node_child_process17 = require("node:child_process");
38994
39134
  var import_node_fs42 = require("node:fs");
38995
- var import_promises7 = require("node:fs/promises");
39135
+ var import_promises8 = require("node:fs/promises");
38996
39136
  init_clean_exit();
38997
39137
  init_cli_shared();
38998
39138
  init_error_codes();
@@ -40265,7 +40405,7 @@ function postMergeReconWarnings(input) {
40265
40405
  }
40266
40406
 
40267
40407
  // src/pr-commands.ts
40268
- var import_promises6 = require("node:fs/promises");
40408
+ var import_promises7 = require("node:fs/promises");
40269
40409
  init_cli_shared();
40270
40410
  init_error_codes();
40271
40411
  var GC_GH_TIMEOUT_MS2 = 2e4;
@@ -40419,10 +40559,10 @@ function registerPrLifecycleCommands(program3) {
40419
40559
  let body;
40420
40560
  try {
40421
40561
  if (o.title || o.titleFile) {
40422
- 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 });
40423
40563
  }
40424
40564
  if (o.body || o.bodyFile) {
40425
- 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 });
40426
40566
  }
40427
40567
  } catch (e) {
40428
40568
  return fail(`pr edit: ${e.message}`);
@@ -40452,7 +40592,7 @@ function registerPrLifecycleCommands(program3) {
40452
40592
  }
40453
40593
  let body;
40454
40594
  try {
40455
- 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 });
40456
40596
  } catch (e) {
40457
40597
  return fail(`pr comment: ${e.message}`);
40458
40598
  }
@@ -42813,34 +42953,6 @@ function renderResidueSweepLines(report) {
42813
42953
  return lines2;
42814
42954
  }
42815
42955
 
42816
- // src/pr-deploy-dev.ts
42817
- function slugOfRepo(repo) {
42818
- return repo.split("/")[1]?.toLowerCase() ?? "";
42819
- }
42820
- async function planDevDeployOnDevelopmentMerge(repo, baseRef, deps) {
42821
- if (baseRef !== "development") return { applicable: false, reason: "not-development-base" };
42822
- const slug = slugOfRepo(repo);
42823
- if (!slug) return { applicable: false, reason: "unreadable" };
42824
- let project2;
42825
- try {
42826
- project2 = await fetchProjectBySlug(slug, deps);
42827
- } catch {
42828
- return { applicable: false, reason: "unreadable" };
42829
- }
42830
- const model = project2?.deployModel;
42831
- if (!model || !isCentralDispatchModel(model)) return { applicable: false, reason: "not-tenant" };
42832
- return {
42833
- applicable: true,
42834
- // The issue's fallback ask, verbatim-shaped: the gap must be visible and actionable at the moment it opens.
42835
- manualPointer: `dev not redeployed \u2014 run \`mmi-cli devops runtime tenant redeploy ${repo} dev\``
42836
- };
42837
- }
42838
- async function dispatchDevDeploy(repo, deps) {
42839
- const res = await tenantDeploy({ repo, stage: "dev" }, deps);
42840
- if (res.ok) return { ok: true };
42841
- return { ok: false, detail: res.body?.error ?? res.error ?? `HTTP ${res.status}` };
42842
- }
42843
-
42844
42956
  // src/command-register-collaboration.ts
42845
42957
  var DEFAULT_PRIORITY = "medium";
42846
42958
  function argvWantsJson2() {
@@ -43038,7 +43150,7 @@ function registerCollaborationCommands(program3) {
43038
43150
  try {
43039
43151
  title = await resolveIssueTitle(
43040
43152
  { title: opts.title, titleFile: opts.titleFile },
43041
- { readFile: import_promises7.readFile, readStdin }
43153
+ { readFile: import_promises8.readFile, readStdin }
43042
43154
  );
43043
43155
  } catch (e) {
43044
43156
  return fail(
@@ -43092,8 +43204,8 @@ function registerCollaborationCommands(program3) {
43092
43204
  let surfaceInferred;
43093
43205
  try {
43094
43206
  issueType = resolveCreateType(o.type, "issue create", o.label);
43095
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43096
- 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 });
43097
43209
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
43098
43210
  priority = resolveCreatePriority(o.priority, "issue create");
43099
43211
  extraLabels = [...o.label ?? []];
@@ -43369,7 +43481,7 @@ function registerCollaborationCommands(program3) {
43369
43481
  }
43370
43482
  let body;
43371
43483
  try {
43372
- 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 });
43373
43485
  } catch (e) {
43374
43486
  return fail(`issue comment: ${e.message}`);
43375
43487
  }
@@ -43429,8 +43541,8 @@ ${list}`);
43429
43541
  let title;
43430
43542
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
43431
43543
  try {
43432
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43433
- 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 });
43434
43546
  priority = resolveCreatePriority(o.priority, "report");
43435
43547
  if (!ISSUE_TYPES.includes(o.type)) {
43436
43548
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -43484,8 +43596,8 @@ ${list}`);
43484
43596
  let args;
43485
43597
  try {
43486
43598
  skill = assertSkillName(o.skill);
43487
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
43488
- 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 });
43489
43601
  title = buildSkillLessonTitle(skill, rawTitle);
43490
43602
  priority = resolveCreatePriority(o.priority, "skill-lesson");
43491
43603
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -43546,8 +43658,8 @@ ${list}`);
43546
43658
  let body;
43547
43659
  let title;
43548
43660
  try {
43549
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
43550
- 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 });
43551
43663
  } catch (e) {
43552
43664
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
43553
43665
  }
@@ -44082,8 +44194,6 @@ ${list}`);
44082
44194
  const baseRef = prMeta.base;
44083
44195
  const headRefOid = (prMeta.oid ?? "").trim() || void 0;
44084
44196
  if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
44085
- const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
44086
- const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
44087
44197
  const closingGuardInput = await withAlreadyClosedCommitTargets(
44088
44198
  await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
44089
44199
  async (n) => {
@@ -44313,7 +44423,6 @@ ${list}`);
44313
44423
  const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
44314
44424
  console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
44315
44425
  console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
44316
- if (devDeployPlan.applicable) console.warn(`pr merge: ${devDeployPlan.manualPointer} once the merge lands (auto-merge does not redeploy the dev stage).`);
44317
44426
  if (upgradedToAuto && !o.auto) {
44318
44427
  console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
44319
44428
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
@@ -44423,8 +44532,6 @@ ${list}`);
44423
44532
  } else if (repoForPostCleanup) {
44424
44533
  clearPostMergeReconRecovery(process.cwd(), repoForPostCleanup, number);
44425
44534
  }
44426
- const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
44427
- const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
44428
44535
  let observedMethod;
44429
44536
  if (remoteNotAttemptedReason === "pr-already-merged" && repoForPostCleanup) {
44430
44537
  try {
@@ -44459,14 +44566,10 @@ ${list}`);
44459
44566
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
44460
44567
  boardAdvanceStatus: boardAdvance.status,
44461
44568
  ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
44462
- ...recovery ? { postMergeReconRecovery: recovery } : {},
44463
- ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
44569
+ ...recovery ? { postMergeReconRecovery: recovery } : {}
44464
44570
  }));
44465
44571
  for (const line of renderPrMergeCleanupLines(localCleanup)) console.warn(line);
44466
44572
  for (const line of renderResidueSweepLines(residueSweep)) console.warn(line);
44467
- if (devDeploy && !devDeploy.ok) {
44468
- console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
44469
- }
44470
44573
  for (const line of postMergeReconWarnings({
44471
44574
  context: "pr merge",
44472
44575
  boardAdvance,
@@ -44530,7 +44633,7 @@ function trainGitArgs(args) {
44530
44633
  async function announceSummaryRefusal(path2, isHub = false) {
44531
44634
  let lines2;
44532
44635
  try {
44533
- lines2 = summaryFileLines(await (0, import_promises8.readFile)(path2, "utf8"));
44636
+ lines2 = summaryFileLines(await (0, import_promises9.readFile)(path2, "utf8"));
44534
44637
  } catch (e) {
44535
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`;
44536
44639
  }
@@ -44638,8 +44741,8 @@ function trainApplyDeps() {
44638
44741
  }
44639
44742
  return body.status;
44640
44743
  },
44641
- readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
44642
- removeFile: (path2) => (0, import_promises8.unlink)(path2),
44744
+ readFile: (path2) => (0, import_promises9.readFile)(path2, "utf8"),
44745
+ removeFile: (path2) => (0, import_promises9.unlink)(path2),
44643
44746
  // #6521/#6523: the project's own release channel AND releaseLanguage, read from registry META in ONE
44644
44747
  // checked read. Same shape as fetchEdgeDomains below: a FAILED read refuses by name so the announce
44645
44748
  // note reports a failed read, never a project that configured nothing.
@@ -44967,7 +45070,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44967
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)" },
44968
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)" },
44969
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)" },
44970
- { 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" }
44971
45075
  ];
44972
45076
  for (const f of RELEASE_ONLY_FLAGS) {
44973
45077
  if (commandName === "release") {
@@ -44995,6 +45099,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44995
45099
  if (o.announceSummaryFile && commandName !== "release") {
44996
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>`);
44997
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
+ }
44998
45105
  if (o.abort && commandName !== "release") {
44999
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`);
45000
45107
  }
@@ -45037,7 +45144,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45037
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");
45038
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");
45039
45146
  if (!o.apply) return fail("release: --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
45040
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45147
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
45041
45148
  return fail("release: --supersede-deploy --supersede-publish accepts only --apply, --repo, --json and --out; it closes ledger legs and dispatches nothing");
45042
45149
  }
45043
45150
  if (o.repo) {
@@ -45054,7 +45161,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45054
45161
  if (o.retryPublish) {
45055
45162
  if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
45056
45163
  if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
45057
- 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");
45058
45165
  if (o.repo) {
45059
45166
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
45060
45167
  if (!guard.ok) return fail(`release: ${guard.message}`);
@@ -45078,7 +45185,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45078
45185
  if (o.abort) {
45079
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");
45080
45187
  if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
45081
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45188
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
45082
45189
  return fail("release: --abort accepts only --apply, --repo, --json and --out; promotion flags cannot be combined with rollback");
45083
45190
  }
45084
45191
  if (o.repo) {
@@ -45097,7 +45204,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45097
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");
45098
45205
  if (o.retryPublish) return fail("release: --supersede-deploy cannot be combined with --retry-publish");
45099
45206
  if (!o.apply) return fail("release: --supersede-deploy requires --apply after explicit master approval; nothing was written");
45100
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45207
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
45101
45208
  return fail("release: --supersede-deploy accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
45102
45209
  }
45103
45210
  if (o.repo) {
@@ -45116,7 +45223,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45116
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");
45117
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");
45118
45225
  if (!o.apply) return fail("release: --supersede-publish requires --apply after explicit master approval; nothing was written");
45119
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45226
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
45120
45227
  return fail("release: --supersede-publish accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
45121
45228
  }
45122
45229
  if (o.repo) {
@@ -45144,7 +45251,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45144
45251
  const raw = await withReleaseProgress(
45145
45252
  trainApplyDeps(),
45146
45253
  "Checking the existing release",
45147
- (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 })
45148
45255
  );
45149
45256
  const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
45150
45257
  const projectInfoSync = await withReleaseProgress(
@@ -45208,7 +45315,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45208
45315
  if (o.apply) {
45209
45316
  try {
45210
45317
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
45211
- 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 });
45212
45319
  const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
45213
45320
  const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
45214
45321
  let projectInfoSync;
@@ -45459,7 +45566,10 @@ function rcRunCauseFromLog(log) {
45459
45566
  const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
45460
45567
  return causes.length ? `: ${causes.join("; ")}` : "";
45461
45568
  }
45462
- 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 = {}) {
45463
45573
  const recordPath = repoRuntimeStatePath(root, "suite", "cut.json");
45464
45574
  const base = (0, import_node_path43.join)(repoRuntimeStatePath(root, "suite"), "checkouts");
45465
45575
  const checkout = async (repo) => {
@@ -45507,6 +45617,17 @@ function suiteTrainIo(root, summaryFile) {
45507
45617
  if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
45508
45618
  return { version: live.deployedTag, host };
45509
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
+ };
45510
45631
  const tagSha = async (repo, tag) => {
45511
45632
  try {
45512
45633
  return await gh(["api", `repos/${repo}/commits/${encodeURIComponent(tag)}`, "--jq", ".sha"]);
@@ -45553,8 +45674,9 @@ function suiteTrainIo(root, summaryFile) {
45553
45674
  return { tag: result.tag, sha: result.tagSha };
45554
45675
  }
45555
45676
  });
45556
- const release = async (repo, expected, rcSha) => inTrain(repo, async (deps) => {
45677
+ const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
45557
45678
  await prepareRcCheckout((args) => deps.run("git", args), repo);
45679
+ const plainWords = { announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles), big: opts?.big, suiteTag: opts?.suiteTag };
45558
45680
  const current = await tagSha(repo, expected);
45559
45681
  if (current) {
45560
45682
  await deps.run("git", ["fetch", "origin", "--tags"]);
@@ -45563,10 +45685,10 @@ function suiteTrainIo(root, summaryFile) {
45563
45685
  } catch {
45564
45686
  throw new Error(`${repo}: existing ${expected} does not carry this suite's rc SHA ${rcSha}; refuse stale release`);
45565
45687
  }
45566
- const result = await runReleaseResume(deps, { watch: true });
45688
+ const result = await runReleaseResume(deps, { watch: true, ...plainWords });
45567
45689
  if (result.tag !== expected || result.state !== "aligned") throw new Error(`${repo}: release resume not complete at ${expected} (${result.tag}/${result.state})`);
45568
45690
  } else {
45569
- 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 }));
45570
45692
  if (result.tag !== expected || result.deployStatus !== "success") throw new Error(`${repo}: release is not green at expected ${expected} (${result.tag}/${result.deployStatus})`);
45571
45693
  }
45572
45694
  });
@@ -45654,6 +45776,7 @@ function suiteTrainIo(root, summaryFile) {
45654
45776
  recordPath,
45655
45777
  registry: registry2,
45656
45778
  backend,
45779
+ hold,
45657
45780
  head: async (repo, branch) => {
45658
45781
  try {
45659
45782
  return await gh(["api", `repos/${repo}/git/ref/heads/${branch}`, "--jq", ".object.sha"]);
@@ -45717,6 +45840,15 @@ function suiteTrainIo(root, summaryFile) {
45717
45840
  // src/suite-commands.ts
45718
45841
  init_cli_shared();
45719
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
+ }
45720
45852
  function registerSuiteCommands(program3) {
45721
45853
  const suite = program3.command("suite").description("MM-Work suite status, candidate cut and release (#7262/#7263)");
45722
45854
  const leadRoot = async () => {
@@ -45764,23 +45896,25 @@ function registerSuiteCommands(program3) {
45764
45896
  return fail(`suite abandon: ${e.message}`);
45765
45897
  }
45766
45898
  });
45767
- 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) => {
45768
45900
  try {
45769
45901
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
45770
45902
  if (!o.announceSummaryFile) throw new Error("suite release requires --announce-summary-file before promoting any member");
45771
45903
  const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
45772
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 ?? []);
45773
45906
  const root = await leadRoot();
45774
45907
  const unlock = await acquireTrainWriterLock(root, "suite release");
45775
45908
  let result;
45776
45909
  try {
45777
- result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile), true);
45910
+ result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
45778
45911
  } finally {
45779
45912
  await unlock();
45780
45913
  }
45781
45914
  console.log(o.json ? JSON.stringify(result, null, 2) : [
45782
45915
  `suite release: ${result.leadRcTag} \u2192 main`,
45783
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}`)] : [],
45784
45918
  ` - MM-Work: ${result.leadReleased ? "released" : "pending"}; announcement: ${result.announcement}`
45785
45919
  ].join("\n"));
45786
45920
  } catch (e) {
@@ -48812,7 +48946,7 @@ function renderTrackToDirectResult(result) {
48812
48946
  }
48813
48947
 
48814
48948
  // src/schedules-commands.ts
48815
- var import_promises9 = require("node:fs/promises");
48949
+ var import_promises10 = require("node:fs/promises");
48816
48950
  init_clean_exit();
48817
48951
  init_github_client();
48818
48952
  init_cli_shared();
@@ -49006,9 +49140,9 @@ function registerSchedulesCommands(program3) {
49006
49140
  await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
49007
49141
  return;
49008
49142
  }
49009
- const docText = await (0, import_promises9.readFile)(o.doc, "utf8");
49143
+ const docText = await (0, import_promises10.readFile)(o.doc, "utf8");
49010
49144
  const spliced = spliceDoc(docText, renderDocSection(entries, now.toISOString()));
49011
- await (0, import_promises9.writeFile)(o.doc, spliced, "utf8");
49145
+ await (0, import_promises10.writeFile)(o.doc, spliced, "utf8");
49012
49146
  reportParked(parkedLines);
49013
49147
  reportDrift(drift);
49014
49148
  console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
@@ -49746,7 +49880,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
49746
49880
  // src/tenant-artifact.ts
49747
49881
  var import_node_crypto15 = require("node:crypto");
49748
49882
  var import_node_fs48 = require("node:fs");
49749
- var import_promises10 = require("node:fs/promises");
49883
+ var import_promises11 = require("node:fs/promises");
49750
49884
  var import_node_path46 = require("node:path");
49751
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}$/;
49752
49886
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
@@ -49758,7 +49892,7 @@ async function sha256File(path2) {
49758
49892
  async function putTenantArtifact(repo, stage, inputPath, deps) {
49759
49893
  if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
49760
49894
  const path2 = (0, import_node_path46.resolve)(inputPath);
49761
- const info = await (0, import_promises10.stat)(path2);
49895
+ const info = await (0, import_promises11.stat)(path2);
49762
49896
  if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
49763
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`);
49764
49898
  const sha2562 = await sha256File(path2);
@@ -50240,7 +50374,7 @@ init_github_client();
50240
50374
  init_hub_auth();
50241
50375
 
50242
50376
  // src/schedules-lift-command.ts
50243
- var import_promises11 = require("node:fs/promises");
50377
+ var import_promises12 = require("node:fs/promises");
50244
50378
  var import_node_path49 = require("node:path");
50245
50379
  init_clean_exit();
50246
50380
  init_cli_shared();
@@ -50263,14 +50397,14 @@ var RegistryUnreachableError = class extends Error {
50263
50397
  async function readWorkflowFiles(dir) {
50264
50398
  let names;
50265
50399
  try {
50266
- names = await (0, import_promises11.readdir)(dir);
50400
+ names = await (0, import_promises12.readdir)(dir);
50267
50401
  } catch {
50268
50402
  return [];
50269
50403
  }
50270
50404
  const files = [];
50271
50405
  for (const name of names.sort()) {
50272
50406
  if (!/\.ya?ml$/.test(name)) continue;
50273
- 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") });
50274
50408
  }
50275
50409
  return files;
50276
50410
  }
@@ -51997,6 +52131,7 @@ function ciAuditDeps2() {
51997
52131
  client: defaultGitHubClient(),
51998
52132
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
51999
52133
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
52134
+ getDeployFacts: async (slug) => fetchDeployFactsBySlug(slug, registryClientDeps(await cfgPromise)),
52000
52135
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
52001
52136
  // Resolve from the installed package root so the reconcile works from any org repo checkout; when the
52002
52137
  // Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.59",
3
+ "version": "4.5.61",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",