@mutmutco/cli 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +753 -118
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4789,7 +4789,7 @@ mutation($projectId: ID!, $itemId: ID!) {
4789
4789
  async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
4790
4790
  await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
4791
4791
  }
4792
- function resolveBoardConfig(cfg) {
4792
+ function missingBoardConfigFields(cfg) {
4793
4793
  const missing = [];
4794
4794
  if (!cfg.projectOwner) missing.push("projectOwner");
4795
4795
  if (!cfg.projectNumber) missing.push("projectNumber");
@@ -4799,9 +4799,20 @@ function resolveBoardConfig(cfg) {
4799
4799
  for (const status of BOARD_STATUSES) {
4800
4800
  if (!cfg.statusOptions?.[status]) missing.push(`statusOptions.${status}`);
4801
4801
  }
4802
- if (missing.length) {
4803
- throw new Error(
4804
- `Hub registry board META missing ${missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
4802
+ return missing;
4803
+ }
4804
+ function diagnoseBoardConfigGap(cfg) {
4805
+ const missing = missingBoardConfigFields(cfg);
4806
+ if (!missing.length) return null;
4807
+ return cfg.projectId ? { kind: "registry-meta-missing", missing } : { kind: "no-project-detected" };
4808
+ }
4809
+ function resolveBoardConfig(cfg) {
4810
+ const gap = diagnoseBoardConfigGap(cfg);
4811
+ if (gap) {
4812
+ throw gap.kind === "no-project-detected" ? new Error(
4813
+ "no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli project get <owner/repo>` to check registration"
4814
+ ) : new Error(
4815
+ `Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
4805
4816
  );
4806
4817
  }
4807
4818
  return {
@@ -4864,6 +4875,11 @@ function detailCandidates(report) {
4864
4875
  ...report.secondary.claimable
4865
4876
  ].filter((item) => item.contentType === "Issue");
4866
4877
  }
4878
+ function boardNotFoundError(ref, board2, opts = {}) {
4879
+ const verb = opts.verb ?? "is not on";
4880
+ const remedy = opts.remedy ?? "if it lives on a different board, pass --repo <owner/repo> for the repo that owns it";
4881
+ return new Error(`${ref} ${verb} the ${board2.owner} #${board2.number} board; ${remedy}`);
4882
+ }
4867
4883
  function findClaimableItem(report, selector) {
4868
4884
  const candidates = [...report.primary.claimable, ...report.secondary.claimable];
4869
4885
  const found = candidates.find((item) => item.repository.toLowerCase() === selector.repo.toLowerCase() && item.number === selector.number);
@@ -4882,7 +4898,10 @@ function findClaimableItem(report, selector) {
4882
4898
  if (existing.assignees.length) throw new Error(`${existing.ref} is already assigned to @${existing.assignees.join(", @")}`);
4883
4899
  throw new Error(`${existing.ref} is not claimable`);
4884
4900
  }
4885
- throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
4901
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, report.project, {
4902
+ verb: "is not a claimable item on",
4903
+ remedy: "it may already be Done (excluded from claim tracking), or it lives on a different board \u2014 pass --repo <owner/repo> for the repo that owns it"
4904
+ });
4886
4905
  }
4887
4906
  function renderBoardItem(item) {
4888
4907
  const assignees = item.assignees.length ? `@${item.assignees.join(", @")}` : "unassigned";
@@ -5022,11 +5041,11 @@ async function readBoard(options, deps = {}) {
5022
5041
  }
5023
5042
  return report;
5024
5043
  }
5025
- function findBoardItem(items, selector) {
5044
+ function findBoardItem(items, selector, board2) {
5026
5045
  const found = items.find(
5027
5046
  (candidate) => candidate.repository.toLowerCase() === selector.repo.toLowerCase() && candidate.number === selector.number
5028
5047
  );
5029
- if (!found) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
5048
+ if (!found) throw boardNotFoundError(`${selector.repo}#${selector.number}`, board2);
5030
5049
  return found;
5031
5050
  }
5032
5051
  async function resolveCurrentRepo(options, deps) {
@@ -5045,7 +5064,9 @@ async function moveBoardItem(options, deps = {}) {
5045
5064
  const selector = parseIssueSelector(options.selector, currentRepo);
5046
5065
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
5047
5066
  const item = lookup.item;
5048
- if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
5067
+ if (!item) {
5068
+ throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
5069
+ }
5049
5070
  const optionId = cfg.statusOptions[options.status];
5050
5071
  try {
5051
5072
  await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
@@ -5091,7 +5112,7 @@ async function showBoardItem(options, deps = {}) {
5091
5112
  const currentRepo = await resolveCurrentRepo(options, deps);
5092
5113
  const selector = parseIssueSelector(options.selector, currentRepo);
5093
5114
  const { item } = await fetchIssueProjectItem(client, cfg, selector);
5094
- if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
5115
+ if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
5095
5116
  if (item.contentType === "Issue") {
5096
5117
  try {
5097
5118
  item.details = await fetchIssueDetails(client, item.repository, item.number);
@@ -5104,9 +5125,10 @@ async function showBoardItem(options, deps = {}) {
5104
5125
  async function prepareClaimContext(options, selectors, deps, collected) {
5105
5126
  const cfg = resolveBoardConfig(options.config);
5106
5127
  const client = deps.client ?? defaultGitHubClient();
5128
+ const board2 = { owner: cfg.projectOwner, number: cfg.projectNumber };
5107
5129
  for (const selector of selectors) {
5108
5130
  try {
5109
- findBoardItem(collected.items, selector);
5131
+ findBoardItem(collected.items, selector, board2);
5110
5132
  } catch {
5111
5133
  const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
5112
5134
  if (fallback) collected.items.push(fallback);
@@ -5132,7 +5154,7 @@ async function prepareClaimContext(options, selectors, deps, collected) {
5132
5154
  }
5133
5155
  async function claimOneBoardItem(ctx, selector, options) {
5134
5156
  const { cfg, client, report } = ctx;
5135
- const flatItem = findBoardItem(ctx.items, selector);
5157
+ const flatItem = findBoardItem(ctx.items, selector, { owner: cfg.projectOwner, number: cfg.projectNumber });
5136
5158
  if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
5137
5159
  throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
5138
5160
  }
@@ -10423,25 +10445,113 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
10423
10445
  }
10424
10446
  return { foldPaths, tolerated, predicted };
10425
10447
  }
10426
- async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
10448
+ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
10427
10449
  await deps.run("git", ["checkout", "main"]);
10428
10450
  await ffOnlyPull(deps, "main");
10451
+ preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
10429
10452
  if (predicted.length === 0) {
10430
10453
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
10431
10454
  } else {
10432
10455
  await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
10433
10456
  }
10434
10457
  }
10435
- async function mergeSourceToMain(deps, deployModel, args) {
10436
- const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
10437
- deps,
10438
- deployModel,
10439
- args.remoteRef,
10440
- args.blockingPrefix,
10441
- args.realignMessage
10458
+ async function probeFoldFailureState(deps) {
10459
+ const branch = await currentBranch(deps);
10460
+ const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
10461
+ const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
10462
+ const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean).catch(() => "");
10463
+ const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean).catch(() => "");
10464
+ const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
10465
+ return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
10466
+ }
10467
+ function shaLabel(sha) {
10468
+ return sha ? sha.slice(0, 7) : "(unknown)";
10469
+ }
10470
+ async function finishFoldAutoRestore(deps, causeMessage, startBranch, what) {
10471
+ try {
10472
+ await deps.run("git", ["checkout", startBranch]);
10473
+ } catch (e) {
10474
+ return new Error(
10475
+ `${causeMessage}
10476
+
10477
+ fold failed; ${what}, but returning to ${startBranch} afterwards failed: ${e instanceof Error ? e.message : String(e)}. Finish manually: git checkout ${startBranch}`
10478
+ );
10479
+ }
10480
+ return new Error(
10481
+ `${causeMessage}
10482
+
10483
+ fold failed; ${what} and the checkout was returned to ${startBranch}. Nothing was pushed to origin and no tag was created \u2014 rerun the release once the fold issue above is fixed.`
10442
10484
  );
10443
- await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
10444
- return { foldPaths };
10485
+ }
10486
+ function foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha) {
10487
+ const steps = preFoldMainSha ? [
10488
+ ` 1. git status # inspect what is actually there`,
10489
+ ` 2. git log --oneline ${preFoldMainSha}..main # commits beyond the pre-fold main \u2014 confirm they are only this run's merge/fold attempt`,
10490
+ ` 3. git checkout main && git reset --hard ${preFoldMainSha} # back to the exact commit the fold built on`,
10491
+ ` 4. git checkout ${startBranch}`
10492
+ ] : [
10493
+ ` 1. git status # inspect what is actually there`,
10494
+ ` 2. git log --oneline origin/main..main # this run never committed to main (it failed before the merge) \u2014 any commits here predate this run; preserve them`,
10495
+ ` 3. git checkout ${startBranch}`
10496
+ ];
10497
+ return new Error(
10498
+ `${causeMessage}
10499
+
10500
+ fold failed leaving the checkout in a state train --apply will not auto-restore (branch=${probe.branch || "(unknown)"}, dirty-working-tree=${probe.dirty}, main=${shaLabel(probe.mainSha)}, origin/main=${shaLabel(probe.originMainSha)}, pre-fold main=${preFoldMainSha ? shaLabel(preFoldMainSha) : "not captured"}, origin/main is${probe.originIsAncestorOfMain ? "" : " NOT"} an ancestor of main). Nothing was pushed to origin and no tag was created.
10501
+ Recovery sequence:
10502
+ ${steps.join("\n")}`
10503
+ );
10504
+ }
10505
+ async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha) {
10506
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
10507
+ const probe = await probeFoldFailureState(deps);
10508
+ if (probe.mergeInProgress) {
10509
+ try {
10510
+ await deps.run("git", ["merge", "--abort"]);
10511
+ } catch (e) {
10512
+ return new Error(
10513
+ `${causeMessage}
10514
+
10515
+ fold failed with an in-progress merge left on ${probe.branch || "(unknown branch)"}; automatic \`git merge --abort\` failed: ${e instanceof Error ? e.message : String(e)}. Nothing was pushed to origin and no tag was created.
10516
+ Recovery sequence:
10517
+ 1. git merge --abort
10518
+ 2. git checkout ${startBranch}`
10519
+ );
10520
+ }
10521
+ return finishFoldAutoRestore(deps, causeMessage, startBranch, "the in-progress merge was aborted");
10522
+ }
10523
+ if (probe.branch === "main" && preFoldMainSha) {
10524
+ const mainDescendsFromPreFold = await deps.run("git", ["merge-base", "--is-ancestor", preFoldMainSha, "main"]).then(() => true).catch(() => false);
10525
+ if (mainDescendsFromPreFold) {
10526
+ try {
10527
+ await deps.run("git", ["reset", "--hard", preFoldMainSha]);
10528
+ } catch (e) {
10529
+ return new Error(
10530
+ `${causeMessage}
10531
+
10532
+ fold failed after this run's merge/fold committed locally on main (now ${shaLabel(probe.mainSha)}, unpushed, built on the pre-fold main ${shaLabel(preFoldMainSha)}); automatic \`git reset --hard ${shaLabel(preFoldMainSha)}\` failed: ${e instanceof Error ? e.message : String(e)}. Nothing was pushed to origin and no tag was created.
10533
+ Recovery sequence:
10534
+ 1. git reset --hard ${preFoldMainSha}
10535
+ 2. git checkout ${startBranch}`
10536
+ );
10537
+ }
10538
+ const aheadNote = probe.originMainSha && preFoldMainSha !== probe.originMainSha ? `; local main carried commit(s) origin/main (${shaLabel(probe.originMainSha)}) does not have \u2014 they were preserved` : "";
10539
+ return finishFoldAutoRestore(
10540
+ deps,
10541
+ causeMessage,
10542
+ startBranch,
10543
+ `local main was reset to its pre-fold state ${shaLabel(preFoldMainSha)} (discarding only this run's merge/fold commit(s) at ${shaLabel(probe.mainSha)})${aheadNote}`
10544
+ );
10545
+ }
10546
+ }
10547
+ return foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha);
10548
+ }
10549
+ async function runFoldStage(deps, startBranch, preFold, fn) {
10550
+ try {
10551
+ return await fn();
10552
+ } catch (e) {
10553
+ throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
10554
+ }
10445
10555
  }
10446
10556
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
10447
10557
  await ensureTagPushed(deps, tag, releaseSha);
@@ -10534,15 +10644,20 @@ async function runTrainApplyPipeline(mode, input) {
10534
10644
  const deployModel2 = await preflight(deps, ctx, "main", meta);
10535
10645
  const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
10536
10646
  const rcShaAtRelease = !directTrack && hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
10537
- const { foldPaths: foldPaths2 } = await mergeSourceToMain(deps, deployModel2, {
10538
- sourceRef: "development",
10539
- remoteRef: "origin/development",
10540
- mergeLabel: "development -> main",
10541
- blockingPrefix: "development -> main merge would conflict on untolerated path(s)",
10542
- realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
10647
+ const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
10648
+ deps,
10649
+ deployModel2,
10650
+ "origin/development",
10651
+ "development -> main merge would conflict on untolerated path(s)",
10652
+ "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
10653
+ );
10654
+ const preFold2 = {};
10655
+ const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
10656
+ await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
10657
+ const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10658
+ const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10659
+ return { versionFold: versionFold3, releaseSha: releaseSha3 };
10543
10660
  });
10544
- const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10545
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10546
10661
  const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
10547
10662
  const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
10548
10663
  if (directTrack) {
@@ -10619,10 +10734,14 @@ async function runTrainApplyPipeline(mode, input) {
10619
10734
  );
10620
10735
  }
10621
10736
  const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
10622
- await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted);
10623
- const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10624
- const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
10625
- const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10737
+ const preFold = {};
10738
+ const { tag, versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
10739
+ await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
10740
+ const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10741
+ const versionFold2 = await foldReleaseVersion(deps, deployModel, tag2, foldPaths);
10742
+ const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10743
+ return { tag: tag2, versionFold: versionFold2, releaseSha: releaseSha2 };
10744
+ });
10626
10745
  const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
10627
10746
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
10628
10747
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
@@ -14806,16 +14925,24 @@ function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
14806
14925
  return false;
14807
14926
  });
14808
14927
  }
14928
+ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRootExists = false) {
14929
+ const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
14930
+ if (needing.length > 0) return needing.map((pin) => pin.path);
14931
+ if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion(releasedVersion)) {
14932
+ return [(0, import_node_path15.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
14933
+ }
14934
+ return [];
14935
+ }
14809
14936
  async function applyCursorPluginCacheSeed(input) {
14810
14937
  if (!isSemverVersion(input.releasedVersion)) return false;
14811
- const pinsToSeed = cursorPluginPinsNeedingSeed(input.pins, input.releasedVersion);
14812
- if (pinsToSeed.length === 0) return false;
14938
+ const targets = cursorPluginCacheSeedTargets(input.pins, input.releasedVersion, input.cacheRoot, input.cacheRootExists);
14939
+ if (targets.length === 0) return false;
14813
14940
  const tmpRoot = await input.mkdtemp("mmi-cursor-seed-");
14814
14941
  const source = await resolvePluginMmiSource(input.releasedVersion, input.hubCheckout, tmpRoot, input.execFileP);
14815
14942
  if (!source) return false;
14816
14943
  input.log(` \u21BB seeding Cursor MMI plugin cache \u2192 ${input.releasedVersion}\u2026`);
14817
- for (const pin of pinsToSeed) {
14818
- syncDirContents(source, pin.path);
14944
+ for (const dest of targets) {
14945
+ syncDirContents(source, dest);
14819
14946
  }
14820
14947
  (0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
14821
14948
  return true;
@@ -15095,6 +15222,33 @@ function buildMmiPluginCacheCleanupCheck(input) {
15095
15222
  }))
15096
15223
  };
15097
15224
  }
15225
+ var CURSOR_PLUGIN_CACHE_CLEANUP_LABEL = "leftover older MMI plugin cache pins (Cursor)";
15226
+ var CURSOR_PLUGIN_CACHE_CLEANUP_FIX = "older Cursor MMI plugin cache pins can be removed manually after confirming in Cursor Settings \u2192 Plugins \u2192 Marketplace which pin is registered";
15227
+ function buildCursorPluginCacheCleanupCheck(input) {
15228
+ const base = {
15229
+ ok: true,
15230
+ label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL,
15231
+ fix: CURSOR_PLUGIN_CACHE_CLEANUP_FIX
15232
+ };
15233
+ if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
15234
+ const semverPins = input.pins.filter((pin) => isSemverVersion2(pin.version));
15235
+ if (semverPins.length === 0) return base;
15236
+ const activeVersion = semverPins.map((pin) => pin.version).reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
15237
+ const protectedVersions = new Set(
15238
+ [normalizeVersion(input.releasedVersion), normalizeVersion(activeVersion)].filter((v) => Boolean(v))
15239
+ );
15240
+ const leftoverPins = semverPins.filter((pin) => !protectedVersions.has(normalizeVersion(pin.version)));
15241
+ if (leftoverPins.length === 0) return base;
15242
+ const leftovers = leftoverPins.map((pin) => ({ surface: "cursor", root: input.cacheRoot, name: pin.name, path: pin.path }));
15243
+ const listed = leftoverPins.map((pin) => `${pin.name} (${pin.version})`).join(", ");
15244
+ return {
15245
+ ...base,
15246
+ ok: false,
15247
+ severityOverride: "advisory",
15248
+ leftovers,
15249
+ fix: `${leftoverPins.length} older Cursor MMI plugin cache pin(s) left behind by marketplace re-clones: ${listed}. Not auto-removed: which pin Cursor's Team Marketplace registration is bound to cannot be read programmatically (no per-user CLI, #2409), and the registration may still point at an older pin \u2014 after confirming in Cursor Settings \u2192 Plugins \u2192 Marketplace that the newer pin is the registered one, the older dir(s) can be removed manually`
15250
+ };
15251
+ }
15098
15252
  var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
15099
15253
  var NESTED_PLUGIN_TREE_FIX = "SessionStart and mmi-cli doctor auto-clear when possible; if this persists, run the MAX_PATH-safe robocopy cleanup for a self-nested MMI plugin cache tree (#1126)";
15100
15254
  function nestedPluginTreeCleanupCommand(paths, isWindows) {
@@ -15117,7 +15271,21 @@ function buildNestedPluginTreeCheck(input) {
15117
15271
  fix: `${nested.length} self-nested MMI plugin cache tree(s) (#1126) exceed MAX_PATH and can't self-clean \u2014 run: ${nestedPluginTreeCleanupCommand(nested.map((n) => n.path), input.isWindows)}`
15118
15272
  };
15119
15273
  }
15274
+ function nestedTreeReinstallGapFix(surfaces) {
15275
+ const recoveries = surfaces.map((s) => PLUGIN_SURFACE_HEAL[s].recovery).join(" and ");
15276
+ return `self-nested MMI plugin cache tree(s) cleared, but the plugin was NOT reinstalled for ${surfaces.join(" + ")} (host CLI unavailable or reinstall failed) \u2014 run: ${recoveries}`;
15277
+ }
15120
15278
  var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
15279
+ function codexEnabledPluginVersionFromList(jsonText, pluginId = MMI_PLUGIN_ID) {
15280
+ try {
15281
+ const parsed = JSON.parse(jsonText);
15282
+ const rows = Array.isArray(parsed?.installed) ? parsed.installed : [];
15283
+ const row = rows.find((r) => r?.pluginId === pluginId && r.installed === true && r.enabled === true);
15284
+ return isSemverVersion2(row?.version) ? row.version.trim().replace(/^v/, "") : void 0;
15285
+ } catch {
15286
+ return void 0;
15287
+ }
15288
+ }
15121
15289
  function buildCodexActiveCacheCheck(input) {
15122
15290
  const base = {
15123
15291
  ok: true,
@@ -15125,6 +15293,18 @@ function buildCodexActiveCacheCheck(input) {
15125
15293
  fix: CODEX_PLUGIN_RECOVERY
15126
15294
  };
15127
15295
  if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
15296
+ if (isSemverVersion2(input.codexActiveVersion)) {
15297
+ if (compareVersions(input.codexActiveVersion, input.releasedVersion) < 0) {
15298
+ return {
15299
+ ...base,
15300
+ ok: false,
15301
+ activeCacheVersion: input.codexActiveVersion,
15302
+ releasedVersion: input.releasedVersion,
15303
+ fix: `\`codex plugin list\` reports mmi@mutmutco enabled at ${input.codexActiveVersion} < ${input.releasedVersion} (a newer cache dir or install record may also exist and mask it) \u2014 run \`mmi-cli doctor --apply\` (forces the Codex remove/re-add when \`codex\` is on PATH; restart Codex after), or manually: ${CODEX_PLUGIN_RECOVERY}`
15304
+ };
15305
+ }
15306
+ return { ...base, activeCacheVersion: input.codexActiveVersion, releasedVersion: input.releasedVersion };
15307
+ }
15128
15308
  if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
15129
15309
  return base;
15130
15310
  }
@@ -15172,6 +15352,55 @@ function reloadAction(surface) {
15172
15352
  return "restart Claude Code (or run /reload-plugins)";
15173
15353
  }
15174
15354
  }
15355
+ function restartAction(surface) {
15356
+ switch (surface) {
15357
+ case "claude-vscode":
15358
+ return "fully restart VS Code";
15359
+ case "codex":
15360
+ return "fully restart Codex";
15361
+ case "opencode":
15362
+ return "fully restart OpenCode";
15363
+ case "cursor":
15364
+ return "fully restart Cursor";
15365
+ case "claude-cli":
15366
+ case "shell":
15367
+ default:
15368
+ return "fully restart Claude Code";
15369
+ }
15370
+ }
15371
+ function parseSessionSkillRootVersion(pluginRoot) {
15372
+ const trimmed = pluginRoot?.trim();
15373
+ if (!trimmed) return void 0;
15374
+ const segments = trimmed.split(/[\\/]+/).filter(Boolean);
15375
+ const mmiIdx = segments.findIndex((seg, i) => seg === "mmi" && segments[i - 1] === "mutmutco");
15376
+ if (mmiIdx < 0) return void 0;
15377
+ const candidate = segments[mmiIdx + 1];
15378
+ return candidate && /^v?\d+\.\d+\.\d+/.test(candidate) ? candidate.replace(/^v/, "") : void 0;
15379
+ }
15380
+ var SESSION_SKILL_ROOT_LABEL = "Session-loaded MMI skill roots (vs on-disk plugin cache)";
15381
+ function buildSessionSkillRootCheck(input) {
15382
+ const base = {
15383
+ ok: true,
15384
+ label: SESSION_SKILL_ROOT_LABEL,
15385
+ fix: `fully restart the host to load the current MMI skill roots \u2014 a running session does not hot-refresh them, and a plugin reload does not re-register them`
15386
+ };
15387
+ if (!input.isOrgRepo) return base;
15388
+ const sessionVersion = parseSessionSkillRootVersion(input.pluginRoot);
15389
+ if (!sessionVersion) return base;
15390
+ const activeCacheVersion = highestSemver(input.cacheVersions ?? []);
15391
+ if (!isSemverVersion2(activeCacheVersion)) return { ...base, sessionVersion };
15392
+ if (compareVersions(sessionVersion, activeCacheVersion) >= 0) {
15393
+ return { ...base, sessionVersion, activeCacheVersion };
15394
+ }
15395
+ return {
15396
+ ...base,
15397
+ ok: false,
15398
+ severityOverride: "advisory",
15399
+ sessionVersion,
15400
+ activeCacheVersion,
15401
+ fix: `This ${input.surface} session loaded MMI skill roots from ${sessionVersion} but the on-disk plugin cache is ${activeCacheVersion} \u2014 a running session does NOT hot-refresh skill roots and a plugin reload does not re-register them, so ${restartAction(input.surface)} to load ${activeCacheVersion}. Until then this session keeps executing the ${sessionVersion} skill bundle; halt autonomous /grind and /build first.`
15402
+ };
15403
+ }
15175
15404
  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`;
15176
15405
  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`;
15177
15406
  var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
@@ -15703,6 +15932,9 @@ function cursorPluginInstallFix(input) {
15703
15932
  const localSeed = input.hubCheckout ? `temporary fallback: copy ${joinCachePath(input.hubCheckout, "plugins", "mmi")} to ${cacheDir}, then restart Cursor` : `temporary fallback: copy plugins/mmi from a local MMI-Hub checkout to ${cacheDir}, then restart Cursor`;
15704
15933
  return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL}, ${CURSOR_HOOKS_JSON_REL}, or the cursor-hook dispatcher script \u2014 ${autoSeed}; ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
15705
15934
  }
