@mutmutco/cli 3.51.0 → 3.52.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 +1029 -437
  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
+ );
5742
+ }
5743
+ function pathsAreCaseInsensitive(platform2 = process.platform) {
5744
+ return platform2 === "win32" || platform2 === "darwin";
5706
5745
  }
5707
- function normPath(p) {
5708
- return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
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 = {
@@ -7443,7 +7489,7 @@ async function worktreeBranches() {
7443
7489
  let dirty = true;
7444
7490
  try {
7445
7491
  const { stdout: status } = await execFileP2("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7446
- dirty = status.trim().length > 0;
7492
+ dirty = !isRemovableDirt(classifyWorktreeDirt(status));
7447
7493
  } catch {
7448
7494
  dirty = true;
7449
7495
  }
@@ -7859,6 +7905,12 @@ function parseIssueSelector(selector, defaultRepo) {
7859
7905
  if (local) return { repo: defaultRepo, number: Number(local[1]) };
7860
7906
  throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
7861
7907
  }
7908
+ function sameRepo(itemRepo, selectorKey) {
7909
+ const repo = itemRepo.toLowerCase();
7910
+ if (repo === selectorKey) return true;
7911
+ if (selectorKey.includes("/")) return false;
7912
+ return repo.slice(repo.indexOf("/") + 1) === selectorKey;
7913
+ }
7862
7914
  function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
7863
7915
  const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
7864
7916
  const groups = { primary: empty(), secondary: empty() };
@@ -7866,7 +7918,7 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
7866
7918
  const repoKey = currentRepo.toLowerCase();
7867
7919
  for (const item of items) {
7868
7920
  if (!ACTIVE_STATUSES.has(item.status)) continue;
7869
- const scope = item.repository.toLowerCase() === repoKey ? "primary" : "secondary";
7921
+ const scope = sameRepo(item.repository, repoKey) ? "primary" : "secondary";
7870
7922
  const assignees = item.assignees.map((a) => a.toLowerCase());
7871
7923
  const assignedToViewer = assignees.includes(viewerKey);
7872
7924
  if (assignedToViewer) {
@@ -8413,11 +8465,11 @@ var defaultRetrySleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, m
8413
8465
  async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
8414
8466
  const attempts = Math.max(1, opts.attempts ?? 5);
8415
8467
  const delayMs = opts.delayMs ?? 300;
8416
- const sleep2 = opts.sleep ?? defaultRetrySleep;
8468
+ const sleep3 = opts.sleep ?? defaultRetrySleep;
8417
8469
  for (let attempt = 0; attempt < attempts; attempt += 1) {
8418
8470
  const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
8419
8471
  if (itemId) return itemId;
8420
- if (attempt < attempts - 1) await sleep2(delayMs * (attempt + 1));
8472
+ if (attempt < attempts - 1) await sleep3(delayMs * (attempt + 1));
8421
8473
  }
8422
8474
  return void 0;
8423
8475
  }
@@ -9000,9 +9052,196 @@ async function gatherStaleWorktreeWarning(gitRun = defaultGitRun) {
9000
9052
  }
9001
9053
  }
9002
9054
 
9003
- // src/hook-activity.ts
9055
+ // src/released-version-cache.ts
9004
9056
  var import_node_fs12 = require("node:fs");
9005
9057
  var import_node_path10 = require("node:path");
9058
+ var RELEASED_VERSION_CACHE_MS = 24 * 36e5;
9059
+ function releasedVersionCachePath(runtimeRoot) {
9060
+ return (0, import_node_path10.join)(runtimeRoot, "head-ts", ".released-version");
9061
+ }
9062
+ function readReleasedVersionCache(cachePath, now = Date.now(), read = import_node_fs12.readFileSync) {
9063
+ let parsed;
9064
+ try {
9065
+ parsed = JSON.parse(read(cachePath, "utf8"));
9066
+ } catch {
9067
+ return void 0;
9068
+ }
9069
+ if (!parsed || typeof parsed !== "object") return void 0;
9070
+ const { version, at } = parsed;
9071
+ if (typeof version !== "string" || !version.trim()) return void 0;
9072
+ if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
9073
+ const age = now - at;
9074
+ if (age < 0 || age >= RELEASED_VERSION_CACHE_MS) return void 0;
9075
+ return { version, at };
9076
+ }
9077
+ function writeReleasedVersionCache(cachePath, version, now = Date.now()) {
9078
+ try {
9079
+ (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(cachePath), { recursive: true });
9080
+ (0, import_node_fs12.writeFileSync)(cachePath, JSON.stringify({ version, at: now }), "utf8");
9081
+ } catch {
9082
+ }
9083
+ }
9084
+ function cacheAgeHours(at, now = Date.now()) {
9085
+ return Math.max(0, Math.floor((now - at) / 36e5));
9086
+ }
9087
+ function cachedReadNote(cachedAt, now = Date.now()) {
9088
+ const hours = cacheAgeHours(cachedAt, now);
9089
+ return `(cached ${hours < 1 ? "<1" : hours}h ago \u2014 the banner reads npm at most once a day)`;
9090
+ }
9091
+
9092
+ // src/marketplace-autoupdate.ts
9093
+ var KNOWN_MARKETPLACES_RELATIVE = [".claude", "plugins", "known_marketplaces.json"];
9094
+ var MMI_MARKETPLACE_NAME = "mutmutco";
9095
+ function readFileSyncSafe(path2, read) {
9096
+ try {
9097
+ return read(path2, "utf8");
9098
+ } catch {
9099
+ return void 0;
9100
+ }
9101
+ }
9102
+ var AUTO_UPDATE_DEFAULT_ON = /* @__PURE__ */ new Set([
9103
+ "claude-plugins-official",
9104
+ "claude-code-marketplace",
9105
+ "claude-code-plugins",
9106
+ "anthropic-marketplace",
9107
+ "anthropic-plugins",
9108
+ "agent-skills",
9109
+ "anthropic-agent-skills"
9110
+ ]);
9111
+ function resolveAutoUpdate(probe) {
9112
+ const { name, registered, declared, settingsDeclared } = probe;
9113
+ if (!registered) {
9114
+ return { name, effective: false, reason: "not registered \u2014 nothing to update" };
9115
+ }
9116
+ if (typeof declared === "boolean") {
9117
+ return { name, effective: declared, reason: `declared ${declared} in known_marketplaces.json` };
9118
+ }
9119
+ if (typeof settingsDeclared === "boolean") {
9120
+ return { name, effective: settingsDeclared, reason: `declared ${settingsDeclared} under extraKnownMarketplaces.${name}` };
9121
+ }
9122
+ if (AUTO_UPDATE_DEFAULT_ON.has(name.toLowerCase())) {
9123
+ return { name, effective: true, reason: "undeclared \u2014 an official Anthropic marketplace, on by default" };
9124
+ }
9125
+ return {
9126
+ name,
9127
+ effective: false,
9128
+ reason: "undeclared \u2014 third-party marketplaces are OFF by default, so nothing auto-updates and no update notice ever prints"
9129
+ };
9130
+ }
9131
+ function readKnownMarketplace(raw, name) {
9132
+ if (!raw) return { registered: false };
9133
+ let parsed;
9134
+ try {
9135
+ parsed = JSON.parse(raw);
9136
+ } catch {
9137
+ return { registered: false };
9138
+ }
9139
+ if (!parsed || typeof parsed !== "object") return { registered: false };
9140
+ const entry = parsed[name];
9141
+ if (!entry || typeof entry !== "object") return { registered: false };
9142
+ const { autoUpdate, source } = entry;
9143
+ const ref = source && typeof source === "object" ? source.ref : void 0;
9144
+ return {
9145
+ registered: true,
9146
+ ...typeof autoUpdate === "boolean" ? { declared: autoUpdate } : {},
9147
+ ...typeof ref === "string" && ref ? { ref } : {}
9148
+ };
9149
+ }
9150
+ function readSettingsAutoUpdate(raw, name) {
9151
+ if (!raw) return void 0;
9152
+ let parsed;
9153
+ try {
9154
+ parsed = JSON.parse(raw);
9155
+ } catch {
9156
+ return void 0;
9157
+ }
9158
+ const extra = parsed?.extraKnownMarketplaces;
9159
+ if (!extra || typeof extra !== "object") return void 0;
9160
+ const entry = extra[name];
9161
+ if (!entry || typeof entry !== "object") return void 0;
9162
+ const { autoUpdate } = entry;
9163
+ return typeof autoUpdate === "boolean" ? autoUpdate : void 0;
9164
+ }
9165
+ var AUTO_UPDATE_TOGGLE_STEPS = "`/plugin` \u2192 Marketplaces tab \u2192 select the marketplace \u2192 Enable auto-update";
9166
+ var CATALOG_CONTENT_REF = "main";
9167
+ var CATALOG_REF_PIN_STEPS = `add \`"ref": "${CATALOG_CONTENT_REF}"\` to this marketplace's \`source\` in ~/.claude/plugins/known_marketplaces.json, then restart Claude`;
9168
+ function resolveCatalogRef(probe) {
9169
+ const { name, registered, ref, autoUpdate } = probe;
9170
+ if (!registered) return void 0;
9171
+ if (ref) {
9172
+ const agrees = ref === CATALOG_CONTENT_REF;
9173
+ return {
9174
+ name,
9175
+ ref,
9176
+ autoUpdate,
9177
+ unpinned: false,
9178
+ agrees,
9179
+ 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`
9180
+ };
9181
+ }
9182
+ return {
9183
+ name,
9184
+ autoUpdate,
9185
+ unpinned: true,
9186
+ agrees: false,
9187
+ reason: `no ref pinned \u2014 the catalog is served from the repo's default branch while plugin content is pinned to ${CATALOG_CONTENT_REF}`
9188
+ };
9189
+ }
9190
+ function checkMarketplaceAutoUpdate(probe) {
9191
+ if (!probe) return null;
9192
+ const evidence = [`${probe.name}: auto-update ${probe.effective ? "on" : "off"} \u2014 ${probe.reason}`];
9193
+ if (probe.effective) {
9194
+ return { ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
9195
+ }
9196
+ return {
9197
+ ok: true,
9198
+ warn: true,
9199
+ label: `marketplace auto-update (${probe.name})`,
9200
+ detail: `off \u2014 this machine will not pick up a new plugin release on its own; turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`,
9201
+ verbose: evidence
9202
+ };
9203
+ }
9204
+ function checkMarketplaceCatalogRef(probe) {
9205
+ if (!probe) return null;
9206
+ const label = `marketplace catalog ref (${probe.name})`;
9207
+ const evidence = [`${probe.name}: ${probe.reason}`, `auto-update: ${probe.autoUpdate ? "on" : "off"}`];
9208
+ if (probe.agrees) {
9209
+ return { ok: true, label, detail: `${probe.ref}`, verbose: evidence };
9210
+ }
9211
+ const drift = probe.unpinned ? "unpinned" : `pinned to ${probe.ref}, not ${CATALOG_CONTENT_REF}`;
9212
+ if (probe.autoUpdate) {
9213
+ return {
9214
+ ok: false,
9215
+ label,
9216
+ fix: `${drift} while auto-update is ON \u2014 this machine installs plugin releases advertised by a branch nobody released; ${CATALOG_REF_PIN_STEPS}`,
9217
+ verbose: evidence
9218
+ };
9219
+ }
9220
+ return {
9221
+ ok: true,
9222
+ warn: true,
9223
+ label,
9224
+ detail: `${drift} \u2014 harmless while auto-update is off, and the thing to fix before turning it on; ${CATALOG_REF_PIN_STEPS}`,
9225
+ verbose: evidence
9226
+ };
9227
+ }
9228
+ function marketplaceRows(name, known, settings) {
9229
+ const { registered, declared, ref } = readKnownMarketplace(known, name);
9230
+ const autoUpdate = resolveAutoUpdate({
9231
+ name,
9232
+ registered,
9233
+ declared,
9234
+ settingsDeclared: readSettingsAutoUpdate(settings, name)
9235
+ });
9236
+ const rows = [checkMarketplaceAutoUpdate(autoUpdate), checkMarketplaceCatalogRef(
9237
+ resolveCatalogRef({ name, registered, ref, autoUpdate: autoUpdate.effective })
9238
+ )];
9239
+ return rows.filter((r) => r !== null);
9240
+ }
9241
+
9242
+ // src/hook-activity.ts
9243
+ var import_node_fs13 = require("node:fs");
9244
+ var import_node_path11 = require("node:path");
9006
9245
  var DEFAULT_SURFACE = "claude";
9007
9246
  function activityLogPath(cwd) {
9008
9247
  return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
@@ -9015,16 +9254,16 @@ function appendHookActivity(cwd, entry) {
9015
9254
  surface: DEFAULT_SURFACE,
9016
9255
  ...entry
9017
9256
  };
9018
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(path2), { recursive: true });
9019
- (0, import_node_fs12.appendFileSync)(path2, `${JSON.stringify(line)}
9257
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path2), { recursive: true });
9258
+ (0, import_node_fs13.appendFileSync)(path2, `${JSON.stringify(line)}
9020
9259
  `, "utf8");
9021
9260
  } catch {
9022
9261
  }
9023
9262
  }
9024
9263
 
9025
9264
  // src/worktree.ts
9026
- var import_node_fs13 = require("node:fs");
9027
- var import_node_path11 = require("node:path");
9265
+ var import_node_fs14 = require("node:fs");
9266
+ var import_node_path12 = require("node:path");
9028
9267
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
9029
9268
  var PKG = "package.json";
9030
9269
  var LOCKFILE = "package-lock.json";
@@ -9032,21 +9271,21 @@ var NODE_MODULES = "node_modules";
9032
9271
  var realFsProbe = {
9033
9272
  isDir: (p) => {
9034
9273
  try {
9035
- return (0, import_node_fs13.statSync)(p).isDirectory();
9274
+ return (0, import_node_fs14.statSync)(p).isDirectory();
9036
9275
  } catch {
9037
9276
  return false;
9038
9277
  }
9039
9278
  },
9040
9279
  isFile: (p) => {
9041
9280
  try {
9042
- return (0, import_node_fs13.statSync)(p).isFile();
9281
+ return (0, import_node_fs14.statSync)(p).isFile();
9043
9282
  } catch {
9044
9283
  return false;
9045
9284
  }
9046
9285
  },
9047
9286
  listDirs: (p) => {
9048
9287
  try {
9049
- return (0, import_node_fs13.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9288
+ return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9050
9289
  } catch {
9051
9290
  return [];
9052
9291
  }
@@ -9054,12 +9293,12 @@ var realFsProbe = {
9054
9293
  };
9055
9294
  function scanInstallDirs(root, fs2 = realFsProbe) {
9056
9295
  const factsFor = (dir) => {
9057
- const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
9296
+ const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
9058
9297
  return {
9059
9298
  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))
9299
+ hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
9300
+ hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
9301
+ hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
9063
9302
  };
9064
9303
  };
9065
9304
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -9069,7 +9308,7 @@ function npmInstallTargets(dirs) {
9069
9308
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
9070
9309
  }
9071
9310
  function isLinkedWorktree(root, fs2 = realFsProbe) {
9072
- return fs2.isFile((0, import_node_path11.join)(root, ".git"));
9311
+ return fs2.isFile((0, import_node_path12.join)(root, ".git"));
9073
9312
  }
9074
9313
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
9075
9314
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -9079,8 +9318,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
9079
9318
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
9080
9319
  }
9081
9320
  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);
9321
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
9322
+ (0, import_node_fs14.copyFileSync)(from, to);
9084
9323
  }
9085
9324
  async function provisionWorktree(worktreeRoot, deps) {
9086
9325
  const fs2 = deps.fs ?? realFsProbe;
@@ -9092,7 +9331,7 @@ async function provisionWorktree(worktreeRoot, deps) {
9092
9331
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
9093
9332
  const installed = [];
9094
9333
  for (const target of targets) {
9095
- const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
9334
+ const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
9096
9335
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
9097
9336
  await deps.runInstall(target.command, cwd);
9098
9337
  installed.push(target);
@@ -9101,7 +9340,7 @@ async function provisionWorktree(worktreeRoot, deps) {
9101
9340
  const copySkipped = [];
9102
9341
  const primary = await deps.primaryCheckout();
9103
9342
  for (const rel of LOCAL_ONLY_FILES) {
9104
- const dest = (0, import_node_path11.join)(worktreeRoot, rel);
9343
+ const dest = (0, import_node_path12.join)(worktreeRoot, rel);
9105
9344
  if (fs2.isFile(dest)) {
9106
9345
  copySkipped.push({ file: rel, reason: "already-present" });
9107
9346
  continue;
@@ -9110,24 +9349,30 @@ async function provisionWorktree(worktreeRoot, deps) {
9110
9349
  copySkipped.push({ file: rel, reason: "no-primary" });
9111
9350
  continue;
9112
9351
  }
9113
- if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
9352
+ if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
9114
9353
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
9115
9354
  continue;
9116
9355
  }
9117
- copyFile((0, import_node_path11.join)(primary, rel), dest);
9356
+ copyFile((0, import_node_path12.join)(primary, rel), dest);
9118
9357
  copied.push(rel);
9119
9358
  log(`copied local config: ${rel}`);
9120
9359
  }
9121
9360
  return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
9122
9361
  }
9362
+ function capWorktreeDirName(name, max = 40) {
9363
+ if (name.length <= max) return name;
9364
+ const hard = name.slice(0, max);
9365
+ const lastDash = hard.lastIndexOf("-");
9366
+ return (lastDash >= max - 12 ? hard.slice(0, lastDash) : hard).replace(/-+$/, "");
9367
+ }
9123
9368
  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);
9369
+ const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
9370
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path12.basename)(repoRoot2), safe);
9126
9371
  }
9127
9372
  async function primaryCheckoutRootOf(git) {
9128
9373
  try {
9129
9374
  const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
9130
- return out ? (0, import_node_path11.dirname)(out) : void 0;
9375
+ return out ? (0, import_node_path12.dirname)(out) : void 0;
9131
9376
  } catch {
9132
9377
  return void 0;
9133
9378
  }
@@ -9183,7 +9428,15 @@ async function resolveWhoami(deps) {
9183
9428
  } catch {
9184
9429
  session = void 0;
9185
9430
  }
9186
- if (session?.login) return { login: session.login, source: "hub-session", sessionExpiresAt: session.expiresAt };
9431
+ if (session?.login) {
9432
+ return {
9433
+ login: session.login,
9434
+ source: "hub-session",
9435
+ sessionExpiresAt: session.expiresAt,
9436
+ // Exact-match, and only here. The `github` branch below deliberately never carries a role.
9437
+ ...session.role === "owner" ? { role: "owner" } : {}
9438
+ };
9439
+ }
9187
9440
  let ghLogin;
9188
9441
  try {
9189
9442
  ghLogin = await deps.ghLogin();
@@ -9198,6 +9451,10 @@ function whoamiLine(report, caveat) {
9198
9451
  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
9452
  return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
9200
9453
  }
9454
+ function authorityLine(report) {
9455
+ if (report.source !== "hub-session" || report.role !== "owner" || !report.login) return null;
9456
+ 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.`;
9457
+ }
9201
9458
 
9202
9459
  // src/command-ladder-hint.ts
9203
9460
  var COMMAND_LADDER_WRITES = [
@@ -9233,7 +9490,7 @@ function commandLadderHint() {
9233
9490
  }
9234
9491
 
9235
9492
  // src/index.ts
9236
- var import_node_path27 = require("node:path");
9493
+ var import_node_path30 = require("node:path");
9237
9494
 
9238
9495
  // src/merge-ci-policy.ts
9239
9496
  function resolveMergeCiPolicy(input) {
@@ -9321,16 +9578,16 @@ function pollStateFromParsed(parsed) {
9321
9578
  }
9322
9579
  var PR_MERGEABLE_UNKNOWN_POLL_RETRIES = 4;
9323
9580
  var PR_MERGEABLE_UNKNOWN_POLL_MS = 3e3;
9324
- async function resolveSettledMergeableState(pollMergeable, sleep2) {
9581
+ async function resolveSettledMergeableState(pollMergeable, sleep3) {
9325
9582
  for (let attempt = 1; attempt <= PR_MERGEABLE_UNKNOWN_POLL_RETRIES; attempt++) {
9326
9583
  const state = await pollMergeable();
9327
9584
  if (state !== "UNKNOWN") return state;
9328
- if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep2(PR_MERGEABLE_UNKNOWN_POLL_MS);
9585
+ if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep3(PR_MERGEABLE_UNKNOWN_POLL_MS);
9329
9586
  }
9330
9587
  return "UNKNOWN";
9331
9588
  }
9332
9589
  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}`;
9590
+ 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
9591
  }
9335
9592
  function conflictingResult(policy, baseBranch, waitedMs) {
9336
9593
  return { policy, status: "conflicting", reason: conflictingPrMessage(baseBranch), detail: "conflicting", waitedMs };
@@ -9686,6 +9943,12 @@ var MANAGED_GITIGNORE_LINES = [
9686
9943
  // and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
9687
9944
  ".codex/",
9688
9945
  ".agents/",
9946
+ // #3525: `jerv-cli fusion attest record` writes `.jerv/attest/<head>.attest.json` INTO the worktree,
9947
+ // so every worktree that has ever passed the fusion gate reports `?? .jerv/` forever. `worktree land`
9948
+ // refuses to clean a dirty worktree, which made the attestation artifact the gate requires the exact
9949
+ // thing blocking the cleanup that gate's own worktree needs. Org-universal: every repo runs the same
9950
+ // gate, so this belongs in the managed block rather than in one repo's own ignores.
9951
+ ".jerv/",
9689
9952
  // #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
9690
9953
  // via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
9691
9954
  // doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
@@ -10079,10 +10342,10 @@ function labelsToPrune(orgLabelNames) {
10079
10342
  const org = new Set(orgLabelNames);
10080
10343
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
10081
10344
  }
10082
- function resolveSeedContent(seed, vars, readFile6) {
10083
- if (seed.source === "self") return readFile6(seed.target);
10345
+ function resolveSeedContent(seed, vars, readFile7) {
10346
+ if (seed.source === "self") return readFile7(seed.target);
10084
10347
  if (seed.source.startsWith("seed:")) {
10085
- const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
10348
+ const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
10086
10349
  return tmpl == null ? null : renderSeed(tmpl, vars);
10087
10350
  }
10088
10351
  return null;
@@ -10748,8 +11011,8 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
10748
11011
  if (first.mergeStatus !== "failed") return first;
10749
11012
  const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
10750
11013
  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);
11014
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11015
+ await sleep3(PR_LAND_MERGE_RETRY_DELAY_MS);
10753
11016
  const retried = await deps.mergeAuto(prNumber, repo);
10754
11017
  if (retried.mergeStatus !== "failed") return retried;
10755
11018
  return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
@@ -10759,13 +11022,13 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
10759
11022
  async function confirmAutoMergeEnqueued(deps, options) {
10760
11023
  const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
10761
11024
  const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
10762
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11025
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10763
11026
  for (let attempt = 0; attempt < retries; attempt++) {
10764
11027
  if (await deps.readMerged().catch(() => false)) return "merged";
10765
11028
  const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
10766
11029
  if (stuck) return "enqueued";
10767
11030
  if (attempt < retries - 1) {
10768
- await sleep2(delayMs);
11031
+ await sleep3(delayMs);
10769
11032
  await deps.reEnqueue().catch(() => {
10770
11033
  });
10771
11034
  }
@@ -10776,7 +11039,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
10776
11039
  async function readGhPrStateWithRetry(fetchState, options) {
10777
11040
  const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
10778
11041
  const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
10779
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11042
+ const sleep3 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10780
11043
  let lastError = "empty state";
10781
11044
  for (let attempt = 0; attempt < retries; attempt++) {
10782
11045
  try {
@@ -10786,7 +11049,7 @@ async function readGhPrStateWithRetry(fetchState, options) {
10786
11049
  } catch (e) {
10787
11050
  lastError = String(e.message || "gh pr view failed");
10788
11051
  }
10789
- if (attempt < retries - 1) await sleep2(delayMs);
11052
+ if (attempt < retries - 1) await sleep3(delayMs);
10790
11053
  }
10791
11054
  return { ok: false, error: lastError };
10792
11055
  }
@@ -10849,12 +11112,12 @@ async function runPrLand(prNumber, options, deps) {
10849
11112
  }
10850
11113
 
10851
11114
  // src/wave-status.ts
10852
- var import_node_fs15 = require("node:fs");
11115
+ var import_node_fs16 = require("node:fs");
10853
11116
 
10854
11117
  // src/stage-runner.ts
10855
11118
  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");
11119
+ var import_node_fs15 = require("node:fs");
11120
+ var import_node_path13 = require("node:path");
10858
11121
  var import_node_net = require("node:net");
10859
11122
  var import_node_util6 = require("node:util");
10860
11123
 
@@ -10993,11 +11256,11 @@ function appendForceRecreate(up) {
10993
11256
  return `${up.trimEnd()} --force-recreate`;
10994
11257
  }
10995
11258
  function stageStatePath(cwd = process.cwd()) {
10996
- return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
11259
+ return (0, import_node_path13.join)(cwd, "tmp", "stage", "state.json");
10997
11260
  }
10998
11261
  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");
11262
+ const dir = (0, import_node_path13.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path13.resolve)(cwd, gitCommonDir);
11263
+ return (0, import_node_path13.join)(dir, "mmi", "stage", "state.json");
11001
11264
  }
11002
11265
  function normPath2(path2) {
11003
11266
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -11221,14 +11484,14 @@ function stageProcessEnv(stagePort, extraEnv) {
11221
11484
  }
11222
11485
  async function ensureStageRuntimeEnv(config, opts, cwd) {
11223
11486
  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
11487
+ const target = (0, import_node_path13.join)(cwd, config.ensureEnv.target);
11488
+ const example = (0, import_node_path13.join)(cwd, config.ensureEnv.example);
11489
+ if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
11490
+ (0, import_node_fs15.copyFileSync)(example, target);
11491
+ } else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
11492
+ const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
11493
+ exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
11494
+ targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
11232
11495
  });
11233
11496
  if (stale) {
11234
11497
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -11236,8 +11499,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
11236
11499
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
11237
11500
  }
11238
11501
  }
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");
11502
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
11503
+ (0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11241
11504
  }
11242
11505
  }
11243
11506
  async function gitText(cwd, args) {
@@ -11267,20 +11530,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
11267
11530
  return void 0;
11268
11531
  }
11269
11532
  function readState(path2) {
11270
- if (!(0, import_node_fs14.existsSync)(path2)) return null;
11533
+ if (!(0, import_node_fs15.existsSync)(path2)) return null;
11271
11534
  try {
11272
- return JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
11535
+ return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
11273
11536
  } catch {
11274
11537
  return null;
11275
11538
  }
11276
11539
  }
11277
11540
  function mkdirFor(path2) {
11278
11541
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
11279
- (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
11542
+ (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
11280
11543
  }
11281
11544
  function writeState(path2, state) {
11282
11545
  mkdirFor(path2);
11283
- (0, import_node_fs14.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11546
+ (0, import_node_fs15.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11284
11547
  }
11285
11548
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
11286
11549
  const reservation = {
@@ -11300,7 +11563,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
11300
11563
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
11301
11564
  }
11302
11565
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
11303
- (0, import_node_fs14.rmSync)(path2, { force: true });
11566
+ (0, import_node_fs15.rmSync)(path2, { force: true });
11304
11567
  }
11305
11568
  }
11306
11569
  async function killTree(pid) {
@@ -11458,8 +11721,8 @@ async function runStage(config = {}, opts = {}) {
11458
11721
  await ensureStageRuntimeEnv(config, opts, cwd);
11459
11722
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
11460
11723
  } catch (e) {
11461
- (0, import_node_fs14.rmSync)(statePath, { force: true });
11462
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs14.rmSync)(globalStatePath, { force: true });
11724
+ (0, import_node_fs15.rmSync)(statePath, { force: true });
11725
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs15.rmSync)(globalStatePath, { force: true });
11463
11726
  throw e;
11464
11727
  }
11465
11728
  const started = await startStage(config, {
@@ -11480,9 +11743,9 @@ function parseNextFromHead(headText) {
11480
11743
  }
11481
11744
  function readStageSummary(worktreePath) {
11482
11745
  const statePath = stageStatePath(worktreePath);
11483
- if (!(0, import_node_fs15.existsSync)(statePath)) return void 0;
11746
+ if (!(0, import_node_fs16.existsSync)(statePath)) return void 0;
11484
11747
  try {
11485
- const state = JSON.parse((0, import_node_fs15.readFileSync)(statePath, "utf8"));
11748
+ const state = JSON.parse((0, import_node_fs16.readFileSync)(statePath, "utf8"));
11486
11749
  const port = typeof state.port === "number" ? state.port : void 0;
11487
11750
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
11488
11751
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -11567,7 +11830,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
11567
11830
  }
11568
11831
 
11569
11832
  // src/index.ts
11570
- var import_node_os8 = require("node:os");
11833
+ var import_node_os9 = require("node:os");
11571
11834
 
11572
11835
  // src/attach-to-project.ts
11573
11836
  async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
@@ -11601,7 +11864,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
11601
11864
  // src/gh-create.ts
11602
11865
  var import_promises = require("node:fs/promises");
11603
11866
  var import_node_os3 = require("node:os");
11604
- var import_node_path13 = require("node:path");
11867
+ var import_node_path14 = require("node:path");
11605
11868
  var import_node_crypto3 = require("node:crypto");
11606
11869
  var ISSUE_TYPES = ["bug", "feature", "task"];
11607
11870
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -11655,8 +11918,8 @@ async function bodyArgsViaFile(args, deps = {}) {
11655
11918
  const remove2 = deps.remove ?? import_promises.unlink;
11656
11919
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
11657
11920
  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(() => {
11921
+ const file = (0, import_node_path14.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
11922
+ await ensureDir((0, import_node_path14.dirname)(file), { recursive: true }).catch(() => {
11660
11923
  });
11661
11924
  await write(file, args[i + 1], "utf8");
11662
11925
  return {
@@ -11723,23 +11986,61 @@ function parseExistingPr(stderr) {
11723
11986
  if (!match) return void 0;
11724
11987
  return { number: Number(match[1]), url: match[0] };
11725
11988
  }
11726
- async function ghCreate(args) {
11989
+ var GH_CREATE_UPSTREAM_RETRIES = 3;
11990
+ var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
11991
+ function httpStatusCodes(stderr) {
11992
+ const out = [];
11993
+ for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
11994
+ for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
11995
+ return out;
11996
+ }
11997
+ function isUpstreamGitHubFault(stderr) {
11998
+ const codes = httpStatusCodes(stderr);
11999
+ if (codes.some((c) => c >= 400 && c < 500)) return false;
12000
+ if (codes.some((c) => c >= 500 && c < 600)) return true;
12001
+ return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
12002
+ }
12003
+ function mayRetryCreate(verb) {
12004
+ return verb === "pr";
12005
+ }
12006
+ function upstreamFaultMessage(verb, stderr) {
12007
+ 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];
12008
+ 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";
12009
+ 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.`;
12010
+ }
12011
+ async function ghCreate(args, deps = {}) {
12012
+ const exec = deps.exec ?? execFileP2;
12013
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11727
12014
  const swapped = await bodyArgsViaFile(args);
11728
12015
  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 };
12016
+ for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
12017
+ try {
12018
+ const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
12019
+ return parseCreatedUrl(stdout);
12020
+ } catch (e) {
12021
+ const err = e;
12022
+ const errText = `${err.stderr ?? ""}
12023
+ ${err.message ?? ""}`;
12024
+ const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
12025
+ const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
12026
+ if (existing) {
12027
+ await swapped.cleanup();
12028
+ console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
12029
+ return { ...existing, existing: true };
12030
+ }
12031
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
12032
+ if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
12033
+ const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
12034
+ console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
12035
+ await sleep3(waitMs);
12036
+ continue;
12037
+ }
12038
+ await swapped.cleanup();
12039
+ if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
12040
+ return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
12041
+ }
11739
12042
  }
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})` : ""}`);
12043
+ throw new Error("ghCreate: retry loop exhausted without a verdict");
11743
12044
  } finally {
11744
12045
  await swapped.cleanup();
11745
12046
  }
@@ -12893,8 +13194,8 @@ function parseVerifyBroker(stdout) {
12893
13194
  }
12894
13195
 
12895
13196
  // src/train-apply.ts
12896
- var import_node_fs16 = require("node:fs");
12897
- var import_node_path14 = require("node:path");
13197
+ var import_node_fs17 = require("node:fs");
13198
+ var import_node_path15 = require("node:path");
12898
13199
  function resolveDeployModel2(meta, repo) {
12899
13200
  const m = meta?.deployModel;
12900
13201
  if (isDeployModel(m)) return m;
@@ -13003,7 +13304,7 @@ function mergeTreeConflictFiles(stdout) {
13003
13304
  return stdout.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
13004
13305
  }
13005
13306
  async function runMergeTreePreflight(deps, ours, theirs) {
13006
- const sleep2 = resolveSleep(deps);
13307
+ const sleep3 = resolveSleep(deps);
13007
13308
  let lastError;
13008
13309
  for (let attempt = 0; attempt < MERGE_TREE_PREFLIGHT_ATTEMPTS; attempt++) {
13009
13310
  try {
@@ -13013,7 +13314,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
13013
13314
  lastError = e;
13014
13315
  const files = mergeTreeConflictFiles(String(e.stdout ?? ""));
13015
13316
  if (files.length > 0) return files;
13016
- if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep2(MERGE_TREE_PREFLIGHT_DELAY_MS);
13317
+ if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep3(MERGE_TREE_PREFLIGHT_DELAY_MS);
13017
13318
  }
13018
13319
  }
13019
13320
  const msg = lastError?.message ?? String(lastError);
@@ -13217,7 +13518,7 @@ function isPersistentGitPushFailure(e) {
13217
13518
  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
13519
  }
13219
13520
  async function runGitWithRetry(deps, args, shouldRetry) {
13220
- const sleep2 = resolveSleep(deps);
13521
+ const sleep3 = resolveSleep(deps);
13221
13522
  let lastError;
13222
13523
  for (let attempt = 1; attempt <= GIT_NETWORK_ATTEMPTS; attempt++) {
13223
13524
  try {
@@ -13225,7 +13526,7 @@ async function runGitWithRetry(deps, args, shouldRetry) {
13225
13526
  } catch (e) {
13226
13527
  lastError = e;
13227
13528
  if (attempt >= GIT_NETWORK_ATTEMPTS || !shouldRetry(e)) throw e;
13228
- await sleep2(GIT_NETWORK_RETRY_DELAY_MS * attempt);
13529
+ await sleep3(GIT_NETWORK_RETRY_DELAY_MS * attempt);
13229
13530
  }
13230
13531
  }
13231
13532
  throw lastError;
@@ -13246,13 +13547,13 @@ var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project
13246
13547
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
13247
13548
  var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
13248
13549
  async function correlateRun(deps, args) {
13249
- const sleep2 = resolveSleep(deps);
13550
+ const sleep3 = resolveSleep(deps);
13250
13551
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
13251
13552
  const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
13252
13553
  let lastError;
13253
13554
  let parsedAnyResponse = false;
13254
13555
  for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
13255
- if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
13556
+ if (attempt > 0) await sleep3(CORRELATE_DELAY_MS);
13256
13557
  const listArgs = [
13257
13558
  "run",
13258
13559
  "list",
@@ -13445,13 +13746,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
13445
13746
  const t = Date.parse(String(r.startedAt ?? ""));
13446
13747
  return !Number.isFinite(t) || t >= freshThreshold;
13447
13748
  };
13448
- const sleep2 = resolveSleep(deps);
13749
+ const sleep3 = resolveSleep(deps);
13449
13750
  let lastStatus = "not checked";
13450
13751
  let lastError;
13451
13752
  const everObserved = /* @__PURE__ */ new Set();
13452
13753
  const autoSatisfied = /* @__PURE__ */ new Set();
13453
13754
  for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
13454
- if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
13755
+ if (attempt > 0) await sleep3(TRAIN_CHECK_DELAY_MS);
13455
13756
  let checkRuns;
13456
13757
  let statuses;
13457
13758
  try {
@@ -13585,14 +13886,14 @@ function isTransientDispatchFailure(e) {
13585
13886
  return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
13586
13887
  }
13587
13888
  async function dispatchTenantDeployWithRetry(deps, input) {
13588
- const sleep2 = resolveSleep(deps);
13889
+ const sleep3 = resolveSleep(deps);
13589
13890
  for (let attempt = 1; ; attempt++) {
13590
13891
  try {
13591
13892
  await deps.dispatchTenantDeploy(input);
13592
13893
  return;
13593
13894
  } catch (e) {
13594
13895
  if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
13595
- await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
13896
+ await sleep3(DISPATCH_RETRY_DELAY_MS * attempt);
13596
13897
  }
13597
13898
  }
13598
13899
  }
@@ -13612,9 +13913,9 @@ var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
13612
13913
  var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
13613
13914
  async function reconcilePublishFailure(deps, runId) {
13614
13915
  if (runId == null) return "failure";
13615
- const sleep2 = resolveSleep(deps);
13916
+ const sleep3 = resolveSleep(deps);
13616
13917
  for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
13617
- if (attempt > 0) await sleep2(PUBLISH_LOG_RETRY_DELAY_MS);
13918
+ if (attempt > 0) await sleep3(PUBLISH_LOG_RETRY_DELAY_MS);
13618
13919
  const log = await fetchControlRunLog(deps, runId);
13619
13920
  if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
13620
13921
  }
@@ -13802,17 +14103,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
13802
14103
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
13803
14104
  }
13804
14105
  function readLocalGateWorkflows() {
13805
- const dir = (0, import_node_path14.join)(".github", "workflows");
14106
+ const dir = (0, import_node_path15.join)(".github", "workflows");
13806
14107
  let names;
13807
14108
  try {
13808
- names = (0, import_node_fs16.readdirSync)(dir);
14109
+ names = (0, import_node_fs17.readdirSync)(dir);
13809
14110
  } catch {
13810
14111
  return null;
13811
14112
  }
13812
14113
  const files = [];
13813
14114
  for (const name of names.filter(isGateWorkflowPath)) {
13814
14115
  try {
13815
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(dir, name), "utf8") });
14116
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(dir, name), "utf8") });
13816
14117
  } catch {
13817
14118
  }
13818
14119
  }
@@ -14408,13 +14709,13 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
14408
14709
  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
14710
  };
