@mutmutco/codex-plugin 4.5.100 → 4.5.102

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.100",
3
+ "version": "4.5.102",
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;
@@ -6327,6 +6327,24 @@ function parseRateLimitHeaders(out) {
6327
6327
  };
6328
6328
  return { remaining: first("remaining"), limit: first("limit"), resetSec: first("reset") };
6329
6329
  }
6330
+ function describeRefusal(out) {
6331
+ const header = (name) => new RegExp(`^${name}:\\s*(.+?)\\s*$`, "im").exec(out)?.[1];
6332
+ let message2 = "";
6333
+ const body = out.slice(out.search(/\r?\n\r?\n/) + 1).trim();
6334
+ try {
6335
+ message2 = String(JSON.parse(body).message ?? "");
6336
+ } catch {
6337
+ message2 = body;
6338
+ }
6339
+ const parts = [
6340
+ ["resource", header("x-ratelimit-resource")],
6341
+ ["remaining", header("x-ratelimit-remaining")],
6342
+ ["reset", header("x-ratelimit-reset")],
6343
+ ["retry-after", header("retry-after")]
6344
+ ].filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${v}`);
6345
+ const text = message2.replace(/\s+/g, " ").slice(0, 160);
6346
+ return [parts.join(" "), text ? `"${text}"` : ""].filter(Boolean).join(" ") || "no rate-limit headers or message";
6347
+ }
6330
6348
  var etagCache = /* @__PURE__ */ new Map();
6331
6349
  function appPoolDetail() {
6332
6350
  const guard = appPollGuardSnapshot();
@@ -6360,7 +6378,7 @@ async function defaultGhApi(args) {
6360
6378
  }
6361
6379
  if (source === "app" && appToken && (status === 403 || status === 429)) {
6362
6380
  noteAppPollResponse(parseRateLimitHeaders(errOut), true, Date.now());
6363
- console.warn(`poll credential: App pool refused the read (HTTP ${status}, ${appPoolDetail()}) \u2014 REST poll reads switch to the ambient user token until the App pool resets (#7843)`);
6381
+ console.warn(`poll credential: App pool refused the read (HTTP ${status}, ${appPoolDetail()}; response: ${describeRefusal(errOut)}) \u2014 REST poll reads switch to the ambient user token until the App pool resets (#7843)`);
6364
6382
  lastPollSource = "user";
6365
6383
  source = "user";
6366
6384
  continue;
@@ -17604,10 +17622,10 @@ var rollout_plan_default = {
17604
17622
  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)."
17605
17623
  },
17606
17624
  baseline: {
17607
- version: "4.5.100",
17608
- tag: "v4.5.100",
17609
- commit: "ba367dd0d814",
17610
- npm: "@mutmutco/cli@4.5.100"
17625
+ version: "4.5.102",
17626
+ tag: "v4.5.102",
17627
+ commit: "69ad9c05771f",
17628
+ npm: "@mutmutco/cli@4.5.102"
17611
17629
  },
17612
17630
  exitCriterion: "fleet-n-of-n",
17613
17631
  hubOnlyShortcut: "forbidden",
@@ -17624,14 +17642,14 @@ var rollout_plan_default = {
17624
17642
  repo: "mutmutco/mmi-hub",
17625
17643
  role: "canary",
17626
17644
  schedule: "train",
17627
- v3Target: "v4.5.100"
17645
+ v3Target: "v4.5.102"
17628
17646
  }
17629
17647
  ],
17630
17648
  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.",
17631
17649
  rollback: {
17632
17650
  independent: true,
17633
- mechanism: "npm dist-tag latest -> 4.5.100 and redeploy the Hub Lambda from tag v4.5.100 (ba367dd0d814); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17634
- v3Target: "v4.5.100 (@mutmutco/cli@4.5.100, tag commit ba367dd0d814 \u2014 last known-good release carrying the repo-index v4-only contract)"
17651
+ mechanism: "npm dist-tag latest -> 4.5.102 and redeploy the Hub Lambda from tag v4.5.102 (69ad9c05771f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17652
+ v3Target: "v4.5.102 (@mutmutco/cli@4.5.102, tag commit 69ad9c05771f \u2014 last known-good release carrying the repo-index v4-only contract)"
17635
17653
  }
17636
17654
  },