15935
+ function cursorSeededFromNothingAdvisoryFix(releasedVersion) {
15936
+ return `Cursor MMI plugin cache seeded from nothing${releasedVersion ? ` at ${releasedVersion}` : ""} \u2014 the cache is healthy, but the Team Marketplace pin REGISTRATION could not be verified (no per-user CLI, #2409): if MMI commands do not load after restarting Cursor, a master-admin must register/refresh the MMI Team Marketplace in Cursor Settings \u2192 Plugins \u2192 Marketplace (\xA76.8); ${CURSOR_MARKETPLACE_INSTALL_GUIDE}`;
15937
+ }
15706
15938
  function buildCursorPluginInstallCheck(input) {
15707
15939
  const base = {
15708
15940
  ok: true,
@@ -15820,9 +16052,7 @@ function buildHubDeployFreshnessCheck(input) {
15820
16052
  };
15821
16053
  }
15822
16054
  var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
15823
- var PLAYWRIGHT_MCP_VISION_CAP_FIX = "remove --caps=vision (and vision-first defaults) from Playwright MCP args \u2014 use DOM-first tools; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
15824
16055
  var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
15825
- var PLAYWRIGHT_MCP_OUTPUT_DIR_FIX = "configure Playwright MCP args with --output-dir tmp/playwright-mcp; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
15826
16056
  function textHasPlaywrightMcp(content) {
15827
16057
  const normalized = content.replace(/\r\n/g, "\n");
15828
16058
  return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
@@ -15837,45 +16067,10 @@ function textHasPlaywrightVisionCap(content) {
15837
16067
  if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
15838
16068
  return false;
15839
16069
  }
15840
- function buildPlaywrightMcpVisionCapCheck(input) {
15841
- const base = {
15842
- ok: true,
15843
- label: PLAYWRIGHT_MCP_VISION_CAP_LABEL,
15844
- fix: PLAYWRIGHT_MCP_VISION_CAP_FIX
15845
- };
15846
- if (!input.isOrgRepo) return base;
15847
- const offending = input.configs.filter((c) => textHasPlaywrightVisionCap(c.content)).map((c) => c.path);
15848
- if (offending.length === 0) return base;
15849
- return {
15850
- ...base,
15851
- ok: false,
15852
- offendingPaths: offending,
15853
- fix: `${PLAYWRIGHT_MCP_VISION_CAP_FIX} \u2014 found in: ${offending.join(", ")}`
15854
- };
15855
- }
15856
16070
  function textHasCanonicalPlaywrightOutputDir(content) {
15857
16071
  const normalized = content.replace(/\r\n/g, "\n");
15858
16072
  return /--output-dir(?:\s*=\s*|["'\s,]+)tmp\/playwright-mcp\b/.test(normalized);
15859
16073
  }
15860
- function textNeedsPlaywrightOutputDir(content) {
15861
- return textHasPlaywrightMcp(content) && !textHasCanonicalPlaywrightOutputDir(content);
15862
- }
15863
- function buildPlaywrightMcpOutputDirCheck(input) {
15864
- const base = {
15865
- ok: true,
15866
- label: PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
15867
- fix: PLAYWRIGHT_MCP_OUTPUT_DIR_FIX
15868
- };
15869
- if (!input.isOrgRepo) return base;
15870
- const offending = input.configs.filter((c) => textNeedsPlaywrightOutputDir(c.content)).map((c) => c.path);
15871
- if (offending.length === 0) return base;
15872
- return {
15873
- ...base,
15874
- ok: false,
15875
- offendingPaths: offending,
15876
- fix: `${PLAYWRIGHT_MCP_OUTPUT_DIR_FIX} \u2014 missing in: ${offending.join(", ")}`
15877
- };
15878
- }
15879
16074
  var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
15880
16075
  var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
15881
16076
  var BROWSER_ARTIFACTS_FIX = "move or delete stray Playwright output at repo root; re-run MCP with --output-dir tmp/playwright-mcp \u2014 see skills/browser-automation/SKILL.md";
@@ -15915,6 +16110,12 @@ function buildSelfUpdateHaltPayload(input) {
15915
16110
  checks: input.checks
15916
16111
  };
15917
16112
  }
16113
+ var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
16114
+ var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
16115
+ function selfUpdateHaltSentinelLine(input) {
16116
+ const to = input.report.releasedVersion ?? "latest";
16117
+ return `${DOCTOR_SELF_UPDATE_HALT_SENTINEL} rerunRequired=true updatedTo=${to} rerun="mmi-cli ${input.rerunArgs.join(" ")}"`;
16118
+ }
15918
16119
  var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
15919
16120
  function buildSelfUpdateReexecArgs(opts) {
15920
16121
  const args = ["doctor"];
@@ -16102,6 +16303,262 @@ function buildPluginResolvabilityCheck(input) {
16102
16303
  }
16103
16304
  }
16104
16305
 
16306
+ // src/mcp-reconcile.ts
16307
+ var PLAYWRIGHT_MCP_SPEC = {
16308
+ name: "playwright",
16309
+ command: "npx",
16310
+ args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
16311
+ legacyNames: []
16312
+ };
16313
+ var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
16314
+ var MCP_RECONCILE_FIX = "register/reconcile the org Playwright MCP server (npx -y @playwright/mcp@latest --output-dir tmp/playwright-mcp) \u2014 run `mmi-cli doctor --apply`; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
16315
+ function extractServerBlock(content, format, spec) {
16316
+ return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
16317
+ }
16318
+ function extractJsonServer(content, spec) {
16319
+ let parsed;
16320
+ try {
16321
+ parsed = JSON.parse(content);
16322
+ } catch {
16323
+ return { kind: "unparseable" };
16324
+ }
16325
+ if (parsed == null || typeof parsed !== "object") return { kind: "unparseable" };
16326
+ const servers = parsed.mcpServers;
16327
+ if (servers == null || typeof servers !== "object") return { kind: "absent" };
16328
+ const bag = servers;
16329
+ for (const name of [spec.name, ...spec.legacyNames]) {
16330
+ if (Object.prototype.hasOwnProperty.call(bag, name)) {
16331
+ return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText: JSON.stringify(bag[name]) };
16332
+ }
16333
+ }
16334
+ return { kind: "absent" };
16335
+ }
16336
+ function extractTomlServer(content, spec) {
16337
+ const blocks = parseTomlBlocks(content.replace(/\r\n/g, "\n").split("\n"));
16338
+ for (const name of [spec.name, ...spec.legacyNames]) {
16339
+ const full = `mcp_servers.${name}`;
16340
+ const block = blocks.find((b) => b.name === full && !b.isArrayTable);
16341
+ if (block) {
16342
+ const blockText = [block.headerRaw, ...block.body].filter((l) => l != null).join("\n");
16343
+ return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText };
16344
+ }
16345
+ }
16346
+ return { kind: "absent" };
16347
+ }
16348
+ function detectMcpState(target, spec) {
16349
+ if (!target.present) return { target, state: "skip", action: "skip", reasons: [] };
16350
+ if (target.content == null) {
16351
+ if (target.createWhenFileAbsent) return { target, state: "absent", action: "install", reasons: [] };
16352
+ return { target, state: "skip", action: "skip", reasons: [] };
16353
+ }
16354
+ const extract = extractServerBlock(target.content, target.format, spec);
16355
+ if (extract.kind === "unparseable") {
16356
+ return { target, state: "drifted", action: "reconcile", reasons: ["unparseable"] };
16357
+ }
16358
+ if (extract.kind === "absent") {
16359
+ return { target, state: "absent", action: "install", reasons: [] };
16360
+ }
16361
+ const reasons = [];
16362
+ if (extract.isLegacy) reasons.push("legacy-name");
16363
+ if (textHasPlaywrightVisionCap(extract.blockText)) reasons.push("vision-cap");
16364
+ if (!textHasCanonicalPlaywrightOutputDir(extract.blockText)) reasons.push("output-dir");
16365
+ if (reasons.length === 0) {
16366
+ return { target, state: "healthy", action: "none", reasons: [], matchedName: extract.matchedName };
16367
+ }
16368
+ return { target, state: "drifted", action: "reconcile", reasons, matchedName: extract.matchedName };
16369
+ }
16370
+ function planMcpReconcile(targets, spec) {
16371
+ return targets.map((t) => detectMcpState(t, spec));
16372
+ }
16373
+ function buildMcpReconcileCheck(plan) {
16374
+ const base = {
16375
+ ok: true,
16376
+ label: MCP_RECONCILE_LABEL,
16377
+ fix: MCP_RECONCILE_FIX,
16378
+ severityOverride: "advisory"
16379
+ };
16380
+ const gaps = plan.filter((p) => p.action === "install" || p.action === "reconcile");
16381
+ if (gaps.length === 0) return base;
16382
+ const detail = gaps.map((g) => `${g.target.path} (${g.action === "install" ? "missing" : g.reasons.join("+") || "drifted"})`).join(", ");
16383
+ return {
16384
+ ...base,
16385
+ ok: false,
16386
+ fix: `${MCP_RECONCILE_FIX} \u2014 ${detail}`,
16387
+ mcpGaps: gaps.map((g) => ({ path: g.target.path, action: g.action, reasons: g.reasons }))
16388
+ };
16389
+ }
16390
+ function applyMcpReconcile(targets, spec, opts) {
16391
+ const results = [];
16392
+ for (const item of planMcpReconcile(targets, spec)) {
16393
+ if (item.action !== "install" && item.action !== "reconcile") continue;
16394
+ const t = item.target;
16395
+ if (t.isRepoFile && !opts.repoWritesAllowed) {
16396
+ results.push({ target: t, action: item.action, outcome: "skipped-repo-write" });
16397
+ continue;
16398
+ }
16399
+ const next = t.format === "json" ? renderJsonMcpConfig(t.content, spec) : renderTomlMcpConfig(t.content, spec);
16400
+ if (next == null) {
16401
+ results.push({ target: t, action: item.action, outcome: "unparseable" });
16402
+ continue;
16403
+ }
16404
+ results.push({ target: t, action: item.action, outcome: opts.write(t.path, next) ? "wrote" : "write-failed" });
16405
+ }
16406
+ return results;
16407
+ }
16408
+ function renderJsonMcpConfig(existing, spec) {
16409
+ const canonicalEntry = { command: spec.command, args: [...spec.args] };
16410
+ if (existing == null || existing.trim() === "") {
16411
+ return `${JSON.stringify({ mcpServers: { [spec.name]: canonicalEntry } }, null, 2)}
16412
+ `;
16413
+ }
16414
+ let parsed;
16415
+ try {
16416
+ parsed = JSON.parse(existing);
16417
+ } catch {
16418
+ return null;
16419
+ }
16420
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
16421
+ const obj = parsed;
16422
+ const rawServers = obj.mcpServers;
16423
+ if (rawServers != null && (typeof rawServers !== "object" || Array.isArray(rawServers))) return null;
16424
+ const servers = rawServers ?? {};
16425
+ const prior = servers[spec.name] ?? spec.legacyNames.map((n) => servers[n]).find((v) => v != null);
16426
+ for (const legacy of spec.legacyNames) delete servers[legacy];
16427
+ const priorObj = prior != null && typeof prior === "object" && !Array.isArray(prior) ? prior : {};
16428
+ servers[spec.name] = { ...priorObj, ...canonicalEntry };
16429
+ obj.mcpServers = servers;
16430
+ const text = `${JSON.stringify(obj, null, 2)}
16431
+ `;
16432
+ return existing.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
16433
+ }
16434
+ function isTomlEscaped(line, i) {
16435
+ let n = 0;
16436
+ for (let j = i - 1; j >= 0 && line[j] === "\\"; j--) n += 1;
16437
+ return n % 2 === 1;
16438
+ }
16439
+ function advanceTomlStringState(line, state) {
16440
+ let multi = state;
16441
+ let single = null;
16442
+ let i = 0;
16443
+ while (i < line.length) {
16444
+ if (multi != null) {
16445
+ if (line.startsWith(multi, i) && !(multi === '"""' && isTomlEscaped(line, i))) {
16446
+ multi = null;
16447
+ i += 3;
16448
+ } else {
16449
+ i += 1;
16450
+ }
16451
+ continue;
16452
+ }
16453
+ if (single != null) {
16454
+ if (line[i] === single && !(single === '"' && isTomlEscaped(line, i))) single = null;
16455
+ i += 1;
16456
+ continue;
16457
+ }
16458
+ if (line[i] === "#") break;
16459
+ if (line.startsWith('"""', i)) {
16460
+ multi = '"""';
16461
+ i += 3;
16462
+ continue;
16463
+ }
16464
+ if (line.startsWith("'''", i)) {
16465
+ multi = "'''";
16466
+ i += 3;
16467
+ continue;
16468
+ }
16469
+ if (line[i] === '"' || line[i] === "'") {
16470
+ single = line[i];
16471
+ i += 1;
16472
+ continue;
16473
+ }
16474
+ i += 1;
16475
+ }
16476
+ return multi;
16477
+ }
16478
+ function parseTomlBlocks(lines) {
16479
+ const blocks = [];
16480
+ let current = { headerRaw: null, name: null, isArrayTable: false, body: [] };
16481
+ let multi = null;
16482
+ const tableRe = /^\s*\[\s*([^[\]]+?)\s*\]\s*$/;
16483
+ const arrayTableRe = /^\s*\[\[\s*([^[\]]+?)\s*\]\]\s*$/;
16484
+ for (const line of lines) {
16485
+ if (multi == null) {
16486
+ const am = arrayTableRe.exec(line);
16487
+ const tm = am == null ? tableRe.exec(line) : null;
16488
+ if (am != null || tm != null) {
16489
+ blocks.push(current);
16490
+ current = { headerRaw: line, name: (am ?? tm)[1].trim(), isArrayTable: am != null, body: [] };
16491
+ continue;
16492
+ }
16493
+ }
16494
+ multi = advanceTomlStringState(line, multi);
16495
+ current.body.push(line);
16496
+ }
16497
+ blocks.push(current);
16498
+ return blocks;
16499
+ }
16500
+ function mergeCanonicalTomlBody(body, commandLine, argsLine) {
16501
+ const preserved = [];
16502
+ for (let i = 0; i < body.length; i++) {
16503
+ const line = body[i];
16504
+ const trimmed = line.trim();
16505
+ if (/^command\s*=/.test(trimmed)) continue;
16506
+ if (/^args\s*=/.test(trimmed)) {
16507
+ let open = (line.match(/\[/g) ?? []).length;
16508
+ let close = (line.match(/\]/g) ?? []).length;
16509
+ while (open > close && i + 1 < body.length) {
16510
+ i += 1;
16511
+ open += (body[i].match(/\[/g) ?? []).length;
16512
+ close += (body[i].match(/\]/g) ?? []).length;
16513
+ }
16514
+ continue;
16515
+ }
16516
+ preserved.push(line);
16517
+ }
16518
+ return [commandLine, argsLine, ...preserved];
16519
+ }
16520
+ function renderTomlMcpConfig(existing, spec) {
16521
+ const commandLine = `command = ${JSON.stringify(spec.command)}`;
16522
+ const argsLine = `args = [${spec.args.map((a) => JSON.stringify(a)).join(", ")}]`;
16523
+ const canonicalHeader = `[mcp_servers.${spec.name}]`;
16524
+ const canonicalName = `mcp_servers.${spec.name}`;
16525
+ if (existing == null || existing.trim() === "") {
16526
+ return `${canonicalHeader}
16527
+ ${commandLine}
16528
+ ${argsLine}
16529
+ `;
16530
+ }
16531
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
16532
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
16533
+ const legacyFull = new Set(spec.legacyNames.map((n) => `mcp_servers.${n}`));
16534
+ const all = parseTomlBlocks(lines);
16535
+ if (all.some((b) => b.isArrayTable && b.name != null && (b.name === canonicalName || legacyFull.has(b.name)))) return null;
16536
+ const targetBlock = all.find((b) => !b.isArrayTable && b.name === canonicalName) ?? all.find((b) => !b.isArrayTable && b.name != null && legacyFull.has(b.name));
16537
+ const out = [];
16538
+ for (const b of all) {
16539
+ if (b === targetBlock) {
16540
+ out.push({ headerRaw: canonicalHeader, name: canonicalName, isArrayTable: false, body: mergeCanonicalTomlBody(b.body, commandLine, argsLine) });
16541
+ continue;
16542
+ }
16543
+ if (targetBlock != null && !b.isArrayTable && b.name != null && legacyFull.has(b.name)) continue;
16544
+ out.push(b);
16545
+ }
16546
+ if (targetBlock != null) {
16547
+ const joined = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n");
16548
+ return eol === "\r\n" ? joined.replace(/\n/g, "\r\n") : joined;
16549
+ }
16550
+ const base = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n").replace(/\n+$/, "");
16551
+ const block = `${canonicalHeader}
16552
+ ${commandLine}
16553
+ ${argsLine}`;
16554
+ const combined = base === "" ? `${block}
16555
+ ` : `${base}
16556
+
16557
+ ${block}
16558
+ `;
16559
+ return eol === "\r\n" ? combined.replace(/\n/g, "\r\n") : combined;
16560
+ }
16561
+
16105
16562
  // src/cli-doctor-shared.ts
16106
16563
  var import_node_fs15 = require("node:fs");
16107
16564
  var import_node_path16 = require("node:path");
@@ -16358,7 +16815,20 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
16358
16815
  function runHostBin(bin, args, opts) {
16359
16816
  return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
16360
16817
  }
16361
- function reexecMmiCli(args) {
16818
+ function reexecStrategies(input) {
16819
+ const strategies = [
16820
+ input.platform === "win32" ? { command: "cmd.exe", args: ["/c", "mmi-cli"], kind: "cmd-shim" } : { command: "mmi-cli", args: [], kind: "direct" }
16821
+ ];
16822
+ if (input.argv1) strategies.push({ command: input.execPath, args: [input.argv1], kind: "direct" });
16823
+ return strategies;
16824
+ }
16825
+ var CMD_NOT_RECOGNIZED_EXIT_CODE = 9009;
16826
+ function reexecAttemptOutcome(strategy, code) {
16827
+ if (code < 0) return "strategy-failed";
16828
+ if (strategy.kind === "cmd-shim" && code === CMD_NOT_RECOGNIZED_EXIT_CODE) return "strategy-failed";
16829
+ return "result";
16830
+ }
16831
+ function spawnReexecAttempt(command, args, env) {
16362
16832
  return new Promise((resolve6) => {
16363
16833
  let settled = false;
16364
16834
  const done = (code) => {
@@ -16367,12 +16837,20 @@ function reexecMmiCli(args) {
16367
16837
  resolve6(code);
16368
16838
  }
16369
16839
  };
16370
- const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
16371
- const child = isWin ? (0, import_node_child_process10.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process10.spawn)("mmi-cli", args, { stdio: "inherit", env });
16840
+ const child = (0, import_node_child_process10.spawn)(command, args, { stdio: "inherit", env });
16372
16841
  child.on("error", () => done(-1));
16373
16842
  child.on("exit", (code) => done(code ?? 0));
16374
16843
  });
16375
16844
  }
16845
+ async function reexecMmiCli(args) {
16846
+ const strategies = reexecStrategies({ platform: process.platform, argv1: process.argv[1], execPath: process.execPath });
16847
+ const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
16848
+ for (const strategy of strategies) {
16849
+ const code = await spawnReexecAttempt(strategy.command, [...strategy.args, ...args], env);
16850
+ if (reexecAttemptOutcome(strategy, code) === "result") return code;
16851
+ }
16852
+ return -1;
16853
+ }
16376
16854
  function hostBinAvailable(bin) {
16377
16855
  return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
16378
16856
  }
@@ -16392,6 +16870,15 @@ async function runCodexPlugin(args) {
16392
16870
  return false;
16393
16871
  }
16394
16872
  }
16873
+ async function codexEnabledMmiVersion() {
16874
+ if (!await hostBinAvailable("codex")) return void 0;
16875
+ try {
16876
+ const { stdout } = await runHostBin("codex", ["plugin", "list", "--json"], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
16877
+ return codexEnabledPluginVersionFromList(stdout, MMI_PLUGIN_ID);
16878
+ } catch {
16879
+ return void 0;
16880
+ }
16881
+ }
16395
16882
  async function marketplaceAddRefSupported(bin) {
16396
16883
  try {
16397
16884
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -16739,6 +17226,12 @@ function opencodeInstalledVersionForDoctor() {
16739
17226
  diskVersion: readOpencodeAdapterDiskVersion()
16740
17227
  });
16741
17228
  }
17229
+ function opencodePersistedVersionForDoctor() {
17230
+ return pickOpencodeActiveVersion({
17231
+ cacheVersion: readOpencodeLoadedCacheVersion(),
17232
+ diskVersion: readOpencodeAdapterDiskVersion()
17233
+ });
17234
+ }
16742
17235
  function opencodePluginVersionsForReport() {
16743
17236
  return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
16744
17237
  }
@@ -16856,8 +17349,8 @@ function hasNestedMmiChild(versionDir) {
16856
17349
  }
16857
17350
  }
16858
17351
  function nestedPluginTreeSnapshot() {
16859
- return mmiPluginCacheRootSnapshots().filter((root) => root.surface === "claude").flatMap(
16860
- (root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface: "claude", path: e.path, nested: hasNestedMmiChild(e.path) }))
17352
+ return mmiPluginCacheRootSnapshots().flatMap(
17353
+ (root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface: root.surface, path: e.path, nested: hasNestedMmiChild(e.path) }))
16861
17354
  );
