@mutmutco/cli 3.10.0 → 3.11.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.
package/dist/main.cjs CHANGED
@@ -3408,7 +3408,7 @@ var program = new Command();
3408
3408
 
3409
3409
  // src/index.ts
3410
3410
  var import_promises3 = require("node:fs/promises");
3411
- var import_node_fs20 = require("node:fs");
3411
+ var import_node_fs21 = require("node:fs");
3412
3412
 
3413
3413
  // src/bootstrap-org-ruleset.ts
3414
3414
  var ORG_NO_AGENT_FILES_RULESET_NAME = "mmi-no-agent-files-org";
@@ -3540,7 +3540,6 @@ function resolveClientVersionManifestCandidates(distDir = __dirname) {
3540
3540
  return [
3541
3541
  (0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
3542
3542
  (0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
3543
- (0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
3544
3543
  (0, import_node_path2.join)(distDir, "..", "package.json")
3545
3544
  ];
3546
3545
  }
@@ -4521,7 +4520,10 @@ function buildSessionStartPlan(verbs) {
4521
4520
  // Identity reads are memoized process-wide, so this adds no extra gh/Hub round-trip.
4522
4521
  { name: "whoami", run: verbs.whoami },
4523
4522
  // board slice (#1230): assigned + claimable glance, 3s cap, fail-soft — flushed after whoami.
4524
- { name: "board slice", run: verbs.boardSlice }
4523
+ { name: "board slice", run: verbs.boardSlice },
4524
+ // command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
4525
+ // not after a denied gh call. <1KB, fail-soft — never blocks the banner.
4526
+ { name: "command ladder", run: verbs.commandLadderHint }
4525
4527
  ],
4526
4528
  sequential: [
4527
4529
  // local train sync (#2582): fast-forward local development/main/rc to origin BEFORE doctor + before
@@ -5838,9 +5840,31 @@ async function refreshBoardSliceCache(deps) {
5838
5840
  }
5839
5841
  }
5840
5842
 
5841
- // src/worktree.ts
5843
+ // src/hook-activity.ts
5842
5844
  var import_node_fs9 = require("node:fs");
5843
5845
  var import_node_path9 = require("node:path");
5846
+ var DEFAULT_SURFACE = "claude";
5847
+ function activityLogPath(cwd) {
5848
+ return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
5849
+ }
5850
+ function appendHookActivity(cwd, entry) {
5851
+ try {
5852
+ const path2 = activityLogPath(cwd);
5853
+ const line = {
5854
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5855
+ surface: DEFAULT_SURFACE,
5856
+ ...entry
5857
+ };
5858
+ (0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
5859
+ (0, import_node_fs9.appendFileSync)(path2, `${JSON.stringify(line)}
5860
+ `, "utf8");
5861
+ } catch {
5862
+ }
5863
+ }
5864
+
5865
+ // src/worktree.ts
5866
+ var import_node_fs10 = require("node:fs");
5867
+ var import_node_path10 = require("node:path");
5844
5868
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
5845
5869
  var PKG = "package.json";
5846
5870
  var LOCKFILE = "package-lock.json";
@@ -5848,21 +5872,21 @@ var NODE_MODULES = "node_modules";
5848
5872
  var realFsProbe = {
5849
5873
  isDir: (p) => {
5850
5874
  try {
5851
- return (0, import_node_fs9.statSync)(p).isDirectory();
5875
+ return (0, import_node_fs10.statSync)(p).isDirectory();
5852
5876
  } catch {
5853
5877
  return false;
5854
5878
  }
5855
5879
  },
5856
5880
  isFile: (p) => {
5857
5881
  try {
5858
- return (0, import_node_fs9.statSync)(p).isFile();
5882
+ return (0, import_node_fs10.statSync)(p).isFile();
5859
5883
  } catch {
5860
5884
  return false;
5861
5885
  }
5862
5886
  },
5863
5887
  listDirs: (p) => {
5864
5888
  try {
5865
- return (0, import_node_fs9.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5889
+ return (0, import_node_fs10.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5866
5890
  } catch {
5867
5891
  return [];
5868
5892
  }
@@ -5870,12 +5894,12 @@ var realFsProbe = {
5870
5894
  };
5871
5895
  function scanInstallDirs(root, fs2 = realFsProbe) {
5872
5896
  const factsFor = (dir) => {
5873
- const abs = dir ? (0, import_node_path9.join)(root, dir) : root;
5897
+ const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
5874
5898
  return {
5875
5899
  dir,
5876
- hasPackageJson: fs2.isFile((0, import_node_path9.join)(abs, PKG)),
5877
- hasLockfile: fs2.isFile((0, import_node_path9.join)(abs, LOCKFILE)),
5878
- hasNodeModules: fs2.isDir((0, import_node_path9.join)(abs, NODE_MODULES))
5900
+ hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
5901
+ hasLockfile: fs2.isFile((0, import_node_path10.join)(abs, LOCKFILE)),
5902
+ hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES))
5879
5903
  };
5880
5904
  };
5881
5905
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -5885,7 +5909,7 @@ function npmInstallTargets(dirs) {
5885
5909
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
5886
5910
  }
5887
5911
  function isLinkedWorktree(root, fs2 = realFsProbe) {
5888
- return fs2.isFile((0, import_node_path9.join)(root, ".git"));
5912
+ return fs2.isFile((0, import_node_path10.join)(root, ".git"));
5889
5913
  }
5890
5914
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5891
5915
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -5895,8 +5919,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5895
5919
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
5896
5920
  }
5897
5921
  function defaultCopyFile(from, to) {
5898
- (0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(to), { recursive: true });
5899
- (0, import_node_fs9.copyFileSync)(from, to);
5922
+ (0, import_node_fs10.mkdirSync)((0, import_node_path10.dirname)(to), { recursive: true });
5923
+ (0, import_node_fs10.copyFileSync)(from, to);
5900
5924
  }
5901
5925
  async function provisionWorktree(worktreeRoot, deps) {
5902
5926
  const fs2 = deps.fs ?? realFsProbe;
@@ -5908,7 +5932,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5908
5932
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
5909
5933
  const installed = [];
5910
5934
  for (const target of targets) {
5911
- const cwd = target.dir ? (0, import_node_path9.join)(worktreeRoot, target.dir) : worktreeRoot;
5935
+ const cwd = target.dir ? (0, import_node_path10.join)(worktreeRoot, target.dir) : worktreeRoot;
5912
5936
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
5913
5937
  await deps.runInstall(target.command, cwd);
5914
5938
  installed.push(target);
@@ -5917,7 +5941,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5917
5941
  const copySkipped = [];
5918
5942
  const primary = await deps.primaryCheckout();
5919
5943
  for (const rel of LOCAL_ONLY_FILES) {
5920
- const dest = (0, import_node_path9.join)(worktreeRoot, rel);
5944
+ const dest = (0, import_node_path10.join)(worktreeRoot, rel);
5921
5945
  if (fs2.isFile(dest)) {
5922
5946
  copySkipped.push({ file: rel, reason: "already-present" });
5923
5947
  continue;
@@ -5926,11 +5950,11 @@ async function provisionWorktree(worktreeRoot, deps) {
5926
5950
  copySkipped.push({ file: rel, reason: "no-primary" });
5927
5951
  continue;
5928
5952
  }
5929
- if (!fs2.isFile((0, import_node_path9.join)(primary, rel))) {
5953
+ if (!fs2.isFile((0, import_node_path10.join)(primary, rel))) {
5930
5954
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
5931
5955
  continue;
5932
5956
  }
5933
- copyFile((0, import_node_path9.join)(primary, rel), dest);
5957
+ copyFile((0, import_node_path10.join)(primary, rel), dest);
5934
5958
  copied.push(rel);
5935
5959
  log(`copied local config: ${rel}`);
5936
5960
  }
@@ -5938,13 +5962,50 @@ async function provisionWorktree(worktreeRoot, deps) {
5938
5962
  }
5939
5963
  function defaultWorktreePath(repoRoot, branch) {
5940
5964
  const safe = branch.replace(/[/\\]+/g, "-");
5941
- return (0, import_node_path9.join)((0, import_node_path9.dirname)(repoRoot), "mmi-worktrees", safe);
5965
+ return (0, import_node_path10.join)((0, import_node_path10.dirname)(repoRoot), "mmi-worktrees", safe);
5942
5966
  }
5943
5967
  function resolveWorktreeBase(from, remote) {
5944
5968
  const remotePrefix = `${remote}/`;
5945
5969
  const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
5946
5970
  return { base: from, fetchBranch };
5947
5971
  }
5972
+ var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
5973
+ function isGitConfigLockError(error) {
5974
+ return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
5975
+ }
5976
+ var ADD_WORKTREE_BACKOFF_MS = [100, 250, 500, 750, 1e3];
5977
+ var ADD_WORKTREE_MAX_ATTEMPTS = 6;
5978
+ async function cleanupOrphanBranch(branch, base, preExistingOid, deps) {
5979
+ if (preExistingOid !== void 0) return;
5980
+ const currentOid = await deps.revParse(`refs/heads/${branch}`);
5981
+ if (currentOid === void 0) return;
5982
+ const baseOid = await deps.revParse(base);
5983
+ if (baseOid !== void 0 && currentOid !== baseOid) return;
5984
+ await deps.deleteBranch(branch);
5985
+ }
5986
+ async function addWorktreeRobust(wtPath, branch, base, deps) {
5987
+ const maxAttempts = deps.maxAttempts ?? ADD_WORKTREE_MAX_ATTEMPTS;
5988
+ const backoff = deps.backoffMs ?? ADD_WORKTREE_BACKOFF_MS;
5989
+ const log = deps.log ?? (() => {
5990
+ });
5991
+ const preExistingOid = await deps.revParse(`refs/heads/${branch}`);
5992
+ for (let i = 0; i < maxAttempts; i++) {
5993
+ try {
5994
+ await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
5995
+ return;
5996
+ } catch (e) {
5997
+ await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
5998
+ });
5999
+ const retriesLeft = i < maxAttempts - 1;
6000
+ if (retriesLeft && isGitConfigLockError(e)) {
6001
+ log(`worktree add: .git/config lock busy, retrying (${i + 1}/${maxAttempts - 1})`);
6002
+ await deps.sleep(backoff[Math.min(i, backoff.length - 1)]);
6003
+ continue;
6004
+ }
6005
+ throw e;
6006
+ }
6007
+ }
6008
+ }
5948
6009
 
5949
6010
  // src/whoami.ts
5950
6011
  async function resolveWhoami(deps) {
@@ -5969,8 +6030,39 @@ function whoamiLine(report) {
5969
6030
  return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
5970
6031
  }
5971
6032
 
6033
+ // src/command-ladder-hint.ts
6034
+ var COMMAND_LADDER_WRITES = [
6035
+ { gh: "gh issue create", replacement: "mmi-cli issue create" },
6036
+ { gh: "gh issue comment", replacement: "mmi-cli issue comment" },
6037
+ { gh: "gh pr create", replacement: "mmi-cli pr create" },
6038
+ { gh: "gh pr merge", replacement: "mmi-cli pr merge (or mmi-cli pr land)" }
6039
+ ];
6040
+ var COMMAND_LADDER_READS = [
6041
+ { gh: "gh issue view <N>", replacement: "mmi-cli issue view <N>" },
6042
+ { gh: "gh pr view <N>", replacement: "mmi-cli pr view <N>" },
6043
+ { gh: "gh pr checks <N>", replacement: "mmi-cli pr checks-wait <N>" }
6044
+ ];
6045
+ var COMMAND_LADDER_BOARD = [
6046
+ { gh: "gh issue list / project", replacement: "mmi-cli board read" }
6047
+ ];
6048
+ function commandLadderHint() {
6049
+ const pad = 28;
6050
+ const lines = [];
6051
+ lines.push("[mmi] Command ladder (#2609): reach for mmi-cli first");
6052
+ for (const e of COMMAND_LADDER_WRITES) {
6053
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6054
+ }
6055
+ for (const e of COMMAND_LADDER_READS) {
6056
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6057
+ }
6058
+ for (const e of COMMAND_LADDER_BOARD) {
6059
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6060
+ }
6061
+ return lines.join("\n");
6062
+ }
6063
+
5972
6064
  // src/index.ts
5973
- var import_node_path20 = require("node:path");
6065
+ var import_node_path21 = require("node:path");
5974
6066
 
5975
6067
  // src/merge-ci-policy.ts
5976
6068
  function resolveMergeCiPolicy(input) {
@@ -6912,7 +7004,7 @@ async function auditRepoCi(repo, deps) {
6912
7004
  ok: hasRequiredChecks,
6913
7005
  label: "product required status checks active",
6914
7006
  detail: hasRequiredChecks ? void 0 : `no required status checks active \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset`,
6915
- remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
7007
+ remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap apply --execute automation (#1440)`
6916
7008
  });
6917
7009
  if (hasGateWorkflow) {
6918
7010
  const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
@@ -7221,6 +7313,13 @@ async function runPrLand(prNumber, options, deps) {
7221
7313
  error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
7222
7314
  };
7223
7315
  }
7316
+ const prState = await deps.readPrState(prNumber, repo);
7317
+ if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
7318
+ return {
7319
+ ...base,
7320
+ error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
7321
+ };
7322
+ }
7224
7323
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
7225
7324
  base.mergeStatus = merge.mergeStatus;
7226
7325
  if (merge.mergeStatus === "failed") {
@@ -7241,10 +7340,10 @@ async function runPrLand(prNumber, options, deps) {
7241
7340
  }
7242
7341
 
7243
7342
  // src/wave-status.ts
7244
- var import_node_fs11 = require("node:fs");
7343
+ var import_node_fs12 = require("node:fs");
7245
7344
 
7246
7345
  // src/gc.ts
7247
- var import_node_path10 = require("node:path");
7346
+ var import_node_path11 = require("node:path");
7248
7347
  var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
7249
7348
  var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
7250
7349
  var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
@@ -7515,8 +7614,8 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
7515
7614
  return { ok: true, path: worktreePath };
7516
7615
  }
7517
7616
  function siblingMmiWorktreesRoot(repoRoot) {
7518
- const parent = (0, import_node_path10.dirname)(repoRoot);
7519
- return (0, import_node_path10.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path10.join)(parent, "mmi-worktrees");
7617
+ const parent = (0, import_node_path11.dirname)(repoRoot);
7618
+ return (0, import_node_path11.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path11.join)(parent, "mmi-worktrees");
7520
7619
  }
7521
7620
  function classifySiblingWorktreeDir(entry) {
7522
7621
  if (!entry.ownedByCurrentRepo) {
@@ -8177,8 +8276,8 @@ function formatGcPlan(plan, apply) {
8177
8276
 
8178
8277
  // src/stage-runner.ts
8179
8278
  var import_node_child_process6 = require("node:child_process");
8180
- var import_node_fs10 = require("node:fs");
8181
- var import_node_path11 = require("node:path");
8279
+ var import_node_fs11 = require("node:fs");
8280
+ var import_node_path12 = require("node:path");
8182
8281
  var import_node_net = require("node:net");
8183
8282
  var import_node_util6 = require("node:util");
8184
8283
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
@@ -8295,11 +8394,11 @@ function appendForceRecreate(up) {
8295
8394
  return `${up.trimEnd()} --force-recreate`;
8296
8395
  }
8297
8396
  function stageStatePath(cwd = process.cwd()) {
8298
- return (0, import_node_path11.join)(cwd, "tmp", "stage", "state.json");
8397
+ return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
8299
8398
  }
8300
8399
  function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
8301
- const dir = (0, import_node_path11.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path11.resolve)(cwd, gitCommonDir);
8302
- return (0, import_node_path11.join)(dir, "mmi", "stage", "state.json");
8400
+ const dir = (0, import_node_path12.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path12.resolve)(cwd, gitCommonDir);
8401
+ return (0, import_node_path12.join)(dir, "mmi", "stage", "state.json");
8303
8402
  }
8304
8403
  function normPath2(path2) {
8305
8404
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -8523,14 +8622,14 @@ function stageProcessEnv(stagePort, extraEnv) {
8523
8622
  }
8524
8623
  async function ensureStageRuntimeEnv(config, opts, cwd) {
8525
8624
  if (!config.ensureEnv) return;
8526
- const target = (0, import_node_path11.join)(cwd, config.ensureEnv.target);
8527
- const example = (0, import_node_path11.join)(cwd, config.ensureEnv.example);
8528
- if (!(0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(example)) {
8529
- (0, import_node_fs10.copyFileSync)(example, target);
8530
- } else if ((0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(example)) {
8531
- const stale = detectStaleEnvFile((0, import_node_fs10.readFileSync)(example, "utf8"), (0, import_node_fs10.readFileSync)(target, "utf8"), {
8532
- exampleMtimeMs: (0, import_node_fs10.statSync)(example).mtimeMs,
8533
- targetMtimeMs: (0, import_node_fs10.statSync)(target).mtimeMs
8625
+ const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
8626
+ const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
8627
+ if (!(0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8628
+ (0, import_node_fs11.copyFileSync)(example, target);
8629
+ } else if ((0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8630
+ const stale = detectStaleEnvFile((0, import_node_fs11.readFileSync)(example, "utf8"), (0, import_node_fs11.readFileSync)(target, "utf8"), {
8631
+ exampleMtimeMs: (0, import_node_fs11.statSync)(example).mtimeMs,
8632
+ targetMtimeMs: (0, import_node_fs11.statSync)(target).mtimeMs
8534
8633
  });
8535
8634
  if (stale) {
8536
8635
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -8538,8 +8637,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
8538
8637
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
8539
8638
  }
8540
8639
  }
8541
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs10.existsSync)(target)) {
8542
- (0, import_node_fs10.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs10.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
8640
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs11.existsSync)(target)) {
8641
+ (0, import_node_fs11.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs11.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
8543
8642
  }
8544
8643
  }
8545
8644
  async function gitText(cwd, args) {
@@ -8569,20 +8668,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
8569
8668
  return void 0;
8570
8669
  }
8571
8670
  function readState(path2) {
8572
- if (!(0, import_node_fs10.existsSync)(path2)) return null;
8671
+ if (!(0, import_node_fs11.existsSync)(path2)) return null;
8573
8672
  try {
8574
- return JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
8673
+ return JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
8575
8674
  } catch {
8576
8675
  return null;
8577
8676
  }
8578
8677
  }
8579
8678
  function mkdirFor(path2) {
8580
8679
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
8581
- (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
8680
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8582
8681
  }
8583
8682
  function writeState(path2, state) {
8584
8683
  mkdirFor(path2);
8585
- (0, import_node_fs10.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8684
+ (0, import_node_fs11.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8586
8685
  }
8587
8686
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
8588
8687
  const reservation = {
@@ -8602,7 +8701,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
8602
8701
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
8603
8702
  }
8604
8703
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
8605
- (0, import_node_fs10.rmSync)(path2, { force: true });
8704
+ (0, import_node_fs11.rmSync)(path2, { force: true });
8606
8705
  }
8607
8706
  }
8608
8707
  async function killTree(pid) {
@@ -8758,8 +8857,8 @@ async function runStage(config = {}, opts = {}) {
8758
8857
  await ensureStageRuntimeEnv(config, opts, cwd);
8759
8858
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
8760
8859
  } catch (e) {
8761
- (0, import_node_fs10.rmSync)(statePath, { force: true });
8762
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs10.rmSync)(globalStatePath, { force: true });
8860
+ (0, import_node_fs11.rmSync)(statePath, { force: true });
8861
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs11.rmSync)(globalStatePath, { force: true });
8763
8862
  throw e;
8764
8863
  }
8765
8864
  const started = await startStage(config, {
@@ -8784,9 +8883,9 @@ function parseNextFromHead(headText) {
8784
8883
  }
8785
8884
  function readStageSummary(worktreePath) {
8786
8885
  const statePath = stageStatePath(worktreePath);
8787
- if (!(0, import_node_fs11.existsSync)(statePath)) return void 0;
8886
+ if (!(0, import_node_fs12.existsSync)(statePath)) return void 0;
8788
8887
  try {
8789
- const state = JSON.parse((0, import_node_fs11.readFileSync)(statePath, "utf8"));
8888
+ const state = JSON.parse((0, import_node_fs12.readFileSync)(statePath, "utf8"));
8790
8889
  const port = typeof state.port === "number" ? state.port : void 0;
8791
8890
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
8792
8891
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -8902,7 +9001,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
8902
9001
  // src/gh-create.ts
8903
9002
  var import_promises = require("node:fs/promises");
8904
9003
  var import_node_os3 = require("node:os");
8905
- var import_node_path12 = require("node:path");
9004
+ var import_node_path13 = require("node:path");
8906
9005
  var import_node_crypto3 = require("node:crypto");
8907
9006
  var ISSUE_TYPES = ["bug", "feature", "task"];
8908
9007
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -8945,8 +9044,8 @@ async function bodyArgsViaFile(args, deps = {}) {
8945
9044
  const remove = deps.remove ?? import_promises.unlink;
8946
9045
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
8947
9046
  const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
8948
- const file = (0, import_node_path12.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
8949
- await ensureDir((0, import_node_path12.dirname)(file), { recursive: true }).catch(() => {
9047
+ const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
9048
+ await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
8950
9049
  });
8951
9050
  await write(file, args[i + 1], "utf8");
8952
9051
  return {
@@ -12000,8 +12099,8 @@ async function announceRelease(deps, args) {
12000
12099
  }
12001
12100
 
12002
12101
  // src/port-registry.ts
12003
- var import_node_fs12 = require("node:fs");
12004
- var import_node_path13 = require("node:path");
12102
+ var import_node_fs13 = require("node:fs");
12103
+ var import_node_path14 = require("node:path");
12005
12104
 
12006
12105
  // ../infra/port-geometry.mjs
12007
12106
  var PORT_BLOCK = 100;
@@ -12015,8 +12114,8 @@ function nextPortBlock(registry2) {
12015
12114
  return [base, base + PORT_SPAN];
12016
12115
  }
12017
12116
  function loadPortRegistry(path2) {
12018
- if (!(0, import_node_fs12.existsSync)(path2)) return {};
12019
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
12117
+ if (!(0, import_node_fs13.existsSync)(path2)) return {};
12118
+ const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
12020
12119
  const out = {};
12021
12120
  for (const [key, value] of Object.entries(raw)) {
12022
12121
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -12030,9 +12129,9 @@ function ensurePortRange(repo, path2) {
12030
12129
  const existing = registry2[repo];
12031
12130
  if (existing) return existing;
12032
12131
  const range = nextPortBlock(registry2);
12033
- const raw = (0, import_node_fs12.existsSync)(path2) ? JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8")) : {};
12132
+ const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
12034
12133
  raw[repo] = range;
12035
- (0, import_node_fs12.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12134
+ (0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12036
12135
  return range;
12037
12136
  }
12038
12137
  function portCursorSeed(registry2) {
@@ -12054,20 +12153,26 @@ function existingPortRange(repo, registry2) {
12054
12153
  return registry2[repo] ?? null;
12055
12154
  }
12056
12155
  function portRangeInfraAt(root, source) {
12057
- const registryPath = (0, import_node_path13.join)(root, "infra", "port-ranges.json");
12058
- const ddbScriptPath = (0, import_node_path13.join)(root, "infra", "port-ddb.mjs");
12059
- if (!(0, import_node_fs12.existsSync)(registryPath) || !(0, import_node_fs12.existsSync)(ddbScriptPath)) return null;
12156
+ const registryPath = (0, import_node_path14.join)(root, "infra", "port-ranges.json");
12157
+ const ddbScriptPath = (0, import_node_path14.join)(root, "infra", "port-ddb.mjs");
12158
+ if (!(0, import_node_fs13.existsSync)(registryPath) || !(0, import_node_fs13.existsSync)(ddbScriptPath)) return null;
12060
12159
  return { root, source, registryPath, ddbScriptPath };
12061
12160
  }
12062
- function resolvePortRangeInfra(cwd) {
12161
+ function resolvePortRangeInfra(cwd, packageDir) {
12063
12162
  const direct = portRangeInfraAt(cwd, "cwd");
12064
12163
  if (direct) return direct;
12065
- for (let dir = cwd; ; dir = (0, import_node_path13.dirname)(dir)) {
12066
- const sibling = portRangeInfraAt((0, import_node_path13.join)(dir, "MMI-Hub"), "sibling-hub");
12164
+ for (let dir = cwd; ; dir = (0, import_node_path14.dirname)(dir)) {
12165
+ const sibling = portRangeInfraAt((0, import_node_path14.join)(dir, "MMI-Hub"), "sibling-hub");
12067
12166
  if (sibling) return sibling;
12068
- const parent = (0, import_node_path13.dirname)(dir);
12069
- if (parent === dir) return null;
12167
+ const parent = (0, import_node_path14.dirname)(dir);
12168
+ if (parent === dir) break;
12070
12169
  }
12170
+ if (packageDir) {
12171
+ const pkgRoot = (0, import_node_path14.join)(packageDir, "..", "..");
12172
+ const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
12173
+ if (pkgFrom) return pkgFrom;
12174
+ }
12175
+ return null;
12071
12176
  }
12072
12177
  async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
12073
12178
  const registry2 = loadPortRegistry(path2);
@@ -12954,27 +13059,39 @@ async function tenantDeploy(payload, deps) {
12954
13059
  // src/wiki-publish-command.ts
12955
13060
  async function wikiPublish(deps, opts) {
12956
13061
  if (!opts.pagesDir) {
12957
- deps.err("wiki publish: <pages-dir> is required");
13062
+ if (opts.json) {
13063
+ deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
13064
+ } else {
13065
+ deps.err("wiki publish: <pages-dir> is required");
13066
+ }
12958
13067
  return false;
12959
13068
  }
12960
13069
  const minted = await deps.mint(opts.repo);
12961
13070
  if (!minted.ok) {
12962
- deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13071
+ if (opts.json) {
13072
+ deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
13073
+ } else {
13074
+ deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13075
+ }
12963
13076
  return false;
12964
13077
  }
12965
- const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir });
13078
+ const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
12966
13079
  if (!ok) {
12967
13080
  return false;
12968
13081
  }
12969
- deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13082
+ if (opts.json) {
13083
+ deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
13084
+ } else {
13085
+ deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13086
+ }
12970
13087
  return true;
12971
13088
  }
12972
13089
 
12973
13090
  // src/wiki-publish-git.ts
12974
13091
  var import_node_child_process8 = require("node:child_process");
12975
- var import_node_fs13 = require("node:fs");
13092
+ var import_node_fs14 = require("node:fs");
12976
13093
  var import_node_os5 = require("node:os");
12977
- var import_node_path14 = require("node:path");
13094
+ var import_node_path15 = require("node:path");
12978
13095
  var WIKI_STAMP_FILE = ".wiki-state.json";
12979
13096
  function redactWikiSecrets(msg) {
12980
13097
  return msg.replace(/x-access-token:[^@\s]+@/gi, "x-access-token:***@").replace(/(AUTHORIZATION:\s*basic\s+)[A-Za-z0-9+/=]+/gi, "$1***");
@@ -12986,6 +13103,7 @@ function publishWikiViaGit(deps, opts) {
12986
13103
  const dir = deps.mkdtemp();
12987
13104
  const url = `https://github.com/${opts.repo}.wiki.git`;
12988
13105
  const authArgs = wikiAuthArgs(opts.token);
13106
+ const json = opts.json ?? false;
12989
13107
  try {
12990
13108
  deps.gitRun(["init", "-q"], dir);
12991
13109
  deps.gitRun(["remote", "add", "origin", url], dir);
@@ -12995,20 +13113,20 @@ function publishWikiViaGit(deps, opts) {
12995
13113
  let copied = 0;
12996
13114
  for (const f of deps.readdir(opts.pagesDir)) {
12997
13115
  if (f.endsWith(".md")) {
12998
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, f), (0, import_node_path14.join)(dir, f));
13116
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, f), (0, import_node_path15.join)(dir, f));
12999
13117
  copied++;
13000
13118
  }
13001
13119
  }
13002
13120
  if (!copied) {
13003
- deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
13121
+ if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
13004
13122
  return false;
13005
13123
  }
13006
- if (deps.fileExists((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
13007
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path14.join)(dir, WIKI_STAMP_FILE));
13124
+ if (deps.fileExists((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
13125
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path15.join)(dir, WIKI_STAMP_FILE));
13008
13126
  }
13009
13127
  deps.gitRun(["add", "-A"], dir);
13010
13128
  if (deps.gitProbe(["diff", "--cached", "--quiet"], dir)) {
13011
- deps.log("wiki publish: already up to date");
13129
+ if (!json) deps.log("wiki publish: already up to date");
13012
13130
  return true;
13013
13131
  }
13014
13132
  deps.gitRun(
@@ -13026,15 +13144,15 @@ function publishWikiViaGit(deps, opts) {
13026
13144
  try {
13027
13145
  deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
13028
13146
  } catch (e) {
13029
- deps.err(
13147
+ if (!json) deps.err(
13030
13148
  `wiki publish failed: push to ${opts.repo}.wiki.git rejected: ${redactWikiSecrets(e.message)} \u2014 if the wiki advanced before this push, re-run wiki publish to pick up the latest wiki state`
13031
13149
  );
13032
13150
  return false;
13033
13151
  }
13034
- deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
13152
+ if (!json) deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
13035
13153
  return true;
13036
13154
  } catch (e) {
13037
- deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
13155
+ if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
13038
13156
  return false;
13039
13157
  } finally {
13040
13158
  deps.rm(dir);
@@ -13054,7 +13172,7 @@ function readWikiStampViaGit(deps, opts) {
13054
13172
  return { ok: true, stamp: {} };
13055
13173
  }
13056
13174
  deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
13057
- const stampPath = (0, import_node_path14.join)(dir, WIKI_STAMP_FILE);
13175
+ const stampPath = (0, import_node_path15.join)(dir, WIKI_STAMP_FILE);
13058
13176
  if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
13059
13177
  const parsed = JSON.parse(deps.readFile(stampPath));
13060
13178
  return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
@@ -13066,12 +13184,12 @@ function readWikiStampViaGit(deps, opts) {
13066
13184
  }
13067
13185
  function realWikiGitPublishDeps() {
13068
13186
  return {
13069
- mkdtemp: () => (0, import_node_fs13.mkdtempSync)((0, import_node_path14.join)((0, import_node_os5.tmpdir)(), "wiki-")),
13070
- readdir: (dir) => (0, import_node_fs13.readdirSync)(dir),
13071
- copyFile: (src, dest) => (0, import_node_fs13.copyFileSync)(src, dest),
13072
- fileExists: (p) => (0, import_node_fs13.existsSync)(p),
13073
- readFile: (p) => (0, import_node_fs13.readFileSync)(p, "utf8"),
13074
- rm: (p) => (0, import_node_fs13.rmSync)(p, { recursive: true, force: true }),
13187
+ mkdtemp: () => (0, import_node_fs14.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "wiki-")),
13188
+ readdir: (dir) => (0, import_node_fs14.readdirSync)(dir),
13189
+ copyFile: (src, dest) => (0, import_node_fs14.copyFileSync)(src, dest),
13190
+ fileExists: (p) => (0, import_node_fs14.existsSync)(p),
13191
+ readFile: (p) => (0, import_node_fs14.readFileSync)(p, "utf8"),
13192
+ rm: (p) => (0, import_node_fs14.rmSync)(p, { recursive: true, force: true }),
13075
13193
  gitProbe: (args, cwd) => {
13076
13194
  try {
13077
13195
  (0, import_node_child_process8.execFileSync)("git", args, { cwd, stdio: "pipe" });
@@ -13090,8 +13208,8 @@ function realWikiGitPublishDeps() {
13090
13208
 
13091
13209
  // src/wiki-pages.ts
13092
13210
  var import_node_crypto4 = require("node:crypto");
13093
- var import_node_fs14 = require("node:fs");
13094
- var import_node_path15 = require("node:path");
13211
+ var import_node_fs15 = require("node:fs");
13212
+ var import_node_path16 = require("node:path");
13095
13213
  var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
13096
13214
  var attr = (raw, key) => {
13097
13215
  const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
@@ -13117,15 +13235,15 @@ var DURABLE_GLOBS = [
13117
13235
  "docs/org-readme.md"
13118
13236
  ];
13119
13237
  var walkMarkdown = (root, rel) => {
13120
- const abs = (0, import_node_path15.join)(root, rel);
13238
+ const abs = (0, import_node_path16.join)(root, rel);
13121
13239
  let st;
13122
13240
  try {
13123
- st = (0, import_node_fs14.statSync)(abs);
13241
+ st = (0, import_node_fs15.statSync)(abs);
13124
13242
  } catch {
13125
13243
  return [];
13126
13244
  }
13127
13245
  if (st.isFile()) return rel.endsWith(".md") ? [rel] : [];
13128
- return (0, import_node_fs14.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path15.join)(rel, name)));
13246
+ return (0, import_node_fs15.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path16.join)(rel, name)));
13129
13247
  };
13130
13248
  function collectFeaturePages({ listDocs, readDoc }) {
13131
13249
  const seen = /* @__PURE__ */ new Set();
@@ -13144,7 +13262,7 @@ function fsReaders(rootDir) {
13144
13262
  listDocs: () => DURABLE_GLOBS.flatMap((g) => walkMarkdown(rootDir, g)),
13145
13263
  readDoc: (p) => {
13146
13264
  try {
13147
- return (0, import_node_fs14.readFileSync)((0, import_node_path15.join)(rootDir, p), "utf8");
13265
+ return (0, import_node_fs15.readFileSync)((0, import_node_path16.join)(rootDir, p), "utf8");
13148
13266
  } catch {
13149
13267
  return "";
13150
13268
  }
@@ -14473,7 +14591,12 @@ async function targetRepo(deps, opts) {
14473
14591
  return opts.repo ?? repoOf(await vaultSlug(deps, opts));
14474
14592
  }
14475
14593
  async function secretsWhere(deps, opts) {
14476
- deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
14594
+ const p = vaultPointer(await vaultSlug(deps, opts));
14595
+ if (opts.json) {
14596
+ deps.log(JSON.stringify(p, null, 2));
14597
+ return;
14598
+ }
14599
+ deps.log(formatVaultPointer(p));
14477
14600
  }
14478
14601
  async function readErr(res) {
14479
14602
  try {
@@ -14536,6 +14659,10 @@ async function secretsList(deps, opts) {
14536
14659
  return;
14537
14660
  }
14538
14661
  const { secrets } = await res.json();
14662
+ if (opts.json) {
14663
+ deps.log(JSON.stringify({ secrets: secrets ?? [] }, null, 2));
14664
+ return;
14665
+ }
14539
14666
  deps.log(formatSecretList(secrets ?? []));
14540
14667
  }
14541
14668
  var CAPABILITIES_TIMEOUT_MS = 2e4;
@@ -14751,6 +14878,13 @@ function requiredRuntimeSecretNames(stage2, contract, opts = {}) {
14751
14878
  function stageKey2(stage2, key) {
14752
14879
  return key.includes("/") ? key : `${stage2}/${key}`;
14753
14880
  }
14881
+ function keyDeclaredPerStage(declaredSecrets, key) {
14882
+ if (!declaredSecrets) return void 0;
14883
+ const entry = declaredSecrets[key];
14884
+ if (!entry) return void 0;
14885
+ if (!Array.isArray(entry.stages) || entry.stages.length === 0) return false;
14886
+ return true;
14887
+ }
14754
14888
  async function secretsPreflight(deps, opts) {
14755
14889
  const repo = await targetRepo(deps, opts);
14756
14890
  const qs = new URLSearchParams({ repo }).toString();
@@ -14771,9 +14905,23 @@ async function secretsPreflight(deps, opts) {
14771
14905
  }
14772
14906
  const { secrets } = await res.json();
14773
14907
  const present = new Set((secrets ?? []).map((s) => s.key));
14774
- const missing = opts.required.filter(
14775
- (key) => !present.has(stageKey2(opts.stage, key)) && !(key.includes("/") ? false : present.has(key))
14776
- );
14908
+ const missing = opts.required.filter((key) => {
14909
+ const perStage = keyDeclaredPerStage(opts.declaredSecrets, key);
14910
+ if (key.includes("/")) {
14911
+ return !present.has(key);
14912
+ }
14913
+ if (perStage === false) {
14914
+ return !present.has(key);
14915
+ }
14916
+ if (perStage === true) {
14917
+ return !present.has(stageKey2(opts.stage, key));
14918
+ }
14919
+ return !present.has(stageKey2(opts.stage, key)) && !present.has(key);
14920
+ });
14921
+ if (opts.json) {
14922
+ deps.log(JSON.stringify({ ok: missing.length === 0, stage: opts.stage, required: opts.required, missing, present: Array.from(present) }, null, 2));
14923
+ return missing.length === 0;
14924
+ }
14777
14925
  if (missing.length) {
14778
14926
  deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
14779
14927
  return false;
@@ -14998,9 +15146,6 @@ async function secretsImportRailsCredentials(deps, opts) {
14998
15146
  }
14999
15147
  return true;
15000
15148
  }
15001
- async function secretsEdit(deps, key, opts) {
15002
- return secretsSet(deps, key, opts);
15003
- }
15004
15149
  async function secretsRemove(deps, key, opts) {
15005
15150
  if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
15006
15151
  const repo = await targetRepo(deps, opts);
@@ -15096,8 +15241,12 @@ async function secretsCopy(deps, opts) {
15096
15241
  }
15097
15242
  return true;
15098
15243
  }
15244
+ function resolveSpawnTarget(command, args, platform2 = process.platform) {
15245
+ return platform2 === "win32" ? { cmd: "cmd.exe", args: ["/d", "/s", "/c", command, ...args] } : { cmd: command, args };
15246
+ }
15099
15247
  function defaultSpawn(command, args, env) {
15100
- const r = (0, import_node_child_process9.spawnSync)(command, args, { stdio: "inherit", env });
15248
+ const target = resolveSpawnTarget(command, args);
15249
+ const r = (0, import_node_child_process9.spawnSync)(target.cmd, target.args, { stdio: "inherit", env });
15101
15250
  if (r.error) throw r.error;
15102
15251
  return r.status ?? 1;
15103
15252
  }
@@ -15157,8 +15306,8 @@ async function secretsUse(deps, key, opts) {
15157
15306
  }
15158
15307
 
15159
15308
  // src/secrets-commands.ts
15160
- var import_node_fs15 = require("node:fs");
15161
- var import_node_path16 = require("node:path");
15309
+ var import_node_fs16 = require("node:fs");
15310
+ var import_node_path17 = require("node:path");
15162
15311
  var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
15163
15312
  var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
15164
15313
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
@@ -15166,18 +15315,18 @@ function collectMap(value, previous = []) {
15166
15315
  return [...previous, value];
15167
15316
  }
15168
15317
  async function decryptRailsCredentials(input) {
15169
- const appDir = (0, import_node_path16.resolve)(input.appDir ?? process.cwd());
15318
+ const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
15170
15319
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
15171
15320
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
15172
- const credentialsPath = (0, import_node_path16.resolve)(appDir, credentialsFile);
15173
- const masterKeyPath = (0, import_node_path16.resolve)(appDir, masterKeyFile);
15321
+ const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
15322
+ const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
15174
15323
  const env = {
15175
15324
  ...process.env,
15176
15325
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
15177
15326
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
15178
15327
  };
15179
- if ((0, import_node_fs15.existsSync)(masterKeyPath)) {
15180
- env.RAILS_MASTER_KEY = (0, import_node_fs15.readFileSync)(masterKeyPath, "utf8").trim();
15328
+ if ((0, import_node_fs16.existsSync)(masterKeyPath)) {
15329
+ env.RAILS_MASTER_KEY = (0, import_node_fs16.readFileSync)(masterKeyPath, "utf8").trim();
15181
15330
  }
15182
15331
  const script = [
15183
15332
  'require "json"',
@@ -15231,8 +15380,8 @@ async function withSecrets(run) {
15231
15380
  }
15232
15381
  function registerSecretsCommands(program3) {
15233
15382
  const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
15234
- secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
15235
- secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
15383
+ secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
15384
+ secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
15236
15385
  secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
15237
15386
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
15238
15387
  }));
@@ -15243,13 +15392,13 @@ function registerSecretsCommands(program3) {
15243
15392
  secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
15244
15393
  let body;
15245
15394
  try {
15246
- body = (0, import_node_fs15.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
15395
+ body = (0, import_node_fs16.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
15247
15396
  } catch (e) {
15248
15397
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
15249
15398
  }
15250
15399
  if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
15251
15400
  }));
15252
- secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
15401
+ secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--json", "machine-readable output").action(async (o) => {
15253
15402
  if (!["dev", "rc", "main"].includes(o.stage)) {
15254
15403
  return fail("secrets preflight: --stage must be dev, rc, or main");
15255
15404
  }
@@ -15270,7 +15419,7 @@ function registerSecretsCommands(program3) {
15270
15419
  if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
15271
15420
  d.err('secrets preflight: requiredRuntimeSecrets is unset: treating as an empty contract (no required keys). Declare an explicit {"dev":[],"rc":[],"main":[]} in registry META to silence this warning.');
15272
15421
  }
15273
- const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
15422
+ const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required, json: o.json, declaredSecrets: meta?.secrets });
15274
15423
  if (!ok) process.exitCode = 1;
15275
15424
  });
15276
15425
  secrets.command("get <key>").description("print one secret value, RAW \u2014 MASTER-ONLY (WS6 #2184); non-master: `secrets use` to consume keyless or `secrets verify` to validate").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "vault slug override for org-infra namespaces (cloudflare, shared, \u2026) \u2014 master-gated").action((key, o) => withSecrets(async (d) => {
@@ -15285,14 +15434,12 @@ function registerSecretsCommands(program3) {
15285
15434
  const ok = await secretsVerify(d, key, o);
15286
15435
  if (!ok) process.exitCode = 1;
15287
15436
  }));
15288
- secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
15437
+ const setHandler = (key, o) => withSecrets(async (d) => {
15289
15438
  const ok = await secretsSet(d, key, o);
15290
15439
  if (!ok) process.exitCode = 1;
15291
- }));
15292
- secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
15293
- const ok = await secretsEdit(d, key, o);
15294
- if (!ok) process.exitCode = 1;
15295
- }));
15440
+ });
15441
+ secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
15442
+ secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
15296
15443
  secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
15297
15444
  const stages = ["dev", "rc", "main"];
15298
15445
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
@@ -15318,7 +15465,7 @@ function registerSecretsCommands(program3) {
15318
15465
  {
15319
15466
  ...d,
15320
15467
  decryptRailsCredentials,
15321
- removeFile: (path2) => (0, import_node_fs15.unlinkSync)((0, import_node_path16.resolve)(o.appDir ?? process.cwd(), path2))
15468
+ removeFile: (path2) => (0, import_node_fs16.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
15322
15469
  },
15323
15470
  {
15324
15471
  repo: o.repo,
@@ -15407,10 +15554,10 @@ function registerEdgeCommands(program3) {
15407
15554
  }
15408
15555
 
15409
15556
  // src/doctor-run.ts
15410
- var import_node_fs19 = require("node:fs");
15557
+ var import_node_fs20 = require("node:fs");
15411
15558
  var import_node_child_process11 = require("node:child_process");
15412
15559
  var import_promises2 = require("node:fs/promises");
15413
- var import_node_path19 = require("node:path");
15560
+ var import_node_path20 = require("node:path");
15414
15561
  var import_node_os7 = require("node:os");
15415
15562
 
15416
15563
  // src/plugin-guard.ts
@@ -15433,13 +15580,14 @@ function buildGuardSessionStartLine(state, opts = {}) {
15433
15580
 
15434
15581
  // src/cursor-plugin-seed.ts
15435
15582
  var import_node_child_process10 = require("node:child_process");
15436
- var import_node_fs16 = require("node:fs");
15583
+ var import_node_fs17 = require("node:fs");
15437
15584
  var import_node_os6 = require("node:os");
15438
- var import_node_path17 = require("node:path");
15585
+ var import_node_path18 = require("node:path");
15439
15586
  var import_node_util7 = require("node:util");
15440
15587
 
15441
15588
  // src/doctor.ts
15442
15589
  var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
15590
+ var GITHUB_AUTH_LABEL = "GitHub auth identity (gh ops)";
15443
15591
  var AWS_CROSS_ACCOUNT_LABEL = "AWS cross-account identity (master-agent audits)";
15444
15592
  var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
15445
15593
  var MMI_AGENTIC_ONBOARDING_GUIDE = {
@@ -15453,7 +15601,7 @@ function buildGithubAuthCheck(input) {
15453
15601
  const ok = Boolean(input.login?.trim());
15454
15602
  return {
15455
15603
  ok,
15456
- label: "GitHub auth identity (gh ops)",
15604
+ label: GITHUB_AUTH_LABEL,
15457
15605
  fix: input.ghInstalled ? GH_PROJECT_LOGIN_FIX : `install GitHub CLI (https://cli.github.com), then: ${GH_PROJECT_LOGIN_FIX.replace(/^run: /, "")}`
15458
15606
  };
15459
15607
  }
@@ -15631,6 +15779,136 @@ function buildRepoLocalWorktreeCheck(input) {
15631
15779
  fix: REPO_LOCAL_WORKTREE_FIX
15632
15780
  };
15633
15781
  }
15782
+ var LOCKFILE_OPTIONALS_LABEL = "npm lockfile keeps optionalDependencies across platforms (Windows lockfile drop #2594)";
15783
+ var LOCKFILE_OPTIONALS_FIX = "regenerate the lockfile without platform filtering: `rm package-lock.json && npm install` on a machine that resolves every optionalDependency, then commit it (a Windows-only lockfile drops Linux-only optionals on CI)";
15784
+ function buildLockfileOptionalsCheck(input) {
15785
+ const base = {
15786
+ ok: true,
15787
+ label: LOCKFILE_OPTIONALS_LABEL,
15788
+ fix: LOCKFILE_OPTIONALS_FIX,
15789
+ missingOptionals: []
15790
+ };
15791
+ if (!input.isOrgRepo) return base;
15792
+ const optionalDeps = input.packageJsonOptionalDeps ?? {};
15793
+ const packages = input.lockfilePackages ?? {};
15794
+ const missingOptionals = Object.keys(optionalDeps).filter((name) => !(`node_modules/${name}` in packages));
15795
+ if (missingOptionals.length === 0) return base;
15796
+ return {
15797
+ ...base,
15798
+ ok: false,
15799
+ missingOptionals,
15800
+ detail: `${missingOptionals.length} optionalDependency entr${missingOptionals.length === 1 ? "y" : "ies"} missing from the lockfile packages map: ${missingOptionals.join(", ")}`
15801
+ };
15802
+ }
15803
+ var FILE_ENCODING_LABEL = "managed file encoding (UTF-8, no BOM, no mojibake)";
15804
+ var FILE_ENCODING_FIX = 're-save the flagged file(s) as UTF-8 without BOM (VS Code: status-bar encoding button \u2192 "Save with Encoding" \u2192 UTF-8), and set `PYTHONUTF8=1` for Python tooling on Windows so it never falls back to cp1252';
15805
+ function inspectFileEncoding(bytes) {
15806
+ const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
15807
+ let invalidUtf8 = false;
15808
+ let replacementChars = false;
15809
+ try {
15810
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
15811
+ replacementChars = text.includes("\uFFFD");
15812
+ } catch {
15813
+ invalidUtf8 = true;
15814
+ replacementChars = new TextDecoder("utf-8", { fatal: false }).decode(bytes).includes("\uFFFD");
15815
+ }
15816
+ return { bom, invalidUtf8, replacementChars };
15817
+ }
15818
+ function buildManagedFileEncodingCheck(input) {
15819
+ const base = {
15820
+ ok: true,
15821
+ label: FILE_ENCODING_LABEL,
15822
+ fix: FILE_ENCODING_FIX,
15823
+ findings: []
15824
+ };
15825
+ if (!input.isOrgRepo) return base;
15826
+ const defective = input.findings.filter((f) => f.bom || f.invalidUtf8 || f.replacementChars);
15827
+ if (defective.length === 0) return base;
15828
+ return {
15829
+ ...base,
15830
+ ok: false,
15831
+ severityOverride: "advisory",
15832
+ findings: defective,
15833
+ detail: `${defective.length} managed file(s) not clean UTF-8: ${defective.map((f) => f.path).join(", ")}`
15834
+ };
15835
+ }
15836
+ var LOCAL_BEHIND_ORIGIN_LABEL = "local release branches current with origin (development/main/rc)";
15837
+ var LOCAL_BEHIND_ORIGIN_FIX = "run `git fetch origin && git switch <branch> && git pull --ff-only` for each flagged branch (purely behind, so the fast-forward is safe)";
15838
+ var RELEASE_TRACKING_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master", "rc", "release"]);
15839
+ function isReleaseTrackingBranch(name) {
15840
+ if (!name) return false;
15841
+ const base = name.split("/").pop() ?? name;
15842
+ if (RELEASE_TRACKING_BRANCHES.has(base)) return true;
15843
+ return name.split("/")[0] === "release";
15844
+ }
15845
+ function buildStaleLocalBranchCheck(input) {
15846
+ const base = {
15847
+ ok: true,
15848
+ label: LOCAL_BEHIND_ORIGIN_LABEL,
15849
+ fix: LOCAL_BEHIND_ORIGIN_FIX,
15850
+ stale: []
15851
+ };
15852
+ if (!input.isOrgRepo) return base;
15853
+ const stale = input.branches.filter((b) => isReleaseTrackingBranch(b.name) && b.behind > 0 && b.ahead === 0).map((b) => ({ name: b.name, behind: b.behind }));
15854
+ if (stale.length === 0) return base;
15855
+ return {
15856
+ ...base,
15857
+ ok: false,
15858
+ severityOverride: "advisory",
15859
+ stale,
15860
+ detail: `${stale.length} release branch(es) behind origin: ${stale.map((s) => `${s.name} (-${s.behind})`).join(", ")}`
15861
+ };
15862
+ }
15863
+ var REDACTOR_LIVENESS_LABEL = "secret-redact hook liveness (PostToolUse redactor crashes)";
15864
+ var REDACTOR_LIVENESS_FIX = 'grep the hook activity log for \'"script":"secret-redact","outcome":"failed"\' (path: `git rev-parse --git-path mmi-runtime/hooks/activity.jsonl`); reinstall the MMI plugin or run `mmi-cli doctor --apply` to repair, then reopen the session \u2014 the redactor stayed fail-open so tool calls proceeded, but secrets may have passed through unredacted while it was down';
15865
+ function summarizeRedactorFailures(lines) {
15866
+ const parsed = [];
15867
+ for (const raw of lines) {
15868
+ const trimmed = raw.trim();
15869
+ if (!trimmed) continue;
15870
+ try {
15871
+ parsed.push(JSON.parse(trimmed));
15872
+ } catch {
15873
+ }
15874
+ }
15875
+ let sessionStartIdx = -1;
15876
+ for (let i = 0; i < parsed.length; i += 1) {
15877
+ if (parsed[i].event === "SessionStart") sessionStartIdx = i;
15878
+ }
15879
+ const start = sessionStartIdx >= 0 ? sessionStartIdx : 0;
15880
+ let failedCount = 0;
15881
+ let lastFailedTs;
15882
+ let lastFailedAction;
15883
+ for (let i = start; i < parsed.length; i += 1) {
15884
+ const e = parsed[i];
15885
+ if (e.script === "secret-redact" && e.outcome === "failed") {
15886
+ failedCount += 1;
15887
+ lastFailedTs = e.ts;
15888
+ lastFailedAction = e.action;
15889
+ }
15890
+ }
15891
+ return { failedCount, lastFailedTs, lastFailedAction };
15892
+ }
15893
+ function buildRedactorLivenessCheck(input) {
15894
+ const base = {
15895
+ ok: true,
15896
+ label: REDACTOR_LIVENESS_LABEL,
15897
+ fix: REDACTOR_LIVENESS_FIX,
15898
+ failedCount: 0
15899
+ };
15900
+ if (!input.isOrgRepo) return base;
15901
+ if (input.failedCount === 0) return base;
15902
+ const when = input.lastFailedTs ? ` (most recent ${input.lastFailedTs})` : "";
15903
+ const reason = input.lastFailedAction ? `: ${input.lastFailedAction}` : "";
15904
+ return {
15905
+ ok: false,
15906
+ label: REDACTOR_LIVENESS_LABEL,
15907
+ fix: `secret-redact crashed ${input.failedCount} time(s) this session${when}${reason}. ${REDACTOR_LIVENESS_FIX}`,
15908
+ failedCount: input.failedCount,
15909
+ lastFailedTs: input.lastFailedTs
15910
+ };
15911
+ }
15634
15912
  var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
15635
15913
  var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
15636
15914
  function buildScratchGcCheck(plan) {
@@ -16089,7 +16367,16 @@ function buildDoctorJsonPayload(input) {
16089
16367
  return {
16090
16368
  ok: doctorExitCode(input.checks) === 0,
16091
16369
  hasAdvisories: input.checks.some((c) => !c.ok && isAdvisoryDoctorCheck(c)),
16092
- checks: input.checks,
16370
+ checks: input.checks.map((check) => {
16371
+ const metadata = doctorMetadataForCheck(check);
16372
+ return {
16373
+ ...check,
16374
+ ...metadata,
16375
+ label: check.label,
16376
+ metadata,
16377
+ durationMs: check.durationMs ?? 0
16378
+ };
16379
+ }),
16093
16380
  updateReport: input.updateReport,
16094
16381
  ...input.resources.length ? { resources: input.resources } : {}
16095
16382
  };
@@ -16148,6 +16435,25 @@ function buildInstalledPluginVersionCheck(input) {
16148
16435
  staleSurfaces: stale
16149
16436
  };
16150
16437
  }
16438
+ var CLI_VERSION_DRIFT_LABEL = "mmi-cli version drift (installed vs latest published)";
16439
+ var CLI_VERSION_DRIFT_FIX = "run: mmi-cli doctor --apply --no-repo-writes (or npm install -g @mutmutco/cli@latest)";
16440
+ function buildCliVersionDriftCheck(input) {
16441
+ const base = {
16442
+ ok: true,
16443
+ label: CLI_VERSION_DRIFT_LABEL,
16444
+ fix: CLI_VERSION_DRIFT_FIX,
16445
+ currentVersion: input.currentVersion,
16446
+ releasedVersion: input.releasedVersion
16447
+ };
16448
+ if (!input.releasedVersion || !isSemverVersion(input.releasedVersion) || !isSemverVersion(input.currentVersion)) return base;
16449
+ if (compareVersions(input.currentVersion, input.releasedVersion) >= 0) return base;
16450
+ return {
16451
+ ...base,
16452
+ ok: false,
16453
+ label: `${CLI_VERSION_DRIFT_LABEL}: ${input.currentVersion} < ${input.releasedVersion}`,
16454
+ fix: `${CLI_VERSION_DRIFT_FIX} (installed ${input.currentVersion} is behind published ${input.releasedVersion})`
16455
+ };
16456
+ }
16151
16457
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
16152
16458
  function buildOpencodeVersionCheck(input) {
16153
16459
  const fix = pluginRecoveryFix("opencode");
@@ -16445,13 +16751,13 @@ function buildCursorHookCliCheck(input) {
16445
16751
  return base;
16446
16752
  }
16447
16753
  var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
16754
+ var HUB_COMPAT_LABEL = "Hub compatibility (client version vs Hub minimum)";
16448
16755
  function buildHubCompatCheck(input) {
16449
- const label = "Hub compatibility (client version vs Hub minimum)";
16450
16756
  const min = input.versionInfo?.minClientVersion;
16451
16757
  if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
16452
- return { ok: true, label, fix: HUB_COMPAT_FIX };
16758
+ return { ok: true, label: HUB_COMPAT_LABEL, fix: HUB_COMPAT_FIX };
16453
16759
  }
16454
- return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16760
+ return { ok: versionAtLeast(input.installedVersion, min), label: `${HUB_COMPAT_LABEL}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16455
16761
  }
16456
16762
  var HUB_DEPLOY_FRESHNESS_LABEL = "Hub deploy freshness (x-hub-version vs CLI / release)";
16457
16763
  var HUB_DEPLOY_FRESHNESS_FIX = "prod Hub updates on /release (MMI-Hub is direct-track); lag vs development is expected between releases";
@@ -16486,8 +16792,6 @@ function buildHubDeployFreshnessCheck(input) {
16486
16792
  staleAgainst
16487
16793
  };
16488
16794
  }
16489
- var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
16490
- var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
16491
16795
  function textHasPlaywrightMcp(content) {
16492
16796
  const normalized = content.replace(/\r\n/g, "\n");
16493
16797
  return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
@@ -16545,7 +16849,7 @@ function buildSelfUpdateHaltPayload(input) {
16545
16849
  checks: input.checks
16546
16850
  };
16547
16851
  }
16548
- var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
16852
+ var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 2;
16549
16853
  var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
16550
16854
  function selfUpdateHaltSentinelLine(input) {
16551
16855
  const to = input.report.releasedVersion ?? "latest";
@@ -16555,8 +16859,11 @@ var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
16555
16859
  function buildSelfUpdateReexecArgs(opts) {
16556
16860
  const args = ["doctor"];
16557
16861
  if (opts.banner) args.push("--banner");
16862
+ if (opts.fast) args.push("--fast");
16558
16863
  if (opts.preflight) args.push("--preflight");
16559
16864
  if (opts.verbose) args.push("--verbose");
16865
+ if (opts.ci) args.push("--ci");
16866
+ if (opts.strictAdvisory) args.push("--strict-advisory");
16560
16867
  if (opts.guide) args.push("--guide");
16561
16868
  if (opts.json) args.push("--json");
16562
16869
  if (opts.apply) args.push("--apply");
@@ -16577,28 +16884,12 @@ function preflightOutcome(input) {
16577
16884
  function pluginAutonomousHaltLine(reloadHint) {
16578
16885
  return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
16579
16886
  }
16580
- var DOCTOR_EXTENDED_CHECK_LABELS = /* @__PURE__ */ new Set([
16581
- AWS_CROSS_ACCOUNT_LABEL,
16582
- HUB_DEPLOY_FRESHNESS_LABEL,
16583
- PLAYWRIGHT_MCP_VISION_CAP_LABEL,
16584
- PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
16585
- BROWSER_ARTIFACTS_LABEL,
16586
- SCRATCH_GC_LABEL,
16587
- GIT_GC_LABEL,
16588
- OPENCODE_VERSION_LABEL,
16589
- OPENCODE_DESKTOP_BOOTSTRAP_LABEL
16590
- ]);
16591
- function isDoctorExtendedCheck(label) {
16592
- if (DOCTOR_EXTENDED_CHECK_LABELS.has(label)) return true;
16593
- if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return true;
16594
- if (label.startsWith("@mutmutco design-system") || label.startsWith("@mutmutco registry components")) return true;
16595
- return false;
16596
- }
16597
16887
  function isAdvisoryDoctorCheck(check) {
16598
16888
  if (check.severityOverride) return check.severityOverride === "advisory";
16599
- return isDoctorExtendedCheck(check.label);
16889
+ return (check.metadata ?? doctorMetadataForLabel(check.label)).severity === "advisory";
16600
16890
  }
16601
- function doctorExitCode(checks) {
16891
+ function doctorExitCode(checks, opts = {}) {
16892
+ if (opts.doctorError) return 2;
16602
16893
  return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
16603
16894
  }
16604
16895
  function renderPluginUpdateReportStaleOnly(report, allChecksPass = false) {
@@ -16624,6 +16915,7 @@ function doctorVerdictLine(input) {
16624
16915
  const gaps = input.checks.filter((c) => !c.ok);
16625
16916
  const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
16626
16917
  const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
16918
+ const skipSuffix = input.skippedSlowChecks ? ` \xB7 skipped ${input.skippedSlowChecks} slow checks` : "";
16627
16919
  if (input.shouldApply) {
16628
16920
  if (hardGaps.length > 0) {
16629
16921
  return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
@@ -16637,19 +16929,30 @@ function doctorVerdictLine(input) {
16637
16929
  }
16638
16930
  const attention = gaps.length;
16639
16931
  if (attention > 0) {
16640
- return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
16932
+ return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal${skipSuffix}.`;
16641
16933
  }
16642
- return "\u2713 Everything healthy.";
16934
+ return `\u2713 healthy \xB7 ${input.checks.length} checks \xB7 0 gaps${skipSuffix}`;
16935
+ }
16936
+ function doctorCheckSummaryLine(check) {
16937
+ const meta = doctorMetadataForCheck(check);
16938
+ return ` ${doctorCheckGlyph(check)} ${meta.id} \u2014 ${meta.protects.join("; ")} \u2014 ${check.fix}`;
16643
16939
  }
16644
16940
  function doctorHumanLines(input) {
16645
16941
  const gaps = input.checks.filter((c) => !c.ok);
16942
+ const healthyCount = input.checks.length - gaps.length;
16646
16943
  const lines = [
16647
- doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount }),
16648
- "",
16649
- "Checks",
16650
- ...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${c.label}`)
16944
+ doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount, skippedSlowChecks: input.skippedSlowChecks })
16651
16945
  ];
16652
- if (gaps.length > 0) {
16946
+ if (gaps.length > 0 || input.verbose) {
16947
+ lines.push("", "Checks");
16948
+ if (input.verbose) {
16949
+ lines.push(...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${doctorMetadataForCheck(c).id} \u2014 ${c.label}`));
16950
+ } else {
16951
+ lines.push(...gaps.map(doctorCheckSummaryLine));
16952
+ if (healthyCount > 0) lines.push(` \u2713 ${healthyCount} healthy check${healthyCount === 1 ? "" : "s"} collapsed`);
16953
+ }
16954
+ }
16955
+ if (gaps.length > 0 && input.verbose) {
16653
16956
  lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
16654
16957
  for (const c of gaps) {
16655
16958
  const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
@@ -16659,10 +16962,10 @@ function doctorHumanLines(input) {
16659
16962
  if (input.shouldApply && input.pluginReloadRequired) {
16660
16963
  lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
16661
16964
  }
16662
- const stale = renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0);
16965
+ const stale = input.verbose || gaps.length > 0 ? renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0) : [];
16663
16966
  if (stale.length) {
16664
16967
  lines.push("", ...stale);
16665
- } else if (!input.verbose) {
16968
+ } else if (!input.verbose && gaps.length > 0) {
16666
16969
  lines.push("", DOCTOR_VERBOSE_HINT);
16667
16970
  }
16668
16971
  if (!input.verbose) return lines;
@@ -16710,6 +17013,100 @@ function doctorAuditLines(input) {
16710
17013
  return lines;
16711
17014
  }
16712
17015
  var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
17016
+ var MMI_CLI_ON_PATH_LABEL = "mmi-cli on PATH";
17017
+ var PLUGIN_CLONE_LABEL = "plugin git clone (SSH\u2192HTTPS rewrite)";
17018
+ var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17019
+ var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17020
+ var DOCTOR_CHECK_METADATA = [
17021
+ { id: "github.auth", category: "auth", severity: "hard", cost: "fast", heal: "none", label: GITHUB_AUTH_LABEL, protects: ["GitHub issue, PR, project, and registry operations use a resolved viewer identity"] },
17022
+ { id: "cli.path", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: MMI_CLI_ON_PATH_LABEL, protects: ["Session hooks and surface commands can invoke mmi-cli"] },
17023
+ { id: "hub.compat", category: "hub", severity: "hard", cost: "fast", heal: "auto", label: HUB_COMPAT_LABEL, protects: ["Client stays above the Hub minimum version floor"] },
17024
+ { id: "hub.deploy-freshness", category: "hub", severity: "advisory", cost: "slow", heal: "none", label: HUB_DEPLOY_FRESHNESS_LABEL, protects: ["Operators can see prod Hub lag against release and local CLI versions"] },
17025
+ { id: "cli.version-drift", category: "repo", severity: "advisory", cost: "slow", heal: "none", label: CLI_VERSION_DRIFT_LABEL, protects: ["The installed mmi-cli is not behind the npm-published version (stale installs miss commands)"] },
17026
+ { id: "aws.cross-account", category: "aws", severity: "advisory", cost: "slow", heal: "none", label: AWS_CROSS_ACCOUNT_LABEL, protects: ["Master-agent AWS audits do not start from root credentials"] },
17027
+ { id: "plugin.clone-rewrite", category: "plugin", severity: "hard", cost: "fast", heal: "none", label: PLUGIN_CLONE_LABEL, protects: ["Plugin marketplace clones can use the HTTPS rewrite expected on this machine"] },
17028
+ { id: "plugin.install-record", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_LABEL, protects: ["Claude/Codex plugin loader sees mmi@mutmutco installed for this project"] },
17029
+ { id: "plugin.legacy-id", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: LEGACY_PLUGIN_ID_LABEL, protects: ["Legacy mmi@mmi rows cannot mask the canonical mmi@mutmutco install"] },
17030
+ { id: "plugin.config-drift", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_DRIFT_LABEL, protects: ["Duplicate or stale plugin rows collapse to one current user-scope row"] },
17031
+ { id: "plugin.resolvability", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: PLUGIN_RESOLVABILITY_LABEL, protects: ["Marketplace clone and plugin cache both exist for the active surface"] },
17032
+ { id: "plugin.installed-version", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: INSTALLED_PLUGIN_VERSION_LABEL, protects: ["Claude/Codex installed plugin records reach the latest release"] },
17033
+ { id: "plugin.cache-cleanup", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: MMI_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Stale Claude/Codex cache directories do not shadow current installs"] },
17034
+ { id: "plugin.nested-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: NESTED_PLUGIN_TREE_LABEL, protects: ["Self-nested plugin cache trees do not exceed host path limits"] },
17035
+ { id: "codex.active-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: CODEX_ACTIVE_CACHE_LABEL, protects: ["Codex actually loads the released plugin cache"] },
17036
+ { id: "session.skill-root", category: "plugin", severity: "advisory", cost: "fast", heal: "none", label: SESSION_SKILL_ROOT_LABEL, protects: ["Running sessions know when a restart is needed to load current skills"] },
17037
+ { id: "repo.gitignore-block", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: GITIGNORE_BLOCK_LABEL, protects: ["Org-managed ignore entries keep generated and scratch files out of git"] },
17038
+ { id: "repo.worktree-location", category: "repo", severity: "hard", cost: "fast", heal: "none", label: REPO_LOCAL_WORKTREE_LABEL, protects: ["Worktrees stay in the canonical sibling root"] },
17039
+ { id: "hooks.redactor-liveness", category: "hooks", severity: "advisory", cost: "fast", heal: "none", label: REDACTOR_LIVENESS_LABEL, protects: ["A crashed secret-redactor cannot silently pass secrets into model context forever"] },
17040
+ { id: "repo.scratch-gc", category: "repo", severity: "advisory", cost: "slow", heal: "auto", label: SCRATCH_GC_LABEL, protects: ["Scratch, plan, and browser artifact clutter stays bounded"] },
17041
+ { id: "repo.git-gc", category: "git", severity: "advisory", cost: "slow", heal: "auto", label: GIT_GC_LABEL, protects: ["Merged branches, stale refs, and dead worktree metadata stay bounded"] },
17042
+ { id: "browser.artifacts", category: "browser", severity: "advisory", cost: "slow", heal: "none", label: BROWSER_ARTIFACTS_LABEL, protects: ["Playwright/browser output stays under tmp/playwright-mcp"] },
17043
+ { id: "mcp.playwright-registration", category: "browser", severity: "hard", cost: "fast", heal: "auto", label: MCP_RECONCILE_LABEL, protects: ["Org-managed Playwright MCP registrations exist and stay drift-free"] },
17044
+ { id: "opencode.version", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_VERSION_LABEL, protects: ["OpenCode loads the released MMI adapter"] },
17045
+ { id: "opencode.hook-active", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_HOOK_ACTIVE_LABEL, protects: ["OpenCode shell hooks stamp the active MMI adapter surface"] },
17046
+ { id: "opencode.config-plugin", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_CONFIG_PLUGIN_LABEL, protects: ["OpenCode config includes the MMI npm adapter"] },
17047
+ { id: "opencode.surface-assets", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_SURFACE_ASSETS_LABEL, protects: ["OpenCode commands and skills are materialized"] },
17048
+ { id: "opencode.desktop-bootstrap", category: "opencode", severity: "advisory", cost: "slow", heal: "none", label: OPENCODE_DESKTOP_BOOTSTRAP_LABEL, protects: ["OpenCode Desktop is not pinned to a deleted project bootstrap"] },
17049
+ { id: "opencode.legacy-config", category: "opencode", severity: "hard", cost: "fast", heal: "none", label: OPENCODE_LEGACY_CONFIG_LABEL, protects: ["Legacy OpenCode config cannot load stale bare plugin ids"] },
17050
+ { id: "cursor.install", category: "cursor", severity: "hard", cost: "fast", heal: "auto", label: CURSOR_PLUGIN_INSTALL_LABEL, protects: ["Cursor Team Marketplace cache exists and is complete"] },
17051
+ { id: "cursor.cache-cleanup", category: "cursor", severity: "advisory", cost: "fast", heal: "none", label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Old Cursor cache pins are visible without unsafe automatic deletion"] },
17052
+ { id: "cursor.enabled-pin", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_ENABLED_PIN_LABEL, protects: ["Cursor Team Marketplace enabled pin matches the healthy cache"] },
17053
+ { id: "cursor.third-party-extensibility", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_THIRD_PARTY_EXTENSIBILITY_LABEL, protects: ["Cursor does not import third-party hooks that break the Team Marketplace plugin"] },
17054
+ { id: "cursor.hook-cli", category: "cursor", severity: "hard", cost: "fast", heal: "surface", label: CURSOR_HOOK_CLI_LABEL, protects: ["Cursor hooks can reach a bundled or PATH mmi-cli"] },
17055
+ // Incident coverage (#2594) — three real incident classes that previously had zero doctor coverage.
17056
+ { id: "repo-hygiene.lockfile-platform-optionals", category: "repo-hygiene", severity: "hard", cost: "fast", heal: "surface", label: LOCKFILE_OPTIONALS_LABEL, protects: ["A Windows-generated lockfile does not silently drop Linux-only optionalDependencies before CI"] },
17057
+ { id: "repo-hygiene.file-encoding", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: FILE_ENCODING_LABEL, protects: ["Managed source and config files stay clean UTF-8 without BOM or mojibake"] },
17058
+ { id: "repo-hygiene.local-behind-origin", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: LOCAL_BEHIND_ORIGIN_LABEL, protects: ["Local release branches (development/main/rc) do not fall behind origin after a release"] }
17059
+ ];
17060
+ var DOCTOR_CHECK_METADATA_BY_LABEL = Object.fromEntries(
17061
+ DOCTOR_CHECK_METADATA.map((meta) => [meta.label, meta])
17062
+ );
17063
+ var DOCTOR_EXTENDED_CHECK_LABELS = new Set(
17064
+ DOCTOR_CHECK_METADATA.filter((meta) => meta.cost === "slow").map((meta) => meta.label)
17065
+ );
17066
+ function derivedDoctorCheckMetadata(label) {
17067
+ if (label.startsWith(HUB_COMPAT_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_COMPAT_LABEL], label };
17068
+ if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_DEPLOY_FRESHNESS_LABEL], label };
17069
+ if (label.startsWith("@mutmutco design-system")) {
17070
+ return {
17071
+ id: "registry.design-system-components",
17072
+ category: "hub",
17073
+ severity: "advisory",
17074
+ cost: "slow",
17075
+ heal: "none",
17076
+ label,
17077
+ protects: ["Design-system component cache can be compared against the live registry"]
17078
+ };
17079
+ }
17080
+ if (label.startsWith("@mutmutco registry components")) {
17081
+ return {
17082
+ id: "registry.components",
17083
+ category: "hub",
17084
+ severity: "advisory",
17085
+ cost: "slow",
17086
+ heal: "none",
17087
+ label,
17088
+ protects: ["Repo component cache can be compared against the live registry"]
17089
+ };
17090
+ }
17091
+ return void 0;
17092
+ }
17093
+ function fallbackDoctorCheckMetadata(label) {
17094
+ return {
17095
+ id: `legacy.${label.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/(^\.|\.$)/g, "") || "unknown"}`,
17096
+ category: "repo",
17097
+ severity: "hard",
17098
+ cost: "fast",
17099
+ heal: "none",
17100
+ label,
17101
+ protects: ["Legacy check without catalog metadata"]
17102
+ };
17103
+ }
17104
+ function doctorMetadataForLabel(label) {
17105
+ return DOCTOR_CHECK_METADATA_BY_LABEL[label] ?? derivedDoctorCheckMetadata(label) ?? fallbackDoctorCheckMetadata(label);
17106
+ }
17107
+ function doctorMetadataForCheck(check) {
17108
+ return check.metadata ?? doctorMetadataForLabel(check.label);
17109
+ }
16713
17110
  function buildPluginResolvabilityCheck(input) {
16714
17111
  const { state } = buildPluginGuardDecision(input);
16715
17112
  const surface = input.surface ?? "shell";
@@ -16755,17 +17152,17 @@ function ghReleaseTarballApiArgs(tag) {
16755
17152
  }
16756
17153
  function cursorUserGlobalStatePath() {
16757
17154
  if (process.platform === "win32") {
16758
- const base = process.env.APPDATA || (0, import_node_path17.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
16759
- return (0, import_node_path17.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
17155
+ const base = process.env.APPDATA || (0, import_node_path18.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
17156
+ return (0, import_node_path18.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
16760
17157
  }
16761
17158
  if (process.platform === "darwin") {
16762
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17159
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
16763
17160
  }
16764
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17161
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
16765
17162
  }
16766
17163
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16767
17164
  const dbPath = cursorUserGlobalStatePath();
16768
- if (!(0, import_node_fs16.existsSync)(dbPath)) return void 0;
17165
+ if (!(0, import_node_fs17.existsSync)(dbPath)) return void 0;
16769
17166
  try {
16770
17167
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
16771
17168
  timeout: 5e3
@@ -16779,57 +17176,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16779
17176
  }
16780
17177
  }
16781
17178
  function syncDirContents(src, dest) {
16782
- (0, import_node_fs16.mkdirSync)(dest, { recursive: true });
16783
- for (const name of (0, import_node_fs16.readdirSync)(dest)) {
16784
- (0, import_node_fs16.rmSync)((0, import_node_path17.join)(dest, name), { recursive: true, force: true });
17179
+ (0, import_node_fs17.mkdirSync)(dest, { recursive: true });
17180
+ for (const name of (0, import_node_fs17.readdirSync)(dest)) {
17181
+ (0, import_node_fs17.rmSync)((0, import_node_path18.join)(dest, name), { recursive: true, force: true });
16785
17182
  }
16786
- (0, import_node_fs16.cpSync)(src, dest, { recursive: true });
17183
+ (0, import_node_fs17.cpSync)(src, dest, { recursive: true });
16787
17184
  }
16788
17185
  function releaseTag(releasedVersion) {
16789
17186
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
16790
17187
  }
16791
17188
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
16792
- const tarFile = (0, import_node_path17.join)(tmpRoot, "archive.tar");
17189
+ const tarFile = (0, import_node_path18.join)(tmpRoot, "archive.tar");
16793
17190
  try {
16794
17191
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
16795
17192
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
16796
17193
  timeout: 6e4
16797
17194
  });
16798
17195
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
16799
- const pluginMmi = (0, import_node_path17.join)(tmpRoot, "plugins", "mmi");
16800
- return (0, import_node_fs16.existsSync)((0, import_node_path17.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17196
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, "plugins", "mmi");
17197
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16801
17198
  } catch {
16802
17199
  return void 0;
16803
17200
  }
16804
17201
  }
16805
17202
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
16806
- const tarPath = (0, import_node_path17.join)(tmpRoot, "repo.tgz");
17203
+ const tarPath = (0, import_node_path18.join)(tmpRoot, "repo.tgz");
16807
17204
  try {
16808
- (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
17205
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16809
17206
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
16810
17207
  timeout: 12e4,
16811
17208
  maxBuffer: 100 * 1024 * 1024,
16812
17209
  encoding: "buffer",
16813
17210
  windowsHide: true
16814
17211
  });
16815
- (0, import_node_fs16.writeFileSync)(tarPath, stdout);
17212
+ (0, import_node_fs17.writeFileSync)(tarPath, stdout);
16816
17213
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
16817
- const top = (0, import_node_fs16.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
17214
+ const top = (0, import_node_fs17.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
16818
17215
  if (!top) return void 0;
16819
- const pluginMmi = (0, import_node_path17.join)(tmpRoot, top, "plugins", "mmi");
16820
- return (0, import_node_fs16.existsSync)((0, import_node_path17.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17216
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, top, "plugins", "mmi");
17217
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16821
17218
  } catch {
16822
17219
  return void 0;
16823
17220
  }
16824
17221
  }
16825
17222
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
16826
- (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
17223
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16827
17224
  const tag = releaseTag(releasedVersion);
16828
17225
  if (hubCheckout) {
16829
17226
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
16830
17227
  if (fromHub) return fromHub;
16831
17228
  }
16832
- return downloadPluginMmiViaGh(tag, (0, import_node_path17.join)(tmpRoot, "gh"));
17229
+ return downloadPluginMmiViaGh(tag, (0, import_node_path18.join)(tmpRoot, "gh"));
16833
17230
  }
16834
17231
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
16835
17232
  if (!isSemverVersion2(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -16843,7 +17240,7 @@ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRoo
16843
17240
  const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
16844
17241
  if (needing.length > 0) return needing.map((pin) => pin.path);
16845
17242
  if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion2(releasedVersion)) {
16846
- return [(0, import_node_path17.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
17243
+ return [(0, import_node_path18.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
16847
17244
  }
16848
17245
  return [];
16849
17246
  }
@@ -16858,10 +17255,10 @@ async function applyCursorPluginCacheSeed(input) {
16858
17255
  for (const dest of targets) {
16859
17256
  syncDirContents(source, dest);
16860
17257
  }
16861
- (0, import_node_fs16.rmSync)(tmpRoot, { recursive: true, force: true });
17258
+ (0, import_node_fs17.rmSync)(tmpRoot, { recursive: true, force: true });
16862
17259
  return true;
16863
17260
  }
16864
- var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17261
+ var CURSOR_ENABLED_PIN_LABEL2 = "Cursor Team Marketplace enabled plugin pin";
16865
17262
  function cursorSplitBrainCapabilities() {
16866
17263
  const skills = OPENCODE_WORKFLOW_COMMANDS.map((c) => `/${c}`).join(", ");
16867
17264
  return `org skills (${skills}) and SessionStart hooks`;
@@ -16881,7 +17278,7 @@ function parseCursorPluginLog(text) {
16881
17278
  return { enabledPin, loadSucceeded, loadFailed };
16882
17279
  }
16883
17280
  function buildCursorEnabledPinCheck(input) {
16884
- const label = CURSOR_ENABLED_PIN_LABEL;
17281
+ const label = CURSOR_ENABLED_PIN_LABEL2;
16885
17282
  const healthy = { ok: true, label, fix: "" };
16886
17283
  if (!input.isOrgRepo) return healthy;
16887
17284
  if (!input.enabledPin && !input.loadFailed) return healthy;
@@ -16916,7 +17313,7 @@ var PLAYWRIGHT_MCP_SPEC = {
16916
17313
  args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
16917
17314
  legacyNames: []
16918
17315
  };
16919
- var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17316
+ var MCP_RECONCILE_LABEL2 = "MCP server registrations (org-managed Playwright)";
16920
17317
  var MCP_RECONCILE_FIX = "register/reconcile the org Playwright MCP server (npx -y @playwright/mcp@latest --output-dir tmp/playwright-mcp) \u2014 run `mmi-cli doctor --apply`; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
16921
17318
  function extractServerBlock(content, format, spec) {
16922
17319
  return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
@@ -17030,7 +17427,7 @@ function planClaudeCliMcpHeal(item, spec) {
17030
17427
  function buildMcpReconcileCheck(plan) {
17031
17428
  const base = {
17032
17429
  ok: true,
17033
- label: MCP_RECONCILE_LABEL,
17430
+ label: MCP_RECONCILE_LABEL2,
17034
17431
  fix: MCP_RECONCILE_FIX,
17035
17432
  severityOverride: "advisory"
17036
17433
  };
@@ -17217,9 +17614,9 @@ ${block}
17217
17614
  }
17218
17615
 
17219
17616
  // src/cli-doctor-shared.ts
17220
- var import_node_fs17 = require("node:fs");
17221
- var import_node_path18 = require("node:path");
17222
17617
  var import_node_fs18 = require("node:fs");
17618
+ var import_node_path19 = require("node:path");
17619
+ var import_node_fs19 = require("node:fs");
17223
17620
  var GC_GH_TIMEOUT_MS = 2e4;
17224
17621
  async function awsCallerArn() {
17225
17622
  try {
@@ -17265,7 +17662,7 @@ async function localBranchHeads() {
17265
17662
  }
17266
17663
  async function currentRepoWorktreeGitRoot(repoRoot) {
17267
17664
  const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17268
- return gitCommonDir ? (0, import_node_path18.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
17665
+ return gitCommonDir ? (0, import_node_path19.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
17269
17666
  }
17270
17667
  async function worktreeBranches() {
17271
17668
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -17285,18 +17682,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
17285
17682
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
17286
17683
  if (!match?.[1]) return void 0;
17287
17684
  const raw = match[1].trim();
17288
- return (0, import_node_path18.isAbsolute)(raw) ? raw : (0, import_node_path18.resolve)(worktreePath, raw);
17685
+ return (0, import_node_path19.isAbsolute)(raw) ? raw : (0, import_node_path19.resolve)(worktreePath, raw);
17289
17686
  }
17290
17687
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17291
17688
  if (!worktreeGitRoot) return false;
17292
17689
  try {
17293
- const entries = (0, import_node_fs18.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17690
+ const entries = (0, import_node_fs19.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17294
17691
  for (const ent of entries) {
17295
17692
  if (!ent.isDirectory()) continue;
17296
17693
  try {
17297
- const gitdirPath = (0, import_node_fs17.readFileSync)((0, import_node_path18.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17298
- const resolvedGitdir = (0, import_node_path18.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path18.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17299
- if (sameWorktreeMetadataPath((0, import_node_path18.dirname)(resolvedGitdir), worktreePath)) return true;
17694
+ const gitdirPath = (0, import_node_fs18.readFileSync)((0, import_node_path19.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17695
+ const resolvedGitdir = (0, import_node_path19.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path19.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17696
+ if (sameWorktreeMetadataPath((0, import_node_path19.dirname)(resolvedGitdir), worktreePath)) return true;
17300
17697
  } catch {
17301
17698
  }
17302
17699
  }
@@ -17306,7 +17703,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17306
17703
  }
17307
17704
  function pathExistsKnown(path2) {
17308
17705
  try {
17309
- (0, import_node_fs18.statSync)(path2);
17706
+ (0, import_node_fs19.statSync)(path2);
17310
17707
  return true;
17311
17708
  } catch (e) {
17312
17709
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
@@ -17315,10 +17712,10 @@ function pathExistsKnown(path2) {
17315
17712
  }
17316
17713
  }
17317
17714
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17318
- const gitPath = (0, import_node_path18.join)(path2, ".git");
17715
+ const gitPath = (0, import_node_path19.join)(path2, ".git");
17319
17716
  let st;
17320
17717
  try {
17321
- st = (0, import_node_fs18.lstatSync)(gitPath);
17718
+ st = (0, import_node_fs19.lstatSync)(gitPath);
17322
17719
  } catch (e) {
17323
17720
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17324
17721
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -17335,7 +17732,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17335
17732
  if (st.isDirectory()) return { path: path2, gitType: "dir" };
17336
17733
  if (!st.isFile()) return { path: path2, gitType: "other" };
17337
17734
  try {
17338
- const gitFileContent = (0, import_node_fs17.readFileSync)(gitPath, "utf8");
17735
+ const gitFileContent = (0, import_node_fs18.readFileSync)(gitPath, "utf8");
17339
17736
  const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
17340
17737
  const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
17341
17738
  return {
@@ -17352,7 +17749,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17352
17749
  }
17353
17750
  function inspectDeadWorktreeDirContent(path2) {
17354
17751
  try {
17355
- return { entries: (0, import_node_fs18.readdirSync)(path2) };
17752
+ return { entries: (0, import_node_fs19.readdirSync)(path2) };
17356
17753
  } catch (e) {
17357
17754
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17358
17755
  return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
@@ -17371,8 +17768,8 @@ async function siblingWorktreeDirs() {
17371
17768
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
17372
17769
  const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
17373
17770
  try {
17374
- const entries = (0, import_node_fs18.readdirSync)(siblingRoot, { withFileTypes: true });
17375
- return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path18.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
17771
+ const entries = (0, import_node_fs19.readdirSync)(siblingRoot, { withFileTypes: true });
17772
+ return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path19.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
17376
17773
  } catch {
17377
17774
  return [];
17378
17775
  }
@@ -17422,7 +17819,7 @@ async function fetchHubVersionInfo(baseUrl) {
17422
17819
  }
17423
17820
  function readRepoVersion() {
17424
17821
  try {
17425
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17822
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17426
17823
  } catch {
17427
17824
  return void 0;
17428
17825
  }
@@ -17574,11 +17971,11 @@ async function applyPluginHeal(token, surface, log, opts) {
17574
17971
  }
17575
17972
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
17576
17973
  const homeDir = surface === "codex" ? ".codex" : ".claude";
17577
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
17974
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
17578
17975
  };
17579
17976
  function readInstalledPlugins(surface = detectSurface(process.env)) {
17580
17977
  try {
17581
- return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(surface), "utf8"));
17978
+ return JSON.parse((0, import_node_fs20.readFileSync)(installedPluginsPath(surface), "utf8"));
17582
17979
  } catch {
17583
17980
  return null;
17584
17981
  }
@@ -17586,13 +17983,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
17586
17983
  function marketplaceCloneCandidates(surface, home) {
17587
17984
  if (surface === "codex") {
17588
17985
  return [
17589
- (0, import_node_path19.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
17590
- (0, import_node_path19.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
17986
+ (0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
17987
+ (0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
17591
17988
  ];
17592
17989
  }
17593
- return [(0, import_node_path19.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
17990
+ return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
17594
17991
  }
17595
- function marketplaceClonePresent(surface, home, exists = import_node_fs19.existsSync) {
17992
+ function marketplaceClonePresent(surface, home, exists = import_node_fs20.existsSync) {
17596
17993
  return marketplaceCloneCandidates(surface, home).some(exists);
17597
17994
  }
17598
17995
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
@@ -17602,14 +17999,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
17602
17999
  isOrgRepo,
17603
18000
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
17604
18001
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
17605
- pluginCachePresent: (0, import_node_fs19.existsSync)((0, import_node_path19.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
18002
+ pluginCachePresent: (0, import_node_fs20.existsSync)((0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
17606
18003
  };
17607
18004
  }
17608
18005
  function installedPluginSources() {
17609
18006
  return ["claude", "codex"].map((surface) => {
17610
- const recordPath = (0, import_node_path19.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18007
+ const recordPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
17611
18008
  try {
17612
- return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
18009
+ return { surface, installed: JSON.parse((0, import_node_fs20.readFileSync)(recordPath, "utf8")), recordPath };
17613
18010
  } catch {
17614
18011
  return { surface, installed: null, recordPath };
17615
18012
  }
@@ -17617,7 +18014,7 @@ function installedPluginSources() {
17617
18014
  }
17618
18015
  function readClaudeSettings() {
17619
18016
  try {
17620
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18017
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17621
18018
  } catch {
17622
18019
  return null;
17623
18020
  }
@@ -17639,7 +18036,7 @@ function writeProjectInstallRecord(record) {
17639
18036
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
17640
18037
  list.push(record);
17641
18038
  file.plugins[MMI_PLUGIN_ID] = list;
17642
- (0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18039
+ (0, import_node_fs20.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17643
18040
  `, "utf8");
17644
18041
  return true;
17645
18042
  } catch {
@@ -17652,9 +18049,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17652
18049
  if (!file) return false;
17653
18050
  if (!file.plugins) file.plugins = {};
17654
18051
  const path2 = installedPluginsPath();
17655
- (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
18052
+ (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
17656
18053
  file.plugins[pluginId] = records;
17657
- (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18054
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17658
18055
  `, "utf8");
17659
18056
  return true;
17660
18057
  } catch {
@@ -17662,22 +18059,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17662
18059
  }
17663
18060
  }
17664
18061
  function opencodeConfigDir() {
17665
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".config", "opencode");
18062
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "opencode");
17666
18063
  }
17667
18064
  function opencodeConfigPath() {
17668
- return (0, import_node_path19.join)(opencodeConfigDir(), "opencode.jsonc");
18065
+ return (0, import_node_path20.join)(opencodeConfigDir(), "opencode.jsonc");
17669
18066
  }
17670
18067
  function opencodeCommandsDir() {
17671
- return (0, import_node_path19.join)(opencodeConfigDir(), "commands");
18068
+ return (0, import_node_path20.join)(opencodeConfigDir(), "commands");
17672
18069
  }
17673
18070
  function opencodeSkillsPath() {
17674
- return (0, import_node_path19.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
18071
+ return (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
17675
18072
  }
17676
18073
  function opencodeConfigSnapshot() {
17677
18074
  const path2 = opencodeConfigPath();
17678
- if (!(0, import_node_fs19.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
18075
+ if (!(0, import_node_fs20.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
17679
18076
  try {
17680
- const raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
18077
+ const raw = (0, import_node_fs20.readFileSync)(path2, "utf8");
17681
18078
  const parsed = JSON.parse(stripJsonc(raw));
17682
18079
  const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
17683
18080
  const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
@@ -17700,9 +18097,9 @@ function writeOpencodeConfigPlugin(snapshot) {
17700
18097
  const plan = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
17701
18098
  if (plan.action === "already") return true;
17702
18099
  if (!plan.text || plan.action === "unsafe") return false;
17703
- (0, import_node_fs19.mkdirSync)((0, import_node_path19.dirname)(path2), { recursive: true });
17704
- if (snapshot.hasConfig) (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
17705
- (0, import_node_fs19.writeFileSync)(path2, plan.text, "utf8");
18100
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
18101
+ if (snapshot.hasConfig) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
18102
+ (0, import_node_fs20.writeFileSync)(path2, plan.text, "utf8");
17706
18103
  return true;
17707
18104
  } catch {
17708
18105
  return false;
@@ -17717,9 +18114,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17717
18114
  const normalized = skillsPath.replace(/\\/g, "/");
17718
18115
  if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
17719
18116
  parsed.skills = { ...skills, paths };
17720
- (0, import_node_fs19.mkdirSync)((0, import_node_path19.dirname)(snapshot.path), { recursive: true });
17721
- if (snapshot.hasConfig && (0, import_node_fs19.existsSync)(snapshot.path)) (0, import_node_fs19.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
17722
- (0, import_node_fs19.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
18117
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(snapshot.path), { recursive: true });
18118
+ if (snapshot.hasConfig && (0, import_node_fs20.existsSync)(snapshot.path)) (0, import_node_fs20.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
18119
+ (0, import_node_fs20.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
17723
18120
  `, "utf8");
17724
18121
  return true;
17725
18122
  } catch {
@@ -17728,7 +18125,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17728
18125
  }
17729
18126
  function opencodeExistingCommands() {
17730
18127
  try {
17731
- return (0, import_node_fs19.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
18128
+ return (0, import_node_fs20.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
17732
18129
  } catch {
17733
18130
  return [];
17734
18131
  }
@@ -17736,9 +18133,9 @@ function opencodeExistingCommands() {
17736
18133
  function writeOpencodeCommandFiles() {
17737
18134
  try {
17738
18135
  const dir = opencodeCommandsDir();
17739
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
18136
+ (0, import_node_fs20.mkdirSync)(dir, { recursive: true });
17740
18137
  for (const command of OPENCODE_WORKFLOW_COMMANDS) {
17741
- (0, import_node_fs19.writeFileSync)((0, import_node_path19.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
18138
+ (0, import_node_fs20.writeFileSync)((0, import_node_path20.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
17742
18139
  }
17743
18140
  return true;
17744
18141
  } catch {
@@ -17747,12 +18144,12 @@ function writeOpencodeCommandFiles() {
17747
18144
  }
17748
18145
  function readOpencodeAdapterDiskVersion() {
17749
18146
  const candidates = [
17750
- (0, import_node_path19.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
17751
- (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
18147
+ (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
18148
+ (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
17752
18149
  ];
17753
18150
  for (const path2 of candidates) {
17754
18151
  try {
17755
- const parsed = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
18152
+ const parsed = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
17756
18153
  if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
17757
18154
  } catch {
17758
18155
  continue;
@@ -17768,7 +18165,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17768
18165
  try {
17769
18166
  const specs = opencodeMmiPluginSpecs(snapshot);
17770
18167
  log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
17771
- (0, import_node_fs19.mkdirSync)(opencodeConfigDir(), { recursive: true });
18168
+ (0, import_node_fs20.mkdirSync)(opencodeConfigDir(), { recursive: true });
17772
18169
  await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
17773
18170
  return true;
17774
18171
  } catch {
@@ -17776,12 +18173,12 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17776
18173
  }
17777
18174
  }
17778
18175
  function opencodePackagesRoot() {
17779
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
18176
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
17780
18177
  }
17781
18178
  function opencodePluginCacheDirs() {
17782
18179
  const root = opencodePackagesRoot();
17783
18180
  try {
17784
- return (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path19.join)(root, e.name));
18181
+ return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path20.join)(root, e.name));
17785
18182
  } catch {
17786
18183
  return [];
17787
18184
  }
@@ -17789,7 +18186,7 @@ function opencodePluginCacheDirs() {
17789
18186
  function readOpencodeCacheDirVersion(cacheDir) {
17790
18187
  try {
17791
18188
  const parsed = JSON.parse(
17792
- (0, import_node_fs19.readFileSync)((0, import_node_path19.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
18189
+ (0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
17793
18190
  );
17794
18191
  return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17795
18192
  } catch {
@@ -17812,9 +18209,9 @@ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
17812
18209
  });
17813
18210
  if (!plan.quarantine) continue;
17814
18211
  try {
17815
- const quarantineRoot = (0, import_node_path19.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
17816
- (0, import_node_fs19.mkdirSync)(quarantineRoot, { recursive: true });
17817
- (0, import_node_fs19.renameSync)(cacheDir, (0, import_node_path19.join)(quarantineRoot, (0, import_node_path19.basename)(cacheDir)));
18212
+ const quarantineRoot = (0, import_node_path20.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
18213
+ (0, import_node_fs20.mkdirSync)(quarantineRoot, { recursive: true });
18214
+ (0, import_node_fs20.renameSync)(cacheDir, (0, import_node_path20.join)(quarantineRoot, (0, import_node_path20.basename)(cacheDir)));
17818
18215
  log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
17819
18216
  moved = true;
17820
18217
  } catch {
@@ -17840,30 +18237,30 @@ function opencodePluginVersionsForReport() {
17840
18237
  }
17841
18238
  function opencodeDesktopLogsRoot() {
17842
18239
  if (process.platform === "win32") {
17843
- const base = process.env.APPDATA || (0, import_node_path19.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
17844
- return (0, import_node_path19.join)(base, "ai.opencode.desktop", "logs");
18240
+ const base = process.env.APPDATA || (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
18241
+ return (0, import_node_path20.join)(base, "ai.opencode.desktop", "logs");
17845
18242
  }
17846
18243
  if (process.platform === "darwin") {
17847
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
18244
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
17848
18245
  }
17849
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
18246
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
17850
18247
  }
17851
18248
  function opencodeDesktopBootstrapSnapshot() {
17852
18249
  const root = opencodeDesktopLogsRoot();
17853
18250
  try {
17854
- const sessionDirs = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path19.join)(root, entry.name)).sort((a, b) => (0, import_node_fs19.statSync)(b).mtimeMs - (0, import_node_fs19.statSync)(a).mtimeMs);
18251
+ const sessionDirs = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path20.join)(root, entry.name)).sort((a, b) => (0, import_node_fs20.statSync)(b).mtimeMs - (0, import_node_fs20.statSync)(a).mtimeMs);
17855
18252
  const newest = sessionDirs[0];
17856
18253
  if (!newest) return [];
17857
- const logPath = (0, import_node_path19.join)(newest, "renderer.log");
17858
- const text = (0, import_node_fs19.readFileSync)(logPath, "utf8");
17859
- return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs19.existsSync)(directory)).map((directory) => ({ directory, logPath }));
18254
+ const logPath = (0, import_node_path20.join)(newest, "renderer.log");
18255
+ const text = (0, import_node_fs20.readFileSync)(logPath, "utf8");
18256
+ return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs20.existsSync)(directory)).map((directory) => ({ directory, logPath }));
17860
18257
  } catch {
17861
18258
  return [];
17862
18259
  }
17863
18260
  }
17864
18261
  function opencodeLegacyConfigSnapshot() {
17865
- const legacyPath = (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
17866
- if (!(0, import_node_fs19.existsSync)(legacyPath)) return {};
18262
+ const legacyPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
18263
+ if (!(0, import_node_fs20.existsSync)(legacyPath)) return {};
17867
18264
  const content = readTextFile(legacyPath);
17868
18265
  if (content == null) return {};
17869
18266
  const plugins = parseOpencodeLegacyConfigPlugins(content);
@@ -17875,24 +18272,24 @@ function opencodeLegacyConfigSnapshot() {
17875
18272
  function quarantineOpencodeLegacyConfig(legacyPath) {
17876
18273
  try {
17877
18274
  const backupPath = `${legacyPath}.bak`;
17878
- if ((0, import_node_fs19.existsSync)(backupPath)) return false;
17879
- (0, import_node_fs19.renameSync)(legacyPath, backupPath);
18275
+ if ((0, import_node_fs20.existsSync)(backupPath)) return false;
18276
+ (0, import_node_fs20.renameSync)(legacyPath, backupPath);
17880
18277
  return true;
17881
18278
  } catch {
17882
18279
  return false;
17883
18280
  }
17884
18281
  }
17885
18282
  function cursorPluginCacheRoot() {
17886
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18283
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
17887
18284
  }
17888
18285
  function cursorLogsRoot() {
17889
- if (process.platform === "win32") return (0, import_node_path19.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
17890
- if (process.platform === "darwin") return (0, import_node_path19.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
17891
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
18286
+ if (process.platform === "win32") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
18287
+ if (process.platform === "darwin") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
18288
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
17892
18289
  }
17893
18290
  function safeReaddirNames(dir) {
17894
18291
  try {
17895
- return (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
18292
+ return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
17896
18293
  } catch {
17897
18294
  return [];
17898
18295
  }
@@ -17901,15 +18298,15 @@ function readCursorPluginLogText() {
17901
18298
  const root = cursorLogsRoot();
17902
18299
  const sessions = safeReaddirNames(root).sort().reverse();
17903
18300
  for (const session of sessions.slice(0, 5)) {
17904
- const sessionDir = (0, import_node_path19.join)(root, session);
18301
+ const sessionDir = (0, import_node_path20.join)(root, session);
17905
18302
  const texts = [];
17906
18303
  for (const win of safeReaddirNames(sessionDir).filter((n) => n.startsWith("window"))) {
17907
- const dir = (0, import_node_path19.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
18304
+ const dir = (0, import_node_path20.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
17908
18305
  for (const file of safeReaddirNames(dir)) {
17909
18306
  if (!/^Cursor Plugins.*\.log$/i.test(file)) continue;
17910
- const path2 = (0, import_node_path19.join)(dir, file);
18307
+ const path2 = (0, import_node_path20.join)(dir, file);
17911
18308
  try {
17912
- texts.push({ text: (0, import_node_fs19.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs19.statSync)(path2).mtimeMs });
18309
+ texts.push({ text: (0, import_node_fs20.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs20.statSync)(path2).mtimeMs });
17913
18310
  } catch {
17914
18311
  }
17915
18312
  }
@@ -17923,32 +18320,32 @@ function readCursorPluginLogText() {
17923
18320
  function cursorPluginCachePinSnapshots() {
17924
18321
  const root = cursorPluginCacheRoot();
17925
18322
  try {
17926
- return (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
17927
- const path2 = (0, import_node_path19.join)(root, entry.name);
17928
- const pluginJson = (0, import_node_path19.join)(path2, ".cursor-plugin", "plugin.json");
17929
- const hooksJson = (0, import_node_path19.join)(path2, "hooks", "hooks.json");
17930
- const cliBundle = (0, import_node_path19.join)(path2, "cli", "dist", "index.cjs");
17931
- const cursorHook = (0, import_node_path19.join)(path2, "scripts", "cursor-hook.mjs");
18323
+ return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
18324
+ const path2 = (0, import_node_path20.join)(root, entry.name);
18325
+ const pluginJson = (0, import_node_path20.join)(path2, ".cursor-plugin", "plugin.json");
18326
+ const hooksJson = (0, import_node_path20.join)(path2, "hooks", "hooks.json");
18327
+ const cliBundle = (0, import_node_path20.join)(path2, "cli", "dist", "index.cjs");
18328
+ const cursorHook = (0, import_node_path20.join)(path2, "scripts", "cursor-hook.mjs");
17932
18329
  let version;
17933
18330
  try {
17934
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
18331
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(pluginJson, "utf8"));
17935
18332
  version = typeof raw.version === "string" ? raw.version : void 0;
17936
18333
  } catch {
17937
18334
  version = void 0;
17938
18335
  }
17939
18336
  let isEmpty = true;
17940
18337
  try {
17941
- isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
18338
+ isEmpty = (0, import_node_fs20.readdirSync)(path2).length === 0;
17942
18339
  } catch {
17943
18340
  isEmpty = true;
17944
18341
  }
17945
18342
  return {
17946
18343
  name: entry.name,
17947
18344
  path: path2,
17948
- hasPluginJson: (0, import_node_fs19.existsSync)(pluginJson),
17949
- hasHooksJson: (0, import_node_fs19.existsSync)(hooksJson),
17950
- hasCliBundle: (0, import_node_fs19.existsSync)(cliBundle),
17951
- hasCursorHookScript: (0, import_node_fs19.existsSync)(cursorHook),
18345
+ hasPluginJson: (0, import_node_fs20.existsSync)(pluginJson),
18346
+ hasHooksJson: (0, import_node_fs20.existsSync)(hooksJson),
18347
+ hasCliBundle: (0, import_node_fs20.existsSync)(cliBundle),
18348
+ hasCursorHookScript: (0, import_node_fs20.existsSync)(cursorHook),
17952
18349
  isEmpty,
17953
18350
  version
17954
18351
  };
@@ -17958,19 +18355,19 @@ function cursorPluginCachePinSnapshots() {
17958
18355
  }
17959
18356
  }
17960
18357
  function hubCheckoutForCursorSeed() {
17961
- const manifest = (0, import_node_path19.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
17962
- return (0, import_node_fs19.existsSync)(manifest) ? process.cwd() : void 0;
18358
+ const manifest = (0, import_node_path20.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
18359
+ return (0, import_node_fs20.existsSync)(manifest) ? process.cwd() : void 0;
17963
18360
  }
17964
18361
  function mmiPluginCacheRootSnapshots() {
17965
18362
  const roots = [
17966
- { surface: "claude", root: (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
17967
- { surface: "codex", root: (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
18363
+ { surface: "claude", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
18364
+ { surface: "codex", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
17968
18365
  ];
17969
18366
  return roots.flatMap(({ surface, root }) => {
17970
18367
  try {
17971
- const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18368
+ const entries = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
17972
18369
  name: entry.name,
17973
- path: (0, import_node_path19.join)(root, entry.name),
18370
+ path: (0, import_node_path20.join)(root, entry.name),
17974
18371
  isDirectory: entry.isDirectory()
17975
18372
  }));
17976
18373
  return [{ surface, root, entries }];
@@ -17981,7 +18378,7 @@ function mmiPluginCacheRootSnapshots() {
17981
18378
  }
17982
18379
  function hasNestedMmiChild(versionDir) {
17983
18380
  try {
17984
- return (0, import_node_fs19.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
18381
+ return (0, import_node_fs20.statSync)((0, import_node_path20.join)(versionDir, "mmi")).isDirectory();
17985
18382
  } catch {
17986
18383
  return false;
17987
18384
  }
@@ -17992,10 +18389,10 @@ function nestedPluginTreeSnapshot() {
17992
18389
  );
17993
18390
  }
17994
18391
  function uniqueQuarantineTarget(path2) {
17995
- if (!(0, import_node_fs19.existsSync)(path2)) return path2;
18392
+ if (!(0, import_node_fs20.existsSync)(path2)) return path2;
17996
18393
  for (let i = 1; i < 100; i += 1) {
17997
18394
  const candidate = `${path2}-${i}`;
17998
- if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
18395
+ if (!(0, import_node_fs20.existsSync)(candidate)) return candidate;
17999
18396
  }
18000
18397
  return `${path2}-${Date.now()}`;
18001
18398
  }
@@ -18004,10 +18401,10 @@ function quarantinePluginCacheDirs(plan) {
18004
18401
  const failed = [];
18005
18402
  for (const move of plan) {
18006
18403
  try {
18007
- if (!(0, import_node_fs19.existsSync)(move.from)) continue;
18404
+ if (!(0, import_node_fs20.existsSync)(move.from)) continue;
18008
18405
  const target = uniqueQuarantineTarget(move.to);
18009
- (0, import_node_fs19.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
18010
- (0, import_node_fs19.renameSync)(move.from, target);
18406
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(target), { recursive: true });
18407
+ (0, import_node_fs20.renameSync)(move.from, target);
18011
18408
  moved += 1;
18012
18409
  } catch {
18013
18410
  failed.push(move);
@@ -18026,23 +18423,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
18026
18423
  }
18027
18424
  async function clearNestedPluginTreeDir(targetPath) {
18028
18425
  try {
18029
- if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
18426
+ if (!(0, import_node_fs20.existsSync)(targetPath)) return true;
18030
18427
  if (isWin) {
18031
- const emptyDir = (0, import_node_path19.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
18032
- (0, import_node_fs19.mkdirSync)(emptyDir, { recursive: true });
18428
+ const emptyDir = (0, import_node_path20.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
18429
+ (0, import_node_fs20.mkdirSync)(emptyDir, { recursive: true });
18033
18430
  try {
18034
18431
  await robocopyMirrorEmpty(emptyDir, targetPath);
18035
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18432
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
18036
18433
  } finally {
18037
18434
  try {
18038
- (0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
18435
+ (0, import_node_fs20.rmSync)(emptyDir, { recursive: true, force: true });
18039
18436
  } catch {
18040
18437
  }
18041
18438
  }
18042
- return !(0, import_node_fs19.existsSync)(targetPath);
18439
+ return !(0, import_node_fs20.existsSync)(targetPath);
18043
18440
  }
18044
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18045
- return !(0, import_node_fs19.existsSync)(targetPath);
18441
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
18442
+ return !(0, import_node_fs20.existsSync)(targetPath);
18046
18443
  } catch {
18047
18444
  return false;
18048
18445
  }
@@ -18055,18 +18452,18 @@ async function applyNestedPluginTreeCleanup(paths, log) {
18055
18452
  }
18056
18453
  return true;
18057
18454
  }
18058
- var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
18455
+ var gitignorePath = () => (0, import_node_path20.join)(process.cwd(), ".gitignore");
18059
18456
  function readTextFile(path2) {
18060
18457
  try {
18061
- if (!(0, import_node_fs19.existsSync)(path2)) return null;
18062
- return (0, import_node_fs19.readFileSync)(path2, "utf8");
18458
+ if (!(0, import_node_fs20.existsSync)(path2)) return null;
18459
+ return (0, import_node_fs20.readFileSync)(path2, "utf8");
18063
18460
  } catch {
18064
18461
  return null;
18065
18462
  }
18066
18463
  }
18067
18464
  function mcpDirExists(path2) {
18068
18465
  try {
18069
- return (0, import_node_fs19.existsSync)(path2);
18466
+ return (0, import_node_fs20.existsSync)(path2);
18070
18467
  } catch {
18071
18468
  return false;
18072
18469
  }
@@ -18074,60 +18471,60 @@ function mcpDirExists(path2) {
18074
18471
  function mcpConfigTargets() {
18075
18472
  const cwd = process.cwd();
18076
18473
  const home = (0, import_node_os7.homedir)();
18077
- const cursorProjectDir = (0, import_node_path19.join)(cwd, ".cursor");
18078
- const cursorUserDir = (0, import_node_path19.join)(home, ".cursor");
18079
- const codexDir = (0, import_node_path19.join)(home, ".codex");
18474
+ const cursorProjectDir = (0, import_node_path20.join)(cwd, ".cursor");
18475
+ const cursorUserDir = (0, import_node_path20.join)(home, ".cursor");
18476
+ const codexDir = (0, import_node_path20.join)(home, ".codex");
18080
18477
  return [
18081
18478
  // Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
18082
18479
  {
18083
18480
  host: "claude-code",
18084
18481
  label: "Claude Code project MCP",
18085
- path: (0, import_node_path19.join)(cwd, ".mcp.json"),
18482
+ path: (0, import_node_path20.join)(cwd, ".mcp.json"),
18086
18483
  format: "json",
18087
18484
  present: true,
18088
18485
  // parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
18089
18486
  isRepoFile: true,
18090
18487
  createWhenFileAbsent: false,
18091
- content: readTextFile((0, import_node_path19.join)(cwd, ".mcp.json"))
18488
+ content: readTextFile((0, import_node_path20.join)(cwd, ".mcp.json"))
18092
18489
  },
18093
18490
  // Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
18094
18491
  {
18095
18492
  host: "cursor-project",
18096
18493
  label: "Cursor project MCP",
18097
- path: (0, import_node_path19.join)(cursorProjectDir, "mcp.json"),
18494
+ path: (0, import_node_path20.join)(cursorProjectDir, "mcp.json"),
18098
18495
  format: "json",
18099
18496
  present: mcpDirExists(cursorProjectDir),
18100
18497
  isRepoFile: true,
18101
18498
  createWhenFileAbsent: true,
18102
- content: readTextFile((0, import_node_path19.join)(cursorProjectDir, "mcp.json"))
18499
+ content: readTextFile((0, import_node_path20.join)(cursorProjectDir, "mcp.json"))
18103
18500
  },
18104
18501
  // Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
18105
18502
  {
18106
18503
  host: "cursor-user",
18107
18504
  label: "Cursor user MCP",
18108
- path: (0, import_node_path19.join)(cursorUserDir, "mcp.json"),
18505
+ path: (0, import_node_path20.join)(cursorUserDir, "mcp.json"),
18109
18506
  format: "json",
18110
18507
  present: mcpDirExists(cursorUserDir),
18111
18508
  isRepoFile: false,
18112
18509
  createWhenFileAbsent: true,
18113
- content: readTextFile((0, import_node_path19.join)(cursorUserDir, "mcp.json"))
18510
+ content: readTextFile((0, import_node_path20.join)(cursorUserDir, "mcp.json"))
18114
18511
  },
18115
18512
  // Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
18116
18513
  {
18117
18514
  host: "codex",
18118
18515
  label: "Codex user config",
18119
- path: (0, import_node_path19.join)(codexDir, "config.toml"),
18516
+ path: (0, import_node_path20.join)(codexDir, "config.toml"),
18120
18517
  format: "toml",
18121
18518
  present: mcpDirExists(codexDir),
18122
18519
  isRepoFile: false,
18123
18520
  createWhenFileAbsent: true,
18124
- content: readTextFile((0, import_node_path19.join)(codexDir, "config.toml"))
18521
+ content: readTextFile((0, import_node_path20.join)(codexDir, "config.toml"))
18125
18522
  }
18126
18523
  ];
18127
18524
  }
18128
18525
  function writeMcpConfigFile(path2, content) {
18129
18526
  try {
18130
- (0, import_node_fs19.writeFileSync)(path2, content, "utf8");
18527
+ (0, import_node_fs20.writeFileSync)(path2, content, "utf8");
18131
18528
  return true;
18132
18529
  } catch {
18133
18530
  return false;
@@ -18207,7 +18604,7 @@ function strayBrowserArtifactPaths() {
18207
18604
  const cwd = process.cwd();
18208
18605
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
18209
18606
  try {
18210
- return (0, import_node_fs19.existsSync)((0, import_node_path19.join)(cwd, rel));
18607
+ return (0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, rel));
18211
18608
  } catch {
18212
18609
  return false;
18213
18610
  }
@@ -18215,37 +18612,119 @@ function strayBrowserArtifactPaths() {
18215
18612
  }
18216
18613
  function readGitignore() {
18217
18614
  try {
18218
- return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
18615
+ return (0, import_node_fs20.readFileSync)(gitignorePath(), "utf8");
18219
18616
  } catch {
18220
18617
  return null;
18221
18618
  }
18222
18619
  }
18223
18620
  function writeGitignore(content) {
18224
18621
  try {
18225
- (0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
18622
+ (0, import_node_fs20.writeFileSync)(gitignorePath(), content, "utf8");
18226
18623
  return true;
18227
18624
  } catch {
18228
18625
  return false;
18229
18626
  }
18230
18627
  }
18628
+ function readRepoJson(rel) {
18629
+ try {
18630
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), rel), "utf8"));
18631
+ } catch {
18632
+ return null;
18633
+ }
18634
+ }
18635
+ var ENCODING_SCAN_ROOT_FILES = ["AGENTS.md", "README.md", "CLAUDE.md", "package.json", "opencode.json"];
18636
+ var ENCODING_SCAN_DIRS = [".claude", "hooks", "scripts"];
18637
+ var ENCODING_SCAN_EXTS = /* @__PURE__ */ new Set([".md", ".json", ".jsonc", ".ts", ".js", ".cjs", ".mjs", ".sh", ".ps1", ".py", ".txt"]);
18638
+ var ENCODING_SCAN_MAX_FILES = 400;
18639
+ function listTextFilesUnder(dirRel, out, budget) {
18640
+ let entries;
18641
+ try {
18642
+ entries = (0, import_node_fs20.readdirSync)((0, import_node_path20.join)(process.cwd(), dirRel), { withFileTypes: true });
18643
+ } catch {
18644
+ return;
18645
+ }
18646
+ for (const ent of entries) {
18647
+ if (budget.count >= ENCODING_SCAN_MAX_FILES) return;
18648
+ const rel = `${dirRel}/${ent.name}`;
18649
+ if (ent.isDirectory()) {
18650
+ if (ent.name === "node_modules" || ent.name === "dist" || ent.name === ".git" || ent.name.startsWith(".")) continue;
18651
+ listTextFilesUnder(rel, out, budget);
18652
+ } else if (ent.isFile()) {
18653
+ const ext = ent.name.slice(ent.name.lastIndexOf("."));
18654
+ if (ENCODING_SCAN_EXTS.has(ext)) {
18655
+ out.push(rel);
18656
+ budget.count++;
18657
+ }
18658
+ }
18659
+ }
18660
+ }
18661
+ function managedFileEncodingFindings() {
18662
+ const cwd = process.cwd();
18663
+ const paths = [];
18664
+ for (const f of ENCODING_SCAN_ROOT_FILES) {
18665
+ try {
18666
+ if ((0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, f))) paths.push(f);
18667
+ } catch {
18668
+ }
18669
+ }
18670
+ const budget = { count: 0 };
18671
+ for (const d of ENCODING_SCAN_DIRS) listTextFilesUnder(d, paths, budget);
18672
+ const findings = [];
18673
+ for (const rel of paths) {
18674
+ try {
18675
+ const probe = inspectFileEncoding((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cwd, rel)));
18676
+ if (probe.bom || probe.invalidUtf8 || probe.replacementChars) findings.push({ path: rel, ...probe });
18677
+ } catch {
18678
+ }
18679
+ }
18680
+ return findings;
18681
+ }
18682
+ async function branchAheadBehind(branch) {
18683
+ const spec = `origin/${branch}...${branch}`;
18684
+ try {
18685
+ const { stdout } = await execFileP2("git", ["rev-list", "--left-right", "--count", spec], { timeout: GIT_TIMEOUT_MS });
18686
+ const [behind, ahead] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
18687
+ if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
18688
+ return { behind, ahead };
18689
+ } catch {
18690
+ return null;
18691
+ }
18692
+ }
18693
+ async function localReleaseBranchSnapshots() {
18694
+ let local;
18695
+ try {
18696
+ const { stdout } = await execFileP2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS });
18697
+ local = stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
18698
+ } catch {
18699
+ return [];
18700
+ }
18701
+ const releaseBranches = local.filter((b) => b === "development" || b === "main" || b === "master" || b === "rc" || b.startsWith("release/"));
18702
+ const rows = [];
18703
+ for (const branch of releaseBranches) {
18704
+ const ab = await branchAheadBehind(branch);
18705
+ if (ab) rows.push({ name: branch, ...ab });
18706
+ }
18707
+ return rows;
18708
+ }
18231
18709
  async function runDoctor(opts, io = consoleIo, readOrigin) {
18232
18710
  if (opts.guide) {
18233
18711
  if (opts.json) io.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
18234
18712
  else io.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
18235
18713
  return;
18236
18714
  }
18237
- const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
18715
+ const repairLocal = !opts.fast && (!opts.json || Boolean(opts.apply) || Boolean(opts.preflight));
18238
18716
  const repoWritesAllowed = !opts.noRepoWrites;
18239
- const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
18717
+ const runExtended = !opts.fast && (Boolean(opts.verbose) || Boolean(opts.json));
18718
+ const slowSkippedCount = opts.fast ? DOCTOR_EXTENDED_CHECK_LABELS.size : 0;
18240
18719
  const checks = [];
18241
18720
  const REWRITE_KEY = "url.https://github.com/.insteadOf";
18242
18721
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
18243
18722
  const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
18244
- githubLogin(),
18723
+ opts.fast ? Promise.resolve(void 0) : githubLogin(),
18245
18724
  execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
18246
- fetchReleasedVersion(),
18725
+ opts.fast ? Promise.resolve({ version: void 0, source: "unavailable" }) : fetchReleasedVersion(),
18247
18726
  loadConfig(),
18248
- awsCallerArn(),
18727
+ opts.fast ? Promise.resolve(void 0) : awsCallerArn(),
18249
18728
  execFileP2("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false),
18250
18729
  // unset → repair below
18251
18730
  // hub-v3 step-back: org-membership gates the repo-local checks/repairs by the git `origin` remote,
@@ -18264,7 +18743,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18264
18743
  const semverPrefix = /^\d+\.\d+\.\d+/;
18265
18744
  const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
18266
18745
  const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
18267
- const cursorCacheStale = (0, import_node_fs19.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18746
+ const cursorCacheStale = (0, import_node_fs20.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18268
18747
  const healPlan = doctorHealPlan({
18269
18748
  isOrgRepo,
18270
18749
  surface,
@@ -18286,20 +18765,22 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18286
18765
  if (opts.preflight && !healPlan.needsEagerHeal) return;
18287
18766
  const eagerHeal = Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
18288
18767
  if (eagerHeal && healPlan.needsEagerHeal) io.err(DOCTOR_PREFLIGHT_NOTICE);
18289
- const repairFull = !opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
18290
- let ghInstalled = true;
18291
- if (!login) {
18292
- try {
18293
- await execFileP2("gh", ["--version"]);
18294
- } catch {
18295
- ghInstalled = false;
18768
+ const repairFull = !opts.fast && (!opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal);
18769
+ if (!opts.fast) {
18770
+ let ghInstalled = true;
18771
+ if (!login) {
18772
+ try {
18773
+ await execFileP2("gh", ["--version"]);
18774
+ } catch {
18775
+ ghInstalled = false;
18776
+ }
18296
18777
  }
18778
+ checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18297
18779
  }
18298
- checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18299
18780
  let onPath = pathProbe;
18300
18781
  if (!onPath) {
18301
18782
  const root = process.env.CLAUDE_PLUGIN_ROOT;
18302
- if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18783
+ if (root && (0, import_node_fs20.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18303
18784
  }
18304
18785
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
18305
18786
  const reloadHint = reloadAction(surface);
@@ -18342,7 +18823,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18342
18823
  return;
18343
18824
  }
18344
18825
  checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
18345
- const hubVersionInfo = await fetchHubVersionInfo(cfg.sagaApiUrl);
18826
+ const hubVersionInfo = opts.fast ? null : await fetchHubVersionInfo(cfg.sagaApiUrl);
18346
18827
  const installedVersion = resolveClientVersion();
18347
18828
  checks.push(
18348
18829
  buildHubCompatCheck({
@@ -18360,6 +18841,12 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18360
18841
  releasedVersion
18361
18842
  })
18362
18843
  );
18844
+ checks.push(
18845
+ buildCliVersionDriftCheck({
18846
+ currentVersion: installedVersion,
18847
+ releasedVersion
18848
+ })
18849
+ );
18363
18850
  }
18364
18851
  if (runExtended) {
18365
18852
  checks.push(buildAwsCrossAccountCheck({ callerArn }));
@@ -18446,8 +18933,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18446
18933
  checks.push(gitignoreCheck);
18447
18934
  checks.push(buildRepoLocalWorktreeCheck({
18448
18935
  isOrgRepo,
18449
- hasRepoLocalWorktrees: (0, import_node_fs19.existsSync)((0, import_node_path19.join)(process.cwd(), ".worktrees"))
18936
+ hasRepoLocalWorktrees: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), ".worktrees"))
18450
18937
  }));
18938
+ if (isOrgRepo) {
18939
+ const pkg = readRepoJson("package.json");
18940
+ const lock = readRepoJson("package-lock.json");
18941
+ checks.push(buildLockfileOptionalsCheck({
18942
+ isOrgRepo,
18943
+ packageJsonOptionalDeps: pkg?.optionalDependencies ?? null,
18944
+ lockfilePackages: lock?.packages ?? null
18945
+ }));
18946
+ } else {
18947
+ checks.push(buildLockfileOptionalsCheck({ isOrgRepo, packageJsonOptionalDeps: null, lockfilePackages: null }));
18948
+ }
18451
18949
  let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
18452
18950
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
18453
18951
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
@@ -18632,7 +19130,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18632
19130
  return versions.sort((a, b) => compareVersions(b, a))[0];
18633
19131
  };
18634
19132
  const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
18635
- const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
19133
+ const codexActiveVersion = () => !opts.fast && isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
18636
19134
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
18637
19135
  isOrgRepo,
18638
19136
  roots: mmiPluginCacheRootSnapshots(),
@@ -18735,7 +19233,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18735
19233
  }
18736
19234
  checks.push(nestedPluginTreeCheck);
18737
19235
  const cursorCacheRoot = cursorPluginCacheRoot();
18738
- const cursorCacheRootExists = (0, import_node_fs19.existsSync)(cursorCacheRoot);
19236
+ const cursorCacheRootExists = (0, import_node_fs20.existsSync)(cursorCacheRoot);
18739
19237
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
18740
19238
  checks.push(
18741
19239
  buildCursorPluginCacheCleanupCheck({
@@ -18763,7 +19261,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18763
19261
  cacheRootExists: cursorCacheRootExists,
18764
19262
  hubCheckout: hubCheckoutForCursorSeed(),
18765
19263
  execFileP: execFileP2,
18766
- mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path19.join)((0, import_node_os7.tmpdir)(), prefix)),
19264
+ mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path20.join)((0, import_node_os7.tmpdir)(), prefix)),
18767
19265
  log: (m) => io.err(m)
18768
19266
  });
18769
19267
  if (seeded) {
@@ -18840,6 +19338,23 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18840
19338
  cacheVersions: sessionCacheVersions
18841
19339
  })
18842
19340
  );
19341
+ {
19342
+ let redactorFailureLines = [];
19343
+ try {
19344
+ redactorFailureLines = (0, import_node_fs20.readFileSync)(activityLogPath(process.cwd()), "utf8").split("\n");
19345
+ } catch {
19346
+ redactorFailureLines = [];
19347
+ }
19348
+ const redactorSummary = summarizeRedactorFailures(redactorFailureLines);
19349
+ checks.push(
19350
+ buildRedactorLivenessCheck({
19351
+ isOrgRepo,
19352
+ failedCount: redactorSummary.failedCount,
19353
+ lastFailedTs: redactorSummary.lastFailedTs,
19354
+ lastFailedAction: redactorSummary.lastFailedAction
19355
+ })
19356
+ );
19357
+ }
18843
19358
  if (runExtended) {
18844
19359
  checks.push(
18845
19360
  buildBrowserArtifactsCheck({
@@ -18864,6 +19379,14 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18864
19379
  checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
18865
19380
  } catch {
18866
19381
  }
19382
+ try {
19383
+ checks.push(buildManagedFileEncodingCheck({ isOrgRepo, findings: managedFileEncodingFindings() }));
19384
+ } catch {
19385
+ }
19386
+ try {
19387
+ checks.push(buildStaleLocalBranchCheck({ isOrgRepo, branches: await localReleaseBranchSnapshots() }));
19388
+ } catch {
19389
+ }
18867
19390
  } else if (opts.apply && repoWritesAllowed) {
18868
19391
  const scratchRun = executeScratchGc(repoRoot, { apply: true });
18869
19392
  if (scratchRun.applied?.pruned.length) {
@@ -18884,6 +19407,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18884
19407
  healedCount,
18885
19408
  pluginReloadRequired,
18886
19409
  reloadHint,
19410
+ slowSkippedCount,
18887
19411
  // Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
18888
19412
  // an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
18889
19413
  // echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
@@ -18909,6 +19433,7 @@ function emitDoctorReport(opts, io, ctx) {
18909
19433
  const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
18910
19434
  const gaps = checks.filter((c) => !c.ok);
18911
19435
  if (opts.preflight) {
19436
+ process.exitCode = doctorExitCode(checks);
18912
19437
  const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
18913
19438
  if (outcome.line) io.err(outcome.line);
18914
19439
  if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
@@ -18935,7 +19460,8 @@ function emitDoctorReport(opts, io, ctx) {
18935
19460
  healedCount,
18936
19461
  pluginReloadRequired,
18937
19462
  reloadHint,
18938
- verbose: Boolean(opts.verbose)
19463
+ verbose: Boolean(opts.verbose),
19464
+ skippedSlowChecks: ctx.slowSkippedCount
18939
19465
  })) io.log(line);
18940
19466
  for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
18941
19467
  }
@@ -18958,23 +19484,23 @@ function mergeGuardHook(settings) {
18958
19484
  next.hooks = hooks;
18959
19485
  return next;
18960
19486
  }
18961
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path19.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
19487
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path20.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
18962
19488
  function ensureUserScopeGuardHook(opts = {}) {
18963
19489
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
18964
19490
  try {
18965
19491
  let current = null;
18966
- if ((0, import_node_fs19.existsSync)(path2)) {
19492
+ if ((0, import_node_fs20.existsSync)(path2)) {
18967
19493
  try {
18968
- current = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
19494
+ current = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
18969
19495
  } catch {
18970
19496
  return "failed";
18971
19497
  }
18972
19498
  }
18973
19499
  if (settingsHasGuardHook(current)) return "already";
18974
19500
  const merged = mergeGuardHook(current);
18975
- (0, import_node_fs19.mkdirSync)((0, import_node_path19.dirname)(path2), { recursive: true });
18976
- if ((0, import_node_fs19.existsSync)(path2)) (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
18977
- (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
19501
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
19502
+ if ((0, import_node_fs20.existsSync)(path2)) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
19503
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
18978
19504
  `, "utf8");
18979
19505
  return "written";
18980
19506
  } catch {
@@ -19111,7 +19637,7 @@ async function applyGcPlan(plan, remote) {
19111
19637
  cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
19112
19638
  beforeWorktrees,
19113
19639
  startingPath: branch.worktreePath,
19114
- pathExists: (p) => (0, import_node_fs20.existsSync)(p),
19640
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
19115
19641
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
19116
19642
  teardownWorktreeStage,
19117
19643
  deferredStore,
@@ -19134,7 +19660,7 @@ async function applyGcPlan(plan, remote) {
19134
19660
  for (const wt of plan.worktreeDirs) {
19135
19661
  try {
19136
19662
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
19137
- realpath: (path2) => (0, import_node_fs20.realpathSync)(path2)
19663
+ realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
19138
19664
  });
19139
19665
  if (!cleanupTarget.ok) {
19140
19666
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -19174,16 +19700,21 @@ async function requireFreshTrainCli(commandName) {
19174
19700
  throw new Error(staleTrainCliMessage(report, commandName));
19175
19701
  }
19176
19702
  var program2 = new Command();
19177
- program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
19703
+ program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
19178
19704
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
19179
- rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
19180
- const path2 = (0, import_node_path20.join)(process.cwd(), ".gitignore");
19181
- const current = (0, import_node_fs20.existsSync)(path2) ? (0, import_node_fs20.readFileSync)(path2, "utf8") : null;
19705
+ rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
19706
+ const path2 = (0, import_node_path21.join)(process.cwd(), ".gitignore");
19707
+ const current = (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null;
19182
19708
  const plan = planManagedGitignore(current);
19183
19709
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
19710
+ if (opts.json) {
19711
+ console.log(JSON.stringify(plan, null, 2));
19712
+ if (!opts.write && plan.changed) process.exitCode = 1;
19713
+ return;
19714
+ }
19184
19715
  if (opts.write) {
19185
19716
  if (plan.changed) {
19186
- (0, import_node_fs20.writeFileSync)(path2, plan.content, "utf8");
19717
+ (0, import_node_fs21.writeFileSync)(path2, plan.content, "utf8");
19187
19718
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
19188
19719
  } else {
19189
19720
  console.log("mmi-cli rules gitignore: up to date");
@@ -19348,7 +19879,7 @@ function runWorktreeInstall(command, cwd, quiet) {
19348
19879
  async function primaryCheckoutRoot(worktreeRoot) {
19349
19880
  try {
19350
19881
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
19351
- return out ? (0, import_node_path20.dirname)(out) : void 0;
19882
+ return out ? (0, import_node_path21.dirname)(out) : void 0;
19352
19883
  } catch {
19353
19884
  return void 0;
19354
19885
  }
@@ -19363,26 +19894,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
19363
19894
  function acquireWorktreeSetupLock(worktreeRoot) {
19364
19895
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
19365
19896
  const take = () => {
19366
- const fd = (0, import_node_fs20.openSync)(lockPath, "wx");
19897
+ const fd = (0, import_node_fs21.openSync)(lockPath, "wx");
19367
19898
  try {
19368
- (0, import_node_fs20.writeSync)(fd, String(Date.now()));
19899
+ (0, import_node_fs21.writeSync)(fd, String(Date.now()));
19369
19900
  } finally {
19370
- (0, import_node_fs20.closeSync)(fd);
19901
+ (0, import_node_fs21.closeSync)(fd);
19371
19902
  }
19372
19903
  return () => {
19373
19904
  try {
19374
- (0, import_node_fs20.rmSync)(lockPath, { force: true });
19905
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19375
19906
  } catch {
19376
19907
  }
19377
19908
  };
19378
19909
  };
19379
19910
  try {
19380
- (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(lockPath), { recursive: true });
19911
+ (0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(lockPath), { recursive: true });
19381
19912
  return take();
19382
19913
  } catch {
19383
19914
  try {
19384
- if (Date.now() - (0, import_node_fs20.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
19385
- (0, import_node_fs20.rmSync)(lockPath, { force: true });
19915
+ if (Date.now() - (0, import_node_fs21.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
19916
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19386
19917
  return take();
19387
19918
  }
19388
19919
  } catch {
@@ -19400,7 +19931,21 @@ worktree.command("create <branch>").description("create a worktree from a base r
19400
19931
  const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
19401
19932
  if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
19402
19933
  }
19403
- await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
19934
+ await addWorktreeRobust(wtPath, branch, base, {
19935
+ git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
19936
+ revParse: async (ref) => {
19937
+ try {
19938
+ return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
19939
+ } catch {
19940
+ return void 0;
19941
+ }
19942
+ },
19943
+ deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
19944
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
19945
+ log: (m) => {
19946
+ if (!o.json) console.error(` ${m}`);
19947
+ }
19948
+ });
19404
19949
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
19405
19950
  if (!o.json) console.error(` ${m}`);
19406
19951
  }));
@@ -19519,7 +20064,7 @@ async function attachToProject(issueNumber, repo, priority) {
19519
20064
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
19520
20065
  function scheduleRelatedDiscovery(o) {
19521
20066
  try {
19522
- const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
20067
+ const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
19523
20068
  if (o.repo) args.push("--repo", o.repo);
19524
20069
  spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
19525
20070
  } catch {
@@ -19872,7 +20417,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
19872
20417
  const vars = rawValues("--var");
19873
20418
  if (o.secretsFile) {
19874
20419
  try {
19875
- vars.push(`secrets=${(0, import_node_fs20.readFileSync)(o.secretsFile, "utf8")}`);
20420
+ vars.push(`secrets=${(0, import_node_fs21.readFileSync)(o.secretsFile, "utf8")}`);
19876
20421
  } catch (e) {
19877
20422
  return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
19878
20423
  }
@@ -20176,7 +20721,7 @@ issue.command("view <number>").description("read an issue as structured JSON \u2
20176
20721
  return fail(`issue view: ${raw}${argvNote}`);
20177
20722
  }
20178
20723
  });
20179
- issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
20724
+ issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").option("--fail-soft", "suppress errors (used by the background auto-caller; direct runs fail loudly)").action(async (o) => {
20180
20725
  const number = Number(o.number);
20181
20726
  if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
20182
20727
  const repo = await resolveRepo(o.repo);
@@ -20208,7 +20753,10 @@ issue.command("discover-related").description("find related issues for an existi
20208
20753
  ]);
20209
20754
  if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
20210
20755
  await execFileP2("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
20211
- } catch {
20756
+ } catch (e) {
20757
+ if (o.failSoft) return;
20758
+ const err = e;
20759
+ return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
20212
20760
  }
20213
20761
  });
20214
20762
  issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
@@ -20427,14 +20975,16 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
20427
20975
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
20428
20976
  console.log(JSON.stringify(created));
20429
20977
  });
20430
- pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
20978
+ pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').action(async (number, o) => {
20431
20979
  const n = Number(number);
20432
20980
  if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
20433
20981
  const repo = await resolveRepo(o.repo);
20434
20982
  if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
20435
- const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
20983
+ const fields = normalizeIssueViewJsonFields(o.json);
20984
+ const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
20985
+ const effective = fields === normalizeIssueViewJsonFields(void 0) ? defaultPrFields : fields;
20436
20986
  try {
20437
- const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
20987
+ const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
20438
20988
  console.log(JSON.stringify(data));
20439
20989
  } catch (e) {
20440
20990
  const err = e;
@@ -20442,9 +20992,9 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
20442
20992
  }
20443
20993
  });
20444
20994
  async function listCiWorkflowPaths(cwd = process.cwd()) {
20445
- const wfDir = (0, import_node_path20.join)(cwd, ".github", "workflows");
20446
- if (!(0, import_node_fs20.existsSync)(wfDir)) return [];
20447
- return (0, import_node_fs20.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
20995
+ const wfDir = (0, import_node_path21.join)(cwd, ".github", "workflows");
20996
+ if (!(0, import_node_fs21.existsSync)(wfDir)) return [];
20997
+ return (0, import_node_fs21.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
20448
20998
  }
20449
20999
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
20450
21000
  const repo = repoOpt ?? await resolveRepo();
@@ -20456,16 +21006,29 @@ async function resolveMergeCiPolicyForCheckout(repoOpt) {
20456
21006
  }
20457
21007
  function ciAuditDeps() {
20458
21008
  const cfgPromise = loadConfig();
21009
+ const root = hubRoot();
20459
21010
  return {
20460
21011
  client: defaultGitHubClient(),
20461
21012
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
20462
21013
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
20463
- // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
20464
- // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
20465
- // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
20466
- readSeedFile: (path2) => (0, import_node_fs20.existsSync)(path2) ? (0, import_node_fs20.readFileSync)(path2, "utf8") : null
21014
+ // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
21015
+ // Resolve from the installed package root so the reconcile works from any org repo checkout; when the
21016
+ // Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
21017
+ // gate re-seed step is skipped gracefully rather than failing mid-run.
21018
+ readSeedFile: (path2) => {
21019
+ if (!root) return null;
21020
+ const fullPath = (0, import_node_path21.join)(root, path2);
21021
+ return (0, import_node_fs21.existsSync)(fullPath) ? (0, import_node_fs21.readFileSync)(fullPath, "utf8") : null;
21022
+ }
20467
21023
  };
20468
21024
  }
21025
+ function hubRoot() {
21026
+ const fromPkg = (0, import_node_path21.join)(__dirname, "..", "..");
21027
+ const marker = "skills/bootstrap/seeds/manifest.json";
21028
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(fromPkg, marker))) return fromPkg;
21029
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), marker))) return process.cwd();
21030
+ return null;
21031
+ }
20469
21032
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
20470
21033
  const result = await resolveMergeCiPolicyForCheckout(o.repo);
20471
21034
  if (o.json) return printLine(JSON.stringify(result));
@@ -20539,6 +21102,12 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20539
21102
  checksPassing: checks === "success" || checks === "no-checks-reported"
20540
21103
  };
20541
21104
  },
21105
+ readPrState: async (prNumber, repo) => {
21106
+ const args = repo ? ["--repo", repo] : [];
21107
+ const result2 = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
21108
+ if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
21109
+ return JSON.parse(result2.state);
21110
+ },
20542
21111
  mergeAuto: async (prNumber, repo) => {
20543
21112
  const args = repo ? ["--repo", repo] : [];
20544
21113
  const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
@@ -20633,7 +21202,7 @@ async function createDeferredWorktreeStore() {
20633
21202
  },
20634
21203
  write: async (entries) => {
20635
21204
  try {
20636
- await (0, import_promises3.mkdir)((0, import_node_path20.dirname)(registryPath), { recursive: true });
21205
+ await (0, import_promises3.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
20637
21206
  await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
20638
21207
  } catch {
20639
21208
  }
@@ -20647,13 +21216,13 @@ var realWorktreeDirRemover = {
20647
21216
  probe: (p) => {
20648
21217
  let st;
20649
21218
  try {
20650
- st = (0, import_node_fs20.lstatSync)(p);
21219
+ st = (0, import_node_fs21.lstatSync)(p);
20651
21220
  } catch {
20652
21221
  return null;
20653
21222
  }
20654
21223
  if (st.isSymbolicLink()) return "link";
20655
21224
  try {
20656
- (0, import_node_fs20.readlinkSync)(p);
21225
+ (0, import_node_fs21.readlinkSync)(p);
20657
21226
  return "link";
20658
21227
  } catch {
20659
21228
  }
@@ -20661,7 +21230,7 @@ var realWorktreeDirRemover = {
20661
21230
  },
20662
21231
  readdir: (p) => {
20663
21232
  try {
20664
- return (0, import_node_fs20.readdirSync)(p);
21233
+ return (0, import_node_fs21.readdirSync)(p);
20665
21234
  } catch {
20666
21235
  return [];
20667
21236
  }
@@ -20670,9 +21239,9 @@ var realWorktreeDirRemover = {
20670
21239
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20671
21240
  detachLink: (p) => {
20672
21241
  try {
20673
- (0, import_node_fs20.rmdirSync)(p);
21242
+ (0, import_node_fs21.rmdirSync)(p);
20674
21243
  } catch {
20675
- (0, import_node_fs20.unlinkSync)(p);
21244
+ (0, import_node_fs21.unlinkSync)(p);
20676
21245
  }
20677
21246
  },
20678
21247
  removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -20702,9 +21271,9 @@ async function worktreeHasStageState(worktreePath) {
20702
21271
  }
20703
21272
  }
20704
21273
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20705
- if (!(0, import_node_fs20.existsSync)(statePath)) return false;
21274
+ if (!(0, import_node_fs21.existsSync)(statePath)) return false;
20706
21275
  try {
20707
- const state = JSON.parse((0, import_node_fs20.readFileSync)(statePath, "utf8"));
21276
+ const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
20708
21277
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20709
21278
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20710
21279
  } catch {
@@ -20776,7 +21345,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20776
21345
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
20777
21346
  beforeWorktrees,
20778
21347
  startingPath,
20779
- pathExists: (p) => (0, import_node_fs20.existsSync)(p),
21348
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
20780
21349
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20781
21350
  teardownWorktreeStage,
20782
21351
  deferredStore,
@@ -20997,7 +21566,7 @@ function stageScopedRunOpts(o) {
20997
21566
  };
20998
21567
  }
20999
21568
  function printLine(value) {
21000
- (0, import_node_fs20.writeSync)(1, `${value}
21569
+ (0, import_node_fs21.writeSync)(1, `${value}
21001
21570
  `);
21002
21571
  }
21003
21572
  function stageKeepAlive() {
@@ -21011,8 +21580,8 @@ async function resolveStage() {
21011
21580
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
21012
21581
  return decideStage({
21013
21582
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
21014
- hasCompose: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
21015
- hasEnvExample: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
21583
+ hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), "docker-compose.yml")),
21584
+ hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), ".env.example"))
21016
21585
  });
21017
21586
  }
21018
21587
  async function fetchStageVaultEnvMerge() {
@@ -21053,10 +21622,10 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
21053
21622
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
21054
21623
  return;
21055
21624
  }
21056
- const infra = resolvePortRangeInfra(process.cwd());
21625
+ const infra = resolvePortRangeInfra(process.cwd(), __dirname);
21057
21626
  if (!infra) {
21058
21627
  return failGraceful(
21059
- `port-range: no MMI-Hub allocator files found from ${process.cwd()}; run from an MMI-Hub checkout instead: mmi-cli port-range ${repo}${o.json ? " --json" : ""}`
21628
+ `port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
21060
21629
  );
21061
21630
  }
21062
21631
  const path2 = infra.registryPath;
@@ -21450,7 +22019,7 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
21450
22019
  if (!audit.ok) process.exitCode = 1;
21451
22020
  });
21452
22021
  var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
21453
- wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (pagesDir, o) => {
22022
+ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").option("--json", "machine-readable output").action(async (pagesDir, o) => {
21454
22023
  const repo = await resolveRepo(o.repo);
21455
22024
  if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
21456
22025
  const cfg = await loadConfig();
@@ -21461,7 +22030,7 @@ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:
21461
22030
  log: (msg) => console.log(msg),
21462
22031
  err: (msg) => console.error(msg)
21463
22032
  },
21464
- { repo, pagesDir }
22033
+ { repo, pagesDir, json: o.json }
21465
22034
  );
21466
22035
  if (!ok) process.exitCode = 1;
21467
22036
  });
@@ -21486,9 +22055,8 @@ wiki.command("plan").description("compute the release wiki plan for the cwd repo
21486
22055
  );
21487
22056
  if (!ok) process.exitCode = 1;
21488
22057
  });
21489
- var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
22058
+ var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action((o) => {
21490
22059
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
21491
- if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
21492
22060
  if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
21493
22061
  const steps = bootstrapPlan(o.repo, o.class);
21494
22062
  console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
@@ -21505,7 +22073,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
21505
22073
  client: defaultGitHubClient(),
21506
22074
  projectMeta: meta,
21507
22075
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
21508
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs20.existsSync)(path2) ? (0, import_node_fs20.readFileSync)(path2, "utf8") : null,
22076
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
21509
22077
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
21510
22078
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
21511
22079
  requiredGcpApis: (() => {
@@ -21538,7 +22106,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
21538
22106
  else console.log(renderOrgRulesetDriftReport(plan));
21539
22107
  if (plan.action !== "noop") process.exitCode = 1;
21540
22108
  });
21541
- bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
22109
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
21542
22110
  const o = {
21543
22111
  class: rawValue("--class", "deployable"),
21544
22112
  projectType: rawValue("--project-type", ""),
@@ -21556,12 +22124,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
21556
22124
  return fail(`bootstrap apply: ${e.message}`);
21557
22125
  }
21558
22126
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
21559
- if (!(0, import_node_fs20.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
21560
- const manifest = loadBootstrapSeeds((0, import_node_fs20.readFileSync)(manifestPath, "utf8"));
22127
+ if (!(0, import_node_fs21.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
22128
+ const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
21561
22129
  const baseBranch = o.class === "content" ? "main" : "development";
21562
22130
  const slug = parsedRepo.slug;
21563
22131
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
21564
- const readFile2 = (p) => (0, import_node_fs20.existsSync)(p) ? (0, import_node_fs20.readFileSync)(p, "utf8") : null;
22132
+ const readFile2 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
21565
22133
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
21566
22134
  const rawVars = {};
21567
22135
  for (const value of rawValues("--var")) {
@@ -21858,16 +22426,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21858
22426
  const repoClass = o.class;
21859
22427
  targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
21860
22428
  } else {
21861
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs20.existsSync)("projects.json") ? (0, import_node_fs20.readFileSync)("projects.json", "utf8") : null;
22429
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs21.existsSync)("projects.json") ? (0, import_node_fs21.readFileSync)("projects.json", "utf8") : null;
21862
22430
  if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
21863
- const fanoutJson = (0, import_node_fs20.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs20.readFileSync)(".github/fanout-targets.json", "utf8") : null;
22431
+ const fanoutJson = (0, import_node_fs21.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs21.readFileSync)(".github/fanout-targets.json", "utf8") : null;
21864
22432
  targets = loadAccessTargets(projectsJson, fanoutJson);
21865
22433
  }
21866
22434
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
21867
- const fileMatrix = (0, import_node_fs20.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs20.readFileSync)("access-matrix.json", "utf8")) : {};
22435
+ const fileMatrix = (0, import_node_fs21.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs21.readFileSync)("access-matrix.json", "utf8")) : {};
21868
22436
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
21869
22437
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
21870
- const fileContracts = (0, import_node_fs20.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs20.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
22438
+ const fileContracts = (0, import_node_fs21.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs21.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
21871
22439
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
21872
22440
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
21873
22441
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -21875,7 +22443,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21875
22443
  });
21876
22444
  access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
21877
22445
  var isWin2 = process.platform === "win32";
21878
- program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
22446
+ program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action((opts) => (
21879
22447
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
21880
22448
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
21881
22449
  ));
@@ -21884,9 +22452,13 @@ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugi
21884
22452
  program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
21885
22453
  if (isInsideRepoSubdir(process.cwd())) {
21886
22454
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
22455
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
22456
+ return;
22457
+ }
22458
+ if (!await isOrgRepoRoot()) {
22459
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (non-org repo)" });
21887
22460
  return;
21888
22461
  }
21889
- if (!await isOrgRepoRoot()) return;
21890
22462
  spawnDeferredGcSweep();
21891
22463
  const { parallel, sequential } = buildSessionStartPlan({
21892
22464
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
@@ -21906,6 +22478,12 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21906
22478
  // never costs banner time and next session's glance renders instantly within budget.
21907
22479
  scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
21908
22480
  }),
22481
+ // command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
22482
+ // not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
22483
+ commandLadderHint: async (io) => {
22484
+ const hint = commandLadderHint();
22485
+ if (hint) io.log(hint);
22486
+ },
21909
22487
  // local train sync (#2582): ff local development/main/rc to origin so this checkout never lags a
21910
22488
  // release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
21911
22489
  // fail-soft, silent unless a branch moved.
@@ -21924,6 +22502,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21924
22502
  spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
21925
22503
  consoleIo.log(worktreeBanner);
21926
22504
  }
22505
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
21927
22506
  });
21928
22507
  installProcessBackstop();
21929
22508
  program2.parseAsync().catch((e) => failGraceful(e.message));