@mutmutco/cli 3.51.0 → 3.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +1222 -549
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
34
34
  awsCallerArn: () => awsCallerArn,
35
35
  classifyParseError: () => classifyParseError,
36
+ envHealLockPath: () => envHealLockPath,
36
37
  gcPlan: () => gcPlan,
37
38
  isOrgRegisteredRepo: () => isOrgRegisteredRepo,
38
39
  registryClientDeps: () => registryClientDeps,
@@ -3409,8 +3410,8 @@ function useColor() {
3409
3410
  var program = new Command();
3410
3411
 
3411
3412
  // src/index.ts
3412
- var import_promises7 = require("node:fs/promises");
3413
- var import_node_fs30 = require("node:fs");
3413
+ var import_promises8 = require("node:fs/promises");
3414
+ var import_node_fs32 = require("node:fs");
3414
3415
  var import_node_child_process13 = require("node:child_process");
3415
3416
 
3416
3417
  // src/cli-shared.ts
@@ -3820,7 +3821,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3820
3821
  const attempts = opts.attempts ?? 3;
3821
3822
  const baseDelayMs = opts.baseDelayMs ?? 250;
3822
3823
  const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3823
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
3824
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
3824
3825
  let lastErr;
3825
3826
  for (let i = 0; i < attempts; i++) {
3826
3827
  const isLast = i === attempts - 1;
@@ -3828,14 +3829,14 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3828
3829
  try {
3829
3830
  const res = await fetchImpl(url, attemptInit);
3830
3831
  if (!isLast && retryOn(res)) {
3831
- await sleep2(baseDelayMs * 2 ** i);
3832
+ await sleep3(baseDelayMs * 2 ** i);
3832
3833
  continue;
3833
3834
  }
3834
3835
  return res;
3835
3836
  } catch (e) {
3836
3837
  lastErr = e;
3837
3838
  if (isLast) throw e;
3838
- await sleep2(baseDelayMs * 2 ** i);
3839
+ await sleep3(baseDelayMs * 2 ** i);
3839
3840
  }
3840
3841
  }
3841
3842
  throw lastErr;
@@ -3860,13 +3861,23 @@ function defaultHubSessionCachePath(env = process.env) {
3860
3861
  const base = env.XDG_STATE_HOME || (0, import_node_path3.join)((0, import_node_os.homedir)(), ".local", "state");
3861
3862
  return (0, import_node_path3.join)(base, "mmi-cli", "hub-session.json");
3862
3863
  }
3864
+ function roleFromToken(token) {
3865
+ const part = token?.split(".")[1];
3866
+ if (!part) return void 0;
3867
+ try {
3868
+ const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
3869
+ return claims.role === "owner" ? "owner" : void 0;
3870
+ } catch {
3871
+ return void 0;
3872
+ }
3873
+ }
3863
3874
  function readCache(path2, apiUrl, now, githubTokenFingerprint) {
3864
3875
  try {
3865
3876
  const session = JSON.parse((0, import_node_fs3.readFileSync)(path2, "utf8"));
3866
3877
  if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
3867
3878
  if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
3868
3879
  if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
3869
- return session;
3880
+ return { ...session, role: roleFromToken(session.token) };
3870
3881
  } catch {
3871
3882
  return null;
3872
3883
  }
@@ -3913,6 +3924,9 @@ async function hubAuthSession(deps) {
3913
3924
  token: body.token,
3914
3925
  expiresAt: body.expiresAt,
3915
3926
  login: typeof body.login === "string" ? body.login : void 0,
3927
+ // #3513: from the SIGNED token, not the response body — one rule at every hop, so the cache
3928
+ // read and the network read cannot disagree about what counts as an assertion.
3929
+ role: roleFromToken(body.token),
3916
3930
  apiUrl,
3917
3931
  githubTokenFingerprint
3918
3932
  };
@@ -5084,6 +5098,26 @@ var import_node_fs10 = require("node:fs");
5084
5098
 
5085
5099
  // src/gc.ts
5086
5100
  var import_node_path7 = require("node:path");
5101
+ var AGENT_ARTIFACT_PREFIXES = [".jerv/"];
5102
+ function classifyWorktreeDirt(porcelain) {
5103
+ const lines = porcelain.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0);
5104
+ if (!lines.length) return "clean";
5105
+ if (lines.some((l) => l.slice(0, 2) !== "??")) return "tracked-changes";
5106
+ const isArtifact = (path2) => AGENT_ARTIFACT_PREFIXES.some((prefix) => (
5107
+ // Equals the prefix without its trailing slash (`.jerv`) or sits under it (`.jerv/attest/x.json`);
5108
+ // a sibling like `.jerv-lease.json` must NOT match by string-prefix alone.
5109
+ path2 === prefix.slice(0, -1) || path2.startsWith(prefix)
5110
+ ));
5111
+ const untrackedPaths = lines.map((l) => {
5112
+ let path2 = l.slice(3);
5113
+ if (path2.length >= 2 && path2.startsWith('"') && path2.endsWith('"')) path2 = path2.slice(1, -1);
5114
+ return path2;
5115
+ });
5116
+ return untrackedPaths.every(isArtifact) ? "artifacts-only" : "untracked-files";
5117
+ }
5118
+ function isRemovableDirt(dirt) {
5119
+ return dirt === "clean" || dirt === "artifacts-only";
5120
+ }
5087
5121
  var DEFERRED_SWEEP_COMMAND = "mmi-cli worktree gc sweep-deferred";
5088
5122
  var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
5089
5123
  var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
@@ -5144,7 +5178,7 @@ async function sweepDeferredWorktrees(store, deps) {
5144
5178
  if (!entries.length) return { removed: [], stillDeferred: [], skipped: [] };
5145
5179
  let live;
5146
5180
  try {
5147
- live = parseWorktreePorcelain(await deps.git(["worktree", "list", "--porcelain"]));
5181
+ live = parseWorktreePorcelainEntries(await deps.git(["worktree", "list", "--porcelain"]));
5148
5182
  } catch {
5149
5183
  return { removed: [], stillDeferred: entries, skipped: [] };
5150
5184
  }
@@ -5158,8 +5192,8 @@ async function sweepDeferredWorktrees(store, deps) {
5158
5192
  continue;
5159
5193
  }
5160
5194
  if (current) {
5161
- const dirty = (await deps.git(["-C", entry.path, "status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
5162
- if (dirty) {
5195
+ const porcelain = await deps.git(["-C", entry.path, "status", "--porcelain"]).catch(() => void 0);
5196
+ if (!isRemovableDirt(porcelain === void 0 ? "tracked-changes" : classifyWorktreeDirt(porcelain))) {
5163
5197
  stillDeferred.push(entry);
5164
5198
  continue;
5165
5199
  }
@@ -5179,7 +5213,7 @@ async function sweepDeferredWorktrees(store, deps) {
5179
5213
  async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
5180
5214
  const attempts = opts.attempts ?? 4;
5181
5215
  const backoff = opts.backoffMs ?? [500, 2e3, 5e3, 1e4];
5182
- const sleep2 = opts.sleep ?? defaultSleep;
5216
+ const sleep3 = opts.sleep ?? defaultSleep;
5183
5217
  const removed = [];
5184
5218
  const skipped = [];
5185
5219
  let stillDeferred = [];
@@ -5189,7 +5223,7 @@ async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
5189
5223
  skipped.push(...result.skipped);
5190
5224
  stillDeferred = result.stillDeferred;
5191
5225
  if (!stillDeferred.length) break;
5192
- if (i < attempts - 1) await sleep2(backoff[Math.min(i, backoff.length - 1)]);
5226
+ if (i < attempts - 1) await sleep3(backoff[Math.min(i, backoff.length - 1)]);
5193
5227
  }
5194
5228
  return { removed, stillDeferred, skipped };
5195
5229
  }
@@ -5281,7 +5315,7 @@ function buildRemoteBranchCleanupReport(branch, input) {
5281
5315
  return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
5282
5316
  }
5283
5317
  async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, options = {}) {
5284
- const sleep2 = options.sleep ?? defaultSleep;
5318
+ const sleep3 = options.sleep ?? defaultSleep;
5285
5319
  const maxAttempts = options.maxAttempts ?? 5;
5286
5320
  const backoff = [500, 1e3, 1500, 2e3];
5287
5321
  if (!input.attempted) {
@@ -5292,7 +5326,7 @@ async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, option
5292
5326
  }
5293
5327
  let existsAfter = await deps.exists(branch, { prune: true });
5294
5328
  for (let i = 1; i < maxAttempts && existsAfter === true; i++) {
5295
- await sleep2(backoff[Math.min(i - 1, backoff.length - 1)]);
5329
+ await sleep3(backoff[Math.min(i - 1, backoff.length - 1)]);
5296
5330
  existsAfter = await deps.exists(branch, { prune: true });
5297
5331
  }
5298
5332
  return buildRemoteBranchCleanupReport(branch, {
@@ -5688,7 +5722,7 @@ function parseBranchHeadRows(stdout) {
5688
5722
  }
5689
5723
  return out;
5690
5724
  }
5691
- function parseWorktreePorcelain(stdout) {
5725
+ function parseWorktreePorcelainEntries(stdout) {
5692
5726
  const out = [];
5693
5727
  for (const block of stdout.split(/\r?\n(?=worktree )/)) {
5694
5728
  let path2 = "";
@@ -5697,15 +5731,24 @@ function parseWorktreePorcelain(stdout) {
5697
5731
  if (line.startsWith("worktree ")) path2 = line.slice("worktree ".length).trim();
5698
5732
  if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length).trim();
5699
5733
  }
5700
- if (path2 && branch) out.push({ path: path2, branch });
5734
+ if (path2) out.push(branch ? { path: path2, branch } : { path: path2 });
5701
5735
  }
5702
5736
  return out;
5703
5737
  }
5704
- function samePath(a, b) {
5705
- return a.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase() === b.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
5738
+ function parseWorktreePorcelain(stdout) {
5739
+ return parseWorktreePorcelainEntries(stdout).filter(
5740
+ (w) => typeof w.branch === "string"
5741
+ );
5706
5742
  }
5707
- function normPath(p) {
5708
- return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
5743
+ function pathsAreCaseInsensitive(platform2 = process.platform) {
5744
+ return platform2 === "win32" || platform2 === "darwin";
5745
+ }
5746
+ function normPath(p, platform2 = process.platform) {
5747
+ const unified = p.replace(/\\/g, "/").replace(/\/+$/, "");
5748
+ return pathsAreCaseInsensitive(platform2) ? unified.toLowerCase() : unified;
5749
+ }
5750
+ function samePath(a, b, platform2 = process.platform) {
5751
+ return normPath(a, platform2) === normPath(b, platform2);
5709
5752
  }
5710
5753
  function toNativePath(p) {
5711
5754
  return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
@@ -5753,7 +5796,7 @@ function selectWorktreeComposeProjects(worktreePath, projects) {
5753
5796
  return names;
5754
5797
  }
5755
5798
  function deriveComposeProjectName(worktreePath) {
5756
- const norm = normPath(worktreePath);
5799
+ const norm = normPath(worktreePath).toLowerCase();
5757
5800
  if (!norm) return void 0;
5758
5801
  const base = norm.slice(norm.lastIndexOf("/") + 1);
5759
5802
  const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
@@ -5862,9 +5905,10 @@ async function fastForwardCurrentBranch(git) {
5862
5905
  }
5863
5906
  async function returnCheckoutToBase(git, base, mergedBranch, currentBranch2) {
5864
5907
  if (currentBranch2 === mergedBranch) {
5865
- const dirty = (await git(["status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
5866
- if (dirty) {
5867
- return { report: { branch: base, switched: false, sync: "skipped", reason: "dirty-worktree" }, canDeleteBranch: false };
5908
+ const porcelain = await git(["status", "--porcelain"]).catch(() => void 0);
5909
+ const dirt = porcelain === void 0 ? "tracked-changes" : classifyWorktreeDirt(porcelain);
5910
+ if (!isRemovableDirt(dirt)) {
5911
+ return { report: { branch: base, switched: false, sync: "skipped", reason: dirt === "untracked-files" ? "untracked-files" : "dirty-worktree" }, canDeleteBranch: false };
5868
5912
  }
5869
5913
  try {
5870
5914
  await git(["checkout", base]);
@@ -5947,20 +5991,22 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5947
5991
  return report;
5948
5992
  }
5949
5993
  const worktreeDirtyAtActTime = async (path2) => {
5950
- if (options.pathExists && !options.pathExists(path2)) return false;
5951
- if (options.isWorktreeDirty) return options.isWorktreeDirty(path2);
5994
+ if (options.pathExists && !options.pathExists(path2)) return void 0;
5995
+ if (options.isWorktreeDirty) return await options.isWorktreeDirty(path2) ? "tracked-changes" : void 0;
5952
5996
  try {
5953
- return (await options.execGit(["-C", path2, "status", "--porcelain"]) || "").trim().length > 0;
5997
+ return classifyWorktreeDirt(await options.execGit(["-C", path2, "status", "--porcelain"]));
5954
5998
  } catch {
5955
- return true;
5999
+ return "tracked-changes";
5956
6000
  }
5957
6001
  };
5958
6002
  if (wtPath && mainWorktreeTarget) {
5959
6003
  report.worktree = { path: wtPath, status: "not-attempted", reason: "main-worktree" };
5960
6004
  } else if (wtPath) {
5961
- if (await worktreeDirtyAtActTime(wtPath)) {
5962
- report.worktree = { path: wtPath, status: "not-attempted", reason: "dirty-worktree" };
5963
- report.localBranch = { name: branch, status: "not-attempted", reason: "dirty-worktree" };
6005
+ const dirt = await worktreeDirtyAtActTime(wtPath);
6006
+ if (dirt !== void 0 && !isRemovableDirt(dirt)) {
6007
+ const reason = dirt === "untracked-files" ? "untracked-files" : "dirty-worktree";
6008
+ report.worktree = { path: wtPath, status: "not-attempted", reason };
6009
+ report.localBranch = { name: branch, status: "not-attempted", reason };
5964
6010
  return report;
5965
6011
  }
5966
6012
  const removeDeps = {
@@ -6183,11 +6229,27 @@ function resolveReleaseTrack(meta, hints, repo) {
6183
6229
  if (inferred) return inferred;
6184
6230
  return "full";
6185
6231
  }
6232
+ function metaWithDeclaredClass(meta, declaredClass) {
6233
+ if (!declaredClass) return meta;
6234
+ const stated = typeof meta?.releaseTrack === "string" || typeof meta?.class === "string" || typeof meta?.deployModel === "string";
6235
+ if (stated) return meta;
6236
+ return { ...meta ?? {}, class: declaredClass };
6237
+ }
6186
6238
  function branchesForTrack(track) {
6187
6239
  if (track === "trunk") return ["main"];
6188
6240
  if (track === "direct") return ["development", "main"];
6189
6241
  return ["development", "rc", "main"];
6190
6242
  }
6243
+ function promotionBranchesForTrack(track) {
6244
+ if (track === "trunk") return [];
6245
+ if (track === "direct") return ["main"];
6246
+ return ["rc", "main"];
6247
+ }
6248
+ function isPromotionBase(base, track) {
6249
+ if (!base) return false;
6250
+ if (!track) return base !== "development";
6251
+ return promotionBranchesForTrack(track).includes(base);
6252
+ }
6191
6253
 
6192
6254
  // src/readiness-audit.ts
6193
6255
  var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
@@ -7443,7 +7505,7 @@ async function worktreeBranches() {
7443
7505
  let dirty = true;
7444
7506
  try {
7445
7507
  const { stdout: status } = await execFileP2("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7446
- dirty = status.trim().length > 0;
7508
+ dirty = !isRemovableDirt(classifyWorktreeDirt(status));
7447
7509
  } catch {
7448
7510
  dirty = true;
7449
7511
  }
@@ -7859,6 +7921,12 @@ function parseIssueSelector(selector, defaultRepo) {
7859
7921
  if (local) return { repo: defaultRepo, number: Number(local[1]) };
7860
7922
  throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
7861
7923
  }
7924
+ function sameRepo(itemRepo, selectorKey) {
7925
+ const repo = itemRepo.toLowerCase();
7926
+ if (repo === selectorKey) return true;
7927
+ if (selectorKey.includes("/")) return false;
7928
+ return repo.slice(repo.indexOf("/") + 1) === selectorKey;
7929
+ }
7862
7930
  function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
7863
7931
  const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
7864
7932
  const groups = { primary: empty(), secondary: empty() };
@@ -7866,7 +7934,7 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
7866
7934
  const repoKey = currentRepo.toLowerCase();
7867
7935
  for (const item of items) {
7868
7936
  if (!ACTIVE_STATUSES.has(item.status)) continue;
7869
- const scope = item.repository.toLowerCase() === repoKey ? "primary" : "secondary";
7937
+ const scope = sameRepo(item.repository, repoKey) ? "primary" : "secondary";
7870
7938
  const assignees = item.assignees.map((a) => a.toLowerCase());
7871
7939
  const assignedToViewer = assignees.includes(viewerKey);
7872
7940
  if (assignedToViewer) {
@@ -8413,11 +8481,11 @@ var defaultRetrySleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, m
8413
8481
  async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
8414
8482
  const attempts = Math.max(1, opts.attempts ?? 5);
8415
8483
  const delayMs = opts.delayMs ?? 300;
8416
- const sleep2 = opts.sleep ?? defaultRetrySleep;
8484
+ const sleep3 = opts.sleep ?? defaultRetrySleep;
8417
8485
  for (let attempt = 0; attempt < attempts; attempt += 1) {
8418
8486
  const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
8419
8487
  if (itemId) return itemId;
8420
- if (attempt < attempts - 1) await sleep2(delayMs * (attempt + 1));
8488
+ if (attempt < attempts - 1) await sleep3(delayMs * (attempt + 1));
8421
8489
  }
8422
8490
  return void 0;
8423
8491
  }
@@ -9000,9 +9068,196 @@ async function gatherStaleWorktreeWarning(gitRun = defaultGitRun) {
9000
9068
  }
9001
9069
  }
9002
9070
 
9003
- // src/hook-activity.ts
9071
+ // src/released-version-cache.ts
9004
9072
  var import_node_fs12 = require("node:fs");
9005
9073
  var import_node_path10 = require("node:path");
9074
+ var RELEASED_VERSION_CACHE_MS = 24 * 36e5;
9075
+ function releasedVersionCachePath(runtimeRoot) {
9076
+ return (0, import_node_path10.join)(runtimeRoot, "head-ts", ".released-version");
9077
+ }
9078
+ function readReleasedVersionCache(cachePath, now = Date.now(), read = import_node_fs12.readFileSync) {
9079
+ let parsed;
9080
+ try {
9081
+ parsed = JSON.parse(read(cachePath, "utf8"));
9082
+ } catch {
9083
+ return void 0;
9084
+ }
9085
+ if (!parsed || typeof parsed !== "object") return void 0;
9086
+ const { version, at } = parsed;
9087
+ if (typeof version !== "string" || !version.trim()) return void 0;
9088
+ if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
9089
+ const age = now - at;
9090
+ if (age < 0 || age >= RELEASED_VERSION_CACHE_MS) return void 0;
9091
+ return { version, at };
9092
+ }
9093
+ function writeReleasedVersionCache(cachePath, version, now = Date.now()) {
9094
+ try {
9095
+ (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(cachePath), { recursive: true });
9096
+ (0, import_node_fs12.writeFileSync)(cachePath, JSON.stringify({ version, at: now }), "utf8");
9097
+ } catch {
9098
+ }
9099
+ }
9100
+ function cacheAgeHours(at, now = Date.now()) {
9101
+ return Math.max(0, Math.floor((now - at) / 36e5));
9102
+ }
9103
+ function cachedReadNote(cachedAt, now = Date.now()) {
9104
+ const hours = cacheAgeHours(cachedAt, now);
9105
+ return `(cached ${hours < 1 ? "<1" : hours}h ago \u2014 the banner reads npm at most once a day)`;
9106
+ }
9107
+
9108
+ // src/marketplace-autoupdate.ts
9109
+ var KNOWN_MARKETPLACES_RELATIVE = [".claude", "plugins", "known_marketplaces.json"];
9110
+ var MMI_MARKETPLACE_NAME = "mutmutco";
9111
+ function readFileSyncSafe(path2, read) {
9112
+ try {
9113
+ return read(path2, "utf8");
9114
+ } catch {
9115
+ return void 0;
9116
+ }
9117
+ }
9118
+ var AUTO_UPDATE_DEFAULT_ON = /* @__PURE__ */ new Set([
9119
+ "claude-plugins-official",
9120
+ "claude-code-marketplace",
9121
+ "claude-code-plugins",
9122
+ "anthropic-marketplace",
9123
+ "anthropic-plugins",
9124
+ "agent-skills",
9125
+ "anthropic-agent-skills"
9126
+ ]);
9127
+ function resolveAutoUpdate(probe) {
9128
+ const { name, registered, declared, settingsDeclared } = probe;
9129
+ if (!registered) {
9130
+ return { name, effective: false, reason: "not registered \u2014 nothing to update" };
9131
+ }
9132
+ if (typeof declared === "boolean") {
9133
+ return { name, effective: declared, reason: `declared ${declared} in known_marketplaces.json` };
9134
+ }
9135
+ if (typeof settingsDeclared === "boolean") {
9136
+ return { name, effective: settingsDeclared, reason: `declared ${settingsDeclared} under extraKnownMarketplaces.${name}` };
9137
+ }
9138
+ if (AUTO_UPDATE_DEFAULT_ON.has(name.toLowerCase())) {
9139
+ return { name, effective: true, reason: "undeclared \u2014 an official Anthropic marketplace, on by default" };
9140
+ }
9141
+ return {
9142
+ name,
9143
+ effective: false,
9144
+ reason: "undeclared \u2014 third-party marketplaces are OFF by default, so nothing auto-updates and no update notice ever prints"
9145
+ };
9146
+ }
9147
+ function readKnownMarketplace(raw, name) {
9148
+ if (!raw) return { registered: false };
9149
+ let parsed;
9150
+ try {
9151
+ parsed = JSON.parse(raw);
9152
+ } catch {
9153
+ return { registered: false };
9154
+ }
9155
+ if (!parsed || typeof parsed !== "object") return { registered: false };
9156
+ const entry = parsed[name];
9157
+ if (!entry || typeof entry !== "object") return { registered: false };
9158
+ const { autoUpdate, source } = entry;
9159
+ const ref = source && typeof source === "object" ? source.ref : void 0;
9160
+ return {
9161
+ registered: true,
9162
+ ...typeof autoUpdate === "boolean" ? { declared: autoUpdate } : {},
9163
+ ...typeof ref === "string" && ref ? { ref } : {}
9164
+ };
9165
+ }
9166
+ function readSettingsAutoUpdate(raw, name) {
9167
+ if (!raw) return void 0;
9168
+ let parsed;
9169
+ try {
9170
+ parsed = JSON.parse(raw);
9171
+ } catch {
9172
+ return void 0;
9173
+ }
9174
+ const extra = parsed?.extraKnownMarketplaces;
9175
+ if (!extra || typeof extra !== "object") return void 0;
9176
+ const entry = extra[name];
9177
+ if (!entry || typeof entry !== "object") return void 0;
9178
+ const { autoUpdate } = entry;
9179
+ return typeof autoUpdate === "boolean" ? autoUpdate : void 0;
9180
+ }
9181
+ var AUTO_UPDATE_TOGGLE_STEPS = "`/plugin` \u2192 Marketplaces tab \u2192 select the marketplace \u2192 Enable auto-update";
9182
+ var CATALOG_CONTENT_REF = "main";
9183
+ var CATALOG_REF_PIN_STEPS = `add \`"ref": "${CATALOG_CONTENT_REF}"\` to this marketplace's \`source\` in ~/.claude/plugins/known_marketplaces.json, then restart Claude`;
9184
+ function resolveCatalogRef(probe) {
9185
+ const { name, registered, ref, autoUpdate } = probe;
9186
+ if (!registered) return void 0;
9187
+ if (ref) {
9188
+ const agrees = ref === CATALOG_CONTENT_REF;
9189
+ return {
9190
+ name,
9191
+ ref,
9192
+ autoUpdate,
9193
+ unpinned: false,
9194
+ agrees,
9195
+ reason: agrees ? `pinned to ${ref} \u2014 catalog and plugin content come from the same branch` : `pinned to ${ref} \u2014 plugin content is pinned to ${CATALOG_CONTENT_REF}, so the two disagree`
9196
+ };
9197
+ }
9198
+ return {
9199
+ name,
9200
+ autoUpdate,
9201
+ unpinned: true,
9202
+ agrees: false,
9203
+ reason: `no ref pinned \u2014 the catalog is served from the repo's default branch while plugin content is pinned to ${CATALOG_CONTENT_REF}`
9204
+ };
9205
+ }
9206
+ function checkMarketplaceAutoUpdate(probe) {
9207
+ if (!probe) return null;
9208
+ const evidence = [`${probe.name}: auto-update ${probe.effective ? "on" : "off"} \u2014 ${probe.reason}`];
9209
+ if (probe.effective) {
9210
+ return { ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
9211
+ }
9212
+ return {
9213
+ ok: true,
9214
+ warn: true,
9215
+ label: `marketplace auto-update (${probe.name})`,
9216
+ detail: `off \u2014 this machine will not pick up a new plugin release on its own; turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`,
9217
+ verbose: evidence
9218
+ };
9219
+ }
9220
+ function checkMarketplaceCatalogRef(probe) {
9221
+ if (!probe) return null;
9222
+ const label = `marketplace catalog ref (${probe.name})`;
9223
+ const evidence = [`${probe.name}: ${probe.reason}`, `auto-update: ${probe.autoUpdate ? "on" : "off"}`];
9224
+ if (probe.agrees) {
9225
+ return { ok: true, label, detail: `${probe.ref}`, verbose: evidence };
9226
+ }
9227
+ const drift = probe.unpinned ? "unpinned" : `pinned to ${probe.ref}, not ${CATALOG_CONTENT_REF}`;
9228
+ if (probe.autoUpdate) {
9229
+ return {
9230
+ ok: false,
9231
+ label,
9232
+ fix: `${drift} while auto-update is ON \u2014 this machine installs plugin releases advertised by a branch nobody released; ${CATALOG_REF_PIN_STEPS}`,
9233
+ verbose: evidence
9234
+ };
9235
+ }
9236
+ return {
9237
+ ok: true,
9238
+ warn: true,
9239
+ label,
9240
+ detail: `${drift} \u2014 harmless while auto-update is off, and the thing to fix before turning it on; ${CATALOG_REF_PIN_STEPS}`,
9241
+ verbose: evidence
9242
+ };
9243
+ }
9244
+ function marketplaceRows(name, known, settings) {
9245
+ const { registered, declared, ref } = readKnownMarketplace(known, name);
9246
+ const autoUpdate = resolveAutoUpdate({
9247
+ name,
9248
+ registered,
9249
+ declared,
9250
+ settingsDeclared: readSettingsAutoUpdate(settings, name)
9251
+ });
9252
+ const rows = [checkMarketplaceAutoUpdate(autoUpdate), checkMarketplaceCatalogRef(
9253
+ resolveCatalogRef({ name, registered, ref, autoUpdate: autoUpdate.effective })
9254
+ )];
9255
+ return rows.filter((r) => r !== null);
9256
+ }
9257
+
9258
+ // src/hook-activity.ts
9259
+ var import_node_fs13 = require("node:fs");
9260
+ var import_node_path11 = require("node:path");
9006
9261
  var DEFAULT_SURFACE = "claude";
9007
9262
  function activityLogPath(cwd) {
9008
9263
  return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
@@ -9015,16 +9270,16 @@ function appendHookActivity(cwd, entry) {
9015
9270
  surface: DEFAULT_SURFACE,
9016
9271
  ...entry
9017
9272
  };
9018
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(path2), { recursive: true });
9019
- (0, import_node_fs12.appendFileSync)(path2, `${JSON.stringify(line)}
9273
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path2), { recursive: true });
9274
+ (0, import_node_fs13.appendFileSync)(path2, `${JSON.stringify(line)}
9020
9275
  `, "utf8");
9021
9276
  } catch {
9022
9277
  }
9023
9278
  }
9024
9279
 
9025
9280
  // src/worktree.ts
9026
- var import_node_fs13 = require("node:fs");
9027
- var import_node_path11 = require("node:path");
9281
+ var import_node_fs14 = require("node:fs");
9282
+ var import_node_path12 = require("node:path");
9028
9283
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
9029
9284
  var PKG = "package.json";
9030
9285
  var LOCKFILE = "package-lock.json";
@@ -9032,21 +9287,21 @@ var NODE_MODULES = "node_modules";
9032
9287
  var realFsProbe = {
9033
9288
  isDir: (p) => {
9034
9289
  try {
9035
- return (0, import_node_fs13.statSync)(p).isDirectory();
9290
+ return (0, import_node_fs14.statSync)(p).isDirectory();
9036
9291
  } catch {
9037
9292
  return false;
9038
9293
  }
9039
9294
  },
9040
9295
  isFile: (p) => {
9041
9296
  try {
9042
- return (0, import_node_fs13.statSync)(p).isFile();
9297
+ return (0, import_node_fs14.statSync)(p).isFile();
9043
9298
  } catch {
9044
9299
  return false;
9045
9300
  }
9046
9301
  },
9047
9302
  listDirs: (p) => {
9048
9303
  try {
9049
- return (0, import_node_fs13.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9304
+ return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9050
9305
  } catch {
9051
9306
  return [];
9052
9307
  }
@@ -9054,12 +9309,12 @@ var realFsProbe = {
9054
9309
  };
9055
9310
  function scanInstallDirs(root, fs2 = realFsProbe) {
9056
9311
  const factsFor = (dir) => {
9057
- const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
9312
+ const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
9058
9313
  return {
9059
9314
  dir,
9060
- hasPackageJson: fs2.isFile((0, import_node_path11.join)(abs, PKG)),
9061
- hasLockfile: fs2.isFile((0, import_node_path11.join)(abs, LOCKFILE)),
9062
- hasNodeModules: fs2.isDir((0, import_node_path11.join)(abs, NODE_MODULES))
9315
+ hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
9316
+ hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
9317
+ hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
9063
9318
  };
9064
9319
  };
9065
9320
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -9069,7 +9324,7 @@ function npmInstallTargets(dirs) {
9069
9324
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
9070
9325
  }
9071
9326
  function isLinkedWorktree(root, fs2 = realFsProbe) {
9072
- return fs2.isFile((0, import_node_path11.join)(root, ".git"));
9327
+ return fs2.isFile((0, import_node_path12.join)(root, ".git"));
9073
9328
  }
9074
9329
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
9075
9330
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -9079,8 +9334,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
9079
9334
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
9080
9335
  }
9081
9336
  function defaultCopyFile(from, to) {
9082
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(to), { recursive: true });
9083
- (0, import_node_fs13.copyFileSync)(from, to);
9337
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
9338
+ (0, import_node_fs14.copyFileSync)(from, to);
9084
9339
  }
9085
9340
  async function provisionWorktree(worktreeRoot, deps) {
9086
9341
  const fs2 = deps.fs ?? realFsProbe;
@@ -9092,7 +9347,7 @@ async function provisionWorktree(worktreeRoot, deps) {
9092
9347
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
9093
9348
  const installed = [];
9094
9349
  for (const target of targets) {
9095
- const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
9350
+ const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
9096
9351
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
9097
9352
  await deps.runInstall(target.command, cwd);
9098
9353
  installed.push(target);
@@ -9101,7 +9356,7 @@ async function provisionWorktree(worktreeRoot, deps) {
9101
9356
  const copySkipped = [];
9102
9357
  const primary = await deps.primaryCheckout();
9103
9358
  for (const rel of LOCAL_ONLY_FILES) {
9104
- const dest = (0, import_node_path11.join)(worktreeRoot, rel);
9359
+ const dest = (0, import_node_path12.join)(worktreeRoot, rel);
9105
9360
  if (fs2.isFile(dest)) {
9106
9361
  copySkipped.push({ file: rel, reason: "already-present" });
9107
9362
  continue;
@@ -9110,24 +9365,30 @@ async function provisionWorktree(worktreeRoot, deps) {
9110
9365
  copySkipped.push({ file: rel, reason: "no-primary" });
9111
9366
  continue;
9112
9367
  }
9113
- if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
9368
+ if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
9114
9369
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
9115
9370
  continue;
9116
9371
  }
9117
- copyFile((0, import_node_path11.join)(primary, rel), dest);
9372
+ copyFile((0, import_node_path12.join)(primary, rel), dest);
9118
9373
  copied.push(rel);
9119
9374
  log(`copied local config: ${rel}`);
9120
9375
  }
9121
9376
  return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
9122
9377
  }
9378
+ function capWorktreeDirName(name, max = 40) {
9379
+ if (name.length <= max) return name;
9380
+ const hard = name.slice(0, max);
9381
+ const lastDash = hard.lastIndexOf("-");
9382
+ return (lastDash >= max - 12 ? hard.slice(0, lastDash) : hard).replace(/-+$/, "");
9383
+ }
9123
9384
  function defaultWorktreePath(repoRoot2, branch) {
9124
- const safe = branch.replace(/[/\\]+/g, "-");
9125
- return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path11.basename)(repoRoot2), safe);
9385
+ const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
9386
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path12.basename)(repoRoot2), safe);
9126
9387
  }
9127
9388
  async function primaryCheckoutRootOf(git) {
9128
9389
  try {
9129
9390
  const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
9130
- return out ? (0, import_node_path11.dirname)(out) : void 0;
9391
+ return out ? (0, import_node_path12.dirname)(out) : void 0;
9131
9392
  } catch {
9132
9393
  return void 0;
9133
9394
  }
@@ -9183,7 +9444,15 @@ async function resolveWhoami(deps) {
9183
9444
  } catch {
9184
9445
  session = void 0;
9185
9446
  }
9186
- if (session?.login) return { login: session.login, source: "hub-session", sessionExpiresAt: session.expiresAt };
9447
+ if (session?.login) {
9448
+ return {
9449
+ login: session.login,
9450
+ source: "hub-session",
9451
+ sessionExpiresAt: session.expiresAt,
9452
+ // Exact-match, and only here. The `github` branch below deliberately never carries a role.
9453
+ ...session.role === "owner" ? { role: "owner" } : {}
9454
+ };
9455
+ }
9187
9456
  let ghLogin;
9188
9457
  try {
9189
9458
  ghLogin = await deps.ghLogin();
@@ -9198,6 +9467,10 @@ function whoamiLine(report, caveat) {
9198
9467
  const line = `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.`;
9199
9468
  return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
9200
9469
  }
9470
+ function authorityLine(report) {
9471
+ if (report.source !== "hub-session" || report.role !== "owner" || !report.login) return null;
9472
+ return `full authority: ${report.login} owns the org \u2014 doctrine, board, roles, matrix, skills and process are his to decide; never ask whether a change is his to make. Secret VALUES are still never echoed.`;
9473
+ }
9201
9474
 
9202
9475
  // src/command-ladder-hint.ts
9203
9476
  var COMMAND_LADDER_WRITES = [
@@ -9233,7 +9506,7 @@ function commandLadderHint() {
9233
9506
  }
9234
9507
 
9235
9508
  // src/index.ts
9236
- var import_node_path27 = require("node:path");
9509
+ var import_node_path30 = require("node:path");
9237
9510
 
9238
9511
  // src/merge-ci-policy.ts
9239
9512
  function resolveMergeCiPolicy(input) {
@@ -9321,16 +9594,16 @@ function pollStateFromParsed(parsed) {
9321
9594
  }
9322
9595
  var PR_MERGEABLE_UNKNOWN_POLL_RETRIES = 4;
9323
9596
  var PR_MERGEABLE_UNKNOWN_POLL_MS = 3e3;
9324
- async function resolveSettledMergeableState(pollMergeable, sleep2) {
9597
+ async function resolveSettledMergeableState(pollMergeable, sleep3) {
9325
9598
  for (let attempt = 1; attempt <= PR_MERGEABLE_UNKNOWN_POLL_RETRIES; attempt++) {
9326
9599
  const state = await pollMergeable();
9327
9600
  if (state !== "UNKNOWN") return state;
9328
- if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep2(PR_MERGEABLE_UNKNOWN_POLL_MS);
9601
+ if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep3(PR_MERGEABLE_UNKNOWN_POLL_MS);
9329
9602
  }
9330
9603
  return "UNKNOWN";
9331
9604
  }
9332
9605
  function conflictingPrMessage(baseBranch) {
9333
- return `PR is CONFLICTING with ${baseBranch} \u2014 GitHub will not queue checks until the conflict is resolved. Resolve with: git fetch origin ${baseBranch} && git rebase origin/${baseBranch}`;
9606
+ return `PR is CONFLICTING with ${baseBranch} \u2014 GitHub will not queue checks until the conflict is resolved. Resolve with: git fetch origin ${baseBranch} && git merge origin/${baseBranch} (merge, not rebase \u2014 a pushed branch cannot be rebased without a force-push, which doctrine forbids)`;
9334
9607
  }
9335
9608
  function conflictingResult(policy, baseBranch, waitedMs) {
9336
9609
  return { policy, status: "conflicting", reason: conflictingPrMessage(baseBranch), detail: "conflicting", waitedMs };
@@ -9686,6 +9959,12 @@ var MANAGED_GITIGNORE_LINES = [
9686
9959
  // and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
9687
9960
  ".codex/",
9688
9961
  ".agents/",
9962
+ // #3525: `jerv-cli fusion attest record` writes `.jerv/attest/<head>.attest.json` INTO the worktree,
9963
+ // so every worktree that has ever passed the fusion gate reports `?? .jerv/` forever. `worktree land`
9964
+ // refuses to clean a dirty worktree, which made the attestation artifact the gate requires the exact
9965
+ // thing blocking the cleanup that gate's own worktree needs. Org-universal: every repo runs the same
9966
+ // gate, so this belongs in the managed block rather than in one repo's own ignores.
9967
+ ".jerv/",
9689
9968
  // #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
9690
9969
  // via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
9691
9970
  // doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
@@ -9755,6 +10034,112 @@ function planManagedGitignore(current) {
9755
10034
  return { changed, content, added, removed };
9756
10035
  }
9757
10036
 
10037
+ // src/docs-index-command.ts
10038
+ var import_node_fs15 = require("node:fs");
10039
+ var import_node_path13 = require("node:path");
10040
+ var DOCS_INDEX_PATH = "docs/index.md";
10041
+ function isRoutableDocsPath(relPath) {
10042
+ const normalized = relPath.replace(/\\/g, "/");
10043
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
10044
+ }
10045
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
10046
+ function plainInline(text) {
10047
+ return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
10048
+ }
10049
+ function extractDocMeta(relPath, raw) {
10050
+ const lines = raw.split(/\r?\n/);
10051
+ let i = 0;
10052
+ if (lines[i]?.trim() === "---") {
10053
+ i++;
10054
+ while (i < lines.length && lines[i].trim() !== "---") i++;
10055
+ if (i < lines.length) i++;
10056
+ }
10057
+ let title = "";
10058
+ let scope = "";
10059
+ for (; i < lines.length; i++) {
10060
+ const line = lines[i];
10061
+ const trimmed = line.trim();
10062
+ if (trimmed === "") continue;
10063
+ if (trimmed.startsWith("<!--")) {
10064
+ while (i < lines.length && !lines[i].includes("-->")) i++;
10065
+ continue;
10066
+ }
10067
+ if (!title && trimmed.startsWith("# ")) {
10068
+ title = trimmed.slice(2).trim();
10069
+ continue;
10070
+ }
10071
+ if (!title) {
10072
+ break;
10073
+ }
10074
+ if (trimmed.startsWith("#")) continue;
10075
+ scope = plainInline(trimmed);
10076
+ break;
10077
+ }
10078
+ if (!title) {
10079
+ const base = relPath.split("/").pop() ?? relPath;
10080
+ title = base.replace(/\.md$/, "");
10081
+ }
10082
+ return { path: relPath, title, scope };
10083
+ }
10084
+ function renderDocsIndex(entries) {
10085
+ const lines = [
10086
+ GENERATED_HEADER,
10087
+ "",
10088
+ "# Documentation index",
10089
+ "",
10090
+ "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
10091
+ "can never diverge from the records it lists.",
10092
+ ""
10093
+ ];
10094
+ for (const entry of entries) {
10095
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
10096
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
10097
+ }
10098
+ return lines.join("\n") + "\n";
10099
+ }
10100
+ function sameContent(a, b) {
10101
+ const normalize = (text) => text.replace(/\r\n/g, "\n");
10102
+ return normalize(a) === normalize(b);
10103
+ }
10104
+ function docsIndex(deps, opts = {}) {
10105
+ const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
10106
+ const content = renderDocsIndex(entries);
10107
+ const current = deps.readIndex();
10108
+ const drift = current === null || !sameContent(current, content);
10109
+ let wrote = false;
10110
+ if (!opts.check && drift) {
10111
+ deps.writeIndex(content);
10112
+ wrote = true;
10113
+ }
10114
+ return { content, drift, wrote };
10115
+ }
10116
+ function walkMarkdown(dir) {
10117
+ const out = [];
10118
+ const stack = [dir];
10119
+ while (stack.length) {
10120
+ const current = stack.pop();
10121
+ for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
10122
+ const full = (0, import_node_path13.join)(current, entry.name);
10123
+ if (entry.isDirectory()) {
10124
+ stack.push(full);
10125
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
10126
+ out.push((0, import_node_path13.relative)(dir, full).split(import_node_path13.sep).join("/"));
10127
+ }
10128
+ }
10129
+ }
10130
+ return out;
10131
+ }
10132
+ function createDocsIndexDeps(repoRoot2) {
10133
+ const docsDir = (0, import_node_path13.join)(repoRoot2, "docs");
10134
+ const indexPath = (0, import_node_path13.join)(repoRoot2, DOCS_INDEX_PATH);
10135
+ return {
10136
+ listDocs: () => (0, import_node_fs15.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
10137
+ readDoc: (relPath) => (0, import_node_fs15.readFileSync)((0, import_node_path13.join)(docsDir, relPath), "utf8"),
10138
+ readIndex: () => (0, import_node_fs15.existsSync)(indexPath) ? (0, import_node_fs15.readFileSync)(indexPath, "utf8") : null,
10139
+ writeIndex: (content) => (0, import_node_fs15.writeFileSync)(indexPath, content, "utf8")
10140
+ };
10141
+ }
10142
+
9758
10143
  // src/gate-budget.ts
9759
10144
  var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
9760
10145
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -10001,6 +10386,7 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
10001
10386
  out.REPO_NAME ??= parsed.name;
10002
10387
  out.REPO_SLUG ??= parsed.slug;
10003
10388
  out.CLASS ??= cls;
10389
+ out.PROJECT_OWNER ??= parsed.owner;
10004
10390
  const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
10005
10391
  const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
10006
10392
  for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
@@ -10079,10 +10465,10 @@ function labelsToPrune(orgLabelNames) {
10079
10465
  const org = new Set(orgLabelNames);
10080
10466
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
10081
10467
  }
10082
- function resolveSeedContent(seed, vars, readFile6) {
10083
- if (seed.source === "self") return readFile6(seed.target);
10468
+ function resolveSeedContent(seed, vars, readFile7) {
10469
+ if (seed.source === "self") return readFile7(seed.target);
10084
10470
  if (seed.source.startsWith("seed:")) {
10085
- const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
10471
+ const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
10086
10472
  return tmpl == null ? null : renderSeed(tmpl, vars);
10087
10473
  }
10088
10474
  return null;
@@ -10201,6 +10587,26 @@ function boardFieldsQueryArgs(projectId) {
10201
10587
  const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
10202
10588
  return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
10203
10589
  }
10590
+ function seededDocsIndex(seeds) {
10591
+ const docs2 = seeds.map((s) => ({ rel: s.path.replace(/\\/g, "/"), content: s.content })).filter((s) => s.rel.startsWith("docs/") && s.rel.endsWith(".md")).map((s) => ({ rel: s.rel.slice("docs/".length), content: s.content })).filter((s) => isRoutableDocsPath(s.rel));
10592
+ if (!docs2.length) return null;
10593
+ const entries = docs2.slice().sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0).map((s) => extractDocMeta(s.rel, s.content));
10594
+ return renderDocsIndex(entries);
10595
+ }
10596
+ function linkedProjectsQueryArgs(owner, name) {
10597
+ const query = "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { projectsV2(first: 10) { nodes { id number title } } } }";
10598
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
10599
+ }
10600
+ function pickLinkedProject(json) {
10601
+ const nodes = json?.data?.repository?.projectsV2?.nodes;
10602
+ if (!Array.isArray(nodes)) return void 0;
10603
+ const usable = nodes.filter(
10604
+ (n) => typeof n?.id === "string" && Boolean(n.id)
10605
+ );
10606
+ if (usable.length !== 1) return void 0;
10607
+ const only = usable[0];
10608
+ return typeof only.number === "number" && Number.isFinite(only.number) ? { id: only.id, number: only.number } : { id: only.id };
10609
+ }
10204
10610
  function decideSeedPrAction(openPrs) {
10205
10611
  const list = Array.isArray(openPrs) ? openPrs : [];
10206
10612
  for (const pr2 of list) {
@@ -10748,8 +11154,8 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
10748
11154
  if (first.mergeStatus !== "failed") return first;
10749
11155
  const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
10750
11156
  if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
10751
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10752
- await sleep2(PR_LAND_MERGE_RETRY_DELAY_MS);
11157
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11158
+ await sleep3(PR_LAND_MERGE_RETRY_DELAY_MS);
10753
11159
  const retried = await deps.mergeAuto(prNumber, repo);
10754
11160
  if (retried.mergeStatus !== "failed") return retried;
10755
11161
  return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
@@ -10759,13 +11165,13 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
10759
11165
  async function confirmAutoMergeEnqueued(deps, options) {
10760
11166
  const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
10761
11167
  const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
10762
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11168
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10763
11169
  for (let attempt = 0; attempt < retries; attempt++) {
10764
11170
  if (await deps.readMerged().catch(() => false)) return "merged";
10765
11171
  const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
10766
11172
  if (stuck) return "enqueued";
10767
11173
  if (attempt < retries - 1) {
10768
- await sleep2(delayMs);
11174
+ await sleep3(delayMs);
10769
11175
  await deps.reEnqueue().catch(() => {
10770
11176
  });
10771
11177
  }
@@ -10776,7 +11182,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
10776
11182
  async function readGhPrStateWithRetry(fetchState, options) {
10777
11183
  const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
10778
11184
  const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
10779
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11185
+ const sleep3 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10780
11186
  let lastError = "empty state";
10781
11187
  for (let attempt = 0; attempt < retries; attempt++) {
10782
11188
  try {
@@ -10786,7 +11192,7 @@ async function readGhPrStateWithRetry(fetchState, options) {
10786
11192
  } catch (e) {
10787
11193
  lastError = String(e.message || "gh pr view failed");
10788
11194
  }
10789
- if (attempt < retries - 1) await sleep2(delayMs);
11195
+ if (attempt < retries - 1) await sleep3(delayMs);
10790
11196
  }
10791
11197
  return { ok: false, error: lastError };
10792
11198
  }
@@ -10849,12 +11255,12 @@ async function runPrLand(prNumber, options, deps) {
10849
11255
  }
10850
11256
 
10851
11257
  // src/wave-status.ts
10852
- var import_node_fs15 = require("node:fs");
11258
+ var import_node_fs17 = require("node:fs");
10853
11259
 
10854
11260
  // src/stage-runner.ts
10855
11261
  var import_node_child_process7 = require("node:child_process");
10856
- var import_node_fs14 = require("node:fs");
10857
- var import_node_path12 = require("node:path");
11262
+ var import_node_fs16 = require("node:fs");
11263
+ var import_node_path14 = require("node:path");
10858
11264
  var import_node_net = require("node:net");
10859
11265
  var import_node_util6 = require("node:util");
10860
11266
 
@@ -10993,11 +11399,11 @@ function appendForceRecreate(up) {
10993
11399
  return `${up.trimEnd()} --force-recreate`;
10994
11400
  }
10995
11401
  function stageStatePath(cwd = process.cwd()) {
10996
- return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
11402
+ return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
10997
11403
  }
10998
11404
  function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
10999
- const dir = (0, import_node_path12.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path12.resolve)(cwd, gitCommonDir);
11000
- return (0, import_node_path12.join)(dir, "mmi", "stage", "state.json");
11405
+ const dir = (0, import_node_path14.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path14.resolve)(cwd, gitCommonDir);
11406
+ return (0, import_node_path14.join)(dir, "mmi", "stage", "state.json");
11001
11407
  }
11002
11408
  function normPath2(path2) {
11003
11409
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -11221,14 +11627,14 @@ function stageProcessEnv(stagePort, extraEnv) {
11221
11627
  }
11222
11628
  async function ensureStageRuntimeEnv(config, opts, cwd) {
11223
11629
  if (!config.ensureEnv) return;
11224
- const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
11225
- const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
11226
- if (!(0, import_node_fs14.existsSync)(target) && (0, import_node_fs14.existsSync)(example)) {
11227
- (0, import_node_fs14.copyFileSync)(example, target);
11228
- } else if ((0, import_node_fs14.existsSync)(target) && (0, import_node_fs14.existsSync)(example)) {
11229
- const stale = detectStaleEnvFile((0, import_node_fs14.readFileSync)(example, "utf8"), (0, import_node_fs14.readFileSync)(target, "utf8"), {
11230
- exampleMtimeMs: (0, import_node_fs14.statSync)(example).mtimeMs,
11231
- targetMtimeMs: (0, import_node_fs14.statSync)(target).mtimeMs
11630
+ const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
11631
+ const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
11632
+ if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11633
+ (0, import_node_fs16.copyFileSync)(example, target);
11634
+ } else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11635
+ const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
11636
+ exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
11637
+ targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
11232
11638
  });
11233
11639
  if (stale) {
11234
11640
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -11236,8 +11642,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
11236
11642
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
11237
11643
  }
11238
11644
  }
11239
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs14.existsSync)(target)) {
11240
- (0, import_node_fs14.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs14.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11645
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
11646
+ (0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11241
11647
  }
11242
11648
  }
11243
11649
  async function gitText(cwd, args) {
@@ -11267,20 +11673,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
11267
11673
  return void 0;
11268
11674
  }
11269
11675
  function readState(path2) {
11270
- if (!(0, import_node_fs14.existsSync)(path2)) return null;
11676
+ if (!(0, import_node_fs16.existsSync)(path2)) return null;
11271
11677
  try {
11272
- return JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
11678
+ return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
11273
11679
  } catch {
11274
11680
  return null;
11275
11681
  }
11276
11682
  }
11277
11683
  function mkdirFor(path2) {
11278
11684
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
11279
- (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
11685
+ (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
11280
11686
  }
11281
11687
  function writeState(path2, state) {
11282
11688
  mkdirFor(path2);
11283
- (0, import_node_fs14.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11689
+ (0, import_node_fs16.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11284
11690
  }
11285
11691
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
11286
11692
  const reservation = {
@@ -11300,7 +11706,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
11300
11706
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
11301
11707
  }
11302
11708
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
11303
- (0, import_node_fs14.rmSync)(path2, { force: true });
11709
+ (0, import_node_fs16.rmSync)(path2, { force: true });
11304
11710
  }
11305
11711
  }
11306
11712
  async function killTree(pid) {
@@ -11458,8 +11864,8 @@ async function runStage(config = {}, opts = {}) {
11458
11864
  await ensureStageRuntimeEnv(config, opts, cwd);
11459
11865
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
11460
11866
  } catch (e) {
11461
- (0, import_node_fs14.rmSync)(statePath, { force: true });
11462
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs14.rmSync)(globalStatePath, { force: true });
11867
+ (0, import_node_fs16.rmSync)(statePath, { force: true });
11868
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs16.rmSync)(globalStatePath, { force: true });
11463
11869
  throw e;
11464
11870
  }
11465
11871
  const started = await startStage(config, {
@@ -11480,9 +11886,9 @@ function parseNextFromHead(headText) {
11480
11886
  }
11481
11887
  function readStageSummary(worktreePath) {
11482
11888
  const statePath = stageStatePath(worktreePath);
11483
- if (!(0, import_node_fs15.existsSync)(statePath)) return void 0;
11889
+ if (!(0, import_node_fs17.existsSync)(statePath)) return void 0;
11484
11890
  try {
11485
- const state = JSON.parse((0, import_node_fs15.readFileSync)(statePath, "utf8"));
11891
+ const state = JSON.parse((0, import_node_fs17.readFileSync)(statePath, "utf8"));
11486
11892
  const port = typeof state.port === "number" ? state.port : void 0;
11487
11893
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
11488
11894
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -11567,7 +11973,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
11567
11973
  }
11568
11974
 
11569
11975
  // src/index.ts
11570
- var import_node_os8 = require("node:os");
11976
+ var import_node_os9 = require("node:os");
11571
11977
 
11572
11978
  // src/attach-to-project.ts
11573
11979
  async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
@@ -11601,7 +12007,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
11601
12007
  // src/gh-create.ts
11602
12008
  var import_promises = require("node:fs/promises");
11603
12009
  var import_node_os3 = require("node:os");
11604
- var import_node_path13 = require("node:path");
12010
+ var import_node_path15 = require("node:path");
11605
12011
  var import_node_crypto3 = require("node:crypto");
11606
12012
  var ISSUE_TYPES = ["bug", "feature", "task"];
11607
12013
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -11655,8 +12061,8 @@ async function bodyArgsViaFile(args, deps = {}) {
11655
12061
  const remove2 = deps.remove ?? import_promises.unlink;
11656
12062
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
11657
12063
  const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
11658
- const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
11659
- await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
12064
+ const file = (0, import_node_path15.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
12065
+ await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
11660
12066
  });
11661
12067
  await write(file, args[i + 1], "utf8");
11662
12068
  return {
@@ -11723,23 +12129,61 @@ function parseExistingPr(stderr) {
11723
12129
  if (!match) return void 0;
11724
12130
  return { number: Number(match[1]), url: match[0] };
11725
12131
  }
11726
- async function ghCreate(args) {
12132
+ var GH_CREATE_UPSTREAM_RETRIES = 3;
12133
+ var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
12134
+ function httpStatusCodes(stderr) {
12135
+ const out = [];
12136
+ for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
12137
+ for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
12138
+ return out;
12139
+ }
12140
+ function isUpstreamGitHubFault(stderr) {
12141
+ const codes = httpStatusCodes(stderr);
12142
+ if (codes.some((c) => c >= 400 && c < 500)) return false;
12143
+ if (codes.some((c) => c >= 500 && c < 600)) return true;
12144
+ return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
12145
+ }
12146
+ function mayRetryCreate(verb) {
12147
+ return verb === "pr";
12148
+ }
12149
+ function upstreamFaultMessage(verb, stderr) {
12150
+ const requestId = /\b([0-9A-F]{4}:[0-9A-F]{4,6}:[0-9A-F]+:[0-9A-F]+:[0-9A-F]+)\b/.exec(stderr)?.[1];
12151
+ const advice = mayRetryCreate(verb) ? "Retrying is the right action" : "The write may have completed server-side before the error \u2014 VERIFY before retrying, or a retry will duplicate it";
12152
+ return `gh ${verb} create failed: GitHub's API is failing (server-side 5xx) \u2014 this is NOT a problem with your diff or arguments. GitHub request ID: ${requestId ?? "unavailable"}. ${advice}; note githubstatus.com may still read green while this is happening.`;
12153
+ }
12154
+ async function ghCreate(args, deps = {}) {
12155
+ const exec = deps.exec ?? execFileP2;
12156
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11727
12157
  const swapped = await bodyArgsViaFile(args);
11728
12158
  try {
11729
- const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
11730
- return parseCreatedUrl(stdout);
11731
- } catch (e) {
11732
- const err = e;
11733
- const existing = parseExistingPr(`${err.stderr ?? ""}
11734
- ${err.message ?? ""}`);
11735
- if (existing) {
11736
- await swapped.cleanup();
11737
- console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
11738
- return { ...existing, existing: true };
12159
+ for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
12160
+ try {
12161
+ const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
12162
+ return parseCreatedUrl(stdout);
12163
+ } catch (e) {
12164
+ const err = e;
12165
+ const errText = `${err.stderr ?? ""}
12166
+ ${err.message ?? ""}`;
12167
+ const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
12168
+ const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
12169
+ if (existing) {
12170
+ await swapped.cleanup();
12171
+ console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
12172
+ return { ...existing, existing: true };
12173
+ }
12174
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
12175
+ if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
12176
+ const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
12177
+ console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
12178
+ await sleep3(waitMs);
12179
+ continue;
12180
+ }
12181
+ await swapped.cleanup();
12182
+ if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
12183
+ return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
12184
+ }
11739
12185
  }
11740
- await swapped.cleanup();
11741
- const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
11742
- fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
12186
+ throw new Error("ghCreate: retry loop exhausted without a verdict");
11743
12187
  } finally {
11744
12188
  await swapped.cleanup();
11745
12189
  }
@@ -12893,8 +13337,8 @@ function parseVerifyBroker(stdout) {
12893
13337
  }
12894
13338
 
12895
13339
  // src/train-apply.ts
12896
- var import_node_fs16 = require("node:fs");
12897
- var import_node_path14 = require("node:path");
13340
+ var import_node_fs18 = require("node:fs");
13341
+ var import_node_path16 = require("node:path");
12898
13342
  function resolveDeployModel2(meta, repo) {
12899
13343
  const m = meta?.deployModel;
12900
13344
  if (isDeployModel(m)) return m;
@@ -13003,7 +13447,7 @@ function mergeTreeConflictFiles(stdout) {
13003
13447
  return stdout.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
13004
13448
  }
13005
13449
  async function runMergeTreePreflight(deps, ours, theirs) {
13006
- const sleep2 = resolveSleep(deps);
13450
+ const sleep3 = resolveSleep(deps);
13007
13451
  let lastError;
13008
13452
  for (let attempt = 0; attempt < MERGE_TREE_PREFLIGHT_ATTEMPTS; attempt++) {
13009
13453
  try {
@@ -13013,7 +13457,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
13013
13457
  lastError = e;
13014
13458
  const files = mergeTreeConflictFiles(String(e.stdout ?? ""));
13015
13459
  if (files.length > 0) return files;
13016
- if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep2(MERGE_TREE_PREFLIGHT_DELAY_MS);
13460
+ if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep3(MERGE_TREE_PREFLIGHT_DELAY_MS);
13017
13461
  }
13018
13462
  }
13019
13463
  const msg = lastError?.message ?? String(lastError);
@@ -13217,7 +13661,7 @@ function isPersistentGitPushFailure(e) {
13217
13661
  return /non-fast-forward|\[rejected\]|failed to push|protected branch|hook declined|permission denied|could not read Username|authentication failed|Authentication failed|403 Forbidden|GH006|GH013/i.test(msg);
13218
13662
  }
13219
13663
  async function runGitWithRetry(deps, args, shouldRetry) {
13220
- const sleep2 = resolveSleep(deps);
13664
+ const sleep3 = resolveSleep(deps);
13221
13665
  let lastError;
13222
13666
  for (let attempt = 1; attempt <= GIT_NETWORK_ATTEMPTS; attempt++) {
13223
13667
  try {
@@ -13225,7 +13669,7 @@ async function runGitWithRetry(deps, args, shouldRetry) {
13225
13669
  } catch (e) {
13226
13670
  lastError = e;
13227
13671
  if (attempt >= GIT_NETWORK_ATTEMPTS || !shouldRetry(e)) throw e;
13228
- await sleep2(GIT_NETWORK_RETRY_DELAY_MS * attempt);
13672
+ await sleep3(GIT_NETWORK_RETRY_DELAY_MS * attempt);
13229
13673
  }
13230
13674
  }
13231
13675
  throw lastError;
@@ -13246,13 +13690,13 @@ var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project
13246
13690
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
13247
13691
  var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
13248
13692
  async function correlateRun(deps, args) {
13249
- const sleep2 = resolveSleep(deps);
13693
+ const sleep3 = resolveSleep(deps);
13250
13694
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
13251
13695
  const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
13252
13696
  let lastError;
13253
13697
  let parsedAnyResponse = false;
13254
13698
  for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
13255
- if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
13699
+ if (attempt > 0) await sleep3(CORRELATE_DELAY_MS);
13256
13700
  const listArgs = [
13257
13701
  "run",
13258
13702
  "list",
@@ -13445,13 +13889,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
13445
13889
  const t = Date.parse(String(r.startedAt ?? ""));
13446
13890
  return !Number.isFinite(t) || t >= freshThreshold;
13447
13891
  };
13448
- const sleep2 = resolveSleep(deps);
13892
+ const sleep3 = resolveSleep(deps);
13449
13893
  let lastStatus = "not checked";
13450
13894
  let lastError;
13451
13895
  const everObserved = /* @__PURE__ */ new Set();
13452
13896
  const autoSatisfied = /* @__PURE__ */ new Set();
13453
13897
  for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
13454
- if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
13898
+ if (attempt > 0) await sleep3(TRAIN_CHECK_DELAY_MS);
13455
13899
  let checkRuns;
13456
13900
  let statuses;
13457
13901
  try {
@@ -13585,14 +14029,14 @@ function isTransientDispatchFailure(e) {
13585
14029
  return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
13586
14030
  }
13587
14031
  async function dispatchTenantDeployWithRetry(deps, input) {
13588
- const sleep2 = resolveSleep(deps);
14032
+ const sleep3 = resolveSleep(deps);
13589
14033
  for (let attempt = 1; ; attempt++) {
13590
14034
  try {
13591
14035
  await deps.dispatchTenantDeploy(input);
13592
14036
  return;
13593
14037
  } catch (e) {
13594
14038
  if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
13595
- await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
14039
+ await sleep3(DISPATCH_RETRY_DELAY_MS * attempt);
13596
14040
  }
13597
14041
  }
13598
14042
  }
@@ -13612,9 +14056,9 @@ var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
13612
14056
  var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
13613
14057
  async function reconcilePublishFailure(deps, runId) {
13614
14058
  if (runId == null) return "failure";
13615
- const sleep2 = resolveSleep(deps);
14059
+ const sleep3 = resolveSleep(deps);
13616
14060
  for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
13617
- if (attempt > 0) await sleep2(PUBLISH_LOG_RETRY_DELAY_MS);
14061
+ if (attempt > 0) await sleep3(PUBLISH_LOG_RETRY_DELAY_MS);
13618
14062
  const log = await fetchControlRunLog(deps, runId);
13619
14063
  if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
13620
14064
  }
@@ -13802,17 +14246,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
13802
14246
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
13803
14247
  }
13804
14248
  function readLocalGateWorkflows() {
13805
- const dir = (0, import_node_path14.join)(".github", "workflows");
14249
+ const dir = (0, import_node_path16.join)(".github", "workflows");
13806
14250
  let names;
13807
14251
  try {
13808
- names = (0, import_node_fs16.readdirSync)(dir);
14252
+ names = (0, import_node_fs18.readdirSync)(dir);
13809
14253
  } catch {
13810
14254
  return null;
13811
14255
  }
13812
14256
  const files = [];
13813
14257
  for (const name of names.filter(isGateWorkflowPath)) {
13814
14258
  try {
13815
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(dir, name), "utf8") });
14259
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, name), "utf8") });
13816
14260
  } catch {
13817
14261
  }
13818
14262
  }
@@ -14408,13 +14852,13 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
14408
14852
  note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
14409
14853
  };
14410
14854
  }
14411
- const sleep2 = resolveSleep(deps);
14855
+ const sleep3 = resolveSleep(deps);
14412
14856
  let last;
14413
14857
  for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
14414
14858
  last = await attemptRetire(deps, ctx);
14415
14859
  if (last.status === "retired") return last;
14416
14860
  if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
14417
- await sleep2(RETIRE_BACKOFF_MS * attempt);
14861
+ await sleep3(RETIRE_BACKOFF_MS * attempt);
14418
14862
  }
14419
14863
  const f = last;
14420
14864
  if (f.category === "wait-timeout") return f;
@@ -14945,9 +15389,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
14945
15389
  return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
14946
15390
  }
14947
15391
  async function watchReleaseRun(deps, ctx, workflow, sha) {
14948
- const sleep2 = sleeper(deps);
15392
+ const sleep3 = sleeper(deps);
14949
15393
  for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
14950
- if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
15394
+ if (attempt > 0) await sleep3(HOTFIX_RUN_FIND_DELAY_MS);
14951
15395
  let rows;
14952
15396
  try {
14953
15397
  const out = await deps.run("gh", [
@@ -15085,7 +15529,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
15085
15529
  try {
15086
15530
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
15087
15531
  const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
15088
- const sleep2 = sleeper(deps);
15532
+ const sleep3 = sleeper(deps);
15089
15533
  let attempt = 0;
15090
15534
  for (; ; ) {
15091
15535
  attempt++;
@@ -15094,7 +15538,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
15094
15538
  break;
15095
15539
  } catch (err) {
15096
15540
  if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
15097
- await sleep2(HOTFIX_VERIFY_RETRY_MS);
15541
+ await sleep3(HOTFIX_VERIFY_RETRY_MS);
15098
15542
  }
15099
15543
  }
15100
15544
  const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
@@ -15708,116 +16152,10 @@ function renderAccessReport(report) {
15708
16152
  return lines.join("\n");
15709
16153
  }
15710
16154
 
15711
- // src/docs-index-command.ts
15712
- var import_node_fs17 = require("node:fs");
15713
- var import_node_path15 = require("node:path");
15714
- var DOCS_INDEX_PATH = "docs/index.md";
15715
- function isRoutableDocsPath(relPath) {
15716
- const normalized = relPath.replace(/\\/g, "/");
15717
- return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
15718
- }
15719
- var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
15720
- function plainInline(text) {
15721
- return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
15722
- }
15723
- function extractDocMeta(relPath, raw) {
15724
- const lines = raw.split(/\r?\n/);
15725
- let i = 0;
15726
- if (lines[i]?.trim() === "---") {
15727
- i++;
15728
- while (i < lines.length && lines[i].trim() !== "---") i++;
15729
- if (i < lines.length) i++;
15730
- }
15731
- let title = "";
15732
- let scope = "";
15733
- for (; i < lines.length; i++) {
15734
- const line = lines[i];
15735
- const trimmed = line.trim();
15736
- if (trimmed === "") continue;
15737
- if (trimmed.startsWith("<!--")) {
15738
- while (i < lines.length && !lines[i].includes("-->")) i++;
15739
- continue;
15740
- }
15741
- if (!title && trimmed.startsWith("# ")) {
15742
- title = trimmed.slice(2).trim();
15743
- continue;
15744
- }
15745
- if (!title) {
15746
- break;
15747
- }
15748
- if (trimmed.startsWith("#")) continue;
15749
- scope = plainInline(trimmed);
15750
- break;
15751
- }
15752
- if (!title) {
15753
- const base = relPath.split("/").pop() ?? relPath;
15754
- title = base.replace(/\.md$/, "");
15755
- }
15756
- return { path: relPath, title, scope };
15757
- }
15758
- function renderDocsIndex(entries) {
15759
- const lines = [
15760
- GENERATED_HEADER,
15761
- "",
15762
- "# Documentation index",
15763
- "",
15764
- "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
15765
- "can never diverge from the records it lists.",
15766
- ""
15767
- ];
15768
- for (const entry of entries) {
15769
- const link = `[${plainInline(entry.title)}](${entry.path})`;
15770
- lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
15771
- }
15772
- return lines.join("\n") + "\n";
15773
- }
15774
- function sameContent(a, b) {
15775
- const normalize = (text) => text.replace(/\r\n/g, "\n");
15776
- return normalize(a) === normalize(b);
15777
- }
15778
- function docsIndex(deps, opts = {}) {
15779
- const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
15780
- const content = renderDocsIndex(entries);
15781
- const current = deps.readIndex();
15782
- const drift = current === null || !sameContent(current, content);
15783
- let wrote = false;
15784
- if (!opts.check && drift) {
15785
- deps.writeIndex(content);
15786
- wrote = true;
15787
- }
15788
- return { content, drift, wrote };
15789
- }
15790
- function walkMarkdown(dir) {
15791
- const out = [];
15792
- const stack = [dir];
15793
- while (stack.length) {
15794
- const current = stack.pop();
15795
- for (const entry of (0, import_node_fs17.readdirSync)(current, { withFileTypes: true })) {
15796
- const full = (0, import_node_path15.join)(current, entry.name);
15797
- if (entry.isDirectory()) {
15798
- stack.push(full);
15799
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
15800
- out.push((0, import_node_path15.relative)(dir, full).split(import_node_path15.sep).join("/"));
15801
- }
15802
- }
15803
- }
15804
- return out;
15805
- }
15806
- function createDocsIndexDeps(repoRoot2) {
15807
- const docsDir = (0, import_node_path15.join)(repoRoot2, "docs");
15808
- const indexPath = (0, import_node_path15.join)(repoRoot2, DOCS_INDEX_PATH);
15809
- return {
15810
- listDocs: () => (0, import_node_fs17.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
15811
- readDoc: (relPath) => (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(docsDir, relPath), "utf8"),
15812
- readIndex: () => (0, import_node_fs17.existsSync)(indexPath) ? (0, import_node_fs17.readFileSync)(indexPath, "utf8") : null,
15813
- writeIndex: (content) => (0, import_node_fs17.writeFileSync)(indexPath, content, "utf8")
15814
- };
15815
- }
15816
-
15817
16155
  // src/doc-refs-core.ts
15818
16156
  var import_node_child_process9 = require("node:child_process");
15819
- var import_node_fs18 = require("node:fs");
15820
- var import_node_path16 = require("node:path");
16157
+ var import_node_fs19 = require("node:fs");
16158
+ var import_node_path17 = require("node:path");
15821
16159
  var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
15822
16160
  var PIN_MENTION_RE = /<!--\s*pinned by\b/;
15823
16161
  var REF_RE = /`([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?::\d+)?`/g;
@@ -15851,7 +16189,7 @@ function extractPins(markdown) {
15851
16189
  });
15852
16190
  return pins;
15853
16191
  }
15854
- function checkPins(root, readFile6, docs2) {
16192
+ function checkPins(root, readFile7, docs2) {
15855
16193
  const findings = [];
15856
16194
  for (const [doc, markdown] of Object.entries(docs2)) {
15857
16195
  for (const pin of extractPins(markdown)) {
@@ -15859,7 +16197,7 @@ function checkPins(root, readFile6, docs2) {
15859
16197
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
15860
16198
  continue;
15861
16199
  }
15862
- const source = readFile6((0, import_node_path16.join)(root, pin.file));
16200
+ const source = readFile7((0, import_node_path17.join)(root, pin.file));
15863
16201
  if (source == null) {
15864
16202
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
15865
16203
  continue;
@@ -15920,17 +16258,17 @@ function checkRefs(root, deps, docs2) {
15920
16258
  for (const [doc, markdown] of Object.entries(docs2)) {
15921
16259
  for (const { ref, line } of extractRefs(markdown)) {
15922
16260
  const first = ref.split("/")[0];
15923
- if (!exists((0, import_node_path16.join)(root, first))) continue;
15924
- if (!exists((0, import_node_path16.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
16261
+ if (!exists((0, import_node_path17.join)(root, first))) continue;
16262
+ if (!exists((0, import_node_path17.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
15925
16263
  }
15926
- const docDir = import_node_path16.posix.dirname(doc);
16264
+ const docDir = import_node_path17.posix.dirname(doc);
15927
16265
  for (const { target, line } of extractLinks(markdown)) {
15928
- const resolved = import_node_path16.posix.normalize(import_node_path16.posix.join(docDir === "." ? "" : docDir, target));
16266
+ const resolved = import_node_path17.posix.normalize(import_node_path17.posix.join(docDir === "." ? "" : docDir, target));
15929
16267
  if (resolved.startsWith("..")) {
15930
16268
  candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
15931
16269
  continue;
15932
16270
  }
15933
- if (!exists((0, import_node_path16.join)(root, resolved))) {
16271
+ if (!exists((0, import_node_path17.join)(root, resolved))) {
15934
16272
  candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
15935
16273
  }
15936
16274
  }
@@ -15964,22 +16302,22 @@ function checkCommands(docs2, commandPaths) {
15964
16302
  return { ok: findings.length === 0, findings };
15965
16303
  }
15966
16304
  function readFileOrNull(path2) {
15967
- return (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
16305
+ return (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null;
15968
16306
  }
15969
16307
  function walk(dir, root, out) {
15970
- for (const entry of (0, import_node_fs18.readdirSync)(dir)) {
15971
- const full = (0, import_node_path16.join)(dir, entry);
15972
- if ((0, import_node_fs18.statSync)(full).isDirectory()) walk(full, root, out);
16308
+ for (const entry of (0, import_node_fs19.readdirSync)(dir)) {
16309
+ const full = (0, import_node_path17.join)(dir, entry);
16310
+ if ((0, import_node_fs19.statSync)(full).isDirectory()) walk(full, root, out);
15973
16311
  else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
15974
16312
  }
15975
16313
  return out;
15976
16314
  }
15977
16315
  function defaultListDocs(root) {
15978
- const docsDir = (0, import_node_path16.join)(root, "docs");
15979
- const docs2 = ((0, import_node_fs18.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
16316
+ const docsDir = (0, import_node_path17.join)(root, "docs");
16317
+ const docs2 = ((0, import_node_fs19.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
15980
16318
  (rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
15981
16319
  );
15982
- return [...ROOT_DOCS.filter((rel) => (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, rel))), ...docs2];
16320
+ return [...ROOT_DOCS.filter((rel) => (0, import_node_fs19.existsSync)((0, import_node_path17.join)(root, rel))), ...docs2];
15983
16321
  }
15984
16322
  function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
15985
16323
  const inRepo = relPaths.filter((p) => !p.startsWith(".."));
@@ -15997,16 +16335,16 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.exec
15997
16335
  }
15998
16336
  }
15999
16337
  function runDocRefs(root, deps = {}) {
16000
- const readFile6 = deps.readFile ?? readFileOrNull;
16001
- const exists = deps.exists ?? import_node_fs18.existsSync;
16338
+ const readFile7 = deps.readFile ?? readFileOrNull;
16339
+ const exists = deps.exists ?? import_node_fs19.existsSync;
16002
16340
  const listDocs = deps.listDocs ?? defaultListDocs;
16003
16341
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
16004
16342
  const commandPaths = deps.commandPaths ?? null;
16005
16343
  const docs2 = Object.fromEntries(
16006
- listDocs(root).map((rel) => [rel, readFile6((0, import_node_path16.join)(root, rel))]).filter(([, body]) => body != null)
16344
+ listDocs(root).map((rel) => [rel, readFile7((0, import_node_path17.join)(root, rel))]).filter(([, body]) => body != null)
16007
16345
  );
16008
16346
  const findings = [
16009
- ...checkPins(root, readFile6, docs2).findings,
16347
+ ...checkPins(root, readFile7, docs2).findings,
16010
16348
  ...checkRefs(root, { exists, isIgnored }, docs2).findings
16011
16349
  ];
16012
16350
  if (commandPaths == null) {
@@ -16078,7 +16416,28 @@ function docsAuditStatus(fetch2, opts) {
16078
16416
  if (!fetch2.ok) {
16079
16417
  return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
16080
16418
  }
16419
+ if (!isValidIsoDate(opts.today)) {
16420
+ throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
16421
+ }
16081
16422
  if (fetch2.verdict === null) {
16423
+ const armedAt = opts.armedAt;
16424
+ if (armedAt && isValidIsoDate(armedAt)) {
16425
+ const cadenceDays = opts.cadenceDays ?? 7;
16426
+ const graceDays = opts.graceDays ?? 3;
16427
+ const sinceArmed = ageInDays(armedAt, opts.today);
16428
+ if (sinceArmed <= cadenceDays + graceDays) {
16429
+ return {
16430
+ ok: true,
16431
+ state: "awaiting-first-tick",
16432
+ line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
16433
+ };
16434
+ }
16435
+ return {
16436
+ ok: false,
16437
+ state: "missing",
16438
+ line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
16439
+ };
16440
+ }
16082
16441
  return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
16083
16442
  }
16084
16443
  const verdict = fetch2.verdict;
@@ -16107,9 +16466,6 @@ function docsAuditStatus(fetch2, opts) {
16107
16466
  line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
16108
16467
  };
16109
16468
  }
16110
- if (!isValidIsoDate(opts.today)) {
16111
- throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
16112
- }
16113
16469
  const cadence = opts.cadenceDays ?? 7;
16114
16470
  const grace = opts.graceDays ?? 3;
16115
16471
  const age = ageInDays(verdict.date, opts.today);
@@ -16945,8 +17301,8 @@ function writeError(res) {
16945
17301
  }
16946
17302
 
16947
17303
  // src/secrets-commands.ts
16948
- var import_node_fs19 = require("node:fs");
16949
- var import_node_path17 = require("node:path");
17304
+ var import_node_fs20 = require("node:fs");
17305
+ var import_node_path18 = require("node:path");
16950
17306
  var import_node_os5 = require("node:os");
16951
17307
 
16952
17308
  // src/project-runtime.ts
@@ -17063,25 +17419,25 @@ var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
17063
17419
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
17064
17420
  function resolvePreflightRepoScope(optRepo, cwdRepo) {
17065
17421
  const targetRepo2 = optRepo ? optRepo.includes("/") ? optRepo : `mutmutco/${optRepo}` : cwdRepo;
17066
- const sameRepo = !optRepo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
17067
- return { targetRepo: targetRepo2, sameRepo };
17422
+ const sameRepo2 = !optRepo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
17423
+ return { targetRepo: targetRepo2, sameRepo: sameRepo2 };
17068
17424
  }
17069
17425
  function collectMap(value, previous = []) {
17070
17426
  return [...previous, value];
17071
17427
  }
17072
17428
  async function decryptRailsCredentials(input) {
17073
- const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
17429
+ const appDir = (0, import_node_path18.resolve)(input.appDir ?? process.cwd());
17074
17430
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17075
17431
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
17076
- const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
17077
- const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
17432
+ const credentialsPath = (0, import_node_path18.resolve)(appDir, credentialsFile);
17433
+ const masterKeyPath = (0, import_node_path18.resolve)(appDir, masterKeyFile);
17078
17434
  const env = {
17079
17435
  ...process.env,
17080
17436
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
17081
17437
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
17082
17438
  };
17083
- if ((0, import_node_fs19.existsSync)(masterKeyPath)) {
17084
- env.RAILS_MASTER_KEY = (0, import_node_fs19.readFileSync)(masterKeyPath, "utf8").trim();
17439
+ if ((0, import_node_fs20.existsSync)(masterKeyPath)) {
17440
+ env.RAILS_MASTER_KEY = (0, import_node_fs20.readFileSync)(masterKeyPath, "utf8").trim();
17085
17441
  }
17086
17442
  const script = [
17087
17443
  'require "json"',
@@ -17091,9 +17447,9 @@ async function decryptRailsCredentials(input) {
17091
17447
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
17092
17448
  "puts JSON.generate(config.config)"
17093
17449
  ].join("\n");
17094
- const scriptDir = (0, import_node_fs19.mkdtempSync)((0, import_node_path17.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
17095
- const scriptPath = (0, import_node_path17.join)(scriptDir, "decrypt.rb");
17096
- (0, import_node_fs19.writeFileSync)(scriptPath, script, "utf8");
17450
+ const scriptDir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
17451
+ const scriptPath = (0, import_node_path18.join)(scriptDir, "decrypt.rb");
17452
+ (0, import_node_fs20.writeFileSync)(scriptPath, script, "utf8");
17097
17453
  try {
17098
17454
  const args = ["exec", "ruby", scriptPath];
17099
17455
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -17105,7 +17461,7 @@ async function decryptRailsCredentials(input) {
17105
17461
  });
17106
17462
  return JSON.parse(stdout);
17107
17463
  } finally {
17108
- (0, import_node_fs19.rmSync)(scriptDir, { recursive: true, force: true });
17464
+ (0, import_node_fs20.rmSync)(scriptDir, { recursive: true, force: true });
17109
17465
  }
17110
17466
  }
17111
17467
  async function readSecretStdin() {
@@ -17195,7 +17551,7 @@ function registerSecretsCommands(program3) {
17195
17551
  let body;
17196
17552
  if (o.file) {
17197
17553
  try {
17198
- body = (0, import_node_fs19.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
17554
+ body = (0, import_node_fs20.readFileSync)((0, import_node_path18.resolve)(o.file), "utf8");
17199
17555
  } catch (e) {
17200
17556
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
17201
17557
  }
@@ -17229,11 +17585,11 @@ function registerSecretsCommands(program3) {
17229
17585
  const stage = o.stage;
17230
17586
  const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
17231
17587
  const cwdRepo = repoFromRemoteUrl((await execFileP2("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
17232
- const { sameRepo } = resolvePreflightRepoScope(o.repo, cwdRepo);
17588
+ const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
17233
17589
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
17234
17590
  const fact = facts?.stages?.[stage] ?? null;
17235
- const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
17236
- const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
17591
+ const composeText = sameRepo2 ? await readStageBranchCompose(branch) : null;
17592
+ const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo2), force: false, repo });
17237
17593
  if (!verdict.ok) {
17238
17594
  d.err(`secrets preflight: ${verdict.reason}`);
17239
17595
  filelessOk = false;
@@ -17300,7 +17656,7 @@ function registerSecretsCommands(program3) {
17300
17656
  {
17301
17657
  ...d,
17302
17658
  decryptRailsCredentials,
17303
- removeFile: (path2) => (0, import_node_fs19.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
17659
+ removeFile: (path2) => (0, import_node_fs20.unlinkSync)((0, import_node_path18.resolve)(o.appDir ?? process.cwd(), path2))
17304
17660
  },
17305
17661
  {
17306
17662
  repo: o.repo,
@@ -17474,7 +17830,7 @@ function checkGithubPools(probe) {
17474
17830
  }
17475
17831
 
17476
17832
  // src/box-commands.ts
17477
- var import_node_fs20 = require("node:fs");
17833
+ var import_node_fs21 = require("node:fs");
17478
17834
 
17479
17835
  // src/box.ts
17480
17836
  var BOX_KEYS = {
@@ -17677,7 +18033,7 @@ function registerBoxCommands(program3) {
17677
18033
  }
17678
18034
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
17679
18035
  else if (o.ssh && o.script) {
17680
- (0, import_node_fs20.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
18036
+ (0, import_node_fs21.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
17681
18037
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
17682
18038
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
17683
18039
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -17775,7 +18131,7 @@ function lambdaTargetFromArn(arn) {
17775
18131
  }
17776
18132
  function awsScheduleEntry(payload) {
17777
18133
  if (!payload || typeof payload !== "object") return null;
17778
- const { Name, ScheduleExpression, State, Target, Description } = payload;
18134
+ const { Name, ScheduleExpression, State, Target, Description, CreationDate } = payload;
17779
18135
  if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
17780
18136
  if (State !== "ENABLED" || isJervResource(Name)) return null;
17781
18137
  let target = "";
@@ -17798,6 +18154,7 @@ function awsScheduleEntry(payload) {
17798
18154
  }
17799
18155
  const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
17800
18156
  const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
18157
+ const armedAt = armedAtFromCreationDate(CreationDate);
17801
18158
  return {
17802
18159
  name: Name,
17803
18160
  cadence: ScheduleExpression,
@@ -17805,9 +18162,25 @@ function awsScheduleEntry(payload) {
17805
18162
  llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
17806
18163
  resolved: "live",
17807
18164
  source,
17808
- ...scheduleId ? { scheduleId } : {}
18165
+ ...scheduleId ? { scheduleId } : {},
18166
+ ...armedAt ? { armedAt } : {}
17809
18167
  };
17810
18168
  }
18169
+ function armedAtFromCreationDate(creationDate) {
18170
+ if (creationDate instanceof Date) {
18171
+ return Number.isNaN(creationDate.getTime()) ? void 0 : creationDate.toISOString().slice(0, 10);
18172
+ }
18173
+ if (typeof creationDate === "string" && creationDate.trim()) {
18174
+ const parsed = Date.parse(creationDate);
18175
+ return Number.isNaN(parsed) ? void 0 : new Date(parsed).toISOString().slice(0, 10);
18176
+ }
18177
+ if (typeof creationDate === "number" && Number.isFinite(creationDate)) {
18178
+ const ms = creationDate > 1e12 ? creationDate : creationDate * 1e3;
18179
+ const date = new Date(ms);
18180
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().slice(0, 10);
18181
+ }
18182
+ return void 0;
18183
+ }
17811
18184
  function awsScheduleRefs(payload) {
17812
18185
  if (!payload || typeof payload !== "object") return [];
17813
18186
  const { Schedules: schedules } = payload;
@@ -17873,6 +18246,12 @@ function spliceDoc(docText, generatedSection) {
17873
18246
  }
17874
18247
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
17875
18248
  }
18249
+ function driftClearableFrom(drift, repoName) {
18250
+ if (drift.class !== "file-vs-registry-stale") return false;
18251
+ const slash = drift.name.indexOf("/");
18252
+ if (slash <= 0) return false;
18253
+ return drift.name.slice(0, slash).toLowerCase() === repoName.toLowerCase();
18254
+ }
17876
18255
  var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
17877
18256
  function isHarbourLlmLauncher(executor) {
17878
18257
  return HARBOUR_LLM_LAUNCHERS.has(executor);
@@ -18253,9 +18632,105 @@ function registerSchedulesCommands(program3) {
18253
18632
  });
18254
18633
  }
18255
18634
 
18256
- // src/schedules-lift-command.ts
18635
+ // src/file-lock.ts
18257
18636
  var import_promises3 = require("node:fs/promises");
18258
- var import_node_path18 = require("node:path");
18637
+ var import_node_path19 = require("node:path");
18638
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
18639
+ var IMMEDIATE_RETRY_BUDGET = 3;
18640
+ var FileLockBusyError = class extends Error {
18641
+ lockPath;
18642
+ constructor(label, lockPath, maxWaitMs) {
18643
+ super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
18644
+ this.name = "FileLockBusyError";
18645
+ this.lockPath = lockPath;
18646
+ }
18647
+ };
18648
+ function resolveFileLockOpts(opts = {}) {
18649
+ return {
18650
+ staleMs: opts.staleMs ?? 3e4,
18651
+ retryMs: opts.retryMs ?? 50,
18652
+ maxWaitMs: opts.maxWaitMs ?? 5e3,
18653
+ label: opts.label ?? "file lock"
18654
+ };
18655
+ }
18656
+ async function acquireFileLock(lockPath, opts, deadline) {
18657
+ let immediateRetries = 0;
18658
+ for (; ; ) {
18659
+ let handle;
18660
+ try {
18661
+ handle = await (0, import_promises3.open)(lockPath, "wx");
18662
+ } catch (e) {
18663
+ const code = e.code;
18664
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
18665
+ const retryNow = async () => {
18666
+ if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18667
+ if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
18668
+ };
18669
+ try {
18670
+ const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
18671
+ if (age > opts.staleMs) {
18672
+ try {
18673
+ await (0, import_promises3.unlink)(lockPath);
18674
+ await retryNow();
18675
+ continue;
18676
+ } catch (unlinkError) {
18677
+ if (unlinkError instanceof FileLockBusyError) throw unlinkError;
18678
+ const unlinkCode = unlinkError.code;
18679
+ if (unlinkCode === "ENOENT") {
18680
+ await retryNow();
18681
+ continue;
18682
+ }
18683
+ }
18684
+ }
18685
+ } catch (statError) {
18686
+ if (statError instanceof FileLockBusyError) throw statError;
18687
+ if (statError.code !== "ENOENT") throw statError;
18688
+ await retryNow();
18689
+ continue;
18690
+ }
18691
+ if (Date.now() >= deadline) {
18692
+ throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18693
+ }
18694
+ await sleep(opts.retryMs);
18695
+ continue;
18696
+ }
18697
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18698
+ try {
18699
+ await handle.writeFile(token);
18700
+ } catch (e) {
18701
+ await handle.close().catch(() => void 0);
18702
+ throw e;
18703
+ }
18704
+ return { token, handle };
18705
+ }
18706
+ }
18707
+ async function fileLockHeldBy(lockPath, token) {
18708
+ try {
18709
+ return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
18710
+ } catch {
18711
+ return false;
18712
+ }
18713
+ }
18714
+ async function releaseFileLock(lockPath, guard) {
18715
+ await guard.handle.close().catch(() => void 0);
18716
+ if (await fileLockHeldBy(lockPath, guard.token)) {
18717
+ await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
18718
+ }
18719
+ }
18720
+ async function withFileLock(lockPath, opts, fn) {
18721
+ const resolved = resolveFileLockOpts(opts);
18722
+ await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(lockPath), { recursive: true }).catch(() => void 0);
18723
+ const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
18724
+ try {
18725
+ return await fn();
18726
+ } finally {
18727
+ await releaseFileLock(lockPath, guard);
18728
+ }
18729
+ }
18730
+
18731
+ // src/schedules-lift-command.ts
18732
+ var import_promises4 = require("node:fs/promises");
18733
+ var import_node_path20 = require("node:path");
18259
18734
 
18260
18735
  // src/schedules-lift.ts
18261
18736
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -18354,14 +18829,14 @@ var RegistryUnreachableError = class extends Error {
18354
18829
  async function readWorkflowFiles(dir) {
18355
18830
  let names;
18356
18831
  try {
18357
- names = await (0, import_promises3.readdir)(dir);
18832
+ names = await (0, import_promises4.readdir)(dir);
18358
18833
  } catch {
18359
18834
  return [];
18360
18835
  }
18361
18836
  const files = [];
18362
18837
  for (const name of names.sort()) {
18363
18838
  if (!/\.ya?ml$/.test(name)) continue;
18364
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises3.readFile)((0, import_node_path18.join)(dir, name), "utf8") });
18839
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path20.join)(dir, name), "utf8") });
18365
18840
  }
18366
18841
  return files;
18367
18842
  }
@@ -18903,11 +19378,11 @@ function registerQueryCommands(program3) {
18903
19378
  }
18904
19379
 
18905
19380
  // src/bootstrap-commands.ts
18906
- var import_node_fs21 = require("node:fs");
19381
+ var import_node_fs22 = require("node:fs");
18907
19382
 
18908
19383
  // src/bootstrap-verify.ts
18909
19384
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
18910
- var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
19385
+ var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
18911
19386
  var requiredIssueTemplates = [
18912
19387
  ".github/ISSUE_TEMPLATE/bug.yml",
18913
19388
  ".github/ISSUE_TEMPLATE/feature.yml",
@@ -19064,6 +19539,11 @@ function unfilledDocPlaceholders(text) {
19064
19539
  for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
19065
19540
  return [...found];
19066
19541
  }
19542
+ function filledDocCheck(label, text, path2) {
19543
+ if (text === null) return { ok: false, label, detail: `${path2} not readable via API` };
19544
+ const unfilled = unfilledDocPlaceholders(text);
19545
+ return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
19546
+ }
19067
19547
  async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19068
19548
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
19069
19549
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
@@ -19137,19 +19617,9 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19137
19617
  label: "README has Agent context section",
19138
19618
  detail: readme === null ? "README.md not readable via API" : void 0
19139
19619
  });
19140
- const readmePlaceholders = unfilledDocPlaceholders(readme);
19141
- checks.push({
19142
- ok: readmePlaceholders.length === 0,
19143
- label: "README Agent context is filled (no template placeholders)",
19144
- detail: readmePlaceholders.length ? `unfilled: ${readmePlaceholders.join(", ")}` : void 0
19145
- });
19620
+ checks.push(filledDocCheck("README Agent context is filled (no template placeholders)", readme, "README.md"));
19146
19621
  const architecture = await contentText(deps, repo, baseBranch, "architecture.md");
19147
- const archPlaceholders = unfilledDocPlaceholders(architecture);
19148
- checks.push({
19149
- ok: archPlaceholders.length === 0,
19150
- label: "architecture.md is filled (no template placeholders)",
19151
- detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
19152
- });
19622
+ checks.push(filledDocCheck("architecture.md is filled (no template placeholders)", architecture, "architecture.md"));
19153
19623
  const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
19154
19624
  const labelNames = new Set(labels.map((l) => l.name));
19155
19625
  for (const label of requiredLabels) {
@@ -19451,7 +19921,7 @@ function registerBootstrapCommands(program3) {
19451
19921
  client: defaultGitHubClient(),
19452
19922
  projectMeta: meta,
19453
19923
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
19454
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
19924
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
19455
19925
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
19456
19926
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
19457
19927
  requiredGcpApis: (() => {
@@ -19472,7 +19942,11 @@ function registerBootstrapCommands(program3) {
19472
19942
  return null;
19473
19943
  }
19474
19944
  }
19475
- }, resolveReleaseTrack(meta));
19945
+ // #3537: verify runs BEFORE registration by construction (skill Step 0a), so `meta` is null on every
19946
+ // fresh repo and `resolveReleaseTrack` falls through to `full` — which then outranks the operator's
19947
+ // own `--class content` inside expectedBranches, and the report demands a development and an rc the
19948
+ // track forbids. The declared class fills that gap only when the registry states nothing itself.
19949
+ }, resolveReleaseTrack(metaWithDeclaredClass(meta, o.class), void 0, repo));
19476
19950
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
19477
19951
  if (!report.ok) process.exitCode = 1;
19478
19952
  });
@@ -19502,12 +19976,12 @@ function registerBootstrapCommands(program3) {
19502
19976
  return fail(`bootstrap apply: ${e.message}`);
19503
19977
  }
19504
19978
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
19505
- 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`);
19506
- const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
19979
+ if (!(0, import_node_fs22.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`);
19980
+ const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
19507
19981
  const baseBranch = o.class === "content" ? "main" : "development";
19508
19982
  const slug = parsedRepo.slug;
19509
19983
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
19510
- const readFile6 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
19984
+ const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
19511
19985
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
19512
19986
  const rawVars = {};
19513
19987
  for (const value of cmdOpts.var ?? []) {
@@ -19520,6 +19994,17 @@ function registerBootstrapCommands(program3) {
19520
19994
  } catch {
19521
19995
  }
19522
19996
  const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
19997
+ if (!vars.PROJECT_ID) {
19998
+ try {
19999
+ const r = await gh(linkedProjectsQueryArgs(parsedRepo.owner, parsedRepo.name));
20000
+ const linked = pickLinkedProject(JSON.parse(r.stdout));
20001
+ if (linked) {
20002
+ vars.PROJECT_ID = linked.id;
20003
+ if (linked.number != null && vars.PROJECT_NUMBER == null) vars.PROJECT_NUMBER = String(linked.number);
20004
+ }
20005
+ } catch {
20006
+ }
20007
+ }
19523
20008
  if (vars.PROJECT_ID) {
19524
20009
  try {
19525
20010
  const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
@@ -19561,6 +20046,7 @@ function registerBootstrapCommands(program3) {
19561
20046
  }
19562
20047
  }
19563
20048
  }
20049
+ const docsForIndex = [];
19564
20050
  for (const seed of manifest.seeds) {
19565
20051
  if (!seed.classes.includes(o.class)) continue;
19566
20052
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -19585,15 +20071,41 @@ function registerBootstrapCommands(program3) {
19585
20071
  }
19586
20072
  const planned = planSeedAction(resolved, exists);
19587
20073
  const isBlock = resolved.source === "managed-block";
19588
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
20074
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
19589
20075
  const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
19590
20076
  actions.push(action);
20077
+ const docBody = content ?? remoteContent;
20078
+ if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
20079
+ docsForIndex.push({ path: resolved.target, content: docBody });
20080
+ }
19591
20081
  if (o.execute && (action.action === "create" || action.action === "update")) {
19592
20082
  await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
19593
20083
  applied.push(`${action.action} ${resolved.target}`);
19594
20084
  if (seedPlan.mode === "pr") seededToBranch++;
19595
20085
  }
19596
20086
  }
20087
+ const indexContent = seededDocsIndex(docsForIndex);
20088
+ if (indexContent) {
20089
+ let indexCurrent = null;
20090
+ try {
20091
+ const r = await gh(["api", `repos/${repo}/contents/${enc(DOCS_INDEX_PATH)}?ref=${seedPlan.ref}`]);
20092
+ const parsed = JSON.parse(r.stdout);
20093
+ indexCurrent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : "";
20094
+ } catch {
20095
+ }
20096
+ const indexAction = indexCurrent === null ? "create" : "skip";
20097
+ actions.push({
20098
+ target: DOCS_INDEX_PATH,
20099
+ action: indexAction,
20100
+ ownership: "org",
20101
+ reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
20102
+ });
20103
+ if (o.execute && indexAction === "create") {
20104
+ await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
20105
+ applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
20106
+ if (seedPlan.mode === "pr") seededToBranch++;
20107
+ }
20108
+ }
19597
20109
  let seedPrUrl;
19598
20110
  if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
19599
20111
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
@@ -19640,7 +20152,7 @@ function registerBootstrapCommands(program3) {
19640
20152
  }
19641
20153
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
19642
20154
  if (rulesetSeed) {
19643
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
20155
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
19644
20156
  if (rulesetContent) {
19645
20157
  try {
19646
20158
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -19703,12 +20215,12 @@ LIVE apply to ${repo}:
19703
20215
  }
19704
20216
 
19705
20217
  // src/stage-commands.ts
19706
- var import_node_fs23 = require("node:fs");
19707
- var import_node_path20 = require("node:path");
20218
+ var import_node_fs24 = require("node:fs");
20219
+ var import_node_path22 = require("node:path");
19708
20220
 
19709
20221
  // src/port-registry.ts
19710
- var import_node_fs22 = require("node:fs");
19711
- var import_node_path19 = require("node:path");
20222
+ var import_node_fs23 = require("node:fs");
20223
+ var import_node_path21 = require("node:path");
19712
20224
 
19713
20225
  // ../infra/port-geometry.mjs
19714
20226
  var PORT_BLOCK = 100;
@@ -19722,8 +20234,8 @@ function nextPortBlock(registry2) {
19722
20234
  return [base, base + PORT_SPAN];
19723
20235
  }
19724
20236
  function loadPortRegistry(path2) {
19725
- if (!(0, import_node_fs22.existsSync)(path2)) return {};
19726
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
20237
+ if (!(0, import_node_fs23.existsSync)(path2)) return {};
20238
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
19727
20239
  const out = {};
19728
20240
  for (const [key, value] of Object.entries(raw)) {
19729
20241
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -19737,9 +20249,9 @@ function ensurePortRange(repo, path2) {
19737
20249
  const existing = registry2[repo];
19738
20250
  if (existing) return existing;
19739
20251
  const range = nextPortBlock(registry2);
19740
- const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
20252
+ const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
19741
20253
  raw[repo] = range;
19742
- (0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
20254
+ (0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
19743
20255
  return range;
19744
20256
  }
19745
20257
  function portCursorSeed(registry2) {
@@ -19761,22 +20273,22 @@ function existingPortRange(repo, registry2) {
19761
20273
  return registry2[repo] ?? null;
19762
20274
  }
19763
20275
  function portRangeInfraAt(root, source) {
19764
- const registryPath = (0, import_node_path19.join)(root, "infra", "port-ranges.json");
19765
- const ddbScriptPath = (0, import_node_path19.join)(root, "infra", "port-ddb.mjs");
19766
- if (!(0, import_node_fs22.existsSync)(registryPath) || !(0, import_node_fs22.existsSync)(ddbScriptPath)) return null;
20276
+ const registryPath = (0, import_node_path21.join)(root, "infra", "port-ranges.json");
20277
+ const ddbScriptPath = (0, import_node_path21.join)(root, "infra", "port-ddb.mjs");
20278
+ if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
19767
20279
  return { root, source, registryPath, ddbScriptPath };
19768
20280
  }
19769
20281
  function resolvePortRangeInfra(cwd, packageDir) {
19770
20282
  const direct = portRangeInfraAt(cwd, "cwd");
19771
20283
  if (direct) return direct;
19772
- for (let dir = cwd; ; dir = (0, import_node_path19.dirname)(dir)) {
19773
- const sibling = portRangeInfraAt((0, import_node_path19.join)(dir, "MMI-Hub"), "sibling-hub");
20284
+ for (let dir = cwd; ; dir = (0, import_node_path21.dirname)(dir)) {
20285
+ const sibling = portRangeInfraAt((0, import_node_path21.join)(dir, "MMI-Hub"), "sibling-hub");
19774
20286
  if (sibling) return sibling;
19775
- const parent = (0, import_node_path19.dirname)(dir);
20287
+ const parent = (0, import_node_path21.dirname)(dir);
19776
20288
  if (parent === dir) break;
19777
20289
  }
19778
20290
  if (packageDir) {
19779
- const pkgRoot = (0, import_node_path19.join)(packageDir, "..", "..");
20291
+ const pkgRoot = (0, import_node_path21.join)(packageDir, "..", "..");
19780
20292
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
19781
20293
  if (pkgFrom) return pkgFrom;
19782
20294
  }
@@ -19952,8 +20464,8 @@ function registerStageCommands(program3) {
19952
20464
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
19953
20465
  return decideStage({
19954
20466
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
19955
- hasCompose: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
19956
- hasEnvExample: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
20467
+ hasCompose: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), "docker-compose.yml")),
20468
+ hasEnvExample: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), ".env.example"))
19957
20469
  });
19958
20470
  }
19959
20471
  async function fetchStageVaultEnvMerge() {
@@ -20388,9 +20900,9 @@ function registerBoardCommands(program3) {
20388
20900
  }
20389
20901
 
20390
20902
  // src/merge-cleanup.ts
20391
- var import_node_fs24 = require("node:fs");
20392
- var import_promises5 = require("node:fs/promises");
20393
- var import_node_path22 = require("node:path");
20903
+ var import_node_fs25 = require("node:fs");
20904
+ var import_promises6 = require("node:fs/promises");
20905
+ var import_node_path24 = require("node:path");
20394
20906
  var import_node_child_process11 = require("node:child_process");
20395
20907
 
20396
20908
  // src/board-advance.ts
@@ -20476,117 +20988,64 @@ function boardAdvanceFailureMessage(result) {
20476
20988
  }
20477
20989
 
20478
20990
  // src/deferred-registry-store.ts
20479
- var import_promises4 = require("node:fs/promises");
20480
- var import_node_path21 = require("node:path");
20481
- var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
20991
+ var import_promises5 = require("node:fs/promises");
20992
+ var import_node_path23 = require("node:path");
20993
+ var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
20482
20994
  async function atomicWrite(target, contents) {
20483
20995
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
20484
- await (0, import_promises4.writeFile)(tmp, contents, "utf8");
20996
+ await (0, import_promises5.writeFile)(tmp, contents, "utf8");
20485
20997
  try {
20486
20998
  await renameWithRetry(tmp, target);
20487
20999
  } catch (e) {
20488
- await (0, import_promises4.unlink)(tmp).catch(() => void 0);
21000
+ await (0, import_promises5.unlink)(tmp).catch(() => void 0);
20489
21001
  throw e;
20490
21002
  }
20491
21003
  }
20492
21004
  async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
20493
21005
  for (let i = 0; ; i++) {
20494
21006
  try {
20495
- await (0, import_promises4.rename)(from, to);
21007
+ await (0, import_promises5.rename)(from, to);
20496
21008
  return;
20497
21009
  } catch (e) {
20498
21010
  const code = e.code;
20499
21011
  if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") throw e;
20500
- await sleep(backoffMs);
21012
+ await sleep2(backoffMs);
20501
21013
  }
20502
21014
  }
20503
21015
  }
20504
21016
  async function readStrict(registryPath) {
20505
21017
  let text;
20506
21018
  try {
20507
- text = await (0, import_promises4.readFile)(registryPath, "utf8");
21019
+ text = await (0, import_promises5.readFile)(registryPath, "utf8");
20508
21020
  } catch (e) {
20509
21021
  if (e.code === "ENOENT") return [];
20510
21022
  throw e;
20511
21023
  }
20512
21024
  return parseDeferredWorktreesFile(text);
20513
21025
  }
20514
- async function acquireLock(lockPath, opts, deadline) {
20515
- for (; ; ) {
20516
- let handle;
20517
- try {
20518
- handle = await (0, import_promises4.open)(lockPath, "wx");
20519
- } catch (e) {
20520
- const code = e.code;
20521
- if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
20522
- try {
20523
- const age = Date.now() - (await (0, import_promises4.stat)(lockPath)).mtimeMs;
20524
- if (age > opts.staleMs) {
20525
- try {
20526
- await (0, import_promises4.unlink)(lockPath);
20527
- continue;
20528
- } catch (unlinkError) {
20529
- const code2 = unlinkError.code;
20530
- if (code2 === "ENOENT") continue;
20531
- }
20532
- }
20533
- } catch (statError) {
20534
- if (statError.code !== "ENOENT") throw statError;
20535
- continue;
20536
- }
20537
- if (Date.now() >= deadline) {
20538
- throw new Error(`deferred registry lock busy: ${lockPath} held longer than ${opts.maxWaitMs}ms`);
20539
- }
20540
- await sleep(opts.retryMs);
20541
- continue;
20542
- }
20543
- const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
20544
- try {
20545
- await handle.writeFile(token);
20546
- } catch (e) {
20547
- await handle.close().catch(() => void 0);
20548
- throw e;
20549
- }
20550
- return { token, handle };
20551
- }
20552
- }
20553
- async function lockHeldBy(lockPath, token) {
20554
- try {
20555
- return await (0, import_promises4.readFile)(lockPath, "utf8") === token;
20556
- } catch {
20557
- return false;
20558
- }
20559
- }
20560
- async function releaseLock(lockPath, guard) {
20561
- await guard.handle.close().catch(() => void 0);
20562
- if (await lockHeldBy(lockPath, guard.token)) {
20563
- await (0, import_promises4.unlink)(lockPath).catch(() => void 0);
20564
- }
20565
- }
21026
+ var acquireLock = acquireFileLock;
21027
+ var lockHeldBy = fileLockHeldBy;
21028
+ var releaseLock = releaseFileLock;
20566
21029
  function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
20567
- const opts = {
20568
- staleMs: lockOpts.staleMs ?? 3e4,
20569
- retryMs: lockOpts.retryMs ?? 50,
20570
- maxWaitMs: lockOpts.maxWaitMs ?? 5e3
20571
- };
21030
+ const opts = resolveFileLockOpts({ ...lockOpts, label: "deferred registry lock" });
20572
21031
  const lockPath = `${registryPath}.lock`;
20573
21032
  return {
20574
21033
  // Lenient read for the sweep's initial fetch: any error (missing/corrupt) yields an empty queue.
20575
21034
  read: async () => {
20576
21035
  try {
20577
- return parseDeferredWorktreesFile(await (0, import_promises4.readFile)(registryPath, "utf8"));
21036
+ return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
20578
21037
  } catch {
20579
21038
  return [];
20580
21039
  }
20581
21040
  },
20582
21041
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
20583
21042
  write: async (entries) => {
20584
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
21043
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
20585
21044
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
20586
21045
  },
20587
21046
  // Serialized read-modify-write under the repo-wide lock (#2846).
20588
21047
  update: async (mutate) => {
20589
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
21048
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
20590
21049
  const deadline = Date.now() + opts.maxWaitMs;
20591
21050
  for (; ; ) {
20592
21051
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -20603,7 +21062,7 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
20603
21062
  if (Date.now() >= deadline) {
20604
21063
  throw new Error(`deferred registry lock contention: ${lockPath} lost to a concurrent holder past ${opts.maxWaitMs}ms`);
20605
21064
  }
20606
- await sleep(opts.retryMs);
21065
+ await sleep2(opts.retryMs);
20607
21066
  }
20608
21067
  }
20609
21068
  };
@@ -20650,11 +21109,17 @@ function renderGcApplyResult(result) {
20650
21109
  lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
20651
21110
  lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
20652
21111
  lines.push(` dead sibling dirs removed: ${result.removedWorktreeDirs.length ? result.removedWorktreeDirs.join(", ") : "none"}`);
20653
- lines.push(` worktrees pruned: ${result.pruned ? "yes" : "no"}`);
21112
+ lines.push(` stale registrations pruned: ${result.pruned ? "yes" : "no"} (clears registrations only \u2014 never deletes a directory)`);
20654
21113
  if (result.failed.length) {
20655
21114
  lines.push(` failed (${result.failed.length}):`);
20656
21115
  for (const f of result.failed) lines.push(` - ${f}`);
20657
21116
  }
21117
+ const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
21118
+ if (removedNothing) {
21119
+ lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
21120
+ lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
21121
+ lines.push(" directory and re-run to clear its registration.");
21122
+ }
20658
21123
  return lines.join("\n");
20659
21124
  }
20660
21125
  async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
@@ -20713,7 +21178,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
20713
21178
  return cleanupPrMergeLocalBranch(branch.branch, {
20714
21179
  beforeWorktrees,
20715
21180
  startingPath: branch.worktreePath,
20716
- pathExists: (p) => (0, import_node_fs24.existsSync)(p),
21181
+ pathExists: (p) => (0, import_node_fs25.existsSync)(p),
20717
21182
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20718
21183
  teardownWorktreeStage,
20719
21184
  deferredStore,
@@ -20735,12 +21200,12 @@ async function applyGcPlan(plan, remote, opts = {}) {
20735
21200
  const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
20736
21201
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20737
21202
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
20738
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path22.dirname)((0, import_node_path22.dirname)(worktreeGitRoot)) : repoRoot2;
21203
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path24.dirname)((0, import_node_path24.dirname)(worktreeGitRoot)) : repoRoot2;
20739
21204
  const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
20740
21205
  for (const wt of plan.worktreeDirs) {
20741
21206
  try {
20742
21207
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
20743
- realpath: (path2) => (0, import_node_fs24.realpathSync)(path2)
21208
+ realpath: (path2) => (0, import_node_fs25.realpathSync)(path2)
20744
21209
  });
20745
21210
  if (!cleanupTarget.ok) {
20746
21211
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -20901,13 +21366,13 @@ var realWorktreeDirRemover = {
20901
21366
  probe: (p) => {
20902
21367
  let st;
20903
21368
  try {
20904
- st = (0, import_node_fs24.lstatSync)(p);
21369
+ st = (0, import_node_fs25.lstatSync)(p);
20905
21370
  } catch {
20906
21371
  return null;
20907
21372
  }
20908
21373
  if (st.isSymbolicLink()) return "link";
20909
21374
  try {
20910
- (0, import_node_fs24.readlinkSync)(p);
21375
+ (0, import_node_fs25.readlinkSync)(p);
20911
21376
  return "link";
20912
21377
  } catch {
20913
21378
  }
@@ -20915,7 +21380,7 @@ var realWorktreeDirRemover = {
20915
21380
  },
20916
21381
  readdir: (p) => {
20917
21382
  try {
20918
- return (0, import_node_fs24.readdirSync)(p);
21383
+ return (0, import_node_fs25.readdirSync)(p);
20919
21384
  } catch {
20920
21385
  return [];
20921
21386
  }
@@ -20924,12 +21389,12 @@ var realWorktreeDirRemover = {
20924
21389
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20925
21390
  detachLink: (p) => {
20926
21391
  try {
20927
- (0, import_node_fs24.rmdirSync)(p);
21392
+ (0, import_node_fs25.rmdirSync)(p);
20928
21393
  } catch {
20929
- (0, import_node_fs24.unlinkSync)(p);
21394
+ (0, import_node_fs25.unlinkSync)(p);
20930
21395
  }
20931
21396
  },
20932
- removeTree: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
21397
+ removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
20933
21398
  };
20934
21399
  async function resolvePrimaryCheckout(execGit) {
20935
21400
  try {
@@ -20959,9 +21424,9 @@ async function worktreeHasStageState(worktreePath) {
20959
21424
  }
20960
21425
  }
20961
21426
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20962
- if (!(0, import_node_fs24.existsSync)(statePath)) return false;
21427
+ if (!(0, import_node_fs25.existsSync)(statePath)) return false;
20963
21428
  try {
20964
- const state = JSON.parse((0, import_node_fs24.readFileSync)(statePath, "utf8"));
21429
+ const state = JSON.parse((0, import_node_fs25.readFileSync)(statePath, "utf8"));
20965
21430
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20966
21431
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20967
21432
  } catch {
@@ -21168,8 +21633,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
21168
21633
  }
21169
21634
 
21170
21635
  // src/worktree-lifecycle-commands.ts
21171
- var import_node_fs25 = require("node:fs");
21172
- var import_node_path23 = require("node:path");
21636
+ var import_node_fs26 = require("node:fs");
21637
+ var import_node_path25 = require("node:path");
21173
21638
  var GH_TIMEOUT_MS = 2e4;
21174
21639
  var DEFAULT_BASE = "origin/development";
21175
21640
  var DEFAULT_REMOTE = "origin";
@@ -21272,7 +21737,7 @@ function classifyStaleLeaks(input) {
21272
21737
  var defaultOrphanDirScanDeps = {
21273
21738
  listDirs: (root) => {
21274
21739
  try {
21275
- return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path23.join)(root, e.name));
21740
+ return (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path25.join)(root, e.name));
21276
21741
  } catch {
21277
21742
  return [];
21278
21743
  }
@@ -21328,13 +21793,13 @@ function registerWorktreeCommands(program3) {
21328
21793
  if (PROTECTED_BRANCHES2.has(branch)) {
21329
21794
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
21330
21795
  }
21331
- const gitFile = (0, import_node_path23.join)(wtPath, ".git");
21332
- const isLinked = (0, import_node_fs25.existsSync)(gitFile) && (0, import_node_fs25.statSync)(gitFile).isFile();
21796
+ const gitFile = (0, import_node_path25.join)(wtPath, ".git");
21797
+ const isLinked = (0, import_node_fs26.existsSync)(gitFile) && (0, import_node_fs26.statSync)(gitFile).isFile();
21333
21798
  if (apply && !isLinked) {
21334
21799
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
21335
21800
  }
21336
21801
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
21337
- const primaryCheckout = commonDir ? (0, import_node_path23.dirname)(commonDir) : wtPath;
21802
+ const primaryCheckout = commonDir ? (0, import_node_path25.dirname)(commonDir) : wtPath;
21338
21803
  const stageSummary = readStageSummary(wtPath);
21339
21804
  const hasStage = stageSummary != null;
21340
21805
  const steps = landSteps(branch, true, hasStage);
@@ -21445,10 +21910,10 @@ async function gatherWorktreeContext() {
21445
21910
  if (s) stages.push({ path: wt.path, port: s.port });
21446
21911
  }
21447
21912
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
21448
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path23.dirname)((0, import_node_path23.dirname)(worktreeGitRoot)) : repoRoot2;
21913
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path25.dirname)((0, import_node_path25.dirname)(worktreeGitRoot)) : repoRoot2;
21449
21914
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
21450
21915
  let orphanDirs = [];
21451
- if ((0, import_node_fs25.existsSync)(wtRoot)) {
21916
+ if ((0, import_node_fs26.existsSync)(wtRoot)) {
21452
21917
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
21453
21918
  ...defaultOrphanDirScanDeps,
21454
21919
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
@@ -21471,7 +21936,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
21471
21936
  }
21472
21937
 
21473
21938
  // src/issue-commands.ts
21474
- var import_node_fs26 = require("node:fs");
21939
+ var import_node_fs27 = require("node:fs");
21475
21940
  var import_node_crypto5 = require("node:crypto");
21476
21941
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
21477
21942
  async function editIssue(client, options, deps = {}) {
@@ -21485,7 +21950,7 @@ async function editIssue(client, options, deps = {}) {
21485
21950
  if (options.body !== void 0 || options.bodyFile !== void 0) {
21486
21951
  let body;
21487
21952
  if (options.bodyFile) {
21488
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs26.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
21953
+ const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs27.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
21489
21954
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
21490
21955
  } else {
21491
21956
  body = options.body ?? "";
@@ -21982,7 +22447,7 @@ function extendCreateCommand(issue2, batchAttach) {
21982
22447
  if (opts.batch) {
21983
22448
  let specs;
21984
22449
  try {
21985
- const raw = (0, import_node_fs26.readFileSync)(opts.batch, "utf8");
22450
+ const raw = (0, import_node_fs27.readFileSync)(opts.batch, "utf8");
21986
22451
  specs = JSON.parse(raw);
21987
22452
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
21988
22453
  } catch (e) {
@@ -22040,12 +22505,12 @@ ${lines}`);
22040
22505
  }
22041
22506
 
22042
22507
  // src/train-commands.ts
22043
- var import_node_fs28 = require("node:fs");
22044
- var import_node_path25 = require("node:path");
22508
+ var import_node_fs29 = require("node:fs");
22509
+ var import_node_path27 = require("node:path");
22045
22510
 
22046
22511
  // src/plugin-guard-io.ts
22047
- var import_node_fs27 = require("node:fs");
22048
- var import_node_path24 = require("node:path");
22512
+ var import_node_fs28 = require("node:fs");
22513
+ var import_node_path26 = require("node:path");
22049
22514
  var import_node_os6 = require("node:os");
22050
22515
 
22051
22516
  // src/plugin-guard.ts
@@ -22175,11 +22640,11 @@ function runHostBin(bin, args, opts) {
22175
22640
  }
22176
22641
  var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
22177
22642
  const homeDir = surface === "codex" ? ".codex" : ".claude";
22178
- return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
22643
+ return (0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
22179
22644
  };
22180
22645
  function readInstalledPlugins(surface = detectSurface(process.env)) {
22181
22646
  try {
22182
- return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath2(surface), "utf8"));
22647
+ return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath2(surface), "utf8"));
22183
22648
  } catch {
22184
22649
  return null;
22185
22650
  }
@@ -22187,13 +22652,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
22187
22652
  function marketplaceCloneCandidates(surface, home) {
22188
22653
  if (surface === "codex") {
22189
22654
  return [
22190
- (0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
22191
- (0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
22655
+ (0, import_node_path26.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
22656
+ (0, import_node_path26.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
22192
22657
  ];
22193
22658
  }
22194
- return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
22659
+ return [(0, import_node_path26.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
22195
22660
  }
22196
- function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
22661
+ function marketplaceClonePresent(surface, home, exists = import_node_fs28.existsSync) {
22197
22662
  return marketplaceCloneCandidates(surface, home).some(exists);
22198
22663
  }
22199
22664
  async function fetchNpmReleasedVersion() {
@@ -22221,7 +22686,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
22221
22686
  isOrgRepo,
22222
22687
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
22223
22688
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
22224
- pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
22689
+ pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
22225
22690
  };
22226
22691
  }
22227
22692
  function claudePluginGuardState(isOrgRepo) {
@@ -22356,7 +22821,7 @@ function formatTrainStatus(r) {
22356
22821
  // src/train-commands.ts
22357
22822
  function readRepoVersion() {
22358
22823
  try {
22359
- return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
22824
+ return JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
22360
22825
  } catch {
22361
22826
  return void 0;
22362
22827
  }
@@ -22502,6 +22967,9 @@ function registerDeployCommands(program3) {
22502
22967
  }
22503
22968
 
22504
22969
  // src/discovery-commands.ts
22970
+ var import_node_fs30 = require("node:fs");
22971
+ var import_node_os7 = require("node:os");
22972
+ var import_node_path28 = require("node:path");
22505
22973
  var GC_GH_TIMEOUT_MS3 = 2e4;
22506
22974
  async function collectStatus() {
22507
22975
  let branch = "";
@@ -22586,6 +23054,23 @@ async function recommendNext(deps) {
22586
23054
  const top = sorted[0];
22587
23055
  return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
22588
23056
  }
23057
+ function onboardPluginGate(deps) {
23058
+ const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
23059
+ if (!registered) {
23060
+ return { ok: false, detail: `${MMI_MARKETPLACE_NAME} marketplace not registered on this machine` };
23061
+ }
23062
+ const autoUpdate = resolveAutoUpdate({
23063
+ name: MMI_MARKETPLACE_NAME,
23064
+ registered,
23065
+ declared,
23066
+ settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
23067
+ }).effective;
23068
+ const catalog = resolveCatalogRef({ name: MMI_MARKETPLACE_NAME, registered, ref, autoUpdate });
23069
+ const gaps = [];
23070
+ if (!autoUpdate) gaps.push(`auto-update is off \u2014 turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`);
23071
+ if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
23072
+ return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
23073
+ }
22589
23074
  async function collectOnboardStatus() {
22590
23075
  const cfg = await loadConfig();
22591
23076
  let track = "unknown";
@@ -22659,7 +23144,12 @@ async function collectOnboardStatus() {
22659
23144
  else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
22660
23145
  else nextCommand = "mmi-cli board read \u2014 no claimable items found";
22661
23146
  }
22662
- return { track, board, registry: registry2, secrets, nextCommand };
23147
+ const home = (0, import_node_os7.homedir)();
23148
+ const plugin = onboardPluginGate({
23149
+ readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
23150
+ readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
23151
+ });
23152
+ return { track, board, registry: registry2, secrets, plugin, nextCommand };
22663
23153
  }
22664
23154
  function registerDiscoveryCommands(program3) {
22665
23155
  program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
@@ -22703,6 +23193,7 @@ function registerDiscoveryCommands(program3) {
22703
23193
  console.log(`Board: ${report.board.ok ? "\u2713" : "\u2717"} ${report.board.detail}`);
22704
23194
  console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
22705
23195
  console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
23196
+ console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
22706
23197
  console.log(`
22707
23198
  Next command: ${report.nextCommand}`);
22708
23199
  }
@@ -22862,7 +23353,7 @@ function registerExplainCommand(program3) {
22862
23353
  }
22863
23354
 
22864
23355
  // src/pr-commands.ts
22865
- var import_promises6 = require("node:fs/promises");
23356
+ var import_promises7 = require("node:fs/promises");
22866
23357
  var GC_GH_TIMEOUT_MS4 = 2e4;
22867
23358
  var CHECKS_WATCH_POLL_MS = 15e3;
22868
23359
  var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
@@ -23006,10 +23497,10 @@ function registerPrLifecycleCommands(program3) {
23006
23497
  let body;
23007
23498
  try {
23008
23499
  if (o.title || o.titleFile) {
23009
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23500
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23010
23501
  }
23011
23502
  if (o.body || o.bodyFile) {
23012
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23503
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23013
23504
  }
23014
23505
  } catch (e) {
23015
23506
  return fail(`pr edit: ${e.message}`);
@@ -23039,7 +23530,7 @@ function registerPrLifecycleCommands(program3) {
23039
23530
  }
23040
23531
  let body;
23041
23532
  try {
23042
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23533
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23043
23534
  } catch (e) {
23044
23535
  return fail(`pr comment: ${e.message}`);
23045
23536
  }
@@ -23443,7 +23934,7 @@ function renderDoctorText(checks, opts) {
23443
23934
  return lines.join("\n");
23444
23935
  }
23445
23936
  function doctorReportExitCode(checks) {
23446
- return checks.some((c) => !c.ok) ? 1 : 0;
23937
+ return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
23447
23938
  }
23448
23939
 
23449
23940
  // src/doctor-clean.ts
@@ -23477,6 +23968,7 @@ function checkGithubAuth(probe) {
23477
23968
  };
23478
23969
  }
23479
23970
  function checkAwsIdentity(probe) {
23971
+ if (probe.probed === false) return null;
23480
23972
  if (!probe.isOrgRepo) return null;
23481
23973
  const arn = probe.callerArn?.trim();
23482
23974
  return {
@@ -23485,7 +23977,11 @@ function checkAwsIdentity(probe) {
23485
23977
  fix: "use a non-root IAM user/session profile (set AWS_PROFILE or run: aws sso login), then verify `aws sts get-caller-identity` is not :root",
23486
23978
  // Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
23487
23979
  // configured at all" is also green here. Under --verbose those two must not look the same.
23488
- verbose: [`caller: ${arn || "(no AWS identity configured \u2014 allowed)"}`]
23980
+ //
23981
+ // #3485: the empty case says "not resolved", never "not configured". `awsCallerArn` returns undefined
23982
+ // for ANY failure — expired SSO, no `aws` binary, a timeout — so asserting "no AWS identity configured"
23983
+ // is the same false-fact green this issue is about, one layer down. Evidence states what was observed.
23984
+ verbose: [`caller: ${arn || "(none resolved \u2014 no AWS configured, or the probe failed; either is allowed)"}`]
23489
23985
  };
23490
23986
  }
23491
23987
  function checkRepoWorktrees(probe) {
@@ -23515,6 +24011,11 @@ function checkWorktreeRoots(probe) {
23515
24011
  return {
23516
24012
  ok: false,
23517
24013
  label: "worktree roots",
24014
+ // #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
24015
+ // exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
24016
+ // review of directories that may hold unlanded work, and a size-based auto-sweep once nearly deleted
24017
+ // an unlanded correctness fix. Counting it made `mmi-cli doctor` exit 1 until someone swept by hand.
24018
+ reportOnly: true,
23518
24019
  // The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
23519
24020
  // and doctor itself — with or without --apply — never touches these directories.
23520
24021
  fix: `${probe.stray.length} worktrees root(s) outside ${probe.authoritative} \u2014 ${named}; no gc scans them. Review, then sweep deliberately from the owning repo with \`mmi-cli worktree gc --root <path> --apply\` (report-only here: doctor never deletes these)${cutShort}`,
@@ -23530,7 +24031,8 @@ function checkClaudePlugin(probe) {
23530
24031
  const { installed, released, guardState } = probe;
23531
24032
  const evidence = [
23532
24033
  `installed: ${installed ?? "(none)"}`,
23533
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
24034
+ // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
24035
+ `released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
23534
24036
  `resolvable: ${guardState}`
23535
24037
  ];
23536
24038
  const trigger = pluginHealTrigger(probe);
@@ -23552,15 +24054,16 @@ function checkClaudePlugin(probe) {
23552
24054
  verbose: evidence
23553
24055
  };
23554
24056
  }
24057
+ if (!released) return null;
23555
24058
  return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin", verbose: evidence };
23556
24059
  }
23557
- function checkCliVersion(input) {
24060
+ function checkCliVersion(input, releasedNote) {
23558
24061
  const report = buildVersionLagReport(input);
23559
24062
  const evidence = [
23560
24063
  `running: ${report.currentVersion}`,
23561
- `published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`
24064
+ `published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
23562
24065
  ];
23563
- if (!report.releasedVersion) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
24066
+ if (!report.releasedVersion) return null;
23564
24067
  if (report.ok) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
23565
24068
  return {
23566
24069
  ok: false,
@@ -23595,6 +24098,12 @@ function checkPluginCache(input) {
23595
24098
  return {
23596
24099
  ok: false,
23597
24100
  label: "plugin cache",
24101
+ // #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
24102
+ // A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
24103
+ // `mmi-cli plugin prune --apply` clears it in one command and a re-run goes green, so this is an
24104
+ // ordinary red the operator is expected to act on. "The doctor cannot heal it automatically" was never
24105
+ // the bar — see the contract for why report-only is a per-row ruling rather than a rule you can apply
24106
+ // from here.
23598
24107
  // Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
23599
24108
  // on its own so a cache with zero versioned dirs still gets an honest ✗.
23600
24109
  detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
@@ -23623,10 +24132,14 @@ function checkSchedules(probe) {
23623
24132
  ...probe.incomplete.map((i) => `incomplete: ${i}`),
23624
24133
  ...probe.drift.map((d) => `drift: ${d}`)
23625
24134
  ];
24135
+ const clearableHere = probe.clearableHere ?? 0;
23626
24136
  if (probe.incomplete.length) {
23627
24137
  return {
23628
24138
  ok: false,
23629
24139
  label: "schedules",
24140
+ // #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
24141
+ // nothing in this checkout clears it.
24142
+ reportOnly: true,
23630
24143
  detail: `${probe.incomplete.length} source(s) unreadable`,
23631
24144
  fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
23632
24145
  verbose: evidence
@@ -23636,8 +24149,14 @@ function checkSchedules(probe) {
23636
24149
  return {
23637
24150
  ok: false,
23638
24151
  label: "schedules",
24152
+ // #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
24153
+ // that conditional rather than blanket: when a finding IS clearable from this checkout — a
24154
+ // `file-vs-registry-stale` row for this repo, one `org schedules register` away — the row gates
24155
+ // like any other actionable red. Exempting a fault the operator can fix in one command is the
24156
+ // tolerated-red failure the tier exists to prevent, not an instance of it.
24157
+ ...clearableHere > 0 ? {} : { reportOnly: true },
23639
24158
  detail: `${probe.drift.length} drift finding(s)`,
23640
- fix: "run `mmi-cli org schedules` \u2014 each drift line names its per-class remedy (file/registry/live mismatch or harbour enforcement), applied in the owning repo",
24159
+ fix: clearableHere > 0 ? `${clearableHere} of ${probe.drift.length} clearable from this repo \u2014 from an up-to-date checkout run \`mmi-cli org schedules register\` here, then \`mmi-cli org schedules\` for the rest (each line names its own per-class remedy, applied in the owning repo)` : "run `mmi-cli org schedules` \u2014 each drift line names its per-class remedy (file/registry/live mismatch or harbour enforcement), applied in the owning repo",
23641
24160
  verbose: evidence
23642
24161
  };
23643
24162
  }
@@ -23652,6 +24171,10 @@ function checkDocsAudit(probe) {
23652
24171
  return {
23653
24172
  ok: false,
23654
24173
  label: "docs-audit",
24174
+ // #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
24175
+ // owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
24176
+ // a dead-man check defeats the check (#3067 pillar 4: silence is an alarm, never a success).
24177
+ reportOnly: true,
23655
24178
  fix: `${probe.detail} \u2014 re-run the janitor in the owning repo or re-arm its schedule (the remedy never lives in this doctor)`,
23656
24179
  verbose: [probe.detail]
23657
24180
  };
@@ -23705,13 +24228,15 @@ function gcReapable(plan) {
23705
24228
  return plan.branches.length + plan.trackingRefs.length + plan.worktreeDirs.length;
23706
24229
  }
23707
24230
  async function runDoctorClean(opts, io, deps) {
23708
- const apply = Boolean(opts.apply);
23709
- const applyRepo = apply && opts.repoWrites !== false;
24231
+ const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
24232
+ const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
24233
+ const probeReleased = !opts.fast || Boolean(opts.self);
24234
+ const probeAws = !opts.fast && !opts.banner;
23710
24235
  const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
23711
24236
  deps.githubLogin(),
23712
24237
  deps.isOrgRepo(),
23713
- opts.fast ? Promise.resolve(void 0) : deps.releasedVersion(),
23714
- opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn(),
24238
+ probeReleased ? deps.releasedVersion() : Promise.resolve(void 0),
24239
+ probeAws ? deps.awsCallerArn() : Promise.resolve(void 0),
23715
24240
  // #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
23716
24241
  // network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
23717
24242
  // `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
@@ -23726,7 +24251,7 @@ async function runDoctorClean(opts, io, deps) {
23726
24251
  if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
23727
24252
  checks.push(...checkGithubPools(await deps.githubPools()));
23728
24253
  }
23729
- const aws = checkAwsIdentity({ isOrgRepo, callerArn });
24254
+ const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
23730
24255
  if (aws) checks.push(aws);
23731
24256
  checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
23732
24257
  if (isOrgRepo) {
@@ -23745,9 +24270,10 @@ async function runDoctorClean(opts, io, deps) {
23745
24270
  checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
23746
24271
  }
23747
24272
  }
23748
- const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo) };
24273
+ const releasedNote = deps.releasedVersionNote?.();
24274
+ const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo), releasedNote };
23749
24275
  const healTrigger = pluginHealTrigger(pluginProbe);
23750
- if (apply && deps.healPlugin && healTrigger) {
24276
+ if (applyEnv && deps.healPlugin && healTrigger) {
23751
24277
  const heal = await deps.healPlugin();
23752
24278
  const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
23753
24279
  const healEvidence = [
@@ -23764,18 +24290,25 @@ async function runDoctorClean(opts, io, deps) {
23764
24290
  } : {
23765
24291
  ok: false,
23766
24292
  label,
23767
- fix: `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
24293
+ // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
24294
+ // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
24295
+ // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
24296
+ // genuine gap and still gates.
24297
+ ...heal.skipped ? { reportOnly: true } : {},
24298
+ fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
23768
24299
  verbose: healEvidence
23769
24300
  });
23770
- restartPending = true;
24301
+ if (!heal.skipped) restartPending = true;
23771
24302
  } else {
23772
24303
  const plugin = checkClaudePlugin(pluginProbe);
23773
- checks.push(plugin);
23774
- if (!plugin.ok) restartPending = true;
24304
+ if (plugin) {
24305
+ checks.push(plugin);
24306
+ if (!plugin.ok) restartPending = true;
24307
+ }
23775
24308
  }
23776
24309
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
23777
24310
  const cliReport = buildVersionLagReport(cliInput);
23778
- if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
24311
+ if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
23779
24312
  const heal = await deps.updateCli(cliReport.releasedVersion);
23780
24313
  const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
23781
24314
  checks.push(heal.ok ? {
@@ -23786,17 +24319,48 @@ async function runDoctorClean(opts, io, deps) {
23786
24319
  } : {
23787
24320
  ok: false,
23788
24321
  label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
23789
- fix: `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
24322
+ // #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
24323
+ ...heal.skipped ? { reportOnly: true } : {},
24324
+ fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
23790
24325
  verbose: healEvidence
23791
24326
  });
23792
24327
  } else {
23793
- checks.push(checkCliVersion(cliInput));
24328
+ const cli = checkCliVersion(cliInput, releasedNote);
24329
+ if (cli) checks.push(cli);
24330
+ }
24331
+ const cacheProbe = deps.pluginCache();
24332
+ const cacheStale = cacheProbe.stale.length > 0 || cacheProbe.staging.length > 0;
24333
+ if (applyEnv && deps.prunePluginCache && cacheStale) {
24334
+ const pruned = await deps.prunePluginCache();
24335
+ const removed = pruned.removed ?? [];
24336
+ const pruneEvidence = [
24337
+ `cached versions: ${cacheProbe.cached}`,
24338
+ `stale: ${cacheProbe.stale.length ? cacheProbe.stale.join(", ") : "(none)"}`,
24339
+ `orphaned staging dirs: ${cacheProbe.staging.length ? cacheProbe.staging.join(", ") : "(none)"}`,
24340
+ `prune: ${pruned.detail}`,
24341
+ ...removed.map((r) => `removed: ${r}`)
24342
+ ];
24343
+ checks.push(pruned.ok ? {
24344
+ ok: true,
24345
+ label: "plugin cache",
24346
+ detail: removed.length ? `pruned ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}` : "nothing to prune",
24347
+ verbose: pruneEvidence
24348
+ } : {
24349
+ ok: false,
24350
+ label: "plugin cache",
24351
+ // Same split as the other env heals (#3489): a contention skip is not this run's gap.
24352
+ ...pruned.skipped ? { reportOnly: true } : {},
24353
+ fix: pruned.skipped ? `${pruned.detail} \u2014 another doctor on this machine is pruning it now; re-run once it finishes` : `prune failed (${pruned.detail}) \u2014 run \`mmi-cli plugin prune --apply\``,
24354
+ verbose: pruneEvidence
24355
+ });
24356
+ } else {
24357
+ checks.push(checkPluginCache(cacheProbe));
23794
24358
  }
23795
- checks.push(checkPluginCache(deps.pluginCache()));
23796
24359
  if (deps.sessionPayload) {
23797
24360
  const payload = checkSessionPayload(deps.sessionPayload());
23798
24361
  if (payload) checks.push(payload);
23799
24362
  }
24363
+ if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
23800
24364
  if (!opts.fast && !opts.banner && !opts.preflight) {
23801
24365
  const probe = await deps.schedulesNotebook().catch((e) => ({
23802
24366
  armed: 0,
@@ -23822,7 +24386,7 @@ async function runDoctorClean(opts, io, deps) {
23822
24386
  const roots = checkWorktreeRoots(probe);
23823
24387
  if (roots) checks.push(roots);
23824
24388
  }
23825
- if (isOrgRepo && !opts.fast && !opts.banner) {
24389
+ if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
23826
24390
  try {
23827
24391
  checks.push(checkTrainSync(await deps.syncTrain()));
23828
24392
  } catch (e) {
@@ -23924,17 +24488,17 @@ function parseOriginRepo(remoteUrl) {
23924
24488
  }
23925
24489
  function ghHostsConfigPath(env, platform2) {
23926
24490
  const sep2 = platform2 === "win32" ? "\\" : "/";
23927
- const join24 = (...parts) => parts.join(sep2);
24491
+ const join26 = (...parts) => parts.join(sep2);
23928
24492
  const explicit = env.GH_CONFIG_DIR?.trim();
23929
- if (explicit) return join24(explicit, "hosts.yml");
24493
+ if (explicit) return join26(explicit, "hosts.yml");
23930
24494
  if (platform2 === "win32") {
23931
24495
  const appData = (env.AppData ?? env.APPDATA)?.trim();
23932
- return appData ? join24(appData, "GitHub CLI", "hosts.yml") : void 0;
24496
+ return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
23933
24497
  }
23934
24498
  const xdg = env.XDG_CONFIG_HOME?.trim();
23935
- if (xdg) return join24(xdg, "gh", "hosts.yml");
24499
+ if (xdg) return join26(xdg, "gh", "hosts.yml");
23936
24500
  const home = env.HOME?.trim();
23937
- return home ? join24(home, ".config", "gh", "hosts.yml") : void 0;
24501
+ return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
23938
24502
  }
23939
24503
  function parseGhHostsAccounts(yaml, host = "github.com") {
23940
24504
  let hostIndent = null;
@@ -23984,9 +24548,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
23984
24548
  }
23985
24549
 
23986
24550
  // src/doctor-io.ts
23987
- var import_node_fs29 = require("node:fs");
23988
- var import_node_os7 = require("node:os");
23989
- var import_node_path26 = require("node:path");
24551
+ var import_node_fs31 = require("node:fs");
24552
+ var import_node_os8 = require("node:os");
24553
+ var import_node_path29 = require("node:path");
23990
24554
  var import_node_child_process12 = require("node:child_process");
23991
24555
  var import_node_util8 = require("node:util");
23992
24556
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
@@ -23994,7 +24558,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
23994
24558
  function installedClaudePluginVersion() {
23995
24559
  try {
23996
24560
  const file = JSON.parse(
23997
- (0, import_node_fs29.readFileSync)((0, import_node_path26.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
24561
+ (0, import_node_fs31.readFileSync)((0, import_node_path29.join)((0, import_node_os8.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23998
24562
  );
23999
24563
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
24000
24564
  if (versions.length === 0) return void 0;
@@ -24015,13 +24579,13 @@ function worktreeRootSync() {
24015
24579
  }
24016
24580
  var gitignorePath = () => {
24017
24581
  const root = worktreeRootSync();
24018
- return root === null ? null : (0, import_node_path26.join)(root, ".gitignore");
24582
+ return root === null ? null : (0, import_node_path29.join)(root, ".gitignore");
24019
24583
  };
24020
24584
  function readGitignore() {
24021
24585
  const path2 = gitignorePath();
24022
24586
  if (path2 === null) return null;
24023
24587
  try {
24024
- return (0, import_node_fs29.readFileSync)(path2, "utf8");
24588
+ return (0, import_node_fs31.readFileSync)(path2, "utf8");
24025
24589
  } catch {
24026
24590
  return null;
24027
24591
  }
@@ -24030,7 +24594,7 @@ function writeGitignore(content) {
24030
24594
  const path2 = gitignorePath();
24031
24595
  if (path2 === null) return false;
24032
24596
  try {
24033
- (0, import_node_fs29.writeFileSync)(path2, content, "utf8");
24597
+ (0, import_node_fs31.writeFileSync)(path2, content, "utf8");
24034
24598
  return true;
24035
24599
  } catch {
24036
24600
  return false;
@@ -24054,7 +24618,7 @@ async function repoRoot() {
24054
24618
  }
24055
24619
  function hasRepoLocalWorktrees() {
24056
24620
  const root = worktreeRootSync();
24057
- return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path26.join)(root, ".worktrees"));
24621
+ return root !== null && (0, import_node_fs31.existsSync)((0, import_node_path29.join)(root, ".worktrees"));
24058
24622
  }
24059
24623
 
24060
24624
  // src/index.ts
@@ -24089,13 +24653,61 @@ ${r.stderr ?? ""}`).catch(() => "");
24089
24653
  function ghMultiAccountCaveat(announcedLogin) {
24090
24654
  try {
24091
24655
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
24092
- if (!hostsPath || !(0, import_node_fs30.existsSync)(hostsPath)) return void 0;
24093
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs30.readFileSync)(hostsPath, "utf8")));
24656
+ if (!hostsPath || !(0, import_node_fs32.existsSync)(hostsPath)) return void 0;
24657
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs32.readFileSync)(hostsPath, "utf8")));
24094
24658
  } catch {
24095
24659
  return void 0;
24096
24660
  }
24097
24661
  }
24098
- function mmiDoctorDeps() {
24662
+ var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
24663
+ var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
24664
+ function envHealLockPath(home) {
24665
+ return (0, import_node_path30.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
24666
+ }
24667
+ async function withEnvHealLock(what, run) {
24668
+ try {
24669
+ return await withFileLock(
24670
+ envHealLockPath((0, import_node_os9.homedir)()),
24671
+ { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
24672
+ run
24673
+ );
24674
+ } catch (e) {
24675
+ if (e instanceof FileLockBusyError) {
24676
+ return { ok: false, skipped: true, detail: `${what} skipped \u2014 ${e.message}` };
24677
+ }
24678
+ return { ok: false, detail: `${what} could not take the env-heal lock \u2014 ${e.message}` };
24679
+ }
24680
+ }
24681
+ function docsJanitorScheduleId(repo) {
24682
+ return `${repo.split("/").pop()}/docs-janitor`;
24683
+ }
24684
+ function throttledReleasedVersion() {
24685
+ let note;
24686
+ return {
24687
+ note: () => note,
24688
+ read: async () => {
24689
+ const cachePath = releasedVersionCachePath(repoRuntimeStatePath(process.cwd()));
24690
+ const cached = readReleasedVersionCache(cachePath);
24691
+ if (cached) {
24692
+ note = cachedReadNote(cached.at);
24693
+ return cached.version;
24694
+ }
24695
+ const live = await fetchNpmReleasedVersion();
24696
+ if (live) writeReleasedVersionCache(cachePath, live);
24697
+ note = void 0;
24698
+ return live;
24699
+ }
24700
+ };
24701
+ }
24702
+ function mmiDoctorDeps(opts = {}) {
24703
+ const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
24704
+ let notebook;
24705
+ const notebookOnce = () => notebook ??= fetchNotebook();
24706
+ const docsJanitorArmedAt = async (repo) => {
24707
+ const wanted = docsJanitorScheduleId(repo);
24708
+ const { entries } = await notebookOnce();
24709
+ return entries.find((e) => e.scheduleId === wanted)?.armedAt;
24710
+ };
24099
24711
  return {
24100
24712
  githubLogin,
24101
24713
  ghInstalled,
@@ -24104,12 +24716,17 @@ function mmiDoctorDeps() {
24104
24716
  isOrgRepo: () => isOrgRepoRoot(),
24105
24717
  installedPluginVersion: installedClaudePluginVersion,
24106
24718
  pluginGuardState: claudePluginGuardState,
24107
- releasedVersion: fetchNpmReleasedVersion,
24719
+ releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
24720
+ releasedVersionNote: throttled ? throttled.note : void 0,
24108
24721
  // #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
24109
- updateCli: npmSelfUpdateCli,
24722
+ // #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
24723
+ // marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
24724
+ // rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
24725
+ // carries a CLI shim, so a concurrent npm global install races the same PATH surface.
24726
+ updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
24110
24727
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
24111
24728
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
24112
- healPlugin: () => healClaudePluginForDoctor(),
24729
+ healPlugin: () => withEnvHealLock("Claude plugin reinstall", () => healClaudePluginForDoctor()),
24113
24730
  currentCliVersion: resolveClientVersion,
24114
24731
  readGitignore,
24115
24732
  writeGitignore,
@@ -24145,9 +24762,9 @@ function mmiDoctorDeps() {
24145
24762
  },
24146
24763
  pluginCache: () => {
24147
24764
  const plan = buildPluginCachePlan(
24148
- (0, import_node_os8.homedir)(),
24765
+ (0, import_node_os9.homedir)(),
24149
24766
  runningPluginVersion(process.env, resolveClientVersion()),
24150
- pluginCacheFsDeps((0, import_node_os8.homedir)(), () => 0)
24767
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), () => 0)
24151
24768
  );
24152
24769
  return {
24153
24770
  stale: plan.prune,
@@ -24157,16 +24774,45 @@ function mmiDoctorDeps() {
24157
24774
  stagingBytes: plan.stagingBytes
24158
24775
  };
24159
24776
  },
24777
+ // #3485 wave 2 item 11: the --apply reap, through the SAME plan + reaper `plugin prune --apply` drives.
24778
+ // The plan's own keep-policy refuses the running and newest versions, so this cannot delete what is
24779
+ // loaded. Under the #3489 env-heal lock: deleting cache dirs while another doctor is mid-reinstall is
24780
+ // the same machine-global hazard the lock exists for.
24781
+ prunePluginCache: () => withEnvHealLock("plugin cache prune", async () => {
24782
+ const plan = buildPluginCachePlan(
24783
+ (0, import_node_os9.homedir)(),
24784
+ runningPluginVersion(process.env, resolveClientVersion()),
24785
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
24786
+ { withBytes: true }
24787
+ );
24788
+ const result = applyPluginCachePlan(
24789
+ plan,
24790
+ (p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }),
24791
+ stagingApplyFsGuard((0, import_node_os9.homedir)())
24792
+ );
24793
+ const removed = [...result.removed, ...result.removedStaging ?? []];
24794
+ if (result.failed.length) {
24795
+ return {
24796
+ ok: false,
24797
+ removed,
24798
+ detail: `${removed.length} removed, ${result.failed.length} failed \u2014 ${result.failed.map((f) => `${f.version}: ${f.error}`).join("; ")}`
24799
+ };
24800
+ }
24801
+ return { ok: true, removed, detail: removed.length ? `removed ${removed.join(", ")}` : "nothing to prune" };
24802
+ }),
24160
24803
  // #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
24161
24804
  // runDoctorClean skips this dep entirely on fast/banner/preflight runs.
24162
24805
  schedulesNotebook: async () => {
24163
- const { entries, incomplete, drift } = await fetchNotebook();
24806
+ const { entries, incomplete, drift, reconciliation } = await notebookOnce();
24807
+ const repoName = await repoSlug().catch(() => "");
24808
+ const clearableHere = repoName ? reconciliation.filter((d) => driftClearableFrom(d, repoName)).length : 0;
24164
24809
  return {
24165
24810
  armed: entries.length,
24166
24811
  live: entries.filter((e) => e.resolved === "live").length,
24167
24812
  declared: entries.filter((e) => e.resolved === "declared").length,
24168
24813
  incomplete,
24169
- drift
24814
+ drift,
24815
+ clearableHere
24170
24816
  };
24171
24817
  },
24172
24818
  // #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
@@ -24175,7 +24821,8 @@ function mmiDoctorDeps() {
24175
24821
  docsAudit: async () => {
24176
24822
  const repo = await currentRepoFullName();
24177
24823
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
24178
- const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
24824
+ const armedAt = await docsJanitorArmedAt(repo).catch(() => void 0);
24825
+ const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today, armedAt });
24179
24826
  return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
24180
24827
  },
24181
24828
  // #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
@@ -24184,7 +24831,21 @@ function mmiDoctorDeps() {
24184
24831
  worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
24185
24832
  // #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
24186
24833
  // A local record read — cheap enough for every lane, including the banner.
24187
- sessionPayload: () => readSessionPayload(process.cwd())
24834
+ sessionPayload: () => readSessionPayload(process.cwd()),
24835
+ // #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
24836
+ // which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
24837
+ marketplaceRows: () => {
24838
+ try {
24839
+ const home = (0, import_node_os9.homedir)();
24840
+ return marketplaceRows(
24841
+ MMI_MARKETPLACE_NAME,
24842
+ readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
24843
+ readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
24844
+ );
24845
+ } catch {
24846
+ return [];
24847
+ }
24848
+ }
24188
24849
  };
24189
24850
  }
24190
24851
  async function requireFreshTrainCli(commandName) {
@@ -24309,19 +24970,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
24309
24970
  });
24310
24971
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
24311
24972
  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) => {
24312
- const path2 = (0, import_node_path27.join)(process.cwd(), ".gitignore");
24313
- const current = (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null;
24973
+ const path2 = (0, import_node_path30.join)(process.cwd(), ".gitignore");
24974
+ const current = (0, import_node_fs32.existsSync)(path2) ? (0, import_node_fs32.readFileSync)(path2, "utf8") : null;
24314
24975
  const plan = planManagedGitignore(current);
24315
24976
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
24316
24977
  if (opts.json) {
24317
- if (opts.write && plan.changed) (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
24978
+ if (opts.write && plan.changed) (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
24318
24979
  console.log(JSON.stringify(plan, null, 2));
24319
24980
  if (!opts.write && plan.changed) process.exitCode = 1;
24320
24981
  return;
24321
24982
  }
24322
24983
  if (opts.write) {
24323
24984
  if (plan.changed) {
24324
- (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
24985
+ (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
24325
24986
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
24326
24987
  } else {
24327
24988
  console.log("mmi-cli org rules gitignore: up to date");
@@ -24447,8 +25108,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
24447
25108
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
24448
25109
  let root;
24449
25110
  if (o.root !== void 0) {
24450
- root = (0, import_node_path27.resolve)(o.root);
24451
- if (!(0, import_node_fs30.existsSync)(root) || !(0, import_node_fs30.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
25111
+ root = (0, import_node_path30.resolve)(o.root);
25112
+ if (!(0, import_node_fs32.existsSync)(root) || !(0, import_node_fs32.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
24452
25113
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
24453
25114
  if (isPathUnderDirectory(gcRepoRoot, root)) {
24454
25115
  return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
@@ -24522,26 +25183,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
24522
25183
  function acquireWorktreeSetupLock(worktreeRoot) {
24523
25184
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
24524
25185
  const take = () => {
24525
- const fd = (0, import_node_fs30.openSync)(lockPath, "wx");
25186
+ const fd = (0, import_node_fs32.openSync)(lockPath, "wx");
24526
25187
  try {
24527
- (0, import_node_fs30.writeSync)(fd, String(Date.now()));
25188
+ (0, import_node_fs32.writeSync)(fd, String(Date.now()));
24528
25189
  } finally {
24529
- (0, import_node_fs30.closeSync)(fd);
25190
+ (0, import_node_fs32.closeSync)(fd);
24530
25191
  }
24531
25192
  return () => {
24532
25193
  try {
24533
- (0, import_node_fs30.rmSync)(lockPath, { force: true });
25194
+ (0, import_node_fs32.rmSync)(lockPath, { force: true });
24534
25195
  } catch {
24535
25196
  }
24536
25197
  };
24537
25198
  };
24538
25199
  try {
24539
- (0, import_node_fs30.mkdirSync)((0, import_node_path27.dirname)(lockPath), { recursive: true });
25200
+ (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(lockPath), { recursive: true });
24540
25201
  return take();
24541
25202
  } catch {
24542
25203
  try {
24543
- if (Date.now() - (0, import_node_fs30.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
24544
- (0, import_node_fs30.rmSync)(lockPath, { force: true });
25204
+ if (Date.now() - (0, import_node_fs32.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
25205
+ (0, import_node_fs32.rmSync)(lockPath, { force: true });
24545
25206
  return take();
24546
25207
  }
24547
25208
  } catch {
@@ -24983,7 +25644,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
24983
25644
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
24984
25645
  if (o.secretsFile) {
24985
25646
  try {
24986
- vars.push(`secrets=${(0, import_node_fs30.readFileSync)(o.secretsFile, "utf8")}`);
25647
+ vars.push(`secrets=${(0, import_node_fs32.readFileSync)(o.secretsFile, "utf8")}`);
24987
25648
  } catch (e) {
24988
25649
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
24989
25650
  }
@@ -25099,8 +25760,8 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
25099
25760
  const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
25100
25761
  if (branch) {
25101
25762
  const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
25102
- const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
25103
- if (sameRepo) {
25763
+ const sameRepo2 = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
25764
+ if (sameRepo2) {
25104
25765
  const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
25105
25766
  if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
25106
25767
  ${filelessTransitionGuide(target, o.stage)}`);
@@ -25297,8 +25958,8 @@ withExamples(mutating(
25297
25958
  let targetRepo2;
25298
25959
  try {
25299
25960
  issueType = resolveCreateType(o.type, "issue create", o.label);
25300
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
25301
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
25961
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
25962
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25302
25963
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
25303
25964
  priority = resolveCreatePriority(o.priority, "issue create");
25304
25965
  extraLabels = [...o.label ?? []];
@@ -25441,7 +26102,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
25441
26102
  }
25442
26103
  let body;
25443
26104
  try {
25444
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
26105
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25445
26106
  } catch (e) {
25446
26107
  return fail(`issue comment: ${e.message}`);
25447
26108
  }
@@ -25501,8 +26162,8 @@ program2.command("report").description("file a friction report on the Hub board
25501
26162
  let title;
25502
26163
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
25503
26164
  try {
25504
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
25505
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
26165
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
26166
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25506
26167
  priority = resolveCreatePriority(o.priority, "report");
25507
26168
  if (!ISSUE_TYPES.includes(o.type)) {
25508
26169
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -25551,8 +26212,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
25551
26212
  let args;
25552
26213
  try {
25553
26214
  skill = assertSkillName(o.skill);
25554
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
25555
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
26215
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
26216
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
25556
26217
  title = buildSkillLessonTitle(skill, rawTitle);
25557
26218
  priority = resolveCreatePriority(o.priority, "skill-lesson");
25558
26219
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -25603,8 +26264,8 @@ withExamples(pr.command("create").description("create a PR and print {number,url
25603
26264
  let body;
25604
26265
  let title;
25605
26266
  try {
25606
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
25607
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
26267
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
26268
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25608
26269
  } catch (e) {
25609
26270
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
25610
26271
  }
@@ -25639,11 +26300,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
25639
26300
  }
25640
26301
  });
25641
26302
  async function listCiWorkflowPaths(cwd = process.cwd()) {
25642
- const wfDir = (0, import_node_path27.join)(cwd, ".github", "workflows");
25643
- if (!(0, import_node_fs30.existsSync)(wfDir)) return [];
25644
- return (0, import_node_fs30.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
26303
+ const wfDir = (0, import_node_path30.join)(cwd, ".github", "workflows");
26304
+ if (!(0, import_node_fs32.existsSync)(wfDir)) return [];
26305
+ return (0, import_node_fs32.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
25645
26306
  try {
25646
- return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path27.join)(wfDir, name), "utf8"));
26307
+ return workflowReportsPrChecks((0, import_node_fs32.readFileSync)((0, import_node_path30.join)(wfDir, name), "utf8"));
25647
26308
  } catch {
25648
26309
  return true;
25649
26310
  }
@@ -25670,16 +26331,16 @@ function ciAuditDeps() {
25670
26331
  // gate re-seed step is skipped gracefully rather than failing mid-run.
25671
26332
  readSeedFile: (path2) => {
25672
26333
  if (!root) return null;
25673
- const fullPath = (0, import_node_path27.join)(root, path2);
25674
- return (0, import_node_fs30.existsSync)(fullPath) ? (0, import_node_fs30.readFileSync)(fullPath, "utf8") : null;
26334
+ const fullPath = (0, import_node_path30.join)(root, path2);
26335
+ return (0, import_node_fs32.existsSync)(fullPath) ? (0, import_node_fs32.readFileSync)(fullPath, "utf8") : null;
25675
26336
  }
25676
26337
  };
25677
26338
  }
25678
26339
  function hubRoot() {
25679
- const fromPkg = (0, import_node_path27.join)(__dirname, "..", "..");
26340
+ const fromPkg = (0, import_node_path30.join)(__dirname, "..", "..");
25680
26341
  const marker = "skills/bootstrap/seeds/manifest.json";
25681
- if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(fromPkg, marker))) return fromPkg;
25682
- if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(process.cwd(), marker))) return process.cwd();
26342
+ if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(fromPkg, marker))) return fromPkg;
26343
+ if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), marker))) return process.cwd();
25683
26344
  return null;
25684
26345
  }
25685
26346
  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) => {
@@ -25736,11 +26397,18 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
25736
26397
  const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
25737
26398
  const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim();
25738
26399
  const [repoFromGh, base] = viewed.split(/\s+/);
25739
- if (base && base !== "development") {
25740
- throw new Error(`pr land: base branch must be development (got ${base}) \u2014 promotion merges stay human-only`);
25741
- }
25742
26400
  const repo = repoOpt ?? repoFromGh;
25743
26401
  if (!repo) throw new Error("pr land: could not resolve PR repo");
26402
+ let track;
26403
+ try {
26404
+ const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
26405
+ if (meta) track = resolveReleaseTrack(meta, void 0, repo);
26406
+ } catch {
26407
+ }
26408
+ if (isPromotionBase(base, track)) {
26409
+ const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
26410
+ throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
26411
+ }
25744
26412
  return repo;
25745
26413
  },
25746
26414
  fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
@@ -25929,7 +26597,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
25929
26597
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
25930
26598
  beforeWorktrees,
25931
26599
  startingPath,
25932
- pathExists: (p) => (0, import_node_fs30.existsSync)(p),
26600
+ pathExists: (p) => (0, import_node_fs32.existsSync)(p),
25933
26601
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
25934
26602
  teardownWorktreeStage,
25935
26603
  deferredStore,
@@ -26039,8 +26707,8 @@ function trainApplyDeps() {
26039
26707
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
26040
26708
  announce: (args) => announceRelease({
26041
26709
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
26042
- readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8"),
26043
- removeFile: (path2) => (0, import_promises7.unlink)(path2)
26710
+ readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
26711
+ removeFile: (path2) => (0, import_promises8.unlink)(path2)
26044
26712
  }, args),
26045
26713
  fetchEdgeDomains: async (slug) => {
26046
26714
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -26270,10 +26938,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
26270
26938
  targets = resolution.targets;
26271
26939
  }
26272
26940
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
26273
- const fileMatrix = (0, import_node_fs30.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs30.readFileSync)("access-matrix.json", "utf8")) : {};
26941
+ const fileMatrix = (0, import_node_fs32.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs32.readFileSync)("access-matrix.json", "utf8")) : {};
26274
26942
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
26275
26943
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
26276
- const fileContracts = (0, import_node_fs30.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs30.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
26944
+ const fileContracts = (0, import_node_fs32.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs32.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
26277
26945
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
26278
26946
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
26279
26947
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -26281,7 +26949,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
26281
26949
  });
26282
26950
  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)));
26283
26951
  var isWin2 = process.platform === "win32";
26284
- 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", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").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)").option("--self", "verify CLI/plugin version parity, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#2689)").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(async (opts) => {
26952
+ 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 (env repairs only \u2014 never the repo working tree) with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").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)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nThree rows (worktree roots, schedules, docs-audit) are report-only: they still print \u2717 with their fix,\nbut never move the exit code, because their remedy is a deliberate human review or lives in the repo\nthat owns the fault. Every other red row gates, including ones only you can clear by hand (#3485).\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
26285
26953
  if (opts.guide) {
26286
26954
  consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
26287
26955
  return;
@@ -26307,16 +26975,16 @@ function directoryBytes(path2) {
26307
26975
  let total = 0;
26308
26976
  let entries;
26309
26977
  try {
26310
- entries = (0, import_node_fs30.readdirSync)(path2, { withFileTypes: true });
26978
+ entries = (0, import_node_fs32.readdirSync)(path2, { withFileTypes: true });
26311
26979
  } catch {
26312
26980
  return 0;
26313
26981
  }
26314
26982
  for (const entry of entries) {
26315
- const child2 = (0, import_node_path27.join)(path2, entry.name);
26983
+ const child2 = (0, import_node_path30.join)(path2, entry.name);
26316
26984
  if (entry.isDirectory()) total += directoryBytes(child2);
26317
26985
  else {
26318
26986
  try {
26319
- total += (0, import_node_fs30.statSync)(child2).size;
26987
+ total += (0, import_node_fs32.statSync)(child2).size;
26320
26988
  } catch {
26321
26989
  }
26322
26990
  }
@@ -26324,25 +26992,25 @@ function directoryBytes(path2) {
26324
26992
  return total;
26325
26993
  }
26326
26994
  function listDirEntries(dir) {
26327
- return (0, import_node_fs30.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
26995
+ return (0, import_node_fs32.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
26328
26996
  }
26329
26997
  function readInstalledPluginRefs(home) {
26330
26998
  const p = installedPluginsPath(home);
26331
- if (!(0, import_node_fs30.existsSync)(p)) return [];
26999
+ if (!(0, import_node_fs32.existsSync)(p)) return [];
26332
27000
  try {
26333
- return installedPluginPaths((0, import_node_fs30.readFileSync)(p, "utf8"));
27001
+ return installedPluginPaths((0, import_node_fs32.readFileSync)(p, "utf8"));
26334
27002
  } catch {
26335
27003
  return null;
26336
27004
  }
26337
27005
  }
26338
27006
  function pluginCacheFsDeps(home, dirBytes) {
26339
27007
  return {
26340
- exists: (p) => (0, import_node_fs30.existsSync)(p),
26341
- listVersionDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
27008
+ exists: (p) => (0, import_node_fs32.existsSync)(p),
27009
+ listVersionDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
26342
27010
  dirBytes,
26343
- listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
27011
+ listStagingDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
26344
27012
  try {
26345
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path27.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
27013
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path30.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs32.statSync)(p).mtimeMs) };
26346
27014
  } catch {
26347
27015
  return { name: d.name, mtimeMs: Date.now() };
26348
27016
  }
@@ -26356,10 +27024,10 @@ function stagingApplyFsGuard(home) {
26356
27024
  return {
26357
27025
  referencedPaths: () => readInstalledPluginRefs(home),
26358
27026
  mtimeMs: (name) => {
26359
- const p = (0, import_node_path27.join)(stagingRoot, name);
26360
- if (!(0, import_node_fs30.existsSync)(p)) return null;
27027
+ const p = (0, import_node_path30.join)(stagingRoot, name);
27028
+ if (!(0, import_node_fs32.existsSync)(p)) return null;
26361
27029
  try {
26362
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
27030
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs32.statSync)(q).mtimeMs);
26363
27031
  } catch {
26364
27032
  return null;
26365
27033
  }
@@ -26369,13 +27037,13 @@ function stagingApplyFsGuard(home) {
26369
27037
  }
26370
27038
  program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
26371
27039
  const plan = buildPluginCachePlan(
26372
- (0, import_node_os8.homedir)(),
27040
+ (0, import_node_os9.homedir)(),
26373
27041
  runningPluginVersion(process.env, resolveClientVersion()),
26374
- pluginCacheFsDeps((0, import_node_os8.homedir)(), directoryBytes),
27042
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
26375
27043
  { withBytes: true }
26376
27044
  );
26377
27045
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
26378
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs30.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
27046
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os9.homedir)())) : void 0;
26379
27047
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
26380
27048
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
26381
27049
  else console.log(renderPluginCachePlan(plan, result));
@@ -26404,6 +27072,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
26404
27072
  });
26405
27073
  const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
26406
27074
  if (line) io.log(line);
27075
+ const authority = authorityLine(report);
27076
+ if (authority) io.log(authority);
26407
27077
  },
26408
27078
  boardSlice: (io) => runBoardSlice(io, {
26409
27079
  loadConfig: () => loadConfigForRepo(),
@@ -26433,8 +27103,10 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
26433
27103
  const line = localTrainSyncBannerLine(result);
26434
27104
  if (line) io.log(line);
26435
27105
  },
27106
+ // #3485 item 7: the ONLY lane that throttles the npm read — it runs on every session start, on a
27107
+ // blocking hook. Every interactive lane still reads live.
26436
27108
  doctor: async (io) => {
26437
- await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
27109
+ await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
26438
27110
  }
26439
27111
  });
26440
27112
  await runSessionStart(parallel, sequential, bannerIo);
@@ -26457,6 +27129,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
26457
27129
  DEFAULT_PRIORITY,
26458
27130
  awsCallerArn,
26459
27131
  classifyParseError,
27132
+ envHealLockPath,
26460
27133
  gcPlan,
26461
27134
  isOrgRegisteredRepo,
26462
27135
  registryClientDeps,