@mutmutco/cli 4.4.16 → 4.4.17

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 +335 -55
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5436,7 +5436,7 @@ function resolveVerbAliasShim(argv) {
5436
5436
  }
5437
5437
 
5438
5438
  // src/command-composition.ts
5439
- var import_node_os21 = require("node:os");
5439
+ var import_node_os22 = require("node:os");
5440
5440
  var import_node_path47 = require("node:path");
5441
5441
 
5442
5442
  // src/board-read.ts
@@ -15938,10 +15938,10 @@ var rollout_plan_default = {
15938
15938
  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)."
15939
15939
  },
15940
15940
  baseline: {
15941
- version: "4.4.16",
15942
- tag: "v4.4.16",
15943
- commit: "1a7eeee13010",
15944
- npm: "@mutmutco/cli@4.4.16"
15941
+ version: "4.4.17",
15942
+ tag: "v4.4.17",
15943
+ commit: "748489250635",
15944
+ npm: "@mutmutco/cli@4.4.17"
15945
15945
  },
15946
15946
  exitCriterion: "fleet-n-of-n",
15947
15947
  hubOnlyShortcut: "forbidden",
@@ -15958,14 +15958,14 @@ var rollout_plan_default = {
15958
15958
  repo: "mutmutco/mmi-hub",
15959
15959
  role: "canary",
15960
15960
  schedule: "train",
15961
- v3Target: "v4.4.16"
15961
+ v3Target: "v4.4.17"
15962
15962
  }
15963
15963
  ],
15964
15964
  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.",
15965
15965
  rollback: {
15966
15966
  independent: true,
15967
- mechanism: "npm dist-tag latest -> 4.4.16 and redeploy the Hub Lambda from tag v4.4.16 (1a7eeee13010); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15968
- v3Target: "v4.4.16 (@mutmutco/cli@4.4.16, tag commit 1a7eeee13010 \u2014 last known-good release carrying the repo-index v4-only contract)"
15967
+ mechanism: "npm dist-tag latest -> 4.4.17 and redeploy the Hub Lambda from tag v4.4.17 (748489250635); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15968
+ v3Target: "v4.4.17 (@mutmutco/cli@4.4.17, tag commit 748489250635 \u2014 last known-good release carrying the repo-index v4-only contract)"
15969
15969
  }
15970
15970
  },
15971
15971
  {
@@ -18803,6 +18803,7 @@ function isReleaseShaDeployEnumerationCandidate(row, nonDeployEvents) {
18803
18803
 
18804
18804
  // src/train-apply-deploy.ts
18805
18805
  var import_node_fs19 = require("node:fs");
18806
+ var import_node_os10 = require("node:os");
18806
18807
  var import_node_path17 = require("node:path");
18807
18808
 
18808
18809
  // src/vercel-deployments.ts
@@ -19082,6 +19083,24 @@ async function appendServedChannelPublish(deps, input, dispatch) {
19082
19083
  };
19083
19084
  }
19084
19085
  var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
19086
+ var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
19087
+ async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
19088
+ let script;
19089
+ try {
19090
+ script = await deps.run("git", ["show", `${tagSha}:${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT}`]);
19091
+ } catch (error) {
19092
+ const reason = error instanceof Error ? error.message : String(error);
19093
+ throw new Error(`could not extract ${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT} from tagged commit ${tagSha.slice(0, 12)}: ${reason}`);
19094
+ }
19095
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path17.join)((0, import_node_os10.tmpdir)(), "jerv-gateway-release-deploy-"));
19096
+ const extracted = (0, import_node_path17.join)(dir, "jerv-gateway-release-deploy.sh");
19097
+ try {
19098
+ (0, import_node_fs19.writeFileSync)(extracted, script, "utf8");
19099
+ return await deps.run("bash", [extracted, "--tag", tag]);
19100
+ } finally {
19101
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
19102
+ }
19103
+ }
19085
19104
  async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch) {
19086
19105
  if (!isJervHubRepo(repo)) return dispatch;
19087
19106
  if (dispatch.deployStatus !== "success") {
@@ -19092,7 +19111,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
19092
19111
  };
19093
19112
  }
