@mutmutco/cli 3.78.0 → 3.80.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 +1560 -265
  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.",
@@ -6425,7 +6432,7 @@ function scanInstallDirs(root, fs2 = realFsProbe) {
6425
6432
  return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
6426
6433
  }
6427
6434
  function npmInstallTargets(dirs) {
6428
- return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
6435
+ return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install --no-package-lock" }));
6429
6436
  }
6430
6437
  function isLinkedWorktree(root, fs2 = realFsProbe) {
6431
6438
  return fs2.isFile((0, import_node_path10.join)(root, ".git"));
@@ -9430,6 +9437,132 @@ 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
+ }
9515
+ async function runReleasePublishRetry(deps, runId, options = {}) {
9516
+ if (!options.approved) {
9517
+ throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
9518
+ }
9519
+ if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`invalid publish run id ${runId}`);
9520
+ const ctx = await buildTrainApplyContext(deps);
9521
+ if (!isHubControlRepo(ctx.repo)) {
9522
+ throw new Error("release --retry-publish is limited to mutmutco/MMI-Hub");
9523
+ }
9524
+ await requireCleanTree(deps);
9525
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
9526
+ const readRun = async () => {
9527
+ const raw = await deps.run("gh", ["run", "view", String(runId), "--repo", ctx.repo, "--json", "databaseId,workflowName,event,headBranch,headSha,status,conclusion,url"]);
9528
+ try {
9529
+ return JSON.parse(raw);
9530
+ } catch {
9531
+ throw new Error(`publish run ${runId} metadata was not valid JSON`);
9532
+ }
9533
+ };
9534
+ const run = await readRun();
9535
+ if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || run.conclusion !== "failure") {
9536
+ throw new Error(`run ${runId} is not a completed failed Hub publish release run; nothing was written`);
9537
+ }
9538
+ const tag = run.headBranch ?? "";
9539
+ const tagSha = run.headSha ?? "";
9540
+ if (!/^v\d+\.\d+\.\d+$/.test(tag) || !/^[0-9a-f]{40}$/.test(tagSha)) {
9541
+ throw new Error(`run ${runId} has invalid release identity (${tag || "(no tag)"} / ${tagSha || "(no sha)"}); nothing was written`);
9542
+ }
9543
+ if (await probeRemoteTag(deps, tag) !== tagSha) {
9544
+ throw new Error(`run ${runId} does not match origin ${tag}; nothing was written`);
9545
+ }
9546
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
9547
+ await deps.run("gh", ["run", "rerun", String(runId), "--repo", ctx.repo, "--failed"]);
9548
+ if (options.watch) {
9549
+ await deps.run("gh", ["run", "watch", String(runId), "--repo", ctx.repo, "--exit-status"]);
9550
+ const completed = await readRun();
9551
+ if (completed.status !== "completed" || completed.conclusion !== "success") {
9552
+ throw new Error(`publish run ${runId} did not finish successfully after retry`);
9553
+ }
9554
+ }
9555
+ return {
9556
+ command: "release-retry-publish",
9557
+ repo: ctx.repo,
9558
+ tag,
9559
+ tagSha,
9560
+ runId,
9561
+ runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
9562
+ status: options.watch ? "success" : "pending",
9563
+ note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} failed jobs queued for retry`
9564
+ };
9565
+ }
9433
9566
  async function runTrainApplyPipeline(mode, input) {
9434
9567
  const { deps, ctx, command, meta, branchHints, watch, options } = input;
9435
9568
  const directTrack = input.directTrack ?? false;
@@ -12318,6 +12451,18 @@ function formatVaultPointer(p) {
12318
12451
  return lines.join("\n");
12319
12452
  }
12320
12453
  var TIMEOUT_MS = 8e3;
12454
+ var VAULT_READ_ATTEMPTS = 2;
12455
+ async function fetchVaultRead(deps, url, init) {
12456
+ let lastError;
12457
+ for (let attempt = 0; attempt < VAULT_READ_ATTEMPTS; attempt += 1) {
12458
+ try {
12459
+ return await deps.fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
12460
+ } catch (error) {
12461
+ lastError = error;
12462
+ }
12463
+ }
12464
+ throw lastError;
12465
+ }
12321
12466
  var repoOf = (slug) => `${OWNER}/${slug}`;
12322
12467
  var RECALL_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "ap-northeast-1"];
12323
12468
  var PROVIDER_VERIFY_TIMEOUT_MS = 8e3;
@@ -12405,11 +12550,10 @@ async function fetchSecretValue(deps, key, opts) {
12405
12550
  const repo = await targetRepo(deps, opts);
12406
12551
  const slug = opts.slug?.toLowerCase();
12407
12552
  try {
12408
- const res = await deps.fetch(`${deps.apiUrl}/secrets/get`, {
12553
+ const res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
12409
12554
  method: "POST",
12410
12555
  headers: await deps.headers({ "content-type": "application/json" }),
12411
- body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} }),
12412
- signal: AbortSignal.timeout(TIMEOUT_MS)
12556
+ body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
12413
12557
  });
12414
12558
  if (!res.ok) return null;
12415
12559
  const { value } = await res.json();
@@ -12423,10 +12567,9 @@ async function secretsList(deps, opts) {
12423
12567
  const qs = new URLSearchParams({ repo }).toString();
12424
12568
  let res;
12425
12569
  try {
12426
- res = await deps.fetch(`${deps.apiUrl}/secrets/list?${qs}`, {
12570
+ res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/list?${qs}`, {
12427
12571
  method: "GET",
12428
- headers: await deps.headers(),
12429
- signal: AbortSignal.timeout(TIMEOUT_MS)
12572
+ headers: await deps.headers()
12430
12573
  });
12431
12574
  } catch (e) {
12432
12575
  deps.err(`secrets list: ${e.message}`);
@@ -13478,12 +13621,17 @@ function secretsUseExitCode(result) {
13478
13621
  return void 0;
13479
13622
  }
13480
13623
  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
- });
13624
+ let res;
13625
+ try {
13626
+ res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
13627
+ method: "POST",
13628
+ headers: await deps.headers({ "content-type": "application/json" }),
13629
+ body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
13630
+ });
13631
+ } catch (error) {
13632
+ deps.err(`secrets use: ${error.message}`);
13633
+ return null;
13634
+ }
13487
13635
  if (!res.ok) {
13488
13636
  const body = await readJsonBody(res);
13489
13637
  if (res.status === 404 && body.code === "secret_not_found") {
@@ -14175,7 +14323,7 @@ function buildPluginGuardLine(state, opts = {}) {
14175
14323
  if (state === "healthy" || state === "not-org") return { exitCode: 0 };
14176
14324
  const recovery = opts.recovery ?? "mmi-cli plugin heal";
14177
14325
  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";
14326
+ 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
14327
  return {
14180
14328
  line: `[mmi-guard] ${reason}; run ${recovery} and ${restartHint}.`,
14181
14329
  exitCode: 1
@@ -14195,6 +14343,9 @@ function detectSurface(env) {
14195
14343
  if (env.MMI_AGENT_SURFACE === "kimi" || has("KIMI_PLUGIN_ROOT") || has("KIMI_CODE_HOME")) {
14196
14344
  return "kimi";
14197
14345
  }
14346
+ if (env.MMI_AGENT_SURFACE === "kilo" || has("KILO") || Object.keys(env).some((k) => /^(?:KILO|KILOCODE)_/.test(k))) {
14347
+ return "kilo";
14348
+ }
14198
14349
  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
14350
  return "cursor";
14200
14351
  }
@@ -14214,6 +14365,8 @@ function surfaceToken(surface) {
14214
14365
  return "codex";
14215
14366
  case "kimi":
14216
14367
  return "kimi";
14368
+ case "kilo":
14369
+ return "kilo";
14217
14370
  case "cursor":
14218
14371
  return "cursor";
14219
14372
  case "opencode":
@@ -14231,10 +14384,12 @@ function reloadAction(surface) {
14231
14384
  return "restart Codex";
14232
14385
  case "kimi":
14233
14386
  return "run /reload (or start a new session) in Kimi Code";
14387
+ case "kilo":
14388
+ return "run /reload (or start a new session) in Kilo Code";
14234
14389
  case "opencode":
14235
14390
  return "restart OpenCode";
14236
14391
  case "cursor":
14237
- return "restart Cursor (or refresh the Team Marketplace from Dashboard \u2192 Settings \u2192 Plugins)";
14392
+ return "reload the Cursor window";
14238
14393
  case "claude-cli":
14239
14394
  case "shell":
14240
14395
  default:
@@ -14243,6 +14398,7 @@ function reloadAction(surface) {
14243
14398
  }
14244
14399
  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
14400
  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";
14401
+ var CURSOR_RECOVERY = "mmi-cli plugin heal # installs ~/.cursor/plugins/local/mmi from mutmutco/MMI-Hub";
14246
14402
  var PLUGIN_SURFACE_HEAL = {
14247
14403
  claude: {
14248
14404
  delivery: "plugin-cli",
@@ -14268,6 +14424,24 @@ var PLUGIN_SURFACE_HEAL = {
14268
14424
  ],
14269
14425
  fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} and review /hooks`,
14270
14426
  updateRecipe: [CODEX_RECOVERY]
14427
+ },
14428
+ cursor: {
14429
+ delivery: "local-checkout",
14430
+ recovery: CURSOR_RECOVERY,
14431
+ healSteps: null,
14432
+ fix: (surface) => `${CURSOR_RECOVERY} # then ${reloadAction(surface)}`,
14433
+ updateRecipe: [CURSOR_RECOVERY]
14434
+ },
14435
+ kilo: {
14436
+ // kilo-p1: no marketplace step — `kilo plugin <npm>` installs + patches the config in one non-interactive
14437
+ // verb. --global writes ~/.config/kilo/opencode.json; the plugin's server() provisions the skills.
14438
+ delivery: "plugin-cli",
14439
+ recovery: "kilo plugin @mutmutco/kilo-plugin --global",
14440
+ healSteps: [
14441
+ { args: ["plugin", "@mutmutco/kilo-plugin", "--global"], gated: true }
14442
+ ],
14443
+ fix: (surface) => `kilo plugin @mutmutco/kilo-plugin --global # then ${reloadAction(surface)} to load the plugin and its provisioned skills`,
14444
+ updateRecipe: ["kilo plugin @mutmutco/kilo-plugin --global"]
14271
14445
  }
14272
14446
  };
14273
14447
  function nonClaudeSurfaceHealMessage(surface) {
@@ -14277,7 +14451,13 @@ function nonClaudeSurfaceHealMessage(surface) {
14277
14451
  if (surface === "kimi") {
14278
14452
  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
14453
  }
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)";
14454
+ if (surface === "kilo") {
14455
+ 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";
14456
+ }
14457
+ if (surface === "cursor") {
14458
+ 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";
14459
+ }
14460
+ 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
14461
  }
14282
14462
  function healStepAborts(step, ok) {
14283
14463
  return !ok && step.gated;
@@ -14319,6 +14499,8 @@ function runHostBin(bin, args, opts) {
14319
14499
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os5.homedir)()) {
14320
14500
  if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path16.join)(home, ".codex");
14321
14501
  if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path16.join)(home, ".kimi-code");
14502
+ if (surface === "kilo") return env.KILO_CONFIG_DIR?.trim() || (0, import_node_path16.join)(home, ".config", "kilo");
14503
+ if (surface === "cursor") return (0, import_node_path16.join)(home, ".cursor");
14322
14504
  return (0, import_node_path16.join)(home, ".claude");
14323
14505
  }
14324
14506
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
@@ -14340,6 +14522,8 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
14340
14522
  ];
14341
14523
  }
14342
14524
  if (surface === "kimi") return [];
14525
+ if (surface === "kilo") return [];
14526
+ if (surface === "cursor") return [];
14343
14527
  return [(0, import_node_path16.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
14344
14528
  }
14345
14529
  function marketplaceClonePresent(surface, home, exists = import_node_fs18.existsSync, env = process.env) {
@@ -14349,11 +14533,13 @@ function runHostBinSync(bin, args) {
14349
14533
  return isWin ? (0, import_node_child_process7.execFileSync)("cmd.exe", ["/c", bin, ...args], {
14350
14534
  encoding: "utf8",
14351
14535
  stdio: ["ignore", "pipe", "ignore"],
14352
- timeout: 15e3
14536
+ timeout: 15e3,
14537
+ windowsHide: true
14353
14538
  }) : (0, import_node_child_process7.execFileSync)(bin, args, {
14354
14539
  encoding: "utf8",
14355
14540
  stdio: ["ignore", "pipe", "ignore"],
14356
- timeout: 15e3
14541
+ timeout: 15e3,
14542
+ windowsHide: true
14357
14543
  });
14358
14544
  }
14359
14545
  function codexPluginStatus() {
@@ -14417,32 +14603,75 @@ async function fetchNpmReleasedVersion() {
14417
14603
  }
14418
14604
  }
14419
14605
  var NPM_INSTALL_TIMEOUT_MS = 12e4;
14420
- async function npmSelfUpdateCli(target) {
14606
+ async function npmSelfUpdateCli(target, onStep) {
14421
14607
  const command = cliUpdateCommand(target);
14422
14608
  try {
14609
+ onStep?.(command);
14423
14610
  await runHostBin("npm", ["install", "-g", `@mutmutco/cli@${target ?? "latest"}`], { timeout: NPM_INSTALL_TIMEOUT_MS });
14424
14611
  return { ok: true, detail: `${command} exited 0` };
14425
14612
  } catch (e) {
14426
14613
  return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
14427
14614
  }
14428
14615
  }
14616
+ function kiloConfigListsPlugin(configRoot, home = (0, import_node_os5.homedir)(), read = (p) => (0, import_node_fs18.readFileSync)(p, "utf8"), exists = import_node_fs18.existsSync) {
14617
+ const candidates = ["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc", "config.json"];
14618
+ for (const dir of [configRoot, (0, import_node_path16.join)(home, ".kilo")]) {
14619
+ for (const file of candidates) {
14620
+ const path2 = (0, import_node_path16.join)(dir, file);
14621
+ if (!exists(path2)) continue;
14622
+ try {
14623
+ const stripped = read(path2).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
14624
+ const parsed = JSON.parse(stripped);
14625
+ const plugins = Array.isArray(parsed?.plugin) ? parsed.plugin : [];
14626
+ if (plugins.some((p) => {
14627
+ const spec = String(Array.isArray(p) ? p[0] : p);
14628
+ return spec.includes("@mutmutco/kilo-plugin") || spec.includes(".kilo-plugin");
14629
+ })) return true;
14630
+ } catch {
14631
+ }
14632
+ }
14633
+ }
14634
+ return false;
14635
+ }
14636
+ function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os5.homedir)()) {
14637
+ return (0, import_node_path16.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
14638
+ }
14639
+ function cursorPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
14640
+ return [
14641
+ ".cursor-plugin/plugin.json",
14642
+ "skills/mmi/SKILL.md",
14643
+ "hooks/cursor-hooks.json",
14644
+ "scripts/hook-run.mjs",
14645
+ "scripts/hook-policy.mjs"
14646
+ ].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
14647
+ }
14648
+ function kimiPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
14649
+ return [
14650
+ ".kimi-plugin/plugin.json",
14651
+ "skills/mmi/SKILL.md",
14652
+ "scripts/hook-run.mjs"
14653
+ ].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
14654
+ }
14429
14655
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
14430
14656
  const root = surfaceConfigRoot(surface);
14431
14657
  const installed = readInstalledPlugins(surface);
14432
14658
  const codexStatus = surface === "codex" ? codexPluginStatus() : void 0;
14433
14659
  return {
14434
14660
  isOrgRepo,
14435
- installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
14436
- marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os5.homedir)()),
14661
+ 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.
14662
+ 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.
14663
+ surface === "kilo" && kiloConfigListsPlugin(root) || surface === "cursor" && (0, import_node_fs18.existsSync)(cursorLocalPluginRoot()),
14664
+ // Kilo has no marketplace to clone — the config file IS the install record, so this dimension of
14665
+ // the shared guard table is vacuously satisfied.
14666
+ marketplaceClonePresent: surface === "kimi" || surface === "kilo" || surface === "cursor" ? true : marketplaceClonePresent(surface, (0, import_node_os5.homedir)()),
14437
14667
  // 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(
