@mutmutco/codex-plugin 4.5.99 → 4.5.101

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.99",
3
+ "version": "4.5.101",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -6043,7 +6043,7 @@ function isRetryableGitHubWaitReadError(e) {
6043
6043
  const codes = /* @__PURE__ */ new Set();
6044
6044
  if (typeof status === "number" && status > 0) codes.add(status);
6045
6045
  for (const m of text.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) codes.add(Number(m[1]));
6046
- const rateLimited = e?.rateLimited === true || /API rate limit already exceeded|rate limit already exceeded|RATE_LIMITED|secondary rate|abuse detection/i.test(text);
6046
+ const rateLimited = e?.rateLimited === true || /rate limit (?:already )?exceeded|RATE_LIMITED|secondary rate|abuse detection/i.test(text);
6047
6047
  if ([401, 404, 409, 422].some((c) => codes.has(c))) return false;
6048
6048
  if (codes.has(403) && !rateLimited) return false;
6049
6049
  if (codes.has(429) || rateLimited) return true;
@@ -8288,6 +8288,28 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
8288
8288
  return { ok: false, status: 0, body: null, error: e.message, unreachable: "transport" };
8289
8289
  }
8290
8290
  }
8291
+ async function edgeDnsForStage(slug, stage, deps, ensure = false) {
8292
+ if (!deps.baseUrl) throw new Error("Hub API URL unavailable");
8293
+ const token = await deps.token();
8294
+ if (!token) throw new Error("Hub session token unavailable");
8295
+ const path2 = `/projects/${encodeURIComponent(slug)}/edge-dns/${stage}`;
8296
+ let body;
8297
+ if (ensure) {
8298
+ const write = await postJson(path2, {}, deps, "POST", { noRetry: true });
8299
+ if (!write.ok) throw new Error(`Hub edge dns ensure failed: ${write.error ?? `HTTP ${write.status}`}`);
8300
+ body = write.body;
8301
+ } else {
8302
+ const res = await retriedFetch(
8303
+ deps,
8304
+ `${deps.baseUrl.replace(/\/$/, "")}${path2}`,
8305
+ { method: "GET", headers: { Authorization: `Bearer ${token}` } }
8306
+ );
8307
+ if (!res.ok) throw new Error(`Hub edge dns read failed: HTTP ${res.status}`);
8308
+ body = await res.json();
8309
+ }
8310
+ if (typeof body?.required !== "boolean") throw new Error("Hub edge dns response invalid");
8311
+ return body;
8312
+ }
8291
8313
  async function edgeCertificateForStage(slug, stage, deps, ensure = false) {
8292
8314
  if (!deps.baseUrl) throw new Error("Hub API URL unavailable");
8293
8315
  const token = await deps.token();
@@ -15861,8 +15883,11 @@ function edgeDomainsByStage(meta) {
15861
15883
  }
15862
15884
  return out;
15863
15885
  }
