()
| 23 | // EVOLVER_USE_PARENT_GIT=true flag is still honored for forward |
| 24 | // compatibility but is no longer required. |
| 25 | function getRepoRoot() { |
| 26 | // Always check EVOLVER_REPO_ROOT first, even when a cached value exists. |
| 27 | // .env is loaded during index.js bootstrap AFTER this function has |
| 28 | // already been called at least once (for locating the .env file |
| 29 | // itself). Caching the pre-dotenv result would permanently shadow any |
| 30 | // EVOLVER_REPO_ROOT later populated from .env. See #526. |
| 31 | if (process.env.EVOLVER_REPO_ROOT) { |
| 32 | _cachedRepoRoot = process.env.EVOLVER_REPO_ROOT; |
| 33 | return _cachedRepoRoot; |
| 34 | } |
| 35 | |
| 36 | if (_cachedRepoRoot) return _cachedRepoRoot; |
| 37 | |
| 38 | const ownDir = path.resolve(__dirname, '..', '..'); |
| 39 | |
| 40 | const noParent = String(process.env.EVOLVER_NO_PARENT_GIT || '').toLowerCase() === 'true'; |
| 41 | // Older flag kept for backward compatibility. Setting it to 'false' |
| 42 | // explicitly is treated as an opt-out, mirroring EVOLVER_NO_PARENT_GIT. |
| 43 | const legacyFlag = process.env.EVOLVER_USE_PARENT_GIT; |
| 44 | const legacyOptOut = typeof legacyFlag === 'string' && legacyFlag.toLowerCase() === 'false'; |
| 45 | |
| 46 | // Both upward walks below must stop at the parent of the nearest |
| 47 | // `node_modules` ancestor — never escape into whatever `.git` happens |
| 48 | // to live above it (issue #541). On macOS with Homebrew, the global |
| 49 | // install lives at `/opt/homebrew/lib/node_modules/@evomap/evolver` |
| 50 | // and `/opt/homebrew` is itself a git repo; an unbounded walk |
| 51 | // therefore resolves repoRoot to `/opt/homebrew`, sending |
| 52 | // workspaceRoot / memoryDir / evolutionDir to a directory that |
| 53 | // doesn't belong to the user and silently producing evolution |
| 54 | // proposals for the wrong codebase. |
| 55 | // |
| 56 | // Boundary semantics: returns the parent of the nearest `node_modules` |
| 57 | // ancestor (inclusive — a `.git` at that parent IS still picked up), |
| 58 | // or null if `dir` is not inside any `node_modules` (dev clone / |
| 59 | // user project root). Callers stop AFTER checking the boundary path |
| 60 | // itself. |
| 61 | // |
| 62 | // For a local install (`<project>/node_modules/@evomap/evolver`), the |
| 63 | // parent of node_modules IS the user's project, so the boundary |
| 64 | // includes `<project>` and `<project>/.git` is still picked up |
| 65 | // correctly. For a dev clone, the boundary is null and the walk is |
| 66 | // unbounded as before. |
| 67 | function _nodeModulesBoundary(dir) { |
| 68 | const segments = dir.split(path.sep); |
| 69 | const nmIdx = segments.lastIndexOf('node_modules'); |
| 70 | if (nmIdx <= 0) return null; |
| 71 | return segments.slice(0, nmIdx).join(path.sep) || path.sep; |
| 72 | } |
| 73 | |
| 74 | function _walkForGit(start) { |
| 75 | const stopAt = _nodeModulesBoundary(start); |
| 76 | let dir = start; |
| 77 | while (dir !== path.dirname(dir)) { |
| 78 | if (fs.existsSync(path.join(dir, '.git'))) { |
| 79 | if (!process.env.EVOLVER_QUIET_PARENT_GIT) { |
| 80 | // Diagnostic, not data. Goes to stderr so it cannot poison |
| 81 | // a child process's stdout-as-JSON contract — the Stop hook |
| 82 | // emits JSON on stdout, and any caller invoking the hook via |
no test coverage detected