@mutmutco/cli 2.52.1 → 2.54.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 (3) hide show
  1. package/dist/main.cjs +684 -309
  2. package/dist/saga.cjs +16 -8
  3. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3408,7 +3408,7 @@ var program = new Command();
3408
3408
 
3409
3409
  // src/index.ts
3410
3410
  var import_promises8 = require("node:fs/promises");
3411
- var import_node_fs27 = require("node:fs");
3411
+ var import_node_fs28 = require("node:fs");
3412
3412
 
3413
3413
  // src/rules-sync.ts
3414
3414
  function normalizeEol(s) {
@@ -3601,7 +3601,7 @@ async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
3601
3601
  }
3602
3602
  return last;
3603
3603
  }
3604
- var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
3604
+ var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
3605
3605
  async function removeWorktreeWithRecovery(wtPath, deps) {
3606
3606
  const maxAttempts = deps.maxAttempts ?? 3;
3607
3607
  const backoff = deps.backoffMs ?? [250, 1e3];
@@ -4609,7 +4609,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
4609
4609
  async function predictMergeConflicts(deps, ours, theirs) {
4610
4610
  return runMergeTreePreflight(deps, ours, theirs);
4611
4611
  }
4612
- async function mergeWithSpineResolution(deps, sourceRef, label, resolve5, extraTolerated = []) {
4612
+ async function mergeWithSpineResolution(deps, sourceRef, label, resolve6, extraTolerated = []) {
4613
4613
  try {
4614
4614
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
4615
4615
  return;
@@ -4623,7 +4623,7 @@ async function mergeWithSpineResolution(deps, sourceRef, label, resolve5, extraT
4623
4623
  unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
4624
4624
  );
4625
4625
  }
4626
- await deps.run("git", ["checkout", `--${resolve5}`, "--", ...unmerged]);
4626
+ await deps.run("git", ["checkout", `--${resolve6}`, "--", ...unmerged]);
4627
4627
  await deps.run("git", ["add", "--", ...unmerged]);
4628
4628
  await deps.run("git", ["commit", "--no-edit"]);
4629
4629
  }
@@ -4740,7 +4740,7 @@ function requireProjectMetaForTrain(load, repo) {
4740
4740
  var CORRELATE_ATTEMPTS = 5;
4741
4741
  var CORRELATE_DELAY_MS = 1500;
4742
4742
  var CORRELATE_SKEW_SLACK_MS = 1e4;
4743
- var defaultSleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
4743
+ var defaultSleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
4744
4744
  function resolveSleep(deps) {
4745
4745
  return deps.sleep ?? defaultSleep2;
4746
4746
  }
@@ -4959,6 +4959,23 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
4959
4959
  `timed out waiting for required train checks on ${sha}: ${lastError ? `last error: ${lastError}` : lastStatus}`
4960
4960
  );
4961
4961
  }
4962
+ function partialTrainRecoveryError(cause, input) {
4963
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
4964
+ const branch = input.stage;
4965
+ const releaseState = input.stage === "rc" ? "GitHub Release n/a" : "GitHub Release not created";
4966
+ const releaseCommand = input.stage === "main" ? `
4967
+ 2. gh release create ${input.tag} --target main --generate-notes --latest --repo ${input.repo}` : "";
4968
+ const deployStep = input.stage === "main" ? "3" : "2";
4969
+ return new Error(
4970
+ `${causeMessage}
4971
+
4972
+ partial train state: tag ${input.tag} is already pushed; origin/${branch} has not been pushed; ${releaseState}; deploy not dispatched.
4973
+ Recovery sequence:
4974
+ 1. git push origin ${branch}` + releaseCommand + `
4975
+ ${deployStep}. mmi-cli tenant redeploy ${input.repo} ${input.stage} --watch
4976
+ Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
4977
+ );
4978
+ }
4962
4979
  async function ensureTagPushed(deps, tag, sha) {
4963
4980
  const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
4964
4981
  const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
@@ -5167,19 +5184,25 @@ async function mergeSourceToMain(deps, deployModel, args) {
5167
5184
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
5168
5185
  await ensureTagPushed(deps, tag, releaseSha);
5169
5186
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
5170
- const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
5187
+ let checks;
5188
+ try {
5189
+ checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
5190
+ } catch (e) {
5191
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
5192
+ }
5171
5193
  await deps.run("git", ["push", "origin", "main"]);
5172
5194
  const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
5173
5195
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
5174
5196
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
5175
5197
  const autoRunSince = (deps.now ?? Date.now)();
5176
5198
  const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
5177
- const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
5199
+ const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
5178
5200
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
5179
- if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
5201
+ if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
5202
+ const reason = deployDispatch.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
5180
5203
  dispatch = {
5181
5204
  ...dispatch,
5182
- note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
5205
+ note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
5183
5206
  };
5184
5207
  }
5185
5208
  return { checks, releaseUrl, announceNote, dispatch };
@@ -5217,7 +5240,12 @@ async function runTrainApplyPipeline(mode, input) {
5217
5240
  const resumeNote = resume.tag ? resume.note : void 0;
5218
5241
  await ensureTagPushed(deps, tag2, rcSha);
5219
5242
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
5220
- const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
5243
+ let checks2;
5244
+ try {
5245
+ checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
5246
+ } catch (e) {
5247
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
5248
+ }
5221
5249
  const autoRunSince = (deps.now ?? Date.now)();
5222
5250
  await deps.run("git", ["push", "origin", "rc"]);
5223
5251
  const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
@@ -5653,7 +5681,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
5653
5681
  const attempts = opts.attempts ?? 3;
5654
5682
  const baseDelayMs = opts.baseDelayMs ?? 250;
5655
5683
  const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
5656
- const sleep = opts.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
5684
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
5657
5685
  let lastErr;
5658
5686
  for (let i = 0; i < attempts; i++) {
5659
5687
  const isLast = i === attempts - 1;
@@ -5712,7 +5740,7 @@ function hardExit(code) {
5712
5740
  async function cleanExit(code) {
5713
5741
  process.exitCode = code;
5714
5742
  await closeHttpPool();
5715
- await new Promise((resolve5) => setImmediate(resolve5));
5743
+ await new Promise((resolve6) => setImmediate(resolve6));
5716
5744
  process.exit(code);
5717
5745
  }
5718
5746
  async function failGraceful(msg) {
@@ -6084,18 +6112,23 @@ function headPrompt(state) {
6084
6112
  const decisions = shownDecisions(state.decisions);
6085
6113
  const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
6086
6114
  return [
6087
- "You maintain ONE durable slot of a work-session: PINNED (things worth remembering). Given the CURRENT",
6088
- "HEAD and the recent TRANSCRIPT + DECISIONS, return an updated PINNED only. Keep it tight and concrete;",
6089
- "keep anything the user pinned; never invent; preserve Turkish characters (\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do",
6090
- "NOT manage next or the checklist \u2014 the note path owns those. The ANCHOR is the read-only North-Star \u2014",
6091
- "NEVER change it. Never restate an unverified artifact-claim (a named file, PR, flag, or board state)",
6092
- "as settled fact \u2014 keep it as the belief it was recorded as.",
6115
+ "You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
6116
+ "SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
6117
+ "recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
6118
+ "PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
6119
+ "(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
6120
+ "read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
6121
+ "PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
6093
6122
  "You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
6094
6123
  "supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
6095
6124
  "already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
6096
6125
  "is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
6097
- "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor, next, or checklist.",
6098
- 'Output ONLY a JSON object: {"pinned":[string],"supersede":[{"older":int,"newer":int,"reason":string}]}.',
6126
+ "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
6127
+ 'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
6128
+ "for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
6129
+ "the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
6130
+ "Preserve Turkish characters exactly.",
6131
+ 'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
6099
6132
  "",
6100
6133
  "CURRENT HEAD:",
6101
6134
  JSON.stringify(state.head ?? {}, null, 2),
@@ -6119,6 +6152,9 @@ function parseHeadUpdate(raw) {
6119
6152
  if (!obj || typeof obj !== "object") return null;
6120
6153
  const u = {};
6121
6154
  if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
6155
+ if (typeof obj.next === "string" && obj.next.trim()) {
6156
+ u.nextAuto = obj.next.trim().slice(0, 280);
6157
+ }
6122
6158
  if (Array.isArray(obj.supersede)) {
6123
6159
  const supersede = obj.supersede.filter((e) => {
6124
6160
  if (!e || typeof e !== "object") return false;
@@ -6131,12 +6167,12 @@ function parseHeadUpdate(raw) {
6131
6167
  }
6132
6168
  async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
6133
6169
  const { cmd, args, shell: shell2 } = resolveEngine(process.platform, process.env.SAGA_HEAD_ENGINE);
6134
- return await new Promise((resolve5) => {
6170
+ return await new Promise((resolve6) => {
6135
6171
  let child;
6136
6172
  try {
6137
6173
  child = (0, import_node_child_process3.spawn)(cmd, args, { shell: shell2, windowsHide: true });
6138
6174
  } catch {
6139
- return resolve5("");
6175
+ return resolve6("");
6140
6176
  }
6141
6177
  let out = "";
6142
6178
  let done = false;
@@ -6144,7 +6180,7 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
6144
6180
  if (done) return;
6145
6181
  done = true;
6146
6182
  clearTimeout(timer);
6147
- resolve5(v);
6183
+ resolve6(v);
6148
6184
  };
6149
6185
  const timer = setTimeout(() => {
6150
6186
  try {
@@ -6292,8 +6328,8 @@ async function readStdin(opts = {}) {
6292
6328
  })().catch(() => {
6293
6329
  });
6294
6330
  let timer;
6295
- const timeout = new Promise((resolve5) => {
6296
- timer = setTimeout(resolve5, timeoutMs);
6331
+ const timeout = new Promise((resolve6) => {
6332
+ timer = setTimeout(resolve6, timeoutMs);
6297
6333
  });
6298
6334
  try {
6299
6335
  await Promise.race([drain, timeout]);
@@ -6966,7 +7002,7 @@ var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.exe
6966
7002
  var DOCKER_TIMEOUT_MS = 15e3;
6967
7003
  var EARLY_EXIT_GRACE_MS = 2e3;
6968
7004
  function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
6969
- return new Promise((resolve5, reject) => {
7005
+ return new Promise((resolve6, reject) => {
6970
7006
  let settled = false;
6971
7007
  const finish = (fn) => {
6972
7008
  if (settled) return;
@@ -6976,7 +7012,7 @@ function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
6976
7012
  child.removeAllListeners("exit");
6977
7013
  fn();
6978
7014
  };
6979
- const timer = setTimeout(() => finish(resolve5), graceMs);
7015
+ const timer = setTimeout(() => finish(resolve6), graceMs);
6980
7016
  child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
6981
7017
  child.on("exit", (code, signal) => {
6982
7018
  const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
@@ -7186,10 +7222,10 @@ function pickStagePort(range, isFree) {
7186
7222
  throw new Error(`no free stage port in range ${start}-${end} \u2014 every port is in use`);
7187
7223
  }
7188
7224
  function isPortFree(port) {
7189
- return new Promise((resolve5) => {
7225
+ return new Promise((resolve6) => {
7190
7226
  const srv = (0, import_node_net.createServer)();
7191
- srv.once("error", () => resolve5(false));
7192
- srv.once("listening", () => srv.close(() => resolve5(true)));
7227
+ srv.once("error", () => resolve6(false));
7228
+ srv.once("listening", () => srv.close(() => resolve6(true)));
7193
7229
  srv.listen(port, "127.0.0.1");
7194
7230
  });
7195
7231
  }
@@ -7365,6 +7401,18 @@ function writeState(path2, state) {
7365
7401
  mkdirFor(path2);
7366
7402
  (0, import_node_fs9.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
7367
7403
  }
7404
+ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
7405
+ const reservation = {
7406
+ pid: 0,
7407
+ command: "",
7408
+ cwd,
7409
+ statePath,
7410
+ startedAt: now().toISOString(),
7411
+ port
7412
+ };
7413
+ writeState(statePath, reservation);
7414
+ if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
7415
+ }
7368
7416
  async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
7369
7417
  await killTree(state.pid);
7370
7418
  if (state.teardown?.command.trim()) {
@@ -7388,7 +7436,7 @@ async function killTree(pid) {
7388
7436
  } catch {
7389
7437
  }
7390
7438
  }
7391
- await new Promise((resolve5) => setTimeout(resolve5, 500));
7439
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
7392
7440
  try {
7393
7441
  process.kill(-pid, "SIGKILL");
7394
7442
  } catch {
@@ -7409,7 +7457,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
7409
7457
  } catch (e) {
7410
7458
  last = e.message;
7411
7459
  }
7412
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
7460
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
7413
7461
  }
7414
7462
  throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
7415
7463
  }
@@ -7505,6 +7553,8 @@ async function runStage(config = {}, opts = {}) {
7505
7553
  if (problems.length) throw new Error(problems.join("; "));
7506
7554
  const cwd = opts.cwd ?? process.cwd();
7507
7555
  const timeoutMs = opts.timeoutMs ?? 6e4;
7556
+ const statePath = opts.statePath ?? stageStatePath(cwd);
7557
+ const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
7508
7558
  const portGuard = resolveStagePortGuard(opts);
7509
7559
  await stopStage({ ...opts, cwd, requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd });
7510
7560
  const reserved = await reservedPortsForWorktree(cwd);
@@ -7515,11 +7565,20 @@ async function runStage(config = {}, opts = {}) {
7515
7565
  stagePort = await resolveStagePort(config, portGuard, reserved);
7516
7566
  }
7517
7567
  const sub = (s) => substituteStagePort(s, stagePort);
7518
- await ensureStageRuntimeEnv(config, opts, cwd);
7568
+ if (stagePort != null) {
7569
+ writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
7570
+ }
7519
7571
  const extraEnv = stageExtraEnv(config, stagePort);
7520
7572
  const build2 = config.build?.trim();
7521
7573
  const ranBuild = Boolean(build2);
7522
- if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
7574
+ try {
7575
+ await ensureStageRuntimeEnv(config, opts, cwd);
7576
+ if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
7577
+ } catch (e) {
7578
+ (0, import_node_fs9.rmSync)(statePath, { force: true });
7579
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs9.rmSync)(globalStatePath, { force: true });
7580
+ throw e;
7581
+ }
7523
7582
  const started = await startStage(config, {
7524
7583
  ...opts,
7525
7584
  cwd,
@@ -8519,15 +8578,28 @@ function planDirtyForAutosave(localHash, meta, project2, slug, queue) {
8519
8578
  if (metaEntry?.hash === localHash) return false;
8520
8579
  return true;
8521
8580
  }
8522
- function planSlugsNeedingAutosave(deps, project2, slugs) {
8581
+ function resolveAutosaveProject(meta, queue, defaultProject, slug) {
8582
+ const candidates = /* @__PURE__ */ new Set();
8583
+ const suffix = `/${slug}`;
8584
+ for (const key of Object.keys(meta)) {
8585
+ if (key.endsWith(suffix)) candidates.add(key.slice(0, -suffix.length));
8586
+ }
8587
+ for (const entry of queue) {
8588
+ if (entry.slug === slug) candidates.add(entry.project);
8589
+ }
8590
+ if (candidates.size === 1) return [...candidates][0];
8591
+ return defaultProject;
8592
+ }
8593
+ function plansNeedingAutosave(deps, project2, slugs) {
8523
8594
  const meta = parseMeta(deps.readMetaRaw());
8524
8595
  const queue = parseQueue(deps.readQueueRaw());
8525
8596
  const out = [];
8526
8597
  for (const slug of slugs) {
8527
8598
  const raw = deps.readLocal(slug);
8528
8599
  if (raw == null) continue;
8600
+ const autosaveProject = resolveAutosaveProject(meta, queue, project2, slug);
8529
8601
  const hash = hashContent(normalizeEol(raw));
8530
- if (planDirtyForAutosave(hash, meta, project2, slug, queue)) out.push(slug);
8602
+ if (planDirtyForAutosave(hash, meta, autosaveProject, slug, queue)) out.push({ project: autosaveProject, slug });
8531
8603
  }
8532
8604
  return out;
8533
8605
  }
@@ -8555,11 +8627,11 @@ async function planAutoEnqueueDirty(deps, opts = {}) {
8555
8627
  }
8556
8628
  }
8557
8629
  const enqueued = [];
8558
- for (const slug of planSlugsNeedingAutosave(deps, project2, slugs)) {
8559
- const raw = deps.readLocal(slug);
8630
+ for (const entry of plansNeedingAutosave(deps, project2, slugs)) {
8631
+ const raw = deps.readLocal(entry.slug);
8560
8632
  if (raw == null) continue;
8561
- enqueuePlanPush(deps, { project: project2, slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
8562
- enqueued.push(slug);
8633
+ enqueuePlanPush(deps, { project: entry.project, slug: entry.slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
8634
+ enqueued.push(entry.slug);
8563
8635
  }
8564
8636
  if (enqueued.length) deps.detachSync();
8565
8637
  return enqueued;
@@ -11210,6 +11282,11 @@ function defaultWorktreePath(repoRoot, branch) {
11210
11282
  const safe = branch.replace(/[/\\]+/g, "-");
11211
11283
  return (0, import_node_path15.join)((0, import_node_path15.dirname)(repoRoot), "mmi-worktrees", safe);
11212
11284
  }
11285
+ function resolveWorktreeBase(from, remote) {
11286
+ const remotePrefix = `${remote}/`;
11287
+ const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
11288
+ return { base: from, fetchBranch };
11289
+ }
11213
11290
 
11214
11291
  // src/northstar-context.ts
11215
11292
  var SESSION_START_NORTHSTAR_TIMEOUT_MS = 3e3;
@@ -11350,7 +11427,7 @@ function whoamiLine(report) {
11350
11427
  }
11351
11428
 
11352
11429
  // src/index.ts
11353
- var import_node_path24 = require("node:path");
11430
+ var import_node_path25 = require("node:path");
11354
11431
 
11355
11432
  // src/merge-ci-policy.ts
11356
11433
  function resolveMergeCiPolicy(input) {
@@ -12414,7 +12491,7 @@ var PR_LAND_STATE_READ_DELAY_MS = 2e3;
12414
12491
  async function readGhPrStateWithRetry(fetchState2, options) {
12415
12492
  const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
12416
12493
  const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
12417
- const sleep = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
12494
+ const sleep = options?.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
12418
12495
  let lastError = "empty state";
12419
12496
  for (let attempt = 0; attempt < retries; attempt++) {
12420
12497
  try {
@@ -13621,35 +13698,18 @@ async function detectPublicIp(fetchImpl = fetch) {
13621
13698
  if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${IP_ECHO_URL}: "${ip.slice(0, 80)}"`);
13622
13699
  return ip;
13623
13700
  }
13624
- function ghDispatchArgs(workflow, inputs) {
13625
- const args = ["workflow", "run", workflow, "--repo", STAGE_LIVE_HUB_REPO];
13626
- for (const [key, value] of Object.entries(inputs)) args.push("-f", `${key}=${value}`);
13627
- return args;
13628
- }
13629
13701
  function stageLiveUpSteps(t) {
13630
13702
  return [
13631
13703
  { label: `detect your public IP (${IP_ECHO_URL}, bounded)` },
13632
- {
13633
- label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the central deployer`,
13634
- command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
13635
- },
13636
- {
13637
- label: `gate ${t.host} to your IP at the Cloudflare edge (ephemeral firewall_custom skip rule)`,
13638
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip: "<your ip>" }).join(" ")}`
13639
- },
13704
+ { label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
13705
+ { label: `gate ${t.host} to your IP at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
13640
13706
  { label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
13641
13707
  ];
13642
13708
  }
13643
13709
  function stageLiveDownSteps(t) {
13644
13710
  return [
13645
- {
13646
- label: `stop the ${t.slug} dev runtime`,
13647
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
13648
- },
13649
- {
13650
- label: "remove the Cloudflare edge gate (the stage goes dark even if restarted)",
13651
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }).join(" ")}`
13652
- }
13711
+ { label: `stop the ${t.slug} dev runtime via the Hub backend (tenant-control stop)` },
13712
+ { label: `remove the Cloudflare edge gate for ${t.host} via the Hub backend (tenant-control cf-gate-clear)` }
13653
13713
  ];
13654
13714
  }
13655
13715
  async function runStageLiveUp(deps, t) {
@@ -13657,8 +13717,8 @@ async function runStageLiveUp(deps, t) {
13657
13717
  const ip = (await deps.detectIp()).trim();
13658
13718
  if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
13659
13719
  if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
13660
- await deps.run("gh", ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref, stage: "dev" }));
13661
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip }));
13720
+ await deps.deployDev({ repo: t.repo, ref: t.ref });
13721
+ await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
13662
13722
  return {
13663
13723
  command: "stage --live",
13664
13724
  mode: "up",
@@ -13672,8 +13732,8 @@ async function runStageLiveUp(deps, t) {
13672
13732
  }
13673
13733
  async function runStageLiveDown(deps, t) {
13674
13734
  if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
13675
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }));
13676
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }));
13735
+ await deps.control({ repo: t.repo, action: "stop" });
13736
+ await deps.control({ repo: t.repo, action: "cf-gate-clear", host: t.host });
13677
13737
  return {
13678
13738
  command: "stage --live",
13679
13739
  mode: "down",
@@ -14174,7 +14234,7 @@ function clean3(out) {
14174
14234
  return out.trim();
14175
14235
  }
14176
14236
  function sleeper(deps) {
14177
- return deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
14237
+ return deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
14178
14238
  }
14179
14239
  function normalizeHotfixVersion(input) {
14180
14240
  const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
@@ -14400,12 +14460,13 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
14400
14460
  "report",
14401
14461
  meta.publishDir
14402
14462
  );
14403
- const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
14463
+ const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
14404
14464
  let dispatch = appendPublishDispatch(deploy, publish);
14405
- if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14465
+ if (!publish && deploy.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14466
+ const reason = deploy.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
14406
14467
  dispatch = {
14407
14468
  ...dispatch,
14408
- note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
14469
+ note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
14409
14470
  };
14410
14471
  }
14411
14472
  deployNote = dispatch.note;
@@ -16847,8 +16908,8 @@ function vaultPointer(slug) {
16847
16908
  slug,
16848
16909
  root,
16849
16910
  tiers: {
16850
- project: `${root}/${PROJECT_TIER_SEGMENT}/* (project-admin self-serve)`,
16851
- org: [`${root}/rc/*`, `${root}/main/*`].map((p) => `${p} (master-gated)`)
16911
+ project: `${root}/{dev,rc,main}/* (project-admin self-serve for this repo)`,
16912
+ org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
16852
16913
  },
16853
16914
  stages: ["dev", "rc", "main"],
16854
16915
  // Google OAuth is one client per repo; creds live at every stage under the standard key names
@@ -16861,15 +16922,16 @@ function vaultPointer(slug) {
16861
16922
  function formatVaultPointer(p) {
16862
16923
  const lines = [
16863
16924
  `vault root: ${p.root}`,
16864
- ` project tier (self-serve): ${p.tiers.project}`,
16865
- ` org tier (master-gated): ${p.tiers.org.join(" \xB7 ")}`,
16925
+ ` project repo tree: ${p.tiers.project}`,
16926
+ ` org-infra tree: ${p.tiers.org.join(" \xB7 ")}`,
16866
16927
  `stages: ${p.stages.join(", ")} (local is port-agnostic, reuses dev)`,
16867
16928
  `well-known keys:`,
16868
16929
  ...Object.entries(p.wellKnown).map(([k, keys]) => ` ${k}: ${keys.join(", ")}`),
16869
16930
  ``,
16870
16931
  `enumerate actual keys: mmi-cli secrets list`,
16871
16932
  `read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
16872
- `set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`,
16933
+ `set a key: mmi-cli secrets set <stage>/<KEY> (value via stdin; project-admin self-serves own repo)`,
16934
+ `import Rails creds: mmi-cli secrets import-rails-credentials --stage main --map secret_key_base=SECRET_KEY_BASE`,
16873
16935
  `copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
16874
16936
  ];
16875
16937
  return lines.join("\n");
@@ -17222,6 +17284,89 @@ async function secretsSet(deps, key, opts) {
17222
17284
  }
17223
17285
  return putSecret(deps, key, value, opts);
17224
17286
  }
17287
+ function parseRailsCredentialMapping(raw, stage2) {
17288
+ const eq = raw.indexOf("=");
17289
+ if (eq <= 0 || eq === raw.length - 1) return null;
17290
+ const credentialPath = raw.slice(0, eq).trim();
17291
+ const envKey = raw.slice(eq + 1).trim();
17292
+ const vaultKey = stageKey2(stage2, envKey);
17293
+ if (!credentialPath || !envKey || !isValidSecretKey(vaultKey)) return null;
17294
+ return { credentialPath, envKey, vaultKey };
17295
+ }
17296
+ function credentialValueAt(root, path2) {
17297
+ let cur = root;
17298
+ for (const part of path2.split(".").filter(Boolean)) {
17299
+ if (!cur || typeof cur !== "object" || !(part in cur)) return void 0;
17300
+ cur = cur[part];
17301
+ }
17302
+ return cur;
17303
+ }
17304
+ function credentialValueToSecret(value) {
17305
+ if (value === void 0 || value === null) return null;
17306
+ if (typeof value === "string") return value || null;
17307
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
17308
+ return JSON.stringify(value);
17309
+ }
17310
+ async function secretsImportRailsCredentials(deps, opts) {
17311
+ const mappings = opts.mappings.map((m) => parseRailsCredentialMapping(m, opts.stage));
17312
+ const bad = mappings.findIndex((m) => !m);
17313
+ if (bad !== -1) {
17314
+ deps.err(`invalid Rails credential mapping ${JSON.stringify(opts.mappings[bad])}; use credential.path=ENV_KEY`);
17315
+ return false;
17316
+ }
17317
+ const parsed = mappings;
17318
+ if (!parsed.length) {
17319
+ deps.err("secrets import-rails-credentials: at least one --map credential.path=ENV_KEY is required");
17320
+ return false;
17321
+ }
17322
+ if (opts.removeFiles && !deps.removeFile) {
17323
+ deps.err("secrets import-rails-credentials: --remove-files is unavailable in this execution context");
17324
+ return false;
17325
+ }
17326
+ let credentials;
17327
+ try {
17328
+ credentials = await deps.decryptRailsCredentials({
17329
+ appDir: opts.appDir,
17330
+ credentialsFile: opts.credentialsFile,
17331
+ masterKeyFile: opts.masterKeyFile
17332
+ });
17333
+ } catch (e) {
17334
+ deps.err(`secrets import-rails-credentials: could not decrypt Rails credentials: ${e.message}`);
17335
+ return false;
17336
+ }
17337
+ const imports = [];
17338
+ for (const mapping of parsed) {
17339
+ const value = credentialValueToSecret(credentialValueAt(credentials, mapping.credentialPath));
17340
+ if (!value) {
17341
+ deps.err(`credential path ${mapping.credentialPath} not found or empty; nothing written`);
17342
+ return false;
17343
+ }
17344
+ imports.push({ ...mapping, value });
17345
+ }
17346
+ if (opts.dryRun) {
17347
+ for (const item of imports) deps.log(`would import ${item.credentialPath} -> ${item.vaultKey}`);
17348
+ deps.log(`${imports.length} Rails credential mapping(s) checked; no values printed and nothing written`);
17349
+ return true;
17350
+ }
17351
+ let written = 0;
17352
+ for (const item of imports) {
17353
+ const ok = await putSecret(deps, item.vaultKey, item.value, opts);
17354
+ if (!ok) {
17355
+ deps.err(
17356
+ `Rails credentials import stopped after a partial write (${written}/${imports.length}); local encrypted files left untouched`
17357
+ );
17358
+ return false;
17359
+ }
17360
+ written += 1;
17361
+ deps.log(`imported ${item.credentialPath} -> ${item.vaultKey}`);
17362
+ }
17363
+ if (opts.removeFiles) {
17364
+ const files = [...new Set([opts.credentialsFile, opts.masterKeyFile].filter((p) => Boolean(p)))];
17365
+ for (const file of files) await deps.removeFile(file);
17366
+ if (files.length) deps.log(`removed ${files.length} local Rails encrypted credential file(s) after successful import`);
17367
+ }
17368
+ return true;
17369
+ }
17225
17370
  async function secretsEdit(deps, key, opts) {
17226
17371
  return secretsSet(deps, key, opts);
17227
17372
  }
@@ -17332,12 +17477,52 @@ async function secretsUse(deps, key, opts) {
17332
17477
  ` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role (it can read its own ${tier} tier). Never bake it into an image or commit it.`,
17333
17478
  ` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
17334
17479
  " \u2022 Local dev: pull it into a gitignored .env from the vault. To confirm access without printing in PowerShell: `$null = mmi-cli secrets get " + key + "`; in POSIX shells: `mmi-cli secrets get " + key + " >/dev/null`. Never paste it into tracked files or chat.",
17335
- tier === "project" ? " \u2022 If this dev secret graduates to a real prod credential, ask the master to promote it to the org tier (rc/ or main/)." : " \u2022 This is an ORG-tier secret \u2014 master-gated. If you need standing access, ask the master for a `secrets grant`."
17480
+ tier === "project" ? " \u2022 Bare keys default to dev/. Use an explicit rc/<KEY> or main/<KEY> when the stage needs its own value." : " \u2022 For your own product repo, project-admins self-serve this stage key. Org-infra/cross-slug keys remain master-gated."
17336
17481
  ].join("\n")
17337
17482
  );
17338
17483
  }
17339
17484
 
17340
17485
  // src/secrets-commands.ts
17486
+ var import_node_fs22 = require("node:fs");
17487
+ var import_node_path20 = require("node:path");
17488
+ var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
17489
+ var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
17490
+ var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
17491
+ function collectMap(value, previous = []) {
17492
+ return [...previous, value];
17493
+ }
17494
+ async function decryptRailsCredentials(input) {
17495
+ const appDir = (0, import_node_path20.resolve)(input.appDir ?? process.cwd());
17496
+ const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17497
+ const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
17498
+ const credentialsPath = (0, import_node_path20.resolve)(appDir, credentialsFile);
17499
+ const masterKeyPath = (0, import_node_path20.resolve)(appDir, masterKeyFile);
17500
+ const env = {
17501
+ ...process.env,
17502
+ MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
17503
+ MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
17504
+ };
17505
+ if ((0, import_node_fs22.existsSync)(masterKeyPath)) {
17506
+ env.RAILS_MASTER_KEY = (0, import_node_fs22.readFileSync)(masterKeyPath, "utf8").trim();
17507
+ }
17508
+ const script = [
17509
+ 'require "json"',
17510
+ 'require "active_support/encrypted_configuration"',
17511
+ 'config_path = ENV.fetch("MMI_RAILS_CREDENTIALS_FILE")',
17512
+ 'key_path = ENV.fetch("MMI_RAILS_MASTER_KEY_FILE")',
17513
+ 'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
17514
+ "puts JSON.generate(config.config)"
17515
+ ].join("; ");
17516
+ const args = ["exec", "ruby", "-e", script];
17517
+ const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
17518
+ const cmdArgs = process.platform === "win32" ? ["/c", "bundle", ...args] : args;
17519
+ const { stdout } = await execFileP2(cmd, cmdArgs, {
17520
+ cwd: appDir,
17521
+ env,
17522
+ timeout: RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS
17523
+ });
17524
+ return JSON.parse(stdout);
17525
+ }
17341
17526
  async function readSecretStdin() {
17342
17527
  if (process.stdin.isTTY) {
17343
17528
  process.stderr.write(
@@ -17432,6 +17617,32 @@ function registerSecretsCommands(program3) {
17432
17617
  });
17433
17618
  if (!ok) process.exitCode = 1;
17434
17619
  }));
17620
+ secrets.command("import-rails-credentials").description("decrypt Rails credentials and import explicit mappings into the vault (values never printed)").requiredOption("--stage <dev|rc|main>", "target vault stage").option("--map <credential.path=ENV_KEY>", "explicit credential-to-env mapping; repeat for each key", collectMap, []).option("--app-dir <path>", "Rails app directory (defaults to cwd)").option("--credentials-file <path>", `encrypted credentials path relative to --app-dir (default: ${DEFAULT_RAILS_CREDENTIALS_FILE})`).option("--master-key-file <path>", `master key path relative to --app-dir (default: ${DEFAULT_RAILS_MASTER_KEY_FILE})`).option("--dry-run", "decrypt and show mapped key names without writing values").option("--remove-files", "delete credentials/master key files after every mapped value imports successfully").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
17621
+ const stages = ["dev", "rc", "main"];
17622
+ if (!stages.includes(o.stage)) {
17623
+ return fail("secrets import-rails-credentials: --stage must be dev, rc, or main");
17624
+ }
17625
+ const credentialsFile = o.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17626
+ const masterKeyFile = o.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
17627
+ const ok = await secretsImportRailsCredentials(
17628
+ {
17629
+ ...d,
17630
+ decryptRailsCredentials,
17631
+ removeFile: (path2) => (0, import_node_fs22.unlinkSync)((0, import_node_path20.resolve)(o.appDir ?? process.cwd(), path2))
17632
+ },
17633
+ {
17634
+ repo: o.repo,
17635
+ stage: o.stage,
17636
+ mappings: o.map,
17637
+ appDir: o.appDir,
17638
+ credentialsFile,
17639
+ masterKeyFile,
17640
+ dryRun: o.dryRun,
17641
+ removeFiles: o.removeFiles
17642
+ }
17643
+ );
17644
+ if (!ok) process.exitCode = 1;
17645
+ }));
17435
17646
  secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
17436
17647
  secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
17437
17648
  secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
@@ -17593,16 +17804,33 @@ function authorizeBodyHasMismatch(body) {
17593
17804
  }
17594
17805
 
17595
17806
  // src/doctor-run.ts
17596
- var import_node_fs26 = require("node:fs");
17807
+ var import_node_fs27 = require("node:fs");
17597
17808
  var import_promises7 = require("node:fs/promises");
17598
- var import_node_path23 = require("node:path");
17809
+ var import_node_path24 = require("node:path");
17599
17810
  var import_node_os5 = require("node:os");
17600
17811
 
17812
+ // src/plugin-guard.ts
17813
+ function buildPluginGuardDecision(i) {
17814
+ if (!i.isOrgRepo) return { state: "not-org" };
17815
+ if (!i.installRecordPresent) return { state: "no-install" };
17816
+ if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
17817
+ return { state: "healthy" };
17818
+ }
17819
+ function buildGuardSessionStartLine(state, opts = {}) {
17820
+ if (state === "healthy" || state === "not-org") return { exitCode: 0 };
17821
+ const recovery = opts.recovery ?? "mmi-cli plugin-heal";
17822
+ const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
17823
+ return {
17824
+ line: `[mmi-guard] ${reason}; run ${recovery} and restart Claude Code / reload plugins.`,
17825
+ exitCode: 1
17826
+ };
17827
+ }
17828
+
17601
17829
  // src/cursor-plugin-seed.ts
17602
17830
  var import_node_child_process12 = require("node:child_process");
17603
- var import_node_fs22 = require("node:fs");
17831
+ var import_node_fs23 = require("node:fs");
17604
17832
  var import_node_os4 = require("node:os");
17605
- var import_node_path20 = require("node:path");
17833
+ var import_node_path21 = require("node:path");
17606
17834
  var import_node_util7 = require("node:util");
17607
17835
  function isSemverVersion(v) {
17608
17836
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
@@ -17619,17 +17847,17 @@ function ghReleaseTarballApiArgs(tag) {
17619
17847
  }
17620
17848
  function cursorUserGlobalStatePath() {
17621
17849
  if (process.platform === "win32") {
17622
- const base = process.env.APPDATA || (0, import_node_path20.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
17623
- return (0, import_node_path20.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
17850
+ const base = process.env.APPDATA || (0, import_node_path21.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
17851
+ return (0, import_node_path21.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
17624
17852
  }
17625
17853
  if (process.platform === "darwin") {
17626
- return (0, import_node_path20.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17854
+ return (0, import_node_path21.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17627
17855
  }
17628
- return (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17856
+ return (0, import_node_path21.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17629
17857
  }
17630
17858
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
17631
17859
  const dbPath = cursorUserGlobalStatePath();
17632
- if (!(0, import_node_fs22.existsSync)(dbPath)) return void 0;
17860
+ if (!(0, import_node_fs23.existsSync)(dbPath)) return void 0;
17633
17861
  try {
17634
17862
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
17635
17863
  timeout: 5e3
@@ -17643,57 +17871,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
17643
17871
  }
17644
17872
  }
17645
17873
  function syncDirContents(src, dest) {
17646
- (0, import_node_fs22.mkdirSync)(dest, { recursive: true });
17647
- for (const name of (0, import_node_fs22.readdirSync)(dest)) {
17648
- (0, import_node_fs22.rmSync)((0, import_node_path20.join)(dest, name), { recursive: true, force: true });
17874
+ (0, import_node_fs23.mkdirSync)(dest, { recursive: true });
17875
+ for (const name of (0, import_node_fs23.readdirSync)(dest)) {
17876
+ (0, import_node_fs23.rmSync)((0, import_node_path21.join)(dest, name), { recursive: true, force: true });
17649
17877
  }
17650
- (0, import_node_fs22.cpSync)(src, dest, { recursive: true });
17878
+ (0, import_node_fs23.cpSync)(src, dest, { recursive: true });
17651
17879
  }
17652
17880
  function releaseTag(releasedVersion) {
17653
17881
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
17654
17882
  }
17655
17883
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
17656
- const tarFile = (0, import_node_path20.join)(tmpRoot, "archive.tar");
17884
+ const tarFile = (0, import_node_path21.join)(tmpRoot, "archive.tar");
17657
17885
  try {
17658
17886
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
17659
17887
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
17660
17888
  timeout: 6e4
17661
17889
  });
17662
17890
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
17663
- const pluginMmi = (0, import_node_path20.join)(tmpRoot, "plugins", "mmi");
17664
- return (0, import_node_fs22.existsSync)((0, import_node_path20.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17891
+ const pluginMmi = (0, import_node_path21.join)(tmpRoot, "plugins", "mmi");
17892
+ return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17665
17893
  } catch {
17666
17894
  return void 0;
17667
17895
  }
17668
17896
  }
17669
17897
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
17670
- const tarPath = (0, import_node_path20.join)(tmpRoot, "repo.tgz");
17898
+ const tarPath = (0, import_node_path21.join)(tmpRoot, "repo.tgz");
17671
17899
  try {
17672
- (0, import_node_fs22.mkdirSync)(tmpRoot, { recursive: true });
17900
+ (0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
17673
17901
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
17674
17902
  timeout: 12e4,
17675
17903
  maxBuffer: 100 * 1024 * 1024,
17676
17904
  encoding: "buffer",
17677
17905
  windowsHide: true
17678
17906
  });
17679
- (0, import_node_fs22.writeFileSync)(tarPath, stdout);
17907
+ (0, import_node_fs23.writeFileSync)(tarPath, stdout);
17680
17908
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
17681
- const top = (0, import_node_fs22.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
17909
+ const top = (0, import_node_fs23.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
17682
17910
  if (!top) return void 0;
17683
- const pluginMmi = (0, import_node_path20.join)(tmpRoot, top, "plugins", "mmi");
17684
- return (0, import_node_fs22.existsSync)((0, import_node_path20.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17911
+ const pluginMmi = (0, import_node_path21.join)(tmpRoot, top, "plugins", "mmi");
17912
+ return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17685
17913
  } catch {
17686
17914
  return void 0;
17687
17915
  }
17688
17916
  }
17689
17917
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
17690
- (0, import_node_fs22.mkdirSync)(tmpRoot, { recursive: true });
17918
+ (0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
17691
17919
  const tag = releaseTag(releasedVersion);
17692
17920
  if (hubCheckout) {
17693
17921
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
17694
17922
  if (fromHub) return fromHub;
17695
17923
  }
17696
- return downloadPluginMmiViaGh(tag, (0, import_node_path20.join)(tmpRoot, "gh"));
17924
+ return downloadPluginMmiViaGh(tag, (0, import_node_path21.join)(tmpRoot, "gh"));
17697
17925
  }
17698
17926
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
17699
17927
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -17714,7 +17942,7 @@ async function applyCursorPluginCacheSeed(input) {
17714
17942
  for (const pin of pinsToSeed) {
17715
17943
  syncDirContents(source, pin.path);
17716
17944
  }
17717
- (0, import_node_fs22.rmSync)(tmpRoot, { recursive: true, force: true });
17945
+ (0, import_node_fs23.rmSync)(tmpRoot, { recursive: true, force: true });
17718
17946
  return true;
17719
17947
  }
17720
17948
 
@@ -17789,20 +18017,6 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
17789
18017
  const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
17790
18018
  return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
17791
18019
  }
17792
- function isMmiPluginEnabled(settings) {
17793
- return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
17794
- }
17795
- function buildSettingsPluginDriftCheck(input) {
17796
- const base = {
17797
- ok: true,
17798
- label: "org plugin wiring in .claude/settings.json (mmi@mutmutco)",
17799
- fix: "the Claude Code app pruned mmi@mutmutco from the tracked .claude/settings.json (it does this at session start when the mutmutco marketplace source does not resolve, #1805); restore it with `git checkout -- .claude/settings.json` before committing, or the whole org skill set is disabled for the branch"
17800
- };
17801
- const enabled = input.settings?.enabledPlugins;
17802
- if (!input.isOrgRepo || !enabled || Object.keys(enabled).length === 0) return base;
17803
- if (!enabled[MMI_PLUGIN_ID]) return { ...base, ok: false };
17804
- return base;
17805
- }
17806
18020
  function hasProjectInstallRecord(file, pluginId, projectPath) {
17807
18021
  const records = file?.plugins?.[pluginId];
17808
18022
  if (!Array.isArray(records)) return false;
@@ -17825,7 +18039,7 @@ function buildPluginInstallRecordCheck(input) {
17825
18039
  fix: pluginInstallManualFix(input.projectPath, input.surface),
17826
18040
  pluginId
17827
18041
  };
17828
- if (!input.isOrgRepo || !isMmiPluginEnabled(input.settings)) return base;
18042
+ if (!input.isOrgRepo) return base;
17829
18043
  if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
17830
18044
  return {
17831
18045
  ...base,
@@ -18086,8 +18300,9 @@ function reloadAction(surface) {
18086
18300
  return "restart Claude Code (or run /reload-plugins)";
18087
18301
  }
18088
18302
  }
18089
- var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mutmutco`;
18303
+ var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
18090
18304
  var CODEX_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
18305
+ var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
18091
18306
  var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
18092
18307
  var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
18093
18308
  var OPENCODE_PLUGIN_INSTALL_COMMAND = `mmi-cli doctor --apply`;
@@ -18136,7 +18351,10 @@ var PLUGIN_SURFACE_HEAL = {
18136
18351
  healSteps: [
18137
18352
  { args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
18138
18353
  { args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
18139
- { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub"], gated: true },
18354
+ // Pin --ref main: the marketplace clone must track `main` (the released branch the plugin pins in
18355
+ // .claude-plugin/marketplace.json source.ref), removing the dev-vs-main skew that triggers the prune
18356
+ // and doubles fetch exposure. Mirrors the existing codex entry (#2038).
18357
+ { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
18140
18358
  { args: ["plugin", "install", "mmi@mutmutco"], gated: true },
18141
18359
  { args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
18142
18360
  ],
@@ -18159,10 +18377,10 @@ var PLUGIN_SURFACE_HEAL = {
18159
18377
  },
18160
18378
  cursor: {
18161
18379
  delivery: "cursor-cache",
18162
- recovery: "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace",
18380
+ recovery: CURSOR_RECOVERY,
18163
18381
  healSteps: null,
18164
- fix: (surface) => `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`,
18165
- updateRecipe: []
18382
+ fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
18383
+ updateRecipe: [CURSOR_RECOVERY]
18166
18384
  },
18167
18385
  opencode: {
18168
18386
  delivery: "npm",
@@ -18203,9 +18421,21 @@ function surfaceToken(surface) {
18203
18421
  var PLUGIN_UPDATE_RECIPES = {
18204
18422
  claude: PLUGIN_SURFACE_HEAL.claude.updateRecipe,
18205
18423
  codex: PLUGIN_SURFACE_HEAL.codex.updateRecipe,
18424
+ cursor: PLUGIN_SURFACE_HEAL.cursor.updateRecipe,
18206
18425
  opencode: PLUGIN_SURFACE_HEAL.opencode.updateRecipe,
18207
18426
  cli: ["npm install -g @mutmutco/cli@latest"]
18208
18427
  };
18428
+ var PLUGIN_GUIDE_SURFACES = [
18429
+ { key: "cli", label: "npm CLI", versionKeys: ["cli"] },
18430
+ { key: "claude", label: "Claude Code", versionKeys: ["claudePlugin"] },
18431
+ { key: "codex", label: "Codex", versionKeys: ["codexMarketplace", "codexActiveCache"] },
18432
+ { key: "cursor", label: "Cursor", versionKeys: [] },
18433
+ { key: "opencode", label: "OpenCode", versionKeys: ["opencodePlugin"] }
18434
+ ];
18435
+ function renderSurfaceGuide(label, steps) {
18436
+ if (!steps.length) return [];
18437
+ return [` ${label}`, ...steps.map((step) => ` ${step}`)];
18438
+ }
18209
18439
  function highestSemver(versions) {
18210
18440
  return versions.reduce((best, v) => {
18211
18441
  if (!isSemverVersion2(v)) return best;
@@ -18233,20 +18463,24 @@ function buildPluginUpdateReport(input) {
18233
18463
  function renderPluginUpdateReport(report) {
18234
18464
  const v = report.versions;
18235
18465
  const show = (x) => x ?? "unknown";
18236
- return [
18237
- "MMI versions:",
18238
- ` CLI: ${show(v.cli)}`,
18239
- ` Claude plugin: ${show(v.claudePlugin)}`,
18240
- ` Codex marketplace: ${show(v.codexMarketplace)}`,
18241
- ` Codex active cache: ${show(v.codexActiveCache)}`,
18242
- ` OpenCode plugin: ${show(v.opencodePlugin)}`,
18243
- ` latest release: ${show(v.released)}`,
18244
- "Update recipes (per surface):",
18245
- ` Claude: ${report.recipes.claude.join(" ; ")}`,
18246
- ` Codex: ${report.recipes.codex.join(" ; ")}`,
18247
- ` OpenCode: ${report.recipes.opencode.join(" ; ")}`,
18248
- ` npm CLI: ${report.recipes.cli.join(" ; ")}`
18466
+ const versionRows = [
18467
+ ["mmi-cli", show(v.cli)],
18468
+ ["Claude plugin", show(v.claudePlugin)],
18469
+ ["Codex marketplace", show(v.codexMarketplace)],
18470
+ ["Codex active cache", show(v.codexActiveCache)],
18471
+ ["OpenCode plugin", show(v.opencodePlugin)]
18249
18472
  ];
18473
+ const pad = Math.max(...versionRows.map(([label]) => label.length));
18474
+ const lines = [
18475
+ `MMI versions (target release: ${show(v.released)})`,
18476
+ ...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`),
18477
+ "",
18478
+ "Update commands by surface"
18479
+ ];
18480
+ for (const surface of PLUGIN_GUIDE_SURFACES) {
18481
+ lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
18482
+ }
18483
+ return lines;
18250
18484
  }
18251
18485
  function buildDoctorJsonPayload(input) {
18252
18486
  return {
@@ -18757,18 +18991,21 @@ function renderPluginUpdateReportStaleOnly(report) {
18757
18991
  const released = v.released;
18758
18992
  if (!released) return [];
18759
18993
  const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
18760
- const recipeLines = [];
18761
- if (isStale(v.cli)) recipeLines.push(` npm CLI: ${report.recipes.cli.join(" ; ")}`);
18762
- if (isStale(v.claudePlugin)) recipeLines.push(` Claude: ${report.recipes.claude.join(" ; ")}`);
18763
- if (isStale(v.codexMarketplace) || isStale(v.codexActiveCache)) {
18764
- recipeLines.push(` Codex: ${report.recipes.codex.join(" ; ")}`);
18994
+ const blocks = [];
18995
+ for (const surface of PLUGIN_GUIDE_SURFACES) {
18996
+ if (!surface.versionKeys.some((k) => isStale(v[k]))) continue;
18997
+ blocks.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
18765
18998
  }
18766
- if (isStale(v.opencodePlugin)) recipeLines.push(` OpenCode: ${report.recipes.opencode.join(" ; ")}`);
18767
- if (!recipeLines.length) return [];
18768
- return ["Update recipes (stale surfaces):", ...recipeLines];
18999
+ if (!blocks.length) return [];
19000
+ return ["Update commands (stale surfaces):", ...blocks];
18769
19001
  }
19002
+ var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
18770
19003
  function renderTerseDoctorReport(input) {
18771
- if (!input.gaps.length) return [];
19004
+ const cliVersion = input.updateReport.versions.cli;
19005
+ const versionSuffix = cliVersion ? ` (mmi-cli ${cliVersion})` : "";
19006
+ if (!input.gaps.length) {
19007
+ return [`\u2713 MMI doctor: all checks passed${versionSuffix}.`, DOCTOR_VERBOSE_HINT];
19008
+ }
18772
19009
  const lines = [];
18773
19010
  for (const c of input.gaps) {
18774
19011
  lines.push(`\u2717 ${c.label}`);
@@ -18779,20 +19016,51 @@ function renderTerseDoctorReport(input) {
18779
19016
  lines.push("");
18780
19017
  lines.push(...stale);
18781
19018
  }
19019
+ lines.push("");
19020
+ lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
18782
19021
  return lines;
18783
19022
  }
19023
+ var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
19024
+ function buildPluginResolvabilityCheck(input) {
19025
+ const { state } = buildPluginGuardDecision(input);
19026
+ const surface = input.surface ?? "shell";
19027
+ switch (state) {
19028
+ case "not-org":
19029
+ return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
19030
+ case "healthy":
19031
+ return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
19032
+ case "no-install":
19033
+ return {
19034
+ ok: false,
19035
+ label: PLUGIN_RESOLVABILITY_LABEL,
19036
+ fix: `run: ${PLUGIN_SURFACE_HEAL[surfaceToken(surface) ?? "claude"]?.recovery ?? PLUGIN_SURFACE_HEAL.claude.recovery}`,
19037
+ state
19038
+ };
19039
+ case "unresolved":
19040
+ return {
19041
+ ok: false,
19042
+ label: PLUGIN_RESOLVABILITY_LABEL,
19043
+ fix: "run: mmi-cli plugin-heal",
19044
+ state
19045
+ };
19046
+ default: {
19047
+ const _exhaustive = state;
19048
+ return _exhaustive;
19049
+ }
19050
+ }
19051
+ }
18784
19052
 
18785
19053
  // src/kb-drift-report.ts
18786
- var import_node_fs23 = require("node:fs");
18787
- var import_node_path21 = require("node:path");
19054
+ var import_node_fs24 = require("node:fs");
19055
+ var import_node_path22 = require("node:path");
18788
19056
  function yesterdayIso() {
18789
19057
  const d = /* @__PURE__ */ new Date();
18790
19058
  d.setUTCDate(d.getUTCDate() - 1);
18791
19059
  return d.toISOString().slice(0, 10);
18792
19060
  }
18793
19061
  async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
18794
- const sagaIo = (0, import_node_path21.join)(repoRoot, "infra", "saga-io.mjs");
18795
- if (!(0, import_node_fs23.existsSync)(sagaIo)) return null;
19062
+ const sagaIo = (0, import_node_path22.join)(repoRoot, "infra", "saga-io.mjs");
19063
+ if (!(0, import_node_fs24.existsSync)(sagaIo)) return null;
18796
19064
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
18797
19065
  for (const date of [today, yesterdayIso()]) {
18798
19066
  try {
@@ -18808,9 +19076,9 @@ async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
18808
19076
  }
18809
19077
 
18810
19078
  // src/cli-doctor-shared.ts
18811
- var import_node_fs24 = require("node:fs");
18812
- var import_node_path22 = require("node:path");
18813
19079
  var import_node_fs25 = require("node:fs");
19080
+ var import_node_path23 = require("node:path");
19081
+ var import_node_fs26 = require("node:fs");
18814
19082
  var GC_GH_TIMEOUT_MS = 2e4;
18815
19083
  async function awsCallerArn() {
18816
19084
  try {
@@ -18856,7 +19124,7 @@ async function localBranchHeads() {
18856
19124
  }
18857
19125
  async function currentRepoWorktreeGitRoot(repoRoot) {
18858
19126
  const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
18859
- return gitCommonDir ? (0, import_node_path22.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
19127
+ return gitCommonDir ? (0, import_node_path23.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
18860
19128
  }
18861
19129
  async function worktreeBranches() {
18862
19130
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -18876,18 +19144,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
18876
19144
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
18877
19145
  if (!match?.[1]) return void 0;
18878
19146
  const raw = match[1].trim();
18879
- return (0, import_node_path22.isAbsolute)(raw) ? raw : (0, import_node_path22.resolve)(worktreePath, raw);
19147
+ return (0, import_node_path23.isAbsolute)(raw) ? raw : (0, import_node_path23.resolve)(worktreePath, raw);
18880
19148
  }
18881
19149
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
18882
19150
  if (!worktreeGitRoot) return false;
18883
19151
  try {
18884
- const entries = (0, import_node_fs25.readdirSync)(worktreeGitRoot, { withFileTypes: true });
19152
+ const entries = (0, import_node_fs26.readdirSync)(worktreeGitRoot, { withFileTypes: true });
18885
19153
  for (const ent of entries) {
18886
19154
  if (!ent.isDirectory()) continue;
18887
19155
  try {
18888
- const gitdirPath = (0, import_node_fs24.readFileSync)((0, import_node_path22.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
18889
- const resolvedGitdir = (0, import_node_path22.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path22.resolve)(worktreeGitRoot, ent.name, gitdirPath);
18890
- if (sameWorktreeMetadataPath((0, import_node_path22.dirname)(resolvedGitdir), worktreePath)) return true;
19156
+ const gitdirPath = (0, import_node_fs25.readFileSync)((0, import_node_path23.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
19157
+ const resolvedGitdir = (0, import_node_path23.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path23.resolve)(worktreeGitRoot, ent.name, gitdirPath);
19158
+ if (sameWorktreeMetadataPath((0, import_node_path23.dirname)(resolvedGitdir), worktreePath)) return true;
18891
19159
  } catch {
18892
19160
  }
18893
19161
  }
@@ -18897,7 +19165,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
18897
19165
  }
18898
19166
  function pathExistsKnown(path2) {
18899
19167
  try {
18900
- (0, import_node_fs25.statSync)(path2);
19168
+ (0, import_node_fs26.statSync)(path2);
18901
19169
  return true;
18902
19170
  } catch (e) {
18903
19171
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
@@ -18906,10 +19174,10 @@ function pathExistsKnown(path2) {
18906
19174
  }
18907
19175
  }
18908
19176
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
18909
- const gitPath = (0, import_node_path22.join)(path2, ".git");
19177
+ const gitPath = (0, import_node_path23.join)(path2, ".git");
18910
19178
  let st;
18911
19179
  try {
18912
- st = (0, import_node_fs25.lstatSync)(gitPath);
19180
+ st = (0, import_node_fs26.lstatSync)(gitPath);
18913
19181
  } catch (e) {
18914
19182
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
18915
19183
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -18926,7 +19194,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
18926
19194
  if (st.isDirectory()) return { path: path2, gitType: "dir" };
18927
19195
  if (!st.isFile()) return { path: path2, gitType: "other" };
18928
19196
  try {
18929
- const gitFileContent = (0, import_node_fs24.readFileSync)(gitPath, "utf8");
19197
+ const gitFileContent = (0, import_node_fs25.readFileSync)(gitPath, "utf8");
18930
19198
  const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
18931
19199
  const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
18932
19200
  return {
@@ -18943,7 +19211,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
18943
19211
  }
18944
19212
  function inspectDeadWorktreeDirContent(path2) {
18945
19213
  try {
18946
- return { entries: (0, import_node_fs25.readdirSync)(path2) };
19214
+ return { entries: (0, import_node_fs26.readdirSync)(path2) };
18947
19215
  } catch (e) {
18948
19216
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
18949
19217
  return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
@@ -18962,8 +19230,8 @@ async function siblingWorktreeDirs() {
18962
19230
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
18963
19231
  const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
18964
19232
  try {
18965
- const entries = (0, import_node_fs25.readdirSync)(siblingRoot, { withFileTypes: true });
18966
- return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path22.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
19233
+ const entries = (0, import_node_fs26.readdirSync)(siblingRoot, { withFileTypes: true });
19234
+ return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path23.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
18967
19235
  } catch {
18968
19236
  return [];
18969
19237
  }
@@ -19013,7 +19281,7 @@ async function fetchHubVersionInfo(baseUrl) {
19013
19281
  }
19014
19282
  function readRepoVersion() {
19015
19283
  try {
19016
- return JSON.parse((0, import_node_fs26.readFileSync)((0, import_node_path23.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
19284
+ return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
19017
19285
  } catch {
19018
19286
  return void 0;
19019
19287
  }
@@ -19149,20 +19417,30 @@ async function applyPluginHeal(token, surface, log, opts) {
19149
19417
  }
19150
19418
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
19151
19419
  const homeDir = surface === "codex" ? ".codex" : ".claude";
19152
- return (0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
19420
+ return (0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
19153
19421
  };
19154
- function readInstalledPlugins() {
19422
+ function readInstalledPlugins(surface = detectSurface(process.env)) {
19155
19423
  try {
19156
- return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(), "utf8"));
19424
+ return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath(surface), "utf8"));
19157
19425
  } catch {
19158
19426
  return null;
19159
19427
  }
19160
19428
  }
19429
+ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
19430
+ const homeDir = surface === "codex" ? ".codex" : ".claude";
19431
+ const installed = readInstalledPlugins(surface);
19432
+ return {
19433
+ isOrgRepo,
19434
+ installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
19435
+ marketplaceClonePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
19436
+ pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
19437
+ };
19438
+ }
19161
19439
  function installedPluginSources() {
19162
19440
  return ["claude", "codex"].map((surface) => {
19163
- const recordPath = (0, import_node_path23.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19441
+ const recordPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19164
19442
  try {
19165
- return { surface, installed: JSON.parse((0, import_node_fs26.readFileSync)(recordPath, "utf8")), recordPath };
19443
+ return { surface, installed: JSON.parse((0, import_node_fs27.readFileSync)(recordPath, "utf8")), recordPath };
19166
19444
  } catch {
19167
19445
  return { surface, installed: null, recordPath };
19168
19446
  }
@@ -19170,7 +19448,7 @@ function installedPluginSources() {
19170
19448
  }
19171
19449
  function readClaudeSettings() {
19172
19450
  try {
19173
- return JSON.parse((0, import_node_fs26.readFileSync)((0, import_node_path23.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19451
+ return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19174
19452
  } catch {
19175
19453
  return null;
19176
19454
  }
@@ -19192,7 +19470,7 @@ function writeProjectInstallRecord(record) {
19192
19470
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
19193
19471
  list.push(record);
19194
19472
  file.plugins[MMI_PLUGIN_ID] = list;
19195
- (0, import_node_fs26.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19473
+ (0, import_node_fs27.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19196
19474
  `, "utf8");
19197
19475
  return true;
19198
19476
  } catch {
@@ -19205,9 +19483,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19205
19483
  if (!file) return false;
19206
19484
  if (!file.plugins) file.plugins = {};
19207
19485
  const path2 = installedPluginsPath();
19208
- (0, import_node_fs26.copyFileSync)(path2, `${path2}.bak`);
19486
+ (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
19209
19487
  file.plugins[pluginId] = records;
19210
- (0, import_node_fs26.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19488
+ (0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19211
19489
  `, "utf8");
19212
19490
  return true;
19213
19491
  } catch {
@@ -19215,22 +19493,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19215
19493
  }
19216
19494
  }
19217
19495
  function opencodeConfigDir() {
19218
- return (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".config", "opencode");
19496
+ return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "opencode");
19219
19497
  }
19220
19498
  function opencodeConfigPath() {
19221
- return (0, import_node_path23.join)(opencodeConfigDir(), "opencode.jsonc");
19499
+ return (0, import_node_path24.join)(opencodeConfigDir(), "opencode.jsonc");
19222
19500
  }
19223
19501
  function opencodeCommandsDir() {
19224
- return (0, import_node_path23.join)(opencodeConfigDir(), "commands");
19502
+ return (0, import_node_path24.join)(opencodeConfigDir(), "commands");
19225
19503
  }
19226
19504
  function opencodeSkillsPath() {
19227
- return (0, import_node_path23.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
19505
+ return (0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
19228
19506
  }
19229
19507
  function opencodeConfigSnapshot() {
19230
19508
  const path2 = opencodeConfigPath();
19231
- if (!(0, import_node_fs26.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
19509
+ if (!(0, import_node_fs27.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
19232
19510
  try {
19233
- const raw = (0, import_node_fs26.readFileSync)(path2, "utf8");
19511
+ const raw = (0, import_node_fs27.readFileSync)(path2, "utf8");
19234
19512
  const parsed = JSON.parse(stripJsonc(raw));
19235
19513
  const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
19236
19514
  const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
@@ -19253,9 +19531,9 @@ function writeOpencodeConfigPlugin(snapshot) {
19253
19531
  const plan2 = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
19254
19532
  if (plan2.action === "already") return true;
19255
19533
  if (!plan2.text || plan2.action === "unsafe") return false;
19256
- (0, import_node_fs26.mkdirSync)((0, import_node_path23.dirname)(path2), { recursive: true });
19257
- if (snapshot.hasConfig) (0, import_node_fs26.copyFileSync)(path2, `${path2}.bak`);
19258
- (0, import_node_fs26.writeFileSync)(path2, plan2.text, "utf8");
19534
+ (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
19535
+ if (snapshot.hasConfig) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
19536
+ (0, import_node_fs27.writeFileSync)(path2, plan2.text, "utf8");
19259
19537
  return true;
19260
19538
  } catch {
19261
19539
  return false;
@@ -19270,9 +19548,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
19270
19548
  const normalized = skillsPath.replace(/\\/g, "/");
19271
19549
  if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
19272
19550
  parsed.skills = { ...skills, paths };
19273
- (0, import_node_fs26.mkdirSync)((0, import_node_path23.dirname)(snapshot.path), { recursive: true });
19274
- if (snapshot.hasConfig && (0, import_node_fs26.existsSync)(snapshot.path)) (0, import_node_fs26.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
19275
- (0, import_node_fs26.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
19551
+ (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(snapshot.path), { recursive: true });
19552
+ if (snapshot.hasConfig && (0, import_node_fs27.existsSync)(snapshot.path)) (0, import_node_fs27.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
19553
+ (0, import_node_fs27.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
19276
19554
  `, "utf8");
19277
19555
  return true;
19278
19556
  } catch {
@@ -19281,7 +19559,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
19281
19559
  }
19282
19560
  function opencodeExistingCommands() {
19283
19561
  try {
19284
- return (0, import_node_fs26.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
19562
+ return (0, import_node_fs27.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
19285
19563
  } catch {
19286
19564
  return [];
19287
19565
  }
@@ -19289,9 +19567,9 @@ function opencodeExistingCommands() {
19289
19567
  function writeOpencodeCommandFiles() {
19290
19568
  try {
19291
19569
  const dir = opencodeCommandsDir();
19292
- (0, import_node_fs26.mkdirSync)(dir, { recursive: true });
19570
+ (0, import_node_fs27.mkdirSync)(dir, { recursive: true });
19293
19571
  for (const command of OPENCODE_WORKFLOW_COMMANDS) {
19294
- (0, import_node_fs26.writeFileSync)((0, import_node_path23.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
19572
+ (0, import_node_fs27.writeFileSync)((0, import_node_path24.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
19295
19573
  }
19296
19574
  return true;
19297
19575
  } catch {
@@ -19300,12 +19578,12 @@ function writeOpencodeCommandFiles() {
19300
19578
  }
19301
19579
  function readOpencodeAdapterDiskVersion() {
19302
19580
  const candidates = [
19303
- (0, import_node_path23.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
19304
- (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
19581
+ (0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
19582
+ (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
19305
19583
  ];
19306
19584
  for (const path2 of candidates) {
19307
19585
  try {
19308
- const parsed = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
19586
+ const parsed = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
19309
19587
  if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
19310
19588
  } catch {
19311
19589
  continue;
@@ -19321,7 +19599,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
19321
19599
  try {
19322
19600
  const specs = opencodeMmiPluginSpecs(snapshot);
19323
19601
  log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
19324
- (0, import_node_fs26.mkdirSync)(opencodeConfigDir(), { recursive: true });
19602
+ (0, import_node_fs27.mkdirSync)(opencodeConfigDir(), { recursive: true });
19325
19603
  await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
19326
19604
  return true;
19327
19605
  } catch {
@@ -19336,30 +19614,30 @@ function opencodePluginVersionsForReport() {
19336
19614
  }
19337
19615
  function opencodeDesktopLogsRoot() {
19338
19616
  if (process.platform === "win32") {
19339
- const base = process.env.APPDATA || (0, import_node_path23.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
19340
- return (0, import_node_path23.join)(base, "ai.opencode.desktop", "logs");
19617
+ const base = process.env.APPDATA || (0, import_node_path24.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
19618
+ return (0, import_node_path24.join)(base, "ai.opencode.desktop", "logs");
19341
19619
  }
19342
19620
  if (process.platform === "darwin") {
19343
- return (0, import_node_path23.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
19621
+ return (0, import_node_path24.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
19344
19622
  }
19345
- return (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
19623
+ return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
19346
19624
  }
19347
19625
  function opencodeDesktopBootstrapSnapshot() {
19348
19626
  const root = opencodeDesktopLogsRoot();
19349
19627
  try {
19350
- const sessionDirs = (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path23.join)(root, entry.name)).sort((a, b) => (0, import_node_fs26.statSync)(b).mtimeMs - (0, import_node_fs26.statSync)(a).mtimeMs);
19628
+ const sessionDirs = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path24.join)(root, entry.name)).sort((a, b) => (0, import_node_fs27.statSync)(b).mtimeMs - (0, import_node_fs27.statSync)(a).mtimeMs);
19351
19629
  const newest = sessionDirs[0];
19352
19630
  if (!newest) return [];
19353
- const logPath = (0, import_node_path23.join)(newest, "renderer.log");
19354
- const text = (0, import_node_fs26.readFileSync)(logPath, "utf8");
19355
- return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs26.existsSync)(directory)).map((directory) => ({ directory, logPath }));
19631
+ const logPath = (0, import_node_path24.join)(newest, "renderer.log");
19632
+ const text = (0, import_node_fs27.readFileSync)(logPath, "utf8");
19633
+ return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs27.existsSync)(directory)).map((directory) => ({ directory, logPath }));
19356
19634
  } catch {
19357
19635
  return [];
19358
19636
  }
19359
19637
  }
19360
19638
  function opencodeLegacyConfigSnapshot() {
19361
- const legacyPath = (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
19362
- if (!(0, import_node_fs26.existsSync)(legacyPath)) return {};
19639
+ const legacyPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
19640
+ if (!(0, import_node_fs27.existsSync)(legacyPath)) return {};
19363
19641
  const content = readTextFile(legacyPath);
19364
19642
  if (content == null) return {};
19365
19643
  const plugins = parseOpencodeLegacyConfigPlugins(content);
@@ -19371,43 +19649,43 @@ function opencodeLegacyConfigSnapshot() {
19371
19649
  function quarantineOpencodeLegacyConfig(legacyPath) {
19372
19650
  try {
19373
19651
  const backupPath = `${legacyPath}.bak`;
19374
- if ((0, import_node_fs26.existsSync)(backupPath)) return false;
19375
- (0, import_node_fs26.renameSync)(legacyPath, backupPath);
19652
+ if ((0, import_node_fs27.existsSync)(backupPath)) return false;
19653
+ (0, import_node_fs27.renameSync)(legacyPath, backupPath);
19376
19654
  return true;
19377
19655
  } catch {
19378
19656
  return false;
19379
19657
  }
19380
19658
  }
19381
19659
  function cursorPluginCacheRoot() {
19382
- return (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
19660
+ return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
19383
19661
  }
19384
19662
  function cursorPluginCachePinSnapshots() {
19385
19663
  const root = cursorPluginCacheRoot();
19386
19664
  try {
19387
- return (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19388
- const path2 = (0, import_node_path23.join)(root, entry.name);
19389
- const pluginJson = (0, import_node_path23.join)(path2, ".cursor-plugin", "plugin.json");
19390
- const hooksJson = (0, import_node_path23.join)(path2, "hooks", "hooks.json");
19391
- const cliBundle = (0, import_node_path23.join)(path2, "cli", "dist", "index.cjs");
19665
+ return (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19666
+ const path2 = (0, import_node_path24.join)(root, entry.name);
19667
+ const pluginJson = (0, import_node_path24.join)(path2, ".cursor-plugin", "plugin.json");
19668
+ const hooksJson = (0, import_node_path24.join)(path2, "hooks", "hooks.json");
19669
+ const cliBundle = (0, import_node_path24.join)(path2, "cli", "dist", "index.cjs");
19392
19670
  let version;
19393
19671
  try {
19394
- const raw = JSON.parse((0, import_node_fs26.readFileSync)(pluginJson, "utf8"));
19672
+ const raw = JSON.parse((0, import_node_fs27.readFileSync)(pluginJson, "utf8"));
19395
19673
  version = typeof raw.version === "string" ? raw.version : void 0;
19396
19674
  } catch {
19397
19675
  version = void 0;
19398
19676
  }
19399
19677
  let isEmpty = true;
19400
19678
  try {
19401
- isEmpty = (0, import_node_fs26.readdirSync)(path2).length === 0;
19679
+ isEmpty = (0, import_node_fs27.readdirSync)(path2).length === 0;
19402
19680
  } catch {
19403
19681
  isEmpty = true;
19404
19682
  }
19405
19683
  return {
19406
19684
  name: entry.name,
19407
19685
  path: path2,
19408
- hasPluginJson: (0, import_node_fs26.existsSync)(pluginJson),
19409
- hasHooksJson: (0, import_node_fs26.existsSync)(hooksJson),
19410
- hasCliBundle: (0, import_node_fs26.existsSync)(cliBundle),
19686
+ hasPluginJson: (0, import_node_fs27.existsSync)(pluginJson),
19687
+ hasHooksJson: (0, import_node_fs27.existsSync)(hooksJson),
19688
+ hasCliBundle: (0, import_node_fs27.existsSync)(cliBundle),
19411
19689
  isEmpty,
19412
19690
  version
19413
19691
  };
@@ -19417,19 +19695,19 @@ function cursorPluginCachePinSnapshots() {
19417
19695
  }
19418
19696
  }
19419
19697
  function hubCheckoutForCursorSeed() {
19420
- const manifest = (0, import_node_path23.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19421
- return (0, import_node_fs26.existsSync)(manifest) ? process.cwd() : void 0;
19698
+ const manifest = (0, import_node_path24.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19699
+ return (0, import_node_fs27.existsSync)(manifest) ? process.cwd() : void 0;
19422
19700
  }
19423
19701
  function mmiPluginCacheRootSnapshots() {
19424
19702
  const roots = [
19425
- { surface: "claude", root: (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19426
- { surface: "codex", root: (0, import_node_path23.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
19703
+ { surface: "claude", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19704
+ { surface: "codex", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
19427
19705
  ];
19428
19706
  return roots.flatMap(({ surface, root }) => {
19429
19707
  try {
19430
- const entries = (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19708
+ const entries = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19431
19709
  name: entry.name,
19432
- path: (0, import_node_path23.join)(root, entry.name),
19710
+ path: (0, import_node_path24.join)(root, entry.name),
19433
19711
  isDirectory: entry.isDirectory()
19434
19712
  }));
19435
19713
  return [{ surface, root, entries }];
@@ -19440,7 +19718,7 @@ function mmiPluginCacheRootSnapshots() {
19440
19718
  }
19441
19719
  function hasNestedMmiChild(versionDir) {
19442
19720
  try {
19443
- return (0, import_node_fs26.statSync)((0, import_node_path23.join)(versionDir, "mmi")).isDirectory();
19721
+ return (0, import_node_fs27.statSync)((0, import_node_path24.join)(versionDir, "mmi")).isDirectory();
19444
19722
  } catch {
19445
19723
  return false;
19446
19724
  }
@@ -19451,10 +19729,10 @@ function nestedPluginTreeSnapshot() {
19451
19729
  );
19452
19730
  }
19453
19731
  function uniqueQuarantineTarget(path2) {
19454
- if (!(0, import_node_fs26.existsSync)(path2)) return path2;
19732
+ if (!(0, import_node_fs27.existsSync)(path2)) return path2;
19455
19733
  for (let i = 1; i < 100; i += 1) {
19456
19734
  const candidate = `${path2}-${i}`;
19457
- if (!(0, import_node_fs26.existsSync)(candidate)) return candidate;
19735
+ if (!(0, import_node_fs27.existsSync)(candidate)) return candidate;
19458
19736
  }
19459
19737
  return `${path2}-${Date.now()}`;
19460
19738
  }
@@ -19463,10 +19741,10 @@ function quarantinePluginCacheDirs(plan2) {
19463
19741
  const failed = [];
19464
19742
  for (const move of plan2) {
19465
19743
  try {
19466
- if (!(0, import_node_fs26.existsSync)(move.from)) continue;
19744
+ if (!(0, import_node_fs27.existsSync)(move.from)) continue;
19467
19745
  const target = uniqueQuarantineTarget(move.to);
19468
- (0, import_node_fs26.mkdirSync)((0, import_node_path23.dirname)(target), { recursive: true });
19469
- (0, import_node_fs26.renameSync)(move.from, target);
19746
+ (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(target), { recursive: true });
19747
+ (0, import_node_fs27.renameSync)(move.from, target);
19470
19748
  moved += 1;
19471
19749
  } catch {
19472
19750
  failed.push(move);
@@ -19485,23 +19763,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
19485
19763
  }
19486
19764
  async function clearNestedPluginTreeDir(targetPath) {
19487
19765
  try {
19488
- if (!(0, import_node_fs26.existsSync)(targetPath)) return true;
19766
+ if (!(0, import_node_fs27.existsSync)(targetPath)) return true;
19489
19767
  if (isWin) {
19490
- const emptyDir = (0, import_node_path23.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
19491
- (0, import_node_fs26.mkdirSync)(emptyDir, { recursive: true });
19768
+ const emptyDir = (0, import_node_path24.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
19769
+ (0, import_node_fs27.mkdirSync)(emptyDir, { recursive: true });
19492
19770
  try {
19493
19771
  await robocopyMirrorEmpty(emptyDir, targetPath);
19494
- (0, import_node_fs26.rmSync)(targetPath, { recursive: true, force: true });
19772
+ (0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
19495
19773
  } finally {
19496
19774
  try {
19497
- (0, import_node_fs26.rmSync)(emptyDir, { recursive: true, force: true });
19775
+ (0, import_node_fs27.rmSync)(emptyDir, { recursive: true, force: true });
19498
19776
  } catch {
19499
19777
  }
19500
19778
  }
19501
- return !(0, import_node_fs26.existsSync)(targetPath);
19779
+ return !(0, import_node_fs27.existsSync)(targetPath);
19502
19780
  }
19503
- (0, import_node_fs26.rmSync)(targetPath, { recursive: true, force: true });
19504
- return !(0, import_node_fs26.existsSync)(targetPath);
19781
+ (0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
19782
+ return !(0, import_node_fs27.existsSync)(targetPath);
19505
19783
  } catch {
19506
19784
  return false;
19507
19785
  }
@@ -19514,11 +19792,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
19514
19792
  }
19515
19793
  return true;
19516
19794
  }
19517
- var gitignorePath = () => (0, import_node_path23.join)(process.cwd(), ".gitignore");
19795
+ var gitignorePath = () => (0, import_node_path24.join)(process.cwd(), ".gitignore");
19518
19796
  function readTextFile(path2) {
19519
19797
  try {
19520
- if (!(0, import_node_fs26.existsSync)(path2)) return null;
19521
- return (0, import_node_fs26.readFileSync)(path2, "utf8");
19798
+ if (!(0, import_node_fs27.existsSync)(path2)) return null;
19799
+ return (0, import_node_fs27.readFileSync)(path2, "utf8");
19522
19800
  } catch {
19523
19801
  return null;
19524
19802
  }
@@ -19527,10 +19805,10 @@ function playwrightMcpConfigSnapshots() {
19527
19805
  const cwd = process.cwd();
19528
19806
  const home = (0, import_node_os5.homedir)();
19529
19807
  const candidates = [
19530
- (0, import_node_path23.join)(cwd, ".mcp.json"),
19531
- (0, import_node_path23.join)(cwd, ".cursor", "mcp.json"),
19532
- (0, import_node_path23.join)(home, ".cursor", "mcp.json"),
19533
- (0, import_node_path23.join)(home, ".codex", "config.toml")
19808
+ (0, import_node_path24.join)(cwd, ".mcp.json"),
19809
+ (0, import_node_path24.join)(cwd, ".cursor", "mcp.json"),
19810
+ (0, import_node_path24.join)(home, ".cursor", "mcp.json"),
19811
+ (0, import_node_path24.join)(home, ".codex", "config.toml")
19534
19812
  ];
19535
19813
  const out = [];
19536
19814
  for (const path2 of candidates) {
@@ -19543,7 +19821,7 @@ function strayBrowserArtifactPaths() {
19543
19821
  const cwd = process.cwd();
19544
19822
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
19545
19823
  try {
19546
- return (0, import_node_fs26.existsSync)((0, import_node_path23.join)(cwd, rel));
19824
+ return (0, import_node_fs27.existsSync)((0, import_node_path24.join)(cwd, rel));
19547
19825
  } catch {
19548
19826
  return false;
19549
19827
  }
@@ -19562,8 +19840,8 @@ function latestIso(values) {
19562
19840
  return best;
19563
19841
  }
19564
19842
  function latestNorthstarContinuityAt() {
19565
- const meta = parseMeta(readTextFile((0, import_node_path23.join)(process.cwd(), META_FILE)));
19566
- const queue = parseQueue(readTextFile((0, import_node_path23.join)(process.cwd(), QUEUE_FILE)));
19843
+ const meta = parseMeta(readTextFile((0, import_node_path24.join)(process.cwd(), META_FILE)));
19844
+ const queue = parseQueue(readTextFile((0, import_node_path24.join)(process.cwd(), QUEUE_FILE)));
19567
19845
  return latestIso([
19568
19846
  ...Object.values(meta).map((entry) => entry.syncedAt),
19569
19847
  ...queue.map((entry) => entry.queuedAt)
@@ -19577,14 +19855,14 @@ async function latestBranchWorkAt() {
19577
19855
  }
19578
19856
  function readGitignore() {
19579
19857
  try {
19580
- return (0, import_node_fs26.readFileSync)(gitignorePath(), "utf8");
19858
+ return (0, import_node_fs27.readFileSync)(gitignorePath(), "utf8");
19581
19859
  } catch {
19582
19860
  return null;
19583
19861
  }
19584
19862
  }
19585
19863
  function writeGitignore(content) {
19586
19864
  try {
19587
- (0, import_node_fs26.writeFileSync)(gitignorePath(), content, "utf8");
19865
+ (0, import_node_fs27.writeFileSync)(gitignorePath(), content, "utf8");
19588
19866
  return true;
19589
19867
  } catch {
19590
19868
  return false;
@@ -19623,7 +19901,7 @@ async function runDoctor(opts, io = consoleIo) {
19623
19901
  const semverPrefix = /^\d+\.\d+\.\d+/;
19624
19902
  const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
19625
19903
  const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
19626
- const cursorCacheStale = (0, import_node_fs26.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
19904
+ const cursorCacheStale = (0, import_node_fs27.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
19627
19905
  const healPlan = doctorHealPlan({
19628
19906
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19629
19907
  surface,
@@ -19658,7 +19936,7 @@ async function runDoctor(opts, io = consoleIo) {
19658
19936
  let onPath = pathProbe;
19659
19937
  if (!onPath) {
19660
19938
  const root = process.env.CLAUDE_PLUGIN_ROOT;
19661
- if (root && (0, import_node_fs26.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19939
+ if (root && (0, import_node_fs27.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19662
19940
  }
19663
19941
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
19664
19942
  const reloadHint = reloadAction(surface);
@@ -19734,7 +20012,6 @@ async function runDoctor(opts, io = consoleIo) {
19734
20012
  }
19735
20013
  }
19736
20014
  checks.push(pluginCheck);
19737
- checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
19738
20015
  let legacyPluginCheck = buildLegacyPluginInstallCheck({
19739
20016
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19740
20017
  sources: installedPluginSources(),
@@ -19791,6 +20068,18 @@ async function runDoctor(opts, io = consoleIo) {
19791
20068
  }
19792
20069
  }
19793
20070
  checks.push(driftCheck);
20071
+ checks.push(
20072
+ buildPluginResolvabilityCheck({
20073
+ ...snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl)),
20074
+ surface
20075
+ })
20076
+ );
20077
+ if (repairFull && Boolean(cfg.sagaApiUrl) && (surfaceToken(surface) === "claude" || surfaceToken(surface) === "codex")) {
20078
+ const guardResult = ensureUserScopeGuardHook();
20079
+ if (guardResult === "written") {
20080
+ io.err(` \u21BB installed user-scope MMI guard hook (${surface === "codex" ? "~/.codex" : "~/.claude"}/settings.json) \u2014 survives a plugin prune`);
20081
+ }
20082
+ }
19794
20083
  let installedVersionCheck = buildInstalledPluginVersionCheck({
19795
20084
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19796
20085
  sources: installedPluginSources(),
@@ -19985,7 +20274,7 @@ async function runDoctor(opts, io = consoleIo) {
19985
20274
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19986
20275
  surface,
19987
20276
  cacheRoot: cursorCacheRoot,
19988
- cacheRootExists: (0, import_node_fs26.existsSync)(cursorCacheRoot),
20277
+ cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
19989
20278
  pins: cursorPins,
19990
20279
  hubCheckout: hubCheckoutForCursorSeed(),
19991
20280
  releasedVersion
@@ -19996,7 +20285,7 @@ async function runDoctor(opts, io = consoleIo) {
19996
20285
  releasedVersion,
19997
20286
  hubCheckout: hubCheckoutForCursorSeed(),
19998
20287
  execFileP: execFileP2,
19999
- mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path23.join)((0, import_node_os5.tmpdir)(), prefix)),
20288
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path24.join)((0, import_node_os5.tmpdir)(), prefix)),
20000
20289
  log: (m) => io.err(m)
20001
20290
  });
20002
20291
  if (seeded) {
@@ -20005,7 +20294,7 @@ async function runDoctor(opts, io = consoleIo) {
20005
20294
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20006
20295
  surface,
20007
20296
  cacheRoot: cursorCacheRoot,
20008
- cacheRootExists: (0, import_node_fs26.existsSync)(cursorCacheRoot),
20297
+ cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
20009
20298
  pins: cursorPins,
20010
20299
  hubCheckout: hubCheckoutForCursorSeed(),
20011
20300
  releasedVersion
@@ -20175,6 +20464,79 @@ async function runDoctor(opts, io = consoleIo) {
20175
20464
  io.log(gaps.length ? `
20176
20465
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
20177
20466
  }
20467
+ var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
20468
+ var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
20469
+ function settingsHasGuardHook(settings) {
20470
+ const groups = settings?.hooks?.SessionStart;
20471
+ if (!Array.isArray(groups)) return false;
20472
+ return groups.some(
20473
+ (g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(USER_SCOPE_GUARD_MARKER))
20474
+ );
20475
+ }
20476
+ function mergeGuardHook(settings) {
20477
+ const next = settings && typeof settings === "object" ? { ...settings } : {};
20478
+ if (settingsHasGuardHook(next)) return next;
20479
+ const hooks = { ...next.hooks ?? {} };
20480
+ const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
20481
+ sessionStart.push({ hooks: [{ type: "command", command: USER_SCOPE_GUARD_COMMAND, timeout: 10 }] });
20482
+ hooks.SessionStart = sessionStart;
20483
+ next.hooks = hooks;
20484
+ return next;
20485
+ }
20486
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path24.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
20487
+ function ensureUserScopeGuardHook(opts = {}) {
20488
+ const path2 = opts.settingsPath ?? userScopeSettingsPath();
20489
+ try {
20490
+ let current = null;
20491
+ if ((0, import_node_fs27.existsSync)(path2)) {
20492
+ try {
20493
+ current = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
20494
+ } catch {
20495
+ return "failed";
20496
+ }
20497
+ }
20498
+ if (settingsHasGuardHook(current)) return "already";
20499
+ const merged = mergeGuardHook(current);
20500
+ (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
20501
+ if ((0, import_node_fs27.existsSync)(path2)) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
20502
+ (0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
20503
+ `, "utf8");
20504
+ return "written";
20505
+ } catch {
20506
+ return "failed";
20507
+ }
20508
+ }
20509
+ async function runGuard(opts = {}) {
20510
+ void opts;
20511
+ try {
20512
+ const surface = detectSurface(process.env);
20513
+ const cfg = await loadConfig();
20514
+ const input = snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl));
20515
+ const { state } = buildPluginGuardDecision(input);
20516
+ const { line, exitCode } = buildGuardSessionStartLine(state);
20517
+ if (line) console.error(line);
20518
+ process.exitCode = exitCode;
20519
+ } catch {
20520
+ process.exitCode = 0;
20521
+ }
20522
+ }
20523
+ async function runPluginHeal(surface = detectSurface(process.env)) {
20524
+ const token = surface === "codex" ? "codex" : "claude";
20525
+ const tableKey = surfaceToken(surface) ?? "claude";
20526
+ const descriptor = PLUGIN_SURFACE_HEAL[tableKey];
20527
+ if (!descriptor.healSteps) {
20528
+ console.log(descriptor.recovery);
20529
+ console.log(`Then: ${reloadAction(surface)}`);
20530
+ return;
20531
+ }
20532
+ const healed = await applyPluginHeal(token, surface, console.log, { force: true });
20533
+ if (healed) {
20534
+ console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
20535
+ } else {
20536
+ console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
20537
+ ${descriptor.recovery}`);
20538
+ }
20539
+ }
20178
20540
 
20179
20541
  // src/index.ts
20180
20542
  var GC_GH_TIMEOUT_MS2 = 2e4;
@@ -20267,7 +20629,7 @@ async function applyGcPlan(plan2, remote) {
20267
20629
  cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
20268
20630
  beforeWorktrees,
20269
20631
  startingPath: branch.worktreePath,
20270
- pathExists: (p) => (0, import_node_fs27.existsSync)(p),
20632
+ pathExists: (p) => (0, import_node_fs28.existsSync)(p),
20271
20633
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20272
20634
  teardownWorktreeStage,
20273
20635
  deferredStore,
@@ -20290,7 +20652,7 @@ async function applyGcPlan(plan2, remote) {
20290
20652
  for (const wt of plan2.worktreeDirs) {
20291
20653
  try {
20292
20654
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
20293
- realpath: (path2) => (0, import_node_fs27.realpathSync)(path2)
20655
+ realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
20294
20656
  });
20295
20657
  if (!cleanupTarget.ok) {
20296
20658
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -20370,10 +20732,10 @@ async function runRulesSync(opts, io = consoleIo) {
20370
20732
  for (const entry of fetched) {
20371
20733
  if ("error" in entry) continue;
20372
20734
  const { file, source } = entry;
20373
- const current = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
20735
+ const current = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
20374
20736
  if (needsUpdate(source, current)) {
20375
20737
  const slash = file.lastIndexOf("/");
20376
- if (slash > 0) (0, import_node_fs27.mkdirSync)(file.slice(0, slash), { recursive: true });
20738
+ if (slash > 0) (0, import_node_fs28.mkdirSync)(file.slice(0, slash), { recursive: true });
20377
20739
  await (0, import_promises8.writeFile)(file, normalizeEol(source), "utf8");
20378
20740
  changed++;
20379
20741
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
@@ -20387,13 +20749,13 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
20387
20749
  if (!await runRulesSync(opts)) process.exitCode = 1;
20388
20750
  });
20389
20751
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
20390
- const path2 = (0, import_node_path24.join)(process.cwd(), ".gitignore");
20391
- const current = (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null;
20752
+ const path2 = (0, import_node_path25.join)(process.cwd(), ".gitignore");
20753
+ const current = (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
20392
20754
  const plan2 = planManagedGitignore(current);
20393
20755
  const drift = [...plan2.added.map((l) => `+${l}`), ...plan2.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
20394
20756
  if (opts.write) {
20395
20757
  if (plan2.changed) {
20396
- (0, import_node_fs27.writeFileSync)(path2, plan2.content, "utf8");
20758
+ (0, import_node_fs28.writeFileSync)(path2, plan2.content, "utf8");
20397
20759
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
20398
20760
  } else {
20399
20761
  console.log("mmi-cli rules gitignore: up to date");
@@ -20420,7 +20782,7 @@ async function runDocsSync(opts, io = consoleIo) {
20420
20782
  return null;
20421
20783
  }
20422
20784
  },
20423
- localContent: async (f) => (0, import_node_fs27.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
20785
+ localContent: async (f) => (0, import_node_fs28.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
20424
20786
  writeDoc: async (f, c) => {
20425
20787
  await (0, import_promises8.writeFile)(f, c, "utf8");
20426
20788
  }
@@ -20571,7 +20933,7 @@ function runWorktreeInstall(command, cwd, quiet) {
20571
20933
  const [bin, ...args] = command.split(" ");
20572
20934
  const file = isWin2 ? "cmd.exe" : bin;
20573
20935
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
20574
- return new Promise((resolve5, reject) => {
20936
+ return new Promise((resolve6, reject) => {
20575
20937
  const child = (0, import_node_child_process13.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
20576
20938
  const timer = setTimeout(() => {
20577
20939
  try {
@@ -20586,7 +20948,7 @@ function runWorktreeInstall(command, cwd, quiet) {
20586
20948
  });
20587
20949
  child.on("exit", (code) => {
20588
20950
  clearTimeout(timer);
20589
- if (code === 0) resolve5();
20951
+ if (code === 0) resolve6();
20590
20952
  else reject(new Error(`${command} exited ${code} in ${cwd}`));
20591
20953
  });
20592
20954
  });
@@ -20594,7 +20956,7 @@ function runWorktreeInstall(command, cwd, quiet) {
20594
20956
  async function primaryCheckoutRoot(worktreeRoot) {
20595
20957
  try {
20596
20958
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
20597
- return out ? (0, import_node_path24.dirname)(out) : void 0;
20959
+ return out ? (0, import_node_path25.dirname)(out) : void 0;
20598
20960
  } catch {
20599
20961
  return void 0;
20600
20962
  }
@@ -20607,28 +20969,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
20607
20969
  };
20608
20970
  }
20609
20971
  function acquireWorktreeSetupLock(worktreeRoot) {
20610
- const lockPath = (0, import_node_path24.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
20972
+ const lockPath = (0, import_node_path25.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
20611
20973
  const take = () => {
20612
- const fd = (0, import_node_fs27.openSync)(lockPath, "wx");
20974
+ const fd = (0, import_node_fs28.openSync)(lockPath, "wx");
20613
20975
  try {
20614
- (0, import_node_fs27.writeSync)(fd, String(Date.now()));
20976
+ (0, import_node_fs28.writeSync)(fd, String(Date.now()));
20615
20977
  } finally {
20616
- (0, import_node_fs27.closeSync)(fd);
20978
+ (0, import_node_fs28.closeSync)(fd);
20617
20979
  }
20618
20980
  return () => {
20619
20981
  try {
20620
- (0, import_node_fs27.rmSync)(lockPath, { force: true });
20982
+ (0, import_node_fs28.rmSync)(lockPath, { force: true });
20621
20983
  } catch {
20622
20984
  }
20623
20985
  };
20624
20986
  };
20625
20987
  try {
20626
- (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(lockPath), { recursive: true });
20988
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(lockPath), { recursive: true });
20627
20989
  return take();
20628
20990
  } catch {
20629
20991
  try {
20630
- if (Date.now() - (0, import_node_fs27.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
20631
- (0, import_node_fs27.rmSync)(lockPath, { force: true });
20992
+ if (Date.now() - (0, import_node_fs28.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
20993
+ (0, import_node_fs28.rmSync)(lockPath, { force: true });
20632
20994
  return take();
20633
20995
  }
20634
20996
  } catch {
@@ -20637,18 +20999,21 @@ function acquireWorktreeSetupLock(worktreeRoot) {
20637
20999
  }
20638
21000
  }
20639
21001
  var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
20640
- worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch the base from", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
21002
+ worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
20641
21003
  try {
20642
21004
  const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20643
21005
  const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
20644
- const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
20645
- if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
20646
- await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
21006
+ const { base, fetchBranch } = resolveWorktreeBase(o.from, o.remote);
21007
+ if (fetchBranch) {
21008
+ const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
21009
+ if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
21010
+ }
21011
+ await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
20647
21012
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
20648
21013
  if (!o.json) console.error(` ${m}`);
20649
21014
  }));
20650
- if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
20651
- console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
21015
+ if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base, ...report }, null, 2));
21016
+ console.log(`worktree ready: ${wtPath} (branch ${branch} from ${base})`);
20652
21017
  console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
20653
21018
  console.log(` copied: ${report.copied.join(", ") || "none"}`);
20654
21019
  } catch (e) {
@@ -20873,8 +21238,8 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
20873
21238
  async function resolveDnsBounded(host, timeoutMs = 3e3) {
20874
21239
  const { lookup } = await import("node:dns/promises");
20875
21240
  const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
20876
- const timeout = new Promise((resolve5) => {
20877
- setTimeout(() => resolve5(void 0), timeoutMs).unref?.();
21241
+ const timeout = new Promise((resolve6) => {
21242
+ setTimeout(() => resolve6(void 0), timeoutMs).unref?.();
20878
21243
  });
20879
21244
  return Promise.race([probe, timeout]);
20880
21245
  }
@@ -21629,9 +21994,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
21629
21994
  console.log(JSON.stringify(created));
21630
21995
  });
21631
21996
  async function listCiWorkflowPaths(cwd = process.cwd()) {
21632
- const wfDir = (0, import_node_path24.join)(cwd, ".github", "workflows");
21633
- if (!(0, import_node_fs27.existsSync)(wfDir)) return [];
21634
- return (0, import_node_fs27.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
21997
+ const wfDir = (0, import_node_path25.join)(cwd, ".github", "workflows");
21998
+ if (!(0, import_node_fs28.existsSync)(wfDir)) return [];
21999
+ return (0, import_node_fs28.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
21635
22000
  }
21636
22001
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
21637
22002
  const repo = repoOpt ?? await resolveRepo();
@@ -21650,7 +22015,7 @@ function ciAuditDeps() {
21650
22015
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
21651
22016
  // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
21652
22017
  // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
21653
- readSeedFile: (path2) => (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null
22018
+ readSeedFile: (path2) => (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null
21654
22019
  };
21655
22020
  }
21656
22021
  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) => {
@@ -21680,7 +22045,7 @@ pr.command("checks-wait <number>").description("bounded wait for PR checks; skip
21680
22045
  const result = await waitForPrChecks({
21681
22046
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
21682
22047
  pollChecks: () => pollGhPrChecks(number, repoArgs),
21683
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
22048
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
21684
22049
  });
21685
22050
  if (o.json) printLine(JSON.stringify(result));
21686
22051
  else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
@@ -21707,7 +22072,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
21707
22072
  waitForChecks: (prNumber, repo) => waitForPrChecks({
21708
22073
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
21709
22074
  pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
21710
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
22075
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
21711
22076
  }),
21712
22077
  mergeAuto: async (prNumber, repo) => {
21713
22078
  const args = repo ? ["--repo", repo] : [];
@@ -21743,7 +22108,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
21743
22108
  while (Date.now() < deadlineMs) {
21744
22109
  const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout, { retries: 2, delayMs: 1e3 });
21745
22110
  if (stateRead.ok && stateRead.state === "MERGED") return true;
21746
- await new Promise((resolve5) => setTimeout(resolve5, PR_LAND_POLL_MS));
22111
+ await new Promise((resolve6) => setTimeout(resolve6, PR_LAND_POLL_MS));
21747
22112
  }
21748
22113
  return false;
21749
22114
  }
@@ -21803,7 +22168,7 @@ async function createDeferredWorktreeStore() {
21803
22168
  },
21804
22169
  write: async (entries) => {
21805
22170
  try {
21806
- await (0, import_promises8.mkdir)((0, import_node_path24.dirname)(registryPath), { recursive: true });
22171
+ await (0, import_promises8.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
21807
22172
  await (0, import_promises8.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
21808
22173
  } catch {
21809
22174
  }
@@ -21817,13 +22182,13 @@ var realWorktreeDirRemover = {
21817
22182
  probe: (p) => {
21818
22183
  let st;
21819
22184
  try {
21820
- st = (0, import_node_fs27.lstatSync)(p);
22185
+ st = (0, import_node_fs28.lstatSync)(p);
21821
22186
  } catch {
21822
22187
  return null;
21823
22188
  }
21824
22189
  if (st.isSymbolicLink()) return "link";
21825
22190
  try {
21826
- (0, import_node_fs27.readlinkSync)(p);
22191
+ (0, import_node_fs28.readlinkSync)(p);
21827
22192
  return "link";
21828
22193
  } catch {
21829
22194
  }
@@ -21831,7 +22196,7 @@ var realWorktreeDirRemover = {
21831
22196
  },
21832
22197
  readdir: (p) => {
21833
22198
  try {
21834
- return (0, import_node_fs27.readdirSync)(p);
22199
+ return (0, import_node_fs28.readdirSync)(p);
21835
22200
  } catch {
21836
22201
  return [];
21837
22202
  }
@@ -21840,9 +22205,9 @@ var realWorktreeDirRemover = {
21840
22205
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
21841
22206
  detachLink: (p) => {
21842
22207
  try {
21843
- (0, import_node_fs27.rmdirSync)(p);
22208
+ (0, import_node_fs28.rmdirSync)(p);
21844
22209
  } catch {
21845
- (0, import_node_fs27.unlinkSync)(p);
22210
+ (0, import_node_fs28.unlinkSync)(p);
21846
22211
  }
21847
22212
  },
21848
22213
  removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -21857,7 +22222,7 @@ async function resolvePrimaryCheckout(execGit) {
21857
22222
  function worktreeRemoveDeps(execGit) {
21858
22223
  return {
21859
22224
  git: execGit,
21860
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
22225
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
21861
22226
  removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
21862
22227
  };
21863
22228
  }
@@ -21872,9 +22237,9 @@ async function worktreeHasStageState(worktreePath) {
21872
22237
  }
21873
22238
  }
21874
22239
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
21875
- if (!(0, import_node_fs27.existsSync)(statePath)) return false;
22240
+ if (!(0, import_node_fs28.existsSync)(statePath)) return false;
21876
22241
  try {
21877
- const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
22242
+ const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
21878
22243
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
21879
22244
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
21880
22245
  } catch {
@@ -21947,7 +22312,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21947
22312
  } : await cleanupPrMergeLocalBranch(headRef, {
21948
22313
  beforeWorktrees,
21949
22314
  startingPath,
21950
- pathExists: (p) => (0, import_node_fs27.existsSync)(p),
22315
+ pathExists: (p) => (0, import_node_fs28.existsSync)(p),
21951
22316
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
21952
22317
  teardownWorktreeStage,
21953
22318
  deferredStore,
@@ -22144,7 +22509,7 @@ function stageScopedRunOpts(o) {
22144
22509
  };
22145
22510
  }
22146
22511
  function printLine(value) {
22147
- (0, import_node_fs27.writeSync)(1, `${value}
22512
+ (0, import_node_fs28.writeSync)(1, `${value}
22148
22513
  `);
22149
22514
  }
22150
22515
  function stageKeepAlive() {
@@ -22161,8 +22526,8 @@ async function resolveStage() {
22161
22526
  local,
22162
22527
  shell: shellFor(),
22163
22528
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
22164
- hasCompose: (0, import_node_fs27.existsSync)((0, import_node_path24.join)(process.cwd(), "docker-compose.yml")),
22165
- hasEnvExample: (0, import_node_fs27.existsSync)((0, import_node_path24.join)(process.cwd(), ".env.example"))
22529
+ hasCompose: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), "docker-compose.yml")),
22530
+ hasEnvExample: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), ".env.example"))
22166
22531
  });
22167
22532
  }
22168
22533
  async function fetchStageVaultEnvMerge() {
@@ -22261,9 +22626,17 @@ async function runStageLiveCommand(o) {
22261
22626
  if (o.json) return console.log(JSON.stringify({ command: "stage --live", mode, slug: target.slug, repo: target.repo, ref: o.down ? void 0 : target.ref, steps }, null, 2));
22262
22627
  return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
22263
22628
  }
22629
+ const rcDeps = registryClientDeps(await loadConfig());
22264
22630
  const deps = {
22265
22631
  detectIp: () => detectPublicIp(),
22266
- run: async (file, args) => (await execFileP2(file, args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout
22632
+ deployDev: async ({ repo, ref }) => {
22633
+ const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
22634
+ if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
22635
+ },
22636
+ control: async ({ repo, action, host, ip }) => {
22637
+ const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
22638
+ if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
22639
+ }
22267
22640
  };
22268
22641
  try {
22269
22642
  const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
@@ -22616,7 +22989,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
22616
22989
  client: defaultGitHubClient(),
22617
22990
  projectMeta: meta,
22618
22991
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
22619
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null,
22992
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
22620
22993
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
22621
22994
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
22622
22995
  requiredGcpApis: (() => {
@@ -22660,12 +23033,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
22660
23033
  return fail(`bootstrap apply: ${e.message}`);
22661
23034
  }
22662
23035
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
22663
- if (!(0, import_node_fs27.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
22664
- const manifest = loadBootstrapSeeds((0, import_node_fs27.readFileSync)(manifestPath, "utf8"));
23036
+ if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
23037
+ const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
22665
23038
  const baseBranch = o.class === "content" ? "main" : "development";
22666
23039
  const slug = parsedRepo.slug;
22667
23040
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
22668
- const readFile7 = (p) => (0, import_node_fs27.existsSync)(p) ? (0, import_node_fs27.readFileSync)(p, "utf8") : null;
23041
+ const readFile7 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
22669
23042
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22670
23043
  const rawVars = {};
22671
23044
  for (const value of rawValues("--var")) {
@@ -22916,16 +23289,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
22916
23289
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
22917
23290
  targets = [{ repo: o.repo, class: o.class }];
22918
23291
  } else {
22919
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs27.existsSync)("projects.json") ? (0, import_node_fs27.readFileSync)("projects.json", "utf8") : null;
23292
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs28.existsSync)("projects.json") ? (0, import_node_fs28.readFileSync)("projects.json", "utf8") : null;
22920
23293
  if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
22921
- const fanoutJson = (0, import_node_fs27.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs27.readFileSync)(".github/fanout-targets.json", "utf8") : null;
23294
+ const fanoutJson = (0, import_node_fs28.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs28.readFileSync)(".github/fanout-targets.json", "utf8") : null;
22922
23295
  targets = loadAccessTargets(projectsJson, fanoutJson);
22923
23296
  }
22924
23297
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
22925
- const fileMatrix = (0, import_node_fs27.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs27.readFileSync)("access-matrix.json", "utf8")) : {};
23298
+ const fileMatrix = (0, import_node_fs28.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")) : {};
22926
23299
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
22927
23300
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
22928
- const fileContracts = (0, import_node_fs27.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs27.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
23301
+ const fileContracts = (0, import_node_fs28.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs28.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
22929
23302
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
22930
23303
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
22931
23304
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -23012,6 +23385,8 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
23012
23385
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
23013
23386
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
23014
23387
  ));
23388
+ program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
23389
+ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
23015
23390
  program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
23016
23391
  if (isInsideRepoSubdir(process.cwd())) {
23017
23392
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");