@mutmutco/codex-plugin 4.5.86 → 4.5.87

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.
package/cli/dist/main.cjs CHANGED
@@ -7447,25 +7447,6 @@ function workflowEntry(repo, workflowPath, yamlText) {
7447
7447
  harbourUnmanaged: isHarbourUnmanaged(yamlText)
7448
7448
  };
7449
7449
  }
7450
- function decorateAwsModel(entries, registry2, verdicts) {
7451
- if (!registry2 || registry2.length === 0) return [...entries];
7452
- const rowById = new Map(registry2.map((r) => [r.id, r]));
7453
- const literalById = /* @__PURE__ */ new Map();
7454
- for (const v of verdicts ?? []) {
7455
- if (typeof v.modelId === "string" && v.modelId.trim() && !literalById.has(v.id)) {
7456
- literalById.set(v.id, v.modelId);
7457
- }
7458
- }
7459
- return entries.map((e) => {
7460
- if (!e.scheduleId) return e;
7461
- const row = rowById.get(e.scheduleId);
7462
- if (!row || row.executor !== "cursor-agent") return e;
7463
- const literal = literalById.get(e.scheduleId);
7464
- if (literal) return { ...e, model: literal };
7465
- if (row.model) return { ...e, model: `role: ${row.model}` };
7466
- return { ...e, model: "org-default" };
7467
- });
7468
- }
7469
7450
  var DECLARED_ENTRIES = [
7470
7451
  { name: "mmi-backup (mm-central, mm-work, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh; every Docker volume is tarred and every running Postgres container is pg_dumpall'd beside it (#7147; source scripts/nightly-backup.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
7471
7452
  // #7148: a backup nobody restored is a hope. Monthly, restore the latest artifact of every
@@ -7511,12 +7492,10 @@ function formatSchedulesTable(entries, now = /* @__PURE__ */ new Date()) {
7511
7492
  const cadW = w((e) => e.cadence, "CADENCE");
7512
7493
  const exeW = w((e) => e.executor, "EXECUTOR");
7513
7494
  const llmW = w((e) => e.llm, "LLM");
7514
- const modelOf = (e) => e.model ?? "-";
7515
- const modW = w((e) => modelOf(e), "MODEL");
7516
7495
  const resW = w((e) => resolvedOf.get(e) ?? e.resolved, "RESOLVED");
7517
- const row = (n, c, x, l, m, r, s) => `${n.padEnd(nameW)} ${c.padEnd(cadW)} ${x.padEnd(exeW)} ${l.padEnd(llmW)} ${m.padEnd(modW)} ${r.padEnd(resW)} ${s}`;
7518
- const lines2 = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "MODEL", "RESOLVED", "SOURCE")];
7519
- for (const e of entries) lines2.push(row(e.name, e.cadence, e.executor, e.llm, modelOf(e), resolvedOf.get(e) ?? e.resolved, e.source));
7496
+ const row = (n, c, x, l, r, s) => `${n.padEnd(nameW)} ${c.padEnd(cadW)} ${x.padEnd(exeW)} ${l.padEnd(llmW)} ${r.padEnd(resW)} ${s}`;
7497
+ const lines2 = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "RESOLVED", "SOURCE")];
7498
+ for (const e of entries) lines2.push(row(e.name, e.cadence, e.executor, e.llm, resolvedOf.get(e) ?? e.resolved, e.source));
7520
7499
  return lines2.join("\n");
7521
7500
  }