19094
19113
  try {
19095
- const output = await deps.run("bash", ["scripts/jerv-gateway-release-deploy.sh", "--tag", tag]);
19114
+ const output = await runTaggedJervGatewayLocalWrapper(deps, tag, tagSha);
19096
19115
  const receipt = output.trim().split(/\r?\n/).reverse().map((line) => {
19097
19116
  try {
19098
19117
  return JSON.parse(line);
@@ -19110,11 +19129,12 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
19110
19129
  deployStatus: "success"
19111
19130
  };
19112
19131
  } catch (error) {
19113
- const detail = (error instanceof Error ? error.message : String(error)).split(/\r?\n/, 1)[0].slice(0, 240);
19132
+ const full = error instanceof Error ? error.message : String(error);
19114
19133
  return {
19115
19134
  ...dispatch,
19116
19135
  // #6095: the helper's receipt, health or rollback failed after a proven publish — the release resumes, never re-tags.
19117
- note: `${dispatch.note}; Jerv Gateway deploy FAILED (${detail}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
19136
+ // #6985: keep the FULL failing output in the note so the ledger can persist its tail (the first-line 240-char clip hid the real cause).
19137
+ note: `${dispatch.note}; Jerv Gateway deploy FAILED (${full}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
19118
19138
  workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
19119
19139
  deployStatus: "failure"
19120
19140
  };
@@ -19851,12 +19871,12 @@ Resume it: ${resumeCommand}
19851
19871
  // src/train-doctor.ts
19852
19872
  var import_node_child_process9 = require("node:child_process");
19853
19873
  var import_node_fs23 = require("node:fs");
19854
- var import_node_os13 = require("node:os");
19874
+ var import_node_os14 = require("node:os");
19855
19875
  var import_node_path23 = require("node:path");
19856
19876
 
19857
19877
  // src/doctor-io.ts
19858
19878
  var import_node_fs20 = require("node:fs");
19859
- var import_node_os10 = require("node:os");
19879
+ var import_node_os11 = require("node:os");
19860
19880
  var import_node_path18 = require("node:path");
19861
19881
  var import_node_child_process7 = require("node:child_process");
19862
19882
  var import_node_util4 = require("node:util");
@@ -19886,7 +19906,7 @@ function execFileCapture(file, args, opts = {}) {
19886
19906
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
19887
19907
  function stagedClaudePluginVersion() {
19888
19908
  try {
19889
- const stagingRoot = (0, import_node_path18.join)((0, import_node_os10.homedir)(), ".claude", "plugins", "staging");
19909
+ const stagingRoot = (0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "staging");
19890
19910
  const record = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(stagingRoot, "launch-target.json"), "utf8"));
19891
19911
  if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
19892
19912
  const manifest = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
@@ -19899,7 +19919,7 @@ function stagedClaudePluginVersion() {
19899
19919
  function installedClaudePluginVersion() {
19900
19920
  try {
19901
19921
  const file = JSON.parse(
19902
- (0, import_node_fs20.readFileSync)((0, import_node_path18.join)((0, import_node_os10.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
19922
+ (0, import_node_fs20.readFileSync)((0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
19903
19923
  );
19904
19924
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
19905
19925
  if (versions.length === 0) return void 0;
@@ -20071,11 +20091,11 @@ function compatHolds(range, version) {
20071
20091
 
20072
20092
  // src/claude-binary-doctor.ts
20073
20093
  var import_node_fs21 = require("node:fs");
20074
- var import_node_os12 = require("node:os");
20094
+ var import_node_os13 = require("node:os");
20075
20095
  var import_node_path20 = require("node:path");
20076
20096
 
20077
20097
  // src/jerv-cli-spawn.ts
20078
- var import_node_os11 = require("node:os");
20098
+ var import_node_os12 = require("node:os");
20079
20099
  var import_node_path19 = require("node:path");
20080
20100
  init_cli_shared();
20081
20101
  var JERV_CLI_ENTRY = (0, import_node_path19.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
@@ -20100,7 +20120,7 @@ function normalizeSpawnPathEntry(entry, platform2 = process.platform) {
20100
20120
  if (msys) return `${msys[1].toUpperCase()}:\\${msys[2].replace(/\//g, "\\")}`;
20101
20121
  return trimmed;
20102
20122
  }
20103
- function jervCliCandidateDirs(env = process.env, home = (0, import_node_os11.homedir)(), platform2 = process.platform) {
20123
+ function jervCliCandidateDirs(env = process.env, home = (0, import_node_os12.homedir)(), platform2 = process.platform) {
20104
20124
  const seen = /* @__PURE__ */ new Set();
20105
20125
  const out = [];
20106
20126
  const push = (dir) => {
@@ -20171,7 +20191,7 @@ function globalNodeModulesRoots(host) {
20171
20191
  };
20172
20192
  const prefix = env.npm_config_prefix?.trim();
20173
20193
  if (prefix) push(platform2 === "win32" ? (0, import_node_path20.join)(prefix, "node_modules") : (0, import_node_path20.join)(prefix, "lib", "node_modules"));
20174
- for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os12.homedir)(), platform2)) {
20194
+ for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os13.homedir)(), platform2)) {
20175
20195
  push((0, import_node_path20.join)(dir, "node_modules"));
20176
20196
  push((0, import_node_path20.join)((0, import_node_path20.dirname)(dir), "lib", "node_modules"));
20177
20197
  }
@@ -20818,6 +20838,53 @@ var MAX_TEXT = 500;
20818
20838
  function truncate(text, max) {
20819
20839
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
20820
20840
  }
20841
+ var FAILING_OUTPUT_LEGS = /* @__PURE__ */ new Set(["publish", "deploy", "alignment"]);
20842
+ var LEDGER_OUTPUT_FILE_LINES = 50;
20843
+ var LEDGER_OUTPUT_NOTE_LINES = 5;
20844
+ function lastNonEmptyLines(text, n) {
20845
+ const lines2 = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
20846
+ while (lines2.length > 0 && lines2[lines2.length - 1] === "") lines2.pop();
20847
+ return lines2.slice(-n);
20848
+ }
20849
+ function attachOutputNote(note, suffix) {
20850
+ const room = Math.max(0, MAX_TEXT - suffix.length - 3);
20851
+ const head = note && room > 20 ? truncate(note, room) : "";
20852
+ return truncate(head ? `${head} \u2014 ${suffix}` : suffix, MAX_TEXT);
20853
+ }
20854
+ function writeFailingLegOutput(artifactPath, output) {
20855
+ const fileLines = lastNonEmptyLines(output, LEDGER_OUTPUT_FILE_LINES);
20856
+ (0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(artifactPath), { recursive: true });
20857
+ (0, import_node_fs22.writeFileSync)(artifactPath, `${fileLines.join("\n")}
20858
+ `, "utf8");
20859
+ const tail = fileLines.slice(-LEDGER_OUTPUT_NOTE_LINES).join("\n");
20860
+ const noteSuffix = truncate(`failing-leg output: ${artifactPath}
20861
+ ${tail}`, MAX_TEXT);
20862
+ return { artifactPath, noteSuffix };
20863
+ }
20864
+ function stampPhaseRecord(ledgerPath, tag, phase, entry, previousRetries, now) {
20865
+ const { output: failingOutput, ...fields } = entry;
20866
+ let note = fields.note;
20867
+ if (fields.state === "failed" && FAILING_OUTPUT_LEGS.has(phase)) {
20868
+ const raw = failingOutput ?? fields.error ?? fields.note;
20869
+ if (raw) {
20870
+ try {
20871
+ const persisted = writeFailingLegOutput(
20872
+ (0, import_node_path22.join)((0, import_node_path22.dirname)((0, import_node_path22.dirname)(ledgerPath)), "ledger-output", `${tag}-${phase}.txt`),
20873
+ raw
20874
+ );
20875
+ note = attachOutputNote(note, persisted.noteSuffix);
20876
+ } catch {
20877
+ }
20878
+ }
20879
+ }
20880
+ return {
20881
+ ...fields,
20882
+ note: note !== void 0 ? truncate(note, MAX_TEXT) : void 0,
20883
+ error: fields.error !== void 0 ? truncate(fields.error, MAX_TEXT * 2) : void 0,
20884
+ retries: fields.retries ?? previousRetries,
20885
+ updatedAt: now().toISOString()
20886
+ };
20887
+ }
20821
20888
  function normalizeLedgerAnchors(anchors) {
20822
20889
  const repo = anchors.repo?.trim();
20823
20890
  const tag = anchors.tag?.trim();
@@ -21029,13 +21096,7 @@ function openReleaseLedgerWriter(cwd, opts = {}) {
21029
21096
  updatedAt: now().toISOString()
21030
21097
  };
21031
21098
  const previousRetries = base.phases[phase].retries ?? 0;
21032
- const record = {
21033
- ...entry,
21034
- note: entry.note !== void 0 ? truncate(entry.note, MAX_TEXT) : void 0,
21035
- error: entry.error !== void 0 ? truncate(entry.error, MAX_TEXT * 2) : void 0,
21036
- retries: entry.retries ?? previousRetries,
21037
- updatedAt: now().toISOString()
21038
- };
21099
+ const record = stampPhaseRecord(path2, normalized.tag, phase, entry, previousRetries, now);
21039
21100
  const updated = {
21040
21101
  ...base,
21041
21102
  phases: { ...base.phases, [phase]: record },
@@ -21707,6 +21768,48 @@ function ledgerLaneResumeCommand(prior) {
21707
21768
  if (prior.lane === "hotfix") return `mmi-cli devops hotfix release ${prior.tag}`;
21708
21769
  return "mmi-cli devops release --resume (or, only when proven unpublished, mmi-cli devops release --abort --apply)";
21709
21770
  }
21771
+ var RETRY_DEPLOY_LEGS = /* @__PURE__ */ new Set(["deploy", "alignment"]);
21772
+ function joinLegNames(legs) {
21773
+ if (legs.length <= 1) return legs[0] ?? "";
21774
+ if (legs.length === 2) return `${legs[0]} and ${legs[1]}`;
21775
+ return `${legs.slice(0, -1).join(", ")} and ${legs[legs.length - 1]}`;
21776
+ }
21777
+ function formatFailedLegTails(ledger) {
21778
+ const chunks = [];
21779
+ for (const phase of ["publish", "deploy", "alignment"]) {
21780
+ const rec = ledger.phases[phase];
21781
+ if (rec.state !== "failed") continue;
21782
+ const text = rec.note ?? rec.error ?? "";
21783
+ const marker = "failing-leg output: ";
21784
+ const idx = text.indexOf(marker);
21785
+ if (idx < 0) continue;
21786
+ chunks.push(`${phase} ${text.slice(idx)}`);
21787
+ }
21788
+ return chunks.length ? `; ${chunks.join("; ")}` : "";
21789
+ }
21790
+ function hotfixRetryDeployShape(ledger) {
21791
+ if (ledger.lane !== "hotfix") return false;
21792
+ if (ledger.phases.githubRelease.state !== "complete") return false;
21793
+ const open2 = RELEASE_LEDGER_PHASES.filter((phase) => {
21794
+ const state = ledger.phases[phase].state;
21795
+ return state === "pending" || state === "failed";
21796
+ });
21797
+ if (open2.length === 0) return false;
21798
+ if (!open2.every((phase) => RETRY_DEPLOY_LEGS.has(phase))) return false;
21799
+ return open2.some((phase) => ledger.phases[phase].state === "failed");
21800
+ }
21801
+ function ledgerFailedRemedy(ledger) {
21802
+ const derived = deriveReleasePhaseStatus(ledger);
21803
+ const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
21804
+ const tails = formatFailedLegTails(ledger);
21805
+ if (hotfixRetryDeployShape(ledger)) {
21806
+ const failed = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
21807
+ return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(failed)} of the same tag: \`mmi-cli devops hotfix release ${ledger.tag} --retry-deploy\` (the deploy helper is idempotent; alignment re-attempts the fold-port PR) (#6985)${tails}`;
21808
+ }
21809
+ const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
21810
+ const supersession = deployOnlyDeadlock ? `; when the deploy failure is content-caused on the already-published ${ledger.tag} (the immutable tag can never deploy green), the sanctioned exit is \`mmi-cli devops release --supersede-deploy --apply\` after explicit master approval \u2014 the next patch train carries the cause fix (#6553)` : "";
21811
+ return `diagnose the failed leg, then ${ledgerLaneResumeCommand(ledger)} once its cause is fixed (an abort only with fresh approval)${supersession}${tails}`;
21812
+ }
21710
21813
  function alignmentPrNumbers(note) {
21711
21814
  return [...new Set([...(note ?? "").matchAll(/\/pull\/(\d+)/g)].map((m) => Number.parseInt(m[1], 10)))];
21712
21815
  }
@@ -22098,9 +22201,10 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
22098
22201
  }
22099
22202
  if (derived.phaseStatus !== "phases-green") {
22100
22203
  const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
22204
+ const next = derived.phaseStatus === "phases-failed" ? ledgerFailedRemedy(prior) : `Finish it first: ${ledgerLaneResumeCommand(prior)} (#5987/#6068)`;
22101
22205
  return {
22102
22206
  ok: false,
22103
- error: `a prior ${prior.lane ?? "release"} ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new train run must not stack on an unresolved one. Finish it first: ${ledgerLaneResumeCommand(prior)} (#5987/#6068)`
22207
+ error: `a prior ${prior.lane ?? "release"} ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new train run must not stack on an unresolved one. ${next}`
22104
22208
  };
22105
22209
  }
22106
22210
  let liveSha = "";
@@ -22124,6 +22228,124 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
22124
22228
  return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
22125
22229
  });
22126
22230
  }
22231
+ async function defaultRetryDeploy(deps, prior) {
22232
+ const input = {
22233
+ note: `hotfix --retry-deploy ${prior.tag}`,
22234
+ deployStatus: "success",
22235
+ workflowRuns: []
22236
+ };
22237
+ const dispatch = await appendJervGatewayReleaseDeploy(deps, prior.repo, prior.tag, prior.tagSha, input);
22238
+ const host = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
22239
+ if (!host) {
22240
+ throw new Error(
22241
+ `hotfix --retry-deploy: the ${prior.tag} deploy helper was not invoked (not the operator-host Gateway) \u2014 nothing was written (#6985)`
22242
+ );
22243
+ }
22244
+ const entry = deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha });
22245
+ if (entry.state === "failed") {
22246
+ return { ...entry, error: entry.note ?? dispatch.note, output: dispatch.note };
22247
+ }
22248
+ return entry;
22249
+ }
22250
+ async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
22251
+ const path2 = releaseLedgerPath(cwd);
22252
+ return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
22253
+ let prior;
22254
+ try {
22255
+ prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
22256
+ } catch (e) {
22257
+ throw new ReleaseLedgerError(
22258
+ "unreadable",
22259
+ `hotfix --retry-deploy: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand; nothing was written (#6985)`
22260
+ );
22261
+ }
22262
+ if (prior.lane !== "hotfix") {
22263
+ throw new Error(
22264
+ `hotfix --retry-deploy: the active ledger is a ${prior.lane ?? "release"} lane ledger for ${prior.tag} \u2014 --retry-deploy is the hotfix lane's recovery for a published hotfix; nothing was written (#6985)`
22265
+ );
22266
+ }
22267
+ if (prior.phases.githubRelease.state !== "complete") {
22268
+ throw new Error(
22269
+ `hotfix --retry-deploy: ${prior.tag} is not a published hotfix (githubRelease=${prior.phases.githubRelease.state}) \u2014 --retry-deploy re-runs deploy/alignment of a PUBLISHED tag; nothing was written (#6985)`
22270
+ );
22271
+ }
22272
+ if (!hotfixRetryDeployShape(prior)) {
22273
+ const derived = deriveReleasePhaseStatus(prior);
22274
+ const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
22275
+ const extra = open2.filter((l) => l.phase !== "deploy" && l.phase !== "alignment");
22276
+ if (extra.length > 0) {
22277
+ throw new Error(
22278
+ `hotfix --retry-deploy: ${prior.tag} has unresolved legs besides deploy/alignment (${extra.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 it re-runs only the failed deploy and/or fold-port alignment of a published hotfix of the same tag; nothing was written (#6985)`
22279
+ );
22280
+ }
22281
+ throw new Error(
22282
+ `hotfix --retry-deploy: ${prior.tag} has no failed deploy or alignment leg to retry (${open2.map((l) => `${l.phase}=${l.state}`).join(", ") || "phases-green"}) \u2014 nothing was written (#6985)`
22283
+ );
22284
+ }
22285
+ if (prior.phases.alignment.state === "failed" && !opts.retryAlignment) {
22286
+ throw new Error(
22287
+ `hotfix --retry-deploy: the ${prior.tag} alignment (fold-port) leg is failed \u2014 nothing was written (#6985)`
22288
+ );
22289
+ }
22290
+ if (prior.phases.deploy.state === "failed" && !isJervHubRepo(prior.repo) && prior.phases.deploy.workflow !== "jerv-gateway") {
22291
+ throw new Error(
22292
+ `hotfix --retry-deploy: the ${prior.tag} deploy leg is ${prior.phases.deploy.workflow ?? "not the operator-host helper"} \u2014 diagnose that run; nothing was written (#6985)`
22293
+ );
22294
+ }
22295
+ let liveSha = "";
22296
+ try {
22297
+ liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
22298
+ } catch (e) {
22299
+ throw new Error(
22300
+ `hotfix --retry-deploy: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to retry on an unproven state; retry when origin answers (#6985)`
22301
+ );
22302
+ }
22303
+ if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
22304
+ throw new Error(
22305
+ `hotfix --retry-deploy: 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 tag is not retryable; nothing was written (#6985)`
22306
+ );
22307
+ }
22308
+ const retried = [];
22309
+ const phases = { ...prior.phases };
22310
+ const now = () => /* @__PURE__ */ new Date();
22311
+ if (phases.deploy.state === "failed") {
22312
+ retried.push("deploy");
22313
+ const entry = opts.retryDeploy ? await opts.retryDeploy(prior) : await defaultRetryDeploy(deps, prior);
22314
+ phases.deploy = stampPhaseRecord(path2, prior.tag, "deploy", entry, phases.deploy.retries ?? 0, now);
22315
+ }
22316
+ if (phases.alignment.state === "failed") {
22317
+ retried.push("alignment");
22318
+ try {
22319
+ const entry = await opts.retryAlignment(prior);
22320
+ phases.alignment = stampPhaseRecord(path2, prior.tag, "alignment", entry, phases.alignment.retries ?? 0, now);
22321
+ } catch (e) {
22322
+ const message2 = e instanceof Error ? e.message : String(e);
22323
+ phases.alignment = stampPhaseRecord(
22324
+ path2,
22325
+ prior.tag,
22326
+ "alignment",
22327
+ { state: "failed", sha: prior.tagSha, error: message2, output: message2 },
22328
+ phases.alignment.retries ?? 0,
22329
+ now
22330
+ );
22331
+ }
22332
+ }
22333
+ atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
22334
+ const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
22335
+ const status = stillFailed.length === 0 ? "success" : "failed";
22336
+ return {
22337
+ command: "hotfix-retry-deploy",
22338
+ repo: prior.repo,
22339
+ tag: prior.tag,
22340
+ tagSha: prior.tagSha,
22341
+ retried,
22342
+ status,
22343
+ note: status === "success" ? `retried the failed ${joinLegNames(retried)} of published ${prior.tag} \u2014 the ledger is phases-green (#6985)` : `retried the failed ${joinLegNames(retried)} of published ${prior.tag} \u2014 ${joinLegNames(stillFailed)} still failed (#6985)`,
22344
+ ...retried.includes("deploy") ? { deploy: phases.deploy } : {},
22345
+ ...retried.includes("alignment") ? { alignment: phases.alignment } : {}
22346
+ };
22347
+ });
22348
+ }
22127
22349
 
22128
22350
  // src/train-doctor.ts
22129
22351
  init_client_version();
@@ -22221,7 +22443,7 @@ function readOriginWorkflowsDefault(root, ref) {
22221
22443
  }
22222
22444
  var NPM_REGISTRY = "https://registry.npmjs.org";
22223
22445
  async function probeRegistryWhoami(train) {
22224
- const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0, import_node_os13.tmpdir)(), "mmi-npmrc-"));
22446
+ const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0, import_node_os14.tmpdir)(), "mmi-npmrc-"));
22225
22447
  const rc = (0, import_node_path23.join)(dir, "npmrc");
