@mutmutco/cli 4.4.16 → 4.5.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 +666 -115
  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
@@ -6245,6 +6245,44 @@ function collectPullRequestWorkflowContexts(workflows) {
6245
6245
  }
6246
6246
  return [...contexts].sort((a, b) => a.localeCompare(b));
6247
6247
  }
6248
+ function jobNeeds(job) {
6249
+ const lines2 = job.body.split(/\r?\n/).slice(1);
6250
+ const at = lines2.findIndex((line) => /^\s+needs:/.test(line));
6251
+ if (at < 0) return [];
6252
+ const indent = lines2[at].match(/^(\s*)/)?.[1].length ?? 0;
6253
+ const inline = lines2[at].replace(/^\s*needs:\s*/, "").replace(/#.*$/, "").trim();
6254
+ if (inline) return inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter(Boolean);
6255
+ const items = [];
6256
+ for (const line of lines2.slice(at + 1)) {
6257
+ if (!line.trim()) continue;
6258
+ if ((line.match(/^(\s*)/)?.[1].length ?? 0) <= indent) break;
6259
+ items.push(unquote(line.replace(/^\s*-\s*/, "")));
6260
+ }
6261
+ return items.filter(Boolean);
6262
+ }
6263
+ function unquote(s) {
6264
+ return s.replace(/#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
6265
+ }
6266
+ function unblockingGateJobs(workflows, required) {
6267
+ const unblocking = /* @__PURE__ */ new Set();
6268
+ for (const wf of workflows) {
6269
+ if (!workflowTriggersPullRequest(wf.body)) continue;
6270
+ const jobs = parseWorkflowJobs(wf.body);
6271
+ const needs = new Map(jobs.map((job) => [job.id, jobNeeds(job)]));
6272
+ const covered = /* @__PURE__ */ new Set();
6273
+ const stack = jobs.map((job) => job.id).filter((id) => required.has(id));
6274
+ while (stack.length) {
6275
+ const id = stack.pop();
6276
+ if (covered.has(id)) continue;
6277
+ covered.add(id);
6278
+ stack.push(...needs.get(id) ?? []);
6279
+ }
6280
+ for (const job of jobs) {
6281
+ if (!covered.has(job.id) && jobReportsPullRequestCheck(job)) unblocking.add(job.id);
6282
+ }
6283
+ }
6284
+ return [...unblocking].sort((a, b) => a.localeCompare(b));
6285
+ }
6248
6286
  function pathFilteredPullRequestWorkflows(workflows) {
6249
6287
  const filtered = [];
6250
6288
  for (const wf of workflows) {
@@ -15938,10 +15976,10 @@ var rollout_plan_default = {
15938
15976
  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
15977
  },
15940
15978
  baseline: {
15941
- version: "4.4.16",
15942
- tag: "v4.4.16",
15943
- commit: "1a7eeee13010",
15944
- npm: "@mutmutco/cli@4.4.16"
15979
+ version: "4.5.0",
15980
+ tag: "v4.5.0",
15981
+ commit: "e9c83872b853",
15982
+ npm: "@mutmutco/cli@4.5.0"
15945
15983
  },
15946
15984
  exitCriterion: "fleet-n-of-n",
15947
15985
  hubOnlyShortcut: "forbidden",
@@ -15958,14 +15996,14 @@ var rollout_plan_default = {
15958
15996
  repo: "mutmutco/mmi-hub",
15959
15997
  role: "canary",
15960
15998
  schedule: "train",
15961
- v3Target: "v4.4.16"
15999
+ v3Target: "v4.5.0"
15962
16000
  }
15963
16001
  ],
15964
16002
  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
16003
  rollback: {
15966
16004
  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)"
16005
+ mechanism: "npm dist-tag latest -> 4.5.0 and redeploy the Hub Lambda from tag v4.5.0 (e9c83872b853); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16006
+ v3Target: "v4.5.0 (@mutmutco/cli@4.5.0, tag commit e9c83872b853 \u2014 last known-good release carrying the repo-index v4-only contract)"
15969
16007
  }
15970
16008
  },
15971
16009
  {
@@ -18487,6 +18525,13 @@ async function awaitAlignmentLanded(deps, ctx, leg, base, opts = {}) {
18487
18525
  await sleep2(ALIGNMENT_LAND_POLL_MS);
18488
18526
  }
18489
18527
  }
18528
+ async function awaitAlignmentsLanded(deps, ctx, legs) {
18529
+ const [dev, rc] = await Promise.all([
18530
+ awaitAlignmentLanded(deps, ctx, legs.dev, "development"),
18531
+ legs.rc ? awaitAlignmentLanded(deps, ctx, legs.rc, "rc") : void 0
18532
+ ]);
18533
+ return { dev, ...rc ? { rc } : {} };
18534
+ }
18490
18535
  async function alignRcForward(deps, ctx, tag) {
18491
18536
  deps.warn?.("Release: Bringing the release candidate up to date");
18492
18537
  await runGitRemoteRead(deps, ["fetch", "origin", "main", "rc"]);
@@ -18803,6 +18848,7 @@ function isReleaseShaDeployEnumerationCandidate(row, nonDeployEvents) {
18803
18848
 
18804
18849
  // src/train-apply-deploy.ts
18805
18850
  var import_node_fs19 = require("node:fs");
18851
+ var import_node_os10 = require("node:os");
18806
18852
  var import_node_path17 = require("node:path");
18807
18853
 
18808
18854
  // src/vercel-deployments.ts
@@ -18888,6 +18934,12 @@ async function observePublishIfRequired(deps, ctx, meta, model, stage, releaseSh
18888
18934
  deployStatus: aggregateWorkflowRuns(workflowRuns)
18889
18935
  };
18890
18936
  }
18937
+ async function joinDeployAndPublish(deploy, publish) {
18938
+ const [d, p] = await Promise.all([deploy, publish]);
18939
+ if (!p) return [d, p];
18940
+ const mark = (x) => x.workflowRuns ? { ...x, workflowRuns: x.workflowRuns.map((r) => ({ ...r, parallel: true })) } : x;
18941
+ return [mark(d), mark(p)];
18942
+ }
18891
18943
  function appendPublishDispatch(deploy, publish) {
18892
18944
  if (!publish) return deploy;
18893
18945
  return {
@@ -19052,19 +19104,29 @@ async function appendServedChannelPublish(deps, input, dispatch) {
19052
19104
  };
19053
19105
  }
19054
19106
  const probe = await (deps.readServedChannelVersion ?? fetchServedChannelVersion)(channelUrl);
19107
+ const evidence = (conclusion, text) => [...runs, servedChannelRow(HUB_SERVED_PRODUCT, conclusion, text)];
19108
+ if (!probe.ok && staleServedProcess(probe)) {
19109
+ const redeploy = mmiGetRedeployCommand(input.repo);
19110
+ return {
19111
+ ...dispatch,
19112
+ note: `${dispatch.note}; get-publish.yml succeeded but ${channelUrl} answered HTTP ${probe.status} \u2014 STALE SERVED PROCESS: the files may be current, but the process serving them predates GET /release/version; recover with \`${redeploy}\``,
19113
+ workflowRuns: evidence("failure", `${channelUrl} answered HTTP ${probe.status} (stale served process)`),
19114
+ deployStatus: "failure"
19115
+ };
19116
+ }
19055
19117
  if (!probe.ok) {
19056
19118
  const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
19057
19119
  return {
19058
19120
  ...dispatch,
19059
19121
  note: `${dispatch.note}; get-publish.yml succeeded but the served channel version could NOT be read at ${channelUrl} (${why}) \u2014 the move is unverified`,
19060
- workflowRuns: runs
19122
+ workflowRuns: evidence("pending", `${channelUrl} unreadable (${why})`)
19061
19123
  };
19062
19124
  }
19063
19125
  if (expected && probe.version !== expected) {
19064
19126
  return {
19065
19127
  ...dispatch,
19066
19128
  note: `${dispatch.note}; get-publish.yml succeeded but the served channel still reports ${probe.version} (expected launcher ${expected}) \u2014 THE CHANNEL DID NOT MOVE; recover with \`${remedy}\``,
19067
- workflowRuns: runs,
19129
+ workflowRuns: evidence("failure", `${channelUrl} reports ${probe.version}, expected ${expected}`),
19068
19130
  deployStatus: "failure"
19069
19131
  };
19070
19132
  }
@@ -19072,16 +19134,107 @@ async function appendServedChannelPublish(deps, input, dispatch) {
19072
19134
  return {
19073
19135
  ...dispatch,
19074
19136
  note: `${dispatch.note}; get-publish.yml succeeded and ${channelUrl} reports ${probe.version}, but the local launcher version could not be read \u2014 nothing was compared, so the move is unverified`,
19075
- workflowRuns: runs
19137
+ workflowRuns: evidence("pending", `${channelUrl} reports ${probe.version}; nothing to compare against`)
19076
19138
  };
19077
19139
  }
19078
19140
  return {
19079
19141
  ...dispatch,
19080
19142
  note: `${dispatch.note}; served channel moved \u2014 ${channelUrl} now reports ${probe.version}`,
19081
- workflowRuns: runs
19143
+ workflowRuns: evidence("success", `${channelUrl} reports ${probe.version}`)
19082
19144
  };
19083
19145
  }
19146
+ function staleServedProcess(probe) {
19147
+ return probe.status === 401 || probe.status === 403 || probe.status === 404;
19148
+ }
19149
+ function servedChannelRow(product, conclusion, evidence) {
19150
+ return { workflow: `served-channel(${product})`, runUrlNote: evidence, conclusion };
19151
+ }
19152
+ var JERV_GATEWAY_REMEDY = "from the Jerv-Hub checkout, re-run the Gateway deploy on its current release tag: `bash scripts/jerv-gateway-release-deploy.sh --tag <jerv-hub tag>`";
19153
+ var SERVED_CHANNELS = {
19154
+ "mutmutco/mm-strategy": {
19155
+ product: "mm-strategy",
19156
+ url: "https://strategy.mutmut.work/release/version",
19157
+ mover: "the MM-Strategy container deploy (the running server's own version is the pointer)",
19158
+ remedy: "`mmi-cli devops runtime tenant redeploy mutmutco/MM-Strategy main`"
19159
+ },
19160
+ "mutmutco/jerv-hub": {
19161
+ product: "jerv",
19162
+ url: "https://get.jerv.ai/jerv/release/version",
19163
+ mover: "the Jerv Gateway deploy (`jerv-gate fetch-release --product jerv`)",
19164
+ remedy: JERV_GATEWAY_REMEDY
19165
+ },
19166
+ "mutmutco/jerv-jervcode": {
19167
+ product: "jervcode",
19168
+ url: "https://get.jerv.ai/jervcode/release/version",
19169
+ mover: "the Jerv Gateway deploy, which fetches JervCode's latest release \u2014 a JervCode release alone does not move it",
19170
+ remedy: JERV_GATEWAY_REMEDY
19171
+ }
19172
+ };
19173
+ function servedChannelFor(repo) {
19174
+ return SERVED_CHANNELS[repo.toLowerCase()];
19175
+ }
19176
+ var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
19177
+ var SERVED_CHANNEL_PROBE_BACKOFF_MS = 1e4;
19178
+ async function appendServedChannelProof(deps, input, dispatch) {
19179
+ const channel = servedChannelFor(input.repo);
19180
+ if (!channel) return dispatch;
19181
+ const expected = input.tag.replace(/^v/, "");
19182
+ const withProof = (conclusion, evidence, note, failed = false) => ({
19183
+ ...dispatch,
19184
+ note: `${dispatch.note}; ${note}`,
19185
+ workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
19186
+ ...failed ? { deployStatus: "failure" } : {}
19187
+ });
19188
+ if (!input.shipped) {
19189
+ return withProof("pending", `${channel.url} not probed`, `served channel ${channel.url} not probed \u2014 deploy/publish are not proven green, so it is not expected to have moved`);
19190
+ }
19191
+ const read = deps.readServedChannelVersion ?? fetchServedChannelVersion;
19192
+ let probe = await read(channel.url);
19193
+ for (let attempt = 1; attempt < SERVED_CHANNEL_PROBE_ATTEMPTS && probe.ok && probe.version !== expected; attempt++) {
19194
+ await (deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(SERVED_CHANNEL_PROBE_BACKOFF_MS * attempt);
19195
+ probe = await read(channel.url);
19196
+ }
19197
+ if (!probe.ok && staleServedProcess(probe)) {
19198
+ return withProof(
19199
+ "failure",
19200
+ `${channel.url} answered HTTP ${probe.status} (stale served process)`,
19201
+ `${channel.url} answered HTTP ${probe.status} \u2014 STALE SERVED PROCESS: the process serving ${channel.product} predates GET /release/version; it moves with ${channel.mover}; recover with ${channel.remedy}`,
19202
+ true
19203
+ );
19204
+ }
19205
+ if (!probe.ok) {
19206
+ const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
19207
+ return withProof("pending", `${channel.url} unreadable (${why})`, `served channel ${channel.url} could NOT be read (${why}) \u2014 the move is unverified`);
19208
+ }
19209
+ if (probe.version !== expected) {
19210
+ return withProof(
19211
+ "failure",
19212
+ `${channel.url} reports ${probe.version}, expected ${expected}`,
19213
+ `served channel ${channel.url} still reports ${probe.version} (expected ${expected}) \u2014 THE CHANNEL DID NOT MOVE: new installs get ${probe.version}; it moves with ${channel.mover}; recover with ${channel.remedy}`,
19214
+ true
19215
+ );
19216
+ }
19217
+ return withProof("success", `${channel.url} reports ${probe.version}`, `served channel moved \u2014 ${channel.url} reports ${probe.version}`);
19218
+ }
19084
19219
  var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
19220
+ var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
19221
+ async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
19222
+ let script;
19223
+ try {
19224
+ script = await deps.run("git", ["show", `${tagSha}:${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT}`]);
19225
+ } catch (error) {
19226
+ const reason = error instanceof Error ? error.message : String(error);
19227
+ throw new Error(`could not extract ${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT} from tagged commit ${tagSha.slice(0, 12)}: ${reason}`);
19228
+ }
19229
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path17.join)((0, import_node_os10.tmpdir)(), "jerv-gateway-release-deploy-"));
19230
+ const extracted = (0, import_node_path17.join)(dir, "jerv-gateway-release-deploy.sh");
19231
+ try {
19232
+ (0, import_node_fs19.writeFileSync)(extracted, script, "utf8");
19233
+ return await deps.run("bash", [extracted, "--tag", tag]);
19234
+ } finally {
19235
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
19236
+ }
19237
+ }
19085
19238
  async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch) {
19086
19239
  if (!isJervHubRepo(repo)) return dispatch;
19087
19240
  if (dispatch.deployStatus !== "success") {
@@ -19092,7 +19245,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
19092
19245
  };
19093
19246
  }