15864
- function inferEdgeDomainsForWrite(slug) {
15886
+ function inferEdgeDomainsForWrite(slug, suite) {
15865
15887
  const subdomain = defaultSubdomain(slug);
15888
+ if (suite === "mm-work" && slug !== suite) {
15889
+ return { dev: `dev.${subdomain}.mutmut.work`, main: `${subdomain}.mutmut.work` };
15890
+ }
15866
15891
  return {
15867
15892
  dev: `dev.${subdomain}.mutatismutandis.co`,
15868
15893
  rc: `rc.${subdomain}.mutatismutandis.co`,
@@ -16236,10 +16261,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16236
16261
  }
16237
16262
  const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
16238
16263
  const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
16239
- const inferredDomains = inferEdgeDomainsForWrite(slug);
16240
- const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).map((branch) => {
16264
+ const suite = vars.SUITE || options.existing?.suite || void 0;
16265
+ const inferredDomains = inferEdgeDomainsForWrite(slug, suite);
16266
+ const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).flatMap((branch) => {
16241
16267
  const stage = branch === "development" ? "dev" : branch;
16242
- return [stage, inferredDomains[stage]];
16268
+ return inferredDomains[stage] ? [[stage, inferredDomains[stage]]] : [];
16243
16269
  }));
16244
16270
  const num = (v) => {
16245
16271
  if (v == null || v === "") return void 0;
@@ -16263,6 +16289,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16263
16289
  // Identity. name/division default off the repo name when the skill didn't pass them.
16264
16290
  name: vars.NAME || parsedRepo.name,
16265
16291
  division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
16292
+ suite: vars.SUITE || void 0,
16266
16293
  repos: [`${parsedRepo.owner}/${slug}`],
16267
16294
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
16268
16295
  class: cls,
@@ -16362,6 +16389,11 @@ function decideSeedPrAction(openPrs) {
16362
16389
  }
16363
16390
  return { action: "create" };
16364
16391
  }
16392
+ function decideFirstDevDeploy(input) {
16393
+ if (!input.devRowWritten) return "skip";
16394
+ if (input.seedMode === "direct") return "dispatch";
16395
+ return (input.seedPrs ?? []).some((pr) => typeof pr?.mergedAt === "string" && pr.mergedAt !== "") ? "dispatch" : "defer";
16396
+ }
16365
16397
  function decideSeedDelivery(branchRules) {
16366
16398
  const rules = Array.isArray(branchRules) ? branchRules : [];
16367
16399
  const types = new Set(
@@ -17572,10 +17604,10 @@ var rollout_plan_default = {
17572
17604
  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)."
17573
17605
  },
17574
17606
  baseline: {
17575
- version: "4.5.99",
17576
- tag: "v4.5.99",
17577
- commit: "44a40b83250e",
17578
- npm: "@mutmutco/cli@4.5.99"
17607
+ version: "4.5.101",
17608
+ tag: "v4.5.101",
17609
+ commit: "a3c82f0f7aa3",
17610
+ npm: "@mutmutco/cli@4.5.101"
17579
17611
  },
17580
17612
  exitCriterion: "fleet-n-of-n",
17581
17613
  hubOnlyShortcut: "forbidden",
@@ -17592,14 +17624,14 @@ var rollout_plan_default = {
17592
17624
  repo: "mutmutco/mmi-hub",
17593
17625
  role: "canary",
17594
17626
  schedule: "train",
17595
- v3Target: "v4.5.99"
17627
+ v3Target: "v4.5.101"
17596
17628
  }
17597
17629
  ],
17598
17630
  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.",
17599
17631
  rollback: {
17600
17632
  independent: true,
17601
- mechanism: "npm dist-tag latest -> 4.5.99 and redeploy the Hub Lambda from tag v4.5.99 (44a40b83250e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17602
- v3Target: "v4.5.99 (@mutmutco/cli@4.5.99, tag commit 44a40b83250e \u2014 last known-good release carrying the repo-index v4-only contract)"
17633
+ mechanism: "npm dist-tag latest -> 4.5.101 and redeploy the Hub Lambda from tag v4.5.101 (a3c82f0f7aa3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17634
+ v3Target: "v4.5.101 (@mutmutco/cli@4.5.101, tag commit a3c82f0f7aa3 \u2014 last known-good release carrying the repo-index v4-only contract)"
17603
17635
  }
17604
17636
  },