14668
+ // Kilo (kilo-p1) keeps no cache dir either: the plugin's server() provisions ~/.kilo behind the
14669
+ // version stamp, so the stamp's presence is the cache signal.
14670
+ 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
14671
  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
14672
  ) : (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", "mutmutco", "mmi"))
14441
14673
  };
14442
14674
  }
14443
- function activePluginGuardState(isOrgRepo) {
14444
- return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
14445
- }
14446
14675
  async function runClaudePlugin(args) {
14447
14676
  try {
14448
14677
  await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
@@ -14459,6 +14688,98 @@ async function runCodexPlugin(args) {
14459
14688
  return false;
14460
14689
  }
14461
14690
  }
14691
+ async function runKiloPlugin(args) {
14692
+ try {
14693
+ await runHostBin("kilo", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
14694
+ return true;
14695
+ } catch {
14696
+ return false;
14697
+ }
14698
+ }
14699
+ function captureCodexHookLauncher() {
14700
+ const status = codexPluginStatus();
14701
+ if (!status.installed || !status.enabled || !status.version) return void 0;
14702
+ const root = (0, import_node_path16.join)(surfaceConfigRoot("codex"), "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version);
14703
+ const files = ["mmi-hook", "mmi-hook.exe"].flatMap((name) => {
14704
+ const path2 = (0, import_node_path16.join)(root, "bin", name);
14705
+ try {
14706
+ return [{ name, content: (0, import_node_fs18.readFileSync)(path2) }];
14707
+ } catch {
14708
+ return [];
14709
+ }
14710
+ });
14711
+ return files.length === 2 ? { root, files } : void 0;
14712
+ }
14713
+ function restoreCodexHookLauncher(snapshot) {
14714
+ if (!snapshot || (0, import_node_fs18.existsSync)((0, import_node_path16.join)(snapshot.root, "scripts", "hook-run.mjs"))) return false;
14715
+ const bin = (0, import_node_path16.join)(snapshot.root, "bin");
14716
+ (0, import_node_fs18.mkdirSync)(bin, { recursive: true });
14717
+ for (const file of snapshot.files) {
14718
+ const path2 = (0, import_node_path16.join)(bin, file.name);
14719
+ (0, import_node_fs18.writeFileSync)(path2, file.content);
14720
+ if (file.name === "mmi-hook") (0, import_node_fs18.chmodSync)(path2, 493);
14721
+ }
14722
+ return true;
14723
+ }
14724
+ function canonicalCursorRemote(remote) {
14725
+ return /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com[:/])mutmutco\/MMI-Hub(?:\.git)?\/?$/i.test(remote.trim().replace(/\\/g, "/"));
14726
+ }
14727
+ async function installCursorPluginCheckout(env = process.env) {
14728
+ const configRoot = surfaceConfigRoot("cursor", env);
14729
+ const pluginsRoot = (0, import_node_path16.join)(configRoot, "plugins");
14730
+ const target = (0, import_node_path16.join)(pluginsRoot, "local", "mmi");
14731
+ const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
14732
+ if ((0, import_node_fs18.existsSync)(target) && !source) {
14733
+ try {
14734
+ const { stdout } = await runHostBin("git", ["-C", target, "remote", "get-url", "origin"], { timeout: 15e3 });
14735
+ if (!canonicalCursorRemote(stdout)) {
14736
+ return { ok: false, detail: `refused to replace unrelated Cursor plugin directory at ${target}` };
14737
+ }
14738
+ } catch {
14739
+ return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target}` };
14740
+ }
14741
+ }
14742
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "local"), { recursive: true });
14743
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "staging"), { recursive: true });
14744
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "quarantine"), { recursive: true });
14745
+ const suffix = `${Date.now()}-${process.pid}`;
14746
+ const staged = (0, import_node_path16.join)(pluginsRoot, "staging", `mmi-${suffix}`);
14747
+ const quarantined = (0, import_node_path16.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
14748
+ try {
14749
+ if (source) {
14750
+ (0, import_node_fs18.cpSync)(source, staged, {
14751
+ recursive: true,
14752
+ filter: (path2) => !path2.split(/[\\/]/).some((part) => part === ".git" || part === "node_modules")
14753
+ });
14754
+ } else {
14755
+ await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
14756
+ timeout: CLAUDE_PLUGIN_TIMEOUT_MS
14757
+ });
14758
+ }
14759
+ if (!cursorPluginTreeHealthy(staged)) {
14760
+ (0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
14761
+ return { ok: false, detail: "downloaded Cursor plugin is incomplete; existing install was preserved" };
14762
+ }
14763
+ let movedOld = false;
14764
+ if ((0, import_node_fs18.existsSync)(target)) {
14765
+ (0, import_node_fs18.renameSync)(target, quarantined);
14766
+ movedOld = true;
14767
+ }
14768
+ try {
14769
+ (0, import_node_fs18.renameSync)(staged, target);
14770
+ } catch (error) {
14771
+ if (movedOld && !(0, import_node_fs18.existsSync)(target)) (0, import_node_fs18.renameSync)(quarantined, target);
14772
+ throw error;
14773
+ }
14774
+ return {
14775
+ ok: true,
14776
+ detail: movedOld ? `installed canonical Cursor plugin; previous checkout quarantined at ${quarantined}` : `installed canonical Cursor plugin at ${target}`
14777
+ };
14778
+ } catch (error) {
14779
+ if ((0, import_node_fs18.existsSync)(staged)) (0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
14780
+ return { ok: false, detail: error.message.trim().slice(0, 240).replace(/\s+/g, " ") };
14781
+ }
14782
+ }
14462
14783
  async function marketplaceAddRefSupported(bin) {
14463
14784
  try {
14464
14785
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -14486,49 +14807,79 @@ function pluginReadGrantNote(login = "<your-github-login>") {
14486
14807
  }
14487
14808
  async function applyPluginHeal(surface, log, opts) {
14488
14809
  const token = surfaceToken(surface);
14489
- if (token !== "claude" && token !== "codex") return false;
14490
- if (!opts?.force && !PLUGIN_SURFACE_HEAL[token]) return false;
14810
+ if (token !== "claude" && token !== "codex" && token !== "kilo") return false;
14491
14811
  const descriptor = PLUGIN_SURFACE_HEAL[token];
14812
+ if (!descriptor || !descriptor.healSteps) return false;
14813
+ if (token === "kilo") {
14814
+ log(" \u21BB reinstalling the MMI plugin via `kilo plugin` (install \u2192 server() provisions the skills)\u2026");
14815
+ for (const step of descriptor.healSteps) {
14816
+ const ok = await runKiloPlugin([...step.args]);
14817
+ if (healStepAborts(step, ok)) return false;
14818
+ }
14819
+ return true;
14820
+ }
14492
14821
  const tableSteps = descriptor.healSteps;
14493
14822
  if (!tableSteps) return false;
14823
+ const loadedCodexLauncher = token === "codex" ? captureCodexHookLauncher() : void 0;
14494
14824
  const bin = token;
14495
14825
  const refSupported = await marketplaceAddRefSupported(bin);
14496
14826
  const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
14497
14827
  log(healBannerLine(bin, token, refSupported));
14498
14828
  const pinsPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
14499
14829
  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;
14830
+ try {
14831
+ for (const step of steps) {
14832
+ const ok = token === "claude" ? await runClaudePlugin([...step.args]) : await runCodexPlugin([...step.args]);
14833
+ if (healStepAborts(step, ok)) return false;
14834
+ }
14835
+ } finally {
14836
+ if (restoreCodexHookLauncher(loadedCodexLauncher)) {
14837
+ log(" retained the windowless Codex hook bridge for the loaded session; restart removes its need");
14838
+ }
14503
14839
  }
14504
14840
  const restored = token === "claude" ? restoreMarketplacePinsOnDisk(pinsPath, pins) : void 0;
14505
14841
  if (restored) log(` ${restored}`);
14506
14842
  return true;
14507
14843
  }
14508
- async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
14844
+ async function healClaudePluginForDoctor(surface = detectSurface(process.env), onStep) {
14509
14845
  if (surfaceToken(surface) !== "claude") {
14510
14846
  return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
14511
14847
  }
14512
14848
  const steps = [];
14513
- const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
14849
+ const ok = await applyPluginHeal(surface, (msg) => {
14850
+ const line = msg.trim();
14851
+ steps.push(line);
14852
+ if (line) onStep?.(line);
14853
+ });
14514
14854
  const pinNote = steps.find((s) => s.startsWith("re-pinned ") || s.includes("re-pin by hand"));
14515
14855
  return {
14516
14856
  ok,
14517
14857
  detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${pinNote ? `; ${pinNote}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
14518
14858
  };
14519
14859
  }
14520
- async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
14860
+ async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
14521
14861
  const token = surfaceToken(surface);
14522
- if (token !== "claude" && token !== "codex") {
14862
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14523
14863
  return { ok: false, detail: `not a supported plugin surface (${surface})` };
14524
14864
  }
14525
- if (token === "claude") return healClaudePluginForDoctor(surface);
14865
+ if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
14866
+ if (token === "cursor") return installCursorPluginCheckout();
14526
14867
  const steps = [];
14527
- const applied = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
14868
+ const applied = await applyPluginHeal(surface, (msg) => {
14869
+ const line = msg.trim();
14870
+ steps.push(line);
14871
+ if (line) onStep?.(line);
14872
+ });
14528
14873
  const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
14529
14874
  const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
14530
14875
  const ok = applied && guardState === "healthy";
14531
14876
  const verification = snapshot ? `record=${snapshot.installRecordPresent ? "yes" : "no"}, marketplace=${snapshot.marketplaceClonePresent ? "yes" : "no"}, enabled-cache=${snapshot.pluginCachePresent ? "yes" : "no"}` : "reinstall steps did not complete";
14877
+ if (token === "kilo") {
14878
+ return {
14879
+ ok,
14880
+ detail: ok ? `kilo plugin install succeeded; full guard verified (${verification})` : `\`kilo plugin\` reinstall failed full-guard verification (${verification})${steps.length ? `; ${steps[steps.length - 1]}` : ""}`
14881
+ };
14882
+ }
14532
14883
  return {
14533
14884
  ok,
14534
14885
  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 +14913,14 @@ async function runGuard(readOrigin) {
14562
14913
  const surface = detectSurface(process.env);
14563
14914
  try {
14564
14915
  const token = surfaceToken(surface);
14565
- if (token !== "claude" && token !== "codex") {
14916
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14566
14917
  process.exitCode = 0;
14567
14918
  return;
14568
14919
  }
14569
14920
  const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
14570
14921
  const input = snapshotPluginGuardInput(surface, isOrgRepo);
14571
14922
  const { state } = buildPluginGuardDecision(input);
14572
- const { line, exitCode } = buildPluginGuardLine(state);
14923
+ const { line, exitCode } = buildPluginGuardLine(state, { restartHint: reloadAction(surface) });
14573
14924
  if (line) console.error(line);
14574
14925
  if (token === "codex" && exitCode === 0) {
14575
14926
  const trust = codexHookTrustState();
@@ -14582,6 +14933,10 @@ async function runGuard(readOrigin) {
14582
14933
  if (surfaceToken(surface) === "codex") {
14583
14934
  console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
14584
14935
  process.exitCode = 1;
14936
+ } else if (surfaceToken(surface) === "kilo" || surfaceToken(surface) === "cursor") {
14937
+ const host = surfaceToken(surface) === "cursor" ? "Cursor" : "Kilo";
14938
+ console.error(`[mmi-guard] Could not inspect the active ${host} plugin; run \`mmi-cli plugin heal\`.`);
14939
+ process.exitCode = 1;
14585
14940
  } else {
14586
14941
  process.exitCode = 0;
14587
14942
  }
@@ -14589,21 +14944,24 @@ async function runGuard(readOrigin) {
14589
14944
  }
14590
14945
  async function runPluginHeal(surface = detectSurface(process.env)) {
14591
14946
  const token = surfaceToken(surface);
14592
- if (token !== "claude" && token !== "codex") {
14947
+ if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
14593
14948
  console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
14594
14949
  return;
14595
14950
  }
14596
14951
  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;
14952
+ const cursorResult = token === "cursor" ? await installCursorPluginCheckout() : void 0;
14953
+ if (cursorResult) console.log(` \u21BB ${cursorResult.detail}`);
14954
+ const applied = cursorResult ? cursorResult.ok : await applyPluginHeal(surface, console.log, { force: true });
14955
+ const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
14599
14956
  if (healed) {
14600
14957
  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}`);
14958
+ const reload = token === "kilo" ? " The plugin provisions the skills on first load." : "";
14959
+ console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.${trust}${reload}`);
14602
14960
  } else {
14603
14961
  process.exitCode = 1;
14604
- const refSupported = await marketplaceAddRefSupported(token);
14605
- const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
14606
- const note = refAbsenceNote(token, refSupported);
14962
+ const refSupported = token === "kilo" || token === "cursor" ? false : await marketplaceAddRefSupported(token);
14963
+ const recovery = token === "kilo" || token === "cursor" ? descriptor.recovery : refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
14964
+ const note = token === "kilo" || token === "cursor" ? "" : refAbsenceNote(token, refSupported);
14607
14965
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
14608
14966
  ${recovery}${note}${pluginReadGrantNote()}`);
14609
14967
  }
@@ -16126,12 +16484,20 @@ function resolveIssueBody(input, deps) {
16126
16484
  noun: "body"
16127
16485
  });
16128
16486
  }
16129
- function resolveIssueTitle(input, deps) {
16130
- return resolveTextArg({ value: input.title, file: input.titleFile }, deps, {
16487
+ async function resolveIssueTitle(input, deps) {
16488
+ const title = await resolveTextArg({ value: input.title, file: input.titleFile }, deps, {
16131
16489
  value: "--title",
16132
16490
  file: "--title-file",
16133
16491
  noun: "title"
16134
16492
  });
16493
+ const normalized = title.replace(/(?:\r\n|\n|\r)$/, "");
16494
+ if (!normalized.trim()) {
16495
+ throw new TextArgError("--title produced an empty title", ERROR_CODES.ERR_EMPTY_INPUT, "--title");
16496
+ }
16497
+ if (/[\r\n]/.test(normalized)) {
16498
+ throw new TextArgError("--title must be one line; internal line breaks are not allowed", ERROR_CODES.ERR_INVALID_INPUT, "--title");
16499
+ }
16500
+ return normalized;
16135
16501
  }
16136
16502
 
16137
16503
  // src/issue-view-json.ts
@@ -16219,6 +16585,40 @@ var PATH_OVERRIDES = {
16219
16585
  "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
16220
16586
  "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
16221
16587
  };
16588
+ var COMMAND_OWNERSHIP = {
16589
+ onboard: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16590
+ status: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16591
+ next: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
16592
+ doctor: { module_owner: "cli/src/doctor-clean.ts", consumer: "agent-session" },
16593
+ whoami: { module_owner: "cli/src/whoami.ts", consumer: "agent-session" },
16594
+ commands: { module_owner: "cli/src/command-manifest.ts", consumer: "agent-session" },
16595
+ explain: { module_owner: "cli/src/explain-command.ts", consumer: "agent-session" },
16596
+ board: { module_owner: "cli/src/board-commands.ts", consumer: "agent-workflow" },
16597
+ issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
16598
+ worktree: { module_owner: "cli/src/worktree-lifecycle-commands.ts", consumer: "agent-workflow" },
16599
+ stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
16600
+ pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
16601
+ ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
16602
+ rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16603
+ release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16604
+ hotfix: { module_owner: "cli/src/hotfix-apply.ts", consumer: "release-operator" },
16605
+ train: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
16606
+ bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
16607
+ secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
16608
+ docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
16609
+ tests: { module_owner: "cli/src/test-policy-core.ts", consumer: "repo-gates" },
16610
+ wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
16611
+ report: { module_owner: "cli/src/report.ts", consumer: "campaign-orchestrator" },
16612
+ "skill-lesson": { module_owner: "cli/src/skill-lesson.ts", consumer: "campaign-orchestrator" },
16613
+ org: { module_owner: "cli/src/command-consolidation.ts", consumer: "org-operator" },
16614
+ runtime: { module_owner: "cli/src/command-consolidation.ts", consumer: "runtime-operator" },
16615
+ plugin: { module_owner: "cli/src/plugin-guard-io.ts", consumer: "host-runtime" }
16616
+ };
16617
+ function commandOwnership(name) {
16618
+ const ownership = COMMAND_OWNERSHIP[name];
16619
+ if (!ownership) throw new Error(`command taxonomy: root "${name}" has no module owner or consumer`);
16620
+ return ownership;
16621
+ }
16222
16622
  var DEFAULT_ADMIN_MARKER = "(master-only)";
16223
16623
  var ADMIN_MARKERS = {
16224
16624
  "secrets org-catalog": "(writes master-only)"
@@ -16233,7 +16633,8 @@ function primaryMetadata(name) {
16233
16633
  return {
16234
16634
  category: SUPPORT_PRIMARY.has(name) ? "support" : "core",
16235
16635
  discovery: "primary",
16236
- help_group: helpGroup
16636
+ help_group: helpGroup,
16637
+ ...commandOwnership(name)
16237
16638
  };
16238
16639
  }
16239
16640
  }
@@ -16248,7 +16649,8 @@ function topLevelMetadata(name) {
16248
16649
  return {
16249
16650
  category: name === "plugin" ? "internal" : "admin",
16250
16651
  discovery: "all-only",
16251
- help_group: "Operations"
16652
+ help_group: "Operations",
16653
+ ...commandOwnership(name)
16252
16654
  };
16253
16655
  }
16254
16656
  function setCommandMetadata(command, metadata) {
@@ -16265,7 +16667,7 @@ var REQUIRED_OPTION_HELP = {
16265
16667
  };
16266
16668
  function classifyTree(command, path2, inherited, hideFromParent) {
16267
16669
  const override = PATH_OVERRIDES[path2];
16268
- const metadata = override ?? inherited;
16670
+ const metadata = override ? { ...inherited, ...override } : inherited;
16269
16671
  setCommandMetadata(command, metadata);
16270
16672
  const hideByOverride = override !== void 0 && metadata.discovery === "hidden";
16271
16673
  if ((hideByOverride || hideFromParent) && metadata.discovery !== "primary") {
@@ -16401,7 +16803,9 @@ function buildCommand(cmd, path2) {
16401
16803
  const metadata = commandMetadata(cmd) ?? {
16402
16804
  category: "core",
16403
16805
  discovery: "primary",
16404
- help_group: "Plan and work"
16806
+ help_group: "Plan and work",
16807
+ module_owner: "unclassified",
16808
+ consumer: "unclassified"
16405
16809
  };
16406
16810
  const out = {
16407
16811
  name: cmd.name(),
@@ -20960,7 +21364,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20960
21364
  name: e.name,
20961
21365
  executor: e.executor,
20962
21366
  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'
21367
+ remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run schedules register so it creates a SCHEDULE# row'
20964
21368
  });
20965
21369
  }
20966
21370
  }
@@ -20973,7 +21377,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20973
21377
  name: r.id,
20974
21378
  executor: r.executor || live.executor,
20975
21379
  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"
21380
+ remedy: "re-run schedules register to refresh the SCHEDULE# row from the current header/cron"
20977
21381
  });
20978
21382
  }
20979
21383
  continue;
@@ -20985,7 +21389,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
20985
21389
  name: r.id,
20986
21390
  executor: r.executor || "github-actions",
20987
21391
  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)`
21392
+ 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
21393
  });
20990
21394
  }
20991
21395
  }
@@ -21433,27 +21837,27 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
21433
21837
  const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
21434
21838
  const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
21435
21839
  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".`);
21840
+ 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
21841
  }
21438
21842
  const h = header;
21439
21843
  if (h.schedule !== expectedId) {
21440
- throw new Error(`schedules lift: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
21844
+ throw new Error(`schedules register: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
21441
21845
  }
21442
21846
  for (const cron of crons) {
21443
21847
  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.`);
21848
+ 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
21849
  }
21446
21850
  }
21447
21851
  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.`);
21852
+ 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
21853
  }
21450
21854
  const target = header.target;
21451
21855
  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.`);
21856
+ 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
21857
  }
21454
21858
  const model = header.model;
21455
21859
  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.`);
21860
+ 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
21861
  }