19094
19247
  try {
19095
- const output = await deps.run("bash", ["scripts/jerv-gateway-release-deploy.sh", "--tag", tag]);
19248
+ const output = await runTaggedJervGatewayLocalWrapper(deps, tag, tagSha);
19096
19249
  const receipt = output.trim().split(/\r?\n/).reverse().map((line) => {
19097
19250
  try {
19098
19251
  return JSON.parse(line);
@@ -19110,26 +19263,27 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
19110
19263
  deployStatus: "success"
19111
19264
  };
19112
19265
  } catch (error) {
19113
- const detail = (error instanceof Error ? error.message : String(error)).split(/\r?\n/, 1)[0].slice(0, 240);
19266
+ const full = error instanceof Error ? error.message : String(error);
19114
19267
  return {
19115
19268
  ...dispatch,
19116
19269
  // #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`,
19270
+ // #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).
19271
+ note: `${dispatch.note}; Jerv Gateway deploy FAILED (${full}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
19118
19272
  workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
19119
19273
  deployStatus: "failure"
19120
19274
  };
19121
19275
  }
19122
19276
  }
19123
19277
  async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
19124
- const workflowRuns = [];
19125
- for (const target of targets) {
19278
+ const parallel = targets.length > 1 ? { parallel: true } : {};
19279
+ const workflowRuns = await Promise.all(targets.map(async (target) => {
19126
19280
  try {
19127
19281
  const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
19128
- workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run, repo));
19282
+ return { ...await watchWorkflowRun(deps, target.workflow, run, repo), ...parallel };
19129
19283
  } catch {
19130
- workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
19284
+ return { workflow: target.workflow, conclusion: "failure", ...parallel };
19131
19285
  }
19132
- }
19286
+ }));
19133
19287
  if (!enumerateSha) return workflowRuns;
19134
19288
  const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
19135
19289
  workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
@@ -19184,8 +19338,8 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
19184
19338
  return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
19185
19339
  }
19186
19340
  function shipLegsGreen(deploy, publish) {
19187
- const green = (s) => s === "complete" || s === "skipped";
19188
- return green(deploy?.state) && green(publish?.state);
19341
+ const green = (leg) => leg?.superseded !== true && (leg?.state === "complete" || leg?.state === "skipped");
19342
+ return green(deploy) && green(publish);
19189
19343
  }
19190
19344
  async function announceShippedRelease(deps, args, model, legs, deferred) {
19191
19345
  if (!deps.announce) return void 0;
@@ -19851,12 +20005,12 @@ Resume it: ${resumeCommand}
19851
20005
  // src/train-doctor.ts
19852
20006
  var import_node_child_process9 = require("node:child_process");
19853
20007
  var import_node_fs23 = require("node:fs");
19854
- var import_node_os13 = require("node:os");
20008
+ var import_node_os14 = require("node:os");
19855
20009
  var import_node_path23 = require("node:path");
19856
20010
 
19857
20011
  // src/doctor-io.ts
19858
20012
  var import_node_fs20 = require("node:fs");
19859
- var import_node_os10 = require("node:os");
20013
+ var import_node_os11 = require("node:os");
19860
20014
  var import_node_path18 = require("node:path");
19861
20015
  var import_node_child_process7 = require("node:child_process");
19862
20016
  var import_node_util4 = require("node:util");
@@ -19886,7 +20040,7 @@ function execFileCapture(file, args, opts = {}) {
19886
20040
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
19887
20041
  function stagedClaudePluginVersion() {
19888
20042
  try {
19889
- const stagingRoot = (0, import_node_path18.join)((0, import_node_os10.homedir)(), ".claude", "plugins", "staging");
20043
+ const stagingRoot = (0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "staging");
19890
20044
  const record = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(stagingRoot, "launch-target.json"), "utf8"));
19891
20045
  if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
19892
20046
  const manifest = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
@@ -19899,7 +20053,7 @@ function stagedClaudePluginVersion() {
19899
20053
  function installedClaudePluginVersion() {
19900
20054
  try {
19901
20055
  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")
20056
+ (0, import_node_fs20.readFileSync)((0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
19903
20057
  );
19904
20058
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
19905
20059
  if (versions.length === 0) return void 0;
@@ -20071,11 +20225,11 @@ function compatHolds(range, version) {
20071
20225
 
20072
20226
  // src/claude-binary-doctor.ts
20073
20227
  var import_node_fs21 = require("node:fs");
20074
- var import_node_os12 = require("node:os");
20228
+ var import_node_os13 = require("node:os");
20075
20229
  var import_node_path20 = require("node:path");
20076
20230
 
20077
20231
  // src/jerv-cli-spawn.ts
20078
- var import_node_os11 = require("node:os");
20232
+ var import_node_os12 = require("node:os");
20079
20233
  var import_node_path19 = require("node:path");
20080
20234
  init_cli_shared();
20081
20235
  var JERV_CLI_ENTRY = (0, import_node_path19.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
@@ -20100,7 +20254,7 @@ function normalizeSpawnPathEntry(entry, platform2 = process.platform) {
20100
20254
  if (msys) return `${msys[1].toUpperCase()}:\\${msys[2].replace(/\//g, "\\")}`;
20101
20255
  return trimmed;
20102
20256
  }
20103
- function jervCliCandidateDirs(env = process.env, home = (0, import_node_os11.homedir)(), platform2 = process.platform) {
20257
+ function jervCliCandidateDirs(env = process.env, home = (0, import_node_os12.homedir)(), platform2 = process.platform) {
20104
20258
  const seen = /* @__PURE__ */ new Set();
20105
20259
  const out = [];
20106
20260
  const push = (dir) => {
@@ -20171,7 +20325,7 @@ function globalNodeModulesRoots(host) {
20171
20325
  };
20172
20326
  const prefix = env.npm_config_prefix?.trim();
20173
20327
  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)) {
20328
+ for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os13.homedir)(), platform2)) {
20175
20329
  push((0, import_node_path20.join)(dir, "node_modules"));
20176
20330
  push((0, import_node_path20.join)((0, import_node_path20.dirname)(dir), "lib", "node_modules"));
20177
20331
  }
@@ -20818,6 +20972,53 @@ var MAX_TEXT = 500;
20818
20972
  function truncate(text, max) {
20819
20973
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
20820
20974
  }
20975
+ var FAILING_OUTPUT_LEGS = /* @__PURE__ */ new Set(["publish", "deploy", "alignment"]);
20976
+ var LEDGER_OUTPUT_FILE_LINES = 50;
20977
+ var LEDGER_OUTPUT_NOTE_LINES = 5;
20978
+ function lastNonEmptyLines(text, n) {
20979
+ const lines2 = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
20980
+ while (lines2.length > 0 && lines2[lines2.length - 1] === "") lines2.pop();
20981
+ return lines2.slice(-n);
20982
+ }
20983
+ function attachOutputNote(note, suffix) {
20984
+ const room = Math.max(0, MAX_TEXT - suffix.length - 3);
20985
+ const head = note && room > 20 ? truncate(note, room) : "";
20986
+ return truncate(head ? `${head} \u2014 ${suffix}` : suffix, MAX_TEXT);
20987
+ }
20988
+ function writeFailingLegOutput(artifactPath, output) {
20989
+ const fileLines = lastNonEmptyLines(output, LEDGER_OUTPUT_FILE_LINES);
20990
+ (0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(artifactPath), { recursive: true });
20991
+ (0, import_node_fs22.writeFileSync)(artifactPath, `${fileLines.join("\n")}
20992
+ `, "utf8");
20993
+ const tail = fileLines.slice(-LEDGER_OUTPUT_NOTE_LINES).join("\n");
20994
+ const noteSuffix = truncate(`failing-leg output: ${artifactPath}
20995
+ ${tail}`, MAX_TEXT);
20996
+ return { artifactPath, noteSuffix };
20997
+ }
20998
+ function stampPhaseRecord(ledgerPath, tag, phase, entry, previousRetries, now) {
20999
+ const { output: failingOutput, ...fields } = entry;
21000
+ let note = fields.note;
21001
+ if (fields.state === "failed" && FAILING_OUTPUT_LEGS.has(phase)) {
21002
+ const raw = failingOutput ?? fields.error ?? fields.note;
21003
+ if (raw) {
21004
+ try {
21005
+ const persisted = writeFailingLegOutput(
21006
+ (0, import_node_path22.join)((0, import_node_path22.dirname)((0, import_node_path22.dirname)(ledgerPath)), "ledger-output", `${tag}-${phase}.txt`),
21007
+ raw
21008
+ );
21009
+ note = attachOutputNote(note, persisted.noteSuffix);
21010
+ } catch {
21011
+ }
21012
+ }
21013
+ }
21014
+ return {
21015
+ ...fields,
21016
+ note: note !== void 0 ? truncate(note, MAX_TEXT) : void 0,
21017
+ error: fields.error !== void 0 ? truncate(fields.error, MAX_TEXT * 2) : void 0,
21018
+ retries: fields.retries ?? previousRetries,
21019
+ updatedAt: now().toISOString()
21020
+ };
21021
+ }
20821
21022
  function normalizeLedgerAnchors(anchors) {
20822
21023
  const repo = anchors.repo?.trim();
20823
21024
  const tag = anchors.tag?.trim();
@@ -21029,13 +21230,7 @@ function openReleaseLedgerWriter(cwd, opts = {}) {
21029
21230
  updatedAt: now().toISOString()
21030
21231
  };
21031
21232
  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
- };
21233
+ const record = stampPhaseRecord(path2, normalized.tag, phase, entry, previousRetries, now);
21039
21234
  const updated = {
21040
21235
  ...base,
21041
21236
  phases: { ...base.phases, [phase]: record },
@@ -21123,7 +21318,9 @@ var TRANSIENT_TRANSPORT_MARKERS = [
21123
21318
  /socket hang ?up/i,
21124
21319
  /remote end hung ?up unexpectedly/i,
21125
21320
  /early EOF/i,
21126
- /tls handshake timeout/i
21321
+ /tls handshake timeout/i,
21322
+ /kex_exchange_identification: connection closed by remote host/i,
21323
+ /connection closed by remote host/i
21127
21324
  ];
21128
21325
  function classifyTransientTransportFailure(evidence) {
21129
21326
  if (!evidence) return false;
@@ -21707,6 +21904,48 @@ function ledgerLaneResumeCommand(prior) {
21707
21904
  if (prior.lane === "hotfix") return `mmi-cli devops hotfix release ${prior.tag}`;
21708
21905
  return "mmi-cli devops release --resume (or, only when proven unpublished, mmi-cli devops release --abort --apply)";
21709
21906
  }
21907
+ var RETRY_DEPLOY_LEGS = /* @__PURE__ */ new Set(["deploy", "alignment"]);
21908
+ function joinLegNames(legs) {
21909
+ if (legs.length <= 1) return legs[0] ?? "";
21910
+ if (legs.length === 2) return `${legs[0]} and ${legs[1]}`;
21911
+ return `${legs.slice(0, -1).join(", ")} and ${legs[legs.length - 1]}`;
21912
+ }
21913
+ function formatFailedLegTails(ledger) {
21914
+ const chunks = [];
21915
+ for (const phase of ["publish", "deploy", "alignment"]) {
21916
+ const rec = ledger.phases[phase];
21917
+ if (rec.state !== "failed") continue;
21918
+ const text = rec.note ?? rec.error ?? "";
21919
+ const marker = "failing-leg output: ";
21920
+ const idx = text.indexOf(marker);
21921
+ if (idx < 0) continue;
21922
+ chunks.push(`${phase} ${text.slice(idx)}`);
21923
+ }
21924
+ return chunks.length ? `; ${chunks.join("; ")}` : "";
21925
+ }
21926
+ function hotfixRetryDeployShape(ledger) {
21927
+ if (ledger.lane !== "hotfix") return false;
21928
+ if (ledger.phases.githubRelease.state !== "complete") return false;
21929
+ const open2 = RELEASE_LEDGER_PHASES.filter((phase) => {
21930
+ const state = ledger.phases[phase].state;
21931
+ return state === "pending" || state === "failed";
21932
+ });
21933
+ if (open2.length === 0) return false;
21934
+ if (!open2.every((phase) => RETRY_DEPLOY_LEGS.has(phase))) return false;
21935
+ return open2.some((phase) => ledger.phases[phase].state === "failed");
21936
+ }
21937
+ function ledgerFailedRemedy(ledger) {
21938
+ const derived = deriveReleasePhaseStatus(ledger);
21939
+ const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
21940
+ const tails = formatFailedLegTails(ledger);
21941
+ if (hotfixRetryDeployShape(ledger)) {
21942
+ const failed = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
21943
+ 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}`;
21944
+ }
21945
+ const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
21946
+ 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)` : "";
21947
+ return `diagnose the failed leg, then ${ledgerLaneResumeCommand(ledger)} once its cause is fixed (an abort only with fresh approval)${supersession}${tails}`;
21948
+ }
21710
21949
  function alignmentPrNumbers(note) {
21711
21950
  return [...new Set([...(note ?? "").matchAll(/\/pull\/(\d+)/g)].map((m) => Number.parseInt(m[1], 10)))];
21712
21951
  }
@@ -21752,6 +21991,70 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
21752
21991
  return prior.tag;
21753
21992
  });
21754
21993
  }
21994
+ async function healUnfoldedRunLegs(deps, cwd, model) {
21995
+ const path2 = releaseLedgerPath(cwd);
21996
+ let prior;
21997
+ try {
21998
+ prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
21999
+ } catch {
22000
+ return void 0;
22001
+ }
22002
+ const legs = ["deploy", "publish"];
22003
+ const openLeg = (rec) => rec.state === "pending" && rec.superseded !== true;
22004
+ if (!legs.some((phase) => openLeg(prior.phases[phase]))) return void 0;
22005
+ const anchors = { repo: prior.repo, tag: prior.tag, tagSha: prior.tagSha };
22006
+ const folded = [];
22007
+ let current = prior;
22008
+ if (legs.some((phase) => openLeg(prior.phases[phase]) && prior.phases[phase].runId != null)) {
22009
+ let reverified;
22010
+ try {
22011
+ reverified = await reverifyPersistedRunPhases(deps, path2, anchors, { sha: prior.tagSha, repo: prior.repo });
22012
+ } catch {
22013
+ return void 0;
22014
+ }
22015
+ if (!reverified.ledger) return void 0;
22016
+ current = reverified.ledger;
22017
+ for (const phase of legs) {
22018
+ const before = prior.phases[phase];
22019
+ const after = current.phases[phase];
22020
+ if (openLeg(before) && before.runId != null && after.state !== "pending") folded.push({ phase, state: after.state, runId: after.runId });
22021
+ }
22022
+ }
22023
+ const targets = prodReleaseTargets(model);
22024
+ const runless = legs.filter((phase) => openLeg(current.phases[phase]) && current.phases[phase].runId == null);
22025
+ if (runless.length > 0 && targets.length > 0) {
22026
+ let rows;
22027
+ try {
22028
+ rows = await listOwnWorkflowRuns(deps, releaseRunRepoFor(model, prior.repo), targets, prior.tagSha);
22029
+ } catch {
22030
+ rows = [];
22031
+ }
22032
+ const inputs = phaseInputsFromRunRows(model, rows, prior.tagSha, { repo: prior.repo });
22033
+ const foldable = runless.filter((phase) => inputs[phase].runId != null && inputs[phase].state !== "pending");
22034
+ if (foldable.length > 0) {
22035
+ const written = await withReleaseLedger(path2, anchors, (ledger) => {
22036
+ if (!ledger) return { value: [] };
22037
+ const phases = { ...ledger.phases };
22038
+ const done = [];
22039
+ for (const phase of foldable) {
22040
+ const live = phases[phase];
22041
+ if (!openLeg(live) || live.runId != null) continue;
22042
+ const entry = compactEntry(inputs[phase]);
22043
+ phases[phase] = {
22044
+ ...entry,
22045
+ note: `${entry.note ?? `${phase} run on the release SHA`} \u2014 folded by train doctor --heal from the concluded run (#7003)`,
22046
+ retries: live.retries,
22047
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22048
+ };
22049
+ done.push({ phase, state: entry.state, runId: entry.runId });
22050
+ }
22051
+ return done.length ? { ledger: { ...ledger, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, value: done } : { value: done };
22052
+ }).catch(() => []);
22053
+ folded.push(...written);
22054
+ }
22055
+ }
22056
+ return folded.length ? { tag: prior.tag, folded } : void 0;
22057
+ }
21755
22058
  function supersedeableDeployLegShape(rec) {
21756
22059
  return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
21757
22060
  }
@@ -22098,9 +22401,10 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
22098
22401
  }
22099
22402
  if (derived.phaseStatus !== "phases-green") {
22100
22403
  const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
22404
+ const next = derived.phaseStatus === "phases-failed" ? ledgerFailedRemedy(prior) : `Finish it first: ${ledgerLaneResumeCommand(prior)} (#5987/#6068)`;
22101
22405
  return {
22102
22406
  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)`
22407
+ 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
22408
  };
22105
22409
  }
22106
22410
  let liveSha = "";
@@ -22124,6 +22428,124 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
22124
22428
  return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
22125
22429
  });
22126
22430
  }
22431
+ async function defaultRetryDeploy(deps, prior) {
22432
+ const input = {
22433
+ note: `hotfix --retry-deploy ${prior.tag}`,
22434
+ deployStatus: "success",
22435
+ workflowRuns: []
22436
+ };
22437
+ const dispatch = await appendJervGatewayReleaseDeploy(deps, prior.repo, prior.tag, prior.tagSha, input);
22438
+ const host = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
22439
+ if (!host) {
22440
+ throw new Error(
22441
+ `hotfix --retry-deploy: the ${prior.tag} deploy helper was not invoked (not the operator-host Gateway) \u2014 nothing was written (#6985)`
22442
+ );
22443
+ }
22444
+ const entry = deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha });
22445
+ if (entry.state === "failed") {
22446
+ return { ...entry, error: entry.note ?? dispatch.note, output: dispatch.note };
22447
+ }
22448
+ return entry;
22449
+ }
22450
+ async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
22451
+ const path2 = releaseLedgerPath(cwd);
22452
+ return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
22453
+ let prior;
22454
+ try {
22455
+ prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
22456
+ } catch (e) {
22457
+ throw new ReleaseLedgerError(
22458
+ "unreadable",
22459
+ `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)`
22460
+ );
22461
+ }
22462
+ if (prior.lane !== "hotfix") {
22463
+ throw new Error(
22464
+ `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)`
22465
+ );
22466
+ }
22467
+ if (prior.phases.githubRelease.state !== "complete") {
22468
+ throw new Error(
22469
+ `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)`
22470
+ );
22471
+ }
22472
+ if (!hotfixRetryDeployShape(prior)) {
22473
+ const derived = deriveReleasePhaseStatus(prior);
22474
+ const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
22475
+ const extra = open2.filter((l) => l.phase !== "deploy" && l.phase !== "alignment");
22476
+ if (extra.length > 0) {
22477
+ throw new Error(
22478
+ `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)`
22479
+ );
22480
+ }
22481
+ throw new Error(
22482
+ `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)`
22483
+ );
22484
+ }
22485
+ if (prior.phases.alignment.state === "failed" && !opts.retryAlignment) {
22486
+ throw new Error(
22487
+ `hotfix --retry-deploy: the ${prior.tag} alignment (fold-port) leg is failed \u2014 nothing was written (#6985)`
22488
+ );
22489
+ }
22490
+ if (prior.phases.deploy.state === "failed" && !isJervHubRepo(prior.repo) && prior.phases.deploy.workflow !== "jerv-gateway") {
22491
+ throw new Error(
22492
+ `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)`
22493
+ );
22494
+ }
22495
+ let liveSha = "";
22496
+ try {
22497
+ liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
22498
+ } catch (e) {
22499
+ throw new Error(
22500
+ `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)`
22501
+ );
22502
+ }
22503
+ if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
22504
+ throw new Error(
22505
+ `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)`
22506
+ );
22507
+ }
22508
+ const retried = [];
22509
+ const phases = { ...prior.phases };
22510
+ const now = () => /* @__PURE__ */ new Date();
22511
+ if (phases.deploy.state === "failed") {
22512
+ retried.push("deploy");
22513
+ const entry = opts.retryDeploy ? await opts.retryDeploy(prior) : await defaultRetryDeploy(deps, prior);
22514
+ phases.deploy = stampPhaseRecord(path2, prior.tag, "deploy", entry, phases.deploy.retries ?? 0, now);
22515
+ }
22516
+ if (phases.alignment.state === "failed") {
22517
+ retried.push("alignment");
22518
+ try {
22519
+ const entry = await opts.retryAlignment(prior);
22520
+ phases.alignment = stampPhaseRecord(path2, prior.tag, "alignment", entry, phases.alignment.retries ?? 0, now);
22521
+ } catch (e) {
22522
+ const message2 = e instanceof Error ? e.message : String(e);
22523
+ phases.alignment = stampPhaseRecord(
22524
+ path2,
22525
+ prior.tag,
22526
+ "alignment",
22527
+ { state: "failed", sha: prior.tagSha, error: message2, output: message2 },
22528
+ phases.alignment.retries ?? 0,
22529
+ now
22530
+ );
22531
+ }
22532
+ }
22533
+ atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
22534
+ const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
22535
+ const status = stillFailed.length === 0 ? "success" : "failed";
22536
+ return {
22537
+ command: "hotfix-retry-deploy",
22538
+ repo: prior.repo,
22539
+ tag: prior.tag,
22540
+ tagSha: prior.tagSha,
22541
+ retried,
22542
+ status,
22543
+ 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)`,
22544
+ ...retried.includes("deploy") ? { deploy: phases.deploy } : {},
22545
+ ...retried.includes("alignment") ? { alignment: phases.alignment } : {}
22546
+ };
22547
+ });
22548
+ }
22127
22549
 
22128
22550
  // src/train-doctor.ts
22129
22551
  init_client_version();
@@ -22221,7 +22643,7 @@ function readOriginWorkflowsDefault(root, ref) {
22221
22643
  }
22222
22644
  var NPM_REGISTRY = "https://registry.npmjs.org";
22223
22645
  async function probeRegistryWhoami(train) {
22224
- const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0, import_node_os13.tmpdir)(), "mmi-npmrc-"));
22646
+ const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0, import_node_os14.tmpdir)(), "mmi-npmrc-"));
22225
22647
  const rc = (0, import_node_path23.join)(dir, "npmrc");
22226
22648
  try {
22227
22649
  (0, import_node_fs23.writeFileSync)(rc, "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n");
@@ -22260,6 +22682,7 @@ var defaultDeps2 = {
22260
22682
  }
22261
22683
  },
22262
22684
  healAlignmentLeg: healStaleAlignmentLeg,
22685
+ healRunLegs: healUnfoldedRunLegs,
22263
22686
  readSurfacesRaw: (cwd) => {
22264
22687
  try {
22265
22688
  return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
@@ -22606,6 +23029,21 @@ async function runTrainDoctor(input) {
22606
23029
  try {
22607
23030
  let ledger = parseReleaseLedger(ledgerRaw);
22608
23031
  let derived = deriveReleasePhaseStatus(ledger);
23032
+ const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
23033
+ if (heal && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
23034
+ const run = await deps.healRunLegs(train, cwd, deployModel);
23035
+ if (run) {
23036
+ add({
23037
+ code: "ledger-unfolded",
23038
+ severity: "healed",
23039
+ source: "origin",
23040
+ title: `folded ${run.folded.map((f) => `${f.phase}=${f.state}${f.runId ? ` (run ${f.runId})` : ""}`).join(", ")} into the ${run.tag} ledger from the concluded run(s) on the tag SHA (#7003)`,
23041
+ remedy: "nothing \u2014 the leg now records what GitHub already concluded"
23042
+ });
23043
+ ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
23044
+ derived = deriveReleasePhaseStatus(ledger);
23045
+ }
23046
+ }
22609
23047
  if (heal && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
22610
23048
  const closedTag = await deps.healAlignmentLeg(train, cwd);
22611
23049
  if (closedTag) {
@@ -22644,7 +23082,7 @@ async function runTrainDoctor(input) {
22644
23082
  // #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
22645
23083
  // ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
22646
23084
  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}`
23085
+ remedy: failed ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges, and a deploy/publish leg whose run already concluded folds under the same heal)${supersession}`
22648
23086
  });
