@mutmutco/cli 3.95.0 → 3.96.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 +377 -34
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -16990,7 +16990,7 @@ async function boardDoctor(options, deps = {}) {
16990
16990
  };
16991
16991
  await Promise.all(Array.from({ length: Math.min(concurrency, suspicious.length) }, () => ghostWorker()));
16992
16992
  }
16993
- const claimedItems = collected.items.filter(
16993
+ const claimedItems = options.skipClaimLiveness ? [] : collected.items.filter(
16994
16994
  (item) => item.contentType === "Issue" && item.assignees.length > 0 && item.status === "In Progress"
16995
16995
  );
16996
16996
  const liveClaims = [];
@@ -19342,6 +19342,12 @@ var INDEXABLE_EXT = /* @__PURE__ */ new Set([
19342
19342
  ".html"
19343
19343
  ]);
19344
19344
  var SYMBOL_RE = /^(?:export\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
19345
+ function repoIndexPathTieBreak(path2) {
19346
+ const p = path2.replace(/\\/g, "/");
19347
+ if (/(^|\/)__tests__\//.test(p) || /(^|\/)tests?\//.test(p)) return 1;
19348
+ if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
19349
+ return 0;
19350
+ }
19345
19351
  function repoIndexStorePath(cwd) {
19346
19352
  return repoRuntimeStatePath(cwd, "repo-index", "index.json");
19347
19353
  }
@@ -19552,7 +19558,9 @@ function searchRepoIndex(idx, query, limit = 20) {
19552
19558
  }
19553
19559
  }
19554
19560
  }
19555
- hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
19561
+ hits.sort(
19562
+ (a, b) => b.score - a.score || repoIndexPathTieBreak(a.path) - repoIndexPathTieBreak(b.path) || a.path.localeCompare(b.path)
19563
+ );
19556
19564
  const seen = /* @__PURE__ */ new Set();
19557
19565
  const out = [];
19558
19566
  for (const h of hits) {
@@ -19568,10 +19576,10 @@ function inferRepoSlug(cwd, exec = import_node_child_process11.execFileSync) {
19568
19576
  try {
19569
19577
  const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
19570
19578
  const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
19571
- if (m?.[1]) return m[1];
19579
+ if (m?.[1]) return m[1].toLowerCase();
19572
19580
  } catch {
19573
19581
  }
19574
- return (0, import_node_path20.basename)(cwd) || "local";
19582
+ return ((0, import_node_path20.basename)(cwd) || "local").toLowerCase();
19575
19583
  }
19576
19584
 
19577
19585
  // src/repo-index-cloud-client.ts
@@ -19718,8 +19726,11 @@ var import_node_child_process12 = require("node:child_process");
19718
19726
  var MAX_EMBED_BACKFILL_ROUNDS = 40;
19719
19727
  function normalizeRepo(raw) {
19720
19728
  const t = raw.trim().replace(/\.git$/, "");
19721
- if (t.includes("/")) return t;
19722
- return `mutmutco/${t}`;
19729
+ if (t.includes("/")) {
19730
+ const [owner, name] = t.split("/");
19731
+ return `${(owner || "").toLowerCase()}/${(name || "").toLowerCase()}`;
19732
+ }
19733
+ return `mutmutco/${t.toLowerCase()}`;
19723
19734
  }
19724
19735
  function rosterRepos(projects) {
19725
19736
  const set = /* @__PURE__ */ new Set();
@@ -19730,10 +19741,11 @@ function rosterRepos(projects) {
19730
19741
  return [...set].sort((a, b) => a.localeCompare(b));
19731
19742
  }
19732
19743
  function shallowClone(repo, dest, token) {
19744
+ const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
19733
19745
  (0, import_node_child_process12.execFileSync)(
19734
19746
  "git",
19735
- ["-c", `http.extraHeader=Authorization: Bearer ${token}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
19736
- { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
19747
+ ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
19748
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
19737
19749
  );
19738
19750
  }
19739
19751
  async function syncEstateRepoIndex(opts) {
@@ -27866,6 +27878,11 @@ async function recommendNext(repo, deps) {
27866
27878
  const top = sorted[0];
27867
27879
  return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
27868
27880
  }
27881
+ function requiredEstateCommandsPresent(commandNames) {
27882
+ const required = ["repo-index"];
27883
+ const present = new Set(commandNames);
27884
+ return required.filter((name) => !present.has(name));
27885
+ }
27869
27886
  function onboardPluginGate(deps) {
27870
27887
  const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
27871
27888
  if (!registered) {
@@ -27883,8 +27900,13 @@ function onboardPluginGate(deps) {
27883
27900
  if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
27884
27901
  return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
27885
27902
  }
27886
- async function collectOnboardStatus() {
27903
+ async function collectOnboardStatus(opts = {}) {
27887
27904
  const cfg = await loadConfig();
27905
+ const missingEstate = requiredEstateCommandsPresent(opts.commandNames ?? []);
27906
+ const estateCli = missingEstate.length === 0 ? { ok: true, detail: "estate commands present (repo-index)" } : {
27907
+ ok: false,
27908
+ detail: `global CLI missing ${missingEstate.join(", ")} \u2014 run mmi-cli doctor to heal`
27909
+ };
27888
27910
  let track = "unknown";
27889
27911
  try {
27890
27912
  const revParse2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
@@ -27938,7 +27960,9 @@ async function collectOnboardStatus() {
27938
27960
  }
27939
27961
  }
27940
27962
  let nextCommand = "mmi-cli doctor";
27941
- if (!cfg.sagaApiUrl) {
27963
+ if (!estateCli.ok) {
27964
+ nextCommand = "mmi-cli doctor \u2014 heal global CLI (missing repo-index)";
27965
+ } else if (!cfg.sagaApiUrl) {
27942
27966
  nextCommand = "mmi-cli doctor \u2014 fix Hub API URL configuration first";
27943
27967
  } else if (!registry2.ok) {
27944
27968
  nextCommand = "mmi-cli org project set <owner/repo>";
@@ -27961,7 +27985,7 @@ async function collectOnboardStatus() {
27961
27985
  readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs37.readFileSync),
27962
27986
  readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs37.readFileSync)
27963
27987
  });
27964
- return { track, board, registry: registry2, secrets, plugin, nextCommand };
27988
+ return { track, board, registry: registry2, secrets, plugin, estateCli, nextCommand };
27965
27989
  }
27966
27990
  function registerDiscoveryCommands(program3) {
27967
27991
  program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
@@ -27998,7 +28022,8 @@ function registerDiscoveryCommands(program3) {
27998
28022
  });
27999
28023
  program3.command("onboard").description("report repo track/board/secrets/registry readiness and the concrete next command").option("--json", "machine-readable output").action(async (o) => {
28000
28024
  try {
28001
- const report = await collectOnboardStatus();
28025
+ const commandNames = program3.commands.map((c) => c.name());
28026
+ const report = await collectOnboardStatus({ commandNames });
28002
28027
  if (o.json) {
28003
28028
  console.log(JSON.stringify(report, null, 2));
28004
28029
  } else {
@@ -28007,6 +28032,7 @@ function registerDiscoveryCommands(program3) {
28007
28032
  console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
28008
28033
  console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
28009
28034
  console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
28035
+ console.log(`Estate CLI: ${report.estateCli.ok ? "\u2713" : "\u2717"} ${report.estateCli.detail}`);
28010
28036
  console.log(`
28011
28037
  Next command: ${report.nextCommand}`);
28012
28038
  }
@@ -30056,7 +30082,7 @@ async function runDoctorClean(opts, io, deps) {
30056
30082
  if (!opts.json) io.log(`\u21BB ${line}`);
30057
30083
  };
30058
30084
  const healStep = (message) => {
30059
- if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
30085
+ if (!opts.json && opts.verbose) io.log(`${VERBOSE_INDENT}${message.trim()}`);
30060
30086
  };
30061
30087
  const releasedNote = deps.releasedVersionNote?.();
30062
30088
  let pluginHealed = false;
@@ -30091,30 +30117,63 @@ async function runDoctorClean(opts, io, deps) {
30091
30117
  }
30092
30118
  }
30093
30119
  async function runCliRow() {
30120
+ const missing = deps.missingCliCommands?.() ?? [];
30094
30121
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
30095
30122
  const cliReport = buildVersionLagReport(cliInput);
30096
- if (!(applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm")) {
30123
+ const capabilityGap = missing.length > 0 && Boolean(cliReport.releasedVersion);
30124
+ const shouldUpdate = Boolean(
30125
+ applyEnv && deps.updateCli && (versionAutoUpdateAction(cliReport) === "npm" || capabilityGap)
30126
+ );
30127
+ if (!shouldUpdate) {
30097
30128
  const cli = checkCliVersion(cliInput, releasedNote);
30098
- if (cli) emitNow(cli);
30129
+ if (cli) {
30130
+ if (missing.length) {
30131
+ emitNow({
30132
+ ...cli,
30133
+ ok: false,
30134
+ detail: cli.ok ? `missing commands: ${missing.join(", ")}` : `${cli.detail ?? ""}`.trim() || `missing commands: ${missing.join(", ")}`,
30135
+ fix: `run \`mmi-cli doctor\` to heal the global CLI (missing: ${missing.join(", ")}), or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
30136
+ verbose: [...cli.verbose ?? [], `missing commands: ${missing.join(", ")}`]
30137
+ });
30138
+ } else {
30139
+ emitNow(cli);
30140
+ }
30141
+ } else if (missing.length) {
30142
+ emitNow({
30143
+ id: "cli-version",
30144
+ ok: false,
30145
+ label: "mmi-cli",
30146
+ detail: `missing commands: ${missing.join(", ")}`,
30147
+ fix: `run \`mmi-cli doctor\` to heal the global CLI, or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
30148
+ verbose: [`missing commands: ${missing.join(", ")}`]
30149
+ });
30150
+ }
30099
30151
  return;
30100
30152
  }
30101
- healIntent(`mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} via npm install -g`);
30102
- const heal = await deps.updateCli(cliReport.releasedVersion, healStep);
30103
- const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
30153
+ const target = cliReport.releasedVersion;
30154
+ const intent = capabilityGap && cliReport.ok ? `mmi-cli \u2014 self-updating to ${target} via npm install -g (missing commands: ${missing.join(", ")})` : `mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${target} via npm install -g`;
30155
+ healIntent(intent);
30156
+ const heal = await deps.updateCli(target, healStep);
30157
+ const healEvidence = [
30158
+ `running: ${cliReport.currentVersion}`,
30159
+ `published: ${target ?? "(unknown)"}`,
30160
+ `heal: ${heal.detail}`,
30161
+ ...missing.length ? [`missing commands before heal: ${missing.join(", ")}`] : []
30162
+ ];
30104
30163
  emitNow(heal.ok ? {
30105
30164
  id: "cli-version",
30106
30165
  ok: true,
30107
30166
  label: "mmi-cli",
30108
- detail: `${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
30167
+ detail: capabilityGap && cliReport.ok ? `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g (was missing: ${missing.join(", ")}); the next mmi-cli invocation runs the new version` : `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
30109
30168
  verbose: healEvidence
30110
30169
  } : {
30111
30170
  id: "cli-version",
30112
30171
  ok: false,
30113
30172
  label: "mmi-cli",
30114
- detail: `${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
30173
+ detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
30115
30174
  // #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
30116
30175
  ...heal.skipped ? { reportOnly: true } : {},
30117
- fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
30176
+ fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(target)}\``,
30118
30177
  verbose: healEvidence
30119
30178
  });
30120
30179
  }
@@ -30177,8 +30236,10 @@ async function runDoctorClean(opts, io, deps) {
30177
30236
  }
30178
30237
  async function runDocsIndexRow() {
30179
30238
  let probe;
30239
+ let root;
30180
30240
  try {
30181
- probe = deps.docsIndexState(await deps.repoRoot());
30241
+ root = await deps.repoRoot();
30242
+ probe = deps.docsIndexState(root);
30182
30243
  } catch (e) {
30183
30244
  const message = e instanceof Error ? e.message : String(e);
30184
30245
  emitNow({
@@ -30194,10 +30255,44 @@ async function runDoctorClean(opts, io, deps) {
30194
30255
  });
30195
30256
  return;
30196
30257
  }
30258
+ let healedWrite = false;
30259
+ if (applyRepo && probe?.drift && deps.healDocsIndex) {
30260
+ healIntent("docs index \u2014 regenerating docs/index.md");
30261
+ try {
30262
+ probe = deps.healDocsIndex(root);
30263
+ healedWrite = !probe.drift;
30264
+ } catch (e) {
30265
+ const message = e instanceof Error ? e.message : String(e);
30266
+ emitNow({
30267
+ ok: false,
30268
+ id: "docs-index",
30269
+ label: "docs index",
30270
+ detail: `heal failed \u2014 ${message}`,
30271
+ fix: "run `mmi-cli docs index --write` and commit docs/index.md",
30272
+ verbose: [`healDocsIndex threw: ${message}`]
30273
+ });
30274
+ return;
30275
+ }
30276
+ }
30197
30277
  const docs2 = checkDocsIndex(probe);
30198
- if (docs2) emitNow(docs2);
30278
+ if (!docs2) return;
30279
+ if (healedWrite) {
30280
+ emitNow({
30281
+ ...docs2,
30282
+ detail: "rewrote docs/index.md (working-tree heal \u2014 commit still needed)"
30283
+ });
30284
+ restartPending = true;
30285
+ return;
30286
+ }
30287
+ emitNow(docs2);
30199
30288
  }
30200
30289
  async function runRepoIndexRow() {
30290
+ const missing = deps.missingCliCommands?.() ?? [];
30291
+ if (missing.includes("repo-index")) {
30292
+ const row2 = checkRepoIndexCloud({ kind: "command-absent" });
30293
+ if (row2) emitNow(row2);
30294
+ return;
30295
+ }
30201
30296
  if (lane.preflight && !opts.self && !lane.full) {
30202
30297
  emitNow({
30203
30298
  ok: true,
@@ -30227,8 +30322,176 @@ async function runDoctorClean(opts, io, deps) {
30227
30322
  const row = checkRepoIndexCloud(probe);
30228
30323
  if (row) emitNow(row);
30229
30324
  }
30325
+ async function runBoardDoctorRow() {
30326
+ if (!deps.boardDoctorFix) return;
30327
+ try {
30328
+ const result = await deps.boardDoctorFix({ fix: applyRepo });
30329
+ if (!result) {
30330
+ emitNow({
30331
+ ok: true,
30332
+ warn: true,
30333
+ reportOnly: true,
30334
+ id: "board-doctor",
30335
+ label: "board doctor",
30336
+ detail: "skipped",
30337
+ verbose: ["boardDoctorFix returned undefined"]
30338
+ });
30339
+ return;
30340
+ }
30341
+ if (result.timedOut) {
30342
+ emitNow({
30343
+ ok: false,
30344
+ warn: true,
30345
+ reportOnly: true,
30346
+ id: "board-doctor",
30347
+ label: "board doctor",
30348
+ detail: "timed out",
30349
+ fix: "run `mmi-cli board doctor --fix`",
30350
+ verbose: [`scanned before timeout: ${result.scanned}`, `findings: ${result.findings}`]
30351
+ });
30352
+ return;
30353
+ }
30354
+ const allFixed = applyRepo && result.findings > 0 && result.failed === 0 && result.fixed >= result.findings;
30355
+ const ok = result.findings === 0 || Boolean(allFixed);
30356
+ emitNow(ok ? {
30357
+ ok: true,
30358
+ id: "board-doctor",
30359
+ label: "board doctor",
30360
+ detail: `scanned ${result.scanned}; fixed ${result.fixed}`,
30361
+ verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
30362
+ } : {
30363
+ ok: false,
30364
+ // Gate when fix was attempted and mechanical findings remain; detect-only stays visible but
30365
+ // report-only would hide a red that `board doctor --fix` clears — prefer gate after a failed fix.
30366
+ reportOnly: !applyRepo,
30367
+ id: "board-doctor",
30368
+ label: "board doctor",
30369
+ detail: `scanned ${result.scanned}; ${result.findings} findings; fixed ${result.fixed}; failed ${result.failed}`,
30370
+ fix: "run `mmi-cli board doctor --fix`",
30371
+ verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
30372
+ });
30373
+ } catch (e) {
30374
+ const message = e instanceof Error ? e.message : String(e);
30375
+ emitNow({
30376
+ ok: true,
30377
+ warn: true,
30378
+ reportOnly: true,
30379
+ id: "board-doctor",
30380
+ label: "board doctor",
30381
+ detail: `skipped \u2014 ${message}`,
30382
+ verbose: [`boardDoctorFix threw: ${message}`]
30383
+ });
30384
+ }
30385
+ }
30386
+ async function runSchedulesDriftRow() {
30387
+ if (!deps.schedulesDriftState) return;
30388
+ try {
30389
+ const state = await deps.schedulesDriftState();
30390
+ if (!state) {
30391
+ emitNow({
30392
+ ok: true,
30393
+ warn: true,
30394
+ reportOnly: true,
30395
+ id: "schedules-drift",
30396
+ label: "schedules drift",
30397
+ detail: "skipped",
30398
+ verbose: ["schedulesDriftState returned undefined"]
30399
+ });
30400
+ return;
30401
+ }
30402
+ if (state.timedOut) {
30403
+ emitNow({
30404
+ ok: false,
30405
+ warn: true,
30406
+ reportOnly: true,
30407
+ id: "schedules-drift",
30408
+ label: "schedules drift",
30409
+ detail: "timed out",
30410
+ fix: "run `mmi-cli org schedules` / `org schedules register`",
30411
+ verbose: ["schedulesDriftState timed out"]
30412
+ });
30413
+ return;
30414
+ }
30415
+ if (state.incomplete.length) {
30416
+ emitNow({
30417
+ ok: true,
30418
+ warn: true,
30419
+ reportOnly: true,
30420
+ id: "schedules-drift",
30421
+ label: "schedules drift",
30422
+ detail: `incomplete \u2014 ${state.incomplete.join("; ")}`,
30423
+ fix: "run `mmi-cli org schedules` / `org schedules register`",
30424
+ verbose: state.incomplete
30425
+ });
30426
+ return;
30427
+ }
30428
+ if (state.driftLines.length === 0) {
30429
+ emitNow({
30430
+ ok: true,
30431
+ reportOnly: true,
30432
+ id: "schedules-drift",
30433
+ label: "schedules drift",
30434
+ detail: "no drift",
30435
+ verbose: ["no drift lines"]
30436
+ });
30437
+ return;
30438
+ }
30439
+ emitNow({
30440
+ ok: false,
30441
+ reportOnly: true,
30442
+ id: "schedules-drift",
30443
+ label: "schedules drift",
30444
+ detail: `${state.driftLines.length} drift line(s)`,
30445
+ fix: "run `mmi-cli org schedules` / `org schedules register`",
30446
+ verbose: state.driftLines
30447
+ });
30448
+ } catch (e) {
30449
+ const message = e instanceof Error ? e.message : String(e);
30450
+ emitNow({
30451
+ ok: true,
30452
+ warn: true,
30453
+ reportOnly: true,
30454
+ id: "schedules-drift",
30455
+ label: "schedules drift",
30456
+ detail: `skipped \u2014 ${message}`,
30457
+ verbose: [`schedulesDriftState threw: ${message}`]
30458
+ });
30459
+ }
30460
+ }
30230
30461
  async function runHousekeeperRows() {
30231
30462
  const repoRoot2 = await deps.repoRoot();
30463
+ if (applyRepo && deps.sweepDeferred) {
30464
+ try {
30465
+ const sweep = await deps.sweepDeferred();
30466
+ const detailParts = [];
30467
+ if (sweep.removed.length) detailParts.push(`removed ${sweep.removed.length}`);
30468
+ if (sweep.stillQueued) detailParts.push(`${sweep.stillQueued} still queued`);
30469
+ if (sweep.skipped) detailParts.push(`${sweep.skipped} skipped`);
30470
+ emitNow({
30471
+ id: "deferred-worktrees",
30472
+ ok: true,
30473
+ label: "deferred worktrees",
30474
+ detail: detailParts.length ? detailParts.join("; ") : "nothing queued",
30475
+ verbose: [
30476
+ ...sweep.removed.map((p) => `removed: ${p}`),
30477
+ `stillQueued: ${sweep.stillQueued}`,
30478
+ `skipped: ${sweep.skipped}`
30479
+ ]
30480
+ });
30481
+ if (sweep.removed.length) restartPending = true;
30482
+ } catch (e) {
30483
+ const message = e instanceof Error ? e.message : String(e);
30484
+ emitNow({
30485
+ id: "deferred-worktrees",
30486
+ ok: true,
30487
+ warn: true,
30488
+ reportOnly: true,
30489
+ label: "deferred worktrees",
30490
+ detail: `sweep failed (fail-soft) \u2014 ${message}`,
30491
+ verbose: [`sweepDeferred threw: ${message}`]
30492
+ });
30493
+ }
30494
+ }
30232
30495
  try {
30233
30496
  const plan = await deps.gcPlan("origin", 200);
30234
30497
  if (applyRepo) {
@@ -30285,9 +30548,7 @@ async function runDoctorClean(opts, io, deps) {
30285
30548
  emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor` (without --no-repo-writes)", verbose: scratchEvidence });
30286
30549
  }
30287
30550
  }
30288
- const table = [
30289
- // Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
30290
- // below is being measured with (#3954).
30551
+ const prefix = [
30291
30552
  { id: "plugin", when: true, run: runPluginRow },
30292
30553
  { id: "cli-version", when: true, run: runCliRow },
30293
30554
  { id: "github-auth", when: true, run: runGithubAuthRow },
@@ -30296,18 +30557,22 @@ async function runDoctorClean(opts, io, deps) {
30296
30557
  { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
30297
30558
  { id: "plugin-cache", when: true, run: runPluginCacheRow },
30298
30559
  { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
30299
- { id: "marketplace", when: true, run: runMarketplaceRows },
30300
- // A `docs/` tree walk plus one batched `git check-ignore` — cheap next to the two rows below it, but a
30301
- // disk walk and a subprocess all the same, so full lane only (#4091).
30560
+ { id: "marketplace", when: true, run: runMarketplaceRows }
30561
+ ];
30562
+ for (const entry of prefix) if (entry.when) await entry.run();
30563
+ const parallel = [
30302
30564
  { id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
30303
30565
  // Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
30304
30566
  { id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
30305
- // The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
30306
- // and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
30567
+ { id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
30568
+ { id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
30569
+ ];
30570
+ await Promise.all(parallel.filter((entry) => entry.when).map((entry) => entry.run()));
30571
+ const suffix = [
30307
30572
  { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
30308
30573
  { id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
30309
30574
  ];
30310
- for (const entry of table) if (entry.when) await entry.run();
30575
+ for (const entry of suffix) if (entry.when) await entry.run();
30311
30576
  const exitCode = doctorReportExitCode(checks);
30312
30577
  if (opts.json) {
30313
30578
  const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
@@ -30767,6 +31032,84 @@ function mmiDoctorDeps(opts = {}) {
30767
31032
  const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
30768
31033
  return { drift, docCount: listDocs().length };
30769
31034
  },
31035
+ // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
31036
+ healDocsIndex: (root) => {
31037
+ if (!(0, import_node_fs39.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
31038
+ const real = createDocsIndexDeps(root);
31039
+ let docs2;
31040
+ const listDocs = () => docs2 ??= real.listDocs();
31041
+ docsIndex({ ...real, listDocs }, { check: false });
31042
+ const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
31043
+ return { drift, docCount: listDocs().length };
31044
+ },
31045
+ // #4169: mirror `worktree gc --apply` — sweep deferred removals before applyGcPlan.
31046
+ sweepDeferred: async () => {
31047
+ const deferredStore = await createDeferredWorktreeStore();
31048
+ const removalContext = await currentWorktreeRemovalContext("doctor deferred sweep");
31049
+ const result = await sweepDeferredWorktrees(
31050
+ deferredStore,
31051
+ // #2841: same `-c core.fsmonitor=false` guard as `worktree gc --apply` / sweep-deferred.
31052
+ worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31053
+ removalContext
31054
+ );
31055
+ return {
31056
+ removed: result.removed,
31057
+ stillQueued: result.stillDeferred.length,
31058
+ skipped: result.skipped.length
31059
+ };
31060
+ },
31061
+ // #4170: light board doctor with 15s ceiling. skipClaimLiveness keeps Phase 3 off.
31062
+ boardDoctorFix: async ({ fix } = {}) => {
31063
+ const BOARD_DOCTOR_TIMEOUT_MS = 15e3;
31064
+ try {
31065
+ const cfg = await loadConfigForRepo();
31066
+ const work = boardDoctor({
31067
+ config: cfg,
31068
+ fix: Boolean(fix),
31069
+ skipClaimLiveness: true
31070
+ });
31071
+ const raced = await Promise.race([
31072
+ work.then((r) => ({ ...r, timedOut: false })),
31073
+ new Promise((resolve5) => {
31074
+ setTimeout(() => resolve5({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
31075
+ })
31076
+ ]);
31077
+ if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
31078
+ return {
31079
+ scanned: raced.scanned,
31080
+ findings: raced.findings,
31081
+ fixed: raced.fixed,
31082
+ failed: raced.failed
31083
+ };
31084
+ } catch {
31085
+ return void 0;
31086
+ }
31087
+ },
31088
+ // #4171: schedules drift notebook — report-only, ~20s ceiling.
31089
+ schedulesDriftState: async () => {
31090
+ const SCHEDULES_DRIFT_TIMEOUT_MS = 2e4;
31091
+ try {
31092
+ const raced = await Promise.race([
31093
+ fetchNotebook().then((nb) => ({
31094
+ driftLines: nb.drift,
31095
+ incomplete: nb.incomplete,
31096
+ timedOut: false
31097
+ })),
31098
+ new Promise((resolve5) => {
31099
+ setTimeout(() => resolve5({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
31100
+ })
31101
+ ]);
31102
+ return raced;
31103
+ } catch {
31104
+ return void 0;
31105
+ }
31106
+ },
31107
+ // #4173: estate commands absent from the running Commander program (stale global CLI).
31108
+ missingCliCommands: () => {
31109
+ const required = ["repo-index"];
31110
+ const names = new Set((opts.program?.commands ?? []).map((c) => c.name()));
31111
+ return required.filter((n) => !names.has(n));
31112
+ },
30770
31113
  // #4156: short-timeout Hub status for the current repo. Fail-soft ? never throws to doctor.
30771
31114
  repoIndexCloudState: async (root) => {
30772
31115
  const local = repoIndexStatus(root);
@@ -33801,7 +34144,7 @@ program2.command("doctor").description("heal CLI/plugin wiring and clean up repo
33801
34144
  verbose: opts.verbose
33802
34145
  },
33803
34146
  consoleIo,
33804
- mmiDoctorDeps()
34147
+ mmiDoctorDeps({ program: program2 })
33805
34148
  );
33806
34149
  });
33807
34150
  program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
@@ -33944,7 +34287,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
33944
34287
  // #3485 item 7: the ONLY lane that throttles the npm read ? it runs on every session start, on a
33945
34288
  // blocking hook. Every interactive lane still reads live.
33946
34289
  doctor: async (io) => {
33947
- await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
34290
+ await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true, program: program2 }));
33948
34291
  }
33949
34292
  });
33950
34293
  await runSessionStart(parallel, sequential, bannerIo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.95.0",
3
+ "version": "3.96.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",