7522
7501
  function withTickState(entry, now = /* @__PURE__ */ new Date()) {
@@ -7524,11 +7503,11 @@ function withTickState(entry, now = /* @__PURE__ */ new Date()) {
7524
7503
  }
7525
7504
  function renderDocSection(entries, generatedAtIso) {
7526
7505
  const lines2 = [
7527
- "| Job | Cadence | Executor | LLM | Model | Resolved | Source |",
7528
- "|---|---|---|---|---|---|---|"
7506
+ "| Job | Cadence | Executor | LLM | Resolved | Source |",
7507
+ "|---|---|---|---|---|---|"
7529
7508
  ];
7530
7509
  for (const e of entries) {
7531
- lines2.push(`| ${e.name} | \`${e.cadence}\` | ${e.executor} | ${e.llm} | ${e.model ?? "-"} | ${e.resolved} | ${e.source} |`);
7510
+ lines2.push(`| ${e.name} | \`${e.cadence}\` | ${e.executor} | ${e.llm} | ${e.resolved} | ${e.source} |`);
7532
7511
  }
7533
7512
  return [
7534
7513
  DOC_START_MARKER,
@@ -7546,10 +7525,6 @@ function spliceDoc(docText, generatedSection) {
7546
7525
  }
7547
7526
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
7548
7527
  }
7549
- var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
7550
- function isHarbourLlmLauncher(executor) {
7551
- return HARBOUR_LLM_LAUNCHERS.has(executor);
7552
- }
7553
7528
  function splitCadence(cadence) {
7554
7529
  return cadence.split("+").map((c) => c.replace(/\s+/g, " ").trim()).filter(Boolean);
7555
7530
  }
@@ -7657,6 +7632,10 @@ function isHarbourUnmanaged(yamlText) {
7657
7632
  function strayCronDrifts(workflows) {
7658
7633
  return workflows.filter((w) => !isHarbourUnmanaged(w.yamlText)).filter((w) => workflowTriggersSchedule(w.yamlText)).map((w) => strayCronDrift(w.name)).sort((a, b) => a.name.localeCompare(b.name));
7659
7634
  }
7635
+ var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["codex-agent"]);
7636
+ function isHarbourLlmLauncher(executor) {
7637
+ return HARBOUR_LLM_LAUNCHERS.has(executor);
7638
+ }
7660
7639
  function unlauncheredLlmDrift(entry) {
7661
7640
  if (entry.llm !== "yes") return null;
7662
7641
  if (isHarbourLlmLauncher(entry.executor)) return null;
@@ -7667,7 +7646,7 @@ function unlauncheredLlmDrift(entry) {
7667
7646
  name: entry.name,
7668
7647
  executor: entry.executor,
7669
7648
  detail: `llm: yes with executor outside the harbour launcher set (${allowed})`,
7670
- remedy: `move the lane to executor: ${allowed} and run it through the Hub harbour key (docs/schedules.md rule 4)`
7649
+ remedy: `move the lane to executor: ${allowed} and run it through the Hub harbour (docs/schedules.md rule 3)`
7671
7650
  };
7672
7651
  }
7673
7652
  function unlauncheredLlmDrifts(entries) {
@@ -7739,8 +7718,8 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
7739
7718
  var SCHEDULE_EXECUTORS = /* @__PURE__ */ new Set([
7740
7719
  "github-actions",
7741
7720
  // deterministic workflow — clocked by the harbour, fired via workflow_dispatch
7742
- "cursor-agent",
7743
- // LLM lane — the harbour's ONLY launcher (docs/schedules.md rule 5)
7721
+ "codex-agent",
7722
+ // LLM lane — the harbour's ONLY scheduled-LLM launcher (docs/schedules.md rule 3, #7780)
7744
7723
  "lambda",
7745
7724
  // fleet function row — requires the `# target: mmi-fleet-*` header (#3205/#3246)
7746
7725
  "box systemd",
@@ -7821,8 +7800,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7821
7800
  if (llmFromHeader(yamlText) === "unknown") {
7822
7801
  throw new Error(`schedules register: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
7823
7802
  }
7824
- if (llmFromHeader(yamlText) === "yes" && h.executor !== "cursor-agent") {
7825
- throw new Error(`schedules register: ${expectedId} header \`llm: yes\` requires the harbour launcher \`executor: cursor-agent\` (declared: \`${h.executor}\`) \u2014 scheduled LLM work runs only through a harbour key (docs/schedules.md rule 5); change the executor or the llm value.`);
7803
+ if (llmFromHeader(yamlText) === "yes" && h.executor !== "codex-agent") {
7804
+ throw new Error(`schedules register: ${expectedId} header \`llm: yes\` requires the harbour launcher \`executor: codex-agent\` (declared: \`${h.executor}\`) \u2014 scheduled LLM work runs only through the Codex executor (docs/schedules.md rule 3, #7780); change the executor or the llm value.`);
7826
7805
  }
7827
7806
  const target = header.target;
7828
7807
  if (target !== void 0 && h.executor !== "lambda") {
@@ -7831,9 +7810,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7831
7810
  if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
7832
7811
  throw new Error(`schedules register: ${expectedId} header \`target: ${target}\` must be a bare lambda function name in the reserved fleet namespace \`mmi-fleet-*\` (allowed after the prefix: A-Z a-z 0-9 _ -; total max 140). The dispatcher can only invoke mmi-fleet-* functions.`);
7833
7812
  }
7834
- const model = header.model;
7835
- if (model !== void 0 && !/^[a-z][a-z0-9-]{0,30}$/.test(model)) {
7836
- throw new Error(`schedules register: ${expectedId} header \`model: ${model}\` must be a model ROLE KEY (lowercase kebab, max 31 chars \u2014 e.g. janitor, arbiter), never a model id \u2014 ids resolve from the vault at dispatch.`);
7813
+ if (header.model !== void 0) {
7814
+ throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 the Codex executor pins its model (#7780), so no row declares one. Remove the line.`);
7837
7815
  }
7838
7816
  return {
7839
7817
  id: expectedId,
@@ -7847,8 +7825,7 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7847
7825
  kill: h.kill,
7848
7826
  repo,
7849
7827
  sourcePath: workflowPath,
7850
- ...target !== void 0 ? { target } : {},
7851
- ...model !== void 0 ? { model } : {}
7828
+ ...target !== void 0 ? { target } : {}
7852
7829
  };
7853
7830
  }
7854
7831
  function scheduleRecordsFromWorkflows(repo, files) {
@@ -7993,22 +7970,6 @@ async function fetchSchedulesList(deps) {
7993
7970
  return null;
7994
7971
  }
7995
7972
  }
7996
- async function fetchRunVerdicts(deps) {
7997
- if (!deps.baseUrl) return null;
7998
- const token = await deps.token();
7999
- if (!token) return null;
8000
- try {
8001
- const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/schedules/verdicts`, {
8002
- method: "GET",
8003
- headers: { Authorization: `Bearer ${token}` }
8004
- });
8005
- if (!res.ok) return null;
8006
- const body = await res.json();
8007
- return Array.isArray(body?.runVerdicts) ? body.runVerdicts : null;
8008
- } catch {
8009
- return null;
8010
- }
8011
- }
8012
7973
  async function fetchOrgConfig(deps) {
8013
7974
  if (!deps.baseUrl) return null;
8014
7975
  const token = await deps.token();
@@ -11324,9 +11285,6 @@ function detectSurface(env) {
11324
11285
  if (env.MMI_AGENT_SURFACE === "jervcode" || has("PI_SESSION_ID")) {
11325
11286
  return "jervcode";
11326
11287
  }
11327
- if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
11328
- return "cursor";
11329
- }
11330
11288
  if (env.MMI_AGENT_SURFACE === "opencode" || has("OPENCODE_CLIENT") || has("OPENCODE_SERVER_USERNAME")) return "opencode";
11331
11289
  const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
11332
11290
  const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
@@ -11341,8 +11299,6 @@ function surfaceToken(surface) {
11341
11299
  return "claude";
11342
11300
  case "codex":
11343
11301
  return "codex";
11344
- case "cursor":
11345
- return "cursor";
11346
11302
  case "jervcode":
11347
11303
  return "jervcode";
11348
11304
  case "hermes":
@@ -11362,8 +11318,6 @@ function reloadAction(surface) {
11362
11318
  return "restart Codex";
11363
11319
  case "opencode":
11364
11320
  return "restart OpenCode";
11365
- case "cursor":
11366
- return "reload the Cursor window";
11367
11321
  case "jervcode":
11368
11322
  return "restart JervCode (the package set is read at launch)";
11369
11323
  case "hermes":
@@ -11376,7 +11330,6 @@ function reloadAction(surface) {
11376
11330
  }
11377
11331
  var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
11378
11332
  var CODEX_RECOVERY = "codex plugin remove mmi@mutmutco && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco";
11379
- var CURSOR_RECOVERY = "mmi-cli plugin heal # installs ~/.cursor/plugins/local/mmi from mutmutco/MMI-Hub";
11380
11333
  var PLUGIN_SURFACE_HEAL = {
11381
11334
  claude: {
11382
11335
  delivery: "plugin-cli",
@@ -11402,23 +11355,13 @@ var PLUGIN_SURFACE_HEAL = {
11402
11355
  ],
11403
11356
  fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} to load MMI commands`,
11404
11357
  updateRecipe: [CODEX_RECOVERY]
11405
- },
11406
- cursor: {
11407
- delivery: "local-checkout",
11408
- recovery: CURSOR_RECOVERY,
11409
- healSteps: null,
11410
- fix: (surface) => `${CURSOR_RECOVERY} # then ${reloadAction(surface)}`,
11411
- updateRecipe: [CURSOR_RECOVERY]
11412
11358
  }
11413
11359
  };
11414
11360
  function nonClaudeSurfaceHealMessage(surface) {
11415
11361
  if (surface === "codex") {
11416
11362
  return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Update the CLI too: npm i -g @mutmutco/cli";
11417
11363
  }
11418
- if (surface === "cursor") {
11419
- return "Cursor ships an MMI plugin (#3920). Install or repair its managed local checkout with:\n mmi-cli plugin heal\n Then reload the Cursor window. For one-off CLI use, pass --plugin-dir <MMI-Hub checkout>.\n Update the CLI too: npm i -g @mutmutco/cli";
11420
- }
11421
- return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, Cursor, JervCode, and Hermes only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
11364
+ return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, JervCode, and Hermes only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
11422
11365
  }
11423
11366
  function healStepAborts(step, ok) {
11424
11367
  return !ok && step.gated;
@@ -11468,7 +11411,6 @@ function hermesConfigRoot(env = process.env, home = (0, import_node_os7.homedir)
11468
11411
  }
11469
11412
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os7.homedir)()) {
11470
11413
  if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path10.join)(home, ".codex");
11471
- if (surface === "cursor") return (0, import_node_path10.join)(home, ".cursor");
11472
11414
  if (surface === "hermes") return hermesConfigRoot(env, home);
11473
11415
  if (surface === "jervcode") {
11474
11416
  return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path10.join)(home, ".jerv", "agent");
@@ -11493,7 +11435,7 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
11493
11435
  (0, import_node_path10.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
11494
11436
  ];
11495
11437
  }
11496
- if (surface === "cursor" || surface === "hermes") return [];
11438
+ if (surface === "hermes") return [];
11497
11439
  if (surface === "jervcode") return [];
11498
11440
  return [(0, import_node_path10.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
11499
11441
  }
@@ -11538,15 +11480,6 @@ async function fetchNpmReleasedVersion() {
11538
11480
  return void 0;
11539
11481
  }
11540
11482
  }
11541
- function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os7.homedir)()) {
11542
- return (0, import_node_path10.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
11543
- }
11544
- function cursorPluginTreeHealthy(root, exists = import_node_fs12.existsSync) {
11545
- return [
11546
- ".cursor-plugin/plugin.json",
11547
- "skills/bootstrap/SKILL.md"
11548
- ].every((path2) => exists((0, import_node_path10.join)(root, ...path2.split("/"))));
11549
- }
11550
11483
  var JERVCODE_NPM_PACKAGE = "@mutmutco/pi-plugin";
11551
11484
  function jervcodeAgentDirs(env = process.env, home = (0, import_node_os7.homedir)()) {
11552
11485
  const primary = surfaceConfigRoot("jervcode", env, home);
@@ -11633,10 +11566,10 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
11633
11566
  // Codex's native projection is authoritative; its legacy installed_plugins.json may be empty
11634
11567
  // while `plugin list --json` proves the managed plugin installed (#6111).
11635
11568
  installRecordPresent: surface === "codex" ? codexStatus?.installed === true : hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // #4188: jervcode's install record is the settings-file packages[] entry itself.
11636
- surface === "jervcode" && piEntry !== null || surface === "cursor" && (0, import_node_fs12.existsSync)(cursorLocalPluginRoot()) || surface === "hermes" && (0, import_node_fs12.existsSync)(hermesPluginRoot()),
11569
+ surface === "jervcode" && piEntry !== null || surface === "hermes" && (0, import_node_fs12.existsSync)(hermesPluginRoot()),
11637
11570
  // JervCode's settings entry and native local surfaces have no marketplace clone.
11638
- marketplaceClonePresent: surface === "cursor" || surface === "jervcode" || surface === "hermes" ? true : marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
11639
- pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "cursor" ? cursorPluginTreeHealthy(cursorLocalPluginRoot()) : surface === "codex" ? Boolean(
11571
+ marketplaceClonePresent: surface === "jervcode" || surface === "hermes" ? true : marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
11572
+ pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "codex" ? Boolean(
11640
11573
  codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
11641
11574
  ) : (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, "plugins", "cache", "mutmutco", "mmi"))
11642
11575
  };
@@ -11652,84 +11585,6 @@ async function runPluginCli(bin, args, log) {
11652
11585
  return false;
11653
11586
  }
11654
11587
  }
11655
- function canonicalCursorRemote(remote) {
11656
- return /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com[:/])mutmutco\/MMI-Hub(?:\.git)?\/?$/i.test(remote.trim().replace(/\\/g, "/"));
11657
- }
11658
- function readJsonFile(path2) {
11659
- try {
11660
- return JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
11661
- } catch {
11662
- return null;
11663
- }
11664
- }
11665
- async function cursorTreeManaged(target) {
11666
- if ((0, import_node_fs12.existsSync)((0, import_node_path10.join)(target, ".git"))) {
11667
- try {
11668
- const { stdout } = await runHostBin("git", ["-C", target, "remote", "get-url", "origin"], { timeout: 15e3 });
11669
- if (canonicalCursorRemote(stdout)) return { ok: true };
11670
- return { ok: false, detail: `refused to replace unrelated Cursor plugin directory at ${target}` };
11671
- } catch {
11672
- return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target} (git checkout with no readable origin)` };
11673
- }
11674
- }
11675
- if (readJsonFile((0, import_node_path10.join)(target, "package.json"))?.name === "@mutmutco/cursor-plugin") return { ok: true };
11676
- if (readJsonFile((0, import_node_path10.join)(target, ".cursor-plugin", "plugin.json"))?.name === "mmi") return { ok: true };
11677
- return {
11678
- ok: false,
11679
- detail: `refused to replace unmanaged Cursor plugin directory at ${target} \u2014 no canonical git remote, no @mutmutco/cursor-plugin package.json and no MMI plugin manifest`
11680
- };
11681
- }
11682
- async function installCursorPluginCheckout(env = process.env) {
11683
- const configRoot = surfaceConfigRoot("cursor", env);
11684
- const pluginsRoot = (0, import_node_path10.join)(configRoot, "plugins");
11685
- const target = (0, import_node_path10.join)(pluginsRoot, "local", "mmi");
11686
- const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
11687
- if ((0, import_node_fs12.existsSync)(target) && !source) {
11688
- const managed = await cursorTreeManaged(target);
11689
- if (!managed.ok) return { ok: false, detail: managed.detail };
11690
- }
11691
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "local"), { recursive: true });
11692
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "staging"), { recursive: true });
11693
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "quarantine"), { recursive: true });
11694
- const suffix = `${Date.now()}-${process.pid}`;
11695
- const staged = (0, import_node_path10.join)(pluginsRoot, "staging", `mmi-${suffix}`);
11696
- const quarantined = (0, import_node_path10.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
11697
- try {
11698
- if (source) {
11699
- (0, import_node_fs12.cpSync)(source, staged, {
11700
- recursive: true,
11701
- filter: (path2) => !path2.split(/[\\/]/).some((part) => part === ".git" || part === "node_modules")
11702
- });
11703
- } else {
11704
- await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
11705
- timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
11706
- step: "gh repo clone mutmutco/MMI-Hub"
11707
- });
11708
- }
11709
- if (!cursorPluginTreeHealthy(staged)) {
11710
- (0, import_node_fs12.rmSync)(staged, { recursive: true, force: true });
11711
- return { ok: false, detail: "downloaded Cursor plugin is incomplete; existing install was preserved" };
11712
- }
11713
- let movedOld = false;
11714
- if ((0, import_node_fs12.existsSync)(target)) {
11715
- (0, import_node_fs12.renameSync)(target, quarantined);
11716
- movedOld = true;
11717
- }
11718
- try {
11719
- (0, import_node_fs12.renameSync)(staged, target);
11720
- } catch (error) {
11721
- if (movedOld && !(0, import_node_fs12.existsSync)(target)) (0, import_node_fs12.renameSync)(quarantined, target);
11722
- throw error;
11723
- }
11724
- return {
11725
- ok: true,
11726
- detail: movedOld ? `installed canonical Cursor plugin; previous checkout quarantined at ${quarantined}` : `installed canonical Cursor plugin at ${target}`
11727
- };
11728
- } catch (error) {
11729
- if ((0, import_node_fs12.existsSync)(staged)) (0, import_node_fs12.rmSync)(staged, { recursive: true, force: true });
11730
- return { ok: false, detail: error.message.trim().slice(0, 240).replace(/\s+/g, " ") };
11731
- }
11732
- }
11733
11588
  async function marketplaceAddRefSupported(bin) {
11734
11589
  try {
11735
11590
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -11852,11 +11707,10 @@ async function healJervCodeForDoctor() {
11852
11707
  }
11853
11708
  async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
11854
11709
  const token = surfaceToken(surface);
11855
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11710
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11856
11711
  return { ok: false, detail: `not a supported plugin surface (${surface})` };
11857
11712
  }
11858
11713
  if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
11859
- if (token === "cursor") return installCursorPluginCheckout();
11860
11714
  if (token === "jervcode") return healJervCodeForDoctor();
11861
11715
  const steps = [];
11862
11716
  const applied = await applyPluginHeal(surface, (msg) => {
@@ -11952,7 +11806,7 @@ async function runGuard(readOrigin) {
11952
11806
  const surface = detectSurface(process.env);
11953
11807
  try {
11954
11808
  const token = surfaceToken(surface);
11955
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11809
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11956
11810
  process.exitCode = 0;
11957
11811
  return;
11958
11812
  }
@@ -11966,9 +11820,8 @@ async function runGuard(readOrigin) {
11966
11820
  if (surfaceToken(surface) === "codex") {
11967
11821
  console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
11968
11822
  process.exitCode = 1;
11969
- } else if (surfaceToken(surface) === "cursor" || surfaceToken(surface) === "jervcode") {
11970
- const host = surfaceToken(surface) === "cursor" ? "Cursor" : "JervCode";
11971
- console.error(`[mmi-guard] Could not inspect the active ${host} plugin; run \`mmi-cli plugin heal\`.`);
11823
+ } else if (surfaceToken(surface) === "jervcode") {
11824
+ console.error("[mmi-guard] Could not inspect the active JervCode plugin; run `mmi-cli plugin heal`.");
11972
11825
  process.exitCode = 1;
11973
11826
  } else {
11974
11827
  process.exitCode = 0;
@@ -11977,7 +11830,7 @@ async function runGuard(readOrigin) {
11977
11830
  }
11978
11831
  async function runPluginHeal(surface = detectSurface(process.env)) {
11979
11832
  const token = surfaceToken(surface);
11980
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11833
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11981
11834
  console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
11982
11835
  return;
11983
11836
  }
@@ -11994,17 +11847,15 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
11994
11847
  return;
11995
11848
  }
11996
11849
  const descriptor = PLUGIN_SURFACE_HEAL[token];
11997
- const cursorResult = token === "cursor" ? await installCursorPluginCheckout() : void 0;
11998
- if (cursorResult) console.log(` \u21BB ${cursorResult.detail}`);
11999
- const applied = cursorResult ? cursorResult.ok : await applyPluginHeal(surface, console.log, { force: true });
11850
+ const applied = await applyPluginHeal(surface, console.log, { force: true });
12000
11851
  const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
12001
11852
  if (healed) {
12002
11853
  console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.`);
12003
11854
  } else {
12004
11855
  process.exitCode = 1;
12005
- const refSupported = token === "cursor" ? false : await marketplaceAddRefSupported(token);
12006
- const recovery = token === "cursor" ? descriptor.recovery : refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
12007
- const note = token === "cursor" ? "" : refAbsenceNote(token, refSupported);
11856
+ const refSupported = await marketplaceAddRefSupported(token);
11857
+ const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
11858
+ const note = refAbsenceNote(token, refSupported);
12008
11859
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
12009
11860
  ${recovery}${note}${pluginReadGrantNote()}`);
12010
11861
  }
@@ -12017,7 +11868,6 @@ var SESSION_ID_ENV_VARS = [
12017
11868
  "CLAUDE_CODE_SESSION_ID",
12018
11869
  "CODEX_SESSION_ID",
12019
11870
  "CODEX_THREAD_ID",
12020
- "CURSOR_SESSION_ID",
12021
11871
  "HERMES_SESSION_ID",
12022
11872
  "PI_SESSION_ID",
12023
11873
  // #6193: lane runners spawn the CLI through the host's extension shell operations, which strip
@@ -16436,8 +16286,18 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16436
16286
  if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
16437
16287
  const version = tag.replace(/^v/, "");
16438
16288
  let foldNoteExtra = "";
16289
+ const timings = [];
16290
+ const timed = async (label, fn) => {
16291
+ const t0 = (deps.now ?? Date.now)();
16292
+ try {
16293
+ return await fn();
16294
+ } finally {
16295
+ timings.push(`${label} ${(((deps.now ?? Date.now)() - t0) / 1e3).toFixed(1)}s`);
16296
+ }
16297
+ };
16298
+ const withTimings = (note) => `${note}; fold timings: ${timings.join(", ")}`;
16439
16299
  if (model === "hub-serverless") {
16440
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]);
16300
+ await timed("release-distribution prepare", () => deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]));
16441
16301
  if (verifyRelease) {
16442
16302
  await refuseDivergentPublishedNpmPack(deps, version);
16443
16303
  } else {
@@ -16445,10 +16305,10 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16445
16305
  if (restored) foldNoteExtra = `; ${restored}`;
16446
16306
  }
