@mutmutco/codex-plugin 4.5.86 → 4.5.88

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
  }
@@ -7659,15 +7634,13 @@ function strayCronDrifts(workflows) {
7659
7634
  }
7660
7635
  function unlauncheredLlmDrift(entry) {
7661
7636
  if (entry.llm !== "yes") return null;
7662
- if (isHarbourLlmLauncher(entry.executor)) return null;
7663
7637
  if (entry.executor === "aws-scheduler \u2192 mmi-hub-schedule-dispatcher") return null;
7664
- const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
7665
7638
  return {
7666
7639
  class: "unlaunchered-llm",
7667
7640
  name: entry.name,
7668
7641
  executor: entry.executor,
7669
- 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)`
7642
+ detail: "llm: yes but no scheduled-LLM executor is supported (Cursor retired, #7779; Codex executor retired, #7821)",
7643
+ remedy: "retire the lane or set it to llm: no until the harbour redesign (#7821; docs/schedules.md rule 3)"
7671
7644
  };
7672
7645
  }
7673
7646
  function unlauncheredLlmDrifts(entries) {
@@ -7739,8 +7712,6 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
7739
7712
  var SCHEDULE_EXECUTORS = /* @__PURE__ */ new Set([
7740
7713
  "github-actions",
7741
7714
  // 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)
7744
7715
  "lambda",
7745
7716
  // fleet function row — requires the `# target: mmi-fleet-*` header (#3205/#3246)
7746
7717
  "box systemd",
@@ -7821,8 +7792,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7821
7792
  if (llmFromHeader(yamlText) === "unknown") {
7822
7793
  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
7794
  }
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.`);
7795
+ if (llmFromHeader(yamlText) === "yes") {
7796
+ throw new Error(`schedules register: ${expectedId} header \`llm: yes\` is refused \u2014 no scheduled-LLM executor is supported: Cursor (\`cursor-agent\`) was retired (#7779) and the Codex executor was retired (#7821). Set \`llm: no\` or retire the lane until the harbour redesign.`);
7826
7797
  }
7827
7798
  const target = header.target;
7828
7799
  if (target !== void 0 && h.executor !== "lambda") {
@@ -7831,9 +7802,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7831
7802
  if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
7832
7803
  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
7804
  }
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.`);
7805
+ if (header.model !== void 0) {
7806
+ throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 a model role key is valid only on an LLM lane, and no scheduled-LLM executor is supported (Cursor retired #7779; Codex executor retired #7821). Remove the line.`);
7837
7807
  }
7838
7808
  return {
7839
7809
  id: expectedId,
@@ -7847,8 +7817,7 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7847
7817
  kill: h.kill,
7848
7818
  repo,
7849
7819
  sourcePath: workflowPath,
7850
- ...target !== void 0 ? { target } : {},
7851
- ...model !== void 0 ? { model } : {}
7820
+ ...target !== void 0 ? { target } : {}
7852
7821
  };
7853
7822
  }
