@mutmutco/claude-plugin 4.5.134 → 4.5.136

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,7 +1,7 @@
1
1
  {
2
2
  "name": "mmi",
3
3
  "displayName": "MMI",
4
- "version": "4.5.134",
4
+ "version": "4.5.136",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -17509,6 +17509,7 @@ function assertGitBashOnWindowsForJervHubRelease(repo, platform2 = process.platf
17509
17509
 
17510
17510
  // src/train-apply-correlation.ts
17511
17511
  var import_node_fs19 = require("node:fs");
17512
+ init_github_quota();
17512
17513
  var import_node_path17 = require("node:path");
17513
17514
 
17514
17515
  // src/fleet-lockstep-enforce.ts
@@ -18274,10 +18275,10 @@ var rollout_plan_default = {
18274
18275
  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)."
18275
18276
  },
18276
18277
  baseline: {
18277
- version: "4.5.134",
18278
- tag: "v4.5.134",
18279
- commit: "f976ea4f8325",
18280
- npm: "@mutmutco/cli@4.5.134"
18278
+ version: "4.5.136",
18279
+ tag: "v4.5.136",
18280
+ commit: "642526387569",
18281
+ npm: "@mutmutco/cli@4.5.136"
18281
18282
  },
18282
18283
  exitCriterion: "fleet-n-of-n",
18283
18284
  hubOnlyShortcut: "forbidden",
@@ -18294,14 +18295,14 @@ var rollout_plan_default = {
18294
18295
  repo: "mutmutco/mmi-hub",
18295
18296
  role: "canary",
18296
18297
  schedule: "train",
18297
- v3Target: "v4.5.134"
18298
+ v3Target: "v4.5.136"
18298
18299
  }
18299
18300
  ],
18300
18301
  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.",
18301
18302
  rollback: {
18302
18303
  independent: true,
18303
- mechanism: "npm dist-tag latest -> 4.5.134 and redeploy the Hub Lambda from tag v4.5.134 (f976ea4f8325); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18304
- v3Target: "v4.5.134 (@mutmutco/cli@4.5.134, tag commit f976ea4f8325 \u2014 last known-good release carrying the repo-index v4-only contract)"
18304
+ mechanism: "npm dist-tag latest -> 4.5.136 and redeploy the Hub Lambda from tag v4.5.136 (642526387569); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18305
+ v3Target: "v4.5.136 (@mutmutco/cli@4.5.136, tag commit 642526387569 \u2014 last known-good release carrying the repo-index v4-only contract)"
18305
18306
  }
18306
18307
  },
18307
18308
  {
@@ -20419,11 +20420,22 @@ async function loadReleaseTrackBranchHints(deps) {
20419
20420
  ]);
20420
20421
  return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
20421
20422
  }
