@mutmutco/cli 3.79.0 → 3.80.0

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 +260 -218
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6432,7 +6432,7 @@ function scanInstallDirs(root, fs2 = realFsProbe) {
6432
6432
  return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
6433
6433
  }
6434
6434
  function npmInstallTargets(dirs) {
6435
- return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
6435
+ return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install --no-package-lock" }));
6436
6436
  }
6437
6437
  function isLinkedWorktree(root, fs2 = realFsProbe) {
6438
6438
  return fs2.isFile((0, import_node_path10.join)(root, ".git"));
@@ -9512,6 +9512,57 @@ async function runReleaseAbort(deps, options = {}) {
9512
9512
  note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
9513
9513
  };
9514
9514
  }
9515
+ async function runReleasePublishRetry(deps, runId, options = {}) {
9516
+ if (!options.approved) {
9517
+ throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
9518
+ }
9519
+ if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`invalid publish run id ${runId}`);
9520
+ const ctx = await buildTrainApplyContext(deps);
9521
+ if (!isHubControlRepo(ctx.repo)) {
9522
+ throw new Error("release --retry-publish is limited to mutmutco/MMI-Hub");
9523
+ }
9524
+ await requireCleanTree(deps);
9525
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
9526
+ const readRun = async () => {
9527
+ const raw = await deps.run("gh", ["run", "view", String(runId), "--repo", ctx.repo, "--json", "databaseId,workflowName,event,headBranch,headSha,status,conclusion,url"]);
9528
+ try {
9529
+ return JSON.parse(raw);
9530
+ } catch {
9531
+ throw new Error(`publish run ${runId} metadata was not valid JSON`);
9532
+ }
9533
+ };
9534
+ const run = await readRun();
9535
+ if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || run.conclusion !== "failure") {
9536
+ throw new Error(`run ${runId} is not a completed failed Hub publish release run; nothing was written`);
9537
+ }
9538
+ const tag = run.headBranch ?? "";
9539
+ const tagSha = run.headSha ?? "";
9540
+ if (!/^v\d+\.\d+\.\d+$/.test(tag) || !/^[0-9a-f]{40}$/.test(tagSha)) {
9541
+ throw new Error(`run ${runId} has invalid release identity (${tag || "(no tag)"} / ${tagSha || "(no sha)"}); nothing was written`);
9542
+ }
9543
+ if (await probeRemoteTag(deps, tag) !== tagSha) {
9544
+ throw new Error(`run ${runId} does not match origin ${tag}; nothing was written`);
9545
+ }
9546
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
9547
+ await deps.run("gh", ["run", "rerun", String(runId), "--repo", ctx.repo, "--failed"]);
9548
+ if (options.watch) {
9549
+ await deps.run("gh", ["run", "watch", String(runId), "--repo", ctx.repo, "--exit-status"]);
9550
+ const completed = await readRun();
9551
+ if (completed.status !== "completed" || completed.conclusion !== "success") {
9552
+ throw new Error(`publish run ${runId} did not finish successfully after retry`);
9553
+ }
9554
+ }
9555
+ return {
9556
+ command: "release-retry-publish",
9557
+ repo: ctx.repo,
9558
+ tag,
9559
+ tagSha,
9560
+ runId,
9561
+ runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
9562
+ status: options.watch ? "success" : "pending",
9563
+ note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} failed jobs queued for retry`
9564
+ };
9565
+ }
9515
9566
  async function runTrainApplyPipeline(mode, input) {
9516
9567
  const { deps, ctx, command, meta, branchHints, watch, options } = input;
9517
9568
  const directTrack = input.directTrack ?? false;
@@ -14552,9 +14603,10 @@ async function fetchNpmReleasedVersion() {
14552
14603
  }
14553
14604
  }
14554
14605
  var NPM_INSTALL_TIMEOUT_MS = 12e4;
14555
- async function npmSelfUpdateCli(target) {
14606
+ async function npmSelfUpdateCli(target, onStep) {
14556
14607
  const command = cliUpdateCommand(target);
14557
14608
  try {
14609
+ onStep?.(command);
14558
14610
  await runHostBin("npm", ["install", "-g", `@mutmutco/cli@${target ?? "latest"}`], { timeout: NPM_INSTALL_TIMEOUT_MS });
14559
14611
  return { ok: true, detail: `${command} exited 0` };
14560
14612
  } catch (e) {
@@ -14620,9 +14672,6 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
14620
14672
  ) : (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", "mutmutco", "mmi"))
14621
14673
  };
14622
14674
  }
14623
- function activePluginGuardState(isOrgRepo) {
14624
- return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
14625
- }
14626
14675
  async function runClaudePlugin(args) {
14627
14676
  try {
14628
14677
  await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
@@ -14792,27 +14841,35 @@ async function applyPluginHeal(surface, log, opts) {
14792
14841
  if (restored) log(` ${restored}`);
14793
14842
  return true;
14794
14843
  }
14795
- async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
14844
+ async function healClaudePluginForDoctor(surface = detectSurface(process.env), onStep) {
14796
14845
  if (surfaceToken(surface) !== "claude") {
14797
14846
  return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
14798
14847
  }
14799
14848
  const steps = [];
14800
- const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
14849
+ const ok = await applyPluginHeal(surface, (msg) => {
14850
+ const line = msg.trim();
14851
+ steps.push(line);
14852
+ if (line) onStep?.(line);
14853
+ });
14801
14854
  const pinNote = steps.find((s) => s.startsWith("re-pinned ") || s.includes("re-pin by hand"));
14802
14855
  return {
14803
14856
  ok,
14804
14857
  detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${pinNote ? `; ${pinNote}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
