@mutmutco/cli 3.1.0 → 3.3.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 +960 -144
  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
  }
@@ -9197,9 +9219,23 @@ function parseNpmVersion(stdout) {
9197
9219
  function staleTrainCliMessage(report, commandName) {
9198
9220
  return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
9199
9221
  }
9200
- function versionAutoUpdateAction(report, hasPluginRoot) {
9222
+ async function resolveReleasedVersion(runners) {
9223
+ try {
9224
+ const npmVersion = parseNpmVersion(await runners.npm());
9225
+ if (npmVersion) return { version: npmVersion, source: "npm" };
9226
+ } catch {
9227
+ }
9228
+ try {
9229
+ const ghVersion = parseManifestVersion(await runners.gh());
9230
+ if (ghVersion) return { version: ghVersion, source: "gh" };
9231
+ } catch {
9232
+ }
9233
+ return { version: void 0, source: "unavailable" };
9234
+ }
9235
+ function versionAutoUpdateAction(report, hasPluginRoot, releasedSource) {
9201
9236
  if (report.ok || report.staleAgainst !== "released") return "none";
9202
- return hasPluginRoot ? "plugin-pull" : "npm";
9237
+ if (hasPluginRoot) return "plugin-pull";
9238
+ return releasedSource === "gh" ? "npm-unreachable" : "npm";
9203
9239
  }
9204
9240
 
9205
9241
  // src/issue-related.ts
@@ -10423,25 +10459,113 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
10423
10459
  }
10424
10460
  return { foldPaths, tolerated, predicted };
10425
10461
  }
10426
- async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
10462
+ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
10427
10463
  await deps.run("git", ["checkout", "main"]);
10428
10464
  await ffOnlyPull(deps, "main");
10465
+ preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
10429
10466
  if (predicted.length === 0) {
10430
10467
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
10431
10468
  } else {
10432
10469
  await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
10433
10470
  }
10434
10471
  }
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
10472
+ async function probeFoldFailureState(deps) {
10473
+ const branch = await currentBranch(deps);
10474
+ const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
10475
+ const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
10476
+ const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean).catch(() => "");
10477
+ const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean).catch(() => "");
10478
+ const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
10479
+ return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
10480
+ }
10481
+ function shaLabel(sha) {
10482
+ return sha ? sha.slice(0, 7) : "(unknown)";
10483
+ }
10484
+ async function finishFoldAutoRestore(deps, causeMessage, startBranch, what) {
10485
+ try {
10486
+ await deps.run("git", ["checkout", startBranch]);
10487
+ } catch (e) {
10488
+ return new Error(
10489
+ `${causeMessage}
10490
+
10491
+ fold failed; ${what}, but returning to ${startBranch} afterwards failed: ${e instanceof Error ? e.message : String(e)}. Finish manually: git checkout ${startBranch}`
10492
+ );
10493
+ }
10494
+ return new Error(
10495
+ `${causeMessage}
10496
+
10497
+ 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
10498
  );
10443
- await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
10444
- return { foldPaths };
10499
+ }
10500
+ function foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha) {
10501
+ const steps = preFoldMainSha ? [
10502
+ ` 1. git status # inspect what is actually there`,
10503
+ ` 2. git log --oneline ${preFoldMainSha}..main # commits beyond the pre-fold main \u2014 confirm they are only this run's merge/fold attempt`,
10504
+ ` 3. git checkout main && git reset --hard ${preFoldMainSha} # back to the exact commit the fold built on`,
10505
+ ` 4. git checkout ${startBranch}`
10506
+ ] : [
10507
+ ` 1. git status # inspect what is actually there`,
10508
+ ` 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`,
10509
+ ` 3. git checkout ${startBranch}`
10510
+ ];
10511
+ return new Error(
10512
+ `${causeMessage}
10513
+
10514
+ 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.
10515
+ Recovery sequence:
10516
+ ${steps.join("\n")}`
10517
+ );
10518
+ }
10519
+ async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha) {
10520
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
10521
+ const probe = await probeFoldFailureState(deps);
10522
+ if (probe.mergeInProgress) {
10523
+ try {
10524
+ await deps.run("git", ["merge", "--abort"]);
10525
+ } catch (e) {
10526
+ return new Error(
10527
+ `${causeMessage}
10528
+
10529
+ 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.
10530
+ Recovery sequence:
10531
+ 1. git merge --abort
10532
+ 2. git checkout ${startBranch}`
10533
+ );
10534
+ }
10535
+ return finishFoldAutoRestore(deps, causeMessage, startBranch, "the in-progress merge was aborted");
10536
+ }
10537
+ if (probe.branch === "main" && preFoldMainSha) {
10538
+ const mainDescendsFromPreFold = await deps.run("git", ["merge-base", "--is-ancestor", preFoldMainSha, "main"]).then(() => true).catch(() => false);
10539
+ if (mainDescendsFromPreFold) {
10540
+ try {
10541
+ await deps.run("git", ["reset", "--hard", preFoldMainSha]);
10542
+ } catch (e) {
10543
+ return new Error(
10544
+ `${causeMessage}
10545
+
10546
+ 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.
10547
+ Recovery sequence:
10548
+ 1. git reset --hard ${preFoldMainSha}
10549
+ 2. git checkout ${startBranch}`
10550
+ );
10551
+ }
10552
+ const aheadNote = probe.originMainSha && preFoldMainSha !== probe.originMainSha ? `; local main carried commit(s) origin/main (${shaLabel(probe.originMainSha)}) does not have \u2014 they were preserved` : "";
10553
+ return finishFoldAutoRestore(
10554
+ deps,
10555
+ causeMessage,
10556
+ startBranch,
10557
+ `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}`
10558
+ );
10559
+ }
10560
+ }
10561
+ return foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha);
10562
+ }
10563
+ async function runFoldStage(deps, startBranch, preFold, fn) {
10564
+ try {
10565
+ return await fn();
10566
+ } catch (e) {
10567
+ throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
10568
+ }
10445
10569
  }
10446
10570
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
10447
10571
  await ensureTagPushed(deps, tag, releaseSha);
@@ -10534,15 +10658,20 @@ async function runTrainApplyPipeline(mode, input) {
10534
10658
  const deployModel2 = await preflight(deps, ctx, "main", meta);
10535
10659
  const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
10536
10660
  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."
10661
+ const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
10662
+ deps,
10663
+ deployModel2,
10664
+ "origin/development",
10665
+ "development -> main merge would conflict on untolerated path(s)",
10666
+ "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
10667
+ );
10668
+ const preFold2 = {};
10669
+ const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
10670
+ await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
10671
+ const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10672
+ const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10673
+ return { versionFold: versionFold3, releaseSha: releaseSha3 };
10543
10674
  });
10544
- const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10545
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10546
10675
  const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
10547
10676
  const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
10548
10677
  if (directTrack) {
@@ -10619,10 +10748,14 @@ async function runTrainApplyPipeline(mode, input) {
10619
10748
  );
10620
10749
  }
10621
10750
  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");
10751
+ const preFold = {};
10752
+ const { tag, versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
10753
+ await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
10754
+ const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10755
+ const versionFold2 = await foldReleaseVersion(deps, deployModel, tag2, foldPaths);
10756
+ const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10757
+ return { tag: tag2, versionFold: versionFold2, releaseSha: releaseSha2 };
10758
+ });
10626
10759
  const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
10627
10760
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
10628
10761
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
@@ -12389,6 +12522,19 @@ async function fetchTrainAuthority(repo, deps) {
12389
12522
  return { ok: false, error: e.message };
12390
12523
  }
12391
12524
  }
