@mutmutco/cli 3.10.0 → 3.12.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
@@ -4955,7 +4957,7 @@ function parseIssueSelector(selector, defaultRepo) {
4955
4957
  throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
4956
4958
  }
4957
4959
  function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
4958
- const empty = () => ({ userOwned: [], claimable: [], taken: [] });
4960
+ const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
4959
4961
  const groups = { primary: empty(), secondary: empty() };
4960
4962
  const viewerKey = viewer.toLowerCase();
4961
4963
  const repoKey = currentRepo.toLowerCase();
@@ -4970,6 +4972,8 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
4970
4972
  groups[scope].claimable.push(item);
4971
4973
  } else if (item.assignees.length > 0) {
4972
4974
  groups[scope].taken.push(item);
4975
+ } else {
4976
+ groups[scope].unownedInFlight.push(item);
4973
4977
  }
4974
4978
  }
4975
4979
  sortBuckets(groups.primary);
@@ -5640,6 +5644,7 @@ function renderScope(lines, label, title, buckets) {
5640
5644
  lines.push("", `${label} \xB7 ${title}`);
5641
5645
  renderOwned(lines, buckets.userOwned);
5642
5646
  renderClaimable(lines, buckets.claimable);
5647
+ renderUnowned(lines, buckets.unownedInFlight);
5643
5648
  renderTaken(lines, buckets.taken);
5644
5649
  }
5645
5650
  function renderOwned(lines, items) {
@@ -5662,17 +5667,23 @@ function renderTaken(lines, items) {
5662
5667
  lines.push("Taken");
5663
5668
  for (const item of items) lines.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}`);
5664
5669
  }
5670
+ function renderUnowned(lines, items) {
5671
+ if (!items.length) return;
5672
+ lines.push("Unowned in-flight (unassigned \u2014 needs an owner)");
5673
+ for (const item of items) lines.push(` ${item.status} \xB7 ${renderTitledItem(item)}`);
5674
+ }
5665
5675
  function renderTitledItem(item) {
5666
5676
  const pri = item.priority ? ` \xB7 ${item.priority}` : "";
5667
5677
  return `${item.ref} - [${item.type ?? "item"}]${pri} ${item.title}`;
5668
5678
  }
5669
5679
  function hasItems(buckets) {
5670
- return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0;
5680
+ return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0 || buckets.unownedInFlight.length > 0;
5671
5681
  }
5672
5682
  function sortBuckets(buckets) {
5673
5683
  buckets.userOwned.sort(compareItems);
5674
5684
  buckets.claimable.sort(compareItems);
5675
5685
  buckets.taken.sort(compareItems);
5686
+ buckets.unownedInFlight.sort(compareItems);
5676
5687
  }
5677
5688
  function compareItems(a, b) {
5678
5689
  return (STATUS_ORDER.get(a.status) ?? 99) - (STATUS_ORDER.get(b.status) ?? 99) || a.repository.localeCompare(b.repository) || a.number - b.number;
@@ -5838,9 +5849,31 @@ async function refreshBoardSliceCache(deps) {
5838
5849
  }
5839
5850
  }
5840
5851
 
5841
- // src/worktree.ts
5852
+ // src/hook-activity.ts
5842
5853
  var import_node_fs9 = require("node:fs");
5843
5854
  var import_node_path9 = require("node:path");
5855
+ var DEFAULT_SURFACE = "claude";
5856
+ function activityLogPath(cwd) {
5857
+ return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
5858
+ }
5859
+ function appendHookActivity(cwd, entry) {
5860
+ try {
5861
+ const path2 = activityLogPath(cwd);
5862
+ const line = {
5863
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5864
+ surface: DEFAULT_SURFACE,
5865
+ ...entry
5866
+ };
5867
+ (0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
5868
+ (0, import_node_fs9.appendFileSync)(path2, `${JSON.stringify(line)}
5869
+ `, "utf8");
5870
+ } catch {
5871
+ }
5872
+ }
5873
+
5874
+ // src/worktree.ts
5875
+ var import_node_fs10 = require("node:fs");
5876
+ var import_node_path10 = require("node:path");
5844
5877
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
5845
5878
  var PKG = "package.json";
5846
5879
  var LOCKFILE = "package-lock.json";
@@ -5848,21 +5881,21 @@ var NODE_MODULES = "node_modules";
5848
5881
  var realFsProbe = {
5849
5882
  isDir: (p) => {
5850
5883
  try {
5851
- return (0, import_node_fs9.statSync)(p).isDirectory();
5884
+ return (0, import_node_fs10.statSync)(p).isDirectory();
5852
5885
  } catch {
5853
5886
  return false;
5854
5887
  }
5855
5888
  },
5856
5889
  isFile: (p) => {
5857
5890
  try {
5858
- return (0, import_node_fs9.statSync)(p).isFile();
5891
+ return (0, import_node_fs10.statSync)(p).isFile();
5859
5892
  } catch {
5860
5893
  return false;
5861
5894
  }
5862
5895
  },
5863
5896
  listDirs: (p) => {
5864
5897
  try {
5865
- return (0, import_node_fs9.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5898
+ return (0, import_node_fs10.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5866
5899
  } catch {
5867
5900
  return [];
5868
5901
  }
@@ -5870,12 +5903,12 @@ var realFsProbe = {
5870
5903
  };
5871
5904
  function scanInstallDirs(root, fs2 = realFsProbe) {
5872
5905
  const factsFor = (dir) => {
5873
- const abs = dir ? (0, import_node_path9.join)(root, dir) : root;
5906
+ const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
5874
5907
  return {
5875
5908
  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))
5909
+ hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
5910
+ hasLockfile: fs2.isFile((0, import_node_path10.join)(abs, LOCKFILE)),
5911
+ hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES))
5879
5912
  };
5880
5913
  };
5881
5914
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -5885,7 +5918,7 @@ function npmInstallTargets(dirs) {
5885
5918
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
5886
5919
  }
5887
5920
  function isLinkedWorktree(root, fs2 = realFsProbe) {
5888
- return fs2.isFile((0, import_node_path9.join)(root, ".git"));
5921
+ return fs2.isFile((0, import_node_path10.join)(root, ".git"));
5889
5922
  }
5890
5923
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5891
5924
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -5895,8 +5928,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5895
5928
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
5896
5929
  }
5897
5930
  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);
5931
+ (0, import_node_fs10.mkdirSync)((0, import_node_path10.dirname)(to), { recursive: true });
5932
+ (0, import_node_fs10.copyFileSync)(from, to);
5900
5933
  }
5901
5934
  async function provisionWorktree(worktreeRoot, deps) {
5902
5935
  const fs2 = deps.fs ?? realFsProbe;
@@ -5908,7 +5941,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5908
5941
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
5909
5942
  const installed = [];
5910
5943
  for (const target of targets) {
5911
- const cwd = target.dir ? (0, import_node_path9.join)(worktreeRoot, target.dir) : worktreeRoot;
5944
+ const cwd = target.dir ? (0, import_node_path10.join)(worktreeRoot, target.dir) : worktreeRoot;
5912
5945
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
5913
5946
  await deps.runInstall(target.command, cwd);
5914
5947
  installed.push(target);
@@ -5917,7 +5950,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5917
5950
  const copySkipped = [];
5918
5951
  const primary = await deps.primaryCheckout();
5919
5952
  for (const rel of LOCAL_ONLY_FILES) {
5920
- const dest = (0, import_node_path9.join)(worktreeRoot, rel);
5953
+ const dest = (0, import_node_path10.join)(worktreeRoot, rel);
5921
5954
  if (fs2.isFile(dest)) {
5922
5955
  copySkipped.push({ file: rel, reason: "already-present" });
5923
5956
  continue;
@@ -5926,11 +5959,11 @@ async function provisionWorktree(worktreeRoot, deps) {
5926
5959
  copySkipped.push({ file: rel, reason: "no-primary" });
5927
5960
  continue;
5928
5961
  }
5929
- if (!fs2.isFile((0, import_node_path9.join)(primary, rel))) {
5962
+ if (!fs2.isFile((0, import_node_path10.join)(primary, rel))) {
5930
5963
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
5931
5964
  continue;
5932
5965
  }
5933
- copyFile((0, import_node_path9.join)(primary, rel), dest);
5966
+ copyFile((0, import_node_path10.join)(primary, rel), dest);
5934
5967
  copied.push(rel);
5935
5968
  log(`copied local config: ${rel}`);
5936
5969
  }
@@ -5938,13 +5971,50 @@ async function provisionWorktree(worktreeRoot, deps) {
5938
5971
  }
5939
5972
  function defaultWorktreePath(repoRoot, branch) {
5940
5973
  const safe = branch.replace(/[/\\]+/g, "-");
5941
- return (0, import_node_path9.join)((0, import_node_path9.dirname)(repoRoot), "mmi-worktrees", safe);
5974
+ return (0, import_node_path10.join)((0, import_node_path10.dirname)(repoRoot), "mmi-worktrees", safe);
5942
5975
  }
5943
5976
  function resolveWorktreeBase(from, remote) {
5944
5977
  const remotePrefix = `${remote}/`;
5945
5978
  const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
5946
5979
  return { base: from, fetchBranch };
5947
5980
  }
5981
+ var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
5982
+ function isGitConfigLockError(error) {
5983
+ return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
5984
+ }
5985
+ var ADD_WORKTREE_BACKOFF_MS = [100, 250, 500, 750, 1e3];
5986
+ var ADD_WORKTREE_MAX_ATTEMPTS = 6;
5987
+ async function cleanupOrphanBranch(branch, base, preExistingOid, deps) {
5988
+ if (preExistingOid !== void 0) return;
5989
+ const currentOid = await deps.revParse(`refs/heads/${branch}`);
5990
+ if (currentOid === void 0) return;
5991
+ const baseOid = await deps.revParse(base);
5992
+ if (baseOid !== void 0 && currentOid !== baseOid) return;
5993
+ await deps.deleteBranch(branch);
5994
+ }
5995
+ async function addWorktreeRobust(wtPath, branch, base, deps) {
5996
+ const maxAttempts = deps.maxAttempts ?? ADD_WORKTREE_MAX_ATTEMPTS;
5997
+ const backoff = deps.backoffMs ?? ADD_WORKTREE_BACKOFF_MS;
5998
+ const log = deps.log ?? (() => {
5999
+ });
6000
+ const preExistingOid = await deps.revParse(`refs/heads/${branch}`);
6001
+ for (let i = 0; i < maxAttempts; i++) {
6002
+ try {
6003
+ await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
6004
+ return;
6005
+ } catch (e) {
6006
+ await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
6007
+ });
6008
+ const retriesLeft = i < maxAttempts - 1;
6009
+ if (retriesLeft && isGitConfigLockError(e)) {
6010
+ log(`worktree add: .git/config lock busy, retrying (${i + 1}/${maxAttempts - 1})`);
6011
+ await deps.sleep(backoff[Math.min(i, backoff.length - 1)]);
6012
+ continue;
6013
+ }
6014
+ throw e;
6015
+ }
6016
+ }
6017
+ }
5948
6018
 
5949
6019
  // src/whoami.ts
5950
6020
  async function resolveWhoami(deps) {
@@ -5969,8 +6039,39 @@ function whoamiLine(report) {
5969
6039
  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
6040
  }
5971
6041
 
6042
+ // src/command-ladder-hint.ts
6043
+ var COMMAND_LADDER_WRITES = [
6044
+ { gh: "gh issue create", replacement: "mmi-cli issue create" },
6045
+ { gh: "gh issue comment", replacement: "mmi-cli issue comment" },
6046
+ { gh: "gh pr create", replacement: "mmi-cli pr create" },
6047
+ { gh: "gh pr merge", replacement: "mmi-cli pr merge (or mmi-cli pr land)" }
6048
+ ];
6049
+ var COMMAND_LADDER_READS = [
6050
+ { gh: "gh issue view <N>", replacement: "mmi-cli issue view <N>" },
6051
+ { gh: "gh pr view <N>", replacement: "mmi-cli pr view <N>" },
6052
+ { gh: "gh pr checks <N>", replacement: "mmi-cli pr checks-wait <N>" }
6053
+ ];
6054
+ var COMMAND_LADDER_BOARD = [
6055
+ { gh: "gh issue list / project", replacement: "mmi-cli board read" }
6056
+ ];
6057
+ function commandLadderHint() {
6058
+ const pad = 28;
6059
+ const lines = [];
6060
+ lines.push("[mmi] Command ladder (#2609): reach for mmi-cli first");
6061
+ for (const e of COMMAND_LADDER_WRITES) {
6062
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6063
+ }
6064
+ for (const e of COMMAND_LADDER_READS) {
6065
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6066
+ }
6067
+ for (const e of COMMAND_LADDER_BOARD) {
6068
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6069
+ }
6070
+ return lines.join("\n");
6071
+ }
6072
+
5972
6073
  // src/index.ts
5973
- var import_node_path20 = require("node:path");
6074
+ var import_node_path21 = require("node:path");
5974
6075
 
5975
6076
  // src/merge-ci-policy.ts
5976
6077
  function resolveMergeCiPolicy(input) {
@@ -6912,7 +7013,7 @@ async function auditRepoCi(repo, deps) {
6912
7013
  ok: hasRequiredChecks,
6913
7014
  label: "product required status checks active",
6914
7015
  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)`
7016
+ 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
7017
  });
6917
7018
  if (hasGateWorkflow) {
6918
7019
  const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
@@ -7221,6 +7322,13 @@ async function runPrLand(prNumber, options, deps) {
7221
7322
  error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
7222
7323
  };
7223
7324
  }
7325
+ const prState = await deps.readPrState(prNumber, repo);
7326
+ if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
7327
+ return {
7328
+ ...base,
7329
+ error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
7330
+ };
7331
+ }
7224
7332
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
7225
7333
  base.mergeStatus = merge.mergeStatus;
7226
7334
  if (merge.mergeStatus === "failed") {
@@ -7241,10 +7349,10 @@ async function runPrLand(prNumber, options, deps) {
7241
7349
  }
7242
7350
 
7243
7351
  // src/wave-status.ts
7244
- var import_node_fs11 = require("node:fs");
7352
+ var import_node_fs12 = require("node:fs");
7245
7353
 
7246
7354
  // src/gc.ts
7247
- var import_node_path10 = require("node:path");
7355
+ var import_node_path11 = require("node:path");
7248
7356
  var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
7249
7357
  var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
7250
7358
  var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
@@ -7515,8 +7623,8 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
7515
7623
  return { ok: true, path: worktreePath };
7516
7624
  }
7517
7625
  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");
7626
+ const parent = (0, import_node_path11.dirname)(repoRoot);
7627
+ return (0, import_node_path11.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path11.join)(parent, "mmi-worktrees");
7520
7628
  }