17605
17637
  {
@@ -21791,7 +21823,7 @@ async function runTenantControl(deps, options) {
21791
21823
  const watch = options.watch ?? tenantControlWatches(action);
21792
21824
  const base = { command: "tenant-control", repo, stage, action };
21793
21825
  const since = (deps.now ?? Date.now)();
21794
- const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
21826
+ const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, edge: options.edge, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
21795
21827
  if (!d.ok) {
21796
21828
  const transport = d.category === "transport-failed";
21797
21829
  return {
@@ -25644,29 +25676,33 @@ Cut by ${formatSessionIdentity(cutBy)}
25644
25676
  `;
25645
25677
  }
25646
25678
  var PLAIN_WORDS_HEADING = "## In plain words";
25679
+ var PLAIN_WORDS_HEADING_TR = "## Sade s\xF6zlerle";
25647
25680
  var PLAIN_WORDS_MARKER = "<!-- mmi:big -->";
25648
- function buildPlainWordsSection(lines2, big) {
25681
+ function buildPlainWordsSection(lines2, big, heading = PLAIN_WORDS_HEADING) {
25649
25682
  const [title, ...notes] = lines2;
25650
- const body = [PLAIN_WORDS_HEADING, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
25683
+ const body = [heading, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
25651
25684
  if (big) body.push(PLAIN_WORDS_MARKER);
25652
25685
  return body.join("\n");
25653
25686
  }
25654
25687
  function noChangePlainWordsLine(suiteTag) {
25655
- return `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.`;
25688
+ return suiteTag ? `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.` : "No changes to this module in this release.";
25656
25689
  }
25657
25690
  function upsertPlainWordsSection(body, section) {
25658
- const lines2 = body.split(/\r?\n/);
25659
- const at = lines2.findIndex((l) => l.trim() === PLAIN_WORDS_HEADING);
25660
25691
  let rest = body;
25661
- if (at >= 0) {
25662
- let end = lines2.length;
25663
- for (let i = at + 1; i < lines2.length; i++) {
25664
- if (lines2[i].trim().startsWith("## ")) {
25665
- end = i;
25666
- break;
25692
+ for (const heading of [PLAIN_WORDS_HEADING, PLAIN_WORDS_HEADING_TR]) {
25693
+ const lines2 = rest.split(/\r?\n/);
25694
+ let stripping = false;
25695
+ let found = false;
25696
+ const kept = lines2.filter((line) => {
25697
+ if (line.trim() === heading) {
25698
+ stripping = true;
25699
+ found = true;
25700
+ return false;
25667
25701
  }
25668
- }
25669
- rest = [...lines2.slice(0, at), ...lines2.slice(end)].join("\n");
25702
+ if (line.trim().startsWith("## ")) stripping = false;
25703
+ return !stripping;
25704
+ });
25705
+ if (found) rest = kept.join("\n");
25670
25706
  }
25671
25707
  const trimmedRest = rest.trim();
25672
25708
  return trimmedRest ? `${section}
@@ -25679,8 +25715,11 @@ function plainWordsSummaryLines(content) {
25679
25715
  async function writePlainWordsIfSuiteMember(deps, args) {
25680
25716
  if (args.meta?.suite !== "mm-work") return;
25681
25717
  try {
25682
- const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag ?? args.tag)];
25683
- const section = buildPlainWordsSection(lines2, args.big === true);
25718
+ const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag)];
25719
+ const section = [
25720
+ buildPlainWordsSection(lines2, args.big === true),
25721
+ ...args.summaryLinesTr?.length ? [buildPlainWordsSection(args.summaryLinesTr, false, PLAIN_WORDS_HEADING_TR)] : []
25722
+ ].join("\n\n");
25684
25723
  const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
25685
25724
  await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
25686
25725
  } catch {
@@ -25689,7 +25728,8 @@ async function writePlainWordsIfSuiteMember(deps, args) {
25689
25728
  async function writePlainWordsSection(deps, options, repo, meta, tag) {
25690
25729
  try {
25691
25730
  const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
25692
- await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, big: options.big, suiteTag: options.suiteTag });
25731
+ const summaryLinesTr = options.announceSummaryFileTr ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFileTr)) : void 0;
25732
+ await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, summaryLinesTr, big: options.big, suiteTag: options.suiteTag });
25693
25733
  } catch {
25694
25734
  }
25695
25735
  }
@@ -33890,6 +33930,28 @@ function edgeCertificateChecks(results) {
33890
33930
  }];
33891
33931
  });
33892
33932
  }