12525
+ async function mintWikiToken(repo, deps) {
12526
+ const res = await postJson("/wiki-mint", { repo }, deps, "POST", { noRetry: true });
12527
+ if (res.error) return { ok: false, error: res.error };
12528
+ const body = res.body ?? {};
12529
+ if (res.status === 426) return { ok: false, error: upgradeRequiredError({ status: 426 }, res.body) };
12530
+ if (res.status === 403) return { ok: false, error: `master-admin only (HTTP 403)${body.error ? ` \u2014 ${body.error}` : ""}` };
12531
+ if (res.status === 404) {
12532
+ return { ok: false, error: "the Hub API did not recognize /wiki-mint (HTTP 404) \u2014 the deployed Hub predates this command; it answers once a Hub release carrying it is deployed" };
12533
+ }
12534
+ if (!res.ok) return { ok: false, error: `wiki-mint HTTP ${res.status}${body.error ? ` \u2014 ${body.error}` : ""}` };
12535
+ if (!body.token || !body.expiresAt) return { ok: false, error: "malformed wiki-mint response (no token)" };
12536
+ return { ok: true, mint: { token: body.token, expiresAt: body.expiresAt, repository: body.repository ?? repo, permissions: body.permissions ?? {} } };
12537
+ }
12392
12538
  async function fetchProjectsList(deps) {
12393
12539
  if (!deps.baseUrl) return null;
12394
12540
  const token = await deps.token();
@@ -12526,6 +12672,32 @@ async function tenantDeploy(payload, deps) {
12526
12672
  return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
12527
12673
  }
12528
12674
 
12675
+ // src/wiki-publish-command.ts
12676
+ async function wikiPublish(deps, opts) {
12677
+ if (!opts.pagesDir) {
12678
+ deps.err("wiki publish: <pages-dir> is required");
12679
+ return false;
12680
+ }
12681
+ const script = deps.scriptPath();
12682
+ if (!deps.scriptExists(script)) {
12683
+ deps.err(`wiki publish: publish script not found at ${script} \u2014 run from the repo checkout root`);
12684
+ return false;
12685
+ }
12686
+ const minted = await deps.mint(opts.repo);
12687
+ if (!minted.ok) {
12688
+ deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
12689
+ return false;
12690
+ }
12691
+ const env = { ...process.env, WIKI_REPO: opts.repo, WIKI_TOKEN: minted.mint.token };
12692
+ const code = deps.spawn("node", [script, opts.pagesDir], env);
12693
+ if (code !== 0) {
12694
+ deps.err(`wiki publish failed: publish script exited ${code} (token was scoped to ${opts.repo}, contents:write, expires ${minted.mint.expiresAt})`);
12695
+ return false;
12696
+ }
12697
+ deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
12698
+ return true;
12699
+ }
12700
+
12529
12701
  // src/project-readiness.ts
12530
12702
  function stagesForTrack(meta) {
12531
12703
  return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
@@ -13669,7 +13841,9 @@ function vaultPointer(slug) {
13669
13841
  slug,
13670
13842
  root,
13671
13843
  tiers: {
13672
- project: `${root}/{dev,rc,main}/* (project-admin self-serve for this repo)`,
13844
+ // #2482: a stageless catalog entry (stages: []) lives at the slug ROOT — one value per repo
13845
+ // alongside the staged tree for per-stage entries. Both are the same project-admin wall (#2032).
13846
+ project: `${root}/<KEY> (stageless, one value per repo) + ${root}/{dev,rc,main}/* (staged entries) (project-admin self-serve for this repo)`,
13673
13847
  org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
13674
13848
  },
13675
13849
  stages: ["dev", "rc", "main"],
@@ -14806,16 +14980,24 @@ function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
14806
14980
  return false;
14807
14981
  });
14808
14982
  }
14983
+ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRootExists = false) {
14984
+ const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
14985
+ if (needing.length > 0) return needing.map((pin) => pin.path);
14986
+ if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion(releasedVersion)) {
14987
+ return [(0, import_node_path15.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
14988
+ }
14989
+ return [];
14990
+ }
14809
14991
  async function applyCursorPluginCacheSeed(input) {
14810
14992
  if (!isSemverVersion(input.releasedVersion)) return false;
14811
- const pinsToSeed = cursorPluginPinsNeedingSeed(input.pins, input.releasedVersion);
14812
- if (pinsToSeed.length === 0) return false;
14993
+ const targets = cursorPluginCacheSeedTargets(input.pins, input.releasedVersion, input.cacheRoot, input.cacheRootExists);
14994
+ if (targets.length === 0) return false;
14813
14995
  const tmpRoot = await input.mkdtemp("mmi-cursor-seed-");
14814
14996
  const source = await resolvePluginMmiSource(input.releasedVersion, input.hubCheckout, tmpRoot, input.execFileP);
14815
14997
  if (!source) return false;
14816
14998
  input.log(` \u21BB seeding Cursor MMI plugin cache \u2192 ${input.releasedVersion}\u2026`);
14817
- for (const pin of pinsToSeed) {
14818
- syncDirContents(source, pin.path);
14999
+ for (const dest of targets) {
15000
+ syncDirContents(source, dest);
14819
15001
  }
14820
15002
  (0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
14821
15003
  return true;
@@ -15095,6 +15277,33 @@ function buildMmiPluginCacheCleanupCheck(input) {
15095
15277
  }))
15096
15278
  };
15097
15279
  }