16862
17355
  }
16863
17356
  function uniqueQuarantineTarget(path2) {
@@ -16933,21 +17426,95 @@ function readTextFile(path2) {
16933
17426
  return null;
16934
17427
  }
16935
17428
  }
16936
- function playwrightMcpConfigSnapshots() {
17429
+ function mcpDirExists(path2) {
17430
+ try {
17431
+ return (0, import_node_fs17.existsSync)(path2);
17432
+ } catch {
17433
+ return false;
17434
+ }
17435
+ }
17436
+ function mcpConfigTargets() {
16937
17437
  const cwd = process.cwd();
16938
17438
  const home = (0, import_node_os6.homedir)();
16939
- const candidates = [
16940
- (0, import_node_path17.join)(cwd, ".mcp.json"),
16941
- (0, import_node_path17.join)(cwd, ".cursor", "mcp.json"),
16942
- (0, import_node_path17.join)(home, ".cursor", "mcp.json"),
16943
- (0, import_node_path17.join)(home, ".codex", "config.toml")
17439
+ const cursorProjectDir = (0, import_node_path17.join)(cwd, ".cursor");
17440
+ const cursorUserDir = (0, import_node_path17.join)(home, ".cursor");
17441
+ const codexDir = (0, import_node_path17.join)(home, ".codex");
17442
+ return [
17443
+ // Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
17444
+ {
17445
+ host: "claude-code",
17446
+ label: "Claude Code project MCP",
17447
+ path: (0, import_node_path17.join)(cwd, ".mcp.json"),
17448
+ format: "json",
17449
+ present: true,
17450
+ // parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
17451
+ isRepoFile: true,
17452
+ createWhenFileAbsent: false,
17453
+ content: readTextFile((0, import_node_path17.join)(cwd, ".mcp.json"))
17454
+ },
17455
+ // Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
17456
+ {
17457
+ host: "cursor-project",
17458
+ label: "Cursor project MCP",
17459
+ path: (0, import_node_path17.join)(cursorProjectDir, "mcp.json"),
17460
+ format: "json",
17461
+ present: mcpDirExists(cursorProjectDir),
17462
+ isRepoFile: true,
17463
+ createWhenFileAbsent: true,
17464
+ content: readTextFile((0, import_node_path17.join)(cursorProjectDir, "mcp.json"))
17465
+ },
17466
+ // Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
17467
+ {
17468
+ host: "cursor-user",
17469
+ label: "Cursor user MCP",
17470
+ path: (0, import_node_path17.join)(cursorUserDir, "mcp.json"),
17471
+ format: "json",
17472
+ present: mcpDirExists(cursorUserDir),
17473
+ isRepoFile: false,
17474
+ createWhenFileAbsent: true,
17475
+ content: readTextFile((0, import_node_path17.join)(cursorUserDir, "mcp.json"))
17476
+ },
17477
+ // Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
17478
+ {
17479
+ host: "codex",
17480
+ label: "Codex user config",
17481
+ path: (0, import_node_path17.join)(codexDir, "config.toml"),
17482
+ format: "toml",
17483
+ present: mcpDirExists(codexDir),
17484
+ isRepoFile: false,
17485
+ createWhenFileAbsent: true,
17486
+ content: readTextFile((0, import_node_path17.join)(codexDir, "config.toml"))
17487
+ }
16944
17488
  ];
16945
- const out = [];
16946
- for (const path2 of candidates) {
16947
- const content = readTextFile(path2);
16948
- if (content != null) out.push({ path: path2, content });
17489
+ }
17490
+ function writeMcpConfigFile(path2, content) {
17491
+ try {
17492
+ (0, import_node_fs17.writeFileSync)(path2, content, "utf8");
17493
+ return true;
17494
+ } catch {
17495
+ return false;
16949
17496
  }
16950
- return out;
17497
+ }
17498
+ async function reconcileMcpRegistrations(input) {
17499
+ if (!input.isOrgRepo) return buildMcpReconcileCheck([]);
17500
+ const targets = mcpConfigTargets();
17501
+ if (!input.apply) return buildMcpReconcileCheck(planMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC));
17502
+ const results = applyMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC, {
17503
+ repoWritesAllowed: input.repoWritesAllowed,
17504
+ write: writeMcpConfigFile
17505
+ });
17506
+ for (const r of results) {
17507
+ if (r.outcome === "wrote") {
17508
+ input.onHealed();
17509
+ const verb = r.action === "install" ? "registered" : "reconciled";
17510
+ input.log(` \u21BB ${verb} Playwright MCP in ${r.target.path} \u2014 restart the host to load it`);
17511
+ } else if (r.outcome === "unparseable") {
17512
+ input.log(` \u26A0 ${r.target.label}: could not safely patch ${r.target.path} (unparseable) \u2014 fix it by hand`);
17513
+ } else if (r.outcome === "write-failed") {
17514
+ input.log(` \u26A0 ${r.target.label}: failed to write ${r.target.path}`);
17515
+ }
17516
+ }
17517
+ return buildMcpReconcileCheck(planMcpReconcile(mcpConfigTargets(), PLAYWRIGHT_MCP_SPEC));
16951
17518
  }
16952
17519
  function strayBrowserArtifactPaths() {
16953
17520
  const cwd = process.cwd();
@@ -17071,13 +17638,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17071
17638
  checks.push(versionReport);
17072
17639
  if (selfUpdatedCli) {
17073
17640
  if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
17074
- const code = await reexecMmiCli(buildSelfUpdateReexecArgs(opts));
17641
+ const reexecArgs = buildSelfUpdateReexecArgs(opts);
17642
+ const code = await reexecMmiCli(reexecArgs);
17075
17643
  if (code >= 0) {
17076
17644
  if (code > 0) process.exitCode = code;
17077
17645
  return;
17078
17646
  }
17079
- if (opts.json) io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
17080
- else io.err(selfUpdateHaltLine(versionReport));
17647
+ process.exitCode = DOCTOR_SELF_UPDATE_HALT_EXIT_CODE;
17648
+ if (opts.json) {
17649
+ io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
17650
+ } else {
17651
+ io.err(selfUpdateHaltLine(versionReport));
17652
+ io.err(selfUpdateHaltSentinelLine({ report: versionReport, rerunArgs: reexecArgs }));
17653
+ }
17081
17654
  return;
17082
17655
  }
17083
17656
  checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
@@ -17278,10 +17851,20 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17278
17851
  releasedVersion
17279
17852
  });
