@mutmutco/cli 4.3.21 → 4.3.23

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 +88 -24
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12242,7 +12242,10 @@ async function claimOneBoardItem(ctx, selector, options) {
12242
12242
  }
12243
12243
  return;
12244
12244
  }
12245
- if (!options.force) throw new Error(withPowerShellChainHint(laneContestMessage(item.ref, contest, "claim")));
12245
+ if (!options.force) {
12246
+ const refusal = laneContestMessage(item.ref, contest, "claim");
12247
+ throw new Error(options.bulk ? refusal : withPowerShellChainHint(refusal));
12248
+ }
12246
12249
  previousHolder = displaced();
12247
12250
  };
12248
12251
  await refuseIfContested();
@@ -12313,7 +12316,7 @@ async function claimBoardIssues(options, deps = {}) {
12313
12316
  const selector = selectors[index];
12314
12317
  const ref = `${selector.repo}#${selector.number}`;
12315
12318
  try {
12316
- const result = await claimOneBoardItem(ctx, selector, options);
12319
+ const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
12317
12320
  results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
12318
12321
  } catch (e) {
12319
12322
  results[index] = { ref, claimed: false, reason: e.message };
@@ -15516,10 +15519,10 @@ var rollout_plan_default = {
15516
15519
  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)."
15517
15520
  },
15518
15521
  baseline: {
15519
- version: "4.3.21",
15520
- tag: "v4.3.21",
15521
- commit: "f55fc2217cd3",
15522
- npm: "@mutmutco/cli@4.3.21"
15522
+ version: "4.3.23",
15523
+ tag: "v4.3.23",
15524
+ commit: "64b06ac32d90",
15525
+ npm: "@mutmutco/cli@4.3.23"
15523
15526
  },
15524
15527
  exitCriterion: "fleet-n-of-n",
15525
15528
  hubOnlyShortcut: "forbidden",
@@ -15536,14 +15539,14 @@ var rollout_plan_default = {
15536
15539
  repo: "mutmutco/mmi-hub",
15537
15540
  role: "canary",
15538
15541
  schedule: "train",
15539
- v3Target: "v4.3.21"
15542
+ v3Target: "v4.3.23"
15540
15543
  }
15541
15544
  ],
15542
15545
  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.",
15543
15546
  rollback: {
15544
15547
  independent: true,
15545
- mechanism: "npm dist-tag latest -> 4.3.21 and redeploy the Hub Lambda from tag v4.3.21 (f55fc2217cd3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15546
- v3Target: "v4.3.21 (@mutmutco/cli@4.3.21, tag commit f55fc2217cd3 \u2014 last known-good release carrying the repo-index v4-only contract)"
15548
+ mechanism: "npm dist-tag latest -> 4.3.23 and redeploy the Hub Lambda from tag v4.3.23 (64b06ac32d90); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15549
+ v3Target: "v4.3.23 (@mutmutco/cli@4.3.23, tag commit 64b06ac32d90 \u2014 last known-good release carrying the repo-index v4-only contract)"
15547
15550
  }
15548
15551
  },
15549
15552
  {
@@ -17911,8 +17914,29 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
17911
17914
  }
17912
17915
  return [...contexts];
17913
17916
  }