7854
7823
  function scheduleRecordsFromWorkflows(repo, files) {
@@ -7993,22 +7962,6 @@ async function fetchSchedulesList(deps) {
7993
7962
  return null;
7994
7963
  }
7995
7964
  }
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
7965
  async function fetchOrgConfig(deps) {
8013
7966
  if (!deps.baseUrl) return null;
8014
7967
  const token = await deps.token();
@@ -11324,9 +11277,6 @@ function detectSurface(env) {
11324
11277
  if (env.MMI_AGENT_SURFACE === "jervcode" || has("PI_SESSION_ID")) {
11325
11278
  return "jervcode";
11326
11279
  }
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
11280
  if (env.MMI_AGENT_SURFACE === "opencode" || has("OPENCODE_CLIENT") || has("OPENCODE_SERVER_USERNAME")) return "opencode";
11331
11281
  const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
11332
11282
  const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
@@ -11341,8 +11291,6 @@ function surfaceToken(surface) {
11341
11291
  return "claude";
11342
11292
  case "codex":
11343
11293
  return "codex";
11344
- case "cursor":
11345
- return "cursor";
11346
11294
  case "jervcode":
11347
11295
  return "jervcode";
11348
11296
  case "hermes":
@@ -11362,8 +11310,6 @@ function reloadAction(surface) {
11362
11310
  return "restart Codex";
11363
11311
  case "opencode":
11364
11312
  return "restart OpenCode";
11365
- case "cursor":
11366
- return "reload the Cursor window";
11367
11313
  case "jervcode":
11368
11314
  return "restart JervCode (the package set is read at launch)";
11369
11315
  case "hermes":
@@ -11376,7 +11322,6 @@ function reloadAction(surface) {
11376
11322
  }
11377
11323
  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
11324
  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
11325
  var PLUGIN_SURFACE_HEAL = {
11381
11326
  claude: {
11382
11327
  delivery: "plugin-cli",
@@ -11402,23 +11347,13 @@ var PLUGIN_SURFACE_HEAL = {
11402
11347
  ],
11403
11348
  fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} to load MMI commands`,
11404
11349
  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
11350
  }
11413
11351
  };
11414
11352
  function nonClaudeSurfaceHealMessage(surface) {
11415
11353
  if (surface === "codex") {
11416
11354
  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
11355
  }
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)";
11356
+ 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
11357
  }
11423
11358
  function healStepAborts(step, ok) {
11424
11359
  return !ok && step.gated;
@@ -11468,7 +11403,6 @@ function hermesConfigRoot(env = process.env, home = (0, import_node_os7.homedir)
11468
11403
  }
11469
11404
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os7.homedir)()) {
11470
11405
  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
11406
  if (surface === "hermes") return hermesConfigRoot(env, home);
11473
11407
  if (surface === "jervcode") {
11474
11408
  return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path10.join)(home, ".jerv", "agent");
@@ -11493,7 +11427,7 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
11493
11427
  (0, import_node_path10.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
11494
11428
  ];
11495
11429
  }
11496
- if (surface === "cursor" || surface === "hermes") return [];
11430
+ if (surface === "hermes") return [];
11497
11431
  if (surface === "jervcode") return [];
11498
11432
  return [(0, import_node_path10.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
11499
11433
  }
@@ -11538,15 +11472,6 @@ async function fetchNpmReleasedVersion() {
11538
11472
  return void 0;
11539
11473
  }
11540
11474
  }
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
11475
  var JERVCODE_NPM_PACKAGE = "@mutmutco/pi-plugin";
11551
11476
  function jervcodeAgentDirs(env = process.env, home = (0, import_node_os7.homedir)()) {
11552
11477
  const primary = surfaceConfigRoot("jervcode", env, home);
@@ -11633,10 +11558,10 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
11633
11558
  // Codex's native projection is authoritative; its legacy installed_plugins.json may be empty
11634
11559
  // while `plugin list --json` proves the managed plugin installed (#6111).
11635
11560
  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()),
11561
+ surface === "jervcode" && piEntry !== null || surface === "hermes" && (0, import_node_fs12.existsSync)(hermesPluginRoot()),
11637
11562
  // 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(
11563
+ marketplaceClonePresent: surface === "jervcode" || surface === "hermes" ? true : marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
11564
+ pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "codex" ? Boolean(
11640
11565
  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
11566
  ) : (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, "plugins", "cache", "mutmutco", "mmi"))
11642
11567
  };
@@ -11652,84 +11577,6 @@ async function runPluginCli(bin, args, log) {
11652
11577
  return false;
11653
11578
  }
11654
11579
  }
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
11580
  async function marketplaceAddRefSupported(bin) {
11734
11581
  try {
11735
11582
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -11852,11 +11699,10 @@ async function healJervCodeForDoctor() {
11852
11699
  }
11853
11700
  async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
11854
11701
  const token = surfaceToken(surface);
11855
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11702
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11856
11703
  return { ok: false, detail: `not a supported plugin surface (${surface})` };
11857
11704
  }
11858
11705
  if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
11859
- if (token === "cursor") return installCursorPluginCheckout();
11860
11706
  if (token === "jervcode") return healJervCodeForDoctor();
11861
11707
  const steps = [];
11862
11708
  const applied = await applyPluginHeal(surface, (msg) => {
@@ -11952,7 +11798,7 @@ async function runGuard(readOrigin) {
11952
11798
  const surface = detectSurface(process.env);
11953
11799
  try {
11954
11800
  const token = surfaceToken(surface);
11955
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11801
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11956
11802
  process.exitCode = 0;
11957
11803
  return;
11958
11804
  }
@@ -11966,9 +11812,8 @@ async function runGuard(readOrigin) {
11966
11812
  if (surfaceToken(surface) === "codex") {
11967
11813
  console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
11968
11814
  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\`.`);
11815
+ } else if (surfaceToken(surface) === "jervcode") {
11816
+ console.error("[mmi-guard] Could not inspect the active JervCode plugin; run `mmi-cli plugin heal`.");
11972
11817
  process.exitCode = 1;
11973
11818
  } else {
11974
11819
  process.exitCode = 0;
@@ -11977,7 +11822,7 @@ async function runGuard(readOrigin) {
11977
11822
  }
11978
11823
  async function runPluginHeal(surface = detectSurface(process.env)) {
11979
11824
  const token = surfaceToken(surface);
11980
- if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "jervcode") {
11825
+ if (token !== "claude" && token !== "codex" && token !== "jervcode") {
11981
11826
  console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
11982
11827
  return;
11983
11828
  }
@@ -11994,17 +11839,15 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
11994
11839
  return;
11995
11840
  }
11996
11841
  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 });
11842
+ const applied = await applyPluginHeal(surface, console.log, { force: true });
12000
11843
  const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
12001
11844
  if (healed) {
12002
11845
  console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.`);
12003
11846
  } else {
12004
11847
  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);
11848
+ const refSupported = await marketplaceAddRefSupported(token);
11849
+ const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
11850
+ const note = refAbsenceNote(token, refSupported);
12008
11851
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
12009
11852
  ${recovery}${note}${pluginReadGrantNote()}`);
12010
11853
  }
@@ -12017,7 +11860,6 @@ var SESSION_ID_ENV_VARS = [
12017
11860
  "CLAUDE_CODE_SESSION_ID",
12018
11861
  "CODEX_SESSION_ID",
12019
11862
  "CODEX_THREAD_ID",
12020
- "CURSOR_SESSION_ID",
12021
11863
  "HERMES_SESSION_ID",
12022
11864
  "PI_SESSION_ID",
12023
11865
  // #6193: lane runners spawn the CLI through the host's extension shell operations, which strip
@@ -12546,6 +12388,7 @@ function renderBoardItem(item) {
12546
12388
  if (claim) {
12547
12389
  const who = `${claim.holder}, ${formatClaimAge(claim.ageMs)} ago`;
12548
12390
  lines2.push(claim.currentlyClaimed ? `Claimed by: ${who}` : `Last claimed by: ${who} (not currently claimed)`);
12391
+ if (claim.stale) lines2.push(`Claim stamp: STALE, idle ${formatClaimAge(claim.idleMs)}; any live session may claim it`);
12549
12392
  }
12550
12393
  lines2.push(
12551
12394
  `Type: ${item.type ?? "item"}${item.labels.length ? ` \xB7 ${item.labels.join(", ")}` : ""}`,
@@ -12846,6 +12689,10 @@ async function readBoard(options, deps = {}) {
12846
12689
  if (options.includeBundleDetails || options.includeAllBodies) {
12847
12690
  await attachBundleDetails(report, client, options.allowPartial ?? false, { all: options.includeAllBodies });
12848
12691
  }
12692
+ const readComments = claimMarkerCommentsReader(client);
12693
+ for (const scope of ["primary", "secondary"]) {
12694
+ await markStaleClaimStamps(report[scope], readComments, { pullRequests, writable: writableOrUnknown(writable) });
12695
+ }
12849
12696
  for (const scope of ["primary", "secondary"]) {
12850
12697
  const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
12851
12698
  requireBundledBody: Boolean(options.includeBundleDetails)
@@ -12854,7 +12701,7 @@ async function readBoard(options, deps = {}) {
12854
12701
  report.warnings.push(...filtered.warnings);
12855
12702
  }
12856
12703
  for (const scope of ["primary", "secondary"]) {
12857
- const demoted = await demoteLiveClaimedClaimables(report[scope], client);
12704
+ const demoted = await demoteLiveClaimedClaimables(report[scope], client, void 0, { fetchComments: readComments });
12858
12705
  report.warnings.push(...demoted.warnings);
12859
12706
  report.partial = report.partial || demoted.partial;
12860
12707
  }
@@ -13144,7 +12991,7 @@ function renderClaimable(lines2, items) {
13144
12991
  function renderTaken(lines2, items) {
13145
12992
  if (!items.length) return;
13146
12993
  lines2.push("Taken");
13147
- for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}`);
12994
+ for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}${renderClaimLane(item)}`);
13148
12995
  }
13149
12996
  function renderUnowned(lines2, items) {
13150
12997
  if (!items.length) return;
@@ -13155,7 +13002,11 @@ function renderTitledItem(item) {
13155
13002
  const pri = item.priority ? ` \xB7 ${item.priority}` : "";
13156
13003
  const loop = item.loopState ? ` \xB7 loop:${item.loopState.state}` : "";
13157
13004
  const stale = item.loopStaleness?.stale ? ` \xB7 stale:${formatClaimAge(item.loopStaleness.idleMs)}` : "";
13158
- return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale} ${item.title}`;
13005
+ return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale}${renderClaimLane(item)} ${item.title}`;
13006
+ }
13007
+ function renderClaimLane(item) {
13008
+ if (!item.claim) return "";
13009
+ return item.claim.stale ? ` \xB7 stale:${formatClaimAge(item.claim.idleMs)} (${item.claim.holder})` : ` \xB7 lane ${item.claim.holder}`;
13159
13010
  }
13160
13011
  function hasItems(buckets) {
13161
13012
  return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0 || buckets.unownedInFlight.length > 0;
@@ -13280,6 +13131,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
13280
13131
  }
13281
13132
  var CLAIM_MARKER_PATTERN = /<!--\s*mmi-claim:\s*(\{[\s\S]*?\})\s*-->/g;
13282
13133
  var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
13134
+ var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
13283
13135
  function isValidClaimMarker(parsed) {
13284
13136
  if (!parsed || typeof parsed !== "object") return false;
13285
13137
  const marker = parsed;
@@ -13316,34 +13168,86 @@ function describeClaimMarker(marker) {
13316
13168
  }
13317
13169
  function boardItemClaim(item) {
13318
13170
  const marker = item.details ? latestClaimMarker(item.details.comments) : void 0;
13319
- if (!marker) return void 0;
13171
+ return marker ? claimFromMarker(item, marker) : void 0;
13172
+ }
13173
+ function claimStampStaleness(marker, evidence = {}, now = Date.now()) {
13174
+ const times = [marker.ts, ...evidence.activityAt ?? []].map((t) => t ? Date.parse(t) : Number.NaN);
13175
+ const idleMs = now - Math.max(...times.filter(Number.isFinite));
13176
+ return { stale: !evidence.linkedWork && idleMs >= CLAIM_STAMP_IDLE_MS, idleMs };
13177
+ }
13178
+ function workIsLive(at, now = Date.now()) {
13179
+ const idle = now - Date.parse(at ?? "");
13180
+ return !Number.isFinite(idle) || idle < CLAIM_STAMP_IDLE_MS;
13181
+ }
13182
+ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
13320
13183
  return {
13321
13184
  marker,
13322
13185
  holder: describeClaimMarker(marker),
13323
- ageMs: Date.now() - Date.parse(marker.ts),
13324
- currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress"
13186
+ ageMs: now - Date.parse(marker.ts),
13187
+ currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
13188
+ ...claimStampStaleness(marker, { activityAt: [item.updatedAt, ...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13325
13189
  };
13326
13190
  }
13327
- function classifyClaimableMarker(comments, actor, now = Date.now()) {
13191
+ function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
13328
13192
  const marker = comments ? latestClaimMarker(comments) : void 0;
13329
13193
  if (!marker) return { verdict: "claimable" };
13330
13194
  const ownership = laneOwnership(marker, actor);
13331
13195
  if (ownership === "mine") return { verdict: "owned", holder: describeClaimMarker(marker) };
13332
- const age = now - Date.parse(marker.ts);
13333
- if (Number.isFinite(age) && age >= 0 && age < CLAIM_MARKER_LIVE_MS) {
13196
+ if (!claimStampStaleness(marker, { activityAt }, now).stale) {
13334
13197
  return { verdict: "taken", holder: describeClaimMarker(marker) };
13335
13198
  }
13336
13199
  return { verdict: "claimable" };
13337
13200
  }
13201
+ function claimMarkerCommentsReader(client) {
13202
+ const cache = /* @__PURE__ */ new Map();
13203
+ return (item) => {
13204
+ let read = cache.get(item);
13205
+ if (!read) {
13206
+ read = (async () => {
13207
+ if (item.detailsUnavailable) return void 0;
13208
+ if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13209
+ const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13210
+ return comments.map((comment) => ({ body: comment.body ?? "" }));
13211
+ })();
13212
+ cache.set(item, read);
13213
+ }
13214
+ return read;
13215
+ };
13216
+ }
13217
+ async function markStaleClaimStamps(scope, readComments, options = {}) {
13218
+ const actor = options.actor ?? describeSessionIdentity();
13219
+ const now = options.now ?? Date.now();
13220
+ const promoted = [];
13221
+ for (const bucket of ["userOwned", "claimable", "unownedInFlight", "taken"]) {
13222
+ const kept = [];
13223
+ for (const item of scope[bucket]) {
13224
+ kept.push(item);
13225
+ if (item.contentType !== "Issue") continue;
13226
+ const comments = await readComments(item).catch(() => void 0);
13227
+ const marker = comments ? latestClaimMarker(comments) : void 0;
13228
+ if (!marker) continue;
13229
+ const prs = (options.pullRequests ?? []).filter((pr) => pr.closesIssues.some((ref) => ref.toLowerCase() === item.ref.toLowerCase()));
13230
+ item.claim = claimFromMarker(item, marker, {
13231
+ activityAt: prs.map((pr) => pr.updatedAt),
13232
+ linkedWork: prs.some((pr) => !pr.updatedAt)
13233
+ }, now);
13234
+ const movable = bucket === "userOwned" || bucket === "unownedInFlight";
13235
+ if (movable && item.claim.stale && laneOwnership(marker, actor) !== "mine" && (!options.writable || options.writable.has(item.repository.toLowerCase()))) {
13236
+ kept.pop();
13237
+ promoted.push(item);
13238
+ }
13239
+ }
13240
+ scope[bucket] = kept;
13241
+ }
13242
+ if (promoted.length) {
13243
+ scope.claimable.push(...promoted);
13244
+ scope.claimable.sort(compareItems);
13245
+ }
13246
+ }
13338
13247
  async function demoteLiveClaimedClaimables(scope, client, actor = describeSessionIdentity(), deps = {}) {
13339
13248
  const warnings = [];
13340
13249
  let partial = false;
13341
- const fetchComments = deps.fetchComments ?? (async (item) => {
13342
- if (item.detailsUnavailable) return void 0;
13343
- if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13344
- const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13345
- return comments.map((comment) => ({ body: comment.body ?? "" }));
13346
- });
13250
+ const fetchComments = deps.fetchComments ?? claimMarkerCommentsReader(client);
13347
13251
  const stillClaimable = [];
13348
13252
  const movedTaken = [];
13349
13253
  const movedOwned = [];
@@ -13363,7 +13267,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13363
13267
  warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
13364
13268
  continue;
13365
13269
  }
13366
- const verdict = classifyClaimableMarker(comments, actor, deps.now);
13270
+ const verdict = classifyClaimableMarker(comments, actor, deps.now, [item.updatedAt]);
13367
13271
  if (verdict.verdict === "claimable") {
13368
13272
  stillClaimable.push(item);
13369
13273
  } else if (verdict.verdict === "owned") {
@@ -13458,6 +13362,12 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13458
13362
  const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
13459
13363
  const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
13460
13364
  out.marker = latestClaimMarker(rows);
13365
+ for (let i = comments.length - 1; i >= 0; i -= 1) {
13366
+ if (comments[i].created_at && !latestClaimMarker([rows[i]])) {
13367
+ out.lastCommentAt = comments[i].created_at;
13368
+ break;
13369
+ }
13370
+ }
13461
13371
  if (out.marker) {
13462
13372
  for (let i = rows.length - 1; i >= 0; i -= 1) {
13463
13373
  if (latestClaimMarker([rows[i]])) {
@@ -13484,7 +13394,10 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13484
13394
  if (from && from.toLowerCase() !== repo.toLowerCase()) return false;
13485
13395
  return pullClosesIssue(source, number);
13486
13396
  });
13487
- if (pr?.source?.issue?.number) out.openPr = `open PR #${pr.source.issue.number} would close it on merge`;
13397
+ if (pr?.source?.issue?.number) {
13398
+ out.openPr = `open PR #${pr.source.issue.number} would close it on merge`;
13399
+ out.openPrAt = pr.source.issue.updated_at ?? void 0;
13400
+ }
13488
13401
  } catch {
13489
13402
  out.failed.push("timeline");
13490
13403
  }
@@ -13495,11 +13408,24 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13495
13408
  } catch {
13496
13409
  out.failed.push("branches");
13497
13410
  }
13411
+ if (out.branch) {
13412
+ try {
13413
+ const head = await client.rest(
13414
+ "GET",
13415
+ `repos/${repo}/branches/${encodeURIComponent(out.branch)}`
13416
+ );
13417
+ out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
13418
+ } catch {
13419
+ }
13420
+ }
13498
13421
  if (!out.branch) {
13499
13422
  try {
13500
13423
  const pulls = await fetchOpenPulls(repo);
13501
13424
  const hit = pulls.find((pr) => pullClosesIssue(pr, number));
13502
- if (hit?.head?.ref) out.branch = `${hit.head.ref} (head of open PR #${hit.number} closing #${number})`;
13425
+ if (hit?.head?.ref) {
13426
+ out.branch = `${hit.head.ref} (head of open PR #${hit.number} closing #${number})`;
13427
+ out.branchAt = hit.updated_at ?? void 0;
13428
+ }
13503
13429
  } catch {
13504
13430
  out.failed.push("pulls");
13505
13431
  }
@@ -13510,13 +13436,24 @@ function liveEvidenceLines(evidence, repo) {
13510
13436
  const live = [];
13511
13437
  if (evidence.marker && evidence.sessionState === "live") {
13512
13438
  live.push(`claim session ${describeClaimMarker(evidence.marker)} has recent local transcript activity`);
13513
- } else if (evidence.marker && evidence.sessionState !== "dead" && evidence.markerAgeMs !== void 0 && evidence.markerAgeMs < CLAIM_MARKER_LIVE_MS) {
13514
- live.push(`claim marker from ${describeClaimMarker(evidence.marker)} is only ${formatClaimAge(evidence.markerAgeMs)} old (session not locally probeable)`);
13439
+ } else if (evidence.marker && evidence.sessionState !== "dead") {
13440
+ const { stale, idleMs } = claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] });
13441
+ const marker = describeClaimMarker(evidence.marker);
13442
+ if (!stale) {
13443
+ live.push(evidence.lastCommentAt && Date.parse(evidence.lastCommentAt) > Date.parse(evidence.marker.ts) ? `claim marker from ${marker} is ${formatClaimAge(evidence.markerAgeMs)} old but the issue was commented ${formatClaimAge(idleMs)} ago (session not locally probeable)` : `claim marker from ${marker} is only ${formatClaimAge(evidence.markerAgeMs)} old (session not locally probeable)`);
13444
+ }
13515
13445
  }
13516
- if (evidence.openPr) live.push(evidence.openPr);
13517
- if (evidence.branch) live.push(`live branch ${evidence.branch} on ${repo}`);
13446
+ if (evidence.openPr && workIsLive(evidence.openPrAt)) live.push(evidence.openPr);
13447
+ if (evidence.branch && workIsLive(evidence.branchAt)) live.push(`live branch ${evidence.branch} on ${repo}`);
13518
13448
  return live;
13519
13449
  }
13450
+ function staleWorkLines(evidence) {
13451
+ const age = (at) => formatClaimAge(Date.now() - Date.parse(at ?? ""));
13452
+ return [
13453
+ evidence.branch && !workIsLive(evidence.branchAt) ? `branch ${evidence.branch} (last commit ${age(evidence.branchAt)} ago)` : void 0,
13454
+ evidence.openPr && !workIsLive(evidence.openPrAt) ? `${evidence.openPr.replace(/ would close it on merge$/, "")} (updated ${age(evidence.openPrAt)} ago)` : void 0
13455
+ ].filter((line) => Boolean(line));
13456
+ }
13520
13457
  function sameClaimHost(a, b) {
13521
13458
  return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
13522
13459
  }
@@ -13549,13 +13486,17 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
13549
13486
  const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
13550
13487
  const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
13551
13488
  const unverifiable = ownership === "mine" ? [] : evidence.failed;
13489
+ const contested = ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0);
13490
+ const carry = staleWorkLines(evidence);
13491
+ const staleStamp = !contested && !resume && ownership !== "mine" && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
13552
13492
  return {
13553
- contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
13493
+ contested,
13554
13494
  live,
13555
13495
  unverifiable,
13556
13496
  ownership,
13557
13497
  marker: evidence.marker,
13558
- resume
13498
+ resume,
13499
+ ...staleStamp ? { staleStamp } : {}
13559
13500
  };
13560
13501
  }
13561
13502
  function laneContestParts(contest) {
@@ -13999,11 +13940,13 @@ async function claimOneBoardItem(ctx, selector, options) {
13999
13940
  };
14000
13941
  let previousHolder;
14001
13942
  let resumeEvidence;
13943
+ let staleStamp;
13944
+ const tookOver = () => ({ outcome: "took-over", holder, previousHolder, ...staleStamp ? { staleStamp } : {} });
14002
13945
  const claimedReceipt = () => ({
14003
13946
  ...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
14004
- ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder }
13947
+ ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "claimed", holder }
14005
13948
  });
14006
- const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
13949
+ const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "held", holder };
14007
13950
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
14008
13951
  const pre = evaluateClaim(flatItem, assignedLogin);
14009
13952
  if (!pre.ok) throw new NotClaimableError(pre.reason, { ref: flatItem.ref, status: flatItem.status });
@@ -14016,6 +13959,7 @@ async function claimOneBoardItem(ctx, selector, options) {
14016
13959
  const refuseIfContested = async () => {
14017
13960
  const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
14018
13961
  resumeEvidence = void 0;
13962
+ staleStamp = void 0;
14019
13963
  const displaced = () => ({
14020
13964
  login: assignedLogin,
14021
13965
  ...contest.marker ? {
@@ -14028,6 +13972,9 @@ async function claimOneBoardItem(ctx, selector, options) {
14028
13972
  if (contest.resume && contest.marker) {
14029
13973
  resumeEvidence = contest.resume;
14030
13974
  previousHolder = displaced();
13975
+ } else if (contest.staleStamp) {
13976
+ staleStamp = contest.staleStamp;
13977
+ previousHolder = displaced();
14031
13978
  }
14032
13979
  return;
14033
13980
  }
@@ -14042,7 +13989,7 @@ async function claimOneBoardItem(ctx, selector, options) {
14042
13989
  if (options.check) {
14043
13990
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
14044
13991
  }
14045
- const renewed = await postClaimMarkerComment(client, item, ctx.session);
13992
+ const renewed = await postClaimMarkerComment(client, item, ctx.session, staleStamp);
14046
13993
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
14047
13994
  }
14048
13995
  if (options.check) {
@@ -14054,7 +14001,7 @@ async function claimOneBoardItem(ctx, selector, options) {
14054
14001
  } catch (e) {
14055
14002
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
14056
14003
  }
14057
- await postClaimMarkerComment(client, item, ctx.session);
14004
+ await postClaimMarkerComment(client, item, ctx.session, staleStamp);
14058
14005
  let credential = "user";
14059
14006
  try {
14060
14007
  ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
@@ -14108,7 +14055,7 @@ async function claimBoardIssues(options, deps = {}) {
14108
14055
  const ref = `${selector.repo}#${selector.number}`;
14109
14056
  try {
14110
14057
  const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
14111
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
14058
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, staleStamp: result.staleStamp, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
14112
14059
  } catch (e) {
14113
14060
  results[index] = { ref, claimed: false, reason: e.message };
14114
14061
  }
@@ -14296,7 +14243,7 @@ async function setBoardPriorityForSelector(options, deps = {}) {
14296
14243
  }
14297
14244
  return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
14298
14245
  }
14299
- async function postClaimMarkerComment(client, item, actor = describeSessionIdentity()) {
14246
+ async function postClaimMarkerComment(client, item, actor = describeSessionIdentity(), staleStamp) {
14300
14247
  try {
14301
14248
  const marker = {
14302
14249
  v: 1,
@@ -14307,7 +14254,7 @@ async function postClaimMarkerComment(client, item, actor = describeSessionIdent
14307
14254
  };
14308
14255
  await postIssueComment(client, {
14309
14256
  ref: item.ref,
14310
- body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.
14257
+ body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.${staleStamp ? ` Took over from ${staleStamp}.` : ""}
14311
14258
 
14312
14259
  <!-- mmi-claim: ${JSON.stringify(marker)} -->`
14313
14260
  });
@@ -16436,8 +16383,18 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16436
16383
  if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
16437
16384
  const version = tag.replace(/^v/, "");
16438
16385
  let foldNoteExtra = "";
16386
+ const timings = [];
16387
+ const timed = async (label, fn) => {
16388
+ const t0 = (deps.now ?? Date.now)();
16389
+ try {
16390
+ return await fn();
16391
+ } finally {
16392
+ timings.push(`${label} ${(((deps.now ?? Date.now)() - t0) / 1e3).toFixed(1)}s`);
16393
+ }
16394
+ };
16395
+ const withTimings = (note) => `${note}; fold timings: ${timings.join(", ")}`;
16439
16396
  if (model === "hub-serverless") {
16440
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]);
16397
+ await timed("release-distribution prepare", () => deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]));
16441
16398
  if (verifyRelease) {
16442
16399
  await refuseDivergentPublishedNpmPack(deps, version);
16443
16400
  } else {
@@ -16445,10 +16402,10 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16445
16402
  if (restored) foldNoteExtra = `; ${restored}`;
16446
16403
  }
16447
16404
  } else {
16448
- const refresh = await refreshSharedInstallerDeps(deps);
16405
+ const refresh = await timed("installer refresh", () => refreshSharedInstallerDeps(deps));
16449
16406
  if (refresh) foldNoteExtra = `; ${refresh}`;
16450
- await installAppFoldDeps(deps);
16451
- await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
16407
+ await timed("npm ci", () => installAppFoldDeps(deps));
16408
+ await timed("npm version", () => deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]));
16452
16409
  const patch = Number(version.split(".")[2] ?? "0");
16453
16410
  if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
16454
16411
  await alignExistingHotfixReleaseMarker(deps, version);
@@ -16467,12 +16424,12 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
16467
16424
  );
16468
16425
  }
16469
16426
  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}`;
16427
+ if (!staged) return withTimings(`version fold: manifests already at ${version} \u2014 nothing committed${foldNoteExtra}`);
16471
16428
  await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
16472
16429
  if (model === "hub-serverless" && verifyRelease) {
16473
- await deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]);
16430
+ await timed("release-distribution verify", () => deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]));
16474
16431
  }
16475
- return `version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}`;
16432
+ return withTimings(`version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}`);
16476
16433
  }
16477
16434
  function foldPortManualCommand(model, version) {
16478
16435
  return model === "hub-serverless" ? `node scripts/release-distribution.mjs prepare ${version}` : `npm version ${version} --no-git-tag-version --allow-same-version`;
@@ -17268,10 +17225,10 @@ var rollout_plan_default = {
17268
17225
  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
17226
  },
17270
17227
  baseline: {
17271
- version: "4.5.86",
17272
- tag: "v4.5.86",
17273
- commit: "6485afaf5af8",
17274
- npm: "@mutmutco/cli@4.5.86"
17228
+ version: "4.5.88",
17229
+ tag: "v4.5.88",
17230
+ commit: "6e198cd64755",
17231
+ npm: "@mutmutco/cli@4.5.88"
17275
17232
  },
17276
17233
  exitCriterion: "fleet-n-of-n",
17277
17234
  hubOnlyShortcut: "forbidden",
@@ -17288,14 +17245,14 @@ var rollout_plan_default = {
17288
17245
  repo: "mutmutco/mmi-hub",
17289
17246
  role: "canary",
17290
17247
  schedule: "train",
17291
- v3Target: "v4.5.86"
17248
+ v3Target: "v4.5.88"
17292
17249
  }
17293
17250
  ],
17294
17251
  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
17252
  rollback: {
17296
17253
  independent: true,
17297
- mechanism: "npm dist-tag latest -> 4.5.86 and redeploy the Hub Lambda from tag v4.5.86 (6485afaf5af8); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17298
- v3Target: "v4.5.86 (@mutmutco/cli@4.5.86, tag commit 6485afaf5af8 \u2014 last known-good release carrying the repo-index v4-only contract)"
17254
+ mechanism: "npm dist-tag latest -> 4.5.88 and redeploy the Hub Lambda from tag v4.5.88 (6e198cd64755); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17255
+ v3Target: "v4.5.88 (@mutmutco/cli@4.5.88, tag commit 6e198cd64755 \u2014 last known-good release carrying the repo-index v4-only contract)"
17299
17256
  }
17300
17257
  },
17301
17258
  {
@@ -19609,7 +19566,7 @@ var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.par
19609
19566
  var TRAIN_CHECK_ATTEMPTS = 40;
19610
19567
  var TRAIN_QUEUED_CHECK_ATTEMPTS = 80;
19611
19568
  var TRAIN_CHECK_DELAY_MS = 15e3;
19612
- var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done", "guard", "verdict"]);
19569
+ var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
19613
19570
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
19614
19571
  var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
19615
19572
  async function correlateRun(deps, args) {
@@ -19963,7 +19920,7 @@ async function alignRcForward(deps, ctx, tag) {
19963
19920
  return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
19964
19921
  }
19965
19922
  var FLEET_FOREIGN_VERDICT_GRACE_ATTEMPTS = 3;
19966
- function resolveContextState(context, checkRuns, statuses) {
19923
+ function resolveContextState(context, checkRuns, statuses, opts = {}) {
19967
19924
  let sawFailure = false;
19968
19925
  let sawSuccess = false;
19969
19926
  let sawPending = false;
@@ -19982,11 +19939,12 @@ function resolveContextState(context, checkRuns, statuses) {
19982
19939
  else if (s.state === "failure" || s.state === "error") sawFailure = true;
19983
19940
  else sawPending = true;
19984
19941
  }
19942
+ if (sawSuccess && opts.successWins) return "success";
19985
19943
  if (sawPending) return "pending";
19986
19944
  if (sawSuccess) return "success";
19987
19945
  return sawFailure ? "failed" : "pending";
19988
19946
  }
19989
- async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
19947
+ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince, opts = {}) {
19990
19948
  if (required.length === 0) {
19991
19949
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
19992
19950
  }
@@ -20016,6 +19974,8 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20016
19974
  let workflowRunNote = "";
20017
19975
  let workflowRunEverSeen = false;
20018
19976
  let hadQueuedWorkflowRun = false;
19977
+ let freshActiveRun = false;
19978
+ let anyActiveRun = false;
20019
19979
  const everObserved = /* @__PURE__ */ new Set();
20020
19980
  const autoSatisfied = /* @__PURE__ */ new Set();
20021
19981
  let fleetForeignStreak = 0;
@@ -20048,7 +20008,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20048
20008
  "--limit",
20049
20009
  "100",
20050
20010
  "--json",
20051
- "databaseId,status,conclusion,headSha"
20011
+ "databaseId,status,conclusion,headSha,createdAt"
20052
20012
  ]);
20053
20013
  const parsedWorkflowRuns = JSON.parse(workflowOut);
20054
20014
  if (!Array.isArray(parsedWorkflowRuns)) throw new Error("workflow-runs response was not an array");
@@ -20057,6 +20017,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20057
20017
  workflowRunRead = "found";
20058
20018
  workflowRunEverSeen = true;
20059
20019
  const activeRun = matchingRuns.find((run) => run.status === "queued" || run.status === "in_progress");
20020
+ freshActiveRun = freshThreshold !== void 0 && matchingRuns.some((run) => {
20021
+ if (!["queued", "in_progress", "waiting", "pending"].includes(String(run.status))) return false;
20022
+ const t = Date.parse(String(run.createdAt ?? ""));
20023
+ return Number.isFinite(t) && t >= freshThreshold;
20024
+ });
20025
+ if (freshActiveRun) hadQueuedWorkflowRun = true;
20026
+ anyActiveRun = matchingRuns.some((run) => ["queued", "in_progress", "waiting", "pending"].includes(String(run.status)));
20060
20027
  if (activeRun) {
20061
20028
  if (activeRun.status === "queued") hadQueuedWorkflowRun = true;
20062
20029
  workflowRunNote = activeRun.status === "queued" ? "Actions run is queued behind busy runners" : "Actions run is in progress";
@@ -20065,10 +20032,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20065
20032
  }
20066
20033
  } else {
20067
20034
  workflowRunRead = "empty";
20035
+ freshActiveRun = false;
20036
+ anyActiveRun = false;
20068
20037
  workflowRunNote = "";
20069
20038
  }
20070
20039
  } catch (e) {
20071
20040
  workflowRunRead = "failed";
20041
+ freshActiveRun = false;
20042
+ anyActiveRun = false;
20072
20043
  workflowRunNote = `workflow-run lookup failed: ${e.message || String(e)}`;
20073
20044
  }
20074
20045
  for (const c of required) {
@@ -20080,7 +20051,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20080
20051
  }
20081
20052
  }
20082
20053
  const pending = required.filter((c) => !autoSatisfied.has(c));
20083
- const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
20054
+ const holdStale = freshThreshold !== void 0 && (attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS || freshActiveRun);
20084
20055
  const states = pending.map((c) => {
20085
20056
  if (fleetPosted && c === FLEET_LOCKSTEP_CONTEXT) {
20086
20057
  const resolution = resolveFleetVerdictState(fleetPosted, statuses);
@@ -20092,6 +20063,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
20092
20063
  fleetForeignStreak = 0;
20093
20064
  return [c, resolution.state];
20094
20065
  }
20066
+ if (opts.exactCommitGreenSuffices) {
20067
+ const state = resolveContextState(c, checkRuns, statuses, { successWins: true });
20068
+ const inGrace = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
20069
+ return [c, state === "failed" && (anyActiveRun || inGrace) ? "pending" : state];
20070
+ }
20095
20071
  if (holdStale) {
20096
20072
  const runsFor = checkRuns.filter((r) => r.name === c);
20097
20073
  if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
@@ -21372,6 +21348,10 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21372
21348
  };
21373
21349
  }
21374
21350
  try {
21351
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
21352
+ if (isBundleOnlySuiteMember(ctx.repo, meta.suite)) {
21353
+ return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
21354
+ }
21375
21355
  await deps.run("git", ["fetch", "origin", "rc"]);
21376
21356
  const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
21377
21357
  if (rcNow !== releasedRcSha) {
@@ -21904,14 +21884,6 @@ function installedClaudePluginVersion() {
21904
21884
  return void 0;
21905
21885
  }
21906
21886
  }
21907
- function manifestVersion(path2) {
21908
- try {
21909
- const manifest = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
21910
- return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
21911
- } catch {
21912
- return void 0;
21913
- }
21914
- }
21915
21887
  function readHermesPluginEvidence(env = process.env) {
21916
21888
  const host = hermesConfigRoot(env);
21917
21889
  const root = hermesPluginRoot(env);
@@ -21963,9 +21935,6 @@ function readHermesPluginEvidence(env = process.env) {
21963
21935
  function installedSurfacePluginVersion(surface) {
21964
21936
  const token = surfaceToken(surface);
21965
21937
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
21966
- if (token === "cursor") {
21967
- return manifestVersion((0, import_node_path18.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
21968
- }
21969
21938
  if (token === "jervcode") {
21970
21939
  return installedJervCodePackageVersion();
21971
21940
  }
@@ -23310,7 +23279,12 @@ var TRANSIENT_TRANSPORT_MARKERS = [
23310
23279
  ];
23311
23280
  function classifyTransientTransportFailure(evidence) {
23312
23281
  if (!evidence) return false;
23313
- return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence));
23282
+ return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence)) || isSetupBunSpawnEtxtbsy(evidence);
23283
+ }
23284
+ function isSetupBunSpawnEtxtbsy(evidence) {
23285
+ if (!/\bspawn ETXTBSY\b/.test(evidence)) return false;
23286
+ const steps = [...evidence.matchAll(/"steps":\[([^\]]*)\]/g)].flatMap((m) => [...m[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((s) => s[1]));
23287
+ return steps.length > 0 && steps.every((name) => /\bsetup[- ]bun\b|oven-sh\/setup-bun/i.test(name));
23314
23288
  }
23315
23289
  var TRANSIENT_EVIDENCE_MAX_CHARS = 16e3;
23316
23290
  var AUTO_RERUN_MAX_RETRIES = 1;
@@ -23357,8 +23331,16 @@ async function transientEvidenceForRun(deps, repo, runId) {
23357
23331
  parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--log-failed"]));
23358
23332
  } catch {
23359
23333
  }
23360
- const evidence = parts.filter(Boolean).join("\n");
23361
- return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
23334
+ const [head, ...rest] = parts.filter(Boolean);
23335
+ if (rest.length === 0 || !head?.startsWith("[")) {
23336
+ const evidence = parts.filter(Boolean).join("\n");
23337
+ return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
23338
+ }
23339
+ const header = head.slice(0, TRANSIENT_EVIDENCE_MAX_CHARS / 4);
23340
+ const log = rest.join("\n");
23341
+ const room = TRANSIENT_EVIDENCE_MAX_CHARS - header.length - 1;
23342
+ return `${header}
23343
+ ${log.length > room ? log.slice(-room) : log}`;
23362
23344
  }
23363
23345
  function extractFailedRequiredContexts(failure) {
23364
23346
  const match = /required train check failed:\s*([^\n(]+)/.exec(failure);
@@ -23747,17 +23729,23 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23747
23729
  async (current) => {
23748
23730
  if (!current) return { value: { ledger: void 0, notes } };
23749
23731
  const phases = { ...current.phases };
23750
- for (const phase of ["deploy", "publish"]) {
23732
+ for (const phase of input.phases ?? ["deploy", "publish"]) {
23751
23733
  const rec = phases[phase];
23752
23734
  if (rec.updatedAt === "never") continue;
23753
23735
  if (rec.runId == null) {
23754
- if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 the live deploy/publish re-derivation below decides the leg`);
23736
+ if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 live phase evidence decides the leg`);
23755
23737
  continue;
23756
23738
  }
23757
- if (rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
23739
+ if (phase === "ordinaryCi" && rec.runRepo != null && rec.runRepo !== input.repo) {
23740
+ 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)`);
23741
+ }
23742
+ if (phase === "ordinaryCi" && rec.sha != null && rec.sha.toLowerCase() !== input.sha.toLowerCase()) {
23743
+ 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)`);
23744
+ }
23745
+ if (phase === "ordinaryCi" && rec.runProvenance !== "sha" || phase !== "ordinaryCi" && rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
23758
23746
  throw new ReleaseLedgerError(
23759
23747
  "invalid-record",
23760
- `the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'|'dispatch' required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
23748
+ `the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'${phase === "ordinaryCi" ? "" : "|'dispatch'"} required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
23761
23749
  );
23762
23750
  }
23763
23751
  const runRepo = rec.runRepo ?? input.repo;
@@ -23792,7 +23780,8 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
23792
23780
  }
23793
23781
  if (meta.conclusion === "success") {
23794
23782
  if (rec.state !== "complete") {
23795
- phases[phase] = { ...rec, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
23783
+ const { error: _error, ...withoutPriorError } = rec;
23784
+ phases[phase] = { ...withoutPriorError, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
23796
23785
  notes.push(`${phase}: ${liveLabel} re-verified green live`);
23797
23786
  }
23798
23787
  continue;
@@ -25070,6 +25059,19 @@ async function runTrainDoctor(input) {
25070
25059
  try {
25071
25060
  let ledger = parseReleaseLedger(ledgerRaw);
25072
25061
  let derived = deriveReleasePhaseStatus(ledger);
25062
+ const ordinaryCi = ledger.phases.ordinaryCi;
25063
+ if (healLedger && ordinaryCi.state === "failed" && ordinaryCi.runId != null) {
25064
+ const outcome = await reverifyPersistedRunPhases(train, ledgerPath, {
25065
+ repo: ledger.repo,
25066
+ tag: ledger.tag,
25067
+ tagSha: ledger.tagSha
25068
+ }, { sha: ledger.tagSha, repo: ledger.repo, phases: ["ordinaryCi"] });
25069
+ if (outcome.notes.some((note) => note.startsWith("ordinaryCi:") && note.includes("re-verified green live"))) {
25070
+ 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" });
25071
+ ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
25072
+ derived = deriveReleasePhaseStatus(ledger);
25073
+ }
25074
+ }
25073
25075
  const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
25074
25076
  if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
25075
25077
  const run = await deps.healRunLegs(train, cwd, deployModel);
@@ -26174,7 +26176,9 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
26174
26176
  const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
26175
26177
  try {
26176
26178
  deps.warn?.("Release: Checking the package before publishing");
26179
+ const dryRunStart = (deps.now ?? Date.now)();
26177
26180
  await verifyPublishDryRun(deps, ctx, meta, deployModel);
26181
+ deps.warn?.(`Release: publish dry run took ${(((deps.now ?? Date.now)() - dryRunStart) / 1e3).toFixed(1)}s`);
26178
26182
  } catch (e) {
26179
26183
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
26180
26184
  }
@@ -26412,7 +26416,7 @@ async function runRcandResumeLocked(deps, options) {
26412
26416
  await requireCleanTree(deps);
26413
26417
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
26414
26418
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
26415
- const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", false, deps.suiteCut);
26419
+ const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", options.suiteSolo === true, deps.suiteCut);
26416
26420
  if (suiteRefusal) throw new Error(`rcand --resume: ${suiteRefusal}`);
26417
26421
  const branchHints = await loadReleaseTrackBranchHints(deps);
26418
26422
  if (isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct") {
@@ -27725,7 +27729,6 @@ async function listWorkflowPaths(deps, repo, branch) {
27725
27729
  return e?.status === 404 ? [] : void 0;
27726
27730
  }
27727
27731
  }
27728
- var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
27729
27732
  function gateWorkflowFiles(prWorkflowPaths) {
27730
27733
  const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
27731
27734
  return files.length ? files : DEFAULT_GATE_FILES;
@@ -28062,8 +28065,7 @@ async function auditRepoCi(repo, callerDeps) {
28062
28065
  }
28063
28066
  }
28064
28067
  if (explicitNoCi) {
28065
- const resolved = await getEmittedPrContexts();
28066
- const emitted = resolved?.filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
28068
+ const emitted = await getEmittedPrContexts();
28067
28069
  if (emitted && emitted.length > 0) {
28068
28070
  checks.push({
28069
28071
  ok: false,
@@ -28194,7 +28196,7 @@ async function auditRepoCi(repo, callerDeps) {
28194
28196
  if (hasRequiredChecks) {
28195
28197
  const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
28196
28198
  const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
28197
- const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job) && !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
28199
+ const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
28198
28200
  checks.push({
28199
28201
  ok: unblocking.length === 0,
28200
28202
  label: UNBLOCKING_GATE_JOBS_LABEL,
@@ -28217,8 +28219,7 @@ async function auditRepoCi(repo, callerDeps) {
28217
28219
  for (const ref of releaseBranches) {
28218
28220
  emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
28219
28221
  }
28220
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28221
- const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore });
28222
+ const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
28222
28223
  checks.push({
28223
28224
  ok: unreachable.length === 0,
28224
28225
  label: RELEASE_BRANCH_REACHABLE_LABEL,
@@ -28685,8 +28686,7 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
28685
28686
  if (authority.coversReleaseBranches && authority.source !== "registry META requiredChecks") {
28686
28687
  const track = resolveReleaseTrack(meta, void 0, repo);
28687
28688
  const releaseBranches = branchesForTrack(track).filter((b) => b !== baseBranch);
28688
- const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
28689
- const requiredForReleaseBranches = authority.contexts.filter((context) => !ignore.has(context));
28689
+ const requiredForReleaseBranches = authority.contexts.filter((context) => !TRAIN_PR_ONLY_CONTEXTS.has(context));
28690
28690
  for (const branch of releaseBranches) {
28691
28691
  if (await branchPresence(deps, repo, branch) !== true) continue;
28692
28692
  const emittedThere = await resolveEmittedPrContexts(deps, repo, branch);
@@ -30122,11 +30122,13 @@ function registerBoardCommands(program3) {
30122
30122
  }
30123
30123
  function claimVerdict(ref, result) {
30124
30124
  const holder = formatClaimHolder(result.holder);
30125
- const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
30125
+ const previousHolder = result.staleStamp ?? (result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane");
30126
30126
  const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
30127
30127
  if (result.checked) {
30128
30128
  if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
30129
- if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
30129
+ if (result.outcome === "took-over") {
30130
+ return result.staleStamp ? `Check ${ref}: ${previousHolder} - claim would take it over for ${holder} (nothing written)` : `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
30131
+ }
30130
30132
  if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
30131
30133
  return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
30132
30134
  }
@@ -30507,7 +30509,7 @@ function trainPlan(command, options = {}) {
30507
30509
  { label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
30508
30510
  { label: "tag release and publish GitHub Release", gated: true },
30509
30511
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
30510
- { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true },
30512
+ ...!isBundleOnlySuiteMember(options.repo ?? "", options.suite) ? [{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true }] : [],
30511
30513
  { label: "roll development forward and align rc to the released main", gated: true },
30512
30514
  { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli oracle org project sync-info --apply", gated: true }
30513
30515
  ];
@@ -30515,6 +30517,8 @@ function trainPlan(command, options = {}) {
30515
30517
  return [
30516
30518
  authority,
30517
30519
  { label: "verify current branch is rc", gated: true },
30520
+ // #7783: the version the apply will mint, read from the rc tag at origin/rc's head (never a stale higher cycle).
30521
+ { label: `resolve the release tag from the rc head (${options.releaseTagError ? `error: ${options.releaseTagError}` : `releaseTag=${options.releaseTag ?? "(unresolved)"}`})`, command: "node scripts/next-version.mjs release", gated: true },
30518
30522
  { label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
30519
30523
  { label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
30520
30524
  { label: "preflight GitHub Actions hosted job start (billing/spending) before minting a tag", command: "train dispatches actions-job-start-canary.yml on MMI-Hub (#5604); exact-run --retry-publish skips this", gated: true },
@@ -31409,7 +31413,7 @@ var SETTABLE_VAR_HINTS = {
31409
31413
  requiredGcpApis: "comma-string",
31410
31414
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
31411
31415
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
31412
- tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}}; use {artifact} in argv when required; a secretOutput is master-only',
31416
+ tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
31413
31417
  secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
31414
31418
  edgeDomains: "JSON {dev,rc,main} domain map",
31415
31419
  statusOptions: "JSON name\u2192id map",
@@ -31440,6 +31444,18 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
31440
31444
  }
31441
31445
  return null;
31442
31446
  }
31447
+ function tenantTaskWriteRefusal(existing, patch) {
31448
+ const remove2 = Array.isArray(patch.tenantTasksRemove) ? patch.tenantTasksRemove : [];
31449
+ const stored = existing?.tenantTasks ?? {};
31450
+ const slug = existing?.slug ?? "this project";
31451
+ const missing = remove2.filter((name) => stored[name] === void 0);
31452
+ 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`;
31453
+ const sent = patch.tenantTasks;
31454
+ if (sent === null || sent === void 0) return null;
31455
+ const merged = /* @__PURE__ */ new Set([...Object.keys(stored), ...Object.keys(sent)]);
31456
+ 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>`;
31457
+ return null;
31458
+ }
31443
31459
  function secretRemovalRefusal(existing, patch) {
31444
31460
  const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
31445
31461
  if (remove2.length === 0) return null;
@@ -31614,12 +31630,26 @@ function buildProjectSetPatch(input) {
31614
31630
  }
31615
31631
  patch.secretsRemove = keys;
31616
31632
  }
31633
+ if (input.unsetTasks?.length) {
31634
+ if (patch.tenantTasks !== void 0) {
31635
+ throw new Error("org project set: --unset-task cannot be combined with --var tenantTasks or --unset tenantTasks \u2014 pass one or the other");
31636
+ }
31637
+ const names = [];
31638
+ for (const raw of input.unsetTasks) {
31639
+ const name = raw.trim();
31640
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) {
31641
+ throw new Error(`org project set: --unset-task must be a task name (lowercase kebab-case) \u2014 got ${JSON.stringify(raw)}`);
31642
+ }
31643
+ if (!names.includes(name)) names.push(name);
31644
+ }
31645
+ patch.tenantTasksRemove = names;
31646
+ }
31617
31647
  if (input.clearWebProfile) {
31618
31648
  patch.oauth = null;
31619
31649
  patch.edgeDomains = null;
31620
31650
  }
31621
31651
  if (Object.keys(patch).length === 0) {
31622
- throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, and/or --clear-web-profile");
31652
+ throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, --unset-task NAME, and/or --clear-web-profile");
31623
31653
  }
31624
31654
  return patch;
31625
31655
  }
@@ -35796,8 +35826,24 @@ async function readSuiteStatus(deps) {
35796
35826
  })));
35797
35827
  if (!deps.leadTags || !deps.registryAt) return { suite: "mm-work", members: rows };
35798
35828
  const tags = await deps.leadTags().catch(() => []);
35799
- const latest = (rc) => tags.filter((t) => /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(t) && t.includes("-rc.") === rc).sort((a, b) => compareVersions(b, a))[0];
35800
- const leadTag = { rc: latest(true), main: latest(false) };
35829
+ 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];
35830
+ let rc;
35831
+ let rcLabel = "n/a";
35832
+ const rcTags = tags.filter((t) => /^v\d+\.\d+\.\d+-rc\.\d+$/.test(t)).sort((a, b) => compareVersions(b, a));
35833
+ if (rcTags.length) {
35834
+ const rcHead = await deps.head(rows[0].repo, "rc").catch(() => void 0);
35835
+ if (rcHead === void 0) rcLabel = "unknown";
35836
+ else {
35837
+ for (const t of rcTags) {
35838
+ if ((await deps.tagCommit(rows[0].repo, t).catch(() => void 0))?.toLowerCase() === rcHead?.toLowerCase()) {
35839
+ rc = t;
35840
+ break;
35841
+ }
35842
+ }
35843
+ rcLabel = rc ?? "none at head";
35844
+ }
35845
+ }
35846
+ const leadTag = { rc, main: latest(false) };
35801
35847
  const lead = rows[0].stages;
35802
35848
  for (const stage of ["rc", "main"]) {
35803
35849
  const tag = leadTag[stage];
@@ -35813,7 +35859,7 @@ async function readSuiteStatus(deps) {
35813
35859
  }
35814
35860
  }
35815
35861
  const recorded = leadTag.main && deps.leadReleaseBody ? parseSuiteMapping(await deps.leadReleaseBody(leadTag.main).catch(() => null)) : void 0;
35816
- return { suite: "mm-work", lead: { dev: lead[0].head, rc: leadTag.rc ?? "n/a", main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
35862
+ return { suite: "mm-work", lead: { dev: lead[0].head, rc: rcLabel, main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
35817
35863
  }
35818
35864
  var short = (v) => /^[a-f0-9]{40}$/i.test(v) ? v.slice(0, 12) : v;
35819
35865
  var proofText = (p) => p ? p.state === "proven" ? "proven" : p.state === "mismatch" ? `mismatch (${p.detail})` : `unprovable (${p.detail})` : "";
@@ -35855,7 +35901,8 @@ async function setSuiteHold(deps, member, hold) {
35855
35901
  const after = await deps.read(slug);
35856
35902
  if (!after.ok || !after.project) throw new Error(`${found.repo}: write sent but read-back failed; verify with org project get`);
35857
35903
  const got = after.project.suiteHold ?? null;
35858
- if (JSON.stringify(got) !== JSON.stringify(hold)) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
35904
+ const same2 = got === null || hold === null ? got === hold : got.reason === hold.reason && got.since === hold.since;
35905
+ if (!same2) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
35859
35906
  if (track && after.project.releaseTrack !== track) throw new Error(`${found.repo}: releaseTrack changed from ${track} to ${after.project.releaseTrack ?? "unset"} by the write; restore it with org project set --release-track ${track}`);
35860
35907
  return hold ? `${found.repo}: HELD (${hold.reason})` : `${found.repo}: hold cleared`;
35861
35908
  }
@@ -36037,7 +36084,7 @@ async function carriedPins(deps, member, bundles) {
36037
36084
  }
36038
36085
  throw new Error(`${member.repo}: nothing new since rc, but no pinned MM-Work bundle carries development ${member.development.slice(0, 12)}; pin it before the suite cut`);
36039
36086
  }
36040
- function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
36087
+ function abandonableCut(recordPath, ownerGo) {
36041
36088
  const record = readSuiteCut(recordPath);
36042
36089
  if (!record) throw new Error("no suite cut record to abandon");
36043
36090
  if (record.leadReleased || record.members.some((m) => m.released)) {
@@ -36046,6 +36093,19 @@ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Dat
36046
36093
  if (record.pinPr && !ownerGo) {
36047
36094
  throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go), release it (suite release), or retire it with suite abandon --owner-go`);
36048
36095
  }
36096
+ return record;
36097
+ }
36098
+ async function suiteMarkAbandoned(deps, ownerGo = false) {
36099
+ const record = abandonableCut(deps.recordPath, ownerGo);
36100
+ const tags = [
36101
+ ...record.members.flatMap((m) => m.tag && m.sha ? [{ repo: m.repo, tag: m.tag, sha: m.sha }] : []),
36102
+ ...record.leadRcTag && record.leadRcSha ? [{ repo: SUITE_REPO, tag: record.leadRcTag, sha: record.leadRcSha }] : []
36103
+ ];
36104
+ for (const t of tags) await deps.markAbandoned(t.repo, t.tag, t.sha);
36105
+ return tags.map((t) => `${t.repo}@${t.tag}`);
36106
+ }
36107
+ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
36108
+ const record = abandonableCut(recordPath, ownerGo);
36049
36109
  const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}-${now.toISOString().replace(/[:.]/g, "")}.json`;
36050
36110
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
36051
36111
  (0, import_node_fs29.renameSync)(recordPath, archive);
@@ -36932,12 +36992,20 @@ ${lines2.join("\n")}
36932
36992
  Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
36933
36993
  }
36934
36994
  var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
36935
- async function findHotfixPickGaps(deps, sources, tolerated) {
36936
- if (sources.length === 0) return [];
36995
+ async function scanHotfixPickGaps(deps, sources, tolerated, rcRef) {
36996
+ if (sources.length === 0) return { gaps: [], rcCarried: 0 };
36937
36997
  const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
36938
36998
  const carried = /* @__PURE__ */ new Set();
36939
36999
  const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
36940
37000
  for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
37001
+ const notOnRc = /* @__PURE__ */ new Set();
37002
+ const rcPicked = /* @__PURE__ */ new Set();
37003
+ if (rcRef) {
37004
+ (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));
37005
+ const rcOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", `origin/development..${rcRef}`]);
37006
+ for (const m of rcOnly.matchAll(CHERRY_PICKED_FROM)) rcPicked.add(m[1].toLowerCase());
37007
+ }
37008
+ let rcCarried = 0;
36941
37009
  const gaps = [];
36942
37010
  const devOnly = (await deps.run("git", ["log", "--no-merges", "--reverse", "--format=%H%x09%s", "origin/main..origin/development"])).split("\n").map((s) => s.trim()).filter(Boolean);
36943
37011
  for (const line of devOnly) {
@@ -36945,11 +37013,37 @@ async function findHotfixPickGaps(deps, sources, tolerated) {
36945
37013
  if (!sha) continue;
36946
37014
  const lower = sha.toLowerCase();
36947
37015
  if (namedShas.has(lower) || carried.has(lower)) continue;
37016
+ if (rcRef && (!notOnRc.has(lower) || rcPicked.has(lower))) {
37017
+ rcCarried++;
37018
+ continue;
37019
+ }
36948
37020
  const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
36949
37021
  if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
36950
37022
  gaps.push({ sha, subject: subjectParts.join(" ") });
36951
37023
  }
36952
- return gaps;
37024
+ return { gaps, rcCarried };
37025
+ }
37026
+ async function resolveActiveRcCandidate(deps) {
37027
+ try {
37028
+ await deps.run("git", ["rev-parse", "--verify", "--quiet", "origin/rc"]);
37029
+ } catch {
37030
+ return { reason: "origin/rc does not exist" };
37031
+ }
37032
+ try {
37033
+ 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));
37034
+ const tag = rcTags.sort((a, b) => Number(a.split("-rc.")[1]) - Number(b.split("-rc.")[1])).pop();
37035
+ if (!tag) return { reason: "no rc tag points at the origin/rc head, so it is not an active candidate" };
37036
+ if ((await deps.run("git", ["ls-remote", "origin", `refs/abandoned-rc/${tag}`])).trim()) {
37037
+ return { reason: `${tag} was abandoned (marker refs/abandoned-rc/${tag} on origin), so it is not an active candidate` };
37038
+ }
37039
+ const released = tag.replace(/-rc\.\d+$/, "");
37040
+ if ((await deps.run("git", ["tag", "--list", released])).trim()) {
37041
+ return { reason: `${released} is already released, so ${tag} is no longer a pending candidate` };
37042
+ }
37043
+ return { ref: "origin/rc", tag };
37044
+ } catch (e) {
37045
+ return { reason: `the origin/rc candidate could not be read (${e.message ?? e})` };
37046
+ }
36953
37047
  }
36954
37048
  function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
36955
37049
  const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
@@ -37169,7 +37263,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
37169
37263
  const label = sources.map((s) => s.label).join(", ");
37170
37264
  const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
37171
37265
  const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
37172
- const pickGaps = await findHotfixPickGaps(deps, sources, pickTolerated);
37266
+ let rcCandidate;
37267
+ if (resolveReleaseTrack(meta, void 0, ctx.repo) === "full") {
37268
+ const found = await resolveActiveRcCandidate(deps);
37269
+ if ("reason" in found) notes.push(`pick-completeness guard checked against origin/main only: ${found.reason} (#7799)`);
37270
+ else rcCandidate = found;
37271
+ }
37272
+ const { gaps: pickGaps, rcCarried } = await scanHotfixPickGaps(deps, sources, pickTolerated, rcCandidate?.ref);
37273
+ if (rcCandidate && rcCarried > 0) {
37274
+ notes.push(`pick-completeness: ${rcCarried} development commit(s) carried by rc candidate ${rcCandidate.tag}, not counted as gaps (#7799)`);
37275
+ }
37173
37276
  const unacked = pickGaps.filter((g) => {
37174
37277
  const sha = g.sha.toLowerCase();
37175
37278
  return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
@@ -37262,7 +37365,9 @@ async function runHotfixStartLocked(deps, options, doctor) {
37262
37365
 
37263
37366
  <!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
37264
37367
 
37265
- <!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
37368
+ <!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}${rcCandidate && rcCarried > 0 ? `
37369
+
37370
+ <!-- mmi-hotfix-rc-carried: ${rcCarried} commits carried by ${rcCandidate.tag} -->` : ""}
37266
37371
 
37267
37372
  Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
37268
37373
  ]));
@@ -37502,7 +37607,7 @@ async function waitForHotfixRequiredChecks(deps, ctx, mergedSha, required, fresh
37502
37607
  const recover = hooks.recover ?? transientRecoveryForFailedPhase;
37503
37608
  let checks;
37504
37609
  try {
37505
- checks = await wait(deps, ctx, mergedSha, required, freshSince);
37610
+ checks = await wait(deps, ctx, mergedSha, required, freshSince, { exactCommitGreenSuffices: true });
37506
37611
  } catch (e) {
37507
37612
  const failureText = e instanceof Error ? e.message : String(e);
37508
37613
  let recovery;
@@ -37531,7 +37636,7 @@ release ledger: ${recoveryFailure} - manual intervention required`);
37531
37636
 
37532
37637
  ${recovery.note}`);
37533
37638
  try {
37534
- checks = await wait(deps, ctx, mergedSha, required, void 0);
37639
+ checks = await wait(deps, ctx, mergedSha, required, void 0, { exactCommitGreenSuffices: true });
37535
37640
  } catch (e2) {
37536
37641
  const revalidationFailure = e2 instanceof Error ? e2.message : String(e2);
37537
37642
  const prior2 = (await ledger.load(anchors).catch(() => void 0))?.phases.ordinaryCi;
@@ -40048,6 +40153,30 @@ function releaseProgressVerdict(status, subject = "Release") {
40048
40153
  // src/suite-pin.ts
40049
40154
  var import_promises6 = require("node:fs/promises");
40050
40155
  var import_node_path34 = require("node:path");
40156
+ var SuitePinError = class _SuitePinError extends Error {
40157
+ diagnostic;
40158
+ constructor(repo, tag, error) {
40159
+ const e = error instanceof _SuitePinError ? { ...error.diagnostic } : error && typeof error === "object" ? error : {};
40160
+ 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;
40161
+ const signal = typeof e.signal === "string" && /^(SIGTERM|SIGKILL|SIGINT|SIGHUP|SIGABRT|SIGPIPE)$/.test(e.signal) ? e.signal : void 0;
40162
+ const hint = [error instanceof _SuitePinError ? error.message : e.message, e.stderr].filter((s) => typeof s === "string").map((s) => s.slice(0, 4096)).join("\n");
40163
+ const carriedCategory = error instanceof _SuitePinError && typeof e.category === "string" && ["timeout", "permission", "index-lock", "terminated", "exit", "unknown"].includes(e.category) ? e.category : void 0;
40164
+ 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");
40165
+ const diagnostic = {
40166
+ category,
40167
+ ...code !== void 0 ? { code } : {},
40168
+ ...signal ? { signal } : {},
40169
+ ...typeof e.killed === "boolean" ? { killed: e.killed } : {},
40170
+ ...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) && e.durationMs >= 0 && e.durationMs <= 864e5 ? { durationMs: e.durationMs } : {},
40171
+ ...e.cleanupFailed === true ? { cleanupFailed: true } : {}
40172
+ };
40173
+ const safeRepo = /^[\w.-]+\/[\w.-]+$/.test(repo) && repo.length <= 200 ? repo : "[invalid repo]";
40174
+ const safeTag = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(tag) && tag.length <= 80 ? tag : "[invalid tag]";
40175
+ 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;
40176
+ 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`);
40177
+ this.diagnostic = diagnostic;
40178
+ }
40179
+ };
40051
40180
  async function openSuiteCutPinPr(deps, input) {
40052
40181
  if (!/^[a-f0-9]{40}$/.test(input.cut) || !input.pins.length) throw new Error("suite pin PR needs an anchored cut and at least one module");
40053
40182
  const ids = input.pins.map((p) => p.module);
@@ -40190,8 +40319,8 @@ async function openSuiteRestorePinPr(deps, input) {
40190
40319
  }
40191
40320
  async function openSuitePinPr(deps, input) {
40192
40321
  const { repo, tag, stage } = input;
40193
- const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`;
40194
40322
  let scratch = "";
40323
+ let failure;
40195
40324
  try {
40196
40325
  const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
40197
40326
  if (!get.content) throw new Error("MM-Work seed/modules.json was unreadable");
@@ -40256,9 +40385,19 @@ async function openSuitePinPr(deps, input) {
40256
40385
  await run("gh", ["pr", "close", old2.url, "--repo", SUITE_REPO, "--comment", `Superseded by ${pr} (${member.id} ${stage} at ${tag}).`]);
40257
40386
  return { status: "pending", pr, url: pin };
40258
40387
  } catch (e) {
40259
- throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
40388
+ failure = new SuitePinError(repo, tag, e);
40389
+ throw failure;
40260
40390
  } finally {
40261
- if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
40391
+ if (scratch) {
40392
+ try {
40393
+ await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
40394
+ } catch (e) {
40395
+ const cleanupFailure = failure ?? new SuitePinError(repo, tag, e);
40396
+ cleanupFailure.diagnostic.cleanupFailed = true;
40397
+ cleanupFailure.message += "; owned scratch cleanup also failed";
40398
+ if (!failure) throw cleanupFailure;
40399
+ }
40400
+ }
40262
40401
  }
40263
40402
  }
40264
40403
 
@@ -45813,7 +45952,7 @@ async function requireFreshTrainCli(commandName) {
45813
45952
  process.exit(converge.reexecCode);
45814
45953
  }
45815
45954
  var GH_TRAIN_TIMEOUT_MS = 3e4;
45816
- var GH_RUN_WATCH_TIMEOUT_MS = 90 * 6e4;
45955
+ var GH_RUN_WATCH_TIMEOUT_MS = 240 * 6e4;
45817
45956
  var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
45818
45957
  var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
45819
45958
  var NPM_TRAIN_TIMEOUT_MS = 6e4;
@@ -46235,10 +46374,43 @@ function emitTrainResult(label, output, out) {
46235
46374
  if (!out) printLine(output);
46236
46375
  else printLine(`Wrote ${label} result to ${out} (UTF-8, ${writeUtf8Receipt(out, output)} bytes)`);
46237
46376
  }
46377
+ var TrainPinFollowUpError = class extends Error {
46378
+ constructor(completed, error) {
46379
+ const failure = new SuitePinError(completed.repo, completed.tag, error);
46380
+ super(failure.message);
46381
+ this.completed = completed;
46382
+ this.diagnostic = failure.diagnostic;
46383
+ }
46384
+ completed;
46385
+ diagnostic;
46386
+ };
46238
46387
  function writeTrainFailureReceipt(commandName, json, out, error, suiteSolo = false) {
46239
- if (!out) return;
46240
- const output = json ? JSON.stringify({ command: `${commandName} --apply`, verdict: "failed", ...suiteSolo ? { suiteSolo: true, suitePin: { status: "failed", error: error.message } } : {}, error: error.message }, null, 2) : `mmi-cli ${commandName} --apply: FAILED \u2014 ${error.message}`;
46241
- emitTrainResult(`${commandName} apply`, output, out);
46388
+ const completed = error instanceof TrainPinFollowUpError ? error.completed : void 0;
46389
+ if (!out && !completed) return;
46390
+ const command = commandName.includes(" ") ? commandName : `${commandName} --apply`;
46391
+ const legs = completed?.releaseVerdict?.legs ?? (completed?.command === "hotfix-release" ? hotfixFollowUpLegs(completed.runs, completed.foldStatus, ledgerAlignmentFollowUpOutcome(completed.ledger), completed.foldNote, completed.deployNote) : []);
46392
+ const failedLeg = { leg: "suite-pin-pr", status: "failed", error: error.message };
46393
+ const failure = completed ? {
46394
+ ...completed,
46395
+ suiteSolo: true,
46396
+ suitePin: { status: "failed", error: error.message, diagnostic: error.diagnostic },
46397
+ failedLeg: "suite-pin-pr",
46398
+ ...completed.command === "hotfix-release" ? { legs: [...legs, failedLeg] } : {},
46399
+ releaseVerdict: {
46400
+ releaseUrl: null,
46401
+ ...completed.releaseVerdict,
46402
+ releaseStatus: "succeeded",
46403
+ followUpStatus: "failed",
46404
+ promoted: true,
46405
+ tag: completed.tag,
46406
+ legs: [...legs, failedLeg]
46407
+ }
46408
+ } : { command, ...suiteSolo ? { suiteSolo: true } : {} };
46409
+ const evidence = JSON.stringify({ ...failure, verdict: "failed", error: error.message }, null, 2);
46410
+ 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 ? `
46411
+ Completed train evidence and failed follow-up:
46412
+ ${evidence}` : ""}`;
46413
+ emitTrainResult(command, output, out);
46242
46414
  }
46243
46415
  function renderTenantRedeploy(r) {
46244
46416
  return `mmi-cli devops runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
@@ -46264,8 +46436,15 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46264
46436
  const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
46265
46437
  return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
46266
46438
  }
46439
+ async function pinCompletedMember(completed) {
46440
+ try {
46441
+ return await pinSoloMember(completed.repo, completed.tag);
46442
+ } catch (e) {
46443
+ throw new TrainPinFollowUpError(completed, e);
46444
+ }
46445
+ }
46267
46446
  for (const commandName of ["rcand", "release"]) {
46268
- const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone (requires --owner-go; never valid on rcand)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
46447
+ const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone, or cut the suite lead alone (requires --owner-go; a member rcand is always refused)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
46269
46448
  const RELEASE_ONLY_FLAGS = [
46270
46449
  { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
46271
46450
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
@@ -46451,7 +46630,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46451
46630
  const raw2 = await withReleaseProgress(
46452
46631
  trainApplyDeps(),
46453
46632
  "Checking the existing release candidate",
46454
- (deps) => runRcandResume(deps, { watch: o.watch })
46633
+ (deps) => runRcandResume(deps, { watch: o.watch, suiteSolo: o.suiteSolo })
46455
46634
  );
46456
46635
  return emitRcandContinuation("rcand --resume", raw2, o);
46457
46636
  }
@@ -46467,7 +46646,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46467
46646
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46468
46647
  );
46469
46648
  const { followUpStatus, releaseVerdict } = buildResumeReleaseVerdict(result, projectInfoSync);
46470
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46649
+ const suitePin = o.suiteSolo ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict }) : void 0;
46471
46650
  if (suitePin?.status === "pending") {
46472
46651
  releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46473
46652
  releaseVerdict.followUpStatus = "pending";
@@ -46480,6 +46659,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46480
46659
  } catch (e) {
46481
46660
  applyTrainFollowUpExit("failed");
46482
46661
  consoleIo.err(`mmi-cli: Release: ${progressSubject(commandName)} blocked \u2014 see the error below`);
46662
+ writeTrainFailureReceipt(`${commandName} --resume`, o.json === true, o.out, e, o.suiteSolo === true);
46483
46663
  return failGraceful(`${commandName} --resume: ${e.message}`);
46484
46664
  }
46485
46665
  }
@@ -46533,8 +46713,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46533
46713
  () => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
46534
46714
  );
46535
46715
  }
46536
- const suitePin = o.suiteSolo && commandName === "release" ? await pinSoloMember(result.repo, result.tag) : void 0;
46537
46716
  const verdict = buildReleaseVerdict(commandName, result, projectInfoSync);
46717
+ const suitePin = o.suiteSolo && commandName === "release" ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict: verdict.releaseVerdict }) : void 0;
46538
46718
  if (suitePin?.status === "pending" && verdict.releaseVerdict) {
46539
46719
  verdict.releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
46540
46720
  verdict.releaseVerdict.followUpStatus = "pending";
@@ -46563,6 +46743,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46563
46743
  const repo = o.repo ?? await resolveRepo();
46564
46744
  const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
46565
46745
  let releaseTrack;
46746
+ let suite;
46747
+ let preOneZeroPlan = false;
46566
46748
  let releaseChannel = null;
46567
46749
  let releaseLanguage = "en";
46568
46750
  if (repo) {
@@ -46579,14 +46761,24 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46579
46761
  }
46580
46762
  const raw = read.project?.releaseTrack;
46581
46763
  if (isReleaseTrack(raw)) releaseTrack = raw;
46764
+ suite = read.project?.suite;
46765
+ preOneZeroPlan = preOneZeroInForce(read.project ?? void 0);
46582
46766
  releaseChannel = read.project?.releaseChannel?.trim() || null;
46583
46767
  releaseLanguage = read.project?.releaseLanguage?.trim() || "en";
46584
46768
  }
46585
- const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev, releaseTrack });
46769
+ let releaseTarget;
46770
+ if (commandName === "release" && !o.repo && !o.dev && releaseTrack !== "direct" && releaseTrack !== "trunk" && repo?.toLowerCase() !== "mutmutco/mmi-hub") {
46771
+ try {
46772
+ releaseTarget = { releaseTag: (await execFileP("node", ["scripts/next-version.mjs", "release", ...preOneZeroPlan ? ["--pre-one-zero"] : []], { timeout: 15e3 })).stdout.trim() };
46773
+ } catch (e) {
46774
+ releaseTarget = { releaseTagError: (e.stderr || e.message).trim() };
46775
+ }
46776
+ }
46777
+ const steps = trainPlan(commandName, { ...targets ?? {}, ...releaseTarget ?? {}, repo, dev: o.dev, releaseTrack, suite });
46586
46778
  let doctor;
46587
46779
  if (repo && (!o.repo || (await resolveRepo())?.toLowerCase() === repo.toLowerCase())) {
46588
46780
  try {
46589
- doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo });
46781
+ doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo, suiteSolo: o.suiteSolo === true });
46590
46782
  } catch (e) {
46591
46783
  consoleIo.err(`mmi-cli: train doctor did not complete: ${e.message}`);
46592
46784
  }
@@ -46674,10 +46866,10 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46674
46866
  foldPort
46675
46867
  }));
46676
46868
  } catch (e) {
46677
- if (sub === "release" && o.suiteSolo && o.out) {
46678
- const error = e.message;
46679
- emitTrainResult("hotfix release", o.json ? JSON.stringify({ command: "hotfix release", suiteSolo: true, suitePin: { status: "failed", error }, verdict: "failed" }, null, 2) : `hotfix release: suite pin PR FAILED \u2014 ${error}`, o.out);
46869
+ if (sub === "release") {
46870
+ writeTrainFailureReceipt("hotfix release", o.json || Boolean(hotfixCmd.opts().json), o.out, e, o.suiteSolo === true);
46680
46871
  }
46872
+ applyTrainFollowUpExit("failed");
46681
46873
  return failGraceful(`hotfix ${sub}: ${e.message}`);
46682
46874
  }
46683
46875
  }
@@ -46705,7 +46897,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
46705
46897
  }
46706
46898
  return runHotfixSub("release", async () => {
46707
46899
  const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
46708
- const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
46900
+ const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
46709
46901
  const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
46710
46902
  return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
46711
46903
  }, o, renderHotfixRelease);
@@ -47041,6 +47233,13 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
47041
47233
  publishMain,
47042
47234
  preflightMain,
47043
47235
  pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
47236
+ markAbandoned: async (repo, tag, sha) => {
47237
+ try {
47238
+ await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/abandoned-rc/${tag}`, "-f", `sha=${sha}`]);
47239
+ } catch (e) {
47240
+ if (!/already exists/i.test(e.message)) throw e;
47241
+ }
47242
+ },
47044
47243
  restoreMain: (input) => openSuiteRestorePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
47045
47244
  verifyLeadCut: async (sha) => {
47046
47245
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -47205,9 +47404,11 @@ function registerSuiteCommands(program3) {
47205
47404
  const unlock = await acquireTrainWriterLock(root, "suite abandon");
47206
47405
  let result;
47207
47406
  let restored;
47407
+ let marked;
47208
47408
  try {
47209
47409
  const io = suiteTrainIo(root);
47210
47410
  restored = o.ownerGo === true ? await suiteRestoreMainPins(io) : void 0;
47411
+ marked = await suiteMarkAbandoned(io, o.ownerGo === true);
47211
47412
  result = suiteAbandon(io.recordPath, o.ownerGo === true);
47212
47413
  } finally {
47213
47414
  await unlock();
@@ -47216,6 +47417,7 @@ function registerSuiteCommands(program3) {
47216
47417
  `suite abandon: archived the cut record to ${result.archive}`,
47217
47418
  ...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
47218
47419
  ...restored ? [` main pins the cut planned for unreleased tags are restored by ${restored.pr} (${restored.status}, #7758)`] : [],
47420
+ ...marked.length ? [` marked abandoned on origin, so the hotfix guard stops counting them as active (#7801): ${marked.join(", ")}`] : [],
47219
47421
  " the next suite rcand --owner-go re-snapshots every member"
47220
47422
  ].join("\n"));
47221
47423
  } catch (e) {
@@ -47734,7 +47936,6 @@ Next command: ${report.nextCommand}`);
47734
47936
  var RESTART_LINE = "\u21BB Restart Claude to finish.";
47735
47937
  var HOST_RESTART_LINES = {
47736
47938
  codex: "\u21BB Restart Codex to finish.",
47737
- cursor: "\u21BB Restart Cursor to finish.",
47738
47939
  jervcode: "\u21BB Restart JervCode to finish."
47739
47940
  };
47740
47941
  function restartLineForHost(host) {
@@ -47966,92 +48167,6 @@ var surfaces_default = {
47966
48167
  "Hosted tools and disabled or untrusted hooks bypass local enforcement."
47967
48168
  ]
47968
48169
  },
47969
- {
47970
- token: "cursor",
47971
- displayName: "Cursor",
47972
- lifecycle: "active",
47973
- artifactIds: [
47974
- "mmi-cursor-plugin",
47975
- "mmi-skills",
47976
- "mmi-cli"
47977
- ],
47978
- plannedArtifactIds: [],
47979
- assembly: {
47980
- mode: "package-directory",
47981
- rootPath: "packages/cursor-plugin",
47982
- sync: [
47983
- {
47984
- mode: "directories",
47985
- sourcePath: "skills",
47986
- targetPath: "packages/cursor-plugin/skills"
47987
- },
47988
- {
47989
- mode: "files",
47990
- sourcePath: "bin",
47991
- targetPath: "packages/cursor-plugin/bin",
47992
- include: [
47993
- "mmi-cli",
47994
- "mmi-cli.cmd"
47995
- ]
47996
- },
47997
- {
47998
- mode: "files",
47999
- sourcePath: "cli",
48000
- targetPath: "packages/cursor-plugin/cli",
48001
- include: [
48002
- "dist/index.cjs",
48003
- "dist/main.cjs",
48004
- "package.json"
48005
- ]
48006
- },
48007
- {
48008
- mode: "files",
48009
- sourcePath: ".cursor-plugin",
48010
- targetPath: "packages/cursor-plugin/.cursor-plugin",
48011
- include: [
48012
- "plugin.json"
48013
- ]
48014
- }
48015
- ]
48016
- },
48017
- install: {
48018
- mechanism: "github-source",
48019
- locator: "~/.cursor/plugins/local/mmi"
48020
- },
48021
- upgrade: {
48022
- mechanism: "reinstall",
48023
- reload: "workspace"
48024
- },
48025
- skills: {
48026
- delivery: "plugin",
48027
- sourceSurfaceId: "mmi-skills",
48028
- artifactId: "mmi-skills",
48029
- invocation: {
48030
- entry: "/epic",
48031
- any: "/<skill>"
48032
- }
48033
- },
48034
- cli: {
48035
- delivery: "standalone",
48036
- artifactId: "mmi-cli"
48037
- },
48038
- ownership: {
48039
- trust: "host",
48040
- cache: "mmi-hub",
48041
- repair: "mmi-hub"
48042
- },
48043
- certification: {
48044
- hostCommand: "cursor-agent",
48045
- versionArgs: [
48046
- "--version"
48047
- ]
48048
- },
48049
- enforcementCeilings: [
48050
- "Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
48051
- "Final assistant output is outside Cursor hook control.",
48052
- "Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
48053
- ]
48054
- },
48055
48170
  {
48056
48171
  token: "jervcode",
48057
48172
  displayName: "JervCode",
@@ -48281,38 +48396,6 @@ var surfaces_default = {
48281
48396
  },
48282
48397
  publishVisibility: "public"
48283
48398
  },
48284
- {
48285
- id: "mmi-cursor-plugin",
48286
- classification: "packaging",
48287
- kind: "plugin",
48288
- ownerPath: "packages/cursor-plugin/package.json",
48289
- deliveryPath: "packages/cursor-plugin/package.json",
48290
- delivery: "npm",
48291
- applicability: "Cursor IDE and Cursor Agent CLI; npm package @mutmutco/cursor-plugin carrying the plugin manifest and skills. The managed checkout at ~/.cursor/plugins/local/mmi is switched to each release by the fleet updater (#4945).",
48292
- versionCoordinated: true,
48293
- surfaceToken: "cursor",
48294
- versionPaths: [
48295
- {
48296
- path: ".cursor-plugin/plugin.json",
48297
- pointer: "version"
48298
- },
48299
- {
48300
- path: "packages/cursor-plugin/package.json",
48301
- pointer: "version"
48302
- }
48303
- ],
48304
- additionalPaths: [
48305
- "packages/cursor-plugin/.cursor-plugin",
48306
- "packages/cursor-plugin/skills",
48307
- "packages/cursor-plugin/bin",
48308
- "packages/cursor-plugin/cli"
48309
- ],
48310
- artifactIdentity: {
48311
- kind: "npm-pack",
48312
- packagePath: "packages/cursor-plugin"
48313
- },
48314
- publishVisibility: "public"
48315
- },
48316
48399
  {
48317
48400
  id: "mmi-claude-marketplace",
48318
48401
  classification: "delivery",
@@ -48604,7 +48687,7 @@ var surfaces_default = {
48604
48687
  };
48605
48688
 
48606
48689
  // src/surface-doctor.ts
48607
- var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "cursor", "jervcode", "hermes"]);
48690
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "hermes"]);
48608
48691
  function isActiveToken(value) {
48609
48692
  return TOKENS.has(value);
48610
48693
  }
@@ -50511,20 +50594,12 @@ async function readOrFail(read) {
50511
50594
  async function defaultRegistryRead() {
50512
50595
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
50513
50596
  }
50514
- async function defaultRunVerdictRead() {
50515
- return fetchRunVerdicts(registryClientDeps(await loadConfig()));
50516
- }
50517
- function modelRoleKeyOf(row) {
50518
- const model = row.model;
50519
- return typeof model === "string" && model.trim() ? model : void 0;
50520
- }
50521
- async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead, readRunVerdicts = defaultRunVerdictRead) {
50522
- const [gh2, aws, registryRead, projectsRead, runVerdictRead] = await Promise.all([
50597
+ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead) {
50598
+ const [gh2, aws, registryRead, projectsRead] = await Promise.all([
50523
50599
  githubEntries(client),
50524
50600
  awsEntries(),
50525
50601
  readOrFail(readRegistry),
50526
- readOrFail(readProjects),
50527
- readOrFail(readRunVerdicts)
50602
+ readOrFail(readProjects)
50528
50603
  ]);
50529
50604
  const readFailures = [];
50530
50605
  if (registryRead.state === "failed") {
@@ -50534,16 +50609,10 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50534
50609
  readFailures.push(`registry: could not read the projects list \u2014 schedulesMode opt-outs unresolved (nothing suppressed) \u2014 ${projectsRead.error}`);
50535
50610
  }
50536
50611
  const registry2 = registryRead.state === "ok" ? registryRead.value : null;
50537
- const runVerdicts = runVerdictRead.state === "ok" ? runVerdictRead.value : null;
50538
50612
  const projects = projectsRead.state === "ok" ? projectsRead.value : null;
50539
- const decoratedAws = decorateAwsModel(
50540
- aws.entries,
50541
- registry2?.map((r) => ({ id: r.id, executor: r.executor, model: modelRoleKeyOf(r) })) ?? null,
50542
- runVerdicts
50543
- );
50544
50613
  const recon = assembleReconciliation(gh2.entries, new Set(gh2.readRepos), registry2, new Set(gh2.workflowNames), {
50545
50614
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
50546
- awsEntries: decoratedAws,
50615
+ awsEntries: aws.entries,
50547
50616
  schedulerRead: Boolean(aws.schedulerRead)
50548
50617
  }, new Set(gh2.disabledWorkflowNames));
50549
50618
  const selfManaged = /* @__PURE__ */ new Set();
@@ -50557,7 +50626,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
50557
50626
  );
50558
50627
  const driftLines = reconciliation.map(renderDrift);
50559
50628
  return {
50560
- entries: sortEntries([...gh2.entries, ...decoratedAws, ...DECLARED_ENTRIES]),
50629
+ entries: sortEntries([...gh2.entries, ...aws.entries, ...DECLARED_ENTRIES]),
50561
50630
  incomplete: [...gh2.incomplete, ...aws.incomplete, ...recon.incomplete, ...readFailures],
50562
50631
  drift: driftLines,
50563
50632
  reconciliation,
@@ -53282,7 +53351,7 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
53282
53351
  }
53283
53352
  if (!result.ok) return failGraceful(`org project deploy retire: ${result.error ?? "read failed"}`);
53284
53353
  });
53285
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
53354
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--unset-task <NAME...>", "retire exactly these tenant tasks (repeatable; lowercase kebab-case names). Refuses a name that is not declared. tenantTasks merges per entry and is capped at 20 stored in total").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
53286
53355
  const cfg = await loadConfig();
53287
53356
  let target;
53288
53357
  try {
@@ -53313,6 +53382,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53313
53382
  vars,
53314
53383
  unsets: o.unset ?? [],
53315
53384
  unsetSecrets: o.unsetSecret ?? [],
53385
+ unsetTasks: o.unsetTask ?? [],
53316
53386
  clearWebProfile: Boolean(o.clearWebProfile)
53317
53387
  });
53318
53388
  } catch (e) {
@@ -53323,6 +53393,8 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
53323
53393
  if (clearRefusal) return fail(clearRefusal);
53324
53394
  const removalRefusal = secretRemovalRefusal(existing, patch);
53325
53395
  if (removalRefusal) return fail(removalRefusal);
53396
+ const taskRefusal = tenantTaskWriteRefusal(existing, patch);
53397
+ if (taskRefusal) return fail(taskRefusal);
53326
53398
  const boardError = boardLinkWriteError(patch, existing);
53327
53399
  if (boardError) return fail(`org project set: ${boardError}`);
53328
53400
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));