22649
23087
  } else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
22650
23088
  add({
@@ -23400,6 +23838,8 @@ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase,
23400
23838
  }
23401
23839
  async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch, input, recover = recoverPhaseWithMaybeTransientRerun) {
23402
23840
  if (dispatch.deployStatus !== "failure") return dispatch;
23841
+ const persisted = await ledger?.load(anchors).catch(() => void 0);
23842
+ if (persisted?.phases[phase]?.superseded === true) return dispatch;
23403
23843
  let workflowRuns = dispatch.workflowRuns?.length ? [...dispatch.workflowRuns] : void 0;
23404
23844
  const notes = [];
23405
23845
  let fallbackRecovered = false;
@@ -23407,6 +23847,7 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
23407
23847
  if (failedRows.length > 0) {
23408
23848
  for (const failed of failedRows) {
23409
23849
  const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
23850
+ if (persisted?.phases[rowPhase]?.superseded === true) continue;
23410
23851
  const recovery = await recover(deps, ledger, anchors, rowPhase, {
23411
23852
  repo: input.repo,
23412
23853
  sha: input.sha,
@@ -23581,7 +24022,10 @@ ${recovery.note}`), recoveryInput);
23581
24022
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
23582
24023
  await recordPhase(ledger, deps, ledgerAnchors, "githubRelease", phaseEntry({ state: "complete", sha: releaseSha, runUrl: releaseUrl, note: `verified GitHub Release ${tag} against ${releaseSha.slice(0, 12)}` }), { landed: "the immutable tag, the green required-check wall, and the origin/main fast-forward" });
23583
24024
  let alignment = alignEarly ? await alignEarly() : void 0;
23584
- const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
24025
+ const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
24026
+ dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir),
24027
+ observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch)
24028
+ );
23585
24029
  const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
23586
24030
  const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
23587
24031
  const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
@@ -23603,7 +24047,6 @@ ${recovery.note}`), recoveryInput);
23603
24047
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
23604
24048
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
23605
24049
  });
23606
- const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch);
23607
24050
  const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