15280
+ var CURSOR_PLUGIN_CACHE_CLEANUP_LABEL = "leftover older MMI plugin cache pins (Cursor)";
15281
+ 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";
15282
+ function buildCursorPluginCacheCleanupCheck(input) {
15283
+ const base = {
15284
+ ok: true,
15285
+ label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL,
15286
+ fix: CURSOR_PLUGIN_CACHE_CLEANUP_FIX
15287
+ };
15288
+ if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
15289
+ const semverPins = input.pins.filter((pin) => isSemverVersion2(pin.version));
15290
+ if (semverPins.length === 0) return base;
15291
+ const activeVersion = semverPins.map((pin) => pin.version).reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
15292
+ const protectedVersions = new Set(
15293
+ [normalizeVersion(input.releasedVersion), normalizeVersion(activeVersion)].filter((v) => Boolean(v))
15294
+ );
15295
+ const leftoverPins = semverPins.filter((pin) => !protectedVersions.has(normalizeVersion(pin.version)));
15296
+ if (leftoverPins.length === 0) return base;
15297
+ const leftovers = leftoverPins.map((pin) => ({ surface: "cursor", root: input.cacheRoot, name: pin.name, path: pin.path }));
15298
+ const listed = leftoverPins.map((pin) => `${pin.name} (${pin.version})`).join(", ");
15299
+ return {
15300
+ ...base,
15301
+ ok: false,
15302
+ severityOverride: "advisory",
15303
+ leftovers,
15304
+ 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`
15305
+ };
15306
+ }
15098
15307
  var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
15099
15308
  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
15309
  function nestedPluginTreeCleanupCommand(paths, isWindows) {
@@ -15117,7 +15326,21 @@ function buildNestedPluginTreeCheck(input) {
15117
15326
  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
15327
  };
15119
15328
  }
15329
+ function nestedTreeReinstallGapFix(surfaces) {
15330
+ const recoveries = surfaces.map((s) => PLUGIN_SURFACE_HEAL[s].recovery).join(" and ");
15331
+ 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}`;
15332
+ }
15120
15333
  var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
15334
+ function codexEnabledPluginVersionFromList(jsonText, pluginId = MMI_PLUGIN_ID) {
15335
+ try {
15336
+ const parsed = JSON.parse(jsonText);
15337
+ const rows = Array.isArray(parsed?.installed) ? parsed.installed : [];
15338
+ const row = rows.find((r) => r?.pluginId === pluginId && r.installed === true && r.enabled === true);
15339
+ return isSemverVersion2(row?.version) ? row.version.trim().replace(/^v/, "") : void 0;
15340
+ } catch {
15341
+ return void 0;
15342
+ }
15343
+ }
15121
15344
  function buildCodexActiveCacheCheck(input) {
15122
15345
  const base = {
15123
15346
  ok: true,
@@ -15125,6 +15348,19 @@ function buildCodexActiveCacheCheck(input) {
15125
15348
  fix: CODEX_PLUGIN_RECOVERY
15126
15349
  };
15127
15350
  if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
15351
+ if (input.codexPresent === false) return base;
15352
+ if (isSemverVersion2(input.codexActiveVersion)) {
15353
+ if (compareVersions(input.codexActiveVersion, input.releasedVersion) < 0) {
15354
+ return {
15355
+ ...base,
15356
+ ok: false,
15357
+ activeCacheVersion: input.codexActiveVersion,
15358
+ releasedVersion: input.releasedVersion,
15359
+ 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}`
15360
+ };
15361
+ }
15362
+ return { ...base, activeCacheVersion: input.codexActiveVersion, releasedVersion: input.releasedVersion };
15363
+ }
15128
15364
  if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
15129
15365
  return base;
15130
15366
  }
@@ -15172,6 +15408,55 @@ function reloadAction(surface) {
15172
15408
  return "restart Claude Code (or run /reload-plugins)";
15173
15409
  }
15174
15410
  }
15411
+ function restartAction(surface) {
15412
+ switch (surface) {
15413
+ case "claude-vscode":
15414
+ return "fully restart VS Code";
15415
+ case "codex":
15416
+ return "fully restart Codex";
15417
+ case "opencode":
15418
+ return "fully restart OpenCode";
15419
+ case "cursor":
15420
+ return "fully restart Cursor";
15421
+ case "claude-cli":
15422
+ case "shell":
15423
+ default:
15424
+ return "fully restart Claude Code";
15425
+ }
15426
+ }
15427
+ function parseSessionSkillRootVersion(pluginRoot) {
15428
+ const trimmed = pluginRoot?.trim();
15429
+ if (!trimmed) return void 0;
15430
+ const segments = trimmed.split(/[\\/]+/).filter(Boolean);
15431
+ const mmiIdx = segments.findIndex((seg, i) => seg === "mmi" && segments[i - 1] === "mutmutco");
15432
+ if (mmiIdx < 0) return void 0;
15433
+ const candidate = segments[mmiIdx + 1];
15434
+ return candidate && /^v?\d+\.\d+\.\d+/.test(candidate) ? candidate.replace(/^v/, "") : void 0;
15435
+ }
15436
+ var SESSION_SKILL_ROOT_LABEL = "Session-loaded MMI skill roots (vs on-disk plugin cache)";
15437
+ function buildSessionSkillRootCheck(input) {
15438
+ const base = {
15439
+ ok: true,
15440
+ label: SESSION_SKILL_ROOT_LABEL,
15441
+ 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`
15442
+ };
15443
+ if (!input.isOrgRepo) return base;
15444
+ const sessionVersion = parseSessionSkillRootVersion(input.pluginRoot);
15445
+ if (!sessionVersion) return base;
15446
+ const activeCacheVersion = highestSemver(input.cacheVersions ?? []);
15447
+ if (!isSemverVersion2(activeCacheVersion)) return { ...base, sessionVersion };
15448
+ if (compareVersions(sessionVersion, activeCacheVersion) >= 0) {
15449
+ return { ...base, sessionVersion, activeCacheVersion };
15450
+ }
15451
+ return {
15452
+ ...base,
15453
+ ok: false,
15454
+ severityOverride: "advisory",
15455
+ sessionVersion,
15456
+ activeCacheVersion,
15457
+ 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.`
15458
+ };
15459
+ }
15175
15460
  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
15461
  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
15462
  var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
@@ -15342,12 +15627,13 @@ function buildPluginUpdateReport(input) {
15342
15627
  ...codexMarketplace ? { codexMarketplace } : {},
15343
15628
  ...codexActiveCache ? { codexActiveCache } : {},
15344
15629
  ...opencodePlugin ? { opencodePlugin } : {},
15345
- ...isSemverVersion2(input.releasedVersion) ? { released: input.releasedVersion } : {}
15630
+ ...isSemverVersion2(input.releasedVersion) ? { released: input.releasedVersion } : {},
15631
+ ...input.releasedVersionSource ? { releasedSource: input.releasedVersionSource } : {}
15346
15632
  },
15347
15633
  recipes: PLUGIN_UPDATE_RECIPES
15348
15634
  };
15349
15635
  }
15350
- function renderPluginUpdateReport(report) {
15636
+ function renderPluginVersionBlock(report) {
15351
15637
  const v = report.versions;
15352
15638
  const show = (x) => x ?? "unknown";
15353
15639
  const versionRows = [
@@ -15358,16 +15644,10 @@ function renderPluginUpdateReport(report) {
15358
15644
  ["OpenCode plugin", show(v.opencodePlugin)]
15359
15645
  ];
15360
15646
  const pad = Math.max(...versionRows.map(([label]) => label.length));
15361
- const lines = [
15647
+ return [
15362
15648
  `MMI versions (target release: ${show(v.released)})`,
15363
- ...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`),
15364
- "",
15365
- "Update commands by surface"
15649
+ ...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`)
15366
15650
  ];
15367
- for (const surface of PLUGIN_GUIDE_SURFACES) {
15368
- lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
15369
- }
15370
- return lines;
15371
15651
  }
15372
15652
  function buildDoctorJsonPayload(input) {
15373
15653
  return {
@@ -15703,6 +15983,9 @@ function cursorPluginInstallFix(input) {
15703
15983
  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
15984
  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
15985
  }
15986
+ function cursorSeededFromNothingAdvisoryFix(releasedVersion) {
15987
+ 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}`;
15988
+ }
15706
15989
  function buildCursorPluginInstallCheck(input) {
15707
15990
  const base = {
15708
15991
  ok: true,
@@ -15820,9 +16103,7 @@ function buildHubDeployFreshnessCheck(input) {
15820
16103
  };
15821
16104
  }
15822
16105
  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
16106
  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
16107
  function textHasPlaywrightMcp(content) {
15827
16108
  const normalized = content.replace(/\r\n/g, "\n");
15828
16109
  return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
@@ -15837,45 +16118,10 @@ function textHasPlaywrightVisionCap(content) {
15837
16118
  if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
15838
16119
  return false;
15839
16120
  }
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
16121
  function textHasCanonicalPlaywrightOutputDir(content) {
15857
16122
  const normalized = content.replace(/\r\n/g, "\n");
15858
16123
  return /--output-dir(?:\s*=\s*|["'\s,]+)tmp\/playwright-mcp\b/.test(normalized);
15859
16124
  }
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
16125
  var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
15880
16126
  var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
15881
16127
  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 +16161,12 @@ function buildSelfUpdateHaltPayload(input) {
15915
16161
  checks: input.checks
15916
16162
  };
15917
16163
  }
16164
+ var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
16165
+ var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
16166
+ function selfUpdateHaltSentinelLine(input) {
16167
+ const to = input.report.releasedVersion ?? "latest";
16168
+ return `${DOCTOR_SELF_UPDATE_HALT_SENTINEL} rerunRequired=true updatedTo=${to} rerun="mmi-cli ${input.rerunArgs.join(" ")}"`;
16169
+ }
15918
16170
  var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
15919
16171
  function buildSelfUpdateReexecArgs(opts) {
15920
16172
  const args = ["doctor"];
@@ -16029,7 +16281,7 @@ function doctorHumanLines(input) {
16029
16281
  lines.push("", DOCTOR_VERBOSE_HINT);
16030
16282
  }
16031
16283
  if (!input.verbose) return lines;
16032
- lines.push("", ...renderPluginUpdateReport(input.updateReport));
16284
+ lines.push("", ...renderPluginVersionBlock(input.updateReport));
16033
16285
  lines.push(
16034
16286
  "",
16035
16287
  doctorSummaryLine({ checks: input.checks, updateReport: input.updateReport, shouldApply: input.shouldApply, healedCount: input.healedCount }),
@@ -16102,6 +16354,313 @@ function buildPluginResolvabilityCheck(input) {
16102
16354
  }
16103
16355
  }
16104
16356
 
16357
+ // src/mcp-reconcile.ts
16358
+ var PLAYWRIGHT_MCP_SPEC = {
16359
+ name: "playwright",
16360
+ command: "npx",
16361
+ args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
16362
+ legacyNames: []
16363
+ };
16364
+ var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
16365
+ 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";
16366
+ function extractServerBlock(content, format, spec) {
16367
+ return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
16368
+ }
16369
+ function extractJsonServer(content, spec) {
16370
+ let parsed;
16371
+ try {
16372
+ parsed = JSON.parse(content);
16373
+ } catch {
16374
+ return { kind: "unparseable" };
16375
+ }
16376
+ if (parsed == null || typeof parsed !== "object") return { kind: "unparseable" };
16377
+ const servers = parsed.mcpServers;
16378
+ if (servers == null || typeof servers !== "object") return { kind: "absent" };
16379
+ const bag = servers;
16380
+ for (const name of [spec.name, ...spec.legacyNames]) {
16381
+ if (Object.prototype.hasOwnProperty.call(bag, name)) {
16382
+ return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText: JSON.stringify(bag[name]) };
16383
+ }
16384
+ }
16385
+ return { kind: "absent" };
16386
+ }
16387
+ function extractTomlServer(content, spec) {
16388
+ const blocks = parseTomlBlocks(content.replace(/\r\n/g, "\n").split("\n"));
16389
+ for (const name of [spec.name, ...spec.legacyNames]) {
16390
+ const full = `mcp_servers.${name}`;
16391
+ const block = blocks.find((b) => b.name === full && !b.isArrayTable);
16392
+ if (block) {
16393
+ const blockText = [block.headerRaw, ...block.body].filter((l) => l != null).join("\n");
16394
+ return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText };
16395
+ }
16396
+ }
16397
+ return { kind: "absent" };
16398
+ }
16399
+ function detectMcpState(target, spec) {
16400
+ if (!target.present) return { target, state: "skip", action: "skip", reasons: [] };
16401
+ if (target.format === "claude-cli") return detectClaudeCliMcpState(target, spec);
16402
+ if (target.content == null) {
16403
+ if (target.createWhenFileAbsent) return { target, state: "absent", action: "install", reasons: [] };
16404
+ return { target, state: "skip", action: "skip", reasons: [] };
16405
+ }
16406
+ const extract = extractServerBlock(target.content, target.format, spec);
16407
+ if (extract.kind === "unparseable") {
16408
+ return { target, state: "drifted", action: "reconcile", reasons: ["unparseable"] };
16409
+ }
16410
+ if (extract.kind === "absent") {
16411
+ return { target, state: "absent", action: "install", reasons: [] };
16412
+ }
16413
+ const reasons = [];
16414
+ if (extract.isLegacy) reasons.push("legacy-name");
16415
+ if (textHasPlaywrightVisionCap(extract.blockText)) reasons.push("vision-cap");
16416
+ if (!textHasCanonicalPlaywrightOutputDir(extract.blockText)) reasons.push("output-dir");
16417
+ if (reasons.length === 0) {
16418
+ return { target, state: "healthy", action: "none", reasons: [], matchedName: extract.matchedName };
16419
+ }
16420
+ return { target, state: "drifted", action: "reconcile", reasons, matchedName: extract.matchedName };
16421
+ }
16422
+ function planMcpReconcile(targets, spec) {
16423
+ return targets.map((t) => detectMcpState(t, spec));
16424
+ }
16425
+ function parseClaudeMcpGetOutput(stdout) {
16426
+ const commandMatch = /^\s*Command:\s*(.+)$/m.exec(stdout);
16427
+ if (commandMatch == null) return null;
16428
+ const argsMatch = /^\s*Args:\s*(.*)$/m.exec(stdout);
16429
+ const env = {};
16430
+ const lines = stdout.replace(/\r\n/g, "\n").split("\n");
16431
+ const headerIdx = lines.findIndex((l) => /^\s*Environment:\s*$/.test(l));
16432
+ if (headerIdx !== -1) {
16433
+ for (const line of lines.slice(headerIdx + 1)) {
16434
+ if (line.trim() === "") continue;
16435
+ if (!/^[ \t]/.test(line)) break;
16436
+ const kv = /^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
16437
+ if (kv != null) env[kv[1]] = kv[2];
16438
+ }
16439
+ }
16440
+ return { command: commandMatch[1].trim(), args: (argsMatch?.[1] ?? "").trim(), env };
16441
+ }
16442
+ function detectClaudeCliMcpState(target, spec) {
16443
+ if (target.content == null) {
16444
+ return { target, state: "absent", action: "install", reasons: [] };
16445
+ }
16446
+ const matchedName = target.claudeCliMatchedName ?? spec.name;
16447
+ const isLegacy = matchedName !== spec.name;
16448
+ const parsed = parseClaudeMcpGetOutput(target.content);
16449
+ if (parsed == null) {
16450
+ const reasons2 = isLegacy ? ["legacy-name", "unparseable"] : ["unparseable"];
16451
+ return { target, state: "drifted", action: "reconcile", reasons: reasons2, matchedName };
16452
+ }
16453
+ const reasons = [];
16454
+ if (isLegacy) reasons.push("legacy-name");
16455
+ if (textHasPlaywrightVisionCap(parsed.args)) reasons.push("vision-cap");
16456
+ if (!textHasCanonicalPlaywrightOutputDir(parsed.args)) reasons.push("output-dir");
16457
+ if (reasons.length === 0) return { target, state: "healthy", action: "none", reasons: [], matchedName };
16458
+ return { target, state: "drifted", action: "reconcile", reasons, matchedName };
16459
+ }
16460
+ var CLAUDE_MCP_USER_SCOPE = "user";
16461
+ function planClaudeCliMcpHeal(item, spec) {
16462
+ if (item.action !== "install" && item.action !== "reconcile") return [];
16463
+ const commands = [];
16464
+ if (item.action === "reconcile" && item.matchedName != null) {
16465
+ commands.push({ op: "remove", args: ["remove", item.matchedName, "-s", CLAUDE_MCP_USER_SCOPE] });
16466
+ }
16467
+ const parsed = item.target.content != null ? parseClaudeMcpGetOutput(item.target.content) : null;
16468
+ const envFlags = Object.entries(parsed?.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
16469
+ commands.push({
16470
+ op: "add",
16471
+ args: ["add", spec.name, "-s", CLAUDE_MCP_USER_SCOPE, ...envFlags, "--", spec.command, ...spec.args]
16472
+ });
16473
+ return commands;
16474
+ }
16475
+ function buildMcpReconcileCheck(plan) {
16476
+ const base = {
16477
+ ok: true,
16478
+ label: MCP_RECONCILE_LABEL,
16479
+ fix: MCP_RECONCILE_FIX,
16480
+ severityOverride: "advisory"
16481
+ };
16482
+ const gaps = plan.filter((p) => p.action === "install" || p.action === "reconcile");
16483
+ if (gaps.length === 0) return base;
16484
+ const detail = gaps.map((g) => `${g.target.path} (${g.action === "install" ? "missing" : g.reasons.join("+") || "drifted"})`).join(", ");
16485
+ return {
16486
+ ...base,
16487
+ ok: false,
16488
+ fix: `${MCP_RECONCILE_FIX} \u2014 ${detail}`,
16489
+ mcpGaps: gaps.map((g) => ({ path: g.target.path, action: g.action, reasons: g.reasons }))
16490
+ };
16491
+ }
16492
+ function applyMcpReconcile(targets, spec, opts) {
16493
+ const results = [];
16494
+ for (const item of planMcpReconcile(targets, spec)) {
16495
+ if (item.action !== "install" && item.action !== "reconcile") continue;
16496
+ const t = item.target;
16497
+ if (t.isRepoFile && !opts.repoWritesAllowed) {
16498
+ results.push({ target: t, action: item.action, outcome: "skipped-repo-write" });
16499
+ continue;
16500
+ }
16501
+ const next = t.format === "json" ? renderJsonMcpConfig(t.content, spec) : renderTomlMcpConfig(t.content, spec);
16502
+ if (next == null) {
16503
+ results.push({ target: t, action: item.action, outcome: "unparseable" });
16504
+ continue;
16505
+ }
16506
+ results.push({ target: t, action: item.action, outcome: opts.write(t.path, next) ? "wrote" : "write-failed" });
16507
+ }
16508
+ return results;
16509
+ }
16510
+ function renderJsonMcpConfig(existing, spec) {
16511
+ const canonicalEntry = { command: spec.command, args: [...spec.args] };
16512
+ if (existing == null || existing.trim() === "") {
16513
+ return `${JSON.stringify({ mcpServers: { [spec.name]: canonicalEntry } }, null, 2)}
16514
+ `;
16515
+ }
16516
+ let parsed;
16517
+ try {
16518
+ parsed = JSON.parse(existing);
16519
+ } catch {
16520
+ return null;
16521
+ }
16522
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
16523
+ const obj = parsed;
16524
+ const rawServers = obj.mcpServers;
16525
+ if (rawServers != null && (typeof rawServers !== "object" || Array.isArray(rawServers))) return null;
16526
+ const servers = rawServers ?? {};
16527
+ const prior = servers[spec.name] ?? spec.legacyNames.map((n) => servers[n]).find((v) => v != null);
16528
+ for (const legacy of spec.legacyNames) delete servers[legacy];
16529
+ const priorObj = prior != null && typeof prior === "object" && !Array.isArray(prior) ? prior : {};
16530
+ servers[spec.name] = { ...priorObj, ...canonicalEntry };
16531
+ obj.mcpServers = servers;
16532
+ const text = `${JSON.stringify(obj, null, 2)}
16533
+ `;
16534
+ return existing.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
16535
+ }
16536
+ function isTomlEscaped(line, i) {
16537
+ let n = 0;
16538
+ for (let j = i - 1; j >= 0 && line[j] === "\\"; j--) n += 1;
16539
+ return n % 2 === 1;
16540
+ }
16541
+ function advanceTomlStringState(line, state) {
16542
+ let multi = state;
16543
+ let single = null;
16544
+ let i = 0;
16545
+ while (i < line.length) {
16546
+ if (multi != null) {
16547
+ if (line.startsWith(multi, i) && !(multi === '"""' && isTomlEscaped(line, i))) {
16548
+ multi = null;
16549
+ i += 3;
16550
+ } else {
16551
+ i += 1;
16552
+ }
16553
+ continue;
16554
+ }
16555
+ if (single != null) {
16556
+ if (line[i] === single && !(single === '"' && isTomlEscaped(line, i))) single = null;
16557
+ i += 1;
16558
+ continue;
16559
+ }
16560
+ if (line[i] === "#") break;
16561
+ if (line.startsWith('"""', i)) {
16562
+ multi = '"""';
16563
+ i += 3;
16564
+ continue;
16565
+ }
16566
+ if (line.startsWith("'''", i)) {
16567
+ multi = "'''";
16568
+ i += 3;
16569
+ continue;
16570
+ }
16571
+ if (line[i] === '"' || line[i] === "'") {
16572
+ single = line[i];
16573
+ i += 1;
16574
+ continue;
16575
+ }
16576
+ i += 1;
16577
+ }
16578
+ return multi;
16579
+ }
16580
+ function parseTomlBlocks(lines) {
16581
+ const blocks = [];
16582
+ let current = { headerRaw: null, name: null, isArrayTable: false, body: [] };
16583
+ let multi = null;
16584
+ const tableRe = /^\s*\[\s*([^[\]]+?)\s*\]\s*$/;
16585
+ const arrayTableRe = /^\s*\[\[\s*([^[\]]+?)\s*\]\]\s*$/;
16586
+ for (const line of lines) {
16587
+ if (multi == null) {
16588
+ const am = arrayTableRe.exec(line);
16589
+ const tm = am == null ? tableRe.exec(line) : null;
16590
+ if (am != null || tm != null) {
16591
+ blocks.push(current);
16592
+ current = { headerRaw: line, name: (am ?? tm)[1].trim(), isArrayTable: am != null, body: [] };
16593
+ continue;
16594
+ }
16595
+ }
16596
+ multi = advanceTomlStringState(line, multi);
16597
+ current.body.push(line);
16598
+ }
16599
+ blocks.push(current);
16600
+ return blocks;
16601
+ }
16602
+ function mergeCanonicalTomlBody(body, commandLine, argsLine) {
16603
+ const preserved = [];
16604
+ for (let i = 0; i < body.length; i++) {
16605
+ const line = body[i];
16606
+ const trimmed = line.trim();
16607
+ if (/^command\s*=/.test(trimmed)) continue;
16608
+ if (/^args\s*=/.test(trimmed)) {
16609
+ let open = (line.match(/\[/g) ?? []).length;
16610
+ let close = (line.match(/\]/g) ?? []).length;
16611
+ while (open > close && i + 1 < body.length) {
16612
+ i += 1;
16613
+ open += (body[i].match(/\[/g) ?? []).length;
16614
+ close += (body[i].match(/\]/g) ?? []).length;
16615
+ }
16616
+ continue;
16617
+ }
16618
+ preserved.push(line);
16619
+ }
16620
+ return [commandLine, argsLine, ...preserved];
16621
+ }
16622
+ function renderTomlMcpConfig(existing, spec) {
16623
+ const commandLine = `command = ${JSON.stringify(spec.command)}`;
16624
+ const argsLine = `args = [${spec.args.map((a) => JSON.stringify(a)).join(", ")}]`;
16625
+ const canonicalHeader = `[mcp_servers.${spec.name}]`;
16626
+ const canonicalName = `mcp_servers.${spec.name}`;
16627
+ if (existing == null || existing.trim() === "") {
16628
+ return `${canonicalHeader}
16629
+ ${commandLine}
16630
+ ${argsLine}
16631
+ `;
16632
+ }
16633
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
16634
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
16635
+ const legacyFull = new Set(spec.legacyNames.map((n) => `mcp_servers.${n}`));
16636
+ const all = parseTomlBlocks(lines);
16637
+ if (all.some((b) => b.isArrayTable && b.name != null && (b.name === canonicalName || legacyFull.has(b.name)))) return null;
16638
+ const targetBlock = all.find((b) => !b.isArrayTable && b.name === canonicalName) ?? all.find((b) => !b.isArrayTable && b.name != null && legacyFull.has(b.name));
16639
+ const out = [];
16640
+ for (const b of all) {
16641
+ if (b === targetBlock) {
16642
+ out.push({ headerRaw: canonicalHeader, name: canonicalName, isArrayTable: false, body: mergeCanonicalTomlBody(b.body, commandLine, argsLine) });
16643
+ continue;
16644
+ }
16645
+ if (targetBlock != null && !b.isArrayTable && b.name != null && legacyFull.has(b.name)) continue;
16646
+ out.push(b);
16647
+ }
16648
+ if (targetBlock != null) {
16649
+ const joined = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n");
16650
+ return eol === "\r\n" ? joined.replace(/\n/g, "\r\n") : joined;
16651
+ }
16652
+ const base = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n").replace(/\n+$/, "");
16653
+ const block = `${canonicalHeader}
16654
+ ${commandLine}
16655
+ ${argsLine}`;
16656
+ const combined = base === "" ? `${block}
16657
+ ` : `${base}
16658
+
16659
+ ${block}
16660
+ `;
16661
+ return eol === "\r\n" ? combined.replace(/\n/g, "\r\n") : combined;
16662
+ }
16663
+
16105
16664
  // src/cli-doctor-shared.ts
16106
16665
  var import_node_fs15 = require("node:fs");
16107
16666
  var import_node_path16 = require("node:path");
@@ -16314,20 +16873,22 @@ function readRepoVersion() {
16314
16873
  }
16315
16874
  }
16316
16875
  async function fetchReleasedVersion() {
16317
- try {
16318
- const { stdout } = await execFileP2("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
16319
- return parseManifestVersion(stdout);
16320
- } catch {
16321
- return void 0;
16322
- }
16876
+ return resolveReleasedVersion({
16877
+ npm: async () => (await runHostBin("npm", npmReleasedVersionArgs(), { timeout: NPM_VIEW_TIMEOUT_MS })).stdout,
16878
+ gh: async () => (await execFileP2("gh", pluginManifestVersionArgs(), { timeout: 5e3 })).stdout
16879
+ });
16323
16880
  }
16324
16881
  var NPM_UPDATE_TIMEOUT_MS = 12e4;
16325
16882
  var NPM_VIEW_TIMEOUT_MS = 15e3;
16326
16883
  var PLUGIN_PULL_TIMEOUT_MS = 3e4;
16327
- async function applyVersionAutoUpdate(report, log) {
16328
- const action = versionAutoUpdateAction(report, Boolean(process.env.CLAUDE_PLUGIN_ROOT));
16884
+ async function applyVersionAutoUpdate(report, log, releasedSource) {
16885
+ const action = versionAutoUpdateAction(report, Boolean(process.env.CLAUDE_PLUGIN_ROOT), releasedSource);
16329
16886
  if (action === "none") return { report, applied: "none" };
16330
16887
  const target = report.releasedVersion ?? "latest";
16888
+ if (action === "npm-unreachable") {
16889
+ log(` \u2717 mmi-cli ${report.currentVersion} is stale (released ${target}, resolved via gh) \u2014 npm registry unreachable this pass; when npm is back: npm install -g @mutmutco/cli@latest`);
16890
+ return { report, applied: "none" };
16891
+ }
16331
16892
  if (action === "plugin-pull") {
16332
16893
  try {
16333
16894
  const root = (await execFileP2("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
@@ -16358,7 +16919,20 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
16358
16919
  function runHostBin(bin, args, opts) {
16359
16920
  return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
16360
16921
  }
16361
- function reexecMmiCli(args) {
16922
+ function reexecStrategies(input) {
16923
+ const strategies = [
16924
+ input.platform === "win32" ? { command: "cmd.exe", args: ["/c", "mmi-cli"], kind: "cmd-shim" } : { command: "mmi-cli", args: [], kind: "direct" }
16925
+ ];
16926
+ if (input.argv1) strategies.push({ command: input.execPath, args: [input.argv1], kind: "direct" });
16927
+ return strategies;
16928
+ }
16929
+ var CMD_NOT_RECOGNIZED_EXIT_CODE = 9009;
16930
+ function reexecAttemptOutcome(strategy, code) {
16931
+ if (code < 0) return "strategy-failed";
16932
+ if (strategy.kind === "cmd-shim" && code === CMD_NOT_RECOGNIZED_EXIT_CODE) return "strategy-failed";
16933
+ return "result";
16934
+ }
16935
+ function spawnReexecAttempt(command, args, env) {
16362
16936
  return new Promise((resolve6) => {
16363
16937
  let settled = false;
16364
16938
  const done = (code) => {
@@ -16367,12 +16941,20 @@ function reexecMmiCli(args) {
16367
16941
  resolve6(code);
16368
16942
  }
16369
16943
  };
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 });
16944
+ const child = (0, import_node_child_process10.spawn)(command, args, { stdio: "inherit", env });
16372
16945
  child.on("error", () => done(-1));
16373
16946
  child.on("exit", (code) => done(code ?? 0));
16374
16947
  });
16375
16948
  }
16949
+ async function reexecMmiCli(args) {
16950
+ const strategies = reexecStrategies({ platform: process.platform, argv1: process.argv[1], execPath: process.execPath });
16951
+ const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
16952
+ for (const strategy of strategies) {
16953
+ const code = await spawnReexecAttempt(strategy.command, [...strategy.args, ...args], env);
16954
+ if (reexecAttemptOutcome(strategy, code) === "result") return code;
16955
+ }
16956
+ return -1;
16957
+ }
16376
16958
  function hostBinAvailable(bin) {
16377
16959
  return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
16378
16960
  }
@@ -16392,6 +16974,15 @@ async function runCodexPlugin(args) {
16392
16974
  return false;
16393
16975
  }
16394
16976
  }
16977
+ async function codexEnabledMmiVersion() {
16978
+ if (!await hostBinAvailable("codex")) return void 0;
16979
+ try {
16980
+ const { stdout } = await runHostBin("codex", ["plugin", "list", "--json"], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
16981
+ return codexEnabledPluginVersionFromList(stdout, MMI_PLUGIN_ID);
16982
+ } catch {
16983
+ return void 0;
16984
+ }
16985
+ }
16395
16986
  async function marketplaceAddRefSupported(bin) {
16396
16987
  try {
16397
16988
  const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
@@ -16739,6 +17330,12 @@ function opencodeInstalledVersionForDoctor() {
16739
17330
  diskVersion: readOpencodeAdapterDiskVersion()
16740
17331
  });
16741
17332
  }
17333
+ function opencodePersistedVersionForDoctor() {
17334
+ return pickOpencodeActiveVersion({
17335
+ cacheVersion: readOpencodeLoadedCacheVersion(),
17336
+ diskVersion: readOpencodeAdapterDiskVersion()
17337
+ });
17338
+ }
16742
17339
  function opencodePluginVersionsForReport() {
16743
17340
  return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
16744
17341
  }
@@ -16856,8 +17453,8 @@ function hasNestedMmiChild(versionDir) {
16856
17453
  }
16857
17454
  }
16858
17455
  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) }))
17456
+ return mmiPluginCacheRootSnapshots().flatMap(
17457
+ (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
17458
  );
16862
17459
  }
16863
17460
  function uniqueQuarantineTarget(path2) {
@@ -16933,21 +17530,144 @@ function readTextFile(path2) {
16933
17530
  return null;
16934
17531
  }
16935
17532
  }
16936
- function playwrightMcpConfigSnapshots() {
17533
+ function mcpDirExists(path2) {
17534
+ try {
17535
+ return (0, import_node_fs17.existsSync)(path2);
17536
+ } catch {
17537
+ return false;
17538
+ }
17539
+ }
17540
+ function mcpConfigTargets() {
16937
17541
  const cwd = process.cwd();
16938
17542
  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")
17543
+ const cursorProjectDir = (0, import_node_path17.join)(cwd, ".cursor");
17544
+ const cursorUserDir = (0, import_node_path17.join)(home, ".cursor");
17545
+ const codexDir = (0, import_node_path17.join)(home, ".codex");
17546
+ return [
17547
+ // Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
17548
+ {
17549
+ host: "claude-code",
17550
+ label: "Claude Code project MCP",
17551
+ path: (0, import_node_path17.join)(cwd, ".mcp.json"),
17552
+ format: "json",
17553
+ present: true,
17554
+ // parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
17555
+ isRepoFile: true,
17556
+ createWhenFileAbsent: false,
17557
+ content: readTextFile((0, import_node_path17.join)(cwd, ".mcp.json"))
17558
+ },
17559
+ // Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
17560
+ {
17561
+ host: "cursor-project",
17562
+ label: "Cursor project MCP",
17563
+ path: (0, import_node_path17.join)(cursorProjectDir, "mcp.json"),
17564
+ format: "json",
17565
+ present: mcpDirExists(cursorProjectDir),
17566
+ isRepoFile: true,
17567
+ createWhenFileAbsent: true,
17568
+ content: readTextFile((0, import_node_path17.join)(cursorProjectDir, "mcp.json"))
17569
+ },
17570
+ // Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
17571
+ {
17572
+ host: "cursor-user",
17573
+ label: "Cursor user MCP",
17574
+ path: (0, import_node_path17.join)(cursorUserDir, "mcp.json"),
17575
+ format: "json",
17576
+ present: mcpDirExists(cursorUserDir),
17577
+ isRepoFile: false,
17578
+ createWhenFileAbsent: true,
17579
+ content: readTextFile((0, import_node_path17.join)(cursorUserDir, "mcp.json"))
17580
+ },
17581
+ // Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
17582
+ {
17583
+ host: "codex",
17584
+ label: "Codex user config",
17585
+ path: (0, import_node_path17.join)(codexDir, "config.toml"),
17586
+ format: "toml",
17587
+ present: mcpDirExists(codexDir),
17588
+ isRepoFile: false,
17589
+ createWhenFileAbsent: true,
17590
+ content: readTextFile((0, import_node_path17.join)(codexDir, "config.toml"))
17591
+ }
16944
17592
  ];
16945
- const out = [];
16946
- for (const path2 of candidates) {
16947
- const content = readTextFile(path2);
16948
- if (content != null) out.push({ path: path2, content });
17593
+ }
17594
+ function writeMcpConfigFile(path2, content) {
17595
+ try {
17596
+ (0, import_node_fs17.writeFileSync)(path2, content, "utf8");
17597
+ return true;
17598
+ } catch {
17599
+ return false;
16949
17600
  }
16950
- return out;
17601
+ }
17602
+ var CLAUDE_CLI_MCP_LABEL = "Claude Code MCP store (user scope)";
17603
+ var CLAUDE_CLI_MCP_PATH = "claude mcp (user scope)";
17604
+ async function claudeCliMcpTarget(spec) {
17605
+ const base = {
17606
+ host: "claude-cli",
17607
+ label: CLAUDE_CLI_MCP_LABEL,
17608
+ path: CLAUDE_CLI_MCP_PATH,
17609
+ format: "claude-cli",
17610
+ isRepoFile: false,
17611
+ createWhenFileAbsent: true
17612
+ };
17613
+ if (!await hostBinAvailable("claude")) return { ...base, present: false, content: null };
17614
+ for (const name of [spec.name, ...spec.legacyNames]) {
17615
+ try {
17616
+ const { stdout } = await runHostBin("claude", ["mcp", "get", name], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
17617
+ return { ...base, present: true, content: stdout, claudeCliMatchedName: name };
17618
+ } catch {
17619
+ }
17620
+ }
17621
+ return { ...base, present: true, content: null };
17622
+ }
17623
+ async function runClaudeCliMcpCommands(commands) {
17624
+ for (const cmd of commands) {
17625
+ try {
17626
+ await runHostBin("claude", ["mcp", ...cmd.args], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
17627
+ } catch {
17628
+ return false;
17629
+ }
17630
+ }
17631
+ return true;
17632
+ }
17633
+ async function reconcileMcpRegistrations(input) {
17634
+ if (!input.isOrgRepo) return buildMcpReconcileCheck([]);
17635
+ const snapshot = async () => [...mcpConfigTargets(), await claudeCliMcpTarget(PLAYWRIGHT_MCP_SPEC)];
17636
+ const targets = await snapshot();
17637
+ if (!input.apply) return buildMcpReconcileCheck(planMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC));
17638
+ const fileTargets = targets.filter((t) => t.format !== "claude-cli");
17639
+ const results = applyMcpReconcile(fileTargets, PLAYWRIGHT_MCP_SPEC, {
17640
+ repoWritesAllowed: input.repoWritesAllowed,
17641
+ write: writeMcpConfigFile
17642
+ });
17643
+ for (const r of results) {
17644
+ if (r.outcome === "wrote") {
17645
+ input.onHealed();
17646
+ input.onReloadRequired();
17647
+ const verb = r.action === "install" ? "registered" : "reconciled";
17648
+ input.log(` \u21BB ${verb} Playwright MCP in ${r.target.path} \u2014 restart the host to load it`);
17649
+ } else if (r.outcome === "unparseable") {
17650
+ input.log(` \u26A0 ${r.target.label}: could not safely patch ${r.target.path} (unparseable) \u2014 fix it by hand`);
17651
+ } else if (r.outcome === "write-failed") {
17652
+ input.log(` \u26A0 ${r.target.label}: failed to write ${r.target.path}`);
17653
+ }
17654
+ }
17655
+ const claudeTarget = targets.find((t) => t.format === "claude-cli");
17656
+ if (claudeTarget != null) {
17657
+ const item = detectMcpState(claudeTarget, PLAYWRIGHT_MCP_SPEC);
17658
+ if (item.action === "install" || item.action === "reconcile") {
17659
+ const commands = planClaudeCliMcpHeal(item, PLAYWRIGHT_MCP_SPEC);
17660
+ if (await runClaudeCliMcpCommands(commands)) {
17661
+ input.onHealed();
17662
+ input.onReloadRequired();
17663
+ const verb = item.action === "install" ? "registered" : "reconciled";
17664
+ input.log(` \u21BB ${verb} Playwright MCP via \`claude mcp add\` (${CLAUDE_CLI_MCP_PATH}) \u2014 restart Claude Code to load it`);
17665
+ } else {
17666
+ input.log(` \u26A0 ${CLAUDE_CLI_MCP_LABEL}: \`claude mcp add\` failed for ${PLAYWRIGHT_MCP_SPEC.name} \u2014 run it by hand`);
17667
+ }
17668
+ }
17669
+ }
17670
+ return buildMcpReconcileCheck(planMcpReconcile(await snapshot(), PLAYWRIGHT_MCP_SPEC));
16951
17671
  }
16952
17672
  function strayBrowserArtifactPaths() {
16953
17673
  const cwd = process.cwd();
@@ -16986,7 +17706,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16986
17706
  const checks = [];
16987
17707
  const REWRITE_KEY = "url.https://github.com/.insteadOf";
16988
17708
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
16989
- const [login, pathProbe, releasedVersion, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
17709
+ const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
16990
17710
  githubLogin(),
16991
17711
  execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
16992
17712
  fetchReleasedVersion(),
@@ -16998,6 +17718,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16998
17718
  // not the now-always-defaulted `cfg.sagaApiUrl`. Independent git read — resolved concurrently here.
16999
17719
  readOrigin ? isOrgRepoRoot(readOrigin) : isOrgRepoRoot()
17000
17720
  ]);
17721
+ const releasedVersion = releasedVersionResolution.version;
17001
17722
  const surface = detectSurface(process.env);
17002
17723
  const versionReportProbe = buildVersionLagReport({
17003
17724
  currentVersion: resolveClientVersion(),
@@ -17063,7 +17784,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17063
17784
  });
17064
17785
  let selfUpdatedCli = false;
17065
17786
  if (repairFull && !process.env[DOCTOR_POST_SELF_UPDATE_ENV]) {
17066
- const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m));
17787
+ const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m), releasedVersionResolution.source);
17067
17788
  versionReport = updated.report;
17068
17789
  selfUpdatedCli = updated.applied === "npm";
17069
17790
  }
@@ -17071,13 +17792,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17071
17792
  checks.push(versionReport);
17072
17793
  if (selfUpdatedCli) {
17073
17794
  if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
17074
- const code = await reexecMmiCli(buildSelfUpdateReexecArgs(opts));
17795
+ const reexecArgs = buildSelfUpdateReexecArgs(opts);
17796
+ const code = await reexecMmiCli(reexecArgs);
17075
17797
  if (code >= 0) {
17076
17798
  if (code > 0) process.exitCode = code;
17077
17799
  return;
17078
17800
  }
17079
- if (opts.json) io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
17080
- else io.err(selfUpdateHaltLine(versionReport));
17801
+ process.exitCode = DOCTOR_SELF_UPDATE_HALT_EXIT_CODE;
17802
+ if (opts.json) {
17803
+ io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
17804
+ } else {
17805
+ io.err(selfUpdateHaltLine(versionReport));
17806
+ io.err(selfUpdateHaltSentinelLine({ report: versionReport, rerunArgs: reexecArgs }));
17807
+ }
17081
17808
  return;
17082
17809
  }
17083
17810
  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 +18005,20 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17278
18005
  releasedVersion
17279
18006
  });
17280
18007
  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;
18008
+ const persistedCurrent = () => buildOpencodeVersionCheck({
18009
+ isOrgRepo,
18010
+ installedVersion: opencodePersistedVersionForDoctor(),
18011
+ releasedVersion
18012
+ }).ok;
18013
+ let refreshed = false;
18014
+ let quarantined = false;
18015
+ if (!persistedCurrent()) {
18016
+ refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
18017
+ quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
18018
+ }
18019
+ if (persistedCurrent()) {
18020
+ const wasReinstalled = refreshed || quarantined;
18021
+ opencodeInstalledVersion = opencodePersistedVersionForDoctor() ?? opencodeInstalledVersion;
17285
18022
  opencodeVersionCheck = buildOpencodeVersionCheck({
17286
18023
  isOrgRepo,
17287
18024
  installedVersion: opencodeInstalledVersion,
@@ -17290,7 +18027,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17290
18027
  if (opencodeVersionCheck.ok) {
17291
18028
  markPluginReloadRequired();
17292
18029
  markHealed();
17293
- io.err(` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${opencodeInstalledVersion ?? releasedVersion ?? "latest"} \u2014 ${reloadAction("opencode")} to load it`);
18030
+ const version = opencodeInstalledVersion ?? releasedVersion ?? "latest";
18031
+ 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
18032
  }
17295
18033
  }
17296
18034
  }
@@ -17361,6 +18099,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17361
18099
  return versions.sort((a, b) => compareVersions(b, a))[0];
17362
18100
  };
17363
18101
  const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
18102
+ const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
17364
18103
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
17365
18104
  isOrgRepo,
17366
18105
  roots: mmiPluginCacheRootSnapshots(),
@@ -17393,14 +18132,16 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17393
18132
  };
17394
18133
  }
17395
18134
  checks.push(cacheCleanupCheck);
18135
+ const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
17396
18136
  let codexActiveCacheCheck = buildCodexActiveCacheCheck({
17397
18137
  isOrgRepo,
17398
18138
  releasedVersion,
17399
18139
  codexCacheVersions: codexCacheVersions(),
17400
- codexRecordVersion: codexRecordVersion()
18140
+ codexRecordVersion: codexRecordVersion(),
18141
+ codexActiveVersion: await codexActiveVersion(),
18142
+ codexPresent: canDriveCodex
17401
18143
  });
17402
18144
  if (!codexActiveCacheCheck.ok && repairFull) {
17403
- const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
17404
18145
  if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
17405
18146
  markPluginReloadRequired();
17406
18147
  markHealed();
@@ -17409,7 +18150,9 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17409
18150
  isOrgRepo,
17410
18151
  releasedVersion,
17411
18152
  codexCacheVersions: codexCacheVersions(),
17412
- codexRecordVersion: codexRecordVersion()
18153
+ codexRecordVersion: codexRecordVersion(),
18154
+ codexActiveVersion: await codexActiveVersion(),
18155
+ codexPresent: canDriveCodex
17413
18156
  });
