@mutmutco/cli 4.5.77 → 4.5.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +204 -46
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6986,6 +6986,24 @@ function findClaimedButUnregisteredClosings(text, closing, alreadyClosed = []) {
6986
6986
  }
6987
6987
  return [...claimed].filter((n) => !registered.has(n) && !closed.has(n));
6988
6988
  }
6989
+ function findLineStartClosings(text, repo) {
6990
+ const found = /* @__PURE__ */ new Set();
6991
+ let inFence = false;
6992
+ for (const line of text.split(/\r?\n/)) {
6993
+ if (FENCE_LINE_RE.test(line)) {
6994
+ inFence = !inFence;
6995
+ continue;
6996
+ }
6997
+ if (inFence || !LINE_START_CLOSING_RE.test(line)) continue;
6998
+ const m = findClosingMentions(line)[0];
6999
+ if (!m || m.negated || !Number.isInteger(m.issue) || m.issue <= 0) continue;
7000
+ const link = /\[#\d+\]\(([^)\s]+)\)/.exec(line);
7001
+ if (link && !(link[1] ?? "").toLowerCase().includes(`github.com/${repo.toLowerCase()}/issues/${m.issue}`)) continue;
7002
+ found.add(m.issue);
7003
+ }
7004
+ const ambiguous = new Set(findCrossRepoAmbiguousClosings(text, [...found], repo));
7005
+ return [...found].filter((n) => !ambiguous.has(n));
7006
+ }
6989
7007
  function claimedClosingWarning(unregistered, context = "pr merge") {
6990
7008
  const named = unregistered.map((n) => `#${n}`).join(", ");
6991
7009
  const first = `#${unregistered[0] ?? "N"}`;
@@ -14348,7 +14366,7 @@ var PRODUCT_PENDING_RULESET_PREFIX = "mmi-product-required-checks-pending-";
14348
14366
  var PRODUCT_GATE_CONTEXT = "gate";
14349
14367
  var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
14350
14368
  function reseedProductRulesetStrictness(current, seed, declared = {}) {
14351
- const parse = (raw) => {
14369
+ const parse2 = (raw) => {
14352
14370
  const policy2 = JSON.parse(raw);
14353
14371
  if (!policy2 || typeof policy2 !== "object" || Array.isArray(policy2) || policy2.name !== PRODUCT_RULESET_NAME || policy2.target !== "branch" || !Array.isArray(policy2.rules) || policy2.rules.some((r) => !r || typeof r !== "object" || Array.isArray(r) || typeof r.type !== "string")) {
14354
14372
  throw new Error("product ruleset must be a branch ruleset object with well-formed rules");
@@ -14360,11 +14378,11 @@ function reseedProductRulesetStrictness(current, seed, declared = {}) {
14360
14378
  }
14361
14379
  return { policy: policy2, parameters: parameters2 };
14362
14380
  };
14363
- const canonical = parse(seed);
14381
+ const canonical = parse2(seed);
14364
14382
  const strict = canonical.parameters.strict_required_status_checks_policy;
14365
14383
  if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
14366
14384
  if (current === null) return seed;
14367
- const { policy, parameters } = parse(current);
14385
+ const { policy, parameters } = parse2(current);
14368
14386
  let changed = parameters.strict_required_status_checks_policy !== strict;
14369
14387
  parameters.strict_required_status_checks_policy = strict;
14370
14388
  if (declared.requiredChecks) {
@@ -17231,10 +17249,10 @@ var rollout_plan_default = {
17231
17249
  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)."
17232
17250
  },
17233
17251
  baseline: {
17234
- version: "4.5.77",
17235
- tag: "v4.5.77",
17236
- commit: "760ee9725625",
17237
- npm: "@mutmutco/cli@4.5.77"
17252
+ version: "4.5.79",
17253
+ tag: "v4.5.79",
17254
+ commit: "542d3fde121b",
17255
+ npm: "@mutmutco/cli@4.5.79"
17238
17256
  },
17239
17257
  exitCriterion: "fleet-n-of-n",
17240
17258
  hubOnlyShortcut: "forbidden",
@@ -17251,14 +17269,14 @@ var rollout_plan_default = {
17251
17269
  repo: "mutmutco/mmi-hub",
17252
17270
  role: "canary",
17253
17271
  schedule: "train",
17254
- v3Target: "v4.5.77"
17272
+ v3Target: "v4.5.79"
17255
17273
  }
17256
17274
  ],
17257
17275
  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.",
17258
17276
  rollback: {
17259
17277
  independent: true,
17260
- mechanism: "npm dist-tag latest -> 4.5.77 and redeploy the Hub Lambda from tag v4.5.77 (760ee9725625); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17261
- v3Target: "v4.5.77 (@mutmutco/cli@4.5.77, tag commit 760ee9725625 \u2014 last known-good release carrying the repo-index v4-only contract)"
17278
+ mechanism: "npm dist-tag latest -> 4.5.79 and redeploy the Hub Lambda from tag v4.5.79 (542d3fde121b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17279
+ v3Target: "v4.5.79 (@mutmutco/cli@4.5.79, tag commit 542d3fde121b \u2014 last known-good release carrying the repo-index v4-only contract)"
17262
17280
  }
17263
17281
  },
17264
17282
  {
@@ -20053,12 +20071,12 @@ async function isStrayUnreleasedTag(deps, tag, remoteSha, repo) {
20053
20071
  return deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName"]).then(() => false).catch((e) => /not found|HTTP 404/i.test(e instanceof Error ? e.message : String(e)));
20054
20072
  }
20055
20073
  function cmpReleaseTag(a, b) {
20056
- const parse = (t) => {
20074
+ const parse2 = (t) => {
20057
20075
  const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(t.trim());
20058
20076
  return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
20059
20077
  };
20060
- const left = parse(a);
20061
- const right = parse(b);
20078
+ const left = parse2(a);
20079
+ const right = parse2(b);
20062
20080
  if (!left || !right) throw new Error(`cmpReleaseTag expects vX.Y.Z tags, got ${a} vs ${b}`);
20063
20081
  return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
20064
20082
  }
@@ -20072,10 +20090,10 @@ async function listRemoteReleaseTags(deps) {
20072
20090
  }
20073
20091
  return [...tags].sort(cmpReleaseTag);
20074
20092
  }
20075
- async function assertComputedReleaseTagStillNext(deps, tag) {
20093
+ async function assertComputedReleaseTagStillNext(deps, tag, preOneZero = false) {
20076
20094
  if (!/^v\d+\.\d+\.\d+$/.test(tag)) return;
20077
20095
  const remote = await listRemoteReleaseTags(deps);
20078
- const newer = remote.filter((t) => cmpReleaseTag(t, tag) > 0);
20096
+ const newer = remote.filter((t) => cmpReleaseTag(t, tag) > 0 && !(preOneZero && !t.startsWith("v0.")));
20079
20097
  if (newer.length === 0) return;
20080
20098
  const latest = newer[newer.length - 1];
20081
20099
  throw new Error(
@@ -21697,6 +21715,17 @@ Resume it: ${resumeCommand}
21697
21715
  );
21698
21716
  }
21699
21717
 
21718
+ // src/pre-one-zero.ts
21719
+ function preOneZeroInForce(meta) {
21720
+ return !!meta?.suite && meta.preOneZero !== false;
21721
+ }
21722
+ function assertPreOneZero(repo, meta, tag) {
21723
+ if (preOneZeroInForce(meta) && /^v?[1-9]\d*\./.test(tag)) {
21724
+ throw new Error(`${repo} is a ${meta.suite} suite member held on 0.x.y; ${tag} would be major >= 1. Only the owner lifts this: set registry META preOneZero to false. If the tag came from a stale scripts/next-version.mjs, propagate the current seed first (#7699).`);
21725
+ }
21726
+ return tag;
21727
+ }
21728
+
21700
21729
  // src/train-doctor.ts
21701
21730
  var import_node_child_process9 = require("node:child_process");
21702
21731
  var import_node_fs23 = require("node:fs");
@@ -24833,7 +24862,7 @@ async function runTrainDoctor(input) {
24833
24862
  let newestRemoteReleaseTag;
24834
24863
  try {
24835
24864
  remoteTags = tagNames(await runGitRemoteRead(train, ["ls-remote", "--tags", "origin", "refs/tags/v*"]));
24836
- const releaseTags = await listRemoteReleaseTags(train);
24865
+ const releaseTags = (await listRemoteReleaseTags(train)).filter((t) => !preOneZeroInForce(meta) || t.startsWith("v0."));
24837
24866
  const latest = releaseTags[releaseTags.length - 1];
24838
24867
  newestRemoteReleaseTag = latest;
24839
24868
  if (latest) {
@@ -26055,7 +26084,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
26055
26084
  let tagLedgerStarted = false;
26056
26085
  const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
26057
26086
  try {
26058
- await assertComputedReleaseTagStillNext(deps, tag);
26087
+ await assertComputedReleaseTagStillNext(deps, tag, preOneZeroInForce(meta));
26059
26088
  if (deployModel === "hub-serverless") {
26060
26089
  await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", tag.replace(/^v/, ""), "--release-commit", releaseSha]);
26061
26090
  }
@@ -26200,12 +26229,12 @@ function resumeFollowUpOf(state) {
26200
26229
  return state === "aligned" ? "complete" : state;
26201
26230
  }
26202
26231
  function compareRcTagsNewestFirst(a, b) {
26203
- const parse = (tag) => {
26232
+ const parse2 = (tag) => {
26204
26233
  const match = /^v(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(tag);
26205
26234
  return match ? match.slice(1).map(Number) : [];
26206
26235
  };
26207
- const left = parse(a);
26208
- const right = parse(b);
26236
+ const left = parse2(a);
26237
+ const right = parse2(b);
26209
26238
  for (let i = 0; i < 4; i++) {
26210
26239
  const delta = (right[i] ?? 0) - (left[i] ?? 0);
26211
26240
  if (delta !== 0) return delta;
@@ -26479,7 +26508,7 @@ Nothing was written. Inspect ${ledger.path} before retrying.`);
26479
26508
  async function runReleaseResumePipeline(deps, options, prepared) {
26480
26509
  const watch = options.watch ?? false;
26481
26510
  const { ctx, meta, branchHints, directTrack, deployModel } = prepared;
26482
- const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter((t) => /^v\d+\.\d+\.\d+$/.test(t));
26511
+ const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter((t) => (preOneZeroInForce(meta) ? /^v0\.\d+\.\d+$/ : /^v\d+\.\d+\.\d+$/).test(t));
26483
26512
  const tag = tags[0];
26484
26513
  if (!tag) throw new Error("release --resume: no final v* release tag found \u2014 there is no release to resume");
26485
26514
  const tagSha = await probeRemoteTag(deps, tag);
@@ -26839,7 +26868,7 @@ async function runReleaseAbortLocked(deps, options) {
26839
26868
  throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
26840
26869
  }
26841
26870
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
26842
- const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => /^v\d+\.\d+\.\d+$/.test(tag2));
26871
+ const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => (preOneZeroInForce(meta) ? /^v0\.\d+\.\d+$/ : /^v\d+\.\d+\.\d+$/).test(tag2));
26843
26872
  const tag = tags[0];
26844
26873
  if (!tag) throw new Error("release --abort found no final v* tag; nothing was written");
26845
26874
  const tagSha = await probeRemoteTag(deps, tag);
@@ -27024,7 +27053,7 @@ async function runTrainApplyPipeline(mode, input) {
27024
27053
  "nothing to promote: origin/development is not ahead of origin/rc"
27025
27054
  );
27026
27055
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
27027
- const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
27056
+ const releaseBase = requireValue(clean2(await nextVersion(deps, ctx, meta, "cycle")), "release base");
27028
27057
  const { tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
27029
27058
  deps,
27030
27059
  deployModel2,
@@ -27049,7 +27078,7 @@ async function runTrainApplyPipeline(mode, input) {
27049
27078
  else await mergeWithToleratedResolution(deps, "development", "development -> rc", "theirs", tolerated2);
27050
27079
  rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
27051
27080
  resume = await resolveRcResumeTag(deps, releaseBase, rcSha, rcTagProbe);
27052
- tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
27081
+ tag2 = resume.tag ?? requireValue(clean2(await nextVersion(deps, ctx, meta, "rc")), "rc tag");
27053
27082
  requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
27054
27083
  assertTagAddressableRequiredContexts(deps, requiredChecks, ctx.repo);
27055
27084
  const anchors2 = { repo: ctx.repo, tag: tag2, tagSha: rcSha };
@@ -27129,7 +27158,7 @@ ${spent.note}`), rcandRecovery(tag2));
27129
27158
  "nothing to release: origin/development is not ahead of origin/main"
27130
27159
  );
27131
27160
  const deployModel2 = await preflight(deps, ctx, "main", meta);
27132
- const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
27161
+ const tag2 = requireValue(clean2(await nextVersion(deps, ctx, meta, "cycle")), "release tag");
27133
27162
  const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
27134
27163
  const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
27135
27164
  deps,
@@ -27247,7 +27276,7 @@ ${spent.note}`), rcandRecovery(tag2));
27247
27276
  );
27248
27277
  }
27249
27278
  const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
27250
- const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
27279
+ const tag = requireValue(clean2(await nextVersion(deps, ctx, meta, "release")), "release tag");
27251
27280
  const tagProbe = { remoteSha: await probeRemoteTag(deps, tag) };
27252
27281
  const preFold = {};
27253
27282
  const { versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, "mmi-cli devops release --apply", async () => {
@@ -27306,19 +27335,23 @@ ${spent.note}`), rcandRecovery(tag2));
27306
27335
  ...ledgerReport ? { cutBy: ledgerReport.cutBy, ledger: ledgerReport } : {}
27307
27336
  };
27308
27337
  }
27309
- async function latestLocalReleaseTag(deps, mergedInto) {
27338
+ async function nextVersion(deps, ctx, meta, mode) {
27339
+ const held = preOneZeroInForce(meta);
27340
+ return assertPreOneZero(ctx.repo, meta, clean2(await deps.run("node", ["scripts/next-version.mjs", mode, ...held ? ["--pre-one-zero"] : []])));
27341
+ }
27342
+ async function latestLocalReleaseTag(deps, mergedInto, preOneZero = false) {
27310
27343
  const tags = clean2(await deps.run("git", ["tag", "--list", "v*", ...mergedInto ? ["--merged", mergedInto] : []])).split("\n");
27311
27344
  let latest = { major: 0, minor: 0, patch: 0 };
27312
27345
  for (const tag of tags) {
27313
27346
  const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
27314
- if (!m) continue;
27347
+ if (!m || preOneZero && m[1] !== "0") continue;
27315
27348
  const v = { major: +m[1], minor: +m[2], patch: +m[3] };
27316
27349
  const delta = v.major - latest.major || v.minor - latest.minor || v.patch - latest.patch;
27317
27350
  if (delta > 0) latest = v;
27318
27351
  }
27319
27352
  return latest;
27320
27353
  }
27321
- async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
27354
+ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease, preOneZero = false) {
27322
27355
  const explicitIntent = (env.MMI_BUMP_INTENT ?? "").trim();
27323
27356
  if (explicitIntent) {
27324
27357
  if (!TRAIN_BUMP_INTENTS.includes(explicitIntent)) {
@@ -27331,7 +27364,8 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
27331
27364
  const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(explicitVersion);
27332
27365
  if (!m) throw new Error(`MMI_RELEASE_VERSION must be X.Y.Z (got ${explicitVersion})`);
27333
27366
  const explicit = { major: +m[1], minor: +m[2], patch: +m[3] };
27334
- const latest = latestRelease ?? await latestLocalReleaseTag(deps);
27367
+ if (preOneZero && explicit.major >= 1) throw new Error(`MMI_RELEASE_VERSION ${explicitVersion} is major >= 1 but this suite member is held on 0.x.y; only the owner lifts it (registry META preOneZero=false) (#7699)`);
27368
+ const latest = latestRelease ?? await latestLocalReleaseTag(deps, void 0, preOneZero);
27335
27369
  const delta = explicit.major - latest.major || explicit.minor - latest.minor || explicit.patch - latest.patch;
27336
27370
  if (delta <= 0) {
27337
27371
  throw new Error(
@@ -27342,8 +27376,8 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
27342
27376
  }
27343
27377
  return "patch";
27344
27378
  }
27345
- async function resolveHotfixVersion(deps, env = process.env) {
27346
- const latest = await latestLocalReleaseTag(deps, "origin/main");
27379
+ async function resolveHotfixVersion(deps, env = process.env, preOneZero = false) {
27380
+ const latest = await latestLocalReleaseTag(deps, "origin/main", preOneZero);
27347
27381
  if (latest.major === 0 && latest.minor === 0 && latest.patch === 0) {
27348
27382
  throw new Error("no vX.Y.Z release tag found on origin/main \u2014 cannot derive the hotfix PATCH version");
27349
27383
  }
@@ -27355,14 +27389,14 @@ async function resolveHotfixVersion(deps, env = process.env) {
27355
27389
  declared.push({
27356
27390
  name: "MMI_BUMP_INTENT",
27357
27391
  value: explicitIntent,
27358
- intent: await resolveTrainBumpIntent(deps, { MMI_BUMP_INTENT: explicitIntent }, latest)
27392
+ intent: await resolveTrainBumpIntent(deps, { MMI_BUMP_INTENT: explicitIntent }, latest, preOneZero)
27359
27393
  });
27360
27394
  }
27361
27395
  if (explicitVersion) {
27362
27396
  declared.push({
27363
27397
  name: "MMI_RELEASE_VERSION",
27364
27398
  value: explicitVersion,
27365
- intent: await resolveTrainBumpIntent(deps, { MMI_RELEASE_VERSION: explicitVersion }, latest)
27399
+ intent: await resolveTrainBumpIntent(deps, { MMI_RELEASE_VERSION: explicitVersion }, latest, preOneZero)
27366
27400
  });
27367
27401
  }
27368
27402
  const nonPatch = declared.find((d) => d.intent !== "patch");
@@ -27386,8 +27420,8 @@ async function runTrainApplyLocked(command, deps, options = {}) {
27386
27420
  const ctx = await buildTrainApplyContext(deps);
27387
27421
  await requireCleanTree(deps);
27388
27422
  await runGitRemoteRead(deps, ["fetch", "origin"]);
27389
- const bumpIntent = await resolveTrainBumpIntent(deps);
27390
27423
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
27424
+ const bumpIntent = await resolveTrainBumpIntent(deps, process.env, void 0, preOneZeroInForce(meta));
27391
27425
  const branchHints = await loadReleaseTrackBranchHints(deps);
27392
27426
  const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
27393
27427
  const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
@@ -32604,6 +32638,7 @@ function assignWaves(repos, canarySlug) {
32604
32638
  function statusFor(read, currentSha) {
32605
32639
  if (!read) return { status: "pending", record: {} };
32606
32640
  if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
32641
+ if (read.prUnread) return { status: "red", record: {} };
32607
32642
  const pr = read.pr;
32608
32643
  if (!pr) return { status: "pending", record: {} };
32609
32644
  if (pr.sourceSha && pr.sourceSha !== currentSha) return { status: "pending", record: {} };
@@ -35056,9 +35091,10 @@ LIVE apply to ${repo}:
35056
35091
  desiredByRepo.set(r.repo, desired);
35057
35092
  const drift = compareSeedBytes(desired, content);
35058
35093
  let pr;
35094
+ let prUnread = false;
35059
35095
  try {
35060
35096
  const listBranchPrs = async (branch) => {
35061
- const listed = await gh2(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body,files", "--limit", "100"]);
35097
+ const listed = await gh2(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body,files,headRefName", "--limit", "100"]);
35062
35098
  return JSON.parse(listed.stdout || "[]");
35063
35099
  };
35064
35100
  const targetSpecific = (await listBranchPrs(propagationBranch(r.repo, seed.target))).map((p2) => ({ ...p2, files: p2.files?.map((f) => f.path) }));
@@ -35073,11 +35109,19 @@ LIVE apply to ${repo}:
35073
35109
  const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
35074
35110
  const sourceSha = p.body?.match(/Propagates MMI-Hub@([0-9a-fA-F]{7,40})/)?.[1];
35075
35111
  pr = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid, sourceSha };
35112
+ if (pr.state === "open" && sourceSha && sourceSha !== seedSource.sha && p.headRefName) {
35113
+ const head = await gh2(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${encodeURIComponent(p.headRefName)}`]);
35114
+ const parsedHead = JSON.parse(head.stdout);
35115
+ const headContent = parsedHead.encoding === "base64" && typeof parsedHead.content === "string" ? decodeGitHubContents(parsedHead.content) : null;
35116
+ if (compareSeedBytes(desired, headContent) === "match") pr.sourceSha = seedSource.sha;
35117
+ }
35076
35118
  }
35077
- } catch {
35078
- pr = void 0;
35119
+ } catch (e) {
35120
+ prUnread = true;
35121
+ process.stderr.write(`bootstrap propagate: could not read ${r.repo}'s propagation PRs for ${seed.target}: ${e.message}
35122
+ `);
35079
35123
  }
35080
- reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr });
35124
+ reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr, prUnread });
35081
35125
  }
35082
35126
  let functionGateSatisfied = !isWorkflowSeed;
35083
35127
  if (isWorkflowSeed && canarySlug) {
@@ -35593,10 +35637,41 @@ function formatSuiteStatus(report) {
35593
35637
  var import_node_fs29 = require("node:fs");
35594
35638
  var import_node_path29 = require("node:path");
35595
35639
  var import_node_crypto13 = require("node:crypto");
35640
+
35641
+ // src/kit-version.ts
35642
+ var parse = (v) => /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(typeof v === "string" ? v.trim() : "");
35643
+ function kitSatisfies(shell2, module2) {
35644
+ const s = parse(shell2);
35645
+ const m = parse(module2);
35646
+ if (!s || !m) return true;
35647
+ for (let i = 1; i <= 3; i++) if (Number(s[i]) !== Number(m[i])) return Number(s[i]) > Number(m[i]);
35648
+ return !s[4] || Boolean(m[4]);
35649
+ }
35650
+ function kitFromBundle(js) {
35651
+ const direct = /export\s+(?:const|let|var)\s+__kit\s*=\s*["']([^"']+)["']/.exec(js);
35652
+ if (direct) return direct[1];
35653
+ const local = /[{,]\s*([\w$]+)\s+as\s+__kit\b/.exec(js)?.[1];
35654
+ if (!local) return void 0;
35655
+ const name = local.replace(/[$]/g, "\\$&");
35656
+ return new RegExp(`(?:^|[\\s;,{}])(?:const|let|var)?\\s*${name}\\s*=\\s*["']([^"']+)["']`).exec(js)?.[1];
35657
+ }
35658
+ function kitRefusals(shell2, modules) {
35659
+ return modules.filter((m) => !kitSatisfies(shell2, m.kit)).map((m) => `${m.id}: built on kit ${m.kit}, newer than the MM-Work shell's kit ${shell2}`);
35660
+ }
35661
+
35662
+ // src/suite-train.ts
35596
35663
  var SCHEMA = "mmi-suite-cut/v1";
35597
35664
  var SHA2 = /^[a-f0-9]{40}$/i;
35598
35665
  var RC_TAG = /^v\d+\.\d+\.\d+-rc\.\d+$/;
35599
35666
  var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
35667
+ async function kitCheck(deps, shellSha, bundles, where) {
35668
+ const shell2 = shellSha ? await deps.shellKit(shellSha) : void 0;
35669
+ const kits = await Promise.all(bundles.map(async (b) => ({ id: b.id, kit: b.url ? await deps.bundleKit(b.url) : void 0 })));
35670
+ const refused = kitRefusals(shell2, kits);
35671
+ if (refused.length) throw new Error(`kit check (${where}) refused; a module built on a newer kit crashes the older shell. Release the newer kit to MM-Work first (or rebuild the module on the shell's kit):
35672
+ ${refused.join("\n")}`);
35673
+ return `kit check (${where}): shell kit ${shell2 ?? "unknown"}; ${kits.map((k) => `${k.id} __kit ${k.kit ?? "none"}`).join(", ") || "no modules"}`;
35674
+ }
35600
35675
  function suitePins(record, bundles) {
35601
35676
  return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
35602
35677
  module: m.id,
@@ -35708,6 +35783,8 @@ async function suiteRcand(deps, opts) {
35708
35783
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35709
35784
  `${SUITE_REPO}: rcand`
35710
35785
  ];
35786
+ const lead = await deps.head(SUITE_REPO, "development");
35787
+ plan.push(await kitCheck(deps, lead, active.map((m) => ({ id: m.id, url: m.bundles.dev })), "dev bundles"));
35711
35788
  if (opts.dryRun) return { plan };
35712
35789
  let record = readSuiteCut(deps.recordPath);
35713
35790
  if (record?.leadReleased && record.announcement === "pending") {
@@ -35829,6 +35906,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35829
35906
  throw new Error(`${member.repo}: live backend or development changed during the cut; pins unchanged`);
35830
35907
  }
35831
35908
  const pins = suitePins(record, bundlesOf(modules));
35909
+ await kitCheck(deps, record.leadDevelopment, pins.map((p) => ({ id: p.module, url: p.rc })), "rc bundles");
35832
35910
  await deps.verifyLeadCut(record.leadDevelopment);
35833
35911
  const pr = await deps.pin({ cut: record.leadDevelopment, pins, heldIds: record.heldMembers?.map((h) => h.id) });
35834
35912
  await deps.verifyLeadCut(record.leadDevelopment);
@@ -36610,7 +36688,7 @@ async function runHotfixStartLocked(deps, options, doctor) {
36610
36688
  await deps.run("git", ["fetch", "origin", "--tags"]);
36611
36689
  const { deployModel, meta } = await hotfixPreflight(deps, ctx, "start");
36612
36690
  const bumpNotes = [];
36613
- let resolved = await resolveHotfixVersion(deps);
36691
+ let resolved = await resolveHotfixVersion(deps, process.env, preOneZeroInForce(meta));
36614
36692
  for (; ; ) {
36615
36693
  const priorPr = await findHotfixPr(deps, ctx, resolved.tag);
36616
36694
  if ((priorPr?.state ?? "").toUpperCase() !== "MERGED") break;
@@ -37793,7 +37871,6 @@ async function advanceClosedIssuesToDone(deps) {
37793
37871
  error: `expected an array of closing issue references, got ${closing === null ? "null" : typeof closing}`
37794
37872
  };
37795
37873
  }
37796
- if (closing.length === 0) return { status: "none", entries: [] };
37797
37874
  const refs = [];
37798
37875
  for (const [i, candidate] of closing.entries()) {
37799
37876
  const repo = repoOf2(candidate);
@@ -37807,6 +37884,29 @@ async function advanceClosedIssuesToDone(deps) {
37807
37884
  refs.push({ ref: candidate, repo });
37808
37885
  }
37809
37886
  const entries = [];
37887
+ const fallbackNumbers = /* @__PURE__ */ new Set();
37888
+ let fallbackError;
37889
+ if (deps.fallback) {
37890
+ try {
37891
+ const registered = new Set(refs.filter((r) => r.repo.toLowerCase() === deps.repo.toLowerCase()).map((r) => r.ref.number));
37892
+ for (const n of [...new Set(await deps.fallback.fetchClaimed())].filter((n2) => !registered.has(n2))) {
37893
+ try {
37894
+ const closed = await deps.fallback.closeIssue(n);
37895
+ if ("error" in closed) entries.push({ issue: `#${n}`, moved: false, status: "failed", error: `GitHub did not register the closing reference and the CLI could not close it (${closed.error})` });
37896
+ else if (closed.state === "closed") {
37897
+ fallbackNumbers.add(n);
37898
+ const [owner, name] = deps.repo.split("/");
37899
+ refs.push({ ref: { number: n, repository: { name: name ?? "", owner: { login: owner ?? "" } } }, repo: deps.repo });
37900
+ }
37901
+ } catch (e) {
37902
+ entries.push({ issue: `#${n}`, moved: false, status: "failed", error: `GitHub did not register the closing reference and the CLI could not close it (${e.message})` });
37903
+ }
37904
+ }
37905
+ } catch (e) {
37906
+ fallbackError = e.message;
37907
+ }
37908
+ }
37909
+ if (refs.length === 0 && entries.length === 0) return { status: "none", entries: [], ...fallbackError ? { fallbackError } : {} };
37810
37910
  for (const { ref, repo } of refs) {
37811
37911
  if (repo.toLowerCase() !== deps.repo.toLowerCase()) {
37812
37912
  entries.push({
@@ -37847,14 +37947,15 @@ async function advanceClosedIssuesToDone(deps) {
37847
37947
  entries.push({ issue, moved: false, status: "failed", error: e.message });
37848
37948
  }
37849
37949
  }
37850
- return { status: entries.every((e) => e.moved) ? "ok" : "partial", entries };
37950
+ for (const e of entries) if (fallbackNumbers.has(Number(e.issue.slice(1)))) e.closedByFallback = true;
37951
+ return { status: entries.every((e) => e.moved) && !fallbackError ? "ok" : "partial", entries, ...fallbackError ? { fallbackError } : {} };
37851
37952
  }
37852
37953
  function boardAdvanceFailures(result) {
37853
37954
  return (result?.entries ?? []).filter((e) => !e.moved);
37854
37955
  }
37855
37956
  function boardAdvanceExitCode(result) {
37856
37957
  if (!result) return void 0;
37857
- if (result.status === "fetch-failed") return 1;
37958
+ if (result.status === "fetch-failed" || result.fallbackError) return 1;
37858
37959
  return boardAdvanceFailures(result).length > 0 ? 1 : void 0;
37859
37960
  }
37860
37961
  function boardAdvanceFailureMessage(result) {
@@ -37863,6 +37964,7 @@ function boardAdvanceFailureMessage(result) {
37863
37964
  return `pr merge: could not read the PR's closing issues (${result.error ?? "unknown error"}) \u2014 the PR MERGED, but no board item was advanced. Check the board by hand.`;
37864
37965
  }
37865
37966
  const failures = boardAdvanceFailures(result);
37967
+ if (result.fallbackError) return `pr merge: could not read the PR body to close unregistered closing lines (${result.fallbackError}) \u2014 the PR MERGED; check by hand whether its "Closes #N" issues are still open.`;
37866
37968
  if (!failures.length) return void 0;
37867
37969
  const named = failures.map((f) => f.error ? `${f.issue} (${f.error})` : f.issue).join(", ");
37868
37970
  return `pr merge: board advance incomplete for ${named} \u2014 the PR MERGED, but ${failures.length === 1 ? "this issue" : "these issues"} did not reach Done on the board.`;
@@ -38955,6 +39057,41 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
38955
39057
  }
38956
39058
  return advanceClosedIssuesToDone({
38957
39059
  repo,
39060
+ // #7691: GitHub sometimes fails to register a body `Closes #N`, so the merge closes nothing. Close the
39061
+ // still-OPEN issues the body claims on a deliberate line-start closing line (same repo only). GitHub closes
39062
+ // only for PRs into the default branch, so a PR into rc/main (a train or hotfix PR) never falls back.
39063
+ fallback: {
39064
+ fetchClaimed: async () => {
39065
+ const { stdout } = await execFileP("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "body,baseRefName"], { timeout: GC_GH_TIMEOUT_MS });
39066
+ const { body, baseRefName } = JSON.parse(stdout || "null") ?? {};
39067
+ if (typeof body !== "string" || typeof baseRefName !== "string") throw new Error("gh returned no PR body or base branch");
39068
+ const repoView = await execFileP("gh", ["repo", "view", repo, "--json", "defaultBranchRef"], { timeout: GC_GH_TIMEOUT_MS });
39069
+ const defaultBranch = JSON.parse(repoView.stdout || "null")?.defaultBranchRef?.name;
39070
+ if (typeof defaultBranch !== "string" || !defaultBranch) throw new Error("gh returned no default branch");
39071
+ if (baseRefName !== defaultBranch) return [];
39072
+ return findLineStartClosings(body, repo);
39073
+ },
39074
+ closeIssue: async (n) => {
39075
+ try {
39076
+ const view = await execFileP("gh", ["issue", "view", String(n), "--repo", repo, "--json", "state"], { timeout: GC_GH_TIMEOUT_MS });
39077
+ if (JSON.parse(view.stdout || "null")?.state !== "OPEN") return { state: "already-closed" };
39078
+ await execFileP("gh", [
39079
+ "issue",
39080
+ "close",
39081
+ String(n),
39082
+ "--repo",
39083
+ repo,
39084
+ "--reason",
39085
+ "completed",
39086
+ "--comment",
39087
+ `Closed by the CLI after PR #${prNumber} merged: its body says it closes this issue, but GitHub did not register the closing reference (#7691).`
39088
+ ], { timeout: GH_MUTATION_TIMEOUT_MS });
39089
+ return { state: "closed" };
39090
+ } catch (e) {
39091
+ return { error: e.message };
39092
+ }
39093
+ }
39094
+ },
38958
39095
  fetchClosingIssues: async () => {
38959
39096
  const { stdout } = await execFileP("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS });
38960
39097
  const parsed = JSON.parse(stdout || "null");
@@ -44224,6 +44361,11 @@ ${list}`);
44224
44361
  `
44225
44362
  );
44226
44363
  }
44364
+ const safeTitle = rewriteUnregisteredClosingKeywords(title, findClosingMentions(body).map((m) => m.issue));
44365
+ if (safeTitle !== title) {
44366
+ title = safeTitle;
44367
+ process.stderr.write('pr create: WARNING \u2014 rewrote close-keyword text in the title to "part of #N": the squash commit subject would close it (#7688). Put closing keywords on a "Closes #N" body line.\n');
44368
+ }
44227
44369
  const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
44228
44370
  if (docsCheck && !docsCheck.ok) {
44229
44371
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
@@ -45171,7 +45313,7 @@ async function requireFreshTrainCli(commandName) {
45171
45313
  process.exit(converge.reexecCode);
45172
45314
  }
45173
45315
  var GH_TRAIN_TIMEOUT_MS = 3e4;
45174
- var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
45316
+ var GH_RUN_WATCH_TIMEOUT_MS = 90 * 6e4;
45175
45317
  var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
45176
45318
  var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
45177
45319
  var NPM_TRAIN_TIMEOUT_MS = 6e4;
@@ -46102,9 +46244,9 @@ function isMissingRef(message2) {
46102
46244
  return /404|Not Found|No commit found for SHA/.test(message2);
46103
46245
  }
46104
46246
  function releaseTagsFrom(out) {
46105
- const parse = (t) => t.slice(1).split(".").map(Number);
46247
+ const parse2 = (t) => t.slice(1).split(".").map(Number);
46106
46248
  return out.split(/\s+/).filter((t) => /^v\d+\.\d+\.\d+$/.test(t)).sort((a, b) => {
46107
- const [x, y] = [parse(a), parse(b)];
46249
+ const [x, y] = [parse2(a), parse2(b)];
46108
46250
  return x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
46109
46251
  });
46110
46252
  }
@@ -46395,6 +46537,22 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
46395
46537
  throw new Error(`${pin.module}: MM-Work ${ref} pins do not equal the tested cut (${row?.bundles.main ?? "no main pin"} vs ${pin.main}); release refused`);
46396
46538
  }
46397
46539
  },
46540
+ shellKit: async (sha) => {
46541
+ await exec("git", ["fetch", "--quiet", "origin", "--tags"], root, WATCH_TIMEOUT);
46542
+ try {
46543
+ return (await exec("git", ["describe", "--tags", "--match", "ds-v[0-9]*", "--abbrev=0", sha], root)).replace(/^ds-v/, "") || void 0;
46544
+ } catch {
46545
+ return void 0;
46546
+ }
46547
+ },
46548
+ bundleKit: async (url) => {
46549
+ try {
46550
+ const res = await fetch(url, { signal: AbortSignal.timeout(GH_TIMEOUT) });
46551
+ return res.ok ? kitFromBundle(await res.text()) : void 0;
46552
+ } catch {
46553
+ return void 0;
46554
+ }
46555
+ },
46398
46556
  release,
46399
46557
  shippedTag,
46400
46558
  sameContent: sameContent2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.77",
3
+ "version": "4.5.79",
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",