23608
24051
  deps,
23609
24052
  ledger,
@@ -23619,6 +24062,14 @@ ${recovery.note}`), recoveryInput);
23619
24062
  await recordPhase(ledger, deps, ledgerAnchors, "publish", publishLeg, {
23620
24063
  landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
23621
24064
  });
24065
+ const announcing = announceShippedRelease(
24066
+ deps,
24067
+ { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
24068
+ deployModel,
24069
+ { deploy: deployLeg, publish: publishLeg },
24070
+ releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
24071
+ );
24072
+ const aligning = watch && alignment ? awaitAlignmentsLanded(deps, ctx, alignment) : Promise.resolve(alignment);
23622
24073
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
23623
24074
  dispatch = await appendMmiGetRedeploy(
23624
24075
  deps,
@@ -23630,19 +24081,8 @@ ${recovery.note}`), recoveryInput);
23630
24081
  { repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
23631
24082
  dispatch
23632
24083
  );
23633
- const announcing = announceShippedRelease(
23634
- deps,
23635
- { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
23636
- deployModel,
23637
- { deploy: deployLeg, publish: publishLeg },
23638
- releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
23639
- );
23640
- if (watch && alignment) {
23641
- alignment = {
23642
- dev: await awaitAlignmentLanded(deps, ctx, alignment.dev, "development"),
23643
- ...alignment.rc ? { rc: await awaitAlignmentLanded(deps, ctx, alignment.rc, "rc") } : {}
23644
- };
23645
- }
24084
+ dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
24085
+ alignment = await aligning;
23646
24086
  const announceNote = await announcing;
23647
24087
  return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
23648
24088
  }