14410
14711
  }
14411
- const sleep2 = resolveSleep(deps);
14712
+ const sleep3 = resolveSleep(deps);
14412
14713
  let last;
14413
14714
  for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
14414
14715
  last = await attemptRetire(deps, ctx);
14415
14716
  if (last.status === "retired") return last;
14416
14717
  if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
14417
- await sleep2(RETIRE_BACKOFF_MS * attempt);
14718
+ await sleep3(RETIRE_BACKOFF_MS * attempt);
14418
14719
  }
14419
14720
  const f = last;
14420
14721
  if (f.category === "wait-timeout") return f;
@@ -14945,9 +15246,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
14945
15246
  return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
14946
15247
  }
14947
15248
  async function watchReleaseRun(deps, ctx, workflow, sha) {
14948
- const sleep2 = sleeper(deps);
15249
+ const sleep3 = sleeper(deps);
14949
15250
  for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
14950
- if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
15251
+ if (attempt > 0) await sleep3(HOTFIX_RUN_FIND_DELAY_MS);
14951
15252
  let rows;
14952
15253
  try {
14953
15254
  const out = await deps.run("gh", [
@@ -15085,7 +15386,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
15085
15386
  try {
15086
15387
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
15087
15388
  const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
15088
- const sleep2 = sleeper(deps);
15389
+ const sleep3 = sleeper(deps);
15089
15390
  let attempt = 0;
15090
15391
  for (; ; ) {
15091
15392
  attempt++;
@@ -15094,7 +15395,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
15094
15395
  break;
15095
15396
  } catch (err) {
15096
15397
  if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
15097
- await sleep2(HOTFIX_VERIFY_RETRY_MS);
15398
+ await sleep3(HOTFIX_VERIFY_RETRY_MS);
15098
15399
  }
15099
15400
  }
15100
15401
  const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
@@ -15709,8 +16010,8 @@ function renderAccessReport(report) {
15709
16010
  }
15710
16011
 
15711
16012
  // src/docs-index-command.ts
15712
- var import_node_fs17 = require("node:fs");
15713
- var import_node_path15 = require("node:path");
16013
+ var import_node_fs18 = require("node:fs");
16014
+ var import_node_path16 = require("node:path");
15714
16015
  var DOCS_INDEX_PATH = "docs/index.md";
15715
16016
  function isRoutableDocsPath(relPath) {
15716
16017
  const normalized = relPath.replace(/\\/g, "/");
@@ -15792,32 +16093,32 @@ function walkMarkdown(dir) {
15792
16093
  const stack = [dir];
15793
16094
  while (stack.length) {
15794
16095
  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);
16096
+ for (const entry of (0, import_node_fs18.readdirSync)(current, { withFileTypes: true })) {
16097
+ const full = (0, import_node_path16.join)(current, entry.name);
15797
16098
  if (entry.isDirectory()) {
15798
16099
  stack.push(full);
15799
16100
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
15800
- out.push((0, import_node_path15.relative)(dir, full).split(import_node_path15.sep).join("/"));
16101
+ out.push((0, import_node_path16.relative)(dir, full).split(import_node_path16.sep).join("/"));
15801
16102
  }
15802
16103
  }
15803
16104
  }
15804
16105
  return out;
15805
16106
  }
15806
16107
  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);
16108
+ const docsDir = (0, import_node_path16.join)(repoRoot2, "docs");
16109
+ const indexPath = (0, import_node_path16.join)(repoRoot2, DOCS_INDEX_PATH);
15809
16110
  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")
16111
+ listDocs: () => (0, import_node_fs18.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
16112
+ readDoc: (relPath) => (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(docsDir, relPath), "utf8"),
16113
+ readIndex: () => (0, import_node_fs18.existsSync)(indexPath) ? (0, import_node_fs18.readFileSync)(indexPath, "utf8") : null,
16114
+ writeIndex: (content) => (0, import_node_fs18.writeFileSync)(indexPath, content, "utf8")
15814
16115
  };
15815
16116
  }
15816
16117
 
15817
16118
  // src/doc-refs-core.ts
15818
16119
  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");
16120
+ var import_node_fs19 = require("node:fs");
16121
+ var import_node_path17 = require("node:path");
15821
16122
  var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
15822
16123
  var PIN_MENTION_RE = /<!--\s*pinned by\b/;
15823
16124
  var REF_RE = /`([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?::\d+)?`/g;
@@ -15851,7 +16152,7 @@ function extractPins(markdown) {
15851
16152
  });
15852
16153
  return pins;
15853
16154
  }