17414
18157
  }
17415
18158
  }
@@ -17420,7 +18163,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17420
18163
  entries: nestedPluginTreeSnapshot()
17421
18164
  });
17422
18165
  if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
17423
- const nestedPaths = nestedPluginTreeCheck.nested.map((n) => n.path);
18166
+ const nestedEntries = nestedPluginTreeCheck.nested;
18167
+ const nestedPaths = nestedEntries.map((n) => n.path);
17424
18168
  if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
17425
18169
  nestedPluginTreeCheck = buildNestedPluginTreeCheck({
17426
18170
  isOrgRepo,
@@ -17428,36 +18172,62 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17428
18172
  entries: nestedPluginTreeSnapshot()
17429
18173
  });
17430
18174
  if (nestedPluginTreeCheck.ok) {
17431
- io.err(` \u21BB cleared self-nested MMI plugin cache tree(s) \u2014 reinstalling plugin\u2026`);
18175
+ io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)`);
17432
18176
  }
17433
- if (await applyPluginHeal("claude", surface, (m) => io.err(m))) {
18177
+ let reinstalledAny = false;
18178
+ const unreinstalled = [];
18179
+ for (const token of [...new Set(nestedEntries.map((n) => n.surface))]) {
18180
+ const bin = token === "codex" ? "codex" : "claude";
18181
+ const canDrive = surfaceToken(surface) === token || await hostBinAvailable(bin);
18182
+ if (canDrive && await applyPluginHeal(token, surface, (m) => io.err(m), { force: true })) {
18183
+ reinstalledAny = true;
18184
+ io.err(` \u21BB reinstalled MMI plugin (${token}) after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
18185
+ } else {
18186
+ unreinstalled.push(token);
18187
+ }
18188
+ }
18189
+ if (reinstalledAny) {
17434
18190
  markPluginReloadRequired();
17435
18191
  markHealed();
17436
- io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
17437
18192
  nestedPluginTreeCheck = buildNestedPluginTreeCheck({
17438
18193
  isOrgRepo,
17439
18194
  isWindows: isWin,
17440
18195
  entries: nestedPluginTreeSnapshot()
17441
18196
  });
17442
18197
  }
