@mutmutco/cli 3.78.0 → 3.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/main.cjs +1370 -117
  3. package/package.json +2 -2
package/dist/main.cjs CHANGED
@@ -3580,6 +3580,8 @@ var ERROR_CODES = {
3580
3580
  ERR_CONFLICTING_FLAGS: "ERR_CONFLICTING_FLAGS",
3581
3581
  /** A flag was supplied but its resolved value is empty (e.g. an empty `--title-file` or empty stdin). */
3582
3582
  ERR_EMPTY_INPUT: "ERR_EMPTY_INPUT",
3583
+ /** A supplied value has the right source but an invalid shape (e.g. a title with line breaks). */
3584
+ ERR_INVALID_INPUT: "ERR_INVALID_INPUT",
3583
3585
  /** A flag's value is outside its allowed set (e.g. `--priority nope`). */
3584
3586
  ERR_BAD_ENUM: "ERR_BAD_ENUM",
3585
3587
  /** An unknown flag or subcommand — usually a typo; carries a `did_you_mean`. */
@@ -3612,6 +3614,11 @@ var ERROR_CODE_REFERENCE = [
3612
3614
  meaning: "A flag was supplied but resolved to an empty value (e.g. an empty file or empty stdin).",
3613
3615
  typical_fix: "Provide non-empty content for the flag in `offending_flag` (a file with text, or a real pipe/heredoc for stdin)."
3614
3616
  },
3617
+ {
3618
+ code: ERROR_CODES.ERR_INVALID_INPUT,
3619
+ meaning: "A supplied value has an invalid shape for the flag.",
3620
+ typical_fix: "Correct the value named by `offending_flag` and retry."
3621
+ },
3615
3622
  {
3616
3623
  code: ERROR_CODES.ERR_BAD_ENUM,
3617
3624
  meaning: "A flag value is outside the allowed enum.",
@@ -9430,6 +9437,81 @@ async function runReleaseResume(deps, options = {}) {
9430
9437
  note: `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
9431
9438
  };
9432
9439
  }
9440
+ async function runReleaseAbort(deps, options = {}) {
9441
+ if (!options.approved) {
9442
+ throw new Error("release --abort requires --apply after explicit approval; nothing was written");
9443
+ }
9444
+ const ctx = await buildTrainApplyContext(deps);
9445
+ if (!isHubControlRepo(ctx.repo)) {
9446
+ throw new Error("release --abort is limited to mutmutco/MMI-Hub until other release tracks define equivalent publish-absence proof");
9447
+ }
9448
+ await requireCleanTree(deps);
9449
+ if (await currentBranch(deps) !== "main") {
9450
+ throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
9451
+ }
9452
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
9453
+ const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => /^v\d+\.\d+\.\d+$/.test(tag2));
9454
+ const tag = tags[0];
9455
+ if (!tag) throw new Error("release --abort found no final v* tag; nothing was written");
9456
+ const tagSha = await probeRemoteTag(deps, tag);
9457
+ if (!tagSha) throw new Error(`release --abort: ${tag} is not on origin; nothing was written`);
9458
+ const localTagSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
9459
+ const localMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
9460
+ const originMainSha = clean2(await deps.run("git", ["rev-parse", "origin/main"]));
9461
+ if (localTagSha !== tagSha || localMainSha !== tagSha) {
9462
+ throw new Error(
9463
+ `release --abort: local tag/main do not both identify origin ${tag} (${tagSha.slice(0, 12)}); nothing was written`
9464
+ );
9465
+ }
9466
+ if (!await probeAncestor(deps, "origin/main", tagSha, "release --abort: origin/main ancestry")) {
9467
+ throw new Error(`release --abort: ${tag} does not descend from origin/main; nothing was written`);
9468
+ }
9469
+ const candidateParents = clean2(await deps.run("git", ["rev-list", "--parents", "-n", "1", tagSha])).split(/\s+/);
9470
+ if (candidateParents.length !== 2 || candidateParents[0] !== tagSha) {
9471
+ throw new Error(`release --abort: ${tag} is not a single-parent release bump; nothing was written`);
9472
+ }
9473
+ const candidateBaseSha = candidateParents[1];
9474
+ if (!await probeAncestor(deps, candidateBaseSha, "origin/development", "release --abort: development ancestry")) {
9475
+ throw new Error(`release --abort: current development does not descend from ${tag}'s candidate base; nothing was written`);
9476
+ }
9477
+ if (!await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo)) {
9478
+ throw new Error(`release --abort: ${tag} is released or its unpublished state could not be proven; nothing was written`);
9479
+ }
9480
+ const version = tag.slice(1);
9481
+ try {
9482
+ const published = clean2(await deps.run("npm", ["view", `@mutmutco/cli@${version}`, "version", "--json"]));
9483
+ if (published) {
9484
+ throw new Error(`release --abort: @mutmutco/cli@${version} is already published; ${tag} cannot be reused`);
9485
+ }
9486
+ throw new Error("npm registry returned an empty successful response");
9487
+ } catch (error) {
9488
+ const detail = `${error instanceof Error ? error.message : String(error)} ${String(error.stderr ?? "")}`;
9489
+ if (!/E404|not found|No match found/i.test(detail)) {
9490
+ if (/already published/.test(detail)) throw error;
9491
+ throw new Error(`release --abort: npm publish absence could not be proven (${detail.trim()}); nothing was written`);
9492
+ }
9493
+ }
9494
+ await runGitPush(deps, ["push", "origin", "--delete", tag]);
9495
+ if (await probeRemoteTag(deps, tag)) {
9496
+ throw new Error(`release --abort: origin still reports ${tag} after deletion; local state was preserved`);
9497
+ }
9498
+ await deps.run("git", ["tag", "-d", tag]);
9499
+ await deps.run("git", ["checkout", "development"]);
9500
+ await ffOnlyPull(deps, "development");
9501
+ await deps.run("git", ["branch", "-f", "main", "origin/main"]);
9502
+ const restoredMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
9503
+ if (restoredMainSha !== originMainSha) {
9504
+ throw new Error(`release --abort removed ${tag}, but local main did not restore to origin/main; inspect before retrying`);
9505
+ }
9506
+ return {
9507
+ command: "release-abort",
9508
+ repo: ctx.repo,
9509
+ tag,
9510
+ tagSha,
9511
+ restoredMainSha,
9512
+ note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
9513
+ };
9514
+ }
9433
9515
  async function runTrainApplyPipeline(mode, input) {
9434
9516
  const { deps, ctx, command, meta, branchHints, watch, options } = input;
9435
9517
  const directTrack = input.directTrack ?? false;
@@ -12318,6 +12400,18 @@ function formatVaultPointer(p) {
12318
12400
  return lines.join("\n");
12319
12401
  }
12320
12402
  var TIMEOUT_MS = 8e3;
12403
+ var VAULT_READ_ATTEMPTS = 2;
12404
+ async function fetchVaultRead(deps, url, init) {
12405
+ let lastError;
12406
+ for (let attempt = 0; attempt < VAULT_READ_ATTEMPTS; attempt += 1) {
12407
+ try {
12408
+ return await deps.fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
12409
+ } catch (error) {
12410
+ lastError = error;
12411
+ }
12412
+ }
12413
+ throw lastError;
12414
+ }
12321
12415
  var repoOf = (slug) => `${OWNER}/${slug}`;
12322
12416
  var RECALL_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "ap-northeast-1"];
12323
12417
  var PROVIDER_VERIFY_TIMEOUT_MS = 8e3;
@@ -12405,11 +12499,10 @@ async function fetchSecretValue(deps, key, opts) {
12405
12499
  const repo = await targetRepo(deps, opts);
12406
12500
  const slug = opts.slug?.toLowerCase();
12407
12501
  try {
12408
- const res = await deps.fetch(`${deps.apiUrl}/secrets/get`, {
12502
+ const res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
12409
12503
  method: "POST",
12410
12504
  headers: await deps.headers({ "content-type": "application/json" }),
12411
- body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} }),
12412
- signal: AbortSignal.timeout(TIMEOUT_MS)
12505
+ body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
12413
12506
  });
12414
12507
  if (!res.ok) return null;
12415
12508
  const { value } = await res.json();
@@ -12423,10 +12516,9 @@ async function secretsList(deps, opts) {
12423
12516
  const qs = new URLSearchParams({ repo }).toString();
12424
12517
  let res;
12425
12518
  try {
12426
- res = await deps.fetch(`${deps.apiUrl}/secrets/list?${qs}`, {
12519
+ res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/list?${qs}`, {
12427
12520
  method: "GET",
12428
- headers: await deps.headers(),
12429
- signal: AbortSignal.timeout(TIMEOUT_MS)
12521
+ headers: await deps.headers()
12430
12522
  });
12431
12523
  } catch (e) {
12432
12524
  deps.err(`secrets list: ${e.message}`);
@@ -13478,12 +13570,17 @@ function secretsUseExitCode(result) {
13478
13570
  return void 0;
13479
13571
  }
13480
13572
  async function fetchSecretForUse(deps, { repo, key, slug }) {
13481
- const res = await deps.fetch(`${deps.apiUrl}/secrets/get`, {
13482
- method: "POST",
13483
- headers: await deps.headers({ "content-type": "application/json" }),
13484
- body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} }),
13485
- signal: AbortSignal.timeout(TIMEOUT_MS)
13486
- });
13573
+ let res;
13574
+ try {
13575
+ res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
13576
+ method: "POST",
13577
+ headers: await deps.headers({ "content-type": "application/json" }),
13578
+ body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
13579
+ });
13580
+ } catch (error) {
13581
+ deps.err(`secrets use: ${error.message}`);
13582
+ return null;
13583
+ }
13487
13584
  if (!res.ok) {
13488
13585
  const body = await readJsonBody(res);
13489
13586
  if (res.status === 404 && body.code === "secret_not_found") {
@@ -14175,7 +14272,7 @@ function buildPluginGuardLine(state, opts = {}) {
14175
14272
  if (state === "healthy" || state === "not-org") return { exitCode: 0 };
14176
14273
  const recovery = opts.recovery ?? "mmi-cli plugin heal";
14177
14274
  const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
14178
- const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
14275
+ const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its delivery/cache is unresolved";
14179
14276
  return {
14180
14277
  line: `[mmi-guard] ${reason}; run ${recovery} and ${restartHint}.`,
14181
14278
  exitCode: 1
@@ -14195,6 +14292,9 @@ function detectSurface(env) {
14195
14292
  if (env.MMI_AGENT_SURFACE === "kimi" || has("KIMI_PLUGIN_ROOT") || has("KIMI_CODE_HOME")) {
14196
14293
  return "kimi";
14197
14294
  }
14295
+ if (env.MMI_AGENT_SURFACE === "kilo" || has("KILO") || Object.keys(env).some((k) => /^(?:KILO|KILOCODE)_/.test(k))) {
14296
+ return "kilo";
14297
+ }
14198
14298
  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")) {
14199
14299
  return "cursor";
14200
14300
  }
@@ -14214,6 +14314,8 @@ function surfaceToken(surface) {
14214
14314
  return "codex";
14215
14315
  case "kimi":
14216
14316
  return "kimi";
14317
+ case "kilo":
14318
+ return "kilo";
14217
14319
  case "cursor":
14218
14320
  return "cursor";
14219
14321
  case "opencode":
@@ -14231,10 +14333,12 @@ function reloadAction(surface) {
14231
14333
  return "restart Codex";
14232
14334
  case "kimi":
14233
14335
  return "run /reload (or start a new session) in Kimi Code";
14336
+ case "kilo":
14337
+ return "run /reload (or start a new session) in Kilo Code";
14234
14338
  case "opencode":
14235
14339
  return "restart OpenCode";
14236
14340
  case "cursor":
14237
- return "restart Cursor (or refresh the Team Marketplace from Dashboard \u2192 Settings \u2192 Plugins)";
14341
+ return "reload the Cursor window";
14238
14342
  case "claude-cli":
14239
14343
  case "shell":
14240
14344
  default:
@@ -14243,6 +14347,7 @@ function reloadAction(surface) {
14243
14347
  }
14244
14348
  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`;
14245
14349
  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";
14350
+ var CURSOR_RECOVERY = "mmi-cli plugin heal # installs ~/.cursor/plugins/local/mmi from mutmutco/MMI-Hub";
14246
14351
  var PLUGIN_SURFACE_HEAL = {
14247
14352
  claude: {
14248
14353
  delivery: "plugin-cli",
@@ -14268,6 +14373,24 @@ var PLUGIN_SURFACE_HEAL = {
14268
14373
  ],
14269
14374
  fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} and review /hooks`,
14270
14375
  updateRecipe: [CODEX_RECOVERY]
14376
+ },
14377
+ cursor: {
14378
+ delivery: "local-checkout",
14379
+ recovery: CURSOR_RECOVERY,
14380
+ healSteps: null,
14381
+ fix: (surface) => `${CURSOR_RECOVERY} # then ${reloadAction(surface)}`,
14382
+ updateRecipe: [CURSOR_RECOVERY]
14383
+ },
14384
+ kilo: {
14385
+ // kilo-p1: no marketplace step — `kilo plugin <npm>` installs + patches the config in one non-interactive
14386
+ // verb. --global writes ~/.config/kilo/opencode.json; the plugin's server() provisions the skills.
14387
+ delivery: "plugin-cli",
14388
+ recovery: "kilo plugin @mutmutco/kilo-plugin --global",
14389
+ healSteps: [
14390
+ { args: ["plugin", "@mutmutco/kilo-plugin", "--global"], gated: true }
14391
+ ],
14392
+ fix: (surface) => `kilo plugin @mutmutco/kilo-plugin --global # then ${reloadAction(surface)} to load the plugin and its provisioned skills`,
14393
+ updateRecipe: ["kilo plugin @mutmutco/kilo-plugin --global"]
14271
14394
  }
14272
14395
  };
14273
14396
  function nonClaudeSurfaceHealMessage(surface) {
@@ -14277,7 +14400,13 @@ function nonClaudeSurfaceHealMessage(surface) {
14277
14400
  if (surface === "kimi") {
14278
14401
  return "Kimi Code CLI ships an MMI plugin (kimi-k3). Install or repair it from the Kimi TUI with:\n /plugins install https://github.com/mutmutco/MMI-Hub\n Then run /reload (plugin hooks start only after a reload), and TRUST the install when prompted.\n Update the CLI too: npm i -g @mutmutco/cli";
14279
14402
  }
14280
- return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, and Kimi only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
14403
+ if (surface === "kilo") {
14404
+ return "Kilo Code ships an MMI plugin (kilo-p1). Install or repair it with:\n kilo plugin @mutmutco/kilo-plugin --global\n Then run /reload (or start a new session) \u2014 the plugin\u2019s first run provisions the skills\n into ~/.kilo, and the deny gates + redactor only load after a reload.\n Update the CLI too: npm i -g @mutmutco/cli";
14405
+ }
14406
+ if (surface === "cursor") {
14407
+ 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";
14408
+ }
14409
+ return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, Kimi, Cursor, and Kilo only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
14281
14410
  }
14282
14411
  function healStepAborts(step, ok) {
14283
14412
  return !ok && step.gated;
@@ -14319,6 +14448,8 @@ function runHostBin(bin, args, opts) {
14319
14448
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os5.homedir)()) {
14320
14449
  if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path16.join)(home, ".codex");
14321
14450
  if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path16.join)(home, ".kimi-code");
14451
+ if (surface === "kilo") return env.KILO_CONFIG_DIR?.trim() || (0, import_node_path16.join)(home, ".config", "kilo");
14452
+ if (surface === "cursor") return (0, import_node_path16.join)(home, ".cursor");
14322
14453
  return (0, import_node_path16.join)(home, ".claude");
14323
14454
  }
14324
14455
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
@@ -14340,6 +14471,8 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
14340
14471
  ];
14341
14472
  }
