@mutmutco/cli 3.72.0 → 3.74.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 +1 -1
  2. package/dist/main.cjs +327 -89
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The command-line engine for MMI Future org tooling. It delivers the org-managed `.gitignore` block, reads and claims GitHub Project work, and exposes the model-agnostic commands used by the MMI plugin and non-Claude agents. (Personal agent guides — `AGENTS.md` / `CLAUDE.md` — are developer-owned and gitignored; the CLI does not deliver, overwrite, or fan them out — the whole-spine fanout is retired.)
4
4
 
5
- This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub Claude Code plugin distribution version (Claude-only since #2741; the release train bumps both in lockstep).
5
+ This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub plugin distribution version — the release train bumps the CLI and the Claude, Codex, and Kimi plugin manifests in lockstep.
6
6
 
7
7
  The CLI carries the org **Hub endpoint** intrinsically (override with the `MMI_HUB_URL` env var), so a product repo needs **no committed control-plane config** to reach the Hub — board coords, deploy coordinates, OAuth, and the secrets layout are all discovered from the Hub registry at runtime.
8
8
 
package/dist/main.cjs CHANGED
@@ -3417,7 +3417,7 @@ var program = new Command();
3417
3417
  // src/index.ts
3418
3418
  var import_promises8 = require("node:fs/promises");
3419
3419
  var import_node_fs33 = require("node:fs");
3420
- var import_node_child_process14 = require("node:child_process");
3420
+ var import_node_child_process15 = require("node:child_process");
3421
3421
 
3422
3422
  // src/cli-shared.ts
3423
3423
  var import_node_child_process3 = require("node:child_process");
@@ -13078,8 +13078,8 @@ function selectPrunablePluginVersions(names, currentVersion) {
13078
13078
  }
13079
13079
  return newestFirst.filter((v) => !keep.has(v));
13080
13080
  }