14805
14858
  };
14806
14859
  }
14807
- async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
14860
+ async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
14808
14861
  const token = surfaceToken(surface);
14809
14862
  if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14810
14863
  return { ok: false, detail: `not a supported plugin surface (${surface})` };
14811
14864
  }
14812
- if (token === "claude") return healClaudePluginForDoctor(surface);
14865
+ if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
14813
14866
  if (token === "cursor") return installCursorPluginCheckout();
14814
14867
  const steps = [];
14815
- const applied = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
14868
+ const applied = await applyPluginHeal(surface, (msg) => {
14869
+ const line = msg.trim();
14870
+ steps.push(line);
14871
+ if (line) onStep?.(line);
14872
+ });
14816
14873
  const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
14817
14874
  const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
14818
14875
  const ok = applied && guardState === "healthy";
@@ -27301,19 +27358,6 @@ function renderTally(checks) {
27301
27358
  const healthy = total - checks.filter((c) => !c.ok).length;
27302
27359
  return healthy === total ? `\u2713 all ${total} checks healthy` : `\u2713 ${healthy} of ${total} checks healthy`;
27303
27360
  }
27304
- function renderDoctorText(checks, opts) {
27305
- const lines = [];
27306
- const shown = opts.verbose ? checks : checks.filter((c) => !c.ok || c.warn);
27307
- for (const check of shown) {
27308
- lines.push(renderCheckLine(check));
27309
- if (opts.verbose) {
27310
- for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
27311
- }
27312
- }
27313
- lines.push(renderTally(checks));
27314
- if (opts.restartPending) lines.push(RESTART_LINE);
27315
- return lines.join("\n");
27316
- }
27317
27361
  function doctorReportExitCode(checks) {
27318
27362
  return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
27319
27363
  }
@@ -28003,7 +28047,8 @@ var DOCTOR_SURFACES = Object.freeze(
28003
28047
  installLocator: surface.install.locator,
28004
28048
  upgradeMechanism: surface.upgrade.mechanism,
28005
28049
  reload: surface.upgrade.reload,
28006
- repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator"
28050
+ repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator",
28051
+ trustOwner: surface.ownership.trust === "operator" ? "operator" : "host"
28007
28052
  }))
28008
28053
  );