@@ -23958,6 +24398,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
23958
24398
  dispatch2.deployStatus = historicalDeploy;
23959
24399
  dispatch2.note = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${companion.note}`;
23960
24400
  }
24401
+ if (persisted?.phases.deploy.superseded === true) phaseInputs.deploy = persisted.phases.deploy;
24402
+ if (persisted?.phases.publish.superseded === true) phaseInputs.publish = persisted.phases.publish;
23961
24403
  if (persisted) {
23962
24404
  if (persisted.phases.deploy.superseded !== true) {
23963
24405
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
@@ -23969,7 +24411,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
23969
24411
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
23970
24412
  }
23971
24413
  if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
23972
- const announceNote2 = persisted && !shippedBefore && persisted.phases.deploy.superseded !== true && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
24414
+ const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
24415
+ const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
23973
24416
  deps,
23974
24417
  { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
23975
24418
  deployModel,
@@ -24059,7 +24502,10 @@ ${recovery.note}`), recoveryInput);
24059
24502
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, runUrl: releaseUrl, note: "verified the Release published against the tagged commit" }), { strict: ledgerMode === "strict", landed: "the immutable tag, the re-proven wall, and the origin/main push" });
24060
24503
  let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
24061
24504
  let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
24062
- const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
24505
+ const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
24506
+ dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir),
24507
+ observePublishIfRequired(deps, ctx, meta, deployModel, "main", tagSha, watch)
24508
+ );
24063
24509
  const recoveredDeployDispatch = await recoverFailedDispatchPhase(
24064
24510
  deps,
24065
24511
  ledger,
@@ -24075,12 +24521,13 @@ ${recovery.note}`), recoveryInput);
24075
24521
  );
24076
24522
  const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
24077
24523
  steps.push(`dispatched the ${deployModel} deploy path`);
24078
- const deployLeg = phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
24079
- await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
24080
- strict: ledgerMode === "strict",
24081
- landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
24082
- });
24083
- const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", tagSha, watch);
24524
+ const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
24525
+ if (persisted?.phases.deploy.superseded !== true) {
24526
+ await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
24527
+ strict: ledgerMode === "strict",
24528
+ landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
24529
+ });
24530
+ }
24084
24531
  const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
24085
24532
  deps,
24086
24533
  ledger,
@@ -24089,11 +24536,13 @@ ${recovery.note}`), recoveryInput);
24089
24536
  publishDispatch0,
