@mutmutco/cli 4.3.61 → 4.3.62

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 (3) hide show
  1. package/README.md +4 -0
  2. package/dist/main.cjs +166 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,4 +85,8 @@ node cli/dist/index.cjs --version
85
85
  node cli/dist/index.cjs doctor --json
86
86
  ```
87
87
 
88
+ When reproducing or verifying CLI behavior from a checkout, always run the checkout's built
89
+ `node cli/dist/index.cjs` — the globally installed `mmi-cli` can lag or diverge from this source
90
+ (#6629), and a reproduction against the global install misreports the fix you are validating.
91
+
88
92
  The Claude Code plugin also ships its own `bin/mmi-cli` launcher, so plugin-enabled Bash sessions can use the bundled command even without a global npm install. Codex and other non-Claude surfaces use the standalone npm package or this repo-local fallback.
package/dist/main.cjs CHANGED
@@ -12070,12 +12070,15 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
12070
12070
  resume
12071
12071
  };
12072
12072
  }
12073
- function laneContestMessage(ref, contest, verb) {
12073
+ function laneContestParts(contest) {
12074
12074
  const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : contest.unverifiable.includes("comments") ? "an unreadable lane (its claim-marker comments could not be read, so no holder can be named)" : "another lane";
12075
12075
  const seen = contest.live.join("; ");
12076
12076
  const unreadable = contest.unverifiable.length ? `liveness could not be fully verified (failed to read: ${contest.unverifiable.join(", ")})` : "";
12077
- const why = [seen, unreadable].filter(Boolean).join("; ");
12078
- return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); re-run with --force to take it anyway` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force to release it anyway`;
12077
+ return { holder, why: [seen, unreadable].filter(Boolean).join("; ") };
12078
+ }
12079
+ function laneContestMessage(ref, contest, verb) {
12080
+ const { holder, why } = laneContestParts(contest);
12081
+ return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); re-run with --force to take it anyway` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
12079
12082
  }
12080
12083
  function ghError(e) {
12081
12084
  const err = e;
@@ -12633,9 +12636,19 @@ async function unclaimBoardIssue(options, deps = {}) {
12633
12636
  throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
12634
12637
  }
12635
12638
  const toStatus = options.toStatus ?? "Todo";
12636
- if (!options.force && item.contentType === "Issue") {
12639
+ if (item.contentType === "Issue") {
12637
12640
  const contest = await checkLaneContest(client, item);
12638
- if (contest.contested) throw new Error(withGatedWriteChainHint(laneContestMessage(item.ref, contest, "unclaim")));
12641
+ if (contest.contested) {
12642
+ if (!options.force) {
12643
+ throw new Error(withGatedWriteChainHint(laneContestMessage(item.ref, contest, "unclaim")));
12644
+ }
12645
+ if (!options.holderDead) {
12646
+ const { holder, why } = laneContestParts(contest);
12647
+ throw new Error(withGatedWriteChainHint(
12648
+ `refusing to unclaim ${item.ref} with --force: ${holder} still shows live evidence of active work (${why}); --force no longer overrides a live lane alone \u2014 re-run with --holder-dead once you have evidence the holder's session is dead, or wait for its lane to go stale (#6625)`
12649
+ ));
12650
+ }
12651
+ }
12639
12652
  }
12640
12653
  try {
12641
12654
  await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
@@ -15788,10 +15801,10 @@ var rollout_plan_default = {
15788
15801
  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)."
15789
15802
  },
15790
15803
  baseline: {
15791
- version: "4.3.61",
15792
- tag: "v4.3.61",
15793
- commit: "f4f05eb96516",
15794
- npm: "@mutmutco/cli@4.3.61"
15804
+ version: "4.3.62",
15805
+ tag: "v4.3.62",
15806
+ commit: "d664b30d09d7",
15807
+ npm: "@mutmutco/cli@4.3.62"
15795
15808
  },
15796
15809
  exitCriterion: "fleet-n-of-n",
15797
15810
  hubOnlyShortcut: "forbidden",
@@ -15808,14 +15821,14 @@ var rollout_plan_default = {
15808
15821
  repo: "mutmutco/mmi-hub",
15809
15822
  role: "canary",
15810
15823
  schedule: "train",
15811
- v3Target: "v4.3.61"
15824
+ v3Target: "v4.3.62"
15812
15825
  }
15813
15826
  ],
15814
15827
  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.",
15815
15828
  rollback: {
15816
15829
  independent: true,
15817
- mechanism: "npm dist-tag latest -> 4.3.61 and redeploy the Hub Lambda from tag v4.3.61 (f4f05eb96516); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15818
- v3Target: "v4.3.61 (@mutmutco/cli@4.3.61, tag commit f4f05eb96516 \u2014 last known-good release carrying the repo-index v4-only contract)"
15830
+ mechanism: "npm dist-tag latest -> 4.3.62 and redeploy the Hub Lambda from tag v4.3.62 (d664b30d09d7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15831
+ v3Target: "v4.3.62 (@mutmutco/cli@4.3.62, tag commit d664b30d09d7 \u2014 last known-good release carrying the repo-index v4-only contract)"
15819
15832
  }
15820
15833
  },