15854
- function checkPins(root, readFile6, docs2) {
16155
+ function checkPins(root, readFile7, docs2) {
15855
16156
  const findings = [];
15856
16157
  for (const [doc, markdown] of Object.entries(docs2)) {
15857
16158
  for (const pin of extractPins(markdown)) {
@@ -15859,7 +16160,7 @@ function checkPins(root, readFile6, docs2) {
15859
16160
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
15860
16161
  continue;
15861
16162
  }
15862
- const source = readFile6((0, import_node_path16.join)(root, pin.file));
16163
+ const source = readFile7((0, import_node_path17.join)(root, pin.file));
15863
16164
  if (source == null) {
15864
16165
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
15865
16166
  continue;
@@ -15920,17 +16221,17 @@ function checkRefs(root, deps, docs2) {
15920
16221
  for (const [doc, markdown] of Object.entries(docs2)) {
15921
16222
  for (const { ref, line } of extractRefs(markdown)) {
15922
16223
  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 });
16224
+ if (!exists((0, import_node_path17.join)(root, first))) continue;
16225
+ if (!exists((0, import_node_path17.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
15925
16226
  }
15926
- const docDir = import_node_path16.posix.dirname(doc);
16227
+ const docDir = import_node_path17.posix.dirname(doc);
15927
16228
  for (const { target, line } of extractLinks(markdown)) {
15928
- const resolved = import_node_path16.posix.normalize(import_node_path16.posix.join(docDir === "." ? "" : docDir, target));
16229
+ const resolved = import_node_path17.posix.normalize(import_node_path17.posix.join(docDir === "." ? "" : docDir, target));
15929
16230
  if (resolved.startsWith("..")) {
15930
16231
  candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
15931
16232
  continue;
15932
16233
  }
15933
- if (!exists((0, import_node_path16.join)(root, resolved))) {
16234
+ if (!exists((0, import_node_path17.join)(root, resolved))) {
15934
16235
  candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
15935
16236
  }
15936
16237
  }
@@ -15964,22 +16265,22 @@ function checkCommands(docs2, commandPaths) {
15964
16265
  return { ok: findings.length === 0, findings };
15965
16266
  }
15966
16267
  function readFileOrNull(path2) {
15967
- return (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
16268
+ return (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null;
15968
16269
  }
15969
16270
  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);
16271
+ for (const entry of (0, import_node_fs19.readdirSync)(dir)) {
16272
+ const full = (0, import_node_path17.join)(dir, entry);
16273
+ if ((0, import_node_fs19.statSync)(full).isDirectory()) walk(full, root, out);
15973
16274
  else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
15974
16275
  }
15975
16276
  return out;
15976
16277
  }
15977
16278
  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(
16279
+ const docsDir = (0, import_node_path17.join)(root, "docs");
16280
+ const docs2 = ((0, import_node_fs19.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
15980
16281
  (rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
15981
16282
  );
15982
- return [...ROOT_DOCS.filter((rel) => (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, rel))), ...docs2];
16283
+ return [...ROOT_DOCS.filter((rel) => (0, import_node_fs19.existsSync)((0, import_node_path17.join)(root, rel))), ...docs2];
15983
16284
  }
15984
16285
  function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
15985
16286
  const inRepo = relPaths.filter((p) => !p.startsWith(".."));
@@ -15997,16 +16298,16 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.exec
15997
16298
  }
15998
16299
  }
15999
16300
  function runDocRefs(root, deps = {}) {
16000
- const readFile6 = deps.readFile ?? readFileOrNull;
16001
- const exists = deps.exists ?? import_node_fs18.existsSync;
16301
+ const readFile7 = deps.readFile ?? readFileOrNull;
16302
+ const exists = deps.exists ?? import_node_fs19.existsSync;
16002
16303
  const listDocs = deps.listDocs ?? defaultListDocs;
16003
16304
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
16004
16305
  const commandPaths = deps.commandPaths ?? null;
16005
16306
  const docs2 = Object.fromEntries(
16006
- listDocs(root).map((rel) => [rel, readFile6((0, import_node_path16.join)(root, rel))]).filter(([, body]) => body != null)
16307
+ listDocs(root).map((rel) => [rel, readFile7((0, import_node_path17.join)(root, rel))]).filter(([, body]) => body != null)
16007
16308
  );
16008
16309
  const findings = [
16009
- ...checkPins(root, readFile6, docs2).findings,
16310
+ ...checkPins(root, readFile7, docs2).findings,
16010
16311
  ...checkRefs(root, { exists, isIgnored }, docs2).findings
16011
16312
  ];
16012
16313
  if (commandPaths == null) {
@@ -16078,7 +16379,28 @@ function docsAuditStatus(fetch2, opts) {
16078
16379
  if (!fetch2.ok) {
16079
16380
  return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
16080
16381
  }
16382
+ if (!isValidIsoDate(opts.today)) {
16383
+ throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
16384
+ }
16081
16385
  if (fetch2.verdict === null) {
16386
+ const armedAt = opts.armedAt;
16387
+ if (armedAt && isValidIsoDate(armedAt)) {
16388
+ const cadenceDays = opts.cadenceDays ?? 7;
16389
+ const graceDays = opts.graceDays ?? 3;
16390
+ const sinceArmed = ageInDays(armedAt, opts.today);
16391
+ if (sinceArmed <= cadenceDays + graceDays) {
16392
+ return {
16393
+ ok: true,
16394
+ state: "awaiting-first-tick",
16395
+ line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
16396
+ };
16397
+ }
16398
+ return {
16399
+ ok: false,
16400
+ state: "missing",
16401
+ line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
16402
+ };
16403
+ }
16082
16404
  return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
16083
16405
  }
16084
16406
  const verdict = fetch2.verdict;
@@ -16107,9 +16429,6 @@ function docsAuditStatus(fetch2, opts) {
16107
16429
  line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
16108
16430
  };
16109
16431
  }
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
16432
  const cadence = opts.cadenceDays ?? 7;
16114
16433
  const grace = opts.graceDays ?? 3;
16115
16434
  const age = ageInDays(verdict.date, opts.today);
@@ -16945,8 +17264,8 @@ function writeError(res) {
16945
17264
  }
16946
17265
 
16947
17266
  // src/secrets-commands.ts
16948
- var import_node_fs19 = require("node:fs");
16949
- var import_node_path17 = require("node:path");
17267
+ var import_node_fs20 = require("node:fs");
17268
+ var import_node_path18 = require("node:path");
16950
17269
  var import_node_os5 = require("node:os");
16951
17270
 
16952
17271
  // src/project-runtime.ts
@@ -17063,25 +17382,25 @@ var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
17063
17382
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
17064
17383
  function resolvePreflightRepoScope(optRepo, cwdRepo) {
17065
17384
  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 };
17385
+ const sameRepo2 = !optRepo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
17386
+ return { targetRepo: targetRepo2, sameRepo: sameRepo2 };
17068
17387
  }
17069
17388
  function collectMap(value, previous = []) {
17070
17389
  return [...previous, value];
17071
17390
  }
17072
17391
  async function decryptRailsCredentials(input) {
17073
- const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
17392
+ const appDir = (0, import_node_path18.resolve)(input.appDir ?? process.cwd());
17074
17393
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17075
17394
  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);
17395
+ const credentialsPath = (0, import_node_path18.resolve)(appDir, credentialsFile);
17396
+ const masterKeyPath = (0, import_node_path18.resolve)(appDir, masterKeyFile);
17078
17397
  const env = {
17079
17398
  ...process.env,
17080
17399
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
17081
17400
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
17082
17401
  };
17083
- if ((0, import_node_fs19.existsSync)(masterKeyPath)) {
17084
- env.RAILS_MASTER_KEY = (0, import_node_fs19.readFileSync)(masterKeyPath, "utf8").trim();
17402
+ if ((0, import_node_fs20.existsSync)(masterKeyPath)) {
17403
+ env.RAILS_MASTER_KEY = (0, import_node_fs20.readFileSync)(masterKeyPath, "utf8").trim();
17085
17404
  }
17086
17405
  const script = [
17087
17406
  'require "json"',
@@ -17091,9 +17410,9 @@ async function decryptRailsCredentials(input) {
17091
17410
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
17092
17411
  "puts JSON.generate(config.config)"
17093
17412
  ].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");
17413
+ const scriptDir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
17414
+ const scriptPath = (0, import_node_path18.join)(scriptDir, "decrypt.rb");
17415
+ (0, import_node_fs20.writeFileSync)(scriptPath, script, "utf8");
17097
17416
  try {
17098
17417
  const args = ["exec", "ruby", scriptPath];
17099
17418
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -17105,7 +17424,7 @@ async function decryptRailsCredentials(input) {
17105
17424
  });
17106
17425
  return JSON.parse(stdout);
17107
17426
  } finally {
17108
- (0, import_node_fs19.rmSync)(scriptDir, { recursive: true, force: true });
17427
+ (0, import_node_fs20.rmSync)(scriptDir, { recursive: true, force: true });
17109
17428
  }
17110
17429
  }
17111
17430
  async function readSecretStdin() {
@@ -17195,7 +17514,7 @@ function registerSecretsCommands(program3) {
17195
17514
  let body;
17196
17515
  if (o.file) {
17197
17516
  try {
17198
- body = (0, import_node_fs19.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
17517
+ body = (0, import_node_fs20.readFileSync)((0, import_node_path18.resolve)(o.file), "utf8");
17199
17518
  } catch (e) {
17200
17519
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
17201
17520
  }
@@ -17229,11 +17548,11 @@ function registerSecretsCommands(program3) {
17229
17548
  const stage = o.stage;
17230
17549
  const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
17231
17550
  const cwdRepo = repoFromRemoteUrl((await execFileP2("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
17232
- const { sameRepo } = resolvePreflightRepoScope(o.repo, cwdRepo);
17551
+ const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
17233
17552
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
17234
17553
  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 });
17554
+ const composeText = sameRepo2 ? await readStageBranchCompose(branch) : null;
17555
+ const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo2), force: false, repo });
17237
17556
  if (!verdict.ok) {
17238
17557
  d.err(`secrets preflight: ${verdict.reason}`);
17239
17558
  filelessOk = false;
@@ -17300,7 +17619,7 @@ function registerSecretsCommands(program3) {
17300
17619
  {
17301
17620
  ...d,
17302
17621
  decryptRailsCredentials,
17303
- removeFile: (path2) => (0, import_node_fs19.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
17622
+ removeFile: (path2) => (0, import_node_fs20.unlinkSync)((0, import_node_path18.resolve)(o.appDir ?? process.cwd(), path2))
17304
17623
  },
17305
17624
  {
17306
17625
  repo: o.repo,
@@ -17474,7 +17793,7 @@ function checkGithubPools(probe) {
17474
17793
  }
17475
17794
 
17476
17795
  // src/box-commands.ts
17477
- var import_node_fs20 = require("node:fs");
17796
+ var import_node_fs21 = require("node:fs");
17478
17797
 
17479
17798
  // src/box.ts
17480
17799
  var BOX_KEYS = {
@@ -17677,7 +17996,7 @@ function registerBoxCommands(program3) {
17677
17996
  }
17678
17997
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
17679
17998
  else if (o.ssh && o.script) {
17680
- (0, import_node_fs20.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
17999
+ (0, import_node_fs21.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
17681
18000
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
17682
18001
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
17683
18002
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -17775,7 +18094,7 @@ function lambdaTargetFromArn(arn) {
17775
18094
  }
17776
18095
  function awsScheduleEntry(payload) {
17777
18096
  if (!payload || typeof payload !== "object") return null;
17778
- const { Name, ScheduleExpression, State, Target, Description } = payload;
18097
+ const { Name, ScheduleExpression, State, Target, Description, CreationDate } = payload;
17779
18098
  if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
17780
18099
  if (State !== "ENABLED" || isJervResource(Name)) return null;
17781
18100
  let target = "";
@@ -17798,6 +18117,7 @@ function awsScheduleEntry(payload) {
17798
18117
  }
17799
18118
  const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
17800
18119
  const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
18120
+ const armedAt = armedAtFromCreationDate(CreationDate);
17801
18121
  return {
17802
18122
  name: Name,
17803
18123
  cadence: ScheduleExpression,
@@ -17805,9 +18125,25 @@ function awsScheduleEntry(payload) {
17805
18125
  llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
17806
18126
  resolved: "live",
17807
18127
  source,
17808
- ...scheduleId ? { scheduleId } : {}
18128
+ ...scheduleId ? { scheduleId } : {},
18129
+ ...armedAt ? { armedAt } : {}
17809
18130
  };
17810
18131
  }
18132
+ function armedAtFromCreationDate(creationDate) {
18133
+ if (creationDate instanceof Date) {
18134
+ return Number.isNaN(creationDate.getTime()) ? void 0 : creationDate.toISOString().slice(0, 10);
18135
+ }
18136
+ if (typeof creationDate === "string" && creationDate.trim()) {
18137
+ const parsed = Date.parse(creationDate);
18138
+ return Number.isNaN(parsed) ? void 0 : new Date(parsed).toISOString().slice(0, 10);
18139
+ }
18140
+ if (typeof creationDate === "number" && Number.isFinite(creationDate)) {
18141
+ const ms = creationDate > 1e12 ? creationDate : creationDate * 1e3;
18142
+ const date = new Date(ms);
18143
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().slice(0, 10);
18144
+ }
18145
+ return void 0;
18146
+ }
17811
18147
  function awsScheduleRefs(payload) {
17812
18148
  if (!payload || typeof payload !== "object") return [];
17813
18149
  const { Schedules: schedules } = payload;
@@ -17873,6 +18209,12 @@ function spliceDoc(docText, generatedSection) {
17873
18209
  }
17874
18210
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
17875
18211
  }
18212
+ function driftClearableFrom(drift, repoName) {
18213
+ if (drift.class !== "file-vs-registry-stale") return false;
18214
+ const slash = drift.name.indexOf("/");
18215
+ if (slash <= 0) return false;
18216
+ return drift.name.slice(0, slash).toLowerCase() === repoName.toLowerCase();
18217
+ }
17876
18218
  var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
17877
18219
  function isHarbourLlmLauncher(executor) {
17878
18220
  return HARBOUR_LLM_LAUNCHERS.has(executor);
@@ -18253,9 +18595,105 @@ function registerSchedulesCommands(program3) {
18253
18595
  });
18254
18596
  }
18255
18597
 
18256
- // src/schedules-lift-command.ts
18598
+ // src/file-lock.ts
18257
18599
  var import_promises3 = require("node:fs/promises");
18258
- var import_node_path18 = require("node:path");
18600
+ var import_node_path19 = require("node:path");
18601
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
18602
+ var IMMEDIATE_RETRY_BUDGET = 3;
18603
+ var FileLockBusyError = class extends Error {
18604
+ lockPath;
18605
+ constructor(label, lockPath, maxWaitMs) {
18606
+ super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
18607
+ this.name = "FileLockBusyError";
18608
+ this.lockPath = lockPath;
18609
+ }
18610
+ };
18611
+ function resolveFileLockOpts(opts = {}) {
18612
+ return {
18613
+ staleMs: opts.staleMs ?? 3e4,
18614
+ retryMs: opts.retryMs ?? 50,
18615
+ maxWaitMs: opts.maxWaitMs ?? 5e3,
18616
+ label: opts.label ?? "file lock"
18617
+ };
18618
+ }
18619
+ async function acquireFileLock(lockPath, opts, deadline) {
18620
+ let immediateRetries = 0;
18621
+ for (; ; ) {
18622
+ let handle;
18623
+ try {
18624
+ handle = await (0, import_promises3.open)(lockPath, "wx");
18625
+ } catch (e) {
18626
+ const code = e.code;
18627
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
18628
+ const retryNow = async () => {
18629
+ if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18630
+ if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
18631
+ };
18632
+ try {
18633
+ const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
18634
+ if (age > opts.staleMs) {
18635
+ try {
18636
+ await (0, import_promises3.unlink)(lockPath);
18637
+ await retryNow();
18638
+ continue;
18639
+ } catch (unlinkError) {
18640
+ if (unlinkError instanceof FileLockBusyError) throw unlinkError;
18641
+ const unlinkCode = unlinkError.code;
18642
+ if (unlinkCode === "ENOENT") {
18643
+ await retryNow();
18644
+ continue;
18645
+ }
18646
+ }
18647
+ }
18648
+ } catch (statError) {
18649
+ if (statError instanceof FileLockBusyError) throw statError;
18650
+ if (statError.code !== "ENOENT") throw statError;
18651
+ await retryNow();
18652
+ continue;
18653
+ }
18654
+ if (Date.now() >= deadline) {
18655
+ throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18656
+ }
18657
+ await sleep(opts.retryMs);
18658
+ continue;
18659
+ }
18660
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18661
+ try {
18662
+ await handle.writeFile(token);
18663
+ } catch (e) {
18664
+ await handle.close().catch(() => void 0);
18665
+ throw e;
18666
+ }
18667
+ return { token, handle };
18668
+ }
18669
+ }
18670
+ async function fileLockHeldBy(lockPath, token) {
18671
+ try {
18672
+ return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
18673
+ } catch {
18674
+ return false;
18675
+ }
18676
+ }
18677
+ async function releaseFileLock(lockPath, guard) {
18678
+ await guard.handle.close().catch(() => void 0);
18679
+ if (await fileLockHeldBy(lockPath, guard.token)) {
18680
+ await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
18681
+ }
18682
+ }
18683
+ async function withFileLock(lockPath, opts, fn) {
18684
+ const resolved = resolveFileLockOpts(opts);
18685
+ await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(lockPath), { recursive: true }).catch(() => void 0);
18686
+ const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
18687
+ try {
18688
+ return await fn();
18689
+ } finally {
18690
+ await releaseFileLock(lockPath, guard);
18691
+ }
18692
+ }
18693
+
18694
+ // src/schedules-lift-command.ts
18695
+ var import_promises4 = require("node:fs/promises");
18696
+ var import_node_path20 = require("node:path");
18259
18697
 
18260
18698
  // src/schedules-lift.ts
18261
18699
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -18354,14 +18792,14 @@ var RegistryUnreachableError = class extends Error {
18354
18792
  async function readWorkflowFiles(dir) {
18355
18793
  let names;
18356
18794
  try {
18357
- names = await (0, import_promises3.readdir)(dir);
18795
+ names = await (0, import_promises4.readdir)(dir);
18358
18796
  } catch {
18359
18797
  return [];
18360
18798
  }
18361
18799
  const files = [];
18362
18800
  for (const name of names.sort()) {
18363
18801
  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") });
18802
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path20.join)(dir, name), "utf8") });
18365
18803
  }
18366
18804
  return files;
18367
18805
  }
@@ -18903,7 +19341,7 @@ function registerQueryCommands(program3) {
18903
19341
  }
18904
19342
 
18905
19343
  // src/bootstrap-commands.ts
18906
- var import_node_fs21 = require("node:fs");
19344
+ var import_node_fs22 = require("node:fs");
18907
19345
 
18908
19346
  // src/bootstrap-verify.ts
18909
19347
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -19451,7 +19889,7 @@ function registerBootstrapCommands(program3) {
19451
19889
  client: defaultGitHubClient(),
19452
19890
  projectMeta: meta,
19453
19891
  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,
19892
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
19455
19893
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
19456
19894
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
19457
19895
  requiredGcpApis: (() => {
@@ -19502,12 +19940,12 @@ function registerBootstrapCommands(program3) {
19502
19940
  return fail(`bootstrap apply: ${e.message}`);
19503
19941
  }
19504
19942
  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"));
19943
+ 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`);
19944
+ const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
19507
19945
  const baseBranch = o.class === "content" ? "main" : "development";
19508
19946
  const slug = parsedRepo.slug;
19509
19947
  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;
19948
+ const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
19511
19949
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
19512
19950
  const rawVars = {};
19513
19951
  for (const value of cmdOpts.var ?? []) {
@@ -19585,7 +20023,7 @@ function registerBootstrapCommands(program3) {
19585
20023
  }
19586
20024
  const planned = planSeedAction(resolved, exists);
19587
20025
  const isBlock = resolved.source === "managed-block";
19588
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
20026
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
19589
20027
  const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
19590
20028
  actions.push(action);
19591
20029
  if (o.execute && (action.action === "create" || action.action === "update")) {
@@ -19640,7 +20078,7 @@ function registerBootstrapCommands(program3) {
19640
20078
  }
19641
20079
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
19642
20080
  if (rulesetSeed) {
19643
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
20081
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
19644
20082
  if (rulesetContent) {
19645
20083
  try {
19646
20084
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -19703,12 +20141,12 @@ LIVE apply to ${repo}:
19703
20141
  }
19704
20142
 
19705
20143
  // src/stage-commands.ts
19706
- var import_node_fs23 = require("node:fs");
19707
- var import_node_path20 = require("node:path");
20144
+ var import_node_fs24 = require("node:fs");
20145
+ var import_node_path22 = require("node:path");
19708
20146
 
19709
20147
  // src/port-registry.ts
19710
- var import_node_fs22 = require("node:fs");
19711
- var import_node_path19 = require("node:path");
20148
+ var import_node_fs23 = require("node:fs");
20149
+ var import_node_path21 = require("node:path");
19712
20150
 
19713
20151
  // ../infra/port-geometry.mjs
19714
20152
  var PORT_BLOCK = 100;
@@ -19722,8 +20160,8 @@ function nextPortBlock(registry2) {
19722
20160
  return [base, base + PORT_SPAN];
19723
20161
  }
19724
20162
  function loadPortRegistry(path2) {
19725
- if (!(0, import_node_fs22.existsSync)(path2)) return {};
19726
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
20163
+ if (!(0, import_node_fs23.existsSync)(path2)) return {};
20164
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
19727
20165
  const out = {};
19728
20166
  for (const [key, value] of Object.entries(raw)) {
19729
20167
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -19737,9 +20175,9 @@ function ensurePortRange(repo, path2) {
19737
20175
  const existing = registry2[repo];
19738
20176
  if (existing) return existing;
19739
20177
  const range = nextPortBlock(registry2);
19740
- const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
20178
+ const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
19741
20179
  raw[repo] = range;
19742
- (0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
20180
+ (0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
19743
20181
  return range;
19744
20182
  }
19745
20183
  function portCursorSeed(registry2) {
@@ -19761,22 +20199,22 @@ function existingPortRange(repo, registry2) {
19761
20199
  return registry2[repo] ?? null;
19762
20200
  }
19763
20201
  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;
20202
+ const registryPath = (0, import_node_path21.join)(root, "infra", "port-ranges.json");
20203
+ const ddbScriptPath = (0, import_node_path21.join)(root, "infra", "port-ddb.mjs");
20204
+ if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
19767
20205
  return { root, source, registryPath, ddbScriptPath };
19768
20206
  }
19769
20207
  function resolvePortRangeInfra(cwd, packageDir) {
19770
20208
  const direct = portRangeInfraAt(cwd, "cwd");
19771
20209
  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");
20210
+ for (let dir = cwd; ; dir = (0, import_node_path21.dirname)(dir)) {
20211
+ const sibling = portRangeInfraAt((0, import_node_path21.join)(dir, "MMI-Hub"), "sibling-hub");
19774
20212
  if (sibling) return sibling;
19775
- const parent = (0, import_node_path19.dirname)(dir);
20213
+ const parent = (0, import_node_path21.dirname)(dir);
19776
20214
  if (parent === dir) break;
19777
20215
  }
19778
20216
  if (packageDir) {
19779
- const pkgRoot = (0, import_node_path19.join)(packageDir, "..", "..");
20217
+ const pkgRoot = (0, import_node_path21.join)(packageDir, "..", "..");
19780
20218
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
19781
20219
  if (pkgFrom) return pkgFrom;
19782
20220
  }
@@ -19952,8 +20390,8 @@ function registerStageCommands(program3) {
19952
20390
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
19953
20391
  return decideStage({
19954
20392
  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"))
20393
+ hasCompose: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), "docker-compose.yml")),
20394
+ hasEnvExample: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), ".env.example"))
19957
20395
  });
19958
20396
  }
19959
20397
  async function fetchStageVaultEnvMerge() {
@@ -20388,9 +20826,9 @@ function registerBoardCommands(program3) {
20388
20826
  }
20389
20827
 
20390
20828
  // 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");
20829
+ var import_node_fs25 = require("node:fs");
20830
+ var import_promises6 = require("node:fs/promises");
20831
+ var import_node_path24 = require("node:path");
20394
20832
  var import_node_child_process11 = require("node:child_process");
20395
20833
 
20396
20834
  // src/board-advance.ts
@@ -20476,117 +20914,64 @@ function boardAdvanceFailureMessage(result) {
20476
20914
  }
20477
20915
 
20478
20916
  // 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));
20917
+ var import_promises5 = require("node:fs/promises");
20918
+ var import_node_path23 = require("node:path");
20919
+ var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
20482
20920
  async function atomicWrite(target, contents) {
20483
20921
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
20484
- await (0, import_promises4.writeFile)(tmp, contents, "utf8");
20922
+ await (0, import_promises5.writeFile)(tmp, contents, "utf8");
20485
20923
  try {
20486
20924
  await renameWithRetry(tmp, target);
20487
20925
  } catch (e) {
20488
- await (0, import_promises4.unlink)(tmp).catch(() => void 0);
20926
+ await (0, import_promises5.unlink)(tmp).catch(() => void 0);
20489
20927
  throw e;
20490
20928
  }
20491
20929
  }
20492
20930
  async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
20493
20931
  for (let i = 0; ; i++) {
20494
20932
  try {
20495
- await (0, import_promises4.rename)(from, to);
20933
+ await (0, import_promises5.rename)(from, to);
20496
20934
  return;
20497
20935
  } catch (e) {
20498
20936
  const code = e.code;
20499
20937
  if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") throw e;
20500
- await sleep(backoffMs);
20938
+ await sleep2(backoffMs);
20501
20939
  }
20502
20940
  }
20503
20941
  }
20504
20942
  async function readStrict(registryPath) {
20505
20943
  let text;
20506
20944
  try {
20507
- text = await (0, import_promises4.readFile)(registryPath, "utf8");
20945
+ text = await (0, import_promises5.readFile)(registryPath, "utf8");
20508
20946
  } catch (e) {
20509
20947
  if (e.code === "ENOENT") return [];
20510
20948
  throw e;
20511
20949
  }
20512
20950
  return parseDeferredWorktreesFile(text);
20513
20951
  }
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
- }
20952
+ var acquireLock = acquireFileLock;
20953
+ var lockHeldBy = fileLockHeldBy;
20954
+ var releaseLock = releaseFileLock;
20566
20955
  function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
20567
- const opts = {
20568
- staleMs: lockOpts.staleMs ?? 3e4,
20569
- retryMs: lockOpts.retryMs ?? 50,
20570
- maxWaitMs: lockOpts.maxWaitMs ?? 5e3
20571
- };
20956
+ const opts = resolveFileLockOpts({ ...lockOpts, label: "deferred registry lock" });
20572
20957
  const lockPath = `${registryPath}.lock`;
20573
20958
  return {
20574
20959
  // Lenient read for the sweep's initial fetch: any error (missing/corrupt) yields an empty queue.
20575
20960
  read: async () => {
20576
20961
  try {
20577
- return parseDeferredWorktreesFile(await (0, import_promises4.readFile)(registryPath, "utf8"));
20962
+ return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
20578
20963
  } catch {
20579
20964
  return [];
20580
20965
  }
20581
20966
  },
20582
20967
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
20583
20968
  write: async (entries) => {
20584
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
20969
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
20585
20970
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
20586
20971
  },
20587
20972
  // Serialized read-modify-write under the repo-wide lock (#2846).
20588
20973
  update: async (mutate) => {
20589
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
20974
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
20590
20975
  const deadline = Date.now() + opts.maxWaitMs;
20591
20976
  for (; ; ) {
20592
20977
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -20603,7 +20988,7 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
20603
20988
  if (Date.now() >= deadline) {
20604
20989
  throw new Error(`deferred registry lock contention: ${lockPath} lost to a concurrent holder past ${opts.maxWaitMs}ms`);
20605
20990
  }
20606
- await sleep(opts.retryMs);
20991
+ await sleep2(opts.retryMs);
20607
20992
  }
20608
20993
  }
20609
20994
  };
@@ -20650,11 +21035,17 @@ function renderGcApplyResult(result) {
20650
21035
  lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
20651
21036
  lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
20652
21037
  lines.push(` dead sibling dirs removed: ${result.removedWorktreeDirs.length ? result.removedWorktreeDirs.join(", ") : "none"}`);
20653
- lines.push(` worktrees pruned: ${result.pruned ? "yes" : "no"}`);
21038
+ lines.push(` stale registrations pruned: ${result.pruned ? "yes" : "no"} (clears registrations only \u2014 never deletes a directory)`);
20654
21039
  if (result.failed.length) {
20655
21040
  lines.push(` failed (${result.failed.length}):`);
20656
21041
  for (const f of result.failed) lines.push(` - ${f}`);
20657
21042
  }
21043
+ const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
21044
+ if (removedNothing) {
21045
+ lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
21046
+ lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
21047
+ lines.push(" directory and re-run to clear its registration.");
21048
+ }
20658
21049
  return lines.join("\n");
20659
21050
  }
20660
21051
  async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
@@ -20713,7 +21104,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
20713
21104
  return cleanupPrMergeLocalBranch(branch.branch, {
20714
21105
  beforeWorktrees,
20715
21106
  startingPath: branch.worktreePath,
20716
- pathExists: (p) => (0, import_node_fs24.existsSync)(p),
21107
+ pathExists: (p) => (0, import_node_fs25.existsSync)(p),
20717
21108
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20718
21109
  teardownWorktreeStage,
20719
21110
  deferredStore,
@@ -20735,12 +21126,12 @@ async function applyGcPlan(plan, remote, opts = {}) {
20735
21126
  const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
20736
21127
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20737
21128
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
20738
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path22.dirname)((0, import_node_path22.dirname)(worktreeGitRoot)) : repoRoot2;
21129
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path24.dirname)((0, import_node_path24.dirname)(worktreeGitRoot)) : repoRoot2;
20739
21130
  const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
20740
21131
  for (const wt of plan.worktreeDirs) {
20741
21132
  try {
20742
21133
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
20743
- realpath: (path2) => (0, import_node_fs24.realpathSync)(path2)
21134
+ realpath: (path2) => (0, import_node_fs25.realpathSync)(path2)
20744
21135
  });
20745
21136
  if (!cleanupTarget.ok) {
20746
21137
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -20901,13 +21292,13 @@ var realWorktreeDirRemover = {
20901
21292
  probe: (p) => {
20902
21293
  let st;
20903
21294
  try {
20904
- st = (0, import_node_fs24.lstatSync)(p);
21295
+ st = (0, import_node_fs25.lstatSync)(p);
20905
21296
  } catch {
20906
21297
  return null;
20907
21298
  }
20908
21299
  if (st.isSymbolicLink()) return "link";
20909
21300
  try {
20910
- (0, import_node_fs24.readlinkSync)(p);
21301
+ (0, import_node_fs25.readlinkSync)(p);
20911
21302
  return "link";
20912
21303
  } catch {
20913
21304
  }
@@ -20915,7 +21306,7 @@ var realWorktreeDirRemover = {
20915
21306
  },
20916
21307
  readdir: (p) => {
20917
21308
  try {
20918
- return (0, import_node_fs24.readdirSync)(p);
21309
+ return (0, import_node_fs25.readdirSync)(p);
20919
21310
  } catch {
20920
21311
  return [];
20921
21312
  }
@@ -20924,12 +21315,12 @@ var realWorktreeDirRemover = {
20924
21315
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20925
21316
  detachLink: (p) => {
20926
21317
  try {
20927
- (0, import_node_fs24.rmdirSync)(p);
21318
+ (0, import_node_fs25.rmdirSync)(p);
20928
21319
  } catch {
20929
- (0, import_node_fs24.unlinkSync)(p);
21320
+ (0, import_node_fs25.unlinkSync)(p);
20930
21321
  }
20931
21322
  },
20932
- removeTree: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
21323
+ removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
20933
21324
  };
20934
21325
  async function resolvePrimaryCheckout(execGit) {
20935
21326
  try {
@@ -20959,9 +21350,9 @@ async function worktreeHasStageState(worktreePath) {
20959
21350
  }
20960
21351
  }
20961
21352
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20962
- if (!(0, import_node_fs24.existsSync)(statePath)) return false;
21353
+ if (!(0, import_node_fs25.existsSync)(statePath)) return false;
20963
21354
  try {
20964
- const state = JSON.parse((0, import_node_fs24.readFileSync)(statePath, "utf8"));
21355
+ const state = JSON.parse((0, import_node_fs25.readFileSync)(statePath, "utf8"));
20965
21356
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20966
21357
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20967
21358
  } catch {
@@ -21168,8 +21559,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
21168
21559
  }
21169
21560
 
21170
21561
  // src/worktree-lifecycle-commands.ts
21171
- var import_node_fs25 = require("node:fs");
21172
- var import_node_path23 = require("node:path");
21562
+ var import_node_fs26 = require("node:fs");
21563
+ var import_node_path25 = require("node:path");
21173
21564
  var GH_TIMEOUT_MS = 2e4;
21174
21565
  var DEFAULT_BASE = "origin/development";
21175
21566
  var DEFAULT_REMOTE = "origin";
@@ -21272,7 +21663,7 @@ function classifyStaleLeaks(input) {
21272
21663
  var defaultOrphanDirScanDeps = {
21273
21664
  listDirs: (root) => {
21274
21665
  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));
21666
+ return (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path25.join)(root, e.name));
21276
21667
  } catch {
21277
21668
  return [];
21278
21669
  }
@@ -21328,13 +21719,13 @@ function registerWorktreeCommands(program3) {
21328
21719
  if (PROTECTED_BRANCHES2.has(branch)) {
21329
21720
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
21330
21721
  }
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();
21722
+ const gitFile = (0, import_node_path25.join)(wtPath, ".git");
21723
+ const isLinked = (0, import_node_fs26.existsSync)(gitFile) && (0, import_node_fs26.statSync)(gitFile).isFile();
21333
21724
  if (apply && !isLinked) {
21334
21725
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
21335
21726
  }
21336
21727
  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;
21728
+ const primaryCheckout = commonDir ? (0, import_node_path25.dirname)(commonDir) : wtPath;
21338
21729
  const stageSummary = readStageSummary(wtPath);
21339
21730
  const hasStage = stageSummary != null;
21340
21731
  const steps = landSteps(branch, true, hasStage);
@@ -21445,10 +21836,10 @@ async function gatherWorktreeContext() {
21445
21836
  if (s) stages.push({ path: wt.path, port: s.port });
21446
21837
  }
21447
21838
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
21448
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path23.dirname)((0, import_node_path23.dirname)(worktreeGitRoot)) : repoRoot2;
21839
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path25.dirname)((0, import_node_path25.dirname)(worktreeGitRoot)) : repoRoot2;
21449
21840
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
21450
21841
  let orphanDirs = [];
21451
- if ((0, import_node_fs25.existsSync)(wtRoot)) {
21842
+ if ((0, import_node_fs26.existsSync)(wtRoot)) {
21452
21843
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
21453
21844
  ...defaultOrphanDirScanDeps,
21454
21845
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
@@ -21471,7 +21862,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
21471
21862
  }
21472
21863
 
21473
21864
  // src/issue-commands.ts
21474
- var import_node_fs26 = require("node:fs");
21865
+ var import_node_fs27 = require("node:fs");
21475
21866
  var import_node_crypto5 = require("node:crypto");
21476
21867
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
21477
21868
  async function editIssue(client, options, deps = {}) {
@@ -21485,7 +21876,7 @@ async function editIssue(client, options, deps = {}) {
21485
21876
  if (options.body !== void 0 || options.bodyFile !== void 0) {
21486
21877
  let body;
21487
21878
  if (options.bodyFile) {
21488
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs26.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
21879
+ const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs27.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
21489
21880
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
21490
21881
  } else {
21491
21882
  body = options.body ?? "";
@@ -21982,7 +22373,7 @@ function extendCreateCommand(issue2, batchAttach) {
21982
22373
  if (opts.batch) {
21983
22374
  let specs;
21984
22375
  try {
21985
- const raw = (0, import_node_fs26.readFileSync)(opts.batch, "utf8");
22376
+ const raw = (0, import_node_fs27.readFileSync)(opts.batch, "utf8");
21986
22377
  specs = JSON.parse(raw);
21987
22378
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
21988
22379
  } catch (e) {
@@ -22040,12 +22431,12 @@ ${lines}`);
22040
22431
  }
22041
22432
 
22042
22433
  // src/train-commands.ts
22043
- var import_node_fs28 = require("node:fs");
22044
- var import_node_path25 = require("node:path");
22434
+ var import_node_fs29 = require("node:fs");
22435
+ var import_node_path27 = require("node:path");
22045
22436
 
22046
22437
  // src/plugin-guard-io.ts
22047
- var import_node_fs27 = require("node:fs");
22048
- var import_node_path24 = require("node:path");
22438
+ var import_node_fs28 = require("node:fs");
22439
+ var import_node_path26 = require("node:path");
22049
22440
  var import_node_os6 = require("node:os");
22050
22441
 
22051
22442
  // src/plugin-guard.ts
@@ -22175,11 +22566,11 @@ function runHostBin(bin, args, opts) {
22175
22566
  }
22176
22567
  var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
22177
22568
  const homeDir = surface === "codex" ? ".codex" : ".claude";
22178
- return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
22569
+ return (0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
22179
22570
  };
22180
22571
  function readInstalledPlugins(surface = detectSurface(process.env)) {
22181
22572
  try {
22182
- return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath2(surface), "utf8"));
22573
+ return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath2(surface), "utf8"));
22183
22574
  } catch {
22184
22575
  return null;
22185
22576
  }
@@ -22187,13 +22578,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
22187
22578
  function marketplaceCloneCandidates(surface, home) {
22188
22579
  if (surface === "codex") {
22189
22580
  return [
22190
- (0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
22191
- (0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
22581
+ (0, import_node_path26.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
22582
+ (0, import_node_path26.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
22192
22583
  ];
22193
22584
  }
22194
- return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
22585
+ return [(0, import_node_path26.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
22195
22586
  }
22196
- function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
22587
+ function marketplaceClonePresent(surface, home, exists = import_node_fs28.existsSync) {
22197
22588
  return marketplaceCloneCandidates(surface, home).some(exists);
22198
22589
  }
22199
22590
  async function fetchNpmReleasedVersion() {
@@ -22221,7 +22612,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
22221
22612
  isOrgRepo,
22222
22613
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
22223
22614
  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"))
22615
+ pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
22225
22616
  };
22226
22617
  }
22227
22618
  function claudePluginGuardState(isOrgRepo) {
@@ -22356,7 +22747,7 @@ function formatTrainStatus(r) {
22356
22747
  // src/train-commands.ts
22357
22748
  function readRepoVersion() {
22358
22749
  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;
22750
+ return JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
22360
22751
  } catch {
22361
22752
  return void 0;
22362
22753
  }
@@ -22502,6 +22893,9 @@ function registerDeployCommands(program3) {
22502
22893
  }
22503
22894
 
22504
22895
  // src/discovery-commands.ts
22896
+ var import_node_fs30 = require("node:fs");
22897
+ var import_node_os7 = require("node:os");
22898
+ var import_node_path28 = require("node:path");
22505
22899
  var GC_GH_TIMEOUT_MS3 = 2e4;
22506
22900
  async function collectStatus() {
22507
22901
  let branch = "";
@@ -22586,6 +22980,23 @@ async function recommendNext(deps) {
22586
22980
  const top = sorted[0];
22587
22981
  return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
22588
22982
  }
22983
+ function onboardPluginGate(deps) {
22984
+ const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
22985
+ if (!registered) {
22986
+ return { ok: false, detail: `${MMI_MARKETPLACE_NAME} marketplace not registered on this machine` };
22987
+ }
22988
+ const autoUpdate = resolveAutoUpdate({
22989
+ name: MMI_MARKETPLACE_NAME,
22990
+ registered,
22991
+ declared,
22992
+ settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
22993
+ }).effective;
22994
+ const catalog = resolveCatalogRef({ name: MMI_MARKETPLACE_NAME, registered, ref, autoUpdate });
22995
+ const gaps = [];
22996
+ if (!autoUpdate) gaps.push(`auto-update is off \u2014 turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`);
22997
+ if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
22998
+ return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
22999
+ }
22589
23000
  async function collectOnboardStatus() {
22590
23001
  const cfg = await loadConfig();
22591
23002
  let track = "unknown";
@@ -22659,7 +23070,12 @@ async function collectOnboardStatus() {
22659
23070
  else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
22660
23071
  else nextCommand = "mmi-cli board read \u2014 no claimable items found";
22661
23072
  }
22662
- return { track, board, registry: registry2, secrets, nextCommand };
23073
+ const home = (0, import_node_os7.homedir)();
23074
+ const plugin = onboardPluginGate({
23075
+ readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
23076
+ readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
23077
+ });
23078
+ return { track, board, registry: registry2, secrets, plugin, nextCommand };
22663
23079
  }
22664
23080
  function registerDiscoveryCommands(program3) {
22665
23081
  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 +23119,7 @@ function registerDiscoveryCommands(program3) {
22703
23119
  console.log(`Board: ${report.board.ok ? "\u2713" : "\u2717"} ${report.board.detail}`);
22704
23120
  console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
22705
23121
  console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
23122
+ console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
22706
23123
  console.log(`
22707
23124
  Next command: ${report.nextCommand}`);
22708
23125
  }
@@ -22862,7 +23279,7 @@ function registerExplainCommand(program3) {
22862
23279
  }
22863
23280
 
22864
23281
  // src/pr-commands.ts
22865
- var import_promises6 = require("node:fs/promises");
23282
+ var import_promises7 = require("node:fs/promises");
22866
23283
  var GC_GH_TIMEOUT_MS4 = 2e4;
22867
23284
  var CHECKS_WATCH_POLL_MS = 15e3;
22868
23285
  var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
@@ -23006,10 +23423,10 @@ function registerPrLifecycleCommands(program3) {
23006
23423
  let body;
23007
23424
  try {
23008
23425
  if (o.title || o.titleFile) {
23009
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23426
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23010
23427
  }
23011
23428
  if (o.body || o.bodyFile) {
23012
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23429
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23013
23430
  }
23014
23431
  } catch (e) {
23015
23432
  return fail(`pr edit: ${e.message}`);
@@ -23039,7 +23456,7 @@ function registerPrLifecycleCommands(program3) {
23039
23456
  }
23040
23457
  let body;
23041
23458
  try {
23042
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23459
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23043
23460
  } catch (e) {
23044
23461
  return fail(`pr comment: ${e.message}`);
23045
23462
  }
@@ -23443,7 +23860,7 @@ function renderDoctorText(checks, opts) {
23443
23860
  return lines.join("\n");
23444
23861
  }
23445
23862
  function doctorReportExitCode(checks) {
23446
- return checks.some((c) => !c.ok) ? 1 : 0;
23863
+ return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
23447
23864
  }
23448
23865
 
23449
23866
  // src/doctor-clean.ts
@@ -23477,6 +23894,7 @@ function checkGithubAuth(probe) {
23477
23894
  };
23478
23895
  }
23479
23896
  function checkAwsIdentity(probe) {
23897
+ if (probe.probed === false) return null;
23480
23898
  if (!probe.isOrgRepo) return null;
23481
23899
  const arn = probe.callerArn?.trim();
23482
23900
  return {
@@ -23485,7 +23903,11 @@ function checkAwsIdentity(probe) {
23485
23903
  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
23904
  // Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
23487
23905
  // 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)"}`]
23906
+ //
23907
+ // #3485: the empty case says "not resolved", never "not configured". `awsCallerArn` returns undefined
23908
+ // for ANY failure — expired SSO, no `aws` binary, a timeout — so asserting "no AWS identity configured"
23909
+ // is the same false-fact green this issue is about, one layer down. Evidence states what was observed.
23910
+ verbose: [`caller: ${arn || "(none resolved \u2014 no AWS configured, or the probe failed; either is allowed)"}`]
23489
23911
  };
23490
23912
  }
23491
23913
  function checkRepoWorktrees(probe) {
@@ -23515,6 +23937,11 @@ function checkWorktreeRoots(probe) {
23515
23937
  return {
23516
23938
  ok: false,
23517
23939
  label: "worktree roots",
23940
+ // #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
23941
+ // exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
23942
+ // review of directories that may hold unlanded work, and a size-based auto-sweep once nearly deleted
23943
+ // an unlanded correctness fix. Counting it made `mmi-cli doctor` exit 1 until someone swept by hand.
23944
+ reportOnly: true,
23518
23945
  // The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
23519
23946
  // and doctor itself — with or without --apply — never touches these directories.
23520
23947
  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 +23957,8 @@ function checkClaudePlugin(probe) {
23530
23957
  const { installed, released, guardState } = probe;
23531
23958
  const evidence = [
23532
23959
  `installed: ${installed ?? "(none)"}`,
23533
- `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
23960
+ // #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
23961
+ `released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
23534
23962
  `resolvable: ${guardState}`
23535
23963
  ];
23536
23964
  const trigger = pluginHealTrigger(probe);
@@ -23552,15 +23980,16 @@ function checkClaudePlugin(probe) {
23552
23980
  verbose: evidence
23553
23981
  };
23554
23982
  }
23983
+ if (!released) return null;
23555
23984
  return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin", verbose: evidence };
23556
23985
  }
23557
- function checkCliVersion(input) {
23986
+ function checkCliVersion(input, releasedNote) {
23558
23987
  const report = buildVersionLagReport(input);
23559
23988
  const evidence = [
23560
23989
  `running: ${report.currentVersion}`,
23561
- `published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`
23990
+ `published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
23562
23991
  ];
23563
- if (!report.releasedVersion) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
23992
+ if (!report.releasedVersion) return null;
23564
23993
  if (report.ok) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
23565
23994
  return {
23566
23995
  ok: false,
@@ -23595,6 +24024,12 @@ function checkPluginCache(input) {
23595
24024
  return {
23596
24025
  ok: false,
23597
24026
  label: "plugin cache",
24027
+ // #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
24028
+ // A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
24029
+ // `mmi-cli plugin prune --apply` clears it in one command and a re-run goes green, so this is an
24030
+ // ordinary red the operator is expected to act on. "The doctor cannot heal it automatically" was never
24031
+ // the bar — see the contract for why report-only is a per-row ruling rather than a rule you can apply
24032
+ // from here.
23598
24033
  // Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
23599
24034
  // on its own so a cache with zero versioned dirs still gets an honest ✗.
23600
24035
  detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
@@ -23623,10 +24058,14 @@ function checkSchedules(probe) {
23623
24058
  ...probe.incomplete.map((i) => `incomplete: ${i}`),
23624
24059
  ...probe.drift.map((d) => `drift: ${d}`)
23625
24060
  ];
24061
+ const clearableHere = probe.clearableHere ?? 0;
23626
24062
  if (probe.incomplete.length) {
23627
24063
  return {
23628
24064
  ok: false,
23629
24065
  label: "schedules",
24066
+ // #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
24067
+ // nothing in this checkout clears it.
24068
+ reportOnly: true,
23630
24069
  detail: `${probe.incomplete.length} source(s) unreadable`,
23631
24070
  fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
23632
24071
  verbose: evidence
@@ -23636,8 +24075,14 @@ function checkSchedules(probe) {
23636
24075
  return {
23637
24076
  ok: false,
23638
24077
  label: "schedules",
24078
+ // #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
24079
+ // that conditional rather than blanket: when a finding IS clearable from this checkout — a
24080
+ // `file-vs-registry-stale` row for this repo, one `org schedules register` away — the row gates
24081
+ // like any other actionable red. Exempting a fault the operator can fix in one command is the
24082
+ // tolerated-red failure the tier exists to prevent, not an instance of it.
24083
+ ...clearableHere > 0 ? {} : { reportOnly: true },
23639
24084
  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",
24085
+ 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
24086
  verbose: evidence
23642
24087
  };
23643
24088
  }
@@ -23652,6 +24097,10 @@ function checkDocsAudit(probe) {
23652
24097
  return {
23653
24098
  ok: false,
23654
24099
  label: "docs-audit",
24100
+ // #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
24101
+ // owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
24102
+ // a dead-man check defeats the check (#3067 pillar 4: silence is an alarm, never a success).
24103
+ reportOnly: true,
23655
24104
  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
24105
  verbose: [probe.detail]
23657
24106
  };
@@ -23705,13 +24154,15 @@ function gcReapable(plan) {
23705
24154
  return plan.branches.length + plan.trackingRefs.length + plan.worktreeDirs.length;
23706
24155
  }
23707
24156
  async function runDoctorClean(opts, io, deps) {
23708
- const apply = Boolean(opts.apply);
23709
- const applyRepo = apply && opts.repoWrites !== false;
24157
+ const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
24158
+ const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
24159
+ const probeReleased = !opts.fast || Boolean(opts.self);
24160
+ const probeAws = !opts.fast && !opts.banner;
23710
24161
  const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
23711
24162
  deps.githubLogin(),
23712
24163
  deps.isOrgRepo(),
23713
- opts.fast ? Promise.resolve(void 0) : deps.releasedVersion(),
23714
- opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn(),
24164
+ probeReleased ? deps.releasedVersion() : Promise.resolve(void 0),
24165
+ probeAws ? deps.awsCallerArn() : Promise.resolve(void 0),
23715
24166
  // #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
23716
24167
  // network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
23717
24168
  // `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
@@ -23726,7 +24177,7 @@ async function runDoctorClean(opts, io, deps) {
23726
24177
  if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
23727
24178
  checks.push(...checkGithubPools(await deps.githubPools()));
23728
24179
  }
23729
- const aws = checkAwsIdentity({ isOrgRepo, callerArn });
24180
+ const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
23730
24181
  if (aws) checks.push(aws);
23731
24182
  checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
23732
24183
  if (isOrgRepo) {
@@ -23745,9 +24196,10 @@ async function runDoctorClean(opts, io, deps) {
23745
24196
  checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
23746
24197
  }
23747
24198
  }
23748
- const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo) };
24199
+ const releasedNote = deps.releasedVersionNote?.();
24200
+ const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo), releasedNote };
23749
24201
  const healTrigger = pluginHealTrigger(pluginProbe);
23750
- if (apply && deps.healPlugin && healTrigger) {
24202
+ if (applyEnv && deps.healPlugin && healTrigger) {
23751
24203
  const heal = await deps.healPlugin();
23752
24204
  const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
23753
24205
  const healEvidence = [
@@ -23764,18 +24216,25 @@ async function runDoctorClean(opts, io, deps) {
23764
24216
  } : {
23765
24217
  ok: false,
23766
24218
  label,
23767
- fix: `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
24219
+ // #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
24220
+ // not fix what it found — but it is not this invocation's to clear. The other process is doing it;
24221
+ // waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
24222
+ // genuine gap and still gates.
24223
+ ...heal.skipped ? { reportOnly: true } : {},
24224
+ 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
24225
  verbose: healEvidence
23769
24226
  });
23770
- restartPending = true;
24227
+ if (!heal.skipped) restartPending = true;
23771
24228
  } else {
23772
24229
  const plugin = checkClaudePlugin(pluginProbe);
23773
- checks.push(plugin);
23774
- if (!plugin.ok) restartPending = true;
24230
+ if (plugin) {
24231
+ checks.push(plugin);
24232
+ if (!plugin.ok) restartPending = true;
24233
+ }
23775
24234
  }
23776
24235
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
23777
24236
  const cliReport = buildVersionLagReport(cliInput);
23778
- if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
24237
+ if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
23779
24238
  const heal = await deps.updateCli(cliReport.releasedVersion);
23780
24239
  const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
23781
24240
  checks.push(heal.ok ? {
@@ -23786,17 +24245,48 @@ async function runDoctorClean(opts, io, deps) {
23786
24245
  } : {
23787
24246
  ok: false,
23788
24247
  label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
23789
- fix: `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
24248
+ // #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
24249
+ ...heal.skipped ? { reportOnly: true } : {},
24250
+ 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
24251
  verbose: healEvidence
23791
24252
  });
23792
24253
  } else {
23793
- checks.push(checkCliVersion(cliInput));
24254
+ const cli = checkCliVersion(cliInput, releasedNote);
24255
+ if (cli) checks.push(cli);
24256
+ }
24257
+ const cacheProbe = deps.pluginCache();
24258
+ const cacheStale = cacheProbe.stale.length > 0 || cacheProbe.staging.length > 0;
24259
+ if (applyEnv && deps.prunePluginCache && cacheStale) {
24260
+ const pruned = await deps.prunePluginCache();
24261
+ const removed = pruned.removed ?? [];
24262
+ const pruneEvidence = [
24263
+ `cached versions: ${cacheProbe.cached}`,
24264
+ `stale: ${cacheProbe.stale.length ? cacheProbe.stale.join(", ") : "(none)"}`,
24265
+ `orphaned staging dirs: ${cacheProbe.staging.length ? cacheProbe.staging.join(", ") : "(none)"}`,
24266
+ `prune: ${pruned.detail}`,
24267
+ ...removed.map((r) => `removed: ${r}`)
24268
+ ];
24269
+ checks.push(pruned.ok ? {
24270
+ ok: true,
24271
+ label: "plugin cache",
24272
+ detail: removed.length ? `pruned ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}` : "nothing to prune",
24273
+ verbose: pruneEvidence
24274
+ } : {
24275
+ ok: false,
24276
+ label: "plugin cache",
24277
+ // Same split as the other env heals (#3489): a contention skip is not this run's gap.
24278
+ ...pruned.skipped ? { reportOnly: true } : {},
24279
+ 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\``,
24280
+ verbose: pruneEvidence
24281
+ });
24282
+ } else {
24283
+ checks.push(checkPluginCache(cacheProbe));
23794
24284
  }
23795
- checks.push(checkPluginCache(deps.pluginCache()));
23796
24285
  if (deps.sessionPayload) {
23797
24286
  const payload = checkSessionPayload(deps.sessionPayload());
23798
24287
  if (payload) checks.push(payload);
23799
24288
  }
24289
+ if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
23800
24290
  if (!opts.fast && !opts.banner && !opts.preflight) {
23801
24291
  const probe = await deps.schedulesNotebook().catch((e) => ({
23802
24292
  armed: 0,
@@ -23822,7 +24312,7 @@ async function runDoctorClean(opts, io, deps) {
23822
24312
  const roots = checkWorktreeRoots(probe);
23823
24313
  if (roots) checks.push(roots);
23824
24314
  }
23825
- if (isOrgRepo && !opts.fast && !opts.banner) {
24315
+ if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
23826
24316
  try {
23827
24317
  checks.push(checkTrainSync(await deps.syncTrain()));
23828
24318
  } catch (e) {
@@ -23924,17 +24414,17 @@ function parseOriginRepo(remoteUrl) {
23924
24414
  }
23925
24415
  function ghHostsConfigPath(env, platform2) {
23926
24416
  const sep2 = platform2 === "win32" ? "\\" : "/";
23927
- const join24 = (...parts) => parts.join(sep2);
24417
+ const join26 = (...parts) => parts.join(sep2);
23928
24418
  const explicit = env.GH_CONFIG_DIR?.trim();
23929
- if (explicit) return join24(explicit, "hosts.yml");
24419
+ if (explicit) return join26(explicit, "hosts.yml");
23930
24420
  if (platform2 === "win32") {
23931
24421
  const appData = (env.AppData ?? env.APPDATA)?.trim();
23932
- return appData ? join24(appData, "GitHub CLI", "hosts.yml") : void 0;
24422
+ return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
23933
24423
  }
23934
24424
  const xdg = env.XDG_CONFIG_HOME?.trim();
23935
- if (xdg) return join24(xdg, "gh", "hosts.yml");
24425
+ if (xdg) return join26(xdg, "gh", "hosts.yml");
23936
24426
  const home = env.HOME?.trim();
23937
- return home ? join24(home, ".config", "gh", "hosts.yml") : void 0;
24427
+ return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
23938
24428
  }
23939
24429
  function parseGhHostsAccounts(yaml, host = "github.com") {
23940
24430
  let hostIndent = null;
@@ -23984,9 +24474,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
23984
24474
  }
23985
24475
 
23986
24476
  // 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");
24477
+ var import_node_fs31 = require("node:fs");
24478
+ var import_node_os8 = require("node:os");
24479
+ var import_node_path29 = require("node:path");
23990
24480
  var import_node_child_process12 = require("node:child_process");
23991
24481
  var import_node_util8 = require("node:util");
23992
24482
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
@@ -23994,7 +24484,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
23994
24484
  function installedClaudePluginVersion() {
23995
24485
  try {
23996
24486
  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")
24487
+ (0, import_node_fs31.readFileSync)((0, import_node_path29.join)((0, import_node_os8.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23998
24488
  );
23999
24489
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
24000
24490
  if (versions.length === 0) return void 0;
@@ -24015,13 +24505,13 @@ function worktreeRootSync() {
24015
24505
  }
24016
24506
  var gitignorePath = () => {
24017
24507
  const root = worktreeRootSync();
24018
- return root === null ? null : (0, import_node_path26.join)(root, ".gitignore");
24508
+ return root === null ? null : (0, import_node_path29.join)(root, ".gitignore");
24019
24509
  };
24020
24510
  function readGitignore() {
24021
24511
  const path2 = gitignorePath();
24022
24512
  if (path2 === null) return null;
24023
24513
  try {
24024
- return (0, import_node_fs29.readFileSync)(path2, "utf8");
24514
+ return (0, import_node_fs31.readFileSync)(path2, "utf8");
24025
24515
  } catch {
24026
24516
  return null;
24027
24517
  }
@@ -24030,7 +24520,7 @@ function writeGitignore(content) {
24030
24520
  const path2 = gitignorePath();
24031
24521
  if (path2 === null) return false;
24032
24522
  try {
24033
- (0, import_node_fs29.writeFileSync)(path2, content, "utf8");
24523
+ (0, import_node_fs31.writeFileSync)(path2, content, "utf8");
24034
24524
  return true;
24035
24525
  } catch {
24036
24526
  return false;
@@ -24054,7 +24544,7 @@ async function repoRoot() {
24054
24544
  }
24055
24545
  function hasRepoLocalWorktrees() {
24056
24546
  const root = worktreeRootSync();
24057
- return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path26.join)(root, ".worktrees"));
24547
+ return root !== null && (0, import_node_fs31.existsSync)((0, import_node_path29.join)(root, ".worktrees"));
24058
24548
  }
24059
24549
 
24060
24550
  // src/index.ts
@@ -24089,13 +24579,61 @@ ${r.stderr ?? ""}`).catch(() => "");
24089
24579
  function ghMultiAccountCaveat(announcedLogin) {
24090
24580
  try {
24091
24581
  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")));
24582
+ if (!hostsPath || !(0, import_node_fs32.existsSync)(hostsPath)) return void 0;
24583
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs32.readFileSync)(hostsPath, "utf8")));
24094
24584
  } catch {
24095
24585
  return void 0;
24096
24586
  }
24097
24587
  }
24098
- function mmiDoctorDeps() {
24588
+ var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
24589
+ var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
24590
+ function envHealLockPath(home) {
24591
+ return (0, import_node_path30.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
24592
+ }
24593
+ async function withEnvHealLock(what, run) {
24594
+ try {
24595
+ return await withFileLock(
24596
+ envHealLockPath((0, import_node_os9.homedir)()),
24597
+ { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
24598
+ run
24599
+ );
24600
+ } catch (e) {
24601
+ if (e instanceof FileLockBusyError) {
24602
+ return { ok: false, skipped: true, detail: `${what} skipped \u2014 ${e.message}` };
24603
+ }
24604
+ return { ok: false, detail: `${what} could not take the env-heal lock \u2014 ${e.message}` };
24605
+ }
24606
+ }
24607
+ function docsJanitorScheduleId(repo) {
24608
+ return `${repo.split("/").pop()}/docs-janitor`;
24609
+ }
24610
+ function throttledReleasedVersion() {
24611
+ let note;
24612
+ return {
24613
+ note: () => note,
24614
+ read: async () => {
24615
+ const cachePath = releasedVersionCachePath(repoRuntimeStatePath(process.cwd()));
24616
+ const cached = readReleasedVersionCache(cachePath);
24617
+ if (cached) {
24618
+ note = cachedReadNote(cached.at);
24619
+ return cached.version;
24620
+ }
24621
+ const live = await fetchNpmReleasedVersion();
24622
+ if (live) writeReleasedVersionCache(cachePath, live);
24623
+ note = void 0;
24624
+ return live;
24625
+ }
24626
+ };
24627
+ }
24628
+ function mmiDoctorDeps(opts = {}) {
24629
+ const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
24630
+ let notebook;
24631
+ const notebookOnce = () => notebook ??= fetchNotebook();
24632
+ const docsJanitorArmedAt = async (repo) => {
24633
+ const wanted = docsJanitorScheduleId(repo);
24634
+ const { entries } = await notebookOnce();
24635
+ return entries.find((e) => e.scheduleId === wanted)?.armedAt;
24636
+ };
24099
24637
  return {
24100
24638
  githubLogin,
24101
24639
  ghInstalled,
@@ -24104,12 +24642,17 @@ function mmiDoctorDeps() {
24104
24642
  isOrgRepo: () => isOrgRepoRoot(),
24105
24643
  installedPluginVersion: installedClaudePluginVersion,
24106
24644
  pluginGuardState: claudePluginGuardState,
24107
- releasedVersion: fetchNpmReleasedVersion,
24645
+ releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
24646
+ releasedVersionNote: throttled ? throttled.note : void 0,
24108
24647
  // #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
24109
- updateCli: npmSelfUpdateCli,
24648
+ // #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
24649
+ // marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
24650
+ // rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
24651
+ // carries a CLI shim, so a concurrent npm global install races the same PATH surface.
24652
+ updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
24110
24653
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
24111
24654
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
24112
- healPlugin: () => healClaudePluginForDoctor(),
24655
+ healPlugin: () => withEnvHealLock("Claude plugin reinstall", () => healClaudePluginForDoctor()),
24113
24656
  currentCliVersion: resolveClientVersion,
24114
24657
  readGitignore,
24115
24658
  writeGitignore,
@@ -24145,9 +24688,9 @@ function mmiDoctorDeps() {
24145
24688
  },
24146
24689
  pluginCache: () => {
24147
24690
  const plan = buildPluginCachePlan(
24148
- (0, import_node_os8.homedir)(),
24691
+ (0, import_node_os9.homedir)(),
24149
24692
  runningPluginVersion(process.env, resolveClientVersion()),
24150
- pluginCacheFsDeps((0, import_node_os8.homedir)(), () => 0)
24693
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), () => 0)
24151
24694
  );
24152
24695
  return {
24153
24696
  stale: plan.prune,
@@ -24157,16 +24700,45 @@ function mmiDoctorDeps() {
24157
24700
  stagingBytes: plan.stagingBytes
24158
24701
  };
24159
24702
  },
24703
+ // #3485 wave 2 item 11: the --apply reap, through the SAME plan + reaper `plugin prune --apply` drives.
24704
+ // The plan's own keep-policy refuses the running and newest versions, so this cannot delete what is
24705
+ // loaded. Under the #3489 env-heal lock: deleting cache dirs while another doctor is mid-reinstall is
24706
+ // the same machine-global hazard the lock exists for.
24707
+ prunePluginCache: () => withEnvHealLock("plugin cache prune", async () => {
24708
+ const plan = buildPluginCachePlan(
24709
+ (0, import_node_os9.homedir)(),
24710
+ runningPluginVersion(process.env, resolveClientVersion()),
24711
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
24712
+ { withBytes: true }
24713
+ );
24714
+ const result = applyPluginCachePlan(
24715
+ plan,
24716
+ (p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }),
24717
+ stagingApplyFsGuard((0, import_node_os9.homedir)())
24718
+ );
24719
+ const removed = [...result.removed, ...result.removedStaging ?? []];
24720
+ if (result.failed.length) {
24721
+ return {
24722
+ ok: false,
24723
+ removed,
24724
+ detail: `${removed.length} removed, ${result.failed.length} failed \u2014 ${result.failed.map((f) => `${f.version}: ${f.error}`).join("; ")}`
24725
+ };
24726
+ }
24727
+ return { ok: true, removed, detail: removed.length ? `removed ${removed.join(", ")}` : "nothing to prune" };
24728
+ }),
24160
24729
  // #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
24161
24730
  // runDoctorClean skips this dep entirely on fast/banner/preflight runs.
24162
24731
  schedulesNotebook: async () => {
24163
- const { entries, incomplete, drift } = await fetchNotebook();
24732
+ const { entries, incomplete, drift, reconciliation } = await notebookOnce();
24733
+ const repoName = await repoSlug().catch(() => "");
24734
+ const clearableHere = repoName ? reconciliation.filter((d) => driftClearableFrom(d, repoName)).length : 0;
24164
24735
  return {
24165
24736
  armed: entries.length,
24166
24737
  live: entries.filter((e) => e.resolved === "live").length,
24167
24738
  declared: entries.filter((e) => e.resolved === "declared").length,
24168
24739
  incomplete,
24169
- drift
24740
+ drift,
24741
+ clearableHere
24170
24742
  };
24171
24743
  },
24172
24744
  // #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
@@ -24175,7 +24747,8 @@ function mmiDoctorDeps() {
24175
24747
  docsAudit: async () => {
24176
24748
  const repo = await currentRepoFullName();
24177
24749
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
24178
- const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
24750
+ const armedAt = await docsJanitorArmedAt(repo).catch(() => void 0);
24751
+ const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today, armedAt });
24179
24752
  return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
24180
24753
  },
24181
24754
  // #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
@@ -24184,7 +24757,21 @@ function mmiDoctorDeps() {
24184
24757
  worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
24185
24758
  // #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
24186
24759
  // A local record read — cheap enough for every lane, including the banner.
24187
- sessionPayload: () => readSessionPayload(process.cwd())
24760
+ sessionPayload: () => readSessionPayload(process.cwd()),
24761
+ // #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
24762
+ // which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
24763
+ marketplaceRows: () => {
24764
+ try {
24765
+ const home = (0, import_node_os9.homedir)();
24766
+ return marketplaceRows(
24767
+ MMI_MARKETPLACE_NAME,
24768
+ readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
24769
+ readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
24770
+ );
24771
+ } catch {
24772
+ return [];
24773
+ }
24774
+ }
24188
24775
  };
24189
24776
  }
24190
24777
  async function requireFreshTrainCli(commandName) {
@@ -24309,19 +24896,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
24309
24896
  });
24310
24897
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
24311
24898
  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;
24899
+ const path2 = (0, import_node_path30.join)(process.cwd(), ".gitignore");
24900
+ const current = (0, import_node_fs32.existsSync)(path2) ? (0, import_node_fs32.readFileSync)(path2, "utf8") : null;
24314
24901
  const plan = planManagedGitignore(current);
24315
24902
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
24316
24903
  if (opts.json) {
24317
- if (opts.write && plan.changed) (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
24904
+ if (opts.write && plan.changed) (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
24318
24905
  console.log(JSON.stringify(plan, null, 2));
24319
24906
  if (!opts.write && plan.changed) process.exitCode = 1;
24320
24907
  return;
24321
24908
  }
24322
24909
  if (opts.write) {
24323
24910
  if (plan.changed) {
24324
- (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
24911
+ (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
24325
24912
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
24326
24913
  } else {
24327
24914
  console.log("mmi-cli org rules gitignore: up to date");
@@ -24447,8 +25034,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
24447
25034
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
24448
25035
  let root;
24449
25036
  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`);
25037
+ root = (0, import_node_path30.resolve)(o.root);
25038
+ 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
25039
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
24453
25040
  if (isPathUnderDirectory(gcRepoRoot, root)) {
24454
25041
  return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
@@ -24522,26 +25109,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
24522
25109
  function acquireWorktreeSetupLock(worktreeRoot) {
24523
25110
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
24524
25111
  const take = () => {
24525
- const fd = (0, import_node_fs30.openSync)(lockPath, "wx");
25112
+ const fd = (0, import_node_fs32.openSync)(lockPath, "wx");
24526
25113
  try {
24527
- (0, import_node_fs30.writeSync)(fd, String(Date.now()));
25114
+ (0, import_node_fs32.writeSync)(fd, String(Date.now()));
24528
25115
  } finally {
24529
- (0, import_node_fs30.closeSync)(fd);
25116
+ (0, import_node_fs32.closeSync)(fd);
24530
25117
  }
24531
25118
  return () => {
24532
25119
  try {
24533
- (0, import_node_fs30.rmSync)(lockPath, { force: true });
25120
+ (0, import_node_fs32.rmSync)(lockPath, { force: true });
24534
25121
  } catch {
24535
25122
  }
24536
25123
  };
24537
25124
  };
24538
25125
  try {
24539
- (0, import_node_fs30.mkdirSync)((0, import_node_path27.dirname)(lockPath), { recursive: true });
25126
+ (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(lockPath), { recursive: true });
24540
25127
  return take();
24541
25128
  } catch {
24542
25129
  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 });
25130
+ if (Date.now() - (0, import_node_fs32.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
25131
+ (0, import_node_fs32.rmSync)(lockPath, { force: true });
24545
25132
  return take();
24546
25133
  }
24547
25134
  } catch {
@@ -24983,7 +25570,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
24983
25570
  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
25571
  if (o.secretsFile) {
24985
25572
  try {
24986
- vars.push(`secrets=${(0, import_node_fs30.readFileSync)(o.secretsFile, "utf8")}`);
25573
+ vars.push(`secrets=${(0, import_node_fs32.readFileSync)(o.secretsFile, "utf8")}`);
24987
25574
  } catch (e) {
24988
25575
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
24989
25576
  }
@@ -25099,8 +25686,8 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
25099
25686
  const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
25100
25687
  if (branch) {
25101
25688
  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) {
25689
+ const sameRepo2 = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
25690
+ if (sameRepo2) {
25104
25691
  const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
25105
25692
  if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
25106
25693
  ${filelessTransitionGuide(target, o.stage)}`);
@@ -25297,8 +25884,8 @@ withExamples(mutating(
25297
25884
  let targetRepo2;
25298
25885
  try {
25299
25886
  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 });
25887
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
25888
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25302
25889
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
25303
25890
  priority = resolveCreatePriority(o.priority, "issue create");
25304
25891
  extraLabels = [...o.label ?? []];
@@ -25441,7 +26028,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
25441
26028
  }
25442
26029
  let body;
25443
26030
  try {
25444
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
26031
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25445
26032
  } catch (e) {
25446
26033
  return fail(`issue comment: ${e.message}`);
25447
26034
  }
@@ -25501,8 +26088,8 @@ program2.command("report").description("file a friction report on the Hub board
25501
26088
  let title;
25502
26089
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
25503
26090
  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 });
26091
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
26092
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25506
26093
  priority = resolveCreatePriority(o.priority, "report");
25507
26094
  if (!ISSUE_TYPES.includes(o.type)) {
25508
26095
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -25551,8 +26138,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
25551
26138
  let args;
25552
26139
  try {
25553
26140
  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 });
26141
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
26142
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
25556
26143
  title = buildSkillLessonTitle(skill, rawTitle);
25557
26144
  priority = resolveCreatePriority(o.priority, "skill-lesson");
25558
26145
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -25603,8 +26190,8 @@ withExamples(pr.command("create").description("create a PR and print {number,url
25603
26190
  let body;
25604
26191
  let title;
25605
26192
  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 });
26193
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
26194
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
25608
26195
  } catch (e) {
25609
26196
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
25610
26197
  }
@@ -25639,11 +26226,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
25639
26226
  }
25640
26227
  });
25641
26228
  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) => {
26229
+ const wfDir = (0, import_node_path30.join)(cwd, ".github", "workflows");
26230
+ if (!(0, import_node_fs32.existsSync)(wfDir)) return [];
26231
+ return (0, import_node_fs32.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
25645
26232
  try {
25646
- return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path27.join)(wfDir, name), "utf8"));
26233
+ return workflowReportsPrChecks((0, import_node_fs32.readFileSync)((0, import_node_path30.join)(wfDir, name), "utf8"));
25647
26234
  } catch {
25648
26235
  return true;
25649
26236
  }
@@ -25670,16 +26257,16 @@ function ciAuditDeps() {
25670
26257
  // gate re-seed step is skipped gracefully rather than failing mid-run.
25671
26258
  readSeedFile: (path2) => {
25672
26259
  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;
26260
+ const fullPath = (0, import_node_path30.join)(root, path2);
26261
+ return (0, import_node_fs32.existsSync)(fullPath) ? (0, import_node_fs32.readFileSync)(fullPath, "utf8") : null;
25675
26262
  }
25676
26263
  };
25677
26264
  }
25678
26265
  function hubRoot() {
25679
- const fromPkg = (0, import_node_path27.join)(__dirname, "..", "..");
26266
+ const fromPkg = (0, import_node_path30.join)(__dirname, "..", "..");
25680
26267
  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();
26268
+ if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(fromPkg, marker))) return fromPkg;
26269
+ if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), marker))) return process.cwd();
25683
26270
  return null;
25684
26271
  }
25685
26272
  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) => {
@@ -25929,7 +26516,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
25929
26516
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
25930
26517
  beforeWorktrees,
25931
26518
  startingPath,
25932
- pathExists: (p) => (0, import_node_fs30.existsSync)(p),
26519
+ pathExists: (p) => (0, import_node_fs32.existsSync)(p),
25933
26520
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
25934
26521
  teardownWorktreeStage,
25935
26522
  deferredStore,
@@ -26039,8 +26626,8 @@ function trainApplyDeps() {
26039
26626
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
26040
26627
  announce: (args) => announceRelease({
26041
26628
  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)
26629
+ readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
26630
+ removeFile: (path2) => (0, import_promises8.unlink)(path2)
26044
26631
  }, args),
26045
26632
  fetchEdgeDomains: async (slug) => {
26046
26633
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -26270,10 +26857,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
26270
26857
  targets = resolution.targets;
26271
26858
  }
26272
26859
  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")) : {};
26860
+ const fileMatrix = (0, import_node_fs32.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs32.readFileSync)("access-matrix.json", "utf8")) : {};
26274
26861
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
26275
26862
  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: {} };
26863
+ const fileContracts = (0, import_node_fs32.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs32.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
26277
26864
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
26278
26865
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
26279
26866
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -26281,7 +26868,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
26281
26868
  });
26282
26869
  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
26870
  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) => {
26871
+ 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
26872
  if (opts.guide) {
26286
26873
  consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
26287
26874
  return;
@@ -26307,16 +26894,16 @@ function directoryBytes(path2) {
26307
26894
  let total = 0;
26308
26895
  let entries;
26309
26896
  try {
26310
- entries = (0, import_node_fs30.readdirSync)(path2, { withFileTypes: true });
26897
+ entries = (0, import_node_fs32.readdirSync)(path2, { withFileTypes: true });
26311
26898
  } catch {
26312
26899
  return 0;
26313
26900
  }
26314
26901
  for (const entry of entries) {
26315
- const child2 = (0, import_node_path27.join)(path2, entry.name);
26902
+ const child2 = (0, import_node_path30.join)(path2, entry.name);
26316
26903
  if (entry.isDirectory()) total += directoryBytes(child2);
26317
26904
  else {
26318
26905
  try {
26319
- total += (0, import_node_fs30.statSync)(child2).size;
26906
+ total += (0, import_node_fs32.statSync)(child2).size;
26320
26907
  } catch {
26321
26908
  }
26322
26909
  }
@@ -26324,25 +26911,25 @@ function directoryBytes(path2) {
26324
26911
  return total;
26325
26912
  }
26326
26913
  function listDirEntries(dir) {
26327
- return (0, import_node_fs30.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
26914
+ return (0, import_node_fs32.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
26328
26915
  }
26329
26916
  function readInstalledPluginRefs(home) {
26330
26917
  const p = installedPluginsPath(home);
26331
- if (!(0, import_node_fs30.existsSync)(p)) return [];
26918
+ if (!(0, import_node_fs32.existsSync)(p)) return [];
26332
26919
  try {
26333
- return installedPluginPaths((0, import_node_fs30.readFileSync)(p, "utf8"));
26920
+ return installedPluginPaths((0, import_node_fs32.readFileSync)(p, "utf8"));
26334
26921
  } catch {
26335
26922
  return null;
26336
26923
  }
26337
26924
  }
26338
26925
  function pluginCacheFsDeps(home, dirBytes) {
26339
26926
  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),
26927
+ exists: (p) => (0, import_node_fs32.existsSync)(p),
26928
+ listVersionDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
26342
26929
  dirBytes,
26343
- listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
26930
+ listStagingDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
26344
26931
  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) };
26932
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path30.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs32.statSync)(p).mtimeMs) };
26346
26933
  } catch {
26347
26934
  return { name: d.name, mtimeMs: Date.now() };
26348
26935
  }
@@ -26356,10 +26943,10 @@ function stagingApplyFsGuard(home) {
26356
26943
  return {
26357
26944
  referencedPaths: () => readInstalledPluginRefs(home),
26358
26945
  mtimeMs: (name) => {
26359
- const p = (0, import_node_path27.join)(stagingRoot, name);
26360
- if (!(0, import_node_fs30.existsSync)(p)) return null;
26946
+ const p = (0, import_node_path30.join)(stagingRoot, name);
26947
+ if (!(0, import_node_fs32.existsSync)(p)) return null;
26361
26948
  try {
26362
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
26949
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs32.statSync)(q).mtimeMs);
26363
26950
  } catch {
26364
26951
  return null;
26365
26952
  }
@@ -26369,13 +26956,13 @@ function stagingApplyFsGuard(home) {
26369
26956
  }
26370
26957
  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
26958
  const plan = buildPluginCachePlan(
26372
- (0, import_node_os8.homedir)(),
26959
+ (0, import_node_os9.homedir)(),
26373
26960
  runningPluginVersion(process.env, resolveClientVersion()),
26374
- pluginCacheFsDeps((0, import_node_os8.homedir)(), directoryBytes),
26961
+ pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
26375
26962
  { withBytes: true }
26376
26963
  );
26377
26964
  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;
26965
+ 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
26966
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
26380
26967
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
26381
26968
  else console.log(renderPluginCachePlan(plan, result));
@@ -26404,6 +26991,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
26404
26991
  });
26405
26992
  const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
26406
26993
  if (line) io.log(line);
26994
+ const authority = authorityLine(report);
26995
+ if (authority) io.log(authority);
26407
26996
  },
26408
26997
  boardSlice: (io) => runBoardSlice(io, {
26409
26998
  loadConfig: () => loadConfigForRepo(),
@@ -26433,8 +27022,10 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
26433
27022
  const line = localTrainSyncBannerLine(result);
26434
27023
  if (line) io.log(line);
26435
27024
  },
27025
+ // #3485 item 7: the ONLY lane that throttles the npm read — it runs on every session start, on a
27026
+ // blocking hook. Every interactive lane still reads live.
26436
27027
  doctor: async (io) => {
26437
- await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
27028
+ await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
26438
27029
  }
26439
27030
  });
26440
27031
  await runSessionStart(parallel, sequential, bannerIo);
@@ -26457,6 +27048,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
26457
27048
  DEFAULT_PRIORITY,
26458
27049
  awsCallerArn,
26459
27050
  classifyParseError,
27051
+ envHealLockPath,
26460
27052
  gcPlan,
26461
27053
  isOrgRegisteredRepo,
26462
27054
  registryClientDeps,