24090
24537
  { repo: ctx.repo, sha: tagSha, watched: false, workflow: "publish.yml" }
24091
24538
  ) : null;
24092
- const publishLeg = phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
24093
- await recordPhase(ledger, deps, anchors, "publish", publishLeg, {
24094
- strict: ledgerMode === "strict",
24095
- landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
24096
- });
24539
+ const publishLeg = persisted?.phases.publish.superseded === true ? persisted.phases.publish : phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
24540
+ if (persisted?.phases.publish.superseded !== true) {
24541
+ await recordPhase(ledger, deps, anchors, "publish", publishLeg, {
24542
+ strict: ledgerMode === "strict",
24543
+ landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
24544
+ });
24545
+ }
24097
24546
  const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
24098
24547
  const announcing = announceShippedRelease(
24099
24548
  deps,
@@ -24103,8 +24552,9 @@ ${recovery.note}`), recoveryInput);
24103
24552
  releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
24104
24553
  );
24105
24554
  if (watch) {
24106
- devRollForward = await awaitAlignmentLanded(deps, ctx, devRollForward, "development");
24107
- if (rcAlignment) rcAlignment = await awaitAlignmentLanded(deps, ctx, rcAlignment, "rc");
24555
+ const landed = await awaitAlignmentsLanded(deps, ctx, { dev: devRollForward, rc: rcAlignment });
24556
+ devRollForward = landed.dev;
24557
+ rcAlignment = landed.rc;
24108
24558
  }
24109
24559
  const announceNote = await announcing;
24110
24560
  steps.push(`development roll-forward: ${devRollForward.status}`);
@@ -24759,6 +25209,7 @@ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
24759
25209
  var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
24760
25210
  var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
24761
25211
  var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflows";
25212
+ var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
24762
25213
  var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
24763
25214
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
24764
25215
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
@@ -25191,6 +25642,17 @@ async function auditRepoCi(repo, deps) {
25191
25642
  remediation: missingEmitters.length ? `Fix the pull_request workflows or ${contextAuthority.source} before reconciling` : void 0
25192
25643
  });
25193
25644
  }
25645
+ if (hasRequiredChecks) {
25646
+ const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
25647
+ const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
25648
+ const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job));
25649
+ checks.push({
25650
+ ok: unblocking.length === 0,
25651
+ label: UNBLOCKING_GATE_JOBS_LABEL,
25652
+ detail: unblocking.length ? `gate job(s) [${unblocking.join(", ")}] run on every PR but are neither required nor needed by a required job (required: [${liveContexts.join(", ")}]) \u2014 a red one still merges and turns ${baseBranch} red for every later PR` : void 0,
25653
+ remediation: unblocking.length ? `mmi-cli oracle org project set ${repo} --var requiredChecks=${JSON.stringify(sortedUnique([...liveContexts, ...unblocking]))} then mmi-cli devops ci reconcile --repo ${repo} --apply \u2014 or make a required job \`needs:\` them` : void 0
25654
+ });
25655
+ }
25194
25656
  const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
25195
25657
  const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