14342
14473
  if (surface === "kimi") return [];
14474
+ if (surface === "kilo") return [];
14475
+ if (surface === "cursor") return [];
14343
14476
  return [(0, import_node_path16.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
14344
14477
  }
14345
14478
  function marketplaceClonePresent(surface, home, exists = import_node_fs18.existsSync, env = process.env) {
@@ -14349,11 +14482,13 @@ function runHostBinSync(bin, args) {
14349
14482
  return isWin ? (0, import_node_child_process7.execFileSync)("cmd.exe", ["/c", bin, ...args], {
14350
14483
  encoding: "utf8",
14351
14484
  stdio: ["ignore", "pipe", "ignore"],
14352
- timeout: 15e3
14485
+ timeout: 15e3,
14486
+ windowsHide: true
14353
14487
  }) : (0, import_node_child_process7.execFileSync)(bin, args, {
14354
14488
  encoding: "utf8",
14355
14489
  stdio: ["ignore", "pipe", "ignore"],
14356
- timeout: 15e3
14490
+ timeout: 15e3,
14491
+ windowsHide: true
14357
14492
  });
14358
14493
  }
14359
14494
  function codexPluginStatus() {
@@ -14426,16 +14561,61 @@ async function npmSelfUpdateCli(target) {
14426
14561
  return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
14427
14562
  }
14428
14563
  }
14564
+ function kiloConfigListsPlugin(configRoot, home = (0, import_node_os5.homedir)(), read = (p) => (0, import_node_fs18.readFileSync)(p, "utf8"), exists = import_node_fs18.existsSync) {
14565
+ const candidates = ["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc", "config.json"];
14566
+ for (const dir of [configRoot, (0, import_node_path16.join)(home, ".kilo")]) {
14567
+ for (const file of candidates) {
14568
+ const path2 = (0, import_node_path16.join)(dir, file);
14569
+ if (!exists(path2)) continue;
14570
+ try {
14571
+ const stripped = read(path2).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
14572
+ const parsed = JSON.parse(stripped);
14573
+ const plugins = Array.isArray(parsed?.plugin) ? parsed.plugin : [];
14574
+ if (plugins.some((p) => {
14575
+ const spec = String(Array.isArray(p) ? p[0] : p);
14576
+ return spec.includes("@mutmutco/kilo-plugin") || spec.includes(".kilo-plugin");
14577
+ })) return true;
14578
+ } catch {
14579
+ }
14580
+ }
14581
+ }
14582
+ return false;
14583
+ }
14584
+ function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os5.homedir)()) {
14585
+ return (0, import_node_path16.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
14586
+ }
14587
+ function cursorPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
14588
+ return [
14589
+ ".cursor-plugin/plugin.json",
14590
+ "skills/mmi/SKILL.md",
14591
+ "hooks/cursor-hooks.json",
14592
+ "scripts/hook-run.mjs",
14593
+ "scripts/hook-policy.mjs"
14594
+ ].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
14595
+ }
14596
+ function kimiPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
14597
+ return [
14598
+ ".kimi-plugin/plugin.json",
14599
+ "skills/mmi/SKILL.md",
14600
+ "scripts/hook-run.mjs"
14601
+ ].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
14602
+ }
14429
14603
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
14430
14604
  const root = surfaceConfigRoot(surface);
14431
14605
  const installed = readInstalledPlugins(surface);
14432
14606
  const codexStatus = surface === "codex" ? codexPluginStatus() : void 0;
14433
14607
  return {
14434
14608
  isOrgRepo,
14435
- installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
14436
- marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os5.homedir)()),
14609
+ installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // Kimi's managed plugin directory is its native install record; it has no Claude-style ledger.
14610
+ surface === "kimi" && (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "managed", "mmi")) || // kilo-p1: the install record is the config file itself.
14611
+ surface === "kilo" && kiloConfigListsPlugin(root) || surface === "cursor" && (0, import_node_fs18.existsSync)(cursorLocalPluginRoot()),
14612
+ // Kilo has no marketplace to clone — the config file IS the install record, so this dimension of
14613
+ // the shared guard table is vacuously satisfied.
14614
+ marketplaceClonePresent: surface === "kimi" || surface === "kilo" || surface === "cursor" ? true : marketplaceClonePresent(surface, (0, import_node_os5.homedir)()),
14437
14615
  // Kimi keeps no plugin cache dir — installs are copied to plugins/managed/<id> and run from there.
14438
- pluginCachePresent: surface === "kimi" ? (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "managed", "mmi")) : surface === "codex" ? Boolean(
14616
+ // Kilo (kilo-p1) keeps no cache dir either: the plugin's server() provisions ~/.kilo behind the
14617
+ // version stamp, so the stamp's presence is the cache signal.
14618
+ pluginCachePresent: surface === "kilo" ? (0, import_node_fs18.existsSync)((0, import_node_path16.join)((0, import_node_os5.homedir)(), ".kilo", ".mmi-kilo-version")) : surface === "kimi" ? kimiPluginTreeHealthy((0, import_node_path16.join)(root, "plugins", "managed", "mmi")) : surface === "cursor" ? cursorPluginTreeHealthy(cursorLocalPluginRoot()) : surface === "codex" ? Boolean(
14439
14619
  codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
14440
14620
  ) : (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", "mutmutco", "mmi"))
14441
14621
  };
@@ -14459,6 +14639,98 @@ async function runCodexPlugin(args) {
14459
14639
  return false;
14460
14640
  }
14461
14641
  }