28009
28054
  function doctorSurface(token) {
@@ -28036,11 +28081,8 @@ function diagnoseSurface(evidence) {
28036
28081
  return { ...base, state: "clean" };
28037
28082
  }
28038
28083
  function reloadInstruction(descriptor) {
28039
- if (descriptor.token === "claude") return "restart Claude";
28040
- if (descriptor.token === "codex") return "restart Codex";
28041
- if (descriptor.token === "kimi") return "restart Kimi Code CLI";
28042
- if (descriptor.token === "kilo") return "restart Kilo Code";
28043
- return descriptor.reload === "workspace" ? `reload ${descriptor.displayName}` : `restart ${descriptor.displayName}`;
28084
+ const verb = descriptor.reload === "workspace" ? "reload" : "restart";
28085
+ return `${verb} ${descriptor.displayName}`;
28044
28086
  }
28045
28087
  function planSurfaceRepair(diagnosis) {
28046
28088
  if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
@@ -28085,10 +28127,16 @@ function buildSurfaceDoctorCheck(diagnosis) {
28085
28127
  detail: detailByState[state],
28086
28128
  ...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
28087
28129
  verbose: [
28130
+ // The three numbers the legacy builder used to print. They belong here now that this is the only
28131
+ // plugin row, and a report that names a state without naming the versions behind it is not evidence.
28132
+ `installed: ${diagnosis.installedVersion ?? "(none)"}`,
28133
+ `released: ${diagnosis.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`,
28134
+ `state: ${state}`,
28088
28135
  `registry surface: ${descriptor.token}`,
28089
28136
  `install: ${descriptor.installMechanism} (${descriptor.installLocator})`,
28090
28137
  `repair owner: ${descriptor.repairOwner}`,
28091
- `artifacts: ${descriptor.artifactIds.join(", ")}`
28138
+ `artifacts: ${descriptor.artifactIds.join(", ")}`,
28139
+ ...diagnosis.repairDetail ? [`heal: ${diagnosis.repairDetail}`] : []
28092
28140
  ]
28093
28141
  };
28094
28142
  }
@@ -28186,77 +28234,23 @@ function checkWorktreeRoots(probe) {
28186
28234
  verbose: evidence
28187
28235
  };
28188
28236
  }
28189
- function pluginHealTrigger(probe) {
28190
- if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
28191
- const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
28192
- return behind ? "behind" : null;
28193
- }
28194
- function checkClaudePlugin(probe) {
28195
- const { installed, released, guardState } = probe;
28196
- const codex = probe.surface === "codex";
28197
- const kilo = probe.surface === "kilo";
28198
- const cursor = probe.surface === "cursor";
28199
- const id = codex ? "codex-plugin" : kilo ? "kilo-plugin" : cursor ? "cursor-plugin" : "claude-plugin";
28200
- const label = codex ? "Codex plugin" : kilo ? "Kilo plugin" : cursor ? "Cursor plugin" : "Claude plugin";
28201
- const restart = codex ? "restart Codex" : kilo ? "reload Kilo" : cursor ? "reload Cursor" : "restart Claude";
28202
- const evidence = [
28203
- `installed: ${installed ?? "(none)"}`,
28204
- // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
28205
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
28206
- `resolvable: ${guardState}`
28207
- ];
28208
- const trigger = pluginHealTrigger(probe);
28209
- if (trigger === "unresolved") {
28210
- return {
28211
- id,
28212
- ok: false,
28213
- label,
28214
- detail: guardState === "no-install" ? "not installed" : "unresolved (delivery/cache missing)",
28215
- fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI plugin, then ${restart}`,
28216
- verbose: evidence
28217
- };
28218
- }
28219
- if (trigger === "behind") {
28220
- return {
28221
- id,
28222
- ok: false,
28223
- label,
28224
- detail: `${installed} \u2192 ${released}`,
28225
- // Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
28226
- // fallback, not the first resort (#3282).
28227
- fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall it, then ${restart}`,
28228
- verbose: evidence
28229
- };
28230
- }
28231
- if (!released) {
28232
- return {
28233
- id,
28234
- ok: false,
28235
- reportOnly: true,
28236
- label,
28237
- detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
28238
- fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
28239
- verbose: evidence
28240
- };
28241
- }
28242
- return { id, ok: true, label, ...installed ? { detail: installed } : {}, verbose: evidence };
28243
- }
28244
- function checkCodexHookTrust(probe) {
28237
+ function checkCodexHookTrust(probe, displayName = "Codex") {
28245
28238
  if (!probe?.applicable) return null;
28246
28239
  const evidence = [
28247
28240
  `stored approval rows: ${probe.trustedCount}/${probe.requiredCount}`,
28248
28241
  "current command hashes: verify interactively in /hooks"
28249
28242
  ];
28243
+ const label = `${displayName} hook trust`;
28250
28244
  if (probe.trusted) {
28251
- return { id: "codex-hook-trust", ok: true, label: "Codex hook trust", detail: "trusted", verbose: evidence };
28245
+ return { id: "codex-hook-trust", ok: true, label, detail: "trusted", verbose: evidence };
28252
28246
  }
28253
28247
  return {
28254
28248
  id: "codex-hook-trust",
28255
28249
  ok: true,
28256
28250
  warn: true,
28257
- label: "Codex hook trust",
28251
+ label,
28258
28252
  detail: probe.requiredCount > 0 ? `${probe.trustedCount}/${probe.requiredCount} approval rows present \u2014 current hashes unverified` : "hook bundle could not be verified",
28259
- fix: probe.requiredCount > 0 ? "Codex does not allow silent hook approval; run `/hooks`, review the MMI commands, and trust them" : "run `mmi-cli plugin heal`, restart Codex, then review and trust MMI under `/hooks`",
28253
+ fix: probe.requiredCount > 0 ? `${displayName} does not allow silent hook approval; run \`/hooks\`, review the MMI commands, and trust them` : `run \`mmi-cli plugin heal\`, restart ${displayName}, then review and trust MMI under \`/hooks\``,
28260
28254
  verbose: evidence
28261
28255
  };
28262
28256
  }
@@ -28481,6 +28475,12 @@ function gcReapable(plan) {
28481
28475
  async function runDoctorClean(opts, io, deps) {
28482
28476
  const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
28483
28477
  const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
28478
+ const lane = {
28479
+ banner: Boolean(opts.banner),
28480
+ fast: Boolean(opts.fast),
28481
+ preflight: Boolean(opts.preflight),
28482
+ full: !opts.fast && !opts.banner && !opts.preflight
28483
+ };
28484
28484
  const probeReleased = !opts.fast || Boolean(opts.self);
28485
28485
  const probeAws = !opts.fast && !opts.banner;
28486
28486
  const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
@@ -28495,108 +28495,69 @@ async function runDoctorClean(opts, io, deps) {
28495
28495
  opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
28496
28496
  ]);
28497
28497
  const ghInstalled2 = login ? true : await deps.ghInstalled();
28498
- const registryEvidence = deps.surfaceEvidence?.(isOrgRepo);
28499
- const installed = registryEvidence?.installedVersion ?? deps.installedPluginVersion();
28500
- const pluginSurface = deps.pluginSurface?.() ?? "claude-cli";
28501
- const codexSurface = registryEvidence?.descriptor.token === "codex" || pluginSurface === "codex";
28502
- const cursorSurface = pluginSurface === "cursor";
28503
- const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : codexSurface ? "restart Codex" : cursorSurface ? "reload Cursor" : pluginSurface === "kilo" ? "restart Kilo Code" : "restart Claude";
28498
+ const registryEvidence = deps.surfaceEvidence(isOrgRepo);
28499
+ const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : "restart the agent host";
28504
28500
  const checks = [];
28505
28501
  let restartPending = false;
28506
- checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
28507
- if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
28508
- checks.push(...checkGithubPools(await deps.githubPools()));
28509
- }
28510
- const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
28511
- if (aws) checks.push(aws);
28512
- checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
28513
- if (isOrgRepo) {
28514
- const current = deps.readGitignore();
28515
- const gi = planGitignore(current);
28516
- const giEvidence = [
28517
- `.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
28518
- `managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
28519
- ];
28520
- if (gi.ok) {
28521
- checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
28522
- } else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
28523
- checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
28524
- restartPending = true;
28525
- } else {
28526
- checks.push({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
28527
- }
28528
- }
28529
- const releasedNote = deps.releasedVersionNote?.();
28530
- const pluginProbe = {
28531
- installed,
28532
- released,
28533
- guardState: registryEvidence?.guardState ?? deps.pluginGuardState(isOrgRepo),
28534
- releasedNote,
28535
- surface: pluginSurface
28502
+ const streamed = /* @__PURE__ */ new Set();
28503
+ const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
28504
+ const emitNow = (check) => {
28505
+ checks.push(check);
28506
+ if (opts.json || !worthPrinting(check)) return;
28507
+ streamed.add(check);
28508
+ io.log(renderCheckLine(check));
28509
+ if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
28536
28510
  };
28537
- const registryDiagnosis = registryEvidence ? diagnoseSurface({ ...registryEvidence, releasedVersion: released }) : void 0;
28538
- const registryRepairPlan = registryDiagnosis ? planSurfaceRepair(registryDiagnosis) : null;
28539
- const healTrigger = registryDiagnosis ? registryRepairPlan?.supported ? registryDiagnosis.state : null : pluginHealTrigger(pluginProbe);
28511
+ const healIntent = (line) => {
28512
+ if (!opts.json) io.log(`\u21BB ${line}`);
28513
+ };
28514
+ const healStep = (message) => {
28515
+ if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
28516
+ };
28517
+ const releasedNote = deps.releasedVersionNote?.();
28540
28518
  let pluginHealed = false;
28541
- if (applyEnv && deps.healPlugin && healTrigger && (!registryRepairPlan || registryRepairPlan.supported)) {
28542
- const heal = await deps.healPlugin();
28543
- pluginHealed = heal.ok;
28544
- if (registryEvidence) {
28545
- const repairedDiagnosis = diagnoseSurface({
28519
+ async function runPluginRow() {
28520
+ if (!registryEvidence) return;
28521
+ const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
28522
+ const repair = planSurfaceRepair(diagnosis);
28523
+ if (applyEnv && deps.healPlugin && repair?.supported) {
28524
+ const { descriptor } = registryEvidence;
28525
+ healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
28526
+ const heal = await deps.healPlugin(healStep);
28527
+ pluginHealed = heal.ok;
28528
+ const row = buildSurfaceDoctorCheck(diagnoseSurface({
28546
28529
  ...registryEvidence,
28547
28530
  releasedVersion: released,
28548
28531
  repair: { attempted: true, ok: heal.ok, detail: heal.detail }
28549
- });
28550
- const repairedCheck = buildSurfaceDoctorCheck(repairedDiagnosis);
28551
- if (heal.skipped) repairedCheck.reportOnly = true;
28552
- checks.push(repairedCheck);
28553
- if (!heal.skipped) restartPending = true;
28554
- } else {
28555
- const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
28556
- const healEvidence = [
28557
- `installed: ${installed ?? "(none)"}`,
28558
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
28559
- `resolvable: ${pluginProbe.guardState}`,
28560
- `heal: ${heal.detail}`
28561
- ];
28562
- checks.push(heal.ok ? {
28563
- id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
28564
- ok: true,
28565
- label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
28566
- detail: pluginSurface === "kilo" ? `${measured} \u2014 reinstalled via \`kilo plugin\` (the plugin provisions the skills on next load)` : cursorSurface ? `${measured} \u2014 replaced the managed local checkout (previous copy quarantined)` : `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 ${codexSurface ? "add" : "install"})${codexSurface ? "; review trust in /hooks" : ""}`,
28567
- verbose: healEvidence
28568
- } : {
28569
- id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
28570
- ok: false,
28571
- label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
28572
- detail: measured,
28573
- // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
28574
- // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
28575
- // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
28576
- // genuine gap and still gates.
28577
- ...heal.skipped ? { reportOnly: true } : {},
28578
- fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then ${codexSurface ? "restart Codex" : pluginSurface === "kilo" ? "reload Kilo" : cursorSurface ? "reload Cursor" : "restart Claude"}`,
28579
- verbose: healEvidence
28580
- });
28532
+ }));
28533
+ if (heal.skipped) {
28534
+ row.reportOnly = true;
28535
+ row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
28536
+ }
28537
+ emitNow(row);
28581
28538
  if (!heal.skipped) restartPending = true;
28539
+ } else if (diagnosis.state !== "skipped") {
28540
+ const row = buildSurfaceDoctorCheck(diagnosis);
28541
+ emitNow(row);
28542
+ if (!row.ok && repair?.supported) restartPending = true;
28543
+ }
28544
+ if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
28545
+ const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
28546
+ if (trust) emitNow(trust);
28547
+ }
28548
+ }
28549
+ async function runCliRow() {
28550
+ const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
28551
+ const cliReport = buildVersionLagReport(cliInput);
28552
+ if (!(applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm")) {
28553
+ const cli = checkCliVersion(cliInput, releasedNote);
28554
+ if (cli) emitNow(cli);
28555
+ return;
28582
28556
  }
28583
- } else {
28584
- const plugin = registryDiagnosis ? buildSurfaceDoctorCheck(registryDiagnosis) : checkClaudePlugin(pluginProbe);
28585
- if (plugin) {
28586
- if (!("state" in plugin) || plugin.state !== "skipped") checks.push(plugin);
28587
- if (!plugin.ok && (registryDiagnosis ? Boolean(registryRepairPlan?.supported) : true)) restartPending = true;
28588
- }
28589
- }
28590
- if (codexSurface && (pluginProbe.guardState === "healthy" || pluginHealed)) {
28591
- const trust = checkCodexHookTrust(deps.pluginTrustState?.());
28592
- if (trust) checks.push(trust);
28593
- }
28594
- const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
28595
- const cliReport = buildVersionLagReport(cliInput);
28596
- if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
28597
- const heal = await deps.updateCli(cliReport.releasedVersion);
28557
+ healIntent(`mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} via npm install -g`);
28558
+ const heal = await deps.updateCli(cliReport.releasedVersion, healStep);
28598
28559
  const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
28599
- checks.push(heal.ok ? {
28560
+ emitNow(heal.ok ? {
28600
28561
  id: "cli-version",
28601
28562
  ok: true,
28602
28563
  label: "mmi-cli",
@@ -28612,17 +28573,47 @@ async function runDoctorClean(opts, io, deps) {
28612
28573
  fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
28613
28574
  verbose: healEvidence
28614
28575
  });
28615
- } else {
28616
- const cli = checkCliVersion(cliInput, releasedNote);
28617
- if (cli) checks.push(cli);
28618
28576
  }
28619
- checks.push(checkPluginCache(deps.pluginCache()));
28620
- if (deps.sessionPayload) {
28577
+ async function runGithubAuthRow() {
28578
+ emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
28579
+ }
28580
+ async function runGithubPoolsRows() {
28581
+ for (const pool of checkGithubPools(await deps.githubPools())) emitNow(pool);
28582
+ }
28583
+ async function runAwsRow() {
28584
+ const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
28585
+ if (aws) emitNow(aws);
28586
+ }
28587
+ async function runRepoWorktreesRow() {
28588
+ emitNow(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
28589
+ }
28590
+ async function runGitignoreRow() {
28591
+ const current = deps.readGitignore();
28592
+ const gi = planGitignore(current);
28593
+ const giEvidence = [
28594
+ `.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
28595
+ `managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
28596
+ ];
28597
+ if (gi.ok) {
28598
+ emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
28599
+ } else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
28600
+ emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
28601
+ restartPending = true;
28602
+ } else {
28603
+ emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
28604
+ }
28605
+ }
28606
+ async function runPluginCacheRow() {
28607
+ emitNow(checkPluginCache(deps.pluginCache()));
28608
+ }
28609
+ async function runSessionPayloadRow() {
28621
28610
  const payload = checkSessionPayload(deps.sessionPayload());
28622
- if (payload) checks.push(payload);
28611
+ if (payload) emitNow(payload);
28623
28612
  }
28624
- if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
28625
- if (!opts.fast && !opts.banner && !opts.preflight) {
28613
+ async function runMarketplaceRows() {
28614
+ for (const row of deps.marketplaceRows()) emitNow(row);
28615
+ }
28616
+ async function runSchedulesRow() {
28626
28617
  const probe = await deps.schedulesNotebook().catch((e) => ({
28627
28618
  armed: 0,
28628
28619
  live: 0,
@@ -28631,32 +28622,32 @@ async function runDoctorClean(opts, io, deps) {
28631
28622
  drift: []
28632
28623
  }));
28633
28624
  const sched = checkSchedules(probe);
28634
- if (sched) checks.push(sched);
28625
+ if (sched) emitNow(sched);
28635
28626
  }
28636
- if (!opts.fast && !opts.banner && !opts.preflight && deps.redactorLiveness) {
28627
+ async function runRedactorRow() {
28637
28628
  const redactor = checkRedactorLiveness(deps.redactorLiveness());
28638
- if (redactor) checks.push(redactor);
28629
+ if (redactor) emitNow(redactor);
28639
28630
  }
28640
- if (!opts.fast && !opts.banner && !opts.preflight && deps.docsAudit) {
28631
+ async function runDocsAuditRow() {
28641
28632
  const probe = await deps.docsAudit().catch((e) => ({
28642
28633
  armed: true,
28643
28634
  ok: false,
28644
28635
  detail: `docs-audit probe failed \u2014 ${e.message}`
28645
28636
  }));
28646
28637
  const docsAudit2 = checkDocsAudit(probe);
28647
- if (docsAudit2) checks.push(docsAudit2);
28638
+ if (docsAudit2) emitNow(docsAudit2);
28648
28639
  }
28649
- if (!opts.fast && !opts.banner && !opts.preflight && deps.worktreeRoots) {
28640
+ async function runWorktreeRootsRow() {
28650
28641
  const probe = await deps.worktreeRoots().catch(() => void 0);
28651
28642
  const roots = checkWorktreeRoots(probe);
28652
- if (roots) checks.push(roots);
28643
+ if (roots) emitNow(roots);
28653
28644
  }
28654
- if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
28645
+ async function runTrainSyncRow() {
28655
28646
  try {
28656
- checks.push(checkTrainSync(await deps.syncTrain()));
28647
+ emitNow(checkTrainSync(await deps.syncTrain()));
28657
28648
  } catch (e) {
28658
28649
  const message = e instanceof Error ? e.message : String(e);
28659
- checks.push({
28650
+ emitNow({
28660
28651
  ok: false,
28661
28652
  id: "train-branches",
28662
28653
  label: "train branches",
@@ -28664,6 +28655,8 @@ async function runDoctorClean(opts, io, deps) {
28664
28655
  verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
28665
28656
  });
28666
28657
  }
28658
+ }
28659
+ async function runHousekeeperRows() {
28667
28660
  const repoRoot2 = await deps.repoRoot();
28668
28661
  try {
28669
28662
  const plan = await deps.gcPlan("origin", 200);
@@ -28674,7 +28667,7 @@ async function runDoctorClean(opts, io, deps) {
28674
28667
  if (r.removedBranches.length || r.removedRemoteBranches.length) detailParts.push(`${r.removedBranches.length + r.removedRemoteBranches.length} merged branches`);
28675
28668
  if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
28676
28669
  if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
28677
- checks.push({
28670
+ emitNow({
28678
28671
  id: "branches-worktrees",
28679
28672
  ok: true,
28680
28673
  label: "branches / worktrees",
@@ -28697,7 +28690,7 @@ async function runDoctorClean(opts, io, deps) {
28697
28690
  ...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
28698
28691
  ...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
28699
28692
  ];
28700
- checks.push(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
28693
+ emitNow(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
28701
28694
  id: "branches-worktrees",
28702
28695
  ok: false,
28703
28696
  label: "branches / worktrees",
@@ -28715,12 +28708,35 @@ async function runDoctorClean(opts, io, deps) {
28715
28708
  if (applyRepo && scratch.plan.safeAuto.length > 0) {
28716
28709
  const applied = deps.executeScratchGc(repoRoot2, { apply: true });
28717
28710
  const pruned = applied.applied?.pruned.length ?? 0;
28718
- checks.push({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
28711
+ emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
28719
28712
  if (pruned) restartPending = true;
28720
28713
  } else {
28721
- checks.push(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
28722
- }
28723
- }
28714
+ emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
28715
+ }
28716
+ }
28717
+ const table = [
28718
+ // Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
28719
+ // below is being measured with (#3954).
28720
+ { id: "plugin", when: true, run: runPluginRow },
28721
+ { id: "cli-version", when: true, run: runCliRow },
28722
+ { id: "github-auth", when: true, run: runGithubAuthRow },
28723
+ { id: "github-pools", when: lane.full, run: runGithubPoolsRows },
28724
+ { id: "aws-identity", when: true, run: runAwsRow },
28725
+ { id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
28726
+ { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
28727
+ { id: "plugin-cache", when: true, run: runPluginCacheRow },
28728
+ { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
28729
+ { id: "marketplace", when: true, run: runMarketplaceRows },
28730
+ { id: "schedules", when: lane.full, run: runSchedulesRow },
28731
+ { id: "redactor-liveness", when: lane.full, run: runRedactorRow },
28732
+ { id: "docs-audit", when: lane.full, run: runDocsAuditRow },
28733
+ { id: "worktree-roots", when: lane.full, run: runWorktreeRootsRow },
28734
+ // The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
28735
+ // and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
28736
+ { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
28737
+ { id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
28738
+ ];
28739
+ for (const entry of table) if (entry.when) await entry.run();
28724
28740
  const exitCode = doctorReportExitCode(checks);
28725
28741
  if (opts.json) {
28726
28742
  const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
@@ -28733,7 +28749,7 @@ async function runDoctorClean(opts, io, deps) {
28733
28749
  return exitCode;
28734
28750
  }
28735
28751
  if (opts.banner) {
28736
- const actionable = checks.filter((c) => !c.ok || c.warn);
28752
+ const actionable = checks.filter((c) => (!c.ok || c.warn) && !streamed.has(c));
28737
28753
  for (const c of actionable) {
28738
28754
  io.log(renderReport([c], { restartPending: false }));
28739
28755
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
@@ -28741,10 +28757,16 @@ async function runDoctorClean(opts, io, deps) {
28741
28757
  if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
28742
28758
  return 0;
28743
28759
  }
28744
- const legacyRestart = restartAction === "restart Claude";
28745
- const rendered = renderDoctorText(checks, { verbose: Boolean(opts.verbose), restartPending: restartPending && legacyRestart });
28746
- io.log(restartPending && !legacyRestart ? `${rendered}
28747
- \u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.` : rendered);
28760
+ const rest = checks.filter((c) => !streamed.has(c));
28761
+ const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
28762
+ const lines = [];
28763
+ for (const check of shown) {
28764
+ lines.push(renderCheckLine(check));
28765
+ if (opts.verbose) for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
28766
+ }
28767
+ lines.push(renderTally(checks));
28768
+ if (restartPending) lines.push(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
28769
+ io.log(lines.join("\n"));
28748
28770
  return exitCode;
28749
28771
  }
28750
28772
 
@@ -29065,10 +29087,7 @@ function mmiDoctorDeps(opts = {}) {
29065
29087
  githubRepoReach: githubRepoReachProbe,
29066
29088
  awsCallerArn,
29067
29089
  isOrgRepo: () => isOrgRepoRoot(),
29068
- installedPluginVersion: installedActivePluginVersion,
29069
29090
  surfaceEvidence: surfaceEvidenceOnce,
29070
- pluginGuardState: activePluginGuardState,
29071
- pluginSurface: () => detectSurface(process.env),
29072
29091
  pluginTrustState: () => codexHookTrustState(),
29073
29092
  releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
29074
29093
  releasedVersionNote: throttled ? throttled.note : void 0,
@@ -29077,13 +29096,13 @@ function mmiDoctorDeps(opts = {}) {
29077
29096
  // marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
29078
29097
  // rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
29079
29098
  // carries a CLI shim, so a concurrent npm global install races the same PATH surface.
29080
- updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
29099
+ updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
29081
29100
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
29082
29101
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
29083
- healPlugin: () => {
29102
+ healPlugin: (onStep) => {
29084
29103
  const surface = detectSurface(process.env);
29085
- const host = surface === "codex" ? "Codex" : surface === "kilo" ? "Kilo" : surface === "cursor" ? "Cursor" : "Claude";
29086
- return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface));
29104
+ const host = surfaceEvidenceOnce(true)?.descriptor.displayName ?? surface;
29105
+ return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface, onStep));
29087
29106
  },
29088
29107
  currentCliVersion: resolveClientVersion,
29089
29108
  readGitignore,
@@ -31485,6 +31504,9 @@ function renderReleaseResume(r) {
31485
31504
  function renderReleaseAbort(r) {
31486
31505
  return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
31487
31506
  }
31507
+ function renderReleasePublishRetry(r) {
31508
+ return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
31509
+ }
31488
31510
  function renderRcandResume(r) {
31489
31511
  return `mmi-cli rcand --resume: promoted ${r.repo} \u2192 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
31490
31512
  }
@@ -31532,7 +31554,8 @@ for (const commandName of ["rcand", "release"]) {
31532
31554
  { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
31533
31555
  { 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)" },
31534
31556
  { 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)" },
31535
- { flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" }
31557
+ { flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" },
31558
+ { flags: "--retry-publish <run-id>", description: "with --apply, retry failed jobs of one proven Hub publish release run (#3949)" }
31536
31559
  ];
31537
31560
  for (const f of RELEASE_ONLY_FLAGS) {
31538
31561
  if (commandName === "release") {
@@ -31559,6 +31582,25 @@ for (const commandName of ["rcand", "release"]) {
31559
31582
  if (o.abort && commandName !== "release") {
31560
31583
  return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
31561
31584
  }
31585
+ if (o.retryPublish && commandName !== "release") {
31586
+ return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli release --retry-publish <run-id> --apply --watch`);
31587
+ }
31588
+ if (o.retryPublish) {
31589
+ if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
31590
+ if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
31591
+ if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
31592
+ if (o.repo) {
31593
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
31594
+ if (!guard.ok) return fail(`release: ${guard.message}`);
31595
+ }
31596
+ const runId = Number.parseInt(o.retryPublish, 10);
31597
+ try {
31598
+ const result = await runReleasePublishRetry(trainApplyDeps(), runId, { approved: true, watch: o.watch });
31599
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleasePublishRetry(result));
31600
+ } catch (e) {
31601
+ return failGraceful(`release --retry-publish: ${e.message}`);
31602
+ }
31603
+ }
31562
31604
  if (o.abort) {
31563
31605
  if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
31564
31606
  if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.79.0",
3
+ "version": "3.80.0",
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",