18198
+ if (unreinstalled.length > 0 && nestedPluginTreeCheck.ok) {
18199
+ nestedPluginTreeCheck = { ...nestedPluginTreeCheck, ok: false, fix: nestedTreeReinstallGapFix(unreinstalled) };
18200
+ }
17443
18201
  }
17444
18202
  }
17445
18203
  checks.push(nestedPluginTreeCheck);
17446
18204
  const cursorCacheRoot = cursorPluginCacheRoot();
18205
+ const cursorCacheRootExists = (0, import_node_fs17.existsSync)(cursorCacheRoot);
17447
18206
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
18207
+ checks.push(
18208
+ buildCursorPluginCacheCleanupCheck({
18209
+ isOrgRepo,
18210
+ cacheRoot: cursorCacheRoot,
18211
+ pins: cursorPins,
18212
+ releasedVersion
18213
+ })
18214
+ );
17448
18215
  let cursorPluginCheck = buildCursorPluginInstallCheck({
17449
18216
  isOrgRepo,
17450
18217
  surface,
17451
18218
  cacheRoot: cursorCacheRoot,
17452
- cacheRootExists: (0, import_node_fs17.existsSync)(cursorCacheRoot),
18219
+ cacheRootExists: cursorCacheRootExists,
17453
18220
  pins: cursorPins,
17454
18221
  hubCheckout: hubCheckoutForCursorSeed(),
17455
18222
  releasedVersion
17456
18223
  });
