@mutmutco/codex-plugin 4.5.85 → 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.85",
17272
- tag: "v4.5.85",
17273
- commit: "a1ce2f136846",
17274
- npm: "@mutmutco/cli@4.5.85"
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.85"
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.85 and redeploy the Hub Lambda from tag v4.5.85 (a1ce2f136846); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17298
- v3Target: "v4.5.85 (@mutmutco/cli@4.5.85, tag commit a1ce2f136846 \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"];
@@ -20339,6 +20218,10 @@ function validSuiteCutContext(repo, context) {
20339
20218
  function authorisedSuiteCut(repo, meta, context) {
20340
20219
  return validSuiteCutContext(repo, context) && !!meta?.suite && context.lead.toLowerCase() === suiteLeadRepo(repo, meta.suite).toLowerCase();
20341
20220
  }
20221
+ function acceptedSuiteContext(repo, meta, context) {
20222
+ if (authorisedSuiteCut(repo, meta, context)) return true;
20223
+ return !!context && context.token === SUITE_LEAD_TOKEN && !!meta?.suite && !isBundleOnlySuiteMember(repo, meta.suite) && context.repo.toLowerCase() === repo.toLowerCase();
20224
+ }
20342
20225
  var BUNDLE_ONLY_RC_EXCEPTION = "suite member: rc is bundle-only (no rc runtime)";
20343
20226
  function isBundleOnlySuiteMember(repo, suite) {
20344
20227
  if (!suite) return false;
@@ -21368,6 +21251,10 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21368
21251
  };
21369
21252
  }
21370
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
+ }
21371
21258
  await deps.run("git", ["fetch", "origin", "rc"]);
21372
21259
  const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
21373
21260
  if (rcNow !== releasedRcSha) {
@@ -21900,14 +21787,6 @@ function installedClaudePluginVersion() {
21900
21787
  return void 0;
21901
21788
  }
21902
21789
  }