17280
17853
  if (!opencodeVersionCheck.ok && repairFull) {
17281
- const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
17282
- const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
17283
- if (refreshed || quarantined) {
17284
- opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
17854
+ const persistedCurrent = () => buildOpencodeVersionCheck({
17855
+ isOrgRepo,
17856
+ installedVersion: opencodePersistedVersionForDoctor(),
17857
+ releasedVersion
17858
+ }).ok;
17859
+ let refreshed = false;
17860
+ let quarantined = false;
17861
+ if (!persistedCurrent()) {
17862
+ refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
17863
+ quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
17864
+ }
17865
+ if (persistedCurrent()) {
17866
+ const wasReinstalled = refreshed || quarantined;
17867
+ opencodeInstalledVersion = opencodePersistedVersionForDoctor() ?? opencodeInstalledVersion;
17285
17868
  opencodeVersionCheck = buildOpencodeVersionCheck({
17286
17869
  isOrgRepo,
17287
17870
  installedVersion: opencodeInstalledVersion,
@@ -17290,7 +17873,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17290
17873
  if (opencodeVersionCheck.ok) {
17291
17874
  markPluginReloadRequired();
17292
17875
  markHealed();
17293
- io.err(` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${opencodeInstalledVersion ?? releasedVersion ?? "latest"} \u2014 ${reloadAction("opencode")} to load it`);
17876
+ const version = opencodeInstalledVersion ?? releasedVersion ?? "latest";
17877
+ io.err(wasReinstalled ? ` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${version} \u2014 ${reloadAction("opencode")} to load it` : ` \u21BB OpenCode MMI plugin is current on disk (${version}); the running session is stale \u2014 ${reloadAction("opencode")} to load it`);
17294
17878
  }
17295
17879
  }
17296
17880
  }
@@ -17361,6 +17945,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17361
17945
  return versions.sort((a, b) => compareVersions(b, a))[0];
17362
17946
  };
17363
17947
  const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
17948
+ const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
17364
17949
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
17365
17950
  isOrgRepo,
17366
17951
  roots: mmiPluginCacheRootSnapshots(),
@@ -17397,7 +17982,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17397
17982
  isOrgRepo,
17398
17983
  releasedVersion,
17399
17984
  codexCacheVersions: codexCacheVersions(),
17400
- codexRecordVersion: codexRecordVersion()
17985
+ codexRecordVersion: codexRecordVersion(),
17986
+ codexActiveVersion: await codexActiveVersion()
17401
17987
  });