17457
18224
  if (!cursorPluginCheck.ok && repairLocal) {
18225
+ const seedingFromNothing = cursorPins.length === 0;
17458
18226
  const seeded = await applyCursorPluginCacheSeed({
17459
18227
  pins: cursorPins,
17460
18228
  releasedVersion,
18229
+ cacheRoot: cursorCacheRoot,
18230
+ cacheRootExists: cursorCacheRootExists,
17461
18231
  hubCheckout: hubCheckoutForCursorSeed(),
17462
18232
  execFileP: execFileP2,
17463
18233
  mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
@@ -17469,14 +18239,26 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17469
18239
  isOrgRepo,
17470
18240
  surface,
17471
18241
  cacheRoot: cursorCacheRoot,
17472
- cacheRootExists: (0, import_node_fs17.existsSync)(cursorCacheRoot),
18242
+ cacheRootExists: cursorCacheRootExists,
17473
18243
  pins: cursorPins,
17474
18244
  hubCheckout: hubCheckoutForCursorSeed(),
17475
18245
  releasedVersion
17476
18246
  });
17477
18247
  if (cursorPluginCheck.ok) {
17478
18248
  markHealed();
17479
- io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
18249
+ if (seedingFromNothing) {
18250
+ io.err(
18251
+ ` \u21BB seeded Cursor MMI plugin cache from nothing \u2192 ${releasedVersion ?? "latest"} (cache only \u2014 Team Marketplace registration not verified, #2409) \u2014 ${reloadAction(surface)}`
18252
+ );
18253
+ cursorPluginCheck = {
18254
+ ...cursorPluginCheck,
18255
+ ok: false,
18256
+ severityOverride: "advisory",
18257
+ fix: cursorSeededFromNothingAdvisoryFix(releasedVersion)
18258
+ };
18259
+ } else {
18260
+ io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
18261
+ }
17480
18262
  }
17481
18263
  }
