@mutmutco/cli 3.139.1 → 3.139.3

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.
package/dist/main.cjs CHANGED
@@ -7078,9 +7078,15 @@ function gateSeedVars(cls, releaseTrack, runtime = "node") {
7078
7078
  GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA
7079
7079
  };
7080
7080
  const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
7081
+ const windowsCompat = {
7082
+ // #5113: opt-in informational windows-latest proof. Default OFF — the CLI fills the rendered job
7083
+ // YAML (or '') at the final layering step; never hand-passed.
7084
+ GATE_WINDOWS_COMPAT_JOB_YAML: ""
7085
+ };
7081
7086
  if (track === "trunk") {
7082
7087
  return {
7083
7088
  ...runtimeVars,
7089
+ ...windowsCompat,
7084
7090
  GATE_PUSH_BRANCHES_YAML: "[main]",
7085
7091
  GATE_FULL_RUN_BRANCH: "main",
7086
7092
  GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
@@ -7089,6 +7095,7 @@ function gateSeedVars(cls, releaseTrack, runtime = "node") {
7089
7095
  if (track === "direct") {
7090
7096
  return {
7091
7097
  ...runtimeVars,
7098
+ ...windowsCompat,
7092
7099
  GATE_PUSH_BRANCHES_YAML: "[development, main]",
7093
7100
  GATE_FULL_RUN_BRANCH: "development",
7094
7101
  GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
@@ -7096,6 +7103,7 @@ function gateSeedVars(cls, releaseTrack, runtime = "node") {
7096
7103
  }
7097
7104
  return {
7098
7105
  ...runtimeVars,
7106
+ ...windowsCompat,
7099
7107
  GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
7100
7108
  GATE_FULL_RUN_BRANCH: "development",
7101
7109
  GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
@@ -7112,8 +7120,38 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
7112
7120
  for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
7113
7121
  out[key] ??= value;
7114
7122
  }
7123
+ if (out.GATE_WINDOWS_COMPAT === "true" && !out.GATE_WINDOWS_COMPAT_JOB_YAML) {
7124
+ out.GATE_WINDOWS_COMPAT_JOB_YAML = windowsCompatJobYaml(out);
7125
+ }
7115
7126
  return out;
7116
7127
  }
7128
+ function windowsCompatJobYaml(vars) {
7129
+ const workdir = vars.GATE_WORKDIR ?? ".";
7130
+ const cmd = vars.GATE_CMD ?? DEFAULT_GATE_CMD;
7131
+ const install = vars.GATE_INSTALL_CMD ?? "npm ci";
7132
+ const fullRunBranch = vars.GATE_FULL_RUN_BRANCH ?? "development";
7133
+ const runtime = vars.GATE_RUNTIME === "python" ? "python" : "node";
7134
+ const setup = runtime === "python" ? ` - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
7135
+ with: { python-version: '${vars.GATE_PY_VERSION ?? DEFAULT_GATE_PY_VERSION}' }` : ` - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
7136
+ with: { node-version: 24, cache: npm, cache-dependency-path: ${vars.GATE_CACHE_DEP_PATH ?? "package-lock.json"} }`;
7137
+ return ` # MMI-Hub#5113: opt-in Windows compatibility proof \u2014 informational only. Do NOT add this job to a
7138
+ # required-contexts ruleset without a deliberate repo decision: the required gate stays the Linux lane
7139
+ # (faster, cheaper, where autonomous agents run). GitHub-hosted Windows minutes cost more than Linux,
7140
+ # hence opt-in. defaults.run.shell=bash is Git for Windows bash on windows-latest, so the check syntax
7141
+ # the Linux gate runs keeps working here.
7142
+ windows-compat:
7143
+ if: \${{ github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref_name == '${fullRunBranch}' || github.ref_name == 'main')) }}
7144
+ runs-on: windows-latest
7145
+ defaults:
7146
+ run: { working-directory: ${workdir}, shell: bash }
7147
+ steps:
7148
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
7149
+ ${setup}
7150
+ - run: ${install}
7151
+ # Fast proof only \u2014 the full suite stays on the Linux gate.
7152
+ - run: ${cmd}
7153
+ `;
7154
+ }
7117
7155
  function gateConfigToVars(gate) {
7118
7156
  const out = {};
7119
7157
  if (!gate || typeof gate !== "object") return out;
@@ -7124,6 +7162,7 @@ function gateConfigToVars(gate) {
7124
7162
  if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
7125
7163
  const seconds = typeof gate.maxSeconds === "number" ? String(gate.maxSeconds) : gate.maxSeconds;
7126
7164
  if (typeof seconds === "string" && /^\d+$/.test(seconds.trim()) && Number(seconds) > 0) out.GATE_MAX_SECONDS = seconds.trim();
7165
+ if (gate.windowsCompat === true) out.GATE_WINDOWS_COMPAT = "true";
7127
7166
  return out;
7128
7167
  }
7129
7168
  function seedMatchesDeployModel(seed, deployModel) {
@@ -10707,10 +10746,10 @@ var rollout_plan_default = {
10707
10746
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
10708
10747
  },
10709
10748
  baseline: {
10710
- version: "3.139.1",
10711
- tag: "v3.139.1",
10712
- commit: "882483d74da4",
10713
- npm: "@mutmutco/cli@3.139.1"
10749
+ version: "3.139.3",
10750
+ tag: "v3.139.3",
10751
+ commit: "19258f64de5f",
10752
+ npm: "@mutmutco/cli@3.139.3"
10714
10753
  },
10715
10754
  exitCriterion: "fleet-n-of-n",
10716
10755
  hubOnlyShortcut: "forbidden",
@@ -10727,14 +10766,14 @@ var rollout_plan_default = {
10727
10766
  repo: "mutmutco/mmi-hub",
10728
10767
  role: "canary",
10729
10768
  schedule: "train",
10730
- v3Target: "v3.139.1"
10769
+ v3Target: "v3.139.3"
10731
10770
  }
10732
10771
  ],
10733
10772
  rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
10734
10773
  rollback: {
10735
10774
  independent: true,
10736
- mechanism: "npm dist-tag latest -> 3.139.1 and redeploy the Hub Lambda from tag v3.139.1 (882483d74da4); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10737
- v3Target: "v3.139.1 (@mutmutco/cli@3.139.1, tag commit 882483d74da4 \u2014 the preserved latest-v3 distribution, D6b)"
10775
+ mechanism: "npm dist-tag latest -> 3.139.3 and redeploy the Hub Lambda from tag v3.139.3 (19258f64de5f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10776
+ v3Target: "v3.139.3 (@mutmutco/cli@3.139.3, tag commit 19258f64de5f \u2014 the preserved latest-v3 distribution, D6b)"
10738
10777
  }
10739
10778
  },
10740
10779
  {
@@ -22696,8 +22735,9 @@ var import_node_crypto5 = require("node:crypto");
22696
22735
  var import_node_child_process12 = require("node:child_process");
22697
22736
  var import_node_fs23 = require("node:fs");
22698
22737
  var import_node_path21 = require("node:path");
22699
- var REPO_INDEX_SCHEMA = 1;
22700
- var HARD_DENY = [
22738
+
22739
+ // ../infra/repo-index-path-policy.mjs
22740
+ var HARD_DENY = Object.freeze([
22701
22741
  /(^|\/)\.env(\.|$)/i,
22702
22742
  /(^|\/)\.env\./i,
22703
22743
  /credentials/i,
@@ -22708,7 +22748,20 @@ var HARD_DENY = [
22708
22748
  /(^|\/)id_rsa/i,
22709
22749
  /(^|\/)id_ed25519/i,
22710
22750
  /\.keystore$/i
22711
- ];
22751
+ ]);
22752
+ function isHardDeniedRepoIndexPath(value) {
22753
+ return typeof value !== "string" || HARD_DENY.some((pattern) => pattern.test(value));
22754
+ }
22755
+ function isSafeRepoIndexPath(value) {
22756
+ if (typeof value !== "string" || !value || value.length > 1024) return false;
22757
+ if (value.startsWith("/") || value.includes("\\") || /[\u0000-\u001f\u007f]/u.test(value)) return false;
22758
+ const segments = value.split("/");
22759
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
22760
+ return !isHardDeniedRepoIndexPath(value);
22761
+ }
22762
+
22763
+ // src/repo-index.ts
22764
+ var REPO_INDEX_SCHEMA = 1;
22712
22765
  var INDEXABLE_EXT = /* @__PURE__ */ new Set([
22713
22766
  ".ts",
22714
22767
  ".tsx",
@@ -22742,10 +22795,10 @@ function repoIndexStorePath(cwd) {
22742
22795
  return repoRuntimeStatePath(cwd, "repo-index", "index.json");
22743
22796
  }
22744
22797
  function isHardDeniedPath(relPosix) {
22745
- return HARD_DENY.some((re) => re.test(relPosix));
22798
+ return isHardDeniedRepoIndexPath(relPosix);
22746
22799
  }
22747
22800
  function isIndexablePath(relPosix) {
22748
- if (isHardDeniedPath(relPosix)) return false;
22801
+ if (!isSafeRepoIndexPath(relPosix)) return false;
22749
22802
  if (relPosix.startsWith(".git/")) return false;
22750
22803
  if (relPosix.includes("node_modules/")) return false;
22751
22804
  if (relPosix.includes("dist/")) return false;
@@ -23215,6 +23268,7 @@ ${chunk.symbol ?? ""}
23215
23268
  ${chunk.blurb ?? ""}`;
23216
23269
  }
23217
23270
  }
23271
+ var V4_EMBED_TIMEOUT_MS = 15 * 6e4;
23218
23272
  function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
23219
23273
  if (!chunks.length) return { embeddings: [] };
23220
23274
  const runner = (0, import_node_path23.join)(cwd, "repo-indexer", "src", "batch.mjs");
@@ -23223,7 +23277,7 @@ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
23223
23277
  if (!(0, import_node_fs25.existsSync)(file)) return { embeddings: [], reason: "embeddings-unavailable" };
23224
23278
  const request = { texts: chunks.map((chunk) => ({ id: chunk.id, text: embeddingInput(cwd, chunk) })), maxBatch: V4_EMBED_BATCH };
23225
23279
  const env = { ...process.env, ...modelDirectory ? { MMI_REPO_INDEXER_MODEL_DIR: modelDirectory } : {} };
23226
- const result = (0, import_node_child_process13.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: 3e5, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
23280
+ const result = (0, import_node_child_process13.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
23227
23281
  if (result.error || result.status !== 0) return { embeddings: [], reason: "embeddings-unavailable" };
23228
23282
  try {
23229
23283
  const response = JSON.parse(result.stdout);
@@ -23297,6 +23351,9 @@ async function buildRepoIndexV4(cwd, repo, opts = {}) {
23297
23351
  `, "utf8");
23298
23352
  return envelope;
23299
23353
  }
23354
+ function repoIndexV4StorePath(cwd) {
23355
+ return statePath(cwd);
23356
+ }
23300
23357
 
23301
23358
  // src/repo-index-v4/shards.ts
23302
23359
  var V4_STAGE_MAX_BODY_BYTES = 750 * 1024;
@@ -32371,7 +32428,7 @@ var surfaces_default = {
32371
32428
  ownership: {
32372
32429
  trust: "host",
32373
32430
  cache: "host",
32374
- repair: "mmi-cli"
32431
+ repair: "mmi-hub"
32375
32432
  },
32376
32433
  certification: {
32377
32434
  hostCommand: "claude",
@@ -32506,7 +32563,7 @@ var surfaces_default = {
32506
32563
  ownership: {
32507
32564
  trust: "operator",
32508
32565
  cache: "host",
32509
- repair: "mmi-cli"
32566
+ repair: "mmi-hub"
32510
32567
  },
32511
32568
  certification: {
32512
32569
  hostCommand: "codex",
@@ -32767,8 +32824,8 @@ var surfaces_default = {
32767
32824
  },
32768
32825
  ownership: {
32769
32826
  trust: "host",
32770
- cache: "mmi-cli",
32771
- repair: "mmi-cli"
32827
+ cache: "mmi-hub",
32828
+ repair: "mmi-hub"
32772
32829
  },
32773
32830
  certification: {
32774
32831
  hostCommand: "cursor-agent",
@@ -32879,7 +32936,7 @@ var surfaces_default = {
32879
32936
  ownership: {
32880
32937
  trust: "host",
32881
32938
  cache: "host",
32882
- repair: "mmi-cli"
32939
+ repair: "mmi-hub"
32883
32940
  },
32884
32941
  certification: {
32885
32942
  hostCommand: "kilo",
@@ -32997,7 +33054,7 @@ var surfaces_default = {
32997
33054
  ownership: {
32998
33055
  trust: "host",
32999
33056
  cache: "host",
33000
- repair: "mmi-cli"
33057
+ repair: "mmi-hub"
33001
33058
  },
33002
33059
  certification: {
33003
33060
  hostCommand: "jervcode",
@@ -34125,19 +34182,14 @@ function checkPluginCache(input) {
34125
34182
  parts.push(`${input.staging.length} orphaned staging dir(s)${size}`);
34126
34183
  }
34127
34184
  return {
34128
- ok: false,
34185
+ ok: true,
34186
+ warn: true,
34129
34187
  id: "plugin-cache",
34130
34188
  label: "plugin cache",
34131
- // #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
34132
- // A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
34133
- // `mmi-cli plugin prune --apply` clears it in one command and a re-run goes green, so this is an
34134
- // ordinary red the operator is expected to act on. "The doctor cannot heal it automatically" was never
34135
- // the bar — see the contract for why report-only is a per-row ruling rather than a rule you can apply
34136
- // from here.
34137
- // Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
34138
- // on its own so a cache with zero versioned dirs still gets an honest ✗.
34189
+ // Installed-surface convergence belongs to mmi-hub; the host owns its cache lifecycle. Doctor
34190
+ // reports old generations but never races either writer by deleting them.
34139
34191
  detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
34140
- fix: "run `mmi-cli plugin prune` to review, then `mmi-cli plugin prune --apply` to delete",
34192
+ fix: "run `mmi-hub status` and `mmi-hub update`; if only superseded cache remains, let the host retire it after all older sessions close",
34141
34193
  verbose: evidence
34142
34194
  };
34143
34195
  }
@@ -34242,24 +34294,34 @@ function checkRepoIndexCloud(probe) {
34242
34294
  probe.builtAt ? `builtAt: ${probe.builtAt}` : "builtAt: n/a",
34243
34295
  typeof probe.fileCount === "number" ? `fileCount: ${probe.fileCount}` : "fileCount: n/a",
34244
34296
  typeof probe.embCount === "number" ? `embCount: ${probe.embCount}` : "embCount: n/a",
34245
- typeof probe.localPresent === "boolean" ? `local cache: ${probe.localPresent ? "present" : "absent"}` : "local cache: n/a"
34297
+ typeof probe.localPresent === "boolean" ? `local v3 cache: ${probe.localPresent ? "present" : "absent"}` : "local v3 cache: n/a",
34298
+ `local v4: ${probe.localV4State ?? "unknown"}${probe.localV4Commit ? ` @ ${probe.localV4Commit.slice(0, 12)}` : ""}${typeof probe.localV4EmbeddingCoverage === "number" ? ` (${Math.round(probe.localV4EmbeddingCoverage * 100)}% embeddings)` : ""}`,
34299
+ `cloud v4: ${probe.cloudV4State ?? "unknown"}${probe.cloudV4Commit ? ` @ ${probe.cloudV4Commit.slice(0, 12)}` : ""}`,
34300
+ `v4 cohort: ${probe.v4ReadPercent ?? 0}%`,
34301
+ `v4 shadows: ${probe.v4ShadowComparisons ?? 0}/${probe.v4ShadowMinimum ?? 50} \u2014 ${probe.v4ShadowCutoverReady ? "cutover-ready" : "not ready"}`,
34302
+ ...Object.entries(probe.v4ShadowErrors ?? {}).map(([kind, count]) => `v4 shadow error ${kind}: ${count}`)
34246
34303
  ];
34247
34304
  switch (probe.kind) {
34248
- case "healthy":
34305
+ case "healthy": {
34306
+ const v4Unready = probe.cloudV4State === "invalid" || probe.cloudV4State === "tombstoned" || probe.cloudV4State === "degraded" && (probe.localV4EmbeddingCoverage ?? 1) < 0.95;
34307
+ const detail = probe.detail ?? `cloud v3 ok${typeof probe.fileCount === "number" ? ` \u2014 ${probe.fileCount} files` : ""}; v4 ${probe.cloudV4State ?? "absent"}, cohort ${probe.v4ReadPercent ?? 0}%`;
34249
34308
  return {
34250
- ok: true,
34309
+ ok: !v4Unready || (probe.v4ReadPercent ?? 0) === 0,
34310
+ ...v4Unready && (probe.v4ReadPercent ?? 0) === 0 ? { warn: true, verified: false } : {},
34251
34311
  id: "repo-index",
34252
34312
  label: "repo-index",
34253
- detail: probe.detail ?? `cloud ok${typeof probe.fileCount === "number" ? ` \u2014 ${probe.fileCount} files` : ""}`,
34313
+ detail,
34314
+ ...v4Unready ? { fix: "run `mmi-hub update`, then dispatch `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile`; keep the v4 cohort at 0 until shadows pass" } : {},
34254
34315
  verbose: evidence
34255
34316
  };
34317
+ }
34256
34318
  case "command-absent":
34257
34319
  return {
34258
34320
  ok: false,
34259
34321
  id: "repo-index",
34260
34322
  label: "repo-index",
34261
34323
  detail: "this mmi-cli build has no repo-index command",
34262
- fix: "run `mmi-cli doctor` to heal the global CLI, or `npm install -g @mutmutco/cli`",
34324
+ fix: "run `mmi-hub status`, then `mmi-hub update` to converge the CLI and installed host surfaces",
34263
34325
  verbose: evidence
34264
34326
  };
34265
34327
  case "deploy-lag":
@@ -34278,7 +34340,7 @@ function checkRepoIndexCloud(probe) {
34278
34340
  id: "repo-index",
34279
34341
  label: "repo-index",
34280
34342
  detail: probe.detail ?? "no cloud projection for this repo",
34281
- fix: "run harbour repo-index-reconcile or `mmi-cli oracle repo-index sync-estate --repo <owner/name>`",
34343
+ fix: "dispatch `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile` or run the authenticated per-repo sync",
34282
34344
  verbose: evidence
34283
34345
  };
34284
34346
  case "stale":
@@ -34287,7 +34349,7 @@ function checkRepoIndexCloud(probe) {
34287
34349
  id: "repo-index",
34288
34350
  label: "repo-index",
34289
34351
  detail: probe.detail ?? "cloud projection looks stale vs recent pushes",
34290
- fix: "trigger `repo-index-reconcile` (workflow_dispatch) or wait for the 6h harbour tick",
34352
+ fix: "dispatch `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile` or wait for the registered 6h schedule",
34291
34353
  verbose: evidence
34292
34354
  };
34293
34355
  case "auth":
@@ -34305,7 +34367,7 @@ function checkRepoIndexCloud(probe) {
34305
34367
  id: "repo-index",
34306
34368
  label: "repo-index",
34307
34369
  detail: probe.detail ?? "Hub API URL not configured",
34308
- fix: "run `mmi-cli doctor` to repair Hub wiring",
34370
+ fix: "run `mmi-hub status` and `mmi-hub update`; Hub URL wiring is installer-owned",
34309
34371
  verbose: evidence
34310
34372
  };
34311
34373
  case "network":
@@ -34335,6 +34397,39 @@ function planGitignore(current) {
34335
34397
  const { content, changed } = upsertManagedGitignoreBlock(current);
34336
34398
  return changed ? { ok: false, content } : { ok: true };
34337
34399
  }
34400
+ function checkLineEndings(probe) {
34401
+ if (probe.error) {
34402
+ return {
34403
+ ok: true,
34404
+ verified: false,
34405
+ id: "line-endings",
34406
+ label: "line endings",
34407
+ detail: `could not inspect tracked shell scripts: ${probe.error}`,
34408
+ fix: "run `git ls-files --eol -- '*.sh'` from the repository root, then re-run `mmi-cli doctor`"
34409
+ };
34410
+ }
34411
+ const problems = [
34412
+ ...probe.attributesPresent ? [] : [".gitattributes is missing"],
34413
+ ...probe.crlfShellScripts.map((path2) => `${path2} is CRLF in the index`)
34414
+ ];
34415
+ if (!problems.length) {
34416
+ return {
34417
+ ok: true,
34418
+ id: "line-endings",
34419
+ label: "line endings",
34420
+ detail: ".gitattributes present; tracked *.sh index entries are LF"
34421
+ };
34422
+ }
34423
+ return {
34424
+ ok: false,
34425
+ id: "line-endings",
34426
+ label: "line endings",
34427
+ detail: problems.join("; "),
34428
+ fix: "add the canonical .gitattributes if missing, then run `git add --renormalize .`",
34429
+ command: "git add --renormalize .",
34430
+ verbose: probe.crlfShellScripts.map((path2) => `i/crlf: ${path2}`)
34431
+ };
34432
+ }
34338
34433
  async function runDoctorClean(opts, io, deps) {
34339
34434
  const full = !opts.fast && !opts.banner && !opts.preflight;
34340
34435
  const applyEnv = full;
@@ -34468,6 +34563,11 @@ async function runDoctorClean(opts, io, deps) {
34468
34563
  const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
34469
34564
  if (aws) emitNow(aws);
34470
34565
  }
34566
+ async function runLineEndingsRow() {
34567
+ const root = await deps.repoRoot();
34568
+ const state = deps.lineEndingState?.(root);
34569
+ if (state) emitNow(checkLineEndings(state));
34570
+ }
34471
34571
  async function runGitignoreRow() {
34472
34572
  const current = deps.readGitignore();
34473
34573
  const gi = planGitignore(current);
@@ -34487,42 +34587,6 @@ async function runDoctorClean(opts, io, deps) {
34487
34587
  }
34488
34588
  }
34489
34589
  async function runPluginCacheRow() {
34490
- if (applyEnv && deps.prunePluginCache && spendOnce("cache-prune")) {
34491
- const cache = deps.pluginCache();
34492
- if (cache.stale.length === 0 && cache.staging.length === 0) {
34493
- emitNow(checkPluginCache(cache));
34494
- return;
34495
- }
34496
- healIntent(`plugin cache \u2014 pruning ${cache.stale.length} superseded version(s) (guarded)`);
34497
- traceHeal("cache-prune");
34498
- const outcome = deps.prunePluginCache();
34499
- if (outcome.removed.length) markHealChanged();
34500
- const evidence = [
34501
- ...outcome.removed.map((v) => `pruned: ${v}`),
34502
- ...outcome.held.map((h) => `still held: ${h.version} (${h.error}) \u2014 never forced`),
34503
- ...outcome.kept.map((k) => `kept: ${k.version} \u2014 ${k.reason}`)
34504
- ];
34505
- if (outcome.held.length === 0) {
34506
- emitNow({
34507
- id: "plugin-cache",
34508
- ok: true,
34509
- label: "plugin cache",
34510
- detail: outcome.removed.length ? `pruned ${outcome.removed.length} superseded version(s)` : "nothing provably superseded (guards kept every candidate)",
34511
- verbose: evidence
34512
- });
34513
- } else {
34514
- emitNow({
34515
- id: "plugin-cache",
34516
- ok: false,
34517
- reportOnly: true,
34518
- label: "plugin cache",
34519
- detail: `${outcome.removed.length} pruned; ${outcome.held.length} still held by a live session`,
34520
- fix: "close the session holding it and re-run `mmi-cli doctor`",
34521
- verbose: evidence
34522
- });
34523
- }
34524
- return;
34525
- }
34526
34590
  emitNow(checkPluginCache(deps.pluginCache()));
34527
34591
  }
34528
34592
  async function runClaudeBinaryRow() {
@@ -34851,6 +34915,7 @@ async function runDoctorClean(opts, io, deps) {
34851
34915
  { id: "aws-identity", when: true, run: runAwsRow },
34852
34916
  { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
34853
34917
  { id: "claude-binary", when: true, run: runClaudeBinaryRow },
34918
+ { id: "line-endings", when: isOrgRepo, run: runLineEndingsRow },
34854
34919
  { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow }
34855
34920
  ];
34856
34921
  const maxPasses = 3;
@@ -35143,6 +35208,20 @@ function writeGitignore(content) {
35143
35208
  return false;
35144
35209
  }
35145
35210
  }
35211
+ function lineEndingState(root) {
35212
+ const attributesPresent = (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, ".gitattributes"));
35213
+ try {
35214
+ const output = (0, import_node_child_process18.execFileSync)("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
35215
+ windowsHide: true,
35216
+ encoding: "utf8",
35217
+ stdio: ["ignore", "pipe", "ignore"]
35218
+ });
35219
+ const crlfShellScripts = output.split(/\r?\n/).filter((line) => line.startsWith("i/crlf ")).map((line) => line.slice(line.indexOf(" ") + 1)).filter(Boolean);
35220
+ return { attributesPresent, crlfShellScripts };
35221
+ } catch (error) {
35222
+ return { attributesPresent, crlfShellScripts: [], error: error.message || "git ls-files --eol failed" };
35223
+ }
35224
+ }
35146
35225
  async function ghInstalled() {
35147
35226
  try {
35148
35227
  await execFileP6("gh", ["--version"]);
@@ -35352,6 +35431,7 @@ function mmiDoctorDeps(opts = {}) {
35352
35431
  currentCliVersion: resolveClientVersion,
35353
35432
  readGitignore,
35354
35433
  writeGitignore,
35434
+ lineEndingState,
35355
35435
  repoRoot,
35356
35436
  executeScratchGc: (root, o) => executeScratchGc(root, { apply: o.apply }),
35357
35437
  // #2759: same fetch+ff-only sync SessionStart runs, wired here too so an on-demand `mmi-cli doctor`
@@ -35520,6 +35600,21 @@ function mmiDoctorDeps(opts = {}) {
35520
35600
  // #4156: short-timeout Hub status for the current repo. Fail-soft — never throws to doctor.
35521
35601
  repoIndexCloudState: async (root) => {
35522
35602
  const local = repoIndexStatus(root);
35603
+ let localV4 = { state: "absent" };
35604
+ try {
35605
+ const parsed = JSON.parse((0, import_node_fs42.readFileSync)(repoIndexV4StorePath(root), "utf8"));
35606
+ const state = parsed.status?.state;
35607
+ if (parsed.schemaVersion === 4 && (state === "ready" || state === "degraded")) {
35608
+ const chunks = parsed.manifest?.chunks?.length ?? 0;
35609
+ localV4 = {
35610
+ state,
35611
+ ...typeof parsed.manifest?.commit === "string" ? { commit: parsed.manifest.commit } : {},
35612
+ embeddingCoverage: chunks ? (parsed.manifest?.embeddings?.length ?? 0) / chunks : 1
35613
+ };
35614
+ } else localV4 = { state: "invalid" };
35615
+ } catch {
35616
+ }
35617
+ const localFields = { localPresent: local.present, localV4State: localV4.state, localV4Commit: localV4.commit, localV4EmbeddingCoverage: localV4.embeddingCoverage };
35523
35618
  try {
35524
35619
  const cfg = await loadConfig();
35525
35620
  const repo = inferRepoSlug(root);
@@ -35528,17 +35623,32 @@ function mmiDoctorDeps(opts = {}) {
35528
35623
  timeoutMs: 4e3
35529
35624
  });
35530
35625
  if (isRepoIndexStatusError(st)) {
35531
- if (st.code === "deploy-lag") return { kind: "deploy-lag", repo, localPresent: local.present };
35532
- if (st.code === "auth") return { kind: "auth", repo, localPresent: local.present };
35533
- if (st.code === "config") return { kind: "config", repo, detail: st.error, localPresent: local.present };
35534
- return { kind: "network", repo, detail: st.error, localPresent: local.present };
35626
+ if (st.code === "deploy-lag") return { kind: "deploy-lag", repo, ...localFields };
35627
+ if (st.code === "auth") return { kind: "auth", repo, ...localFields };
35628
+ if (st.code === "config") return { kind: "config", repo, detail: st.error, ...localFields };
35629
+ return { kind: "network", repo, detail: st.error, ...localFields };
35535
35630
  }
35536
35631
  const present = st.present;
35537
35632
  const builtAt = typeof st.builtAt === "string" ? st.builtAt : void 0;
35538
35633
  const fileCount = typeof st.fileCount === "number" ? st.fileCount : void 0;
35539
35634
  const embCount = typeof st.embCount === "number" ? st.embCount : void 0;
35635
+ const v4 = st.v4;
35636
+ const migration = st.v4Migration;
35637
+ const shadow = st.v4Shadow;
35638
+ const shadowThresholds = shadow?.thresholds;
35639
+ const cloudV4State = typeof v4?.state === "string" && ["ready", "degraded", "invalid", "tombstoned"].includes(v4.state) ? v4.state : "absent";
35640
+ const v4Fields = {
35641
+ ...localFields,
35642
+ cloudV4State,
35643
+ ...typeof v4?.commit === "string" ? { cloudV4Commit: v4.commit } : {},
35644
+ v4ReadPercent: typeof migration?.readPercent === "number" ? migration.readPercent : 0,
35645
+ v4ShadowComparisons: typeof shadow?.comparisonCount === "number" ? shadow.comparisonCount : 0,
35646
+ v4ShadowMinimum: typeof shadowThresholds?.minimumComparisons === "number" ? shadowThresholds.minimumComparisons : 50,
35647
+ v4ShadowCutoverReady: shadow?.cutoverReady === true,
35648
+ v4ShadowErrors: shadow?.errors && typeof shadow.errors === "object" ? shadow.errors : {}
35649
+ };
35540
35650
  if (present === false) {
35541
- return { kind: "missing-projection", repo, localPresent: local.present, fileCount: 0, embCount: 0 };
35651
+ return { kind: "missing-projection", repo, ...v4Fields, fileCount: 0, embCount: 0 };
35542
35652
  }
35543
35653
  if (builtAt) {
35544
35654
  const ageMs = Date.now() - Date.parse(builtAt);
@@ -35549,7 +35659,7 @@ function mmiDoctorDeps(opts = {}) {
35549
35659
  builtAt,
35550
35660
  fileCount,
35551
35661
  embCount,
35552
- localPresent: local.present,
35662
+ ...v4Fields,
35553
35663
  detail: `projection builtAt ${builtAt} (>48h old)`
35554
35664
  };
35555
35665
  }
@@ -35560,13 +35670,13 @@ function mmiDoctorDeps(opts = {}) {
35560
35670
  builtAt,
35561
35671
  fileCount,
35562
35672
  embCount,
35563
- localPresent: local.present
35673
+ ...v4Fields
35564
35674
  };
35565
35675
  } catch (e) {
35566
35676
  return {
35567
35677
  kind: "network",
35568
35678
  detail: e instanceof Error ? e.message : String(e),
35569
- localPresent: local.present
35679
+ ...localFields
35570
35680
  };
35571
35681
  }
35572
35682
  }
@@ -85,6 +85,30 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process.execF
85
85
  var import_node_child_process2 = require("node:child_process");
86
86
  var import_node_path2 = require("node:path");
87
87
 
88
+ // ../infra/repo-index-path-policy.mjs
89
+ var HARD_DENY = Object.freeze([
90
+ /(^|\/)\.env(\.|$)/i,
91
+ /(^|\/)\.env\./i,
92
+ /credentials/i,
93
+ /secrets?\.json$/i,
94
+ /\.pem$/i,
95
+ /\.p12$/i,
96
+ /\.key$/i,
97
+ /(^|\/)id_rsa/i,
98
+ /(^|\/)id_ed25519/i,
99
+ /\.keystore$/i
100
+ ]);
101
+ function isHardDeniedRepoIndexPath(value) {
102
+ return typeof value !== "string" || HARD_DENY.some((pattern) => pattern.test(value));
103
+ }
104
+ function isSafeRepoIndexPath(value) {
105
+ if (typeof value !== "string" || !value || value.length > 1024) return false;
106
+ if (value.startsWith("/") || value.includes("\\") || /[\u0000-\u001f\u007f]/u.test(value)) return false;
107
+ const segments = value.split("/");
108
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
109
+ return !isHardDeniedRepoIndexPath(value);
110
+ }
111
+
88
112
  // src/repo-runtime-state.ts
89
113
  var import_node_crypto = require("node:crypto");
90
114
  var import_node_fs = require("node:fs");
@@ -120,18 +144,6 @@ function repoRuntimeStatePath(cwd, ...parts) {
120
144
  }
121
145
 
122
146
  // src/repo-index.ts
123
- var HARD_DENY = [
124
- /(^|\/)\.env(\.|$)/i,
125
- /(^|\/)\.env\./i,
126
- /credentials/i,
127
- /secrets?\.json$/i,
128
- /\.pem$/i,
129
- /\.p12$/i,
130
- /\.key$/i,
131
- /(^|\/)id_rsa/i,
132
- /(^|\/)id_ed25519/i,
133
- /\.keystore$/i
134
- ];
135
147
  var INDEXABLE_EXT = /* @__PURE__ */ new Set([
136
148
  ".ts",
137
149
  ".tsx",
@@ -155,10 +167,10 @@ var INDEXABLE_EXT = /* @__PURE__ */ new Set([
155
167
  ".html"
156
168
  ]);
157
169
  function isHardDeniedPath(relPosix) {
158
- return HARD_DENY.some((re) => re.test(relPosix));
170
+ return isHardDeniedRepoIndexPath(relPosix);
159
171
  }
160
172
  function isIndexablePath(relPosix) {
161
- if (isHardDeniedPath(relPosix)) return false;
173
+ if (!isSafeRepoIndexPath(relPosix)) return false;
162
174
  if (relPosix.startsWith(".git/")) return false;
163
175
  if (relPosix.includes("node_modules/")) return false;
164
176
  if (relPosix.includes("dist/")) return false;
@@ -418,6 +430,7 @@ ${chunk.symbol ?? ""}
418
430
  ${chunk.blurb ?? ""}`;
419
431
  }
420
432
  }
433
+ var V4_EMBED_TIMEOUT_MS = 15 * 6e4;
421
434
  function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
422
435
  if (!chunks.length) return { embeddings: [] };
423
436
  const runner = (0, import_node_path4.join)(cwd, "repo-indexer", "src", "batch.mjs");
@@ -426,7 +439,7 @@ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
426
439
  if (!(0, import_node_fs3.existsSync)(file)) return { embeddings: [], reason: "embeddings-unavailable" };
427
440
  const request = { texts: chunks.map((chunk) => ({ id: chunk.id, text: embeddingInput(cwd, chunk) })), maxBatch: V4_EMBED_BATCH };
428
441
  const env = { ...process.env, ...modelDirectory ? { MMI_REPO_INDEXER_MODEL_DIR: modelDirectory } : {} };
429
- const result = (0, import_node_child_process3.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: 3e5, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
442
+ const result = (0, import_node_child_process3.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
430
443
  if (result.error || result.status !== 0) return { embeddings: [], reason: "embeddings-unavailable" };
431
444
  try {
432
445
  const response = JSON.parse(result.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.139.1",
3
+ "version": "3.139.3",
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",