33933
+ function edgeDnsChecks(results, repo) {
33934
+ return results.flatMap(({ stage, status, error }) => {
33935
+ if (!error && status?.required === false) return [];
33936
+ const bad = (status?.hosts ?? []).filter((h) => h.status === "missing" || h.status === "conflict");
33937
+ const ok = !error && status?.complete !== false && bad.length === 0;
33938
+ return [{
33939
+ ok,
33940
+ label: `Cloudflare edge DNS: ${stage}`,
33941
+ detail: ok ? void 0 : error ? `DNS records unreadable (${error})` : `${bad.map((h) => `${h.host} ${h.status}${h.hint ? ` (${h.hint})` : ""}`).join("; ")} \u2014 run: mmi-cli devops runtime edge dns ensure ${repo} ${stage}`
33942
+ }];
33943
+ });
33944
+ }
33945
+ function firstDevDeployCheck(repo, facts, factsRead) {
33946
+ const dev = facts?.stages?.dev;
33947
+ if (!factsRead || dev?.present !== true) return [];
33948
+ const ok = Boolean(dev.deployedAt || dev.deployedTag);
33949
+ return [{
33950
+ ok,
33951
+ label: "first dev deploy happened",
33952
+ detail: ok ? void 0 : `DEPLOY#dev has never deployed \u2014 run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`
33953
+ }];
33954
+ }
33893
33955
  async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33894
33956
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
33895
33957
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
@@ -34012,6 +34074,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34012
34074
  if (check) checks.push(check);
34013
34075
  }
34014
34076
  checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
34077
+ checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
34015
34078
  const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
34016
34079
  if (portRangeCheck) checks.push(portRangeCheck);
34017
34080
  checks.push(...deployRowChecks(
@@ -34022,6 +34085,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34022
34085
  deps.deployFactsRead === true,
34023
34086
  deps.projectMeta?.suite
34024
34087
  ));
34088
+ checks.push(...firstDevDeployCheck(repo, deps.deployFacts, deps.deployFactsRead === true));
34025
34089
  const readme = await contentText(deps, repo, baseBranch, "README.md");
34026
34090
  checks.push({
34027
34091
  ok: readme !== null && readme.includes("## Agent context"),
@@ -34460,6 +34524,13 @@ function registerBootstrapCommands(program3) {
34460
34524
  return { stage, error: e.message };
34461
34525
  }
34462
34526
  })),
34527
+ edgeDns: await Promise.all(["dev", "rc", "main"].filter((stage) => deployFacts?.stages?.[stage]?.present).map(async (stage) => {
34528
+ try {
34529
+ return { stage, status: await edgeDnsForStage(slug, stage, reg) };
34530
+ } catch (e) {
34531
+ return { stage, error: e.message };
34532
+ }
34533
+ })),
34463
34534
  pushAllowlistOwners: verifyOwners,
34464
34535
  pushAllowlistOwnersRead: verifyOwnersRead,
34465
34536
  readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
@@ -34857,7 +34928,7 @@ function registerBootstrapCommands(program3) {
34857
34928
  github,
34858
34929
  deployFacts: controlDeployFacts,
34859
34930
  deployFactsRead: controlDeployFacts !== null,
34860
- plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug).main
34931
+ plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE).main
34861
34932
  });
34862
34933
  if (o.box) {
34863
34934
  if (applyDeployModel !== "tenant-container" && applyDeployModel !== "solo-container")
@@ -34878,7 +34949,7 @@ function registerBootstrapCommands(program3) {
34878
34949
  host: found.box.address,
34879
34950
  box: found.box.name,
34880
34951
  owners: scan.rows,
34881
- domains: { ...inferEdgeDomainsForWrite(slug), ...domains, ...boxDomains },
34952
+ domains: { ...inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE), ...domains, ...boxDomains },
34882
34953
  fileless: applyDeployModel === "tenant-container"
34883
34954
  });
34884
34955
  controlRows = planned.rows;
@@ -35309,6 +35380,42 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
35309
35380
  ddbWrites.push({ slug, action: `deploy-${coord.stage}`, record: payload });
35310
35381
  applied.push(`DEPLOY#${coord.stage} ${coord.host}:${coord.port}${coord.domain ? ` ${coord.domain}` : ""}`);
35311
35382
  }