22226
22448
  try {
22227
22449
  (0, import_node_fs23.writeFileSync)(rc, "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n");
@@ -22644,7 +22866,7 @@ async function runTrainDoctor(input) {
22644
22866
  // #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
22645
22867
  // ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
22646
22868
  title: `a prior ${ledger.lane ?? "release"} ledger for ${ledger.tag} is ${derived.phaseStatus} (${open2}) \u2014 a new train must not stack on it (#5987)`,
22647
- remedy: failed ? `diagnose the failed leg, then ${ledgerLaneResumeCommand(ledger)} once its cause is fixed (an abort only with fresh approval)${supersession}` : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges)${supersession}`
22869
+ remedy: failed ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges)${supersession}`
22648
22870
  });
22649
22871
  } else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
22650
22872
  add({
@@ -27122,7 +27344,7 @@ function registerBoardCommands(program3) {
27122
27344
 
27123
27345
  // src/bootstrap-commands.ts
27124
27346
  var import_node_fs26 = require("node:fs");
27125
- var import_node_os14 = require("node:os");
27347
+ var import_node_os15 = require("node:os");
27126
27348
  var import_node_path26 = require("node:path");
27127
27349
  init_cli_shared();
27128
27350
  init_clean_exit();
@@ -29130,7 +29352,7 @@ function registerBootstrapCommands(program3) {
29130
29352
  const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
29131
29353
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
29132
29354
  const putSeed = async (target, content, ref, sha) => {
29133
- const tmp = (0, import_node_path26.join)((0, import_node_os14.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
29355
+ const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
29134
29356
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
29135
29357
  try {
29136
29358
  await gh(contentPutInputArgs(repo, target, tmp));
@@ -29771,7 +29993,7 @@ LIVE apply to ${repo}:
29771
29993
  } catch {
29772
29994
  existingSha = void 0;
29773
29995
  }
29774
- const tmp = (0, import_node_path26.join)((0, import_node_os14.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
29996
+ const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
29775
29997
  const desiredContent = desiredByRepo.get(rec.repo);
29776
29998
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
29777
29999
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
@@ -29946,7 +30168,7 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
29946
30168
  } catch {
29947
30169
  existingSha = void 0;
29948
30170
  }
29949
- const tmp = (0, import_node_path26.join)((0, import_node_os14.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
30171
+ const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
29950
30172
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
29951
30173
  try {
29952
30174
  await gh(contentPutInputArgs(repo, seed.target, tmp));
@@ -31897,7 +32119,7 @@ async function collectWaveStatus(deps) {
31897
32119
  init_github_client();
31898
32120
  init_cli_shared();
31899
32121
  var import_node_fs30 = require("node:fs");
31900
- var import_node_os15 = require("node:os");
32122
+ var import_node_os16 = require("node:os");
31901
32123
  var import_node_path29 = require("node:path");
31902
32124
  var GC_GH_TIMEOUT_MS = 2e4;
31903
32125
  async function collectStatus() {
@@ -32086,7 +32308,7 @@ async function collectOnboardStatus(opts = {}) {
32086
32308
  else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
32087
32309
  else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
32088
32310
  }
32089
- const home = (0, import_node_os15.homedir)();
32311
+ const home = (0, import_node_os16.homedir)();
32090
32312
  const plugin = onboardPluginGate({
32091
32313
  readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
32092
32314
  readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
@@ -34114,7 +34336,7 @@ init_error_codes();
34114
34336
  init_github_client();
34115
34337
 
34116
34338
  // src/issue-body.ts
34117
- var import_node_os16 = require("node:os");
34339
+ var import_node_os17 = require("node:os");
34118
34340
  init_error_codes();
34119
34341
  var TextArgError = class extends Error {
34120
34342
  constructor(message2, code, offendingFlag) {
@@ -34127,7 +34349,7 @@ var TextArgError = class extends Error {
34127
34349
  offendingFlag;
34128
34350
  };
34129
34351
  function emptyStdinMessage(fileFlag) {
34130
- if ((0, import_node_os16.platform)() === "win32") {
34352
+ if ((0, import_node_os17.platform)() === "win32") {
34131
34353
  return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
34132
34354
  }
34133
34355
  return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
@@ -36642,7 +36864,7 @@ function registerSchedulesCommands(program3) {
36642
36864
  // src/secrets-commands.ts
36643
36865
  var import_node_fs33 = require("node:fs");
36644
36866
  var import_node_path31 = require("node:path");
36645
- var import_node_os17 = require("node:os");
36867
+ var import_node_os18 = require("node:os");
36646
36868
  init_cli_shared();
36647
36869
  init_hub_auth();
36648
36870
  init_github_client();
@@ -36768,7 +36990,7 @@ async function decryptRailsCredentials(input) {
36768
36990
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
36769
36991
  "puts JSON.generate(config.config)"
36770
36992
  ].join("\n");
36771
- const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os17.tmpdir)(), "mmi-rails-decrypt-"));
36993
+ const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os18.tmpdir)(), "mmi-rails-decrypt-"));
36772
36994
  const scriptPath = (0, import_node_path31.join)(scriptDir, "decrypt.rb");
36773
36995
  (0, import_node_fs33.writeFileSync)(scriptPath, script, "utf8");
36774
36996
  try {
@@ -38164,7 +38386,7 @@ init_github_client();
38164
38386
  // src/merge-cleanup.ts
38165
38387
  var import_node_fs37 = require("node:fs");
38166
38388
  var import_node_path35 = require("node:path");
38167
- var import_node_os18 = require("node:os");
38389
+ var import_node_os19 = require("node:os");
38168
38390
  init_cli_shared();
38169
38391
  init_github_client();
38170
38392
 
@@ -39234,7 +39456,7 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
39234
39456
  return rewritten === base ? null : rewritten;
39235
39457
  }
39236
39458
  function writeSquashBodyFile(body) {
39237
- const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
39459
+ const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-squash-body-"));
39238
39460
  const path2 = (0, import_node_path35.join)(dir, "body.txt");
39239
39461
  (0, import_node_fs37.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
39240
39462
  `, "utf8");
@@ -39657,7 +39879,7 @@ function postMergeReconWarnings(input) {
39657
39879
  // src/review-verdict.ts
39658
39880
  var import_node_child_process13 = require("node:child_process");
39659
39881
  var import_node_fs39 = require("node:fs");
39660
- var import_node_os19 = require("node:os");
39882
+ var import_node_os20 = require("node:os");
39661
39883
  var import_node_path37 = require("node:path");
39662
39884
  init_cli_shared();
39663
39885
  var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
@@ -39823,7 +40045,7 @@ async function readPrIssueComments(number, repo) {
39823
40045
  return comments;
39824
40046
  }
39825
40047
  async function postPrCommentFromFile(number, repo, body) {
39826
- const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path37.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
40048
+ const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path37.join)((0, import_node_os20.tmpdir)(), "mmi-review-verdict-"));
39827
40049
  const path2 = (0, import_node_path37.join)(dir, "body.md");
39828
40050
  try {
39829
40051
  (0, import_node_fs39.writeFileSync)(path2, body, "utf8");
@@ -42808,7 +43030,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
42808
43030
  var import_node_child_process16 = require("node:child_process");
42809
43031
  var import_node_crypto13 = require("node:crypto");
42810
43032
  var import_node_fs45 = require("node:fs");
42811
- var import_node_os20 = require("node:os");
43033
+ var import_node_os21 = require("node:os");
42812
43034
  var import_node_path42 = require("node:path");
42813
43035
 
42814
43036
  // ../scripts/distribution-digest.mjs
@@ -42959,7 +43181,7 @@ function rebuildTo(packageRoot, outDir) {
42959
43181
  });
42960
43182
  }
42961
43183
  function runDistStatus(root) {
42962
- const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os20.tmpdir)(), "mmi-dist-drift-"));
43184
+ const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
42963
43185
  let overlayCount = 0;
42964
43186
  try {
42965
43187
  const cliOut = (0, import_node_path42.join)(stage, "cli-dist");
@@ -44542,6 +44764,48 @@ ${decision.note}`);
44542
44764
  const localSync = await syncLocalTrainBranches(deps.run, { fetch: false, warn: deps.warn }).catch(() => void 0);
44543
44765
  return { ...result, ...localSync ? { localSync } : {} };
44544
44766
  }
44767
+ async function runHotfixRetryDeploy(deps, versionInput) {
44768
+ const ctx = await buildTrainApplyContext(deps);
44769
+ const { tag } = normalizeHotfixVersion(versionInput);
44770
+ const status = await deps.run("git", ["status", "--porcelain"]);
44771
+ if (porcelainHasBlockingChanges(status)) {
44772
+ throw new Error(`working tree must be clean before hotfix --retry-deploy${describeBlockingPaths(status)}`);
44773
+ }
44774
+ const root = await hotfixCheckoutRoot(deps);
44775
+ await deps.run("git", ["fetch", "origin", "--tags"]);
44776
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
44777
+ const deployModel = requireDeployModel(meta, ctx.repo);
44778
+ const result = await retryHotfixDeployLegs(deps, root, {
44779
+ retryAlignment: async (prior) => {
44780
+ let foldNote2;
44781
+ let foldStatus2 = "ok";
44782
+ try {
44783
+ foldNote2 = await portFoldToDevelopment(deps, ctx, deployModel, prior.tag);
44784
+ } catch (e) {
44785
+ foldStatus2 = "failure";
44786
+ foldNote2 = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(prior.tag)}-manual origin/development && ${foldPortManualCommand(deployModel, prior.tag.replace(/^v/, ""))}, then push it as the canonical head (git push origin ${devFoldBranch(prior.tag)}-manual:refs/heads/${devFoldBranch(prior.tag)}) and open a development-base PR \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
44787
+ }
44788
+ return hotfixFoldAlignmentEntry(foldStatus2, foldNote2, prior.tagSha);
44789
+ }
44790
+ });
44791
+ const ledger = openReleaseLedgerWriter(root);
44792
+ const report = releaseLedgerReport(
44793
+ await ledger.load({ repo: result.repo, tag: result.tag, tagSha: result.tagSha }).catch(() => void 0),
44794
+ ledger.path
44795
+ );
44796
+ const deployNote = result.deploy?.note ?? result.deploy?.error;
44797
+ const foldNote = result.alignment?.note ?? result.alignment?.error;
44798
+ const foldStatus = result.alignment ? result.alignment.state === "failed" ? "failure" : result.alignment.state === "pending" ? "unresolved" : "ok" : void 0;
44799
+ const runs = result.deploy ? [{ workflow: result.deploy.workflow ?? "jerv-gateway", conclusion: result.deploy.state === "complete" ? "success" : result.deploy.state === "failed" ? "failure" : "pending" }] : void 0;
44800
+ return {
44801
+ ...result,
44802
+ ...deployNote ? { deployNote } : {},
44803
+ ...foldNote ? { foldNote } : {},
44804
+ ...foldStatus ? { foldStatus } : {},
44805
+ ...runs ? { runs } : {},
44806
+ ...report ? { ledger: report } : {}
44807
+ };
44808
+ }
44545
44809
  function deriveHotfixState(f) {
44546
44810
  if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
44547
44811
  return { state: "not-started", next: `mmi-cli devops hotfix start --from <pr#|sha> (would mint ${f.tag})` };
@@ -45117,7 +45381,7 @@ async function announceSummaryRefusal(path2) {
45117
45381
  function trainApplyDeps() {
45118
45382
  return {
45119
45383
  run: async (file, args) => {
45120
- const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && args[0] === "scripts/jerv-gateway-release-deploy.sh" ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
45384
+ const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && /jerv-gateway-release-deploy\.sh$/.test(args[0] ?? "") ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
45121
45385
  try {
45122
45386
  return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, args, { timeout })).stdout;
45123
45387
  } catch (e) {
@@ -45792,6 +46056,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45792
46056
  const evidenced = hotfixRunWithEvidence(run);
45793
46057
  return ` - ${evidenced.workflow}: ${evidenced.conclusion}${evidenced.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""} (${evidenced.url ?? evidenced.runUrlNote})`;
45794
46058
  }
46059
+ function renderHotfixRetryDeploy(r) {
46060
+ return [
46061
+ `mmi-cli devops hotfix release --retry-deploy: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`,
46062
+ ...r.deployNote ? [` - deploy: ${r.deployNote}`] : [],
46063
+ ...r.foldNote ? [` - fold: ${r.foldNote}`] : [],
46064
+ ...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
46065
+ ].join("\n");
46066
+ }
45795
46067
  function renderHotfixRelease(r) {
45796
46068
  return [
45797
46069
  `mmi-cli devops hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
@@ -45865,11 +46137,19 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45865
46137
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
45866
46138
  });
45867
46139
  hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--json", "machine-readable output").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o, renderHotfixStart));
45868
- hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => runHotfixSub("release", async () => {
45869
- const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
45870
- const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
45871
- return { ...result, projectInfoSync };
45872
- }, o, renderHotfixRelease));
46140
+ hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
46141
+ if (o.retryDeploy) {
46142
+ if (o.announceSummaryFile || o.carries) {
46143
+ return fail("hotfix release: --retry-deploy accepts only --json and --out; it re-runs failed deploy/alignment legs of the published tag and does not recut");
46144
+ }
46145
+ return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
46146
+ }
46147
+ return runHotfixSub("release", async () => {
46148
+ const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
46149
+ const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
46150
+ return { ...result, projectInfoSync };
46151
+ }, o, renderHotfixRelease);
46152
+ });
45873
46153
  function hotfixStatusDeps() {
45874
46154
  const STATUS_GH_MS = 8e3;
45875
46155
  const STATUS_GIT_MS = 1e4;
@@ -45911,7 +46191,7 @@ function envHealLockPath(home) {
45911
46191
  async function withEnvHealLock(what, run) {
45912
46192
  try {
45913
46193
  return await withFileLock(
45914
- envHealLockPath((0, import_node_os21.homedir)()),
46194
+ envHealLockPath((0, import_node_os22.homedir)()),
45915
46195
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
45916
46196
  run
45917
46197
  );
@@ -46008,7 +46288,7 @@ function mmiDoctorDeps(opts = {}) {
46008
46288
  const configRoot = surfaceConfigRoot(surface);
46009
46289
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
46010
46290
  const plan = buildPluginCachePlan(
46011
- (0, import_node_os21.homedir)(),
46291
+ (0, import_node_os22.homedir)(),
46012
46292
  running,
46013
46293
  pluginCacheFsDeps(configRoot, () => 0),
46014
46294
  { configRoot, includeStaging: surface !== "codex" }
@@ -46036,7 +46316,7 @@ function mmiDoctorDeps(opts = {}) {
46036
46316
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
46037
46317
  const installed = installedActivePluginVersion(surface);
46038
46318
  const plan = buildPluginCachePlan(
46039
- (0, import_node_os21.homedir)(),
46319
+ (0, import_node_os22.homedir)(),
46040
46320
  running,
46041
46321
  pluginCacheFsDeps(configRoot, () => 0),
46042
46322
  { configRoot, includeStaging: surface !== "codex", installedVersion: installed }
@@ -47261,7 +47541,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
47261
47541
  return;
47262
47542
  }
47263
47543
  const plan = buildPluginCachePlan(
47264
- (0, import_node_os21.homedir)(),
47544
+ (0, import_node_os22.homedir)(),
47265
47545
  running,
47266
47546
  pluginCacheFsDeps(configRoot, directoryBytes),
47267
47547
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.4.16",
3
+ "version": "4.4.17",
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",