17914
- async function findAlignmentPr(deps, ctx, target) {
17915
- const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", "main", "--state", "all", "--json", "number,url,state"]));
17917
+ function alignmentBranchName(target, tag) {
17918
+ return `release-align/${target}/${tag}`;
17919
+ }
17920
+ async function pushAlignmentBranch(deps, target, tag) {
17921
+ const branch = alignmentBranchName(target, tag);
17922
+ let tree;
17923
+ try {
17924
+ tree = clean2(await deps.run("git", ["merge-tree", "--write-tree", "--no-messages", `origin/${target}`, "origin/main"])).split("\n")[0] ?? "";
17925
+ } catch {
17926
+ const files = await runMergeTreePreflight(deps, `origin/${target}`, "origin/main");
17927
+ throw new Error(
17928
+ `the ${target} alignment of ${tag} could not merge origin/main${files.length ? ` \u2014 conflicts on ${files.join(", ")}` : ""} (#6288). Reconcile it on a branch cut from origin/${target} that merges origin/main, open that against ${target}, and land it with a true merge (\`mmi-cli devops pr merge <n> --auto --merge\`) \u2014 never hand-resolve on main, and never merge ${target} into main.`
17929
+ );
17930
+ }
17931
+ if (!tree) throw new Error(`git merge-tree wrote no tree for the ${target} alignment of ${tag} (#6288)`);
17932
+ const commit = clean2(await deps.run("git", ["commit-tree", tree, "-p", `origin/${target}`, "-p", "origin/main", "-m", `chore(release): align ${target} to ${tag}`]));
17933
+ if (!commit) throw new Error(`git commit-tree wrote no commit for the ${target} alignment of ${tag} (#6288)`);
17934
+ await deps.run("git", ["push", "origin", "--delete", branch]).catch(() => void 0);
17935
+ await runGitPush(deps, ["push", "origin", `${commit}:refs/heads/${branch}`]);
17936
+ return branch;
17937
+ }
17938
+ async function findAlignmentPr(deps, ctx, target, head) {
17939
+ const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", head, "--state", "all", "--json", "number,url,state"]));
17916
17940
  if (!out) return void 0;
17917
17941
  const rows = JSON.parse(out);
17918
17942
  const row = rows.filter((r) => typeof r.number === "number" && typeof r.url === "string" && (r.state === "OPEN" || r.state === "MERGED")).sort((a, b) => b.number - a.number)[0];
@@ -17933,23 +17957,28 @@ async function rollDevelopmentForward(deps, ctx, tag) {
17933
17957
  return { status: "pushed", note: "development rolled forward to the released main (development has no required checks)" };
17934
17958
  }
17935
17959
  const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..origin/main"]));
17936
- const existing = await findAlignmentPr(deps, ctx, "development");
17960
+ const branch = alignmentBranchName("development", tag);
17961
+ const existing = await findAlignmentPr(deps, ctx, "development", branch);
17937
17962
  if (ahead === "0") {
17963
+ const merged = existing ?? await findAlignmentPr(deps, ctx, "development", "main");
17938
17964
  return {
17939
17965
  status: "aligned",
17940
17966
  alignment: "already-merged",
17941
- note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 development already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 development already contains the released main; nothing to roll forward"
17967
+ note: merged?.state === "MERGED" ? `alignment: already-merged \u2014 development already contains main via alignment PR #${merged.number}` : "alignment: already-merged \u2014 development already contains the released main; nothing to roll forward"
17942
17968
  };
17943
17969
  }
17944
17970
  if (existing?.state === "OPEN") {
17945
17971
  return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
17946
17972
  }
17973
+ const superseded = await findAlignmentPr(deps, ctx, "development", "main");
17974
+ const supersededNote = superseded?.state === "OPEN" ? ` (supersedes the main-headed alignment PR #${superseded.number}, which GitHub closes as merged once this lands \u2014 #6288)` : "";
17975
+ await pushAlignmentBranch(deps, "development", tag);
17947
17976
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
17948
17977
 
17949
- \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
17950
- const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
17978
+ \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). The head is \`development\` with \`main\` merged into it, never \`main\` itself: a strict \`require branches to be up to date\` ruleset can never be satisfied by a main-headed PR, and satisfying it literally would promote every unreleased commit to \`main\` (#6288). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
17979
+ const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", branch, "--title", `chore(release): align development to ${tag}`, "--body", body]));
17951
17980
  const number = parsePrNumber(url);
17952
- return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
17981
+ return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
17953
17982
  }
17954
17983
  var ALIGNMENT_ARM_ATTEMPTS = 3;
17955
17984
  var ALIGNMENT_ARM_BACKOFF_MS = 2e3;
@@ -17990,23 +18019,28 @@ async function alignRcForward(deps, ctx, tag) {
17990
18019
  return { status: "pushed", note: "rc aligned to the released main (rc has no required checks)" };
17991
18020
  }
17992
18021
  const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
17993
- const existing = await findAlignmentPr(deps, ctx, "rc");
18022
+ const branch = alignmentBranchName("rc", tag);
18023
+ const existing = await findAlignmentPr(deps, ctx, "rc", branch);
17994
18024
  if (ahead === "0") {
18025
+ const merged = existing ?? await findAlignmentPr(deps, ctx, "rc", "main");
17995
18026
  return {
17996
18027
  status: "aligned",
17997
18028
  alignment: "already-merged",
17998
- note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 rc already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 rc already contains the released main; nothing to align"
18029
+ note: merged?.state === "MERGED" ? `alignment: already-merged \u2014 rc already contains main via alignment PR #${merged.number}` : "alignment: already-merged \u2014 rc already contains the released main; nothing to align"
17999
18030
  };