21458
21862
  return {
21459
21863
  id: expectedId,
@@ -21478,7 +21882,7 @@ function scheduleRecordsFromWorkflows(repo, files) {
21478
21882
  const rec = scheduleRecordFromWorkflow(repo, path2, text);
21479
21883
  if (!rec) continue;
21480
21884
  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}).`);
21885
+ throw new Error(`schedules register: duplicate schedule id \`${rec.id}\` \u2014 two workflow files in ${repo} share one join key (${rec.sourcePath}).`);
21482
21886
  }
21483
21887
  seen.add(rec.id);
21484
21888
  records.push(rec);
@@ -21530,16 +21934,16 @@ async function runSchedulesLift(opts, deps = {}) {
21530
21934
  const url = await (deps.originUrl ?? (() => gitOut(["remote", "get-url", "origin"])))();
21531
21935
  repo = repoNameFromRemoteUrl(url) ?? void 0;
21532
21936
  if (!repo) {
21533
- throw new Error("schedules lift: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
21937
+ throw new Error("schedules register: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
21534
21938
  }
21535
21939
  }
21536
21940
  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.`);
21941
+ 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
21942
  }
21539
21943
  const dir = opts.dir ?? DEFAULT_WORKFLOWS_DIR;
21540
21944
  const files = await (deps.readFiles ?? readWorkflowFiles)(dir);
21541
21945
  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).`);
21946
+ 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
21947
  }
21544
21948
  const records = scheduleRecordsFromWorkflows(repo, files);
21545
21949
  const payload = { repo, schedules: records };
