@mutmutco/cli 3.9.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs CHANGED
@@ -3408,7 +3408,7 @@ var program = new Command();
3408
3408
 
3409
3409
  // src/index.ts
3410
3410
  var import_promises3 = require("node:fs/promises");
3411
- var import_node_fs19 = 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,10 +4520,18 @@ 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
- // Doctor runs last so it sees the final tree.
4529
+ // local train sync (#2582): fast-forward local development/main/rc to origin BEFORE doctor + before
4530
+ // the agent works, so a checkout never lags behind a release someone pushed to origin (and the
4531
+ // deferred main -> development alignment merge lands locally). Sequential + fail-soft: it runs one
4532
+ // bounded `git fetch` + ff, silent unless a branch actually moved, and never blocks the banner.
4533
+ { name: "train sync", run: verbs.syncTrain },
4534
+ // Doctor runs last so it sees the final (synced) tree.
4528
4535
  { name: "doctor", run: verbs.doctor }
4529
4536
  ]
4530
4537
  };
@@ -4566,6 +4573,103 @@ function scratchGcLines(cwd, env = process.env, now = Date.now()) {
4566
4573
  }
4567
4574
  }
4568
4575
 
4576
+ // src/git-clean-tree.ts
4577
+ function isAgentScratchPath(path2) {
4578
+ const normalized = path2.replace(/\\/g, "/").trim();
4579
+ return /^tmp_[^/]+$/.test(normalized);
4580
+ }
4581
+ function porcelainHasBlockingChanges(porcelain) {
4582
+ return porcelain.split("\n").some((line) => {
4583
+ const trimmed = line.trim();
4584
+ if (!trimmed) return false;
4585
+ const path2 = trimmed.slice(3).split(" -> ")[0]?.trim() ?? "";
4586
+ return path2 !== "" && !isAgentScratchPath(path2);
4587
+ });
4588
+ }
4589
+
4590
+ // src/local-train-sync.ts
4591
+ var TRAIN_BRANCHES = ["development", "main", "rc"];
4592
+ function clean(out) {
4593
+ return out.trim();
4594
+ }
4595
+ async function revParse(run, ref) {
4596
+ try {
4597
+ return clean(await run("git", ["rev-parse", "--verify", "--quiet", ref]));
4598
+ } catch {
4599
+ return "";
4600
+ }
4601
+ }
4602
+ async function isFastForward(run, ancestor, descendant) {
4603
+ try {
4604
+ await run("git", ["merge-base", "--is-ancestor", ancestor, descendant]);
4605
+ return true;
4606
+ } catch {
4607
+ return false;
4608
+ }
4609
+ }
4610
+ async function syncOneBranch(run, branch, currentBranch2, warn) {
4611
+ const localSha = await revParse(run, `refs/heads/${branch}`);
4612
+ if (!localSha) {
4613
+ return { branch, status: "skipped", reason: "no-local-branch", note: `${branch}: no local branch \u2014 skipped` };
4614
+ }
4615
+ const remoteSha = await revParse(run, `refs/remotes/origin/${branch}`);
4616
+ if (!remoteSha) {
4617
+ return { branch, status: "skipped", reason: "no-remote-branch", note: `${branch}: no origin/${branch} \u2014 skipped` };
4618
+ }
4619
+ if (localSha === remoteSha) {
4620
+ return { branch, status: "already-current", note: `${branch}: already at origin/${branch}` };
4621
+ }
4622
+ if (!await isFastForward(run, localSha, remoteSha)) {
4623
+ const note = `${branch}: local diverged from origin/${branch} \u2014 left untouched (never force)`;
4624
+ warn?.(note);
4625
+ return { branch, status: "skipped", reason: "diverged", note };
4626
+ }
4627
+ try {
4628
+ if (branch === currentBranch2) {
4629
+ const porcelain = await run("git", ["status", "--porcelain"]);
4630
+ if (porcelainHasBlockingChanges(porcelain)) {
4631
+ return { branch, status: "skipped", reason: "current-dirty", note: `${branch}: checked out with local changes \u2014 skipped (commit/stash, then rerun)` };
4632
+ }
4633
+ await run("git", ["merge", "--ff-only", `origin/${branch}`]);
4634
+ } else {
4635
+ await run("git", ["fetch", "origin", `${branch}:${branch}`]);
4636
+ }
4637
+ return { branch, status: "synced", note: `${branch}: fast-forwarded to origin/${branch}` };
4638
+ } catch (e) {
4639
+ const note = `${branch}: fast-forward failed \u2014 ${e instanceof Error ? e.message : String(e)}`;
4640
+ warn?.(note);
4641
+ return { branch, status: "skipped", reason: "ff-failed", note };
4642
+ }
4643
+ }
4644
+ async function syncLocalTrainBranches(run, options = {}) {
4645
+ const branches = options.branches ?? TRAIN_BRANCHES;
4646
+ const doFetch = options.fetch ?? true;
4647
+ const fetchRun = options.fetchRun ?? ((args) => run("git", args));
4648
+ if (doFetch) {
4649
+ try {
4650
+ await fetchRun(["fetch", "origin"]);
4651
+ } catch (e) {
4652
+ options.warn?.(`local-train-sync: git fetch origin failed \u2014 syncing against cached refs (${e instanceof Error ? e.message : String(e)})`);
4653
+ }
4654
+ }
4655
+ let currentBranch2 = "";
4656
+ try {
4657
+ currentBranch2 = clean(await run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
4658
+ } catch {
4659
+ currentBranch2 = "";
4660
+ }
4661
+ const results = [];
4662
+ for (const branch of branches) {
4663
+ results.push(await syncOneBranch(run, branch, currentBranch2, options.warn));
4664
+ }
4665
+ return { branches: results, changed: results.some((r) => r.status === "synced") };
4666
+ }
4667
+ function localTrainSyncBannerLine(result) {
4668
+ const moved = result.branches.filter((b) => b.status === "synced").map((b) => b.branch);
4669
+ if (moved.length === 0) return "";
4670
+ return `synced local ${moved.join(", ")} \u2192 origin`;
4671
+ }
4672
+
4569
4673
  // src/board.ts
4570
4674
  var import_node_child_process5 = require("node:child_process");
4571
4675
  var import_node_util5 = require("node:util");
@@ -5736,9 +5840,31 @@ async function refreshBoardSliceCache(deps) {
5736
5840
  }
5737
5841
  }
5738
5842
 
5739
- // src/worktree.ts
5843
+ // src/hook-activity.ts
5740
5844
  var import_node_fs9 = require("node:fs");
5741
5845
  var import_node_path9 = require("node:path");