17637
17655
  {
@@ -21823,7 +21841,7 @@ async function runTenantControl(deps, options) {
21823
21841
  const watch = options.watch ?? tenantControlWatches(action);
21824
21842
  const base = { command: "tenant-control", repo, stage, action };
21825
21843
  const since = (deps.now ?? Date.now)();
21826
- 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 });
21844
+ 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 });
21827
21845
  if (!d.ok) {
21828
21846
  const transport = d.category === "transport-failed";
21829
21847
  return {
@@ -25676,29 +25694,33 @@ Cut by ${formatSessionIdentity(cutBy)}
25676
25694
  `;
25677
25695
  }
25678
25696
  var PLAIN_WORDS_HEADING = "## In plain words";
25697
+ var PLAIN_WORDS_HEADING_TR = "## Sade s\xF6zlerle";
25679
25698
  var PLAIN_WORDS_MARKER = "<!-- mmi:big -->";
25680
- function buildPlainWordsSection(lines2, big) {
25699
+ function buildPlainWordsSection(lines2, big, heading = PLAIN_WORDS_HEADING) {
25681
25700
  const [title, ...notes] = lines2;
25682
- const body = [PLAIN_WORDS_HEADING, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
25701
+ const body = [heading, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
25683
25702
  if (big) body.push(PLAIN_WORDS_MARKER);
25684
25703
  return body.join("\n");
25685
25704
  }
25686
25705
  function noChangePlainWordsLine(suiteTag) {
25687
- return `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.`;
25706
+ 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.";
25688
25707
  }
25689
25708
  function upsertPlainWordsSection(body, section) {
25690
- const lines2 = body.split(/\r?\n/);
25691
- const at = lines2.findIndex((l) => l.trim() === PLAIN_WORDS_HEADING);
25692
25709
  let rest = body;
25693
- if (at >= 0) {
25694
- let end = lines2.length;
25695
- for (let i = at + 1; i < lines2.length; i++) {
25696
- if (lines2[i].trim().startsWith("## ")) {
25697
- end = i;
25698
- break;
25710
+ for (const heading of [PLAIN_WORDS_HEADING, PLAIN_WORDS_HEADING_TR]) {
25711
+ const lines2 = rest.split(/\r?\n/);
25712
+ let stripping = false;
25713
+ let found = false;
25714
+ const kept = lines2.filter((line) => {
25715
+ if (line.trim() === heading) {
25716
+ stripping = true;
25717
+ found = true;
25718
+ return false;
25699
25719
  }
25700
- }
25701
- rest = [...lines2.slice(0, at), ...lines2.slice(end)].join("\n");
25720
+ if (line.trim().startsWith("## ")) stripping = false;
25721
+ return !stripping;
25722
+ });
25723
+ if (found) rest = kept.join("\n");
25702
25724
  }
25703
25725
  const trimmedRest = rest.trim();
25704
25726
  return trimmedRest ? `${section}
@@ -25711,8 +25733,11 @@ function plainWordsSummaryLines(content) {
25711
25733
  async function writePlainWordsIfSuiteMember(deps, args) {
25712
25734
  if (args.meta?.suite !== "mm-work") return;
25713
25735
  try {
25714
- const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag ?? args.tag)];
25715
- const section = buildPlainWordsSection(lines2, args.big === true);
25736
+ const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag)];
25737
+ const section = [
25738
+ buildPlainWordsSection(lines2, args.big === true),
25739
+ ...args.summaryLinesTr?.length ? [buildPlainWordsSection(args.summaryLinesTr, false, PLAIN_WORDS_HEADING_TR)] : []
25740
+ ].join("\n\n");
25716
25741
  const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
25717
25742
  await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
25718
25743
  } catch {
@@ -25721,7 +25746,8 @@ async function writePlainWordsIfSuiteMember(deps, args) {
25721
25746
  async function writePlainWordsSection(deps, options, repo, meta, tag) {
25722
25747
  try {
25723
25748
  const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
25724
- await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, big: options.big, suiteTag: options.suiteTag });
25749
+ const summaryLinesTr = options.announceSummaryFileTr ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFileTr)) : void 0;
25750
+ await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, summaryLinesTr, big: options.big, suiteTag: options.suiteTag });
25725
25751
  } catch {
25726
25752
  }
25727
25753
  }