17482
18264
  }
@@ -17497,20 +18279,30 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17497
18279
  mmiCliOnPath: onPath
17498
18280
  })
17499
18281
  );
17500
- if (runExtended) {
17501
- const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
17502
- checks.push(
17503
- buildPlaywrightMcpVisionCapCheck({
17504
- isOrgRepo,
17505
- configs: playwrightMcpConfigs
17506
- })
17507
- );
18282
+ if (!opts.banner) {
17508
18283
  checks.push(
17509
- buildPlaywrightMcpOutputDirCheck({
18284
+ await reconcileMcpRegistrations({
17510
18285
  isOrgRepo,
17511
- configs: playwrightMcpConfigs
18286
+ apply: Boolean(opts.apply),
18287
+ repoWritesAllowed,
18288
+ log: (m) => io.err(m),
18289
+ onHealed: markHealed,
18290
+ onReloadRequired: markPluginReloadRequired
17512
18291
  })
17513
18292
  );
18293
+ }
18294
+ const sessionPluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
18295
+ const sessionCacheSurface = sessionPluginRoot?.includes(".codex") ? "codex" : sessionPluginRoot?.includes(".claude") ? "claude" : void 0;
18296
+ const sessionCacheVersions = sessionCacheSurface ? mmiPluginCacheRootSnapshots().filter((r) => r.surface === sessionCacheSurface).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name)) : [];
18297
+ checks.push(
18298
+ buildSessionSkillRootCheck({
18299
+ isOrgRepo,
18300
+ surface,
18301
+ pluginRoot: sessionPluginRoot,
18302
+ cacheVersions: sessionCacheVersions
18303
+ })
18304
+ );
18305
+ if (runExtended) {
17514
18306
  checks.push(
17515
18307
  buildBrowserArtifactsCheck({
17516
18308
  isOrgRepo,
@@ -17569,7 +18361,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17569
18361
  codexPluginVersions: sourceVersions("codex"),
17570
18362
  codexCacheVersions: cacheVersionsFor("codex"),
17571
18363
  opencodePluginVersions: opencodePluginVersionsForReport(),
17572
- releasedVersion
18364
+ releasedVersion,
18365
+ releasedVersionSource: releasedVersionResolution.source
17573
18366
  });
17574
18367
  }
17575
18368
  });
