@mutmutco/cli 4.4.17 → 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 +339 -68
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -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.17",
15942
- tag: "v4.4.17",
15943
- commit: "748489250635",
15944
- npm: "@mutmutco/cli@4.4.17"
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.17"
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.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)"
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"]);
@@ -18889,6 +18934,12 @@ async function observePublishIfRequired(deps, ctx, meta, model, stage, releaseSh
18889
18934
  deployStatus: aggregateWorkflowRuns(workflowRuns)
18890
18935
  };
18891
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
+ }
18892
18943
  function appendPublishDispatch(deploy, publish) {
18893
18944
  if (!publish) return deploy;
18894
18945
  return {
@@ -19053,19 +19104,29 @@ async function appendServedChannelPublish(deps, input, dispatch) {
19053
19104
  };
19054
19105
  }
19055
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
+ }
19056
19117
  if (!probe.ok) {
19057
19118
  const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
19058
19119
  return {
19059
19120
  ...dispatch,
19060
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`,
19061
- workflowRuns: runs
19122
+ workflowRuns: evidence("pending", `${channelUrl} unreadable (${why})`)
19062
19123
  };
19063
19124
  }
19064
19125
  if (expected && probe.version !== expected) {
19065
19126
  return {
19066
19127
  ...dispatch,
19067
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}\``,
19068
- workflowRuns: runs,
19129
+ workflowRuns: evidence("failure", `${channelUrl} reports ${probe.version}, expected ${expected}`),
19069
19130
  deployStatus: "failure"
19070
19131
  };
19071
19132
  }
@@ -19073,15 +19134,88 @@ async function appendServedChannelPublish(deps, input, dispatch) {
19073
19134
  return {
19074
19135
  ...dispatch,
19075
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`,
19076
- workflowRuns: runs
19137
+ workflowRuns: evidence("pending", `${channelUrl} reports ${probe.version}; nothing to compare against`)
19077
19138
  };
19078
19139
  }
19079
19140
  return {
19080
19141
  ...dispatch,
19081
19142
  note: `${dispatch.note}; served channel moved \u2014 ${channelUrl} now reports ${probe.version}`,
19082
- workflowRuns: runs
19143
+ workflowRuns: evidence("success", `${channelUrl} reports ${probe.version}`)
19083
19144
  };
19084
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
+ }
19085
19219
  var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
19086
19220
  var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
19087
19221
  async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
@@ -19141,15 +19275,15 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
19141
19275
  }
19142
19276
  }
19143
19277
  async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
19144
- const workflowRuns = [];
19145
- for (const target of targets) {
19278
+ const parallel = targets.length > 1 ? { parallel: true } : {};
19279
+ const workflowRuns = await Promise.all(targets.map(async (target) => {
19146
19280
  try {
19147
19281
  const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
19148
- workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run, repo));
19282
+ return { ...await watchWorkflowRun(deps, target.workflow, run, repo), ...parallel };
19149
19283
  } catch {
19150
- workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
19284
+ return { workflow: target.workflow, conclusion: "failure", ...parallel };
19151
19285
  }
19152
- }
19286
+ }));
19153
19287
  if (!enumerateSha) return workflowRuns;
19154
19288
  const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
19155
19289
  workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
@@ -19204,8 +19338,8 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
19204
19338
  return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
19205
19339
  }
19206
19340
  function shipLegsGreen(deploy, publish) {
19207
- const green = (s) => s === "complete" || s === "skipped";
19208
- 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);
19209
19343
  }
19210
19344
  async function announceShippedRelease(deps, args, model, legs, deferred) {
19211
19345
  if (!deps.announce) return void 0;
@@ -21184,7 +21318,9 @@ var TRANSIENT_TRANSPORT_MARKERS = [
21184
21318
  /socket hang ?up/i,
21185
21319
  /remote end hung ?up unexpectedly/i,
21186
21320
  /early EOF/i,
21187
- /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
21188
21324
  ];
21189
21325
  function classifyTransientTransportFailure(evidence) {
21190
21326
  if (!evidence) return false;
@@ -21855,6 +21991,70 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
21855
21991
  return prior.tag;
21856
21992
  });
21857
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
+ }
21858
22058
  function supersedeableDeployLegShape(rec) {
21859
22059
  return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
21860
22060
  }
@@ -22482,6 +22682,7 @@ var defaultDeps2 = {
22482
22682
  }
22483
22683
  },
22484
22684
  healAlignmentLeg: healStaleAlignmentLeg,
22685
+ healRunLegs: healUnfoldedRunLegs,
22485
22686
  readSurfacesRaw: (cwd) => {
22486
22687
  try {
22487
22688
  return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
@@ -22828,6 +23029,21 @@ async function runTrainDoctor(input) {
22828
23029
  try {
22829
23030
  let ledger = parseReleaseLedger(ledgerRaw);
22830
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
+ }
22831
23047
  if (heal && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
22832
23048
  const closedTag = await deps.healAlignmentLeg(train, cwd);
22833
23049
  if (closedTag) {
@@ -22866,7 +23082,7 @@ async function runTrainDoctor(input) {
22866
23082
  // #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
22867
23083
  // ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
22868
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)`,
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}`
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}`
22870
23086
  });