20423
+ async function readTrainLogin(deps, waitMs = 18e4, attempts = 5) {
20424
+ for (let i = 1; ; i++) {
20425
+ try {
20426
+ return requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
20427
+ } catch (e) {
20428
+ if (i >= attempts || !isRateLimitText(e instanceof Error ? e.message : String(e))) throw e;
20429
+ deps.warn?.(`GitHub login read rate-limited; retrying in ${waitMs / 6e4} min (${i}/${attempts - 1})`);
20430
+ await resolveSleep(deps)(waitMs);
20431
+ }
20432
+ }
20433
+ }
20422
20434
  async function buildTrainApplyContext(deps) {
20423
20435
  const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
20424
20436
  const [owner, name] = repo.split("/");
20425
20437
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
20426
- const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
20438
+ const login = await readTrainLogin(deps);
20427
20439
  const verdict = await deps.trainAuthority(repo);
20428
20440
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
20429
20441
  if (!verdict.train) {
@@ -22443,7 +22455,7 @@ async function runTenantRedeploy(deps, options) {
22443
22455
  const repo = options.repo;
22444
22456
  const [owner, name] = repo.split("/");
22445
22457
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
22446
- const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
22458
+ const login = await readTrainLogin(deps);
22447
22459
  const verdict = await deps.trainAuthority(repo);
22448
22460
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
22449
22461
  if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
@@ -26417,36 +26429,75 @@ async function collectReleaseCredits(deps, repo, body) {
26417
26429
  return [];
26418
26430
  }
26419
26431
  }
26420
- function buildPlainWordsSection(lines2, big, heading = PLAIN_WORDS_HEADING, credits = []) {
26421
- const [title, ...notes] = lines2;
26422
- const body = [heading, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
26432
+ var UPDATE_SECTIONS = {
26433
+ en: { fixed: "Fixed", new: "New" },
26434
+ tr: { fixed: "D\xFCzeldi", new: "Yeni" }
26435
+ };
26436
+ var UPDATE_LINE_MAX = 120;
26437
+ var UPDATE_SECTION_MAX = 8;
26438
+ var UPDATE_LINE = /^[^:\s][^:]{0,39}: \S.*\.$/u;
26439
+ var UPDATE_FORBIDDEN = /#\d|\bv?\d+\.\d+\.\d+|^(?:feat|fix|chore|refactor|docs|test|ci|build|perf|style)\b/;
26440
+ function parseUpdateNotes(content, lang = "en") {
26441
+ const words = UPDATE_SECTIONS[lang];
26442
+ const lines2 = content.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
26443
+ const notes = { fixed: [], new: [] };
26444
+ let section;
26445
+ const seen = /* @__PURE__ */ new Set();
26446
+ for (const line of lines2) {
26447
+ const key = line === words.fixed ? "fixed" : line === words.new ? "new" : void 0;
26448
+ if (key) {
26449
+ if (seen.has(key) || key === "fixed" && seen.has("new")) throw new Error(`${lang} summary: "${words.fixed}" then "${words.new}", each at most once \u2014 see ${SUMMARY_GUIDE}`);
26450
+ seen.add(key);
26451
+ section = key;
26452
+ continue;
26453
+ }
26454
+ if (!section) throw new Error(`${lang} summary: start with "${words.fixed}" or "${words.new}" on its own line \u2014 see ${SUMMARY_GUIDE}`);
26455
+ if (line.length > UPDATE_LINE_MAX || !UPDATE_LINE.test(line) || UPDATE_FORBIDDEN.test(line)) {
26456
+ throw new Error(`${lang} summary line must read "<Area>: <sentence>." in at most ${UPDATE_LINE_MAX} characters, with no issue numbers, versions or commit prefixes: "${line}" \u2014 see ${SUMMARY_GUIDE}`);
26457
+ }
26458
+ notes[section].push(line);
26459
+ }
26460
+ for (const key of seen) {
26461
+ const n = notes[key].length;
26462
+ if (n === 0 || n > UPDATE_SECTION_MAX) throw new Error(`${lang} summary: each section needs 1\u2013${UPDATE_SECTION_MAX} lines (found ${n}) \u2014 see ${SUMMARY_GUIDE}`);
26463
+ }
26464
+ if (!seen.size) throw new Error(`${lang} summary needs a "${words.fixed}" or "${words.new}" section \u2014 see ${SUMMARY_GUIDE}`);
26465
+ return notes;
26466
+ }
26467
+ function buildPlainWordsSection(notes, big, lang = "en", credits = []) {
26468
+ const words = UPDATE_SECTIONS[lang];
26469
+ const body = [lang === "en" ? PLAIN_WORDS_HEADING : PLAIN_WORDS_HEADING_TR];
26470
+ for (const key of ["fixed", "new"]) {
26471
+ if (notes[key].length) body.push(`### ${words[key]}`, ...notes[key].map((n) => `- ${n}`));
26472
+ }
26423
26473
  if (big) body.push(PLAIN_WORDS_MARKER);
26424
26474
  const ids = [...new Set(credits.filter((id) => REPORT_ID.test(id)))].sort().slice(0, 20);
26425
26475
  if (ids.length) body.push(`<!-- mmi:credits ${ids.join(",")} -->`);
26426
26476
  return body.join("\n");
26427
26477
  }
26428
26478
  var SUMMARY_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing";
26429
- function validatePlainWordsSummary(content) {
26430
- const lines2 = content.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
26431
- if (lines2[0]?.startsWith("No changes to this module")) throw new Error(`requires a real MM-Work title, not the no-change placeholder \u2014 see ${SUMMARY_GUIDE}`);
26432
- if (lines2.length < 3 || lines2.length > 6) throw new Error(`requires 3\u20136 curated non-empty lines (title + 2\u20135 notes; found ${lines2.length}) \u2014 see ${SUMMARY_GUIDE}`);
26433
- }
26434
- function validateSuiteReleaseSummary(path2) {
26435
- if (!path2) throw new Error(`requires --announce-summary-file <path> before promoting any member \u2014 see ${SUMMARY_GUIDE}`);
26436
- let content;
26437
- try {
26438
- content = (0, import_node_fs25.readFileSync)(path2, "utf8");
26439
- } catch (e) {
26440
- throw new Error(`could not read summary file ${path2}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
26479
+ var SUMMARY_FLAG = { en: "--announce-summary-file", tr: "--announce-summary-file-tr" };
26480
+ function validateSuiteReleaseSummary(path2, pathTr) {
26481
+ const files = [["en", path2], ["tr", pathTr]];
26482
+ for (const [lang, file] of files) {
26483
+ if (!file) throw new Error(`requires ${SUMMARY_FLAG[lang]} <path> before promoting any member \u2014 see ${SUMMARY_GUIDE}`);
26484
+ }
26485
+ for (const [lang, file] of files) {
26486
+ let content;
26487
+ try {
26488
+ content = (0, import_node_fs25.readFileSync)(file, "utf8");
26489
+ } catch (e) {
26490
+ throw new Error(`could not read summary file ${file}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
26491
+ }
26492
+ parseUpdateNotes(content, lang);
26441
26493
  }
26442
- validatePlainWordsSummary(content);
26443
26494
  }
26444
- async function validateReleaseSummaries(releases, files, releaseExists) {
26495
+ async function validateReleaseSummaries(releases, files, filesTr, releaseExists) {
26445
26496
  const refusals = [];
26446
26497
  for (const { repo, tag } of releases) {
26447
26498
  if (await releaseExists(repo, tag)) continue;
26448
26499
  try {
26449
- validateSuiteReleaseSummary(files[repo]);
26500
+ validateSuiteReleaseSummary(files[repo], filesTr[repo]);
26450
26501
  } catch (e) {
26451
26502
  refusals.push(`${repo}: ${e.message}`);
26452
26503
  }
@@ -26454,15 +26505,20 @@ async function validateReleaseSummaries(releases, files, releaseExists) {
26454
26505
  if (refusals.length) throw new Error(`suite release summaries refused; no member was released:
26455
26506
  ${refusals.join("\n")}`);
26456
26507
  }
26457
- async function requireSuiteReleaseSummary(deps, path2) {
26458
- if (!path2) throw new Error(`a new mm-work suite Release requires --announce-summary-file <path> with a title + 2\u20135 notes \u2014 see ${SUMMARY_GUIDE}`);
26459
- let content;
26460
- try {
26461
- content = await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(path2);
26462
- } catch (e) {
26463
- throw new Error(`could not read --announce-summary-file ${path2}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
26508
+ async function requireSuiteReleaseSummary(deps, path2, pathTr) {
26509
+ const files = [["en", path2], ["tr", pathTr]];
26510
+ for (const [lang, file] of files) {
26511
+ if (!file) throw new Error(`a new mm-work suite Release requires ${SUMMARY_FLAG[lang]} <path> in the update-notes template \u2014 see ${SUMMARY_GUIDE}`);
26512
+ }
26513
+ for (const [lang, file] of files) {
26514
+ let content;
26515
+ try {
26516
+ content = await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(file);
26517
+ } catch (e) {
26518
+ throw new Error(`could not read ${SUMMARY_FLAG[lang]} ${file}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
26519
+ }
26520
+ parseUpdateNotes(content, lang);
26464
26521
  }
26465
- validatePlainWordsSummary(content);
26466
26522
  }
26467
26523
  function upsertPlainWordsSection(body, section) {
26468
26524
  let rest = body;
@@ -26486,19 +26542,15 @@ function upsertPlainWordsSection(body, section) {
26486
26542
 
26487
26543
  ${trimmedRest}` : section;
26488
26544
  }
26489
- function plainWordsSummaryLines(content) {
26490
- return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 6);
26491
- }
26492
26545
  async function writePlainWordsIfSuiteMember(deps, args) {
26493
26546
  if (args.meta?.suite !== "mm-work") return;
26494
26547
  try {
26495
- if (!args.summaryLines?.length) return;
26496
- const lines2 = args.summaryLines;
26548
+ if (!args.summary) return;
26497
26549
  const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
26498
26550
  const credits = await collectReleaseCredits(deps, args.repo, body);
26499
26551
  const section = [
26500
- buildPlainWordsSection(lines2, args.big === true, PLAIN_WORDS_HEADING, credits),
26501
- ...args.summaryLinesTr?.length ? [buildPlainWordsSection(args.summaryLinesTr, false, PLAIN_WORDS_HEADING_TR)] : []
26552
+ buildPlainWordsSection(args.summary, args.big === true, "en", credits),
26553
+ ...args.summaryTr ? [buildPlainWordsSection(args.summaryTr, false, "tr")] : []
26502
26554
  ].join("\n\n");
26503
26555
  await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
26504
26556
  } catch (e) {
@@ -26507,9 +26559,10 @@ async function writePlainWordsIfSuiteMember(deps, args) {
26507
26559
  }
26508
26560
  async function writePlainWordsSection(deps, options, repo, meta, tag) {
26509
26561
  try {
26510
- const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
26511
- const summaryLinesTr = options.announceSummaryFileTr ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFileTr)) : void 0;
26512
- await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, summaryLinesTr, big: options.big, suiteTag: options.suiteTag });
26562
+ const read = deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8"));
26563
+ const summary = options.announceSummaryFile ? parseUpdateNotes(await read(options.announceSummaryFile), "en") : void 0;
26564
+ const summaryTr = options.announceSummaryFileTr ? parseUpdateNotes(await read(options.announceSummaryFileTr), "tr") : void 0;
26565
+ await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summary, summaryTr, big: options.big, suiteTag: options.suiteTag });
26513
26566
  } catch {
26514
26567
  }
26515
26568
  }
@@ -27844,7 +27897,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
27844
27897
  const preMainPartial = await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo);
27845
27898
  const promotedPartial = !preMainPartial && await isPromotedUnreleasedTag(deps, tag, tagSha, ctx.repo);
27846
27899
  const partialRelease = preMainPartial || promotedPartial;
27847
- if (partialRelease && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
27900
+ if (partialRelease && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
27848
27901
  if (!partialRelease) {
27849
27902
  try {
27850
27903
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
@@ -28720,7 +28773,7 @@ async function runTrainApplyLocked(command, deps, options = {}) {
28720
28773
  const branchHints = await loadReleaseTrackBranchHints(deps);
28721
28774
  const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
28722
28775
  assertSuiteAutoVersion(ctx.repo, meta, process.env, command === "release" && !directTrack);
28723
- if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
28776
+ if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
28724
28777
  const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
28725
28778
  if (command === "rcand") {
28726
28779
  if (directTrack) {
@@ -39055,7 +39108,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
39055
39108
  await assertConsumerBomLineage(deps, mergedSha, tag);
39056
39109
  }
39057
39110
  const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
39058
- if (!releaseExists && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
39111
+ if (!releaseExists && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
39059
39112
  if (!releaseExists && isHubControlRepo(ctx.repo)) {
39060
39113
  if (!options.announceSummaryFile) {
39061
39114
  throw new Error("a new MMI-Hub hotfix Release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same hotfix release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing");
@@ -39804,6 +39857,7 @@ function boardAdvanceFailureMessage(result) {
39804
39857
  }
39805
39858
 
39806
39859
  // src/pr-land.ts
39860
+ var PR_LAND_QUEUE_TIMEOUT_MS = 45 * 6e4;
39807
39861
  function prHeadBehindBase(input) {
39808
39862
  if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
39809
39863
  return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
@@ -39927,6 +39981,17 @@ async function runPrLand(prNumber, options, deps) {
39927
39981
  }
39928
39982
  const ciPolicy = await deps.resolveCiPolicy(repo);
39929
39983
  base.ciPolicy = ciPolicy;
39984
+ const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
39985
+ let queueSnapshot;
39986
+ if (hasQueue) {
39987
+ base.mergeQueue = { base: resolved.base, state: "queued" };
39988
+ if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
39989
+ queueSnapshot = await deps.readMergeQueue(prNumber, repo);
39990
+ if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
39991
+ base.mergeStatus = "auto-merge-enqueued";
39992
+ return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
39993
+ }
39994
+ }
39930
39995
  const checksWaitError = (checksWait) => {
39931
39996
  if (checksWait.status === "conflicting") {
39932
39997
  return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
@@ -39962,6 +40027,14 @@ async function runPrLand(prNumber, options, deps) {
39962
40027
  if (merge.mergeStatus === "failed") {
39963
40028
  return { ...base, error: merge.error ?? "merge failed" };
39964
40029
  }
40030
+ if (hasQueue) {
40031
+ if (merge.mergeStatus === "merged") {
40032
+ return queueMerged(base, deps);
40033
+ }
40034
+ const deadline = (deps.now ?? (() => Date.now()))() + PR_LAND_QUEUE_TIMEOUT_MS;
40035
+ await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(PR_LAND_POLL_MS);
40036
+ return pollPrMergeQueue(prNumber, options, deps, base, void 0, deadline);
40037
+ }
39965
40038
  if (merge.mergeStatus === "merged") {
39966
40039
  return {
39967
40040
  ...base,
@@ -39984,6 +40057,53 @@ async function runPrLand(prNumber, options, deps) {
39984
40057
  error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
39985
40058
  };
39986
40059
  }
40060
+ function queueMerged(base, deps) {
40061
+ const result = {
40062
+ ...base,
40063
+ status: "merged-via-queue",
40064
+ mergeStatus: "merged",
40065
+ mergeQueue: { ...base.mergeQueue, state: "merged" },
40066
+ branchFinality: prLandedBranchFinality(base.pr, base.base)
40067
+ };
40068
+ deps.queueProgress?.(result);
40069
+ return result;
40070
+ }
40071
+ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
40072
+ const now = deps.now ?? (() => Date.now());
40073
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
40074
+ const deadline = deadlineMs ?? now() + PR_LAND_QUEUE_TIMEOUT_MS;
40075
+ while (now() < deadline) {
40076
+ const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
40077
+ snapshot = void 0;
40078
+ if (read.state === "MERGED") return queueMerged(base, deps);
40079
+ if (read.isInMergeQueue) {
40080
+ base.mergeQueue = {
40081
+ base: base.base,
40082
+ state: "queued",
40083
+ ...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
40084
+ };
40085
+ deps.queueProgress?.(base);
40086
+ if (options.enqueueOnly) return { ...base, status: "queued" };
40087
+ } else if (read.state === "OPEN" && !read.autoMergeRequest) {
40088
+ const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
40089
+ const result2 = {
40090
+ ...base,
40091
+ mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
40092
+ error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
40093
+ };
40094
+ deps.queueProgress?.(result2);
40095
+ return result2;
40096
+ }
40097
+ await sleep2(PR_LAND_POLL_MS);
40098
+ }
40099
+ const result = {
40100
+ ...base,
40101
+ mergeQueue: { ...base.mergeQueue, state: "timeout" },
40102
+ error: "merge queue did not reach MERGED within 45 minutes \u2014 rerun pr land to resume polling"
40103
+ };
40104
+ deps.queueProgress?.(result);
40105
+ return result;
40106
+ }
39987
40107
 
39988
40108
  // src/test-policy-core.ts
39989
40109
  var import_node_child_process13 = require("node:child_process");
@@ -41044,18 +41164,19 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
41044
41164
  bodyText: method === "--squash" ? extra?.bodyText : void 0
41045
41165
  });
41046
41166
  try {
41047
- return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
41167
+ return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue);
41048
41168
  } finally {
41049
41169
  overrideBody?.cleanup();
41050
41170
  }
41051
41171
  }
41052
- async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
41172
+ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, mergeQueue = false) {
41053
41173
  const headRef = (await io.gh(["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], GC_GH_TIMEOUT_MS).catch(() => "")).trim();
41054
41174
  const deleteBranch = !isProtectedBranch(headRef);
41055
41175
  const readMergeState = () => readGhPrStateWithRetry(
41056
41176
  () => io.gh(["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], GC_GH_TIMEOUT_MS)
41057
41177
  );
41058
41178
  const confirmEnqueueOutcome = async () => {
41179
+ if (mergeQueue) return { mergeStatus: "auto-merge-enqueued" };
41059
41180
  const stateRead = await readMergeState();
41060
41181
  if (!stateRead.ok) {
41061
41182
  return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
@@ -41831,6 +41952,57 @@ function applyChecklistCheck(body, query, checked) {
41831
41952
  return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
41832
41953
  }
41833
41954
 
41955
+ // src/pr-merge-queue.ts
41956
+ init_cli_shared();
41957
+ var readApi = async (args) => {
41958
+ const env = await ghReadEnv();
41959
+ return (await execFileP("gh", ["api", ...args], {
41960
+ timeout: 2e4,
41961
+ ...env ? { env } : {}
41962
+ })).stdout;
41963
+ };
41964
+ async function queueGraphql(repo, fields, gh2) {
41965
+ const [owner, name] = repo.split("/");
41966
+ const response = JSON.parse(await gh2(["graphql", "-f", `query=query { repository(owner:${JSON.stringify(owner)},name:${JSON.stringify(name)}) { ${fields} } }`]));
41967
+ if (response.errors?.length) throw new Error(response.errors.map((e) => e.message).join("; "));
41968
+ if (!response.data?.repository) throw new Error("merge queue repository read failed");
41969
+ return response.data.repository;
41970
+ }
41971
+ async function hasPrMergeQueue(repo, base, gh2 = readApi) {
41972
+ const result = await queueGraphql(repo, `mergeQueue(branch:${JSON.stringify(base)}) { id }`, gh2);
41973
+ return result.mergeQueue !== null;
41974
+ }
41975
+ async function readPrMergeQueue(pr, repo, gh2 = readApi) {
41976
+ if (!/^\d+$/.test(pr)) throw new Error("merge queue read requires a PR number");
41977
+ const result = await queueGraphql(
41978
+ repo,
41979
+ `pullRequest(number:${pr}) { state isInMergeQueue mergeQueueEntry { state position } autoMergeRequest { enabledAt } }`,
41980
+ gh2
41981
+ );
41982
+ if (!result.pullRequest) throw new Error(`merge queue PR #${pr} not found`);
41983
+ return result.pullRequest;
41984
+ }
41985
+ async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
41986
+ const pages = JSON.parse(await gh2([`repos/${repo}/actions/runs?event=merge_group&per_page=100`, "--paginate", "--slurp"]));
41987
+ const prefix = `gh-readonly-queue/${base}/pr-${pr}-`;
41988
+ const run = pages.flatMap((page) => page.workflow_runs).filter((run2) => run2.head_branch?.startsWith(prefix)).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id - a.id)[0];
41989
+ return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
41990
+ }
41991
+ function prMergeQueueLine(result) {
41992
+ const queue = result.mergeQueue;
41993
+ if (!queue) return void 0;
41994
+ switch (queue.state) {
41995
+ case "queued":
41996
+ return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
41997
+ case "merged":
41998
+ return `pr land: PR #${result.pr} merged via merge queue`;
41999
+ case "ejected":
42000
+ return `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
42001
+ case "timeout":
42002
+ return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
42003
+ }
42004
+ }
42005
+
41834
42006
  // src/command-register-collaboration.ts
41835
42007
  init_github_client();
41836
42008
  init_github_quota();
@@ -46778,7 +46950,7 @@ ${list}`);
46778
46950
  }
46779
46951
  class PrHeadBehindBaseError extends Error {
46780
46952
  }
46781
- pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
46953
+ pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
46782
46954
  if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
46783
46955
  try {
46784
46956
  const parsed = parseIssueRef(number, o.repo);
@@ -46813,7 +46985,14 @@ ${list}`);
46813
46985
  return;
46814
46986
  }
46815
46987
  if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
46816
- const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
46988
+ let mergeQueueEnabled = false;
46989
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly }, {
46990
+ hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
46991
+ readMergeQueue: readPrMergeQueue,
46992
+ findMergeGroupRun: findPrMergeGroupRun,
46993
+ queueProgress: (receipt) => {
46994
+ if (!o.json) printLine(prMergeQueueLine(receipt));
46995
+ },
46817
46996
  resolveRepo: async (prNumber, repoOpt) => {
46818
46997
  const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
46819
46998
  let repoFromGh = "";
@@ -46903,7 +47082,8 @@ ${list}`);
46903
47082
  },
46904
47083
  mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
46905
47084
  allowedClosing: landClosingGuardInput?.closing,
46906
- bodyText: landSquashBody
47085
+ bodyText: landSquashBody,
47086
+ mergeQueue: mergeQueueEnabled
46907
47087
  }),
46908
47088
  updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
46909
47089
  pollMerged: async (prNumber, repo, deadlineMs) => {
@@ -46927,7 +47107,7 @@ ${list}`);
46927
47107
  return false;
46928
47108
  }
46929
47109
  });
46930
- if (result.status !== "failed") {
47110
+ if (result.status !== "failed" && result.status !== "queued") {
46931
47111
  const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
46932
47112
  const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
46933
47113
  const recovery = result.repo ? buildPostMergeReconRecovery({
@@ -47470,16 +47650,18 @@ async function announceSummaryLineCountRefusal(path2) {
47470
47650
  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`;
47471
47651
  }
47472
47652
  const lines2 = summaryFileLines(content);
47653
+ const first = content.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
47654
+ if (first && Object.values(UPDATE_SECTIONS).some((w) => first === w.fixed || first === w.new)) return null;
47473
47655
  return lines2.length < 3 || lines2.length > 6 ? `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing` : null;
47474
47656
  }
47475
- async function announceSummaryRefusal(path2, isHub = false) {
47657
+ async function announceSummaryRefusal(path2, isHub = false, isWorkSuite = false) {
47476
47658
  let lines2;
47477
47659
  try {
47478
47660
  lines2 = summaryFileLines(await (0, import_promises9.readFile)(path2, "utf8"));
47479
47661
  } catch (e) {
47480
47662
  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`;
47481
47663
  }
47482
- if (lines2.length < 3 || lines2.length > 6) {
47664
+ if (!isWorkSuite && (lines2.length < 3 || lines2.length > 6)) {
47483
47665
  return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
47484
47666
  }
47485
47667
  if (isHub) {
@@ -47577,7 +47759,13 @@ function trainApplyDeps() {
47577
47759
  // Slack release announcement (#883/#6521/#6523): the org alerts channel for the Hub, the project's own
47578
47760
  // registry releaseChannel META for a product repo — best-effort inside announceRelease itself.
47579
47761
  announce: (args) => announceRelease({
47580
- run: async (file, cmdArgs) => (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
47762
+ // #8236: the announcement reads an already-PUBLISHED Release, so its `release view` rides the App
47763
+ // read pool like the train's other reads (#8030), never the owner's personal REST quota. Scoped
47764
+ // here, not in isAppRoutableGhReadArgs: other train paths may view a draft the App cannot see.
47765
+ run: async (file, cmdArgs) => {
47766
+ const env = file === "gh" && (cmdArgs[0] === "release" && cmdArgs[1] === "view" || isAppRoutableGhReadArgs(cmdArgs)) ? await ghReadEnv() : void 0;
47767
+ return (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS, ...env ? { env } : {} })).stdout;
47768
+ },
47581
47769
  postAnnouncement: async (payload) => {
47582
47770
  const res = await releaseAnnouncement(payload, registryClientDeps(await loadConfig()));
47583
47771
  const body = res.body;
@@ -47951,6 +48139,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
47951
48139
  const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone, or cut the suite lead alone (requires --owner-go; a member rcand is always refused)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
47952
48140
  const RELEASE_ONLY_FLAGS = [
47953
48141
  { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
48142
+ { flags: "--announce-summary-file-tr <path>", description: "Turkish update-notes summary; required next to --announce-summary-file for a new mm-work suite Release (#8230)" },
47954
48143
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
47955
48144
  { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
47956
48145
  { 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)" },
@@ -48144,7 +48333,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
48144
48333
  const raw = await withReleaseProgress(
48145
48334
  trainApplyDeps(),
48146
48335
  "Checking the existing release",
48147
- (deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo, big: o.big })
48336
+ (deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, suiteSolo: o.suiteSolo, big: o.big })
48148
48337
  );
48149
48338
  const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
48150
48339
  const projectInfoSync = await withReleaseProgress(
@@ -48200,7 +48389,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
48200
48389
  }
48201
48390
  if (isWorkSuite) {
48202
48391
  try {
48203
- await requireSuiteReleaseSummary({}, o.announceSummaryFile);
48392
+ await requireSuiteReleaseSummary({}, o.announceSummaryFile, o.announceSummaryFileTr);
48204
48393
  } catch (e) {
48205
48394
  return fail(`release: ${e.message}`);
48206
48395
  }
@@ -48211,14 +48400,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
48211
48400
  isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`
48212
48401
  );
48213
48402
  }
48214
- const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub);
48403
+ const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub, isWorkSuite);
48215
48404
  if (refusal) return fail(refusal);
48216
48405
  }
48217
48406
  }
48218
48407
  if (o.apply) {
48219
48408
  try {
48220
48409
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
48221
- const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
48410
+ const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
48222
48411
  const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
48223
48412
  const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
48224
48413
  let projectInfoSync;
@@ -48403,16 +48592,16 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
48403
48592
  if (o.suiteSolo !== o.ownerGo) return fail("hotfix start: solo suite hotfix requires BOTH --suite-solo --owner-go");
48404
48593
  return runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from, suiteSolo: o.suiteSolo, ack: o.ack ? o.ack.split(",").map((s) => s.trim()).filter(Boolean) : void 0 }), o, renderHotfixStart);
48405
48594
  });
48406
- hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--ack <shas>", "accepted for flag symmetry with hotfix start; hotfix release proves every carry by ancestry, so it is ignored here, never an error (#7338)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
48595
+ hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--announce-summary-file-tr <path>", "Turkish update-notes summary; required next to --announce-summary-file for a new mm-work suite hotfix Release (#8230)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--ack <shas>", "accepted for flag symmetry with hotfix start; hotfix release proves every carry by ancestry, so it is ignored here, never an error (#7338)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
48407
48596
  if (o.suiteSolo !== o.ownerGo) return fail("hotfix release: solo suite release requires BOTH --suite-solo --owner-go");
48408
48597
  if (o.retryDeploy) {
48409
- if (o.announceSummaryFile || o.carries) {
48598
+ if (o.announceSummaryFile || o.announceSummaryFileTr || o.carries) {
48410
48599
  return fail("hotfix release: --retry-deploy accepts only --json and --out; it re-runs failed deploy/alignment legs of the published tag and does not recut");
48411
48600
  }
48412
48601
  return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
48413
48602
  }
48414
48603
  return runHotfixSub("release", async () => {
48415
- const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
48604
+ const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
48416
48605
  const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
48417
48606
  const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
48418
48607
  return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
@@ -48745,10 +48934,11 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48745
48934
  };
48746
48935
  return {
48747
48936
  recordPath,
48748
- validateHotfixSummary: () => requireSuiteReleaseSummary({}, summaryFile),
48937
+ validateHotfixSummary: () => requireSuiteReleaseSummary({}, summaryFile, summaryFileTr),
48749
48938
  validateReleaseSummaries: (releases) => validateReleaseSummaries(
48750
48939
  releases,
48751
48940
  { ...memberSummaryFiles, [SUITE_REPO]: summaryFile },
48941
+ { ...memberSummaryFilesTr, [SUITE_REPO]: summaryFileTr },
48752
48942
  async (repo, tag) => {
48753
48943
  try {
48754
48944
  await gh(["release", "view", tag, "--repo", repo, "--json", "tagName"]);
@@ -48839,7 +49029,7 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48839
49029
  },
48840
49030
  // #7738: a member hotfixes as suite-solo; the lead hotfixes under its suite-lead capability (inTrain sets it).
48841
49031
  hotfixStart: (repo, o) => inTrain(repo, async (deps) => {
48842
- await requireSuiteReleaseSummary(deps, summaryFile);
49032
+ await requireSuiteReleaseSummary(deps, summaryFile, summaryFileTr);
48843
49033
  const r = await runHotfixStart(deps, { from: o.from, ack: o.ack, suiteSolo: repo !== SUITE_REPO });
48844
49034
  return { tag: r.tag };
48845
49035
  }, repo !== SUITE_REPO),
@@ -49023,7 +49213,7 @@ function registerSuiteCommands(program3) {
49023
49213
  return fail(`suite abandon: ${e.message}`);
49024
49214
  }
49025
49215
  });
49026
- 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: required title + 2\u20135 notes for each member being released; carried members need none", (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("--revamp <owner/repo>", `repeatable: marks that repo's release an owner-approved revamp in the plain-words section \u2014 the only "big" signal; versions carry no meaning (#8224)`, (val, prev = []) => [...prev, val], []).option("--big", "alias for --revamp mutmutco/MM-Work (#7510)").option("--json", "structured receipt").action(async (o) => {
49216
+ 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: required update-notes summary (Fixed/New) for each member being released; carried members need none (#8230)", (val, prev = []) => [...prev, val], []).option("--announce-summary-file-tr <path>", "required Turkish update-notes summary (D\xFCzeldi/Yeni) for MM-Work itself (#8230)").option("--member-summary-file-tr <owner/repo=path>", "repeatable: required Turkish update-notes summary for each member being released (#8230)", (val, prev = []) => [...prev, val], []).option("--revamp <owner/repo>", `repeatable: marks that repo's release an owner-approved revamp in the plain-words section \u2014 the only "big" signal; versions carry no meaning (#8224)`, (val, prev = []) => [...prev, val], []).option("--big", "alias for --revamp mutmutco/MM-Work (#7510)").option("--json", "structured receipt").action(async (o) => {
49027
49217
  try {
49028
49218
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
49029
49219
  const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
@@ -49046,7 +49236,7 @@ function registerSuiteCommands(program3) {
49046
49236
  return fail(`suite release: ${e.message}`);
49047
49237
  }
49048
49238
  });
49049
- suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--announce-summary-file <path>", "required title + 2\u20135 notes for the member and lead hotfix Releases").option("--announce-summary-file-tr <path>", "optional Turkish plain-words notes").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
49239
+ suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--announce-summary-file <path>", "required update-notes summary (Fixed/New) for the member and lead hotfix Releases (#8230)").option("--announce-summary-file-tr <path>", "required Turkish update-notes summary (D\xFCzeldi/Yeni) (#8230)").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
49050
49240
  try {
49051
49241
  if (!o.ownerGo && !o.dryRun) throw new Error("suite hotfix requires --owner-go (it ships to production); nothing was written. --dry-run prints the plan");
49052
49242
  const root = await leadRoot();
@@ -50874,7 +51064,8 @@ var surfaces_default = {
50874
51064
  ],
50875
51065
  outputs: [
50876
51066
  "cli/dist/index.cjs",
50877
- "cli/dist/main.cjs"
51067
+ "cli/dist/main.cjs",
51068
+ "cli/dist/runbook-sweep.cjs"
50878
51069
  ]
50879
51070
  }
50880
51071
  ],
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.134",
3
+ "version": "4.5.136",
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/claude-plugin",
3
- "version": "4.5.134",
3
+ "version": "4.5.136",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -34,6 +34,8 @@ name: gate
34
34
  # (seed touch: keep BOM digests in lockstep after classifier land.)
35
35
  on:
36
36
  pull_request:
37
+ merge_group:
38
+ types: [checks_requested]
37
39
  push:
38
40
  branches: {{GATE_PUSH_BRANCHES_YAML}}
39
41
  tags: ['v*']
@@ -99,10 +101,10 @@ jobs:
99
101
  lockfile-path: {{GATE_CACHE_DEP_PATH}}
100
102
  fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
101
103
  - name: Runbook guard
102
- if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
104
+ if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}
103
105
  uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
104
106
  with:
105
- base-ref: origin/${{ github.base_ref }}
107
+ base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }}
106
108
  # #5427: per-job npm cache under $RUNNER_TEMP — never the shared ~/.npm. Twelve mmi-live lanes
107
109
  # share the gha-runner home; npm hardlinks from that cache into node_modules, so one job's
108
110
  # `npm ci` can make another job's vitest/dist/workers/forks.js vanish mid-suite (Cannot find
@@ -1,6 +1,8 @@
1
1
  name: gate
2
2
  on:
3
3
  pull_request:
4
+ merge_group:
5
+ types: [checks_requested]
4
6
  push:
5
7
  branches: {{GATE_PUSH_BRANCHES_YAML}}
6
8
  tags: ['v*']
@@ -49,10 +51,10 @@ jobs:
49
51
  lockfile-path: package-lock.json
50
52
  fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
51
53
  - name: Runbook guard
52
- if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
54
+ if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}
53
55
  uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
54
56
  with:
55
- base-ref: origin/${{ github.base_ref }}
57
+ base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }}
56
58
  - name: Isolate npm cache