@@ -46580,8 +46606,11 @@ function trainApplyDeps() {
46580
46606
  // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
46581
46607
  // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
46582
46608
  // outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
46583
- dispatchTenantControl: async ({ repo, stage, action, lines: lines2, since, until, task, artifact, confirmDataLoss }) => {
46584
- const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...since ? { since } : {}, ...until ? { until } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
46609
+ // #7961: forward every SET field rather than re-listing them, so a new option (e.g. `edge`) cannot be
46610
+ // dropped silently. Unset/false fields are omitted: the route rejects e.g. `edge` on a non-logs action.
46611
+ dispatchTenantControl: async (input) => {
46612
+ const payload = Object.fromEntries(Object.entries(input).filter(([, v]) => v != null && v !== "" && v !== false));
46613
+ const res = await tenantControl(payload, registryClientDeps(await loadConfig()));
46585
46614
  if (res.ok) return { ok: true };
46586
46615
  const body = res.body;
46587
46616
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
@@ -47518,7 +47547,7 @@ function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
47518
47547
  return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
47519
47548
  }
47520
47549
  var suiteStateDir = (root) => (0, import_node_path43.join)(resolvePrimaryGitDir(root), "mmi-runtime", "suite");
47521
- function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47550
+ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr, memberSummaryFilesTr = {}) {
47522
47551
  const recordPath = (0, import_node_path43.join)(suiteStateDir(root), "cut.json");
47523
47552
  const base = (0, import_node_path43.join)(suiteStateDir(root), "checkouts");
47524
47553
  const checkout = async (repo) => {
@@ -47629,7 +47658,12 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47629
47658
  });
47630
47659
  const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
47631
47660
  await prepareRcCheckout((args) => deps.run("git", args), repo);
47632
- const plainWords = { announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles), big: opts?.big, suiteTag: opts?.suiteTag };
47661
+ const plainWords = {
47662
+ announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles),
47663
+ announceSummaryFileTr: memberAnnounceSummaryFile(repo, summaryFileTr, memberSummaryFilesTr),
47664
+ big: opts?.big,
47665
+ suiteTag: opts?.suiteTag
47666
+ };
47633
47667
  const current = await tagSha(repo, expected);
47634
47668
  if (current) {
47635
47669
  await deps.run("git", ["fetch", "origin", "--tags"]);
@@ -47870,6 +47904,12 @@ function parseMemberSummaryFiles(entries) {
47870
47904
  }
47871
47905
  return memberSummaryFiles;
47872
47906
  }
47907
+ function validateSuiteReleaseSummary(path2) {
47908
+ if (!path2) throw new Error("suite release requires --announce-summary-file before promoting any member");
47909
+ const lines2 = (0, import_node_fs44.readFileSync)(path2, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
47910
+ 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");
47911
+ if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
47912
+ }
47873
47913
  function suiteRcDataProdDeps() {
47874
47914
  return {
47875
47915
  fetch: async (url, init) => {
@@ -47964,18 +48004,17 @@ function registerSuiteCommands(program3) {
47964
48004
  return fail(`suite abandon: ${e.message}`);
47965
48005
  }
47966
48006
  });
47967
- 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) => {
48007
+ 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) => {
47968
48008
  try {
47969
48009
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
47970
- if (!o.announceSummaryFile) throw new Error("suite release requires --announce-summary-file before promoting any member");
47971
- const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
47972
- if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
48010
+ validateSuiteReleaseSummary(o.announceSummaryFile);
47973
48011
  const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
48012
+ const memberSummaryFilesTr = parseMemberSummaryFiles(o.memberSummaryFileTr ?? []);
47974
48013
  const root = await leadRoot();
47975
48014
  const unlock = await acquireTrainWriterLock(root, "suite release");
47976
48015
  let result;
47977
48016
  try {
47978
- result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
48017
+ result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles, o.announceSummaryFileTr, memberSummaryFilesTr), true, o.big === true);
47979
48018
  } finally {
47980
48019
  await unlock();
47981
48020
  }
@@ -54316,7 +54355,7 @@ async function reportWrite(label, res) {
54316
54355
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
54317
54356
  }
54318
54357
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
54319
- 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) => {
54358
+ 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) => {
54320
54359
  try {
54321
54360
  if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
54322
54361
  if (action === "retire" && (stage === "dev" || stage === "main")) {
@@ -54329,6 +54368,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
54329
54368
  return;
54330
54369
  }
54331
54370
  }
54371
+ if (o.edge && action !== "logs") return fail("runtime tenant control: --edge is valid only for the logs action");
54332
54372
  let lines2;
54333
54373
  if (o.lines !== void 0) {
54334
54374
  if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
@@ -54345,7 +54385,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
54345
54385
  } else if (o.task || o.artifact) {
54346
54386
  return fail("runtime tenant control: --task/--artifact are valid only for run-task");
54347
54387
  }
54348
- 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 });
54388
+ 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 });
54349
54389
  if (!o.json && action === "verify-secrets" && result.secrets) {
54350
54390
  const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
54351
54391
  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.100",
3
+ "version": "4.5.102",
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.100",
3
+ "version": "4.5.102",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -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