35383
+ for (const coord of boxDeployPlan) {
35384
+ try {
35385
+ const dns = await edgeDnsForStage(slug, coord.stage, registryClientDeps(await loadConfig()), true);
35386
+ const bad = (dns.hosts ?? []).filter((h) => h.status === "conflict");
35387
+ applied.push(`edge dns ${coord.stage}: ${dns.required ? (dns.hosts ?? []).map((h) => `${h.host} ${h.status}`).join(", ") : "not required"}`);
35388
+ for (const h of bad) deferredFailures.push(`edge dns ${coord.stage} ${h.host}: ${h.hint}`);
35389
+ } catch (e) {
35390
+ applied.push(`edge dns ${coord.stage} (failed: ${e.message})`);
35391
+ deferredFailures.push(`edge dns ${coord.stage} not ensured: ${e.message}; re-run: mmi-cli devops runtime edge dns ensure ${repo} ${coord.stage}`);
35392
+ }
35393
+ }
35394
+ let seedPrs = [];
35395
+ if (seedPlan.mode === "pr" && seedPlan.branch) {
35396
+ try {
35397
+ seedPrs = JSON.parse((await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,mergedAt"])).stdout || "[]");
35398
+ } catch {
35399
+ seedPrs = [];
35400
+ }
35401
+ }
35402
+ const firstDeploy = decideFirstDevDeploy({ devRowWritten: boxDeployPlan.some((c) => c.stage === "dev"), seedMode: seedPlan.mode, seedPrs });
35403
+ if (firstDeploy === "defer") {
35404
+ applied.push("first dev deploy: deferred \u2014 the seed PR is not merged; the Hub dev auto-deploy runs on its merge (DEPLOY#dev now exists)");
35405
+ } else if (firstDeploy === "dispatch") {
35406
+ try {
35407
+ const out = await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(["runtime", "tenant", "redeploy", repo, "dev", "--json"])], { timeout: 12e4 });
35408
+ let run = {};
35409
+ try {
35410
+ run = JSON.parse(out.stdout);
35411
+ } catch {
35412
+ }
35413
+ applied.push(`first dev deploy dispatched at development: ${run.runUrl ?? run.dispatch ?? "run not yet correlated"}`);
35414
+ } catch (e) {
35415
+ applied.push(`first dev deploy (failed: ${e.message.split("\n")[0]})`);
35416
+ deferredFailures.push(`first dev deploy not dispatched; run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`);
35417
+ }
35418
+ }
35312
35419
  controlRows = controlRows.map((row) => {
35313
35420
  const coord = boxDeployPlan.find((p) => row.target === `DEPLOY#${p.stage}`);
35314
35421
  return coord ? {
@@ -47419,7 +47526,7 @@ function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
47419
47526
  return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
47420
47527
  }
47421
47528
  var suiteStateDir = (root) => (0, import_node_path43.join)(resolvePrimaryGitDir(root), "mmi-runtime", "suite");
47422
- function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47529
+ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr, memberSummaryFilesTr = {}) {
47423
47530
  const recordPath = (0, import_node_path43.join)(suiteStateDir(root), "cut.json");
47424
47531
  const base = (0, import_node_path43.join)(suiteStateDir(root), "checkouts");
47425
47532
  const checkout = async (repo) => {
@@ -47530,7 +47637,12 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47530
47637
  });
47531
47638
  const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
47532
47639
  await prepareRcCheckout((args) => deps.run("git", args), repo);
47533
- const plainWords = { announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles), big: opts?.big, suiteTag: opts?.suiteTag };
47640
+ const plainWords = {
47641
+ announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles),
47642
+ announceSummaryFileTr: memberAnnounceSummaryFile(repo, summaryFileTr, memberSummaryFilesTr),
47643
+ big: opts?.big,
47644
+ suiteTag: opts?.suiteTag
47645
+ };
47534
47646
  const current = await tagSha(repo, expected);
47535
47647
  if (current) {
47536
47648
  await deps.run("git", ["fetch", "origin", "--tags"]);
@@ -47771,6 +47883,12 @@ function parseMemberSummaryFiles(entries) {
47771
47883
  }
47772
47884
  return memberSummaryFiles;
47773
47885
  }
