@mutmutco/cli 4.5.3 → 4.5.5

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 +537 -226
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4534,7 +4534,7 @@ function useColor() {
4534
4534
  var program = new Command();
4535
4535
 
4536
4536
  // src/command-composition.ts
4537
- var import_node_fs49 = require("node:fs");
4537
+ var import_node_fs50 = require("node:fs");
4538
4538
  init_clean_exit();
4539
4539
  init_cli_shared();
4540
4540
 
@@ -5437,7 +5437,7 @@ function resolveVerbAliasShim(argv) {
5437
5437
 
5438
5438
  // src/command-composition.ts
5439
5439
  var import_node_os22 = require("node:os");
5440
- var import_node_path47 = require("node:path");
5440
+ var import_node_path48 = require("node:path");
5441
5441
 
5442
5442
  // src/board-read.ts
5443
5443
  var import_node_fs13 = require("node:fs");
@@ -15998,10 +15998,10 @@ var rollout_plan_default = {
15998
15998
  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)."
15999
15999
  },
16000
16000
  baseline: {
16001
- version: "4.5.3",
16002
- tag: "v4.5.3",
16003
- commit: "69b2bc590f08",
16004
- npm: "@mutmutco/cli@4.5.3"
16001
+ version: "4.5.5",
16002
+ tag: "v4.5.5",
16003
+ commit: "696fa43d4d49",
16004
+ npm: "@mutmutco/cli@4.5.5"
16005
16005
  },
16006
16006
  exitCriterion: "fleet-n-of-n",
16007
16007
  hubOnlyShortcut: "forbidden",
@@ -16018,14 +16018,14 @@ var rollout_plan_default = {
16018
16018
  repo: "mutmutco/mmi-hub",
16019
16019
  role: "canary",
16020
16020
  schedule: "train",
16021
- v3Target: "v4.5.3"
16021
+ v3Target: "v4.5.5"
16022
16022
  }
16023
16023
  ],
16024
16024
  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.",
16025
16025
  rollback: {
16026
16026
  independent: true,
16027
- mechanism: "npm dist-tag latest -> 4.5.3 and redeploy the Hub Lambda from tag v4.5.3 (69b2bc590f08); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16028
- v3Target: "v4.5.3 (@mutmutco/cli@4.5.3, tag commit 69b2bc590f08 \u2014 last known-good release carrying the repo-index v4-only contract)"
16027
+ mechanism: "npm dist-tag latest -> 4.5.5 and redeploy the Hub Lambda from tag v4.5.5 (696fa43d4d49); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16028
+ v3Target: "v4.5.5 (@mutmutco/cli@4.5.5, tag commit 696fa43d4d49 \u2014 last known-good release carrying the repo-index v4-only contract)"
16029
16029
  }
16030
16030
  },
16031
16031
  {
@@ -18939,37 +18939,65 @@ function isTerminalVercelState(state) {
18939
18939
  function str(v) {
18940
18940
  return typeof v === "string" && v.length > 0 ? v : void 0;
18941
18941
  }
18942
- async function readVercelDeployment(api, repo, target, environment) {
18943
- const query = target.sha ? `sha=${encodeURIComponent(target.sha)}` : `ref=${encodeURIComponent(target.ref ?? "")}`;
18944
- const listed = await api(`repos/${repo}/deployments?${query}&per_page=30`);
18945
- const rows = Array.isArray(listed) ? listed : [];
18946
- const prefix = environment.toLowerCase();
18947
- const hit = rows.find((d) => typeof d.id === "number" && (str(d.environment) ?? "").toLowerCase().startsWith(prefix));
18948
- if (!hit) return void 0;
18949
- const statuses = await api(`repos/${repo}/deployments/${hit.id}/statuses?per_page=1`);
18950
- const latest = Array.isArray(statuses) ? statuses[0] : void 0;
18951
- const state = str(latest?.state) ?? "none";
18942
+ function matchesTarget(environment, target) {
18943
+ const production = environment.toLowerCase().startsWith("production");
18944
+ return target === "production" ? production : !production;
18945
+ }
18946
+ async function readOne(api, repo, id) {
18947
+ const statuses = await api(`repos/${repo}/deployments/${id}/statuses?per_page=10`);
18948
+ const rows = Array.isArray(statuses) ? statuses : [];
18949
+ const latest = rows.find((s) => str(s.state) !== "inactive") ?? rows[0];
18950
+ const raw = str(latest?.state);
18951
+ const state = raw === void 0 ? "none" : raw === "inactive" ? "success" : raw;
18952
18952
  const url = str(latest?.environment_url) ?? str(latest?.target_url);
18953
+ const logUrl = str(latest?.log_url);
18954
+ return { state, ...url ? { url } : {}, ...logUrl ? { logUrl } : {} };
18955
+ }
18956
+ var FAILED = /* @__PURE__ */ new Set(["failure", "error"]);
18957
+ async function readVercelDeployment(api, repo, sha, target, since) {
18958
+ const listed = await api(`repos/${repo}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
18959
+ const rows = Array.isArray(listed) ? listed : [];
18960
+ const newest = /* @__PURE__ */ new Map();
18961
+ for (const d of rows) {
18962
+ const env = str(d.environment) ?? "";
18963
+ if (typeof d.id !== "number" || !matchesTarget(env, target) || newest.has(env)) continue;
18964
+ const created = Date.parse(str(d.created_at) ?? "");
18965
+ if (since !== void 0 && Number.isFinite(created) && created < since) continue;
18966
+ newest.set(env, d.id);
18967
+ }
18968
+ if (newest.size === 0) return void 0;
18969
+ const reads = await Promise.all([...newest].map(async ([env, id]) => ({ env, id, ...await readOne(api, repo, id) })));
18970
+ const failed = reads.find((r) => FAILED.has(r.state));
18971
+ const unfinished = reads.find((r) => !isTerminalVercelState(r.state));
18972
+ const pick = failed ?? unfinished ?? reads[0];
18953
18973
  return {
18954
- id: hit.id,
18955
- environment: str(hit.environment) ?? environment,
18956
- sha: str(hit.sha) ?? target.sha ?? "",
18957
- ref: str(hit.ref) ?? target.ref ?? "",
18958
- state,
18959
- ...url ? { url } : {},
18960
- ...str(latest?.log_url) ? { logUrl: str(latest?.log_url) } : {}
18974
+ id: pick.id,
18975
+ environment: reads.map((r) => r.env).join(", "),
18976
+ sha,
18977
+ state: pick.state,
18978
+ ...reads[0].url ? { url: reads[0].url } : {},
18979
+ ...pick.logUrl ? { logUrl: pick.logUrl } : {}
18961
18980
  };
18962
18981
  }
18963
18982
  var VERCEL_WATCH_ATTEMPTS = 120;
18964
18983
  var VERCEL_WATCH_DELAY_MS = 1e4;
18965
- async function watchVercelProduction(api, repo, sha, sleep2, onTick) {
18984
+ async function watchVercelDeployment(api, repo, sha, target, sleep2, onTick, since) {
18966
18985
  let last;
18986
+ let readOnce = false;
18987
+ let lastError;
18967
18988
  for (let attempt = 0; attempt < VERCEL_WATCH_ATTEMPTS; attempt++) {
18968
18989
  if (attempt > 0) await sleep2(VERCEL_WATCH_DELAY_MS);
18969
- last = await readVercelDeployment(api, repo, { sha }, "Production");
18990
+ try {
18991
+ last = await readVercelDeployment(api, repo, sha, target, since);
18992
+ readOnce = true;
18993
+ } catch (e) {
18994
+ lastError = e;
18995
+ continue;
18996
+ }
18970
18997
  onTick?.(attempt, last);
18971
18998
  if (last && isTerminalVercelState(last.state)) return last;
18972
18999
  }
19000
+ if (!readOnce) throw lastError;
18973
19001
  return last;
18974
19002
  }
18975
19003
  async function probeVercelUrl(url, fetchImpl = fetch) {
@@ -19275,38 +19303,14 @@ function staleServedProcess(probe) {
19275
19303
  function servedChannelRow(product, conclusion, evidence) {
19276
19304
  return { workflow: `served-channel(${product})`, runUrlNote: evidence, conclusion };
19277
19305
  }
19278
- 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>`";
19279
- var SERVED_CHANNELS = {
19280
- "mutmutco/mm-strategy": {
19281
- product: "mm-strategy",
19282
- url: "https://strategy.mutmut.work/release/version",
19283
- mover: "the MM-Strategy container deploy (the running server's own version is the pointer)",
19284
- remedy: "`mmi-cli devops runtime tenant redeploy mutmutco/MM-Strategy main`"
19285
- },
19286
- "mutmutco/jerv-hub": {
19287
- product: "jerv",
19288
- url: "https://get.jerv.ai/jerv/release/version",
19289
- mover: "the Jerv Gateway deploy (`jerv-gate fetch-release --product jerv`)",
19290
- remedy: JERV_GATEWAY_REMEDY
19291
- },
19292
- "mutmutco/jerv-jervcode": {
19293
- product: "jervcode",
19294
- url: "https://get.jerv.ai/jervcode/release/version",
19295
- // Jerv-JervCode#6456: JervCode's own publish.yml `serve` job moves and proves this pointer.
19296
- mover: "JervCode's own release (publish.yml `serve` job: `jerv-gate fetch-release --product jervcode` on jerv-central)",
19297
- remedy: "re-run the `serve` job of the JervCode release's publish.yml run (`gh run rerun <run-id> --job serve --repo mutmutco/Jerv-JervCode`), or `gh workflow run publish.yml --repo mutmutco/Jerv-JervCode -f tag=<tag>`"
19298
- }
19299
- };
19300
19306
  function servedChannelFor(repo, installer) {
19301
- const known = SERVED_CHANNELS[repo.toLowerCase()];
19302
- if (!installer) return known;
19307
+ if (!installer) return void 0;
19303
19308
  if (isHubControlRepo(repo)) return void 0;
19304
- const url = installerVersionUrl(installer);
19305
19309
  return {
19306
19310
  product: installer.product,
19307
- url,
19308
- mover: known?.mover ?? `this repo's release (${installer.family === "jerv" ? "get.jerv.ai" : "get.mutmut.work"} serves /${installer.product})`,
19309
- remedy: known?.remedy ?? `re-run the release's served-channel publish for ${installer.product} (\`mmi-cli devops release --resume\`)`
19311
+ url: installerVersionUrl(installer),
19312
+ mover: `this repo's own release (it serves /${installer.product} on its installer host)`,
19313
+ remedy: `re-run this repo's own served-channel publish for ${installer.product}, then \`mmi-cli devops release --resume\``
19310
19314
  };
19311
19315
  }
19312
19316
  var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
@@ -19444,34 +19448,28 @@ function prodReleaseTargets(model) {
19444
19448
  function trainObservesShipment(model) {
19445
19449
  return isCentralDispatchModel(model) || prodReleaseTargets(model).length > 0 || model === "vercel";
19446
19450
  }
19447
- async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
19448
- const note = "no dispatch: Vercel deploys main on push (Production) \u2014 observed via the Vercel GitHub app Deployment";
19449
- if (ref !== "main") return { note: "no dispatch on rc: a vercel repo is direct-track and has no rc stage", deployStatus: "pending" };
19451
+ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha, since) {
19452
+ const target = ref === "main" ? "production" : "preview";
19453
+ const plane = target === "production" ? "Production" : `${ref} (non-Production)`;
19454
+ const note = `no dispatch: Vercel deploys ${ref} on push (${plane}) \u2014 observed via the Vercel GitHub app Deployment`;
19450
19455
  if (!headSha) return { note, deployStatus: "pending" };
19451
- const api = async (path2) => JSON.parse(await deps.run("gh", ["api", path2]));
19452
- const row = (d, conclusion) => ({
19453
- workflow: "vercel",
19454
- conclusion,
19455
- ...d?.logUrl ? { runUrl: d.logUrl } : { runUrlNote: "no Vercel Deployment posted for this SHA yet" },
19456
- event: "push"
19457
- });
19458
- const verdict = (d) => d?.state === "success" ? "success" : d && isTerminalVercelState(d.state) ? "failure" : "pending";
19456
+ const after = target === "preview" ? rcWindow(since) : void 0;
19459
19457
  let read;
19460
19458
  try {
19461
19459
  if (!watch) {
19462
19460
  deps.warn?.("Release: Checking Vercel deployment status");
19463
- read = await readVercelDeployment(api, ctx.repo, { sha: headSha }, "Production");
19461
+ read = await readVercelDeployment(vercelApi(deps), ctx.repo, headSha, target, after);
19464
19462
  } else {
19465
- deps.warn?.("Release: Waiting for Vercel to deploy production");
19466
- read = await watchVercelProduction(api, ctx.repo, headSha, resolveSleep(deps), (attempt, d) => {
19463
+ deps.warn?.(`Release: Waiting for Vercel to deploy ${ref}`);
19464
+ read = await watchVercelDeployment(vercelApi(deps), ctx.repo, headSha, target, resolveSleep(deps), (attempt, d) => {
19467
19465
  if (attempt > 0 && attempt % 6 === 0) deps.warn?.(`Vercel deployment ${d ? d.state : "not posted yet"} \u2014 ${attempt / 6}m elapsed`);
19468
- });
19466
+ }, after);
19469
19467
  }
19470
19468
  } catch (e) {
19471
- return { note: `${note}; the Deployment read FAILED (${describeReadError(e)}) \u2014 verify on Vercel; never re-tag`, workflowRuns: [row(void 0, "failure")], deployStatus: "failure" };
19469
+ return { note: `${note}; the Deployment read FAILED (${describeReadError(e)}) \u2014 verify on Vercel; never re-tag`, workflowRuns: [vercelRow(void 0, "failure")], deployStatus: "failure" };
19472
19470
  }
19473
- let deployStatus = verdict(read);
19474
- let detail = read ? `Vercel Production deployment ${read.state}${read.url ? ` at ${read.url}` : ""}` : "no Vercel Deployment posted for this SHA (is the Vercel GitHub app connected?)";
19471
+ let deployStatus = vercelVerdict(read);
19472
+ let detail = read ? `Vercel ${plane} deployment ${read.state}${read.url ? ` at ${read.url}` : ""}` : "no Vercel Deployment posted for this SHA (is the Vercel GitHub app connected?)";
19475
19473
  if (watch && deployStatus === "success" && read?.url) {
19476
19474
  const probe = await (deps.probeUrl ?? probeVercelUrl)(read.url);
19477
19475
  if (probe.ok) detail = `${detail}; URL answered HTTP ${probe.status}`;
@@ -19480,7 +19478,27 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
19480
19478
  detail = `${detail}; URL probe FAILED (${probe.error ?? `HTTP ${probe.status}`})`;
19481
19479
  }
19482
19480
  }
19483
- return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
19481
+ return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [vercelRow(read, deployStatus)], deployStatus };
19482
+ }
19483
+ function vercelApi(deps) {
19484
+ return async (path2) => JSON.parse(await deps.run("gh", ["api", path2]));
19485
+ }
19486
+ function vercelVerdict(d) {
19487
+ return d?.state === "success" ? "success" : d && isTerminalVercelState(d.state) ? "failure" : "pending";
19488
+ }
19489
+ function vercelRow(d, conclusion) {
19490
+ return { workflow: "vercel", conclusion, ...d?.logUrl ? { runUrl: d.logUrl } : { runUrlNote: "no Vercel Deployment posted for this SHA yet" }, event: "push" };
19491
+ }
19492
+ async function listVercelDeployRows(deps, repo, sha, ref = "main", since) {
19493
+ const target = ref === "main" ? "production" : "preview";
19494
+ const read = await readVercelDeployment(vercelApi(deps), repo, sha, target, target === "preview" ? rcWindow(since) : void 0);
19495
+ if (!read) return [];
19496
+ const verdict = vercelVerdict(read);
19497
+ const probe = verdict === "success" && read.url ? await (deps.probeUrl ?? probeVercelUrl)(read.url) : void 0;
19498
+ return [vercelRow(read, probe && !probe.ok ? "failure" : verdict)];
19499
+ }
19500
+ function rcWindow(since) {
19501
+ return since === void 0 ? void 0 : since - 6e4;
19484
19502
  }
19485
19503
  function shipLegsGreen(deploy, publish) {
19486
19504
  const green = (leg) => leg?.superseded !== true && (leg?.state === "complete" || leg?.state === "skipped");
@@ -19662,7 +19680,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
19662
19680
  deployStatus: aggregateWorkflowRuns(workflowRuns)
19663
19681
  };
19664
19682
  }
19665
- if (model === "vercel") return vercelDeployDispatch(deps, ctx, ref, watch, autoRunHeadSha);
19683
+ if (model === "vercel") return vercelDeployDispatch(deps, ctx, ref, watch, autoRunHeadSha, autoRunSince);
19666
19684
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
19667
19685
  }
19668
19686
  async function readEdgeDomains(deps, slug) {
@@ -21931,6 +21949,16 @@ function deployPhaseInput(model, dispatch, opts = {}) {
21931
21949
  ...runMetadata({ runId: dispatch.runId, runUrl: dispatch.runUrl, workflow: "tenant-deploy.yml", event: "workflow_dispatch" }, runRepo, "dispatch"),
21932
21950
  note: dispatch.note
21933
21951
  };
21952
+ case "vercel": {
21953
+ const row = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "vercel");
21954
+ return {
21955
+ state: runRowState(row, dispatch.deployStatus),
21956
+ ...sha ? { sha } : {},
21957
+ workflow: "vercel",
21958
+ ...row?.runUrl ? { runUrl: row.runUrl } : {},
21959
+ note: dispatch.note
21960
+ };
21961
+ }
21934
21962
  default:
21935
21963
  return {
21936
21964
  state: dispatch.deployStatus === "success" ? "complete" : dispatch.deployStatus === "failure" ? "failed" : "pending",
@@ -22012,6 +22040,13 @@ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
22012
22040
  note: `${publishRun.workflow} run on the release SHA`
22013
22041
  } : { state: "pending", sha: tagSha, note: "publish.yml release run not yet correlated" }
22014
22042
  };