22871
23087
  } else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
22872
23088
  add({
@@ -23622,6 +23838,8 @@ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase,
23622
23838
  }
23623
23839
  async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch, input, recover = recoverPhaseWithMaybeTransientRerun) {
23624
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;
23625
23843
  let workflowRuns = dispatch.workflowRuns?.length ? [...dispatch.workflowRuns] : void 0;
23626
23844
  const notes = [];
23627
23845
  let fallbackRecovered = false;
@@ -23629,6 +23847,7 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
23629
23847
  if (failedRows.length > 0) {
23630
23848
  for (const failed of failedRows) {
23631
23849
  const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
23850
+ if (persisted?.phases[rowPhase]?.superseded === true) continue;
23632
23851
  const recovery = await recover(deps, ledger, anchors, rowPhase, {
23633
23852
  repo: input.repo,
23634
23853
  sha: input.sha,
@@ -23803,7 +24022,10 @@ ${recovery.note}`), recoveryInput);
23803
24022
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
23804
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" });
23805
24024
  let alignment = alignEarly ? await alignEarly() : void 0;
23806
- 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
+ );
23807
24029
  const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
23808
24030
  const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
23809
24031
  const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
@@ -23825,7 +24047,6 @@ ${recovery.note}`), recoveryInput);
23825
24047
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
23826
24048
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
23827
24049
  });
23828
- const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch);
23829
24050
  const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
23830
24051
  deps,
23831
24052
  ledger,
@@ -23841,6 +24062,14 @@ ${recovery.note}`), recoveryInput);
23841
24062
  await recordPhase(ledger, deps, ledgerAnchors, "publish", publishLeg, {
23842
24063
  landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
23843
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);
23844
24073
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
23845
24074
  dispatch = await appendMmiGetRedeploy(
23846
24075
  deps,
@@ -23852,19 +24081,8 @@ ${recovery.note}`), recoveryInput);
23852
24081
  { repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
23853
24082
  dispatch
23854
24083
  );
23855
- const announcing = announceShippedRelease(
23856
- deps,
23857
- { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
23858
- deployModel,
23859
- { deploy: deployLeg, publish: publishLeg },
23860
- releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
23861
- );
23862
- if (watch && alignment) {
23863
- alignment = {
23864
- dev: await awaitAlignmentLanded(deps, ctx, alignment.dev, "development"),
23865
- ...alignment.rc ? { rc: await awaitAlignmentLanded(deps, ctx, alignment.rc, "rc") } : {}
23866
- };
23867
- }
24084
+ dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
24085
+ alignment = await aligning;
23868
24086
  const announceNote = await announcing;
23869
24087
  return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
23870
24088
  }
@@ -24180,6 +24398,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
24180
24398
  dispatch2.deployStatus = historicalDeploy;
24181
24399
  dispatch2.note = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${companion.note}`;
24182
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;
24183
24403
  if (persisted) {
24184
24404
  if (persisted.phases.deploy.superseded !== true) {
24185
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" });
@@ -24191,7 +24411,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
24191
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" });
24192
24412
  }
24193
24413
  if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
24194
- 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(
24195
24416
  deps,
24196
24417
  { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
24197
24418
  deployModel,
@@ -24281,7 +24502,10 @@ ${recovery.note}`), recoveryInput);
24281
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" });
24282
24503
  let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
24283
24504
  let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
24284
- 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
+ );
24285
24509
  const recoveredDeployDispatch = await recoverFailedDispatchPhase(
24286
24510
  deps,
24287
24511
  ledger,
@@ -24297,12 +24521,13 @@ ${recovery.note}`), recoveryInput);
24297
24521
  );
24298
24522
  const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
24299
24523
  steps.push(`dispatched the ${deployModel} deploy path`);
24300
- const deployLeg = phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
24301
- await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
24302
- strict: ledgerMode === "strict",
24303
- landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
24304
- });
24305
- 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
+ }
24306
24531
  const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
24307
24532
  deps,
24308
24533
  ledger,
@@ -24311,11 +24536,13 @@ ${recovery.note}`), recoveryInput);
24311
24536
  publishDispatch0,