25196
25658
  checks.push({
@@ -27122,7 +27584,7 @@ function registerBoardCommands(program3) {
27122
27584
 
27123
27585
  // src/bootstrap-commands.ts
27124
27586
  var import_node_fs26 = require("node:fs");
27125
- var import_node_os14 = require("node:os");
27587
+ var import_node_os15 = require("node:os");
27126
27588
  var import_node_path26 = require("node:path");
27127
27589
  init_cli_shared();
27128
27590
  init_clean_exit();
@@ -29130,7 +29592,7 @@ function registerBootstrapCommands(program3) {
29130
29592
  const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
29131
29593
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
29132
29594
  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`);
29595
+ 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
29596
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
29135
29597
  try {
29136
29598
  await gh(contentPutInputArgs(repo, target, tmp));
@@ -29771,7 +30233,7 @@ LIVE apply to ${repo}:
29771
30233
  } catch {
29772
30234
  existingSha = void 0;
29773
30235
  }
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`);
30236
+ 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
30237
  const desiredContent = desiredByRepo.get(rec.repo);
29776
30238
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
29777
30239
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
@@ -29946,7 +30408,7 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
29946
30408
  } catch {
29947
30409
  existingSha = void 0;
29948
30410
  }
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`);
30411
+ 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
30412
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
29951
30413
  try {
29952
30414
  await gh(contentPutInputArgs(repo, seed.target, tmp));
@@ -31897,7 +32359,7 @@ async function collectWaveStatus(deps) {
31897
32359
  init_github_client();
31898
32360
  init_cli_shared();
31899
32361
  var import_node_fs30 = require("node:fs");
31900
- var import_node_os15 = require("node:os");
32362
+ var import_node_os16 = require("node:os");
31901
32363
  var import_node_path29 = require("node:path");
31902
32364
  var GC_GH_TIMEOUT_MS = 2e4;
31903
32365
  async function collectStatus() {
@@ -32086,7 +32548,7 @@ async function collectOnboardStatus(opts = {}) {
32086
32548
  else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
32087
32549
  else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
32088
32550
  }
32089
- const home = (0, import_node_os15.homedir)();
32551
+ const home = (0, import_node_os16.homedir)();
32090
32552
  const plugin = onboardPluginGate({
32091
32553
  readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
32092
32554
  readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
@@ -34114,7 +34576,7 @@ init_error_codes();
34114
34576
  init_github_client();
34115
34577
 
34116
34578
  // src/issue-body.ts
34117
- var import_node_os16 = require("node:os");
34579
+ var import_node_os17 = require("node:os");
34118
34580
  init_error_codes();
34119
34581
  var TextArgError = class extends Error {
34120
34582
  constructor(message2, code, offendingFlag) {
@@ -34127,7 +34589,7 @@ var TextArgError = class extends Error {
34127
34589
  offendingFlag;
34128
34590
  };
34129
34591
  function emptyStdinMessage(fileFlag) {
34130
- if ((0, import_node_os16.platform)() === "win32") {
34592
+ if ((0, import_node_os17.platform)() === "win32") {
34131
34593
  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
34594
  }
34133
34595
  return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
@@ -36642,7 +37104,7 @@ function registerSchedulesCommands(program3) {
36642
37104
  // src/secrets-commands.ts
36643
37105
  var import_node_fs33 = require("node:fs");
36644
37106
  var import_node_path31 = require("node:path");
36645
- var import_node_os17 = require("node:os");
37107
+ var import_node_os18 = require("node:os");
36646
37108
  init_cli_shared();
36647
37109
  init_hub_auth();
36648
37110
  init_github_client();
@@ -36768,7 +37230,7 @@ async function decryptRailsCredentials(input) {
36768
37230
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
36769
37231
  "puts JSON.generate(config.config)"
36770
37232
  ].join("\n");
36771
- const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os17.tmpdir)(), "mmi-rails-decrypt-"));
37233
+ const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os18.tmpdir)(), "mmi-rails-decrypt-"));
36772
37234
  const scriptPath = (0, import_node_path31.join)(scriptDir, "decrypt.rb");
36773
37235
  (0, import_node_fs33.writeFileSync)(scriptPath, script, "utf8");
36774
37236
  try {
@@ -38164,7 +38626,7 @@ init_github_client();
38164
38626
  // src/merge-cleanup.ts
38165
38627
  var import_node_fs37 = require("node:fs");
38166
38628
  var import_node_path35 = require("node:path");
38167
- var import_node_os18 = require("node:os");
38629
+ var import_node_os19 = require("node:os");
38168
38630
  init_cli_shared();
38169
38631
  init_github_client();
38170
38632
 
@@ -39234,7 +39696,7 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
39234
39696
  return rewritten === base ? null : rewritten;
39235
39697
  }
39236
39698
  function writeSquashBodyFile(body) {
39237
- const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
39699
+ const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-squash-body-"));
39238
39700
  const path2 = (0, import_node_path35.join)(dir, "body.txt");
39239
39701
  (0, import_node_fs37.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
39240
39702
  `, "utf8");
@@ -39657,7 +40119,7 @@ function postMergeReconWarnings(input) {
39657
40119
  // src/review-verdict.ts
39658
40120
  var import_node_child_process13 = require("node:child_process");
39659
40121
  var import_node_fs39 = require("node:fs");
39660
- var import_node_os19 = require("node:os");
40122
+ var import_node_os20 = require("node:os");
39661
40123
  var import_node_path37 = require("node:path");
39662
40124
  init_cli_shared();
39663
40125
  var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
@@ -39823,7 +40285,7 @@ async function readPrIssueComments(number, repo) {
39823
40285
  return comments;
39824
40286
  }
39825
40287
  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-"));
40288
+ const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path37.join)((0, import_node_os20.tmpdir)(), "mmi-review-verdict-"));
39827
40289
  const path2 = (0, import_node_path37.join)(dir, "body.md");
39828
40290
  try {
39829
40291
  (0, import_node_fs39.writeFileSync)(path2, body, "utf8");
@@ -42808,7 +43270,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
42808
43270
  var import_node_child_process16 = require("node:child_process");
42809
43271
  var import_node_crypto13 = require("node:crypto");
42810
43272
  var import_node_fs45 = require("node:fs");
42811
- var import_node_os20 = require("node:os");
43273
+ var import_node_os21 = require("node:os");
42812
43274
  var import_node_path42 = require("node:path");
42813
43275
 
42814
43276
  // ../scripts/distribution-digest.mjs
@@ -42959,7 +43421,7 @@ function rebuildTo(packageRoot, outDir) {
42959
43421
  });
42960
43422
  }
42961
43423
  function runDistStatus(root) {
42962
- const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os20.tmpdir)(), "mmi-dist-drift-"));
43424
+ const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
42963
43425
  let overlayCount = 0;
42964
43426
  try {
42965
43427
  const cliOut = (0, import_node_path42.join)(stage, "cli-dist");
@@ -43776,6 +44238,32 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
43776
44238
  ${lines2.join("\n")}
43777
44239
  Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
43778
44240
  }
44241
+ var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
44242
+ async function findHotfixPickGaps(deps, sources, tolerated) {
44243
+ if (sources.length === 0) return [];
44244
+ const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
44245
+ const carried = /* @__PURE__ */ new Set();
44246
+ const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
44247
+ for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
44248
+ const gaps = [];
44249
+ const devOnly = (await deps.run("git", ["log", "--no-merges", "--reverse", "--format=%H%x09%s", "origin/main..origin/development"])).split("\n").map((s) => s.trim()).filter(Boolean);
44250
+ for (const line of devOnly) {
44251
+ const [sha, ...subjectParts] = line.split(" ");
44252
+ if (!sha) continue;
44253
+ const lower = sha.toLowerCase();
44254
+ if (namedShas.has(lower) || carried.has(lower)) continue;
44255
+ const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
44256
+ if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
44257
+ gaps.push({ sha, subject: subjectParts.join(" ") });
44258
+ }
44259
+ return gaps;
44260
+ }
44261
+ function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
44262
+ const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
44263
+ return `hotfix start refused: ${tag} would omit ${gaps.length} merged development fix(es) not on origin/main:
44264
+ ${lines2.join("\n")}
44265
+ A hotfix branch is cut from origin/main and carries ONLY the fixes --from names, in order (#4411) \u2014 every other merged development fix stays unshipped while the channel reports healthy (#6994). Add each omitted fix to the pick list and rerun \`mmi-cli devops hotfix start --from ${[...fromSpecs, ...gaps.map((g) => g.sha.slice(0, 7))].join(",")}\`, or, when a listed fix is provably not needed on main (a development-only change, already superseded), rerun with \`--ack ${gaps.map((g) => g.sha.slice(0, 7)).join(",")}\` naming exactly the sha(s) a human excluded \u2014 the ack is recorded in the PR body, never silent.`;
44266
+ }
43779
44267
  async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
43780
44268
  if (sources.length === 0) return [];
43781
44269
  const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
@@ -43962,6 +44450,18 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
43962
44450
  }
43963
44451
  const label = sources.map((s) => s.label).join(", ");
43964
44452
  const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
44453
+ const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
44454
+ const pickGaps = await findHotfixPickGaps(deps, sources, pickTolerated);
44455
+ const unacked = pickGaps.filter((g) => {
44456
+ const sha = g.sha.toLowerCase();
44457
+ return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
44458
+ });
44459
+ if (unacked.length > 0) {
44460
+ throw new Error(formatHotfixPickGapRefusal(unacked, tag, specs));
44461
+ }
44462
+ if (pickGaps.length > 0) {
44463
+ notes.push(`pick-completeness ack: excluded ${pickGaps.map((g) => `${g.sha.slice(0, 7)} ${g.subject}`).join("; ")} by --ack (#6994)`);
44464
+ }
43965
44465
  const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
43966
44466
  if (remoteBranch) {
43967
44467
  await deps.run("git", ["checkout", branch]);
@@ -44026,7 +44526,9 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
44026
44526
  "--body",
44027
44527
  `Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
44028
44528
 
44029
- <!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")} -->
44529
+ <!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
44530
+
44531
+ <!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
44030
44532
 
44031
44533
  Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
44032
44534
  ]));
@@ -44416,9 +44918,9 @@ ${decision.note}`);
44416
44918
  let deployNote;
44417
44919
  let tenantPublishDispatch = null;
44418
44920
  if (deployModel === "hub-serverless") {
44419
- for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
44420
- const watched = await watchReleaseRun(deps, ctx, workflow, mergedSha);
44421
- runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, workflow === "publish.yml" ? "publish" : "deploy", watched));
44921
+ const watchedRuns = await Promise.all(HOTFIX_RELEASE_WORKFLOWS.map((workflow) => watchReleaseRun(deps, ctx, workflow, mergedSha)));
44922
+ for (const watched of watchedRuns) {
44923
+ runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, watched.workflow === "publish.yml" ? "publish" : "deploy", watched));
44422
44924
  }
44423
44925
  deployNote = "watched release-triggered deploy.yml + publish.yml";