15821
15834
  {
@@ -21451,6 +21464,69 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
21451
21464
  };
21452
21465
  });
21453
21466
  }
21467
+ function supersedeablePublishLegShape(rec) {
21468
+ return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
21469
+ }
21470
+ async function supersedeContentCausedPublishLeg(deps, cwd) {
21471
+ const path2 = releaseLedgerPath(cwd);
21472
+ return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
21473
+ let prior;
21474
+ try {
21475
+ prior = parseReleaseLedger((0, import_node_fs21.readFileSync)(path2, "utf8"));
21476
+ } catch (e) {
21477
+ throw new ReleaseLedgerError(
21478
+ "unreadable",
21479
+ `release --supersede-publish: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before any train; nothing was written (#6638)`
21480
+ );
21481
+ }
21482
+ if (prior.phases.publish.superseded === true) {
21483
+ throw new Error(`release --supersede-publish: the ${prior.tag} publish leg is already superseded \u2014 nothing to do; the next train runs normally`);
21484
+ }
21485
+ if (!supersedeablePublishLegShape(prior.phases.publish)) {
21486
+ throw new Error(
21487
+ `release --supersede-publish: the ${prior.tag} publish leg is ${prior.phases.publish.state}` + (prior.phases.publish.runId != null ? ` with run ${prior.phases.publish.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + "; the supersession closes only a FAILED publish leg, or a PENDING one no run correlates (the uncorrelatable historical shape); nothing was written (#6638)"
21488
+ );
21489
+ }
21490
+ const phases = { ...prior.phases };
21491
+ if (phases.alignment.state === "pending") {
21492
+ const closed = await alignmentLegClosedLive(deps, prior);
21493
+ if (closed) phases.alignment = closed;
21494
+ }
21495
+ const unresolved = ["promotion", "ordinaryCi", "githubRelease", "deploy", "alignment"].filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
21496
+ if (unresolved.length > 0) {
21497
+ throw new Error(
21498
+ `release --supersede-publish: ${prior.tag} has unresolved legs besides publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the supersession closes only the publish leg of an otherwise-complete release; resolve those first, nothing was written (#6638)`
21499
+ );
21500
+ }
21501
+ let liveSha = "";
21502
+ try {
21503
+ liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
21504
+ } catch (e) {
21505
+ throw new Error(`release --supersede-publish: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (#6638)`);
21506
+ }
21507
+ if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
21508
+ throw new Error(
21509
+ `release --supersede-publish: origin reports ${prior.tag} at ${liveSha.slice(0, 12) || "no tag"} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 an absent or divergent release is NOT supersedeable (release --abort --apply is the unpublished candidate's own exit); nothing was written (#6638)`
21510
+ );
21511
+ }
21512
+ const note = `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6638)`;
21513
+ atomicWriteJson(path2, {
21514
+ ...prior,
21515
+ phases: {
21516
+ ...phases,
21517
+ publish: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
21518
+ },
21519
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21520
+ });
21521
+ return {
21522
+ command: "release-supersede-publish",
21523
+ repo: prior.repo,
21524
+ tag: prior.tag,
21525
+ tagSha: prior.tagSha,
21526
+ note: `closed the ${prior.tag} publish leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix`
21527
+ };
21528
+ });
21529
+ }
21454
21530
  async function reverifyPriorRunLegs(deps, path2, targetTag) {
21455
21531
  let prior;
21456
21532
  try {
@@ -23243,7 +23319,9 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
23243
23319
  if (persisted.phases.deploy.superseded !== true) {
23244
23320
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
23245
23321
  }
23246
- await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
23322
+ if (persisted.phases.publish.superseded !== true) {
23323
+ await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
23324
+ }
23247
23325
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
23248
23326
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
23249
23327
  }
@@ -23524,6 +23602,17 @@ async function runReleaseSupersedeDeploy(deps, options = {}) {
23524
23602
  throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
23525
23603
  }
23526
23604
  }