16447
16307
  } else {
16448
- const refresh = await refreshSharedInstallerDeps(deps);
16308
+ const refresh = await timed("installer refresh", () => refreshSharedInstallerDeps(deps));
16449
16309
  if (refresh) foldNoteExtra = `; ${refresh}`;
16450
- await installAppFoldDeps(deps);
16451
- await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
16310
+ await timed("npm ci", () => installAppFoldDeps(deps));
16311
+ await timed("npm version", () => deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]));
16452
16312
  const patch = Number(version.split(".")[2] ?? "0");
16453
16313
  if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
16454
16314
  await alignExistingHotfixReleaseMarker(deps, version);
@@ -16467,12 +16327,12 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16467
16327
  );
16468
16328
  }
16469
16329
  const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
16470
- if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed${foldNoteExtra}`;
16330
+ if (!staged) return withTimings(`version fold: manifests already at ${version} \u2014 nothing committed${foldNoteExtra}`);
16471
16331
  await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
16472
16332
  if (model === "hub-serverless" && verifyRelease) {
16473
- await deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]);
16333
+ await timed("release-distribution verify", () => deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]));
16474
16334
  }
16475
- return `version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}`;
16335
+ return withTimings(`version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}`);
16476
16336
  }
16477
16337
  function foldPortManualCommand(model, version) {
16478
16338
  return model === "hub-serverless" ? `node scripts/release-distribution.mjs prepare ${version}` : `npm version ${version} --no-git-tag-version --allow-same-version`;
@@ -17268,10 +17128,10 @@ var rollout_plan_default = {
17268
17128
  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)."
17269
17129
  },
17270
17130
  baseline: {
17271
- version: "4.5.86",
17272
- tag: "v4.5.86",
17273
- commit: "6485afaf5af8",
17274
- npm: "@mutmutco/cli@4.5.86"
17131
+ version: "4.5.87",
17132
+ tag: "v4.5.87",
17133
+ commit: "c8eaaec639a7",
17134
+ npm: "@mutmutco/cli@4.5.87"
17275
17135
  },
17276
17136
  exitCriterion: "fleet-n-of-n",
17277
17137
  hubOnlyShortcut: "forbidden",
@@ -17288,14 +17148,14 @@ var rollout_plan_default = {
17288
17148
  repo: "mutmutco/mmi-hub",
17289
17149
  role: "canary",
17290
17150
  schedule: "train",
17291
- v3Target: "v4.5.86"
17151
+ v3Target: "v4.5.87"
17292
17152
  }
17293
17153
  ],
17294
17154
  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.",
17295
17155
  rollback: {
17296
17156
  independent: true,
17297
- mechanism: "npm dist-tag latest -> 4.5.86 and redeploy the Hub Lambda from tag v4.5.86 (6485afaf5af8); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17298
- v3Target: "v4.5.86 (@mutmutco/cli@4.5.86, tag commit 6485afaf5af8 \u2014 last known-good release carrying the repo-index v4-only contract)"
17157
+ mechanism: "npm dist-tag latest -> 4.5.87 and redeploy the Hub Lambda from tag v4.5.87 (c8eaaec639a7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17158
+ v3Target: "v4.5.87 (@mutmutco/cli@4.5.87, tag commit c8eaaec639a7 \u2014 last known-good release carrying the repo-index v4-only contract)"
17299
17159
  }
17300
17160
  },
17301
17161
  {
@@ -19609,7 +19469,7 @@ var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.par
19609
19469
  var TRAIN_CHECK_ATTEMPTS = 40;
19610
19470
  var TRAIN_QUEUED_CHECK_ATTEMPTS = 80;
19611
19471
  var TRAIN_CHECK_DELAY_MS = 15e3;
19612
- var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done", "guard", "verdict"]);
19472
+ var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
19613
19473
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
19614
19474
  var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
19615
19475
  async function correlateRun(deps, args) {
@@ -19963,7 +19823,7 @@ async function alignRcForward(deps, ctx, tag) {
19963
19823
  return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
19964
19824
  }
19965
19825
  var FLEET_FOREIGN_VERDICT_GRACE_ATTEMPTS = 3;
19966
- function resolveContextState(context, checkRuns, statuses) {
19826
+ function resolveContextState(context, checkRuns, statuses, opts = {}) {
19967
19827
  let sawFailure = false;
19968
19828
  let sawSuccess = false;
19969
19829
  let sawPending = false;
@@ -19982,11 +19842,12 @@ function resolveContextState(context, checkRuns, statuses) {
19982
19842
  else if (s.state === "failure" || s.state === "error") sawFailure = true;
19983
19843
  else sawPending = true;
19984
19844
  }
19845
+ if (sawSuccess && opts.successWins) return "success";
19985
19846
  if (sawPending) return "pending";
19986
19847
  if (sawSuccess) return "success";
19987
19848
  return sawFailure ? "failed" : "pending";
19988
19849
  }
19989
- async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
19850
+ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince, opts = {}) {
19990
19851
  if (required.length === 0) {
19991
19852
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
19992
19853
  }
@@ -20016,6 +19877,8 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20016
19877
  let workflowRunNote = "";
20017
19878
  let workflowRunEverSeen = false;
20018
19879
  let hadQueuedWorkflowRun = false;
19880
+ let freshActiveRun = false;
19881
+ let anyActiveRun = false;
20019
19882
  const everObserved = /* @__PURE__ */ new Set();
20020
19883
  const autoSatisfied = /* @__PURE__ */ new Set();
20021
19884
  let fleetForeignStreak = 0;
@@ -20048,7 +19911,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20048
19911
  "--limit",
20049
19912
  "100",
20050
19913
  "--json",
20051
- "databaseId,status,conclusion,headSha"
19914
+ "databaseId,status,conclusion,headSha,createdAt"
20052
19915
  ]);
20053
19916
  const parsedWorkflowRuns = JSON.parse(workflowOut);
20054
19917
  if (!Array.isArray(parsedWorkflowRuns)) throw new Error("workflow-runs response was not an array");
@@ -20057,6 +19920,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20057
19920
  workflowRunRead = "found";
20058
19921
  workflowRunEverSeen = true;
20059
19922
  const activeRun = matchingRuns.find((run) => run.status === "queued" || run.status === "in_progress");
19923
+ freshActiveRun = freshThreshold !== void 0 && matchingRuns.some((run) => {
19924
+ if (!["queued", "in_progress", "waiting", "pending"].includes(String(run.status))) return false;
19925
+ const t = Date.parse(String(run.createdAt ?? ""));
19926
+ return Number.isFinite(t) && t >= freshThreshold;
19927
+ });
19928
+ if (freshActiveRun) hadQueuedWorkflowRun = true;
19929
+ anyActiveRun = matchingRuns.some((run) => ["queued", "in_progress", "waiting", "pending"].includes(String(run.status)));
20060
19930
  if (activeRun) {
20061
19931
  if (activeRun.status === "queued") hadQueuedWorkflowRun = true;
20062
19932
  workflowRunNote = activeRun.status === "queued" ? "Actions run is queued behind busy runners" : "Actions run is in progress";
@@ -20065,10 +19935,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20065
19935
  }
20066
19936
  } else {
20067
19937
  workflowRunRead = "empty";
19938
+ freshActiveRun = false;
19939
+ anyActiveRun = false;
20068
19940
  workflowRunNote = "";
20069
19941
  }
20070
19942
  } catch (e) {
20071
19943
  workflowRunRead = "failed";
19944
+ freshActiveRun = false;
19945
+ anyActiveRun = false;
20072
19946
  workflowRunNote = `workflow-run lookup failed: ${e.message || String(e)}`;
20073
19947
  }
20074
19948
  for (const c of required) {
@@ -20080,7 +19954,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20080
19954
  }
20081
19955
  }
20082
19956
  const pending = required.filter((c) => !autoSatisfied.has(c));
20083
- const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
19957
+ const holdStale = freshThreshold !== void 0 && (attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS || freshActiveRun);
20084
19958
  const states = pending.map((c) => {
20085
19959
  if (fleetPosted && c === FLEET_LOCKSTEP_CONTEXT) {
20086
19960
  const resolution = resolveFleetVerdictState(fleetPosted, statuses);
@@ -20092,6 +19966,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20092
19966
  fleetForeignStreak = 0;
20093
19967
  return [c, resolution.state];
20094
19968
  }
19969
+ if (opts.exactCommitGreenSuffices) {
19970
+ const state = resolveContextState(c, checkRuns, statuses, { successWins: true });
19971
+ const inGrace = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
19972
+ return [c, state === "failed" && (anyActiveRun || inGrace) ? "pending" : state];
19973
+ }
20095
19974
  if (holdStale) {
20096
19975
  const runsFor = checkRuns.filter((r) => r.name === c);
20097
19976
  if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
@@ -21372,6 +21251,10 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21372
21251
  };
21373
21252
  }
21374
21253
  try {
21254
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
21255
+ if (isBundleOnlySuiteMember(ctx.repo, meta.suite)) {
21256
+ return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
21257
+ }
21375
21258
  await deps.run("git", ["fetch", "origin", "rc"]);
21376
21259
  const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
21377
21260
  if (rcNow !== releasedRcSha) {
@@ -21904,14 +21787,6 @@ function installedClaudePluginVersion() {
21904
21787
  return void 0;
21905
21788
  }
21906
21789
  }
21907
- function manifestVersion(path2) {
21908
- try {
21909
- const manifest = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
21910
- return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
21911
- } catch {
21912
- return void 0;
21913
- }
21914
- }
21915
21790
  function readHermesPluginEvidence(env = process.env) {
21916
21791
  const host = hermesConfigRoot(env);
21917
21792
  const root = hermesPluginRoot(env);
@@ -21963,9 +21838,6 @@ function readHermesPluginEvidence(env = process.env) {
21963
21838
  function installedSurfacePluginVersion(surface) {
21964
21839
  const token = surfaceToken(surface);
21965
21840
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
21966
- if (token === "cursor") {
21967
- return manifestVersion((0, import_node_path18.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
21968
- }
21969
21841
  if (token === "jervcode") {
21970
21842
  return installedJervCodePackageVersion();
21971
21843
  }
@@ -23747,17 +23619,23 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23747
23619
  async (current) => {
23748
23620
  if (!current) return { value: { ledger: void 0, notes } };
23749
23621
  const phases = { ...current.phases };
23750
- for (const phase of ["deploy", "publish"]) {
23622
+ for (const phase of input.phases ?? ["deploy", "publish"]) {
23751
23623
  const rec = phases[phase];
23752
23624
  if (rec.updatedAt === "never") continue;
23753
23625
  if (rec.runId == null) {
23754
- if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 the live deploy/publish re-derivation below decides the leg`);
23626
+ if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 live phase evidence decides the leg`);
23755
23627
  continue;
23756
23628
  }
23757
- if (rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
23629
+ if (phase === "ordinaryCi" && rec.runRepo != null && rec.runRepo !== input.repo) {
23630
+ throw new ReleaseLedgerError("invalid-record", `the persisted ordinaryCi run ${rec.runId} belongs to ${rec.runRepo}, not the release repo ${input.repo} \u2014 refusing unrelated run evidence (#7807)`);
23631
+ }
23632
+ if (phase === "ordinaryCi" && rec.sha != null && rec.sha.toLowerCase() !== input.sha.toLowerCase()) {
23633
+ throw new ReleaseLedgerError("tag-sha-mismatch", `the persisted ordinaryCi boundary is for ${rec.sha.slice(0, 12)} but the active release tag is ${input.sha.slice(0, 12)} \u2014 refusing unrelated SHA evidence (#7807)`);
23634
+ }
23635
+ if (phase === "ordinaryCi" && rec.runProvenance !== "sha" || phase !== "ordinaryCi" && rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
23758
23636
  throw new ReleaseLedgerError(
23759
23637
  "invalid-record",
23760
- `the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'|'dispatch' required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
23638
+ `the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'${phase === "ordinaryCi" ? "" : "|'dispatch'"} required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
23761
23639
  );
23762
23640
  }