17402
17988
  if (!codexActiveCacheCheck.ok && repairFull) {
17403
17989
  const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
@@ -17409,7 +17995,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17409
17995
  isOrgRepo,
17410
17996
  releasedVersion,
17411
17997
  codexCacheVersions: codexCacheVersions(),
17412
- codexRecordVersion: codexRecordVersion()
17998
+ codexRecordVersion: codexRecordVersion(),
17999
+ codexActiveVersion: await codexActiveVersion()
17413
18000
  });
17414
18001
  }
17415
18002
  }
@@ -17420,7 +18007,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17420
18007
  entries: nestedPluginTreeSnapshot()
17421
18008
  });
17422
18009
  if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
17423
- const nestedPaths = nestedPluginTreeCheck.nested.map((n) => n.path);
18010
+ const nestedEntries = nestedPluginTreeCheck.nested;
18011
+ const nestedPaths = nestedEntries.map((n) => n.path);
17424
18012
  if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
17425
18013
  nestedPluginTreeCheck = buildNestedPluginTreeCheck({
17426
18014
  isOrgRepo,
@@ -17428,36 +18016,62 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17428
18016
  entries: nestedPluginTreeSnapshot()
17429
18017
  });
17430
18018
  if (nestedPluginTreeCheck.ok) {
17431
- io.err(` \u21BB cleared self-nested MMI plugin cache tree(s) \u2014 reinstalling plugin\u2026`);
18019
+ io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)`);
18020
+ }
18021
+ let reinstalledAny = false;
18022
+ const unreinstalled = [];
18023
+ for (const token of [...new Set(nestedEntries.map((n) => n.surface))]) {
18024
+ const bin = token === "codex" ? "codex" : "claude";
18025
+ const canDrive = surfaceToken(surface) === token || await hostBinAvailable(bin);
18026
+ if (canDrive && await applyPluginHeal(token, surface, (m) => io.err(m), { force: true })) {
18027
+ reinstalledAny = true;
18028
+ io.err(` \u21BB reinstalled MMI plugin (${token}) after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
18029
+ } else {
18030
+ unreinstalled.push(token);
18031
+ }
17432
18032
  }