14642
+ async function runKiloPlugin(args) {
14643
+ try {
14644
+ await runHostBin("kilo", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
14645
+ return true;
14646
+ } catch {
14647
+ return false;
14648
+ }
14649
+ }
14650
+ function captureCodexHookLauncher() {
14651
+ const status = codexPluginStatus();
14652
+ if (!status.installed || !status.enabled || !status.version) return void 0;
14653
+ const root = (0, import_node_path16.join)(surfaceConfigRoot("codex"), "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version);
14654
+ const files = ["mmi-hook", "mmi-hook.exe"].flatMap((name) => {
14655
+ const path2 = (0, import_node_path16.join)(root, "bin", name);
14656
+ try {
14657
+ return [{ name, content: (0, import_node_fs18.readFileSync)(path2) }];
14658
+ } catch {
14659
+ return [];
14660
+ }
14661
+ });
14662
+ return files.length === 2 ? { root, files } : void 0;
14663
+ }
14664
+ function restoreCodexHookLauncher(snapshot) {
14665
+ if (!snapshot || (0, import_node_fs18.existsSync)((0, import_node_path16.join)(snapshot.root, "scripts", "hook-run.mjs"))) return false;
14666
+ const bin = (0, import_node_path16.join)(snapshot.root, "bin");
14667
+ (0, import_node_fs18.mkdirSync)(bin, { recursive: true });
14668
+ for (const file of snapshot.files) {
14669
+ const path2 = (0, import_node_path16.join)(bin, file.name);
14670
+ (0, import_node_fs18.writeFileSync)(path2, file.content);
14671
+ if (file.name === "mmi-hook") (0, import_node_fs18.chmodSync)(path2, 493);
14672
+ }
14673
+ return true;
14674
+ }
14675
+ function canonicalCursorRemote(remote) {
14676
+ return /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com[:/])mutmutco\/MMI-Hub(?:\.git)?\/?$/i.test(remote.trim().replace(/\\/g, "/"));
14677
+ }
14678
+ async function installCursorPluginCheckout(env = process.env) {
14679
+ const configRoot = surfaceConfigRoot("cursor", env);
14680
+ const pluginsRoot = (0, import_node_path16.join)(configRoot, "plugins");
14681
+ const target = (0, import_node_path16.join)(pluginsRoot, "local", "mmi");
14682
+ const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
14683
+ if ((0, import_node_fs18.existsSync)(target) && !source) {
14684
+ try {
14685
+ const { stdout } = await runHostBin("git", ["-C", target, "remote", "get-url", "origin"], { timeout: 15e3 });
14686
+ if (!canonicalCursorRemote(stdout)) {
14687
+ return { ok: false, detail: `refused to replace unrelated Cursor plugin directory at ${target}` };
14688
+ }
14689
+ } catch {
14690
+ return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target}` };
14691
+ }
14692
+ }
14693
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "local"), { recursive: true });
14694
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "staging"), { recursive: true });
14695
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "quarantine"), { recursive: true });
14696
+ const suffix = `${Date.now()}-${process.pid}`;
14697
+ const staged = (0, import_node_path16.join)(pluginsRoot, "staging", `mmi-${suffix}`);
14698
+ const quarantined = (0, import_node_path16.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
14699
+ try {
14700
+ if (source) {
14701
+ (0, import_node_fs18.cpSync)(source, staged, {
14702
+ recursive: true,
14703
+ filter: (path2) => !path2.split(/[\\/]/).some((part) => part === ".git" || part === "node_modules")
14704
+ });
14705
+ } else {
14706
+ await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
14707
+ timeout: CLAUDE_PLUGIN_TIMEOUT_MS
14708
+ });
14709
+ }
14710
+ if (!cursorPluginTreeHealthy(staged)) {
14711
+ (0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
14712
+ return { ok: false, detail: "downloaded Cursor plugin is incomplete; existing install was preserved" };
14713
+ }
14714
+ let movedOld = false;
14715
+ if ((0, import_node_fs18.existsSync)(target)) {
14716
+ (0, import_node_fs18.renameSync)(target, quarantined);
14717
+ movedOld = true;
14718
+ }
14719
+ try {
14720
+ (0, import_node_fs18.renameSync)(staged, target);
14721
+ } catch (error) {
14722
+ if (movedOld && !(0, import_node_fs18.existsSync)(target)) (0, import_node_fs18.renameSync)(quarantined, target);
14723
+ throw error;
14724
+ }
14725
+ return {
14726
+ ok: true,
14727
+ detail: movedOld ? `installed canonical Cursor plugin; previous checkout quarantined at ${quarantined}` : `installed canonical Cursor plugin at ${target}`
14728
+ };
14729
+ } catch (error) {
14730
+ if ((0, import_node_fs18.existsSync)(staged)) (0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
14731
+ return { ok: false, detail: error.message.trim().slice(0, 240).replace(/\s+/g, " ") };
14732
+ }
14733
+ }
14462
14734
  async function marketplaceAddRefSupported(bin) {
14463
14735
  try {
14464
14736
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -14486,20 +14758,35 @@ function pluginReadGrantNote(login = "<your-github-login>") {
14486
14758
  }
14487
14759
  async function applyPluginHeal(surface, log, opts) {
14488
14760
  const token = surfaceToken(surface);
14489
- if (token !== "claude" && token !== "codex") return false;
14490
- if (!opts?.force && !PLUGIN_SURFACE_HEAL[token]) return false;
14761
+ if (token !== "claude" && token !== "codex" && token !== "kilo") return false;
14491
14762
  const descriptor = PLUGIN_SURFACE_HEAL[token];
14763
+ if (!descriptor || !descriptor.healSteps) return false;
14764
+ if (token === "kilo") {
14765
+ log(" \u21BB reinstalling the MMI plugin via `kilo plugin` (install \u2192 server() provisions the skills)\u2026");
14766
+ for (const step of descriptor.healSteps) {
14767
+ const ok = await runKiloPlugin([...step.args]);
14768
+ if (healStepAborts(step, ok)) return false;
14769
+ }
14770
+ return true;
14771
+ }
14492
14772
  const tableSteps = descriptor.healSteps;
14493
14773
  if (!tableSteps) return false;
14774
+ const loadedCodexLauncher = token === "codex" ? captureCodexHookLauncher() : void 0;
14494
14775
  const bin = token;
14495
14776
  const refSupported = await marketplaceAddRefSupported(bin);
14496
14777
  const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
14497
14778
  log(healBannerLine(bin, token, refSupported));
14498
14779
  const pinsPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
14499
14780
  const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
14500
- for (const step of steps) {
14501
- const ok = token === "claude" ? await runClaudePlugin([...step.args]) : await runCodexPlugin([...step.args]);
14502
- if (healStepAborts(step, ok)) return false;
14781
+ try {
14782
+ for (const step of steps) {
14783
+ const ok = token === "claude" ? await runClaudePlugin([...step.args]) : await runCodexPlugin([...step.args]);
14784
+ if (healStepAborts(step, ok)) return false;
14785
+ }
14786
+ } finally {
14787
+ if (restoreCodexHookLauncher(loadedCodexLauncher)) {
14788
+ log(" retained the windowless Codex hook bridge for the loaded session; restart removes its need");
14789
+ }
14503
14790
  }
14504
14791
  const restored = token === "claude" ? restoreMarketplacePinsOnDisk(pinsPath, pins) : void 0;
14505
14792
  if (restored) log(` ${restored}`);
@@ -14519,16 +14806,23 @@ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
14519
14806
  }
14520
14807
  async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
14521
14808
  const token = surfaceToken(surface);
14522
- if (token !== "claude" && token !== "codex") {
14809
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14523
14810
  return { ok: false, detail: `not a supported plugin surface (${surface})` };
14524
14811
  }
14525
14812
  if (token === "claude") return healClaudePluginForDoctor(surface);
14813
+ if (token === "cursor") return installCursorPluginCheckout();
14526
14814
  const steps = [];
14527
14815
  const applied = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
14528
14816
  const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
14529
14817
  const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
14530
14818
  const ok = applied && guardState === "healthy";
14531
14819
  const verification = snapshot ? `record=${snapshot.installRecordPresent ? "yes" : "no"}, marketplace=${snapshot.marketplaceClonePresent ? "yes" : "no"}, enabled-cache=${snapshot.pluginCachePresent ? "yes" : "no"}` : "reinstall steps did not complete";
14820
+ if (token === "kilo") {
14821
+ return {
14822
+ ok,
14823
+ detail: ok ? `kilo plugin install succeeded; full guard verified (${verification})` : `\`kilo plugin\` reinstall failed full-guard verification (${verification})${steps.length ? `; ${steps[steps.length - 1]}` : ""}`
14824
+ };
14825
+ }
14532
14826
  return {
14533
14827
  ok,
14534
14828
  detail: ok ? `marketplace remove \u2192 add --ref main \u2192 plugin add succeeded; full guard verified (${verification})` : `\`codex plugin\` reinstall failed full-guard verification (${verification})${steps.length ? `; ${steps[steps.length - 1]}` : ""}`
@@ -14562,14 +14856,14 @@ async function runGuard(readOrigin) {
14562
14856
  const surface = detectSurface(process.env);
14563
14857
  try {
14564
14858
  const token = surfaceToken(surface);
14565
- if (token !== "claude" && token !== "codex") {
14859
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14566
14860
  process.exitCode = 0;
14567
14861
  return;
14568
14862
  }
14569
14863
  const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
14570
14864
  const input = snapshotPluginGuardInput(surface, isOrgRepo);
14571
14865
  const { state } = buildPluginGuardDecision(input);
14572
- const { line, exitCode } = buildPluginGuardLine(state);
14866
+ const { line, exitCode } = buildPluginGuardLine(state, { restartHint: reloadAction(surface) });
14573
14867
  if (line) console.error(line);
14574
14868
  if (token === "codex" && exitCode === 0) {
14575
14869
  const trust = codexHookTrustState();
@@ -14582,6 +14876,10 @@ async function runGuard(readOrigin) {
14582
14876
  if (surfaceToken(surface) === "codex") {
14583
14877
  console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
14584
14878
  process.exitCode = 1;
14879
+ } else if (surfaceToken(surface) === "kilo" || surfaceToken(surface) === "cursor") {
14880
+ const host = surfaceToken(surface) === "cursor" ? "Cursor" : "Kilo";
14881
+ console.error(`[mmi-guard] Could not inspect the active ${host} plugin; run \`mmi-cli plugin heal\`.`);
14882
+ process.exitCode = 1;
14585
14883
  } else {
14586
14884
  process.exitCode = 0;
14587
14885
  }
@@ -14589,21 +14887,24 @@ async function runGuard(readOrigin) {
14589
14887
  }
14590
14888
  async function runPluginHeal(surface = detectSurface(process.env)) {
14591
14889
  const token = surfaceToken(surface);
14592
- if (token !== "claude" && token !== "codex") {
14890
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14593
14891
  console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
14594
14892
  return;
14595
14893
  }
14596
14894
  const descriptor = PLUGIN_SURFACE_HEAL[token];
14597
- const applied = await applyPluginHeal(surface, console.log, { force: true });
14598
- const healed = token === "codex" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
14895
+ const cursorResult = token === "cursor" ? await installCursorPluginCheckout() : void 0;
14896
+ if (cursorResult) console.log(` \u21BB ${cursorResult.detail}`);
14897
+ const applied = cursorResult ? cursorResult.ok : await applyPluginHeal(surface, console.log, { force: true });
14898
+ const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
14599
14899
  if (healed) {
14600
14900
  const trust = token === "codex" ? " Then run /hooks and review + trust the MMI hooks." : "";
14601
- console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands.${trust}`);
14901
+ const reload = token === "kilo" ? " The plugin provisions the skills on first load." : "";
14902
+ console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.${trust}${reload}`);
14602
14903
  } else {
14603
14904
  process.exitCode = 1;
14604
- const refSupported = await marketplaceAddRefSupported(token);
14605
- const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
14606
- const note = refAbsenceNote(token, refSupported);
14905
+ const refSupported = token === "kilo" || token === "cursor" ? false : await marketplaceAddRefSupported(token);
14906
+ const recovery = token === "kilo" || token === "cursor" ? descriptor.recovery : refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
14907
+ const note = token === "kilo" || token === "cursor" ? "" : refAbsenceNote(token, refSupported);
14607
14908
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
14608
14909
  ${recovery}${note}${pluginReadGrantNote()}`);
14609
14910
  }
@@ -16126,12 +16427,20 @@ function resolveIssueBody(input, deps) {
16126
16427
  noun: "body"
16127
16428
  });
16128
16429
  }
16129
- function resolveIssueTitle(input, deps) {
16130
- return resolveTextArg({ value: input.title, file: input.titleFile }, deps, {
16430
+ async function resolveIssueTitle(input, deps) {
16431
+ const title = await resolveTextArg({ value: input.title, file: input.titleFile }, deps, {
16131
16432
  value: "--title",
16132
16433
  file: "--title-file",
16133
16434
  noun: "title"
16134
16435
  });
16436
+ const normalized = title.replace(/(?:\r\n|\n|\r)$/, "");
16437
+ if (!normalized.trim()) {
16438
+ throw new TextArgError("--title produced an empty title", ERROR_CODES.ERR_EMPTY_INPUT, "--title");
16439
+ }
16440
+ if (/[\r\n]/.test(normalized)) {
16441
+ throw new TextArgError("--title must be one line; internal line breaks are not allowed", ERROR_CODES.ERR_INVALID_INPUT, "--title");
16442
+ }
16443
+ return normalized;
16135
16444
  }
16136
16445
 
16137
16446
  // src/issue-view-json.ts
@@ -16219,6 +16528,40 @@ var PATH_OVERRIDES = {
16219
16528
  "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
16220
16529
  "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
16221
16530
  };
16531
+ var COMMAND_OWNERSHIP = {
16532
+ onboard: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16533
+ status: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16534
+ next: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16535
+ doctor: { module_owner: "cli/src/doctor-clean.ts", consumer: "agent-session" },
16536
+ whoami: { module_owner: "cli/src/whoami.ts", consumer: "agent-session" },
16537
+ commands: { module_owner: "cli/src/command-manifest.ts", consumer: "agent-session" },
16538
+ explain: { module_owner: "cli/src/explain-command.ts", consumer: "agent-session" },
16539
+ board: { module_owner: "cli/src/board-commands.ts", consumer: "agent-workflow" },
16540
+ issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
16541
+ worktree: { module_owner: "cli/src/worktree-lifecycle-commands.ts", consumer: "agent-workflow" },
16542
+ stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
16543
+ pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
16544
+ ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
16545
+ rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16546
+ release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16547
+ hotfix: { module_owner: "cli/src/hotfix-apply.ts", consumer: "release-operator" },
16548
+ train: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16549
+ bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
16550
+ secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
16551
+ docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
16552
+ tests: { module_owner: "cli/src/test-policy-core.ts", consumer: "repo-gates" },
16553
+ wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
16554
+ report: { module_owner: "cli/src/report.ts", consumer: "campaign-orchestrator" },
16555
+ "skill-lesson": { module_owner: "cli/src/skill-lesson.ts", consumer: "campaign-orchestrator" },
16556
+ org: { module_owner: "cli/src/command-consolidation.ts", consumer: "org-operator" },
16557
+ runtime: { module_owner: "cli/src/command-consolidation.ts", consumer: "runtime-operator" },
16558
+ plugin: { module_owner: "cli/src/plugin-guard-io.ts", consumer: "host-runtime" }
16559
+ };
16560
+ function commandOwnership(name) {
16561
+ const ownership = COMMAND_OWNERSHIP[name];
16562
+ if (!ownership) throw new Error(`command taxonomy: root "${name}" has no module owner or consumer`);
16563
+ return ownership;
16564
+ }
16222
16565
  var DEFAULT_ADMIN_MARKER = "(master-only)";
16223
16566
  var ADMIN_MARKERS = {
16224
16567
  "secrets org-catalog": "(writes master-only)"
@@ -16233,7 +16576,8 @@ function primaryMetadata(name) {
16233
16576
  return {
16234
16577
  category: SUPPORT_PRIMARY.has(name) ? "support" : "core",
16235
16578
  discovery: "primary",
16236
- help_group: helpGroup
16579
+ help_group: helpGroup,
16580
+ ...commandOwnership(name)
16237
16581
  };
16238
16582
  }
16239
16583
  }
@@ -16248,7 +16592,8 @@ function topLevelMetadata(name) {
16248
16592
  return {
16249
16593
  category: name === "plugin" ? "internal" : "admin",
16250
16594
  discovery: "all-only",
16251
- help_group: "Operations"
16595
+ help_group: "Operations",
16596
+ ...commandOwnership(name)
16252
16597
  };
16253
16598
  }
16254
16599
  function setCommandMetadata(command, metadata) {
@@ -16265,7 +16610,7 @@ var REQUIRED_OPTION_HELP = {
16265
16610
  };
16266
16611
  function classifyTree(command, path2, inherited, hideFromParent) {
16267
16612
  const override = PATH_OVERRIDES[path2];
16268
- const metadata = override ?? inherited;
16613
+ const metadata = override ? { ...inherited, ...override } : inherited;
16269
16614
  setCommandMetadata(command, metadata);
16270
16615
  const hideByOverride = override !== void 0 && metadata.discovery === "hidden";
16271
16616
  if ((hideByOverride || hideFromParent) && metadata.discovery !== "primary") {
@@ -16401,7 +16746,9 @@ function buildCommand(cmd, path2) {
16401
16746
  const metadata = commandMetadata(cmd) ?? {
16402
16747
  category: "core",
16403
16748
  discovery: "primary",
16404
- help_group: "Plan and work"
16749
+ help_group: "Plan and work",
16750
+ module_owner: "unclassified",
16751
+ consumer: "unclassified"
16405
16752
  };
16406
16753
  const out = {
16407
16754
  name: cmd.name(),
@@ -20960,7 +21307,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20960
21307
  name: e.name,
20961
21308
  executor: e.executor,
20962
21309
  detail: "armed with no SCHEDULE# row",
20963
- remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run the schedules lift so it registers a SCHEDULE# row'
21310
+ remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run schedules register so it creates a SCHEDULE# row'
20964
21311
  });
20965
21312
  }
20966
21313
  }
@@ -20973,7 +21320,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20973
21320
  name: r.id,
20974
21321
  executor: r.executor || live.executor,
20975
21322
  detail: `live cadence \`${live.cadence}\` disagrees with registered \`${r.cadence}\``,
20976
- remedy: "re-run the schedules lift to refresh the SCHEDULE# row from the current header/cron"
21323
+ remedy: "re-run schedules register to refresh the SCHEDULE# row from the current header/cron"
20977
21324
  });
20978
21325
  }
20979
21326
  continue;
@@ -20985,7 +21332,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20985
21332
  name: r.id,
20986
21333
  executor: r.executor || "github-actions",
20987
21334
  detail: "registered with no workflow file behind it",
20988
- remedy: `restore the workflow in ${r.repo} (or, if it was retired on purpose, re-run the schedules lift so the replace prunes the stale SCHEDULE# row)`
21335
+ remedy: `restore the workflow in ${r.repo} (or, if it was retired on purpose, re-run schedules register so the replace prunes the stale SCHEDULE# row)`
20989
21336
  });
20990
21337
  }
20991
21338
  }