21903
- function manifestVersion(path2) {
21904
- try {
21905
- const manifest = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
21906
- return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
21907
- } catch {
21908
- return void 0;
21909
- }
21910
- }
21911
21790
  function readHermesPluginEvidence(env = process.env) {
21912
21791
  const host = hermesConfigRoot(env);
21913
21792
  const root = hermesPluginRoot(env);
@@ -21959,9 +21838,6 @@ function readHermesPluginEvidence(env = process.env) {
21959
21838
  function installedSurfacePluginVersion(surface) {
21960
21839
  const token = surfaceToken(surface);
21961
21840
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
21962
- if (token === "cursor") {
21963
- return manifestVersion((0, import_node_path18.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
21964
- }
21965
21841
  if (token === "jervcode") {
21966
21842
  return installedJervCodePackageVersion();
21967
21843
  }
@@ -23743,17 +23619,23 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23743
23619
  async (current) => {
23744
23620
  if (!current) return { value: { ledger: void 0, notes } };
23745
23621
  const phases = { ...current.phases };
23746
- for (const phase of ["deploy", "publish"]) {
23622
+ for (const phase of input.phases ?? ["deploy", "publish"]) {
23747
23623
  const rec = phases[phase];
23748
23624
  if (rec.updatedAt === "never") continue;
23749
23625
  if (rec.runId == null) {
23750
- 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`);
23751
23627
  continue;
23752
23628
  }
23753
- 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") {
23754
23636
  throw new ReleaseLedgerError(
23755
23637
  "invalid-record",
23756
- `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)`
23757
23639
  );
23758
23640
  }
23759
23641
  const runRepo = rec.runRepo ?? input.repo;
@@ -23788,7 +23670,8 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23788
23670
  }
23789
23671
  if (meta.conclusion === "success") {
23790
23672
  if (rec.state !== "complete") {
23791
- 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() };
23792
23675
  notes.push(`${phase}: ${liveLabel} re-verified green live`);
23793
23676
  }
23794
23677
  continue;
@@ -24809,7 +24692,7 @@ async function runTrainDoctor(input) {
24809
24692
  const dev = input.dev === true && lane === "release";
24810
24693
  const suiteRefusal = suiteMemberRefusal(repo, meta, lane, input.suiteSolo === true, input.suiteCut);
24811
24694
  if (suiteRefusal) add({ code: "suite-member-solo", severity: "blocker", source: "origin", title: suiteRefusal, remedy: suiteRefusal });
24812
- if (input.suiteCut && !authorisedSuiteCut(repo, meta, input.suiteCut))
24695
+ if (input.suiteCut && !acceptedSuiteContext(repo, meta, input.suiteCut))
24813
24696
  add({ code: "suite-cut-context-invalid", severity: "blocker", source: "origin", title: "suite cut context does not match this member and its registered lead", remedy: "run from the MM-Work suite command with the member registered in META" });
24814
24697
  if (lane === "rcand" && track === "direct") {
24815
24698
  add({ code: "lane-mismatch", severity: "blocker", source: "origin", title: `${repo} is direct-track (no rc) \u2014 rcand does not apply`, remedy: "run `mmi-cli devops release --apply` from development instead of rcand" });
@@ -25066,6 +24949,19 @@ async function runTrainDoctor(input) {
25066
24949
  try {
25067
24950
  let ledger = parseReleaseLedger(ledgerRaw);
25068
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
+ }
25069
24965
  const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
25070
24966
  if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
25071
24967
  const run = await deps.healRunLegs(train, cwd, deployModel);
@@ -25527,7 +25423,7 @@ function enforceGateBudget(deps, repo) {
25527
25423
  async function preflight(deps, ctx, stage, meta, lane) {
25528
25424
  assertGitBashOnWindowsForJervHubRelease(ctx.repo);
25529
25425
  deps.warn?.("Release: Checking release prerequisites");
25530
- if (deps.suiteCut && !authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25426
+ if (deps.suiteCut && !acceptedSuiteContext(ctx.repo, meta, deps.suiteCut))
25531
25427
  throw new Error(`${ctx.repo}: suite cut context does not match registry META; no deployment or tag was changed`);
25532
25428
  const model = requireDeployModel(meta, ctx.repo);
25533
25429
  if (model === "content") {
@@ -26170,7 +26066,9 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
26170
26066
  const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
26171
26067
  try {
26172
26068
  deps.warn?.("Release: Checking the package before publishing");
26069
+ const dryRunStart = (deps.now ?? Date.now)();
26173
26070
  await verifyPublishDryRun(deps, ctx, meta, deployModel);
26071
+ deps.warn?.(`Release: publish dry run took ${(((deps.now ?? Date.now)() - dryRunStart) / 1e3).toFixed(1)}s`);
26174
26072
  } catch (e) {
26175
26073
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
26176
26074
  }
@@ -26408,7 +26306,7 @@ async function runRcandResumeLocked(deps, options) {
26408
26306
  await requireCleanTree(deps);
26409
26307
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
26410
26308
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
26411
- const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", false, deps.suiteCut);
26309
+ const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", options.suiteSolo === true, deps.suiteCut);
26412
26310
  if (suiteRefusal) throw new Error(`rcand --resume: ${suiteRefusal}`);
26413
26311
  const branchHints = await loadReleaseTrackBranchHints(deps);
26414
26312
  if (isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct") {
@@ -27721,7 +27619,6 @@ async function listWorkflowPaths(deps, repo, branch) {
27721
27619
  return e?.status === 404 ? [] : void 0;
27722
27620
  }
27723
27621
  }
27724
- var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
27725
27622
  function gateWorkflowFiles(prWorkflowPaths) {
27726
27623
  const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
27727
27624
  return files.length ? files : DEFAULT_GATE_FILES;
@@ -28058,8 +27955,7 @@ async function auditRepoCi(repo, callerDeps) {
28058
27955
  }
28059
27956
  }
28060
27957
  if (explicitNoCi) {
28061
- const resolved = await getEmittedPrContexts();
28062
- const emitted = resolved?.filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
27958
+ const emitted = await getEmittedPrContexts();
28063
27959
  if (emitted && emitted.length > 0) {
28064
27960
  checks.push({
28065
27961
  ok: false,
@@ -28190,7 +28086,7 @@ async function auditRepoCi(repo, callerDeps) {
28190
28086
  if (hasRequiredChecks) {
28191
28087
  const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
28192
28088
  const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
28193
- 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))));
28194
28090
  checks.push({
28195
28091
  ok: unblocking.length === 0,
28196
28092
  label: UNBLOCKING_GATE_JOBS_LABEL,
@@ -28213,8 +28109,7 @@ async function auditRepoCi(repo, callerDeps) {
28213
28109
  for (const ref of releaseBranches) {
28214
28110
  emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
28215
28111
  }
28216
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28217
- const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore });
28112
+ const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
28218
28113
  checks.push({
28219
28114
  ok: unreachable.length === 0,
28220
28115
  label: RELEASE_BRANCH_REACHABLE_LABEL,
@@ -28681,8 +28576,7 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
28681
28576
  if (authority.coversReleaseBranches && authority.source !== "registry META requiredChecks") {
28682
28577
  const track = resolveReleaseTrack(meta, void 0, repo);
28683
28578
  const releaseBranches = branchesForTrack(track).filter((b) => b !== baseBranch);
28684
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28685
- const requiredForReleaseBranches = authority.contexts.filter((context) => !ignore.has(context));
28579
+ const requiredForReleaseBranches = authority.contexts.filter((context) => !TRAIN_PR_ONLY_CONTEXTS.has(context));
28686
28580
  for (const branch of releaseBranches) {
28687
28581
  if (await branchPresence(deps, repo, branch) !== true) continue;
28688
28582
  const emittedThere = await resolveEmittedPrContexts(deps, repo, branch);
@@ -30503,7 +30397,7 @@ function trainPlan(command, options = {}) {
30503
30397
  { label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
30504
30398
  { label: "tag release and publish GitHub Release", gated: true },
30505
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 },
30506
- { 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 }] : [],
30507
30401
  { label: "roll development forward and align rc to the released main", gated: true },
30508
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 }
30509
30403
  ];
@@ -30511,6 +30405,8 @@ function trainPlan(command, options = {}) {
30511
30405
  return [
30512
30406
  authority,
30513
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 },
30514
30410
  { label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
30515
30411
  { label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
30516
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 },
@@ -31405,7 +31301,7 @@ var SETTABLE_VAR_HINTS = {
31405
31301
  requiredGcpApis: "comma-string",
31406
31302
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
31407
31303
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
31408
- 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',
31409
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",
31410
31306
  edgeDomains: "JSON {dev,rc,main} domain map",
31411
31307
  statusOptions: "JSON name\u2192id map",
@@ -31436,6 +31332,18 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
31436
31332
  }
31437
31333
  return null;
31438
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
+ }
31439
31347
  function secretRemovalRefusal(existing, patch) {
31440
31348
  const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
31441
31349
  if (remove2.length === 0) return null;
@@ -31610,12 +31518,26 @@ function buildProjectSetPatch(input) {
31610
31518
  }
31611
31519
  patch.secretsRemove = keys;
31612
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
+ }
31613
31535
  if (input.clearWebProfile) {
31614
31536
  patch.oauth = null;
31615
31537
  patch.edgeDomains = null;
31616
31538
  }
31617
31539
  if (Object.keys(patch).length === 0) {
31618
- 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");
31619
31541
  }
31620
31542
  return patch;
31621
31543
  }
@@ -35792,8 +35714,24 @@ async function readSuiteStatus(deps) {
35792
35714
  })));
35793
35715
  if (!deps.leadTags || !deps.registryAt) return { suite: "mm-work", members: rows };
35794
35716
  const tags = await deps.leadTags().catch(() => []);
35795
- 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];
35796
- 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) };
35797
35735
  const lead = rows[0].stages;
35798
35736
  for (const stage of ["rc", "main"]) {
35799
35737
  const tag = leadTag[stage];
@@ -35809,7 +35747,7 @@ async function readSuiteStatus(deps) {
35809
35747
  }
35810
35748
  }
35811
35749
  const recorded = leadTag.main && deps.leadReleaseBody ? parseSuiteMapping(await deps.leadReleaseBody(leadTag.main).catch(() => null)) : void 0;
35812
- 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 };
35813
35751
  }
35814
35752
  var short = (v) => /^[a-f0-9]{40}$/i.test(v) ? v.slice(0, 12) : v;
35815
35753
  var proofText = (p) => p ? p.state === "proven" ? "proven" : p.state === "mismatch" ? `mismatch (${p.detail})` : `unprovable (${p.detail})` : "";
@@ -35851,7 +35789,8 @@ async function setSuiteHold(deps, member, hold) {
35851
35789
  const after = await deps.read(slug);
35852
35790
  if (!after.ok || !after.project) throw new Error(`${found.repo}: write sent but read-back failed; verify with org project get`);
35853
35791
  const got = after.project.suiteHold ?? null;
35854
- 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`);
35855
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}`);
35856
35795
  return hold ? `${found.repo}: HELD (${hold.reason})` : `${found.repo}: hold cleared`;
35857
35796
  }
@@ -36033,7 +35972,7 @@ async function carriedPins(deps, member, bundles) {
36033
35972
  }
36034
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`);
36035
35974
  }
36036
- function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
35975
+ function abandonableCut(recordPath, ownerGo) {
36037
35976
  const record = readSuiteCut(recordPath);
36038
35977
  if (!record) throw new Error("no suite cut record to abandon");
36039
35978
  if (record.leadReleased || record.members.some((m) => m.released)) {
@@ -36042,6 +35981,19 @@ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Dat
36042
35981
  if (record.pinPr && !ownerGo) {
36043
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`);
36044
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);
36045
35997
  const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}-${now.toISOString().replace(/[:.]/g, "")}.json`;
36046
35998
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
36047
35999
  (0, import_node_fs29.renameSync)(recordPath, archive);
@@ -36928,12 +36880,20 @@ ${lines2.join("\n")}
36928
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`;
36929
36881
  }
36930
36882
  var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
36931
- async function findHotfixPickGaps(deps, sources, tolerated) {
36932
- if (sources.length === 0) return [];
36883
+ async function scanHotfixPickGaps(deps, sources, tolerated, rcRef) {
36884
+ if (sources.length === 0) return { gaps: [], rcCarried: 0 };
36933
36885
  const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
36934
36886
  const carried = /* @__PURE__ */ new Set();
36935
36887
  const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
36936
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;
36937
36897
  const gaps = [];
36938
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);
36939
36899
  for (const line of devOnly) {
@@ -36941,11 +36901,37 @@ async function findHotfixPickGaps(deps, sources, tolerated) {
36941
36901
  if (!sha) continue;
36942
36902
  const lower = sha.toLowerCase();
36943
36903
  if (namedShas.has(lower) || carried.has(lower)) continue;
36904
+ if (rcRef && (!notOnRc.has(lower) || rcPicked.has(lower))) {
36905
+ rcCarried++;
36906
+ continue;
36907
+ }
36944
36908
  const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
36945
36909
  if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
36946
36910
  gaps.push({ sha, subject: subjectParts.join(" ") });
36947
36911
  }
36948
- 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
+ }
36949
36935
  }
36950
36936
  function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
36951
36937
  const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
@@ -37165,7 +37151,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
37165
37151
  const label = sources.map((s) => s.label).join(", ");
37166
37152
  const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
37167
37153
  const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
37168
- 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
+ }
37169
37164
  const unacked = pickGaps.filter((g) => {
37170
37165
  const sha = g.sha.toLowerCase();
37171
37166
  return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
@@ -37258,7 +37253,9 @@ async function runHotfixStartLocked(deps, options, doctor) {
37258
37253
 
37259
37254
  <!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
37260
37255
 
37261
- <!-- 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} -->` : ""}
37262
37259
 
37263
37260
  Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
37264
37261
  ]));
@@ -37498,7 +37495,7 @@ async function waitForHotfixRequiredChecks(deps, ctx, mergedSha, required, fresh
37498
37495
  const recover = hooks.recover ?? transientRecoveryForFailedPhase;
37499
37496
  let checks;
37500
37497
  try {
37501
- checks = await wait(deps, ctx, mergedSha, required, freshSince);
37498
+ checks = await wait(deps, ctx, mergedSha, required, freshSince, { exactCommitGreenSuffices: true });
37502
37499
  } catch (e) {
37503
37500
  const failureText = e instanceof Error ? e.message : String(e);
37504
37501
  let recovery;
@@ -37527,7 +37524,7 @@ release ledger: ${recoveryFailure} - manual intervention required`);
37527
37524
 
37528
37525
  ${recovery.note}`);
37529
37526
  try {
37530
- checks = await wait(deps, ctx, mergedSha, required, void 0);
37527
+ checks = await wait(deps, ctx, mergedSha, required, void 0, { exactCommitGreenSuffices: true });
37531
37528
  } catch (e2) {
37532
37529
  const revalidationFailure = e2 instanceof Error ? e2.message : String(e2);
37533
37530
  const prior2 = (await ledger.load(anchors).catch(() => void 0))?.phases.ordinaryCi;
@@ -40044,6 +40041,30 @@ function releaseProgressVerdict(status, subject = "Release") {
40044
40041
  // src/suite-pin.ts
40045
40042
  var import_promises6 = require("node:fs/promises");
40046
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
+ };
40047
40068
  async function openSuiteCutPinPr(deps, input) {
40048
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");
40049
40070
  const ids = input.pins.map((p) => p.module);
@@ -40186,8 +40207,8 @@ async function openSuiteRestorePinPr(deps, input) {
40186
40207
  }
40187
40208
  async function openSuitePinPr(deps, input) {
40188
40209
  const { repo, tag, stage } = input;
40189
- 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`;
40190
40210
  let scratch = "";
40211
+ let failure;
40191
40212
  try {
40192
40213
  const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
40193
40214
  if (!get.content) throw new Error("MM-Work seed/modules.json was unreadable");
@@ -40252,9 +40273,19 @@ async function openSuitePinPr(deps, input) {
40252
40273
  await run("gh", ["pr", "close", old2.url, "--repo", SUITE_REPO, "--comment", `Superseded by ${pr} (${member.id} ${stage} at ${tag}).`]);
40253
40274
  return { status: "pending", pr, url: pin };
40254
40275
  } catch (e) {
40255
- throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
40276
+ failure = new SuitePinError(repo, tag, e);
40277
+ throw failure;
40256
40278
  } finally {
40257
- 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
+ }
40258
40289
  }
40259
40290
  }
40260
40291
 
@@ -45809,7 +45840,7 @@ async function requireFreshTrainCli(commandName) {
45809
45840
  process.exit(converge.reexecCode);
45810
45841
  }
45811
45842
  var GH_TRAIN_TIMEOUT_MS = 3e4;
45812
- var GH_RUN_WATCH_TIMEOUT_MS = 90 * 6e4;
45843
+ var GH_RUN_WATCH_TIMEOUT_MS = 240 * 6e4;
45813
45844
  var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
45814
45845
  var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
45815
45846
  var NPM_TRAIN_TIMEOUT_MS = 6e4;
@@ -46231,10 +46262,43 @@ function emitTrainResult(label, output, out) {
46231
46262
  if (!out) printLine(output);
46232
46263
  else printLine(`Wrote ${label} result to ${out} (UTF-8, ${writeUtf8Receipt(out, output)} bytes)`);
46233
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
+ };
46234
46275
  function writeTrainFailureReceipt(commandName, json, out, error, suiteSolo = false) {
46235
- if (!out) return;
46236
- 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}`;
46237
- 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);
46238
46302
  }
46239
46303
  function renderTenantRedeploy(r) {
46240
46304
  return `mmi-cli devops runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
@@ -46260,8 +46324,15 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46260
46324
  const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
46261
46325
  return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
46262
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
+ }
46263
46334
  for (const commandName of ["rcand", "release"]) {
46264
- 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)");
46265
46336
  const RELEASE_ONLY_FLAGS = [
46266
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" },
46267
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)" },
@@ -46447,7 +46518,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46447
46518
  const raw2 = await withReleaseProgress(
46448
46519
  trainApplyDeps(),
46449
46520
  "Checking the existing release candidate",
46450
- (deps) => runRcandResume(deps, { watch: o.watch })
46521
+ (deps) => runRcandResume(deps, { watch: o.watch, suiteSolo: o.suiteSolo })
46451
46522
  );
46452
46523
  return emitRcandContinuation("rcand --resume", raw2, o);
46453
46524
  }
@@ -46463,7 +46534,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46463
46534
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46464
46535
  );
46465
46536
  const { followUpStatus, releaseVerdict } = buildResumeReleaseVerdict(result, projectInfoSync);
46466
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46537
+ const suitePin = o.suiteSolo ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict }) : void 0;
46467
46538
  if (suitePin?.status === "pending") {
46468
46539
  releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46469
46540
  releaseVerdict.followUpStatus = "pending";
@@ -46476,6 +46547,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46476
46547
  } catch (e) {
46477
46548
  applyTrainFollowUpExit("failed");
46478
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);
46479
46551
  return failGraceful(`${commandName} --resume: ${e.message}`);
46480
46552
  }
46481
46553
  }
@@ -46529,8 +46601,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46529
46601
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46530
46602
  );
46531
46603
  }
46532
- const suitePin = o.suiteSolo && commandName === "release" ? await pinSoloMember(result.repo, result.tag) : void 0;
46533
46604
  const verdict = buildReleaseVerdict(commandName, result, projectInfoSync);
46605
+ const suitePin = o.suiteSolo && commandName === "release" ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict: verdict.releaseVerdict }) : void 0;
46534
46606
  if (suitePin?.status === "pending" && verdict.releaseVerdict) {
46535
46607
  verdict.releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46536
46608
  verdict.releaseVerdict.followUpStatus = "pending";
@@ -46559,6 +46631,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46559
46631
  const repo = o.repo ?? await resolveRepo();
46560
46632
  const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
46561
46633
  let releaseTrack;
46634
+ let suite;
46635
+ let preOneZeroPlan = false;
46562
46636
  let releaseChannel = null;
46563
46637
  let releaseLanguage = "en";
46564
46638
  if (repo) {
@@ -46575,14 +46649,24 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46575
46649
  }
46576
46650
  const raw = read.project?.releaseTrack;
46577
46651
  if (isReleaseTrack(raw)) releaseTrack = raw;
46652
+ suite = read.project?.suite;
46653
+ preOneZeroPlan = preOneZeroInForce(read.project ?? void 0);
46578
46654
  releaseChannel = read.project?.releaseChannel?.trim() || null;
46579
46655
  releaseLanguage = read.project?.releaseLanguage?.trim() || "en";
46580
46656
  }
46581
- 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 });
46582
46666
  let doctor;
46583
46667
  if (repo && (!o.repo || (await resolveRepo())?.toLowerCase() === repo.toLowerCase())) {
46584
46668
  try {
46585
- 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 });
46586
46670
  } catch (e) {
46587
46671
  consoleIo.err(`mmi-cli: train doctor did not complete: ${e.message}`);
46588
46672
  }
@@ -46670,10 +46754,10 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46670
46754
  foldPort
46671
46755
  }));
46672
46756
  } catch (e) {
46673
- if (sub === "release" && o.suiteSolo && o.out) {
46674
- const error = e.message;
46675
- 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);
46676
46759
  }
46760
+ applyTrainFollowUpExit("failed");
46677
46761
  return failGraceful(`hotfix ${sub}: ${e.message}`);
46678
46762
  }
46679
46763
  }
@@ -46701,7 +46785,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46701
46785
  }
46702
46786
  return runHotfixSub("release", async () => {
46703
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) } : {} });
46704
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46788
+ const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
46705
46789
  const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
46706
46790
  return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
46707
46791
  }, o, renderHotfixRelease);
@@ -47037,6 +47121,13 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47037
47121
  publishMain,
47038
47122
  preflightMain,
47039
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
+ },
47040
47131
  restoreMain: (input) => openSuiteRestorePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
47041
47132
  verifyLeadCut: async (sha) => {
47042
47133
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -47201,9 +47292,11 @@ function registerSuiteCommands(program3) {
47201
47292
  const unlock = await acquireTrainWriterLock(root, "suite abandon");
47202
47293
  let result;
47203
47294
  let restored;
47295
+ let marked;
47204
47296
  try {
47205
47297
  const io = suiteTrainIo(root);
47206
47298
  restored = o.ownerGo === true ? await suiteRestoreMainPins(io) : void 0;
47299
+ marked = await suiteMarkAbandoned(io, o.ownerGo === true);
47207
47300
  result = suiteAbandon(io.recordPath, o.ownerGo === true);
47208
47301
  } finally {
47209
47302
  await unlock();
@@ -47212,6 +47305,7 @@ function registerSuiteCommands(program3) {
47212
47305
  `suite abandon: archived the cut record to ${result.archive}`,
47213
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"],
47214
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(", ")}`] : [],
47215
47309
  " the next suite rcand --owner-go re-snapshots every member"
47216
47310
  ].join("\n"));
47217
47311
  } catch (e) {
@@ -47730,7 +47824,6 @@ Next command: ${report.nextCommand}`);
47730
47824
  var RESTART_LINE = "\u21BB Restart Claude to finish.";
47731
47825
  var HOST_RESTART_LINES = {
47732
47826
  codex: "\u21BB Restart Codex to finish.",
47733
- cursor: "\u21BB Restart Cursor to finish.",
47734
47827
  jervcode: "\u21BB Restart JervCode to finish."
47735
47828
  };
47736
47829
  function restartLineForHost(host) {
@@ -47962,92 +48055,6 @@ var surfaces_default = {
47962
48055
  "Hosted tools and disabled or untrusted hooks bypass local enforcement."
47963
48056
  ]
47964
48057
  },
47965
- {
47966
- token: "cursor",
47967
- displayName: "Cursor",
47968
- lifecycle: "active",
47969
- artifactIds: [
47970
- "mmi-cursor-plugin",
47971
- "mmi-skills",
47972
- "mmi-cli"
47973
- ],
47974
- plannedArtifactIds: [],
47975
- assembly: {
47976
- mode: "package-directory",
47977
- rootPath: "packages/cursor-plugin",
47978
- sync: [
47979
- {
47980
- mode: "directories",
47981
- sourcePath: "skills",
47982
- targetPath: "packages/cursor-plugin/skills"
47983
- },
47984
- {
47985
- mode: "files",
47986
- sourcePath: "bin",
47987
- targetPath: "packages/cursor-plugin/bin",
47988
- include: [
47989
- "mmi-cli",
47990
- "mmi-cli.cmd"
47991
- ]
47992
- },
47993
- {
47994
- mode: "files",
47995
- sourcePath: "cli",
47996
- targetPath: "packages/cursor-plugin/cli",
47997
- include: [
47998
- "dist/index.cjs",
47999
- "dist/main.cjs",
48000
- "package.json"
48001
- ]
48002
- },
48003
- {
48004
- mode: "files",
48005
- sourcePath: ".cursor-plugin",
48006
- targetPath: "packages/cursor-plugin/.cursor-plugin",
48007
- include: [
48008
- "plugin.json"
48009
- ]
48010
- }
48011
- ]
48012
- },
48013
- install: {
48014
- mechanism: "github-source",
48015
- locator: "~/.cursor/plugins/local/mmi"
48016
- },
48017
- upgrade: {
48018
- mechanism: "reinstall",
48019
- reload: "workspace"
48020
- },
48021
- skills: {
48022
- delivery: "plugin",
48023
- sourceSurfaceId: "mmi-skills",
48024
- artifactId: "mmi-skills",
48025
- invocation: {
48026
- entry: "/epic",
48027
- any: "/<skill>"
48028
- }
48029
- },
48030
- cli: {
48031
- delivery: "standalone",
48032
- artifactId: "mmi-cli"
48033
- },
48034
- ownership: {
48035
- trust: "host",
48036
- cache: "mmi-hub",
48037
- repair: "mmi-hub"
48038
- },
48039
- certification: {
48040
- hostCommand: "cursor-agent",
48041
- versionArgs: [
48042
- "--version"
48043
- ]
48044
- },
48045
- enforcementCeilings: [
48046
- "Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
48047
- "Final assistant output is outside Cursor hook control.",
48048
- "Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
48049
- ]
48050
- },
48051
48058
  {
48052
48059
  token: "jervcode",
48053
48060
  displayName: "JervCode",
@@ -48277,38 +48284,6 @@ var surfaces_default = {
48277
48284
  },
48278
48285
  publishVisibility: "public"
48279
48286
  },
48280
- {
48281
- id: "mmi-cursor-plugin",
48282
- classification: "packaging",
48283
- kind: "plugin",
48284
- ownerPath: "packages/cursor-plugin/package.json",
48285
- deliveryPath: "packages/cursor-plugin/package.json",
48286
- delivery: "npm",
48287
- 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).",
48288
- versionCoordinated: true,
48289
- surfaceToken: "cursor",
48290
- versionPaths: [
48291
- {
48292
- path: ".cursor-plugin/plugin.json",
48293
- pointer: "version"
48294
- },
48295
- {
48296
- path: "packages/cursor-plugin/package.json",
48297
- pointer: "version"
48298
- }
48299
- ],
48300
- additionalPaths: [
48301
- "packages/cursor-plugin/.cursor-plugin",
48302
- "packages/cursor-plugin/skills",
48303
- "packages/cursor-plugin/bin",
48304
- "packages/cursor-plugin/cli"
48305
- ],
48306
- artifactIdentity: {
48307
- kind: "npm-pack",
48308
- packagePath: "packages/cursor-plugin"
48309
- },
48310
- publishVisibility: "public"
48311
- },
48312
48287
  {
48313
48288
  id: "mmi-claude-marketplace",
48314
48289
  classification: "delivery",
@@ -48600,7 +48575,7 @@ var surfaces_default = {
48600
48575
  };
48601
48576
 
48602
48577
  // src/surface-doctor.ts
48603
- var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "cursor", "jervcode", "hermes"]);
48578
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "hermes"]);
48604
48579
  function isActiveToken(value) {
48605
48580
  return TOKENS.has(value);
48606
48581
  }
@@ -50507,20 +50482,12 @@ async function readOrFail(read) {
50507
50482
  async function defaultRegistryRead() {
50508
50483
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
50509
50484
  }
50510
- async function defaultRunVerdictRead() {
50511
- return fetchRunVerdicts(registryClientDeps(await loadConfig()));
50512
- }
50513
- function modelRoleKeyOf(row) {
50514
- const model = row.model;
50515
- return typeof model === "string" && model.trim() ? model : void 0;
50516
- }
50517
- async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead, readRunVerdicts = defaultRunVerdictRead) {
50518
- 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([
50519
50487
  githubEntries(client),
50520
50488
  awsEntries(),
50521
50489
  readOrFail(readRegistry),
50522
- readOrFail(readProjects),
50523
- readOrFail(readRunVerdicts)
50490
+ readOrFail(readProjects)
50524
50491
  ]);
50525
50492
  const readFailures = [];
50526
50493
  if (registryRead.state === "failed") {
@@ -50530,16 +50497,10 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50530
50497
  readFailures.push(`registry: could not read the projects list \u2014 schedulesMode opt-outs unresolved (nothing suppressed) \u2014 ${projectsRead.error}`);
50531
50498
  }
50532
50499
  const registry2 = registryRead.state === "ok" ? registryRead.value : null;
50533
- const runVerdicts = runVerdictRead.state === "ok" ? runVerdictRead.value : null;
50534
50500
  const projects = projectsRead.state === "ok" ? projectsRead.value : null;
50535
- const decoratedAws = decorateAwsModel(
50536
- aws.entries,
50537
- registry2?.map((r) => ({ id: r.id, executor: r.executor, model: modelRoleKeyOf(r) })) ?? null,
50538
- runVerdicts
50539
- );
50540
50501
  const recon = assembleReconciliation(gh2.entries, new Set(gh2.readRepos), registry2, new Set(gh2.workflowNames), {
50541
50502
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
50542
- awsEntries: decoratedAws,
50503
+ awsEntries: aws.entries,
50543
50504
  schedulerRead: Boolean(aws.schedulerRead)
50544
50505
  }, new Set(gh2.disabledWorkflowNames));
50545
50506
  const selfManaged = /* @__PURE__ */ new Set();
@@ -50553,7 +50514,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50553
50514
  );
50554
50515
  const driftLines = reconciliation.map(renderDrift);
50555
50516
  return {
50556
- entries: sortEntries([...gh2.entries, ...decoratedAws, ...DECLARED_ENTRIES]),
50517
+ entries: sortEntries([...gh2.entries, ...aws.entries, ...DECLARED_ENTRIES]),
50557
50518
  incomplete: [...gh2.incomplete, ...aws.incomplete, ...recon.incomplete, ...readFailures],
50558
50519
  drift: driftLines,
50559
50520
  reconciliation,
@@ -53278,7 +53239,7 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
53278
53239
  }
53279
53240
  if (!result.ok) return failGraceful(`org project deploy retire: ${result.error ?? "read failed"}`);
53280
53241
  });
53281
- 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) => {
53282
53243
  const cfg = await loadConfig();
53283
53244
  let target;
53284
53245
  try {
@@ -53309,6 +53270,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53309
53270
  vars,
53310
53271
  unsets: o.unset ?? [],
53311
53272
  unsetSecrets: o.unsetSecret ?? [],
53273
+ unsetTasks: o.unsetTask ?? [],
53312
53274
  clearWebProfile: Boolean(o.clearWebProfile)
53313
53275
  });
53314
53276
  } catch (e) {
@@ -53319,6 +53281,8 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53319
53281
  if (clearRefusal) return fail(clearRefusal);
53320
53282
  const removalRefusal = secretRemovalRefusal(existing, patch);
53321
53283
  if (removalRefusal) return fail(removalRefusal);
53284
+ const taskRefusal = tenantTaskWriteRefusal(existing, patch);
53285
+ if (taskRefusal) return fail(taskRefusal);
53322
53286
  const boardError = boardLinkWriteError(patch, existing);
53323
53287
  if (boardError) return fail(`org project set: ${boardError}`);
53324
53288
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));