@@ -21548,11 +21952,11 @@ async function runSchedulesLift(opts, deps = {}) {
21548
21952
  if (!response.ok) {
21549
21953
  const unreachable = response.unreachable ?? (response.status === 404 ? "route-absent" : void 0);
21550
21954
  if (unreachable) {
21551
- throw new RegistryUnreachableError(`schedules lift: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
21955
+ throw new RegistryUnreachableError(`schedules register: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
21552
21956
  }
21553
- if (response.error) throw new Error(`schedules lift: ${response.error}`);
21957
+ if (response.error) throw new Error(`schedules register: ${response.error}`);
21554
21958
  const detail = response.body?.error ?? "";
21555
- throw new Error(`schedules lift: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
21959
+ throw new Error(`schedules register: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
21556
21960
  }
21557
21961
  return { repo, records, payload, response };
21558
21962
  }
@@ -21573,8 +21977,8 @@ function withArmingReport(body) {
21573
21977
  }
21574
21978
  function registerSchedulesLiftCommand(program3, deps = {}) {
21575
21979
  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) => {
21980
+ if (!schedules) throw new Error("schedules register: registerSchedulesCommands must run first \u2014 register attaches to the `schedules` command");
21981
+ 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
21982
  try {
21579
21983
  const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
21580
21984
  if (o.dryRun) {
@@ -23624,7 +24028,7 @@ function registerBoardCommands(program3) {
23624
24028
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
23625
24029
  "Multiple refs are handled as a batch and return per-item results."
23626
24030
  ]);
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) => {
24031
+ 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
24032
  try {
23629
24033
  const item = await showBoardItem({ config: await loadConfigForBoardSelector2(issueRef, o.repo), selector: issueRef, repo: o.repo, allowPartial: o.allowPartial });
23630
24034
  console.log(o.json ? JSON.stringify(item) : renderBoardItem(item));
@@ -26954,21 +27358,790 @@ function renderTally(checks) {
26954
27358
  const healthy = total - checks.filter((c) => !c.ok).length;
26955
27359
  return healthy === total ? `\u2713 all ${total} checks healthy` : `\u2713 ${healthy} of ${total} checks healthy`;
26956
27360
  }
26957
- function renderDoctorText(checks, opts) {
26958
- const lines = [];
26959
- const shown = opts.verbose ? checks : checks.filter((c) => !c.ok || c.warn);
26960
- for (const check of shown) {
26961
- lines.push(renderCheckLine(check));
26962
- if (opts.verbose) {
26963
- for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
27361
+ function doctorReportExitCode(checks) {
27362
+ return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
27363
+ }
27364
+
27365
+ // ../surfaces.json
27366
+ var surfaces_default = {
27367
+ _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.",
27368
+ schemaVersion: 7,
27369
+ productScope: "agentic-coding",
27370
+ sharedAgentCore: {
27371
+ skills: {
27372
+ ownerPath: "skills",
27373
+ sourceSurfaceId: "mmi-skills"
27374
+ },
27375
+ cli: {
27376
+ ownerPath: "cli",
27377
+ surfaceId: "mmi-cli"
27378
+ },
27379
+ hookPolicy: {
27380
+ launcherPath: "bin/mmi-hook",
27381
+ ownerPath: "scripts/hook-policy.mjs",
27382
+ runnerPath: "scripts/hook-run.mjs",
27383
+ sourceSurfaceId: "mmi-hook-policy",
27384
+ version: 1,
27385
+ windowsLauncherPath: "bin/mmi-hook.exe"
27386
+ },
27387
+ releaseMetadata: {
27388
+ ownerPath: "surfaces.json",
27389
+ bomPath: "distribution-bom.json"
26964
27390
  }
27391
+ },
27392
+ agentSurfaces: [
27393
+ {
27394
+ token: "claude",
27395
+ displayName: "Claude Code",
27396
+ lifecycle: "active",
27397
+ artifactIds: ["mmi-claude-plugin", "mmi-claude-marketplace", "mmi-skills", "mmi-hook-policy", "mmi-hooks", "mmi-cli"],
27398
+ plannedArtifactIds: [],
27399
+ assembly: {
27400
+ mode: "source-tree",
27401
+ rootPath: ".",
27402
+ sync: []
27403
+ },
27404
+ install: {
27405
+ mechanism: "marketplace",
27406
+ locator: "mmi@mutmutco"
27407
+ },
27408
+ upgrade: {
27409
+ mechanism: "reinstall",
27410
+ reload: "session"
27411
+ },
27412
+ skills: {
27413
+ delivery: "plugin",
27414
+ sourceSurfaceId: "mmi-skills",
27415
+ artifactId: "mmi-skills",
27416
+ invocation: {
27417
+ entry: "/mmi:mmi",
27418
+ any: "/mmi:<skill>"
27419
+ }
27420
+ },
27421
+ hooks: {
27422
+ adapterPath: "hooks/hooks.json",
27423
+ sourceSurfaceId: "mmi-hook-policy",
27424
+ gates: [
27425
+ { id: "command-ladder", failure: "closed" },
27426
+ { id: "vault-edit", failure: "closed" },
27427
+ { id: "secret-output", failure: "open" }
27428
+ ],
27429
+ probe: "installed-contract",
27430
+ execution: "command",
27431
+ preToolUse: "enforced",
27432
+ postToolOutput: "rewrite",
27433
+ finalOutput: "unsupported"
27434
+ },
27435
+ cli: {
27436
+ delivery: "bundled",
27437
+ artifactId: "mmi-cli"
27438
+ },
27439
+ ownership: {
27440
+ trust: "host",
27441
+ cache: "host",
27442
+ repair: "mmi-cli"
27443
+ },
27444
+ certification: {
27445
+ hostCommand: "claude",
27446
+ versionArgs: ["--version"]
27447
+ },
27448
+ enforcementCeilings: [
27449
+ "Final assistant text cannot be rewritten by the hook surface.",
27450
+ "Disabled or untrusted hooks enforce nothing."
27451
+ ]
27452
+ },
27453
+ {
27454
+ token: "codex",
27455
+ displayName: "Codex",
27456
+ lifecycle: "active",
27457
+ artifactIds: ["mmi-codex-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27458
+ plannedArtifactIds: [],
27459
+ assembly: {
27460
+ mode: "source-tree",
27461
+ rootPath: ".",
27462
+ sync: []
27463
+ },
27464
+ install: {
27465
+ mechanism: "marketplace",
27466
+ locator: "mmi@mutmutco"
27467
+ },
27468
+ upgrade: {
27469
+ mechanism: "marketplace-upgrade",
27470
+ reload: "session"
27471
+ },
27472
+ skills: {
27473
+ delivery: "plugin",
27474
+ sourceSurfaceId: "mmi-skills",
27475
+ artifactId: "mmi-skills",
27476
+ invocation: {
27477
+ entry: "$mmi:mmi",
27478
+ any: "$mmi:<skill>"
27479
+ }
27480
+ },
27481
+ hooks: {
27482
+ adapterPath: "hooks/codex-hooks.json",
27483
+ sourceSurfaceId: "mmi-hook-policy",
27484
+ gates: [
27485
+ { id: "command-ladder", failure: "closed" },
27486
+ { id: "vault-edit", failure: "closed" },
27487
+ { id: "secret-output", failure: "open" }
27488
+ ],
27489
+ probe: "installed-contract",
27490
+ execution: "node-bootstrap",
27491
+ preToolUse: "enforced",
27492
+ postToolOutput: "detect-only",
27493
+ finalOutput: "unsupported"
27494
+ },
27495
+ cli: {
27496
+ delivery: "standalone",
27497
+ artifactId: "mmi-cli"
27498
+ },
27499
+ ownership: {
27500
+ trust: "operator",
27501
+ cache: "host",
27502
+ repair: "mmi-cli"
27503
+ },
27504
+ certification: {
27505
+ hostCommand: "codex",
27506
+ versionArgs: ["--version"]
27507
+ },
27508
+ enforcementCeilings: [
27509
+ "Post-tool hooks can detect secret output but cannot rewrite it.",
27510
+ "Hosted tools and disabled or untrusted hooks bypass local enforcement."
27511
+ ]
27512
+ },
27513
+ {
27514
+ token: "kimi",
27515
+ displayName: "Kimi Code CLI",
27516
+ lifecycle: "active",
27517
+ artifactIds: ["mmi-kimi-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27518
+ plannedArtifactIds: [],
27519
+ assembly: {
27520
+ mode: "source-tree",
27521
+ rootPath: ".",
27522
+ sync: []
27523
+ },
27524
+ install: {
27525
+ mechanism: "github-source",
27526
+ locator: "https://github.com/mutmutco/MMI-Hub"
27527
+ },
27528
+ upgrade: {
27529
+ mechanism: "reinstall",
27530
+ reload: "session"
27531
+ },
27532
+ skills: {
27533
+ delivery: "plugin",
27534
+ sourceSurfaceId: "mmi-skills",
27535
+ artifactId: "mmi-skills",
27536
+ invocation: {
27537
+ entry: "/skill:mmi",
27538
+ any: "/skill:<skill>"
27539
+ }
27540
+ },
27541
+ hooks: {
27542
+ adapterPath: ".kimi-plugin/plugin.json",
27543
+ sourceSurfaceId: "mmi-hook-policy",
27544
+ gates: [
27545
+ { id: "command-ladder", failure: "closed" },
27546
+ { id: "vault-edit", failure: "closed" },
27547
+ { id: "secret-output", failure: "open" }
27548
+ ],
27549
+ probe: "installed-contract",
27550
+ execution: "inline-manifest",
27551
+ preToolUse: "enforced",
27552
+ postToolOutput: "detect-only",
27553
+ finalOutput: "unsupported"
27554
+ },
27555
+ cli: {
27556
+ delivery: "standalone",
27557
+ artifactId: "mmi-cli"
27558
+ },
27559
+ ownership: {
27560
+ trust: "host",
27561
+ cache: "host",
27562
+ repair: "operator"
27563
+ },
27564
+ certification: {
27565
+ hostCommand: "kimi",
27566
+ versionArgs: ["--version"]
27567
+ },
27568
+ enforcementCeilings: [
27569
+ "Post-tool hooks are observation-only and cannot rewrite secret output.",
27570
+ "Hook errors, timeouts, disabled plugins, and untrusted plugins enforce nothing."
27571
+ ]
27572
+ },
27573
+ {
27574
+ token: "cursor",
27575
+ displayName: "Cursor",
27576
+ lifecycle: "active",
27577
+ artifactIds: ["mmi-cursor-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
27578
+ plannedArtifactIds: [],
27579
+ assembly: {
27580
+ mode: "source-tree",
27581
+ rootPath: ".",
27582
+ sync: []
27583
+ },
27584
+ install: {
27585
+ mechanism: "github-source",
27586
+ locator: "~/.cursor/plugins/local/mmi"
27587
+ },
27588
+ upgrade: {
27589
+ mechanism: "reinstall",
27590
+ reload: "workspace"
27591
+ },
27592
+ skills: {
27593
+ delivery: "plugin",
27594
+ sourceSurfaceId: "mmi-skills",
27595
+ artifactId: "mmi-skills",
27596
+ invocation: {
27597
+ entry: "/mmi",
27598
+ any: "/<skill>"
27599
+ }
27600
+ },
27601
+ hooks: {
27602
+ adapterPath: "hooks/cursor-hooks.json",
27603
+ sourceSurfaceId: "mmi-hook-policy",
27604
+ gates: [
27605
+ { id: "command-ladder", failure: "closed" },
27606
+ { id: "vault-edit", failure: "closed" },
27607
+ { id: "secret-output", failure: "open" }
27608
+ ],
27609
+ probe: "installed-contract",
27610
+ execution: "command",
27611
+ preToolUse: "enforced",
27612
+ postToolOutput: "detect-only",
27613
+ finalOutput: "unsupported"
27614
+ },
27615
+ cli: {
27616
+ delivery: "standalone",
27617
+ artifactId: "mmi-cli"
27618
+ },
27619
+ ownership: {
27620
+ trust: "host",
27621
+ cache: "mmi-cli",
27622
+ repair: "mmi-cli"
27623
+ },
27624
+ certification: {
27625
+ hostCommand: "cursor-agent",
27626
+ versionArgs: ["--version"]
27627
+ },
27628
+ enforcementCeilings: [
27629
+ "Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
27630
+ "Final assistant output is outside Cursor hook control.",
27631
+ "Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
27632
+ ]
27633
+ },
27634
+ {
27635
+ token: "kilo",
27636
+ displayName: "Kilo Code",
27637
+ lifecycle: "active",
27638
+ artifactIds: ["mmi-kilo-plugin", "mmi-kilo-skills", "mmi-hook-policy", "mmi-cli"],
27639
+ plannedArtifactIds: [],
27640
+ assembly: {
27641
+ mode: "package-directory",
27642
+ rootPath: ".kilo-plugin",
27643
+ sync: [
27644
+ {
27645
+ mode: "directories",
27646
+ sourcePath: "skills",
27647
+ targetPath: ".kilo-plugin/skills",
27648
+ excludeNamePrefixes: ["_"]
27649
+ },
27650
+ {
27651
+ mode: "files",
27652
+ sourcePath: "scripts",
27653
+ targetPath: ".kilo-plugin/scripts",
27654
+ include: [
27655
+ "pretooluse-shell-gates.mjs",
27656
+ "vault-edit-gate.mjs",
27657
+ "secret-redact.mjs",
27658
+ "deny-gate-crash.mjs",
27659
+ "secret-echo-lint.mjs",
27660
+ "env-write-lint.mjs",
27661
+ "command-ladder-gate.mjs",
27662
+ "command-ladder-core.mjs",
27663
+ "validate-hook.mjs",
27664
+ "hook-io.mjs",
27665
+ "hook-trace.mjs",
27666
+ "edit-tool-paths.mjs",
27667
+ "throttle-core.mjs",
27668
+ "hook-policy.mjs",
27669
+ "hook-run.mjs"
27670
+ ]
27671
+ }
27672
+ ]
27673
+ },
27674
+ install: {
27675
+ mechanism: "npm",
27676
+ locator: "@mutmutco/kilo-plugin"
27677
+ },
27678
+ upgrade: {
27679
+ mechanism: "package-manager",
27680
+ reload: "session"
27681
+ },
27682
+ skills: {
27683
+ delivery: "provisioned",
27684
+ sourceSurfaceId: "mmi-skills",
27685
+ artifactId: "mmi-kilo-skills",
27686
+ invocation: {
27687
+ entry: "mmi skill via the skill tool",
27688
+ any: "skill tool"
27689
+ }
27690
+ },
27691
+ hooks: {
27692
+ adapterPath: ".kilo-plugin/server.mjs",
27693
+ sourceSurfaceId: "mmi-hook-policy",
27694
+ gates: [
27695
+ { id: "command-ladder", failure: "closed" },
27696
+ { id: "vault-edit", failure: "closed" },
27697
+ { id: "secret-output", failure: "open" }
27698
+ ],
27699
+ probe: "installed-contract",
27700
+ execution: "in-process",
27701
+ preToolUse: "enforced",
27702
+ postToolOutput: "rewrite",
27703
+ finalOutput: "rewrite"
27704
+ },
27705
+ cli: {
27706
+ delivery: "standalone",
27707
+ artifactId: "mmi-cli"
27708
+ },
27709
+ ownership: {
27710
+ trust: "host",
27711
+ cache: "host",
27712
+ repair: "mmi-cli"
27713
+ },
27714
+ certification: {
27715
+ hostCommand: "kilo",
27716
+ versionArgs: ["--version"]
27717
+ },
27718
+ enforcementCeilings: [
27719
+ "Some read, write, and web-fetch tool outputs are not rewriteable by the host.",
27720
+ "Provisioned skills and agents become discoverable only after a fresh session."
27721
+ ]
27722
+ }
27723
+ ],
27724
+ surfaces: [
27725
+ {
27726
+ id: "mmi-skills",
27727
+ classification: "capability",
27728
+ kind: "skills",
27729
+ ownerPath: "skills",
27730
+ deliveryPath: "skills",
27731
+ delivery: "plugin-install",
27732
+ applicability: "plugin-enabled agent surfaces",
27733
+ versionCoordinated: false,
27734
+ artifactIdentity: {
27735
+ kind: "sha256-tree",
27736
+ paths: ["skills"]
27737
+ },
27738
+ verify: [
27739
+ {
27740
+ command: "node",
27741
+ args: ["scripts/check-skill-payload.mjs", "--contract"],
27742
+ expected: "skill payload check: ok"
27743
+ }
27744
+ ],
27745
+ publishVisibility: "public"
27746
+ },
27747
+ {
27748
+ id: "mmi-claude-plugin",
27749
+ classification: "packaging",
27750
+ kind: "plugin",
27751
+ ownerPath: ".claude-plugin/plugin.json",
27752
+ deliveryPath: ".claude-plugin/plugin.json",
27753
+ delivery: "release",
27754
+ applicability: "Claude Code plugin marketplace",
27755
+ versionCoordinated: true,
27756
+ surfaceToken: "claude",
27757
+ versionPaths: [
27758
+ { path: ".claude-plugin/plugin.json", pointer: "version" }
27759
+ ],
27760
+ artifactIdentity: {
27761
+ kind: "sha256-tree",
27762
+ paths: [".claude-plugin/plugin.json", "skills", "hooks", "scripts", "bin"]
27763
+ },
27764
+ publishVisibility: "public"
27765
+ },
27766
+ {
27767
+ id: "mmi-codex-plugin",
27768
+ classification: "packaging",
27769
+ kind: "plugin",
27770
+ ownerPath: ".codex-plugin/plugin.json",
27771
+ deliveryPath: ".codex-plugin/plugin.json",
27772
+ delivery: "release",
27773
+ applicability: "Codex plugin marketplace; carries the MMI lifecycle hooks via hooks/codex-hooks.json (#3563)",
27774
+ versionCoordinated: true,
27775
+ surfaceToken: "codex",
27776
+ versionPaths: [
27777
+ { path: ".codex-plugin/plugin.json", pointer: "version" }
27778
+ ],
27779
+ artifactIdentity: {
27780
+ kind: "sha256-tree",
27781
+ paths: [".codex-plugin/plugin.json", "skills", "hooks/codex-hooks.json", "scripts", "bin"]
27782
+ },
27783
+ publishVisibility: "public"
27784
+ },
27785
+ {
27786
+ id: "mmi-kimi-plugin",
27787
+ classification: "packaging",
27788
+ kind: "plugin",
27789
+ ownerPath: ".kimi-plugin/plugin.json",
27790
+ deliveryPath: ".kimi-plugin/plugin.json",
27791
+ delivery: "release",
27792
+ applicability: "Kimi Code CLI plugin install (GitHub source); inline event mapping invokes the shared hook runner",
27793
+ versionCoordinated: true,
27794
+ surfaceToken: "kimi",
27795
+ versionPaths: [
27796
+ { path: ".kimi-plugin/plugin.json", pointer: "version" }
27797
+ ],
27798
+ artifactIdentity: {
27799
+ kind: "sha256-tree",
27800
+ paths: [".kimi-plugin/plugin.json", "skills", "scripts", "bin"]
27801
+ },
27802
+ publishVisibility: "public"
27803
+ },
27804
+ {
27805
+ id: "mmi-cursor-plugin",
27806
+ classification: "packaging",
27807
+ kind: "plugin",
27808
+ ownerPath: ".cursor-plugin/plugin.json",
27809
+ deliveryPath: "~/.cursor/plugins/local/mmi",
27810
+ delivery: "local-copy",
27811
+ applicability: "Cursor IDE and Cursor Agent CLI; local plugin checkout managed by mmi-cli",
27812
+ versionCoordinated: true,
27813
+ surfaceToken: "cursor",
27814
+ versionPaths: [
27815
+ { path: ".cursor-plugin/plugin.json", pointer: "version" }
27816
+ ],
27817
+ artifactIdentity: {
27818
+ kind: "sha256-tree",
27819
+ paths: [".cursor-plugin/plugin.json", "skills", "hooks/cursor-hooks.json", "scripts", "bin"]
27820
+ },
27821
+ publishVisibility: "public"
27822
+ },
27823
+ {
27824
+ id: "mmi-kilo-plugin",
27825
+ classification: "packaging",
27826
+ kind: "plugin",
27827
+ ownerPath: ".kilo-plugin/package.json",
27828
+ deliveryPath: ".kilo-plugin/package.json",
27829
+ delivery: "npm",
27830
+ applicability: "Kilo Code extension + Kilo CLI; npm package @mutmutco/kilo-plugin (kilo-p1), installed via `kilo plugin @mutmutco/kilo-plugin`",
27831
+ versionCoordinated: true,
27832
+ surfaceToken: "kilo",
27833
+ versionPaths: [
27834
+ { path: ".kilo-plugin/package.json", pointer: "version" }
27835
+ ],
27836
+ additionalPaths: [
27837
+ ".kilo-plugin/server.mjs",
27838
+ ".kilo-plugin/skills",
27839
+ ".kilo-plugin/scripts"
27840
+ ],
27841
+ artifactIdentity: {
27842
+ kind: "npm-pack",
27843
+ packagePath: ".kilo-plugin"
27844
+ },
27845
+ publishVisibility: "public"
27846
+ },
27847
+ {
27848
+ id: "mmi-kilo-skills",
27849
+ classification: "capability",
27850
+ kind: "skills",
27851
+ ownerPath: "skills",
27852
+ deliveryPath: "skills",
27853
+ delivery: "plugin-install",
27854
+ applicability: "Kilo Code (via @mutmutco/kilo-plugin provisioning to ~/.kilo/skills)",
27855
+ versionCoordinated: false,
27856
+ artifactIdentity: {
27857
+ kind: "sha256-tree",
27858
+ paths: [".kilo-plugin/skills"]
27859
+ },
27860
+ surfaceToken: "kilo",
27861
+ publishVisibility: "public"
27862
+ },
27863
+ {
27864
+ id: "mmi-claude-marketplace",
27865
+ classification: "delivery",
27866
+ kind: "marketplace",
27867
+ ownerPath: ".claude-plugin/marketplace.json",
27868
+ deliveryPath: ".claude-plugin/marketplace.json",
27869
+ delivery: "release",
27870
+ applicability: "Claude Code plugin marketplace",
27871
+ versionCoordinated: true,
27872
+ surfaceToken: "claude",
27873
+ versionPaths: [
27874
+ { path: ".claude-plugin/marketplace.json", pointer: "plugins.0.version" },
27875
+ { path: ".claude-plugin/marketplace.json", pointer: "plugins.0.displayName", template: "MMI {version}" }
27876
+ ],
27877
+ artifactIdentity: {
27878
+ kind: "sha256-tree",
27879
+ paths: [".claude-plugin/marketplace.json"]
27880
+ },
27881
+ publishVisibility: "public"
27882
+ },
27883
+ {
27884
+ id: "mmi-cli",
27885
+ classification: "capability",
27886
+ kind: "cli",
27887
+ ownerPath: "cli/package.json",
27888
+ deliveryPath: "cli/package.json",
27889
+ delivery: "npm",
27890
+ applicability: "editor-agnostic agent surfaces",
27891
+ versionCoordinated: true,
27892
+ versionPaths: [
27893
+ { path: "cli/package.json", pointer: "version" }
27894
+ ],
27895
+ additionalPaths: ["cli/dist"],
27896
+ artifactIdentity: {
27897
+ kind: "npm-pack",
27898
+ packagePath: "cli"
27899
+ },
27900
+ publishVisibility: "public"
27901
+ },
27902
+ {
27903
+ id: "mmi-cli-lock",
27904
+ classification: "packaging",
27905
+ kind: "package-lock",
27906
+ ownerPath: "cli/package-lock.json",
27907
+ deliveryPath: "cli/package-lock.json",
27908
+ delivery: "npm",
27909
+ applicability: "editor-agnostic agent surfaces",
27910
+ versionCoordinated: true,
27911
+ versionPaths: [
27912
+ { path: "cli/package-lock.json", pointer: "version" },
27913
+ { path: "cli/package-lock.json", pointer: "packages.$root.version" }
27914
+ ],
27915
+ artifactIdentity: {
27916
+ kind: "sha256-tree",
27917
+ paths: ["cli/package-lock.json"]
27918
+ },
27919
+ publishVisibility: "public"
27920
+ },
27921
+ {
27922
+ id: "mmi-cli-dist",
27923
+ classification: "packaging",
27924
+ kind: "bundle",
27925
+ ownerPath: "cli/dist/index.cjs",
27926
+ deliveryPath: "cli/dist/index.cjs",
27927
+ delivery: "npm",
27928
+ applicability: "editor-agnostic agent surfaces",
27929
+ versionCoordinated: true,
27930
+ versionPaths: [
27931
+ { path: "cli/package.json", pointer: "version" }
27932
+ ],
27933
+ additionalPaths: ["cli/dist/main.cjs"],
27934
+ prepare: [
27935
+ {
27936
+ command: "npm",
27937
+ args: ["--prefix", "cli", "run", "build"],
27938
+ inputs: ["cli/src", "cli/build.mjs", "cli/package.json", "cli/README.md", "cli/tsconfig.json"],
27939
+ outputs: ["cli/dist/index.cjs", "cli/dist/main.cjs"]
27940
+ }
27941
+ ],
27942
+ verify: [
27943
+ {
27944
+ command: "node",
27945
+ args: ["cli/dist/index.cjs", "--version"],
27946
+ expected: "{version}"
27947
+ }
27948
+ ],
27949
+ artifactIdentity: {
27950
+ kind: "sha256-tree",
27951
+ paths: ["cli/dist"]
27952
+ },
27953
+ publishVisibility: "public"
27954
+ },
27955
+ {
27956
+ id: "mmi-hook-policy",
27957
+ classification: "guard",
27958
+ kind: "hooks",
27959
+ ownerPath: "scripts/hook-policy.mjs",
27960
+ deliveryPath: "scripts/hook-policy.mjs",
27961
+ delivery: "plugin-install",
27962
+ applicability: "all five active hook-capable agent surfaces",
27963
+ versionCoordinated: false,
27964
+ artifactIdentity: {
27965
+ kind: "sha256-tree",
27966
+ paths: [
27967
+ "scripts/hook-policy.mjs",
27968
+ "scripts/hook-run.mjs",
27969
+ "scripts/pretooluse-shell-gates.mjs",
27970
+ "scripts/vault-edit-gate.mjs",
27971
+ "scripts/secret-redact.mjs",
27972
+ "scripts/deny-gate-crash.mjs",
27973
+ "scripts/hook-io.mjs",
27974
+ "scripts/hook-trace.mjs",
27975
+ "bin/mmi-hook",
27976
+ "bin/mmi-hook.exe",
27977
+ "native/windows-hook-launcher.c",
27978
+ "scripts/build-windows-hook-launcher.ps1",
27979
+ "scripts/probe-windows-hook-window.ps1"
27980
+ ]
27981
+ },
27982
+ verify: [
27983
+ {
27984
+ command: "node",
27985
+ args: ["scripts/check-hook-contract.mjs"],
27986
+ expected: "hook contract: 5 active adapters proven, 12 repositories covered"
27987
+ }
27988
+ ],
27989
+ publishVisibility: "public"
27990
+ },
27991
+ {
27992
+ id: "mmi-hooks",
27993
+ classification: "guard",
27994
+ kind: "hooks",
27995
+ ownerPath: "hooks/hooks.json",
27996
+ deliveryPath: "hooks/hooks.json",
27997
+ delivery: "plugin",
27998
+ applicability: "Claude hook-enabled agents",
27999
+ versionCoordinated: false,
28000
+ artifactIdentity: {
28001
+ kind: "sha256-tree",
28002
+ paths: ["hooks", "scripts"]
28003
+ },
28004
+ surfaceToken: "claude",
28005
+ publishVisibility: "public"
28006
+ },
28007
+ {
28008
+ id: "mmi-github-app",
28009
+ classification: "integration",
28010
+ kind: "github-app",
28011
+ ownerPath: "infra/src/github-app.ts",
28012
+ deliveryPath: "infra/dist/handler.js",
28013
+ delivery: "lambda",
28014
+ applicability: "org automation and authorization",
28015
+ versionCoordinated: true,
28016
+ versionPaths: [
28017
+ { path: "infra/package.json", pointer: "version" }
28018
+ ],
28019
+ publishVisibility: "n/a",
28020
+ deployedVersionSource: "http-header:x-hub-version"
28021
+ },
28022
+ {
28023
+ id: "mmi-slack-notify",
28024
+ classification: "integration",
28025
+ kind: "notification",
28026
+ ownerPath: "scripts/slack-post.mjs",
28027
+ deliveryPath: "scripts/slack-post.mjs",
28028
+ delivery: "github-actions",
28029
+ applicability: "master DM notifications only; chatops out by default",
28030
+ versionCoordinated: false,
28031
+ publishVisibility: "n/a"
28032
+ }
28033
+ ]
28034
+ };
28035
+
28036
+ // src/surface-doctor.ts
28037
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "kimi", "cursor", "kilo"]);
28038
+ function isActiveToken(value) {
28039
+ return TOKENS.has(value);
28040
+ }
28041
+ var DOCTOR_SURFACES = Object.freeze(
28042
+ (surfaces_default.agentSurfaces ?? []).filter((surface) => surface.lifecycle === "active" && isActiveToken(surface.token)).map((surface) => Object.freeze({
28043
+ token: surface.token,
28044
+ displayName: surface.displayName,
28045
+ artifactIds: Object.freeze([...surface.artifactIds]),
28046
+ installMechanism: surface.install.mechanism,
28047
+ installLocator: surface.install.locator,
28048
+ upgradeMechanism: surface.upgrade.mechanism,
28049
+ reload: surface.upgrade.reload,
28050
+ repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator",
28051
+ trustOwner: surface.ownership.trust === "operator" ? "operator" : "host"
28052
+ }))
28053
+ );
28054
+ function doctorSurface(token) {
28055
+ const descriptor = DOCTOR_SURFACES.find((surface) => surface.token === token);
28056
+ if (!descriptor) throw new Error(`surface registry has no active doctor descriptor for ${token}`);
28057
+ return descriptor;
28058
+ }
28059
+ function diagnoseSurface(evidence) {
28060
+ const base = {
28061
+ descriptor: evidence.descriptor,
28062
+ ...evidence.installedVersion ? { installedVersion: evidence.installedVersion } : {},
28063
+ ...evidence.releasedVersion ? { releasedVersion: evidence.releasedVersion } : {}
28064
+ };
28065
+ if (!evidence.applicable) return { ...base, state: "skipped" };
28066
+ if (evidence.repair?.attempted && !evidence.repair.ok) {
28067
+ return { ...base, state: "repair-failed", repairDetail: evidence.repair.detail };
26965
28068
  }
26966
- lines.push(renderTally(checks));
26967
- if (opts.restartPending) lines.push(RESTART_LINE);
26968
- return lines.join("\n");
28069
+ if (evidence.repair?.attempted && evidence.repair.ok) {
28070
+ return { ...base, state: "clean", repairDetail: evidence.repair.detail };
28071
+ }
28072
+ if (!evidence.installRecordPresent) return { ...base, state: "missing" };
28073
+ if (!evidence.deliveryPresent || !evidence.payloadPresent || evidence.manifest === "missing") {
28074
+ return { ...base, state: "partial" };
28075
+ }
28076
+ if (evidence.manifest === "invalid") return { ...base, state: "corrupt" };
28077
+ if (!evidence.releasedVersion) return { ...base, state: "freshness-unknown" };
28078
+ if (evidence.installedVersion && evidence.releasedVersion && compareVersions(evidence.installedVersion, evidence.releasedVersion) < 0) {
28079
+ return { ...base, state: "stale" };
28080
+ }
28081
+ return { ...base, state: "clean" };
26969
28082
  }
26970
- function doctorReportExitCode(checks) {
26971
- return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
28083
+ function reloadInstruction(descriptor) {
28084
+ const verb = descriptor.reload === "workspace" ? "reload" : "restart";
28085
+ return `${verb} ${descriptor.displayName}`;
28086
+ }
28087
+ function planSurfaceRepair(diagnosis) {
28088
+ if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
28089
+ const descriptor = diagnosis.descriptor;
28090
+ if (descriptor.repairOwner !== "mmi-cli") {
28091
+ return {
28092
+ supported: false,
28093
+ owner: "operator",
28094
+ changes: [],
28095
+ instruction: `repair is unsupported by mmi-cli; reinstall via ${descriptor.installMechanism} (${descriptor.installLocator}), then ${reloadInstruction(descriptor)}`
28096
+ };
28097
+ }
28098
+ return {
28099
+ supported: true,
28100
+ owner: "mmi-cli",
28101
+ command: "mmi-cli plugin heal",
28102
+ changes: ["replace the active MMI plugin installation", `reload via ${descriptor.reload}`],
28103
+ instruction: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`), then ${reloadInstruction(descriptor)}`
28104
+ };
28105
+ }
28106
+ function buildSurfaceDoctorCheck(diagnosis) {
28107
+ const { descriptor, state } = diagnosis;
28108
+ const plan = planSurfaceRepair(diagnosis);
28109
+ const versions = diagnosis.installedVersion && diagnosis.releasedVersion ? compareVersions(diagnosis.installedVersion, diagnosis.releasedVersion) === 0 ? diagnosis.installedVersion : `${diagnosis.installedVersion} \u2192 ${diagnosis.releasedVersion}` : diagnosis.installedVersion;
28110
+ const detailByState = {
28111
+ skipped: "skipped \u2014 host not active",
28112
+ clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
28113
+ "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
28114
+ stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
28115
+ missing: "missing \u2014 no install record",
28116
+ corrupt: "corrupt \u2014 installed manifest is unreadable",
28117
+ partial: "partial \u2014 one or more delivery artifacts are absent",
28118
+ "repair-failed": `repair failed${diagnosis.repairDetail ? ` \u2014 ${diagnosis.repairDetail}` : ""}`
28119
+ };
28120
+ return {
28121
+ id: `${descriptor.token}-plugin`,
28122
+ surface: descriptor.token,
28123
+ state,
28124
+ ok: state === "clean" || state === "skipped",
28125
+ ...state === "freshness-unknown" ? { reportOnly: true } : {},
28126
+ label: `${descriptor.displayName} plugin`,
28127
+ detail: detailByState[state],
28128
+ ...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
28129
+ verbose: [
28130
+ // The three numbers the legacy builder used to print. They belong here now that this is the only
28131
+ // plugin row, and a report that names a state without naming the versions behind it is not evidence.
28132
+ `installed: ${diagnosis.installedVersion ?? "(none)"}`,
28133
+ `released: ${diagnosis.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`,
28134
+ `state: ${state}`,
28135
+ `registry surface: ${descriptor.token}`,
28136
+ `install: ${descriptor.installMechanism} (${descriptor.installLocator})`,
28137
+ `repair owner: ${descriptor.repairOwner}`,
28138
+ `artifacts: ${descriptor.artifactIds.join(", ")}`,
28139
+ ...diagnosis.repairDetail ? [`heal: ${diagnosis.repairDetail}`] : []
28140
+ ]
28141
+ };
28142
+ }
28143
+ function surfaceRestartAction(descriptor) {
28144
+ return reloadInstruction(descriptor);
26972
28145
  }
26973
28146
 
26974
28147
  // src/doctor-clean.ts
@@ -27061,75 +28234,23 @@ function checkWorktreeRoots(probe) {
27061
28234
  verbose: evidence
27062
28235
  };
27063
28236
  }
27064
- function pluginHealTrigger(probe) {
27065
- if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
27066
- const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
27067
- return behind ? "behind" : null;
27068
- }
27069
- function checkClaudePlugin(probe) {
27070
- const { installed, released, guardState } = probe;
27071
- 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";
27075
- const evidence = [
27076
- `installed: ${installed ?? "(none)"}`,
27077
- // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
27078
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
27079
- `resolvable: ${guardState}`
27080
- ];
27081
- const trigger = pluginHealTrigger(probe);
27082
- if (trigger === "unresolved") {
27083
- return {
27084
- id,
27085
- ok: false,
27086
- 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}`,
27089
- verbose: evidence
27090
- };
27091
- }
27092
- if (trigger === "behind") {
27093
- return {
27094
- id,
27095
- ok: false,
27096
- label,
27097
- detail: `${installed} \u2192 ${released}`,
27098
- // Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
27099
- // fallback, not the first resort (#3282).
27100
- fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall it, then ${restart}`,
27101
- verbose: evidence
27102
- };
27103
- }
27104
- if (!released) {
27105
- return {
27106
- id,
27107
- ok: false,
27108
- reportOnly: true,
27109
- label,
27110
- detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
27111
- fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
27112
- verbose: evidence
27113
- };
27114
- }
27115
- return { id, ok: true, label, ...installed ? { detail: installed } : {}, verbose: evidence };
27116
- }
27117
- function checkCodexHookTrust(probe) {
28237
+ function checkCodexHookTrust(probe, displayName = "Codex") {
27118
28238
  if (!probe?.applicable) return null;
27119
28239
  const evidence = [
27120
28240
  `stored approval rows: ${probe.trustedCount}/${probe.requiredCount}`,
27121
28241
  "current command hashes: verify interactively in /hooks"
27122
28242
  ];
28243
+ const label = `${displayName} hook trust`;
27123
28244
  if (probe.trusted) {
27124
- return { id: "codex-hook-trust", ok: true, label: "Codex hook trust", detail: "trusted", verbose: evidence };
28245
+ return { id: "codex-hook-trust", ok: true, label, detail: "trusted", verbose: evidence };
27125
28246
  }
27126
28247
  return {
27127
28248
  id: "codex-hook-trust",
27128
28249
  ok: true,
27129
28250
  warn: true,
27130
- label: "Codex hook trust",
28251
+ label,
27131
28252
  detail: probe.requiredCount > 0 ? `${probe.trustedCount}/${probe.requiredCount} approval rows present \u2014 current hashes unverified` : "hook bundle could not be verified",
27132
- fix: probe.requiredCount > 0 ? "Codex does not allow silent hook approval; run `/hooks`, review the MMI commands, and trust them" : "run `mmi-cli plugin heal`, restart Codex, then review and trust MMI under `/hooks`",
28253
+ fix: probe.requiredCount > 0 ? `${displayName} does not allow silent hook approval; run \`/hooks\`, review the MMI commands, and trust them` : `run \`mmi-cli plugin heal\`, restart ${displayName}, then review and trust MMI under \`/hooks\``,
27133
28254
  verbose: evidence
27134
28255
  };
27135
28256
  }
@@ -27354,6 +28475,12 @@ function gcReapable(plan) {
27354
28475
  async function runDoctorClean(opts, io, deps) {
27355
28476
  const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
27356
28477
  const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
28478
+ const lane = {
28479
+ banner: Boolean(opts.banner),
28480
+ fast: Boolean(opts.fast),
28481
+ preflight: Boolean(opts.preflight),
28482
+ full: !opts.fast && !opts.banner && !opts.preflight
28483
+ };
27357
28484
  const probeReleased = !opts.fast || Boolean(opts.self);
27358
28485
  const probeAws = !opts.fast && !opts.banner;
27359
28486
  const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
@@ -27368,91 +28495,69 @@ 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();
27372
- const pluginSurface = deps.pluginSurface?.() ?? "claude-cli";
27373
- const codexSurface = pluginSurface === "codex";
28498
+ const registryEvidence = deps.surfaceEvidence(isOrgRepo);
28499
+ const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : "restart the agent host";
27374
28500
  const checks = [];
27375
28501
  let restartPending = false;
27376
- checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
27377
- if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
27378
- checks.push(...checkGithubPools(await deps.githubPools()));
27379
- }
27380
- const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
27381
- if (aws) checks.push(aws);
27382
- checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
27383
- if (isOrgRepo) {
27384
- const current = deps.readGitignore();
27385
- const gi = planGitignore(current);
27386
- const giEvidence = [
27387
- `.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
27388
- `managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
27389
- ];
27390
- if (gi.ok) {
27391
- checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
27392
- } else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
27393
- checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
27394
- restartPending = true;
27395
- } else {
27396
- checks.push({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
27397
- }
27398
- }
27399
- const releasedNote = deps.releasedVersionNote?.();
27400
- const pluginProbe = {
27401
- installed,
27402
- released,
27403
- guardState: deps.pluginGuardState(isOrgRepo),
27404
- releasedNote,
27405
- surface: pluginSurface
28502
+ const streamed = /* @__PURE__ */ new Set();
28503
+ const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
28504
+ const emitNow = (check) => {
28505
+ checks.push(check);
28506
+ if (opts.json || !worthPrinting(check)) return;
28507
+ streamed.add(check);
28508
+ io.log(renderCheckLine(check));
28509
+ if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
28510
+ };
28511
+ const healIntent = (line) => {
28512
+ if (!opts.json) io.log(`\u21BB ${line}`);
27406
28513
  };
27407
- const healTrigger = pluginHealTrigger(pluginProbe);
28514
+ const healStep = (message) => {
28515
+ if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
28516
+ };
28517
+ const releasedNote = deps.releasedVersionNote?.();
27408
28518
  let pluginHealed = false;
27409
- if (applyEnv && deps.healPlugin && healTrigger) {
27410
- const heal = await deps.healPlugin();
27411
- 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;
27439
- } else {
27440
- const plugin = checkClaudePlugin(pluginProbe);
27441
- if (plugin) {
27442
- checks.push(plugin);
27443
- if (!plugin.ok) restartPending = true;
28519
+ async function runPluginRow() {
28520
+ if (!registryEvidence) return;
28521
+ const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
28522
+ const repair = planSurfaceRepair(diagnosis);
28523
+ if (applyEnv && deps.healPlugin && repair?.supported) {
28524
+ const { descriptor } = registryEvidence;
28525
+ healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
28526
+ const heal = await deps.healPlugin(healStep);
28527
+ pluginHealed = heal.ok;
28528
+ const row = buildSurfaceDoctorCheck(diagnoseSurface({
28529
+ ...registryEvidence,
28530
+ releasedVersion: released,
28531
+ repair: { attempted: true, ok: heal.ok, detail: heal.detail }
28532
+ }));
28533
+ if (heal.skipped) {
28534
+ row.reportOnly = true;
28535
+ row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
28536
+ }
28537
+ emitNow(row);
28538
+ if (!heal.skipped) restartPending = true;
28539
+ } else if (diagnosis.state !== "skipped") {
28540
+ const row = buildSurfaceDoctorCheck(diagnosis);
28541
+ emitNow(row);
28542
+ if (!row.ok && repair?.supported) restartPending = true;
28543
+ }
28544
+ if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
28545
+ const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
28546
+ if (trust) emitNow(trust);
28547
+ }
28548
+ }
28549
+ async function runCliRow() {
28550
+ const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
28551
+ const cliReport = buildVersionLagReport(cliInput);
28552
+ if (!(applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm")) {
28553
+ const cli = checkCliVersion(cliInput, releasedNote);
28554
+ if (cli) emitNow(cli);
28555
+ return;
27444
28556
  }
27445
- }
27446
- if (codexSurface && (pluginProbe.guardState === "healthy" || pluginHealed)) {
27447
- const trust = checkCodexHookTrust(deps.pluginTrustState?.());
27448
- if (trust) checks.push(trust);
27449
- }
27450
- const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
27451
- const cliReport = buildVersionLagReport(cliInput);
27452
- if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
27453
- const heal = await deps.updateCli(cliReport.releasedVersion);
28557
+ healIntent(`mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} via npm install -g`);
28558
+ const heal = await deps.updateCli(cliReport.releasedVersion, healStep);
27454
28559
  const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
27455
- checks.push(heal.ok ? {
28560
+ emitNow(heal.ok ? {
27456
28561
  id: "cli-version",
27457
28562
  ok: true,
27458
28563
  label: "mmi-cli",
@@ -27468,17 +28573,47 @@ async function runDoctorClean(opts, io, deps) {
27468
28573
  fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
27469
28574
  verbose: healEvidence
27470
28575
  });
27471
- } else {
27472
- const cli = checkCliVersion(cliInput, releasedNote);
27473
- if (cli) checks.push(cli);
27474
28576
  }
27475
- checks.push(checkPluginCache(deps.pluginCache()));
27476
- if (deps.sessionPayload) {
28577
+ async function runGithubAuthRow() {
28578
+ emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
28579
+ }
28580
+ async function runGithubPoolsRows() {
28581
+ for (const pool of checkGithubPools(await deps.githubPools())) emitNow(pool);
28582
+ }
28583
+ async function runAwsRow() {
28584
+ const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
28585
+ if (aws) emitNow(aws);
28586
+ }
28587
+ async function runRepoWorktreesRow() {
28588
+ emitNow(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
28589
+ }
28590
+ async function runGitignoreRow() {
28591
+ const current = deps.readGitignore();
28592
+ const gi = planGitignore(current);
28593
+ const giEvidence = [
28594
+ `.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
28595
+ `managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
28596
+ ];
28597
+ if (gi.ok) {
28598
+ emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
28599
+ } else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
28600
+ emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
28601
+ restartPending = true;
28602
+ } else {
28603
+ emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
28604
+ }
28605
+ }
28606
+ async function runPluginCacheRow() {
28607
+ emitNow(checkPluginCache(deps.pluginCache()));
28608
+ }
28609
+ async function runSessionPayloadRow() {
27477
28610
  const payload = checkSessionPayload(deps.sessionPayload());
27478
- if (payload) checks.push(payload);
28611
+ if (payload) emitNow(payload);
28612
+ }
28613
+ async function runMarketplaceRows() {
28614
+ for (const row of deps.marketplaceRows()) emitNow(row);
27479
28615
  }
27480
- if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
27481
- if (!opts.fast && !opts.banner && !opts.preflight) {
28616
+ async function runSchedulesRow() {
27482
28617
  const probe = await deps.schedulesNotebook().catch((e) => ({
27483
28618
  armed: 0,
27484
28619
  live: 0,
@@ -27487,32 +28622,32 @@ async function runDoctorClean(opts, io, deps) {
27487
28622
  drift: []
27488
28623
  }));
27489
28624
  const sched = checkSchedules(probe);
27490
- if (sched) checks.push(sched);
28625
+ if (sched) emitNow(sched);
27491
28626
  }
27492
- if (!opts.fast && !opts.banner && !opts.preflight && deps.redactorLiveness) {
28627
+ async function runRedactorRow() {
27493
28628
  const redactor = checkRedactorLiveness(deps.redactorLiveness());
27494
- if (redactor) checks.push(redactor);
28629
+ if (redactor) emitNow(redactor);
27495
28630
  }
27496
- if (!opts.fast && !opts.banner && !opts.preflight && deps.docsAudit) {
28631
+ async function runDocsAuditRow() {
27497
28632
  const probe = await deps.docsAudit().catch((e) => ({
27498
28633
  armed: true,
27499
28634
  ok: false,
27500
28635
  detail: `docs-audit probe failed \u2014 ${e.message}`
27501
28636
  }));
27502
28637
  const docsAudit2 = checkDocsAudit(probe);
27503
- if (docsAudit2) checks.push(docsAudit2);
28638
+ if (docsAudit2) emitNow(docsAudit2);
27504
28639
  }
27505
- if (!opts.fast && !opts.banner && !opts.preflight && deps.worktreeRoots) {
28640
+ async function runWorktreeRootsRow() {
27506
28641
  const probe = await deps.worktreeRoots().catch(() => void 0);
27507
28642
  const roots = checkWorktreeRoots(probe);
27508
- if (roots) checks.push(roots);
28643
+ if (roots) emitNow(roots);
27509
28644
  }
27510
- if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
28645
+ async function runTrainSyncRow() {
27511
28646
  try {
27512
- checks.push(checkTrainSync(await deps.syncTrain()));
28647
+ emitNow(checkTrainSync(await deps.syncTrain()));
27513
28648
  } catch (e) {
27514
28649
  const message = e instanceof Error ? e.message : String(e);
27515
- checks.push({
28650
+ emitNow({
27516
28651
  ok: false,
27517
28652
  id: "train-branches",
27518
28653
  label: "train branches",
@@ -27520,6 +28655,8 @@ async function runDoctorClean(opts, io, deps) {
27520
28655
  verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
27521
28656
  });
27522
28657
  }
28658
+ }
28659
+ async function runHousekeeperRows() {
27523
28660
  const repoRoot2 = await deps.repoRoot();
27524
28661
  try {
27525
28662
  const plan = await deps.gcPlan("origin", 200);
@@ -27530,7 +28667,7 @@ async function runDoctorClean(opts, io, deps) {
27530
28667
  if (r.removedBranches.length || r.removedRemoteBranches.length) detailParts.push(`${r.removedBranches.length + r.removedRemoteBranches.length} merged branches`);
27531
28668
  if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
27532
28669
  if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
27533
- checks.push({
28670
+ emitNow({
27534
28671
  id: "branches-worktrees",
27535
28672
  ok: true,
27536
28673
  label: "branches / worktrees",
@@ -27553,7 +28690,7 @@ async function runDoctorClean(opts, io, deps) {
27553
28690
  ...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
27554
28691
  ...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
27555
28692
  ];
27556
- checks.push(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
28693
+ emitNow(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
27557
28694
  id: "branches-worktrees",
27558
28695
  ok: false,
27559
28696
  label: "branches / worktrees",
@@ -27571,35 +28708,65 @@ async function runDoctorClean(opts, io, deps) {
27571
28708
  if (applyRepo && scratch.plan.safeAuto.length > 0) {
27572
28709
  const applied = deps.executeScratchGc(repoRoot2, { apply: true });
27573
28710
  const pruned = applied.applied?.pruned.length ?? 0;
27574
- checks.push({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
28711
+ emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
27575
28712
  if (pruned) restartPending = true;
27576
28713
  } else {
27577
- checks.push(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
27578
- }
27579
- }
28714
+ emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
28715
+ }
28716
+ }
28717
+ const table = [
28718
+ // Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
28719
+ // below is being measured with (#3954).
28720
+ { id: "plugin", when: true, run: runPluginRow },
28721
+ { id: "cli-version", when: true, run: runCliRow },
28722
+ { id: "github-auth", when: true, run: runGithubAuthRow },
28723
+ { id: "github-pools", when: lane.full, run: runGithubPoolsRows },
28724
+ { id: "aws-identity", when: true, run: runAwsRow },
28725
+ { id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
28726
+ { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
28727
+ { id: "plugin-cache", when: true, run: runPluginCacheRow },
28728
+ { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
28729
+ { id: "marketplace", when: true, run: runMarketplaceRows },
28730
+ { id: "schedules", when: lane.full, run: runSchedulesRow },
28731
+ { id: "redactor-liveness", when: lane.full, run: runRedactorRow },
28732
+ { id: "docs-audit", when: lane.full, run: runDocsAuditRow },
28733
+ { id: "worktree-roots", when: lane.full, run: runWorktreeRootsRow },
28734
+ // The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
28735
+ // and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
28736
+ { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
28737
+ { id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
28738
+ ];
28739
+ for (const entry of table) if (entry.when) await entry.run();
27580
28740
  const exitCode = doctorReportExitCode(checks);
27581
28741
  if (opts.json) {
27582
28742
  const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
27583
28743
  io.log(JSON.stringify({
27584
28744
  checks: payload,
27585
28745
  restartPending,
27586
- ...restartPending ? { restartAction: codexSurface ? "restart Codex" : "restart Claude" } : {},
28746
+ ...restartPending ? { restartAction } : {},
27587
28747
  exitCode
27588
28748
  }, null, 2));
27589
28749
  return exitCode;
27590
28750
  }
27591
28751
  if (opts.banner) {
27592
- const actionable = checks.filter((c) => !c.ok || c.warn);
28752
+ const actionable = checks.filter((c) => (!c.ok || c.warn) && !streamed.has(c));
27593
28753
  for (const c of actionable) {
27594
28754
  io.log(renderReport([c], { restartPending: false }));
27595
28755
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
27596
28756
  }
27597
- if (restartPending) io.log(codexSurface ? "\u21BB Restart Codex to finish." : RESTART_LINE);
28757
+ if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
27598
28758
  return 0;
27599
28759
  }
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);
28760
+ const rest = checks.filter((c) => !streamed.has(c));
28761
+ const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
28762
+ const lines = [];
28763
+ for (const check of shown) {
28764
+ lines.push(renderCheckLine(check));
28765
+ if (opts.verbose) for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
28766
+ }
28767
+ lines.push(renderTally(checks));
28768
+ if (restartPending) lines.push(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
28769
+ io.log(lines.join("\n"));
27603
28770
  return exitCode;
27604
28771
  }
27605
28772
 
@@ -27701,17 +28868,43 @@ function installedClaudePluginVersion() {
27701
28868
  return void 0;
27702
28869
  }
27703
28870
  }
27704
- function installedActivePluginVersion(surface = detectSurface(process.env)) {
27705
- if (surface !== "codex") return installedClaudePluginVersion();
28871
+ function manifestVersion(path2) {
28872
+ try {
28873
+ const manifest = JSON.parse((0, import_node_fs33.readFileSync)(path2, "utf8"));
28874
+ return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
28875
+ } catch {
28876
+ return void 0;
28877
+ }
28878
+ }
28879
+ function installedSurfacePluginVersion(surface) {
28880
+ const token = surfaceToken(surface);
28881
+ if (token === "kilo") {
28882
+ try {
28883
+ const stamp = (0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
28884
+ return stamp || void 0;
28885
+ } catch {
28886
+ return void 0;
28887
+ }
28888
+ }
28889
+ if (token === "cursor") {
28890
+ return manifestVersion((0, import_node_path32.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
28891
+ }
28892
+ if (token === "kimi") {
28893
+ return manifestVersion((0, import_node_path32.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
28894
+ }
28895
+ if (token === "claude") return installedClaudePluginVersion();
28896
+ if (token !== "codex") return void 0;
27706
28897
  try {
27707
28898
  const raw = process.platform === "win32" ? (0, import_node_child_process14.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
27708
28899
  encoding: "utf8",
27709
28900
  stdio: ["ignore", "pipe", "ignore"],
27710
- timeout: 15e3
28901
+ timeout: 15e3,
28902
+ windowsHide: true
27711
28903
  }) : (0, import_node_child_process14.execFileSync)("codex", ["plugin", "list", "--json"], {
27712
28904
  encoding: "utf8",
27713
28905
  stdio: ["ignore", "pipe", "ignore"],
27714
- timeout: 15e3
28906
+ timeout: 15e3,
28907
+ windowsHide: true
27715
28908
  });
27716
28909
  const parsed = JSON.parse(raw);
27717
28910
  const plugin = parsed.installed?.find((entry) => entry.pluginId === MMI_PLUGIN_ID2 && entry.installed === true && entry.enabled === true);
@@ -27720,6 +28913,9 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
27720
28913
  return void 0;
27721
28914
  }
27722
28915
  }
28916
+ function installedActivePluginVersion(surface = detectSurface(process.env)) {
28917
+ return installedSurfacePluginVersion(surface);
28918
+ }
27723
28919
  function worktreeRootSync() {
27724
28920
  try {
27725
28921
  const out = (0, import_node_child_process14.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -27857,6 +29053,29 @@ function mmiDoctorDeps(opts = {}) {
27857
29053
  const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
27858
29054
  let notebook;
27859
29055
  const notebookOnce = () => notebook ??= fetchNotebook();
29056
+ let surfaceEvidence;
29057
+ let surfaceEvidenceRead = false;
29058
+ const surfaceEvidenceOnce = (isOrgRepo) => {
29059
+ if (surfaceEvidenceRead) return surfaceEvidence;
29060
+ surfaceEvidenceRead = true;
29061
+ const runtimeSurface = detectSurface(process.env);
29062
+ const token = surfaceToken(runtimeSurface);
29063
+ if (!token || token === "opencode") return void 0;
29064
+ const descriptor = doctorSurface(token);
29065
+ const snapshot = snapshotPluginGuardInput(runtimeSurface, isOrgRepo);
29066
+ const installedVersion = installedSurfacePluginVersion(runtimeSurface);
29067
+ surfaceEvidence = {
29068
+ descriptor,
29069
+ applicable: isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
29070
+ installRecordPresent: snapshot.installRecordPresent,
29071
+ deliveryPresent: snapshot.marketplaceClonePresent,
29072
+ payloadPresent: snapshot.pluginCachePresent,
29073
+ manifest: installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing",
29074
+ guardState: buildPluginGuardDecision(snapshot).state,
29075
+ ...installedVersion ? { installedVersion } : {}
29076
+ };
29077
+ return surfaceEvidence;
29078
+ };
27860
29079
  const docsJanitorArmedAt = async (repo) => {
27861
29080
  const wanted = docsJanitorScheduleId(repo);
27862
29081
  const { entries } = await notebookOnce();
@@ -27868,9 +29087,7 @@ function mmiDoctorDeps(opts = {}) {
27868
29087
  githubRepoReach: githubRepoReachProbe,
27869
29088
  awsCallerArn,
27870
29089
  isOrgRepo: () => isOrgRepoRoot(),
27871
- installedPluginVersion: installedActivePluginVersion,
27872
- pluginGuardState: activePluginGuardState,
27873
- pluginSurface: () => detectSurface(process.env),
29090
+ surfaceEvidence: surfaceEvidenceOnce,
27874
29091
  pluginTrustState: () => codexHookTrustState(),
27875
29092
  releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
27876
29093
  releasedVersionNote: throttled ? throttled.note : void 0,
@@ -27879,12 +29096,13 @@ function mmiDoctorDeps(opts = {}) {
27879
29096
  // marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
27880
29097
  // rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
27881
29098
  // carries a CLI shim, so a concurrent npm global install races the same PATH surface.
27882
- updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
29099
+ updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
27883
29100
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
27884
29101
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
27885
- healPlugin: () => {
29102
+ healPlugin: (onStep) => {
27886
29103
  const surface = detectSurface(process.env);
27887
- return withEnvHealLock(`${surface === "codex" ? "Codex" : "Claude"} plugin reinstall`, () => healActivePluginForDoctor(surface));
29104
+ const host = surfaceEvidenceOnce(true)?.descriptor.displayName ?? surface;
29105
+ return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface, onStep));
27888
29106
  },
27889
29107
  currentCliVersion: resolveClientVersion,
27890
29108
  readGitignore,
@@ -29344,13 +30562,25 @@ function surfaceWaived() {
29344
30562
  }
29345
30563
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
29346
30564
  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.
30565
+ 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"),
30566
+ // --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
30567
+ // and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
30568
+ // is not a plan of the mutation that will run (#3914).
29351
30569
  async (opts) => {
29352
30570
  const type = resolveCreateType(opts.type, "issue create", opts.label);
29353
30571
  const priority = resolveCreatePriority(opts.priority, "issue create");
30572
+ let title;
30573
+ try {
30574
+ title = await resolveIssueTitle(
30575
+ { title: opts.title, titleFile: opts.titleFile },
30576
+ { readFile: import_promises10.readFile, readStdin }
30577
+ );
30578
+ } catch (e) {
30579
+ return fail(
30580
+ `issue create: ${e.message}`,
30581
+ e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0
30582
+ );
30583
+ }
29354
30584
  const planLabels = opts.label ?? [];
29355
30585
  const clash = conflictingSurfaceInputs(opts.surface, planLabels);
29356
30586
  if (clash) fail(clash.message, clash.payload);
@@ -29368,7 +30598,7 @@ withExamples(mutating(
29368
30598
  return {
29369
30599
  command: "issue create",
29370
30600
  type,
29371
- title: opts.title ?? opts.titleFile,
30601
+ title,
29372
30602
  priority,
29373
30603
  repo: opts.repo,
29374
30604
  ...surface ? { surface } : {}
@@ -30271,6 +31501,12 @@ function renderReleaseResume(r) {
30271
31501
  if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
30272
31502
  return lines.join("\n");
30273
31503
  }
31504
+ function renderReleaseAbort(r) {
31505
+ return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
31506
+ }
31507
+ function renderReleasePublishRetry(r) {
31508
+ return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
31509
+ }
30274
31510
  function renderRcandResume(r) {
30275
31511
  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
31512
  }
@@ -30315,9 +31551,11 @@ async function resolveRcandPlanTargets() {
30315
31551
  for (const commandName of ["rcand", "release"]) {
30316
31552
  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
31553
  const RELEASE_ONLY_FLAGS = [
30318
- { flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
31554
+ { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
30319
31555
  { 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)" }
31556
+ { 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)" },
31557
+ { flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" },
31558
+ { flags: "--retry-publish <run-id>", description: "with --apply, retry failed jobs of one proven Hub publish release run (#3949)" }
30321
31559
  ];
30322
31560
  for (const f of RELEASE_ONLY_FLAGS) {
30323
31561
  if (commandName === "release") {
@@ -30341,6 +31579,45 @@ for (const commandName of ["rcand", "release"]) {
30341
31579
  if (o.announceSummaryFile && commandName !== "release") {
30342
31580
  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
31581
  }
31582
+ if (o.abort && commandName !== "release") {
31583
+ return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
31584
+ }
31585
+ if (o.retryPublish && commandName !== "release") {
31586
+ return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli release --retry-publish <run-id> --apply --watch`);
31587
+ }
31588
+ if (o.retryPublish) {
31589
+ if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
31590
+ if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
31591
+ if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
31592
+ if (o.repo) {
31593
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
31594
+ if (!guard.ok) return fail(`release: ${guard.message}`);
31595
+ }
31596
+ const runId = Number.parseInt(o.retryPublish, 10);
31597
+ try {
31598
+ const result = await runReleasePublishRetry(trainApplyDeps(), runId, { approved: true, watch: o.watch });
31599
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleasePublishRetry(result));
31600
+ } catch (e) {
31601
+ return failGraceful(`release --retry-publish: ${e.message}`);
31602
+ }
31603
+ }
31604
+ if (o.abort) {
31605
+ if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
31606
+ if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
31607
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
31608
+ return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
31609
+ }
31610
+ if (o.repo) {
31611
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli release --abort --apply");
31612
+ if (!guard.ok) return fail(`release: ${guard.message}`);
31613
+ }
31614
+ try {
31615
+ const result = await runReleaseAbort(trainApplyDeps(), { approved: true });
31616
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseAbort(result));
31617
+ } catch (e) {
31618
+ return failGraceful(`release --abort: ${e.message}`);
31619
+ }
31620
+ }
30344
31621
  if (o.resume) {
30345
31622
  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
31623
  try {
@@ -30362,6 +31639,24 @@ for (const commandName of ["rcand", "release"]) {
30362
31639
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
30363
31640
  if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
30364
31641
  }
31642
+ if (o.apply && commandName === "release" && (await resolveRepo())?.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) {
31643
+ if (!o.announceSummaryFile) {
31644
+ return fail(
31645
+ "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"
31646
+ );
31647
+ }
31648
+ let summaryLines;
31649
+ try {
31650
+ summaryLines = summaryFileLines(await (0, import_promises10.readFile)(o.announceSummaryFile, "utf8"));
31651
+ } catch (e) {
31652
+ return fail(`release: could not read --announce-summary-file ${o.announceSummaryFile}: ${e.message}`);
31653
+ }
31654
+ if (summaryLines.length < 3 || summaryLines.length > 6) {
31655
+ return fail(
31656
+ `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${summaryLines.length})`
31657
+ );
31658
+ }
31659
+ }
30365
31660
  if (o.apply) {
30366
31661
  try {
30367
31662
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);