47886
+ function validateSuiteReleaseSummary(path2) {
47887
+ if (!path2) throw new Error("suite release requires --announce-summary-file before promoting any member");
47888
+ const lines2 = (0, import_node_fs44.readFileSync)(path2, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
47889
+ if (lines2[0]?.startsWith("No changes to this module")) throw new Error("suite release requires a real MM-Work title, not the no-change placeholder");
47890
+ if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
47891
+ }
47774
47892
  function suiteRcDataProdDeps() {
47775
47893
  return {
47776
47894
  fetch: async (url, init) => {
@@ -47865,18 +47983,17 @@ function registerSuiteCommands(program3) {
47865
47983
  return fail(`suite abandon: ${e.message}`);
47866
47984
  }
47867
47985
  });
47868
- 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) => {
47986
+ 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("--announce-summary-file-tr <path>", "Turkish plain-words summary for MM-Work itself").option("--member-summary-file-tr <owner/repo=path>", "repeatable: a member's own Turkish plain-words summary", (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) => {
47869
47987
  try {
47870
47988
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
47871
- if (!o.announceSummaryFile) throw new Error("suite release requires --announce-summary-file before promoting any member");
47872
- const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
47873
- if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
47989
+ validateSuiteReleaseSummary(o.announceSummaryFile);
47874
47990
  const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
47991
+ const memberSummaryFilesTr = parseMemberSummaryFiles(o.memberSummaryFileTr ?? []);
47875
47992
  const root = await leadRoot();
47876
47993
  const unlock = await acquireTrainWriterLock(root, "suite release");
47877
47994
  let result;
47878
47995
  try {
47879
- result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
47996
+ result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles, o.announceSummaryFileTr, memberSummaryFilesTr), true, o.big === true);
47880
47997
  } finally {
47881
47998
  await unlock();
47882
47999
  }
@@ -53143,6 +53260,17 @@ function registerEdgeCommands(program3) {
53143
53260
  process.exitCode = 1;
53144
53261
  }
53145
53262
  });
53263
+ edge.command("dns <action> <owner/repo> <stage>").description("read (`status`) or create-when-missing (`ensure`, master-only) the proxied Cloudflare A records for a registered DEPLOY# row; existing records are never modified").action(async (action, repo, stage) => {
53264
+ if (action !== "status" && action !== "ensure") return fail("runtime edge dns: <action> must be status or ensure");
53265
+ if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime edge dns: <stage> must be dev, rc, or main");
53266
+ try {
53267
+ const result = await edgeDnsForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
53268
+ console.log(JSON.stringify(result, null, 2));
53269
+ if (result.required && result.complete === false) process.exitCode = 1;
53270
+ } catch (e) {
53271
+ fail(`runtime edge dns: ${e.message}`);
53272
+ }
53273
+ });
53146
53274
  }
53147
53275
 
53148
53276
  // src/command-register-developer.ts
@@ -54206,7 +54334,7 @@ async function reportWrite(label, res) {
54206
54334
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
54207
54335
  }
54208
54336
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
54209
- tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
54337
+ tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--edge", "logs only: Caddy journal for this stage\u2019s hosts (default window: last 15 minutes)").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
54210
54338
  try {
54211
54339
  if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
54212
54340
  if (action === "retire" && (stage === "dev" || stage === "main")) {
@@ -54219,6 +54347,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
54219
54347
  return;
54220
54348
  }
54221
54349
  }
54350
+ if (o.edge && action !== "logs") return fail("runtime tenant control: --edge is valid only for the logs action");
54222
54351
  let lines2;
54223
54352
  if (o.lines !== void 0) {
54224
54353
  if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
@@ -54235,7 +54364,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
54235
54364
  } else if (o.task || o.artifact) {
54236
54365
  return fail("runtime tenant control: --task/--artifact are valid only for run-task");
54237
54366
  }