22043
+ case "vercel": {
22044
+ const vercelRow2 = rows.find((r) => r.workflow === "vercel");
22045
+ return {
22046
+ deploy: vercelRow2 ? { state: runRowState(vercelRow2, "pending"), sha: tagSha, workflow: "vercel", ...vercelRow2.runUrl ? { runUrl: vercelRow2.runUrl } : {}, note: "Vercel GitHub app Deployment on the release SHA" } : { state: "pending", sha: tagSha, note: "no Vercel Deployment posted for the release SHA yet (is the Vercel GitHub app connected?)" },
22047
+ publish: { state: "skipped", sha: tagSha, note: "vercel has no publish leg" }
22048
+ };
22049
+ }
22015
22050
  default:
22016
22051
  return {
22017
22052
  deploy: { state: "pending", sha: tagSha, note: `${model} deploys outside the release SHA's run set \u2014 historical outcome uncorrelatable` },
@@ -22218,6 +22253,17 @@ function hotfixRetryDeployShape(ledger) {
22218
22253
  if (!open2.every((phase) => RETRY_DEPLOY_LEGS.has(phase))) return false;
22219
22254
  return open2.some((phase) => ledger.phases[phase].state === "failed");
22220
22255
  }
22256
+ function rcandRetryDeployShape(ledger) {
22257
+ if (ledger.lane !== "rcand") return false;
22258
+ if (ledger.phases.promotion.state !== "complete") return false;
22259
+ const deploy = ledger.phases.deploy;
22260
+ if (deploy.state !== "failed" || deploy.superseded === true || deploy.workflow !== "tenant-deploy.yml") return false;
22261
+ return RELEASE_LEDGER_PHASES.every((phase) => {
22262
+ if (phase === "deploy") return true;
22263
+ const state = ledger.phases[phase].state;
22264
+ return state === "complete" || state === "skipped";
22265
+ });
22266
+ }
22221
22267
  function ledgerFailedRemedy(ledger) {
22222
22268
  const derived = deriveReleasePhaseStatus(ledger);
22223
22269
  const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
@@ -22226,6 +22272,9 @@ function ledgerFailedRemedy(ledger) {
22226
22272
  const failed = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
22227
22273
  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}`;
22228
22274
  }
22275
+ if (rcandRetryDeployShape(ledger)) {
22276
+ return `fix the rc deploy failure, then retry the failed deploy of the promoted ${ledger.tag} candidate (same tag and SHA, never re-cut): \`mmi-cli devops rcand --retry-deploy --watch\` (#7053)${tails}`;
22277
+ }
22229
22278
  const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
22230
22279
  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)` : "";
22231
22280
  return `diagnose the failed leg, then ${ledgerLaneResumeCommand(ledger)} once its cause is fixed (an abort only with fresh approval)${supersession}${tails}`;
@@ -22275,6 +22324,10 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
22275
22324
  return prior.tag;
22276
22325
  });
22277
22326
  }
22327
+ function rcPromotedAt(ledger) {
22328
+ const at = Date.parse(ledger.phases.promotion.updatedAt);
22329
+ return Number.isFinite(at) ? at : void 0;
22330
+ }
22278
22331
  async function healUnfoldedRunLegs(deps, cwd, model) {
22279
22332
  const path2 = releaseLedgerPath(cwd);
22280
22333
  let prior;
@@ -22306,15 +22359,16 @@ async function healUnfoldedRunLegs(deps, cwd, model) {
22306
22359
  }
22307
22360
  const targets = prodReleaseTargets(model);
22308
22361
  const runless = legs.filter((phase) => openLeg(current.phases[phase]) && current.phases[phase].runId == null);
22309
- if (runless.length > 0 && targets.length > 0) {
22362
+ if (runless.length > 0 && (targets.length > 0 || model === "vercel")) {
22310
22363
  let rows;
22311
22364
  try {
22312
- rows = await listOwnWorkflowRuns(deps, releaseRunRepoFor(model, prior.repo), targets, prior.tagSha);
22365
+ rows = model === "vercel" ? await listVercelDeployRows(deps, prior.repo, prior.tagSha, prior.lane === "rcand" ? "rc" : "main", rcPromotedAt(prior)) : await listOwnWorkflowRuns(deps, releaseRunRepoFor(model, prior.repo), targets, prior.tagSha);
22313
22366
  } catch {
22314
22367
  rows = [];
22315
22368
  }
22316
22369
  const inputs = phaseInputsFromRunRows(model, rows, prior.tagSha, { repo: prior.repo });
22317
- const foldable = runless.filter((phase) => inputs[phase].runId != null && inputs[phase].state !== "pending");
22370
+ const correlated = (entry) => entry.runId != null || entry.workflow === "vercel";
22371
+ const foldable = runless.filter((phase) => correlated(inputs[phase]) && inputs[phase].state !== "pending");
22318
22372
  if (foldable.length > 0) {
22319
22373
  const written = await withReleaseLedger(path2, anchors, (ledger) => {
22320
22374
  if (!ledger) return { value: [] };
@@ -23212,6 +23266,13 @@ async function runTrainDoctor(input) {
23212
23266
  unverified(`required status checks on ${stage}`, e);
23213
23267
  }
23214
23268
  }
23269
+ if (meta?.deployModel === "vercel") {
23270
+ const tip = await git3(["rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"]).catch(() => "");
23271
+ const rows = tip ? await listVercelDeployRows(train, repo, tip).catch(() => void 0) : void 0;
23272
+ if (rows && !rows.length) {
23273
+ add({ code: "vercel-unobserved", severity: "warning", source: "origin", title: `no Vercel Production Deployment on origin/main@${tip.slice(0, 12)} \u2014 the Vercel GitHub app or the git-connected Vercel project may be missing`, remedy: `connect ${repo} to its Vercel project (production branch main) and enable the Vercel GitHub app for the repo; the train reads the app's Deployments, never a Vercel token` });
23274
+ }
23275
+ }
23215
23276
  let ledgerTag;