17433
- if (await applyPluginHeal("claude", surface, (m) => io.err(m))) {
18033
+ if (reinstalledAny) {
17434
18034
  markPluginReloadRequired();
17435
18035
  markHealed();
17436
- io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
17437
18036
  nestedPluginTreeCheck = buildNestedPluginTreeCheck({
17438
18037
  isOrgRepo,
17439
18038
  isWindows: isWin,
17440
18039
  entries: nestedPluginTreeSnapshot()
17441
18040
  });
17442
18041
  }
18042
+ if (unreinstalled.length > 0 && nestedPluginTreeCheck.ok) {
18043
+ nestedPluginTreeCheck = { ...nestedPluginTreeCheck, ok: false, fix: nestedTreeReinstallGapFix(unreinstalled) };
18044
+ }
17443
18045
  }
17444
18046
  }
17445
18047
  checks.push(nestedPluginTreeCheck);
17446
18048
  const cursorCacheRoot = cursorPluginCacheRoot();
18049
+ const cursorCacheRootExists = (0, import_node_fs17.existsSync)(cursorCacheRoot);
17447
18050
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
18051
+ checks.push(
18052
+ buildCursorPluginCacheCleanupCheck({
18053
+ isOrgRepo,
18054
+ cacheRoot: cursorCacheRoot,
18055
+ pins: cursorPins,
18056
+ releasedVersion
18057
+ })
18058
+ );
17448
18059
  let cursorPluginCheck = buildCursorPluginInstallCheck({
17449
18060
  isOrgRepo,
17450
18061
  surface,
17451
18062
  cacheRoot: cursorCacheRoot,
17452
- cacheRootExists: (0, import_node_fs17.existsSync)(cursorCacheRoot),
18063
+ cacheRootExists: cursorCacheRootExists,
17453
18064
  pins: cursorPins,
17454
18065
  hubCheckout: hubCheckoutForCursorSeed(),
17455
18066
  releasedVersion
17456
18067
  });