@@ -19606,7 +20399,7 @@ board.command("prune-priority-labels").description("remove retired priority:* la
19606
20399
  });
19607
20400
  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
20401
  try {
19609
- const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
20402
+ const result = await moveBoardItem({ config: await loadConfigForBoardSelector(issueRef, o.repo), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
19610
20403
  if (o.json) return console.log(JSON.stringify(result));
19611
20404
  console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
19612
20405
  } catch (e) {
@@ -20106,6 +20899,29 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
20106
20899
  }
20107
20900
  if (!audit.ok) process.exitCode = 1;
20108
20901
  });
20902
+ var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
20903
+ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git via scripts/wiki-publish.mjs; the token stays in memory (never printed/committed). Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (pagesDir, o) => {
20904
+ const repo = await resolveRepo(o.repo);
20905
+ if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
20906
+ const cfg = await loadConfig();
20907
+ const ok = await wikiPublish(
20908
+ {
20909
+ mint: (r) => mintWikiToken(r, registryClientDeps(cfg)),
20910
+ // scripts/wiki-publish.mjs lives at the repo root; Lane B runs there. Resolve against cwd.
20911
+ scriptPath: () => (0, import_node_path18.resolve)(process.cwd(), "scripts", "wiki-publish.mjs"),
20912
+ scriptExists: (p) => (0, import_node_fs18.existsSync)(p),
20913
+ spawn: (command, args, env) => {
20914
+ const r = (0, import_node_child_process11.spawnSync)(command, args, { stdio: "inherit", env });
20915
+ if (r.error) throw r.error;
20916
+ return r.status ?? 1;
20917
+ },
20918
+ log: (msg) => console.log(msg),
20919
+ err: (msg) => console.error(msg)
20920
+ },
20921
+ { repo, pagesDir }
20922
+ );
20923
+ if (!ok) process.exitCode = 1;
20924
+ });
20109
20925
  var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
20110
20926
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
20111
20927
  if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");