23216
23277
  let ledgerPath;
23217
23278
  try {
@@ -24176,10 +24237,15 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
24176
24237
  note: notes.length > 0 ? `${dispatch.note}; ${notes.join("; ")}` : dispatch.note
24177
24238
  };
24178
24239
  }
24240
+ var RCAND_RETRY_DEPLOY_COMMAND = "mmi-cli devops rcand --retry-deploy --watch";
24241
+ function rcPushedAt(ledger) {
24242
+ const at = Date.parse(ledger?.phases.promotion.updatedAt ?? "");
24243
+ return Number.isFinite(at) ? at : void 0;
24244
+ }
24179
24245
  async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts = {}) {
24180
24246
  const sha = anchors.tagSha;
24181
24247
  const landed = "the immutable rc tag, the green wall, origin/rc, and the rc deploy dispatch";
24182
- const dispatch = await recoverFailedDispatchPhase(
24248
+ const recovered = await recoverFailedDispatchPhase(
24183
24249
  deps,
24184
24250
  ledger,
24185
24251
  anchors,
@@ -24192,6 +24258,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
24192
24258
  workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "rc deploy workflow"
24193
24259
  }
24194
24260
  );
24261
+ const dispatch = recovered.deployStatus === "failure" && !recovered.note.includes(RCAND_RETRY_DEPLOY_COMMAND) ? { ...recovered, note: `once the cause is fixed, retry this rc deploy with \`${RCAND_RETRY_DEPLOY_COMMAND}\` (same tag and SHA, never re-cut): ${recovered.note}` } : recovered;
24195
24262
  const unverifiable = !isCentralDispatchModel(deployModel) && dispatch.deployStatus === "pending" && dispatch.runId == null && !dispatch.workflowRuns?.length;
24196
24263
  const deployEntry = unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
24197
24264
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployEntry), { ...opts, landed: "the immutable rc tag, the green wall, and origin/rc" });
@@ -24200,7 +24267,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
24200
24267
  return dispatch;
24201
24268
  }
24202
24269
  function releaseAnnounceDeferral(model, summaryFile) {
24203
- return prodReleaseTargets(model).length > 0 ? `\`mmi-cli devops release --resume --watch${summaryFile ? ` --announce-summary-file ${summaryFile}` : ""}\` announces it once they are green` : "a central deploy is proven only under --watch, so this release stays unannounced";
24270
+ return prodReleaseTargets(model).length > 0 || model === "vercel" ? `\`mmi-cli devops release --resume --watch${summaryFile ? ` --announce-summary-file ${summaryFile}` : ""}\` announces it once they are green` : "a central deploy is proven only under --watch, so this release stays unannounced";
24204
24271
  }
24205
24272
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger, alignEarly) {
24206
24273
  const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
@@ -24487,7 +24554,8 @@ Nothing was written. Inspect ${ledger.path} before retrying.`);
24487
24554
  const terminal = deployLeg.state === "complete" || deployLeg.state === "skipped";
24488
24555
  const legStatus = deployLeg.state === "failed" ? "failure" : terminal ? "success" : "pending";
24489
24556
  const hasDispatchEvidence = deployLeg.runId != null || deployLeg.workflow != null || deployLeg.updatedAt !== "never";
24490
- const dispatch02 = terminal || hasDispatchEvidence ? {
24557
+ const vercelReread = deployModel === "vercel" && !terminal;
24558
+ const dispatch02 = (terminal || hasDispatchEvidence) && !vercelReread ? {
24491
24559
  note: deployLeg.note ?? deployLeg.error ?? `persisted rc deploy leg is ${deployLeg.state}`,
24492
24560
  deployStatus: legStatus,
24493
24561
  ...deployLeg.runId != null ? { runId: deployLeg.runId } : {},
@@ -24499,7 +24567,7 @@ Nothing was written. Inspect ${ledger.path} before retrying.`);
24499
24567
  ...deployLeg.event ? { event: deployLeg.event } : {},
24500
24568
  conclusion: legStatus
24501
24569
  }] } : {}
24502
- } : await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, (deps.now ?? Date.now)(), tagSha);
24570
+ } : await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, vercelReread ? rcPushedAt(persisted) : (deps.now ?? Date.now)(), tagSha, "report");
24503
24571
  const dispatch2 = terminal ? dispatch02 : await recordRcandDeployLegs(ledger, deps, anchors, deployModel, dispatch02, { strict: true });
24504
24572
  const ledgerReport2 = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
24505
24573
  return {
@@ -24565,10 +24633,11 @@ ${spent.note}`);
24565
24633
  }
24566
24634
  await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict, landed: "the immutable rc tag on origin at the resumed SHA" });
24567
24635
  const autoRunSince = (deps.now ?? Date.now)();
24636
+ deps.warn?.("Release: Promoting the verified release candidate");
24568
24637
  await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/rc`]);
24569
24638
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/rc (tag preserved, never re-cut)` }), { strict, landed: "the immutable rc tag and the re-proven required-check wall" });
24570
24639
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: tagSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { strict, landed: "the immutable rc tag, the green wall, and origin/rc" });
24571
- const dispatch0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha);
24640
+ const dispatch0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report");
24572
24641
  const dispatch = await recordRcandDeployLegs(ledger, deps, anchors, deployModel, dispatch0, { strict });
24573
24642
  const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
24574
24643
  return {
@@ -24590,6 +24659,64 @@ ${spent.note}`);
24590
24659
  note: `reused immutable ${tag}; pushed its exact SHA to rc and dispatched the rc deploy`
24591
24660
  };
24592
24661
  }
24662
+ async function runRcandRetryDeploy(deps, options = {}) {
24663
+ return withTrainWriter(deps, "rcand --retry-deploy", () => runRcandRetryDeployLocked(deps, options));
24664
+ }
24665
+ async function runRcandRetryDeployLocked(deps, options) {
24666
+ const label = "rcand --retry-deploy";
24667
+ const ctx = await buildTrainApplyContext(deps);
24668
+ await requireCleanTree(deps);
24669
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
24670
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
24671
+ const deployModel = await preflight(deps, ctx, "rc", meta);
24672
+ if (!isCentralDispatchModel(deployModel)) {
24673
+ throw new Error(`${label}: the ${deployModel} rc deploy is the repo's own push-triggered workflow, not a dispatch \u2014 rerun that failed run (gh run rerun <run-id> --failed), then mmi-cli devops rcand --resume re-verifies it; nothing was written`);
24674
+ }
24675
+ const candidate = await activeRcandLedgerCandidate(deps, process.cwd());
24676
+ if (!candidate) {
24677
+ throw new Error(`${label}: no active rcand ledger whose rc tag is live on origin at its recorded SHA \u2014 there is no promoted candidate deploy to retry; nothing was written`);
24678
+ }
24679
+ const { tag, sha: tagSha } = candidate;
24680
+ const ledger = openReleaseLedgerWriter(process.cwd());
24681
+ const anchors = { repo: ctx.repo, tag, tagSha };
24682
+ let persisted;
24683
+ try {
24684
+ persisted = (await reverifyPersistedRunPhases(deps, ledger.path, anchors, { sha: tagSha, repo: ctx.repo })).ledger;
24685
+ } catch (e) {
24686
+ throw new Error(`${label}: ${e instanceof Error ? e.message : String(e)}
24687
+ Nothing was written. Inspect ${ledger.path} before retrying.`);
24688
+ }
24689
+ if (!persisted || !rcandRetryDeployShape(persisted)) {
24690
+ const open2 = persisted ? deriveReleasePhaseStatus(persisted).legs.filter((l) => l.state === "pending" || l.state === "failed") : [];
24691
+ throw new Error(
24692
+ `${label}: the ${tag} ledger has no failed rc deploy to retry on a promoted candidate (${open2.map((l) => `${l.phase}=${l.state}`).join(", ") || "phases-green"}) \u2014 finish an incomplete candidate with mmi-cli devops rcand --resume; nothing was written`
24693
+ );
24694
+ }
24695
+ if (!await probeAncestor(deps, tagSha, "origin/rc", `${label}: origin/rc ancestry`)) {
24696
+ throw new Error(`${label}: origin/rc does not contain ${tag} (${tagSha.slice(0, 12)}) \u2014 the candidate is not promoted; nothing was written`);
24697
+ }
24698
+ const d0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, options.watch ?? false, (deps.now ?? Date.now)(), tagSha, "report");
24699
+ const dispatch = await recordRcandDeployLegs(ledger, deps, anchors, deployModel, d0, { strict: true });
24700
+ const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
24701
+ return {
24702
+ ...ctx,
24703
+ command: "rcand-retry-deploy",
24704
+ stage: "rc",
24705
+ ref: "rc",
24706
+ tag,
24707
+ tagSha,
24708
+ deployModel,
24709
+ promoted: true,
24710
+ checks: persisted.phases.ordinaryCi.note ?? "required-check wall was completed before origin/rc promotion",
24711
+ dispatch: dispatch.note,
24712
+ runId: dispatch.runId,
24713
+ runUrl: dispatch.runUrl,
24714
+ workflowRuns: dispatch.workflowRuns,
24715
+ deployStatus: dispatch.deployStatus,
24716
+ ...ledgerReport ? { ledger: ledgerReport } : {},
24717
+ note: `re-dispatched the failed rc deploy of immutable ${tag} at ${tagSha.slice(0, 12)}; nothing re-cut, re-tagged or re-pushed`
24718
+ };
24719
+ }
24593
24720
  async function runReleaseResumePipeline(deps, options, prepared) {
24594
24721
  const watch = options.watch ?? false;
24595
24722
  const { ctx, meta, branchHints, directTrack, deployModel } = prepared;
@@ -24653,8 +24780,9 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
24653
24780
  deps.warn?.("Release: Checking deployment and publishing progress");
24654
24781
  const historicalTargets = prodReleaseTargets(deployModel);
24655
24782
  const historicalRepo = releaseRunRepoFor(deployModel, ctx.repo);
24656
- let historicalRows = historicalTargets.length ? await listOwnWorkflowRuns(deps, historicalRepo, historicalTargets, tagSha) : [];
24657
- let historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : "pending";
24783
+ const vercel = deployModel === "vercel";
24784
+ let historicalRows = vercel ? await listVercelDeployRows(deps, ctx.repo, tagSha).catch(() => []) : historicalTargets.length ? await listOwnWorkflowRuns(deps, historicalRepo, historicalTargets, tagSha) : [];
24785
+ let historicalDeploy = historicalTargets.length || vercel && historicalRows.length ? aggregateWorkflowRuns(historicalRows) : "pending";
24658
24786
  let historicalNote = `historical ${deployModel} deploy/publish status: ${historicalDeploy}`;
24659
24787
  if (historicalDeploy === "failure") {
24660
24788
  const recovered = await recoverFailedDispatchPhase(
@@ -24666,7 +24794,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
24666
24794
  { repo: historicalRepo, sha: tagSha, watched: false, workflow: "historical release workflow" }
24667
24795
  );
24668
24796
  historicalRows = recovered.workflowRuns ?? historicalRows;
24669
- historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
24797
+ historicalDeploy = historicalTargets.length || vercel && historicalRows.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
24670
24798
  historicalNote = recovered.note;
24671
24799
  }
24672
24800
  const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
@@ -24704,7 +24832,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
24704
24832
  Object.assign(dispatch2, shippedDispatch);
24705
24833
  if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
24706
24834
  const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
24707
- const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
24835
+ const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || deployModel === "vercel" || companion !== null) ? await announceShippedRelease(
24708
24836
  deps,
24709
24837
  { repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
24710
24838
  deployModel,
@@ -25152,6 +25280,7 @@ async function runTrainApplyPipeline(mode, input) {
25152
25280
  if (!start.ok) throw new Error(`rcand refused before the rc tag was pushed: the durable phase ledger could not be started at ${ledger.path} (${start.error}) - nothing was tagged or pushed (#5987/#6068)`);
25153
25281
  ledgerAnchors2 = anchors2;
25154
25282
  }
25283
+ deps.warn?.("Release: Publishing the release candidate tag");
25155
25284
  tagPush = await ensureTagPushed(deps, tag2, rcSha);
25156
25285
  } catch (e) {
25157
25286
  if (ledger && ledgerAnchors2) {
@@ -25188,6 +25317,7 @@ ${spent.note}`), rcandRecovery(tag2));
25188
25317
  }
25189
25318
  await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: rcSha, note: checks2 }), { landed: "the immutable rc tag pushed at " + rcSha.slice(0, 12) });
25190
25319
  const autoRunSince = (deps.now ?? Date.now)();
25320
+ deps.warn?.("Release: Promoting the verified release candidate");
25191
25321
  try {
25192
25322
  await runGitPush(deps, ["push", "origin", "rc"]);
25193
25323
  } catch (e) {
@@ -25196,7 +25326,7 @@ ${spent.note}`), rcandRecovery(tag2));
25196
25326
  }