17457
18068
  if (!cursorPluginCheck.ok && repairLocal) {
18069
+ const seedingFromNothing = cursorPins.length === 0;
17458
18070
  const seeded = await applyCursorPluginCacheSeed({
17459
18071
  pins: cursorPins,
17460
18072
  releasedVersion,
18073
+ cacheRoot: cursorCacheRoot,
18074
+ cacheRootExists: cursorCacheRootExists,
17461
18075
  hubCheckout: hubCheckoutForCursorSeed(),
17462
18076
  execFileP: execFileP2,
17463
18077
  mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
@@ -17469,14 +18083,26 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17469
18083
  isOrgRepo,
17470
18084
  surface,
17471
18085
  cacheRoot: cursorCacheRoot,
17472
- cacheRootExists: (0, import_node_fs17.existsSync)(cursorCacheRoot),
18086
+ cacheRootExists: cursorCacheRootExists,
17473
18087
  pins: cursorPins,
17474
18088
  hubCheckout: hubCheckoutForCursorSeed(),
17475
18089
  releasedVersion
17476
18090
  });
17477
18091
  if (cursorPluginCheck.ok) {
17478
18092
  markHealed();
17479
- io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
18093
+ if (seedingFromNothing) {
18094
+ io.err(
18095
+ ` \u21BB seeded Cursor MMI plugin cache from nothing \u2192 ${releasedVersion ?? "latest"} (cache only \u2014 Team Marketplace registration not verified, #2409) \u2014 ${reloadAction(surface)}`
18096
+ );
18097
+ cursorPluginCheck = {
18098
+ ...cursorPluginCheck,
18099
+ ok: false,
18100
+ severityOverride: "advisory",
18101
+ fix: cursorSeededFromNothingAdvisoryFix(releasedVersion)
18102
+ };
18103
+ } else {
18104
+ io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
18105
+ }
17480
18106
  }
17481
18107
  }