23605
+ async function runReleaseSupersedePublish(deps, options = {}) {
23606
+ if (!options.approved) {
23607
+ throw new Error("release --supersede-publish requires --apply after explicit master approval; nothing was written");
23608
+ }
23609
+ try {
23610
+ return await supersedeContentCausedPublishLeg(deps, process.cwd());
23611
+ } catch (e) {
23612
+ const message2 = e instanceof Error ? e.message : String(e);
23613
+ throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
23614
+ }
23615
+ }
23527
23616
  async function runReleasePublishRetry(deps, runId, options = {}) {
23528
23617
  if (!options.approved) {
23529
23618
  throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
@@ -26318,7 +26407,7 @@ function registerBoardCommands(program3) {
26318
26407
  return failGraceful(`board set-priority failed: ${e.message}`);
26319
26408
  }
26320
26409
  });
26321
- board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--force", "release the claim even when another session shows live evidence of active work (#3727)").option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
26410
+ board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--force", "release a claim whose lane has gone stale or shows no live evidence (#3727); a lane that still shows live evidence also needs --holder-dead (#6625)").option("--holder-dead", "acknowledge the holding lane is known dead \u2014 required with --force when the holder still shows live evidence (#6625)").option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
26322
26411
  const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
26323
26412
  try {
26324
26413
  const result = await unclaimBoardIssue({
@@ -26327,6 +26416,7 @@ function registerBoardCommands(program3) {
26327
26416
  repo: o.repo,
26328
26417
  toStatus,
26329
26418
  force: o.force,
26419
+ holderDead: o.holderDead,
26330
26420
  allowPartial: o.allowPartial
26331
26421
  });
26332
26422
  if (o.json) return console.log(JSON.stringify(result));
@@ -40329,6 +40419,19 @@ function registerCollaborationCommands(program3) {
40329
40419
  delete rest.number;
40330
40420
  return rest;
40331
40421
  }
40422
+ async function resolveTransferredRef(repo, number) {
40423
+ try {
40424
+ const { stdout } = await execFileP("gh", ["api", `repos/${repo}/issues/${number}`], { timeout: GIT_TIMEOUT_MS });
40425
+ const issue2 = JSON.parse(stdout);
40426
+ const match = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/.exec(issue2.html_url ?? "");
40427
+ if (!match || typeof issue2.number !== "number") return void 0;
40428
+ const [currentRepo, currentNumber] = [match[1], Number(match[2])];
40429
+ if (currentRepo.toLowerCase() === repo.toLowerCase() && currentNumber === number) return void 0;
40430
+ return { repo: currentRepo, number: currentNumber, url: issue2.html_url };
40431
+ } catch {
40432
+ return void 0;
40433
+ }
40434
+ }
40332
40435
  issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
40333
40436
  let parsed;
40334
40437
  try {
@@ -40342,19 +40445,33 @@ function registerCollaborationCommands(program3) {
40342
40445
  const fields = normalizeIssueViewJsonFields(o.json);
40343
40446
  const gh = resolveIssueViewGhFields(fields);
40344
40447
  const emit = (data, parent) => console.log(JSON.stringify({ ...gh.numberIsTransport ? withoutTransportNumber(data) : data, ...parent }));
40345
- try {
40448
+ const readIssue = async (targetRepo2, targetNumber) => {
40346
40449
  if (o.comments || o.context) {
40347
40450
  const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
40348
- const data2 = await runIssueViewContext(queryDeps(), { number: n, repo, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: gh.ghFields, boardProjectId });
40349
- emit(data2, await readParentField(n, repo));
40350
- return;
40451
+ const data2 = await runIssueViewContext(queryDeps(), { number: targetNumber, repo: targetRepo2, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: gh.ghFields, boardProjectId });
40452
+ return { data: data2, parent: await readParentField(targetNumber, targetRepo2) };
40351
40453
  }
40352
- const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
40353
- emit(data, await readParentField(n, repo));
40454
+ const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh.ghFields]);
40455
+ return { data, parent: await readParentField(targetNumber, targetRepo2) };
40456
+ };
40457
+ try {
40458
+ const { data, parent } = await readIssue(repo, n);
40459
+ emit(data, parent);
40354
40460
  } catch (e) {
40355
40461
  const err = e;
40356
40462
  const raw = (err.stderr || err.message || String(e)).trim();
40357
40463
  const argvNote = o.json ? ` (received --json argv: ${JSON.stringify(o.json)}, normalized to "${fields}")` : "";
40464
+ const moved = /Could not resolve to an issue/i.test(raw) ? await resolveTransferredRef(repo, n) : void 0;
40465
+ if (moved) {
40466
+ try {
40467
+ const { data, parent } = await readIssue(moved.repo, moved.number);
40468
+ emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, parent);
40469
+ return;
40470
+ } catch (reRead) {
40471
+ const reErr = reRead;
40472
+ return fail(`issue view: ${repo}#${n} was transferred to ${moved.repo}#${moved.number} (${moved.url}), but re-reading it there failed: ${(reErr.stderr || reErr.message || String(reRead)).trim()}`);
40473
+ }
40474
+ }
40358
40475
  return fail(`issue view: ${raw}${argvNote}`);
40359
40476
  }
40360
40477
  });
@@ -44206,6 +44323,9 @@ function renderReleaseAbort(r) {
44206
44323
  function renderReleaseSupersedeDeploy(r) {
44207
44324
  return `mmi-cli devops release --supersede-deploy --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
44208
44325
  }
44326
+ function renderReleaseSupersedePublish(r) {
44327
+ return `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
44328
+ }
44209
44329
  function renderReleasePublishRetry(r) {
44210
44330
  return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
44211
44331
  }
@@ -44431,6 +44551,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44431
44551
  { 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)" },
44432
44552
  { 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)" },
44433
44553
  { flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553)" },
44554
+ { flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 the next patch train then carries the fix (#6638)" },
44434
44555
  { flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
44435
44556
  ];
44436
44557
  for (const f of RELEASE_ONLY_FLAGS) {
@@ -44461,6 +44582,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44461
44582
  if (o.supersedeDeploy && commandName !== "release") {
44462
44583
  return fail(`${commandName}: --supersede-deploy applies only to release \u2014 it closes a content-caused failed deploy leg on a published release's ledger. Run: mmi-cli devops release --supersede-deploy --apply`);
44463
44584
  }
44585
+ if (o.supersedePublish && commandName !== "release") {
44586
+ return fail(`${commandName}: --supersede-publish applies only to release \u2014 it closes a content-caused failed publish leg on a promoted release's ledger. Run: mmi-cli devops release --supersede-publish --apply`);
44587
+ }
44464
44588
  if (o.retryPublish && commandName !== "release") {
44465
44589
  return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
44466
44590
  }
@@ -44508,6 +44632,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44508
44632
  return failGraceful(`release --abort: ${e.message}`);
44509
44633
  }
44510
44634
  }
44635
+ if (o.supersedeDeploy && o.supersedePublish) {
44636
+ return fail("release: --supersede-deploy and --supersede-publish are mutually exclusive \u2014 close the one leg that failed content-caused; the other leg has its own resolution paths");
44637
+ }
44511
44638
  if (o.supersedeDeploy) {
44512
44639
  if (o.resume) return fail("release: --supersede-deploy and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very leg it closes");
44513
44640
  if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
@@ -44527,6 +44654,25 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44527
44654
  return failGraceful(`release --supersede-deploy: ${e.message}`);
44528
44655
  }
44529
44656
  }
44657
+ if (o.supersedePublish) {
44658
+ if (o.resume) return fail("release: --supersede-publish and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-reads the very failed run it closes");
44659
+ if (o.abort) return fail("release: --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PROMOTED release, abort only ever deletes a proven unpublished candidate");
44660
+ if (o.retryPublish) return fail("release: --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
44661
+ if (!o.apply) return fail("release: --supersede-publish requires --apply after explicit master approval; nothing was written");
44662
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
44663
+ return fail("release: --supersede-publish accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
44664
+ }
44665
+ if (o.repo) {
44666
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli devops release --supersede-publish --apply");
44667
+ if (!guard.ok) return fail(`release: ${guard.message}`);
44668
+ }
44669
+ try {
44670
+ const result = await runReleaseSupersedePublish(trainApplyDeps(), { approved: true });
44671
+ return emitTrainResult("release --supersede-publish", o.json ? JSON.stringify(result, null, 2) : renderReleaseSupersedePublish(result), o.out);
44672
+ } catch (e) {
44673
+ return failGraceful(`release --supersede-publish: ${e.message}`);
44674
+ }
44675
+ }
44530
44676
  if (o.resume) {
44531
44677
  if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
44532
44678
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.61",
3
+ "version": "4.3.62",
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",