25197
25327
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: rcSha, note: `immutable ${tag2} pushed at ${rcSha.slice(0, 12)}; origin/rc fast-forwarded to it` }), { landed: "the immutable rc tag and the green required-check wall" });
25198
25328
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: rcSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { landed: "the immutable rc tag, the green wall, and origin/rc" });
25199
- const d0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
25329
+ const d0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report");
25200
25330
  const d2 = await recordRcandDeployLegs(ledger, deps, anchors, deployModel2, d0);
25201
25331
  const rcLedgerReport = releaseLedgerReport(await ledger?.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
25202
25332
  return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, bumpIntent, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus, ...rcLedgerReport ? { ledger: rcLedgerReport } : {} };
@@ -25486,7 +25616,7 @@ async function runTrainApplyLocked(command, deps, options = {}) {
25486
25616
  if (directTrack) {
25487
25617
  throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli devops release --apply` from development instead of rcand");
25488
25618
  }
25489
- return runTrainApplyPipeline("rcand", pipelineInput);
25619
+ return withCheckoutRestoredAfterRelease(deps, await runTrainApplyPipeline("rcand", pipelineInput), "development");
25490
25620
  }
25491
25621
  if (directTrack) {
25492
25622
  return withCheckoutRestoredAfterRelease(
@@ -38218,8 +38348,9 @@ function registerStageCommands(program3) {
38218
38348
  const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]);
38219
38349
  const repo = Array.isArray(project2.repos) && typeof project2.repos[0] === "string" ? project2.repos[0] : await repoSlug();
38220
38350
  try {
38351
+ const sha = await gitOut(["rev-parse", `origin/${branch}`]).catch(() => gitOut(["rev-parse", "HEAD"]));
38221
38352
  const gh = defaultGitHubClient();
38222
- const deployment = await readVercelDeployment((path2) => gh.rest("GET", path2), repo, { ref: branch }, "Preview");
38353
+ const deployment = await readVercelDeployment((path2) => gh.rest("GET", path2), repo, sha, "preview");
38223
38354
  return { branch, ...deployment ? { deployment } : {} };
38224
38355
  } catch (e) {
38225
38356
  return { branch, error: e.message };
@@ -38660,7 +38791,7 @@ function renderVerifySecrets(body) {
38660
38791
 
38661
38792
  // src/command-register-collaboration.ts
38662
38793
  var import_node_child_process15 = require("node:child_process");
38663
- var import_node_fs42 = require("node:fs");
38794
+ var import_node_fs43 = require("node:fs");
38664
38795
  var import_promises7 = require("node:fs/promises");
38665
38796
  init_clean_exit();
38666
38797
  init_cli_shared();
@@ -38684,7 +38815,7 @@ function spawnDetachedSelf(args, deps, opts = {}) {
38684
38815
  }
38685
38816
 
38686
38817
  // src/command-register-collaboration.ts
38687
- var import_node_path40 = require("node:path");
38818
+ var import_node_path41 = require("node:path");
38688
38819
 
38689
38820
  // src/attach-to-project.ts
38690
38821
  async function attachViaHubApp(cfg, contentNodeId) {
@@ -39768,9 +39899,9 @@ function changedFilesSince(base, cwd) {
39768
39899
  const raw = git2(["diff", "--name-status", "-M", base, "--"], cwd).trim();
39769
39900
  const changed = raw ? raw.split("\n").map((line) => {
39770
39901
  const parts = line.split(" ");
39771
- const norm = (p) => p.replace(/\\/g, "/");
39772
- const row = { status: parts[0][0], path: norm(parts[parts.length - 1]) };
39773
- if (parts.length > 2) row.from = norm(parts[1]);
39902
+ const norm2 = (p) => p.replace(/\\/g, "/");
39903
+ const row = { status: parts[0][0], path: norm2(parts[parts.length - 1]) };
39904
+ if (parts.length > 2) row.from = norm2(parts[1]);
39774
39905
  return row;
39775
39906
  }) : [];
39776
39907
  const untracked = git2(["ls-files", "--others", "--exclude-standard", "-z"], cwd).split("\0").filter(Boolean).map((path2) => ({ status: "A", path: path2.replace(/\\/g, "/") }));
@@ -40833,6 +40964,10 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
40833
40964
  return void 0;
40834
40965
  }
40835
40966
 
40967
+ // src/worktree-residue-sweep.ts
40968
+ var import_node_fs42 = require("node:fs");
40969
+ var import_node_path40 = require("node:path");
40970
+
40836
40971
  // src/worktree-merge-cleanup.ts
40837
40972
  var import_node_fs41 = require("node:fs");
40838
40973
  var import_node_path39 = require("node:path");
@@ -41678,6 +41813,122 @@ function renderPrMergeCleanupLines(cleanup) {
41678
41813
  return lines2;
41679
41814
  }
41680
41815
 
41816
+ // src/worktree-residue-sweep.ts
41817
+ var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
41818
+ function norm(p) {
41819
+ const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
41820
+ return process.platform === "win32" ? s.toLowerCase() : s;
41821
+ }
41822
+ function residueLedgerPath(gitCommonDir) {
41823
+ return (0, import_node_path40.join)(gitCommonDir, RESIDUE_LEDGER_FILE);
41824
+ }
41825
+ function readPaths(file, deps) {
41826
+ try {
41827
+ const parsed = JSON.parse((deps.readLedger ?? ((f) => (0, import_node_fs42.readFileSync)(f, "utf8")))(file));
41828
+ return Array.isArray(parsed) ? parsed.filter((p) => typeof p === "string") : [];
41829
+ } catch {
41830
+ return [];
41831
+ }
41832
+ }
41833
+ function writePaths(file, paths, deps) {
41834
+ try {
41835
+ const write = deps.writeLedger ?? ((f, t) => {
41836
+ (0, import_node_fs42.mkdirSync)((0, import_node_path40.dirname)(f), { recursive: true });
41837
+ (0, import_node_fs42.writeFileSync)(f, t, "utf8");
41838
+ });
41839
+ write(file, `${JSON.stringify([...new Set(paths)], null, 1)}
41840
+ `);
41841
+ } catch {
41842
+ }
41843
+ }
41844
+ function recordResidue(gitCommonDir, path2, deps = {}) {
41845
+ const file = residueLedgerPath(gitCommonDir);
41846
+ const paths = readPaths(file, deps);
41847
+ if (!paths.some((p) => norm(p) === norm(path2))) paths.push(path2);
41848
+ writePaths(file, paths, deps);
41849
+ }
41850
+ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
41851
+ const exists = deps.exists ?? import_node_fs42.existsSync;
41852
+ const readDir = deps.readDir ?? ((p) => (0, import_node_fs42.readdirSync)(p));
41853
+ const isDir = deps.isDir ?? ((p) => {
41854
+ try {
41855
+ return (0, import_node_fs42.statSync)(p).isDirectory();
41856
+ } catch {
41857
+ return false;
41858
+ }
41859
+ });
41860
+ const removeRecorded = deps.removeRecorded ?? ((p) => removeResidueDirectory(p));
41861
+ const removeEmpty = deps.removeEmpty ?? ((p) => (0, import_node_fs42.rmdirSync)(p));
41862
+ const report = { swept: [], kept: [], dropped: [], emptyRemoved: [], unrecorded: [] };
41863
+ const live = new Set(registered.map(norm));
41864
+ const primary = registered[0] ? norm(registered[0]) : void 0;
41865
+ const file = residueLedgerPath(gitCommonDir);
41866
+ const recorded = readPaths(file, deps);
41867
+ const remaining = [];
41868
+ for (const path2 of recorded) {
41869
+ const key = norm(path2);
41870
+ if (live.has(key) || key === primary) {
41871
+ report.dropped.push(path2);
41872
+ continue;
41873
+ }
41874
+ if (!exists(path2)) {
41875
+ report.dropped.push(path2);
41876
+ continue;
41877
+ }
41878
+ const removed = await removeRecorded(path2);
41879
+ if (removed.ok && !exists(path2)) report.swept.push(path2);
41880
+ else {
41881
+ report.kept.push({ path: path2, error: removed.ok ? "directory remains" : removed.error });
41882
+ remaining.push(path2);
41883
+ }
41884
+ }
41885
+ if (recorded.length) writePaths(file, remaining, deps);
41886
+ const roots = /* @__PURE__ */ new Set();
41887
+ for (const p of [...registered.slice(1), ...recorded]) roots.add((0, import_node_path40.dirname)(p));
41888
+ const keptKeys = new Set(remaining.map(norm));
41889
+ for (const root of roots) {
41890
+ if (!isDir(root) || live.has(norm(root)) || norm(root) === primary) continue;
41891
+ let children;
41892
+ try {
41893
+ children = readDir(root);
41894
+ } catch {
41895
+ continue;
41896
+ }
41897
+ for (const name of children) {
41898
+ const child2 = (0, import_node_path40.join)(root, name);
41899
+ const key = norm(child2);
41900
+ if (live.has(key) || key === primary || keptKeys.has(key) || !isDir(child2)) continue;
41901
+ let entries;
41902
+ try {
41903
+ entries = readDir(child2);
41904
+ } catch {
41905
+ continue;
41906
+ }
41907
+ if (entries.length === 0) {
41908
+ try {
41909
+ removeEmpty(child2);
41910
+ report.emptyRemoved.push(child2);
41911
+ } catch {
41912
+ }
41913
+ } else {
41914
+ report.unrecorded.push(child2);
41915
+ }
41916
+ }
41917
+ }
41918
+ return report;
41919
+ }
41920
+ function renderResidueSweepLines(report) {
41921
+ if (!report) return [];
41922
+ const lines2 = [];
41923
+ const cleaned = report.swept.length + report.emptyRemoved.length;
41924
+ if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges`);
41925
+ for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
41926
+ if (report.unrecorded.length) {
41927
+ lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root were left untouched (not recorded by a merge): ${report.unrecorded.join(", ")}`);
41928
+ }
41929
+ return lines2;
41930
+ }
41931
+
41681
41932
  // src/pr-deploy-dev.ts
41682
41933
  function slugOfRepo(repo) {
41683
41934
  return repo.split("/")[1]?.toLowerCase() ?? "";
@@ -41712,10 +41963,10 @@ function argvWantsJson2() {
41712
41963
  return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
41713
41964
  }
41714
41965
  function hubRoot() {
41715
- const fromPkg = (0, import_node_path40.join)(__dirname, "..", "..");
41966
+ const fromPkg = (0, import_node_path41.join)(__dirname, "..", "..");
41716
41967
  const marker = "skills/bootstrap/seeds/manifest.json";
41717
- if ((0, import_node_fs42.existsSync)((0, import_node_path40.join)(fromPkg, marker))) return fromPkg;
41718
- if ((0, import_node_fs42.existsSync)((0, import_node_path40.join)(process.cwd(), marker))) return process.cwd();
41968
+ if ((0, import_node_fs43.existsSync)((0, import_node_path41.join)(fromPkg, marker))) return fromPkg;
41969
+ if ((0, import_node_fs43.existsSync)((0, import_node_path41.join)(process.cwd(), marker))) return process.cwd();
41719
41970
  return null;
41720
41971
  }
41721
41972
  function ciAuditDeps() {
@@ -41727,8 +41978,8 @@ function ciAuditDeps() {
41727
41978
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
41728
41979
  readSeedFile: (path2) => {
41729
41980
  if (!root) return null;
41730
- const fullPath = (0, import_node_path40.join)(root, path2);
41731
- return (0, import_node_fs42.existsSync)(fullPath) ? (0, import_node_fs42.readFileSync)(fullPath, "utf8") : null;
41981
+ const fullPath = (0, import_node_path41.join)(root, path2);
41982
+ return (0, import_node_fs43.existsSync)(fullPath) ? (0, import_node_fs43.readFileSync)(fullPath, "utf8") : null;
41732
41983
  }
41733
41984
  };
41734
41985
  }
@@ -42453,11 +42704,11 @@ ${list}`);
42453
42704
  }
42454
42705
  });
42455
42706
  async function listCiWorkflowPaths(cwd = process.cwd()) {
42456
- const wfDir = (0, import_node_path40.join)(cwd, ".github", "workflows");
42457
- if (!(0, import_node_fs42.existsSync)(wfDir)) return [];
42458
- return (0, import_node_fs42.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
42707
+ const wfDir = (0, import_node_path41.join)(cwd, ".github", "workflows");
42708
+ if (!(0, import_node_fs43.existsSync)(wfDir)) return [];
42709
+ return (0, import_node_fs43.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
42459
42710
  try {
42460
- return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path40.join)(wfDir, name), "utf8"));
42711
+ return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0, import_node_path41.join)(wfDir, name), "utf8"));
42461
42712
  } catch {
42462
42713
  return true;
42463
42714
  }
@@ -42653,7 +42904,7 @@ ${list}`);
42653
42904
  jsonParity(pr.command("review-verdict <number>").description("post an explicit review verdict comment: computes the PR diff patch-id and head sha, renders the v1 contract body, and posts it as a PR comment").requiredOption("--repo <owner/repo>", "target repo").requiredOption("--verdict <verdict>", `one of ${REVIEW_VERDICTS.join("|")}`).requiredOption("--scope <text>", "one line: what was reviewed").requiredOption("--risk <text>", "one line: the residual risk").option("--unverified <text>", "a claim the reviewer could not verify (repeatable)", (v, acc) => [...acc, v], []).requiredOption("--reviewer <id>", "reviewer seat or model id").option("--findings-file <path>", "Markdown findings appended after the JSON block")).action(async (number, o) => {
42654
42905
  if (!isReviewVerdict(o.verdict)) return fail(`pr review-verdict: --verdict must be one of ${REVIEW_VERDICTS.join("|")} (got ${o.verdict})`);
42655
42906
  const verdict = o.verdict;
42656
- const findings = o.findingsFile ? (0, import_node_fs42.readFileSync)(o.findingsFile, "utf8") : void 0;
42907
+ const findings = o.findingsFile ? (0, import_node_fs43.readFileSync)(o.findingsFile, "utf8") : void 0;
42657
42908
  let patch;
42658
42909
  let head;
42659
42910
  try {
@@ -42927,7 +43178,7 @@ ${list}`);
42927
43178
  const mergeSquashBody = squashBodyTextForMerge(
42928
43179
  closingGuardInput,
42929
43180
  method === "--squash",
42930
- o.squashBodyFile ? (0, import_node_fs42.readFileSync)(o.squashBodyFile, "utf8") : void 0
43181
+ o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
42931
43182
  );
42932
43183
  if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
42933
43184
  const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
@@ -42959,7 +43210,7 @@ ${list}`);
42959
43210
  const remote = foreignCwd ? `https://github.com/${targetRepo2}.git` : "origin";
42960
43211
  let foreignCheckout;
42961
43212
  if (foreignCwd) {
42962
- const sibling = (0, import_node_path40.join)((0, import_node_path40.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
43213
+ const sibling = (0, import_node_path41.join)((0, import_node_path41.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
42963
43214
  const siblingRepo = repoFromRemoteUrl(await gitOut(["-C", sibling, "remote", "get-url", "origin"]).catch(() => ""));
42964
43215
  if (siblingRepo?.toLowerCase() === targetRepo2.toLowerCase()) {
42965
43216
  const siblingWorktrees = await execFileP("git", ["-C", sibling, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseGitWorktreePorcelain(r.stdout)).catch(() => void 0);
@@ -43057,7 +43308,7 @@ ${list}`);
43057
43308
  }
43058
43309
  if (!repoForPostCleanup) throw e;
43059
43310
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
43060
- const commitMessage = bodyFile ? (0, import_node_fs42.readFileSync)(bodyFile, "utf8") : void 0;
43311
+ const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
43061
43312
  await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
43062
43313
  body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
43063
43314
  timeoutMs: GH_MUTATION_TIMEOUT_MS
@@ -43164,6 +43415,15 @@ ${list}`);
43164
43415
  return;
43165
43416
  }
43166
43417
  const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
43418
+ let gitCommonDir;
43419
+ let residueSweep;
43420
+ if (!(foreignCwd && !foreignCheckout)) {
43421
+ try {
43422
+ gitCommonDir = (0, import_node_path41.resolve)(primaryRoot, (await execFileP("git", ["-C", primaryRoot, "rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim());
43423
+ residueSweep = await sweepWorktreeResidue(gitCommonDir, beforeWorktrees.map((w) => w.path));
43424
+ } catch {
43425
+ }
43426
+ }
43167
43427
  let localCleanup;
43168
43428
  if (foreignCwd && !foreignCheckout) {
43169
43429
  localCleanup = { branch: headRef, localBranch: { name: headRef, status: "not-attempted", reason: "skipped-foreign-cwd" } };
@@ -43177,7 +43437,7 @@ ${list}`);
43177
43437
  preserveWorktree: o.preserveWorktree,
43178
43438
  gcAcknowledged: o.gc,
43179
43439
  expectedHeadOid: headRefOid,
43180
- pathExists: (p) => (0, import_node_fs42.existsSync)(p),
43440
+ pathExists: (p) => (0, import_node_fs43.existsSync)(p),
43181
43441
  // #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
43182
43442
  // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
43183
43443
  // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
@@ -43208,6 +43468,7 @@ ${list}`);
43208
43468
  remote
43209
43469
  });
43210
43470
  const worktree = localCleanup?.worktree;
43471
+ if (gitCommonDir && worktree?.path && worktree.residue === "left") recordResidue(gitCommonDir, worktree.path);
43211
43472
  const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
43212
43473
  kind: "worktree-directory",
43213
43474
  path: worktree.path,
@@ -43272,6 +43533,7 @@ ${list}`);
43272
43533
  ...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
43273
43534
  ...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
43274
43535
  ...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
43536
+ ...residueSweep && (residueSweep.swept.length || residueSweep.emptyRemoved.length || residueSweep.kept.length || residueSweep.unrecorded.length) ? { residueSweep } : {},
43275
43537
  // `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
43276
43538
  // tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
43277
43539
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
@@ -43281,6 +43543,7 @@ ${list}`);
43281
43543
  ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
43282
43544
  }));
43283
43545
  for (const line of renderPrMergeCleanupLines(localCleanup)) console.warn(line);
43546
+ for (const line of renderResidueSweepLines(residueSweep)) console.warn(line);
43284
43547
  if (devDeploy && !devDeploy.ok) {
43285
43548
  console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
43286
43549
  }
@@ -43298,7 +43561,7 @@ ${list}`);
43298
43561
  }
43299
43562
 
43300
43563
  // src/command-register-developer.ts
43301
- var import_node_fs47 = require("node:fs");
43564
+ var import_node_fs48 = require("node:fs");
43302
43565
  init_clean_exit();
43303
43566
  init_cli_shared();
43304
43567
  init_error_codes();
@@ -43331,7 +43594,7 @@ async function resolveWhoami(deps) {
43331
43594
  }
43332
43595
 
43333
43596
  // src/command-register-developer.ts
43334
- var import_node_path45 = require("node:path");
43597
+ var import_node_path46 = require("node:path");
43335
43598
  init_house_map();
43336
43599
  init_hub_url();
43337
43600
 
@@ -43364,7 +43627,7 @@ async function executeWaveLand(plan, deps) {
43364
43627
  }
43365
43628
 
43366
43629
  // src/box-commands.ts
43367
- var import_node_fs43 = require("node:fs");
43630
+ var import_node_fs44 = require("node:fs");
43368
43631
  init_clean_exit();
43369
43632
 
43370
43633
  // src/box.ts
@@ -43576,7 +43839,7 @@ function registerBoxCommands(program3) {
43576
43839
  }
43577
43840
  const remote = command && command.length ? command.join(" ") : void 0;
43578
43841
  const wroteScript = o.ssh && o.script ? o.script : null;
43579
- if (wroteScript) (0, import_node_fs43.writeFileSync)(wroteScript, sshRecipeScript(found, remote), "utf8");
43842
+ if (wroteScript) (0, import_node_fs44.writeFileSync)(wroteScript, sshRecipeScript(found, remote), "utf8");
43580
43843
  if (o.json) {
43581
43844
  console.log(JSON.stringify({
43582
43845
  box: found,
@@ -43598,19 +43861,19 @@ ${SSH_RECIPE_AGENT_NOTE}`);
43598
43861
  // src/dist-drift.ts
43599
43862
  var import_node_child_process16 = require("node:child_process");
43600
43863
  var import_node_crypto13 = require("node:crypto");
43601
- var import_node_fs45 = require("node:fs");
43864
+ var import_node_fs46 = require("node:fs");
43602
43865
  var import_node_os21 = require("node:os");
43603
- var import_node_path42 = require("node:path");
43866
+ var import_node_path43 = require("node:path");
43604
43867
 
43605
43868
  // ../scripts/distribution-digest.mjs
43606
43869
  var import_node_crypto12 = require("node:crypto");
43607
- var import_node_fs44 = require("node:fs");
43608
- var import_node_path41 = require("node:path");
43870
+ var import_node_fs45 = require("node:fs");
43871
+ var import_node_path42 = require("node:path");
43609
43872
  var slash = (value) => value.replaceAll("\\", "/");
43610
43873
  function repoPath(root, declaredPath, label) {
43611
- const absoluteRoot = (0, import_node_path41.resolve)(root);
43612
- const target = (0, import_node_path41.resolve)(root, declaredPath);
43613
- if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path41.sep}`)) {
43874
+ const absoluteRoot = (0, import_node_path42.resolve)(root);
43875
+ const target = (0, import_node_path42.resolve)(root, declaredPath);
43876
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path42.sep}`)) {
43614
43877
  throw new Error(`${label} ${declaredPath} escapes the repository root`);
43615
43878
  }
43616
43879
  return target;
@@ -43618,7 +43881,7 @@ function repoPath(root, declaredPath, label) {
43618
43881
  function digestFiles(files) {
43619
43882
  const hash = (0, import_node_crypto12.createHash)("sha256");
43620
43883
  for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
43621
- const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs44.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs44.readFileSync)(file.absolute);
43884
+ const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs45.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs45.readFileSync)(file.absolute);
43622
43885
  hash.update(file.relative, "utf8");
43623
43886
  hash.update("\0");
43624
43887
  hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
@@ -43633,8 +43896,8 @@ function digestFiles(files) {
43633
43896
  function digestPackedFiles(packageRoot, packedFiles) {
43634
43897
  return digestFiles(packedFiles.map((path2) => {
43635
43898
  const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
43636
- if (!(0, import_node_fs44.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
43637
- return { absolute, relative: slash(path2), stat: (0, import_node_fs44.lstatSync)(absolute) };
43899
+ if (!(0, import_node_fs45.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
43900
+ return { absolute, relative: slash(path2), stat: (0, import_node_fs45.lstatSync)(absolute) };
43638
43901
  }));
43639
43902
  }
43640
43903
 
@@ -43718,13 +43981,13 @@ function renderDistDriftReceipt(receipt) {
43718
43981
  return lines2;
43719
43982
  }
43720
43983
  function readOrNull(path2) {
43721
- return (0, import_node_fs45.existsSync)(path2) ? (0, import_node_fs45.readFileSync)(path2) : null;
43984
+ return (0, import_node_fs46.existsSync)(path2) ? (0, import_node_fs46.readFileSync)(path2) : null;
43722
43985
  }
43723
43986
  function walkFiles(root) {
43724
43987
  const files = [];
43725
43988
  const walk2 = (directory) => {
43726
- for (const entry of (0, import_node_fs45.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
43727
- const child2 = (0, import_node_path42.join)(directory, entry.name);
43989
+ for (const entry of (0, import_node_fs46.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
43990
+ const child2 = (0, import_node_path43.join)(directory, entry.name);
43728
43991
  if (entry.isDirectory()) walk2(child2);
43729
43992
  else files.push(child2);
43730
43993
  }
@@ -43734,10 +43997,10 @@ function walkFiles(root) {
43734
43997
  }
43735
43998
  function bomPathFor(root) {
43736
43999
  try {
43737
- const registry2 = JSON.parse((0, import_node_fs45.readFileSync)((0, import_node_path42.join)(root, "surfaces.json"), "utf8"));
43738
- return (0, import_node_path42.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
44000
+ const registry2 = JSON.parse((0, import_node_fs46.readFileSync)((0, import_node_path43.join)(root, "surfaces.json"), "utf8"));
44001
+ return (0, import_node_path43.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
43739
44002
  } catch {
43740
- return (0, import_node_path42.join)(root, "distribution-bom.json");
44003
+ return (0, import_node_path43.join)(root, "distribution-bom.json");
43741
44004
  }
43742
44005
  }
43743
44006
  function rebuildTo(packageRoot, outDir) {
@@ -43750,35 +44013,35 @@ function rebuildTo(packageRoot, outDir) {
43750
44013
  });
43751
44014
  }
43752
44015
  function runDistStatus(root) {
43753
- const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
44016
+ const stage = (0, import_node_fs46.mkdtempSync)((0, import_node_path43.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
43754
44017
  let overlayCount = 0;
43755
44018
  try {
43756
- const cliOut = (0, import_node_path42.join)(stage, "cli-dist");
43757
- const hubOut = (0, import_node_path42.join)(stage, "hub-dist");
43758
- rebuildTo((0, import_node_path42.join)(root, "cli"), cliOut);
43759
- rebuildTo((0, import_node_path42.join)(root, "updater"), hubOut);
44019
+ const cliOut = (0, import_node_path43.join)(stage, "cli-dist");
44020
+ const hubOut = (0, import_node_path43.join)(stage, "hub-dist");
44021
+ rebuildTo((0, import_node_path43.join)(root, "cli"), cliOut);
44022
+ rebuildTo((0, import_node_path43.join)(root, "updater"), hubOut);
43760
44023
  const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
43761
44024
  const rebuilt = (path2) => {
43762
44025
  const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
43763
- return spec ? readOrNull((0, import_node_path42.join)(outDirFor(spec.packageDir), spec.output)) : null;
44026
+ return spec ? readOrNull((0, import_node_path43.join)(outDirFor(spec.packageDir), spec.output)) : null;
43764
44027
  };
43765
- const committed = (path2) => readOrNull((0, import_node_path42.join)(root, path2));
43766
- const tree = (path2) => readOrNull((0, import_node_path42.join)(root, path2));
43767
- const distRoot = (0, import_node_path42.join)(root, "cli", "dist");
43768
- const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path42.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
43769
- const bom = JSON.parse((0, import_node_fs45.readFileSync)(bomPathFor(root), "utf8"));
44028
+ const committed = (path2) => readOrNull((0, import_node_path43.join)(root, path2));
44029
+ const tree = (path2) => readOrNull((0, import_node_path43.join)(root, path2));
44030
+ const distRoot = (0, import_node_path43.join)(root, "cli", "dist");
44031
+ const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path43.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
44032
+ const bom = JSON.parse((0, import_node_fs46.readFileSync)(bomPathFor(root), "utf8"));
43770
44033
  const digest = (entries) => {
43771
- const overlay = (0, import_node_path42.join)(stage, `overlay-${overlayCount++}`);
44034
+ const overlay = (0, import_node_path43.join)(stage, `overlay-${overlayCount++}`);
43772
44035
  for (const entry of entries) {
43773
- const target = (0, import_node_path42.join)(overlay, entry.path);
43774
- (0, import_node_fs45.mkdirSync)((0, import_node_path42.dirname)(target), { recursive: true });
43775
- (0, import_node_fs45.writeFileSync)(target, entry.bytes);
44036
+ const target = (0, import_node_path43.join)(overlay, entry.path);
44037
+ (0, import_node_fs46.mkdirSync)((0, import_node_path43.dirname)(target), { recursive: true });
44038
+ (0, import_node_fs46.writeFileSync)(target, entry.bytes);
43776
44039
  }
43777
44040
  return digestPackedFiles(overlay, entries.map((entry) => entry.path));
43778
44041
  };
43779
44042
  return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
43780
44043
  } finally {
43781
- (0, import_node_fs45.rmSync)(stage, { recursive: true, force: true });
44044
+ (0, import_node_fs46.rmSync)(stage, { recursive: true, force: true });
43782
44045
  }
43783
44046
  }
43784
44047
 
@@ -43857,7 +44120,7 @@ init_hub_auth();
43857
44120
 
43858
44121
  // src/schedules-lift-command.ts
43859
44122
  var import_promises8 = require("node:fs/promises");
43860
- var import_node_path43 = require("node:path");
44123
+ var import_node_path44 = require("node:path");
43861
44124
  init_clean_exit();
43862
44125
  init_cli_shared();
43863
44126
  var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
@@ -43886,7 +44149,7 @@ async function readWorkflowFiles(dir) {
43886
44149
  const files = [];
43887
44150
  for (const name of names.sort()) {
43888
44151
  if (!/\.ya?ml$/.test(name)) continue;
43889
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path43.join)(dir, name), "utf8") });
44152
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path44.join)(dir, name), "utf8") });
43890
44153
  }
43891
44154
  return files;
43892
44155
  }
@@ -43969,8 +44232,8 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
43969
44232
 
43970
44233
  // src/spawn-policy-core.ts
43971
44234
  var import_node_child_process17 = require("node:child_process");
43972
- var import_node_fs46 = require("node:fs");
43973
- var import_node_path44 = require("node:path");
44235
+ var import_node_fs47 = require("node:fs");
44236
+ var import_node_path45 = require("node:path");
43974
44237
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
43975
44238
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
43976
44239
  var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
@@ -44056,7 +44319,7 @@ function runSpawnPolicy(root) {
44056
44319
  for (const file of files) {
44057
44320
  let raw;
44058
44321
  try {
44059
- raw = (0, import_node_fs46.readFileSync)((0, import_node_path44.join)(root, file), "utf8");
44322
+ raw = (0, import_node_fs47.readFileSync)((0, import_node_path45.join)(root, file), "utf8");
44060
44323
  } catch {
44061
44324
  continue;
44062
44325
  }
@@ -44076,19 +44339,19 @@ function runSpawnPolicy(root) {
44076
44339
  function registerDeveloperCommands(program3) {
44077
44340
  const rules = program3.command("rules").description("org-managed .gitignore delivery");
44078
44341
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
44079
- const path2 = (0, import_node_path45.join)(process.cwd(), ".gitignore");
44080
- const current = (0, import_node_fs47.existsSync)(path2) ? (0, import_node_fs47.readFileSync)(path2, "utf8") : null;
44342
+ const path2 = (0, import_node_path46.join)(process.cwd(), ".gitignore");
44343
+ const current = (0, import_node_fs48.existsSync)(path2) ? (0, import_node_fs48.readFileSync)(path2, "utf8") : null;
44081
44344
  const plan = planManagedGitignore(current);
44082
44345
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
44083
44346
  if (opts.json) {
44084
- if (opts.write && plan.changed) (0, import_node_fs47.writeFileSync)(path2, plan.content, "utf8");
44347
+ if (opts.write && plan.changed) (0, import_node_fs48.writeFileSync)(path2, plan.content, "utf8");
44085
44348
  console.log(JSON.stringify(plan, null, 2));
44086
44349
  if (!opts.write && plan.changed) process.exitCode = 1;
44087
44350
  return;
44088
44351
  }
44089
44352
  if (opts.write) {
44090
44353
  if (plan.changed) {
44091
- (0, import_node_fs47.writeFileSync)(path2, plan.content, "utf8");
44354
+ (0, import_node_fs48.writeFileSync)(path2, plan.content, "utf8");
44092
44355
  console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
44093
44356
  } else {
44094
44357
  console.log("mmi-cli devops org rules gitignore: up to date");
@@ -44354,6 +44617,15 @@ function neutralizeHubProductBrands(text) {
44354
44617
  s = s.replace(/\bdesign-system\s+design-system\b/gi, "design-system");
44355
44618
  return s.replace(/\s{2,}/g, " ").trim();
44356
44619
  }
44620
+ var JERV_SIDE = /\bjerv/i;
44621
+ function namesJervSide(text) {
44622
+ return JERV_SIDE.test(text);
44623
+ }
44624
+ function hubSummaryJervRefusal(lines2) {
44625
+ const bad = lines2.filter(namesJervSide);
44626
+ if (bad.length === 0) return null;
44627
+ return `--announce-summary-file names a Jerv-side project in ${bad.length} line(s) \u2014 an MMI-Hub release note describes MMI-Hub only and never mentions Jerv-side projects; drop or rewrite those lines, then rerun the same command`;
44628
+ }
44357
44629
  function escapeMrkdwn(text) {
44358
44630
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
44359
44631
  }
@@ -44465,6 +44737,7 @@ async function announceRelease(deps, args) {
44465
44737
  }
44466
44738
  lines2 = curateReleaseNotes(view.body ?? "", MAX_BULLETS, neutralize);
44467
44739
  }
44740
+ if (neutralize) lines2 = lines2.filter((l) => !namesJervSide(l));
44468
44741
  if (lines2.length === 0) lines2 = ["\u2022 maintenance release \u2014 see the notes for details"];
44469
44742
  const text = formatAnnouncement({
44470
44743
  repo: args.repo,
@@ -45224,6 +45497,8 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
45224
45497
  if (lines2.length < 3 || lines2.length > 6) {
45225
45498
  throw new Error(`--announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
45226
45499
  }
45500
+ const jerv = hubSummaryJervRefusal(lines2);
45501
+ if (jerv) throw new Error(jerv);
45227
45502
  let rangeLog;
45228
45503
  try {
45229
45504
  rangeLog = await deps.run("git", ["log", "--format=%B", "origin/main..HEAD"]);
@@ -45307,6 +45582,10 @@ ${decision.note}`);
45307
45582
  deployNote = dispatch.note;
45308
45583
  runs.push(hotfixWorkflowRunFromDispatch("tenant-deploy.yml", deploy));
45309
45584
  if (publish) runs.push(hotfixWorkflowRunFromDispatch("publish.yml", publish));
45585
+ } else if (deployModel === "vercel") {
45586
+ const vercel = await dispatchDeploy(deps, ctx, "main", "main", deployModel, true, (deps.now ?? Date.now)(), mergedSha, "report");
45587
+ runs.push(hotfixWorkflowRunFromDispatch("vercel", vercel));
45588
+ deployNote = vercel.note;
45310
45589
  } else {
45311
45590
  deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
45312
45591
  }
@@ -45816,8 +46095,8 @@ function checkHotfixCarries(options) {
45816
46095
  }
45817
46096
 
45818
46097
  // src/train-commands.ts
45819
- var import_node_fs48 = require("node:fs");
45820
- var import_node_path46 = require("node:path");
46098
+ var import_node_fs49 = require("node:fs");
46099
+ var import_node_path47 = require("node:path");
45821
46100
  init_cli_shared();
45822
46101
  init_clean_exit();
45823
46102
  init_client_version();
@@ -45832,7 +46111,7 @@ function resolveReleaseBumpIntent(raw) {
45832
46111
  }
45833
46112
  function readRepoVersion() {
45834
46113
  try {
45835
- return JSON.parse((0, import_node_fs48.readFileSync)((0, import_node_path46.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
46114
+ return JSON.parse((0, import_node_fs49.readFileSync)((0, import_node_path47.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
45836
46115
  } catch {
45837
46116
  return void 0;
45838
46117
  }
@@ -45934,8 +46213,8 @@ async function withReleaseProgress(deps, initial, action) {
45934
46213
  clearInterval(heartbeat);
45935
46214
  }
45936
46215
  }
45937
- function releaseProgressVerdict(status) {
45938
- return status === "complete" ? "Release complete" : status === "failed" ? "Release failed \u2014 a follow-up needs attention" : "Release pending \u2014 waiting for unfinished follow-ups";
46216
+ function releaseProgressVerdict(status, subject = "Release") {
46217
+ return status === "complete" ? `${subject} complete` : status === "failed" ? `${subject} failed \u2014 a follow-up needs attention` : `${subject} pending \u2014 waiting for unfinished follow-ups`;
45939
46218
  }
45940
46219
 
45941
46220
  // src/command-register-train-operations.ts
@@ -45976,7 +46255,7 @@ var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
45976
46255
  var NPM_TRAIN_TIMEOUT_MS = 6e4;
45977
46256
  var NPM_FOLD_TIMEOUT_MS = NODE_PREPARE_TIMEOUT_MS;
45978
46257
  var JERV_GATEWAY_DEPLOY_TIMEOUT_MS = 20 * 6e4;
45979
- async function announceSummaryRefusal(path2) {
46258
+ async function announceSummaryRefusal(path2, isHub = false) {
45980
46259
  let lines2;
45981
46260
  try {
45982
46261
  lines2 = summaryFileLines(await (0, import_promises10.readFile)(path2, "utf8"));
@@ -45986,6 +46265,10 @@ async function announceSummaryRefusal(path2) {
45986
46265
  if (lines2.length < 3 || lines2.length > 6) {
45987
46266
  return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
45988
46267
  }
46268
+ if (isHub) {
46269
+ const jerv = hubSummaryJervRefusal(lines2);
46270
+ if (jerv) return `release: ${jerv}`;
46271
+ }
45989
46272
  try {
45990
46273
  await execFileP("git", ["fetch", "origin"], { timeout: GIT_TIMEOUT_MS });
45991
46274
  } catch (e) {
@@ -46168,8 +46451,31 @@ function renderReleasePublishRetry(r) {
46168
46451
  return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
46169
46452
  }
46170
46453
  function renderRcandResume(r) {
46171
- const base = `mmi-cli devops rcand --resume: promoted ${r.repo} \u2014 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
46172
- return r.ledger ? [base, ...formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`)].join("\n") : base;
46454
+ const verb = r.command === "rcand-retry-deploy" ? "retry-deploy" : "resume";
46455
+ const base = `mmi-cli devops rcand --${verb}: promoted ${r.repo} \u2014 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
46456
+ return appendVerdictAndLedger(base, r.releaseVerdict, r.ledger);
46457
+ }
46458
+ function progressSubject(commandName) {
46459
+ return commandName === "rcand" ? "Release candidate" : "Release";
46460
+ }
46461
+ function emitRcandContinuation(label, raw, o) {
46462
+ const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
46463
+ const { followUpStatus, releaseVerdict } = buildReleaseVerdict("rcand", result, void 0);
46464
+ const reported = { ...result, releaseVerdict };
46465
+ emitTrainResult(label, o.json ? JSON.stringify(reported, null, 2) : renderRcandResume(reported), o.out);
46466
+ consoleIo.err(`mmi-cli: Release: ${releaseProgressVerdict(followUpStatus, progressSubject("rcand"))}`);
46467
+ applyTrainFollowUpExit(followUpStatus);
46468
+ }
46469
+ function appendVerdictAndLedger(base, v, ledger) {
46470
+ const lines2 = [base];
46471
+ if (v) {
46472
+ const verdict = v.followUpStatus === "failed" ? "PROMOTED, FOLLOW-UP FAILED" : v.followUpStatus === "pending" ? "PROMOTED, FOLLOW-UP PENDING" : "SUCCEEDED";
46473
+ const followUpNote = trainFollowUpNote(v.followUpStatus);
46474
+ lines2[0] = `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}${followUpNote ? ` (${followUpNote})` : ""}`;
46475
+ lines2.push(...v.legs.filter((leg) => leg.status === "failed").map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`));
46476
+ }
46477
+ if (ledger) lines2.push(...formatReleaseLedgerReport(ledger).map((line) => ` ${line}`));
46478
+ return lines2.join("\n");
46173
46479
  }
46174
46480
  function renderAlignment(label, alignment) {
46175
46481
  if (alignment.status !== "pr-pending") return `${label}: ${alignment.note}`;
@@ -46277,16 +46583,17 @@ function buildReleaseVerdict(commandName, result, projectInfoSync) {
46277
46583
  alignment: alignmentPending ? "unresolved" : "ok",
46278
46584
  foldPort: "ok"
46279
46585
  });
46586
+ const legs = releaseFollowUpLegs(result, projectInfoSync);
46280
46587
  return {
46281
46588
  followUpStatus,
46282
- releaseVerdict: commandName === "release" ? {
46589
+ releaseVerdict: {
46283
46590
  releaseStatus: "succeeded",
46284
46591
  followUpStatus,
46285
46592
  promoted: result.promoted,
46286
46593
  tag: result.tag,
46287
46594
  releaseUrl: result.release?.url ?? null,
46288
- legs: releaseFollowUpLegs(result, projectInfoSync)
46289
- } : void 0
46595
+ legs: commandName === "rcand" ? legs.filter((leg) => leg.leg !== "project-info") : legs
46596
+ }
46290
46597
  };
46291
46598
  }
46292
46599
  function buildResumeReleaseVerdict(result, projectInfoSync) {
@@ -46339,22 +46646,7 @@ function renderTrainApply(commandName, r) {
46339
46646
  base = `${base}; project info: ${r.projectInfoSync.note}`;
46340
46647
  }
46341
46648
  if (r.announceNote) base = `${base}; announce: ${r.announceNote}`;
46342
- if (r.releaseVerdict) {
46343
- const v = r.releaseVerdict;
46344
- const verdict = v.followUpStatus === "failed" ? "PROMOTED, FOLLOW-UP FAILED" : v.followUpStatus === "pending" ? "PROMOTED, FOLLOW-UP PENDING" : "SUCCEEDED";
46345
- const failedLegs = v.legs.filter((leg) => leg.status === "failed");
46346
- const followUpNote = trainFollowUpNote(v.followUpStatus);
46347
- return [
46348
- `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}${followUpNote ? ` (${followUpNote})` : ""}`,
46349
- ...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`),
46350
- // #5987: the durable ledger's phase legs + derived typed verdict ride the same receipt.
46351
- ...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
46352
- ].join("\n");
46353
- }
46354
- if (r.ledger) {
46355
- return [base, ...formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`)].join("\n");
46356
- }
46357
- return base;
46649
+ return appendVerdictAndLedger(base, r.releaseVerdict, r.ledger);
46358
46650
  }
46359
46651
  function applyTrainFollowUpExit(status) {
46360
46652
  const code = trainFollowUpExitCode(status);
@@ -46404,6 +46696,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46404
46696
  trainCmd.addOption(new Option(f.flags, f.description).hideHelp());
46405
46697
  }
46406
46698
  }
46699
+ const retryDeploy = new Option("--retry-deploy", "re-dispatch the FAILED rc deploy of the promoted candidate in the active rcand ledger \u2014 same tag and SHA, never re-cut or re-pushed (#7053)");
46700
+ trainCmd.addOption(commandName === "rcand" ? retryDeploy : retryDeploy.hideHelp());
46407
46701
  trainCmd.option("--repo <owner/repo>", "dry-run plan for a target repo without relying on the current checkout; --apply still uses the current checkout").action(async (o) => {
46408
46702
  try {
46409
46703
  await requireFreshTrainCli(commandName);
@@ -46431,8 +46725,30 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46431
46725
  if (o.retryPublish && commandName !== "release") {
46432
46726
  return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
46433
46727
  }
46434
- if (o.out && !o.apply && !o.resume) {
46435
- return fail(`${commandName}: --out writes a terminal result receipt (--apply, --resume, --abort, --retry-publish) \u2014 the dry-run plan keeps its stdout/JSON output. Drop --out or rerun with --apply`);
46728
+ if (o.retryDeploy && commandName !== "rcand") {
46729
+ return fail(`${commandName}: --retry-deploy applies only to rcand \u2014 it re-dispatches a failed rc deploy of the promoted candidate. Run: mmi-cli devops rcand --retry-deploy --watch (a failed hotfix deploy: mmi-cli devops hotfix release <tag> --retry-deploy)`);
46730
+ }
46731
+ if (o.out && !o.apply && !o.resume && !o.retryDeploy) {
46732
+ return fail(`${commandName}: --out writes a terminal result receipt (--apply, --resume, --retry-deploy, --abort, --retry-publish) \u2014 the dry-run plan keeps its stdout/JSON output. Drop --out or rerun with --apply`);
46733
+ }
46734
+ if (o.retryDeploy) {
46735
+ if (o.apply || o.resume) return fail("rcand: --retry-deploy cannot be combined with --apply or --resume \u2014 it re-dispatches only the failed rc deploy of the already-promoted candidate");
46736
+ if (o.repo) {
46737
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops rcand --retry-deploy${o.watch ? " --watch" : ""}`);
46738
+ if (!guard.ok) return fail(`rcand: ${guard.message}`);
46739
+ }
46740
+ try {
46741
+ const raw = await withReleaseProgress(
46742
+ trainApplyDeps(),
46743
+ "Checking the failed release candidate deploy",
46744
+ (deps) => runRcandRetryDeploy(deps, { watch: o.watch })
46745
+ );
46746
+ return emitRcandContinuation("rcand --retry-deploy", raw, o);
46747
+ } catch (e) {
46748
+ applyTrainFollowUpExit("failed");
46749
+ consoleIo.err("mmi-cli: Release: Release candidate blocked \u2014 see the error below");
46750
+ return failGraceful(`rcand --retry-deploy: ${e.message}`);
46751
+ }
46436
46752
  }
46437
46753
  if (o.supersedeDeploy && o.supersedePublish) {
46438
46754
  if (o.resume) return fail("release: --supersede-deploy --supersede-publish and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very legs it closes");
@@ -46536,17 +46852,12 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46536
46852
  if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
46537
46853
  try {
46538
46854
  if (commandName === "rcand") {
46539
- const raw2 = await runRcandResume(trainApplyDeps(), { watch: o.watch });
46540
- const result2 = raw2.workflowRuns ? { ...raw2, workflowRuns: raw2.workflowRuns.map(workflowRunWithEvidence) } : raw2;
46541
- emitTrainResult("rcand --resume", o.json ? JSON.stringify(result2, null, 2) : renderRcandResume(result2), o.out);
46542
- applyTrainFollowUpExit(deriveTrainFollowUpStatus({
46543
- projectInfo: "ok",
46544
- deploy: deployFollowUpOutcome(result2.deployStatus),
46545
- rcRetirement: "ok",
46546
- alignment: "ok",
46547
- foldPort: "ok"
46548
- }));
46549
- return;
46855
+ const raw2 = await withReleaseProgress(
46856
+ trainApplyDeps(),
46857
+ "Checking the existing release candidate",
46858
+ (deps) => runRcandResume(deps, { watch: o.watch })
46859
+ );
46860
+ return emitRcandContinuation("rcand --resume", raw2, o);
46550
46861
  }
46551
46862
  const raw = await withReleaseProgress(
46552
46863
  trainApplyDeps(),
@@ -46567,7 +46878,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46567
46878
  return;
46568
46879
  } catch (e) {
46569
46880
  applyTrainFollowUpExit("failed");
46570
- if (commandName === "release") consoleIo.err("mmi-cli: Release: Release blocked \u2014 see the error below");
46881
+ consoleIo.err(`mmi-cli: Release: ${progressSubject(commandName)} blocked \u2014 see the error below`);
46571
46882
  return failGraceful(`${commandName} --resume: ${e.message}`);
46572
46883
  }
46573
46884
  }
@@ -46603,7 +46914,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46603
46914
  isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`
46604
46915
  );
46605
46916
  }
46606
- const refusal = await announceSummaryRefusal(o.announceSummaryFile);
46917
+ const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub);
46607
46918
  if (refusal) return fail(refusal);
46608
46919
  }
46609
46920
  }
@@ -46611,7 +46922,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46611
46922
  try {
46612
46923
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
46613
46924
  const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
46614
- const raw = commandName === "release" ? await withReleaseProgress(trainApplyDeps(), "Preparing release", apply) : await apply(trainApplyDeps());
46925
+ const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
46615
46926
  const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
46616
46927
  let projectInfoSync;
46617
46928
  if (commandName === "release") {
@@ -46629,14 +46940,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46629
46940
  };
46630
46941
  const output = o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported);
46631
46942
  emitTrainResult(`${commandName} apply`, output, o.out);
46632
- if (commandName === "release") consoleIo.err(`mmi-cli: Release: ${releaseProgressVerdict(followUpStatus)}`);
46943
+ consoleIo.err(`mmi-cli: Release: ${releaseProgressVerdict(followUpStatus, progressSubject(commandName))}`);
46633
46944
  applyTrainFollowUpExit(followUpStatus);
46634
46945
  return;
46635
46946
  } catch (e) {
46636
46947
  const error = e;
46637
46948
  writeTrainFailureReceipt(commandName, o.json === true, o.out, error);
46638
46949
  process.exitCode = 1;
46639
- if (commandName === "release") consoleIo.err("mmi-cli: Release: Release blocked \u2014 see the error below");
46950
+ consoleIo.err(`mmi-cli: Release: ${progressSubject(commandName)} blocked \u2014 see the error below`);
46640
46951
  return await failGraceful(`${commandName}: ${error.message}`);
46641
46952
  }
46642
46953
  }
@@ -46816,7 +47127,7 @@ ${r.stderr ?? ""}`).catch(() => "");
46816
47127
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
46817
47128
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
46818
47129
  function envHealLockPath(home) {
46819
- return (0, import_node_path47.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
47130
+ return (0, import_node_path48.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
46820
47131
  }
46821
47132
  async function withEnvHealLock(what, run) {
46822
47133
  try {
@@ -46953,7 +47264,7 @@ function mmiDoctorDeps(opts = {}) {
46953
47264
  );
46954
47265
  const result = applyPluginCachePlan(
46955
47266
  plan,
46956
- (p) => (0, import_node_fs49.rmSync)(p, { recursive: true }),
47267
+ (p) => (0, import_node_fs50.rmSync)(p, { recursive: true }),
46957
47268
  stagingApplyFsGuard(configRoot)
46958
47269
  );
46959
47270
  return {
@@ -46988,7 +47299,7 @@ function mmiDoctorDeps(opts = {}) {
46988
47299
  // has no generated routing index would
46989
47300
  // get a permanent — demanding an artifact it never asked for.
46990
47301
  docsIndexState: (root) => {
46991
- if (!(0, import_node_fs49.existsSync)((0, import_node_path47.join)(root, DOCS_INDEX_PATH))) return void 0;
47302
+ if (!(0, import_node_fs50.existsSync)((0, import_node_path48.join)(root, DOCS_INDEX_PATH))) return void 0;
46992
47303
  const real = createDocsIndexDeps(root);
46993
47304
  let docs;
46994
47305
  const listDocs = () => docs ??= real.listDocs();
@@ -46997,7 +47308,7 @@ function mmiDoctorDeps(opts = {}) {
46997
47308
  },
46998
47309
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
46999
47310
  healDocsIndex: (root) => {
47000
- if (!(0, import_node_fs49.existsSync)((0, import_node_path47.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
47311
+ if (!(0, import_node_fs50.existsSync)((0, import_node_path48.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
47001
47312
  const real = createDocsIndexDeps(root);
47002
47313
  let docs;
47003
47314
  const listDocs = () => docs ??= real.listDocs();
@@ -47623,7 +47934,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
47623
47934
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
47624
47935
  if (o.secretsFile) {
47625
47936
  try {
47626
- vars.push(`secrets=${(0, import_node_fs49.readFileSync)(o.secretsFile, "utf8")}`);
47937
+ vars.push(`secrets=${(0, import_node_fs50.readFileSync)(o.secretsFile, "utf8")}`);
47627
47938
  } catch (e) {
47628
47939
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
47629
47940
  }
@@ -47956,16 +48267,16 @@ function ciAuditDeps2() {
47956
48267
  // gate re-seed step is skipped gracefully rather than failing mid-run.
47957
48268
  readSeedFile: (path2) => {
47958
48269
  if (!root) return null;
47959
- const fullPath = (0, import_node_path47.join)(root, path2);
47960
- return (0, import_node_fs49.existsSync)(fullPath) ? (0, import_node_fs49.readFileSync)(fullPath, "utf8") : null;
48270
+ const fullPath = (0, import_node_path48.join)(root, path2);
48271
+ return (0, import_node_fs50.existsSync)(fullPath) ? (0, import_node_fs50.readFileSync)(fullPath, "utf8") : null;
47961
48272
  }
47962
48273
  };
47963
48274
  }
47964
48275
  function hubRoot2() {
47965
- const fromPkg = (0, import_node_path47.join)(__dirname, "..", "..");
48276
+ const fromPkg = (0, import_node_path48.join)(__dirname, "..", "..");
47966
48277
  const marker = "skills/bootstrap/seeds/manifest.json";
47967
- if ((0, import_node_fs49.existsSync)((0, import_node_path47.join)(fromPkg, marker))) return fromPkg;
47968
- if ((0, import_node_fs49.existsSync)((0, import_node_path47.join)(process.cwd(), marker))) return process.cwd();
48278
+ if ((0, import_node_fs50.existsSync)((0, import_node_path48.join)(fromPkg, marker))) return fromPkg;
48279
+ if ((0, import_node_fs50.existsSync)((0, import_node_path48.join)(process.cwd(), marker))) return process.cwd();
47969
48280
  return null;
47970
48281
  }
47971
48282
  registerQueryCommands(program2);
@@ -48065,10 +48376,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
48065
48376
  targets = resolution.targets;
48066
48377
  }
48067
48378
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
48068
- const fileMatrix = (0, import_node_fs49.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs49.readFileSync)("access-matrix.json", "utf8")) : {};
48379
+ const fileMatrix = (0, import_node_fs50.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs50.readFileSync)("access-matrix.json", "utf8")) : {};
48069
48380
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
48070
48381
  const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
48071
- const sanctioned = (0, import_node_fs49.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs49.readFileSync)("access-matrix.json", "utf8")) : {};
48382
+ const sanctioned = (0, import_node_fs50.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs50.readFileSync)("access-matrix.json", "utf8")) : {};
48072
48383
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
48073
48384
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
48074
48385
  if (!report.ok) process.exitCode = 1;
@@ -48099,16 +48410,16 @@ function directoryBytes(path2) {
48099
48410
  let total = 0;
48100
48411
  let entries;
48101
48412
  try {
48102
- entries = (0, import_node_fs49.readdirSync)(path2, { withFileTypes: true });
48413
+ entries = (0, import_node_fs50.readdirSync)(path2, { withFileTypes: true });
48103
48414
  } catch {
48104
48415
  return 0;
48105
48416
  }
48106
48417
  for (const entry of entries) {
48107
- const child2 = (0, import_node_path47.join)(path2, entry.name);
48418
+ const child2 = (0, import_node_path48.join)(path2, entry.name);
48108
48419
  if (entry.isDirectory()) total += directoryBytes(child2);
48109
48420
  else {
48110
48421
  try {
48111
- total += (0, import_node_fs49.statSync)(child2).size;
48422
+ total += (0, import_node_fs50.statSync)(child2).size;
48112
48423
  } catch {
48113
48424
  }
48114
48425
  }
@@ -48116,25 +48427,25 @@ function directoryBytes(path2) {
48116
48427
  return total;
48117
48428
  }
48118
48429
  function listDirEntries(dir) {
48119
- return (0, import_node_fs49.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
48430
+ return (0, import_node_fs50.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
48120
48431
  }
48121
48432
  function readInstalledPluginRefs(configRoot) {
48122
48433
  const p = installedPluginsPathForConfig(configRoot);
48123
- if (!(0, import_node_fs49.existsSync)(p)) return [];
48434
+ if (!(0, import_node_fs50.existsSync)(p)) return [];
48124
48435
  try {
48125
- return installedPluginPaths((0, import_node_fs49.readFileSync)(p, "utf8"));
48436
+ return installedPluginPaths((0, import_node_fs50.readFileSync)(p, "utf8"));
48126
48437
  } catch {
48127
48438
  return null;
48128
48439
  }
48129
48440
  }
48130
48441
  function pluginCacheFsDeps(configRoot, dirBytes) {
48131
48442
  return {
48132
- exists: (p) => (0, import_node_fs49.existsSync)(p),
48133
- listVersionDirs: (root) => (0, import_node_fs49.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
48443
+ exists: (p) => (0, import_node_fs50.existsSync)(p),
48444
+ listVersionDirs: (root) => (0, import_node_fs50.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
48134
48445
  dirBytes,
48135
- listStagingDirs: (root) => (0, import_node_fs49.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
48446
+ listStagingDirs: (root) => (0, import_node_fs50.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
48136
48447
  try {
48137
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path47.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs49.statSync)(p).mtimeMs) };
48448
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path48.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs50.statSync)(p).mtimeMs) };
48138
48449
  } catch {
48139
48450
  return { name: d.name, mtimeMs: Date.now() };
48140
48451
  }
@@ -48148,10 +48459,10 @@ function stagingApplyFsGuard(configRoot) {
48148
48459
  return {
48149
48460
  referencedPaths: () => readInstalledPluginRefs(configRoot),
48150
48461
  mtimeMs: (name) => {
48151
- const p = (0, import_node_path47.join)(stagingRoot, name);
48152
- if (!(0, import_node_fs49.existsSync)(p)) return null;
48462
+ const p = (0, import_node_path48.join)(stagingRoot, name);
48463
+ if (!(0, import_node_fs50.existsSync)(p)) return null;
48153
48464
  try {
48154
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs49.statSync)(q).mtimeMs);
48465
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs50.statSync)(q).mtimeMs);
48155
48466
  } catch {
48156
48467
  return null;
48157
48468
  }
@@ -48177,7 +48488,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
48177
48488
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
48178
48489
  );
48179
48490
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
48180
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs49.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
48491
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs50.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
48181
48492
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
48182
48493
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
48183
48494
  else console.log(renderPluginCachePlan(plan, result));