17482
18108
  }
@@ -17497,20 +18123,29 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17497
18123
  mmiCliOnPath: onPath
17498
18124
  })
17499
18125
  );
17500
- if (runExtended) {
17501
- const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
17502
- checks.push(
17503
- buildPlaywrightMcpVisionCapCheck({
17504
- isOrgRepo,
17505
- configs: playwrightMcpConfigs
17506
- })
17507
- );
18126
+ if (!opts.banner) {
17508
18127
  checks.push(
17509
- buildPlaywrightMcpOutputDirCheck({
18128
+ await reconcileMcpRegistrations({
17510
18129
  isOrgRepo,
17511
- configs: playwrightMcpConfigs
18130
+ apply: Boolean(opts.apply),
18131
+ repoWritesAllowed,
18132
+ log: (m) => io.err(m),
18133
+ onHealed: markHealed
17512
18134
  })
17513
18135
  );
18136
+ }
18137
+ const sessionPluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
18138
+ const sessionCacheSurface = sessionPluginRoot?.includes(".codex") ? "codex" : sessionPluginRoot?.includes(".claude") ? "claude" : void 0;
18139
+ const sessionCacheVersions = sessionCacheSurface ? mmiPluginCacheRootSnapshots().filter((r) => r.surface === sessionCacheSurface).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name)) : [];
18140
+ checks.push(
18141
+ buildSessionSkillRootCheck({
18142
+ isOrgRepo,
18143
+ surface,
18144
+ pluginRoot: sessionPluginRoot,
18145
+ cacheVersions: sessionCacheVersions
18146
+ })
18147
+ );
18148
+ if (runExtended) {
17514
18149
  checks.push(
17515
18150
  buildBrowserArtifactsCheck({
17516
18151
  isOrgRepo,
@@ -19606,7 +20241,7 @@ board.command("prune-priority-labels").description("remove retired priority:* la
19606
20241
  });
19607
20242
  board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
19608
20243
  try {
19609
- const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
20244
+ const result = await moveBoardItem({ config: await loadConfigForBoardSelector(issueRef, o.repo), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
19610
20245
  if (o.json) return console.log(JSON.stringify(result));
19611
20246
  console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
19612
20247
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",