7521
7629
  function classifySiblingWorktreeDir(entry) {
7522
7630
  if (!entry.ownedByCurrentRepo) {
@@ -8177,8 +8285,8 @@ function formatGcPlan(plan, apply) {
8177
8285
 
8178
8286
  // src/stage-runner.ts
8179
8287
  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");
8288
+ var import_node_fs11 = require("node:fs");
8289
+ var import_node_path12 = require("node:path");
8182
8290
  var import_node_net = require("node:net");
8183
8291
  var import_node_util6 = require("node:util");
8184
8292
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
@@ -8295,11 +8403,11 @@ function appendForceRecreate(up) {
8295
8403
  return `${up.trimEnd()} --force-recreate`;
8296
8404
  }
8297
8405
  function stageStatePath(cwd = process.cwd()) {
8298
- return (0, import_node_path11.join)(cwd, "tmp", "stage", "state.json");
8406
+ return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
8299
8407
  }
8300
8408
  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");
8409
+ const dir = (0, import_node_path12.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path12.resolve)(cwd, gitCommonDir);
8410
+ return (0, import_node_path12.join)(dir, "mmi", "stage", "state.json");
8303
8411
  }
8304
8412
  function normPath2(path2) {
8305
8413
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -8523,14 +8631,14 @@ function stageProcessEnv(stagePort, extraEnv) {
8523
8631
  }
8524
8632
  async function ensureStageRuntimeEnv(config, opts, cwd) {
8525
8633
  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
8634
+ const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
8635
+ const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
8636
+ if (!(0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8637
+ (0, import_node_fs11.copyFileSync)(example, target);
8638
+ } else if ((0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8639
+ const stale = detectStaleEnvFile((0, import_node_fs11.readFileSync)(example, "utf8"), (0, import_node_fs11.readFileSync)(target, "utf8"), {
8640
+ exampleMtimeMs: (0, import_node_fs11.statSync)(example).mtimeMs,
8641
+ targetMtimeMs: (0, import_node_fs11.statSync)(target).mtimeMs
8534
8642
  });
8535
8643
  if (stale) {
8536
8644
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -8538,8 +8646,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
8538
8646
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
8539
8647
  }
8540
8648
  }
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");
8649
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs11.existsSync)(target)) {
8650
+ (0, import_node_fs11.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs11.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
8543
8651
  }
8544
8652
  }
8545
8653
  async function gitText(cwd, args) {
@@ -8569,20 +8677,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
8569
8677
  return void 0;
8570
8678
  }
8571
8679
  function readState(path2) {
8572
- if (!(0, import_node_fs10.existsSync)(path2)) return null;
8680
+ if (!(0, import_node_fs11.existsSync)(path2)) return null;
8573
8681
  try {
8574
- return JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
8682
+ return JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
8575
8683
  } catch {
8576
8684
  return null;
8577
8685
  }
8578
8686
  }
8579
8687
  function mkdirFor(path2) {
8580
8688
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
8581
- (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
8689
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8582
8690
  }
8583
8691
  function writeState(path2, state) {
8584
8692
  mkdirFor(path2);
8585
- (0, import_node_fs10.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8693
+ (0, import_node_fs11.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8586
8694
  }
8587
8695
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
8588
8696
  const reservation = {
@@ -8602,7 +8710,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
8602
8710
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
8603
8711
  }
8604
8712
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
8605
- (0, import_node_fs10.rmSync)(path2, { force: true });
8713
+ (0, import_node_fs11.rmSync)(path2, { force: true });
8606
8714
  }
8607
8715
  }
8608
8716
  async function killTree(pid) {
@@ -8684,6 +8792,7 @@ async function startStage(config = {}, opts = {}) {
8684
8792
  if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
8685
8793
  if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
8686
8794
  const extraEnv = stageExtraEnv(config, stagePort);
8795
+ const vaultProcessEnv = opts.vaultEnvMerge ?? {};
8687
8796
  let up = sub(config.up.trim());
8688
8797
  if (opts.forceRecreate) up = appendForceRecreate(up);
8689
8798
  const identity = await resolveStageIdentity(cwd);
@@ -8696,7 +8805,8 @@ async function startStage(config = {}, opts = {}) {
8696
8805
  detached: process.platform !== "win32",
8697
8806
  windowsHide: true,
8698
8807
  stdio: "ignore",
8699
- env: { ...process.env, ...stageProcessEnv(stagePort, extraEnv) }
8808
+ // Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
8809
+ env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
8700
8810
  });
8701
8811
  const state = {
8702
8812
  pid: child.pid ?? 0,
@@ -8758,8 +8868,8 @@ async function runStage(config = {}, opts = {}) {
8758
8868
  await ensureStageRuntimeEnv(config, opts, cwd);
8759
8869
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
8760
8870
  } catch (e) {
8761
- (0, import_node_fs10.rmSync)(statePath, { force: true });
8762
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs10.rmSync)(globalStatePath, { force: true });
8871
+ (0, import_node_fs11.rmSync)(statePath, { force: true });
8872
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs11.rmSync)(globalStatePath, { force: true });
8763
8873
  throw e;
8764
8874
  }
8765
8875
  const started = await startStage(config, {
@@ -8784,9 +8894,9 @@ function parseNextFromHead(headText) {
8784
8894
  }
8785
8895
  function readStageSummary(worktreePath) {
8786
8896
  const statePath = stageStatePath(worktreePath);
8787
- if (!(0, import_node_fs11.existsSync)(statePath)) return void 0;
8897
+ if (!(0, import_node_fs12.existsSync)(statePath)) return void 0;
8788
8898
  try {
8789
- const state = JSON.parse((0, import_node_fs11.readFileSync)(statePath, "utf8"));
8899
+ const state = JSON.parse((0, import_node_fs12.readFileSync)(statePath, "utf8"));
8790
8900
  const port = typeof state.port === "number" ? state.port : void 0;
8791
8901
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
8792
8902
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -8902,7 +9012,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
8902
9012
  // src/gh-create.ts
8903
9013
  var import_promises = require("node:fs/promises");
8904
9014
  var import_node_os3 = require("node:os");
8905
- var import_node_path12 = require("node:path");
9015
+ var import_node_path13 = require("node:path");
8906
9016
  var import_node_crypto3 = require("node:crypto");
8907
9017
  var ISSUE_TYPES = ["bug", "feature", "task"];
8908
9018
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -8945,8 +9055,8 @@ async function bodyArgsViaFile(args, deps = {}) {
8945
9055
  const remove = deps.remove ?? import_promises.unlink;
8946
9056
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
8947
9057
  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(() => {
9058
+ const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
9059
+ await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
8950
9060
  });
8951
9061
  await write(file, args[i + 1], "utf8");
8952
9062
  return {
@@ -8983,12 +9093,13 @@ function parseAddedItemId(stdout) {
8983
9093
  function isAlreadyOnBoardError(stderr) {
8984
9094
  return /already exists in (?:this|the) project/i.test(stderr);
8985
9095
  }
8986
- function buildPrArgs({ title, body, base, head, repo }) {
9096
+ function buildPrArgs({ title, body, base, head, repo, draft }) {
8987
9097
  const args = ["pr", "create"];
8988
9098
  if (repo) args.push("--repo", repo);
8989
9099
  args.push("--title", title, "--body", body);
8990
9100
  if (base) args.push("--base", base);
8991
9101
  if (head) args.push("--head", head);
9102
+ if (draft) args.push("--draft");
8992
9103
  return args;
8993
9104
  }
8994
9105
 
@@ -9683,7 +9794,6 @@ function deriveStageGap(inputs) {
9683
9794
  return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
9684
9795
  }
9685
9796
  if (!inputs.hasCompose) missing.push("docker-compose.yml");
9686
- if (!inputs.hasEnvExample) missing.push(".env.example");
9687
9797
  if (!inputs.portRange) missing.push("Hub registry portRange");
9688
9798
  return missing.length ? `cannot derive a default local stage \u2014 missing: ${missing.join(", ")}` : null;
9689
9799
  }
@@ -9699,7 +9809,9 @@ function deriveStage(inputs) {
9699
9809
  // answered" — any HTTP status — is the health signal, not a 2xx on the bare root.
9700
9810
  healthAnyStatus: true,
9701
9811
  portRange: inputs.portRange,
9702
- ensureEnv: { example: ".env.example", target: ".env" },
9812
+ // #2655: only bootstrap a `.env` when the repo ships `.env.example` (legacy). Without it, the stage is
9813
+ // vault-native — the runner injects the declared dev secrets into the compose process env (no disk file).
9814
+ ...inputs.hasEnvExample ? { ensureEnv: { example: ".env.example", target: ".env" } } : {},
9703
9815
  // MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
9704
9816
  // reads the container's bind port from .env rather than this overriding it to the publish port (#1505).
9705
9817
  env: {
@@ -9777,7 +9889,9 @@ async function runStageLiveUp(deps, t) {
9777
9889
  ref: t.ref,
9778
9890
  ip,
9779
9891
  dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
9780
- message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions \u2014 tear down with: mmi-cli stage --live --down --apply`
9892
+ // #2656: the gate now writes a skip+block pair and self-verifies from the runner (a non-allowed vantage),
9893
+ // so a gate that failed to close fails the run RED. The stage is private only once that gate run is green.
9894
+ message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions and treat the stage as private ONLY once the cf-gate-allow run is green (it self-verifies the host is blocked) \u2014 tear down with: mmi-cli stage --live --down --apply`
9781
9895
  };
9782
9896
  }
9783
9897
  async function runStageLiveDown(deps, t) {
@@ -10446,7 +10560,16 @@ async function probeRemoteTag(deps, tag) {
10446
10560
  const remoteOut = await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/tags/${tag}`]);
10447
10561
  return clean2(remoteOut).split(/\s+/)[0] || "";
10448
10562
  }
10449
- async function ensureTagPushed(deps, tag, sha, probed) {
10563
+ async function isStrayUnreleasedTag(deps, tag, remoteSha, repo) {
10564
+ try {
10565
+ await deps.run("git", ["merge-base", "--is-ancestor", remoteSha, "origin/main"]);
10566
+ return false;
10567
+ } catch (e) {
10568
+ if (e.code !== 1) return false;
10569
+ }
10570
+ return deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName"]).then(() => false).catch((e) => /not found|HTTP 404/i.test(e instanceof Error ? e.message : String(e)));
10571
+ }
10572
+ async function ensureTagPushed(deps, tag, sha, probed, releaseRepo) {
10450
10573
  const remoteSha = probed ? probed.remoteSha : await probeRemoteTag(deps, tag);
10451
10574
  let localSha = "";
10452
10575
  try {
@@ -10455,8 +10578,14 @@ async function ensureTagPushed(deps, tag, sha, probed) {
10455
10578
  }
10456
10579
  if (remoteSha) {
10457
10580
  if (remoteSha !== sha) {
10581
+ const mismatch = `tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha}`;
10582
+ if (releaseRepo && await isStrayUnreleasedTag(deps, tag, remoteSha, releaseRepo)) {
10583
+ throw new Error(
10584
+ `${mismatch}. The existing tag is not reachable from origin/main and has no GitHub Release \u2014 it was pushed outside the train, not by a completed release. Sanctioned recovery: delete the stray tag (git push origin --delete ${tag}; git tag -d ${tag} if it exists locally), check the repo's Actions for any workflow the stray tag already triggered, then rerun mmi-cli release --apply (if a publish already ran off the stray tag, mint the next version instead of reusing this one). Never complete the release by hand \u2014 a manual GitHub Release or branch push bypasses the train and leaves main behind.`
10585
+ );
10586
+ }
10458
10587
  throw new Error(
10459
- `tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha} \u2014 refusing to touch a pushed tag. Repair manually: either release the existing tagged commit (reset the local branch to ${remoteSha} and rerun), or mint the next candidate version so a fresh tag is used. Never force-move or delete a pushed tag.`
10588
+ `${mismatch} \u2014 refusing to touch a pushed tag. Repair manually: either release the existing tagged commit (reset the local branch to ${remoteSha} and rerun), or mint the next candidate version so a fresh tag is used. Never force-move or delete a pushed tag.`
10460
10589
  );
10461
10590
  }
10462
10591
  if (localSha && localSha !== sha) {
@@ -10823,7 +10952,7 @@ async function runFoldStage(deps, startBranch, preFold, fn) {
10823
10952
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe) {
10824
10953
  let tagPush;
10825
10954
  try {
10826
- tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe);
10955
+ tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
10827
10956
  } catch (e) {
10828
10957
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
10829
10958
  }
@@ -12000,8 +12129,8 @@ async function announceRelease(deps, args) {
12000
12129
  }
12001
12130
 
12002
12131
  // src/port-registry.ts
12003
- var import_node_fs12 = require("node:fs");
12004
- var import_node_path13 = require("node:path");
12132
+ var import_node_fs13 = require("node:fs");
12133
+ var import_node_path14 = require("node:path");
12005
12134
 
12006
12135
  // ../infra/port-geometry.mjs
12007
12136
  var PORT_BLOCK = 100;
@@ -12015,8 +12144,8 @@ function nextPortBlock(registry2) {
12015
12144
  return [base, base + PORT_SPAN];
12016
12145
  }
12017
12146
  function loadPortRegistry(path2) {
12018
- if (!(0, import_node_fs12.existsSync)(path2)) return {};
12019
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
12147
+ if (!(0, import_node_fs13.existsSync)(path2)) return {};
12148
+ const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
12020
12149
  const out = {};
12021
12150
  for (const [key, value] of Object.entries(raw)) {
12022
12151
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -12030,9 +12159,9 @@ function ensurePortRange(repo, path2) {
12030
12159
  const existing = registry2[repo];
12031
12160
  if (existing) return existing;
12032
12161
  const range = nextPortBlock(registry2);
12033
- const raw = (0, import_node_fs12.existsSync)(path2) ? JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8")) : {};
12162
+ const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
12034
12163
  raw[repo] = range;
12035
- (0, import_node_fs12.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12164
+ (0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12036
12165
  return range;
12037
12166
  }
12038
12167
  function portCursorSeed(registry2) {
@@ -12054,20 +12183,26 @@ function existingPortRange(repo, registry2) {
12054
12183
  return registry2[repo] ?? null;
12055
12184
  }
12056
12185
  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;
12186
+ const registryPath = (0, import_node_path14.join)(root, "infra", "port-ranges.json");
12187
+ const ddbScriptPath = (0, import_node_path14.join)(root, "infra", "port-ddb.mjs");
12188
+ if (!(0, import_node_fs13.existsSync)(registryPath) || !(0, import_node_fs13.existsSync)(ddbScriptPath)) return null;
12060
12189
  return { root, source, registryPath, ddbScriptPath };
12061
12190
  }
12062
- function resolvePortRangeInfra(cwd) {
12191
+ function resolvePortRangeInfra(cwd, packageDir) {
12063
12192
  const direct = portRangeInfraAt(cwd, "cwd");
12064
12193
  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");
12194
+ for (let dir = cwd; ; dir = (0, import_node_path14.dirname)(dir)) {
12195
+ const sibling = portRangeInfraAt((0, import_node_path14.join)(dir, "MMI-Hub"), "sibling-hub");
12067
12196
  if (sibling) return sibling;
12068
- const parent = (0, import_node_path13.dirname)(dir);
12069
- if (parent === dir) return null;
12197
+ const parent = (0, import_node_path14.dirname)(dir);
12198
+ if (parent === dir) break;
12199
+ }
12200
+ if (packageDir) {
12201
+ const pkgRoot = (0, import_node_path14.join)(packageDir, "..", "..");
12202
+ const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
12203
+ if (pkgFrom) return pkgFrom;
12070
12204
  }
12205
+ return null;
12071
12206
  }
12072
12207
  async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
12073
12208
  const registry2 = loadPortRegistry(path2);
@@ -12954,27 +13089,39 @@ async function tenantDeploy(payload, deps) {
12954
13089
  // src/wiki-publish-command.ts
12955
13090
  async function wikiPublish(deps, opts) {
12956
13091
  if (!opts.pagesDir) {
12957
- deps.err("wiki publish: <pages-dir> is required");
13092
+ if (opts.json) {
13093
+ deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
13094
+ } else {
13095
+ deps.err("wiki publish: <pages-dir> is required");
13096
+ }
12958
13097
  return false;
12959
13098
  }
12960
13099
  const minted = await deps.mint(opts.repo);
12961
13100
  if (!minted.ok) {
12962
- deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13101
+ if (opts.json) {
13102
+ deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
13103
+ } else {
13104
+ deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13105
+ }
12963
13106
  return false;
12964
13107
  }
12965
- const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir });
13108
+ const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
12966
13109
  if (!ok) {
12967
13110
  return false;
12968
13111
  }
12969
- deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13112
+ if (opts.json) {
13113
+ deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
13114
+ } else {
13115
+ deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13116
+ }
12970
13117
  return true;
12971
13118
  }
12972
13119
 
12973
13120
  // src/wiki-publish-git.ts
12974
13121
  var import_node_child_process8 = require("node:child_process");
12975
- var import_node_fs13 = require("node:fs");
13122
+ var import_node_fs14 = require("node:fs");
12976
13123
  var import_node_os5 = require("node:os");
12977
- var import_node_path14 = require("node:path");
13124
+ var import_node_path15 = require("node:path");
12978
13125
  var WIKI_STAMP_FILE = ".wiki-state.json";
12979
13126
  function redactWikiSecrets(msg) {
12980
13127
  return msg.replace(/x-access-token:[^@\s]+@/gi, "x-access-token:***@").replace(/(AUTHORIZATION:\s*basic\s+)[A-Za-z0-9+/=]+/gi, "$1***");
@@ -12986,6 +13133,7 @@ function publishWikiViaGit(deps, opts) {
12986
13133
  const dir = deps.mkdtemp();
12987
13134
  const url = `https://github.com/${opts.repo}.wiki.git`;
12988
13135
  const authArgs = wikiAuthArgs(opts.token);
13136
+ const json = opts.json ?? false;
12989
13137
  try {
12990
13138
  deps.gitRun(["init", "-q"], dir);
12991
13139
  deps.gitRun(["remote", "add", "origin", url], dir);
@@ -12995,20 +13143,20 @@ function publishWikiViaGit(deps, opts) {
12995
13143
  let copied = 0;
12996
13144
  for (const f of deps.readdir(opts.pagesDir)) {
12997
13145
  if (f.endsWith(".md")) {
12998
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, f), (0, import_node_path14.join)(dir, f));
13146
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, f), (0, import_node_path15.join)(dir, f));
12999
13147
  copied++;
13000
13148
  }
13001
13149
  }
13002
13150
  if (!copied) {
13003
- deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
13151
+ if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
13004
13152
  return false;
13005
13153
  }
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));
13154
+ if (deps.fileExists((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
13155
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path15.join)(dir, WIKI_STAMP_FILE));
13008
13156
  }
13009
13157
  deps.gitRun(["add", "-A"], dir);
13010
13158
  if (deps.gitProbe(["diff", "--cached", "--quiet"], dir)) {
13011
- deps.log("wiki publish: already up to date");
13159
+ if (!json) deps.log("wiki publish: already up to date");
13012
13160
  return true;
13013
13161
  }
13014
13162
  deps.gitRun(
@@ -13026,15 +13174,15 @@ function publishWikiViaGit(deps, opts) {
13026
13174
  try {
13027
13175
  deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
13028
13176
  } catch (e) {
13029
- deps.err(
13177
+ if (!json) deps.err(
13030
13178
  `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
13179
  );
13032
13180
  return false;
13033
13181
  }
13034
- deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
13182
+ if (!json) deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
13035
13183
  return true;
13036
13184
  } catch (e) {
13037
- deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
13185
+ if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
13038
13186
  return false;
13039
13187
  } finally {
13040
13188
  deps.rm(dir);
@@ -13054,7 +13202,7 @@ function readWikiStampViaGit(deps, opts) {
13054
13202
  return { ok: true, stamp: {} };
13055
13203
  }
13056
13204
  deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
13057
- const stampPath = (0, import_node_path14.join)(dir, WIKI_STAMP_FILE);
13205
+ const stampPath = (0, import_node_path15.join)(dir, WIKI_STAMP_FILE);
13058
13206
  if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
13059
13207
  const parsed = JSON.parse(deps.readFile(stampPath));
13060
13208
  return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
@@ -13066,12 +13214,12 @@ function readWikiStampViaGit(deps, opts) {
13066
13214
  }
13067
13215
  function realWikiGitPublishDeps() {
13068
13216
  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 }),
13217
+ mkdtemp: () => (0, import_node_fs14.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "wiki-")),
13218
+ readdir: (dir) => (0, import_node_fs14.readdirSync)(dir),
13219
+ copyFile: (src, dest) => (0, import_node_fs14.copyFileSync)(src, dest),
13220
+ fileExists: (p) => (0, import_node_fs14.existsSync)(p),
13221
+ readFile: (p) => (0, import_node_fs14.readFileSync)(p, "utf8"),
13222
+ rm: (p) => (0, import_node_fs14.rmSync)(p, { recursive: true, force: true }),
13075
13223
  gitProbe: (args, cwd) => {
13076
13224
  try {
13077
13225
  (0, import_node_child_process8.execFileSync)("git", args, { cwd, stdio: "pipe" });
@@ -13090,8 +13238,8 @@ function realWikiGitPublishDeps() {
13090
13238
 
13091
13239
  // src/wiki-pages.ts
13092
13240
  var import_node_crypto4 = require("node:crypto");
13093
- var import_node_fs14 = require("node:fs");
13094
- var import_node_path15 = require("node:path");
13241
+ var import_node_fs15 = require("node:fs");
13242
+ var import_node_path16 = require("node:path");
13095
13243
  var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
13096
13244
  var attr = (raw, key) => {
13097
13245
  const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
@@ -13117,15 +13265,15 @@ var DURABLE_GLOBS = [
13117
13265
  "docs/org-readme.md"
13118
13266
  ];
13119
13267
  var walkMarkdown = (root, rel) => {
13120
- const abs = (0, import_node_path15.join)(root, rel);
13268
+ const abs = (0, import_node_path16.join)(root, rel);
13121
13269
  let st;
13122
13270
  try {
13123
- st = (0, import_node_fs14.statSync)(abs);
13271
+ st = (0, import_node_fs15.statSync)(abs);
13124
13272
  } catch {
13125
13273
  return [];
13126
13274
  }
13127
13275
  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)));
13276
+ return (0, import_node_fs15.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path16.join)(rel, name)));
13129
13277
  };
13130
13278
  function collectFeaturePages({ listDocs, readDoc }) {
13131
13279
  const seen = /* @__PURE__ */ new Set();
@@ -13144,7 +13292,7 @@ function fsReaders(rootDir) {
13144
13292
  listDocs: () => DURABLE_GLOBS.flatMap((g) => walkMarkdown(rootDir, g)),
13145
13293
  readDoc: (p) => {
13146
13294
  try {
13147
- return (0, import_node_fs14.readFileSync)((0, import_node_path15.join)(rootDir, p), "utf8");
13295
+ return (0, import_node_fs15.readFileSync)((0, import_node_path16.join)(rootDir, p), "utf8");
13148
13296
  } catch {
13149
13297
  return "";
13150
13298
  }
@@ -14473,7 +14621,12 @@ async function targetRepo(deps, opts) {
14473
14621
  return opts.repo ?? repoOf(await vaultSlug(deps, opts));
14474
14622
  }
14475
14623
  async function secretsWhere(deps, opts) {
14476
- deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
14624
+ const p = vaultPointer(await vaultSlug(deps, opts));
14625
+ if (opts.json) {
14626
+ deps.log(JSON.stringify(p, null, 2));
14627
+ return;
14628
+ }
14629
+ deps.log(formatVaultPointer(p));
14477
14630
  }
14478
14631
  async function readErr(res) {
14479
14632
  try {
@@ -14536,6 +14689,10 @@ async function secretsList(deps, opts) {
14536
14689
  return;
14537
14690
  }
14538
14691
  const { secrets } = await res.json();
14692
+ if (opts.json) {
14693
+ deps.log(JSON.stringify({ secrets: secrets ?? [] }, null, 2));
14694
+ return;
14695
+ }
14539
14696
  deps.log(formatSecretList(secrets ?? []));
14540
14697
  }
14541
14698
  var CAPABILITIES_TIMEOUT_MS = 2e4;
@@ -14751,6 +14908,13 @@ function requiredRuntimeSecretNames(stage2, contract, opts = {}) {
14751
14908
  function stageKey2(stage2, key) {
14752
14909
  return key.includes("/") ? key : `${stage2}/${key}`;
14753
14910
  }
14911
+ function keyDeclaredPerStage(declaredSecrets, key) {
14912
+ if (!declaredSecrets) return void 0;
14913
+ const entry = declaredSecrets[key];
14914
+ if (!entry) return void 0;
14915
+ if (!Array.isArray(entry.stages) || entry.stages.length === 0) return false;
14916
+ return true;
14917
+ }
14754
14918
  async function secretsPreflight(deps, opts) {
14755
14919
  const repo = await targetRepo(deps, opts);
14756
14920
  const qs = new URLSearchParams({ repo }).toString();
@@ -14771,9 +14935,23 @@ async function secretsPreflight(deps, opts) {
14771
14935
  }
14772
14936
  const { secrets } = await res.json();
14773
14937
  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
- );
14938
+ const missing = opts.required.filter((key) => {
14939
+ const perStage = keyDeclaredPerStage(opts.declaredSecrets, key);
14940
+ if (key.includes("/")) {
14941
+ return !present.has(key);
14942
+ }
14943
+ if (perStage === false) {
14944
+ return !present.has(key);
14945
+ }
14946
+ if (perStage === true) {
14947
+ return !present.has(stageKey2(opts.stage, key));
14948
+ }
14949
+ return !present.has(stageKey2(opts.stage, key)) && !present.has(key);
14950
+ });
14951
+ if (opts.json) {
14952
+ deps.log(JSON.stringify({ ok: missing.length === 0, stage: opts.stage, required: opts.required, missing, present: Array.from(present) }, null, 2));
14953
+ return missing.length === 0;
14954
+ }
14777
14955
  if (missing.length) {
14778
14956
  deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
14779
14957
  return false;
@@ -14998,9 +15176,6 @@ async function secretsImportRailsCredentials(deps, opts) {
14998
15176
  }
14999
15177
  return true;
15000
15178
  }
15001
- async function secretsEdit(deps, key, opts) {
15002
- return secretsSet(deps, key, opts);
15003
- }
15004
15179
  async function secretsRemove(deps, key, opts) {
15005
15180
  if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
15006
15181
  const repo = await targetRepo(deps, opts);
@@ -15096,8 +15271,12 @@ async function secretsCopy(deps, opts) {
15096
15271
  }
15097
15272
  return true;
15098
15273
  }
15274
+ function resolveSpawnTarget(command, args, platform2 = process.platform) {
15275
+ return platform2 === "win32" ? { cmd: "cmd.exe", args: ["/d", "/s", "/c", command, ...args] } : { cmd: command, args };
15276
+ }
15099
15277
  function defaultSpawn(command, args, env) {
15100
- const r = (0, import_node_child_process9.spawnSync)(command, args, { stdio: "inherit", env });
15278
+ const target = resolveSpawnTarget(command, args);
15279
+ const r = (0, import_node_child_process9.spawnSync)(target.cmd, target.args, { stdio: "inherit", env });
15101
15280
  if (r.error) throw r.error;
15102
15281
  return r.status ?? 1;
15103
15282
  }
@@ -15157,8 +15336,8 @@ async function secretsUse(deps, key, opts) {
15157
15336
  }
15158
15337
 
15159
15338
  // src/secrets-commands.ts
15160
- var import_node_fs15 = require("node:fs");
15161
- var import_node_path16 = require("node:path");
15339
+ var import_node_fs16 = require("node:fs");
15340
+ var import_node_path17 = require("node:path");
15162
15341
  var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
15163
15342
  var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
15164
15343
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
@@ -15166,18 +15345,18 @@ function collectMap(value, previous = []) {
15166
15345
  return [...previous, value];
15167
15346
  }
15168
15347
  async function decryptRailsCredentials(input) {
15169
- const appDir = (0, import_node_path16.resolve)(input.appDir ?? process.cwd());
15348
+ const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
15170
15349
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
15171
15350
  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);
15351
+ const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
15352
+ const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
15174
15353
  const env = {
15175
15354
  ...process.env,
15176
15355
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
15177
15356
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
15178
15357
  };
15179
- if ((0, import_node_fs15.existsSync)(masterKeyPath)) {
15180
- env.RAILS_MASTER_KEY = (0, import_node_fs15.readFileSync)(masterKeyPath, "utf8").trim();
15358
+ if ((0, import_node_fs16.existsSync)(masterKeyPath)) {
15359
+ env.RAILS_MASTER_KEY = (0, import_node_fs16.readFileSync)(masterKeyPath, "utf8").trim();
15181
15360
  }
15182
15361
  const script = [
15183
15362
  'require "json"',
@@ -15231,8 +15410,8 @@ async function withSecrets(run) {
15231
15410
  }
15232
15411
  function registerSecretsCommands(program3) {
15233
15412
  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)));
15413
+ 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)));
15414
+ 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
15415
  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
15416
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
15238
15417
  }));
@@ -15243,13 +15422,13 @@ function registerSecretsCommands(program3) {
15243
15422
  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
15423
  let body;
15245
15424
  try {
15246
- body = (0, import_node_fs15.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
15425
+ body = (0, import_node_fs16.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
15247
15426
  } catch (e) {
15248
15427
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
15249
15428
  }
15250
15429
  if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
15251
15430
  }));
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) => {
15431
+ 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
15432
  if (!["dev", "rc", "main"].includes(o.stage)) {
15254
15433
  return fail("secrets preflight: --stage must be dev, rc, or main");
15255
15434
  }
@@ -15270,7 +15449,7 @@ function registerSecretsCommands(program3) {
15270
15449
  if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
15271
15450
  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
15451
  }
15273
- const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
15452
+ const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required, json: o.json, declaredSecrets: meta?.secrets });
15274
15453
  if (!ok) process.exitCode = 1;
15275
15454
  });
15276
15455
  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 +15464,12 @@ function registerSecretsCommands(program3) {
15285
15464
  const ok = await secretsVerify(d, key, o);
15286
15465
  if (!ok) process.exitCode = 1;
15287
15466
  }));
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) => {
15467
+ const setHandler = (key, o) => withSecrets(async (d) => {
15289
15468
  const ok = await secretsSet(d, key, o);
15290
15469
  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
- }));
15470
+ });
15471
+ 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);
15472
+ 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
15473
  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
15474
  const stages = ["dev", "rc", "main"];
15298
15475
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
@@ -15318,7 +15495,7 @@ function registerSecretsCommands(program3) {
15318
15495
  {
15319
15496
  ...d,
15320
15497
  decryptRailsCredentials,
15321
- removeFile: (path2) => (0, import_node_fs15.unlinkSync)((0, import_node_path16.resolve)(o.appDir ?? process.cwd(), path2))
15498
+ removeFile: (path2) => (0, import_node_fs16.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
15322
15499
  },
15323
15500
  {
15324
15501
  repo: o.repo,
@@ -15407,10 +15584,10 @@ function registerEdgeCommands(program3) {
15407
15584
  }
15408
15585
 
15409
15586
  // src/doctor-run.ts
15410
- var import_node_fs19 = require("node:fs");
15587
+ var import_node_fs20 = require("node:fs");
15411
15588
  var import_node_child_process11 = require("node:child_process");
15412
15589
  var import_promises2 = require("node:fs/promises");
15413
- var import_node_path19 = require("node:path");
15590
+ var import_node_path20 = require("node:path");
15414
15591
  var import_node_os7 = require("node:os");
15415
15592
 
15416
15593
  // src/plugin-guard.ts
@@ -15433,13 +15610,14 @@ function buildGuardSessionStartLine(state, opts = {}) {
15433
15610
 
15434
15611
  // src/cursor-plugin-seed.ts
15435
15612
  var import_node_child_process10 = require("node:child_process");
15436
- var import_node_fs16 = require("node:fs");
15613
+ var import_node_fs17 = require("node:fs");
15437
15614
  var import_node_os6 = require("node:os");
15438
- var import_node_path17 = require("node:path");
15615
+ var import_node_path18 = require("node:path");
15439
15616
  var import_node_util7 = require("node:util");
15440
15617
 
15441
15618
  // src/doctor.ts
15442
15619
  var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
15620
+ var GITHUB_AUTH_LABEL = "GitHub auth identity (gh ops)";
15443
15621
  var AWS_CROSS_ACCOUNT_LABEL = "AWS cross-account identity (master-agent audits)";
15444
15622
  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
15623
  var MMI_AGENTIC_ONBOARDING_GUIDE = {
@@ -15453,7 +15631,7 @@ function buildGithubAuthCheck(input) {
15453
15631
  const ok = Boolean(input.login?.trim());
15454
15632
  return {
15455
15633
  ok,
15456
- label: "GitHub auth identity (gh ops)",
15634
+ label: GITHUB_AUTH_LABEL,
15457
15635
  fix: input.ghInstalled ? GH_PROJECT_LOGIN_FIX : `install GitHub CLI (https://cli.github.com), then: ${GH_PROJECT_LOGIN_FIX.replace(/^run: /, "")}`
15458
15636
  };
15459
15637
  }
@@ -15631,6 +15809,136 @@ function buildRepoLocalWorktreeCheck(input) {
15631
15809
  fix: REPO_LOCAL_WORKTREE_FIX
15632
15810
  };
15633
15811
  }
15812
+ var LOCKFILE_OPTIONALS_LABEL = "npm lockfile keeps optionalDependencies across platforms (Windows lockfile drop #2594)";
15813
+ 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)";
15814
+ function buildLockfileOptionalsCheck(input) {
15815
+ const base = {
15816
+ ok: true,
15817
+ label: LOCKFILE_OPTIONALS_LABEL,
15818
+ fix: LOCKFILE_OPTIONALS_FIX,
15819
+ missingOptionals: []
15820
+ };
15821
+ if (!input.isOrgRepo) return base;
15822
+ const optionalDeps = input.packageJsonOptionalDeps ?? {};
15823
+ const packages = input.lockfilePackages ?? {};
15824
+ const missingOptionals = Object.keys(optionalDeps).filter((name) => !(`node_modules/${name}` in packages));
15825
+ if (missingOptionals.length === 0) return base;
15826
+ return {
15827
+ ...base,
15828
+ ok: false,
15829
+ missingOptionals,
15830
+ detail: `${missingOptionals.length} optionalDependency entr${missingOptionals.length === 1 ? "y" : "ies"} missing from the lockfile packages map: ${missingOptionals.join(", ")}`
15831
+ };
15832
+ }
15833
+ var FILE_ENCODING_LABEL = "managed file encoding (UTF-8, no BOM, no mojibake)";
15834
+ 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';
15835
+ function inspectFileEncoding(bytes) {
15836
+ const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
15837
+ let invalidUtf8 = false;
15838
+ let replacementChars = false;
15839
+ try {
15840
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
15841
+ replacementChars = text.includes("\uFFFD");
15842
+ } catch {
15843
+ invalidUtf8 = true;
15844
+ replacementChars = new TextDecoder("utf-8", { fatal: false }).decode(bytes).includes("\uFFFD");
15845
+ }
15846
+ return { bom, invalidUtf8, replacementChars };
15847
+ }
15848
+ function buildManagedFileEncodingCheck(input) {
15849
+ const base = {
15850
+ ok: true,
15851
+ label: FILE_ENCODING_LABEL,
15852
+ fix: FILE_ENCODING_FIX,
15853
+ findings: []
15854
+ };
15855
+ if (!input.isOrgRepo) return base;
15856
+ const defective = input.findings.filter((f) => f.bom || f.invalidUtf8 || f.replacementChars);
15857
+ if (defective.length === 0) return base;
15858
+ return {
15859
+ ...base,
15860
+ ok: false,
15861
+ severityOverride: "advisory",
15862
+ findings: defective,
15863
+ detail: `${defective.length} managed file(s) not clean UTF-8: ${defective.map((f) => f.path).join(", ")}`
15864
+ };
15865
+ }
15866
+ var LOCAL_BEHIND_ORIGIN_LABEL = "local release branches current with origin (development/main/rc)";
15867
+ 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)";
15868
+ var RELEASE_TRACKING_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master", "rc", "release"]);
15869
+ function isReleaseTrackingBranch(name) {
15870
+ if (!name) return false;
15871
+ const base = name.split("/").pop() ?? name;
15872
+ if (RELEASE_TRACKING_BRANCHES.has(base)) return true;
15873
+ return name.split("/")[0] === "release";
15874
+ }
15875
+ function buildStaleLocalBranchCheck(input) {
15876
+ const base = {
15877
+ ok: true,
15878
+ label: LOCAL_BEHIND_ORIGIN_LABEL,
15879
+ fix: LOCAL_BEHIND_ORIGIN_FIX,
15880
+ stale: []
15881
+ };
15882
+ if (!input.isOrgRepo) return base;
15883
+ const stale = input.branches.filter((b) => isReleaseTrackingBranch(b.name) && b.behind > 0 && b.ahead === 0).map((b) => ({ name: b.name, behind: b.behind }));
15884
+ if (stale.length === 0) return base;
15885
+ return {
15886
+ ...base,
15887
+ ok: false,
15888
+ severityOverride: "advisory",
15889
+ stale,
15890
+ detail: `${stale.length} release branch(es) behind origin: ${stale.map((s) => `${s.name} (-${s.behind})`).join(", ")}`
15891
+ };
15892
+ }
15893
+ var REDACTOR_LIVENESS_LABEL = "secret-redact hook liveness (PostToolUse redactor crashes)";
15894
+ 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';
15895
+ function summarizeRedactorFailures(lines) {
15896
+ const parsed = [];
15897
+ for (const raw of lines) {
15898
+ const trimmed = raw.trim();
15899
+ if (!trimmed) continue;
15900
+ try {
15901
+ parsed.push(JSON.parse(trimmed));
15902
+ } catch {
15903
+ }
15904
+ }
15905
+ let sessionStartIdx = -1;
15906
+ for (let i = 0; i < parsed.length; i += 1) {
15907
+ if (parsed[i].event === "SessionStart") sessionStartIdx = i;
15908
+ }
15909
+ const start = sessionStartIdx >= 0 ? sessionStartIdx : 0;
15910
+ let failedCount = 0;
15911
+ let lastFailedTs;
15912
+ let lastFailedAction;
15913
+ for (let i = start; i < parsed.length; i += 1) {
15914
+ const e = parsed[i];
15915
+ if (e.script === "secret-redact" && e.outcome === "failed") {
15916
+ failedCount += 1;
15917
+ lastFailedTs = e.ts;
15918
+ lastFailedAction = e.action;
15919
+ }
15920
+ }
15921
+ return { failedCount, lastFailedTs, lastFailedAction };
15922
+ }
15923
+ function buildRedactorLivenessCheck(input) {
15924
+ const base = {
15925
+ ok: true,
15926
+ label: REDACTOR_LIVENESS_LABEL,
15927
+ fix: REDACTOR_LIVENESS_FIX,
15928
+ failedCount: 0
15929
+ };
15930
+ if (!input.isOrgRepo) return base;
15931
+ if (input.failedCount === 0) return base;
15932
+ const when = input.lastFailedTs ? ` (most recent ${input.lastFailedTs})` : "";
15933
+ const reason = input.lastFailedAction ? `: ${input.lastFailedAction}` : "";
15934
+ return {
15935
+ ok: false,
15936
+ label: REDACTOR_LIVENESS_LABEL,
15937
+ fix: `secret-redact crashed ${input.failedCount} time(s) this session${when}${reason}. ${REDACTOR_LIVENESS_FIX}`,
15938
+ failedCount: input.failedCount,
15939
+ lastFailedTs: input.lastFailedTs
15940
+ };
15941
+ }
15634
15942
  var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
15635
15943
  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
15944
  function buildScratchGcCheck(plan) {
@@ -16089,7 +16397,16 @@ function buildDoctorJsonPayload(input) {
16089
16397
  return {
16090
16398
  ok: doctorExitCode(input.checks) === 0,
16091
16399
  hasAdvisories: input.checks.some((c) => !c.ok && isAdvisoryDoctorCheck(c)),
16092
- checks: input.checks,
16400
+ checks: input.checks.map((check) => {
16401
+ const metadata = doctorMetadataForCheck(check);
16402
+ return {
16403
+ ...check,
16404
+ ...metadata,
16405
+ label: check.label,
16406
+ metadata,
16407
+ durationMs: check.durationMs ?? 0
16408
+ };
16409
+ }),
16093
16410
  updateReport: input.updateReport,
16094
16411
  ...input.resources.length ? { resources: input.resources } : {}
16095
16412
  };
@@ -16148,6 +16465,25 @@ function buildInstalledPluginVersionCheck(input) {
16148
16465
  staleSurfaces: stale
16149
16466
  };
16150
16467
  }
16468
+ var CLI_VERSION_DRIFT_LABEL = "mmi-cli version drift (installed vs latest published)";
16469
+ var CLI_VERSION_DRIFT_FIX = "run: mmi-cli doctor --apply --no-repo-writes (or npm install -g @mutmutco/cli@latest)";
16470
+ function buildCliVersionDriftCheck(input) {
16471
+ const base = {
16472
+ ok: true,
16473
+ label: CLI_VERSION_DRIFT_LABEL,
16474
+ fix: CLI_VERSION_DRIFT_FIX,
16475
+ currentVersion: input.currentVersion,
16476
+ releasedVersion: input.releasedVersion
16477
+ };
16478
+ if (!input.releasedVersion || !isSemverVersion(input.releasedVersion) || !isSemverVersion(input.currentVersion)) return base;
16479
+ if (compareVersions(input.currentVersion, input.releasedVersion) >= 0) return base;
16480
+ return {
16481
+ ...base,
16482
+ ok: false,
16483
+ label: `${CLI_VERSION_DRIFT_LABEL}: ${input.currentVersion} < ${input.releasedVersion}`,
16484
+ fix: `${CLI_VERSION_DRIFT_FIX} (installed ${input.currentVersion} is behind published ${input.releasedVersion})`
16485
+ };
16486
+ }
16151
16487
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
16152
16488
  function buildOpencodeVersionCheck(input) {
16153
16489
  const fix = pluginRecoveryFix("opencode");
@@ -16445,13 +16781,13 @@ function buildCursorHookCliCheck(input) {
16445
16781
  return base;
16446
16782
  }
16447
16783
  var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
16784
+ var HUB_COMPAT_LABEL = "Hub compatibility (client version vs Hub minimum)";
16448
16785
  function buildHubCompatCheck(input) {
16449
- const label = "Hub compatibility (client version vs Hub minimum)";
16450
16786
  const min = input.versionInfo?.minClientVersion;
16451
16787
  if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
16452
- return { ok: true, label, fix: HUB_COMPAT_FIX };
16788
+ return { ok: true, label: HUB_COMPAT_LABEL, fix: HUB_COMPAT_FIX };
16453
16789
  }
16454
- return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16790
+ return { ok: versionAtLeast(input.installedVersion, min), label: `${HUB_COMPAT_LABEL}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16455
16791
  }
16456
16792
  var HUB_DEPLOY_FRESHNESS_LABEL = "Hub deploy freshness (x-hub-version vs CLI / release)";
16457
16793
  var HUB_DEPLOY_FRESHNESS_FIX = "prod Hub updates on /release (MMI-Hub is direct-track); lag vs development is expected between releases";
@@ -16486,8 +16822,6 @@ function buildHubDeployFreshnessCheck(input) {
16486
16822
  staleAgainst
16487
16823
  };
16488
16824
  }
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
16825
  function textHasPlaywrightMcp(content) {
16492
16826
  const normalized = content.replace(/\r\n/g, "\n");
16493
16827
  return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
@@ -16545,7 +16879,7 @@ function buildSelfUpdateHaltPayload(input) {
16545
16879
  checks: input.checks
16546
16880
  };
16547
16881
  }
16548
- var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
16882
+ var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 2;
16549
16883
  var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
16550
16884
  function selfUpdateHaltSentinelLine(input) {
16551
16885
  const to = input.report.releasedVersion ?? "latest";
@@ -16555,8 +16889,11 @@ var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
16555
16889
  function buildSelfUpdateReexecArgs(opts) {
16556
16890
  const args = ["doctor"];
16557
16891
  if (opts.banner) args.push("--banner");
16892
+ if (opts.fast) args.push("--fast");
16558
16893
  if (opts.preflight) args.push("--preflight");
16559
16894
  if (opts.verbose) args.push("--verbose");
16895
+ if (opts.ci) args.push("--ci");
16896
+ if (opts.strictAdvisory) args.push("--strict-advisory");
16560
16897
  if (opts.guide) args.push("--guide");
16561
16898
  if (opts.json) args.push("--json");
16562
16899
  if (opts.apply) args.push("--apply");
@@ -16577,28 +16914,12 @@ function preflightOutcome(input) {
16577
16914
  function pluginAutonomousHaltLine(reloadHint) {
16578
16915
  return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
16579
16916
  }
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
16917
  function isAdvisoryDoctorCheck(check) {
16598
16918
  if (check.severityOverride) return check.severityOverride === "advisory";
16599
- return isDoctorExtendedCheck(check.label);
16919
+ return (check.metadata ?? doctorMetadataForLabel(check.label)).severity === "advisory";
16600
16920
  }
16601
- function doctorExitCode(checks) {
16921
+ function doctorExitCode(checks, opts = {}) {
16922
+ if (opts.doctorError) return 2;
16602
16923
  return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
16603
16924
  }
16604
16925
  function renderPluginUpdateReportStaleOnly(report, allChecksPass = false) {
@@ -16624,6 +16945,7 @@ function doctorVerdictLine(input) {
16624
16945
  const gaps = input.checks.filter((c) => !c.ok);
16625
16946
  const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
16626
16947
  const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
16948
+ const skipSuffix = input.skippedSlowChecks ? ` \xB7 skipped ${input.skippedSlowChecks} slow checks` : "";
16627
16949
  if (input.shouldApply) {
16628
16950
  if (hardGaps.length > 0) {
16629
16951
  return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
@@ -16637,19 +16959,30 @@ function doctorVerdictLine(input) {
16637
16959
  }
16638
16960
  const attention = gaps.length;
16639
16961
  if (attention > 0) {
16640
- return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
16962
+ return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal${skipSuffix}.`;
16641
16963
  }
16642
- return "\u2713 Everything healthy.";
16964
+ return `\u2713 healthy \xB7 ${input.checks.length} checks \xB7 0 gaps${skipSuffix}`;
16965
+ }
16966
+ function doctorCheckSummaryLine(check) {
16967
+ const meta = doctorMetadataForCheck(check);
16968
+ return ` ${doctorCheckGlyph(check)} ${meta.id} \u2014 ${meta.protects.join("; ")} \u2014 ${check.fix}`;
16643
16969
  }
16644
16970
  function doctorHumanLines(input) {
16645
16971
  const gaps = input.checks.filter((c) => !c.ok);
16972
+ const healthyCount = input.checks.length - gaps.length;
16646
16973
  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}`)
16974
+ doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount, skippedSlowChecks: input.skippedSlowChecks })
16651
16975
  ];
16652
- if (gaps.length > 0) {
16976
+ if (gaps.length > 0 || input.verbose) {
16977
+ lines.push("", "Checks");
16978
+ if (input.verbose) {
16979
+ lines.push(...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${doctorMetadataForCheck(c).id} \u2014 ${c.label}`));
16980
+ } else {
16981
+ lines.push(...gaps.map(doctorCheckSummaryLine));
16982
+ if (healthyCount > 0) lines.push(` \u2713 ${healthyCount} healthy check${healthyCount === 1 ? "" : "s"} collapsed`);
16983
+ }
16984
+ }
16985
+ if (gaps.length > 0 && input.verbose) {
16653
16986
  lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
16654
16987
  for (const c of gaps) {
16655
16988
  const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
@@ -16659,10 +16992,10 @@ function doctorHumanLines(input) {
16659
16992
  if (input.shouldApply && input.pluginReloadRequired) {
16660
16993
  lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
16661
16994
  }
16662
- const stale = renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0);
16995
+ const stale = input.verbose || gaps.length > 0 ? renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0) : [];
16663
16996
  if (stale.length) {
16664
16997
  lines.push("", ...stale);
16665
- } else if (!input.verbose) {
16998
+ } else if (!input.verbose && gaps.length > 0) {
16666
16999
  lines.push("", DOCTOR_VERBOSE_HINT);
16667
17000
  }
16668
17001
  if (!input.verbose) return lines;
@@ -16710,6 +17043,100 @@ function doctorAuditLines(input) {
16710
17043
  return lines;
16711
17044
  }
16712
17045
  var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
17046
+ var MMI_CLI_ON_PATH_LABEL = "mmi-cli on PATH";
17047
+ var PLUGIN_CLONE_LABEL = "plugin git clone (SSH\u2192HTTPS rewrite)";
17048
+ var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17049
+ var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17050
+ var DOCTOR_CHECK_METADATA = [
17051
+ { 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"] },
17052
+ { 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"] },
17053
+ { id: "hub.compat", category: "hub", severity: "hard", cost: "fast", heal: "auto", label: HUB_COMPAT_LABEL, protects: ["Client stays above the Hub minimum version floor"] },
17054
+ { 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"] },
17055
+ { 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)"] },
17056
+ { 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"] },
17057
+ { 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"] },
17058
+ { 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"] },
17059
+ { 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"] },
17060
+ { 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"] },
17061
+ { 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"] },
17062
+ { 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"] },
17063
+ { 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"] },
17064
+ { 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"] },
17065
+ { 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"] },
17066
+ { 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"] },
17067
+ { 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"] },
17068
+ { 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"] },
17069
+ { 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"] },
17070
+ { 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"] },
17071
+ { 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"] },
17072
+ { id: "browser.artifacts", category: "browser", severity: "advisory", cost: "slow", heal: "none", label: BROWSER_ARTIFACTS_LABEL, protects: ["Playwright/browser output stays under tmp/playwright-mcp"] },
17073
+ { 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"] },
17074
+ { id: "opencode.version", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_VERSION_LABEL, protects: ["OpenCode loads the released MMI adapter"] },
17075
+ { 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"] },
17076
+ { 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"] },
17077
+ { id: "opencode.surface-assets", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_SURFACE_ASSETS_LABEL, protects: ["OpenCode commands and skills are materialized"] },
17078
+ { 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"] },
17079
+ { 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"] },
17080
+ { 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"] },
17081
+ { 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"] },
17082
+ { 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"] },
17083
+ { 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"] },
17084
+ { 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"] },
17085
+ // Incident coverage (#2594) — three real incident classes that previously had zero doctor coverage.
17086
+ { 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"] },
17087
+ { 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"] },
17088
+ { 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"] }
17089
+ ];
17090
+ var DOCTOR_CHECK_METADATA_BY_LABEL = Object.fromEntries(
17091
+ DOCTOR_CHECK_METADATA.map((meta) => [meta.label, meta])
17092
+ );
17093
+ var DOCTOR_EXTENDED_CHECK_LABELS = new Set(
17094
+ DOCTOR_CHECK_METADATA.filter((meta) => meta.cost === "slow").map((meta) => meta.label)
17095
+ );
17096
+ function derivedDoctorCheckMetadata(label) {
17097
+ if (label.startsWith(HUB_COMPAT_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_COMPAT_LABEL], label };
17098
+ if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_DEPLOY_FRESHNESS_LABEL], label };
17099
+ if (label.startsWith("@mutmutco design-system")) {
17100
+ return {
17101
+ id: "registry.design-system-components",
17102
+ category: "hub",
17103
+ severity: "advisory",
17104
+ cost: "slow",
17105
+ heal: "none",
17106
+ label,
17107
+ protects: ["Design-system component cache can be compared against the live registry"]
17108
+ };
17109
+ }
17110
+ if (label.startsWith("@mutmutco registry components")) {
17111
+ return {
17112
+ id: "registry.components",
17113
+ category: "hub",
17114
+ severity: "advisory",
17115
+ cost: "slow",
17116
+ heal: "none",
17117
+ label,
17118
+ protects: ["Repo component cache can be compared against the live registry"]
17119
+ };
17120
+ }
17121
+ return void 0;
17122
+ }
17123
+ function fallbackDoctorCheckMetadata(label) {
17124
+ return {
17125
+ id: `legacy.${label.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/(^\.|\.$)/g, "") || "unknown"}`,
17126
+ category: "repo",
17127
+ severity: "hard",
17128
+ cost: "fast",
17129
+ heal: "none",
17130
+ label,
17131
+ protects: ["Legacy check without catalog metadata"]
17132
+ };
17133
+ }
17134
+ function doctorMetadataForLabel(label) {
17135
+ return DOCTOR_CHECK_METADATA_BY_LABEL[label] ?? derivedDoctorCheckMetadata(label) ?? fallbackDoctorCheckMetadata(label);
17136
+ }
17137
+ function doctorMetadataForCheck(check) {
17138
+ return check.metadata ?? doctorMetadataForLabel(check.label);
17139
+ }
16713
17140
  function buildPluginResolvabilityCheck(input) {
16714
17141
  const { state } = buildPluginGuardDecision(input);
16715
17142
  const surface = input.surface ?? "shell";
@@ -16755,17 +17182,17 @@ function ghReleaseTarballApiArgs(tag) {
16755
17182
  }
16756
17183
  function cursorUserGlobalStatePath() {
16757
17184
  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");
17185
+ const base = process.env.APPDATA || (0, import_node_path18.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
17186
+ return (0, import_node_path18.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
16760
17187
  }
16761
17188
  if (process.platform === "darwin") {
16762
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17189
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
16763
17190
  }
16764
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17191
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
16765
17192
  }
16766
17193
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16767
17194
  const dbPath = cursorUserGlobalStatePath();
16768
- if (!(0, import_node_fs16.existsSync)(dbPath)) return void 0;
17195
+ if (!(0, import_node_fs17.existsSync)(dbPath)) return void 0;
16769
17196
  try {
16770
17197
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
16771
17198
  timeout: 5e3
@@ -16779,57 +17206,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16779
17206
  }
16780
17207
  }
16781
17208
  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 });
17209
+ (0, import_node_fs17.mkdirSync)(dest, { recursive: true });
17210
+ for (const name of (0, import_node_fs17.readdirSync)(dest)) {
17211
+ (0, import_node_fs17.rmSync)((0, import_node_path18.join)(dest, name), { recursive: true, force: true });
16785
17212
  }
16786
- (0, import_node_fs16.cpSync)(src, dest, { recursive: true });
17213
+ (0, import_node_fs17.cpSync)(src, dest, { recursive: true });
16787
17214
  }
16788
17215
  function releaseTag(releasedVersion) {
16789
17216
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
16790
17217
  }
16791
17218
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
16792
- const tarFile = (0, import_node_path17.join)(tmpRoot, "archive.tar");
17219
+ const tarFile = (0, import_node_path18.join)(tmpRoot, "archive.tar");
16793
17220
  try {
16794
17221
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
16795
17222
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
16796
17223
  timeout: 6e4
16797
17224
  });
16798
17225
  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;
17226
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, "plugins", "mmi");
17227
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16801
17228
  } catch {
16802
17229
  return void 0;
16803
17230
  }
16804
17231
  }
16805
17232
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
16806
- const tarPath = (0, import_node_path17.join)(tmpRoot, "repo.tgz");
17233
+ const tarPath = (0, import_node_path18.join)(tmpRoot, "repo.tgz");
16807
17234
  try {
16808
- (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
17235
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16809
17236
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
16810
17237
  timeout: 12e4,
16811
17238
  maxBuffer: 100 * 1024 * 1024,
16812
17239
  encoding: "buffer",
16813
17240
  windowsHide: true
16814
17241
  });
16815
- (0, import_node_fs16.writeFileSync)(tarPath, stdout);
17242
+ (0, import_node_fs17.writeFileSync)(tarPath, stdout);
16816
17243
  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");
17244
+ const top = (0, import_node_fs17.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
16818
17245
  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;
17246
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, top, "plugins", "mmi");
17247
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16821
17248
  } catch {
16822
17249
  return void 0;
16823
17250
  }
16824
17251
  }
16825
17252
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
16826
- (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
17253
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16827
17254
  const tag = releaseTag(releasedVersion);
16828
17255
  if (hubCheckout) {
16829
17256
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
16830
17257
  if (fromHub) return fromHub;
16831
17258
  }
16832
- return downloadPluginMmiViaGh(tag, (0, import_node_path17.join)(tmpRoot, "gh"));
17259
+ return downloadPluginMmiViaGh(tag, (0, import_node_path18.join)(tmpRoot, "gh"));
16833
17260
  }
16834
17261
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
16835
17262
  if (!isSemverVersion2(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -16843,7 +17270,7 @@ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRoo
16843
17270
  const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
16844
17271
  if (needing.length > 0) return needing.map((pin) => pin.path);
16845
17272
  if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion2(releasedVersion)) {
16846
- return [(0, import_node_path17.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
17273
+ return [(0, import_node_path18.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
16847
17274
  }
16848
17275
  return [];
16849
17276
  }
@@ -16858,10 +17285,10 @@ async function applyCursorPluginCacheSeed(input) {
16858
17285
  for (const dest of targets) {
16859
17286
  syncDirContents(source, dest);
16860
17287
  }
16861
- (0, import_node_fs16.rmSync)(tmpRoot, { recursive: true, force: true });
17288
+ (0, import_node_fs17.rmSync)(tmpRoot, { recursive: true, force: true });
16862
17289
  return true;
16863
17290
  }
16864
- var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17291
+ var CURSOR_ENABLED_PIN_LABEL2 = "Cursor Team Marketplace enabled plugin pin";
16865
17292
  function cursorSplitBrainCapabilities() {
16866
17293
  const skills = OPENCODE_WORKFLOW_COMMANDS.map((c) => `/${c}`).join(", ");
16867
17294
  return `org skills (${skills}) and SessionStart hooks`;
@@ -16881,7 +17308,7 @@ function parseCursorPluginLog(text) {
16881
17308
  return { enabledPin, loadSucceeded, loadFailed };
16882
17309
  }
16883
17310
  function buildCursorEnabledPinCheck(input) {
16884
- const label = CURSOR_ENABLED_PIN_LABEL;
17311
+ const label = CURSOR_ENABLED_PIN_LABEL2;
16885
17312
  const healthy = { ok: true, label, fix: "" };
16886
17313
  if (!input.isOrgRepo) return healthy;
16887
17314
  if (!input.enabledPin && !input.loadFailed) return healthy;
@@ -16916,7 +17343,7 @@ var PLAYWRIGHT_MCP_SPEC = {
16916
17343
  args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
16917
17344
  legacyNames: []
16918
17345
  };
16919
- var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17346
+ var MCP_RECONCILE_LABEL2 = "MCP server registrations (org-managed Playwright)";
16920
17347
  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
17348
  function extractServerBlock(content, format, spec) {
16922
17349
  return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
@@ -17030,7 +17457,7 @@ function planClaudeCliMcpHeal(item, spec) {
17030
17457
  function buildMcpReconcileCheck(plan) {
17031
17458
  const base = {
17032
17459
  ok: true,
17033
- label: MCP_RECONCILE_LABEL,
17460
+ label: MCP_RECONCILE_LABEL2,
17034
17461
  fix: MCP_RECONCILE_FIX,
17035
17462
  severityOverride: "advisory"
17036
17463
  };
@@ -17217,9 +17644,9 @@ ${block}
17217
17644
  }
17218
17645
 
17219
17646
  // src/cli-doctor-shared.ts
17220
- var import_node_fs17 = require("node:fs");
17221
- var import_node_path18 = require("node:path");
17222
17647
  var import_node_fs18 = require("node:fs");
17648
+ var import_node_path19 = require("node:path");
17649
+ var import_node_fs19 = require("node:fs");
17223
17650
  var GC_GH_TIMEOUT_MS = 2e4;
17224
17651
  async function awsCallerArn() {
17225
17652
  try {
@@ -17265,7 +17692,7 @@ async function localBranchHeads() {
17265
17692
  }
17266
17693
  async function currentRepoWorktreeGitRoot(repoRoot) {
17267
17694
  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") : "";
17695
+ return gitCommonDir ? (0, import_node_path19.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
17269
17696
  }
17270
17697
  async function worktreeBranches() {
17271
17698
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -17285,18 +17712,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
17285
17712
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
17286
17713
  if (!match?.[1]) return void 0;
17287
17714
  const raw = match[1].trim();
17288
- return (0, import_node_path18.isAbsolute)(raw) ? raw : (0, import_node_path18.resolve)(worktreePath, raw);
17715
+ return (0, import_node_path19.isAbsolute)(raw) ? raw : (0, import_node_path19.resolve)(worktreePath, raw);
17289
17716
  }
17290
17717
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17291
17718
  if (!worktreeGitRoot) return false;
17292
17719
  try {
17293
- const entries = (0, import_node_fs18.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17720
+ const entries = (0, import_node_fs19.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17294
17721
  for (const ent of entries) {
17295
17722
  if (!ent.isDirectory()) continue;
17296
17723
  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;
17724
+ const gitdirPath = (0, import_node_fs18.readFileSync)((0, import_node_path19.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17725
+ const resolvedGitdir = (0, import_node_path19.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path19.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17726
+ if (sameWorktreeMetadataPath((0, import_node_path19.dirname)(resolvedGitdir), worktreePath)) return true;
17300
17727
  } catch {
17301
17728
  }
17302
17729
  }
@@ -17306,7 +17733,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17306
17733
  }
17307
17734
  function pathExistsKnown(path2) {
17308
17735
  try {
17309
- (0, import_node_fs18.statSync)(path2);
17736
+ (0, import_node_fs19.statSync)(path2);
17310
17737
  return true;
17311
17738
  } catch (e) {
17312
17739
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
@@ -17315,10 +17742,10 @@ function pathExistsKnown(path2) {
17315
17742
  }
17316
17743
  }
17317
17744
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17318
- const gitPath = (0, import_node_path18.join)(path2, ".git");
17745
+ const gitPath = (0, import_node_path19.join)(path2, ".git");
17319
17746
  let st;
17320
17747
  try {
17321
- st = (0, import_node_fs18.lstatSync)(gitPath);
17748
+ st = (0, import_node_fs19.lstatSync)(gitPath);
17322
17749
  } catch (e) {
17323
17750
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17324
17751
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -17335,7 +17762,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17335
17762
  if (st.isDirectory()) return { path: path2, gitType: "dir" };
17336
17763
  if (!st.isFile()) return { path: path2, gitType: "other" };
17337
17764
  try {
17338
- const gitFileContent = (0, import_node_fs17.readFileSync)(gitPath, "utf8");
17765
+ const gitFileContent = (0, import_node_fs18.readFileSync)(gitPath, "utf8");
17339
17766
  const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
17340
17767
  const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
17341
17768
  return {
@@ -17352,7 +17779,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17352
17779
  }
17353
17780
  function inspectDeadWorktreeDirContent(path2) {
17354
17781
  try {
17355
- return { entries: (0, import_node_fs18.readdirSync)(path2) };
17782
+ return { entries: (0, import_node_fs19.readdirSync)(path2) };
17356
17783
  } catch (e) {
17357
17784
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17358
17785
  return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
@@ -17371,8 +17798,8 @@ async function siblingWorktreeDirs() {
17371
17798
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
17372
17799
  const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
17373
17800
  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));
17801
+ const entries = (0, import_node_fs19.readdirSync)(siblingRoot, { withFileTypes: true });
17802
+ return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path19.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
17376
17803
  } catch {
17377
17804
  return [];
17378
17805
  }
@@ -17422,7 +17849,7 @@ async function fetchHubVersionInfo(baseUrl) {
17422
17849
  }
17423
17850
  function readRepoVersion() {
17424
17851
  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;
17852
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17426
17853
  } catch {
17427
17854
  return void 0;
17428
17855
  }
@@ -17574,11 +18001,11 @@ async function applyPluginHeal(token, surface, log, opts) {
17574
18001
  }
17575
18002
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
17576
18003
  const homeDir = surface === "codex" ? ".codex" : ".claude";
17577
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
18004
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
17578
18005
  };
17579
18006
  function readInstalledPlugins(surface = detectSurface(process.env)) {
17580
18007
  try {
17581
- return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(surface), "utf8"));
18008
+ return JSON.parse((0, import_node_fs20.readFileSync)(installedPluginsPath(surface), "utf8"));
17582
18009
  } catch {
17583
18010
  return null;
17584
18011
  }
@@ -17586,13 +18013,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
17586
18013
  function marketplaceCloneCandidates(surface, home) {
17587
18014
  if (surface === "codex") {
17588
18015
  return [
17589
- (0, import_node_path19.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
17590
- (0, import_node_path19.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
18016
+ (0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
18017
+ (0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
17591
18018
  ];
17592
18019
  }
17593
- return [(0, import_node_path19.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
18020
+ return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
17594
18021
  }
17595
- function marketplaceClonePresent(surface, home, exists = import_node_fs19.existsSync) {
18022
+ function marketplaceClonePresent(surface, home, exists = import_node_fs20.existsSync) {
17596
18023
  return marketplaceCloneCandidates(surface, home).some(exists);
17597
18024
  }
17598
18025
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
@@ -17602,14 +18029,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
17602
18029
  isOrgRepo,
17603
18030
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
17604
18031
  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"))
18032
+ pluginCachePresent: (0, import_node_fs20.existsSync)((0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
17606
18033
  };
17607
18034
  }
17608
18035
  function installedPluginSources() {
17609
18036
  return ["claude", "codex"].map((surface) => {
17610
- const recordPath = (0, import_node_path19.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18037
+ const recordPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
17611
18038
  try {
17612
- return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
18039
+ return { surface, installed: JSON.parse((0, import_node_fs20.readFileSync)(recordPath, "utf8")), recordPath };
17613
18040
  } catch {
17614
18041
  return { surface, installed: null, recordPath };
17615
18042
  }
@@ -17617,7 +18044,7 @@ function installedPluginSources() {
17617
18044
  }
17618
18045
  function readClaudeSettings() {
17619
18046
  try {
17620
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18047
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17621
18048
  } catch {
17622
18049
  return null;
17623
18050
  }
@@ -17639,7 +18066,7 @@ function writeProjectInstallRecord(record) {
17639
18066
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
17640
18067
  list.push(record);
17641
18068
  file.plugins[MMI_PLUGIN_ID] = list;
17642
- (0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18069
+ (0, import_node_fs20.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17643
18070
  `, "utf8");
17644
18071
  return true;
17645
18072
  } catch {
@@ -17652,9 +18079,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17652
18079
  if (!file) return false;
17653
18080
  if (!file.plugins) file.plugins = {};
17654
18081
  const path2 = installedPluginsPath();
17655
- (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
18082
+ (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
17656
18083
  file.plugins[pluginId] = records;
17657
- (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18084
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17658
18085
  `, "utf8");
17659
18086
  return true;
17660
18087
  } catch {
@@ -17662,22 +18089,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17662
18089
  }
17663
18090
  }
17664
18091
  function opencodeConfigDir() {
17665
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".config", "opencode");
18092
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "opencode");
17666
18093
  }
17667
18094
  function opencodeConfigPath() {
17668
- return (0, import_node_path19.join)(opencodeConfigDir(), "opencode.jsonc");
18095
+ return (0, import_node_path20.join)(opencodeConfigDir(), "opencode.jsonc");
17669
18096
  }
17670
18097
  function opencodeCommandsDir() {
17671
- return (0, import_node_path19.join)(opencodeConfigDir(), "commands");
18098
+ return (0, import_node_path20.join)(opencodeConfigDir(), "commands");
17672
18099
  }
17673
18100
  function opencodeSkillsPath() {
17674
- return (0, import_node_path19.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
18101
+ return (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
17675
18102
  }
17676
18103
  function opencodeConfigSnapshot() {
17677
18104
  const path2 = opencodeConfigPath();
17678
- if (!(0, import_node_fs19.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
18105
+ if (!(0, import_node_fs20.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
17679
18106
  try {
17680
- const raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
18107
+ const raw = (0, import_node_fs20.readFileSync)(path2, "utf8");
17681
18108
  const parsed = JSON.parse(stripJsonc(raw));
17682
18109
  const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
17683
18110
  const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
@@ -17700,9 +18127,9 @@ function writeOpencodeConfigPlugin(snapshot) {
17700
18127
  const plan = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
17701
18128
  if (plan.action === "already") return true;
17702
18129
  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");
18130
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
18131
+ if (snapshot.hasConfig) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
18132
+ (0, import_node_fs20.writeFileSync)(path2, plan.text, "utf8");
17706
18133
  return true;
17707
18134
  } catch {
17708
18135
  return false;
@@ -17717,9 +18144,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17717
18144
  const normalized = skillsPath.replace(/\\/g, "/");
17718
18145
  if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
17719
18146
  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)}
18147
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(snapshot.path), { recursive: true });
18148
+ if (snapshot.hasConfig && (0, import_node_fs20.existsSync)(snapshot.path)) (0, import_node_fs20.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
18149
+ (0, import_node_fs20.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
17723
18150
  `, "utf8");
17724
18151
  return true;
17725
18152
  } catch {
@@ -17728,7 +18155,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17728
18155
  }
17729
18156
  function opencodeExistingCommands() {
17730
18157
  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());
18158
+ 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
18159
  } catch {
17733
18160
  return [];
17734
18161
  }
@@ -17736,9 +18163,9 @@ function opencodeExistingCommands() {
17736
18163
  function writeOpencodeCommandFiles() {
17737
18164
  try {
17738
18165
  const dir = opencodeCommandsDir();
17739
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
18166
+ (0, import_node_fs20.mkdirSync)(dir, { recursive: true });
17740
18167
  for (const command of OPENCODE_WORKFLOW_COMMANDS) {
17741
- (0, import_node_fs19.writeFileSync)((0, import_node_path19.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
18168
+ (0, import_node_fs20.writeFileSync)((0, import_node_path20.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
17742
18169
  }
17743
18170
  return true;
17744
18171
  } catch {
@@ -17747,12 +18174,12 @@ function writeOpencodeCommandFiles() {
17747
18174
  }
17748
18175
  function readOpencodeAdapterDiskVersion() {
17749
18176
  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")
18177
+ (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
18178
+ (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
17752
18179
  ];
17753
18180
  for (const path2 of candidates) {
17754
18181
  try {
17755
- const parsed = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
18182
+ const parsed = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
17756
18183
  if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
17757
18184
  } catch {
17758
18185
  continue;
@@ -17768,7 +18195,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17768
18195
  try {
17769
18196
  const specs = opencodeMmiPluginSpecs(snapshot);
17770
18197
  log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
17771
- (0, import_node_fs19.mkdirSync)(opencodeConfigDir(), { recursive: true });
18198
+ (0, import_node_fs20.mkdirSync)(opencodeConfigDir(), { recursive: true });
17772
18199
  await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
17773
18200
  return true;
17774
18201
  } catch {
@@ -17776,12 +18203,12 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17776
18203
  }
17777
18204
  }
17778
18205
  function opencodePackagesRoot() {
17779
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
18206
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
17780
18207
  }
17781
18208
  function opencodePluginCacheDirs() {
17782
18209
  const root = opencodePackagesRoot();
17783
18210
  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));
18211
+ 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
18212
  } catch {
17786
18213
  return [];
17787
18214
  }
@@ -17789,7 +18216,7 @@ function opencodePluginCacheDirs() {
17789
18216
  function readOpencodeCacheDirVersion(cacheDir) {
17790
18217
  try {
17791
18218
  const parsed = JSON.parse(
17792
- (0, import_node_fs19.readFileSync)((0, import_node_path19.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
18219
+ (0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
17793
18220
  );
17794
18221
  return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17795
18222
  } catch {
@@ -17812,9 +18239,9 @@ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
17812
18239
  });
17813
18240
  if (!plan.quarantine) continue;
17814
18241
  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)));
18242
+ const quarantineRoot = (0, import_node_path20.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
18243
+ (0, import_node_fs20.mkdirSync)(quarantineRoot, { recursive: true });
18244
+ (0, import_node_fs20.renameSync)(cacheDir, (0, import_node_path20.join)(quarantineRoot, (0, import_node_path20.basename)(cacheDir)));
17818
18245
  log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
17819
18246
  moved = true;
17820
18247
  } catch {
@@ -17840,30 +18267,30 @@ function opencodePluginVersionsForReport() {
17840
18267
  }
17841
18268
  function opencodeDesktopLogsRoot() {
17842
18269
  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");
18270
+ const base = process.env.APPDATA || (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
18271
+ return (0, import_node_path20.join)(base, "ai.opencode.desktop", "logs");
17845
18272
  }
17846
18273
  if (process.platform === "darwin") {
17847
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
18274
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
17848
18275
  }
17849
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
18276
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
17850
18277
  }
17851
18278
  function opencodeDesktopBootstrapSnapshot() {
17852
18279
  const root = opencodeDesktopLogsRoot();
17853
18280
  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);
18281
+ 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
18282
  const newest = sessionDirs[0];
17856
18283
  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 }));
18284
+ const logPath = (0, import_node_path20.join)(newest, "renderer.log");
18285
+ const text = (0, import_node_fs20.readFileSync)(logPath, "utf8");
18286
+ return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs20.existsSync)(directory)).map((directory) => ({ directory, logPath }));
17860
18287
  } catch {
17861
18288
  return [];
17862
18289
  }
17863
18290
  }
17864
18291
  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 {};
18292
+ const legacyPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
18293
+ if (!(0, import_node_fs20.existsSync)(legacyPath)) return {};
17867
18294
  const content = readTextFile(legacyPath);
17868
18295
  if (content == null) return {};
17869
18296
  const plugins = parseOpencodeLegacyConfigPlugins(content);
@@ -17875,24 +18302,24 @@ function opencodeLegacyConfigSnapshot() {
17875
18302
  function quarantineOpencodeLegacyConfig(legacyPath) {
17876
18303
  try {
17877
18304
  const backupPath = `${legacyPath}.bak`;
17878
- if ((0, import_node_fs19.existsSync)(backupPath)) return false;
17879
- (0, import_node_fs19.renameSync)(legacyPath, backupPath);
18305
+ if ((0, import_node_fs20.existsSync)(backupPath)) return false;
18306
+ (0, import_node_fs20.renameSync)(legacyPath, backupPath);
17880
18307
  return true;
17881
18308
  } catch {
17882
18309
  return false;
17883
18310
  }
17884
18311
  }
17885
18312
  function cursorPluginCacheRoot() {
17886
- return (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18313
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
17887
18314
  }
17888
18315
  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");
18316
+ if (process.platform === "win32") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
18317
+ if (process.platform === "darwin") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
18318
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
17892
18319
  }
17893
18320
  function safeReaddirNames(dir) {
17894
18321
  try {
17895
- return (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
18322
+ return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
17896
18323
  } catch {
17897
18324
  return [];
17898
18325
  }
@@ -17901,15 +18328,15 @@ function readCursorPluginLogText() {
17901
18328
  const root = cursorLogsRoot();
17902
18329
  const sessions = safeReaddirNames(root).sort().reverse();
17903
18330
  for (const session of sessions.slice(0, 5)) {
17904
- const sessionDir = (0, import_node_path19.join)(root, session);
18331
+ const sessionDir = (0, import_node_path20.join)(root, session);
17905
18332
  const texts = [];
17906
18333
  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");
18334
+ const dir = (0, import_node_path20.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
17908
18335
  for (const file of safeReaddirNames(dir)) {
17909
18336
  if (!/^Cursor Plugins.*\.log$/i.test(file)) continue;
17910
- const path2 = (0, import_node_path19.join)(dir, file);
18337
+ const path2 = (0, import_node_path20.join)(dir, file);
17911
18338
  try {
17912
- texts.push({ text: (0, import_node_fs19.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs19.statSync)(path2).mtimeMs });
18339
+ texts.push({ text: (0, import_node_fs20.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs20.statSync)(path2).mtimeMs });
17913
18340
  } catch {
17914
18341
  }
17915
18342
  }
@@ -17923,32 +18350,32 @@ function readCursorPluginLogText() {
17923
18350
  function cursorPluginCachePinSnapshots() {
17924
18351
  const root = cursorPluginCacheRoot();
17925
18352
  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");
18353
+ return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
18354
+ const path2 = (0, import_node_path20.join)(root, entry.name);
18355
+ const pluginJson = (0, import_node_path20.join)(path2, ".cursor-plugin", "plugin.json");
18356
+ const hooksJson = (0, import_node_path20.join)(path2, "hooks", "hooks.json");
18357
+ const cliBundle = (0, import_node_path20.join)(path2, "cli", "dist", "index.cjs");
18358
+ const cursorHook = (0, import_node_path20.join)(path2, "scripts", "cursor-hook.mjs");
17932
18359
  let version;
17933
18360
  try {
17934
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
18361
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(pluginJson, "utf8"));
17935
18362
  version = typeof raw.version === "string" ? raw.version : void 0;
17936
18363
  } catch {
17937
18364
  version = void 0;
17938
18365
  }
17939
18366
  let isEmpty = true;
17940
18367
  try {
17941
- isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
18368
+ isEmpty = (0, import_node_fs20.readdirSync)(path2).length === 0;
17942
18369
  } catch {
17943
18370
  isEmpty = true;
17944
18371
  }
17945
18372
  return {
17946
18373
  name: entry.name,
17947
18374
  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),
18375
+ hasPluginJson: (0, import_node_fs20.existsSync)(pluginJson),
18376
+ hasHooksJson: (0, import_node_fs20.existsSync)(hooksJson),
18377
+ hasCliBundle: (0, import_node_fs20.existsSync)(cliBundle),
18378
+ hasCursorHookScript: (0, import_node_fs20.existsSync)(cursorHook),
17952
18379
  isEmpty,
17953
18380
  version
17954
18381
  };
@@ -17958,19 +18385,19 @@ function cursorPluginCachePinSnapshots() {
17958
18385
  }
17959
18386
  }
17960
18387
  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;
18388
+ const manifest = (0, import_node_path20.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
18389
+ return (0, import_node_fs20.existsSync)(manifest) ? process.cwd() : void 0;
17963
18390
  }
17964
18391
  function mmiPluginCacheRootSnapshots() {
17965
18392
  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") }
18393
+ { surface: "claude", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
18394
+ { surface: "codex", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
17968
18395
  ];
17969
18396
  return roots.flatMap(({ surface, root }) => {
17970
18397
  try {
17971
- const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18398
+ const entries = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
17972
18399
  name: entry.name,
17973
- path: (0, import_node_path19.join)(root, entry.name),
18400
+ path: (0, import_node_path20.join)(root, entry.name),
17974
18401
  isDirectory: entry.isDirectory()
17975
18402
  }));
17976
18403
  return [{ surface, root, entries }];
@@ -17981,7 +18408,7 @@ function mmiPluginCacheRootSnapshots() {
17981
18408
  }
17982
18409
  function hasNestedMmiChild(versionDir) {
17983
18410
  try {
17984
- return (0, import_node_fs19.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
18411
+ return (0, import_node_fs20.statSync)((0, import_node_path20.join)(versionDir, "mmi")).isDirectory();
17985
18412
  } catch {
17986
18413
  return false;
17987
18414
  }
@@ -17992,10 +18419,10 @@ function nestedPluginTreeSnapshot() {
17992
18419
  );
17993
18420
  }
17994
18421
  function uniqueQuarantineTarget(path2) {
17995
- if (!(0, import_node_fs19.existsSync)(path2)) return path2;
18422
+ if (!(0, import_node_fs20.existsSync)(path2)) return path2;
17996
18423
  for (let i = 1; i < 100; i += 1) {
17997
18424
  const candidate = `${path2}-${i}`;
17998
- if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
18425
+ if (!(0, import_node_fs20.existsSync)(candidate)) return candidate;
17999
18426
  }
18000
18427
  return `${path2}-${Date.now()}`;
18001
18428
  }
@@ -18004,10 +18431,10 @@ function quarantinePluginCacheDirs(plan) {
18004
18431
  const failed = [];
18005
18432
  for (const move of plan) {
18006
18433
  try {
18007
- if (!(0, import_node_fs19.existsSync)(move.from)) continue;
18434
+ if (!(0, import_node_fs20.existsSync)(move.from)) continue;
18008
18435
  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);
18436
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(target), { recursive: true });
18437
+ (0, import_node_fs20.renameSync)(move.from, target);
18011
18438
  moved += 1;
18012
18439
  } catch {
18013
18440
  failed.push(move);
@@ -18026,23 +18453,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
18026
18453
  }
18027
18454
  async function clearNestedPluginTreeDir(targetPath) {
18028
18455
  try {
18029
- if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
18456
+ if (!(0, import_node_fs20.existsSync)(targetPath)) return true;
18030
18457
  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 });
18458
+ const emptyDir = (0, import_node_path20.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
18459
+ (0, import_node_fs20.mkdirSync)(emptyDir, { recursive: true });
18033
18460
  try {
18034
18461
  await robocopyMirrorEmpty(emptyDir, targetPath);
18035
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18462
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
18036
18463
  } finally {
18037
18464
  try {
18038
- (0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
18465
+ (0, import_node_fs20.rmSync)(emptyDir, { recursive: true, force: true });
18039
18466
  } catch {
18040
18467
  }
18041
18468
  }
18042
- return !(0, import_node_fs19.existsSync)(targetPath);
18469
+ return !(0, import_node_fs20.existsSync)(targetPath);
18043
18470
  }
18044
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18045
- return !(0, import_node_fs19.existsSync)(targetPath);
18471
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
18472
+ return !(0, import_node_fs20.existsSync)(targetPath);
18046
18473
  } catch {
18047
18474
  return false;
18048
18475
  }
@@ -18055,18 +18482,18 @@ async function applyNestedPluginTreeCleanup(paths, log) {
18055
18482
  }
18056
18483
  return true;
18057
18484
  }
18058
- var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
18485
+ var gitignorePath = () => (0, import_node_path20.join)(process.cwd(), ".gitignore");
18059
18486
  function readTextFile(path2) {
18060
18487
  try {
18061
- if (!(0, import_node_fs19.existsSync)(path2)) return null;
18062
- return (0, import_node_fs19.readFileSync)(path2, "utf8");
18488
+ if (!(0, import_node_fs20.existsSync)(path2)) return null;
18489
+ return (0, import_node_fs20.readFileSync)(path2, "utf8");
18063
18490
  } catch {
18064
18491
  return null;
18065
18492
  }
18066
18493
  }
18067
18494
  function mcpDirExists(path2) {
18068
18495
  try {
18069
- return (0, import_node_fs19.existsSync)(path2);
18496
+ return (0, import_node_fs20.existsSync)(path2);
18070
18497
  } catch {
18071
18498
  return false;
18072
18499
  }
@@ -18074,60 +18501,60 @@ function mcpDirExists(path2) {
18074
18501
  function mcpConfigTargets() {
18075
18502
  const cwd = process.cwd();
18076
18503
  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");
18504
+ const cursorProjectDir = (0, import_node_path20.join)(cwd, ".cursor");
18505
+ const cursorUserDir = (0, import_node_path20.join)(home, ".cursor");
18506
+ const codexDir = (0, import_node_path20.join)(home, ".codex");
18080
18507
  return [
18081
18508
  // Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
18082
18509
  {
18083
18510
  host: "claude-code",
18084
18511
  label: "Claude Code project MCP",
18085
- path: (0, import_node_path19.join)(cwd, ".mcp.json"),
18512
+ path: (0, import_node_path20.join)(cwd, ".mcp.json"),
18086
18513
  format: "json",
18087
18514
  present: true,
18088
18515
  // parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
18089
18516
  isRepoFile: true,
18090
18517
  createWhenFileAbsent: false,
18091
- content: readTextFile((0, import_node_path19.join)(cwd, ".mcp.json"))
18518
+ content: readTextFile((0, import_node_path20.join)(cwd, ".mcp.json"))
18092
18519
  },
18093
18520
  // Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
18094
18521
  {
18095
18522
  host: "cursor-project",
18096
18523
  label: "Cursor project MCP",
18097
- path: (0, import_node_path19.join)(cursorProjectDir, "mcp.json"),
18524
+ path: (0, import_node_path20.join)(cursorProjectDir, "mcp.json"),
18098
18525
  format: "json",
18099
18526
  present: mcpDirExists(cursorProjectDir),
18100
18527
  isRepoFile: true,
18101
18528
  createWhenFileAbsent: true,
18102
- content: readTextFile((0, import_node_path19.join)(cursorProjectDir, "mcp.json"))
18529
+ content: readTextFile((0, import_node_path20.join)(cursorProjectDir, "mcp.json"))
18103
18530
  },
18104
18531
  // Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
18105
18532
  {
18106
18533
  host: "cursor-user",
18107
18534
  label: "Cursor user MCP",
18108
- path: (0, import_node_path19.join)(cursorUserDir, "mcp.json"),
18535
+ path: (0, import_node_path20.join)(cursorUserDir, "mcp.json"),
18109
18536
  format: "json",
18110
18537
  present: mcpDirExists(cursorUserDir),
18111
18538
  isRepoFile: false,
18112
18539
  createWhenFileAbsent: true,
18113
- content: readTextFile((0, import_node_path19.join)(cursorUserDir, "mcp.json"))
18540
+ content: readTextFile((0, import_node_path20.join)(cursorUserDir, "mcp.json"))
18114
18541
  },
18115
18542
  // Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
18116
18543
  {
18117
18544
  host: "codex",
18118
18545
  label: "Codex user config",
18119
- path: (0, import_node_path19.join)(codexDir, "config.toml"),
18546
+ path: (0, import_node_path20.join)(codexDir, "config.toml"),
18120
18547
  format: "toml",
18121
18548
  present: mcpDirExists(codexDir),
18122
18549
  isRepoFile: false,
18123
18550
  createWhenFileAbsent: true,
18124
- content: readTextFile((0, import_node_path19.join)(codexDir, "config.toml"))
18551
+ content: readTextFile((0, import_node_path20.join)(codexDir, "config.toml"))
18125
18552
  }
18126
18553
  ];
18127
18554
  }
18128
18555
  function writeMcpConfigFile(path2, content) {
18129
18556
  try {
18130
- (0, import_node_fs19.writeFileSync)(path2, content, "utf8");
18557
+ (0, import_node_fs20.writeFileSync)(path2, content, "utf8");
18131
18558
  return true;
18132
18559
  } catch {
18133
18560
  return false;
@@ -18207,7 +18634,7 @@ function strayBrowserArtifactPaths() {
18207
18634
  const cwd = process.cwd();
18208
18635
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
18209
18636
  try {
18210
- return (0, import_node_fs19.existsSync)((0, import_node_path19.join)(cwd, rel));
18637
+ return (0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, rel));
18211
18638
  } catch {
18212
18639
  return false;
18213
18640
  }
@@ -18215,37 +18642,119 @@ function strayBrowserArtifactPaths() {
18215
18642
  }
18216
18643
  function readGitignore() {
18217
18644
  try {
18218
- return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
18645
+ return (0, import_node_fs20.readFileSync)(gitignorePath(), "utf8");
18219
18646
  } catch {
18220
18647
  return null;
18221
18648
  }
18222
18649
  }
18223
18650
  function writeGitignore(content) {
18224
18651
  try {
18225
- (0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
18652
+ (0, import_node_fs20.writeFileSync)(gitignorePath(), content, "utf8");
18226
18653
  return true;
18227
18654
  } catch {
18228
18655
  return false;
18229
18656
  }
18230
18657
  }
18658
+ function readRepoJson(rel) {
18659
+ try {
18660
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), rel), "utf8"));
18661
+ } catch {
18662
+ return null;
18663
+ }
18664
+ }
18665
+ var ENCODING_SCAN_ROOT_FILES = ["AGENTS.md", "README.md", "CLAUDE.md", "package.json", "opencode.json"];
18666
+ var ENCODING_SCAN_DIRS = [".claude", "hooks", "scripts"];
18667
+ var ENCODING_SCAN_EXTS = /* @__PURE__ */ new Set([".md", ".json", ".jsonc", ".ts", ".js", ".cjs", ".mjs", ".sh", ".ps1", ".py", ".txt"]);
18668
+ var ENCODING_SCAN_MAX_FILES = 400;
18669
+ function listTextFilesUnder(dirRel, out, budget) {
18670
+ let entries;
18671
+ try {
18672
+ entries = (0, import_node_fs20.readdirSync)((0, import_node_path20.join)(process.cwd(), dirRel), { withFileTypes: true });
18673
+ } catch {
18674
+ return;
18675
+ }
18676
+ for (const ent of entries) {
18677
+ if (budget.count >= ENCODING_SCAN_MAX_FILES) return;
18678
+ const rel = `${dirRel}/${ent.name}`;
18679
+ if (ent.isDirectory()) {
18680
+ if (ent.name === "node_modules" || ent.name === "dist" || ent.name === ".git" || ent.name.startsWith(".")) continue;
18681
+ listTextFilesUnder(rel, out, budget);
18682
+ } else if (ent.isFile()) {
18683
+ const ext = ent.name.slice(ent.name.lastIndexOf("."));
18684
+ if (ENCODING_SCAN_EXTS.has(ext)) {
18685
+ out.push(rel);
18686
+ budget.count++;
18687
+ }
18688
+ }
18689
+ }
18690
+ }
18691
+ function managedFileEncodingFindings() {
18692
+ const cwd = process.cwd();
18693
+ const paths = [];
18694
+ for (const f of ENCODING_SCAN_ROOT_FILES) {
18695
+ try {
18696
+ if ((0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, f))) paths.push(f);
18697
+ } catch {
18698
+ }
18699
+ }
18700
+ const budget = { count: 0 };
18701
+ for (const d of ENCODING_SCAN_DIRS) listTextFilesUnder(d, paths, budget);
18702
+ const findings = [];
18703
+ for (const rel of paths) {
18704
+ try {
18705
+ const probe = inspectFileEncoding((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cwd, rel)));
18706
+ if (probe.bom || probe.invalidUtf8 || probe.replacementChars) findings.push({ path: rel, ...probe });
18707
+ } catch {
18708
+ }
18709
+ }
18710
+ return findings;
18711
+ }
18712
+ async function branchAheadBehind(branch) {
18713
+ const spec = `origin/${branch}...${branch}`;
18714
+ try {
18715
+ const { stdout } = await execFileP2("git", ["rev-list", "--left-right", "--count", spec], { timeout: GIT_TIMEOUT_MS });
18716
+ const [behind, ahead] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
18717
+ if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
18718
+ return { behind, ahead };
18719
+ } catch {
18720
+ return null;
18721
+ }
18722
+ }
18723
+ async function localReleaseBranchSnapshots() {
18724
+ let local;
18725
+ try {
18726
+ const { stdout } = await execFileP2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS });
18727
+ local = stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
18728
+ } catch {
18729
+ return [];
18730
+ }
18731
+ const releaseBranches = local.filter((b) => b === "development" || b === "main" || b === "master" || b === "rc" || b.startsWith("release/"));
18732
+ const rows = [];
18733
+ for (const branch of releaseBranches) {
18734
+ const ab = await branchAheadBehind(branch);
18735
+ if (ab) rows.push({ name: branch, ...ab });
18736
+ }
18737
+ return rows;
18738
+ }
18231
18739
  async function runDoctor(opts, io = consoleIo, readOrigin) {
18232
18740
  if (opts.guide) {
18233
18741
  if (opts.json) io.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
18234
18742
  else io.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
18235
18743
  return;
18236
18744
  }
18237
- const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
18745
+ const repairLocal = !opts.fast && (!opts.json || Boolean(opts.apply) || Boolean(opts.preflight));
18238
18746
  const repoWritesAllowed = !opts.noRepoWrites;
18239
- const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
18747
+ const runExtended = !opts.fast && (Boolean(opts.verbose) || Boolean(opts.json));
18748
+ const slowSkippedCount = opts.fast ? DOCTOR_EXTENDED_CHECK_LABELS.size : 0;
18240
18749
  const checks = [];
18241
18750
  const REWRITE_KEY = "url.https://github.com/.insteadOf";
18242
18751
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
18243
18752
  const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
18244
- githubLogin(),
18753
+ opts.fast ? Promise.resolve(void 0) : githubLogin(),
18245
18754
  execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
18246
- fetchReleasedVersion(),
18755
+ opts.fast ? Promise.resolve({ version: void 0, source: "unavailable" }) : fetchReleasedVersion(),
18247
18756
  loadConfig(),
18248
- awsCallerArn(),
18757
+ opts.fast ? Promise.resolve(void 0) : awsCallerArn(),
18249
18758
  execFileP2("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false),
18250
18759
  // unset → repair below
18251
18760
  // hub-v3 step-back: org-membership gates the repo-local checks/repairs by the git `origin` remote,
@@ -18264,7 +18773,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18264
18773
  const semverPrefix = /^\d+\.\d+\.\d+/;
18265
18774
  const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
18266
18775
  const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
18267
- const cursorCacheStale = (0, import_node_fs19.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18776
+ const cursorCacheStale = (0, import_node_fs20.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18268
18777
  const healPlan = doctorHealPlan({
18269
18778
  isOrgRepo,
18270
18779
  surface,
@@ -18286,20 +18795,22 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18286
18795
  if (opts.preflight && !healPlan.needsEagerHeal) return;
18287
18796
  const eagerHeal = Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
18288
18797
  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;
18798
+ const repairFull = !opts.fast && (!opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal);
18799
+ if (!opts.fast) {
18800
+ let ghInstalled = true;
18801
+ if (!login) {
18802
+ try {
18803
+ await execFileP2("gh", ["--version"]);
18804
+ } catch {
18805
+ ghInstalled = false;
18806
+ }
18296
18807
  }
18808
+ checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18297
18809
  }
18298
- checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18299
18810
  let onPath = pathProbe;
18300
18811
  if (!onPath) {
18301
18812
  const root = process.env.CLAUDE_PLUGIN_ROOT;
18302
- if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18813
+ if (root && (0, import_node_fs20.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18303
18814
  }
18304
18815
  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
18816
  const reloadHint = reloadAction(surface);
@@ -18342,7 +18853,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18342
18853
  return;
18343
18854
  }
18344
18855
  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);
18856
+ const hubVersionInfo = opts.fast ? null : await fetchHubVersionInfo(cfg.sagaApiUrl);
18346
18857
  const installedVersion = resolveClientVersion();
18347
18858
  checks.push(
18348
18859
  buildHubCompatCheck({
@@ -18360,6 +18871,12 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18360
18871
  releasedVersion
18361
18872
  })
18362
18873
  );
18874
+ checks.push(
18875
+ buildCliVersionDriftCheck({
18876
+ currentVersion: installedVersion,
18877
+ releasedVersion
18878
+ })
18879
+ );
18363
18880
  }
18364
18881
  if (runExtended) {
18365
18882
  checks.push(buildAwsCrossAccountCheck({ callerArn }));
@@ -18446,8 +18963,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18446
18963
  checks.push(gitignoreCheck);
18447
18964
  checks.push(buildRepoLocalWorktreeCheck({
18448
18965
  isOrgRepo,
18449
- hasRepoLocalWorktrees: (0, import_node_fs19.existsSync)((0, import_node_path19.join)(process.cwd(), ".worktrees"))
18966
+ hasRepoLocalWorktrees: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), ".worktrees"))
18450
18967
  }));
18968
+ if (isOrgRepo) {
18969
+ const pkg = readRepoJson("package.json");
18970
+ const lock = readRepoJson("package-lock.json");
18971
+ checks.push(buildLockfileOptionalsCheck({
18972
+ isOrgRepo,
18973
+ packageJsonOptionalDeps: pkg?.optionalDependencies ?? null,
18974
+ lockfilePackages: lock?.packages ?? null
18975
+ }));
18976
+ } else {
18977
+ checks.push(buildLockfileOptionalsCheck({ isOrgRepo, packageJsonOptionalDeps: null, lockfilePackages: null }));
18978
+ }
18451
18979
  let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
18452
18980
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
18453
18981
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
@@ -18632,7 +19160,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18632
19160
  return versions.sort((a, b) => compareVersions(b, a))[0];
18633
19161
  };
18634
19162
  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);
19163
+ const codexActiveVersion = () => !opts.fast && isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
18636
19164
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
18637
19165
  isOrgRepo,
18638
19166
  roots: mmiPluginCacheRootSnapshots(),
@@ -18735,7 +19263,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18735
19263
  }
18736
19264
  checks.push(nestedPluginTreeCheck);
18737
19265
  const cursorCacheRoot = cursorPluginCacheRoot();
18738
- const cursorCacheRootExists = (0, import_node_fs19.existsSync)(cursorCacheRoot);
19266
+ const cursorCacheRootExists = (0, import_node_fs20.existsSync)(cursorCacheRoot);
18739
19267
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
18740
19268
  checks.push(
18741
19269
  buildCursorPluginCacheCleanupCheck({
@@ -18763,7 +19291,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18763
19291
  cacheRootExists: cursorCacheRootExists,
18764
19292
  hubCheckout: hubCheckoutForCursorSeed(),
18765
19293
  execFileP: execFileP2,
18766
- mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path19.join)((0, import_node_os7.tmpdir)(), prefix)),
19294
+ mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path20.join)((0, import_node_os7.tmpdir)(), prefix)),
18767
19295
  log: (m) => io.err(m)
18768
19296
  });
18769
19297
  if (seeded) {
@@ -18840,6 +19368,23 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18840
19368
  cacheVersions: sessionCacheVersions
18841
19369
  })
18842
19370
  );
19371
+ {
19372
+ let redactorFailureLines = [];
19373
+ try {
19374
+ redactorFailureLines = (0, import_node_fs20.readFileSync)(activityLogPath(process.cwd()), "utf8").split("\n");
19375
+ } catch {
19376
+ redactorFailureLines = [];
19377
+ }
19378
+ const redactorSummary = summarizeRedactorFailures(redactorFailureLines);
19379
+ checks.push(
19380
+ buildRedactorLivenessCheck({
19381
+ isOrgRepo,
19382
+ failedCount: redactorSummary.failedCount,
19383
+ lastFailedTs: redactorSummary.lastFailedTs,
19384
+ lastFailedAction: redactorSummary.lastFailedAction
19385
+ })
19386
+ );
19387
+ }
18843
19388
  if (runExtended) {
18844
19389
  checks.push(
18845
19390
  buildBrowserArtifactsCheck({
@@ -18864,6 +19409,14 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18864
19409
  checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
18865
19410
  } catch {
18866
19411
  }
19412
+ try {
19413
+ checks.push(buildManagedFileEncodingCheck({ isOrgRepo, findings: managedFileEncodingFindings() }));
19414
+ } catch {
19415
+ }
19416
+ try {
19417
+ checks.push(buildStaleLocalBranchCheck({ isOrgRepo, branches: await localReleaseBranchSnapshots() }));
19418
+ } catch {
19419
+ }
18867
19420
  } else if (opts.apply && repoWritesAllowed) {
18868
19421
  const scratchRun = executeScratchGc(repoRoot, { apply: true });
18869
19422
  if (scratchRun.applied?.pruned.length) {
@@ -18884,6 +19437,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18884
19437
  healedCount,
18885
19438
  pluginReloadRequired,
18886
19439
  reloadHint,
19440
+ slowSkippedCount,
18887
19441
  // Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
18888
19442
  // an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
18889
19443
  // echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
@@ -18909,6 +19463,7 @@ function emitDoctorReport(opts, io, ctx) {
18909
19463
  const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
18910
19464
  const gaps = checks.filter((c) => !c.ok);
18911
19465
  if (opts.preflight) {
19466
+ process.exitCode = doctorExitCode(checks);
18912
19467
  const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
18913
19468
  if (outcome.line) io.err(outcome.line);
18914
19469
  if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
@@ -18935,7 +19490,8 @@ function emitDoctorReport(opts, io, ctx) {
18935
19490
  healedCount,
18936
19491
  pluginReloadRequired,
18937
19492
  reloadHint,
18938
- verbose: Boolean(opts.verbose)
19493
+ verbose: Boolean(opts.verbose),
19494
+ skippedSlowChecks: ctx.slowSkippedCount
18939
19495
  })) io.log(line);
18940
19496
  for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
18941
19497
  }
@@ -18958,23 +19514,23 @@ function mergeGuardHook(settings) {
18958
19514
  next.hooks = hooks;
18959
19515
  return next;
18960
19516
  }
18961
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path19.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
19517
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path20.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
18962
19518
  function ensureUserScopeGuardHook(opts = {}) {
18963
19519
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
18964
19520
  try {
18965
19521
  let current = null;
18966
- if ((0, import_node_fs19.existsSync)(path2)) {
19522
+ if ((0, import_node_fs20.existsSync)(path2)) {
18967
19523
  try {
18968
- current = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
19524
+ current = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
18969
19525
  } catch {
18970
19526
  return "failed";
18971
19527
  }
18972
19528
  }
18973
19529
  if (settingsHasGuardHook(current)) return "already";
18974
19530
  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)}
19531
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
19532
+ if ((0, import_node_fs20.existsSync)(path2)) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
19533
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
18978
19534
  `, "utf8");
18979
19535
  return "written";
18980
19536
  } catch {
@@ -19111,7 +19667,7 @@ async function applyGcPlan(plan, remote) {
19111
19667
  cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
19112
19668
  beforeWorktrees,
19113
19669
  startingPath: branch.worktreePath,
19114
- pathExists: (p) => (0, import_node_fs20.existsSync)(p),
19670
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
19115
19671
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
19116
19672
  teardownWorktreeStage,
19117
19673
  deferredStore,
@@ -19134,7 +19690,7 @@ async function applyGcPlan(plan, remote) {
19134
19690
  for (const wt of plan.worktreeDirs) {
19135
19691
  try {
19136
19692
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
19137
- realpath: (path2) => (0, import_node_fs20.realpathSync)(path2)
19693
+ realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
19138
19694
  });
19139
19695
  if (!cleanupTarget.ok) {
19140
19696
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -19174,16 +19730,21 @@ async function requireFreshTrainCli(commandName) {
19174
19730
  throw new Error(staleTrainCliMessage(report, commandName));
19175
19731
  }
19176
19732
  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)");
19733
+ 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
19734
  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;
19735
+ 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) => {
19736
+ const path2 = (0, import_node_path21.join)(process.cwd(), ".gitignore");
19737
+ const current = (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null;
19182
19738
  const plan = planManagedGitignore(current);
19183
19739
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
19740
+ if (opts.json) {
19741
+ console.log(JSON.stringify(plan, null, 2));
19742
+ if (!opts.write && plan.changed) process.exitCode = 1;
19743
+ return;
19744
+ }
19184
19745
  if (opts.write) {
19185
19746
  if (plan.changed) {
19186
- (0, import_node_fs20.writeFileSync)(path2, plan.content, "utf8");
19747
+ (0, import_node_fs21.writeFileSync)(path2, plan.content, "utf8");
19187
19748
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
19188
19749
  } else {
19189
19750
  console.log("mmi-cli rules gitignore: up to date");
@@ -19348,7 +19909,7 @@ function runWorktreeInstall(command, cwd, quiet) {
19348
19909
  async function primaryCheckoutRoot(worktreeRoot) {
19349
19910
  try {
19350
19911
  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;
19912
+ return out ? (0, import_node_path21.dirname)(out) : void 0;
19352
19913
  } catch {
19353
19914
  return void 0;
19354
19915
  }
@@ -19363,26 +19924,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
19363
19924
  function acquireWorktreeSetupLock(worktreeRoot) {
19364
19925
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
19365
19926
  const take = () => {
19366
- const fd = (0, import_node_fs20.openSync)(lockPath, "wx");
19927
+ const fd = (0, import_node_fs21.openSync)(lockPath, "wx");
19367
19928
  try {
19368
- (0, import_node_fs20.writeSync)(fd, String(Date.now()));
19929
+ (0, import_node_fs21.writeSync)(fd, String(Date.now()));
19369
19930
  } finally {
19370
- (0, import_node_fs20.closeSync)(fd);
19931
+ (0, import_node_fs21.closeSync)(fd);
19371
19932
  }
19372
19933
  return () => {
19373
19934
  try {
19374
- (0, import_node_fs20.rmSync)(lockPath, { force: true });
19935
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19375
19936
  } catch {
19376
19937
  }
19377
19938
  };
19378
19939
  };
19379
19940
  try {
19380
- (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(lockPath), { recursive: true });
19941
+ (0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(lockPath), { recursive: true });
19381
19942
  return take();
19382
19943
  } catch {
19383
19944
  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 });
19945
+ if (Date.now() - (0, import_node_fs21.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
19946
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19386
19947
  return take();
19387
19948
  }
19388
19949
  } catch {
@@ -19400,7 +19961,21 @@ worktree.command("create <branch>").description("create a worktree from a base r
19400
19961
  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
19962
  if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
19402
19963
  }
19403
- await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
19964
+ await addWorktreeRobust(wtPath, branch, base, {
19965
+ git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
19966
+ revParse: async (ref) => {
19967
+ try {
19968
+ return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
19969
+ } catch {
19970
+ return void 0;
19971
+ }
19972
+ },
19973
+ deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
19974
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
19975
+ log: (m) => {
19976
+ if (!o.json) console.error(` ${m}`);
19977
+ }
19978
+ });
19404
19979
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
19405
19980
  if (!o.json) console.error(` ${m}`);
19406
19981
  }));
@@ -19519,7 +20094,7 @@ async function attachToProject(issueNumber, repo, priority) {
19519
20094
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
19520
20095
  function scheduleRelatedDiscovery(o) {
19521
20096
  try {
19522
- const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
20097
+ const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
19523
20098
  if (o.repo) args.push("--repo", o.repo);
19524
20099
  spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
19525
20100
  } catch {
@@ -19872,7 +20447,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
19872
20447
  const vars = rawValues("--var");
19873
20448
  if (o.secretsFile) {
19874
20449
  try {
19875
- vars.push(`secrets=${(0, import_node_fs20.readFileSync)(o.secretsFile, "utf8")}`);
20450
+ vars.push(`secrets=${(0, import_node_fs21.readFileSync)(o.secretsFile, "utf8")}`);
19876
20451
  } catch (e) {
19877
20452
  return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
19878
20453
  }
@@ -20176,7 +20751,7 @@ issue.command("view <number>").description("read an issue as structured JSON \u2
20176
20751
  return fail(`issue view: ${raw}${argvNote}`);
20177
20752
  }
20178
20753
  });
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) => {
20754
+ 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
20755
  const number = Number(o.number);
20181
20756
  if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
20182
20757
  const repo = await resolveRepo(o.repo);
@@ -20208,7 +20783,10 @@ issue.command("discover-related").description("find related issues for an existi
20208
20783
  ]);
20209
20784
  if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
20210
20785
  await execFileP2("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
20211
- } catch {
20786
+ } catch (e) {
20787
+ if (o.failSoft) return;
20788
+ const err = e;
20789
+ return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
20212
20790
  }
20213
20791
  });
20214
20792
  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) => {
@@ -20415,7 +20993,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
20415
20993
  console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId, onBoard }));
20416
20994
  });
20417
20995
  var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
20418
- pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
20996
+ pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").action(async (o) => {
20419
20997
  let body;
20420
20998
  let title;
20421
20999
  try {
@@ -20424,17 +21002,19 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
20424
21002
  } catch (e) {
20425
21003
  return fail(`pr create: ${e.message}`);
20426
21004
  }
20427
- const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
21005
+ const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
20428
21006
  console.log(JSON.stringify(created));
20429
21007
  });
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) => {
21008
+ 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
21009
  const n = Number(number);
20432
21010
  if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
20433
21011
  const repo = await resolveRepo(o.repo);
20434
21012
  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";
21013
+ const fields = normalizeIssueViewJsonFields(o.json);
21014
+ const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
21015
+ const effective = fields === normalizeIssueViewJsonFields(void 0) ? defaultPrFields : fields;
20436
21016
  try {
20437
- const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
21017
+ const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
20438
21018
  console.log(JSON.stringify(data));
20439
21019
  } catch (e) {
20440
21020
  const err = e;
@@ -20442,9 +21022,9 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
20442
21022
  }
20443
21023
  });
20444
21024
  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}`);
21025
+ const wfDir = (0, import_node_path21.join)(cwd, ".github", "workflows");
21026
+ if (!(0, import_node_fs21.existsSync)(wfDir)) return [];
21027
+ return (0, import_node_fs21.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
20448
21028
  }
20449
21029
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
20450
21030
  const repo = repoOpt ?? await resolveRepo();
@@ -20456,16 +21036,29 @@ async function resolveMergeCiPolicyForCheckout(repoOpt) {
20456
21036
  }
20457
21037
  function ciAuditDeps() {
20458
21038
  const cfgPromise = loadConfig();
21039
+ const root = hubRoot();
20459
21040
  return {
20460
21041
  client: defaultGitHubClient(),
20461
21042
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
20462
21043
  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
21044
+ // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
21045
+ // Resolve from the installed package root so the reconcile works from any org repo checkout; when the
21046
+ // Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
21047
+ // gate re-seed step is skipped gracefully rather than failing mid-run.
21048
+ readSeedFile: (path2) => {
21049
+ if (!root) return null;
21050
+ const fullPath = (0, import_node_path21.join)(root, path2);
21051
+ return (0, import_node_fs21.existsSync)(fullPath) ? (0, import_node_fs21.readFileSync)(fullPath, "utf8") : null;
21052
+ }
20467
21053
  };
20468
21054
  }
21055
+ function hubRoot() {
21056
+ const fromPkg = (0, import_node_path21.join)(__dirname, "..", "..");
21057
+ const marker = "skills/bootstrap/seeds/manifest.json";
21058
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(fromPkg, marker))) return fromPkg;
21059
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), marker))) return process.cwd();
21060
+ return null;
21061
+ }
20469
21062
  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
21063
  const result = await resolveMergeCiPolicyForCheckout(o.repo);
20471
21064
  if (o.json) return printLine(JSON.stringify(result));
@@ -20539,6 +21132,12 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20539
21132
  checksPassing: checks === "success" || checks === "no-checks-reported"
20540
21133
  };
20541
21134
  },
21135
+ readPrState: async (prNumber, repo) => {
21136
+ const args = repo ? ["--repo", repo] : [];
21137
+ const result2 = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
21138
+ if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
21139
+ return JSON.parse(result2.state);
21140
+ },
20542
21141
  mergeAuto: async (prNumber, repo) => {
20543
21142
  const args = repo ? ["--repo", repo] : [];
20544
21143
  const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
@@ -20633,7 +21232,7 @@ async function createDeferredWorktreeStore() {
20633
21232
  },
20634
21233
  write: async (entries) => {
20635
21234
  try {
20636
- await (0, import_promises3.mkdir)((0, import_node_path20.dirname)(registryPath), { recursive: true });
21235
+ await (0, import_promises3.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
20637
21236
  await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
20638
21237
  } catch {
20639
21238
  }
@@ -20647,13 +21246,13 @@ var realWorktreeDirRemover = {
20647
21246
  probe: (p) => {
20648
21247
  let st;
20649
21248
  try {
20650
- st = (0, import_node_fs20.lstatSync)(p);
21249
+ st = (0, import_node_fs21.lstatSync)(p);
20651
21250
  } catch {
20652
21251
  return null;
20653
21252
  }
20654
21253
  if (st.isSymbolicLink()) return "link";
20655
21254
  try {
20656
- (0, import_node_fs20.readlinkSync)(p);
21255
+ (0, import_node_fs21.readlinkSync)(p);
20657
21256
  return "link";
20658
21257
  } catch {
20659
21258
  }
@@ -20661,7 +21260,7 @@ var realWorktreeDirRemover = {
20661
21260
  },
20662
21261
  readdir: (p) => {
20663
21262
  try {
20664
- return (0, import_node_fs20.readdirSync)(p);
21263
+ return (0, import_node_fs21.readdirSync)(p);
20665
21264
  } catch {
20666
21265
  return [];
20667
21266
  }
@@ -20670,9 +21269,9 @@ var realWorktreeDirRemover = {
20670
21269
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20671
21270
  detachLink: (p) => {
20672
21271
  try {
20673
- (0, import_node_fs20.rmdirSync)(p);
21272
+ (0, import_node_fs21.rmdirSync)(p);
20674
21273
  } catch {
20675
- (0, import_node_fs20.unlinkSync)(p);
21274
+ (0, import_node_fs21.unlinkSync)(p);
20676
21275
  }
20677
21276
  },
20678
21277
  removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -20702,9 +21301,9 @@ async function worktreeHasStageState(worktreePath) {
20702
21301
  }
20703
21302
  }
20704
21303
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20705
- if (!(0, import_node_fs20.existsSync)(statePath)) return false;
21304
+ if (!(0, import_node_fs21.existsSync)(statePath)) return false;
20706
21305
  try {
20707
- const state = JSON.parse((0, import_node_fs20.readFileSync)(statePath, "utf8"));
21306
+ const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
20708
21307
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20709
21308
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20710
21309
  } catch {
@@ -20776,7 +21375,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20776
21375
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
20777
21376
  beforeWorktrees,
20778
21377
  startingPath,
20779
- pathExists: (p) => (0, import_node_fs20.existsSync)(p),
21378
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
20780
21379
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20781
21380
  teardownWorktreeStage,
20782
21381
  deferredStore,
@@ -20997,7 +21596,7 @@ function stageScopedRunOpts(o) {
20997
21596
  };
20998
21597
  }
20999
21598
  function printLine(value) {
21000
- (0, import_node_fs20.writeSync)(1, `${value}
21599
+ (0, import_node_fs21.writeSync)(1, `${value}
21001
21600
  `);
21002
21601
  }
21003
21602
  function stageKeepAlive() {
@@ -21011,8 +21610,8 @@ async function resolveStage() {
21011
21610
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
21012
21611
  return decideStage({
21013
21612
  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"))
21613
+ hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), "docker-compose.yml")),
21614
+ hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), ".env.example"))
21016
21615
  });
21017
21616
  }
21018
21617
  async function fetchStageVaultEnvMerge() {
@@ -21053,10 +21652,10 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
21053
21652
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
21054
21653
  return;
21055
21654
  }
21056
- const infra = resolvePortRangeInfra(process.cwd());
21655
+ const infra = resolvePortRangeInfra(process.cwd(), __dirname);
21057
21656
  if (!infra) {
21058
21657
  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" : ""}`
21658
+ `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
21659
  );
21061
21660
  }
21062
21661
  const path2 = infra.registryPath;
@@ -21450,7 +22049,7 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
21450
22049
  if (!audit.ok) process.exitCode = 1;
21451
22050
  });
21452
22051
  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) => {
22052
+ 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
22053
  const repo = await resolveRepo(o.repo);
21455
22054
  if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
21456
22055
  const cfg = await loadConfig();
@@ -21461,7 +22060,7 @@ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:
21461
22060
  log: (msg) => console.log(msg),
21462
22061
  err: (msg) => console.error(msg)
21463
22062
  },
21464
- { repo, pagesDir }
22063
+ { repo, pagesDir, json: o.json }
21465
22064
  );
21466
22065
  if (!ok) process.exitCode = 1;
21467
22066
  });
@@ -21486,9 +22085,8 @@ wiki.command("plan").description("compute the release wiki plan for the cwd repo
21486
22085
  );
21487
22086
  if (!ok) process.exitCode = 1;
21488
22087
  });
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) => {
22088
+ 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
22089
  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
22090
  if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
21493
22091
  const steps = bootstrapPlan(o.repo, o.class);
21494
22092
  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 +22103,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
21505
22103
  client: defaultGitHubClient(),
21506
22104
  projectMeta: meta,
21507
22105
  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,
22106
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
21509
22107
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
21510
22108
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
21511
22109
  requiredGcpApis: (() => {
@@ -21538,7 +22136,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
21538
22136
  else console.log(renderOrgRulesetDriftReport(plan));
21539
22137
  if (plan.action !== "noop") process.exitCode = 1;
21540
22138
  });
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) => {
22139
+ 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
22140
  const o = {
21543
22141
  class: rawValue("--class", "deployable"),
21544
22142
  projectType: rawValue("--project-type", ""),
@@ -21556,12 +22154,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
21556
22154
  return fail(`bootstrap apply: ${e.message}`);
21557
22155
  }
21558
22156
  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"));
22157
+ 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`);
22158
+ const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
21561
22159
  const baseBranch = o.class === "content" ? "main" : "development";
21562
22160
  const slug = parsedRepo.slug;
21563
22161
  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;
22162
+ const readFile2 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
21565
22163
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
21566
22164
  const rawVars = {};
21567
22165
  for (const value of rawValues("--var")) {
@@ -21858,16 +22456,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21858
22456
  const repoClass = o.class;
21859
22457
  targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
21860
22458
  } 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;
22459
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs21.existsSync)("projects.json") ? (0, import_node_fs21.readFileSync)("projects.json", "utf8") : null;
21862
22460
  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;
22461
+ const fanoutJson = (0, import_node_fs21.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs21.readFileSync)(".github/fanout-targets.json", "utf8") : null;
21864
22462
  targets = loadAccessTargets(projectsJson, fanoutJson);
21865
22463
  }
21866
22464
  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")) : {};
22465
+ const fileMatrix = (0, import_node_fs21.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs21.readFileSync)("access-matrix.json", "utf8")) : {};
21868
22466
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
21869
22467
  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: {} };
22468
+ const fileContracts = (0, import_node_fs21.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs21.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
21871
22469
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
21872
22470
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
21873
22471
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -21875,7 +22473,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21875
22473
  });
21876
22474
  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
22475
  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) => (
22476
+ 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
22477
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
21880
22478
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
21881
22479
  ));
@@ -21884,9 +22482,13 @@ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugi
21884
22482
  program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
21885
22483
  if (isInsideRepoSubdir(process.cwd())) {
21886
22484
  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.");
22485
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
22486
+ return;
22487
+ }
22488
+ if (!await isOrgRepoRoot()) {
22489
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (non-org repo)" });
21887
22490
  return;
21888
22491
  }
21889
- if (!await isOrgRepoRoot()) return;
21890
22492
  spawnDeferredGcSweep();
21891
22493
  const { parallel, sequential } = buildSessionStartPlan({
21892
22494
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
@@ -21906,6 +22508,12 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21906
22508
  // never costs banner time and next session's glance renders instantly within budget.
21907
22509
  scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
21908
22510
  }),
22511
+ // command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
22512
+ // not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
22513
+ commandLadderHint: async (io) => {
22514
+ const hint = commandLadderHint();
22515
+ if (hint) io.log(hint);
22516
+ },
21909
22517
  // local train sync (#2582): ff local development/main/rc to origin so this checkout never lags a
21910
22518
  // release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
21911
22519
  // fail-soft, silent unless a branch moved.
@@ -21924,6 +22532,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21924
22532
  spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
21925
22533
  consoleIo.log(worktreeBanner);
21926
22534
  }
22535
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
21927
22536
  });
21928
22537
  installProcessBackstop();
21929
22538
  program2.parseAsync().catch((e) => failGraceful(e.message));