18000
18031
  }
18001
18032
  if (existing?.state === "OPEN") {
18002
18033
  return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
18003
18034
  }
18035
+ const superseded = await findAlignmentPr(deps, ctx, "rc", "main");
18036
+ const supersededNote = superseded?.state === "OPEN" ? ` (supersedes the main-headed alignment PR #${superseded.number}, which GitHub closes as merged once this lands \u2014 #6288)` : "";
18037
+ await pushAlignmentBranch(deps, "rc", tag);
18004
18038
  const body = `Carries the ${tag} release from \`main\` back to \`rc\`.
18005
18039
 
18006
- \`rc\` requires status checks, so the release train opens this alignment PR instead of bypassing branch protection with a direct push. Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives.`;
18007
- const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "rc", "--head", "main", "--title", `chore(release): align rc to ${tag}`, "--body", body]));
18040
+ \`rc\` requires status checks, so the release train opens this alignment PR instead of bypassing branch protection with a direct push. The head is \`rc\` with \`main\` merged into it, never \`main\` itself \u2014 a strict \`require branches to be up to date\` ruleset can never be satisfied by a main-headed PR (#6288). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives.`;
18041
+ const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "rc", "--head", branch, "--title", `chore(release): align rc to ${tag}`, "--body", body]));
18008
18042
  const number = parsePrNumber(url);
18009
- return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
18043
+ return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
18010
18044
  }
18011
18045
  var FLEET_FOREIGN_VERDICT_GRACE_ATTEMPTS = 3;
18012
18046
  function resolveContextState(context, checkRuns, statuses) {
@@ -21549,6 +21583,23 @@ async function runTrainDoctor(input) {
21549
21583
  derived = deriveReleasePhaseStatus(ledger);
21550
21584
  }
21551
21585
  }