23763
23641
  const runRepo = rec.runRepo ?? input.repo;
@@ -23792,7 +23670,8 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23792
23670
  }
23793
23671
  if (meta.conclusion === "success") {
23794
23672
  if (rec.state !== "complete") {
23795
- phases[phase] = { ...rec, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
23673
+ const { error: _error, ...withoutPriorError } = rec;
23674
+ phases[phase] = { ...withoutPriorError, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
23796
23675
  notes.push(`${phase}: ${liveLabel} re-verified green live`);
23797
23676
  }
23798
23677
  continue;
@@ -25070,6 +24949,19 @@ async function runTrainDoctor(input) {
25070
24949
  try {
25071
24950
  let ledger = parseReleaseLedger(ledgerRaw);
25072
24951
  let derived = deriveReleasePhaseStatus(ledger);
24952
+ const ordinaryCi = ledger.phases.ordinaryCi;
24953
+ if (healLedger && ordinaryCi.state === "failed" && ordinaryCi.runId != null) {
24954
+ const outcome = await reverifyPersistedRunPhases(train, ledgerPath, {
24955
+ repo: ledger.repo,
24956
+ tag: ledger.tag,
24957
+ tagSha: ledger.tagSha
24958
+ }, { sha: ledger.tagSha, repo: ledger.repo, phases: ["ordinaryCi"] });
24959
+ if (outcome.notes.some((note) => note.startsWith("ordinaryCi:") && note.includes("re-verified green live"))) {
24960
+ add({ code: "ledger-ordinary-ci-rerun", severity: "healed", source: "origin", title: `the exact ordinaryCi run ${ordinaryCi.runId} for ${ledger.tag} is green on its recorded release SHA (#7807)`, remedy: "nothing \u2014 the ordinaryCi ledger leg now records the live green run" });
24961
+ ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
24962
+ derived = deriveReleasePhaseStatus(ledger);
24963
+ }
24964
+ }
25073
24965
  const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
25074
24966
  if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
25075
24967
  const run = await deps.healRunLegs(train, cwd, deployModel);
@@ -26174,7 +26066,9 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
26174
26066
  const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
26175
26067
  try {
26176
26068
  deps.warn?.("Release: Checking the package before publishing");
26069
+ const dryRunStart = (deps.now ?? Date.now)();
26177
26070
  await verifyPublishDryRun(deps, ctx, meta, deployModel);
26071
+ deps.warn?.(`Release: publish dry run took ${(((deps.now ?? Date.now)() - dryRunStart) / 1e3).toFixed(1)}s`);
26178
26072
  } catch (e) {
26179
26073
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
26180
26074
  }
@@ -26412,7 +26306,7 @@ async function runRcandResumeLocked(deps, options) {
26412
26306
  await requireCleanTree(deps);
26413
26307
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
26414
26308
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
26415
- const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", false, deps.suiteCut);
26309
+ const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", options.suiteSolo === true, deps.suiteCut);
26416
26310
  if (suiteRefusal) throw new Error(`rcand --resume: ${suiteRefusal}`);
26417
26311
  const branchHints = await loadReleaseTrackBranchHints(deps);
26418
26312
  if (isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct") {
@@ -27725,7 +27619,6 @@ async function listWorkflowPaths(deps, repo, branch) {
27725
27619
  return e?.status === 404 ? [] : void 0;
27726
27620
  }
27727
27621
  }
27728
- var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
27729
27622
  function gateWorkflowFiles(prWorkflowPaths) {
27730
27623
  const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
27731
27624
  return files.length ? files : DEFAULT_GATE_FILES;
@@ -28062,8 +27955,7 @@ async function auditRepoCi(repo, callerDeps) {
28062
27955
  }
28063
27956
  }
28064
27957
  if (explicitNoCi) {
28065
- const resolved = await getEmittedPrContexts();
28066
- const emitted = resolved?.filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
27958
+ const emitted = await getEmittedPrContexts();
28067
27959
  if (emitted && emitted.length > 0) {
28068
27960
  checks.push({
28069
27961
  ok: false,
@@ -28194,7 +28086,7 @@ async function auditRepoCi(repo, callerDeps) {
28194
28086
  if (hasRequiredChecks) {
28195
28087
  const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
28196
28088
  const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
28197
- const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job) && !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
28089
+ const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
28198
28090
  checks.push({
28199
28091
  ok: unblocking.length === 0,
28200
28092
  label: UNBLOCKING_GATE_JOBS_LABEL,
@@ -28217,8 +28109,7 @@ async function auditRepoCi(repo, callerDeps) {
28217
28109
  for (const ref of releaseBranches) {
28218
28110
  emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
28219
28111
  }
28220
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28221
- const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore });
28112
+ const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
28222
28113
  checks.push({
28223
28114
  ok: unreachable.length === 0,
28224
28115
  label: RELEASE_BRANCH_REACHABLE_LABEL,
@@ -28685,8 +28576,7 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
28685
28576
  if (authority.coversReleaseBranches && authority.source !== "registry META requiredChecks") {
28686
28577
  const track = resolveReleaseTrack(meta, void 0, repo);
28687
28578
  const releaseBranches = branchesForTrack(track).filter((b) => b !== baseBranch);
28688
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28689
- const requiredForReleaseBranches = authority.contexts.filter((context) => !ignore.has(context));
28579
+ const requiredForReleaseBranches = authority.contexts.filter((context) => !TRAIN_PR_ONLY_CONTEXTS.has(context));
28690
28580
  for (const branch of releaseBranches) {
28691
28581
  if (await branchPresence(deps, repo, branch) !== true) continue;
28692
28582
  const emittedThere = await resolveEmittedPrContexts(deps, repo, branch);
@@ -30507,7 +30397,7 @@ function trainPlan(command, options = {}) {
30507
30397
  { label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
30508
30398
  { label: "tag release and publish GitHub Release", gated: true },
30509
30399
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
30510
- { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true },
30400
+ ...!isBundleOnlySuiteMember(options.repo ?? "", options.suite) ? [{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true }] : [],
30511
30401
  { label: "roll development forward and align rc to the released main", gated: true },
30512
30402
  { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli oracle org project sync-info --apply", gated: true }
30513
30403
  ];
@@ -30515,6 +30405,8 @@ function trainPlan(command, options = {}) {
30515
30405
  return [
30516
30406
  authority,
30517
30407
  { label: "verify current branch is rc", gated: true },
30408
+ // #7783: the version the apply will mint, read from the rc tag at origin/rc's head (never a stale higher cycle).
30409
+ { label: `resolve the release tag from the rc head (${options.releaseTagError ? `error: ${options.releaseTagError}` : `releaseTag=${options.releaseTag ?? "(unresolved)"}`})`, command: "node scripts/next-version.mjs release", gated: true },
30518
30410
  { label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
30519
30411
  { label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
30520
30412
  { label: "preflight GitHub Actions hosted job start (billing/spending) before minting a tag", command: "train dispatches actions-job-start-canary.yml on MMI-Hub (#5604); exact-run --retry-publish skips this", gated: true },
@@ -31409,7 +31301,7 @@ var SETTABLE_VAR_HINTS = {
31409
31301
  requiredGcpApis: "comma-string",
31410
31302
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
31411
31303
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
31412
- tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}}; use {artifact} in argv when required; a secretOutput is master-only',
31304
+ tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
31413
31305
  secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
31414
31306
  edgeDomains: "JSON {dev,rc,main} domain map",
31415
31307
  statusOptions: "JSON name\u2192id map",
@@ -31440,6 +31332,18 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
31440
31332
  }
31441
31333
  return null;
31442
31334
  }
31335
+ function tenantTaskWriteRefusal(existing, patch) {
31336
+ const remove2 = Array.isArray(patch.tenantTasksRemove) ? patch.tenantTasksRemove : [];
31337
+ const stored = existing?.tenantTasks ?? {};
31338
+ const slug = existing?.slug ?? "this project";
31339
+ const missing = remove2.filter((name) => stored[name] === void 0);
31340
+ if (missing.length > 0) return `org project set: --unset-task ${missing.join(", ")} not declared in ${slug}'s tenantTasks (${Object.keys(stored).length} declared) \u2014 nothing to remove`;
31341
+ const sent = patch.tenantTasks;
31342
+ if (sent === null || sent === void 0) return null;
31343
+ const merged = /* @__PURE__ */ new Set([...Object.keys(stored), ...Object.keys(sent)]);
31344
+ if (merged.size > 20) return `org project set: ${slug} would hold ${merged.size} tenantTasks (${Object.keys(stored).length} declared today); the cap is 20 \u2014 retire some first with --unset-task <name>`;
31345
+ return null;
31346
+ }
31443
31347
  function secretRemovalRefusal(existing, patch) {
31444
31348
  const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
31445
31349
  if (remove2.length === 0) return null;
@@ -31614,12 +31518,26 @@ function buildProjectSetPatch(input) {
31614
31518
  }
31615
31519
  patch.secretsRemove = keys;
31616
31520
  }
31521
+ if (input.unsetTasks?.length) {
31522
+ if (patch.tenantTasks !== void 0) {
31523
+ throw new Error("org project set: --unset-task cannot be combined with --var tenantTasks or --unset tenantTasks \u2014 pass one or the other");
31524
+ }
31525
+ const names = [];
31526
+ for (const raw of input.unsetTasks) {
31527
+ const name = raw.trim();
31528
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) {
31529
+ throw new Error(`org project set: --unset-task must be a task name (lowercase kebab-case) \u2014 got ${JSON.stringify(raw)}`);
31530
+ }
31531
+ if (!names.includes(name)) names.push(name);
31532
+ }
31533
+ patch.tenantTasksRemove = names;
31534
+ }
31617
31535
  if (input.clearWebProfile) {
31618
31536
  patch.oauth = null;
31619
31537
  patch.edgeDomains = null;
31620
31538
  }
31621
31539
  if (Object.keys(patch).length === 0) {
31622
- throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, and/or --clear-web-profile");
31540
+ throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, --unset-task NAME, and/or --clear-web-profile");
31623
31541
  }
31624
31542
  return patch;
31625
31543
  }
@@ -35796,8 +35714,24 @@ async function readSuiteStatus(deps) {
35796
35714
  })));
35797
35715
  if (!deps.leadTags || !deps.registryAt) return { suite: "mm-work", members: rows };
35798
35716
  const tags = await deps.leadTags().catch(() => []);
35799
- const latest = (rc) => tags.filter((t) => /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(t) && t.includes("-rc.") === rc).sort((a, b) => compareVersions(b, a))[0];
35800
- const leadTag = { rc: latest(true), main: latest(false) };
35717
+ const latest = (rc2) => tags.filter((t) => /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(t) && t.includes("-rc.") === rc2).sort((a, b) => compareVersions(b, a))[0];
35718
+ let rc;
35719
+ let rcLabel = "n/a";
35720
+ const rcTags = tags.filter((t) => /^v\d+\.\d+\.\d+-rc\.\d+$/.test(t)).sort((a, b) => compareVersions(b, a));
35721
+ if (rcTags.length) {
35722
+ const rcHead = await deps.head(rows[0].repo, "rc").catch(() => void 0);
35723
+ if (rcHead === void 0) rcLabel = "unknown";
35724
+ else {
35725
+ for (const t of rcTags) {
35726
+ if ((await deps.tagCommit(rows[0].repo, t).catch(() => void 0))?.toLowerCase() === rcHead?.toLowerCase()) {
35727
+ rc = t;
35728
+ break;
35729
+ }
35730
+ }
35731
+ rcLabel = rc ?? "none at head";
35732
+ }
35733
+ }
35734
+ const leadTag = { rc, main: latest(false) };
35801
35735
  const lead = rows[0].stages;
35802
35736
  for (const stage of ["rc", "main"]) {
35803
35737
  const tag = leadTag[stage];
@@ -35813,7 +35747,7 @@ async function readSuiteStatus(deps) {
35813
35747
  }
35814
35748
  }
35815
35749
  const recorded = leadTag.main && deps.leadReleaseBody ? parseSuiteMapping(await deps.leadReleaseBody(leadTag.main).catch(() => null)) : void 0;
35816
- return { suite: "mm-work", lead: { dev: lead[0].head, rc: leadTag.rc ?? "n/a", main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
35750
+ return { suite: "mm-work", lead: { dev: lead[0].head, rc: rcLabel, main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
35817
35751
  }
35818
35752
  var short = (v) => /^[a-f0-9]{40}$/i.test(v) ? v.slice(0, 12) : v;
35819
35753
  var proofText = (p) => p ? p.state === "proven" ? "proven" : p.state === "mismatch" ? `mismatch (${p.detail})` : `unprovable (${p.detail})` : "";
@@ -35855,7 +35789,8 @@ async function setSuiteHold(deps, member, hold) {
35855
35789
  const after = await deps.read(slug);
35856
35790
  if (!after.ok || !after.project) throw new Error(`${found.repo}: write sent but read-back failed; verify with org project get`);
35857
35791
  const got = after.project.suiteHold ?? null;
35858
- if (JSON.stringify(got) !== JSON.stringify(hold)) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
35792
+ const same2 = got === null || hold === null ? got === hold : got.reason === hold.reason && got.since === hold.since;
35793
+ if (!same2) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
35859
35794
  if (track && after.project.releaseTrack !== track) throw new Error(`${found.repo}: releaseTrack changed from ${track} to ${after.project.releaseTrack ?? "unset"} by the write; restore it with org project set --release-track ${track}`);
35860
35795
  return hold ? `${found.repo}: HELD (${hold.reason})` : `${found.repo}: hold cleared`;
35861
35796
  }
@@ -36037,7 +35972,7 @@ async function carriedPins(deps, member, bundles) {
36037
35972
  }
36038
35973
  throw new Error(`${member.repo}: nothing new since rc, but no pinned MM-Work bundle carries development ${member.development.slice(0, 12)}; pin it before the suite cut`);
36039
35974
  }
36040
- function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
35975
+ function abandonableCut(recordPath, ownerGo) {
36041
35976
  const record = readSuiteCut(recordPath);
36042
35977
  if (!record) throw new Error("no suite cut record to abandon");
36043
35978
  if (record.leadReleased || record.members.some((m) => m.released)) {
@@ -36046,6 +35981,19 @@ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Dat
36046
35981
  if (record.pinPr && !ownerGo) {
36047
35982
  throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go), release it (suite release), or retire it with suite abandon --owner-go`);
36048
35983
  }
35984
+ return record;
35985
+ }
35986
+ async function suiteMarkAbandoned(deps, ownerGo = false) {
35987
+ const record = abandonableCut(deps.recordPath, ownerGo);
35988
+ const tags = [
35989
+ ...record.members.flatMap((m) => m.tag && m.sha ? [{ repo: m.repo, tag: m.tag, sha: m.sha }] : []),
35990
+ ...record.leadRcTag && record.leadRcSha ? [{ repo: SUITE_REPO, tag: record.leadRcTag, sha: record.leadRcSha }] : []
35991
+ ];
35992
+ for (const t of tags) await deps.markAbandoned(t.repo, t.tag, t.sha);
35993
+ return tags.map((t) => `${t.repo}@${t.tag}`);
35994
+ }
35995
+ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
35996
+ const record = abandonableCut(recordPath, ownerGo);
36049
35997
  const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}-${now.toISOString().replace(/[:.]/g, "")}.json`;
36050
35998
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
36051
35999
  (0, import_node_fs29.renameSync)(recordPath, archive);
@@ -36932,12 +36880,20 @@ ${lines2.join("\n")}
36932
36880
  Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
36933
36881
  }
36934
36882
  var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
36935
- async function findHotfixPickGaps(deps, sources, tolerated) {
36936
- if (sources.length === 0) return [];
36883
+ async function scanHotfixPickGaps(deps, sources, tolerated, rcRef) {
36884
+ if (sources.length === 0) return { gaps: [], rcCarried: 0 };
36937
36885
  const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
36938
36886
  const carried = /* @__PURE__ */ new Set();
36939
36887
  const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
36940
36888
  for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
36889
+ const notOnRc = /* @__PURE__ */ new Set();
36890
+ const rcPicked = /* @__PURE__ */ new Set();
36891
+ if (rcRef) {
36892
+ (await deps.run("git", ["log", "--no-merges", "--format=%H", `${rcRef}..origin/development`])).split("\n").map((s) => s.trim().toLowerCase()).filter(Boolean).forEach((s) => notOnRc.add(s));
36893
+ const rcOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", `origin/development..${rcRef}`]);
36894
+ for (const m of rcOnly.matchAll(CHERRY_PICKED_FROM)) rcPicked.add(m[1].toLowerCase());
36895
+ }
36896
+ let rcCarried = 0;
36941
36897
  const gaps = [];
36942
36898
  const devOnly = (await deps.run("git", ["log", "--no-merges", "--reverse", "--format=%H%x09%s", "origin/main..origin/development"])).split("\n").map((s) => s.trim()).filter(Boolean);
36943
36899
  for (const line of devOnly) {
@@ -36945,11 +36901,37 @@ async function findHotfixPickGaps(deps, sources, tolerated) {
36945
36901
  if (!sha) continue;
36946
36902
  const lower = sha.toLowerCase();
36947
36903
  if (namedShas.has(lower) || carried.has(lower)) continue;
36904
+ if (rcRef && (!notOnRc.has(lower) || rcPicked.has(lower))) {
36905
+ rcCarried++;
36906
+ continue;
36907
+ }
36948
36908
  const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
36949
36909
  if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
36950
36910
  gaps.push({ sha, subject: subjectParts.join(" ") });
36951
36911
  }
36952
- return gaps;
36912
+ return { gaps, rcCarried };
36913
+ }
36914
+ async function resolveActiveRcCandidate(deps) {
36915
+ try {
36916
+ await deps.run("git", ["rev-parse", "--verify", "--quiet", "origin/rc"]);
36917
+ } catch {
36918
+ return { reason: "origin/rc does not exist" };
36919
+ }
36920
+ try {
36921
+ const rcTags = (await deps.run("git", ["tag", "--points-at", "origin/rc", "--list", "v*"])).split("\n").map((s) => s.trim()).filter((t) => /^v\d+\.\d+\.\d+-rc\.\d+$/.test(t));
36922
+ const tag = rcTags.sort((a, b) => Number(a.split("-rc.")[1]) - Number(b.split("-rc.")[1])).pop();
36923
+ if (!tag) return { reason: "no rc tag points at the origin/rc head, so it is not an active candidate" };
36924
+ if ((await deps.run("git", ["ls-remote", "origin", `refs/abandoned-rc/${tag}`])).trim()) {
36925
+ return { reason: `${tag} was abandoned (marker refs/abandoned-rc/${tag} on origin), so it is not an active candidate` };
36926
+ }
36927
+ const released = tag.replace(/-rc\.\d+$/, "");
36928
+ if ((await deps.run("git", ["tag", "--list", released])).trim()) {
36929
+ return { reason: `${released} is already released, so ${tag} is no longer a pending candidate` };
36930
+ }
36931
+ return { ref: "origin/rc", tag };
36932
+ } catch (e) {
36933
+ return { reason: `the origin/rc candidate could not be read (${e.message ?? e})` };
36934
+ }
36953
36935
  }
36954
36936
  function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
36955
36937
  const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
@@ -37169,7 +37151,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
37169
37151
  const label = sources.map((s) => s.label).join(", ");
37170
37152
  const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
37171
37153
  const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
37172
- const pickGaps = await findHotfixPickGaps(deps, sources, pickTolerated);
37154
+ let rcCandidate;
37155
+ if (resolveReleaseTrack(meta, void 0, ctx.repo) === "full") {
37156
+ const found = await resolveActiveRcCandidate(deps);
37157
+ if ("reason" in found) notes.push(`pick-completeness guard checked against origin/main only: ${found.reason} (#7799)`);
37158
+ else rcCandidate = found;
37159
+ }
37160
+ const { gaps: pickGaps, rcCarried } = await scanHotfixPickGaps(deps, sources, pickTolerated, rcCandidate?.ref);
37161
+ if (rcCandidate && rcCarried > 0) {
37162
+ notes.push(`pick-completeness: ${rcCarried} development commit(s) carried by rc candidate ${rcCandidate.tag}, not counted as gaps (#7799)`);
37163
+ }
37173
37164
  const unacked = pickGaps.filter((g) => {
37174
37165
  const sha = g.sha.toLowerCase();
37175
37166
  return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
@@ -37262,7 +37253,9 @@ async function runHotfixStartLocked(deps, options, doctor) {
37262
37253
 
37263
37254
  <!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
37264
37255
 
37265
- <!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
37256
+ <!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}${rcCandidate && rcCarried > 0 ? `
37257
+
37258
+ <!-- mmi-hotfix-rc-carried: ${rcCarried} commits carried by ${rcCandidate.tag} -->` : ""}
37266
37259
 
37267
37260
  Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
37268
37261
  ]));
@@ -37502,7 +37495,7 @@ async function waitForHotfixRequiredChecks(deps, ctx, mergedSha, required, fresh
37502
37495
  const recover = hooks.recover ?? transientRecoveryForFailedPhase;
37503
37496
  let checks;
37504
37497
  try {
37505
- checks = await wait(deps, ctx, mergedSha, required, freshSince);
37498
+ checks = await wait(deps, ctx, mergedSha, required, freshSince, { exactCommitGreenSuffices: true });
37506
37499
  } catch (e) {
37507
37500
  const failureText = e instanceof Error ? e.message : String(e);
37508
37501
  let recovery;
@@ -37531,7 +37524,7 @@ release ledger: ${recoveryFailure} - manual intervention required`);
37531
37524
 
37532
37525
  ${recovery.note}`);
37533
37526
  try {
37534
- checks = await wait(deps, ctx, mergedSha, required, void 0);
37527
+ checks = await wait(deps, ctx, mergedSha, required, void 0, { exactCommitGreenSuffices: true });
37535
37528
  } catch (e2) {
37536
37529
  const revalidationFailure = e2 instanceof Error ? e2.message : String(e2);
37537
37530
  const prior2 = (await ledger.load(anchors).catch(() => void 0))?.phases.ordinaryCi;
@@ -40048,6 +40041,30 @@ function releaseProgressVerdict(status, subject = "Release") {
40048
40041
  // src/suite-pin.ts
40049
40042
  var import_promises6 = require("node:fs/promises");
40050
40043
  var import_node_path34 = require("node:path");
40044
+ var SuitePinError = class _SuitePinError extends Error {
40045
+ diagnostic;
40046
+ constructor(repo, tag, error) {
40047
+ const e = error instanceof _SuitePinError ? { ...error.diagnostic } : error && typeof error === "object" ? error : {};
40048
+ const code = typeof e.code === "number" && Number.isInteger(e.code) && e.code > 0 && e.code <= 255 ? e.code : typeof e.code === "string" && /^(ETIMEDOUT|EACCES|EPERM|EBUSY|ENOENT|ENOSPC|EIO|EEXIST|ECONNRESET|ECONNREFUSED|EPIPE)$/.test(e.code) ? e.code : void 0;
40049
+ const signal = typeof e.signal === "string" && /^(SIGTERM|SIGKILL|SIGINT|SIGHUP|SIGABRT|SIGPIPE)$/.test(e.signal) ? e.signal : void 0;
40050
+ const hint = [error instanceof _SuitePinError ? error.message : e.message, e.stderr].filter((s) => typeof s === "string").map((s) => s.slice(0, 4096)).join("\n");
40051
+ const carriedCategory = error instanceof _SuitePinError && typeof e.category === "string" && ["timeout", "permission", "index-lock", "terminated", "exit", "unknown"].includes(e.category) ? e.category : void 0;
40052
+ const category = carriedCategory ?? (code === "ETIMEDOUT" ? "timeout" : code === "EACCES" || code === "EPERM" || /permission denied/i.test(hint) ? "permission" : /index\.lock.*(?:exists|unable|denied)|unable.*index\.lock/i.test(hint) ? "index-lock" : signal || e.killed === true ? "terminated" : typeof code === "number" ? "exit" : "unknown");
40053
+ const diagnostic = {
40054
+ category,
40055
+ ...code !== void 0 ? { code } : {},
40056
+ ...signal ? { signal } : {},
40057
+ ...typeof e.killed === "boolean" ? { killed: e.killed } : {},
40058
+ ...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) && e.durationMs >= 0 && e.durationMs <= 864e5 ? { durationMs: e.durationMs } : {},
40059
+ ...e.cleanupFailed === true ? { cleanupFailed: true } : {}
40060
+ };
40061
+ const safeRepo = /^[\w.-]+\/[\w.-]+$/.test(repo) && repo.length <= 200 ? repo : "[invalid repo]";
40062
+ const safeTag = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(tag) && tag.length <= 80 ? tag : "[invalid tag]";
40063
+ const reason = /closed without landing/.test(hint) ? "pin PR closed without landing" : /merged but development/.test(hint) ? "merged PR does not match the development pin" : /unreadable|META unreadable/.test(hint) ? "registry unreadable" : /not a declared MM-Work module/.test(hint) ? "not a declared MM-Work module" : /no .* bundle host/.test(hint) ? "bundle host missing" : diagnostic.category === "permission" ? "permission denied" : diagnostic.category;
40064
+ super(`suite pin PR FAILED for ${safeRepo} ${safeTag}: ${reason}; remedy: open/merge the ${SUITE_REPO} development pin PR for ${safeRepo} ${safeTag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`);
40065
+ this.diagnostic = diagnostic;
40066
+ }
40067
+ };
40051
40068
  async function openSuiteCutPinPr(deps, input) {
40052
40069
  if (!/^[a-f0-9]{40}$/.test(input.cut) || !input.pins.length) throw new Error("suite pin PR needs an anchored cut and at least one module");
40053
40070
  const ids = input.pins.map((p) => p.module);
@@ -40190,8 +40207,8 @@ async function openSuiteRestorePinPr(deps, input) {
40190
40207
  }
40191
40208
  async function openSuitePinPr(deps, input) {
40192
40209
  const { repo, tag, stage } = input;
40193
- const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`;
40194
40210
  let scratch = "";
40211
+ let failure;
40195
40212
  try {
40196
40213
  const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
40197
40214
  if (!get.content) throw new Error("MM-Work seed/modules.json was unreadable");
@@ -40256,9 +40273,19 @@ async function openSuitePinPr(deps, input) {
40256
40273
  await run("gh", ["pr", "close", old2.url, "--repo", SUITE_REPO, "--comment", `Superseded by ${pr} (${member.id} ${stage} at ${tag}).`]);
40257
40274
  return { status: "pending", pr, url: pin };
40258
40275
  } catch (e) {
40259
- throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
40276
+ failure = new SuitePinError(repo, tag, e);
40277
+ throw failure;
40260
40278
  } finally {
40261
- if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
40279
+ if (scratch) {
40280
+ try {
40281
+ await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
40282
+ } catch (e) {
40283
+ const cleanupFailure = failure ?? new SuitePinError(repo, tag, e);
40284
+ cleanupFailure.diagnostic.cleanupFailed = true;
40285
+ cleanupFailure.message += "; owned scratch cleanup also failed";
40286
+ if (!failure) throw cleanupFailure;
40287
+ }
40288
+ }
40262
40289
  }
40263
40290
  }
40264
40291
 
@@ -45813,7 +45840,7 @@ async function requireFreshTrainCli(commandName) {
45813
45840
  process.exit(converge.reexecCode);
45814
45841
  }
45815
45842
  var GH_TRAIN_TIMEOUT_MS = 3e4;
45816
- var GH_RUN_WATCH_TIMEOUT_MS = 90 * 6e4;
45843
+ var GH_RUN_WATCH_TIMEOUT_MS = 240 * 6e4;
45817
45844
  var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
45818
45845
  var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
45819
45846
  var NPM_TRAIN_TIMEOUT_MS = 6e4;
@@ -46235,10 +46262,43 @@ function emitTrainResult(label, output, out) {
46235
46262
  if (!out) printLine(output);
46236
46263
  else printLine(`Wrote ${label} result to ${out} (UTF-8, ${writeUtf8Receipt(out, output)} bytes)`);
46237
46264
  }
46265
+ var TrainPinFollowUpError = class extends Error {
46266
+ constructor(completed, error) {
46267
+ const failure = new SuitePinError(completed.repo, completed.tag, error);
46268
+ super(failure.message);
46269
+ this.completed = completed;
46270
+ this.diagnostic = failure.diagnostic;
46271
+ }
46272
+ completed;
46273
+ diagnostic;
46274
+ };
46238
46275
  function writeTrainFailureReceipt(commandName, json, out, error, suiteSolo = false) {
46239
- if (!out) return;
46240
- const output = json ? JSON.stringify({ command: `${commandName} --apply`, verdict: "failed", ...suiteSolo ? { suiteSolo: true, suitePin: { status: "failed", error: error.message } } : {}, error: error.message }, null, 2) : `mmi-cli ${commandName} --apply: FAILED \u2014 ${error.message}`;
46241
- emitTrainResult(`${commandName} apply`, output, out);
46276
+ const completed = error instanceof TrainPinFollowUpError ? error.completed : void 0;
46277
+ if (!out && !completed) return;
46278
+ const command = commandName.includes(" ") ? commandName : `${commandName} --apply`;
46279
+ const legs = completed?.releaseVerdict?.legs ?? (completed?.command === "hotfix-release" ? hotfixFollowUpLegs(completed.runs, completed.foldStatus, ledgerAlignmentFollowUpOutcome(completed.ledger), completed.foldNote, completed.deployNote) : []);
46280
+ const failedLeg = { leg: "suite-pin-pr", status: "failed", error: error.message };
46281
+ const failure = completed ? {
46282
+ ...completed,
46283
+ suiteSolo: true,
46284
+ suitePin: { status: "failed", error: error.message, diagnostic: error.diagnostic },
46285
+ failedLeg: "suite-pin-pr",
46286
+ ...completed.command === "hotfix-release" ? { legs: [...legs, failedLeg] } : {},
46287
+ releaseVerdict: {
46288
+ releaseUrl: null,
46289
+ ...completed.releaseVerdict,
46290
+ releaseStatus: "succeeded",
46291
+ followUpStatus: "failed",
46292
+ promoted: true,
46293
+ tag: completed.tag,
46294
+ legs: [...legs, failedLeg]
46295
+ }
46296
+ } : { command, ...suiteSolo ? { suiteSolo: true } : {} };
46297
+ const evidence = JSON.stringify({ ...failure, verdict: "failed", error: error.message }, null, 2);
46298
+ const output = json ? evidence : `mmi-cli ${command}: FAILED \u2014 ${completed ? `${completed.repo} ${completed.tag} published; suite-pin-pr follow-up FAILED \u2014 ` : ""}${error.message}${completed ? `
46299
+ Completed train evidence and failed follow-up:
46300
+ ${evidence}` : ""}`;
46301
+ emitTrainResult(command, output, out);
46242
46302
  }
46243
46303
  function renderTenantRedeploy(r) {
46244
46304
  return `mmi-cli devops runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
@@ -46264,8 +46324,15 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46264
46324
  const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
46265
46325
  return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
46266
46326
  }
46327
+ async function pinCompletedMember(completed) {
46328
+ try {
46329
+ return await pinSoloMember(completed.repo, completed.tag);
46330
+ } catch (e) {
46331
+ throw new TrainPinFollowUpError(completed, e);
46332
+ }
46333
+ }
46267
46334
  for (const commandName of ["rcand", "release"]) {
46268
- const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone (requires --owner-go; never valid on rcand)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
46335
+ const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone, or cut the suite lead alone (requires --owner-go; a member rcand is always refused)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
46269
46336
  const RELEASE_ONLY_FLAGS = [
46270
46337
  { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
46271
46338
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
@@ -46451,7 +46518,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46451
46518
  const raw2 = await withReleaseProgress(
46452
46519
  trainApplyDeps(),
46453
46520
  "Checking the existing release candidate",
46454
- (deps) => runRcandResume(deps, { watch: o.watch })
46521
+ (deps) => runRcandResume(deps, { watch: o.watch, suiteSolo: o.suiteSolo })
46455
46522
  );
46456
46523
  return emitRcandContinuation("rcand --resume", raw2, o);
46457
46524
  }
@@ -46467,7 +46534,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46467
46534
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46468
46535
  );
46469
46536
  const { followUpStatus, releaseVerdict } = buildResumeReleaseVerdict(result, projectInfoSync);
46470
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46537
+ const suitePin = o.suiteSolo ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict }) : void 0;
46471
46538
  if (suitePin?.status === "pending") {
46472
46539
  releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46473
46540
  releaseVerdict.followUpStatus = "pending";
@@ -46480,6 +46547,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46480
46547
  } catch (e) {
46481
46548
  applyTrainFollowUpExit("failed");
46482
46549
  consoleIo.err(`mmi-cli: Release: ${progressSubject(commandName)} blocked \u2014 see the error below`);
46550
+ writeTrainFailureReceipt(`${commandName} --resume`, o.json === true, o.out, e, o.suiteSolo === true);
46483
46551
  return failGraceful(`${commandName} --resume: ${e.message}`);
46484
46552
  }
46485
46553
  }
@@ -46533,8 +46601,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46533
46601
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46534
46602
  );
46535
46603
  }
46536
- const suitePin = o.suiteSolo && commandName === "release" ? await pinSoloMember(result.repo, result.tag) : void 0;
46537
46604
  const verdict = buildReleaseVerdict(commandName, result, projectInfoSync);
46605
+ const suitePin = o.suiteSolo && commandName === "release" ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict: verdict.releaseVerdict }) : void 0;
46538
46606
  if (suitePin?.status === "pending" && verdict.releaseVerdict) {
46539
46607
  verdict.releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46540
46608
  verdict.releaseVerdict.followUpStatus = "pending";
@@ -46563,6 +46631,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46563
46631
  const repo = o.repo ?? await resolveRepo();
46564
46632
  const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
46565
46633
  let releaseTrack;
46634
+ let suite;
46635
+ let preOneZeroPlan = false;
46566
46636
  let releaseChannel = null;
46567
46637
  let releaseLanguage = "en";
46568
46638
  if (repo) {
@@ -46579,14 +46649,24 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46579
46649
  }
46580
46650
  const raw = read.project?.releaseTrack;
46581
46651
  if (isReleaseTrack(raw)) releaseTrack = raw;
46652
+ suite = read.project?.suite;
46653
+ preOneZeroPlan = preOneZeroInForce(read.project ?? void 0);
46582
46654
  releaseChannel = read.project?.releaseChannel?.trim() || null;
46583
46655
  releaseLanguage = read.project?.releaseLanguage?.trim() || "en";
46584
46656
  }
46585
- const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev, releaseTrack });
46657
+ let releaseTarget;
46658
+ if (commandName === "release" && !o.repo && !o.dev && releaseTrack !== "direct" && releaseTrack !== "trunk" && repo?.toLowerCase() !== "mutmutco/mmi-hub") {
46659
+ try {
46660
+ releaseTarget = { releaseTag: (await execFileP("node", ["scripts/next-version.mjs", "release", ...preOneZeroPlan ? ["--pre-one-zero"] : []], { timeout: 15e3 })).stdout.trim() };
46661
+ } catch (e) {
46662
+ releaseTarget = { releaseTagError: (e.stderr || e.message).trim() };
46663
+ }
46664
+ }
46665
+ const steps = trainPlan(commandName, { ...targets ?? {}, ...releaseTarget ?? {}, repo, dev: o.dev, releaseTrack, suite });
46586
46666
  let doctor;
46587
46667
  if (repo && (!o.repo || (await resolveRepo())?.toLowerCase() === repo.toLowerCase())) {
46588
46668
  try {
46589
- doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo });
46669
+ doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo, suiteSolo: o.suiteSolo === true });
46590
46670
  } catch (e) {
46591
46671
  consoleIo.err(`mmi-cli: train doctor did not complete: ${e.message}`);
46592
46672
  }
@@ -46674,10 +46754,10 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46674
46754
  foldPort
46675
46755
  }));
46676
46756
  } catch (e) {
46677
- if (sub === "release" && o.suiteSolo && o.out) {
46678
- const error = e.message;
46679
- emitTrainResult("hotfix release", o.json ? JSON.stringify({ command: "hotfix release", suiteSolo: true, suitePin: { status: "failed", error }, verdict: "failed" }, null, 2) : `hotfix release: suite pin PR FAILED \u2014 ${error}`, o.out);
46757
+ if (sub === "release") {
46758
+ writeTrainFailureReceipt("hotfix release", o.json || Boolean(hotfixCmd.opts().json), o.out, e, o.suiteSolo === true);
46680
46759
  }
46760
+ applyTrainFollowUpExit("failed");
46681
46761
  return failGraceful(`hotfix ${sub}: ${e.message}`);
46682
46762
  }
46683
46763
  }
@@ -46705,7 +46785,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46705
46785
  }
46706
46786
  return runHotfixSub("release", async () => {
46707
46787
  const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
46708
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46788
+ const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
46709
46789
  const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
46710
46790
  return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
46711
46791
  }, o, renderHotfixRelease);
@@ -47041,6 +47121,13 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47041
47121
  publishMain,
47042
47122
  preflightMain,
47043
47123
  pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
47124
+ markAbandoned: async (repo, tag, sha) => {
47125
+ try {
47126
+ await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/abandoned-rc/${tag}`, "-f", `sha=${sha}`]);
47127
+ } catch (e) {
47128
+ if (!/already exists/i.test(e.message)) throw e;
47129
+ }
47130
+ },
47044
47131
  restoreMain: (input) => openSuiteRestorePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
47045
47132
  verifyLeadCut: async (sha) => {
47046
47133
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -47205,9 +47292,11 @@ function registerSuiteCommands(program3) {
47205
47292
  const unlock = await acquireTrainWriterLock(root, "suite abandon");
47206
47293
  let result;
47207
47294
  let restored;
47295
+ let marked;
47208
47296
  try {
47209
47297
  const io = suiteTrainIo(root);
47210
47298
  restored = o.ownerGo === true ? await suiteRestoreMainPins(io) : void 0;
47299
+ marked = await suiteMarkAbandoned(io, o.ownerGo === true);
47211
47300
  result = suiteAbandon(io.recordPath, o.ownerGo === true);
47212
47301
  } finally {
47213
47302
  await unlock();
@@ -47216,6 +47305,7 @@ function registerSuiteCommands(program3) {
47216
47305
  `suite abandon: archived the cut record to ${result.archive}`,
47217
47306
  ...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
47218
47307
  ...restored ? [` main pins the cut planned for unreleased tags are restored by ${restored.pr} (${restored.status}, #7758)`] : [],
47308
+ ...marked.length ? [` marked abandoned on origin, so the hotfix guard stops counting them as active (#7801): ${marked.join(", ")}`] : [],
47219
47309
  " the next suite rcand --owner-go re-snapshots every member"
47220
47310
  ].join("\n"));
47221
47311
  } catch (e) {
@@ -47734,7 +47824,6 @@ Next command: ${report.nextCommand}`);
47734
47824
  var RESTART_LINE = "\u21BB Restart Claude to finish.";
47735
47825
  var HOST_RESTART_LINES = {
47736
47826
  codex: "\u21BB Restart Codex to finish.",
47737
- cursor: "\u21BB Restart Cursor to finish.",
47738
47827
  jervcode: "\u21BB Restart JervCode to finish."
47739
47828
  };
47740
47829
  function restartLineForHost(host) {
@@ -47966,92 +48055,6 @@ var surfaces_default = {
47966
48055
  "Hosted tools and disabled or untrusted hooks bypass local enforcement."
47967
48056
  ]
47968
48057
  },
47969
- {
47970
- token: "cursor",
47971
- displayName: "Cursor",
47972
- lifecycle: "active",
47973
- artifactIds: [
47974
- "mmi-cursor-plugin",
47975
- "mmi-skills",
47976
- "mmi-cli"
47977
- ],
47978
- plannedArtifactIds: [],
47979
- assembly: {
47980
- mode: "package-directory",
47981
- rootPath: "packages/cursor-plugin",
47982
- sync: [
47983
- {
47984
- mode: "directories",
47985
- sourcePath: "skills",
47986
- targetPath: "packages/cursor-plugin/skills"
47987
- },
47988
- {
47989
- mode: "files",
47990
- sourcePath: "bin",
47991
- targetPath: "packages/cursor-plugin/bin",
47992
- include: [
47993
- "mmi-cli",
47994
- "mmi-cli.cmd"
47995
- ]
47996
- },
47997
- {
47998
- mode: "files",
47999
- sourcePath: "cli",
48000
- targetPath: "packages/cursor-plugin/cli",
48001
- include: [
48002
- "dist/index.cjs",
48003
- "dist/main.cjs",
48004
- "package.json"
48005
- ]
48006
- },
48007
- {
48008
- mode: "files",
48009
- sourcePath: ".cursor-plugin",
48010
- targetPath: "packages/cursor-plugin/.cursor-plugin",
48011
- include: [
48012
- "plugin.json"
48013
- ]
48014
- }
48015
- ]
48016
- },
48017
- install: {
48018
- mechanism: "github-source",
48019
- locator: "~/.cursor/plugins/local/mmi"
48020
- },
48021
- upgrade: {
48022
- mechanism: "reinstall",
48023
- reload: "workspace"
48024
- },
48025
- skills: {
48026
- delivery: "plugin",
48027
- sourceSurfaceId: "mmi-skills",
48028
- artifactId: "mmi-skills",
48029
- invocation: {
48030
- entry: "/epic",
48031
- any: "/<skill>"
48032
- }
48033
- },
48034
- cli: {
48035
- delivery: "standalone",
48036
- artifactId: "mmi-cli"
48037
- },
48038
- ownership: {
48039
- trust: "host",
48040
- cache: "mmi-hub",
48041
- repair: "mmi-hub"
48042
- },
48043
- certification: {
48044
- hostCommand: "cursor-agent",
48045
- versionArgs: [
48046
- "--version"
48047
- ]
48048
- },
48049
- enforcementCeilings: [
48050
- "Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
48051
- "Final assistant output is outside Cursor hook control.",
48052
- "Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
48053
- ]
48054
- },
48055
48058
  {
48056
48059
  token: "jervcode",
48057
48060
  displayName: "JervCode",
@@ -48281,38 +48284,6 @@ var surfaces_default = {
48281
48284
  },
48282
48285
  publishVisibility: "public"
48283
48286
  },
48284
- {
48285
- id: "mmi-cursor-plugin",
48286
- classification: "packaging",
48287
- kind: "plugin",
48288
- ownerPath: "packages/cursor-plugin/package.json",
48289
- deliveryPath: "packages/cursor-plugin/package.json",
48290
- delivery: "npm",
48291
- applicability: "Cursor IDE and Cursor Agent CLI; npm package @mutmutco/cursor-plugin carrying the plugin manifest and skills. The managed checkout at ~/.cursor/plugins/local/mmi is switched to each release by the fleet updater (#4945).",
48292
- versionCoordinated: true,
48293
- surfaceToken: "cursor",
48294
- versionPaths: [
48295
- {
48296
- path: ".cursor-plugin/plugin.json",
48297
- pointer: "version"
48298
- },
48299
- {
48300
- path: "packages/cursor-plugin/package.json",
48301
- pointer: "version"
48302
- }
48303
- ],
48304
- additionalPaths: [
48305
- "packages/cursor-plugin/.cursor-plugin",
48306
- "packages/cursor-plugin/skills",
48307
- "packages/cursor-plugin/bin",
48308
- "packages/cursor-plugin/cli"
48309
- ],
48310
- artifactIdentity: {
48311
- kind: "npm-pack",
48312
- packagePath: "packages/cursor-plugin"
48313
- },
48314
- publishVisibility: "public"
48315
- },
48316
48287
  {
48317
48288
  id: "mmi-claude-marketplace",
48318
48289
  classification: "delivery",
@@ -48604,7 +48575,7 @@ var surfaces_default = {
48604
48575
  };
48605
48576
 
48606
48577
  // src/surface-doctor.ts
48607
- var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "cursor", "jervcode", "hermes"]);
48578
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "hermes"]);
48608
48579
  function isActiveToken(value) {
48609
48580
  return TOKENS.has(value);
48610
48581
  }
@@ -50511,20 +50482,12 @@ async function readOrFail(read) {
50511
50482
  async function defaultRegistryRead() {
50512
50483
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
50513
50484
  }
50514
- async function defaultRunVerdictRead() {
50515
- return fetchRunVerdicts(registryClientDeps(await loadConfig()));
50516
- }
50517
- function modelRoleKeyOf(row) {
50518
- const model = row.model;
50519
- return typeof model === "string" && model.trim() ? model : void 0;
50520
- }
50521
- async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead, readRunVerdicts = defaultRunVerdictRead) {
50522
- const [gh2, aws, registryRead, projectsRead, runVerdictRead] = await Promise.all([
50485
+ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead) {
50486
+ const [gh2, aws, registryRead, projectsRead] = await Promise.all([
50523
50487
  githubEntries(client),
50524
50488
  awsEntries(),
50525
50489
  readOrFail(readRegistry),
50526
- readOrFail(readProjects),
50527
- readOrFail(readRunVerdicts)
50490
+ readOrFail(readProjects)
50528
50491
  ]);
50529
50492
  const readFailures = [];
50530
50493
  if (registryRead.state === "failed") {
@@ -50534,16 +50497,10 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50534
50497
  readFailures.push(`registry: could not read the projects list \u2014 schedulesMode opt-outs unresolved (nothing suppressed) \u2014 ${projectsRead.error}`);
50535
50498
  }
50536
50499
  const registry2 = registryRead.state === "ok" ? registryRead.value : null;
50537
- const runVerdicts = runVerdictRead.state === "ok" ? runVerdictRead.value : null;
50538
50500
  const projects = projectsRead.state === "ok" ? projectsRead.value : null;
50539
- const decoratedAws = decorateAwsModel(
50540
- aws.entries,
50541
- registry2?.map((r) => ({ id: r.id, executor: r.executor, model: modelRoleKeyOf(r) })) ?? null,
50542
- runVerdicts
50543
- );
50544
50501
  const recon = assembleReconciliation(gh2.entries, new Set(gh2.readRepos), registry2, new Set(gh2.workflowNames), {
50545
50502
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
50546
- awsEntries: decoratedAws,
50503
+ awsEntries: aws.entries,
50547
50504
  schedulerRead: Boolean(aws.schedulerRead)
50548
50505
  }, new Set(gh2.disabledWorkflowNames));
50549
50506
  const selfManaged = /* @__PURE__ */ new Set();
@@ -50557,7 +50514,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50557
50514
  );
50558
50515
  const driftLines = reconciliation.map(renderDrift);
50559
50516
  return {
50560
- entries: sortEntries([...gh2.entries, ...decoratedAws, ...DECLARED_ENTRIES]),
50517
+ entries: sortEntries([...gh2.entries, ...aws.entries, ...DECLARED_ENTRIES]),
50561
50518
  incomplete: [...gh2.incomplete, ...aws.incomplete, ...recon.incomplete, ...readFailures],
50562
50519
  drift: driftLines,
50563
50520
  reconciliation,
@@ -53282,7 +53239,7 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
53282
53239
  }
53283
53240
  if (!result.ok) return failGraceful(`org project deploy retire: ${result.error ?? "read failed"}`);
53284
53241
  });
53285
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
53242
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--unset-task <NAME...>", "retire exactly these tenant tasks (repeatable; lowercase kebab-case names). Refuses a name that is not declared. tenantTasks merges per entry and is capped at 20 stored in total").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
53286
53243
  const cfg = await loadConfig();
53287
53244
  let target;
53288
53245
  try {
@@ -53313,6 +53270,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53313
53270
  vars,
53314
53271
  unsets: o.unset ?? [],
53315
53272
  unsetSecrets: o.unsetSecret ?? [],
53273
+ unsetTasks: o.unsetTask ?? [],
53316
53274
  clearWebProfile: Boolean(o.clearWebProfile)
53317
53275
  });
53318
53276
  } catch (e) {
@@ -53323,6 +53281,8 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53323
53281
  if (clearRefusal) return fail(clearRefusal);
53324
53282
  const removalRefusal = secretRemovalRefusal(existing, patch);
53325
53283
  if (removalRefusal) return fail(removalRefusal);
53284
+ const taskRefusal = tenantTaskWriteRefusal(existing, patch);
53285
+ if (taskRefusal) return fail(taskRefusal);
53326
53286
  const boardError = boardLinkWriteError(patch, existing);
53327
53287
  if (boardError) return fail(`org project set: ${boardError}`);
53328
53288
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));