57
59
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
58
60
  run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
@@ -3,6 +3,8 @@ name: screens
3
3
  # waits on a context that never reports.
4
4
  on:
5
5
  pull_request:
6
+ merge_group:
7
+ types: [checks_requested]
6
8
  push:
7
9
  branches: {{GATE_PUSH_BRANCHES_YAML}}
8
10
  tags: ['v*']
@@ -109,7 +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
- Every new MM-Work suite Release, solo and hotfix included, requires a real `--announce-summary-file` (title plus 2–5 notes, never `No changes to this module`), and a suite release requires a `--member-summary-file owner/repo=path` for every member it releases; optional `--announce-summary-file-tr` and repeatable `--member-summary-file-tr owner/repo=path` add Turkish plain words.
112
+ Every new MM-Work suite Release, solo and hotfix included, requires `--announce-summary-file` and `--announce-summary-file-tr` in the bilingual update-notes template of the [MM-Work GUIDE, "Update notes"](https://github.com/mutmutco/MM-Work/blob/development/packages/mmwork-ds/GUIDE.md) (`Fixed`/`Düzeldi`, then `New`/`Yeni`, with `<Area>: <sentence>.` lines). A suite release requires `--member-summary-file owner/repo=path` and `--member-summary-file-tr owner/repo=path` for every member it releases. Details: the release skill.
113
113
 
114
114
  Announcement delivery uses the developer's GitHub-authenticated Hub session. The Hub checks project
115
115
  authority, resolves the registered channel, and reads the MMI Future Slack key inside its vault boundary.
@@ -15,8 +15,8 @@ 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
- Every new mm-work suite Release (including solo and hotfix releases) requires `--announce-summary-file <path>` with a real title and 2–5 notes (3–6 non-empty lines); missing files, out-of-range summaries and titles starting with `No changes to this module` refuse before tagging. Suite release also requires a valid `--member-summary-file <owner/repo=path>` for every member being released, before promoting any member; carried, held and already-released members need none, and existing Releases can resume without new files.
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; Turkish files remain optional.
18
+ Every new mm-work suite Release (including solo and hotfix releases) requires both `--announce-summary-file <path>` (English) and `--announce-summary-file-tr <path>` (Turkish, "sen" form) in the update-notes template. The canonical wording lives in the [MM-Work GUIDE, "Update notes"](https://github.com/mutmutco/MM-Work/blob/development/packages/mmwork-ds/GUIDE.md); this repo links it and never copies it. Each file has the section word `Fixed` / `Düzeldi` (the problem the person saw, present tense) and/or `New` / `Yeni` (what you can now do) on its own line, Fixed first, each only when it has lines. Under them go 1–8 lines of `<Area>: <sentence>.`, each at most 120 characters, with no issue numbers, versions or commit prefixes. Off-template or missing files refuse before tagging. Do not write the head (app · date · time) or the thanks line: MM-Work renders both from the Release date and credits. Suite release also requires a valid `--member-summary-file <owner/repo=path>` and `--member-summary-file-tr <owner/repo=path>` for every member being released, before promoting any member. Carried, held and already-released members need none, and existing Releases can resume without new files.
19
+ The `/user-releases` feed serves template notes as `updateNotes: {en?, tr?: {fixed[], new[]}}`. Older title-and-notes bodies keep `plainWords` and get `updateNotes: null`.
20
20
 
21
21
  Read META and name the lane before describing or executing anything:
22
22
 
@@ -40,7 +40,7 @@ The JSON is on stdout alone; a stale CLI first prints its self-update notice on
40
40
 
41
41
  Run from the primary checkout on the lane's start branch (`rc`; `development` for direct-track and `--dev`). A Jerv-Hub release on Windows must run under Git Bash (`"C:/Program Files/Git/bin/bash.exe" -lc "…"`) — the train refuses `--apply` before tagging when it is not. Export the bump intent once from the bare argument — `MMI_BUMP_INTENT=minor|major|patch`, unset → `patch`; `MMI_RELEASE_VERSION=X.Y.Z` only for an exact target the tag math cannot derive.
42
42
 
43
- Two cases need a summary file. (1) **MMI-Hub** — always: write a fresh 3–6 line neutral summary to `f=$(mkdir -p .jerv/tmp && mktemp .jerv/tmp/release-summary.XXXXXX)` — sourced from the range `origin/main..origin/development`, rewritten in Hub-subsystem terms, never a product or brand name, and never a Jerv-side project (the train refuses such a line). Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state. **Build and verify every line from ONE command's output**, never from commit subjects or PR titles alone: `git log --stat --format='%h %s%n%b' origin/main..origin/development` gives each commit's subject, its full body (where a bundled PR records `Closes #N`) and the files it changed in a single pass. A line whose claim you cannot point to in that output does not go in the file, and every `#N` a Hub line cites must appear there: the train refuses `--apply` on a citation that ships in no commit of the cut. (2) **Any product repo whose META carries `releaseChannel`** (`mmi-cli oracle org project get {owner}/{repo} --json`) — required too: read `releaseLanguage` (default `en`) and write 3–6 lines IN THAT LANGUAGE, in plain words for the project's audience: what changed and why it matters to a user; no commit prefixes, no issue or PR numbers, no file names, no product-internal jargon. The train refuses `--apply` without the file, and raw release notes are never posted to a project channel. A repo with no `releaseChannel` announces nothing, but an mm-work suite repo still requires the summary for its GitHub Release.
43
+ Two cases need a summary file. (1) **MMI-Hub** — always: write a fresh 3–6 line neutral summary to `f=$(mkdir -p .jerv/tmp && mktemp .jerv/tmp/release-summary.XXXXXX)` — sourced from the range `origin/main..origin/development`, rewritten in Hub-subsystem terms, never a product or brand name, and never a Jerv-side project (the train refuses such a line). Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state. **Build and verify every line from ONE command's output**, never from commit subjects or PR titles alone: `git log --stat --format='%h %s%n%b' origin/main..origin/development` gives each commit's subject, its full body (where a bundled PR records `Closes #N`) and the files it changed in a single pass. A line whose claim you cannot point to in that output does not go in the file, and every `#N` a Hub line cites must appear there: the train refuses `--apply` on a citation that ships in no commit of the cut. (2) **Any product repo whose META carries `releaseChannel`** (`mmi-cli oracle org project get {owner}/{repo} --json`) — required too: read `releaseLanguage` (default `en`) and write 3–6 lines IN THAT LANGUAGE, in plain words for the project's audience: what changed and why it matters to a user; no commit prefixes, no issue or PR numbers, no file names, no product-internal jargon. The train refuses `--apply` without the file, and raw release notes are never posted to a project channel. A repo with no `releaseChannel` announces nothing, but an mm-work suite repo still requires the summary for its GitHub Release. An mm-work suite repo's summary follows the update-notes template below instead of the 3–6 line rule.
44
44
 
45
45
  ```bash
46
46
  mkdir -p .jerv/tmp