21586
+ if (derived.legs.some((l) => l.phase === "alignment" && l.state === "pending")) {
21587
+ for (const target of ["development", "rc"].filter((t) => t !== "rc" || track === "full")) {
21588
+ try {
21589
+ const stale = await findAlignmentPr(train, ctx, target, "main");
21590
+ if (stale?.state !== "OPEN") continue;
21591
+ add({
21592
+ code: "alignment-pr-main-headed",
21593
+ severity: "blocker",
21594
+ source: "origin",
21595
+ title: `alignment PR #${stale.number} (\`main\` -> ${target}) is headed by \`main\` \u2014 under a strict "branches must be up to date" ruleset it can never merge (#6288)`,
21596
+ remedy: `rerun \`mmi-cli devops release --resume\`: the train rebuilds the alignment as \`release-align/${target}/<tag>\` (${target} with \`main\` merged into it), which is up to date by construction; GitHub closes #${stale.number} as merged once that lands. Never merge ${target} into \`main\`, never hand-merge on \`main\`, and never add a bypass actor to the strict rule`
21597
+ });
21598
+ } catch (e) {
21599
+ unverified(`the ${target} alignment PR head`, e);
21600
+ }
21601
+ }
21602
+ }
21552
21603
  if (derived.phaseStatus !== "phases-green") {
21553
21604
  const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed").map((l) => `${l.phase}=${l.state}`).join(", ");
21554
21605
  const failed = derived.phaseStatus === "phases-failed";
@@ -33227,7 +33278,10 @@ function extendCreateCommand(issue, batchAttach) {
33227
33278
  try {
33228
33279
  const raw = (0, import_node_fs31.readFileSync)(opts.batch, "utf8");
33229
33280
  specs = JSON.parse(raw);
33230
- if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
33281
+ if (!Array.isArray(specs)) {
33282
+ const top = specs === null ? "null" : typeof specs === "object" ? `an object with keys: ${Object.keys(specs).join(", ") || "(none)"}` : `a ${typeof specs}`;
33283
+ throw new Error(`batch file must contain a JSON array, but its top level is ${top}`);
33284
+ }
33231
33285
  } catch (e) {
33232
33286
  return fail(`issue create --batch: cannot read/parse ${opts.batch}: ${e.message}`);
33233
33287
  }
@@ -36185,10 +36239,18 @@ function prHeadBehindBase(input) {
36185
36239
  function prHeadUpdateRemedy(head, base) {
36186
36240
  return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
36187
36241
  }
36242
+ var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
36243
+ function isProtectedPrHead(head) {
36244
+ return PROTECTED_PR_HEAD_BRANCHES.includes(head);
36245
+ }
36246
+ function protectedPrHeadRemedy(head, base) {
36247
+ return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
36248
+ }
36188
36249
  var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
36189
36250
  var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
36190
36251
  async function updatePrHeadFromBase(input) {
36191
36252
  const remedy = prHeadUpdateRemedy(input.head, input.base);
36253
+ if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
36192
36254
  if (input.localCheckedOut) {
36193
36255
  const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
36194
36256
  await input.git(["fetch", "origin", input.base]);
@@ -36475,8 +36537,9 @@ var WAIVABLE_KINDS = [
36475
36537
  var TEST_WORK_KIND = "unrequested-test-file";
36476
36538
  var TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
36477
36539
  var PY_TEST_FILE_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
36540
+ var GO_TEST_FILE_RE = /_test\.go$/;
36478
36541
  function isTestPath(path2) {
36479
- return typeof path2 === "string" && (TEST_FILE_RE.test(path2) || PY_TEST_FILE_RE.test(path2));
36542
+ return typeof path2 === "string" && (TEST_FILE_RE.test(path2) || PY_TEST_FILE_RE.test(path2) || GO_TEST_FILE_RE.test(path2));
36480
36543
  }
36481
36544
  function editsExistingTest(paths, addedPaths = []) {
36482
36545
  const added = new Set(Array.isArray(addedPaths) ? addedPaths : []);
@@ -36631,6 +36694,7 @@ var HOTFIX_POLICY_REF = "origin/development";
36631
36694
  var HOTFIX_DIFF_BASE = "origin/main";
36632
36695
  var TEST_RE = TEST_FILE_RE;
36633
36696
  var PY_TEST_RE = PY_TEST_FILE_RE;
36697
+ var GO_TEST_RE = GO_TEST_FILE_RE;
36634
36698
  function translate(glob) {
36635
36699
  let out = "";
36636
36700
  for (let i = 0; i < glob.length; i++) {
@@ -36666,7 +36730,7 @@ function globToRegExp2(glob) {
36666
36730
  return new RegExp(`^${translate(glob)}$`);
36667
36731
  }
36668
36732
  function isTestPath2(path2) {
36669
- return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
36733
+ return TEST_RE.test(path2) || PY_TEST_RE.test(path2) || GO_TEST_RE.test(path2);
36670
36734
  }
36671
36735
  var SUPPORTED_SOURCE = /\.[cm]?[jt]s$/;
36672
36736
  var MAX_ANALYZED_BYTES = 512 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.21",
3
+ "version": "4.3.23",
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",