@@ -21433,27 +21780,27 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
21433
21780
  const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
21434
21781
  const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
21435
21782
  if (missing.length) {
21436
- throw new Error(`schedules lift: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
21783
+ throw new Error(`schedules register: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
21437
21784
  }
21438
21785
  const h = header;
21439
21786
  if (h.schedule !== expectedId) {
21440
- throw new Error(`schedules lift: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
21787
+ throw new Error(`schedules register: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
21441
21788
  }
21442
21789
  for (const cron of crons) {
21443
21790
  if (!cadenceContainsCron(h.cadence, cron)) {
21444
- throw new Error(`schedules lift: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
21791
+ throw new Error(`schedules register: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
21445
21792
  }
21446
21793
  }
21447
21794
  if (llmFromHeader(yamlText) === "unknown") {
21448
- throw new Error(`schedules lift: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
21795
+ 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.`);
21449
21796
  }
21450
21797
  const target = header.target;
21451
21798
  if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
21452
- throw new Error(`schedules lift: ${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.`);
21799
+ 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.`);
21453
21800
  }
21454
21801
  const model = header.model;
21455
21802
  if (model !== void 0 && !/^[a-z][a-z0-9-]{0,30}$/.test(model)) {
21456
- throw new Error(`schedules lift: ${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.`);
21803
+ 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.`);
21457
21804
  }
21458
21805
  return {
21459
21806
  id: expectedId,
@@ -21478,7 +21825,7 @@ function scheduleRecordsFromWorkflows(repo, files) {
21478
21825
  const rec = scheduleRecordFromWorkflow(repo, path2, text);
21479
21826
  if (!rec) continue;
21480
21827
  if (seen.has(rec.id)) {
21481
- throw new Error(`schedules lift: duplicate schedule id \`${rec.id}\` \u2014 two workflow files in ${repo} share one join key (${rec.sourcePath}).`);
21828
+ throw new Error(`schedules register: duplicate schedule id \`${rec.id}\` \u2014 two workflow files in ${repo} share one join key (${rec.sourcePath}).`);
21482
21829
  }
21483
21830
  seen.add(rec.id);
21484
21831
  records.push(rec);
@@ -21530,16 +21877,16 @@ async function runSchedulesLift(opts, deps = {}) {
21530
21877
  const url = await (deps.originUrl ?? (() => gitOut(["remote", "get-url", "origin"])))();
21531
21878
  repo = repoNameFromRemoteUrl(url) ?? void 0;
21532
21879
  if (!repo) {
21533
- throw new Error("schedules lift: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
21880
+ throw new Error("schedules register: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
21534
21881
  }
21535
21882
  }
21536
21883
  if (!SCHEDULE_REPO_RE.test(repo)) {
21537
- throw new SchedulesLiftUsageError(`schedules lift: repo \`${repo}\` is not a bare repo segment (allowed: A-Z a-z 0-9 _ . -; no \`/\` or \`#\`) \u2014 the schedule id is <repo>/<name>, so a separator would form an invalid multi-segment id.`);
21884
+ throw new SchedulesLiftUsageError(`schedules register: repo \`${repo}\` is not a bare repo segment (allowed: A-Z a-z 0-9 _ . -; no \`/\` or \`#\`) \u2014 the schedule id is <repo>/<name>, so a separator would form an invalid multi-segment id.`);
21538
21885
  }
21539
21886
  const dir = opts.dir ?? DEFAULT_WORKFLOWS_DIR;
21540
21887
  const files = await (deps.readFiles ?? readWorkflowFiles)(dir);
21541
21888
  if (!files.length) {
21542
- throw new Error(`schedules lift: no workflow files under ${dir} \u2014 refusing to post an empty lift (the route prunes; run from the repo checkout or pass --dir).`);
21889
+ throw new Error(`schedules register: no workflow files under ${dir} \u2014 refusing to post an empty registration (the route prunes; run from the repo checkout or pass --dir).`);
21543
21890
  }
21544
21891
  const records = scheduleRecordsFromWorkflows(repo, files);
21545
21892
  const payload = { repo, schedules: records };
@@ -21548,11 +21895,11 @@ async function runSchedulesLift(opts, deps = {}) {
21548
21895
  if (!response.ok) {
21549
21896
  const unreachable = response.unreachable ?? (response.status === 404 ? "route-absent" : void 0);
21550
21897
  if (unreachable) {
21551
- throw new RegistryUnreachableError(`schedules lift: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
21898
+ throw new RegistryUnreachableError(`schedules register: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
21552
21899
  }
21553
- if (response.error) throw new Error(`schedules lift: ${response.error}`);
21900
+ if (response.error) throw new Error(`schedules register: ${response.error}`);
21554
21901
  const detail = response.body?.error ?? "";
21555
- throw new Error(`schedules lift: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
21902
+ throw new Error(`schedules register: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
21556
21903
  }
21557
21904
  return { repo, records, payload, response };
21558
21905
  }
@@ -21573,8 +21920,8 @@ function withArmingReport(body) {
21573
21920
  }
21574
21921
  function registerSchedulesLiftCommand(program3, deps = {}) {
21575
21922
  const schedules = program3.commands.find((c) => c.name() === "schedules");
21576
- if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
21577
- jsonParity(schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write")).action(async (o) => {
21923
+ if (!schedules) throw new Error("schedules register: registerSchedulesCommands must run first \u2014 register attaches to the `schedules` command");
21924
+ jsonParity(schedules.command("register").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to register (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write")).action(async (o) => {
21578
21925
  try {
21579
21926
  const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
21580
21927
  if (o.dryRun) {
@@ -23624,7 +23971,7 @@ function registerBoardCommands(program3) {
23624
23971
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
23625
23972
  "Multiple refs are handled as a batch and return per-item results."
23626
23973
  ]);
23627
- board.command("show <issue>").alias("open").description("print one board item (status, assignees, type, url) with its body and comments").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return the item even if its body/comments fetch fails").action(async (issueRef, o) => {
23974
+ board.command("show <issue>").description("print one board item (status, assignees, type, url) with its body and comments").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return the item even if its body/comments fetch fails").action(async (issueRef, o) => {
23628
23975
  try {
23629
23976
  const item = await showBoardItem({ config: await loadConfigForBoardSelector2(issueRef, o.repo), selector: issueRef, repo: o.repo, allowPartial: o.allowPartial });
23630
23977
  console.log(o.json ? JSON.stringify(item) : renderBoardItem(item));
@@ -26971,6 +27318,784 @@ function doctorReportExitCode(checks) {
26971
27318
  return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
26972
27319
  }
26973
27320
 
27321
+ // ../surfaces.json
27322
+ var surfaces_default = {
27323
+ _comment: "Hub-owned agent/developer contract. sharedAgentCore owns policy and capability sources once; agentSurfaces contains only host delivery differences; surfaces inventories and assembles the outward artifacts those adapters reference. See docs/surfaces.schema.json (v7) and docs/surface-delivery-template.md.",
27324
+ schemaVersion: 7,
27325
+ productScope: "agentic-coding",
27326
+ sharedAgentCore: {
27327
+ skills: {
27328
+ ownerPath: "skills",
27329
+ sourceSurfaceId: "mmi-skills"
27330
+ },
27331
+ cli: {
27332
+ ownerPath: "cli",
27333
+ surfaceId: "mmi-cli"
27334
+ },
27335
+ hookPolicy: {
27336
+ launcherPath: "bin/mmi-hook",
27337
+ ownerPath: "scripts/hook-policy.mjs",
27338
+ runnerPath: "scripts/hook-run.mjs",
27339
+ sourceSurfaceId: "mmi-hook-policy",
27340
+ version: 1,
27341
+ windowsLauncherPath: "bin/mmi-hook.exe"
27342
+ },
27343
+ releaseMetadata: {
27344
+ ownerPath: "surfaces.json",
27345
+ bomPath: "distribution-bom.json"
27346
+ }
27347
+ },
27348
+ agentSurfaces: [
27349
+ {
27350
+ token: "claude",
27351
+ displayName: "Claude Code",
27352
+ lifecycle: "active",
27353
+ artifactIds: ["mmi-claude-plugin", "mmi-claude-marketplace", "mmi-skills", "mmi-hook-policy", "mmi-hooks", "mmi-cli"],
27354
+ plannedArtifactIds: [],
27355
+ assembly: {
27356
+ mode: "source-tree",
27357
+ rootPath: ".",
27358
+ sync: []
27359
+ },
27360
+ install: {
27361
+ mechanism: "marketplace",
27362
+ locator: "mmi@mutmutco"
27363
+ },
27364
+ upgrade: {
27365
+ mechanism: "reinstall",
27366
+ reload: "session"
27367
+ },
27368
+ skills: {
27369
+ delivery: "plugin",
27370
+ sourceSurfaceId: "mmi-skills",
27371
+ artifactId: "mmi-skills",
27372
+ invocation: {
27373
+ entry: "/mmi:mmi",
27374
+ any: "/mmi:<skill>"
27375
+ }
27376
+ },
27377
+ hooks: {
27378
+ adapterPath: "hooks/hooks.json",
27379
+ sourceSurfaceId: "mmi-hook-policy",
27380
+ gates: [
27381
+ { id: "command-ladder", failure: "closed" },
27382
+ { id: "vault-edit", failure: "closed" },
27383
+ { id: "secret-output", failure: "open" }
27384
+ ],
27385
+ probe: "installed-contract",
27386
+ execution: "command",
27387
+ preToolUse: "enforced",
27388
+ postToolOutput: "rewrite",
27389
+ finalOutput: "unsupported"
27390
+ },
27391
+ cli: {
27392
+ delivery: "bundled",
27393
+ artifactId: "mmi-cli"
27394
+ },
27395
+ ownership: {
27396
+ trust: "host",
27397
+ cache: "host",
27398
+ repair: "mmi-cli"
27399
+ },
27400
+ certification: {
27401
+ hostCommand: "claude",
27402
+ versionArgs: ["--version"]
27403
+ },
27404
+ enforcementCeilings: [
27405
+ "Final assistant text cannot be rewritten by the hook surface.",
27406
+ "Disabled or untrusted hooks enforce nothing."
27407
+ ]
27408
+ },
27409
+ {
27410
+ token: "codex",
27411
+ displayName: "Codex",
27412
+ lifecycle: "active",
27413
+ artifactIds: ["mmi-codex-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27414
+ plannedArtifactIds: [],
27415
+ assembly: {
27416
+ mode: "source-tree",
27417
+ rootPath: ".",
27418
+ sync: []
27419
+ },
27420
+ install: {
27421
+ mechanism: "marketplace",
27422
+ locator: "mmi@mutmutco"
27423
+ },
27424
+ upgrade: {
27425
+ mechanism: "marketplace-upgrade",
27426
+ reload: "session"
27427
+ },
27428
+ skills: {
27429
+ delivery: "plugin",
27430
+ sourceSurfaceId: "mmi-skills",
27431
+ artifactId: "mmi-skills",
27432
+ invocation: {
27433
+ entry: "$mmi:mmi",
27434
+ any: "$mmi:<skill>"
27435
+ }
27436
+ },
27437
+ hooks: {
27438
+ adapterPath: "hooks/codex-hooks.json",
27439
+ sourceSurfaceId: "mmi-hook-policy",
27440
+ gates: [
27441
+ { id: "command-ladder", failure: "closed" },
27442
+ { id: "vault-edit", failure: "closed" },
27443
+ { id: "secret-output", failure: "open" }
27444
+ ],
27445
+ probe: "installed-contract",
27446
+ execution: "node-bootstrap",
27447
+ preToolUse: "enforced",
27448
+ postToolOutput: "detect-only",
27449
+ finalOutput: "unsupported"
27450
+ },
27451
+ cli: {
27452
+ delivery: "standalone",
27453
+ artifactId: "mmi-cli"
27454
+ },
27455
+ ownership: {
27456
+ trust: "operator",
27457
+ cache: "host",
27458
+ repair: "mmi-cli"
27459
+ },
27460
+ certification: {
27461
+ hostCommand: "codex",
27462
+ versionArgs: ["--version"]
27463
+ },
27464
+ enforcementCeilings: [
27465
+ "Post-tool hooks can detect secret output but cannot rewrite it.",
27466
+ "Hosted tools and disabled or untrusted hooks bypass local enforcement."
27467
+ ]
27468
+ },
27469
+ {
27470
+ token: "kimi",
27471
+ displayName: "Kimi Code CLI",
27472
+ lifecycle: "active",
27473
+ artifactIds: ["mmi-kimi-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27474
+ plannedArtifactIds: [],
27475
+ assembly: {
27476
+ mode: "source-tree",
27477
+ rootPath: ".",
27478
+ sync: []
27479
+ },
27480
+ install: {
27481
+ mechanism: "github-source",
27482
+ locator: "https://github.com/mutmutco/MMI-Hub"
27483
+ },
27484
+ upgrade: {
27485
+ mechanism: "reinstall",
27486
+ reload: "session"
27487
+ },
27488
+ skills: {
27489
+ delivery: "plugin",
27490
+ sourceSurfaceId: "mmi-skills",
27491
+ artifactId: "mmi-skills",
27492
+ invocation: {
27493
+ entry: "/skill:mmi",
27494
+ any: "/skill:<skill>"
27495
+ }
27496
+ },
27497
+ hooks: {
27498
+ adapterPath: ".kimi-plugin/plugin.json",
27499
+ sourceSurfaceId: "mmi-hook-policy",
27500
+ gates: [
27501
+ { id: "command-ladder", failure: "closed" },
27502
+ { id: "vault-edit", failure: "closed" },
27503
+ { id: "secret-output", failure: "open" }
27504
+ ],
27505
+ probe: "installed-contract",
27506
+ execution: "inline-manifest",
27507
+ preToolUse: "enforced",
27508
+ postToolOutput: "detect-only",
27509
+ finalOutput: "unsupported"
27510
+ },
27511
+ cli: {
27512
+ delivery: "standalone",
27513
+ artifactId: "mmi-cli"
27514
+ },
27515
+ ownership: {
27516
+ trust: "host",
27517
+ cache: "host",
27518
+ repair: "operator"
27519
+ },
27520
+ certification: {
27521
+ hostCommand: "kimi",
27522
+ versionArgs: ["--version"]
27523
+ },
27524
+ enforcementCeilings: [
27525
+ "Post-tool hooks are observation-only and cannot rewrite secret output.",
27526
+ "Hook errors, timeouts, disabled plugins, and untrusted plugins enforce nothing."
27527
+ ]
27528
+ },
27529
+ {
27530
+ token: "cursor",
27531
+ displayName: "Cursor",
27532
+ lifecycle: "active",
27533
+ artifactIds: ["mmi-cursor-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27534
+ plannedArtifactIds: [],
27535
+ assembly: {
27536
+ mode: "source-tree",
27537
+ rootPath: ".",
27538
+ sync: []
27539
+ },
27540
+ install: {
27541
+ mechanism: "github-source",
27542
+ locator: "~/.cursor/plugins/local/mmi"
27543
+ },
27544
+ upgrade: {
27545
+ mechanism: "reinstall",
27546
+ reload: "workspace"
27547
+ },
27548
+ skills: {
27549
+ delivery: "plugin",
27550
+ sourceSurfaceId: "mmi-skills",
27551
+ artifactId: "mmi-skills",
27552
+ invocation: {
27553
+ entry: "/mmi",
27554
+ any: "/<skill>"
27555
+ }
27556
+ },
27557
+ hooks: {
27558
+ adapterPath: "hooks/cursor-hooks.json",
27559
+ sourceSurfaceId: "mmi-hook-policy",
27560
+ gates: [
27561
+ { id: "command-ladder", failure: "closed" },
27562
+ { id: "vault-edit", failure: "closed" },
27563
+ { id: "secret-output", failure: "open" }
27564
+ ],
27565
+ probe: "installed-contract",
27566
+ execution: "command",
27567
+ preToolUse: "enforced",
27568
+ postToolOutput: "detect-only",
27569
+ finalOutput: "unsupported"
27570
+ },
27571
+ cli: {
27572
+ delivery: "standalone",
27573
+ artifactId: "mmi-cli"
27574
+ },
27575
+ ownership: {
27576
+ trust: "host",
27577
+ cache: "mmi-cli",
27578
+ repair: "mmi-cli"
27579
+ },
27580
+ certification: {
27581
+ hostCommand: "cursor-agent",
27582
+ versionArgs: ["--version"]
27583
+ },
27584
+ enforcementCeilings: [
27585
+ "Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
27586
+ "Final assistant output is outside Cursor hook control.",
27587
+ "Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
27588
+ ]
27589
+ },
27590
+ {
27591
+ token: "kilo",
27592
+ displayName: "Kilo Code",
27593
+ lifecycle: "active",
27594
+ artifactIds: ["mmi-kilo-plugin", "mmi-kilo-skills", "mmi-hook-policy", "mmi-cli"],
27595
+ plannedArtifactIds: [],
27596
+ assembly: {
27597
+ mode: "package-directory",
27598
+ rootPath: ".kilo-plugin",
27599
+ sync: [
27600
+ {
27601
+ mode: "directories",
27602
+ sourcePath: "skills",
27603
+ targetPath: ".kilo-plugin/skills",
27604
+ excludeNamePrefixes: ["_"]
27605
+ },
27606
+ {
27607
+ mode: "files",
27608
+ sourcePath: "scripts",
27609
+ targetPath: ".kilo-plugin/scripts",
27610
+ include: [
27611
+ "pretooluse-shell-gates.mjs",
27612
+ "vault-edit-gate.mjs",
27613
+ "secret-redact.mjs",
27614
+ "deny-gate-crash.mjs",
27615
+ "secret-echo-lint.mjs",
27616
+ "env-write-lint.mjs",
27617
+ "command-ladder-gate.mjs",
27618
+ "command-ladder-core.mjs",
27619
+ "validate-hook.mjs",
27620
+ "hook-io.mjs",
27621
+ "hook-trace.mjs",
27622
+ "edit-tool-paths.mjs",
27623
+ "throttle-core.mjs",
27624
+ "hook-policy.mjs",
27625
+ "hook-run.mjs"
27626
+ ]
27627
+ }
27628
+ ]
27629
+ },
27630
+ install: {
27631
+ mechanism: "npm",
27632
+ locator: "@mutmutco/kilo-plugin"
27633
+ },
27634
+ upgrade: {
27635
+ mechanism: "package-manager",
27636
+ reload: "session"
27637
+ },
27638
+ skills: {
27639
+ delivery: "provisioned",
27640
+ sourceSurfaceId: "mmi-skills",
27641
+ artifactId: "mmi-kilo-skills",
27642
+ invocation: {
27643
+ entry: "mmi skill via the skill tool",
27644
+ any: "skill tool"
27645
+ }
27646
+ },
27647
+ hooks: {
27648
+ adapterPath: ".kilo-plugin/server.mjs",
27649
+ sourceSurfaceId: "mmi-hook-policy",
27650
+ gates: [
27651
+ { id: "command-ladder", failure: "closed" },
27652
+ { id: "vault-edit", failure: "closed" },
27653
+ { id: "secret-output", failure: "open" }
27654
+ ],
27655
+ probe: "installed-contract",
27656
+ execution: "in-process",
27657
+ preToolUse: "enforced",
27658
+ postToolOutput: "rewrite",
27659
+ finalOutput: "rewrite"
27660
+ },
27661
+ cli: {
27662
+ delivery: "standalone",
27663
+ artifactId: "mmi-cli"
27664
+ },
27665
+ ownership: {
27666
+ trust: "host",
27667
+ cache: "host",
27668
+ repair: "mmi-cli"
27669
+ },
27670
+ certification: {
27671
+ hostCommand: "kilo",
27672
+ versionArgs: ["--version"]
27673
+ },
27674
+ enforcementCeilings: [
27675
+ "Some read, write, and web-fetch tool outputs are not rewriteable by the host.",
27676
+ "Provisioned skills and agents become discoverable only after a fresh session."
27677
+ ]
27678
+ }
27679
+ ],
27680
+ surfaces: [
27681
+ {
27682
+ id: "mmi-skills",
27683
+ classification: "capability",
27684
+ kind: "skills",
27685
+ ownerPath: "skills",
27686
+ deliveryPath: "skills",
27687
+ delivery: "plugin-install",
27688
+ applicability: "plugin-enabled agent surfaces",
27689
+ versionCoordinated: false,
27690
+ artifactIdentity: {
27691
+ kind: "sha256-tree",
27692
+ paths: ["skills"]
27693
+ },
27694
+ verify: [
27695
+ {
27696
+ command: "node",
27697
+ args: ["scripts/check-skill-payload.mjs", "--contract"],
27698
+ expected: "skill payload check: ok"
27699
+ }
27700
+ ],
27701
+ publishVisibility: "public"
27702
+ },
27703
+ {
27704
+ id: "mmi-claude-plugin",
27705
+ classification: "packaging",
27706
+ kind: "plugin",
27707
+ ownerPath: ".claude-plugin/plugin.json",
27708
+ deliveryPath: ".claude-plugin/plugin.json",
27709
+ delivery: "release",
27710
+ applicability: "Claude Code plugin marketplace",
27711
+ versionCoordinated: true,
27712
+ surfaceToken: "claude",
27713
+ versionPaths: [
27714
+ { path: ".claude-plugin/plugin.json", pointer: "version" }
27715
+ ],
27716
+ artifactIdentity: {
27717
+ kind: "sha256-tree",
27718
+ paths: [".claude-plugin/plugin.json", "skills", "hooks", "scripts", "bin"]
27719
+ },
27720
+ publishVisibility: "public"
27721
+ },
27722
+ {
27723
+ id: "mmi-codex-plugin",
27724
+ classification: "packaging",
27725
+ kind: "plugin",
27726
+ ownerPath: ".codex-plugin/plugin.json",
27727
+ deliveryPath: ".codex-plugin/plugin.json",
27728
+ delivery: "release",
27729
+ applicability: "Codex plugin marketplace; carries the MMI lifecycle hooks via hooks/codex-hooks.json (#3563)",
27730
+ versionCoordinated: true,
27731
+ surfaceToken: "codex",
27732
+ versionPaths: [
27733
+ { path: ".codex-plugin/plugin.json", pointer: "version" }
27734
+ ],
27735
+ artifactIdentity: {
27736
+ kind: "sha256-tree",
27737
+ paths: [".codex-plugin/plugin.json", "skills", "hooks/codex-hooks.json", "scripts", "bin"]
27738
+ },
27739
+ publishVisibility: "public"
27740
+ },
27741
+ {
27742
+ id: "mmi-kimi-plugin",
27743
+ classification: "packaging",
27744
+ kind: "plugin",
27745
+ ownerPath: ".kimi-plugin/plugin.json",
27746
+ deliveryPath: ".kimi-plugin/plugin.json",
27747
+ delivery: "release",
27748
+ applicability: "Kimi Code CLI plugin install (GitHub source); inline event mapping invokes the shared hook runner",
27749
+ versionCoordinated: true,
27750
+ surfaceToken: "kimi",
27751
+ versionPaths: [
27752
+ { path: ".kimi-plugin/plugin.json", pointer: "version" }
27753
+ ],
27754
+ artifactIdentity: {
27755
+ kind: "sha256-tree",
27756
+ paths: [".kimi-plugin/plugin.json", "skills", "scripts", "bin"]
27757
+ },
27758
+ publishVisibility: "public"
27759
+ },
27760
+ {
27761
+ id: "mmi-cursor-plugin",
27762
+ classification: "packaging",
27763
+ kind: "plugin",
27764
+ ownerPath: ".cursor-plugin/plugin.json",
27765
+ deliveryPath: "~/.cursor/plugins/local/mmi",
27766
+ delivery: "local-copy",
27767
+ applicability: "Cursor IDE and Cursor Agent CLI; local plugin checkout managed by mmi-cli",
27768
+ versionCoordinated: true,
27769
+ surfaceToken: "cursor",
27770
+ versionPaths: [
27771
+ { path: ".cursor-plugin/plugin.json", pointer: "version" }
27772
+ ],
27773
+ artifactIdentity: {
27774
+ kind: "sha256-tree",
27775
+ paths: [".cursor-plugin/plugin.json", "skills", "hooks/cursor-hooks.json", "scripts", "bin"]
27776
+ },
27777
+ publishVisibility: "public"
27778
+ },
27779
+ {
27780
+ id: "mmi-kilo-plugin",
27781
+ classification: "packaging",
27782
+ kind: "plugin",
27783
+ ownerPath: ".kilo-plugin/package.json",
27784
+ deliveryPath: ".kilo-plugin/package.json",
27785
+ delivery: "npm",
27786
+ applicability: "Kilo Code extension + Kilo CLI; npm package @mutmutco/kilo-plugin (kilo-p1), installed via `kilo plugin @mutmutco/kilo-plugin`",
27787
+ versionCoordinated: true,
27788
+ surfaceToken: "kilo",
27789
+ versionPaths: [
27790
+ { path: ".kilo-plugin/package.json", pointer: "version" }
27791
+ ],
27792
+ additionalPaths: [
27793
+ ".kilo-plugin/server.mjs",
27794
+ ".kilo-plugin/skills",
27795
+ ".kilo-plugin/scripts"
27796
+ ],
27797
+ artifactIdentity: {
27798
+ kind: "npm-pack",
27799
+ packagePath: ".kilo-plugin"
27800
+ },
27801
+ publishVisibility: "public"
27802
+ },
27803
+ {
27804
+ id: "mmi-kilo-skills",
27805
+ classification: "capability",
27806
+ kind: "skills",
27807
+ ownerPath: "skills",
27808
+ deliveryPath: "skills",
27809
+ delivery: "plugin-install",
27810
+ applicability: "Kilo Code (via @mutmutco/kilo-plugin provisioning to ~/.kilo/skills)",
27811
+ versionCoordinated: false,
27812
+ artifactIdentity: {
27813
+ kind: "sha256-tree",
27814
+ paths: [".kilo-plugin/skills"]
27815
+ },
27816
+ surfaceToken: "kilo",
27817
+ publishVisibility: "public"
27818
+ },
27819
+ {
27820
+ id: "mmi-claude-marketplace",
27821
+ classification: "delivery",
27822
+ kind: "marketplace",
27823
+ ownerPath: ".claude-plugin/marketplace.json",
27824
+ deliveryPath: ".claude-plugin/marketplace.json",
27825
+ delivery: "release",
27826
+ applicability: "Claude Code plugin marketplace",
27827
+ versionCoordinated: true,
27828
+ surfaceToken: "claude",
27829
+ versionPaths: [
27830
+ { path: ".claude-plugin/marketplace.json", pointer: "plugins.0.version" },
27831
+ { path: ".claude-plugin/marketplace.json", pointer: "plugins.0.displayName", template: "MMI {version}" }
27832
+ ],
27833
+ artifactIdentity: {
27834
+ kind: "sha256-tree",
27835
+ paths: [".claude-plugin/marketplace.json"]
27836
+ },
27837
+ publishVisibility: "public"
27838
+ },
27839
+ {
27840
+ id: "mmi-cli",
27841
+ classification: "capability",
27842
+ kind: "cli",
27843
+ ownerPath: "cli/package.json",
27844
+ deliveryPath: "cli/package.json",
27845
+ delivery: "npm",
27846
+ applicability: "editor-agnostic agent surfaces",
27847
+ versionCoordinated: true,
27848
+ versionPaths: [
27849
+ { path: "cli/package.json", pointer: "version" }
27850
+ ],
27851
+ additionalPaths: ["cli/dist"],
27852
+ artifactIdentity: {
27853
+ kind: "npm-pack",
27854
+ packagePath: "cli"
27855
+ },
27856
+ publishVisibility: "public"
27857
+ },
27858
+ {
27859
+ id: "mmi-cli-lock",
27860
+ classification: "packaging",
27861
+ kind: "package-lock",
27862
+ ownerPath: "cli/package-lock.json",
27863
+ deliveryPath: "cli/package-lock.json",
27864
+ delivery: "npm",
27865
+ applicability: "editor-agnostic agent surfaces",
27866
+ versionCoordinated: true,
27867
+ versionPaths: [
27868
+ { path: "cli/package-lock.json", pointer: "version" },
27869
+ { path: "cli/package-lock.json", pointer: "packages.$root.version" }
27870
+ ],
27871
+ artifactIdentity: {
27872
+ kind: "sha256-tree",
27873
+ paths: ["cli/package-lock.json"]
27874
+ },
27875
+ publishVisibility: "public"
27876
+ },
27877
+ {
27878
+ id: "mmi-cli-dist",
27879
+ classification: "packaging",
27880
+ kind: "bundle",
27881
+ ownerPath: "cli/dist/index.cjs",
27882
+ deliveryPath: "cli/dist/index.cjs",
27883
+ delivery: "npm",
27884
+ applicability: "editor-agnostic agent surfaces",
27885
+ versionCoordinated: true,
27886
+ versionPaths: [
27887
+ { path: "cli/package.json", pointer: "version" }
27888
+ ],
27889
+ additionalPaths: ["cli/dist/main.cjs"],
27890
+ prepare: [
27891
+ {
27892
+ command: "npm",
27893
+ args: ["--prefix", "cli", "run", "build"],
27894
+ inputs: ["cli/src", "cli/build.mjs", "cli/package.json", "cli/README.md", "cli/tsconfig.json"],
27895
+ outputs: ["cli/dist/index.cjs", "cli/dist/main.cjs"]
27896
+ }
27897
+ ],
27898
+ verify: [
27899
+ {
27900
+ command: "node",
27901
+ args: ["cli/dist/index.cjs", "--version"],
27902
+ expected: "{version}"
27903
+ }
27904
+ ],
27905
+ artifactIdentity: {
27906
+ kind: "sha256-tree",
27907
+ paths: ["cli/dist"]
27908
+ },
27909
+ publishVisibility: "public"
27910
+ },
27911
+ {
27912
+ id: "mmi-hook-policy",
27913
+ classification: "guard",
27914
+ kind: "hooks",
27915
+ ownerPath: "scripts/hook-policy.mjs",
27916
+ deliveryPath: "scripts/hook-policy.mjs",
27917
+ delivery: "plugin-install",
27918
+ applicability: "all five active hook-capable agent surfaces",
27919
+ versionCoordinated: false,
27920
+ artifactIdentity: {
27921
+ kind: "sha256-tree",
27922
+ paths: [
27923
+ "scripts/hook-policy.mjs",
27924
+ "scripts/hook-run.mjs",
27925
+ "scripts/pretooluse-shell-gates.mjs",
27926
+ "scripts/vault-edit-gate.mjs",
27927
+ "scripts/secret-redact.mjs",
27928
+ "scripts/deny-gate-crash.mjs",
27929
+ "scripts/hook-io.mjs",
27930
+ "scripts/hook-trace.mjs",
27931
+ "bin/mmi-hook",
27932
+ "bin/mmi-hook.exe",
27933
+ "native/windows-hook-launcher.c",
27934
+ "scripts/build-windows-hook-launcher.ps1",
27935
+ "scripts/probe-windows-hook-window.ps1"
27936
+ ]
27937
+ },
27938
+ verify: [
27939
+ {
27940
+ command: "node",
27941
+ args: ["scripts/check-hook-contract.mjs"],
27942
+ expected: "hook contract: 5 active adapters proven, 12 repositories covered"
27943
+ }
27944
+ ],
27945
+ publishVisibility: "public"
27946
+ },
27947
+ {
27948
+ id: "mmi-hooks",
27949
+ classification: "guard",
27950
+ kind: "hooks",
27951
+ ownerPath: "hooks/hooks.json",
27952
+ deliveryPath: "hooks/hooks.json",
27953
+ delivery: "plugin",
27954
+ applicability: "Claude hook-enabled agents",
27955
+ versionCoordinated: false,
27956
+ artifactIdentity: {
27957
+ kind: "sha256-tree",
27958
+ paths: ["hooks", "scripts"]
27959
+ },
27960
+ surfaceToken: "claude",
27961
+ publishVisibility: "public"
27962
+ },
27963
+ {
27964
+ id: "mmi-github-app",
27965
+ classification: "integration",
27966
+ kind: "github-app",
27967
+ ownerPath: "infra/src/github-app.ts",
27968
+ deliveryPath: "infra/dist/handler.js",
27969
+ delivery: "lambda",
27970
+ applicability: "org automation and authorization",
27971
+ versionCoordinated: true,
27972
+ versionPaths: [
27973
+ { path: "infra/package.json", pointer: "version" }
27974
+ ],
27975
+ publishVisibility: "n/a",
27976
+ deployedVersionSource: "http-header:x-hub-version"
27977
+ },
27978
+ {
27979
+ id: "mmi-slack-notify",
27980
+ classification: "integration",
27981
+ kind: "notification",
27982
+ ownerPath: "scripts/slack-post.mjs",
27983
+ deliveryPath: "scripts/slack-post.mjs",
27984
+ delivery: "github-actions",
27985
+ applicability: "master DM notifications only; chatops out by default",
27986
+ versionCoordinated: false,
27987
+ publishVisibility: "n/a"
27988
+ }
27989
+ ]
27990
+ };
27991
+
27992
+ // src/surface-doctor.ts
27993
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "kimi", "cursor", "kilo"]);
27994
+ function isActiveToken(value) {
27995
+ return TOKENS.has(value);
27996
+ }
27997
+ var DOCTOR_SURFACES = Object.freeze(
27998
+ (surfaces_default.agentSurfaces ?? []).filter((surface) => surface.lifecycle === "active" && isActiveToken(surface.token)).map((surface) => Object.freeze({
27999
+ token: surface.token,
28000
+ displayName: surface.displayName,
28001
+ artifactIds: Object.freeze([...surface.artifactIds]),
28002
+ installMechanism: surface.install.mechanism,
28003
+ installLocator: surface.install.locator,
28004
+ upgradeMechanism: surface.upgrade.mechanism,
28005
+ reload: surface.upgrade.reload,
28006
+ repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator"
28007
+ }))
28008
+ );
28009
+ function doctorSurface(token) {
28010
+ const descriptor = DOCTOR_SURFACES.find((surface) => surface.token === token);
28011
+ if (!descriptor) throw new Error(`surface registry has no active doctor descriptor for ${token}`);
28012
+ return descriptor;
28013
+ }
28014
+ function diagnoseSurface(evidence) {
28015
+ const base = {
28016
+ descriptor: evidence.descriptor,
28017
+ ...evidence.installedVersion ? { installedVersion: evidence.installedVersion } : {},
28018
+ ...evidence.releasedVersion ? { releasedVersion: evidence.releasedVersion } : {}
28019
+ };
28020
+ if (!evidence.applicable) return { ...base, state: "skipped" };
28021
+ if (evidence.repair?.attempted && !evidence.repair.ok) {
28022
+ return { ...base, state: "repair-failed", repairDetail: evidence.repair.detail };
28023
+ }
28024
+ if (evidence.repair?.attempted && evidence.repair.ok) {
28025
+ return { ...base, state: "clean", repairDetail: evidence.repair.detail };
28026
+ }
28027
+ if (!evidence.installRecordPresent) return { ...base, state: "missing" };
28028
+ if (!evidence.deliveryPresent || !evidence.payloadPresent || evidence.manifest === "missing") {
28029
+ return { ...base, state: "partial" };
28030
+ }
28031
+ if (evidence.manifest === "invalid") return { ...base, state: "corrupt" };
28032
+ if (!evidence.releasedVersion) return { ...base, state: "freshness-unknown" };
28033
+ if (evidence.installedVersion && evidence.releasedVersion && compareVersions(evidence.installedVersion, evidence.releasedVersion) < 0) {
28034
+ return { ...base, state: "stale" };
28035
+ }
28036
+ return { ...base, state: "clean" };
28037
+ }
28038
+ function reloadInstruction(descriptor) {
28039
+ if (descriptor.token === "claude") return "restart Claude";
28040
+ if (descriptor.token === "codex") return "restart Codex";
28041
+ if (descriptor.token === "kimi") return "restart Kimi Code CLI";
28042
+ if (descriptor.token === "kilo") return "restart Kilo Code";
28043
+ return descriptor.reload === "workspace" ? `reload ${descriptor.displayName}` : `restart ${descriptor.displayName}`;
28044
+ }
28045
+ function planSurfaceRepair(diagnosis) {
28046
+ if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
28047
+ const descriptor = diagnosis.descriptor;
28048
+ if (descriptor.repairOwner !== "mmi-cli") {
28049
+ return {
28050
+ supported: false,
28051
+ owner: "operator",
28052
+ changes: [],
28053
+ instruction: `repair is unsupported by mmi-cli; reinstall via ${descriptor.installMechanism} (${descriptor.installLocator}), then ${reloadInstruction(descriptor)}`
28054
+ };
28055
+ }
28056
+ return {
28057
+ supported: true,
28058
+ owner: "mmi-cli",
28059
+ command: "mmi-cli plugin heal",
28060
+ changes: ["replace the active MMI plugin installation", `reload via ${descriptor.reload}`],
28061
+ instruction: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`), then ${reloadInstruction(descriptor)}`
28062
+ };
28063
+ }
28064
+ function buildSurfaceDoctorCheck(diagnosis) {
28065
+ const { descriptor, state } = diagnosis;
28066
+ const plan = planSurfaceRepair(diagnosis);
28067
+ const versions = diagnosis.installedVersion && diagnosis.releasedVersion ? compareVersions(diagnosis.installedVersion, diagnosis.releasedVersion) === 0 ? diagnosis.installedVersion : `${diagnosis.installedVersion} \u2192 ${diagnosis.releasedVersion}` : diagnosis.installedVersion;
28068
+ const detailByState = {
28069
+ skipped: "skipped \u2014 host not active",
28070
+ clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
28071
+ "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
28072
+ stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
28073
+ missing: "missing \u2014 no install record",
28074
+ corrupt: "corrupt \u2014 installed manifest is unreadable",
28075
+ partial: "partial \u2014 one or more delivery artifacts are absent",
28076
+ "repair-failed": `repair failed${diagnosis.repairDetail ? ` \u2014 ${diagnosis.repairDetail}` : ""}`
28077
+ };
28078
+ return {
28079
+ id: `${descriptor.token}-plugin`,
28080
+ surface: descriptor.token,
28081
+ state,
28082
+ ok: state === "clean" || state === "skipped",
28083
+ ...state === "freshness-unknown" ? { reportOnly: true } : {},
28084
+ label: `${descriptor.displayName} plugin`,
28085
+ detail: detailByState[state],
28086
+ ...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
28087
+ verbose: [
28088
+ `registry surface: ${descriptor.token}`,
28089
+ `install: ${descriptor.installMechanism} (${descriptor.installLocator})`,
28090
+ `repair owner: ${descriptor.repairOwner}`,
28091
+ `artifacts: ${descriptor.artifactIds.join(", ")}`
28092
+ ]
28093
+ };
28094
+ }
28095
+ function surfaceRestartAction(descriptor) {
28096
+ return reloadInstruction(descriptor);
28097
+ }
28098
+
26974
28099
  // src/doctor-clean.ts
26975
28100
  function checkGithubAuth(probe) {
26976
28101
  const login = probe.login?.trim();
@@ -27069,9 +28194,11 @@ function pluginHealTrigger(probe) {
27069
28194
  function checkClaudePlugin(probe) {
27070
28195
  const { installed, released, guardState } = probe;
27071
28196
  const codex = probe.surface === "codex";
27072
- const id = codex ? "codex-plugin" : "claude-plugin";
27073
- const label = codex ? "Codex plugin" : "Claude plugin";
27074
- const restart = codex ? "restart Codex" : "restart Claude";
28197
+ const kilo = probe.surface === "kilo";
28198
+ const cursor = probe.surface === "cursor";
28199
+ const id = codex ? "codex-plugin" : kilo ? "kilo-plugin" : cursor ? "cursor-plugin" : "claude-plugin";
28200
+ const label = codex ? "Codex plugin" : kilo ? "Kilo plugin" : cursor ? "Cursor plugin" : "Claude plugin";
28201
+ const restart = codex ? "restart Codex" : kilo ? "reload Kilo" : cursor ? "reload Cursor" : "restart Claude";
27075
28202
  const evidence = [
27076
28203
  `installed: ${installed ?? "(none)"}`,
27077
28204
  // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
@@ -27084,8 +28211,8 @@ function checkClaudePlugin(probe) {
27084
28211
  id,
27085
28212
  ok: false,
27086
28213
  label,
27087
- detail: guardState === "no-install" ? "not installed" : "unresolved (marketplace/cache missing)",
27088
- fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI marketplace + plugin, then ${restart}`,
28214
+ detail: guardState === "no-install" ? "not installed" : "unresolved (delivery/cache missing)",
28215
+ fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI plugin, then ${restart}`,
27089
28216
  verbose: evidence
27090
28217
  };
27091
28218
  }
@@ -27368,9 +28495,12 @@ async function runDoctorClean(opts, io, deps) {
27368
28495
  opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
27369
28496
  ]);
27370
28497
  const ghInstalled2 = login ? true : await deps.ghInstalled();
27371
- const installed = deps.installedPluginVersion();
28498
+ const registryEvidence = deps.surfaceEvidence?.(isOrgRepo);
28499
+ const installed = registryEvidence?.installedVersion ?? deps.installedPluginVersion();
27372
28500
  const pluginSurface = deps.pluginSurface?.() ?? "claude-cli";
27373
- const codexSurface = pluginSurface === "codex";
28501
+ const codexSurface = registryEvidence?.descriptor.token === "codex" || pluginSurface === "codex";
28502
+ const cursorSurface = pluginSurface === "cursor";
28503
+ const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : codexSurface ? "restart Codex" : cursorSurface ? "reload Cursor" : pluginSurface === "kilo" ? "restart Kilo Code" : "restart Claude";
27374
28504
  const checks = [];
27375
28505
  let restartPending = false;
27376
28506
  checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
@@ -27400,47 +28530,61 @@ async function runDoctorClean(opts, io, deps) {
27400
28530
  const pluginProbe = {
27401
28531
  installed,
27402
28532
  released,
27403
- guardState: deps.pluginGuardState(isOrgRepo),
28533
+ guardState: registryEvidence?.guardState ?? deps.pluginGuardState(isOrgRepo),
27404
28534
  releasedNote,
27405
28535
  surface: pluginSurface
27406
28536
  };
27407
- const healTrigger = pluginHealTrigger(pluginProbe);
28537
+ const registryDiagnosis = registryEvidence ? diagnoseSurface({ ...registryEvidence, releasedVersion: released }) : void 0;
28538
+ const registryRepairPlan = registryDiagnosis ? planSurfaceRepair(registryDiagnosis) : null;
28539
+ const healTrigger = registryDiagnosis ? registryRepairPlan?.supported ? registryDiagnosis.state : null : pluginHealTrigger(pluginProbe);
27408
28540
  let pluginHealed = false;
27409
- if (applyEnv && deps.healPlugin && healTrigger) {
28541
+ if (applyEnv && deps.healPlugin && healTrigger && (!registryRepairPlan || registryRepairPlan.supported)) {
27410
28542
  const heal = await deps.healPlugin();
27411
28543
  pluginHealed = heal.ok;
27412
- const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
27413
- const healEvidence = [
27414
- `installed: ${installed ?? "(none)"}`,
27415
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
27416
- `resolvable: ${pluginProbe.guardState}`,
27417
- `heal: ${heal.detail}`
27418
- ];
27419
- checks.push(heal.ok ? {
27420
- id: codexSurface ? "codex-plugin" : "claude-plugin",
27421
- ok: true,
27422
- label: codexSurface ? "Codex plugin" : "Claude plugin",
27423
- detail: `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 ${codexSurface ? "add" : "install"})${codexSurface ? "; review trust in /hooks" : ""}`,
27424
- verbose: healEvidence
27425
- } : {
27426
- id: codexSurface ? "codex-plugin" : "claude-plugin",
27427
- ok: false,
27428
- label: codexSurface ? "Codex plugin" : "Claude plugin",
27429
- detail: measured,
27430
- // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
27431
- // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
27432
- // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
27433
- // genuine gap and still gates.
27434
- ...heal.skipped ? { reportOnly: true } : {},
27435
- fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then ${codexSurface ? "restart Codex" : "restart Claude"}`,
27436
- verbose: healEvidence
27437
- });
27438
- if (!heal.skipped) restartPending = true;
28544
+ if (registryEvidence) {
28545
+ const repairedDiagnosis = diagnoseSurface({
28546
+ ...registryEvidence,
28547
+ releasedVersion: released,
28548
+ repair: { attempted: true, ok: heal.ok, detail: heal.detail }
28549
+ });
28550
+ const repairedCheck = buildSurfaceDoctorCheck(repairedDiagnosis);
28551
+ if (heal.skipped) repairedCheck.reportOnly = true;
28552
+ checks.push(repairedCheck);
28553
+ if (!heal.skipped) restartPending = true;
28554
+ } else {
28555
+ const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
28556
+ const healEvidence = [
28557
+ `installed: ${installed ?? "(none)"}`,
28558
+ `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
28559
+ `resolvable: ${pluginProbe.guardState}`,
28560
+ `heal: ${heal.detail}`
28561
+ ];
28562
+ checks.push(heal.ok ? {
28563
+ id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
28564
+ ok: true,
28565
+ label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
28566
+ detail: pluginSurface === "kilo" ? `${measured} \u2014 reinstalled via \`kilo plugin\` (the plugin provisions the skills on next load)` : cursorSurface ? `${measured} \u2014 replaced the managed local checkout (previous copy quarantined)` : `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 ${codexSurface ? "add" : "install"})${codexSurface ? "; review trust in /hooks" : ""}`,
28567
+ verbose: healEvidence
28568
+ } : {
28569
+ id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
28570
+ ok: false,
28571
+ label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
28572
+ detail: measured,
28573
+ // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
28574
+ // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
28575
+ // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
28576
+ // genuine gap and still gates.
28577
+ ...heal.skipped ? { reportOnly: true } : {},
28578
+ fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then ${codexSurface ? "restart Codex" : pluginSurface === "kilo" ? "reload Kilo" : cursorSurface ? "reload Cursor" : "restart Claude"}`,
28579
+ verbose: healEvidence
28580
+ });
28581
+ if (!heal.skipped) restartPending = true;
28582
+ }
27439
28583
  } else {
27440
- const plugin = checkClaudePlugin(pluginProbe);
28584
+ const plugin = registryDiagnosis ? buildSurfaceDoctorCheck(registryDiagnosis) : checkClaudePlugin(pluginProbe);
27441
28585
  if (plugin) {
27442
- checks.push(plugin);
27443
- if (!plugin.ok) restartPending = true;
28586
+ if (!("state" in plugin) || plugin.state !== "skipped") checks.push(plugin);
28587
+ if (!plugin.ok && (registryDiagnosis ? Boolean(registryRepairPlan?.supported) : true)) restartPending = true;
27444
28588
  }
27445
28589
  }
27446
28590
  if (codexSurface && (pluginProbe.guardState === "healthy" || pluginHealed)) {
@@ -27583,7 +28727,7 @@ async function runDoctorClean(opts, io, deps) {
27583
28727
  io.log(JSON.stringify({
27584
28728
  checks: payload,
27585
28729
  restartPending,
27586
- ...restartPending ? { restartAction: codexSurface ? "restart Codex" : "restart Claude" } : {},
28730
+ ...restartPending ? { restartAction } : {},
27587
28731
  exitCode
27588
28732
  }, null, 2));
27589
28733
  return exitCode;
@@ -27594,12 +28738,13 @@ async function runDoctorClean(opts, io, deps) {
27594
28738
  io.log(renderReport([c], { restartPending: false }));
27595
28739
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
27596
28740
  }
27597
- if (restartPending) io.log(codexSurface ? "\u21BB Restart Codex to finish." : RESTART_LINE);
28741
+ if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
27598
28742
  return 0;
27599
28743
  }
27600
- const rendered = renderDoctorText(checks, { verbose: Boolean(opts.verbose), restartPending: restartPending && !codexSurface });
27601
- io.log(restartPending && codexSurface ? `${rendered}
27602
- \u21BB Restart Codex to finish.` : rendered);
28744
+ const legacyRestart = restartAction === "restart Claude";
28745
+ const rendered = renderDoctorText(checks, { verbose: Boolean(opts.verbose), restartPending: restartPending && legacyRestart });
28746
+ io.log(restartPending && !legacyRestart ? `${rendered}
28747
+ \u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.` : rendered);
27603
28748
  return exitCode;
27604
28749
  }
27605
28750
 
@@ -27701,17 +28846,43 @@ function installedClaudePluginVersion() {
27701
28846
  return void 0;
27702
28847
  }
27703
28848
  }
27704
- function installedActivePluginVersion(surface = detectSurface(process.env)) {
27705
- if (surface !== "codex") return installedClaudePluginVersion();
28849
+ function manifestVersion(path2) {
28850
+ try {
28851
+ const manifest = JSON.parse((0, import_node_fs33.readFileSync)(path2, "utf8"));
28852
+ return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
28853
+ } catch {
28854
+ return void 0;
28855
+ }
28856
+ }
28857
+ function installedSurfacePluginVersion(surface) {
28858
+ const token = surfaceToken(surface);
28859
+ if (token === "kilo") {
28860
+ try {
28861
+ const stamp = (0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
28862
+ return stamp || void 0;
28863
+ } catch {
28864
+ return void 0;
28865
+ }
28866
+ }
28867
+ if (token === "cursor") {
28868
+ return manifestVersion((0, import_node_path32.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
28869
+ }
28870
+ if (token === "kimi") {
28871
+ return manifestVersion((0, import_node_path32.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
28872
+ }
28873
+ if (token === "claude") return installedClaudePluginVersion();
28874
+ if (token !== "codex") return void 0;
27706
28875
  try {
27707
28876
  const raw = process.platform === "win32" ? (0, import_node_child_process14.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
27708
28877
  encoding: "utf8",
27709
28878
  stdio: ["ignore", "pipe", "ignore"],
27710
- timeout: 15e3
28879
+ timeout: 15e3,
28880
+ windowsHide: true
27711
28881
  }) : (0, import_node_child_process14.execFileSync)("codex", ["plugin", "list", "--json"], {
27712
28882
  encoding: "utf8",
27713
28883
  stdio: ["ignore", "pipe", "ignore"],
27714
- timeout: 15e3
28884
+ timeout: 15e3,
28885
+ windowsHide: true
27715
28886
  });
27716
28887
  const parsed = JSON.parse(raw);
27717
28888
  const plugin = parsed.installed?.find((entry) => entry.pluginId === MMI_PLUGIN_ID2 && entry.installed === true && entry.enabled === true);
@@ -27720,6 +28891,9 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
27720
28891
  return void 0;
27721
28892
  }
27722
28893
  }
28894
+ function installedActivePluginVersion(surface = detectSurface(process.env)) {
28895
+ return installedSurfacePluginVersion(surface);
28896
+ }
27723
28897
  function worktreeRootSync() {
27724
28898
  try {
27725
28899
  const out = (0, import_node_child_process14.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -27857,6 +29031,29 @@ function mmiDoctorDeps(opts = {}) {
27857
29031
  const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
27858
29032
  let notebook;
27859
29033
  const notebookOnce = () => notebook ??= fetchNotebook();
29034
+ let surfaceEvidence;
29035
+ let surfaceEvidenceRead = false;
29036
+ const surfaceEvidenceOnce = (isOrgRepo) => {
29037
+ if (surfaceEvidenceRead) return surfaceEvidence;
29038
+ surfaceEvidenceRead = true;
29039
+ const runtimeSurface = detectSurface(process.env);
29040
+ const token = surfaceToken(runtimeSurface);
29041
+ if (!token || token === "opencode") return void 0;
29042
+ const descriptor = doctorSurface(token);
29043
+ const snapshot = snapshotPluginGuardInput(runtimeSurface, isOrgRepo);
29044
+ const installedVersion = installedSurfacePluginVersion(runtimeSurface);
29045
+ surfaceEvidence = {
29046
+ descriptor,
29047
+ applicable: isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
29048
+ installRecordPresent: snapshot.installRecordPresent,
29049
+ deliveryPresent: snapshot.marketplaceClonePresent,
29050
+ payloadPresent: snapshot.pluginCachePresent,
29051
+ manifest: installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing",
29052
+ guardState: buildPluginGuardDecision(snapshot).state,
29053
+ ...installedVersion ? { installedVersion } : {}
29054
+ };
29055
+ return surfaceEvidence;
29056
+ };
27860
29057
  const docsJanitorArmedAt = async (repo) => {
27861
29058
  const wanted = docsJanitorScheduleId(repo);
27862
29059
  const { entries } = await notebookOnce();
@@ -27869,6 +29066,7 @@ function mmiDoctorDeps(opts = {}) {
27869
29066
  awsCallerArn,
27870
29067
  isOrgRepo: () => isOrgRepoRoot(),
27871
29068
  installedPluginVersion: installedActivePluginVersion,
29069
+ surfaceEvidence: surfaceEvidenceOnce,
27872
29070
  pluginGuardState: activePluginGuardState,
27873
29071
  pluginSurface: () => detectSurface(process.env),
27874
29072
  pluginTrustState: () => codexHookTrustState(),
@@ -27884,7 +29082,8 @@ function mmiDoctorDeps(opts = {}) {
27884
29082
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
27885
29083
  healPlugin: () => {
27886
29084
  const surface = detectSurface(process.env);
27887
- return withEnvHealLock(`${surface === "codex" ? "Codex" : "Claude"} plugin reinstall`, () => healActivePluginForDoctor(surface));
29085
+ const host = surface === "codex" ? "Codex" : surface === "kilo" ? "Kilo" : surface === "cursor" ? "Cursor" : "Claude";
29086
+ return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface));
27888
29087
  },
27889
29088
  currentCliVersion: resolveClientVersion,
27890
29089
  readGitignore,
@@ -29344,13 +30543,25 @@ function surfaceWaived() {
29344
30543
  }
29345
30544
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
29346
30545
  withExamples(mutating(
29347
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
29348
- // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
29349
- // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
29350
- // (`--parent`) and title-source are validated by the action on a real run.
30546
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
30547
+ // --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
30548
+ // and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
30549
+ // is not a plan of the mutation that will run (#3914).
29351
30550
  async (opts) => {
29352
30551
  const type = resolveCreateType(opts.type, "issue create", opts.label);
29353
30552
  const priority = resolveCreatePriority(opts.priority, "issue create");
30553
+ let title;
30554
+ try {
30555
+ title = await resolveIssueTitle(
30556
+ { title: opts.title, titleFile: opts.titleFile },
30557
+ { readFile: import_promises10.readFile, readStdin }
30558
+ );
30559
+ } catch (e) {
30560
+ return fail(
30561
+ `issue create: ${e.message}`,
30562
+ e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0
30563
+ );
30564
+ }
29354
30565
  const planLabels = opts.label ?? [];
29355
30566
  const clash = conflictingSurfaceInputs(opts.surface, planLabels);
29356
30567
  if (clash) fail(clash.message, clash.payload);
@@ -29368,7 +30579,7 @@ withExamples(mutating(
29368
30579
  return {
29369
30580
  command: "issue create",
29370
30581
  type,
29371
- title: opts.title ?? opts.titleFile,
30582
+ title,
29372
30583
  priority,
29373
30584
  repo: opts.repo,
29374
30585
  ...surface ? { surface } : {}
@@ -30271,6 +31482,9 @@ function renderReleaseResume(r) {
30271
31482
  if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
30272
31483
  return lines.join("\n");
30273
31484
  }
31485
+ function renderReleaseAbort(r) {
31486
+ return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
31487
+ }
30274
31488
  function renderRcandResume(r) {
30275
31489
  return `mmi-cli rcand --resume: promoted ${r.repo} \u2192 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
30276
31490
  }
@@ -30315,9 +31529,10 @@ async function resolveRcandPlanTargets() {
30315
31529
  for (const commandName of ["rcand", "release"]) {
30316
31530
  const trainCmd = program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").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)");
30317
31531
  const RELEASE_ONLY_FLAGS = [
30318
- { flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
31532
+ { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
30319
31533
  { 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)" },
30320
- { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" }
31534
+ { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
31535
+ { flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" }
30321
31536
  ];
30322
31537
  for (const f of RELEASE_ONLY_FLAGS) {
30323
31538
  if (commandName === "release") {
@@ -30341,6 +31556,26 @@ for (const commandName of ["rcand", "release"]) {
30341
31556
  if (o.announceSummaryFile && commandName !== "release") {
30342
31557
  return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
30343
31558
  }
31559
+ if (o.abort && commandName !== "release") {
31560
+ return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
31561
+ }
31562
+ if (o.abort) {
31563
+ if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
31564
+ if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
31565
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
31566
+ return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
31567
+ }
31568
+ if (o.repo) {
31569
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli release --abort --apply");
31570
+ if (!guard.ok) return fail(`release: ${guard.message}`);
31571
+ }
31572
+ try {
31573
+ const result = await runReleaseAbort(trainApplyDeps(), { approved: true });
31574
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseAbort(result));
31575
+ } catch (e) {
31576
+ return failGraceful(`release --abort: ${e.message}`);
31577
+ }
31578
+ }
30344
31579
  if (o.resume) {
30345
31580
  if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
30346
31581
  try {
@@ -30362,6 +31597,24 @@ for (const commandName of ["rcand", "release"]) {
30362
31597
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
30363
31598
  if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
30364
31599
  }
31600
+ if (o.apply && commandName === "release" && (await resolveRepo())?.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) {
31601
+ if (!o.announceSummaryFile) {
31602
+ return fail(
31603
+ "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command"
31604
+ );
31605
+ }
31606
+ let summaryLines;
31607
+ try {
31608
+ summaryLines = summaryFileLines(await (0, import_promises10.readFile)(o.announceSummaryFile, "utf8"));
31609
+ } catch (e) {
31610
+ return fail(`release: could not read --announce-summary-file ${o.announceSummaryFile}: ${e.message}`);
31611
+ }
31612
+ if (summaryLines.length < 3 || summaryLines.length > 6) {
31613
+ return fail(
31614
+ `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${summaryLines.length})`
31615
+ );
31616
+ }
31617
+ }
30365
31618
  if (o.apply) {
30366
31619
  try {
30367
31620
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);