13081
- function pluginCacheRoot(home) {
13082
- return `${home}/.claude/plugins/cache/mutmutco/mmi`;
13081
+ function pluginCacheRootForConfig(configRoot) {
13082
+ return `${configRoot}/plugins/cache/mutmutco/mmi`;
13083
13083
  }
13084
13084
  function runningPluginVersion(env, cliVersion) {
13085
13085
  const root = env.CLAUDE_PLUGIN_ROOT?.trim();
@@ -13087,11 +13087,11 @@ function runningPluginVersion(env, cliVersion) {
13087
13087
  if (leaf && isVersionDirName(leaf)) return leaf;
13088
13088
  return cliVersion && isVersionDirName(cliVersion) ? cliVersion : void 0;
13089
13089
  }
13090
- function pluginCacheStagingRoot(home) {
13091
- return `${home}/.claude/plugins/cache`;
13090
+ function pluginCacheStagingRootForConfig(configRoot) {
13091
+ return `${configRoot}/plugins/cache`;
13092
13092
  }
13093
- function installedPluginsPath(home) {
13094
- return `${home}/.claude/plugins/installed_plugins.json`;
13093
+ function installedPluginsPathForConfig(configRoot) {
13094
+ return `${configRoot}/plugins/installed_plugins.json`;
13095
13095
  }
13096
13096
  var STAGING_DIR = /^temp_git_\d{13}_[a-z0-9]+$/i;
13097
13097
  function isStagingDirName(name) {
@@ -13163,13 +13163,17 @@ function humanStagingAge(ms) {
13163
13163
  return `${Math.floor(hours / 24)}d old`;
13164
13164
  }
13165
13165
  function buildPluginCachePlan(home, running, deps, opts = {}) {
13166
- const cacheRoot = pluginCacheRoot(home);
13167
- const stagingRoot = pluginCacheStagingRoot(home);
13166
+ const configRoot = opts.configRoot ?? `${home}/.claude`;
13167
+ const cacheRoot = pluginCacheRootForConfig(configRoot);
13168
+ const stagingRoot = pluginCacheStagingRootForConfig(configRoot);
13168
13169
  let stagingEntries;
13169
- try {
13170
- stagingEntries = deps.listStagingDirs(stagingRoot);
13171
- } catch {
13172
- stagingEntries = [];
13170
+ if (opts.includeStaging === false) stagingEntries = [];
13171
+ else {
13172
+ try {
13173
+ stagingEntries = deps.listStagingDirs(stagingRoot);
13174
+ } catch {
13175
+ stagingEntries = [];
13176
+ }
13173
13177
  }
13174
13178
  let referenced;
13175
13179
  try {
@@ -16579,8 +16583,10 @@ function runDocRefs(root, deps = {}) {
16579
16583
  const listDocs = deps.listDocs ?? defaultListDocs;
16580
16584
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
16581
16585
  const commandPaths = deps.commandPaths ?? null;
16586
+ const walked = listDocs(root);
16587
+ const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
16582
16588
  const docs2 = Object.fromEntries(
16583
- listDocs(root).map((rel) => [rel, readFile7((0, import_node_path16.join)(root, rel))]).filter(([, body]) => body != null)
16589
+ walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile7((0, import_node_path16.join)(root, rel))]).filter(([, body]) => body != null)
16584
16590
  );
16585
16591
  const findings = [
16586
16592
  ...checkPins(root, readFile7, docs2).findings,
@@ -21605,7 +21611,7 @@ var import_node_fs27 = require("node:fs");
21605
21611
  var import_promises6 = require("node:fs/promises");
21606
21612
  var import_node_path26 = require("node:path");
21607
21613
  var import_node_os8 = require("node:os");
21608
- var import_node_child_process12 = require("node:child_process");
21614
+ var import_node_child_process13 = require("node:child_process");
21609
21615
 
21610
21616
  // src/board-advance.ts
21611
21617
  function repoOf2(ref) {
@@ -21772,6 +21778,7 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
21772
21778
 
21773
21779
  // src/plugin-guard-io.ts
21774
21780
  var import_node_fs25 = require("node:fs");
21781
+ var import_node_child_process12 = require("node:child_process");
21775
21782
  var import_node_path24 = require("node:path");
21776
21783
  var import_node_os6 = require("node:os");
21777
21784
 
@@ -21797,11 +21804,15 @@ function buildPluginGuardLine(state, opts = {}) {
21797
21804
  var isWin = process.platform === "win32";
21798
21805
  var MMI_PLUGIN_ID = "mmi@mutmutco";
21799
21806
  var LEGACY_MMI_MARKETPLACE = "mmi";
21807
+ var CODEX_MARKETPLACE = "mutmutco";
21800
21808
  function detectSurface(env) {
21801
21809
  const has = (k) => Boolean(env[k]?.trim());
21802
- if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
21810
+ if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_THREAD_ID") || has("CODEX_MANAGED_BY_NPM") || has("CODEX_MANAGED_PACKAGE_ROOT") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
21803
21811
  return "codex";
21804
21812
  }
21813
+ if (env.MMI_AGENT_SURFACE === "kimi" || has("KIMI_PLUGIN_ROOT") || has("KIMI_CODE_HOME")) {
21814
+ return "kimi";
21815
+ }
21805
21816
  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")) {
21806
21817
  return "cursor";
21807
21818
  }
@@ -21819,6 +21830,8 @@ function surfaceToken(surface) {
21819
21830
  return "claude";
21820
21831
  case "codex":
21821
21832
  return "codex";
21833
+ case "kimi":
21834
+ return "kimi";
21822
21835
  case "cursor":
21823
21836
  return "cursor";
21824
21837
  case "opencode":
@@ -21834,6 +21847,8 @@ function reloadAction(surface) {
21834
21847
  return "restart VS Code";
21835
21848
  case "codex":
21836
21849
  return "restart Codex";
21850
+ case "kimi":
21851
+ return "run /reload (or start a new session) in Kimi Code";
21837
21852
  case "opencode":
21838
21853
  return "restart OpenCode";
21839
21854
  case "cursor":
@@ -21845,6 +21860,7 @@ function reloadAction(surface) {
21845
21860
  }
21846
21861
  }
21847
21862
  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`;
21863
+ 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";
21848
21864
  var PLUGIN_SURFACE_HEAL = {
21849
21865
  claude: {
21850
21866
  delivery: "plugin-cli",
@@ -21858,13 +21874,28 @@ var PLUGIN_SURFACE_HEAL = {
21858
21874
  ],
21859
21875
  fix: (surface) => `${CLAUDE_RECOVERY} # then ${reloadAction(surface)} to reload MMI commands`,
21860
21876
  updateRecipe: [CLAUDE_RECOVERY]
21877
+ },
21878
+ codex: {
21879
+ delivery: "plugin-cli",
21880
+ recovery: CODEX_RECOVERY,
21881
+ healSteps: [
21882
+ { args: ["plugin", "remove", MMI_PLUGIN_ID], gated: false },
21883
+ { args: ["plugin", "marketplace", "remove", CODEX_MARKETPLACE], gated: false },
21884
+ { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
21885
+ { args: ["plugin", "add", MMI_PLUGIN_ID], gated: true }
21886
+ ],
21887
+ fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} and review /hooks`,
21888
+ updateRecipe: [CODEX_RECOVERY]
21861
21889
  }
21862
21890
  };
21863
21891
  function nonClaudeSurfaceHealMessage(surface) {
21864
21892
  if (surface === "codex") {
21865
21893
  return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Then run /hooks and TRUST the MMI hooks \u2014 bundled hooks are skipped until reviewed.\n Update the CLI too: npm i -g @mutmutco/cli";
21866
21894
  }
21867
- return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude and Codex only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
21895
+ if (surface === "kimi") {
21896
+ 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";
21897
+ }
21898
+ 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)";
21868
21899
  }
21869
21900
  function healStepAborts(step, ok) {
21870
21901
  return !ok && step.gated;
@@ -21903,28 +21934,97 @@ var NPM_VIEW_TIMEOUT_MS = 15e3;
21903
21934
  function runHostBin(bin, args, opts) {
21904
21935
  return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
21905
21936
  }
21906
- var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
21907
- const homeDir = surface === "codex" ? ".codex" : ".claude";
21908
- return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21937
+ function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os6.homedir)()) {
21938
+ if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path24.join)(home, ".codex");
21939
+ if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path24.join)(home, ".kimi-code");
21940
+ return (0, import_node_path24.join)(home, ".claude");
21941
+ }
21942
+ var installedPluginsPath = (surface = detectSurface(process.env)) => {
21943
+ return (0, import_node_path24.join)(surfaceConfigRoot(surface), "plugins", "installed_plugins.json");
21909
21944
  };
21910
21945
  function readInstalledPlugins(surface = detectSurface(process.env)) {
21911
21946
  try {
21912
- return JSON.parse((0, import_node_fs25.readFileSync)(installedPluginsPath2(surface), "utf8"));
21947
+ return JSON.parse((0, import_node_fs25.readFileSync)(installedPluginsPath(surface), "utf8"));
21913
21948
  } catch {
21914
21949
  return null;
21915
21950
  }
21916
21951
  }
21917
- function marketplaceCloneCandidates(surface, home) {
21952
+ function marketplaceCloneCandidates(surface, home, env = process.env) {
21918
21953
  if (surface === "codex") {
21954
+ const root = surfaceConfigRoot(surface, env, home);
21919
21955
  return [
21920
- (0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
21921
- (0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
21956
+ (0, import_node_path24.join)(root, ".tmp", "marketplaces", CODEX_MARKETPLACE),
21957
+ (0, import_node_path24.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
21922
21958
  ];
21923
21959
  }
21960
+ if (surface === "kimi") return [];
21924
21961
  return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21925
21962
  }
21926
- function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync) {
21927
- return marketplaceCloneCandidates(surface, home).some(exists);
21963
+ function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync, env = process.env) {
21964
+ return marketplaceCloneCandidates(surface, home, env).some(exists);
21965
+ }
21966
+ function runHostBinSync(bin, args) {
21967
+ return isWin ? (0, import_node_child_process12.execFileSync)("cmd.exe", ["/c", bin, ...args], {
21968
+ encoding: "utf8",
21969
+ stdio: ["ignore", "pipe", "ignore"],
21970
+ timeout: 15e3
21971
+ }) : (0, import_node_child_process12.execFileSync)(bin, args, {
21972
+ encoding: "utf8",
21973
+ stdio: ["ignore", "pipe", "ignore"],
21974
+ timeout: 15e3
21975
+ });
21976
+ }
21977
+ function codexPluginStatus() {
21978
+ try {
21979
+ const raw = runHostBinSync("codex", ["plugin", "list", "--json"]);
21980
+ const parsed = JSON.parse(raw);
21981
+ const plugin = parsed.installed?.find((entry) => entry.pluginId === MMI_PLUGIN_ID);
21982
+ return {
21983
+ installed: plugin?.installed === true,
21984
+ enabled: plugin?.enabled === true,
21985
+ ...plugin?.version ? { version: plugin.version } : {}
21986
+ };
21987
+ } catch {
21988
+ return { installed: false, enabled: false };
21989
+ }
21990
+ }
21991
+ function countCodexHookCommands(path2) {
21992
+ try {
21993
+ const parsed = JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8"));
21994
+ let count = 0;
21995
+ for (const groups of Object.values(parsed.hooks ?? {})) {
21996
+ for (const group of groups) {
21997
+ count += (group.hooks ?? []).filter((hook) => hook.type === "command").length;
21998
+ }
21999
+ }
22000
+ return count;
22001
+ } catch {
22002
+ return 0;
22003
+ }
22004
+ }
22005
+ function codexHookTrustState(status = codexPluginStatus()) {
22006
+ if (!status.installed || !status.version) {
22007
+ return { applicable: false, trusted: false, trustedCount: 0, requiredCount: 0 };
22008
+ }
22009
+ const root = surfaceConfigRoot("codex");
22010
+ const hooksPath = (0, import_node_path24.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version, "hooks", "codex-hooks.json");
22011
+ const requiredCount = countCodexHookCommands(hooksPath);
22012
+ let config = "";
22013
+ try {
22014
+ config = (0, import_node_fs25.readFileSync)((0, import_node_path24.join)(root, "config.toml"), "utf8");
22015
+ } catch {
22016
+ return { applicable: true, trusted: false, trustedCount: 0, requiredCount };
22017
+ }
22018
+ const trustedCount = (config.match(/\[hooks\.state\."mmi@mutmutco:hooks\/codex-hooks\.json:[^"]+"\]/g) ?? []).length;
22019
+ return {
22020
+ applicable: true,
22021
+ // Codex stores a hash beside each approval row and intentionally leaves stale rows behind. The hash
22022
+ // input is not a public host contract, so row count can prove only that approvals EXISTED — never that
22023
+ // the current commands are trusted. `/hooks` is the only honest freshness check.
22024
+ trusted: false,
22025
+ trustedCount,
22026
+ requiredCount
22027
+ };
21928
22028
  }
21929
22029
  async function fetchNpmReleasedVersion() {
21930
22030
  try {
@@ -21945,16 +22045,20 @@ async function npmSelfUpdateCli(target) {
21945
22045
  }
21946
22046
  }
21947
22047
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
21948
- const homeDir = surface === "codex" ? ".codex" : ".claude";
22048
+ const root = surfaceConfigRoot(surface);
21949
22049
  const installed = readInstalledPlugins(surface);
22050
+ const codexStatus = surface === "codex" ? codexPluginStatus() : void 0;
21950
22051
  return {
21951
22052
  isOrgRepo,
21952
22053
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
21953
22054
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
21954
- pluginCachePresent: (0, import_node_fs25.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
22055
+ // Kimi keeps no plugin cache dir — installs are copied to plugins/managed/<id> and run from there.
22056
+ pluginCachePresent: surface === "kimi" ? (0, import_node_fs25.existsSync)((0, import_node_path24.join)(root, "plugins", "managed", "mmi")) : surface === "codex" ? Boolean(
22057
+ codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs25.existsSync)((0, import_node_path24.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
22058
+ ) : (0, import_node_fs25.existsSync)((0, import_node_path24.join)(root, "plugins", "cache", "mutmutco", "mmi"))
21955
22059
  };
21956
22060
  }
21957
- function claudePluginGuardState(isOrgRepo) {
22061
+ function activePluginGuardState(isOrgRepo) {
21958
22062
  return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
21959
22063
  }
21960
22064
  async function runClaudePlugin(args) {
@@ -21965,6 +22069,14 @@ async function runClaudePlugin(args) {
21965
22069
  return false;
21966
22070
  }
21967
22071
  }
22072
+ async function runCodexPlugin(args) {
22073
+ try {
22074
+ await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
22075
+ return true;
22076
+ } catch {
22077
+ return false;
22078
+ }
22079
+ }
21968
22080
  async function marketplaceAddRefSupported(bin) {
21969
22081
  try {
21970
22082
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -21991,18 +22103,23 @@ function pluginReadGrantNote(login = "<your-github-login>") {
21991
22103
  gh api -X PUT repos/${PLUGIN_READ_REPO2}/collaborators/${login} -f permission=pull`;
21992
22104
  }
21993
22105
  async function applyPluginHeal(surface, log, opts) {
21994
- if (!opts?.force && surfaceToken(surface) !== "claude") return false;
21995
- const tableSteps = PLUGIN_SURFACE_HEAL.claude.healSteps;
22106
+ const token = surfaceToken(surface);
22107
+ if (token !== "claude" && token !== "codex") return false;
22108
+ if (!opts?.force && !PLUGIN_SURFACE_HEAL[token]) return false;
22109
+ const descriptor = PLUGIN_SURFACE_HEAL[token];
22110
+ const tableSteps = descriptor.healSteps;
21996
22111
  if (!tableSteps) return false;
21997
- const refSupported = await marketplaceAddRefSupported("claude");
22112
+ const bin = token;
22113
+ const refSupported = await marketplaceAddRefSupported(bin);
21998
22114
  const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
21999
- log(healBannerLine("claude", "claude", refSupported));
22115
+ log(healBannerLine(bin, token, refSupported));
22000
22116
  const pinsPath = (0, import_node_path24.join)((0, import_node_os6.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
22001
- const pins = captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]);
22117
+ const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
22002
22118
  for (const step of steps) {
22003
- if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
22119
+ const ok = token === "claude" ? await runClaudePlugin([...step.args]) : await runCodexPlugin([...step.args]);
22120
+ if (healStepAborts(step, ok)) return false;
22004
22121
  }
22005
- const restored = restoreMarketplacePinsOnDisk(pinsPath, pins);
22122
+ const restored = token === "claude" ? restoreMarketplacePinsOnDisk(pinsPath, pins) : void 0;
22006
22123
  if (restored) log(` ${restored}`);
22007
22124
  return true;
22008
22125
  }
@@ -22018,6 +22135,23 @@ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
22018
22135
  detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${pinNote ? `; ${pinNote}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
22019
22136
  };
22020
22137
  }
22138
+ async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
22139
+ const token = surfaceToken(surface);
22140
+ if (token !== "claude" && token !== "codex") {
22141
+ return { ok: false, detail: `not a supported plugin surface (${surface})` };
22142
+ }
22143
+ if (token === "claude") return healClaudePluginForDoctor(surface);
22144
+ const steps = [];
22145
+ const applied = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
22146
+ const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
22147
+ const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
22148
+ const ok = applied && guardState === "healthy";
22149
+ const verification = snapshot ? `record=${snapshot.installRecordPresent ? "yes" : "no"}, marketplace=${snapshot.marketplaceClonePresent ? "yes" : "no"}, enabled-cache=${snapshot.pluginCachePresent ? "yes" : "no"}` : "reinstall steps did not complete";
22150
+ return {
22151
+ ok,
22152
+ 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]}` : ""}`
22153
+ };
22154
+ }
22021
22155
  function readKnownMarketplacesFile(path2) {
22022
22156
  try {
22023
22157
  return (0, import_node_fs25.existsSync)(path2) ? (0, import_node_fs25.readFileSync)(path2, "utf8") : void 0;
@@ -22043,9 +22177,10 @@ function restoreMarketplacePinsOnDisk(path2, pins) {
22043
22177
  return failed.length ? `restore did NOT take for ${failed.map(([n]) => n).join(", ")} \u2014 re-pin by hand` : `re-pinned ${[...pins.keys()].join(", ")} (auto-update + catalog ref survive the reinstall)`;
22044
22178
  }
22045
22179
  async function runGuard(readOrigin) {
22180
+ const surface = detectSurface(process.env);
22046
22181
  try {
22047
- const surface = detectSurface(process.env);
22048
- if (surfaceToken(surface) !== "claude") {
22182
+ const token = surfaceToken(surface);
22183
+ if (token !== "claude" && token !== "codex") {
22049
22184
  process.exitCode = 0;
22050
22185
  return;
22051
22186
  }
@@ -22054,24 +22189,39 @@ async function runGuard(readOrigin) {
22054
22189
  const { state } = buildPluginGuardDecision(input);
22055
22190
  const { line, exitCode } = buildPluginGuardLine(state);
22056
22191
  if (line) console.error(line);
22192
+ if (token === "codex" && exitCode === 0) {
22193
+ const trust = codexHookTrustState();
22194
+ if (trust.applicable && !trust.trusted) {
22195
+ console.error(`[mmi-guard] MMI Codex hook trust cannot be verified non-interactively (${trust.trustedCount}/${trust.requiredCount} approval rows present); run /hooks and review the current hashes.`);
22196
+ }
22197
+ }
22057
22198
  process.exitCode = exitCode;
22058
22199
  } catch {
22059
- process.exitCode = 0;
22200
+ if (surfaceToken(surface) === "codex") {
22201
+ console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
22202
+ process.exitCode = 1;
22203
+ } else {
22204
+ process.exitCode = 0;
22205
+ }
22060
22206
  }
22061
22207
  }
22062
22208
  async function runPluginHeal(surface = detectSurface(process.env)) {
22063
- if (surfaceToken(surface) !== "claude") {
22064
- console.log(nonClaudeSurfaceHealMessage(surfaceToken(surface) ?? void 0));
22209
+ const token = surfaceToken(surface);
22210
+ if (token !== "claude" && token !== "codex") {
22211
+ console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
22065
22212
  return;
22066
22213
  }
22067
- const descriptor = PLUGIN_SURFACE_HEAL.claude;
22068
- const healed = await applyPluginHeal(surface, console.log, { force: true });
22214
+ const descriptor = PLUGIN_SURFACE_HEAL[token];
22215
+ const applied = await applyPluginHeal(surface, console.log, { force: true });
22216
+ const healed = token === "codex" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
22069
22217
  if (healed) {
22070
- console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
22218
+ const trust = token === "codex" ? " Then run /hooks and review + trust the MMI hooks." : "";
22219
+ console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands.${trust}`);
22071
22220
  } else {
22072
- const refSupported = await marketplaceAddRefSupported("claude");
22221
+ process.exitCode = 1;
22222
+ const refSupported = await marketplaceAddRefSupported(token);
22073
22223
  const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
22074
- const note = refAbsenceNote("claude", refSupported);
22224
+ const note = refAbsenceNote(token, refSupported);
22075
22225
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
22076
22226
  ${recovery}${note}${pluginReadGrantNote()}`);
22077
22227
  }
@@ -22633,7 +22783,7 @@ async function remoteBranchExists2(branch, options = {}) {
22633
22783
  }
22634
22784
  var COMPOSE_TIMEOUT_MS = 12e4;
22635
22785
  function spawnDeferredGcSweep() {
22636
- spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
22786
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
22637
22787
  }
22638
22788
  async function createDeferredWorktreeStore() {
22639
22789
  try {
@@ -23590,7 +23740,7 @@ ${spec.body ?? ""}`;
23590
23740
  const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
23591
23741
  return `${batchKey}:${hash}`;
23592
23742
  }
23593
- var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "parent", "repo"]);
23743
+ var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo"]);
23594
23744
  function validateBatchSpecs(specs) {
23595
23745
  const errors = [];
23596
23746
  const validated = [];
@@ -23607,6 +23757,11 @@ function validateBatchSpecs(specs) {
23607
23757
  errors.push({ row, error: `unknown key(s): ${unknown.join(", ")} \u2014 expected only: ${[...BATCH_SPEC_KEYS].join(", ")}` });
23608
23758
  continue;
23609
23759
  }
23760
+ if (spec.label !== void 0) {
23761
+ const alias = Array.isArray(spec.label) ? spec.label : [spec.label];
23762
+ spec.labels = [...spec.labels ?? [], ...alias];
23763
+ delete spec.label;
23764
+ }
23610
23765
  if (spec.repo !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(spec.repo)) {
23611
23766
  errors.push({ row, error: `bad repo "${spec.repo}" \u2014 expected owner/repo` });
23612
23767
  continue;
@@ -25181,6 +25336,10 @@ function pluginHealTrigger(probe) {
25181
25336
  }
25182
25337
  function checkClaudePlugin(probe) {
25183
25338
  const { installed, released, guardState } = probe;
25339
+ const codex = probe.surface === "codex";
25340
+ const id = codex ? "codex-plugin" : "claude-plugin";
25341
+ const label = codex ? "Codex plugin" : "Claude plugin";
25342
+ const restart = codex ? "restart Codex" : "restart Claude";
25184
25343
  const evidence = [
25185
25344
  `installed: ${installed ?? "(none)"}`,
25186
25345
  // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
@@ -25190,38 +25349,57 @@ function checkClaudePlugin(probe) {
25190
25349
  const trigger = pluginHealTrigger(probe);
25191
25350
  if (trigger === "unresolved") {
25192
25351
  return {
25193
- id: "claude-plugin",
25352
+ id,
25194
25353
  ok: false,
25195
- label: "Claude plugin",
25354
+ label,
25196
25355
  detail: guardState === "no-install" ? "not installed" : "unresolved (marketplace/cache missing)",
25197
- fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
25356
+ fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI marketplace + plugin, then ${restart}`,
25198
25357
  verbose: evidence
25199
25358
  };
25200
25359
  }
25201
25360
  if (trigger === "behind") {
25202
25361
  return {
25203
- id: "claude-plugin",
25362
+ id,
25204
25363
  ok: false,
25205
- label: "Claude plugin",
25364
+ label,
25206
25365
  detail: `${installed} \u2192 ${released}`,
25207
25366
  // Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
25208
25367
  // fallback, not the first resort (#3282).
25209
- fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
25368
+ fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall it, then ${restart}`,
25210
25369
  verbose: evidence
25211
25370
  };
25212
25371
  }
25213
25372
  if (!released) {
25214
25373
  return {
25215
- id: "claude-plugin",
25374
+ id,
25216
25375
  ok: false,
25217
25376
  reportOnly: true,
25218
- label: "Claude plugin",
25377
+ label,
25219
25378
  detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
25220
25379
  fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
25221
25380
  verbose: evidence
25222
25381
  };
25223
25382
  }
25224
- return { id: "claude-plugin", ok: true, label: "Claude plugin", ...installed ? { detail: installed } : {}, verbose: evidence };
25383
+ return { id, ok: true, label, ...installed ? { detail: installed } : {}, verbose: evidence };
25384
+ }
25385
+ function checkCodexHookTrust(probe) {
25386
+ if (!probe?.applicable) return null;
25387
+ const evidence = [
25388
+ `stored approval rows: ${probe.trustedCount}/${probe.requiredCount}`,
25389
+ "current command hashes: verify interactively in /hooks"
25390
+ ];
25391
+ if (probe.trusted) {
25392
+ return { id: "codex-hook-trust", ok: true, label: "Codex hook trust", detail: "trusted", verbose: evidence };
25393
+ }
25394
+ return {
25395
+ id: "codex-hook-trust",
25396
+ ok: true,
25397
+ warn: true,
25398
+ label: "Codex hook trust",
25399
+ detail: probe.requiredCount > 0 ? `${probe.trustedCount}/${probe.requiredCount} approval rows present \u2014 current hashes unverified` : "hook bundle could not be verified",
25400
+ 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`",
25401
+ verbose: evidence
25402
+ };
25225
25403
  }
25226
25404
  function checkCliVersion(input, releasedNote) {
25227
25405
  const report = buildVersionLagReport(input);
@@ -25459,6 +25637,8 @@ async function runDoctorClean(opts, io, deps) {
25459
25637
  ]);
25460
25638
  const ghInstalled2 = login ? true : await deps.ghInstalled();
25461
25639
  const installed = deps.installedPluginVersion();
25640
+ const pluginSurface = deps.pluginSurface?.() ?? "claude-cli";
25641
+ const codexSurface = pluginSurface === "codex";
25462
25642
  const checks = [];
25463
25643
  let restartPending = false;
25464
25644
  checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
@@ -25485,10 +25665,18 @@ async function runDoctorClean(opts, io, deps) {
25485
25665
  }
25486
25666
  }
25487
25667
  const releasedNote = deps.releasedVersionNote?.();
25488
- const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo), releasedNote };
25668
+ const pluginProbe = {
25669
+ installed,
25670
+ released,
25671
+ guardState: deps.pluginGuardState(isOrgRepo),
25672
+ releasedNote,
25673
+ surface: pluginSurface
25674
+ };
25489
25675
  const healTrigger = pluginHealTrigger(pluginProbe);
25676
+ let pluginHealed = false;
25490
25677
  if (applyEnv && deps.healPlugin && healTrigger) {
25491
25678
  const heal = await deps.healPlugin();
25679
+ pluginHealed = heal.ok;
25492
25680
  const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
25493
25681
  const healEvidence = [
25494
25682
  `installed: ${installed ?? "(none)"}`,
@@ -25497,22 +25685,22 @@ async function runDoctorClean(opts, io, deps) {
25497
25685
  `heal: ${heal.detail}`
25498
25686
  ];
25499
25687
  checks.push(heal.ok ? {
25500
- id: "claude-plugin",
25688
+ id: codexSurface ? "codex-plugin" : "claude-plugin",
25501
25689
  ok: true,
25502
- label: "Claude plugin",
25503
- detail: `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)`,
25690
+ label: codexSurface ? "Codex plugin" : "Claude plugin",
25691
+ detail: `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 ${codexSurface ? "add" : "install"})${codexSurface ? "; review trust in /hooks" : ""}`,
25504
25692
  verbose: healEvidence
25505
25693
  } : {
25506
- id: "claude-plugin",
25694
+ id: codexSurface ? "codex-plugin" : "claude-plugin",
25507
25695
  ok: false,
25508
- label: "Claude plugin",
25696
+ label: codexSurface ? "Codex plugin" : "Claude plugin",
25509
25697
  detail: measured,
25510
25698
  // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
25511
25699
  // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
25512
25700
  // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
25513
25701
  // genuine gap and still gates.
25514
25702
  ...heal.skipped ? { reportOnly: true } : {},
25515
- 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 restart Claude`,
25703
+ 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"}`,
25516
25704
  verbose: healEvidence
25517
25705
  });
25518
25706
  if (!heal.skipped) restartPending = true;
@@ -25523,6 +25711,10 @@ async function runDoctorClean(opts, io, deps) {
25523
25711
  if (!plugin.ok) restartPending = true;
25524
25712
  }
25525
25713
  }
25714
+ if (codexSurface && (pluginProbe.guardState === "healthy" || pluginHealed)) {
25715
+ const trust = checkCodexHookTrust(deps.pluginTrustState?.());
25716
+ if (trust) checks.push(trust);
25717
+ }
25526
25718
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
25527
25719
  const cliReport = buildVersionLagReport(cliInput);
25528
25720
  if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
@@ -25656,7 +25848,12 @@ async function runDoctorClean(opts, io, deps) {
25656
25848
  const exitCode = doctorReportExitCode(checks);
25657
25849
  if (opts.json) {
25658
25850
  const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
25659
- io.log(JSON.stringify({ checks: payload, restartPending, exitCode }, null, 2));
25851
+ io.log(JSON.stringify({
25852
+ checks: payload,
25853
+ restartPending,
25854
+ ...restartPending ? { restartAction: codexSurface ? "restart Codex" : "restart Claude" } : {},
25855
+ exitCode
25856
+ }, null, 2));
25660
25857
  return exitCode;
25661
25858
  }
25662
25859
  if (opts.banner) {
@@ -25665,10 +25862,12 @@ async function runDoctorClean(opts, io, deps) {
25665
25862
  io.log(renderReport([c], { restartPending: false }));
25666
25863
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
25667
25864
  }
25668
- if (restartPending) io.log(RESTART_LINE);
25865
+ if (restartPending) io.log(codexSurface ? "\u21BB Restart Codex to finish." : RESTART_LINE);
25669
25866
  return 0;
25670
25867
  }
25671
- io.log(renderDoctorText(checks, { verbose: Boolean(opts.verbose), restartPending }));
25868
+ const rendered = renderDoctorText(checks, { verbose: Boolean(opts.verbose), restartPending: restartPending && !codexSurface });
25869
+ io.log(restartPending && codexSurface ? `${rendered}
25870
+ \u21BB Restart Codex to finish.` : rendered);
25672
25871
  return exitCode;
25673
25872
  }
25674
25873
 
@@ -25754,9 +25953,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
25754
25953
  var import_node_fs32 = require("node:fs");
25755
25954
  var import_node_os10 = require("node:os");
25756
25955
  var import_node_path30 = require("node:path");
25757
- var import_node_child_process13 = require("node:child_process");
25956
+ var import_node_child_process14 = require("node:child_process");
25758
25957
  var import_node_util8 = require("node:util");
25759
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process13.execFile);
25958
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process14.execFile);
25760
25959
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
25761
25960
  function installedClaudePluginVersion() {
25762
25961
  try {
@@ -25770,9 +25969,28 @@ function installedClaudePluginVersion() {
25770
25969
  return void 0;
25771
25970
  }
25772
25971
  }
25972
+ function installedActivePluginVersion(surface = detectSurface(process.env)) {
25973
+ if (surface !== "codex") return installedClaudePluginVersion();
25974
+ try {
25975
+ const raw = process.platform === "win32" ? (0, import_node_child_process14.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
25976
+ encoding: "utf8",
25977
+ stdio: ["ignore", "pipe", "ignore"],
25978
+ timeout: 15e3
25979
+ }) : (0, import_node_child_process14.execFileSync)("codex", ["plugin", "list", "--json"], {
25980
+ encoding: "utf8",
25981
+ stdio: ["ignore", "pipe", "ignore"],
25982
+ timeout: 15e3
25983
+ });
25984
+ const parsed = JSON.parse(raw);
25985
+ const plugin = parsed.installed?.find((entry) => entry.pluginId === MMI_PLUGIN_ID2 && entry.installed === true && entry.enabled === true);
25986
+ return plugin?.version;
25987
+ } catch {
25988
+ return void 0;
25989
+ }
25990
+ }
25773
25991
  function worktreeRootSync() {
25774
25992
  try {
25775
- const out = (0, import_node_child_process13.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
25993
+ const out = (0, import_node_child_process14.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
25776
25994
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
25777
25995
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
25778
25996
  return root || null;
@@ -25917,8 +26135,10 @@ function mmiDoctorDeps(opts = {}) {
25917
26135
  githubRepoReach: githubRepoReachProbe,
25918
26136
  awsCallerArn,
25919
26137
  isOrgRepo: () => isOrgRepoRoot(),
25920
- installedPluginVersion: installedClaudePluginVersion,
25921
- pluginGuardState: claudePluginGuardState,
26138
+ installedPluginVersion: installedActivePluginVersion,
26139
+ pluginGuardState: activePluginGuardState,
26140
+ pluginSurface: () => detectSurface(process.env),
26141
+ pluginTrustState: () => codexHookTrustState(),
25922
26142
  releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
25923
26143
  releasedVersionNote: throttled ? throttled.note : void 0,
25924
26144
  // #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
@@ -25929,7 +26149,10 @@ function mmiDoctorDeps(opts = {}) {
25929
26149
  updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
25930
26150
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
25931
26151
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
25932
- healPlugin: () => withEnvHealLock("Claude plugin reinstall", () => healClaudePluginForDoctor()),
26152
+ healPlugin: () => {
26153
+ const surface = detectSurface(process.env);
26154
+ return withEnvHealLock(`${surface === "codex" ? "Codex" : "Claude"} plugin reinstall`, () => healActivePluginForDoctor(surface));
26155
+ },
25933
26156
  currentCliVersion: resolveClientVersion,
25934
26157
  readGitignore,
25935
26158
  writeGitignore,
@@ -25964,10 +26187,14 @@ function mmiDoctorDeps(opts = {}) {
25964
26187
  return { personal, app };
25965
26188
  },
25966
26189
  pluginCache: () => {
26190
+ const surface = detectSurface(process.env);
26191
+ const configRoot = surfaceConfigRoot(surface);
26192
+ const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
25967
26193
  const plan = buildPluginCachePlan(
25968
26194
  (0, import_node_os11.homedir)(),
25969
- runningPluginVersion(process.env, resolveClientVersion()),
25970
- pluginCacheFsDeps((0, import_node_os11.homedir)(), () => 0)
26195
+ running,
26196
+ pluginCacheFsDeps(configRoot, () => 0),
26197
+ { configRoot, includeStaging: surface !== "codex" }
25971
26198
  );
25972
26199
  return {
25973
26200
  stale: plan.prune,
@@ -26016,6 +26243,7 @@ function mmiDoctorDeps(opts = {}) {
26016
26243
  // which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
26017
26244
  marketplaceRows: () => {
26018
26245
  try {
26246
+ if (detectSurface(process.env) === "codex") return [];
26019
26247
  const home = (0, import_node_os11.homedir)();
26020
26248
  return marketplaceRows(
26021
26249
  MMI_MARKETPLACE_NAME,
@@ -26407,7 +26635,7 @@ function runWorktreeInstall(command, cwd, quiet) {
26407
26635
  const file = isWin2 ? "cmd.exe" : bin;
26408
26636
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
26409
26637
  return new Promise((resolve5, reject) => {
26410
- const child2 = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
26638
+ const child2 = (0, import_node_child_process15.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
26411
26639
  const timer = setTimeout(() => {
26412
26640
  try {
26413
26641
  child2.kill();
@@ -26678,7 +26906,7 @@ function scheduleRelatedDiscovery(o) {
26678
26906
  try {
26679
26907
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
26680
26908
  if (o.repo) args.push("--repo", o.repo);
26681
- spawnDetachedSelf(args, { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
26909
+ spawnDetachedSelf(args, { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
26682
26910
  } catch {
26683
26911
  }
26684
26912
  }
@@ -28373,8 +28601,8 @@ function directoryBytes(path2) {
28373
28601
  function listDirEntries(dir) {
28374
28602
  return (0, import_node_fs33.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
28375
28603
  }
28376
- function readInstalledPluginRefs(home) {
28377
- const p = installedPluginsPath(home);
28604
+ function readInstalledPluginRefs(configRoot) {
28605
+ const p = installedPluginsPathForConfig(configRoot);
28378
28606
  if (!(0, import_node_fs33.existsSync)(p)) return [];
28379
28607
  try {
28380
28608
  return installedPluginPaths((0, import_node_fs33.readFileSync)(p, "utf8"));
@@ -28382,7 +28610,7 @@ function readInstalledPluginRefs(home) {
28382
28610
  return null;
28383
28611
  }
28384
28612
  }
28385
- function pluginCacheFsDeps(home, dirBytes) {
28613
+ function pluginCacheFsDeps(configRoot, dirBytes) {
28386
28614
  return {
28387
28615
  exists: (p) => (0, import_node_fs33.existsSync)(p),
28388
28616
  listVersionDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
@@ -28394,14 +28622,14 @@ function pluginCacheFsDeps(home, dirBytes) {
28394
28622
  return { name: d.name, mtimeMs: Date.now() };
28395
28623
  }
28396
28624
  }),
28397
- readInstalledPluginPaths: () => readInstalledPluginRefs(home),
28625
+ readInstalledPluginPaths: () => readInstalledPluginRefs(configRoot),
28398
28626
  now: () => Date.now()
28399
28627
  };
28400
28628
  }
28401
- function stagingApplyFsGuard(home) {
28402
- const stagingRoot = pluginCacheStagingRoot(home);
28629
+ function stagingApplyFsGuard(configRoot) {
28630
+ const stagingRoot = pluginCacheStagingRootForConfig(configRoot);
28403
28631
  return {
28404
- referencedPaths: () => readInstalledPluginRefs(home),
28632
+ referencedPaths: () => readInstalledPluginRefs(configRoot),
28405
28633
  mtimeMs: (name) => {
28406
28634
  const p = (0, import_node_path31.join)(stagingRoot, name);
28407
28635
  if (!(0, import_node_fs33.existsSync)(p)) return null;
@@ -28415,14 +28643,24 @@ function stagingApplyFsGuard(home) {
28415
28643
  };
28416
28644
  }
28417
28645
  program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
28646
+ const surface = detectSurface(process.env);
28647
+ const configRoot = surfaceConfigRoot(surface);
28648
+ const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
28649
+ if (o.apply && surface === "codex" && !running) {
28650
+ const detail = "plugin prune refused: Codex could not identify the enabled MMI version; no cache was deleted";
28651
+ if (o.json) console.log(JSON.stringify({ ok: false, error: detail }));
28652
+ else console.error(detail);
28653
+ process.exitCode = 1;
28654
+ return;
28655
+ }
28418
28656
  const plan = buildPluginCachePlan(
28419
28657
  (0, import_node_os11.homedir)(),
28420
- runningPluginVersion(process.env, resolveClientVersion()),
28421
- pluginCacheFsDeps((0, import_node_os11.homedir)(), directoryBytes),
28422
- { withBytes: true }
28658
+ running,
28659
+ pluginCacheFsDeps(configRoot, directoryBytes),
28660
+ { withBytes: true, configRoot, includeStaging: surface !== "codex" }
28423
28661
  );
28424
28662
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
28425
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os11.homedir)())) : void 0;
28663
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
28426
28664
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
28427
28665
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
28428
28666
  else console.log(renderPluginCachePlan(plan, result));
@@ -28485,7 +28723,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
28485
28723
  for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
28486
28724
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
28487
28725
  if (worktreeBanner) {
28488
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
28726
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
28489
28727
  bannerIo.log(worktreeBanner);
28490
28728
  }
28491
28729
  if (isLinkedWorktree(process.cwd())) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.72.0",
4
- "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
3
+ "version": "3.74.0",
4
+ "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the MMI plugin's skills and gates drive on Claude, Codex, and Kimi.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "author": {