5846
+ var DEFAULT_SURFACE = "claude";
5847
+ function activityLogPath(cwd) {
5848
+ return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
5849
+ }
5850
+ function appendHookActivity(cwd, entry) {
5851
+ try {
5852
+ const path2 = activityLogPath(cwd);
5853
+ const line = {
5854
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5855
+ surface: DEFAULT_SURFACE,
5856
+ ...entry
5857
+ };
5858
+ (0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
5859
+ (0, import_node_fs9.appendFileSync)(path2, `${JSON.stringify(line)}
5860
+ `, "utf8");
5861
+ } catch {
5862
+ }
5863
+ }
5864
+
5865
+ // src/worktree.ts
5866
+ var import_node_fs10 = require("node:fs");
5867
+ var import_node_path10 = require("node:path");
5742
5868
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
5743
5869
  var PKG = "package.json";
5744
5870
  var LOCKFILE = "package-lock.json";
@@ -5746,21 +5872,21 @@ var NODE_MODULES = "node_modules";
5746
5872
  var realFsProbe = {
5747
5873
  isDir: (p) => {
5748
5874
  try {
5749
- return (0, import_node_fs9.statSync)(p).isDirectory();
5875
+ return (0, import_node_fs10.statSync)(p).isDirectory();
5750
5876
  } catch {
5751
5877
  return false;
5752
5878
  }
5753
5879
  },
5754
5880
  isFile: (p) => {
5755
5881
  try {
5756
- return (0, import_node_fs9.statSync)(p).isFile();
5882
+ return (0, import_node_fs10.statSync)(p).isFile();
5757
5883
  } catch {
5758
5884
  return false;
5759
5885
  }
5760
5886
  },
5761
5887
  listDirs: (p) => {
5762
5888
  try {
5763
- return (0, import_node_fs9.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5889
+ return (0, import_node_fs10.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5764
5890
  } catch {
5765
5891
  return [];
5766
5892
  }
@@ -5768,12 +5894,12 @@ var realFsProbe = {
5768
5894
  };
5769
5895
  function scanInstallDirs(root, fs2 = realFsProbe) {
5770
5896
  const factsFor = (dir) => {
5771
- const abs = dir ? (0, import_node_path9.join)(root, dir) : root;
5897
+ const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
5772
5898
  return {
5773
5899
  dir,
5774
- hasPackageJson: fs2.isFile((0, import_node_path9.join)(abs, PKG)),
5775
- hasLockfile: fs2.isFile((0, import_node_path9.join)(abs, LOCKFILE)),
5776
- hasNodeModules: fs2.isDir((0, import_node_path9.join)(abs, NODE_MODULES))
5900
+ hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
5901
+ hasLockfile: fs2.isFile((0, import_node_path10.join)(abs, LOCKFILE)),
5902
+ hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES))
5777
5903
  };
5778
5904
  };
5779
5905
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -5783,7 +5909,7 @@ function npmInstallTargets(dirs) {
5783
5909
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
5784
5910
  }
5785
5911
  function isLinkedWorktree(root, fs2 = realFsProbe) {
5786
- return fs2.isFile((0, import_node_path9.join)(root, ".git"));
5912
+ return fs2.isFile((0, import_node_path10.join)(root, ".git"));
5787
5913
  }
5788
5914
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5789
5915
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -5793,8 +5919,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
5793
5919
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
5794
5920
  }
5795
5921
  function defaultCopyFile(from, to) {
5796
- (0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(to), { recursive: true });
5797
- (0, import_node_fs9.copyFileSync)(from, to);
5922
+ (0, import_node_fs10.mkdirSync)((0, import_node_path10.dirname)(to), { recursive: true });
5923
+ (0, import_node_fs10.copyFileSync)(from, to);
5798
5924
  }
5799
5925
  async function provisionWorktree(worktreeRoot, deps) {
5800
5926
  const fs2 = deps.fs ?? realFsProbe;
@@ -5806,7 +5932,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5806
5932
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
5807
5933
  const installed = [];
5808
5934
  for (const target of targets) {
5809
- const cwd = target.dir ? (0, import_node_path9.join)(worktreeRoot, target.dir) : worktreeRoot;
5935
+ const cwd = target.dir ? (0, import_node_path10.join)(worktreeRoot, target.dir) : worktreeRoot;
5810
5936
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
5811
5937
  await deps.runInstall(target.command, cwd);
5812
5938
  installed.push(target);
@@ -5815,7 +5941,7 @@ async function provisionWorktree(worktreeRoot, deps) {
5815
5941
  const copySkipped = [];
5816
5942
  const primary = await deps.primaryCheckout();
5817
5943
  for (const rel of LOCAL_ONLY_FILES) {
5818
- const dest = (0, import_node_path9.join)(worktreeRoot, rel);
5944
+ const dest = (0, import_node_path10.join)(worktreeRoot, rel);
5819
5945
  if (fs2.isFile(dest)) {
5820
5946
  copySkipped.push({ file: rel, reason: "already-present" });
5821
5947
  continue;
@@ -5824,11 +5950,11 @@ async function provisionWorktree(worktreeRoot, deps) {
5824
5950
  copySkipped.push({ file: rel, reason: "no-primary" });
5825
5951
  continue;
5826
5952
  }
5827
- if (!fs2.isFile((0, import_node_path9.join)(primary, rel))) {
5953
+ if (!fs2.isFile((0, import_node_path10.join)(primary, rel))) {
5828
5954
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
5829
5955
  continue;
5830
5956
  }
5831
- copyFile((0, import_node_path9.join)(primary, rel), dest);
5957
+ copyFile((0, import_node_path10.join)(primary, rel), dest);
5832
5958
  copied.push(rel);
5833
5959
  log(`copied local config: ${rel}`);
5834
5960
  }
@@ -5836,13 +5962,50 @@ async function provisionWorktree(worktreeRoot, deps) {
5836
5962
  }
5837
5963
  function defaultWorktreePath(repoRoot, branch) {
5838
5964
  const safe = branch.replace(/[/\\]+/g, "-");
5839
- return (0, import_node_path9.join)((0, import_node_path9.dirname)(repoRoot), "mmi-worktrees", safe);
5965
+ return (0, import_node_path10.join)((0, import_node_path10.dirname)(repoRoot), "mmi-worktrees", safe);
5840
5966
  }
5841
5967
  function resolveWorktreeBase(from, remote) {
5842
5968
  const remotePrefix = `${remote}/`;
5843
5969
  const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
5844
5970
  return { base: from, fetchBranch };
5845
5971
  }
5972
+ var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
5973
+ function isGitConfigLockError(error) {
5974
+ return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
5975
+ }
5976
+ var ADD_WORKTREE_BACKOFF_MS = [100, 250, 500, 750, 1e3];
5977
+ var ADD_WORKTREE_MAX_ATTEMPTS = 6;
5978
+ async function cleanupOrphanBranch(branch, base, preExistingOid, deps) {
5979
+ if (preExistingOid !== void 0) return;
5980
+ const currentOid = await deps.revParse(`refs/heads/${branch}`);
5981
+ if (currentOid === void 0) return;
5982
+ const baseOid = await deps.revParse(base);
5983
+ if (baseOid !== void 0 && currentOid !== baseOid) return;
5984
+ await deps.deleteBranch(branch);
5985
+ }
5986
+ async function addWorktreeRobust(wtPath, branch, base, deps) {
5987
+ const maxAttempts = deps.maxAttempts ?? ADD_WORKTREE_MAX_ATTEMPTS;
5988
+ const backoff = deps.backoffMs ?? ADD_WORKTREE_BACKOFF_MS;
5989
+ const log = deps.log ?? (() => {
5990
+ });
5991
+ const preExistingOid = await deps.revParse(`refs/heads/${branch}`);
5992
+ for (let i = 0; i < maxAttempts; i++) {
5993
+ try {
5994
+ await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
5995
+ return;
5996
+ } catch (e) {
5997
+ await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
5998
+ });
5999
+ const retriesLeft = i < maxAttempts - 1;
6000
+ if (retriesLeft && isGitConfigLockError(e)) {
6001
+ log(`worktree add: .git/config lock busy, retrying (${i + 1}/${maxAttempts - 1})`);
6002
+ await deps.sleep(backoff[Math.min(i, backoff.length - 1)]);
6003
+ continue;
6004
+ }
6005
+ throw e;
6006
+ }
6007
+ }
6008
+ }
5846
6009
 
5847
6010
  // src/whoami.ts
5848
6011
  async function resolveWhoami(deps) {
@@ -5867,8 +6030,39 @@ function whoamiLine(report) {
5867
6030
  return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
5868
6031
  }
5869
6032
 
6033
+ // src/command-ladder-hint.ts
6034
+ var COMMAND_LADDER_WRITES = [
6035
+ { gh: "gh issue create", replacement: "mmi-cli issue create" },
6036
+ { gh: "gh issue comment", replacement: "mmi-cli issue comment" },
6037
+ { gh: "gh pr create", replacement: "mmi-cli pr create" },
6038
+ { gh: "gh pr merge", replacement: "mmi-cli pr merge (or mmi-cli pr land)" }
6039
+ ];
6040
+ var COMMAND_LADDER_READS = [
6041
+ { gh: "gh issue view <N>", replacement: "mmi-cli issue view <N>" },
6042
+ { gh: "gh pr view <N>", replacement: "mmi-cli pr view <N>" },
6043
+ { gh: "gh pr checks <N>", replacement: "mmi-cli pr checks-wait <N>" }
6044
+ ];
6045
+ var COMMAND_LADDER_BOARD = [
6046
+ { gh: "gh issue list / project", replacement: "mmi-cli board read" }
6047
+ ];
6048
+ function commandLadderHint() {
6049
+ const pad = 28;
6050
+ const lines = [];
6051
+ lines.push("[mmi] Command ladder (#2609): reach for mmi-cli first");
6052
+ for (const e of COMMAND_LADDER_WRITES) {
6053
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6054
+ }
6055
+ for (const e of COMMAND_LADDER_READS) {
6056
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6057
+ }
6058
+ for (const e of COMMAND_LADDER_BOARD) {
6059
+ lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
6060
+ }
6061
+ return lines.join("\n");
6062
+ }
6063
+
5870
6064
  // src/index.ts
5871
- var import_node_path19 = require("node:path");
6065
+ var import_node_path21 = require("node:path");
5872
6066
 
5873
6067
  // src/merge-ci-policy.ts
5874
6068
  function resolveMergeCiPolicy(input) {
@@ -6148,6 +6342,9 @@ var MANAGED_GITIGNORE_LINES = [
6148
6342
  // doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
6149
6343
  // into every repo's .gitignore.
6150
6344
  ".aws-sam/",
6345
+ // jerv-cli lease markers — the ephemeral per-worktree lease ledger `jerv-cli lease open` writes into a
6346
+ // worktree root; never tracked (it lives only for the life of the worktree). Hub#2586.
6347
+ ".jerv-lease.json",
6151
6348
  "/*.png",
6152
6349
  // Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
6153
6350
  // .env.example reference file stays tracked.
@@ -6807,7 +7004,7 @@ async function auditRepoCi(repo, deps) {
6807
7004
  ok: hasRequiredChecks,
6808
7005
  label: "product required status checks active",
6809
7006
  detail: hasRequiredChecks ? void 0 : `no required status checks active \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset`,
6810
- remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
7007
+ remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap apply --execute automation (#1440)`
6811
7008
  });
6812
7009
  if (hasGateWorkflow) {
6813
7010
  const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
@@ -7116,6 +7313,13 @@ async function runPrLand(prNumber, options, deps) {
7116
7313
  error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
7117
7314
  };
7118
7315
  }
7316
+ const prState = await deps.readPrState(prNumber, repo);
7317
+ if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
7318
+ return {
7319
+ ...base,
7320
+ error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
7321
+ };
7322
+ }
7119
7323
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
7120
7324
  base.mergeStatus = merge.mergeStatus;
7121
7325
  if (merge.mergeStatus === "failed") {
@@ -7136,10 +7340,10 @@ async function runPrLand(prNumber, options, deps) {
7136
7340
  }
7137
7341
 
7138
7342
  // src/wave-status.ts
7139
- var import_node_fs11 = require("node:fs");
7343
+ var import_node_fs12 = require("node:fs");
7140
7344
 
7141
7345
  // src/gc.ts
7142
- var import_node_path10 = require("node:path");
7346
+ var import_node_path11 = require("node:path");
7143
7347
  var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
7144
7348
  var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
7145
7349
  var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
@@ -7410,8 +7614,8 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
7410
7614
  return { ok: true, path: worktreePath };
7411
7615
  }
7412
7616
  function siblingMmiWorktreesRoot(repoRoot) {
7413
- const parent = (0, import_node_path10.dirname)(repoRoot);
7414
- return (0, import_node_path10.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path10.join)(parent, "mmi-worktrees");
7617
+ const parent = (0, import_node_path11.dirname)(repoRoot);
7618
+ return (0, import_node_path11.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path11.join)(parent, "mmi-worktrees");
7415
7619
  }
7416
7620
  function classifySiblingWorktreeDir(entry) {
7417
7621
  if (!entry.ownedByCurrentRepo) {
@@ -8072,8 +8276,8 @@ function formatGcPlan(plan, apply) {
8072
8276
 
8073
8277
  // src/stage-runner.ts
8074
8278
  var import_node_child_process6 = require("node:child_process");
8075
- var import_node_fs10 = require("node:fs");
8076
- var import_node_path11 = require("node:path");
8279
+ var import_node_fs11 = require("node:fs");
8280
+ var import_node_path12 = require("node:path");
8077
8281
  var import_node_net = require("node:net");
8078
8282
  var import_node_util6 = require("node:util");
8079
8283
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
@@ -8190,11 +8394,11 @@ function appendForceRecreate(up) {
8190
8394
  return `${up.trimEnd()} --force-recreate`;
8191
8395
  }
8192
8396
  function stageStatePath(cwd = process.cwd()) {
8193
- return (0, import_node_path11.join)(cwd, "tmp", "stage", "state.json");
8397
+ return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
8194
8398
  }
8195
8399
  function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
8196
- const dir = (0, import_node_path11.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path11.resolve)(cwd, gitCommonDir);
8197
- return (0, import_node_path11.join)(dir, "mmi", "stage", "state.json");
8400
+ const dir = (0, import_node_path12.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path12.resolve)(cwd, gitCommonDir);
8401
+ return (0, import_node_path12.join)(dir, "mmi", "stage", "state.json");
8198
8402
  }
8199
8403
  function normPath2(path2) {
8200
8404
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -8418,14 +8622,14 @@ function stageProcessEnv(stagePort, extraEnv) {
8418
8622
  }
8419
8623
  async function ensureStageRuntimeEnv(config, opts, cwd) {
8420
8624
  if (!config.ensureEnv) return;
8421
- const target = (0, import_node_path11.join)(cwd, config.ensureEnv.target);
8422
- const example = (0, import_node_path11.join)(cwd, config.ensureEnv.example);
8423
- if (!(0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(example)) {
8424
- (0, import_node_fs10.copyFileSync)(example, target);
8425
- } else if ((0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(example)) {
8426
- const stale = detectStaleEnvFile((0, import_node_fs10.readFileSync)(example, "utf8"), (0, import_node_fs10.readFileSync)(target, "utf8"), {
8427
- exampleMtimeMs: (0, import_node_fs10.statSync)(example).mtimeMs,
8428
- targetMtimeMs: (0, import_node_fs10.statSync)(target).mtimeMs
8625
+ const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
8626
+ const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
8627
+ if (!(0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8628
+ (0, import_node_fs11.copyFileSync)(example, target);
8629
+ } else if ((0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
8630
+ const stale = detectStaleEnvFile((0, import_node_fs11.readFileSync)(example, "utf8"), (0, import_node_fs11.readFileSync)(target, "utf8"), {
8631
+ exampleMtimeMs: (0, import_node_fs11.statSync)(example).mtimeMs,
8632
+ targetMtimeMs: (0, import_node_fs11.statSync)(target).mtimeMs
8429
8633
  });
8430
8634
  if (stale) {
8431
8635
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -8433,8 +8637,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
8433
8637
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
8434
8638
  }
8435
8639
  }
8436
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs10.existsSync)(target)) {
8437
- (0, import_node_fs10.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs10.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
8640
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs11.existsSync)(target)) {
8641
+ (0, import_node_fs11.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs11.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
8438
8642
  }
8439
8643
  }
8440
8644
  async function gitText(cwd, args) {
@@ -8464,20 +8668,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
8464
8668
  return void 0;
8465
8669
  }
8466
8670
  function readState(path2) {
8467
- if (!(0, import_node_fs10.existsSync)(path2)) return null;
8671
+ if (!(0, import_node_fs11.existsSync)(path2)) return null;
8468
8672
  try {
8469
- return JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
8673
+ return JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
8470
8674
  } catch {
8471
8675
  return null;
8472
8676
  }
8473
8677
  }
8474
8678
  function mkdirFor(path2) {
8475
8679
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
8476
- (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
8680
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8477
8681
  }
8478
8682
  function writeState(path2, state) {
8479
8683
  mkdirFor(path2);
8480
- (0, import_node_fs10.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8684
+ (0, import_node_fs11.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
8481
8685
  }
8482
8686
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
8483
8687
  const reservation = {
@@ -8497,7 +8701,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
8497
8701
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
8498
8702
  }
8499
8703
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
8500
- (0, import_node_fs10.rmSync)(path2, { force: true });
8704
+ (0, import_node_fs11.rmSync)(path2, { force: true });
8501
8705
  }
8502
8706
  }
8503
8707
  async function killTree(pid) {
@@ -8653,8 +8857,8 @@ async function runStage(config = {}, opts = {}) {
8653
8857
  await ensureStageRuntimeEnv(config, opts, cwd);
8654
8858
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
8655
8859
  } catch (e) {
8656
- (0, import_node_fs10.rmSync)(statePath, { force: true });
8657
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs10.rmSync)(globalStatePath, { force: true });
8860
+ (0, import_node_fs11.rmSync)(statePath, { force: true });
8861
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs11.rmSync)(globalStatePath, { force: true });
8658
8862
  throw e;
8659
8863
  }
8660
8864
  const started = await startStage(config, {
@@ -8679,9 +8883,9 @@ function parseNextFromHead(headText) {
8679
8883
  }
8680
8884
  function readStageSummary(worktreePath) {
8681
8885
  const statePath = stageStatePath(worktreePath);
8682
- if (!(0, import_node_fs11.existsSync)(statePath)) return void 0;
8886
+ if (!(0, import_node_fs12.existsSync)(statePath)) return void 0;
8683
8887
  try {
8684
- const state = JSON.parse((0, import_node_fs11.readFileSync)(statePath, "utf8"));
8888
+ const state = JSON.parse((0, import_node_fs12.readFileSync)(statePath, "utf8"));
8685
8889
  const port = typeof state.port === "number" ? state.port : void 0;
8686
8890
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
8687
8891
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -8797,7 +9001,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
8797
9001
  // src/gh-create.ts
8798
9002
  var import_promises = require("node:fs/promises");
8799
9003
  var import_node_os3 = require("node:os");
8800
- var import_node_path12 = require("node:path");
9004
+ var import_node_path13 = require("node:path");
8801
9005
  var import_node_crypto3 = require("node:crypto");
8802
9006
  var ISSUE_TYPES = ["bug", "feature", "task"];
8803
9007
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -8840,8 +9044,8 @@ async function bodyArgsViaFile(args, deps = {}) {
8840
9044
  const remove = deps.remove ?? import_promises.unlink;
8841
9045
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
8842
9046
  const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
8843
- const file = (0, import_node_path12.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
8844
- await ensureDir((0, import_node_path12.dirname)(file), { recursive: true }).catch(() => {
9047
+ const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
9048
+ await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
8845
9049
  });
8846
9050
  await write(file, args[i + 1], "utf8");
8847
9051
  return {
@@ -9689,20 +9893,6 @@ async function runStageLiveDown(deps, t) {
9689
9893
  };
9690
9894
  }
9691
9895
 
9692
- // src/git-clean-tree.ts
9693
- function isAgentScratchPath(path2) {
9694
- const normalized = path2.replace(/\\/g, "/").trim();
9695
- return /^tmp_[^/]+$/.test(normalized);
9696
- }
9697
- function porcelainHasBlockingChanges(porcelain) {
9698
- return porcelain.split("\n").some((line) => {
9699
- const trimmed = line.trim();
9700
- if (!trimmed) return false;
9701
- const path2 = trimmed.slice(3).split(" -> ")[0]?.trim() ?? "";
9702
- return path2 !== "" && !isAgentScratchPath(path2);
9703
- });
9704
- }
9705
-
9706
9896
  // src/tenant-control-parse.ts
9707
9897
  var OUTPUT_BEGIN = "mmi-control-output-begin";
9708
9898
  var OUTPUT_END = "mmi-control-output-end";
@@ -9743,7 +9933,7 @@ function resolveDeployModel2(meta, repo) {
9743
9933
  if (isDeployModel(m)) return m;
9744
9934
  return resolveDeployModel(meta, repo);
9745
9935
  }
9746
- function clean(out) {
9936
+ function clean2(out) {
9747
9937
  return out.trim();
9748
9938
  }
9749
9939
  function requireValue(value, label) {
@@ -9826,11 +10016,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
9826
10016
  if (release.targetCommitish !== expectedTarget) {
9827
10017
  throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
9828
10018
  }
9829
- const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
10019
+ const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
9830
10020
  if (tagSha !== expectedSha) {
9831
10021
  throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
9832
10022
  }
9833
- const branchOut = clean(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
10023
+ const branchOut = clean2(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
9834
10024
  const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
9835
10025
  if (branchSha !== expectedSha) {
9836
10026
  throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
@@ -9891,7 +10081,7 @@ function ensurePositiveCount(out, emptyMessage) {
9891
10081
  if (count <= 0) throw new Error(emptyMessage);
9892
10082
  }
9893
10083
  async function remoteBranchExists(deps, branch) {
9894
- const out = clean(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${branch}`]));
10084
+ const out = clean2(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${branch}`]));
9895
10085
  return out.length > 0;
9896
10086
  }
9897
10087
  async function loadReleaseTrackBranchHints(deps) {
@@ -9903,10 +10093,10 @@ async function loadReleaseTrackBranchHints(deps) {
9903
10093
  return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
9904
10094
  }
9905
10095
  async function buildTrainApplyContext(deps) {
9906
- const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
10096
+ const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
9907
10097
  const [owner, name] = repo.split("/");
9908
10098
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
9909
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
10099
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
9910
10100
  const verdict = await deps.trainAuthority(repo);
9911
10101
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
9912
10102
  if (!verdict.train) {
@@ -9927,11 +10117,11 @@ async function requireCleanTree(deps) {
9927
10117
  if (porcelainHasBlockingChanges(status)) throw new Error("working tree must be clean before train apply");
9928
10118
  }
9929
10119
  async function requireBranch(deps, branch) {
9930
- const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10120
+ const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
9931
10121
  if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
9932
10122
  }
9933
10123
  async function currentBranch(deps) {
9934
- return clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"])) || "(unknown)";
10124
+ return clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"])) || "(unknown)";
9935
10125
  }
9936
10126
  async function restoreCheckoutAfterRelease(deps, startBranch) {
9937
10127
  const status = await deps.run("git", ["status", "--porcelain"]);
@@ -9974,17 +10164,17 @@ async function restoreCheckoutAfterRelease(deps, startBranch) {
9974
10164
  }
9975
10165
  }
9976
10166
  async function withCheckoutRestoredAfterRelease(deps, result, startBranch) {
9977
- return {
9978
- ...result,
9979
- checkout: await restoreCheckoutAfterRelease(deps, startBranch)
9980
- };
10167
+ const checkout = await restoreCheckoutAfterRelease(deps, startBranch);
10168
+ const branches = TRAIN_BRANCHES.filter((b) => b !== startBranch);
10169
+ const localSync = await syncLocalTrainBranches(deps.run, { branches, fetch: false, warn: deps.warn }).catch(() => void 0);
10170
+ return { ...result, checkout, ...localSync ? { localSync } : {} };
9981
10171
  }
9982
10172
  function normGitPath(p) {
9983
10173
  return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
9984
10174
  }
9985
10175
  async function releaseRcBranchLock(deps) {
9986
10176
  const porcelain = await deps.run("git", ["worktree", "list", "--porcelain"]);
9987
- const cwd = normGitPath(clean(await deps.run("git", ["rev-parse", "--show-toplevel"])));
10177
+ const cwd = normGitPath(clean2(await deps.run("git", ["rev-parse", "--show-toplevel"])));
9988
10178
  for (const wt of parseWorktreePorcelain(porcelain)) {
9989
10179
  if (wt.branch !== "rc") continue;
9990
10180
  if (normGitPath(wt.path) === cwd) continue;
@@ -10200,7 +10390,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
10200
10390
  return [...contexts];
10201
10391
  }
10202
10392
  async function findOpenAlignmentPr(deps, ctx) {
10203
- const out = clean(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
10393
+ const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
10204
10394
  if (!out) return void 0;
10205
10395
  const rows = JSON.parse(out);
10206
10396
  const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
@@ -10228,14 +10418,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
10228
10418
  note: `alignment PR already open: ${existing.url} \u2014 land it with \`mmi-cli pr merge ${existing.number} --auto --merge\``
10229
10419
  };
10230
10420
  }
10231
- const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
10421
+ const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
10232
10422
  if (ahead === "0") {
10233
10423
  return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
10234
10424
  }
10235
10425
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
10236
10426
 
10237
10427
  \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
10238
- const url = clean(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
10428
+ const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
10239
10429
  const number = parsePrNumber(url);
10240
10430
  return {
10241
10431
  status: "pr-pending",
@@ -10353,13 +10543,13 @@ Do not delete or force-move the pushed tag; rerun the train only after confirmin
10353
10543
  }
10354
10544
  async function probeRemoteTag(deps, tag) {
10355
10545
  const remoteOut = await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/tags/${tag}`]);
10356
- return clean(remoteOut).split(/\s+/)[0] || "";
10546
+ return clean2(remoteOut).split(/\s+/)[0] || "";
10357
10547
  }
10358
10548
  async function ensureTagPushed(deps, tag, sha, probed) {
10359
10549
  const remoteSha = probed ? probed.remoteSha : await probeRemoteTag(deps, tag);
10360
10550
  let localSha = "";
10361
10551
  try {
10362
- localSha = clean(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
10552
+ localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
10363
10553
  } catch {
10364
10554
  }
10365
10555
  if (remoteSha) {
@@ -10385,7 +10575,7 @@ function probeRcResumeTags(deps, base) {
10385
10575
  }
10386
10576
  async function resolveRcResumeTag(deps, base, sha, probed) {
10387
10577
  const out = probed ?? await runGitRemoteRead(deps, ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
10388
- const onSha = clean(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
10578
+ const onSha = clean2(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
10389
10579
  const unique = [...new Set(onSha)];
10390
10580
  if (unique.length === 0) return { tag: null };
10391
10581
  const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
@@ -10581,7 +10771,7 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
10581
10771
  async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
10582
10772
  await deps.run("git", ["checkout", "main"]);
10583
10773
  await ffOnlyPull(deps, "main");
10584
- preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
10774
+ preFold.mainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
10585
10775
  if (predicted.length === 0) {
10586
10776
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
10587
10777
  } else {
@@ -10592,8 +10782,8 @@ async function probeFoldFailureState(deps) {
10592
10782
  const branch = await currentBranch(deps);
10593
10783
  const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
10594
10784
  const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
10595
- const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean).catch(() => "");
10596
- const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean).catch(() => "");
10785
+ const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean2).catch(() => "");
10786
+ const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean2).catch(() => "");
10597
10787
  const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
10598
10788
  return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
10599
10789
  }
@@ -10654,7 +10844,7 @@ Recovery sequence:
10654
10844
  );
10655
10845
  }
10656
10846
  }
10657
- const rcSha = await deps.run("git", ["rev-parse", "rc"]).then(clean).catch(() => "");
10847
+ const rcSha = await deps.run("git", ["rev-parse", "rc"]).then(clean2).catch(() => "");
10658
10848
  const rcDescends = Boolean(preRcSha) && Boolean(rcSha) ? await deps.run("git", ["merge-base", "--is-ancestor", preRcSha, "rc"]).then(() => true).catch(() => false) : false;
10659
10849
  if (branch === "rc" && rcDescends) {
10660
10850
  try {
@@ -10745,7 +10935,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
10745
10935
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
10746
10936
  }
10747
10937
  await runGitPush(deps, ["push", "origin", "main"]);
10748
- const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10938
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10749
10939
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
10750
10940
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
10751
10941
  const autoRunSince = (deps.now ?? Date.now)();
@@ -10783,21 +10973,21 @@ async function runTrainApplyPipeline(mode, input) {
10783
10973
  "nothing to promote: origin/development is not ahead of origin/rc"
10784
10974
  );
10785
10975
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
10786
- const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
10976
+ const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
10787
10977
  await releaseRcBranchLock(deps);
10788
10978
  const rcTagProbe = await probeRcResumeTags(deps, releaseBase);
10789
10979
  await deps.run("git", ["checkout", "rc"]);
10790
10980
  await ffOnlyPull(deps, "rc");
10791
- const preRcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "pre-merge rc sha");
10981
+ const preRcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "pre-merge rc sha");
10792
10982
  let rcSha;
10793
10983
  let resume;
10794
10984
  let tag2;
10795
10985
  let tagPush;
10796
10986
  try {
10797
10987
  await deps.run("git", ["merge", "development", "--no-edit"]);
10798
- rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
10988
+ rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
10799
10989
  resume = await resolveRcResumeTag(deps, releaseBase, rcSha, rcTagProbe);
10800
- tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10990
+ tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10801
10991
  tagPush = await ensureTagPushed(deps, tag2, rcSha);
10802
10992
  } catch (e) {
10803
10993
  throw await recoverFailedRcand(deps, e, preRcSha);
@@ -10835,8 +11025,8 @@ async function runTrainApplyPipeline(mode, input) {
10835
11025
  "nothing to release: origin/development is not ahead of origin/main"
10836
11026
  );
10837
11027
  const deployModel2 = await preflight(deps, ctx, "main", meta);
10838
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
10839
- const rcShaAtRelease = !directTrack && hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
11028
+ const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
11029
+ const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
10840
11030
  const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
10841
11031
  deps,
10842
11032
  deployModel2,
@@ -10849,7 +11039,7 @@ async function runTrainApplyPipeline(mode, input) {
10849
11039
  const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
10850
11040
  await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
10851
11041
  const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10852
- const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
11042
+ const releaseSha3 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
10853
11043
  return { versionFold: versionFold3, releaseSha: releaseSha3 };
10854
11044
  });
10855
11045
  const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2);
@@ -10927,14 +11117,14 @@ async function runTrainApplyPipeline(mode, input) {
10927
11117
  `hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
10928
11118
  );
10929
11119
  }
10930
- const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
10931
- const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
11120
+ const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
11121
+ const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10932
11122
  const tagProbe = { remoteSha: await probeRemoteTag(deps, tag) };
10933
11123
  const preFold = {};
10934
11124
  const { versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
10935
11125
  await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
10936
11126
  const versionFold2 = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
10937
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
11127
+ const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
10938
11128
  return { versionFold: versionFold2, releaseSha: releaseSha2 };
10939
11129
  });
10940
11130
  const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe);
@@ -11053,7 +11243,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
11053
11243
  }
11054
11244
  try {
11055
11245
  await deps.run("git", ["fetch", "origin", "rc"]);
11056
- const rcNow = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
11246
+ const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
11057
11247
  if (rcNow !== releasedRcSha) {
11058
11248
  return {
11059
11249
  status: "skipped",
@@ -11084,7 +11274,7 @@ async function runTenantRedeploy(deps, options) {
11084
11274
  const repo = options.repo;
11085
11275
  const [owner, name] = repo.split("/");
11086
11276
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
11087
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
11277
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
11088
11278
  const verdict = await deps.trainAuthority(repo);
11089
11279
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
11090
11280
  if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
@@ -11324,7 +11514,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
11324
11514
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
11325
11515
  var HOTFIX_VERIFY_ATTEMPTS = 5;
11326
11516
  var HOTFIX_VERIFY_RETRY_MS = 6e3;
11327
- function clean2(out) {
11517
+ function clean3(out) {
11328
11518
  return out.trim();
11329
11519
  }
11330
11520
  function sleeper(deps) {
@@ -11382,7 +11572,7 @@ async function resolveHotfixSource(deps, ctx, from) {
11382
11572
  if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
11383
11573
  return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
11384
11574
  }
11385
- const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
11575
+ const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
11386
11576
  if (!sha) throw new Error(`could not resolve commit ${from}`);
11387
11577
  return { sha, label: sha.slice(0, 7) };
11388
11578
  }
@@ -11410,7 +11600,7 @@ async function runHotfixStart(deps, options) {
11410
11600
  };
11411
11601
  }
11412
11602
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
11413
- const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11603
+ const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11414
11604
  if (remoteBranch) {
11415
11605
  await deps.run("git", ["checkout", branch]);
11416
11606
  await deps.run("git", ["pull", "--ff-only", "origin", branch]);
@@ -11441,7 +11631,7 @@ async function runHotfixStart(deps, options) {
11441
11631
  await deps.run("git", ["push", "-u", "origin", branch]);
11442
11632
  }
11443
11633
  const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
11444
- const prUrl = clean2(await deps.run("gh", [
11634
+ const prUrl = clean3(await deps.run("gh", [
11445
11635
  "pr",
11446
11636
  "create",
11447
11637
  "--repo",
@@ -11585,7 +11775,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11585
11775
  }
11586
11776
  let verifyNote;
11587
11777
  if (deployModel === "hub-serverless") {
11588
- const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11778
+ const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11589
11779
  const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
11590
11780
  try {
11591
11781
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
@@ -11672,9 +11862,9 @@ async function runHotfixStatus(deps, versionInput) {
11672
11862
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
11673
11863
  }
11674
11864
  async function gatherHotfixFacts(deps, ctx, tag, version) {
11675
- const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
11865
+ const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
11676
11866
  const pr2 = await findHotfixPr(deps, ctx, tag);
11677
- const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
11867
+ const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
11678
11868
  const tagPushed = Boolean(remoteTag);
11679
11869
  const tagSha = remoteTag.split(/\s+/)[0] || "";
11680
11870
  let releaseExists = false;
@@ -11708,7 +11898,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
11708
11898
  }
11709
11899
  }
11710
11900
  }
11711
- const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
11901
+ const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
11712
11902
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
11713
11903
  }
11714
11904
  function compareHotfixVersions(a, b) {
@@ -11743,7 +11933,7 @@ async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
11743
11933
  const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
11744
11934
  if (m) tags.add(m[1]);
11745
11935
  }
11746
- const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
11936
+ const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
11747
11937
  for (const line of branchOut.split("\n").filter(Boolean)) {
11748
11938
  const ref = line.split(/\s+/)[1] ?? "";
11749
11939
  const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
@@ -11909,8 +12099,8 @@ async function announceRelease(deps, args) {
11909
12099
  }
11910
12100
 
11911
12101
  // src/port-registry.ts
11912
- var import_node_fs12 = require("node:fs");
11913
- var import_node_path13 = require("node:path");
12102
+ var import_node_fs13 = require("node:fs");
12103
+ var import_node_path14 = require("node:path");
11914
12104
 
11915
12105
  // ../infra/port-geometry.mjs
11916
12106
  var PORT_BLOCK = 100;
@@ -11924,8 +12114,8 @@ function nextPortBlock(registry2) {
11924
12114
  return [base, base + PORT_SPAN];
11925
12115
  }
11926
12116
  function loadPortRegistry(path2) {
11927
- if (!(0, import_node_fs12.existsSync)(path2)) return {};
11928
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
12117
+ if (!(0, import_node_fs13.existsSync)(path2)) return {};
12118
+ const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
11929
12119
  const out = {};
11930
12120
  for (const [key, value] of Object.entries(raw)) {
11931
12121
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -11939,9 +12129,9 @@ function ensurePortRange(repo, path2) {
11939
12129
  const existing = registry2[repo];
11940
12130
  if (existing) return existing;
11941
12131
  const range = nextPortBlock(registry2);
11942
- const raw = (0, import_node_fs12.existsSync)(path2) ? JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8")) : {};
12132
+ const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
11943
12133
  raw[repo] = range;
11944
- (0, import_node_fs12.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12134
+ (0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
11945
12135
  return range;
11946
12136
  }
11947
12137
  function portCursorSeed(registry2) {
@@ -11963,20 +12153,26 @@ function existingPortRange(repo, registry2) {
11963
12153
  return registry2[repo] ?? null;
11964
12154
  }
11965
12155
  function portRangeInfraAt(root, source) {
11966
- const registryPath = (0, import_node_path13.join)(root, "infra", "port-ranges.json");
11967
- const ddbScriptPath = (0, import_node_path13.join)(root, "infra", "port-ddb.mjs");
11968
- if (!(0, import_node_fs12.existsSync)(registryPath) || !(0, import_node_fs12.existsSync)(ddbScriptPath)) return null;
12156
+ const registryPath = (0, import_node_path14.join)(root, "infra", "port-ranges.json");
12157
+ const ddbScriptPath = (0, import_node_path14.join)(root, "infra", "port-ddb.mjs");
12158
+ if (!(0, import_node_fs13.existsSync)(registryPath) || !(0, import_node_fs13.existsSync)(ddbScriptPath)) return null;
11969
12159
  return { root, source, registryPath, ddbScriptPath };
11970
12160
  }
11971
- function resolvePortRangeInfra(cwd) {
12161
+ function resolvePortRangeInfra(cwd, packageDir) {
11972
12162
  const direct = portRangeInfraAt(cwd, "cwd");
11973
12163
  if (direct) return direct;
11974
- for (let dir = cwd; ; dir = (0, import_node_path13.dirname)(dir)) {
11975
- const sibling = portRangeInfraAt((0, import_node_path13.join)(dir, "MMI-Hub"), "sibling-hub");
12164
+ for (let dir = cwd; ; dir = (0, import_node_path14.dirname)(dir)) {
12165
+ const sibling = portRangeInfraAt((0, import_node_path14.join)(dir, "MMI-Hub"), "sibling-hub");
11976
12166
  if (sibling) return sibling;
11977
- const parent = (0, import_node_path13.dirname)(dir);
11978
- if (parent === dir) return null;
12167
+ const parent = (0, import_node_path14.dirname)(dir);
12168
+ if (parent === dir) break;
12169
+ }
12170
+ if (packageDir) {
12171
+ const pkgRoot = (0, import_node_path14.join)(packageDir, "..", "..");
12172
+ const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
12173
+ if (pkgFrom) return pkgFrom;
11979
12174
  }
12175
+ return null;
11980
12176
  }
11981
12177
  async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
11982
12178
  const registry2 = loadPortRegistry(path2);
@@ -12235,7 +12431,7 @@ function renderAccessReport(report) {
12235
12431
  }
12236
12432
 
12237
12433
  // src/bootstrap-verify.ts
12238
- var TRAIN_BRANCHES = ["development", "rc", "main"];
12434
+ var TRAIN_BRANCHES2 = ["development", "rc", "main"];
12239
12435
  var requiredDocs = ["README.md", "architecture.md"];
12240
12436
  var requiredIssueTemplates = [
12241
12437
  ".github/ISSUE_TEMPLATE/bug.yml",
@@ -12413,7 +12609,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12413
12609
  detail: hasPushAllowlist(protection) ? void 0 : "restrictions.users is empty or unset"
12414
12610
  });
12415
12611
  }
12416
- const strayTrainBranches = TRAIN_BRANCHES.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
12612
+ const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
12417
12613
  checks.push({
12418
12614
  ok: strayTrainBranches.length === 0,
12419
12615
  label: "no stray train branch for this track",
@@ -12863,38 +13059,51 @@ async function tenantDeploy(payload, deps) {
12863
13059
  // src/wiki-publish-command.ts
12864
13060
  async function wikiPublish(deps, opts) {
12865
13061
  if (!opts.pagesDir) {
12866
- deps.err("wiki publish: <pages-dir> is required");
13062
+ if (opts.json) {
13063
+ deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
13064
+ } else {
13065
+ deps.err("wiki publish: <pages-dir> is required");
13066
+ }
12867
13067
  return false;
12868
13068
  }
12869
13069
  const minted = await deps.mint(opts.repo);
12870
13070
  if (!minted.ok) {
12871
- deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13071
+ if (opts.json) {
13072
+ deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
13073
+ } else {
13074
+ deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
13075
+ }
12872
13076
  return false;
12873
13077
  }
12874
- const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir });
13078
+ const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
12875
13079
  if (!ok) {
12876
13080
  return false;
12877
13081
  }
12878
- deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13082
+ if (opts.json) {
13083
+ deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
13084
+ } else {
13085
+ deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
13086
+ }
12879
13087
  return true;
12880
13088
  }
12881
13089
 
12882
13090
  // src/wiki-publish-git.ts
12883
13091
  var import_node_child_process8 = require("node:child_process");
12884
- var import_node_fs13 = require("node:fs");
13092
+ var import_node_fs14 = require("node:fs");
12885
13093
  var import_node_os5 = require("node:os");
12886
- var import_node_path14 = require("node:path");
13094
+ var import_node_path15 = require("node:path");
12887
13095
  var WIKI_STAMP_FILE = ".wiki-state.json";
12888
13096
  function redactWikiSecrets(msg) {
12889
13097
  return msg.replace(/x-access-token:[^@\s]+@/gi, "x-access-token:***@").replace(/(AUTHORIZATION:\s*basic\s+)[A-Za-z0-9+/=]+/gi, "$1***");
12890
13098
  }
13099
+ function wikiAuthArgs(token) {
13100
+ return ["-c", `http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`];
13101
+ }
12891
13102
  function publishWikiViaGit(deps, opts) {
12892
13103
  const dir = deps.mkdtemp();
12893
13104
  const url = `https://github.com/${opts.repo}.wiki.git`;
12894
- const authArgs = [
12895
- "-c",
12896
- `http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${opts.token}`).toString("base64")}`
12897
- ];
13105
+ const authArgs = wikiAuthArgs(opts.token);
13106
+ const json = opts.json ?? false;
12898
13107
  try {
12899
13108
  deps.gitRun(["init", "-q"], dir);
12900
13109
  deps.gitRun(["remote", "add", "origin", url], dir);
@@ -12904,20 +13113,20 @@ function publishWikiViaGit(deps, opts) {
12904
13113
  let copied = 0;
12905
13114
  for (const f of deps.readdir(opts.pagesDir)) {
12906
13115
  if (f.endsWith(".md")) {
12907
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, f), (0, import_node_path14.join)(dir, f));
13116
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, f), (0, import_node_path15.join)(dir, f));
12908
13117
  copied++;
12909
13118
  }
12910
13119
  }
12911
13120
  if (!copied) {
12912
- deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
13121
+ if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
12913
13122
  return false;
12914
13123
  }
12915
- if (deps.fileExists((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
12916
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path14.join)(dir, WIKI_STAMP_FILE));
13124
+ if (deps.fileExists((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
13125
+ deps.copyFile((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path15.join)(dir, WIKI_STAMP_FILE));
12917
13126
  }
12918
13127
  deps.gitRun(["add", "-A"], dir);
12919
13128
  if (deps.gitProbe(["diff", "--cached", "--quiet"], dir)) {
12920
- deps.log("wiki publish: already up to date");
13129
+ if (!json) deps.log("wiki publish: already up to date");
12921
13130
  return true;
12922
13131
  }
12923
13132
  deps.gitRun(
@@ -12935,27 +13144,52 @@ function publishWikiViaGit(deps, opts) {
12935
13144
  try {
12936
13145
  deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
12937
13146
  } catch (e) {
12938
- deps.err(
13147
+ if (!json) deps.err(
12939
13148
  `wiki publish failed: push to ${opts.repo}.wiki.git rejected: ${redactWikiSecrets(e.message)} \u2014 if the wiki advanced before this push, re-run wiki publish to pick up the latest wiki state`
12940
13149
  );
12941
13150
  return false;
12942
13151
  }
12943
- deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
13152
+ if (!json) deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
12944
13153
  return true;
12945
13154
  } catch (e) {
12946
- deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
13155
+ if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
12947
13156
  return false;
12948
13157
  } finally {
12949
13158
  deps.rm(dir);
12950
13159
  }
12951
13160
  }
13161
+ function readWikiStampViaGit(deps, opts) {
13162
+ const dir = deps.mkdtemp();
13163
+ const url = `https://github.com/${opts.repo}.wiki.git`;
13164
+ const authArgs = wikiAuthArgs(opts.token);
13165
+ try {
13166
+ deps.gitRun(["init", "-q"], dir);
13167
+ deps.gitRun(["remote", "add", "origin", url], dir);
13168
+ if (!deps.gitProbe([...authArgs, "ls-remote", "origin"], dir)) {
13169
+ return { ok: false, error: `could not reach ${opts.repo}.wiki.git to read the wiki stamp \u2014 auth or network failure` };
13170
+ }
13171
+ if (!deps.gitProbe([...authArgs, "fetch", "--depth", "1", "origin", "master"], dir)) {
13172
+ return { ok: true, stamp: {} };
13173
+ }
13174
+ deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
13175
+ const stampPath = (0, import_node_path15.join)(dir, WIKI_STAMP_FILE);
13176
+ if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
13177
+ const parsed = JSON.parse(deps.readFile(stampPath));
13178
+ return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
13179
+ } catch (e) {
13180
+ return { ok: false, error: redactWikiSecrets(e.message) };
13181
+ } finally {
13182
+ deps.rm(dir);
13183
+ }
13184
+ }
12952
13185
  function realWikiGitPublishDeps() {
12953
13186
  return {
12954
- mkdtemp: () => (0, import_node_fs13.mkdtempSync)((0, import_node_path14.join)((0, import_node_os5.tmpdir)(), "wiki-")),
12955
- readdir: (dir) => (0, import_node_fs13.readdirSync)(dir),
12956
- copyFile: (src, dest) => (0, import_node_fs13.copyFileSync)(src, dest),
12957
- fileExists: (p) => (0, import_node_fs13.existsSync)(p),
12958
- rm: (p) => (0, import_node_fs13.rmSync)(p, { recursive: true, force: true }),
13187
+ mkdtemp: () => (0, import_node_fs14.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "wiki-")),
13188
+ readdir: (dir) => (0, import_node_fs14.readdirSync)(dir),
13189
+ copyFile: (src, dest) => (0, import_node_fs14.copyFileSync)(src, dest),
13190
+ fileExists: (p) => (0, import_node_fs14.existsSync)(p),
13191
+ readFile: (p) => (0, import_node_fs14.readFileSync)(p, "utf8"),
13192
+ rm: (p) => (0, import_node_fs14.rmSync)(p, { recursive: true, force: true }),
12959
13193
  gitProbe: (args, cwd) => {
12960
13194
  try {
12961
13195
  (0, import_node_child_process8.execFileSync)("git", args, { cwd, stdio: "pipe" });
@@ -12972,6 +13206,124 @@ function realWikiGitPublishDeps() {
12972
13206
  };
12973
13207
  }
12974
13208
 
13209
+ // src/wiki-pages.ts
13210
+ var import_node_crypto4 = require("node:crypto");
13211
+ var import_node_fs15 = require("node:fs");
13212
+ var import_node_path16 = require("node:path");
13213
+ var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
13214
+ var attr = (raw, key) => {
13215
+ const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
13216
+ return m ? m[1].trim() : "";
13217
+ };
13218
+ var sanitizeSlug = (s) => String(s ?? "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
13219
+ function extractWikiMarkers(text, sourcePath = "") {
13220
+ const out = [];
13221
+ for (const m of String(text ?? "").matchAll(MARKER_RE)) {
13222
+ const raw = m[1];
13223
+ const slug = sanitizeSlug(attr(raw, "slug"));
13224
+ const title = attr(raw, "title");
13225
+ if (!slug || !title) continue;
13226
+ out.push({ slug, title, artifact: attr(raw, "artifact"), sourcePath });
13227
+ }
13228
+ return out;
13229
+ }
13230
+ var DURABLE_GLOBS = [
13231
+ "architecture.md",
13232
+ "docs/Architecture",
13233
+ "kb/architecture",
13234
+ "docs/org-architecture.md",
13235
+ "docs/org-readme.md"
13236
+ ];
13237
+ var walkMarkdown = (root, rel) => {
13238
+ const abs = (0, import_node_path16.join)(root, rel);
13239
+ let st;
13240
+ try {
13241
+ st = (0, import_node_fs15.statSync)(abs);
13242
+ } catch {
13243
+ return [];
13244
+ }
13245
+ if (st.isFile()) return rel.endsWith(".md") ? [rel] : [];
13246
+ return (0, import_node_fs15.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path16.join)(rel, name)));
13247
+ };
13248
+ function collectFeaturePages({ listDocs, readDoc }) {
13249
+ const seen = /* @__PURE__ */ new Set();
13250
+ const pages = [];
13251
+ for (const path2 of listDocs()) {
13252
+ for (const mk of extractWikiMarkers(readDoc(path2), path2)) {
13253
+ if (seen.has(mk.slug)) continue;
13254
+ seen.add(mk.slug);
13255
+ pages.push(mk);
13256
+ }
13257
+ }
13258
+ return pages;
13259
+ }
13260
+ function fsReaders(rootDir) {
13261
+ return {
13262
+ listDocs: () => DURABLE_GLOBS.flatMap((g) => walkMarkdown(rootDir, g)),
13263
+ readDoc: (p) => {
13264
+ try {
13265
+ return (0, import_node_fs15.readFileSync)((0, import_node_path16.join)(rootDir, p), "utf8");
13266
+ } catch {
13267
+ return "";
13268
+ }
13269
+ }
13270
+ };
13271
+ }
13272
+ function hashPageInputs({ slug, title, source }) {
13273
+ return (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify([slug ?? "", title ?? "", source ?? ""])).digest("hex");
13274
+ }
13275
+ function computePageStamp(pages, readSource) {
13276
+ const stamp = {};
13277
+ for (const p of pages ?? []) {
13278
+ stamp[p.slug] = {
13279
+ hash: hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) }),
13280
+ sourcePath: p.sourcePath
13281
+ };
13282
+ }
13283
+ return stamp;
13284
+ }
13285
+ function selectChangedPages(pages, prevStamp, readSource) {
13286
+ const prev = prevStamp && typeof prevStamp === "object" ? prevStamp : {};
13287
+ const backfill = Object.keys(prev).length === 0;
13288
+ return (pages ?? []).filter((p) => {
13289
+ if (backfill) return true;
13290
+ const prior = prev[p.slug];
13291
+ if (!prior) return true;
13292
+ const cur = hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) });
13293
+ return prior.hash !== cur;
13294
+ });
13295
+ }
13296
+
13297
+ // src/wiki-plan-command.ts
13298
+ async function wikiPlan(deps, opts) {
13299
+ const wikiRepo = `${opts.repo}.wiki.git`;
13300
+ const pages = deps.collectPages();
13301
+ if (pages.length === 0) {
13302
+ emit(deps, { repo: opts.repo, wikiRepo, backfill: false, pages: [], stamp: {} });
13303
+ return true;
13304
+ }
13305
+ const read = await deps.mintAndReadStamp(opts.repo);
13306
+ if (!read.ok) {
13307
+ deps.err(`wiki plan failed: could not read the wiki stamp for ${opts.repo}: ${read.error}`);
13308
+ return false;
13309
+ }
13310
+ const prevStamp = read.stamp;
13311
+ const backfill = Object.keys(prevStamp).length === 0;
13312
+ const changed = new Set(selectChangedPages(pages, prevStamp, deps.readSource).map((p) => p.slug));
13313
+ const stamp = computePageStamp(pages, deps.readSource);
13314
+ emit(deps, {
13315
+ repo: opts.repo,
13316
+ wikiRepo,
13317
+ backfill,
13318
+ pages: pages.map((p) => ({ ...p, changed: changed.has(p.slug) })),
13319
+ stamp
13320
+ });
13321
+ return true;
13322
+ }
13323
+ function emit(deps, result) {
13324
+ deps.out(JSON.stringify(result, null, 2));
13325
+ }
13326
+
12975
13327
  // src/project-readiness.ts
12976
13328
  function stagesForTrack(meta) {
12977
13329
  return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
@@ -13990,22 +14342,22 @@ var DEPLOY_STAGES = ["dev", "rc", "main"];
13990
14342
  var DEPLOY_DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
13991
14343
  var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
13992
14344
  function buildSetDeployPatch(slug, input) {
13993
- const clean3 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
13994
- const stage2 = clean3(input.stage);
14345
+ const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
14346
+ const stage2 = clean4(input.stage);
13995
14347
  if (!stage2 || !DEPLOY_STAGES.includes(stage2)) {
13996
14348
  throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
13997
14349
  }
13998
- const substrate = clean3(input.substrate) ?? "hetzner-ssh";
14350
+ const substrate = clean4(input.substrate) ?? "hetzner-ssh";
13999
14351
  if (!DEPLOY_SUBSTRATES.includes(substrate)) {
14000
14352
  throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
14001
14353
  }
14002
- const sshHost = clean3(input.sshHost);
14354
+ const sshHost = clean4(input.sshHost);
14003
14355
  if (substrate === "hetzner-ssh" && !sshHost) {
14004
14356
  throw new Error("project set-deploy: hetzner-ssh requires --ssh-host");
14005
14357
  }
14006
- const sshUser = clean3(input.sshUser) ?? "root";
14007
- const deployPath = clean3(input.deployPath) ?? `/opt/mmi/${slug}/${stage2}`;
14008
- const serviceName = clean3(input.service) ?? slug;
14358
+ const sshUser = clean4(input.sshUser) ?? "root";
14359
+ const deployPath = clean4(input.deployPath) ?? `/opt/mmi/${slug}/${stage2}`;
14360
+ const serviceName = clean4(input.service) ?? slug;
14009
14361
  for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
14010
14362
  if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
14011
14363
  }
@@ -14014,9 +14366,9 @@ function buildSetDeployPatch(slug, input) {
14014
14366
  port = Number(input.port);
14015
14367
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
14016
14368
  }
14017
- const domain = clean3(input.domain);
14369
+ const domain = clean4(input.domain);
14018
14370
  if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
14019
- const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean3(a)).filter((a) => a !== void 0);
14371
+ const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
14020
14372
  for (const a of aliases) {
14021
14373
  if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
14022
14374
  }
@@ -14239,7 +14591,12 @@ async function targetRepo(deps, opts) {
14239
14591
  return opts.repo ?? repoOf(await vaultSlug(deps, opts));
14240
14592
  }
14241
14593
  async function secretsWhere(deps, opts) {
14242
- deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
14594
+ const p = vaultPointer(await vaultSlug(deps, opts));
14595
+ if (opts.json) {
14596
+ deps.log(JSON.stringify(p, null, 2));
14597
+ return;
14598
+ }
14599
+ deps.log(formatVaultPointer(p));
14243
14600
  }
14244
14601
  async function readErr(res) {
14245
14602
  try {
@@ -14302,6 +14659,10 @@ async function secretsList(deps, opts) {
14302
14659
  return;
14303
14660
  }
14304
14661
  const { secrets } = await res.json();
14662
+ if (opts.json) {
14663
+ deps.log(JSON.stringify({ secrets: secrets ?? [] }, null, 2));
14664
+ return;
14665
+ }
14305
14666
  deps.log(formatSecretList(secrets ?? []));
14306
14667
  }
14307
14668
  var CAPABILITIES_TIMEOUT_MS = 2e4;
@@ -14517,6 +14878,13 @@ function requiredRuntimeSecretNames(stage2, contract, opts = {}) {
14517
14878
  function stageKey2(stage2, key) {
14518
14879
  return key.includes("/") ? key : `${stage2}/${key}`;
14519
14880
  }
14881
+ function keyDeclaredPerStage(declaredSecrets, key) {
14882
+ if (!declaredSecrets) return void 0;
14883
+ const entry = declaredSecrets[key];
14884
+ if (!entry) return void 0;
14885
+ if (!Array.isArray(entry.stages) || entry.stages.length === 0) return false;
14886
+ return true;
14887
+ }
14520
14888
  async function secretsPreflight(deps, opts) {
14521
14889
  const repo = await targetRepo(deps, opts);
14522
14890
  const qs = new URLSearchParams({ repo }).toString();
@@ -14537,9 +14905,23 @@ async function secretsPreflight(deps, opts) {
14537
14905
  }
14538
14906
  const { secrets } = await res.json();
14539
14907
  const present = new Set((secrets ?? []).map((s) => s.key));
14540
- const missing = opts.required.filter(
14541
- (key) => !present.has(stageKey2(opts.stage, key)) && !(key.includes("/") ? false : present.has(key))
14542
- );
14908
+ const missing = opts.required.filter((key) => {
14909
+ const perStage = keyDeclaredPerStage(opts.declaredSecrets, key);
14910
+ if (key.includes("/")) {
14911
+ return !present.has(key);
14912
+ }
14913
+ if (perStage === false) {
14914
+ return !present.has(key);
14915
+ }
14916
+ if (perStage === true) {
14917
+ return !present.has(stageKey2(opts.stage, key));
14918
+ }
14919
+ return !present.has(stageKey2(opts.stage, key)) && !present.has(key);
14920
+ });
14921
+ if (opts.json) {
14922
+ deps.log(JSON.stringify({ ok: missing.length === 0, stage: opts.stage, required: opts.required, missing, present: Array.from(present) }, null, 2));
14923
+ return missing.length === 0;
14924
+ }
14543
14925
  if (missing.length) {
14544
14926
  deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
14545
14927
  return false;
@@ -14764,9 +15146,6 @@ async function secretsImportRailsCredentials(deps, opts) {
14764
15146
  }
14765
15147
  return true;
14766
15148
  }
14767
- async function secretsEdit(deps, key, opts) {
14768
- return secretsSet(deps, key, opts);
14769
- }
14770
15149
  async function secretsRemove(deps, key, opts) {
14771
15150
  if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
14772
15151
  const repo = await targetRepo(deps, opts);
@@ -14862,8 +15241,12 @@ async function secretsCopy(deps, opts) {
14862
15241
  }
14863
15242
  return true;
14864
15243
  }
15244
+ function resolveSpawnTarget(command, args, platform2 = process.platform) {
15245
+ return platform2 === "win32" ? { cmd: "cmd.exe", args: ["/d", "/s", "/c", command, ...args] } : { cmd: command, args };
15246
+ }
14865
15247
  function defaultSpawn(command, args, env) {
14866
- const r = (0, import_node_child_process9.spawnSync)(command, args, { stdio: "inherit", env });
15248
+ const target = resolveSpawnTarget(command, args);
15249
+ const r = (0, import_node_child_process9.spawnSync)(target.cmd, target.args, { stdio: "inherit", env });
14867
15250
  if (r.error) throw r.error;
14868
15251
  return r.status ?? 1;
14869
15252
  }
@@ -14923,8 +15306,8 @@ async function secretsUse(deps, key, opts) {
14923
15306
  }
14924
15307
 
14925
15308
  // src/secrets-commands.ts
14926
- var import_node_fs14 = require("node:fs");
14927
- var import_node_path15 = require("node:path");
15309
+ var import_node_fs16 = require("node:fs");
15310
+ var import_node_path17 = require("node:path");
14928
15311
  var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
14929
15312
  var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
14930
15313
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
@@ -14932,18 +15315,18 @@ function collectMap(value, previous = []) {
14932
15315
  return [...previous, value];
14933
15316
  }
14934
15317
  async function decryptRailsCredentials(input) {
14935
- const appDir = (0, import_node_path15.resolve)(input.appDir ?? process.cwd());
15318
+ const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
14936
15319
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
14937
15320
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
14938
- const credentialsPath = (0, import_node_path15.resolve)(appDir, credentialsFile);
14939
- const masterKeyPath = (0, import_node_path15.resolve)(appDir, masterKeyFile);
15321
+ const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
15322
+ const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
14940
15323
  const env = {
14941
15324
  ...process.env,
14942
15325
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
14943
15326
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
14944
15327
  };
14945
- if ((0, import_node_fs14.existsSync)(masterKeyPath)) {
14946
- env.RAILS_MASTER_KEY = (0, import_node_fs14.readFileSync)(masterKeyPath, "utf8").trim();
15328
+ if ((0, import_node_fs16.existsSync)(masterKeyPath)) {
15329
+ env.RAILS_MASTER_KEY = (0, import_node_fs16.readFileSync)(masterKeyPath, "utf8").trim();
14947
15330
  }
14948
15331
  const script = [
14949
15332
  'require "json"',
@@ -14997,8 +15380,8 @@ async function withSecrets(run) {
14997
15380
  }
14998
15381
  function registerSecretsCommands(program3) {
14999
15382
  const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
15000
- 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)));
15001
- secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
15383
+ secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
15384
+ secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
15002
15385
  secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
15003
15386
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
15004
15387
  }));
@@ -15009,13 +15392,13 @@ function registerSecretsCommands(program3) {
15009
15392
  secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
15010
15393
  let body;
15011
15394
  try {
15012
- body = (0, import_node_fs14.readFileSync)((0, import_node_path15.resolve)(o.file), "utf8");
15395
+ body = (0, import_node_fs16.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
15013
15396
  } catch (e) {
15014
15397
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
15015
15398
  }
15016
15399
  if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
15017
15400
  }));
15018
- secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
15401
+ secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--json", "machine-readable output").action(async (o) => {
15019
15402
  if (!["dev", "rc", "main"].includes(o.stage)) {
15020
15403
  return fail("secrets preflight: --stage must be dev, rc, or main");
15021
15404
  }
@@ -15036,7 +15419,7 @@ function registerSecretsCommands(program3) {
15036
15419
  if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
15037
15420
  d.err('secrets preflight: requiredRuntimeSecrets is unset: treating as an empty contract (no required keys). Declare an explicit {"dev":[],"rc":[],"main":[]} in registry META to silence this warning.');
15038
15421
  }
15039
- const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
15422
+ const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required, json: o.json, declaredSecrets: meta?.secrets });
15040
15423
  if (!ok) process.exitCode = 1;
15041
15424
  });
15042
15425
  secrets.command("get <key>").description("print one secret value, RAW \u2014 MASTER-ONLY (WS6 #2184); non-master: `secrets use` to consume keyless or `secrets verify` to validate").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "vault slug override for org-infra namespaces (cloudflare, shared, \u2026) \u2014 master-gated").action((key, o) => withSecrets(async (d) => {
@@ -15051,14 +15434,12 @@ function registerSecretsCommands(program3) {
15051
15434
  const ok = await secretsVerify(d, key, o);
15052
15435
  if (!ok) process.exitCode = 1;
15053
15436
  }));
15054
- secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
15437
+ const setHandler = (key, o) => withSecrets(async (d) => {
15055
15438
  const ok = await secretsSet(d, key, o);
15056
15439
  if (!ok) process.exitCode = 1;
15057
- }));
15058
- 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) => {
15059
- const ok = await secretsEdit(d, key, o);
15060
- if (!ok) process.exitCode = 1;
15061
- }));
15440
+ });
15441
+ secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
15442
+ secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
15062
15443
  secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
15063
15444
  const stages = ["dev", "rc", "main"];
15064
15445
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
@@ -15084,7 +15465,7 @@ function registerSecretsCommands(program3) {
15084
15465
  {
15085
15466
  ...d,
15086
15467
  decryptRailsCredentials,
15087
- removeFile: (path2) => (0, import_node_fs14.unlinkSync)((0, import_node_path15.resolve)(o.appDir ?? process.cwd(), path2))
15468
+ removeFile: (path2) => (0, import_node_fs16.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
15088
15469
  },
15089
15470
  {
15090
15471
  repo: o.repo,
@@ -15173,10 +15554,10 @@ function registerEdgeCommands(program3) {
15173
15554
  }
15174
15555
 
15175
15556
  // src/doctor-run.ts
15176
- var import_node_fs18 = require("node:fs");
15557
+ var import_node_fs20 = require("node:fs");
15177
15558
  var import_node_child_process11 = require("node:child_process");
15178
15559
  var import_promises2 = require("node:fs/promises");
15179
- var import_node_path18 = require("node:path");
15560
+ var import_node_path20 = require("node:path");
15180
15561
  var import_node_os7 = require("node:os");
15181
15562
 
15182
15563
  // src/plugin-guard.ts
@@ -15199,13 +15580,14 @@ function buildGuardSessionStartLine(state, opts = {}) {
15199
15580
 
15200
15581
  // src/cursor-plugin-seed.ts
15201
15582
  var import_node_child_process10 = require("node:child_process");
15202
- var import_node_fs15 = require("node:fs");
15583
+ var import_node_fs17 = require("node:fs");
15203
15584
  var import_node_os6 = require("node:os");
15204
- var import_node_path16 = require("node:path");
15585
+ var import_node_path18 = require("node:path");
15205
15586
  var import_node_util7 = require("node:util");
15206
15587
 
15207
15588
  // src/doctor.ts
15208
15589
  var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
15590
+ var GITHUB_AUTH_LABEL = "GitHub auth identity (gh ops)";
15209
15591
  var AWS_CROSS_ACCOUNT_LABEL = "AWS cross-account identity (master-agent audits)";
15210
15592
  var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
15211
15593
  var MMI_AGENTIC_ONBOARDING_GUIDE = {
@@ -15219,7 +15601,7 @@ function buildGithubAuthCheck(input) {
15219
15601
  const ok = Boolean(input.login?.trim());
15220
15602
  return {
15221
15603
  ok,
15222
- label: "GitHub auth identity (gh ops)",
15604
+ label: GITHUB_AUTH_LABEL,
15223
15605
  fix: input.ghInstalled ? GH_PROJECT_LOGIN_FIX : `install GitHub CLI (https://cli.github.com), then: ${GH_PROJECT_LOGIN_FIX.replace(/^run: /, "")}`
15224
15606
  };
15225
15607
  }
@@ -15397,6 +15779,136 @@ function buildRepoLocalWorktreeCheck(input) {
15397
15779
  fix: REPO_LOCAL_WORKTREE_FIX
15398
15780
  };
15399
15781
  }
15782
+ var LOCKFILE_OPTIONALS_LABEL = "npm lockfile keeps optionalDependencies across platforms (Windows lockfile drop #2594)";
15783
+ var LOCKFILE_OPTIONALS_FIX = "regenerate the lockfile without platform filtering: `rm package-lock.json && npm install` on a machine that resolves every optionalDependency, then commit it (a Windows-only lockfile drops Linux-only optionals on CI)";
15784
+ function buildLockfileOptionalsCheck(input) {
15785
+ const base = {
15786
+ ok: true,
15787
+ label: LOCKFILE_OPTIONALS_LABEL,
15788
+ fix: LOCKFILE_OPTIONALS_FIX,
15789
+ missingOptionals: []
15790
+ };
15791
+ if (!input.isOrgRepo) return base;
15792
+ const optionalDeps = input.packageJsonOptionalDeps ?? {};
15793
+ const packages = input.lockfilePackages ?? {};
15794
+ const missingOptionals = Object.keys(optionalDeps).filter((name) => !(`node_modules/${name}` in packages));
15795
+ if (missingOptionals.length === 0) return base;
15796
+ return {
15797
+ ...base,
15798
+ ok: false,
15799
+ missingOptionals,
15800
+ detail: `${missingOptionals.length} optionalDependency entr${missingOptionals.length === 1 ? "y" : "ies"} missing from the lockfile packages map: ${missingOptionals.join(", ")}`
15801
+ };
15802
+ }
15803
+ var FILE_ENCODING_LABEL = "managed file encoding (UTF-8, no BOM, no mojibake)";
15804
+ var FILE_ENCODING_FIX = 're-save the flagged file(s) as UTF-8 without BOM (VS Code: status-bar encoding button \u2192 "Save with Encoding" \u2192 UTF-8), and set `PYTHONUTF8=1` for Python tooling on Windows so it never falls back to cp1252';
15805
+ function inspectFileEncoding(bytes) {
15806
+ const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
15807
+ let invalidUtf8 = false;
15808
+ let replacementChars = false;
15809
+ try {
15810
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
15811
+ replacementChars = text.includes("\uFFFD");
15812
+ } catch {
15813
+ invalidUtf8 = true;
15814
+ replacementChars = new TextDecoder("utf-8", { fatal: false }).decode(bytes).includes("\uFFFD");
15815
+ }
15816
+ return { bom, invalidUtf8, replacementChars };
15817
+ }
15818
+ function buildManagedFileEncodingCheck(input) {
15819
+ const base = {
15820
+ ok: true,
15821
+ label: FILE_ENCODING_LABEL,
15822
+ fix: FILE_ENCODING_FIX,
15823
+ findings: []
15824
+ };
15825
+ if (!input.isOrgRepo) return base;
15826
+ const defective = input.findings.filter((f) => f.bom || f.invalidUtf8 || f.replacementChars);
15827
+ if (defective.length === 0) return base;
15828
+ return {
15829
+ ...base,
15830
+ ok: false,
15831
+ severityOverride: "advisory",
15832
+ findings: defective,
15833
+ detail: `${defective.length} managed file(s) not clean UTF-8: ${defective.map((f) => f.path).join(", ")}`
15834
+ };
15835
+ }
15836
+ var LOCAL_BEHIND_ORIGIN_LABEL = "local release branches current with origin (development/main/rc)";
15837
+ var LOCAL_BEHIND_ORIGIN_FIX = "run `git fetch origin && git switch <branch> && git pull --ff-only` for each flagged branch (purely behind, so the fast-forward is safe)";
15838
+ var RELEASE_TRACKING_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master", "rc", "release"]);
15839
+ function isReleaseTrackingBranch(name) {
15840
+ if (!name) return false;
15841
+ const base = name.split("/").pop() ?? name;
15842
+ if (RELEASE_TRACKING_BRANCHES.has(base)) return true;
15843
+ return name.split("/")[0] === "release";
15844
+ }
15845
+ function buildStaleLocalBranchCheck(input) {
15846
+ const base = {
15847
+ ok: true,
15848
+ label: LOCAL_BEHIND_ORIGIN_LABEL,
15849
+ fix: LOCAL_BEHIND_ORIGIN_FIX,
15850
+ stale: []
15851
+ };
15852
+ if (!input.isOrgRepo) return base;
15853
+ const stale = input.branches.filter((b) => isReleaseTrackingBranch(b.name) && b.behind > 0 && b.ahead === 0).map((b) => ({ name: b.name, behind: b.behind }));
15854
+ if (stale.length === 0) return base;
15855
+ return {
15856
+ ...base,
15857
+ ok: false,
15858
+ severityOverride: "advisory",
15859
+ stale,
15860
+ detail: `${stale.length} release branch(es) behind origin: ${stale.map((s) => `${s.name} (-${s.behind})`).join(", ")}`
15861
+ };
15862
+ }
15863
+ var REDACTOR_LIVENESS_LABEL = "secret-redact hook liveness (PostToolUse redactor crashes)";
15864
+ var REDACTOR_LIVENESS_FIX = 'grep the hook activity log for \'"script":"secret-redact","outcome":"failed"\' (path: `git rev-parse --git-path mmi-runtime/hooks/activity.jsonl`); reinstall the MMI plugin or run `mmi-cli doctor --apply` to repair, then reopen the session \u2014 the redactor stayed fail-open so tool calls proceeded, but secrets may have passed through unredacted while it was down';
15865
+ function summarizeRedactorFailures(lines) {
15866
+ const parsed = [];
15867
+ for (const raw of lines) {
15868
+ const trimmed = raw.trim();
15869
+ if (!trimmed) continue;
15870
+ try {
15871
+ parsed.push(JSON.parse(trimmed));
15872
+ } catch {
15873
+ }
15874
+ }
15875
+ let sessionStartIdx = -1;
15876
+ for (let i = 0; i < parsed.length; i += 1) {
15877
+ if (parsed[i].event === "SessionStart") sessionStartIdx = i;
15878
+ }
15879
+ const start = sessionStartIdx >= 0 ? sessionStartIdx : 0;
15880
+ let failedCount = 0;
15881
+ let lastFailedTs;
15882
+ let lastFailedAction;
15883
+ for (let i = start; i < parsed.length; i += 1) {
15884
+ const e = parsed[i];
15885
+ if (e.script === "secret-redact" && e.outcome === "failed") {
15886
+ failedCount += 1;
15887
+ lastFailedTs = e.ts;
15888
+ lastFailedAction = e.action;
15889
+ }
15890
+ }
15891
+ return { failedCount, lastFailedTs, lastFailedAction };
15892
+ }
15893
+ function buildRedactorLivenessCheck(input) {
15894
+ const base = {
15895
+ ok: true,
15896
+ label: REDACTOR_LIVENESS_LABEL,
15897
+ fix: REDACTOR_LIVENESS_FIX,
15898
+ failedCount: 0
15899
+ };
15900
+ if (!input.isOrgRepo) return base;
15901
+ if (input.failedCount === 0) return base;
15902
+ const when = input.lastFailedTs ? ` (most recent ${input.lastFailedTs})` : "";
15903
+ const reason = input.lastFailedAction ? `: ${input.lastFailedAction}` : "";
15904
+ return {
15905
+ ok: false,
15906
+ label: REDACTOR_LIVENESS_LABEL,
15907
+ fix: `secret-redact crashed ${input.failedCount} time(s) this session${when}${reason}. ${REDACTOR_LIVENESS_FIX}`,
15908
+ failedCount: input.failedCount,
15909
+ lastFailedTs: input.lastFailedTs
15910
+ };
15911
+ }
15400
15912
  var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
15401
15913
  var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
15402
15914
  function buildScratchGcCheck(plan) {
@@ -15855,7 +16367,16 @@ function buildDoctorJsonPayload(input) {
15855
16367
  return {
15856
16368
  ok: doctorExitCode(input.checks) === 0,
15857
16369
  hasAdvisories: input.checks.some((c) => !c.ok && isAdvisoryDoctorCheck(c)),
15858
- checks: input.checks,
16370
+ checks: input.checks.map((check) => {
16371
+ const metadata = doctorMetadataForCheck(check);
16372
+ return {
16373
+ ...check,
16374
+ ...metadata,
16375
+ label: check.label,
16376
+ metadata,
16377
+ durationMs: check.durationMs ?? 0
16378
+ };
16379
+ }),
15859
16380
  updateReport: input.updateReport,
15860
16381
  ...input.resources.length ? { resources: input.resources } : {}
15861
16382
  };
@@ -15914,6 +16435,25 @@ function buildInstalledPluginVersionCheck(input) {
15914
16435
  staleSurfaces: stale
15915
16436
  };
15916
16437
  }
16438
+ var CLI_VERSION_DRIFT_LABEL = "mmi-cli version drift (installed vs latest published)";
16439
+ var CLI_VERSION_DRIFT_FIX = "run: mmi-cli doctor --apply --no-repo-writes (or npm install -g @mutmutco/cli@latest)";
16440
+ function buildCliVersionDriftCheck(input) {
16441
+ const base = {
16442
+ ok: true,
16443
+ label: CLI_VERSION_DRIFT_LABEL,
16444
+ fix: CLI_VERSION_DRIFT_FIX,
16445
+ currentVersion: input.currentVersion,
16446
+ releasedVersion: input.releasedVersion
16447
+ };
16448
+ if (!input.releasedVersion || !isSemverVersion(input.releasedVersion) || !isSemverVersion(input.currentVersion)) return base;
16449
+ if (compareVersions(input.currentVersion, input.releasedVersion) >= 0) return base;
16450
+ return {
16451
+ ...base,
16452
+ ok: false,
16453
+ label: `${CLI_VERSION_DRIFT_LABEL}: ${input.currentVersion} < ${input.releasedVersion}`,
16454
+ fix: `${CLI_VERSION_DRIFT_FIX} (installed ${input.currentVersion} is behind published ${input.releasedVersion})`
16455
+ };
16456
+ }
15917
16457
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
15918
16458
  function buildOpencodeVersionCheck(input) {
15919
16459
  const fix = pluginRecoveryFix("opencode");
@@ -16211,13 +16751,13 @@ function buildCursorHookCliCheck(input) {
16211
16751
  return base;
16212
16752
  }
16213
16753
  var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
16754
+ var HUB_COMPAT_LABEL = "Hub compatibility (client version vs Hub minimum)";
16214
16755
  function buildHubCompatCheck(input) {
16215
- const label = "Hub compatibility (client version vs Hub minimum)";
16216
16756
  const min = input.versionInfo?.minClientVersion;
16217
16757
  if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
16218
- return { ok: true, label, fix: HUB_COMPAT_FIX };
16758
+ return { ok: true, label: HUB_COMPAT_LABEL, fix: HUB_COMPAT_FIX };
16219
16759
  }
16220
- return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16760
+ return { ok: versionAtLeast(input.installedVersion, min), label: `${HUB_COMPAT_LABEL}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
16221
16761
  }
16222
16762
  var HUB_DEPLOY_FRESHNESS_LABEL = "Hub deploy freshness (x-hub-version vs CLI / release)";
16223
16763
  var HUB_DEPLOY_FRESHNESS_FIX = "prod Hub updates on /release (MMI-Hub is direct-track); lag vs development is expected between releases";
@@ -16252,8 +16792,6 @@ function buildHubDeployFreshnessCheck(input) {
16252
16792
  staleAgainst
16253
16793
  };
16254
16794
  }
16255
- var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
16256
- var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
16257
16795
  function textHasPlaywrightMcp(content) {
16258
16796
  const normalized = content.replace(/\r\n/g, "\n");
16259
16797
  return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
@@ -16311,7 +16849,7 @@ function buildSelfUpdateHaltPayload(input) {
16311
16849
  checks: input.checks
16312
16850
  };
16313
16851
  }
16314
- var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
16852
+ var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 2;
16315
16853
  var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
16316
16854
  function selfUpdateHaltSentinelLine(input) {
16317
16855
  const to = input.report.releasedVersion ?? "latest";
@@ -16321,8 +16859,11 @@ var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
16321
16859
  function buildSelfUpdateReexecArgs(opts) {
16322
16860
  const args = ["doctor"];
16323
16861
  if (opts.banner) args.push("--banner");
16862
+ if (opts.fast) args.push("--fast");
16324
16863
  if (opts.preflight) args.push("--preflight");
16325
16864
  if (opts.verbose) args.push("--verbose");
16865
+ if (opts.ci) args.push("--ci");
16866
+ if (opts.strictAdvisory) args.push("--strict-advisory");
16326
16867
  if (opts.guide) args.push("--guide");
16327
16868
  if (opts.json) args.push("--json");
16328
16869
  if (opts.apply) args.push("--apply");
@@ -16343,28 +16884,12 @@ function preflightOutcome(input) {
16343
16884
  function pluginAutonomousHaltLine(reloadHint) {
16344
16885
  return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
16345
16886
  }
16346
- var DOCTOR_EXTENDED_CHECK_LABELS = /* @__PURE__ */ new Set([
16347
- AWS_CROSS_ACCOUNT_LABEL,
16348
- HUB_DEPLOY_FRESHNESS_LABEL,
16349
- PLAYWRIGHT_MCP_VISION_CAP_LABEL,
16350
- PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
16351
- BROWSER_ARTIFACTS_LABEL,
16352
- SCRATCH_GC_LABEL,
16353
- GIT_GC_LABEL,
16354
- OPENCODE_VERSION_LABEL,
16355
- OPENCODE_DESKTOP_BOOTSTRAP_LABEL
16356
- ]);
16357
- function isDoctorExtendedCheck(label) {
16358
- if (DOCTOR_EXTENDED_CHECK_LABELS.has(label)) return true;
16359
- if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return true;
16360
- if (label.startsWith("@mutmutco design-system") || label.startsWith("@mutmutco registry components")) return true;
16361
- return false;
16362
- }
16363
16887
  function isAdvisoryDoctorCheck(check) {
16364
16888
  if (check.severityOverride) return check.severityOverride === "advisory";
16365
- return isDoctorExtendedCheck(check.label);
16889
+ return (check.metadata ?? doctorMetadataForLabel(check.label)).severity === "advisory";
16366
16890
  }
16367
- function doctorExitCode(checks) {
16891
+ function doctorExitCode(checks, opts = {}) {
16892
+ if (opts.doctorError) return 2;
16368
16893
  return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
16369
16894
  }
16370
16895
  function renderPluginUpdateReportStaleOnly(report, allChecksPass = false) {
@@ -16390,6 +16915,7 @@ function doctorVerdictLine(input) {
16390
16915
  const gaps = input.checks.filter((c) => !c.ok);
16391
16916
  const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
16392
16917
  const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
16918
+ const skipSuffix = input.skippedSlowChecks ? ` \xB7 skipped ${input.skippedSlowChecks} slow checks` : "";
16393
16919
  if (input.shouldApply) {
16394
16920
  if (hardGaps.length > 0) {
16395
16921
  return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
@@ -16403,19 +16929,30 @@ function doctorVerdictLine(input) {
16403
16929
  }
16404
16930
  const attention = gaps.length;
16405
16931
  if (attention > 0) {
16406
- return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
16932
+ return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal${skipSuffix}.`;
16407
16933
  }
16408
- return "\u2713 Everything healthy.";
16934
+ return `\u2713 healthy \xB7 ${input.checks.length} checks \xB7 0 gaps${skipSuffix}`;
16935
+ }
16936
+ function doctorCheckSummaryLine(check) {
16937
+ const meta = doctorMetadataForCheck(check);
16938
+ return ` ${doctorCheckGlyph(check)} ${meta.id} \u2014 ${meta.protects.join("; ")} \u2014 ${check.fix}`;
16409
16939
  }
16410
16940
  function doctorHumanLines(input) {
16411
16941
  const gaps = input.checks.filter((c) => !c.ok);
16942
+ const healthyCount = input.checks.length - gaps.length;
16412
16943
  const lines = [
16413
- doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount }),
16414
- "",
16415
- "Checks",
16416
- ...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${c.label}`)
16944
+ doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount, skippedSlowChecks: input.skippedSlowChecks })
16417
16945
  ];
16418
- if (gaps.length > 0) {
16946
+ if (gaps.length > 0 || input.verbose) {
16947
+ lines.push("", "Checks");
16948
+ if (input.verbose) {
16949
+ lines.push(...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${doctorMetadataForCheck(c).id} \u2014 ${c.label}`));
16950
+ } else {
16951
+ lines.push(...gaps.map(doctorCheckSummaryLine));
16952
+ if (healthyCount > 0) lines.push(` \u2713 ${healthyCount} healthy check${healthyCount === 1 ? "" : "s"} collapsed`);
16953
+ }
16954
+ }
16955
+ if (gaps.length > 0 && input.verbose) {
16419
16956
  lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
16420
16957
  for (const c of gaps) {
16421
16958
  const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
@@ -16425,10 +16962,10 @@ function doctorHumanLines(input) {
16425
16962
  if (input.shouldApply && input.pluginReloadRequired) {
16426
16963
  lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
16427
16964
  }
16428
- const stale = renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0);
16965
+ const stale = input.verbose || gaps.length > 0 ? renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0) : [];
16429
16966
  if (stale.length) {
16430
16967
  lines.push("", ...stale);
16431
- } else if (!input.verbose) {
16968
+ } else if (!input.verbose && gaps.length > 0) {
16432
16969
  lines.push("", DOCTOR_VERBOSE_HINT);
16433
16970
  }
16434
16971
  if (!input.verbose) return lines;
@@ -16476,6 +17013,100 @@ function doctorAuditLines(input) {
16476
17013
  return lines;
16477
17014
  }
16478
17015
  var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
17016
+ var MMI_CLI_ON_PATH_LABEL = "mmi-cli on PATH";
17017
+ var PLUGIN_CLONE_LABEL = "plugin git clone (SSH\u2192HTTPS rewrite)";
17018
+ var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17019
+ var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17020
+ var DOCTOR_CHECK_METADATA = [
17021
+ { id: "github.auth", category: "auth", severity: "hard", cost: "fast", heal: "none", label: GITHUB_AUTH_LABEL, protects: ["GitHub issue, PR, project, and registry operations use a resolved viewer identity"] },
17022
+ { id: "cli.path", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: MMI_CLI_ON_PATH_LABEL, protects: ["Session hooks and surface commands can invoke mmi-cli"] },
17023
+ { id: "hub.compat", category: "hub", severity: "hard", cost: "fast", heal: "auto", label: HUB_COMPAT_LABEL, protects: ["Client stays above the Hub minimum version floor"] },
17024
+ { id: "hub.deploy-freshness", category: "hub", severity: "advisory", cost: "slow", heal: "none", label: HUB_DEPLOY_FRESHNESS_LABEL, protects: ["Operators can see prod Hub lag against release and local CLI versions"] },
17025
+ { id: "cli.version-drift", category: "repo", severity: "advisory", cost: "slow", heal: "none", label: CLI_VERSION_DRIFT_LABEL, protects: ["The installed mmi-cli is not behind the npm-published version (stale installs miss commands)"] },
17026
+ { id: "aws.cross-account", category: "aws", severity: "advisory", cost: "slow", heal: "none", label: AWS_CROSS_ACCOUNT_LABEL, protects: ["Master-agent AWS audits do not start from root credentials"] },
17027
+ { id: "plugin.clone-rewrite", category: "plugin", severity: "hard", cost: "fast", heal: "none", label: PLUGIN_CLONE_LABEL, protects: ["Plugin marketplace clones can use the HTTPS rewrite expected on this machine"] },
17028
+ { id: "plugin.install-record", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_LABEL, protects: ["Claude/Codex plugin loader sees mmi@mutmutco installed for this project"] },
17029
+ { id: "plugin.legacy-id", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: LEGACY_PLUGIN_ID_LABEL, protects: ["Legacy mmi@mmi rows cannot mask the canonical mmi@mutmutco install"] },
17030
+ { id: "plugin.config-drift", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_DRIFT_LABEL, protects: ["Duplicate or stale plugin rows collapse to one current user-scope row"] },
17031
+ { id: "plugin.resolvability", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: PLUGIN_RESOLVABILITY_LABEL, protects: ["Marketplace clone and plugin cache both exist for the active surface"] },
17032
+ { id: "plugin.installed-version", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: INSTALLED_PLUGIN_VERSION_LABEL, protects: ["Claude/Codex installed plugin records reach the latest release"] },
17033
+ { id: "plugin.cache-cleanup", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: MMI_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Stale Claude/Codex cache directories do not shadow current installs"] },
17034
+ { id: "plugin.nested-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: NESTED_PLUGIN_TREE_LABEL, protects: ["Self-nested plugin cache trees do not exceed host path limits"] },
17035
+ { id: "codex.active-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: CODEX_ACTIVE_CACHE_LABEL, protects: ["Codex actually loads the released plugin cache"] },
17036
+ { id: "session.skill-root", category: "plugin", severity: "advisory", cost: "fast", heal: "none", label: SESSION_SKILL_ROOT_LABEL, protects: ["Running sessions know when a restart is needed to load current skills"] },
17037
+ { id: "repo.gitignore-block", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: GITIGNORE_BLOCK_LABEL, protects: ["Org-managed ignore entries keep generated and scratch files out of git"] },
17038
+ { id: "repo.worktree-location", category: "repo", severity: "hard", cost: "fast", heal: "none", label: REPO_LOCAL_WORKTREE_LABEL, protects: ["Worktrees stay in the canonical sibling root"] },
17039
+ { id: "hooks.redactor-liveness", category: "hooks", severity: "advisory", cost: "fast", heal: "none", label: REDACTOR_LIVENESS_LABEL, protects: ["A crashed secret-redactor cannot silently pass secrets into model context forever"] },
17040
+ { id: "repo.scratch-gc", category: "repo", severity: "advisory", cost: "slow", heal: "auto", label: SCRATCH_GC_LABEL, protects: ["Scratch, plan, and browser artifact clutter stays bounded"] },
17041
+ { id: "repo.git-gc", category: "git", severity: "advisory", cost: "slow", heal: "auto", label: GIT_GC_LABEL, protects: ["Merged branches, stale refs, and dead worktree metadata stay bounded"] },
17042
+ { id: "browser.artifacts", category: "browser", severity: "advisory", cost: "slow", heal: "none", label: BROWSER_ARTIFACTS_LABEL, protects: ["Playwright/browser output stays under tmp/playwright-mcp"] },
17043
+ { id: "mcp.playwright-registration", category: "browser", severity: "hard", cost: "fast", heal: "auto", label: MCP_RECONCILE_LABEL, protects: ["Org-managed Playwright MCP registrations exist and stay drift-free"] },
17044
+ { id: "opencode.version", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_VERSION_LABEL, protects: ["OpenCode loads the released MMI adapter"] },
17045
+ { id: "opencode.hook-active", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_HOOK_ACTIVE_LABEL, protects: ["OpenCode shell hooks stamp the active MMI adapter surface"] },
17046
+ { id: "opencode.config-plugin", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_CONFIG_PLUGIN_LABEL, protects: ["OpenCode config includes the MMI npm adapter"] },
17047
+ { id: "opencode.surface-assets", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_SURFACE_ASSETS_LABEL, protects: ["OpenCode commands and skills are materialized"] },
17048
+ { id: "opencode.desktop-bootstrap", category: "opencode", severity: "advisory", cost: "slow", heal: "none", label: OPENCODE_DESKTOP_BOOTSTRAP_LABEL, protects: ["OpenCode Desktop is not pinned to a deleted project bootstrap"] },
17049
+ { id: "opencode.legacy-config", category: "opencode", severity: "hard", cost: "fast", heal: "none", label: OPENCODE_LEGACY_CONFIG_LABEL, protects: ["Legacy OpenCode config cannot load stale bare plugin ids"] },
17050
+ { id: "cursor.install", category: "cursor", severity: "hard", cost: "fast", heal: "auto", label: CURSOR_PLUGIN_INSTALL_LABEL, protects: ["Cursor Team Marketplace cache exists and is complete"] },
17051
+ { id: "cursor.cache-cleanup", category: "cursor", severity: "advisory", cost: "fast", heal: "none", label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Old Cursor cache pins are visible without unsafe automatic deletion"] },
17052
+ { id: "cursor.enabled-pin", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_ENABLED_PIN_LABEL, protects: ["Cursor Team Marketplace enabled pin matches the healthy cache"] },
17053
+ { id: "cursor.third-party-extensibility", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_THIRD_PARTY_EXTENSIBILITY_LABEL, protects: ["Cursor does not import third-party hooks that break the Team Marketplace plugin"] },
17054
+ { id: "cursor.hook-cli", category: "cursor", severity: "hard", cost: "fast", heal: "surface", label: CURSOR_HOOK_CLI_LABEL, protects: ["Cursor hooks can reach a bundled or PATH mmi-cli"] },
17055
+ // Incident coverage (#2594) — three real incident classes that previously had zero doctor coverage.
17056
+ { id: "repo-hygiene.lockfile-platform-optionals", category: "repo-hygiene", severity: "hard", cost: "fast", heal: "surface", label: LOCKFILE_OPTIONALS_LABEL, protects: ["A Windows-generated lockfile does not silently drop Linux-only optionalDependencies before CI"] },
17057
+ { id: "repo-hygiene.file-encoding", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: FILE_ENCODING_LABEL, protects: ["Managed source and config files stay clean UTF-8 without BOM or mojibake"] },
17058
+ { id: "repo-hygiene.local-behind-origin", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: LOCAL_BEHIND_ORIGIN_LABEL, protects: ["Local release branches (development/main/rc) do not fall behind origin after a release"] }
17059
+ ];
17060
+ var DOCTOR_CHECK_METADATA_BY_LABEL = Object.fromEntries(
17061
+ DOCTOR_CHECK_METADATA.map((meta) => [meta.label, meta])
17062
+ );
17063
+ var DOCTOR_EXTENDED_CHECK_LABELS = new Set(
17064
+ DOCTOR_CHECK_METADATA.filter((meta) => meta.cost === "slow").map((meta) => meta.label)
17065
+ );
17066
+ function derivedDoctorCheckMetadata(label) {
17067
+ if (label.startsWith(HUB_COMPAT_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_COMPAT_LABEL], label };
17068
+ if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_DEPLOY_FRESHNESS_LABEL], label };
17069
+ if (label.startsWith("@mutmutco design-system")) {
17070
+ return {
17071
+ id: "registry.design-system-components",
17072
+ category: "hub",
17073
+ severity: "advisory",
17074
+ cost: "slow",
17075
+ heal: "none",
17076
+ label,
17077
+ protects: ["Design-system component cache can be compared against the live registry"]
17078
+ };
17079
+ }
17080
+ if (label.startsWith("@mutmutco registry components")) {
17081
+ return {
17082
+ id: "registry.components",
17083
+ category: "hub",
17084
+ severity: "advisory",
17085
+ cost: "slow",
17086
+ heal: "none",
17087
+ label,
17088
+ protects: ["Repo component cache can be compared against the live registry"]
17089
+ };
17090
+ }
17091
+ return void 0;
17092
+ }
17093
+ function fallbackDoctorCheckMetadata(label) {
17094
+ return {
17095
+ id: `legacy.${label.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/(^\.|\.$)/g, "") || "unknown"}`,
17096
+ category: "repo",
17097
+ severity: "hard",
17098
+ cost: "fast",
17099
+ heal: "none",
17100
+ label,
17101
+ protects: ["Legacy check without catalog metadata"]
17102
+ };
17103
+ }
17104
+ function doctorMetadataForLabel(label) {
17105
+ return DOCTOR_CHECK_METADATA_BY_LABEL[label] ?? derivedDoctorCheckMetadata(label) ?? fallbackDoctorCheckMetadata(label);
17106
+ }
17107
+ function doctorMetadataForCheck(check) {
17108
+ return check.metadata ?? doctorMetadataForLabel(check.label);
17109
+ }
16479
17110
  function buildPluginResolvabilityCheck(input) {
16480
17111
  const { state } = buildPluginGuardDecision(input);
16481
17112
  const surface = input.surface ?? "shell";
@@ -16521,17 +17152,17 @@ function ghReleaseTarballApiArgs(tag) {
16521
17152
  }
16522
17153
  function cursorUserGlobalStatePath() {
16523
17154
  if (process.platform === "win32") {
16524
- const base = process.env.APPDATA || (0, import_node_path16.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
16525
- return (0, import_node_path16.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
17155
+ const base = process.env.APPDATA || (0, import_node_path18.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
17156
+ return (0, import_node_path18.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
16526
17157
  }
16527
17158
  if (process.platform === "darwin") {
16528
- return (0, import_node_path16.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17159
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
16529
17160
  }
16530
- return (0, import_node_path16.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17161
+ return (0, import_node_path18.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
16531
17162
  }
16532
17163
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16533
17164
  const dbPath = cursorUserGlobalStatePath();
16534
- if (!(0, import_node_fs15.existsSync)(dbPath)) return void 0;
17165
+ if (!(0, import_node_fs17.existsSync)(dbPath)) return void 0;
16535
17166
  try {
16536
17167
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
16537
17168
  timeout: 5e3
@@ -16545,57 +17176,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
16545
17176
  }
16546
17177
  }
16547
17178
  function syncDirContents(src, dest) {
16548
- (0, import_node_fs15.mkdirSync)(dest, { recursive: true });
16549
- for (const name of (0, import_node_fs15.readdirSync)(dest)) {
16550
- (0, import_node_fs15.rmSync)((0, import_node_path16.join)(dest, name), { recursive: true, force: true });
17179
+ (0, import_node_fs17.mkdirSync)(dest, { recursive: true });
17180
+ for (const name of (0, import_node_fs17.readdirSync)(dest)) {
17181
+ (0, import_node_fs17.rmSync)((0, import_node_path18.join)(dest, name), { recursive: true, force: true });
16551
17182
  }
16552
- (0, import_node_fs15.cpSync)(src, dest, { recursive: true });
17183
+ (0, import_node_fs17.cpSync)(src, dest, { recursive: true });
16553
17184
  }
16554
17185
  function releaseTag(releasedVersion) {
16555
17186
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
16556
17187
  }
16557
17188
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
16558
- const tarFile = (0, import_node_path16.join)(tmpRoot, "archive.tar");
17189
+ const tarFile = (0, import_node_path18.join)(tmpRoot, "archive.tar");
16559
17190
  try {
16560
17191
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
16561
17192
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
16562
17193
  timeout: 6e4
16563
17194
  });
16564
17195
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
16565
- const pluginMmi = (0, import_node_path16.join)(tmpRoot, "plugins", "mmi");
16566
- return (0, import_node_fs15.existsSync)((0, import_node_path16.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17196
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, "plugins", "mmi");
17197
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16567
17198
  } catch {
16568
17199
  return void 0;
16569
17200
  }
16570
17201
  }
16571
17202
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
16572
- const tarPath = (0, import_node_path16.join)(tmpRoot, "repo.tgz");
17203
+ const tarPath = (0, import_node_path18.join)(tmpRoot, "repo.tgz");
16573
17204
  try {
16574
- (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
17205
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16575
17206
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
16576
17207
  timeout: 12e4,
16577
17208
  maxBuffer: 100 * 1024 * 1024,
16578
17209
  encoding: "buffer",
16579
17210
  windowsHide: true
16580
17211
  });
16581
- (0, import_node_fs15.writeFileSync)(tarPath, stdout);
17212
+ (0, import_node_fs17.writeFileSync)(tarPath, stdout);
16582
17213
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
16583
- const top = (0, import_node_fs15.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
17214
+ const top = (0, import_node_fs17.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
16584
17215
  if (!top) return void 0;
16585
- const pluginMmi = (0, import_node_path16.join)(tmpRoot, top, "plugins", "mmi");
16586
- return (0, import_node_fs15.existsSync)((0, import_node_path16.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17216
+ const pluginMmi = (0, import_node_path18.join)(tmpRoot, top, "plugins", "mmi");
17217
+ return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
16587
17218
  } catch {
16588
17219
  return void 0;
16589
17220
  }
16590
17221
  }
16591
17222
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
16592
- (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
17223
+ (0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
16593
17224
  const tag = releaseTag(releasedVersion);
16594
17225
  if (hubCheckout) {
16595
17226
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
16596
17227
  if (fromHub) return fromHub;
16597
17228
  }
16598
- return downloadPluginMmiViaGh(tag, (0, import_node_path16.join)(tmpRoot, "gh"));
17229
+ return downloadPluginMmiViaGh(tag, (0, import_node_path18.join)(tmpRoot, "gh"));
16599
17230
  }
16600
17231
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
16601
17232
  if (!isSemverVersion2(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -16609,7 +17240,7 @@ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRoo
16609
17240
  const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
16610
17241
  if (needing.length > 0) return needing.map((pin) => pin.path);
16611
17242
  if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion2(releasedVersion)) {
16612
- return [(0, import_node_path16.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
17243
+ return [(0, import_node_path18.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
16613
17244
  }
16614
17245
  return [];
16615
17246
  }
@@ -16624,10 +17255,10 @@ async function applyCursorPluginCacheSeed(input) {
16624
17255
  for (const dest of targets) {
16625
17256
  syncDirContents(source, dest);
16626
17257
  }
16627
- (0, import_node_fs15.rmSync)(tmpRoot, { recursive: true, force: true });
17258
+ (0, import_node_fs17.rmSync)(tmpRoot, { recursive: true, force: true });
16628
17259
  return true;
16629
17260
  }
16630
- var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
17261
+ var CURSOR_ENABLED_PIN_LABEL2 = "Cursor Team Marketplace enabled plugin pin";
16631
17262
  function cursorSplitBrainCapabilities() {
16632
17263
  const skills = OPENCODE_WORKFLOW_COMMANDS.map((c) => `/${c}`).join(", ");
16633
17264
  return `org skills (${skills}) and SessionStart hooks`;
@@ -16647,7 +17278,7 @@ function parseCursorPluginLog(text) {
16647
17278
  return { enabledPin, loadSucceeded, loadFailed };
16648
17279
  }
16649
17280
  function buildCursorEnabledPinCheck(input) {
16650
- const label = CURSOR_ENABLED_PIN_LABEL;
17281
+ const label = CURSOR_ENABLED_PIN_LABEL2;
16651
17282
  const healthy = { ok: true, label, fix: "" };
16652
17283
  if (!input.isOrgRepo) return healthy;
16653
17284
  if (!input.enabledPin && !input.loadFailed) return healthy;
@@ -16682,7 +17313,7 @@ var PLAYWRIGHT_MCP_SPEC = {
16682
17313
  args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
16683
17314
  legacyNames: []
16684
17315
  };
16685
- var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
17316
+ var MCP_RECONCILE_LABEL2 = "MCP server registrations (org-managed Playwright)";
16686
17317
  var MCP_RECONCILE_FIX = "register/reconcile the org Playwright MCP server (npx -y @playwright/mcp@latest --output-dir tmp/playwright-mcp) \u2014 run `mmi-cli doctor --apply`; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
16687
17318
  function extractServerBlock(content, format, spec) {
16688
17319
  return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
@@ -16796,7 +17427,7 @@ function planClaudeCliMcpHeal(item, spec) {
16796
17427
  function buildMcpReconcileCheck(plan) {
16797
17428
  const base = {
16798
17429
  ok: true,
16799
- label: MCP_RECONCILE_LABEL,
17430
+ label: MCP_RECONCILE_LABEL2,
16800
17431
  fix: MCP_RECONCILE_FIX,
16801
17432
  severityOverride: "advisory"
16802
17433
  };
@@ -16983,9 +17614,9 @@ ${block}
16983
17614
  }
16984
17615
 
16985
17616
  // src/cli-doctor-shared.ts
16986
- var import_node_fs16 = require("node:fs");
16987
- var import_node_path17 = require("node:path");
16988
- var import_node_fs17 = require("node:fs");
17617
+ var import_node_fs18 = require("node:fs");
17618
+ var import_node_path19 = require("node:path");
17619
+ var import_node_fs19 = require("node:fs");
16989
17620
  var GC_GH_TIMEOUT_MS = 2e4;
16990
17621
  async function awsCallerArn() {
16991
17622
  try {
@@ -17031,7 +17662,7 @@ async function localBranchHeads() {
17031
17662
  }
17032
17663
  async function currentRepoWorktreeGitRoot(repoRoot) {
17033
17664
  const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17034
- return gitCommonDir ? (0, import_node_path17.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
17665
+ return gitCommonDir ? (0, import_node_path19.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
17035
17666
  }
17036
17667
  async function worktreeBranches() {
17037
17668
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -17051,18 +17682,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
17051
17682
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
17052
17683
  if (!match?.[1]) return void 0;
17053
17684
  const raw = match[1].trim();
17054
- return (0, import_node_path17.isAbsolute)(raw) ? raw : (0, import_node_path17.resolve)(worktreePath, raw);
17685
+ return (0, import_node_path19.isAbsolute)(raw) ? raw : (0, import_node_path19.resolve)(worktreePath, raw);
17055
17686
  }
17056
17687
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17057
17688
  if (!worktreeGitRoot) return false;
17058
17689
  try {
17059
- const entries = (0, import_node_fs17.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17690
+ const entries = (0, import_node_fs19.readdirSync)(worktreeGitRoot, { withFileTypes: true });
17060
17691
  for (const ent of entries) {
17061
17692
  if (!ent.isDirectory()) continue;
17062
17693
  try {
17063
- const gitdirPath = (0, import_node_fs16.readFileSync)((0, import_node_path17.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17064
- const resolvedGitdir = (0, import_node_path17.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path17.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17065
- if (sameWorktreeMetadataPath((0, import_node_path17.dirname)(resolvedGitdir), worktreePath)) return true;
17694
+ const gitdirPath = (0, import_node_fs18.readFileSync)((0, import_node_path19.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17695
+ const resolvedGitdir = (0, import_node_path19.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path19.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17696
+ if (sameWorktreeMetadataPath((0, import_node_path19.dirname)(resolvedGitdir), worktreePath)) return true;
17066
17697
  } catch {
17067
17698
  }
17068
17699
  }
@@ -17072,7 +17703,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17072
17703
  }
17073
17704
  function pathExistsKnown(path2) {
17074
17705
  try {
17075
- (0, import_node_fs17.statSync)(path2);
17706
+ (0, import_node_fs19.statSync)(path2);
17076
17707
  return true;
17077
17708
  } catch (e) {
17078
17709
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
@@ -17081,10 +17712,10 @@ function pathExistsKnown(path2) {
17081
17712
  }
17082
17713
  }
17083
17714
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17084
- const gitPath = (0, import_node_path17.join)(path2, ".git");
17715
+ const gitPath = (0, import_node_path19.join)(path2, ".git");
17085
17716
  let st;
17086
17717
  try {
17087
- st = (0, import_node_fs17.lstatSync)(gitPath);
17718
+ st = (0, import_node_fs19.lstatSync)(gitPath);
17088
17719
  } catch (e) {
17089
17720
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17090
17721
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -17101,7 +17732,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17101
17732
  if (st.isDirectory()) return { path: path2, gitType: "dir" };
17102
17733
  if (!st.isFile()) return { path: path2, gitType: "other" };
17103
17734
  try {
17104
- const gitFileContent = (0, import_node_fs16.readFileSync)(gitPath, "utf8");
17735
+ const gitFileContent = (0, import_node_fs18.readFileSync)(gitPath, "utf8");
17105
17736
  const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
17106
17737
  const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
17107
17738
  return {
@@ -17118,7 +17749,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17118
17749
  }
17119
17750
  function inspectDeadWorktreeDirContent(path2) {
17120
17751
  try {
17121
- return { entries: (0, import_node_fs17.readdirSync)(path2) };
17752
+ return { entries: (0, import_node_fs19.readdirSync)(path2) };
17122
17753
  } catch (e) {
17123
17754
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
17124
17755
  return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
@@ -17137,8 +17768,8 @@ async function siblingWorktreeDirs() {
17137
17768
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
17138
17769
  const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
17139
17770
  try {
17140
- const entries = (0, import_node_fs17.readdirSync)(siblingRoot, { withFileTypes: true });
17141
- return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path17.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
17771
+ const entries = (0, import_node_fs19.readdirSync)(siblingRoot, { withFileTypes: true });
17772
+ return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path19.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
17142
17773
  } catch {
17143
17774
  return [];
17144
17775
  }
@@ -17188,7 +17819,7 @@ async function fetchHubVersionInfo(baseUrl) {
17188
17819
  }
17189
17820
  function readRepoVersion() {
17190
17821
  try {
17191
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path18.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17822
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17192
17823
  } catch {
17193
17824
  return void 0;
17194
17825
  }
@@ -17340,11 +17971,11 @@ async function applyPluginHeal(token, surface, log, opts) {
17340
17971
  }
17341
17972
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
17342
17973
  const homeDir = surface === "codex" ? ".codex" : ".claude";
17343
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
17974
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
17344
17975
  };
17345
17976
  function readInstalledPlugins(surface = detectSurface(process.env)) {
17346
17977
  try {
17347
- return JSON.parse((0, import_node_fs18.readFileSync)(installedPluginsPath(surface), "utf8"));
17978
+ return JSON.parse((0, import_node_fs20.readFileSync)(installedPluginsPath(surface), "utf8"));
17348
17979
  } catch {
17349
17980
  return null;
17350
17981
  }
@@ -17352,13 +17983,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
17352
17983
  function marketplaceCloneCandidates(surface, home) {
17353
17984
  if (surface === "codex") {
17354
17985
  return [
17355
- (0, import_node_path18.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
17356
- (0, import_node_path18.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
17986
+ (0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
17987
+ (0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
17357
17988
  ];
17358
17989
  }
17359
- return [(0, import_node_path18.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
17990
+ return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
17360
17991
  }
17361
- function marketplaceClonePresent(surface, home, exists = import_node_fs18.existsSync) {
17992
+ function marketplaceClonePresent(surface, home, exists = import_node_fs20.existsSync) {
17362
17993
  return marketplaceCloneCandidates(surface, home).some(exists);
17363
17994
  }
17364
17995
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
@@ -17368,14 +17999,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
17368
17999
  isOrgRepo,
17369
18000
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
17370
18001
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
17371
- pluginCachePresent: (0, import_node_fs18.existsSync)((0, import_node_path18.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
18002
+ pluginCachePresent: (0, import_node_fs20.existsSync)((0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
17372
18003
  };
17373
18004
  }
17374
18005
  function installedPluginSources() {
17375
18006
  return ["claude", "codex"].map((surface) => {
17376
- const recordPath = (0, import_node_path18.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18007
+ const recordPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
17377
18008
  try {
17378
- return { surface, installed: JSON.parse((0, import_node_fs18.readFileSync)(recordPath, "utf8")), recordPath };
18009
+ return { surface, installed: JSON.parse((0, import_node_fs20.readFileSync)(recordPath, "utf8")), recordPath };
17379
18010
  } catch {
17380
18011
  return { surface, installed: null, recordPath };
17381
18012
  }
@@ -17383,7 +18014,7 @@ function installedPluginSources() {
17383
18014
  }
17384
18015
  function readClaudeSettings() {
17385
18016
  try {
17386
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path18.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18017
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17387
18018
  } catch {
17388
18019
  return null;
17389
18020
  }
@@ -17405,7 +18036,7 @@ function writeProjectInstallRecord(record) {
17405
18036
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
17406
18037
  list.push(record);
17407
18038
  file.plugins[MMI_PLUGIN_ID] = list;
17408
- (0, import_node_fs18.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18039
+ (0, import_node_fs20.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17409
18040
  `, "utf8");
17410
18041
  return true;
17411
18042
  } catch {
@@ -17418,9 +18049,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17418
18049
  if (!file) return false;
17419
18050
  if (!file.plugins) file.plugins = {};
17420
18051
  const path2 = installedPluginsPath();
17421
- (0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
18052
+ (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
17422
18053
  file.plugins[pluginId] = records;
17423
- (0, import_node_fs18.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18054
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17424
18055
  `, "utf8");
17425
18056
  return true;
17426
18057
  } catch {
@@ -17428,22 +18059,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17428
18059
  }
17429
18060
  }
17430
18061
  function opencodeConfigDir() {
17431
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".config", "opencode");
18062
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "opencode");
17432
18063
  }
17433
18064
  function opencodeConfigPath() {
17434
- return (0, import_node_path18.join)(opencodeConfigDir(), "opencode.jsonc");
18065
+ return (0, import_node_path20.join)(opencodeConfigDir(), "opencode.jsonc");
17435
18066
  }
17436
18067
  function opencodeCommandsDir() {
17437
- return (0, import_node_path18.join)(opencodeConfigDir(), "commands");
18068
+ return (0, import_node_path20.join)(opencodeConfigDir(), "commands");
17438
18069
  }
17439
18070
  function opencodeSkillsPath() {
17440
- return (0, import_node_path18.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
18071
+ return (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
17441
18072
  }
17442
18073
  function opencodeConfigSnapshot() {
17443
18074
  const path2 = opencodeConfigPath();
17444
- if (!(0, import_node_fs18.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
18075
+ if (!(0, import_node_fs20.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
17445
18076
  try {
17446
- const raw = (0, import_node_fs18.readFileSync)(path2, "utf8");
18077
+ const raw = (0, import_node_fs20.readFileSync)(path2, "utf8");
17447
18078
  const parsed = JSON.parse(stripJsonc(raw));
17448
18079
  const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
17449
18080
  const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
@@ -17466,9 +18097,9 @@ function writeOpencodeConfigPlugin(snapshot) {
17466
18097
  const plan = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
17467
18098
  if (plan.action === "already") return true;
17468
18099
  if (!plan.text || plan.action === "unsafe") return false;
17469
- (0, import_node_fs18.mkdirSync)((0, import_node_path18.dirname)(path2), { recursive: true });
17470
- if (snapshot.hasConfig) (0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
17471
- (0, import_node_fs18.writeFileSync)(path2, plan.text, "utf8");
18100
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
18101
+ if (snapshot.hasConfig) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
18102
+ (0, import_node_fs20.writeFileSync)(path2, plan.text, "utf8");
17472
18103
  return true;
17473
18104
  } catch {
17474
18105
  return false;
@@ -17483,9 +18114,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17483
18114
  const normalized = skillsPath.replace(/\\/g, "/");
17484
18115
  if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
17485
18116
  parsed.skills = { ...skills, paths };
17486
- (0, import_node_fs18.mkdirSync)((0, import_node_path18.dirname)(snapshot.path), { recursive: true });
17487
- if (snapshot.hasConfig && (0, import_node_fs18.existsSync)(snapshot.path)) (0, import_node_fs18.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
17488
- (0, import_node_fs18.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
18117
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(snapshot.path), { recursive: true });
18118
+ if (snapshot.hasConfig && (0, import_node_fs20.existsSync)(snapshot.path)) (0, import_node_fs20.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
18119
+ (0, import_node_fs20.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
17489
18120
  `, "utf8");
17490
18121
  return true;
17491
18122
  } catch {
@@ -17494,7 +18125,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
17494
18125
  }
17495
18126
  function opencodeExistingCommands() {
17496
18127
  try {
17497
- return (0, import_node_fs18.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
18128
+ return (0, import_node_fs20.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
17498
18129
  } catch {
17499
18130
  return [];
17500
18131
  }
@@ -17502,9 +18133,9 @@ function opencodeExistingCommands() {
17502
18133
  function writeOpencodeCommandFiles() {
17503
18134
  try {
17504
18135
  const dir = opencodeCommandsDir();
17505
- (0, import_node_fs18.mkdirSync)(dir, { recursive: true });
18136
+ (0, import_node_fs20.mkdirSync)(dir, { recursive: true });
17506
18137
  for (const command of OPENCODE_WORKFLOW_COMMANDS) {
17507
- (0, import_node_fs18.writeFileSync)((0, import_node_path18.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
18138
+ (0, import_node_fs20.writeFileSync)((0, import_node_path20.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
17508
18139
  }
17509
18140
  return true;
17510
18141
  } catch {
@@ -17513,12 +18144,12 @@ function writeOpencodeCommandFiles() {
17513
18144
  }
17514
18145
  function readOpencodeAdapterDiskVersion() {
17515
18146
  const candidates = [
17516
- (0, import_node_path18.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
17517
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
18147
+ (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
18148
+ (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
17518
18149
  ];
17519
18150
  for (const path2 of candidates) {
17520
18151
  try {
17521
- const parsed = JSON.parse((0, import_node_fs18.readFileSync)(path2, "utf8"));
18152
+ const parsed = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
17522
18153
  if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
17523
18154
  } catch {
17524
18155
  continue;
@@ -17534,7 +18165,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17534
18165
  try {
17535
18166
  const specs = opencodeMmiPluginSpecs(snapshot);
17536
18167
  log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
17537
- (0, import_node_fs18.mkdirSync)(opencodeConfigDir(), { recursive: true });
18168
+ (0, import_node_fs20.mkdirSync)(opencodeConfigDir(), { recursive: true });
17538
18169
  await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
17539
18170
  return true;
17540
18171
  } catch {
@@ -17542,12 +18173,12 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
17542
18173
  }
17543
18174
  }
17544
18175
  function opencodePackagesRoot() {
17545
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
18176
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
17546
18177
  }
17547
18178
  function opencodePluginCacheDirs() {
17548
18179
  const root = opencodePackagesRoot();
17549
18180
  try {
17550
- return (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path18.join)(root, e.name));
18181
+ return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path20.join)(root, e.name));
17551
18182
  } catch {
17552
18183
  return [];
17553
18184
  }
@@ -17555,7 +18186,7 @@ function opencodePluginCacheDirs() {
17555
18186
  function readOpencodeCacheDirVersion(cacheDir) {
17556
18187
  try {
17557
18188
  const parsed = JSON.parse(
17558
- (0, import_node_fs18.readFileSync)((0, import_node_path18.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
18189
+ (0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
17559
18190
  );
17560
18191
  return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17561
18192
  } catch {
@@ -17578,9 +18209,9 @@ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
17578
18209
  });
17579
18210
  if (!plan.quarantine) continue;
17580
18211
  try {
17581
- const quarantineRoot = (0, import_node_path18.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
17582
- (0, import_node_fs18.mkdirSync)(quarantineRoot, { recursive: true });
17583
- (0, import_node_fs18.renameSync)(cacheDir, (0, import_node_path18.join)(quarantineRoot, (0, import_node_path18.basename)(cacheDir)));
18212
+ const quarantineRoot = (0, import_node_path20.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
18213
+ (0, import_node_fs20.mkdirSync)(quarantineRoot, { recursive: true });
18214
+ (0, import_node_fs20.renameSync)(cacheDir, (0, import_node_path20.join)(quarantineRoot, (0, import_node_path20.basename)(cacheDir)));
17584
18215
  log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
17585
18216
  moved = true;
17586
18217
  } catch {
@@ -17606,30 +18237,30 @@ function opencodePluginVersionsForReport() {
17606
18237
  }
17607
18238
  function opencodeDesktopLogsRoot() {
17608
18239
  if (process.platform === "win32") {
17609
- const base = process.env.APPDATA || (0, import_node_path18.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
17610
- return (0, import_node_path18.join)(base, "ai.opencode.desktop", "logs");
18240
+ const base = process.env.APPDATA || (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
18241
+ return (0, import_node_path20.join)(base, "ai.opencode.desktop", "logs");
17611
18242
  }
17612
18243
  if (process.platform === "darwin") {
17613
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
18244
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
17614
18245
  }
17615
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
18246
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
17616
18247
  }
17617
18248
  function opencodeDesktopBootstrapSnapshot() {
17618
18249
  const root = opencodeDesktopLogsRoot();
17619
18250
  try {
17620
- const sessionDirs = (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path18.join)(root, entry.name)).sort((a, b) => (0, import_node_fs18.statSync)(b).mtimeMs - (0, import_node_fs18.statSync)(a).mtimeMs);
18251
+ const sessionDirs = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path20.join)(root, entry.name)).sort((a, b) => (0, import_node_fs20.statSync)(b).mtimeMs - (0, import_node_fs20.statSync)(a).mtimeMs);
17621
18252
  const newest = sessionDirs[0];
17622
18253
  if (!newest) return [];
17623
- const logPath = (0, import_node_path18.join)(newest, "renderer.log");
17624
- const text = (0, import_node_fs18.readFileSync)(logPath, "utf8");
17625
- return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs18.existsSync)(directory)).map((directory) => ({ directory, logPath }));
18254
+ const logPath = (0, import_node_path20.join)(newest, "renderer.log");
18255
+ const text = (0, import_node_fs20.readFileSync)(logPath, "utf8");
18256
+ return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs20.existsSync)(directory)).map((directory) => ({ directory, logPath }));
17626
18257
  } catch {
17627
18258
  return [];
17628
18259
  }
17629
18260
  }
17630
18261
  function opencodeLegacyConfigSnapshot() {
17631
- const legacyPath = (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
17632
- if (!(0, import_node_fs18.existsSync)(legacyPath)) return {};
18262
+ const legacyPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
18263
+ if (!(0, import_node_fs20.existsSync)(legacyPath)) return {};
17633
18264
  const content = readTextFile(legacyPath);
17634
18265
  if (content == null) return {};
17635
18266
  const plugins = parseOpencodeLegacyConfigPlugins(content);
@@ -17641,24 +18272,24 @@ function opencodeLegacyConfigSnapshot() {
17641
18272
  function quarantineOpencodeLegacyConfig(legacyPath) {
17642
18273
  try {
17643
18274
  const backupPath = `${legacyPath}.bak`;
17644
- if ((0, import_node_fs18.existsSync)(backupPath)) return false;
17645
- (0, import_node_fs18.renameSync)(legacyPath, backupPath);
18275
+ if ((0, import_node_fs20.existsSync)(backupPath)) return false;
18276
+ (0, import_node_fs20.renameSync)(legacyPath, backupPath);
17646
18277
  return true;
17647
18278
  } catch {
17648
18279
  return false;
17649
18280
  }
17650
18281
  }
17651
18282
  function cursorPluginCacheRoot() {
17652
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18283
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
17653
18284
  }
17654
18285
  function cursorLogsRoot() {
17655
- if (process.platform === "win32") return (0, import_node_path18.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
17656
- if (process.platform === "darwin") return (0, import_node_path18.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
17657
- return (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
18286
+ if (process.platform === "win32") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
18287
+ if (process.platform === "darwin") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
18288
+ return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
17658
18289
  }
17659
18290
  function safeReaddirNames(dir) {
17660
18291
  try {
17661
- return (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
18292
+ return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
17662
18293
  } catch {
17663
18294
  return [];
17664
18295
  }
@@ -17667,15 +18298,15 @@ function readCursorPluginLogText() {
17667
18298
  const root = cursorLogsRoot();
17668
18299
  const sessions = safeReaddirNames(root).sort().reverse();
17669
18300
  for (const session of sessions.slice(0, 5)) {
17670
- const sessionDir = (0, import_node_path18.join)(root, session);
18301
+ const sessionDir = (0, import_node_path20.join)(root, session);
17671
18302
  const texts = [];
17672
18303
  for (const win of safeReaddirNames(sessionDir).filter((n) => n.startsWith("window"))) {
17673
- const dir = (0, import_node_path18.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
18304
+ const dir = (0, import_node_path20.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
17674
18305
  for (const file of safeReaddirNames(dir)) {
17675
18306
  if (!/^Cursor Plugins.*\.log$/i.test(file)) continue;
17676
- const path2 = (0, import_node_path18.join)(dir, file);
18307
+ const path2 = (0, import_node_path20.join)(dir, file);
17677
18308
  try {
17678
- texts.push({ text: (0, import_node_fs18.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs18.statSync)(path2).mtimeMs });
18309
+ texts.push({ text: (0, import_node_fs20.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs20.statSync)(path2).mtimeMs });
17679
18310
  } catch {
17680
18311
  }
17681
18312
  }
@@ -17689,32 +18320,32 @@ function readCursorPluginLogText() {
17689
18320
  function cursorPluginCachePinSnapshots() {
17690
18321
  const root = cursorPluginCacheRoot();
17691
18322
  try {
17692
- return (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
17693
- const path2 = (0, import_node_path18.join)(root, entry.name);
17694
- const pluginJson = (0, import_node_path18.join)(path2, ".cursor-plugin", "plugin.json");
17695
- const hooksJson = (0, import_node_path18.join)(path2, "hooks", "hooks.json");
17696
- const cliBundle = (0, import_node_path18.join)(path2, "cli", "dist", "index.cjs");
17697
- const cursorHook = (0, import_node_path18.join)(path2, "scripts", "cursor-hook.mjs");
18323
+ return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
18324
+ const path2 = (0, import_node_path20.join)(root, entry.name);
18325
+ const pluginJson = (0, import_node_path20.join)(path2, ".cursor-plugin", "plugin.json");
18326
+ const hooksJson = (0, import_node_path20.join)(path2, "hooks", "hooks.json");
18327
+ const cliBundle = (0, import_node_path20.join)(path2, "cli", "dist", "index.cjs");
18328
+ const cursorHook = (0, import_node_path20.join)(path2, "scripts", "cursor-hook.mjs");
17698
18329
  let version;
17699
18330
  try {
17700
- const raw = JSON.parse((0, import_node_fs18.readFileSync)(pluginJson, "utf8"));
18331
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(pluginJson, "utf8"));
17701
18332
  version = typeof raw.version === "string" ? raw.version : void 0;
17702
18333
  } catch {
17703
18334
  version = void 0;
17704
18335
  }
17705
18336
  let isEmpty = true;
17706
18337
  try {
17707
- isEmpty = (0, import_node_fs18.readdirSync)(path2).length === 0;
18338
+ isEmpty = (0, import_node_fs20.readdirSync)(path2).length === 0;
17708
18339
  } catch {
17709
18340
  isEmpty = true;
17710
18341
  }
17711
18342
  return {
17712
18343
  name: entry.name,
17713
18344
  path: path2,
17714
- hasPluginJson: (0, import_node_fs18.existsSync)(pluginJson),
17715
- hasHooksJson: (0, import_node_fs18.existsSync)(hooksJson),
17716
- hasCliBundle: (0, import_node_fs18.existsSync)(cliBundle),
17717
- hasCursorHookScript: (0, import_node_fs18.existsSync)(cursorHook),
18345
+ hasPluginJson: (0, import_node_fs20.existsSync)(pluginJson),
18346
+ hasHooksJson: (0, import_node_fs20.existsSync)(hooksJson),
18347
+ hasCliBundle: (0, import_node_fs20.existsSync)(cliBundle),
18348
+ hasCursorHookScript: (0, import_node_fs20.existsSync)(cursorHook),
17718
18349
  isEmpty,
17719
18350
  version
17720
18351
  };
@@ -17724,19 +18355,19 @@ function cursorPluginCachePinSnapshots() {
17724
18355
  }
17725
18356
  }
17726
18357
  function hubCheckoutForCursorSeed() {
17727
- const manifest = (0, import_node_path18.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
17728
- return (0, import_node_fs18.existsSync)(manifest) ? process.cwd() : void 0;
18358
+ const manifest = (0, import_node_path20.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
18359
+ return (0, import_node_fs20.existsSync)(manifest) ? process.cwd() : void 0;
17729
18360
  }
17730
18361
  function mmiPluginCacheRootSnapshots() {
17731
18362
  const roots = [
17732
- { surface: "claude", root: (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
17733
- { surface: "codex", root: (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
18363
+ { surface: "claude", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
18364
+ { surface: "codex", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
17734
18365
  ];
17735
18366
  return roots.flatMap(({ surface, root }) => {
17736
18367
  try {
17737
- const entries = (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18368
+ const entries = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
17738
18369
  name: entry.name,
17739
- path: (0, import_node_path18.join)(root, entry.name),
18370
+ path: (0, import_node_path20.join)(root, entry.name),
17740
18371
  isDirectory: entry.isDirectory()
17741
18372
  }));
17742
18373
  return [{ surface, root, entries }];
@@ -17747,7 +18378,7 @@ function mmiPluginCacheRootSnapshots() {
17747
18378
  }
17748
18379
  function hasNestedMmiChild(versionDir) {
17749
18380
  try {
17750
- return (0, import_node_fs18.statSync)((0, import_node_path18.join)(versionDir, "mmi")).isDirectory();
18381
+ return (0, import_node_fs20.statSync)((0, import_node_path20.join)(versionDir, "mmi")).isDirectory();
17751
18382
  } catch {
17752
18383
  return false;
17753
18384
  }
@@ -17758,10 +18389,10 @@ function nestedPluginTreeSnapshot() {
17758
18389
  );
17759
18390
  }
17760
18391
  function uniqueQuarantineTarget(path2) {
17761
- if (!(0, import_node_fs18.existsSync)(path2)) return path2;
18392
+ if (!(0, import_node_fs20.existsSync)(path2)) return path2;
17762
18393
  for (let i = 1; i < 100; i += 1) {
17763
18394
  const candidate = `${path2}-${i}`;
17764
- if (!(0, import_node_fs18.existsSync)(candidate)) return candidate;
18395
+ if (!(0, import_node_fs20.existsSync)(candidate)) return candidate;
17765
18396
  }
17766
18397
  return `${path2}-${Date.now()}`;
17767
18398
  }
@@ -17770,10 +18401,10 @@ function quarantinePluginCacheDirs(plan) {
17770
18401
  const failed = [];
17771
18402
  for (const move of plan) {
17772
18403
  try {
17773
- if (!(0, import_node_fs18.existsSync)(move.from)) continue;
18404
+ if (!(0, import_node_fs20.existsSync)(move.from)) continue;
17774
18405
  const target = uniqueQuarantineTarget(move.to);
17775
- (0, import_node_fs18.mkdirSync)((0, import_node_path18.dirname)(target), { recursive: true });
17776
- (0, import_node_fs18.renameSync)(move.from, target);
18406
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(target), { recursive: true });
18407
+ (0, import_node_fs20.renameSync)(move.from, target);
17777
18408
  moved += 1;
17778
18409
  } catch {
17779
18410
  failed.push(move);
@@ -17792,23 +18423,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
17792
18423
  }
17793
18424
  async function clearNestedPluginTreeDir(targetPath) {
17794
18425
  try {
17795
- if (!(0, import_node_fs18.existsSync)(targetPath)) return true;
18426
+ if (!(0, import_node_fs20.existsSync)(targetPath)) return true;
17796
18427
  if (isWin) {
17797
- const emptyDir = (0, import_node_path18.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
17798
- (0, import_node_fs18.mkdirSync)(emptyDir, { recursive: true });
18428
+ const emptyDir = (0, import_node_path20.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
18429
+ (0, import_node_fs20.mkdirSync)(emptyDir, { recursive: true });
17799
18430
  try {
17800
18431
  await robocopyMirrorEmpty(emptyDir, targetPath);
17801
- (0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
18432
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
17802
18433
  } finally {
17803
18434
  try {
17804
- (0, import_node_fs18.rmSync)(emptyDir, { recursive: true, force: true });
18435
+ (0, import_node_fs20.rmSync)(emptyDir, { recursive: true, force: true });
17805
18436
  } catch {
17806
18437
  }
17807
18438
  }
17808
- return !(0, import_node_fs18.existsSync)(targetPath);
18439
+ return !(0, import_node_fs20.existsSync)(targetPath);
17809
18440
  }
17810
- (0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
17811
- return !(0, import_node_fs18.existsSync)(targetPath);
18441
+ (0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
18442
+ return !(0, import_node_fs20.existsSync)(targetPath);
17812
18443
  } catch {
17813
18444
  return false;
17814
18445
  }
@@ -17821,18 +18452,18 @@ async function applyNestedPluginTreeCleanup(paths, log) {
17821
18452
  }
17822
18453
  return true;
17823
18454
  }
17824
- var gitignorePath = () => (0, import_node_path18.join)(process.cwd(), ".gitignore");
18455
+ var gitignorePath = () => (0, import_node_path20.join)(process.cwd(), ".gitignore");
17825
18456
  function readTextFile(path2) {
17826
18457
  try {
17827
- if (!(0, import_node_fs18.existsSync)(path2)) return null;
17828
- return (0, import_node_fs18.readFileSync)(path2, "utf8");
18458
+ if (!(0, import_node_fs20.existsSync)(path2)) return null;
18459
+ return (0, import_node_fs20.readFileSync)(path2, "utf8");
17829
18460
  } catch {
17830
18461
  return null;
17831
18462
  }
17832
18463
  }
17833
18464
  function mcpDirExists(path2) {
17834
18465
  try {
17835
- return (0, import_node_fs18.existsSync)(path2);
18466
+ return (0, import_node_fs20.existsSync)(path2);
17836
18467
  } catch {
17837
18468
  return false;
17838
18469
  }
@@ -17840,60 +18471,60 @@ function mcpDirExists(path2) {
17840
18471
  function mcpConfigTargets() {
17841
18472
  const cwd = process.cwd();
17842
18473
  const home = (0, import_node_os7.homedir)();
17843
- const cursorProjectDir = (0, import_node_path18.join)(cwd, ".cursor");
17844
- const cursorUserDir = (0, import_node_path18.join)(home, ".cursor");
17845
- const codexDir = (0, import_node_path18.join)(home, ".codex");
18474
+ const cursorProjectDir = (0, import_node_path20.join)(cwd, ".cursor");
18475
+ const cursorUserDir = (0, import_node_path20.join)(home, ".cursor");
18476
+ const codexDir = (0, import_node_path20.join)(home, ".codex");
17846
18477
  return [
17847
18478
  // Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
17848
18479
  {
17849
18480
  host: "claude-code",
17850
18481
  label: "Claude Code project MCP",
17851
- path: (0, import_node_path18.join)(cwd, ".mcp.json"),
18482
+ path: (0, import_node_path20.join)(cwd, ".mcp.json"),
17852
18483
  format: "json",
17853
18484
  present: true,
17854
18485
  // parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
17855
18486
  isRepoFile: true,
17856
18487
  createWhenFileAbsent: false,
17857
- content: readTextFile((0, import_node_path18.join)(cwd, ".mcp.json"))
18488
+ content: readTextFile((0, import_node_path20.join)(cwd, ".mcp.json"))
17858
18489
  },
17859
18490
  // Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
17860
18491
  {
17861
18492
  host: "cursor-project",
17862
18493
  label: "Cursor project MCP",
17863
- path: (0, import_node_path18.join)(cursorProjectDir, "mcp.json"),
18494
+ path: (0, import_node_path20.join)(cursorProjectDir, "mcp.json"),
17864
18495
  format: "json",
17865
18496
  present: mcpDirExists(cursorProjectDir),
17866
18497
  isRepoFile: true,
17867
18498
  createWhenFileAbsent: true,
17868
- content: readTextFile((0, import_node_path18.join)(cursorProjectDir, "mcp.json"))
18499
+ content: readTextFile((0, import_node_path20.join)(cursorProjectDir, "mcp.json"))
17869
18500
  },
17870
18501
  // Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
17871
18502
  {
17872
18503
  host: "cursor-user",
17873
18504
  label: "Cursor user MCP",
17874
- path: (0, import_node_path18.join)(cursorUserDir, "mcp.json"),
18505
+ path: (0, import_node_path20.join)(cursorUserDir, "mcp.json"),
17875
18506
  format: "json",
17876
18507
  present: mcpDirExists(cursorUserDir),
17877
18508
  isRepoFile: false,
17878
18509
  createWhenFileAbsent: true,
17879
- content: readTextFile((0, import_node_path18.join)(cursorUserDir, "mcp.json"))
18510
+ content: readTextFile((0, import_node_path20.join)(cursorUserDir, "mcp.json"))
17880
18511
  },
17881
18512
  // Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
17882
18513
  {
17883
18514
  host: "codex",
17884
18515
  label: "Codex user config",
17885
- path: (0, import_node_path18.join)(codexDir, "config.toml"),
18516
+ path: (0, import_node_path20.join)(codexDir, "config.toml"),
17886
18517
  format: "toml",
17887
18518
  present: mcpDirExists(codexDir),
17888
18519
  isRepoFile: false,
17889
18520
  createWhenFileAbsent: true,
17890
- content: readTextFile((0, import_node_path18.join)(codexDir, "config.toml"))
18521
+ content: readTextFile((0, import_node_path20.join)(codexDir, "config.toml"))
17891
18522
  }
17892
18523
  ];
17893
18524
  }
17894
18525
  function writeMcpConfigFile(path2, content) {
17895
18526
  try {
17896
- (0, import_node_fs18.writeFileSync)(path2, content, "utf8");
18527
+ (0, import_node_fs20.writeFileSync)(path2, content, "utf8");
17897
18528
  return true;
17898
18529
  } catch {
17899
18530
  return false;
@@ -17973,7 +18604,7 @@ function strayBrowserArtifactPaths() {
17973
18604
  const cwd = process.cwd();
17974
18605
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
17975
18606
  try {
17976
- return (0, import_node_fs18.existsSync)((0, import_node_path18.join)(cwd, rel));
18607
+ return (0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, rel));
17977
18608
  } catch {
17978
18609
  return false;
17979
18610
  }
@@ -17981,37 +18612,119 @@ function strayBrowserArtifactPaths() {
17981
18612
  }
17982
18613
  function readGitignore() {
17983
18614
  try {
17984
- return (0, import_node_fs18.readFileSync)(gitignorePath(), "utf8");
18615
+ return (0, import_node_fs20.readFileSync)(gitignorePath(), "utf8");
17985
18616
  } catch {
17986
18617
  return null;
17987
18618
  }
17988
18619
  }
17989
18620
  function writeGitignore(content) {
17990
18621
  try {
17991
- (0, import_node_fs18.writeFileSync)(gitignorePath(), content, "utf8");
18622
+ (0, import_node_fs20.writeFileSync)(gitignorePath(), content, "utf8");
17992
18623
  return true;
17993
18624
  } catch {
17994
18625
  return false;
17995
18626
  }
17996
18627
  }
18628
+ function readRepoJson(rel) {
18629
+ try {
18630
+ return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), rel), "utf8"));
18631
+ } catch {
18632
+ return null;
18633
+ }
18634
+ }
18635
+ var ENCODING_SCAN_ROOT_FILES = ["AGENTS.md", "README.md", "CLAUDE.md", "package.json", "opencode.json"];
18636
+ var ENCODING_SCAN_DIRS = [".claude", "hooks", "scripts"];
18637
+ var ENCODING_SCAN_EXTS = /* @__PURE__ */ new Set([".md", ".json", ".jsonc", ".ts", ".js", ".cjs", ".mjs", ".sh", ".ps1", ".py", ".txt"]);
18638
+ var ENCODING_SCAN_MAX_FILES = 400;
18639
+ function listTextFilesUnder(dirRel, out, budget) {
18640
+ let entries;
18641
+ try {
18642
+ entries = (0, import_node_fs20.readdirSync)((0, import_node_path20.join)(process.cwd(), dirRel), { withFileTypes: true });
18643
+ } catch {
18644
+ return;
18645
+ }
18646
+ for (const ent of entries) {
18647
+ if (budget.count >= ENCODING_SCAN_MAX_FILES) return;
18648
+ const rel = `${dirRel}/${ent.name}`;
18649
+ if (ent.isDirectory()) {
18650
+ if (ent.name === "node_modules" || ent.name === "dist" || ent.name === ".git" || ent.name.startsWith(".")) continue;
18651
+ listTextFilesUnder(rel, out, budget);
18652
+ } else if (ent.isFile()) {
18653
+ const ext = ent.name.slice(ent.name.lastIndexOf("."));
18654
+ if (ENCODING_SCAN_EXTS.has(ext)) {
18655
+ out.push(rel);
18656
+ budget.count++;
18657
+ }
18658
+ }
18659
+ }
18660
+ }
18661
+ function managedFileEncodingFindings() {
18662
+ const cwd = process.cwd();
18663
+ const paths = [];
18664
+ for (const f of ENCODING_SCAN_ROOT_FILES) {
18665
+ try {
18666
+ if ((0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, f))) paths.push(f);
18667
+ } catch {
18668
+ }
18669
+ }
18670
+ const budget = { count: 0 };
18671
+ for (const d of ENCODING_SCAN_DIRS) listTextFilesUnder(d, paths, budget);
18672
+ const findings = [];
18673
+ for (const rel of paths) {
18674
+ try {
18675
+ const probe = inspectFileEncoding((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cwd, rel)));
18676
+ if (probe.bom || probe.invalidUtf8 || probe.replacementChars) findings.push({ path: rel, ...probe });
18677
+ } catch {
18678
+ }
18679
+ }
18680
+ return findings;
18681
+ }
18682
+ async function branchAheadBehind(branch) {
18683
+ const spec = `origin/${branch}...${branch}`;
18684
+ try {
18685
+ const { stdout } = await execFileP2("git", ["rev-list", "--left-right", "--count", spec], { timeout: GIT_TIMEOUT_MS });
18686
+ const [behind, ahead] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
18687
+ if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
18688
+ return { behind, ahead };
18689
+ } catch {
18690
+ return null;
18691
+ }
18692
+ }
18693
+ async function localReleaseBranchSnapshots() {
18694
+ let local;
18695
+ try {
18696
+ const { stdout } = await execFileP2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS });
18697
+ local = stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
18698
+ } catch {
18699
+ return [];
18700
+ }
18701
+ const releaseBranches = local.filter((b) => b === "development" || b === "main" || b === "master" || b === "rc" || b.startsWith("release/"));
18702
+ const rows = [];
18703
+ for (const branch of releaseBranches) {
18704
+ const ab = await branchAheadBehind(branch);
18705
+ if (ab) rows.push({ name: branch, ...ab });
18706
+ }
18707
+ return rows;
18708
+ }
17997
18709
  async function runDoctor(opts, io = consoleIo, readOrigin) {
17998
18710
  if (opts.guide) {
17999
18711
  if (opts.json) io.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
18000
18712
  else io.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
18001
18713
  return;
18002
18714
  }
18003
- const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
18715
+ const repairLocal = !opts.fast && (!opts.json || Boolean(opts.apply) || Boolean(opts.preflight));
18004
18716
  const repoWritesAllowed = !opts.noRepoWrites;
18005
- const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
18717
+ const runExtended = !opts.fast && (Boolean(opts.verbose) || Boolean(opts.json));
18718
+ const slowSkippedCount = opts.fast ? DOCTOR_EXTENDED_CHECK_LABELS.size : 0;
18006
18719
  const checks = [];
18007
18720
  const REWRITE_KEY = "url.https://github.com/.insteadOf";
18008
18721
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
18009
18722
  const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
18010
- githubLogin(),
18723
+ opts.fast ? Promise.resolve(void 0) : githubLogin(),
18011
18724
  execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
18012
- fetchReleasedVersion(),
18725
+ opts.fast ? Promise.resolve({ version: void 0, source: "unavailable" }) : fetchReleasedVersion(),
18013
18726
  loadConfig(),
18014
- awsCallerArn(),
18727
+ opts.fast ? Promise.resolve(void 0) : awsCallerArn(),
18015
18728
  execFileP2("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false),
18016
18729
  // unset → repair below
18017
18730
  // hub-v3 step-back: org-membership gates the repo-local checks/repairs by the git `origin` remote,
@@ -18030,7 +18743,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18030
18743
  const semverPrefix = /^\d+\.\d+\.\d+/;
18031
18744
  const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
18032
18745
  const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
18033
- const cursorCacheStale = (0, import_node_fs18.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18746
+ const cursorCacheStale = (0, import_node_fs20.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
18034
18747
  const healPlan = doctorHealPlan({
18035
18748
  isOrgRepo,
18036
18749
  surface,
@@ -18052,20 +18765,22 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18052
18765
  if (opts.preflight && !healPlan.needsEagerHeal) return;
18053
18766
  const eagerHeal = Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
18054
18767
  if (eagerHeal && healPlan.needsEagerHeal) io.err(DOCTOR_PREFLIGHT_NOTICE);
18055
- const repairFull = !opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
18056
- let ghInstalled = true;
18057
- if (!login) {
18058
- try {
18059
- await execFileP2("gh", ["--version"]);
18060
- } catch {
18061
- ghInstalled = false;
18768
+ const repairFull = !opts.fast && (!opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal);
18769
+ if (!opts.fast) {
18770
+ let ghInstalled = true;
18771
+ if (!login) {
18772
+ try {
18773
+ await execFileP2("gh", ["--version"]);
18774
+ } catch {
18775
+ ghInstalled = false;
18776
+ }
18062
18777
  }
18778
+ checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18063
18779
  }
18064
- checks.push(buildGithubAuthCheck({ login, ghInstalled }));
18065
18780
  let onPath = pathProbe;
18066
18781
  if (!onPath) {
18067
18782
  const root = process.env.CLAUDE_PLUGIN_ROOT;
18068
- if (root && (0, import_node_fs18.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18783
+ if (root && (0, import_node_fs20.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18069
18784
  }
18070
18785
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
18071
18786
  const reloadHint = reloadAction(surface);
@@ -18108,7 +18823,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18108
18823
  return;
18109
18824
  }
18110
18825
  checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
18111
- const hubVersionInfo = await fetchHubVersionInfo(cfg.sagaApiUrl);
18826
+ const hubVersionInfo = opts.fast ? null : await fetchHubVersionInfo(cfg.sagaApiUrl);
18112
18827
  const installedVersion = resolveClientVersion();
18113
18828
  checks.push(
18114
18829
  buildHubCompatCheck({
@@ -18126,6 +18841,12 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18126
18841
  releasedVersion
18127
18842
  })
18128
18843
  );
18844
+ checks.push(
18845
+ buildCliVersionDriftCheck({
18846
+ currentVersion: installedVersion,
18847
+ releasedVersion
18848
+ })
18849
+ );
18129
18850
  }
18130
18851
  if (runExtended) {
18131
18852
  checks.push(buildAwsCrossAccountCheck({ callerArn }));
@@ -18212,8 +18933,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18212
18933
  checks.push(gitignoreCheck);
18213
18934
  checks.push(buildRepoLocalWorktreeCheck({
18214
18935
  isOrgRepo,
18215
- hasRepoLocalWorktrees: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".worktrees"))
18936
+ hasRepoLocalWorktrees: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), ".worktrees"))
18216
18937
  }));
18938
+ if (isOrgRepo) {
18939
+ const pkg = readRepoJson("package.json");
18940
+ const lock = readRepoJson("package-lock.json");
18941
+ checks.push(buildLockfileOptionalsCheck({
18942
+ isOrgRepo,
18943
+ packageJsonOptionalDeps: pkg?.optionalDependencies ?? null,
18944
+ lockfilePackages: lock?.packages ?? null
18945
+ }));
18946
+ } else {
18947
+ checks.push(buildLockfileOptionalsCheck({ isOrgRepo, packageJsonOptionalDeps: null, lockfilePackages: null }));
18948
+ }
18217
18949
  let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
18218
18950
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
18219
18951
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
@@ -18398,7 +19130,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18398
19130
  return versions.sort((a, b) => compareVersions(b, a))[0];
18399
19131
  };
18400
19132
  const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
18401
- const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
19133
+ const codexActiveVersion = () => !opts.fast && isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
18402
19134
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
18403
19135
  isOrgRepo,
18404
19136
  roots: mmiPluginCacheRootSnapshots(),
@@ -18501,7 +19233,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18501
19233
  }
18502
19234
  checks.push(nestedPluginTreeCheck);
18503
19235
  const cursorCacheRoot = cursorPluginCacheRoot();
18504
- const cursorCacheRootExists = (0, import_node_fs18.existsSync)(cursorCacheRoot);
19236
+ const cursorCacheRootExists = (0, import_node_fs20.existsSync)(cursorCacheRoot);
18505
19237
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
18506
19238
  checks.push(
18507
19239
  buildCursorPluginCacheCleanupCheck({
@@ -18529,7 +19261,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18529
19261
  cacheRootExists: cursorCacheRootExists,
18530
19262
  hubCheckout: hubCheckoutForCursorSeed(),
18531
19263
  execFileP: execFileP2,
18532
- mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path18.join)((0, import_node_os7.tmpdir)(), prefix)),
19264
+ mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path20.join)((0, import_node_os7.tmpdir)(), prefix)),
18533
19265
  log: (m) => io.err(m)
18534
19266
  });
18535
19267
  if (seeded) {
@@ -18606,6 +19338,23 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18606
19338
  cacheVersions: sessionCacheVersions
18607
19339
  })
18608
19340
  );
19341
+ {
19342
+ let redactorFailureLines = [];
19343
+ try {
19344
+ redactorFailureLines = (0, import_node_fs20.readFileSync)(activityLogPath(process.cwd()), "utf8").split("\n");
19345
+ } catch {
19346
+ redactorFailureLines = [];
19347
+ }
19348
+ const redactorSummary = summarizeRedactorFailures(redactorFailureLines);
19349
+ checks.push(
19350
+ buildRedactorLivenessCheck({
19351
+ isOrgRepo,
19352
+ failedCount: redactorSummary.failedCount,
19353
+ lastFailedTs: redactorSummary.lastFailedTs,
19354
+ lastFailedAction: redactorSummary.lastFailedAction
19355
+ })
19356
+ );
19357
+ }
18609
19358
  if (runExtended) {
18610
19359
  checks.push(
18611
19360
  buildBrowserArtifactsCheck({
@@ -18630,6 +19379,14 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18630
19379
  checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
18631
19380
  } catch {
18632
19381
  }
19382
+ try {
19383
+ checks.push(buildManagedFileEncodingCheck({ isOrgRepo, findings: managedFileEncodingFindings() }));
19384
+ } catch {
19385
+ }
19386
+ try {
19387
+ checks.push(buildStaleLocalBranchCheck({ isOrgRepo, branches: await localReleaseBranchSnapshots() }));
19388
+ } catch {
19389
+ }
18633
19390
  } else if (opts.apply && repoWritesAllowed) {
18634
19391
  const scratchRun = executeScratchGc(repoRoot, { apply: true });
18635
19392
  if (scratchRun.applied?.pruned.length) {
@@ -18650,6 +19407,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18650
19407
  healedCount,
18651
19408
  pluginReloadRequired,
18652
19409
  reloadHint,
19410
+ slowSkippedCount,
18653
19411
  // Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
18654
19412
  // an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
18655
19413
  // echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
@@ -18675,6 +19433,7 @@ function emitDoctorReport(opts, io, ctx) {
18675
19433
  const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
18676
19434
  const gaps = checks.filter((c) => !c.ok);
18677
19435
  if (opts.preflight) {
19436
+ process.exitCode = doctorExitCode(checks);
18678
19437
  const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
18679
19438
  if (outcome.line) io.err(outcome.line);
18680
19439
  if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
@@ -18701,7 +19460,8 @@ function emitDoctorReport(opts, io, ctx) {
18701
19460
  healedCount,
18702
19461
  pluginReloadRequired,
18703
19462
  reloadHint,
18704
- verbose: Boolean(opts.verbose)
19463
+ verbose: Boolean(opts.verbose),
19464
+ skippedSlowChecks: ctx.slowSkippedCount
18705
19465
  })) io.log(line);
18706
19466
  for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
18707
19467
  }
@@ -18724,23 +19484,23 @@ function mergeGuardHook(settings) {
18724
19484
  next.hooks = hooks;
18725
19485
  return next;
18726
19486
  }
18727
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path18.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
19487
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path20.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
18728
19488
  function ensureUserScopeGuardHook(opts = {}) {
18729
19489
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
18730
19490
  try {
18731
19491
  let current = null;
18732
- if ((0, import_node_fs18.existsSync)(path2)) {
19492
+ if ((0, import_node_fs20.existsSync)(path2)) {
18733
19493
  try {
18734
- current = JSON.parse((0, import_node_fs18.readFileSync)(path2, "utf8"));
19494
+ current = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
18735
19495
  } catch {
18736
19496
  return "failed";
18737
19497
  }
18738
19498
  }
18739
19499
  if (settingsHasGuardHook(current)) return "already";
18740
19500
  const merged = mergeGuardHook(current);
18741
- (0, import_node_fs18.mkdirSync)((0, import_node_path18.dirname)(path2), { recursive: true });
18742
- if ((0, import_node_fs18.existsSync)(path2)) (0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
18743
- (0, import_node_fs18.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
19501
+ (0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
19502
+ if ((0, import_node_fs20.existsSync)(path2)) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
19503
+ (0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
18744
19504
  `, "utf8");
18745
19505
  return "written";
18746
19506
  } catch {
@@ -18877,7 +19637,7 @@ async function applyGcPlan(plan, remote) {
18877
19637
  cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
18878
19638
  beforeWorktrees,
18879
19639
  startingPath: branch.worktreePath,
18880
- pathExists: (p) => (0, import_node_fs19.existsSync)(p),
19640
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
18881
19641
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
18882
19642
  teardownWorktreeStage,
18883
19643
  deferredStore,
@@ -18900,7 +19660,7 @@ async function applyGcPlan(plan, remote) {
18900
19660
  for (const wt of plan.worktreeDirs) {
18901
19661
  try {
18902
19662
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
18903
- realpath: (path2) => (0, import_node_fs19.realpathSync)(path2)
19663
+ realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
18904
19664
  });
18905
19665
  if (!cleanupTarget.ok) {
18906
19666
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -18940,16 +19700,21 @@ async function requireFreshTrainCli(commandName) {
18940
19700
  throw new Error(staleTrainCliMessage(report, commandName));
18941
19701
  }
18942
19702
  var program2 = new Command();
18943
- program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
19703
+ program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
18944
19704
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
18945
- 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) => {
18946
- const path2 = (0, import_node_path19.join)(process.cwd(), ".gitignore");
18947
- const current = (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null;
19705
+ rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
19706
+ const path2 = (0, import_node_path21.join)(process.cwd(), ".gitignore");
19707
+ const current = (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null;
18948
19708
  const plan = planManagedGitignore(current);
18949
19709
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
19710
+ if (opts.json) {
19711
+ console.log(JSON.stringify(plan, null, 2));
19712
+ if (!opts.write && plan.changed) process.exitCode = 1;
19713
+ return;
19714
+ }
18950
19715
  if (opts.write) {
18951
19716
  if (plan.changed) {
18952
- (0, import_node_fs19.writeFileSync)(path2, plan.content, "utf8");
19717
+ (0, import_node_fs21.writeFileSync)(path2, plan.content, "utf8");
18953
19718
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
18954
19719
  } else {
18955
19720
  console.log("mmi-cli rules gitignore: up to date");
@@ -19114,7 +19879,7 @@ function runWorktreeInstall(command, cwd, quiet) {
19114
19879
  async function primaryCheckoutRoot(worktreeRoot) {
19115
19880
  try {
19116
19881
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
19117
- return out ? (0, import_node_path19.dirname)(out) : void 0;
19882
+ return out ? (0, import_node_path21.dirname)(out) : void 0;
19118
19883
  } catch {
19119
19884
  return void 0;
19120
19885
  }
@@ -19129,26 +19894,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
19129
19894
  function acquireWorktreeSetupLock(worktreeRoot) {
19130
19895
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
19131
19896
  const take = () => {
19132
- const fd = (0, import_node_fs19.openSync)(lockPath, "wx");
19897
+ const fd = (0, import_node_fs21.openSync)(lockPath, "wx");
19133
19898
  try {
19134
- (0, import_node_fs19.writeSync)(fd, String(Date.now()));
19899
+ (0, import_node_fs21.writeSync)(fd, String(Date.now()));
19135
19900
  } finally {
19136
- (0, import_node_fs19.closeSync)(fd);
19901
+ (0, import_node_fs21.closeSync)(fd);
19137
19902
  }
19138
19903
  return () => {
19139
19904
  try {
19140
- (0, import_node_fs19.rmSync)(lockPath, { force: true });
19905
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19141
19906
  } catch {
19142
19907
  }
19143
19908
  };
19144
19909
  };
19145
19910
  try {
19146
- (0, import_node_fs19.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
19911
+ (0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(lockPath), { recursive: true });
19147
19912
  return take();
19148
19913
  } catch {
19149
19914
  try {
19150
- if (Date.now() - (0, import_node_fs19.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
19151
- (0, import_node_fs19.rmSync)(lockPath, { force: true });
19915
+ if (Date.now() - (0, import_node_fs21.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
19916
+ (0, import_node_fs21.rmSync)(lockPath, { force: true });
19152
19917
  return take();
19153
19918
  }
19154
19919
  } catch {
@@ -19166,7 +19931,21 @@ worktree.command("create <branch>").description("create a worktree from a base r
19166
19931
  const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
19167
19932
  if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
19168
19933
  }
19169
- await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
19934
+ await addWorktreeRobust(wtPath, branch, base, {
19935
+ git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
19936
+ revParse: async (ref) => {
19937
+ try {
19938
+ return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
19939
+ } catch {
19940
+ return void 0;
19941
+ }
19942
+ },
19943
+ deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
19944
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
19945
+ log: (m) => {
19946
+ if (!o.json) console.error(` ${m}`);
19947
+ }
19948
+ });
19170
19949
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
19171
19950
  if (!o.json) console.error(` ${m}`);
19172
19951
  }));
@@ -19285,7 +20064,7 @@ async function attachToProject(issueNumber, repo, priority) {
19285
20064
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
19286
20065
  function scheduleRelatedDiscovery(o) {
19287
20066
  try {
19288
- const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
20067
+ const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
19289
20068
  if (o.repo) args.push("--repo", o.repo);
19290
20069
  spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
19291
20070
  } catch {
@@ -19638,7 +20417,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
19638
20417
  const vars = rawValues("--var");
19639
20418
  if (o.secretsFile) {
19640
20419
  try {
19641
- vars.push(`secrets=${(0, import_node_fs19.readFileSync)(o.secretsFile, "utf8")}`);
20420
+ vars.push(`secrets=${(0, import_node_fs21.readFileSync)(o.secretsFile, "utf8")}`);
19642
20421
  } catch (e) {
19643
20422
  return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
19644
20423
  }
@@ -19942,7 +20721,7 @@ issue.command("view <number>").description("read an issue as structured JSON \u2
19942
20721
  return fail(`issue view: ${raw}${argvNote}`);
19943
20722
  }
19944
20723
  });
19945
- issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
20724
+ issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").option("--fail-soft", "suppress errors (used by the background auto-caller; direct runs fail loudly)").action(async (o) => {
19946
20725
  const number = Number(o.number);
19947
20726
  if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
19948
20727
  const repo = await resolveRepo(o.repo);
@@ -19974,7 +20753,10 @@ issue.command("discover-related").description("find related issues for an existi
19974
20753
  ]);
19975
20754
  if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
19976
20755
  await execFileP2("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
19977
- } catch {
20756
+ } catch (e) {
20757
+ if (o.failSoft) return;
20758
+ const err = e;
20759
+ return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
19978
20760
  }
19979
20761
  });
19980
20762
  issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
@@ -20193,14 +20975,16 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
20193
20975
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
20194
20976
  console.log(JSON.stringify(created));
20195
20977
  });
20196
- pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
20978
+ pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').action(async (number, o) => {
20197
20979
  const n = Number(number);
20198
20980
  if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
20199
20981
  const repo = await resolveRepo(o.repo);
20200
20982
  if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
20201
- const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
20983
+ const fields = normalizeIssueViewJsonFields(o.json);
20984
+ const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
20985
+ const effective = fields === normalizeIssueViewJsonFields(void 0) ? defaultPrFields : fields;
20202
20986
  try {
20203
- const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
20987
+ const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
20204
20988
  console.log(JSON.stringify(data));
20205
20989
  } catch (e) {
20206
20990
  const err = e;
@@ -20208,9 +20992,9 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
20208
20992
  }
20209
20993
  });
20210
20994
  async function listCiWorkflowPaths(cwd = process.cwd()) {
20211
- const wfDir = (0, import_node_path19.join)(cwd, ".github", "workflows");
20212
- if (!(0, import_node_fs19.existsSync)(wfDir)) return [];
20213
- return (0, import_node_fs19.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
20995
+ const wfDir = (0, import_node_path21.join)(cwd, ".github", "workflows");
20996
+ if (!(0, import_node_fs21.existsSync)(wfDir)) return [];
20997
+ return (0, import_node_fs21.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
20214
20998
  }
20215
20999
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
20216
21000
  const repo = repoOpt ?? await resolveRepo();
@@ -20222,16 +21006,29 @@ async function resolveMergeCiPolicyForCheckout(repoOpt) {
20222
21006
  }
20223
21007
  function ciAuditDeps() {
20224
21008
  const cfgPromise = loadConfig();
21009
+ const root = hubRoot();
20225
21010
  return {
20226
21011
  client: defaultGitHubClient(),
20227
21012
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
20228
21013
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
20229
- // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
20230
- // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
20231
- // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
20232
- readSeedFile: (path2) => (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null
21014
+ // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
21015
+ // Resolve from the installed package root so the reconcile works from any org repo checkout; when the
21016
+ // Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
21017
+ // gate re-seed step is skipped gracefully rather than failing mid-run.
21018
+ readSeedFile: (path2) => {
21019
+ if (!root) return null;
21020
+ const fullPath = (0, import_node_path21.join)(root, path2);
21021
+ return (0, import_node_fs21.existsSync)(fullPath) ? (0, import_node_fs21.readFileSync)(fullPath, "utf8") : null;
21022
+ }
20233
21023
  };
20234
21024
  }
21025
+ function hubRoot() {
21026
+ const fromPkg = (0, import_node_path21.join)(__dirname, "..", "..");
21027
+ const marker = "skills/bootstrap/seeds/manifest.json";
21028
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(fromPkg, marker))) return fromPkg;
21029
+ if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), marker))) return process.cwd();
21030
+ return null;
21031
+ }
20235
21032
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
20236
21033
  const result = await resolveMergeCiPolicyForCheckout(o.repo);
20237
21034
  if (o.json) return printLine(JSON.stringify(result));
@@ -20305,6 +21102,12 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20305
21102
  checksPassing: checks === "success" || checks === "no-checks-reported"
20306
21103
  };
20307
21104
  },
21105
+ readPrState: async (prNumber, repo) => {
21106
+ const args = repo ? ["--repo", repo] : [];
21107
+ const result2 = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
21108
+ if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
21109
+ return JSON.parse(result2.state);
21110
+ },
20308
21111
  mergeAuto: async (prNumber, repo) => {
20309
21112
  const args = repo ? ["--repo", repo] : [];
20310
21113
  const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
@@ -20399,7 +21202,7 @@ async function createDeferredWorktreeStore() {
20399
21202
  },
20400
21203
  write: async (entries) => {
20401
21204
  try {
20402
- await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
21205
+ await (0, import_promises3.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
20403
21206
  await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
20404
21207
  } catch {
20405
21208
  }
@@ -20413,13 +21216,13 @@ var realWorktreeDirRemover = {
20413
21216
  probe: (p) => {
20414
21217
  let st;
20415
21218
  try {
20416
- st = (0, import_node_fs19.lstatSync)(p);
21219
+ st = (0, import_node_fs21.lstatSync)(p);
20417
21220
  } catch {
20418
21221
  return null;
20419
21222
  }
20420
21223
  if (st.isSymbolicLink()) return "link";
20421
21224
  try {
20422
- (0, import_node_fs19.readlinkSync)(p);
21225
+ (0, import_node_fs21.readlinkSync)(p);
20423
21226
  return "link";
20424
21227
  } catch {
20425
21228
  }
@@ -20427,7 +21230,7 @@ var realWorktreeDirRemover = {
20427
21230
  },
20428
21231
  readdir: (p) => {
20429
21232
  try {
20430
- return (0, import_node_fs19.readdirSync)(p);
21233
+ return (0, import_node_fs21.readdirSync)(p);
20431
21234
  } catch {
20432
21235
  return [];
20433
21236
  }
@@ -20436,9 +21239,9 @@ var realWorktreeDirRemover = {
20436
21239
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20437
21240
  detachLink: (p) => {
20438
21241
  try {
20439
- (0, import_node_fs19.rmdirSync)(p);
21242
+ (0, import_node_fs21.rmdirSync)(p);
20440
21243
  } catch {
20441
- (0, import_node_fs19.unlinkSync)(p);
21244
+ (0, import_node_fs21.unlinkSync)(p);
20442
21245
  }
20443
21246
  },
20444
21247
  removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -20468,9 +21271,9 @@ async function worktreeHasStageState(worktreePath) {
20468
21271
  }
20469
21272
  }
20470
21273
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20471
- if (!(0, import_node_fs19.existsSync)(statePath)) return false;
21274
+ if (!(0, import_node_fs21.existsSync)(statePath)) return false;
20472
21275
  try {
20473
- const state = JSON.parse((0, import_node_fs19.readFileSync)(statePath, "utf8"));
21276
+ const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
20474
21277
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20475
21278
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20476
21279
  } catch {
@@ -20542,7 +21345,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20542
21345
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
20543
21346
  beforeWorktrees,
20544
21347
  startingPath,
20545
- pathExists: (p) => (0, import_node_fs19.existsSync)(p),
21348
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
20546
21349
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20547
21350
  teardownWorktreeStage,
20548
21351
  deferredStore,
@@ -20763,7 +21566,7 @@ function stageScopedRunOpts(o) {
20763
21566
  };
20764
21567
  }
20765
21568
  function printLine(value) {
20766
- (0, import_node_fs19.writeSync)(1, `${value}
21569
+ (0, import_node_fs21.writeSync)(1, `${value}
20767
21570
  `);
20768
21571
  }
20769
21572
  function stageKeepAlive() {
@@ -20777,8 +21580,8 @@ async function resolveStage() {
20777
21580
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
20778
21581
  return decideStage({
20779
21582
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
20780
- hasCompose: (0, import_node_fs19.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
20781
- hasEnvExample: (0, import_node_fs19.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
21583
+ hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), "docker-compose.yml")),
21584
+ hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), ".env.example"))
20782
21585
  });
20783
21586
  }
20784
21587
  async function fetchStageVaultEnvMerge() {
@@ -20819,10 +21622,10 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
20819
21622
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
20820
21623
  return;
20821
21624
  }
20822
- const infra = resolvePortRangeInfra(process.cwd());
21625
+ const infra = resolvePortRangeInfra(process.cwd(), __dirname);
20823
21626
  if (!infra) {
20824
21627
  return failGraceful(
20825
- `port-range: no MMI-Hub allocator files found from ${process.cwd()}; run from an MMI-Hub checkout instead: mmi-cli port-range ${repo}${o.json ? " --json" : ""}`
21628
+ `port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
20826
21629
  );
20827
21630
  }
20828
21631
  const path2 = infra.registryPath;
@@ -21216,7 +22019,7 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
21216
22019
  if (!audit.ok) process.exitCode = 1;
21217
22020
  });
21218
22021
  var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
21219
- wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (pagesDir, o) => {
22022
+ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").option("--json", "machine-readable output").action(async (pagesDir, o) => {
21220
22023
  const repo = await resolveRepo(o.repo);
21221
22024
  if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
21222
22025
  const cfg = await loadConfig();
@@ -21227,13 +22030,33 @@ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:
21227
22030
  log: (msg) => console.log(msg),
21228
22031
  err: (msg) => console.error(msg)
21229
22032
  },
21230
- { repo, pagesDir }
22033
+ { repo, pagesDir, json: o.json }
22034
+ );
22035
+ if (!ok) process.exitCode = 1;
22036
+ });
22037
+ wiki.command("plan").description("compute the release wiki plan for the cwd repo \u2014 collect its opt-in `wiki:page` pages, read the previous incremental stamp from <repo>.wiki.git (scoped minted token), and print a JSON plan naming which pages changed plus the fresh full stamp. Runs from ANY org repo checkout (no repo-local script). Fails loud on the credential/read gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (o) => {
22038
+ const repo = await resolveRepo(o.repo);
22039
+ if (!repo) return fail("wiki plan: could not determine the target repo (pass --repo owner/name)");
22040
+ const cfg = await loadConfig();
22041
+ const readers = fsReaders(process.cwd());
22042
+ const ok = await wikiPlan(
22043
+ {
22044
+ collectPages: () => collectFeaturePages(readers),
22045
+ readSource: (p) => readers.readDoc(p),
22046
+ mintAndReadStamp: async (r) => {
22047
+ const minted = await mintWikiToken(r, registryClientDeps(cfg));
22048
+ if (!minted.ok) return { ok: false, error: minted.error };
22049
+ return readWikiStampViaGit(realWikiGitPublishDeps(), { repo: r, token: minted.mint.token });
22050
+ },
22051
+ out: (json) => console.log(json),
22052
+ err: (msg) => console.error(msg)
22053
+ },
22054
+ { repo }
21231
22055
  );
21232
22056
  if (!ok) process.exitCode = 1;
21233
22057
  });
21234
- var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
22058
+ var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action((o) => {
21235
22059
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
21236
- if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
21237
22060
  if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
21238
22061
  const steps = bootstrapPlan(o.repo, o.class);
21239
22062
  console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
@@ -21250,7 +22073,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
21250
22073
  client: defaultGitHubClient(),
21251
22074
  projectMeta: meta,
21252
22075
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
21253
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
22076
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
21254
22077
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
21255
22078
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
21256
22079
  requiredGcpApis: (() => {
@@ -21283,7 +22106,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
21283
22106
  else console.log(renderOrgRulesetDriftReport(plan));
21284
22107
  if (plan.action !== "noop") process.exitCode = 1;
21285
22108
  });
21286
- bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
22109
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
21287
22110
  const o = {
21288
22111
  class: rawValue("--class", "deployable"),
21289
22112
  projectType: rawValue("--project-type", ""),
@@ -21301,12 +22124,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
21301
22124
  return fail(`bootstrap apply: ${e.message}`);
21302
22125
  }
21303
22126
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
21304
- if (!(0, import_node_fs19.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
21305
- const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
22127
+ if (!(0, import_node_fs21.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
22128
+ const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
21306
22129
  const baseBranch = o.class === "content" ? "main" : "development";
21307
22130
  const slug = parsedRepo.slug;
21308
22131
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
21309
- const readFile2 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
22132
+ const readFile2 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
21310
22133
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
21311
22134
  const rawVars = {};
21312
22135
  for (const value of rawValues("--var")) {
@@ -21603,16 +22426,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21603
22426
  const repoClass = o.class;
21604
22427
  targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
21605
22428
  } else {
21606
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs19.existsSync)("projects.json") ? (0, import_node_fs19.readFileSync)("projects.json", "utf8") : null;
22429
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs21.existsSync)("projects.json") ? (0, import_node_fs21.readFileSync)("projects.json", "utf8") : null;
21607
22430
  if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
21608
- const fanoutJson = (0, import_node_fs19.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs19.readFileSync)(".github/fanout-targets.json", "utf8") : null;
22431
+ const fanoutJson = (0, import_node_fs21.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs21.readFileSync)(".github/fanout-targets.json", "utf8") : null;
21609
22432
  targets = loadAccessTargets(projectsJson, fanoutJson);
21610
22433
  }
21611
22434
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
21612
- const fileMatrix = (0, import_node_fs19.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs19.readFileSync)("access-matrix.json", "utf8")) : {};
22435
+ const fileMatrix = (0, import_node_fs21.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs21.readFileSync)("access-matrix.json", "utf8")) : {};
21613
22436
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
21614
22437
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
21615
- const fileContracts = (0, import_node_fs19.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs19.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
22438
+ const fileContracts = (0, import_node_fs21.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs21.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
21616
22439
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
21617
22440
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
21618
22441
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -21620,7 +22443,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
21620
22443
  });
21621
22444
  access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
21622
22445
  var isWin2 = process.platform === "win32";
21623
- program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
22446
+ program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action((opts) => (
21624
22447
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
21625
22448
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
21626
22449
  ));
@@ -21629,9 +22452,13 @@ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugi
21629
22452
  program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
21630
22453
  if (isInsideRepoSubdir(process.cwd())) {
21631
22454
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
22455
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
22456
+ return;
22457
+ }
22458
+ if (!await isOrgRepoRoot()) {
22459
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (non-org repo)" });
21632
22460
  return;
21633
22461
  }
21634
- if (!await isOrgRepoRoot()) return;
21635
22462
  spawnDeferredGcSweep();
21636
22463
  const { parallel, sequential } = buildSessionStartPlan({
21637
22464
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
@@ -21651,6 +22478,21 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21651
22478
  // never costs banner time and next session's glance renders instantly within budget.
21652
22479
  scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
21653
22480
  }),
22481
+ // command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
22482
+ // not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
22483
+ commandLadderHint: async (io) => {
22484
+ const hint = commandLadderHint();
22485
+ if (hint) io.log(hint);
22486
+ },
22487
+ // local train sync (#2582): ff local development/main/rc to origin so this checkout never lags a
22488
+ // release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
22489
+ // fail-soft, silent unless a branch moved.
22490
+ syncTrain: async (io) => {
22491
+ const gitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
22492
+ const result = await syncLocalTrainBranches(gitRun, { warn: (m) => io.err(`[mmi-hook] train sync: ${m}`) });
22493
+ const line = localTrainSyncBannerLine(result);
22494
+ if (line) io.log(line);
22495
+ },
21654
22496
  doctor: (io) => runDoctor({ banner: true }, io)
21655
22497
  });
21656
22498
  await runSessionStart(parallel, sequential, consoleIo);
@@ -21660,6 +22502,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
21660
22502
  spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
21661
22503
  consoleIo.log(worktreeBanner);
21662
22504
  }
22505
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
21663
22506
  });
21664
22507
  installProcessBackstop();
21665
22508
  program2.parseAsync().catch((e) => failGraceful(e.message));