54238
- const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
54367
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, edge: o.edge, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
54239
54368
  if (!o.json && action === "verify-secrets" && result.secrets) {
54240
54369
  const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
54241
54370
  const { lines: lines3, failure } = renderVerifySecrets(body);
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.99",
3
+ "version": "4.5.101",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.99",
3
+ "version": "4.5.101",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -481,6 +481,9 @@ web tenants, `--project-type desktop-game --deploy-model none --clear-web-profil
481
481
  `--project-type mobile-app --deploy-model none --clear-web-profile` for a phone app distributed through the
482
482
  app stores, and
483
483
  `--class content --project-type content --deploy-model content --clear-web-profile` for a content/KB repo.
484
+ An **MM-Work module** adds `--var SUITE=mm-work` (#7944): it registers `suite: mm-work` and derives its
485
+ own `dev.<sub>.mutmut.work` + `<sub>.mutmut.work` hosts, with no rc host (the suite serves its rc from the
486
+ live host). Without it, apply derives the `*.mutatismutandis.co` web-app hosts, which a module never uses.
484
487
  Run the apply path with the board variables discovered above, or register the same values with
485
488
  `mmi-cli oracle org project set` from the Hub or from the target project checkout:
486
489
 
@@ -4,9 +4,13 @@ MMI owns the central project registry and provisioning workflows. Read
4
4
  `mmi-cli oracle org project get <owner/repo> --json` before changing hostnames or login settings.
5
5
  Use the declared edge domains for each stage; do not guess hostnames from a slug or create a
6
6
  second DNS/deployment control plane in a product repository. New project setup goes through
7
- [bootstrap](../../bootstrap/SKILL.md).
7
+ [bootstrap](../../bootstrap/SKILL.md). An MM-Work suite module serves from its own
8
+ `dev.<name>.mutmut.work` and `<name>.mutmut.work` hosts, with no rc host; bootstrap declares them
9
+ with `--var SUITE=mm-work`.
8
10
 
9
11
  Cloudflare owns public DNS and edge TLS. Preserve unrelated records during an authorized change.
12
+ The Hub creates each registered host's proxied record during bootstrap and never edits an existing
13
+ one; check or re-run it with `mmi-cli devops runtime edge dns status|ensure <owner/repo> <stage>`.
10
14
  A dev stage is private only after its independent edge-block check succeeds. A configured record
11
15
  or successful deployment alone does not prove access control. Use [stage](../../stage/SKILL.md)
12
16
  for the supported private live workflow.
@@ -109,6 +109,7 @@ commit, so build and verify the lines from one pass over that range —
109
109
  subject, body and changed files — never from subjects, PR titles or memory; a line whose claim is not
110
110
  in that output does not go in the file. A Hub summary never mentions a Jerv-side project; the train
111
111
  refuses such a line.
112
+ MM-Work suite release also requires a real `--announce-summary-file` title (never `No changes to this module`); optional `--announce-summary-file-tr` and repeatable `--member-summary-file-tr owner/repo=path` add Turkish plain words alongside English member files supplied with `--member-summary-file`.
112
113
 
113
114
  Announcement delivery uses the developer's GitHub-authenticated Hub session. The Hub checks project
114
115
  authority, resolves the registered channel, and reads the MMI Future Slack key inside its vault boundary.
@@ -15,6 +15,9 @@ Full-track repos ship exactly what is on `rc` (never `development`): merge `rc
15
15
 
16
16
  In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: run `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` instead (a lead-only cut needs `--suite-solo --owner-go`). A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync.
17
17
 
18
+ Suite release requires `--announce-summary-file <path>` with 3–6 curated lines and refuses before promotion when it is missing or its title starts with `No changes to this module`.
19
+ Use `--member-summary-file <owner/repo=path>` for English member notes, `--announce-summary-file-tr <path>` for MM-Work's Turkish notes and repeatable `--member-summary-file-tr <owner/repo=path>` for Turkish member notes; members without English files keep the no-change line.
20
+
18
21
  Read META and name the lane before describing or executing anything:
19
22
 
20
23
  ```bash