24312
24537
  { repo: ctx.repo, sha: tagSha, watched: false, workflow: "publish.yml" }
24313
24538
  ) : null;
24314
- const publishLeg = phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
24315
- await recordPhase(ledger, deps, anchors, "publish", publishLeg, {
24316
- strict: ledgerMode === "strict",
24317
- landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
24318
- });
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
+ }
24319
24546
  const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
24320
24547
  const announcing = announceShippedRelease(
24321
24548
  deps,
@@ -24325,8 +24552,9 @@ ${recovery.note}`), recoveryInput);
24325
24552
  releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
24326
24553
  );
24327
24554
  if (watch) {
24328
- devRollForward = await awaitAlignmentLanded(deps, ctx, devRollForward, "development");
24329
- 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;
24330
24558
  }
24331
24559
  const announceNote = await announcing;
24332
24560
  steps.push(`development roll-forward: ${devRollForward.status}`);
@@ -24981,6 +25209,7 @@ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
24981
25209
  var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
24982
25210
  var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
24983
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)";
24984
25213
  var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
24985
25214
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
24986
25215
  var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
@@ -25413,6 +25642,17 @@ async function auditRepoCi(repo, deps) {
25413
25642
  remediation: missingEmitters.length ? `Fix the pull_request workflows or ${contextAuthority.source} before reconciling` : void 0
25414
25643
  });
25415
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
+ }
25416
25656
  const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
25417
25657
  const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
25418
25658
  checks.push({
@@ -43998,6 +44238,32 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
43998
44238
  ${lines2.join("\n")}
43999
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`;
44000
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
+ }
44001
44267
  async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
44002
44268
  if (sources.length === 0) return [];
44003
44269
  const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
@@ -44184,6 +44450,18 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
44184
44450
  }
44185
44451
  const label = sources.map((s) => s.label).join(", ");
44186
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
+ }
44187
44465
  const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
44188
44466
  if (remoteBranch) {
44189
44467
  await deps.run("git", ["checkout", branch]);
@@ -44248,7 +44526,9 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
44248
44526
  "--body",
44249
44527
  `Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
44250
44528
 
44251
- <!-- 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) -->` : ""}
44252
44532
 
44253
44533
  Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
44254
44534
  ]));
@@ -44638,9 +44918,9 @@ ${decision.note}`);
44638
44918
  let deployNote;
44639
44919
  let tenantPublishDispatch = null;
44640
44920
  if (deployModel === "hub-serverless") {
44641
- for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
44642
- const watched = await watchReleaseRun(deps, ctx, workflow, mergedSha);
44643
- 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));
44644
44924
  }
44645
44925
  deployNote = "watched release-triggered deploy.yml + publish.yml";
44646
44926
  } else if (deployModel === "registry-publish") {
@@ -44649,17 +44929,9 @@ ${decision.note}`);
44649
44929
  runs.push(run);
44650
44930
  deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
44651
44931
  } else if (deployModel === "tenant-container" || deployModel === "solo-container") {
44652
- const deploy0 = await dispatchDeploy(
44653
- deps,
44654
- ctx,
44655
- "main",
44656
- "main",
44657
- deployModel,
44658
- true,
44659
- (deps.now ?? Date.now)(),
44660
- mergedSha,
44661
- "report",
44662
- 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)
44663
44935
  );
44664
44936
  const deploy = await recoverFailedDispatchPhase(
44665
44937
  deps,
@@ -44669,7 +44941,6 @@ ${decision.note}`);
44669
44941
  deploy0,
44670
44942
  { repo: releaseRunRepoFor(deployModel, ctx.repo), sha: mergedSha, watched: true, workflow: "tenant-deploy.yml" }
44671
44943
  );
44672
- const publish0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true);
44673
44944
  const publish = publish0 ? await recoverFailedDispatchPhase(
44674
44945
  deps,
44675
44946
  ledger,
@@ -45491,7 +45762,7 @@ function trainApplyDeps() {
45491
45762
  }
45492
45763
  function formatWorkflowRun(r) {
45493
45764
  const evidenced = workflowRunWithEvidence(r);
45494
- return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
45765
+ return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}${r.parallel ? " (parallel)" : ""}`;
45495
45766
  }
45496
45767
  function renderDeployLine(d) {
45497
45768
  const parts = [d.dispatch];
@@ -46136,7 +46407,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46136
46407
  const steps = trainPlan("hotfix");
46137
46408
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
46138
46409
  });
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));
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));
46140
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) => {
46141
46412
  if (o.retryDeploy) {
46142
46413
  if (o.announceSummaryFile || o.carries) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.4.17",
3
+ "version": "4.5.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",