44424
44926
  } else if (deployModel === "registry-publish") {
@@ -44427,17 +44929,9 @@ ${decision.note}`);
44427
44929
  runs.push(run);
44428
44930
  deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
44429
44931
  } else if (deployModel === "tenant-container" || deployModel === "solo-container") {
44430
- const deploy0 = await dispatchDeploy(
44431
- deps,
44432
- ctx,
44433
- "main",
44434
- "main",
44435
- deployModel,
44436
- true,
44437
- (deps.now ?? Date.now)(),
44438
- mergedSha,
44439
- "report",
44440
- meta.publishDir
44932
+ const [deploy0, publish0] = await joinDeployAndPublish(
44933
+ dispatchDeploy(deps, ctx, "main", "main", deployModel, true, (deps.now ?? Date.now)(), mergedSha, "report", meta.publishDir),
44934
+ observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true)
44441
44935
  );
44442
44936
  const deploy = await recoverFailedDispatchPhase(
44443
44937
  deps,
@@ -44447,7 +44941,6 @@ ${decision.note}`);
44447
44941
  deploy0,
44448
44942
  { repo: releaseRunRepoFor(deployModel, ctx.repo), sha: mergedSha, watched: true, workflow: "tenant-deploy.yml" }
44449
44943
  );
44450
- const publish0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true);
44451
44944
  const publish = publish0 ? await recoverFailedDispatchPhase(
44452
44945
  deps,
44453
44946
  ledger,
@@ -44542,6 +45035,48 @@ ${decision.note}`);
44542
45035
  const localSync = await syncLocalTrainBranches(deps.run, { fetch: false, warn: deps.warn }).catch(() => void 0);
44543
45036
  return { ...result, ...localSync ? { localSync } : {} };
44544
45037
  }
45038
+ async function runHotfixRetryDeploy(deps, versionInput) {
45039
+ const ctx = await buildTrainApplyContext(deps);
45040
+ const { tag } = normalizeHotfixVersion(versionInput);
45041
+ const status = await deps.run("git", ["status", "--porcelain"]);
45042
+ if (porcelainHasBlockingChanges(status)) {
45043
+ throw new Error(`working tree must be clean before hotfix --retry-deploy${describeBlockingPaths(status)}`);
45044
+ }
45045
+ const root = await hotfixCheckoutRoot(deps);
45046
+ await deps.run("git", ["fetch", "origin", "--tags"]);
45047
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
45048
+ const deployModel = requireDeployModel(meta, ctx.repo);
45049
+ const result = await retryHotfixDeployLegs(deps, root, {
45050
+ retryAlignment: async (prior) => {
45051
+ let foldNote2;
45052
+ let foldStatus2 = "ok";
45053
+ try {
45054
+ foldNote2 = await portFoldToDevelopment(deps, ctx, deployModel, prior.tag);
45055
+ } catch (e) {
45056
+ foldStatus2 = "failure";
45057
+ 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`;
45058
+ }
45059
+ return hotfixFoldAlignmentEntry(foldStatus2, foldNote2, prior.tagSha);
45060
+ }
45061
+ });
45062
+ const ledger = openReleaseLedgerWriter(root);
45063
+ const report = releaseLedgerReport(
45064
+ await ledger.load({ repo: result.repo, tag: result.tag, tagSha: result.tagSha }).catch(() => void 0),
45065
+ ledger.path
45066
+ );
45067
+ const deployNote = result.deploy?.note ?? result.deploy?.error;
45068
+ const foldNote = result.alignment?.note ?? result.alignment?.error;
45069
+ const foldStatus = result.alignment ? result.alignment.state === "failed" ? "failure" : result.alignment.state === "pending" ? "unresolved" : "ok" : void 0;
45070
+ const runs = result.deploy ? [{ workflow: result.deploy.workflow ?? "jerv-gateway", conclusion: result.deploy.state === "complete" ? "success" : result.deploy.state === "failed" ? "failure" : "pending" }] : void 0;
45071
+ return {
45072
+ ...result,
45073
+ ...deployNote ? { deployNote } : {},
45074
+ ...foldNote ? { foldNote } : {},
45075
+ ...foldStatus ? { foldStatus } : {},
45076
+ ...runs ? { runs } : {},
45077
+ ...report ? { ledger: report } : {}
45078
+ };
45079
+ }
44545
45080
  function deriveHotfixState(f) {
44546
45081
  if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
44547
45082
  return { state: "not-started", next: `mmi-cli devops hotfix start --from <pr#|sha> (would mint ${f.tag})` };
@@ -45117,7 +45652,7 @@ async function announceSummaryRefusal(path2) {
45117
45652
  function trainApplyDeps() {
45118
45653
  return {
45119
45654
  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;
45655
+ 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
45656
  try {
45122
45657
  return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, args, { timeout })).stdout;
45123
45658
  } catch (e) {
@@ -45227,7 +45762,7 @@ function trainApplyDeps() {
45227
45762
  }
45228
45763
  function formatWorkflowRun(r) {
45229
45764
  const evidenced = workflowRunWithEvidence(r);
45230
- return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
45765
+ return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}${r.parallel ? " (parallel)" : ""}`;
45231
45766
  }
45232
45767
  function renderDeployLine(d) {
45233
45768
  const parts = [d.dispatch];
@@ -45792,6 +46327,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45792
46327
  const evidenced = hotfixRunWithEvidence(run);
45793
46328
  return ` - ${evidenced.workflow}: ${evidenced.conclusion}${evidenced.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""} (${evidenced.url ?? evidenced.runUrlNote})`;
45794
46329
  }
46330
+ function renderHotfixRetryDeploy(r) {
46331
+ return [
46332
+ `mmi-cli devops hotfix release --retry-deploy: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`,
46333
+ ...r.deployNote ? [` - deploy: ${r.deployNote}`] : [],
46334
+ ...r.foldNote ? [` - fold: ${r.foldNote}`] : [],
46335
+ ...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
46336
+ ].join("\n");
46337
+ }
45795
46338
  function renderHotfixRelease(r) {
45796
46339
  return [
45797
46340
  `mmi-cli devops hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
@@ -45864,12 +46407,20 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
45864
46407
  const steps = trainPlan("hotfix");
45865
46408
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
45866
46409
  });
45867
- 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));
46410
+ 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("--ack <shas>", "comma-separated dev shas a human verified are NOT needed on main, overriding the pick-completeness guard for exactly those fixes \u2014 recorded in the PR body, never silent (#6994)").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, ack: o.ack ? o.ack.split(",").map((s) => s.trim()).filter(Boolean) : void 0 }), o, renderHotfixStart));
46411
+ 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) => {
46412
+ if (o.retryDeploy) {
46413
+ if (o.announceSummaryFile || o.carries) {
46414
+ 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");
46415
+ }
46416
+ return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
46417
+ }
46418
+ return runHotfixSub("release", async () => {
46419
+ const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
46420
+ const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
46421
+ return { ...result, projectInfoSync };
46422
+ }, o, renderHotfixRelease);
46423
+ });
45873
46424
  function hotfixStatusDeps() {
45874
46425
  const STATUS_GH_MS = 8e3;
45875
46426
  const STATUS_GIT_MS = 1e4;
@@ -45911,7 +46462,7 @@ function envHealLockPath(home) {
45911
46462
  async function withEnvHealLock(what, run) {
45912
46463
  try {
45913
46464
  return await withFileLock(
45914
- envHealLockPath((0, import_node_os21.homedir)()),
46465
+ envHealLockPath((0, import_node_os22.homedir)()),
45915
46466
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
45916
46467
  run
45917
46468
  );
@@ -46008,7 +46559,7 @@ function mmiDoctorDeps(opts = {}) {
46008
46559
  const configRoot = surfaceConfigRoot(surface);
46009
46560
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
46010
46561
  const plan = buildPluginCachePlan(
46011
- (0, import_node_os21.homedir)(),
46562
+ (0, import_node_os22.homedir)(),
46012
46563
  running,
46013
46564
  pluginCacheFsDeps(configRoot, () => 0),
46014
46565
  { configRoot, includeStaging: surface !== "codex" }
@@ -46036,7 +46587,7 @@ function mmiDoctorDeps(opts = {}) {
46036
46587
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
46037
46588
  const installed = installedActivePluginVersion(surface);
46038
46589
  const plan = buildPluginCachePlan(
46039
- (0, import_node_os21.homedir)(),
46590
+ (0, import_node_os22.homedir)(),
46040
46591
  running,
46041
46592
  pluginCacheFsDeps(configRoot, () => 0),
46042
46593
  { configRoot, includeStaging: surface !== "codex", installedVersion: installed }
@@ -47261,7 +47812,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
47261
47812
  return;
47262
47813
  }
47263
47814
  const plan = buildPluginCachePlan(
47264
- (0, import_node_os21.homedir)(),
47815
+ (0, import_node_os22.homedir)(),
47265
47816
  running,
47266
47817
  pluginCacheFsDeps(configRoot, directoryBytes),
47267
47818
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }