@mutmutco/cli 4.5.124 → 4.5.126

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 +323 -209
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -76,19 +76,19 @@ function hardExit(code) {
76
76
  process.exit(code);
77
77
  }
78
78
  function flushStream(stream) {
79
- return new Promise((resolve7) => {
79
+ return new Promise((resolve8) => {
80
80
  try {
81
- stream.write("", () => resolve7());
81
+ stream.write("", () => resolve8());
82
82
  } catch {
83
- resolve7();
83
+ resolve8();
84
84
  }
85
85
  });
86
86
  }
87
87
  async function flushStdio(timeoutMs = STDIO_FLUSH_TIMEOUT_MS) {
88
88
  await Promise.race([
89
89
  Promise.all([flushStream(process.stdout), flushStream(process.stderr)]),
90
- new Promise((resolve7) => {
91
- setTimeout(resolve7, timeoutMs).unref?.();
90
+ new Promise((resolve8) => {
91
+ setTimeout(resolve8, timeoutMs).unref?.();
92
92
  })
93
93
  ]);
94
94
  }
@@ -96,7 +96,7 @@ async function cleanExit(code) {
96
96
  process.exitCode = code;
97
97
  await closeHttpPool();
98
98
  await flushStdio();
99
- await new Promise((resolve7) => setImmediate(resolve7));
99
+ await new Promise((resolve8) => setImmediate(resolve8));
100
100
  return void 0;
101
101
  }
102
102
  async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
@@ -644,7 +644,7 @@ function rateLimitedReceipt(opts) {
644
644
  };
645
645
  }
646
646
  async function runWithRateLimitBackoff(operation, opts) {
647
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
647
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
648
648
  const now = opts.now ?? Date.now;
649
649
  const log = opts.log ?? ((message2) => console.warn(message2));
650
650
  const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
@@ -734,7 +734,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
734
734
  const attempts = opts.attempts ?? 3;
735
735
  const baseDelayMs = opts.baseDelayMs ?? 250;
736
736
  const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
737
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
737
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
738
738
  let lastErr;
739
739
  for (let i = 0; i < attempts; i++) {
740
740
  const isLast = i === attempts - 1;
@@ -1057,8 +1057,8 @@ async function readStdin(opts = {}) {
1057
1057
  })().catch(() => {
1058
1058
  });
1059
1059
  let timer;
1060
- const timeout = new Promise((resolve7) => {
1061
- timer = setTimeout(resolve7, timeoutMs);
1060
+ const timeout = new Promise((resolve8) => {
1061
+ timer = setTimeout(resolve8, timeoutMs);
1062
1062
  });
1063
1063
  try {
1064
1064
  await Promise.race([drain, timeout]);
@@ -1125,12 +1125,12 @@ function killProcessTree(pid) {
1125
1125
  function execFileHard(file, args, options) {
1126
1126
  const { timeout, step, ...rest } = options;
1127
1127
  const started = Date.now();
1128
- return new Promise((resolve7, reject) => {
1128
+ return new Promise((resolve8, reject) => {
1129
1129
  const child2 = (0, import_node_child_process2.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...rest, timeout: 0 }, (error, stdout, stderr) => {
1130
1130
  clearTimeout(timer);
1131
1131
  if (expired) return;
1132
1132
  if (error) reject(error);
1133
- else resolve7({ stdout: String(stdout), stderr: String(stderr) });
1133
+ else resolve8({ stdout: String(stdout), stderr: String(stderr) });
1134
1134
  });
1135
1135
  let expired = false;
1136
1136
  const timer = setTimeout(() => {
@@ -5656,7 +5656,7 @@ function resolveVerbAliasShim(argv) {
5656
5656
 
5657
5657
  // src/command-composition.ts
5658
5658
  var import_node_os25 = require("node:os");
5659
- var import_node_path55 = require("node:path");
5659
+ var import_node_path56 = require("node:path");
5660
5660
 
5661
5661
  // src/board-read.ts
5662
5662
  var import_node_fs14 = require("node:fs");
@@ -6735,7 +6735,7 @@ async function readRestPrSnapshotWithRetry(prNumber, repo, gh2 = defaultGhApi, o
6735
6735
  const retries = options?.retries ?? PR_SNAPSHOT_READ_RETRIES;
6736
6736
  const untilMs = options?.retryTransientUntilMs;
6737
6737
  const delayMs = options?.delayMs ?? (untilMs !== void 0 ? PR_CHECKS_POLL_MS : PR_SNAPSHOT_READ_DELAY_MS);
6738
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
6738
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
6739
6739
  const now = options?.now ?? (() => Date.now());
6740
6740
  let lastError = "no attempt completed";
6741
6741
  let attempt = 0;
@@ -8766,8 +8766,9 @@ async function retireProject(slug, deps) {
8766
8766
  async function setDeployCoords(slug, payload, deps) {
8767
8767
  return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
8768
8768
  }
8769
- async function retireDeployCoords(slug, stage, deps) {
8770
- return postJson(`/projects/${encodeURIComponent(slug)}/deploy/${encodeURIComponent(stage)}`, {}, deps, "DELETE", { noRetry: true });
8769
+ async function retireDeployCoords(slug, stage, deps, decommissioned = false) {
8770
+ const q = decommissioned ? "?decommissioned=1" : "";
8771
+ return postJson(`/projects/${encodeURIComponent(slug)}/deploy/${encodeURIComponent(stage)}${q}`, {}, deps, "DELETE", { noRetry: true });
8771
8772
  }
8772
8773
  async function postSchedulesLift(repo, schedules, deps) {
8773
8774
  const res = await postJson("/schedules/lift", { repo, schedules }, deps);
@@ -10588,7 +10589,7 @@ async function createPrViaRestFallback(args, swappedArgs, deps, knownPools) {
10588
10589
  }
10589
10590
  async function ghCreate(args, deps = {}) {
10590
10591
  const exec3 = deps.exec ?? execFileP;
10591
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
10592
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
10592
10593
  const now = deps.now ?? Date.now;
10593
10594
  const read = deps.readFile ?? import_promises.readFile;
10594
10595
  const restCreatePr = deps.restCreatePr ?? ((input) => defaultRestCreatePr(exec3, input));
@@ -11416,8 +11417,8 @@ var PRE_SPAWN_DRAIN_TIMEOUT_MS = 2e3;
11416
11417
  async function drainHttpPoolBeforeSpawn() {
11417
11418
  const drained = await Promise.race([
11418
11419
  closeHttpPool().then(() => true),
11419
- new Promise((resolve7) => {
11420
- setTimeout(() => resolve7(false), PRE_SPAWN_DRAIN_TIMEOUT_MS).unref?.();
11420
+ new Promise((resolve8) => {
11421
+ setTimeout(() => resolve8(false), PRE_SPAWN_DRAIN_TIMEOUT_MS).unref?.();
11421
11422
  })
11422
11423
  ]);
11423
11424
  if (!drained) destroyHttpPool();
@@ -15004,7 +15005,7 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
15004
15005
  await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
15005
15006
  return cliPriorityToFieldName(priority);
15006
15007
  }
15007
- var defaultRetrySleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
15008
+ var defaultRetrySleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
15008
15009
  async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
15009
15010
  const attempts = Math.max(1, opts.attempts ?? 5);
15010
15011
  const delayMs = opts.delayMs ?? 300;
@@ -18243,10 +18244,10 @@ var rollout_plan_default = {
18243
18244
  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)."
18244
18245
  },
18245
18246
  baseline: {
18246
- version: "4.5.124",
18247
- tag: "v4.5.124",
18248
- commit: "255e8e974e8e",
18249
- npm: "@mutmutco/cli@4.5.124"
18247
+ version: "4.5.126",
18248
+ tag: "v4.5.126",
18249
+ commit: "bf7736630f4c",
18250
+ npm: "@mutmutco/cli@4.5.126"
18250
18251
  },
18251
18252
  exitCriterion: "fleet-n-of-n",
18252
18253
  hubOnlyShortcut: "forbidden",
@@ -18263,14 +18264,14 @@ var rollout_plan_default = {
18263
18264
  repo: "mutmutco/mmi-hub",
18264
18265
  role: "canary",
18265
18266
  schedule: "train",
18266
- v3Target: "v4.5.124"
18267
+ v3Target: "v4.5.126"
18267
18268
  }
18268
18269
  ],
18269
18270
  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.",
18270
18271
  rollback: {
18271
18272
  independent: true,
18272
- mechanism: "npm dist-tag latest -> 4.5.124 and redeploy the Hub Lambda from tag v4.5.124 (255e8e974e8e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18273
- v3Target: "v4.5.124 (@mutmutco/cli@4.5.124, tag commit 255e8e974e8e \u2014 last known-good release carrying the repo-index v4-only contract)"
18273
+ mechanism: "npm dist-tag latest -> 4.5.126 and redeploy the Hub Lambda from tag v4.5.126 (bf7736630f4c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18274
+ v3Target: "v4.5.126 (@mutmutco/cli@4.5.126, tag commit bf7736630f4c \u2014 last known-good release carrying the repo-index v4-only contract)"
18274
18275
  }
18275
18276
  },
18276
18277
  {
@@ -20353,7 +20354,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
20353
20354
  async function predictMergeConflicts(deps, ours, theirs) {
20354
20355
  return runMergeTreePreflight(deps, ours, theirs);
20355
20356
  }
20356
- async function mergeWithToleratedResolution(deps, sourceRef, label, resolve7, extraTolerated = []) {
20357
+ async function mergeWithToleratedResolution(deps, sourceRef, label, resolve8, extraTolerated = []) {
20357
20358
  try {
20358
20359
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
20359
20360
  return;
@@ -20367,7 +20368,7 @@ async function mergeWithToleratedResolution(deps, sourceRef, label, resolve7, ex
20367
20368
  unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on untolerated path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
20368
20369
  );
20369
20370
  }
20370
- await deps.run("git", ["checkout", `--${resolve7}`, "--", ...unmerged]);
20371
+ await deps.run("git", ["checkout", `--${resolve8}`, "--", ...unmerged]);
20371
20372
  await deps.run("git", ["add", "--", ...unmerged]);
20372
20373
  await deps.run("git", ["commit", "--no-edit"]);
20373
20374
  }
@@ -20547,7 +20548,7 @@ var CORRELATE_SKEW_SLACK_MS = 1e4;
20547
20548
  var CORRELATE_PAGE_LIMIT = 50;
20548
20549
  var RUN_CONFIRM_ATTEMPTS = 3;
20549
20550
  var RUN_CONFIRM_DELAY_MS = 1e3;
20550
- var defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
20551
+ var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
20551
20552
  function resolveSleep(deps) {
20552
20553
  return deps.sleep ?? defaultSleep;
20553
20554
  }
@@ -23726,7 +23727,7 @@ function lockTokenPid(token) {
23726
23727
  const m = /^(\d+)-/.exec(token);
23727
23728
  return m ? Number(m[1]) : void 0;
23728
23729
  }
23729
- var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
23730
+ var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
23730
23731
  var IMMEDIATE_RETRY_BUDGET = 3;
23731
23732
  var FileLockBusyError = class extends Error {
23732
23733
  lockPath;
@@ -24602,7 +24603,7 @@ async function defaultWatchRun(deps, runId, repo) {
24602
24603
  }
24603
24604
  let confirmError;
24604
24605
  for (let attempt = 0; attempt < RUN_CONFIRM_ATTEMPTS2; attempt++) {
24605
- if (attempt > 0) await new Promise((resolve7) => setTimeout(resolve7, RUN_CONFIRM_DELAY_MS2));
24606
+ if (attempt > 0) await new Promise((resolve8) => setTimeout(resolve8, RUN_CONFIRM_DELAY_MS2));
24606
24607
  try {
24607
24608
  const parsed = JSON.parse(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]));
24608
24609
  if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
@@ -25644,11 +25645,11 @@ var defaultDeps = {
25644
25645
  // #5813: the caller supplies that command line. `process.argv` is NOT it — the house shim splices
25645
25646
  // the house root out in place, so re-reading it here re-ran the removed flat alias (`release …`
25646
25647
  // instead of `devops release …`) and the converged child refused with exit 2.
25647
- reexec: (argv) => new Promise((resolve7) => {
25648
+ reexec: (argv) => new Promise((resolve8) => {
25648
25649
  const args = [...argv];
25649
25650
  const child2 = isWin2 ? (0, import_node_child_process8.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", windowsHide: true }) : (0, import_node_child_process8.spawn)("mmi-cli", args, { stdio: "inherit", windowsHide: true });
25650
- child2.on("exit", (code) => resolve7(code ?? 1));
25651
- child2.on("error", () => resolve7(1));
25651
+ child2.on("exit", (code) => resolve8(code ?? 1));
25652
+ child2.on("error", () => resolve8(1));
25652
25653
  }),
25653
25654
  // stderr only — stdout may be carrying --json output.
25654
25655
  warn: (line) => console.error(line)
@@ -25754,7 +25755,7 @@ var defaultDeps2 = {
25754
25755
  }
25755
25756
  },
25756
25757
  registryWhoami: probeRegistryWhoami,
25757
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
25758
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
25758
25759
  env: process.env
25759
25760
  };
25760
25761
  function message(e) {
@@ -37162,7 +37163,7 @@ function readSuiteCut(path2) {
37162
37163
  } catch {
37163
37164
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
37164
37165
  }
37165
- if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinsUnchanged !== void 0 && (record.pinsUnchanged !== true || record.pinPr !== void 0) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr && !record.pinsUnchanged) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0) || m.ruleset !== void 0 && (!["rc", "main"].includes(m.ruleset.after) || typeof m.ruleset.ok !== "boolean" || !Array.isArray(m.ruleset.lines))))
37166
+ if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinsUnchanged !== void 0 && (record.pinsUnchanged !== true || record.pinPr !== void 0) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr && !record.pinsUnchanged) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.carriedPublished !== void 0 && (m.carriedPublished !== true || !m.carried) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0) || m.ruleset !== void 0 && (!["rc", "main"].includes(m.ruleset.after) || typeof m.ruleset.ok !== "boolean" || !Array.isArray(m.ruleset.lines))))
37166
37167
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
37167
37168
  return record;
37168
37169
  }
@@ -37274,7 +37275,22 @@ async function pendingBackend(deps, m, dev) {
37274
37275
  const files = (await deps.changedPaths(m.repo, live, dev)).filter((f) => !NOT_BACKEND.test(f));
37275
37276
  return files.length ? `${files.length} backend file(s) changed since live ${version}, e.g. ${files[0]}` : "";
37276
37277
  }
37278
+ function existingCutDescription(record) {
37279
+ const tags = [...record.members.flatMap((m) => m.tag ? [m.tag] : []), ...record.leadRcTag ? [record.leadRcTag] : []];
37280
+ return `from ${record.leadDevelopment.slice(0, 12)}; tags: ${tags.join(", ") || "none"}; pin PR: ${record.pinPr ?? "none"}`;
37281
+ }
37277
37282
  async function suiteRcand(deps, opts) {
37283
+ const existing = readSuiteCut(deps.recordPath);
37284
+ try {
37285
+ return await suiteRcandRun(deps, opts);
37286
+ } catch (e) {
37287
+ if (existing && !opts.dryRun && opts.ownerGo && readSuiteCut(deps.recordPath))
37288
+ throw new Error(`${e.message}
37289
+ An existing cut record ${existingCutDescription(existing)} was resumed. mmi-cli devops suite abandon --owner-go retires it (subject to abandon safety checks).`);
37290
+ throw e;
37291
+ }
37292
+ }
37293
+ async function suiteRcandRun(deps, opts) {
37278
37294
  if (!opts.ownerGo && !opts.dryRun) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
37279
37295
  const members = suiteMembers(await deps.registry());
37280
37296
  const modules = trainModules(members);
@@ -37288,6 +37304,8 @@ async function suiteRcand(deps, opts) {
37288
37304
  const lead = await deps.head(SUITE_REPO, "development");
37289
37305
  plan.push(await kitCheck(deps, lead, active.map((m) => ({ id: m.id, url: m.bundles.dev })), "dev bundles"));
37290
37306
  if (opts.dryRun) {
37307
+ const existing = readSuiteCut(deps.recordPath);
37308
+ if (existing) plan.push(`existing cut record: ${deps.recordPath} (${existingCutDescription(existing)})`);
37291
37309
  plan.push(...await Promise.all(modules.map(async (m) => `${m.repo}: ${heldById.has(m.id) ? "HELD" : await dryRunStatus(deps, m)}`)));
37292
37310
  return { plan };
37293
37311
  }
@@ -37372,7 +37390,7 @@ async function suiteRcand(deps, opts) {
37372
37390
  }
37373
37391
  const refusals = [];
37374
37392
  for (const member of record.members) {
37375
- if (member.published) continue;
37393
+ if (member.published || member.carriedPublished) continue;
37376
37394
  try {
37377
37395
  await deps.preflight(member.repo);
37378
37396
  } catch (e) {
@@ -37386,28 +37404,60 @@ async function suiteRcand(deps, opts) {
37386
37404
  ${refusals.join("\n")}
37387
37405
  Each module must ship a live release that bind-mounts <deployPath>/data read-only and serves <data>/rc/module.js at /rc/module.js (docs/Guides/suite-trains.md).`);
37388
37406
  }
37389
- for (const member of record.members) {
37390
- if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
37391
- throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
37392
- if (member.carried) {
37393
- await deps.publish(member.repo, pinVersion(member.rcPin));
37394
- continue;
37395
- }
37396
- if (!member.tag) {
37397
- const cut = await deps.rcand(member.repo, member.previousRc, member.rcWasAbsent);
37398
- if (!RC_TAG.test(cut.tag) || !SHA2.test(cut.sha) || cut.sha.toLowerCase() === member.previousRc.toLowerCase()) throw new Error(`${member.repo}: rcand did not return a new immutable rc tag/SHA`);
37399
- if (!await deps.contains(member.repo, member.development, cut.sha)) throw new Error(`${member.repo}: rc tag does not carry the snapshotted development SHA; refuse`);
37400
- member.tag = cut.tag;
37401
- member.sha = cut.sha;
37402
- writeSuiteCut(deps.recordPath, record);
37403
- }
37404
- if ((await deps.tagSha(member.repo, member.tag))?.toLowerCase() !== member.sha.toLowerCase()) throw new Error(`${member.repo}: rc tag ${member.tag} moved or is unreadable; stop`);
37405
- if (!member.published) {
37406
- await deps.publish(member.repo, member.tag);
37407
- member.published = true;
37408
- writeSuiteCut(deps.recordPath, record);
37407
+ const cutRecord = record;
37408
+ let writes = Promise.resolve();
37409
+ const persist = (mutate) => {
37410
+ const next = writes.then(() => {
37411
+ mutate();
37412
+ writeSuiteCut(deps.recordPath, cutRecord);
37413
+ });
37414
+ writes = next.catch(() => {
37415
+ });
37416
+ return next;
37417
+ };
37418
+ let running = 0;
37419
+ const waiting = [];
37420
+ const results = await Promise.allSettled(record.members.map(async (member) => {
37421
+ if (running >= 4) await new Promise((resolve8) => waiting.push(resolve8));
37422
+ else running++;
37423
+ try {
37424
+ if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
37425
+ throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
37426
+ if (member.carried) {
37427
+ if (!member.carriedPublished) {
37428
+ await deps.publish(member.repo, pinVersion(member.rcPin));
37429
+ await persist(() => {
37430
+ member.carriedPublished = true;
37431
+ });
37432
+ }
37433
+ return;
37434
+ }
37435
+ if (!member.tag) {
37436
+ const cut = await deps.rcand(member.repo, member.previousRc, member.rcWasAbsent);
37437
+ if (!RC_TAG.test(cut.tag) || !SHA2.test(cut.sha) || cut.sha.toLowerCase() === member.previousRc.toLowerCase()) throw new Error(`${member.repo}: rcand did not return a new immutable rc tag/SHA`);
37438
+ if (!await deps.contains(member.repo, member.development, cut.sha)) throw new Error(`${member.repo}: rc tag does not carry the snapshotted development SHA; refuse`);
37439
+ await persist(() => {
37440
+ member.tag = cut.tag;
37441
+ member.sha = cut.sha;
37442
+ });
37443
+ }
37444
+ const tag = member.tag;
37445
+ if ((await deps.tagSha(member.repo, tag))?.toLowerCase() !== member.sha.toLowerCase()) throw new Error(`${member.repo}: rc tag ${member.tag} moved or is unreadable; stop`);
37446
+ if (!member.published) {
37447
+ await deps.publish(member.repo, tag);
37448
+ await persist(() => {
37449
+ member.published = true;
37450
+ });
37451
+ }
37452
+ } finally {
37453
+ const next = waiting.shift();
37454
+ if (next) next();
37455
+ else running--;
37409
37456
  }
37410
- }
37457
+ }));
37458
+ const failures = results.flatMap((result, i) => result.status === "rejected" ? [`${record.members[i].repo}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`] : []);
37459
+ if (failures.length) throw new Error(`suite member cuts failed:
37460
+ ${failures.join("\n")}`);
37411
37461
  for (const member of record.members) {
37412
37462
  const [live, devHead] = await Promise.all([
37413
37463
  deps.backend({ repo: member.repo, id: member.id, bundles: {} }),
@@ -37852,8 +37902,10 @@ async function runRcDataSwitch(deps, opts) {
37852
37902
  }
37853
37903
 
37854
37904
  // src/suite-train-io.ts
37855
- var import_node_fs45 = require("node:fs");
37905
+ var import_node_child_process18 = require("node:child_process");
37856
37906
  var import_node_path44 = require("node:path");
37907
+ var import_node_fs45 = require("node:fs");
37908
+ var import_node_path45 = require("node:path");
37857
37909
  init_cli_shared();
37858
37910
 
37859
37911
  // src/command-register-train-operations.ts
@@ -38035,7 +38087,7 @@ function clean3(out) {
38035
38087
  return out.trim();
38036
38088
  }
38037
38089
  function sleeper(deps) {
38038
- return deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
38090
+ return deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
38039
38091
  }
38040
38092
  function normalizeHotfixVersion(input) {
38041
38093
  const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
@@ -39707,7 +39759,7 @@ async function updatePrHeadFromBase(input) {
39707
39759
  } catch (e) {
39708
39760
  throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
39709
39761
  }
39710
- const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
39762
+ const sleep2 = input.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
39711
39763
  for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
39712
39764
  const to = await readHead2();
39713
39765
  if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
@@ -39725,7 +39777,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
39725
39777
  if (first.mergeStatus !== "failed") return first;
39726
39778
  const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
39727
39779
  if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
39728
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
39780
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
39729
39781
  await sleep2(PR_LAND_MERGE_RETRY_DELAY_MS);
39730
39782
  const retried = await deps.mergeAuto(prNumber, repo);
39731
39783
  if (retried.mergeStatus !== "failed") return retried;
@@ -39736,7 +39788,7 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
39736
39788
  async function confirmAutoMergeEnqueued(deps, options) {
39737
39789
  const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
39738
39790
  const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
39739
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
39791
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
39740
39792
  for (let attempt = 0; attempt < retries; attempt++) {
39741
39793
  if (await deps.readMerged().catch(() => false)) return "merged";
39742
39794
  const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
@@ -39753,7 +39805,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
39753
39805
  async function readGhPrStateWithRetry(fetchState, options) {
39754
39806
  const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
39755
39807
  const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
39756
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
39808
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
39757
39809
  let lastError = "empty state";
39758
39810
  for (let attempt = 0; attempt < retries; attempt++) {
39759
39811
  try {
@@ -43381,7 +43433,7 @@ async function checkPrReview(number, repo, head, deps = {
43381
43433
  }
43382
43434
  }
43383
43435
  function computePrPatchId(number, repo) {
43384
- return new Promise((resolve7, reject) => {
43436
+ return new Promise((resolve8, reject) => {
43385
43437
  const gh2 = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
43386
43438
  const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
43387
43439
  let out = "";
@@ -43419,7 +43471,7 @@ function computePrPatchId(number, repo) {
43419
43471
  if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
43420
43472
  const id = out.trim().split(/\s+/)[0];
43421
43473
  if (!/^[0-9a-f]{40}$/.test(id ?? "")) return reject(new Error(`patch-id: PR #${number} diff is empty or unreadable (gh: ${ghErr.trim() || "no stderr"})`));
43422
- resolve7(id);
43474
+ resolve8(id);
43423
43475
  };
43424
43476
  gh2.on("close", (code) => {
43425
43477
  ghCode = code;
@@ -43470,7 +43522,7 @@ init_cli_shared();
43470
43522
  var GIT_TIMEOUT_MS2 = 15e3;
43471
43523
  function catFileBatch(root, ref, paths) {
43472
43524
  if (paths.length === 0) return Promise.resolve([]);
43473
- return new Promise((resolve7) => {
43525
+ return new Promise((resolve8) => {
43474
43526
  const chunks = [];
43475
43527
  let settled = false;
43476
43528
  const child2 = (0, import_node_child_process15.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
@@ -43478,7 +43530,7 @@ function catFileBatch(root, ref, paths) {
43478
43530
  if (settled) return;
43479
43531
  settled = true;
43480
43532
  clearTimeout(timer);
43481
- resolve7(parseCatFileBatch(Buffer.concat(chunks), paths.length));
43533
+ resolve8(parseCatFileBatch(Buffer.concat(chunks), paths.length));
43482
43534
  };
43483
43535
  const timer = setTimeout(() => {
43484
43536
  child2.kill("SIGKILL");
@@ -43903,7 +43955,7 @@ function healthPollIntervalMs() {
43903
43955
  return HEALTH_POLL_INTERVAL_MS;
43904
43956
  }
43905
43957
  function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
43906
- return new Promise((resolve7, reject) => {
43958
+ return new Promise((resolve8, reject) => {
43907
43959
  let settled = false;
43908
43960
  const finish = (fn) => {
43909
43961
  if (settled) return;
@@ -43913,7 +43965,7 @@ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
43913
43965
  child2.removeAllListeners("exit");
43914
43966
  fn();
43915
43967
  };
43916
- const timer = setTimeout(() => finish(resolve7), graceMs);
43968
+ const timer = setTimeout(() => finish(resolve8), graceMs);
43917
43969
  child2.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
43918
43970
  child2.on("exit", (code, signal) => {
43919
43971
  const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
@@ -44106,10 +44158,10 @@ function pickStagePort(range, isFree) {
44106
44158
  throw new Error(`no free stage port in range ${start}-${end} \u2014 every port is in use`);
44107
44159
  }
44108
44160
  function isPortFree(port) {
44109
- return new Promise((resolve7) => {
44161
+ return new Promise((resolve8) => {
44110
44162
  const srv = (0, import_node_net.createServer)();
44111
- srv.once("error", () => resolve7(false));
44112
- srv.once("listening", () => srv.close(() => resolve7(true)));
44163
+ srv.once("error", () => resolve8(false));
44164
+ srv.once("listening", () => srv.close(() => resolve8(true)));
44113
44165
  srv.listen(port, "127.0.0.1");
44114
44166
  });
44115
44167
  }
@@ -44387,7 +44439,7 @@ async function killTree(pid) {
44387
44439
  } catch {
44388
44440
  }
44389
44441
  }
44390
- await new Promise((resolve7) => setTimeout(resolve7, 500));
44442
+ await new Promise((resolve8) => setTimeout(resolve8, 500));
44391
44443
  try {
44392
44444
  process.kill(-pid, "SIGKILL");
44393
44445
  } catch {
@@ -44460,7 +44512,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
44460
44512
  } catch (e) {
44461
44513
  last = e.message;
44462
44514
  }
44463
- await new Promise((resolve7) => setTimeout(resolve7, healthPollIntervalMs()));
44515
+ await new Promise((resolve8) => setTimeout(resolve8, healthPollIntervalMs()));
44464
44516
  }
44465
44517
  throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
44466
44518
  }
@@ -44820,7 +44872,7 @@ function unlinkNodeModulesJunction(wtPath) {
44820
44872
  }
44821
44873
  }
44822
44874
  function defaultSleep2(ms) {
44823
- return new Promise((resolve7) => setTimeout(resolve7, ms));
44875
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
44824
44876
  }
44825
44877
  async function removeResidueDirectory(wtPath, options = {}) {
44826
44878
  const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs42.rmdirSync, exists = import_node_fs42.existsSync } = options;
@@ -46570,7 +46622,7 @@ ${list}`);
46570
46622
  // #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
46571
46623
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
46572
46624
  baseBranch,
46573
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
46625
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
46574
46626
  log: (message2) => console.warn(message2),
46575
46627
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
46576
46628
  // Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
@@ -46725,7 +46777,7 @@ ${list}`);
46725
46777
  // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
46726
46778
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
46727
46779
  baseBranch: "development",
46728
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
46780
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
46729
46781
  log: (message2) => console.warn(message2),
46730
46782
  // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
46731
46783
  // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
@@ -46776,7 +46828,7 @@ ${list}`);
46776
46828
  } else {
46777
46829
  lastFailure = void 0;
46778
46830
  }
46779
- await new Promise((resolve7) => setTimeout(resolve7, PR_LAND_POLL_MS));
46831
+ await new Promise((resolve8) => setTimeout(resolve8, PR_LAND_POLL_MS));
46780
46832
  }
46781
46833
  if (lastFailure) {
46782
46834
  throw new Error(
@@ -46934,7 +46986,7 @@ ${list}`);
46934
46986
  checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
46935
46987
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
46936
46988
  baseBranch,
46937
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
46989
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
46938
46990
  log: (message2) => console.warn(message2),
46939
46991
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
46940
46992
  progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
@@ -47003,7 +47055,7 @@ ${list}`);
47003
47055
  }
47004
47056
  if (!o.auto && mergeRejectedBaseBranchModified(message2)) {
47005
47057
  console.warn(`pr merge: the base branch was modified by a concurrent merge \u2014 waiting ${PR_MERGE_BASE_RACE_RETRY_DELAY_MS / 1e3}s and retrying PR #${number}'s merge once (#6052).`);
47006
- await new Promise((resolve7) => setTimeout(resolve7, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
47058
+ await new Promise((resolve8) => setTimeout(resolve8, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
47007
47059
  await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
47008
47060
  const m2 = String(e2.message || "");
47009
47061
  if (/already been merged/i.test(m2)) {
@@ -47264,7 +47316,7 @@ ${list}`);
47264
47316
  console.warn(`ci checks-wait: zero-runs probe FAILED (${read.error}) \u2014 not treating as delivery failure this cycle`);
47265
47317
  return null;
47266
47318
  },
47267
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
47319
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
47268
47320
  log: (message2) => console.warn(message2),
47269
47321
  timeoutMs: timeoutMs ?? PR_CHECKS_TIMEOUT_MS,
47270
47322
  progress: ({ state, elapsedMs, remainingMs }) => console.warn(`ci checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
@@ -47629,7 +47681,7 @@ function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote, deployNote) {
47629
47681
  }
47630
47682
  return legs;
47631
47683
  }
47632
- async function runProjectInfoSyncLeg(cb, repo, sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))) {
47684
+ async function runProjectInfoSyncLeg(cb, repo, sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms))) {
47633
47685
  for (let attempt = 0; ; attempt++) {
47634
47686
  try {
47635
47687
  return await cb(repo, true);
@@ -48358,18 +48410,63 @@ function rcRunCauseFromLog(log) {
48358
48410
  function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
48359
48411
  return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
48360
48412
  }
48361
- var suiteStateDir = (root) => (0, import_node_path44.join)(resolvePrimaryGitDir(root), "mmi-runtime", "suite");
48413
+ var suiteStateDir = (root) => (0, import_node_path45.join)(resolvePrimaryGitDir(root), "mmi-runtime", "suite");
48414
+ async function runSuiteMemberRcand(repo, previousRc, rcWasAbsent) {
48415
+ const deps = trainApplyDeps();
48416
+ deps.suiteCut = suiteCutContext(repo, SUITE_REPO);
48417
+ deps.announce = async () => ({ status: "skipped", note: "suite announcement belongs to MM-Work" });
48418
+ return rcandInCheckout(repo, deps, previousRc, rcWasAbsent);
48419
+ }
48420
+ async function rcandInCheckout(repo, deps, previousRc, rcWasAbsent) {
48421
+ if (repo !== SUITE_REPO) await prepareDevelopmentCheckout((args) => deps.run("git", args), repo);
48422
+ await deps.run("git", ["fetch", "origin", "--tags"]);
48423
+ let rcNow;
48424
+ try {
48425
+ rcNow = (await deps.run("git", ["rev-parse", "--verify", "origin/rc"])).trim();
48426
+ } catch (e) {
48427
+ if (!rcWasAbsent || !previousRc || repo === SUITE_REPO) throw e;
48428
+ const slug = repo.split("/")[1].toLowerCase();
48429
+ const meta = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
48430
+ if (!meta.ok || meta.project?.releaseTrack !== "full" || meta.project.suite !== "mm-work")
48431
+ throw new Error(`${repo}: first rc branch requires releaseTrack full and suite=mm-work in META; no branch created`);
48432
+ const main = (await deps.run("git", ["rev-parse", "origin/main"])).trim();
48433
+ if (main.toLowerCase() !== previousRc.toLowerCase()) throw new Error(`${repo}: main moved since the cut snapshot; no rc branch created`);
48434
+ await deps.run("git", ["push", "origin", `${previousRc}:refs/heads/rc`]);
48435
+ await deps.run("git", ["fetch", "origin", "rc"]);
48436
+ rcNow = (await deps.run("git", ["rev-parse", "origin/rc"])).trim();
48437
+ }
48438
+ if (!(await deps.run("git", ["branch", "--list", "rc"])).trim()) {
48439
+ await deps.run("git", ["switch", "-c", "rc", "--track", "origin/rc"]);
48440
+ await deps.run("git", ["switch", "development"]);
48441
+ }
48442
+ if (previousRc && rcNow.toLowerCase() !== previousRc.toLowerCase()) {
48443
+ const tags = (await deps.run("git", ["tag", "--points-at", rcNow, "--list", "v*-rc.*"])).trim().split("\n").filter(Boolean);
48444
+ if (tags.length !== 1) throw new Error(`${repo}: rc moved since cut start without exactly one candidate tag; stop`);
48445
+ return { tag: tags[0], sha: rcNow };
48446
+ }
48447
+ try {
48448
+ const result = await runTrainApply("rcand", deps, { watch: true });
48449
+ if (result.deployStatus !== "success") throw new Error(`${repo}: rcand deploy not green (${result.deployStatus})`);
48450
+ return { tag: result.tag, sha: (await deps.run("git", ["rev-parse", result.tag])).trim() };
48451
+ } catch (e) {
48452
+ const existing = (await deps.run("git", ["tag", "--list", "v*-rc.*"])).trim();
48453
+ if (!existing || !/resume|partial|ledger|tag.*pushed/i.test(e.message)) throw e;
48454
+ const result = await runRcandResume(deps, { watch: true });
48455
+ if (result.deployStatus !== "success") throw new Error(`${repo}: resumed rcand deploy not green (${result.deployStatus})`);
48456
+ return { tag: result.tag, sha: result.tagSha };
48457
+ }
48458
+ }
48362
48459
  function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr, memberSummaryFilesTr = {}) {
48363
- const recordPath = (0, import_node_path44.join)(suiteStateDir(root), "cut.json");
48364
- const base = (0, import_node_path44.join)(suiteStateDir(root), "checkouts");
48460
+ const recordPath = (0, import_node_path45.join)(suiteStateDir(root), "cut.json");
48461
+ const base = (0, import_node_path45.join)(suiteStateDir(root), "checkouts");
48365
48462
  const checkout = async (repo) => {
48366
48463
  if (repo === SUITE_REPO) return root;
48367
- const target = (0, import_node_path44.join)(base, repo.split("/")[1]);
48464
+ const target = (0, import_node_path45.join)(base, repo.split("/")[1]);
48368
48465
  if (!(0, import_node_fs45.existsSync)(target)) {
48369
48466
  (0, import_node_fs45.mkdirSync)(base, { recursive: true });
48370
48467
  await exec("git", ["clone", "--quiet", "--branch", "development", `https://github.com/${repo}.git`, target], root, WATCH_TIMEOUT);
48371
48468
  }
48372
- if (!(0, import_node_fs45.existsSync)((0, import_node_path44.join)(target, ".git"))) throw new Error(`${repo}: suite checkout has no .git at ${target}; stop`);
48469
+ if (!(0, import_node_fs45.existsSync)((0, import_node_path45.join)(target, ".git"))) throw new Error(`${repo}: suite checkout has no .git at ${target}; stop`);
48373
48470
  const origin = await exec("git", ["remote", "get-url", "origin"], target);
48374
48471
  if (!origin.toLowerCase().endsWith(`/${repo.toLowerCase()}.git`) && !origin.toLowerCase().endsWith(`/${repo.toLowerCase()}`))
48375
48472
  throw new Error(`${repo}: suite checkout origin mismatch at ${target}; stop`);
@@ -48429,45 +48526,28 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48429
48526
  throw e;
48430
48527
  }
48431
48528
  };
48432
- const runRcand = async (repo, previousRc, rcWasAbsent) => inTrain(repo, async (deps) => {
48433
- if (repo !== SUITE_REPO) await prepareDevelopmentCheckout((args) => deps.run("git", args), repo);
48434
- await deps.run("git", ["fetch", "origin", "--tags"]);
48435
- let rcNow;
48436
- try {
48437
- rcNow = (await deps.run("git", ["rev-parse", "--verify", "origin/rc"])).trim();
48438
- } catch (e) {
48439
- if (!rcWasAbsent || !previousRc || repo === SUITE_REPO) throw e;
48440
- const slug = repo.split("/")[1].toLowerCase();
48441
- const meta = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
48442
- if (!meta.ok || meta.project?.releaseTrack !== "full" || meta.project.suite !== "mm-work")
48443
- throw new Error(`${repo}: first rc branch requires releaseTrack full and suite=mm-work in META; no branch created`);
48444
- const main = (await deps.run("git", ["rev-parse", "origin/main"])).trim();
48445
- if (main.toLowerCase() !== previousRc.toLowerCase()) throw new Error(`${repo}: main moved since the cut snapshot; no rc branch created`);
48446
- await deps.run("git", ["push", "origin", `${previousRc}:refs/heads/rc`]);
48447
- await deps.run("git", ["fetch", "origin", "rc"]);
48448
- rcNow = (await deps.run("git", ["rev-parse", "origin/rc"])).trim();
48449
- }
48450
- if (!(await deps.run("git", ["branch", "--list", "rc"])).trim()) {
48451
- await deps.run("git", ["switch", "-c", "rc", "--track", "origin/rc"]);
48452
- await deps.run("git", ["switch", "development"]);
48453
- }
48454
- if (previousRc && rcNow.toLowerCase() !== previousRc.toLowerCase()) {
48455
- const tags = (await deps.run("git", ["tag", "--points-at", rcNow, "--list", "v*-rc.*"])).trim().split("\n").filter(Boolean);
48456
- if (tags.length !== 1) throw new Error(`${repo}: rc moved since cut start without exactly one candidate tag; stop`);
48457
- return { tag: tags[0], sha: rcNow };
48458
- }
48459
- try {
48460
- const result = await runTrainApply("rcand", deps, { watch: true });
48461
- if (result.deployStatus !== "success") throw new Error(`${repo}: rcand deploy not green (${result.deployStatus})`);
48462
- return { tag: result.tag, sha: (await deps.run("git", ["rev-parse", result.tag])).trim() };
48463
- } catch (e) {
48464
- const existing = (await deps.run("git", ["tag", "--list", "v*-rc.*"])).trim();
48465
- if (!existing || !/resume|partial|ledger|tag.*pushed/i.test(e.message)) throw e;
48466
- const result = await runRcandResume(deps, { watch: true });
48467
- if (result.deployStatus !== "success") throw new Error(`${repo}: resumed rcand deploy not green (${result.deployStatus})`);
48468
- return { tag: result.tag, sha: result.tagSha };
48469
- }
48470
- });
48529
+ const runRcand = async (repo, previousRc, rcWasAbsent) => {
48530
+ if (repo === SUITE_REPO) return inTrain(repo, (deps) => rcandInCheckout(repo, deps, previousRc, rcWasAbsent));
48531
+ const target = await checkout(repo);
48532
+ return new Promise((resolveResult, reject) => {
48533
+ const child2 = (0, import_node_child_process18.fork)((0, import_node_path44.resolve)(process.argv[1]), ["devops", "suite", "member-rcand-internal", repo, previousRc ?? "", ...rcWasAbsent ? ["--rc-was-absent"] : []], {
48534
+ cwd: target,
48535
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
48536
+ execArgv: []
48537
+ });
48538
+ let result;
48539
+ let failure;
48540
+ child2.on("message", (message2) => {
48541
+ result = message2.result;
48542
+ failure = message2.error;
48543
+ });
48544
+ child2.on("error", reject);
48545
+ child2.on("exit", (code) => {
48546
+ if (code === 0 && result) resolveResult(result);
48547
+ else reject(new Error(failure ?? `${repo}: member rcand process exited ${code} without a result`));
48548
+ });
48549
+ });
48550
+ };
48471
48551
  const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
48472
48552
  await prepareRcCheckout((args) => deps.run("git", args), repo);
48473
48553
  const plainWords = {
@@ -48522,7 +48602,7 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48522
48602
  if (!reuse) await gh(["workflow", "run", "module-rc-publish.yml", "--repo", "mutmutco/MMI-Hub", ...fields.flatMap((f) => ["--field", f])]);
48523
48603
  let candidate = reuse;
48524
48604
  for (let i = 0; !candidate && i < 25; i++) {
48525
- await new Promise((resolve7) => setTimeout(resolve7, 1e3));
48605
+ await new Promise((resolve8) => setTimeout(resolve8, 1e3));
48526
48606
  candidate = (await rcRuns()).find((r) => r.displayTitle === title && Date.parse(r.createdAt) >= since);
48527
48607
  }
48528
48608
  if (!candidate) throw new Error(`workflow dispatch outcome ambiguous for "${title}"; inspect Actions before retrying`);
@@ -48677,7 +48757,7 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48677
48757
  for (let i = 0; ; i++) {
48678
48758
  const pin = await openSuitePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, { repo, tag, stage: "main" });
48679
48759
  if (pin.status !== "pending" || i >= 40) return pin;
48680
- await new Promise((resolve7) => setTimeout(resolve7, 3e4));
48760
+ await new Promise((resolve8) => setTimeout(resolve8, 3e4));
48681
48761
  }
48682
48762
  },
48683
48763
  suiteMapping: async (leadTag) => {
@@ -48693,7 +48773,7 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
48693
48773
  if (parseSuiteMapping(body)) return;
48694
48774
  const dir = suiteStateDir(root);
48695
48775
  (0, import_node_fs45.mkdirSync)(dir, { recursive: true });
48696
- const file = (0, import_node_path44.join)(dir, `notes-${leadTag}.md`);
48776
+ const file = (0, import_node_path45.join)(dir, `notes-${leadTag}.md`);
48697
48777
  (0, import_node_fs45.writeFileSync)(file, `${body}
48698
48778
 
48699
48779
  ${suiteMappingBlock(leadTag, mapping)}`);
@@ -48759,11 +48839,23 @@ function suiteRcDataProdDeps() {
48759
48839
  const rc = facts.stages.rc;
48760
48840
  return rc?.present ? rc.substrate ?? "an unnamed substrate" : null;
48761
48841
  },
48762
- sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
48842
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms))
48763
48843
  };
48764
48844
  }
48765
48845
  function registerSuiteCommands(program3) {
48766
48846
  const suite = program3.command("suite").description("MM-Work suite status, candidate cut and release (#7262/#7263)");
48847
+ suite.command("member-rcand-internal", { hidden: true }).argument("<repo>").argument("<previous-rc>").option("--rc-was-absent").action(async (repo, previousRc, o) => {
48848
+ if (!process.send) return fail("member rcand is an internal suite child operation");
48849
+ try {
48850
+ if (repo === SUITE_REPO || (await resolveRepo())?.toLowerCase() !== repo.toLowerCase())
48851
+ throw new Error(`${repo}: member rcand checkout mismatch; stop`);
48852
+ const result = await runSuiteMemberRcand(repo, previousRc || void 0, o.rcWasAbsent);
48853
+ process.send({ result }, () => process.disconnect());
48854
+ } catch (e) {
48855
+ process.exitCode = 1;
48856
+ process.send({ error: e.message }, () => process.disconnect());
48857
+ }
48858
+ });
48767
48859
  const leadRoot = async () => {
48768
48860
  const repo = await resolveRepo();
48769
48861
  if (repo?.toLowerCase() !== SUITE_REPO.toLowerCase()) throw new Error(`suite commands run only from ${SUITE_REPO} (got ${repo ?? "unknown"})`);
@@ -49005,10 +49097,10 @@ function suiteStatusDeps(github, registry2) {
49005
49097
  }
49006
49098
 
49007
49099
  // src/packages-adopt-export.ts
49008
- var import_node_child_process18 = require("node:child_process");
49100
+ var import_node_child_process19 = require("node:child_process");
49009
49101
  var import_node_fs47 = require("node:fs");
49010
49102
  var import_node_os22 = require("node:os");
49011
- var import_node_path45 = require("node:path");
49103
+ var import_node_path46 = require("node:path");
49012
49104
  var import_node_util6 = require("node:util");
49013
49105
  init_cli_shared();
49014
49106
 
@@ -49628,25 +49720,25 @@ function expectedFromRun(run, opts) {
49628
49720
  }
49629
49721
  async function adoptExport(opts, deps) {
49630
49722
  if (!/^sha256:[0-9a-f]{64}$/.test(opts.digest)) throw new Error("--digest must be sha256:<64 hex>, from the export run summary");
49631
- const packagePath = (0, import_node_path45.join)(opts.dir, "package.json");
49632
- const lockPath = (0, import_node_path45.join)(opts.dir, "package-lock.json");
49723
+ const packagePath = (0, import_node_path46.join)(opts.dir, "package.json");
49724
+ const lockPath = (0, import_node_path46.join)(opts.dir, "package-lock.json");
49633
49725
  const packageText = (0, import_node_fs47.readFileSync)(packagePath, "utf8");
49634
49726
  const lockText = (0, import_node_fs47.readFileSync)(lockPath, "utf8");
49635
- const work = (0, import_node_fs47.mkdtempSync)((0, import_node_path45.join)((0, import_node_os22.tmpdir)(), "mmi-adopt-export-"));
49727
+ const work = (0, import_node_fs47.mkdtempSync)((0, import_node_path46.join)((0, import_node_os22.tmpdir)(), "mmi-adopt-export-"));
49636
49728
  try {
49637
49729
  const run = JSON.parse((await deps.ghApi(`repos/${EXPORT_REPO}/actions/runs/${opts.run}`)).toString("utf8"));
49638
- (0, import_node_fs47.writeFileSync)((0, import_node_path45.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
49639
- (0, import_node_fs47.writeFileSync)((0, import_node_path45.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
49640
- (0, import_node_fs47.writeFileSync)((0, import_node_path45.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
49641
- const script = (0, import_node_path45.join)(work, "package_export.py");
49730
+ (0, import_node_fs47.writeFileSync)((0, import_node_path46.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
49731
+ (0, import_node_fs47.writeFileSync)((0, import_node_path46.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
49732
+ (0, import_node_fs47.writeFileSync)((0, import_node_path46.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
49733
+ const script = (0, import_node_path46.join)(work, "package_export.py");
49642
49734
  (0, import_node_fs47.writeFileSync)(script, package_export_default);
49643
49735
  await deps.python(script, ["verify", work, opts.ds, opts.screens]);
49644
- const verified = (0, import_node_path45.join)(work, "verified");
49645
- const metadata = Object.fromEntries(PACKAGES.map(({ name, short: short3 }) => [name, JSON.parse((0, import_node_fs47.readFileSync)((0, import_node_path45.join)(verified, `${short3}.metadata.json`), "utf8"))]));
49736
+ const verified = (0, import_node_path46.join)(work, "verified");
49737
+ const metadata = Object.fromEntries(PACKAGES.map(({ name, short: short3 }) => [name, JSON.parse((0, import_node_fs47.readFileSync)((0, import_node_path46.join)(verified, `${short3}.metadata.json`), "utf8"))]));
49646
49738
  const plan = planAdoption(packageText, lockText, metadata);
49647
49739
  for (const { name } of plan.result.adopted) {
49648
49740
  const short3 = PACKAGES.find((p) => p.name === name).short;
49649
- await deps.npm(["cache", "add", (0, import_node_path45.join)(verified, `${short3}.tgz`)], opts.dir);
49741
+ await deps.npm(["cache", "add", (0, import_node_path46.join)(verified, `${short3}.tgz`)], opts.dir);
49650
49742
  }
49651
49743
  (0, import_node_fs47.writeFileSync)(packagePath, plan.packageText);
49652
49744
  (0, import_node_fs47.writeFileSync)(lockPath, plan.lockText);
@@ -49662,7 +49754,7 @@ async function adoptExport(opts, deps) {
49662
49754
  (0, import_node_fs47.rmSync)(work, { recursive: true, force: true });
49663
49755
  }
49664
49756
  }
49665
- var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process18.execFile);
49757
+ var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process19.execFile);
49666
49758
  var MAX_OUTPUT = 200 * 1024 * 1024;
49667
49759
  async function findPython() {
49668
49760
  for (const bin of ["python3", "python"]) {
@@ -49788,7 +49880,7 @@ init_github_client();
49788
49880
  init_cli_shared();
49789
49881
  var import_node_fs49 = require("node:fs");
49790
49882
  var import_node_os23 = require("node:os");
49791
- var import_node_path46 = require("node:path");
49883
+ var import_node_path47 = require("node:path");
49792
49884
  var GC_GH_TIMEOUT_MS3 = 2e4;
49793
49885
  async function collectStatus() {
49794
49886
  const repo = await resolveRepo();
@@ -49979,8 +50071,8 @@ async function collectOnboardStatus(opts = {}) {
49979
50071
  }
49980
50072
  const home = (0, import_node_os23.homedir)();
49981
50073
  const plugin = onboardPluginGate({
49982
- readKnown: () => readFileSyncSafe((0, import_node_path46.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs49.readFileSync),
49983
- readSettings: () => readFileSyncSafe((0, import_node_path46.join)(home, ".claude", "settings.json"), import_node_fs49.readFileSync)
50074
+ readKnown: () => readFileSyncSafe((0, import_node_path47.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs49.readFileSync),
50075
+ readSettings: () => readFileSyncSafe((0, import_node_path47.join)(home, ".claude", "settings.json"), import_node_fs49.readFileSync)
49984
50076
  });
49985
50077
  return {
49986
50078
  guide: "Read the MMI org-guide skill before project work; load only the relevant topic. Use mmi-help for a short introduction.",
@@ -52779,7 +52871,10 @@ function buildDeployRetirePlan(input) {
52779
52871
  if (!["dev", "rc", "main"].includes(input.stage)) {
52780
52872
  throw new Error("org project deploy retire: --stage must be dev, rc, or main");
52781
52873
  }
52782
- return { slug, stage: input.stage, apply: Boolean(input.apply) };
52874
+ if (input.decommissioned && input.stage !== "main") {
52875
+ throw new Error("org project deploy retire: --decommissioned applies to --stage main only (dev/rc always retire)");
52876
+ }
52877
+ return { slug, stage: input.stage, apply: Boolean(input.apply), decommissioned: Boolean(input.decommissioned) };
52783
52878
  }
52784
52879
  async function runDeployRetire(input, deps) {
52785
52880
  const plan = buildDeployRetirePlan(input);
@@ -52788,7 +52883,20 @@ async function runDeployRetire(input, deps) {
52788
52883
  if (!plan.apply) {
52789
52884
  return { slug: plan.slug, stage: plan.stage, applied: false, ok: facts !== null, existing };
52790
52885
  }
52791
- const res = await retireDeployCoords(plan.slug, plan.stage, deps.registry);
52886
+ if (plan.decommissioned) {
52887
+ const state = deps.boxServiceState ? await deps.boxServiceState(plan.stage) : "unknown";
52888
+ if (state !== "missing") {
52889
+ return {
52890
+ slug: plan.slug,
52891
+ stage: plan.stage,
52892
+ applied: false,
52893
+ ok: false,
52894
+ existing,
52895
+ error: `--decommissioned needs the box to show ${plan.stage} gone (tenant-control status serviceState=missing), got ${state} \u2014 run tenant control <repo> ${plan.stage} retire first`
52896
+ };
52897
+ }
52898
+ }
52899
+ const res = await retireDeployCoords(plan.slug, plan.stage, deps.registry, plan.decommissioned);
52792
52900
  if (!res.ok) {
52793
52901
  return { slug: plan.slug, stage: plan.stage, applied: true, ok: false, existing, error: writeError2(res) };
52794
52902
  }
@@ -53537,7 +53645,7 @@ function registerSessionReport(program3) {
53537
53645
 
53538
53646
  // src/stage-commands.ts
53539
53647
  var import_node_fs51 = require("node:fs");
53540
- var import_node_path47 = require("node:path");
53648
+ var import_node_path48 = require("node:path");
53541
53649
  init_cli_shared();
53542
53650
  init_clean_exit();
53543
53651
 
@@ -53785,8 +53893,8 @@ function registerStageCommands(program3) {
53785
53893
  const repo = Array.isArray(project2?.repos) && typeof project2.repos[0] === "string" ? project2.repos[0] : slug;
53786
53894
  const resolution = decideStage({
53787
53895
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
53788
- hasCompose: (0, import_node_fs51.existsSync)((0, import_node_path47.join)(process.cwd(), "docker-compose.yml")),
53789
- hasEnvExample: (0, import_node_fs51.existsSync)((0, import_node_path47.join)(process.cwd(), ".env.example")),
53896
+ hasCompose: (0, import_node_fs51.existsSync)((0, import_node_path48.join)(process.cwd(), "docker-compose.yml")),
53897
+ hasEnvExample: (0, import_node_fs51.existsSync)((0, import_node_path48.join)(process.cwd(), ".env.example")),
53790
53898
  repo
53791
53899
  });
53792
53900
  if (resolution.source === "derived" && resolution.derived) {
@@ -54081,7 +54189,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
54081
54189
  var import_node_crypto17 = require("node:crypto");
54082
54190
  var import_node_fs52 = require("node:fs");
54083
54191
  var import_promises11 = require("node:fs/promises");
54084
- var import_node_path48 = require("node:path");
54192
+ var import_node_path49 = require("node:path");
54085
54193
  var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
54086
54194
  var MAX_BYTES2 = 5 * 1024 * 1024 * 1024;
54087
54195
  async function sha256File(path2) {
@@ -54091,7 +54199,7 @@ async function sha256File(path2) {
54091
54199
  }
54092
54200
  async function putTenantArtifact(repo, stage, inputPath, deps) {
54093
54201
  if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
54094
- const path2 = (0, import_node_path48.resolve)(inputPath);
54202
+ const path2 = (0, import_node_path49.resolve)(inputPath);
54095
54203
  const info = await (0, import_promises11.stat)(path2);
54096
54204
  if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
54097
54205
  if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES2) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES2} bytes`);
@@ -54281,7 +54389,7 @@ async function resolveWhoami(deps) {
54281
54389
  }
54282
54390
 
54283
54391
  // src/command-register-developer.ts
54284
- var import_node_path54 = require("node:path");
54392
+ var import_node_path55 = require("node:path");
54285
54393
  init_house_map();
54286
54394
  init_hub_url();
54287
54395
 
@@ -54314,21 +54422,21 @@ async function executeWaveLand(plan, deps) {
54314
54422
  }
54315
54423
 
54316
54424
  // src/dist-drift.ts
54317
- var import_node_child_process19 = require("node:child_process");
54425
+ var import_node_child_process20 = require("node:child_process");
54318
54426
  var import_node_crypto19 = require("node:crypto");
54319
54427
  var import_node_fs54 = require("node:fs");
54320
54428
  var import_node_os24 = require("node:os");
54321
- var import_node_path50 = require("node:path");
54429
+ var import_node_path51 = require("node:path");
54322
54430
 
54323
54431
  // ../scripts/distribution-digest.mjs
54324
54432
  var import_node_crypto18 = require("node:crypto");
54325
54433
  var import_node_fs53 = require("node:fs");
54326
- var import_node_path49 = require("node:path");
54434
+ var import_node_path50 = require("node:path");
54327
54435
  var slash = (value) => value.replaceAll("\\", "/");
54328
54436
  function repoPath(root, declaredPath, label) {
54329
- const absoluteRoot = (0, import_node_path49.resolve)(root);
54330
- const target = (0, import_node_path49.resolve)(root, declaredPath);
54331
- if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path49.sep}`)) {
54437
+ const absoluteRoot = (0, import_node_path50.resolve)(root);
54438
+ const target = (0, import_node_path50.resolve)(root, declaredPath);
54439
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path50.sep}`)) {
54332
54440
  throw new Error(`${label} ${declaredPath} escapes the repository root`);
54333
54441
  }
54334
54442
  return target;
@@ -54443,7 +54551,7 @@ function walkFiles(root) {
54443
54551
  const files = [];
54444
54552
  const walk2 = (directory) => {
54445
54553
  for (const entry of (0, import_node_fs54.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
54446
- const child2 = (0, import_node_path50.join)(directory, entry.name);
54554
+ const child2 = (0, import_node_path51.join)(directory, entry.name);
54447
54555
  if (entry.isDirectory()) walk2(child2);
54448
54556
  else files.push(child2);
54449
54557
  }
@@ -54453,14 +54561,14 @@ function walkFiles(root) {
54453
54561
  }
54454
54562
  function bomPathFor(root) {
54455
54563
  try {
54456
- const registry2 = JSON.parse((0, import_node_fs54.readFileSync)((0, import_node_path50.join)(root, "surfaces.json"), "utf8"));
54457
- return (0, import_node_path50.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
54564
+ const registry2 = JSON.parse((0, import_node_fs54.readFileSync)((0, import_node_path51.join)(root, "surfaces.json"), "utf8"));
54565
+ return (0, import_node_path51.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
54458
54566
  } catch {
54459
- return (0, import_node_path50.join)(root, "distribution-bom.json");
54567
+ return (0, import_node_path51.join)(root, "distribution-bom.json");
54460
54568
  }
54461
54569
  }
54462
54570
  function rebuildTo(packageRoot, outDir) {
54463
- (0, import_node_child_process19.execFileSync)(process.execPath, ["build.mjs"], {
54571
+ (0, import_node_child_process20.execFileSync)(process.execPath, ["build.mjs"], {
54464
54572
  cwd: packageRoot,
54465
54573
  env: { ...process.env, MMI_DIST_OUTDIR: outDir },
54466
54574
  windowsHide: true,
@@ -54469,28 +54577,28 @@ function rebuildTo(packageRoot, outDir) {
54469
54577
  });
54470
54578
  }
54471
54579
  function runDistStatus(root) {
54472
- const stage = (0, import_node_fs54.mkdtempSync)((0, import_node_path50.join)((0, import_node_os24.tmpdir)(), "mmi-dist-drift-"));
54580
+ const stage = (0, import_node_fs54.mkdtempSync)((0, import_node_path51.join)((0, import_node_os24.tmpdir)(), "mmi-dist-drift-"));
54473
54581
  let overlayCount = 0;
54474
54582
  try {
54475
- const cliOut = (0, import_node_path50.join)(stage, "cli-dist");
54476
- const hubOut = (0, import_node_path50.join)(stage, "hub-dist");
54477
- rebuildTo((0, import_node_path50.join)(root, "cli"), cliOut);
54478
- rebuildTo((0, import_node_path50.join)(root, "updater"), hubOut);
54583
+ const cliOut = (0, import_node_path51.join)(stage, "cli-dist");
54584
+ const hubOut = (0, import_node_path51.join)(stage, "hub-dist");
54585
+ rebuildTo((0, import_node_path51.join)(root, "cli"), cliOut);
54586
+ rebuildTo((0, import_node_path51.join)(root, "updater"), hubOut);
54479
54587
  const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
54480
54588
  const rebuilt = (path2) => {
54481
54589
  const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
54482
- return spec ? readOrNull((0, import_node_path50.join)(outDirFor(spec.packageDir), spec.output)) : null;
54590
+ return spec ? readOrNull((0, import_node_path51.join)(outDirFor(spec.packageDir), spec.output)) : null;
54483
54591
  };
54484
- const committed = (path2) => readOrNull((0, import_node_path50.join)(root, path2));
54485
- const tree = (path2) => readOrNull((0, import_node_path50.join)(root, path2));
54486
- const distRoot = (0, import_node_path50.join)(root, "cli", "dist");
54487
- const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path50.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
54592
+ const committed = (path2) => readOrNull((0, import_node_path51.join)(root, path2));
54593
+ const tree = (path2) => readOrNull((0, import_node_path51.join)(root, path2));
54594
+ const distRoot = (0, import_node_path51.join)(root, "cli", "dist");
54595
+ const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path51.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
54488
54596
  const bom = JSON.parse((0, import_node_fs54.readFileSync)(bomPathFor(root), "utf8"));
54489
54597
  const digest = (entries) => {
54490
- const overlay = (0, import_node_path50.join)(stage, `overlay-${overlayCount++}`);
54598
+ const overlay = (0, import_node_path51.join)(stage, `overlay-${overlayCount++}`);
54491
54599
  for (const entry of entries) {
54492
- const target = (0, import_node_path50.join)(overlay, entry.path);
54493
- (0, import_node_fs54.mkdirSync)((0, import_node_path50.dirname)(target), { recursive: true });
54600
+ const target = (0, import_node_path51.join)(overlay, entry.path);
54601
+ (0, import_node_fs54.mkdirSync)((0, import_node_path51.dirname)(target), { recursive: true });
54494
54602
  (0, import_node_fs54.writeFileSync)(target, entry.bytes);
54495
54603
  }
54496
54604
  return digestPackedFiles(overlay, entries.map((entry) => entry.path));
@@ -54502,7 +54610,7 @@ function runDistStatus(root) {
54502
54610
  }
54503
54611
 
54504
54612
  // src/doc-refs-live.ts
54505
- var import_node_path51 = require("node:path");
54613
+ var import_node_path52 = require("node:path");
54506
54614
  var concrete = (value) => !!value && !/[<>{}$]/.test(value);
54507
54615
  var liveOption = (tokens2, flag) => {
54508
54616
  const index = tokens2.indexOf(flag);
@@ -54575,7 +54683,7 @@ async function runLiveDocRefs(root, deps, live) {
54575
54683
  const offline = runDocRefs(root, { ...deps, runbooksOnly: true });
54576
54684
  const read = deps.readFile ?? readFileOrNull;
54577
54685
  const docs = Object.fromEntries((deps.listDocs ?? defaultListDocs)(root).flatMap((doc) => {
54578
- const body = read((0, import_node_path51.join)(root, doc));
54686
+ const body = read((0, import_node_path52.join)(root, doc));
54579
54687
  return body !== null && isRunbook(doc, body) && !deps.isIgnored?.([doc]).has(doc) ? [[doc, body]] : [];
54580
54688
  }));
54581
54689
  const remote = await checkRunbookLive(docs, live);
@@ -54584,9 +54692,9 @@ async function runLiveDocRefs(root, deps, live) {
54584
54692
  }
54585
54693
 
54586
54694
  // src/doc-refs-live-io.ts
54587
- var import_node_child_process20 = require("node:child_process");
54695
+ var import_node_child_process21 = require("node:child_process");
54588
54696
  var import_node_util7 = require("node:util");
54589
- var exec2 = (0, import_node_util7.promisify)(import_node_child_process20.execFile);
54697
+ var exec2 = (0, import_node_util7.promisify)(import_node_child_process21.execFile);
54590
54698
  function createRunbookLiveDeps(cliBin) {
54591
54699
  const json = async (args) => {
54592
54700
  const { stdout } = await exec2(process.execPath, [cliBin, ...args, "--json"], { windowsHide: true, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 });
@@ -54612,7 +54720,7 @@ function createRunbookLiveDeps(cliBin) {
54612
54720
  };
54613
54721
  const first = await probe().catch(() => 599);
54614
54722
  if (first < 500) return first;
54615
- await new Promise((resolve7) => setTimeout(resolve7, 2e4));
54723
+ await new Promise((resolve8) => setTimeout(resolve8, 2e4));
54616
54724
  return probe();
54617
54725
  }
54618
54726
  };
@@ -54715,7 +54823,7 @@ init_hub_auth();
54715
54823
 
54716
54824
  // src/schedules-lift-command.ts
54717
54825
  var import_promises12 = require("node:fs/promises");
54718
- var import_node_path52 = require("node:path");
54826
+ var import_node_path53 = require("node:path");
54719
54827
  init_clean_exit();
54720
54828
  init_cli_shared();
54721
54829
  var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
@@ -54744,7 +54852,7 @@ async function readWorkflowFiles(dir) {
54744
54852
  const files = [];
54745
54853
  for (const name of names.sort()) {
54746
54854
  if (!/\.ya?ml$/.test(name)) continue;
54747
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path52.join)(dir, name), "utf8") });
54855
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path53.join)(dir, name), "utf8") });
54748
54856
  }
54749
54857
  return files;
54750
54858
  }
@@ -54826,9 +54934,9 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
54826
54934
  }
54827
54935
 
54828
54936
  // src/spawn-policy-core.ts
54829
- var import_node_child_process21 = require("node:child_process");
54937
+ var import_node_child_process22 = require("node:child_process");
54830
54938
  var import_node_fs55 = require("node:fs");
54831
- var import_node_path53 = require("node:path");
54939
+ var import_node_path54 = require("node:path");
54832
54940
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
54833
54941
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
54834
54942
  var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
@@ -54897,7 +55005,7 @@ function findViolationsInSource(raw) {
54897
55005
  return found;
54898
55006
  }
54899
55007
  function policedFiles(root) {
54900
- const r = (0, import_node_child_process21.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
55008
+ const r = (0, import_node_child_process22.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
54901
55009
  cwd: root,
54902
55010
  encoding: "utf8",
54903
55011
  windowsHide: true,
@@ -54914,7 +55022,7 @@ function runSpawnPolicy(root) {
54914
55022
  for (const file of files) {
54915
55023
  let raw;
54916
55024
  try {
54917
- raw = (0, import_node_fs55.readFileSync)((0, import_node_path53.join)(root, file), "utf8");
55025
+ raw = (0, import_node_fs55.readFileSync)((0, import_node_path54.join)(root, file), "utf8");
54918
55026
  } catch {
54919
55027
  continue;
54920
55028
  }
@@ -54934,7 +55042,7 @@ function runSpawnPolicy(root) {
54934
55042
  function registerDeveloperCommands(program3) {
54935
55043
  const rules = program3.command("rules").description("org-managed .gitignore delivery");
54936
55044
  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) => {
54937
- const path2 = (0, import_node_path54.join)(process.cwd(), ".gitignore");
55045
+ const path2 = (0, import_node_path55.join)(process.cwd(), ".gitignore");
54938
55046
  const current = (0, import_node_fs56.existsSync)(path2) ? (0, import_node_fs56.readFileSync)(path2, "utf8") : null;
54939
55047
  const plan = planManagedGitignore(current);
54940
55048
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
@@ -55215,7 +55323,7 @@ ${r.stderr ?? ""}`).catch(() => "");
55215
55323
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
55216
55324
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
55217
55325
  function envHealLockPath(home) {
55218
- return (0, import_node_path55.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
55326
+ return (0, import_node_path56.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
55219
55327
  }
55220
55328
  async function withEnvHealLock(what, run) {
55221
55329
  try {
@@ -55387,7 +55495,7 @@ function mmiDoctorDeps(opts = {}) {
55387
55495
  // has no generated routing index would
55388
55496
  // get a permanent — demanding an artifact it never asked for.
55389
55497
  docsIndexState: (root) => {
55390
- if (!(0, import_node_fs57.existsSync)((0, import_node_path55.join)(root, DOCS_INDEX_PATH))) return void 0;
55498
+ if (!(0, import_node_fs57.existsSync)((0, import_node_path56.join)(root, DOCS_INDEX_PATH))) return void 0;
55391
55499
  const real = createDocsIndexDeps(root);
55392
55500
  let docs;
55393
55501
  const listDocs = () => docs ??= real.listDocs();
@@ -55396,7 +55504,7 @@ function mmiDoctorDeps(opts = {}) {
55396
55504
  },
55397
55505
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
55398
55506
  healDocsIndex: (root) => {
55399
- if (!(0, import_node_fs57.existsSync)((0, import_node_path55.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
55507
+ if (!(0, import_node_fs57.existsSync)((0, import_node_path56.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
55400
55508
  const real = createDocsIndexDeps(root);
55401
55509
  let docs;
55402
55510
  const listDocs = () => docs ??= real.listDocs();
@@ -55417,8 +55525,8 @@ function mmiDoctorDeps(opts = {}) {
55417
55525
  });
55418
55526
  const raced = await Promise.race([
55419
55527
  work.then((r) => ({ ...r, timedOut: false })),
55420
- new Promise((resolve7) => {
55421
- ceiling = setTimeout(() => resolve7({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
55528
+ new Promise((resolve8) => {
55529
+ ceiling = setTimeout(() => resolve8({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
55422
55530
  })
55423
55531
  ]);
55424
55532
  if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
@@ -55451,8 +55559,8 @@ function mmiDoctorDeps(opts = {}) {
55451
55559
  incomplete: nb.incomplete,
55452
55560
  timedOut: false
55453
55561
  })),
55454
- new Promise((resolve7) => {
55455
- ceiling = setTimeout(() => resolve7({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
55562
+ new Promise((resolve8) => {
55563
+ ceiling = setTimeout(() => resolve8({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
55456
55564
  })
55457
55565
  ]);
55458
55566
  if (!raced.timedOut && raced.incomplete.length === 0) writeSchedulesDriftCache(cachePath, raced.driftLines);
@@ -56064,7 +56172,7 @@ projectDeploy.command("doctor").description("read-only estate port scan; --box l
56064
56172
  if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
56065
56173
  if (!report.ok && !report.error) process.exitCode = 1;
56066
56174
  });
56067
- projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
56175
+ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--decommissioned", "main only (#8148): retire a tenant-container main row after `runtime tenant control <repo> main retire`; the box must show main gone").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
56068
56176
  const stage = projectDeploy.opts().stage;
56069
56177
  if (!stage) return fail("org project deploy retire: --stage is required (dev | rc | main)");
56070
56178
  const cfg = await loadConfig();
@@ -56077,8 +56185,14 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
56077
56185
  let result;
56078
56186
  try {
56079
56187
  result = await runDeployRetire(
56080
- { target, stage, apply: Boolean(o.apply) },
56081
- { registry: registryClientDeps(cfg) }
56188
+ { target, stage, apply: Boolean(o.apply), decommissioned: Boolean(o.decommissioned) },
56189
+ {
56190
+ registry: registryClientDeps(cfg),
56191
+ boxServiceState: async (s) => {
56192
+ const r = await runTenantControl(trainApplyDeps(), { repo: target, stage: s, action: "status", watch: true });
56193
+ return r.conclusion === "success" ? r.serviceState ?? "unknown" : "error";
56194
+ }
56195
+ }
56082
56196
  );
56083
56197
  } catch (e) {
56084
56198
  return fail(e.message);
@@ -56504,16 +56618,16 @@ function ciAuditDeps2() {
56504
56618
  // gate re-seed step is skipped gracefully rather than failing mid-run.
56505
56619
  readSeedFile: (path2) => {
56506
56620
  if (!root) return null;
56507
- const fullPath = (0, import_node_path55.join)(root, path2);
56621
+ const fullPath = (0, import_node_path56.join)(root, path2);
56508
56622
  return (0, import_node_fs57.existsSync)(fullPath) ? (0, import_node_fs57.readFileSync)(fullPath, "utf8") : null;
56509
56623
  }
56510
56624
  };
56511
56625
  }
56512
56626
  function hubRoot2() {
56513
- const fromPkg = (0, import_node_path55.join)(__dirname, "..", "..");
56627
+ const fromPkg = (0, import_node_path56.join)(__dirname, "..", "..");
56514
56628
  const marker = "skills/bootstrap/seeds/manifest.json";
56515
- if ((0, import_node_fs57.existsSync)((0, import_node_path55.join)(fromPkg, marker))) return fromPkg;
56516
- if ((0, import_node_fs57.existsSync)((0, import_node_path55.join)(process.cwd(), marker))) return process.cwd();
56629
+ if ((0, import_node_fs57.existsSync)((0, import_node_path56.join)(fromPkg, marker))) return fromPkg;
56630
+ if ((0, import_node_fs57.existsSync)((0, import_node_path56.join)(process.cwd(), marker))) return process.cwd();
56517
56631
  return null;
56518
56632
  }
56519
56633
  registerQueryCommands(program2);
@@ -56682,7 +56796,7 @@ function directoryBytes(path2) {
56682
56796
  return 0;
56683
56797
  }
56684
56798
  for (const entry of entries) {
56685
- const child2 = (0, import_node_path55.join)(path2, entry.name);
56799
+ const child2 = (0, import_node_path56.join)(path2, entry.name);
56686
56800
  if (entry.isDirectory()) total += directoryBytes(child2);
56687
56801
  else {
56688
56802
  try {
@@ -56712,7 +56826,7 @@ function pluginCacheFsDeps(configRoot, dirBytes) {
56712
56826
  dirBytes,
56713
56827
  listStagingDirs: (root) => (0, import_node_fs57.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
56714
56828
  try {
56715
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path55.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs57.statSync)(p).mtimeMs) };
56829
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path56.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs57.statSync)(p).mtimeMs) };
56716
56830
  } catch {
56717
56831
  return { name: d.name, mtimeMs: Date.now() };
56718
56832
  }
@@ -56726,7 +56840,7 @@ function stagingApplyFsGuard(configRoot) {
56726
56840
  return {
56727
56841
  referencedPaths: () => readInstalledPluginRefs(configRoot),
56728
56842
  mtimeMs: (name) => {
56729
- const p = (0, import_node_path55.join)(stagingRoot, name);
56843
+ const p = (0, import_node_path56.join)(stagingRoot, name);
56730
56844
  if (!(0, import_node_fs57.existsSync)(p)) return null;
56731
56845
  try {
56732
56846
  return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs57.statSync)(q).mtimeMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.124",
3
+ "version": "4.5.126",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",