From e05769694816c7eb8dc0ce2982bf39e536e7cc16 Mon Sep 17 00:00:00 2001 From: shahinyanm Date: Thu, 11 Jun 2026 16:02:06 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20v0.45.1=20=E2=80=94=20refuse=20multi-rep?= =?UTF-8?q?o=20workspace=20parent=20(cross-project=20index=20bleed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit start.sh always passes an explicit project root, so startServer's git-root narrowing (the `!explicitRoot` branch) never runs. When the session starts from a non-git workspace parent nesting several project repos (e.g. /work/loom holding token-pilot, loom-host, aimux), the raw parent was used verbatim and ast-index indexed every sibling into one index — find_usages / read_symbol then bled matches across unrelated projects. isDangerousRoot only caught system/home dirs, so the parent slipped through. Add isMultiRepoParent(root) guard (core/validation.ts): a non-git dir with >=2 immediate child git repos. On match, ast-index is disabled (skipAstIndex) and a warning tells the user to set CLAUDE_PROJECT_DIR to the specific project — fail safe instead of bleeding. Wired into startServer and the server.ts MCP-roots auto-detect. Single-repo roots, monorepos, and roots that are themselves a git repo are unaffected. Tests: tests/core/multi-repo-parent.test.ts (7 cases). tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 23 +++++++- agents/tp-api-surface-tracker.md | 2 +- agents/tp-audit-scanner.md | 2 +- agents/tp-commit-writer.md | 2 +- agents/tp-context-engineer.md | 2 +- agents/tp-dead-code-finder.md | 2 +- agents/tp-debugger.md | 2 +- agents/tp-dep-health.md | 2 +- agents/tp-doc-writer.md | 2 +- agents/tp-history-explorer.md | 2 +- agents/tp-impact-analyzer.md | 2 +- agents/tp-incident-timeline.md | 2 +- agents/tp-incremental-builder.md | 2 +- agents/tp-migration-scout.md | 2 +- agents/tp-onboard.md | 2 +- agents/tp-performance-profiler.md | 2 +- agents/tp-pr-reviewer.md | 2 +- agents/tp-refactor-planner.md | 2 +- agents/tp-review-impact.md | 2 +- agents/tp-run.md | 2 +- agents/tp-session-restorer.md | 2 +- agents/tp-ship-coordinator.md | 2 +- agents/tp-spec-writer.md | 2 +- agents/tp-test-coverage-gapper.md | 2 +- agents/tp-test-triage.md | 2 +- agents/tp-test-writer.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/core/validation.ts | 38 +++++++++++++ src/index.ts | 20 ++++++- src/server.ts | 8 ++- tests/core/multi-repo-parent.test.ts | 83 ++++++++++++++++++++++++++++ 34 files changed, 198 insertions(+), 36 deletions(-) create mode 100644 tests/core/multi-repo-parent.test.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 211650d..cd5a419 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Token Pilot — save 60-90% tokens when AI reads code", - "version": "0.45.0" + "version": "0.45.1" }, "plugins": [ { "name": "token-pilot", "source": "./", "description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 23 MCP tools + 25 subagents + budget watchdog hooks.", - "version": "0.45.0", + "version": "0.45.1", "author": { "name": "Digital-Threads" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0e6ed81..ddf2602 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.45.0", + "version": "0.45.1", "description": "Saves 60-90% tokens on AI code reading. AST-aware lazy reads, symbol navigation, find_usages, structural git diff/log, edit-safety guard, Task-routing matcher, cross-session telemetry (errors + diagnostics), 25 tp-* subagents tiered to haiku/sonnet/opus with budget watchdog.", "author": { "name": "Digital-Threads", diff --git a/CHANGELOG.md b/CHANGELOG.md index ed50e3a..540235e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,28 @@ All notable changes to Token Pilot will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.45.0] - 2026-06-11 +## [0.45.1] - 2026-06-11 + +### Fixed — refuse a multi-repo workspace parent (cross-project index bleed) + +`start.sh` always passes an explicit project root (`${CLAUDE_PROJECT_DIR:-$USER_CWD}`) +to the server, so `startServer`'s git-root **narrowing only runs in the +`!explicitRoot` branch — which is never taken**. When the session is launched +from a non-git workspace parent that nests several project repos (e.g. +`/work/loom` holding `token-pilot`, `loom-host`, `aimux`, …), the raw parent was +used verbatim and ast-index indexed **every** sibling into one index. Symbol +lookups then bled across projects — `find_usages` / `read_symbol` returning +matches from the wrong repo, or `symbol not found`. `isDangerousRoot` only +caught system/home dirs, so the parent slipped through. + +New guard `isMultiRepoParent(root)` (in `core/validation.ts`) detects a non-git +directory with ≥2 immediate child git repos. When the resolved root matches, +ast-index is disabled (`skipAstIndex`) and a warning tells the user to set +`CLAUDE_PROJECT_DIR` to the specific project — fail safe instead of bleeding. +Wired into `startServer` and the `server.ts` MCP-roots auto-detect. Single-repo +roots, monorepos, and roots that are themselves a git repo are unaffected. + + ### Changed — default tool profile is now `full` (adoption fix) diff --git a/agents/tp-api-surface-tracker.md b/agents/tp-api-surface-tracker.md index 500d1c5..9f1e33a 100644 --- a/agents/tp-api-surface-tracker.md +++ b/agents/tp-api-surface-tracker.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__read_symbol - Bash model: haiku -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-audit-scanner.md b/agents/tp-audit-scanner.md index 0ea4e11..73c7506 100644 --- a/agents/tp-audit-scanner.md +++ b/agents/tp-audit-scanner.md @@ -11,7 +11,7 @@ tools: - Grep - Read model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: d172f600bf32277ea6eb4cbbee4542ddd698a986dcd96997d33930561964569b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-commit-writer.md b/agents/tp-commit-writer.md index 4c2278b..50c2464 100644 --- a/agents/tp-commit-writer.md +++ b/agents/tp-commit-writer.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__test_summary - mcp__token-pilot__outline - Bash -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-context-engineer.md b/agents/tp-context-engineer.md index 7701c58..4d675f2 100644 --- a/agents/tp-context-engineer.md +++ b/agents/tp-context-engineer.md @@ -13,7 +13,7 @@ tools: - Edit - Glob model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 68b32af2dacd82ebe52c4eec93edb903d452688274c3065218270627c564d8b0 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-dead-code-finder.md b/agents/tp-dead-code-finder.md index 4ec10c0..79ba556 100644 --- a/agents/tp-dead-code-finder.md +++ b/agents/tp-dead-code-finder.md @@ -11,7 +11,7 @@ tools: - Grep - Read model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: d9b7f5b7ae6f4ae21305c775361bcab097cc774370a6d976c093571d46d55021 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-debugger.md b/agents/tp-debugger.md index d0089f1..e72223c 100644 --- a/agents/tp-debugger.md +++ b/agents/tp-debugger.md @@ -12,7 +12,7 @@ tools: - Read - Bash model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 052413de8d92377edcde6ae5c823f5378db304baccfa29e8866467f42553a500 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-dep-health.md b/agents/tp-dep-health.md index b81bd12..ead79cf 100644 --- a/agents/tp-dep-health.md +++ b/agents/tp-dep-health.md @@ -9,7 +9,7 @@ tools: - Bash - Read model: haiku -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: e14dc57493d816f8c2e017963e2ef5f66bea50fd0b805a80e8a0d97c968427e7 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-doc-writer.md b/agents/tp-doc-writer.md index 8f6389d..45be589 100644 --- a/agents/tp-doc-writer.md +++ b/agents/tp-doc-writer.md @@ -13,7 +13,7 @@ tools: - Edit - Glob model: haiku -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 57d741794ab40e31a7ac49c68ea39a9088f5827cdef866ce81bfca1b7c9180cf requiredMcpServers: - "token-pilot" diff --git a/agents/tp-history-explorer.md b/agents/tp-history-explorer.md index c73a819..165fbc6 100644 --- a/agents/tp-history-explorer.md +++ b/agents/tp-history-explorer.md @@ -10,7 +10,7 @@ tools: - Bash - Read model: haiku -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 7b70fa76a60e3c58a1de4f56c32c0f166424137e203a0cf1c8654e7c9235d904 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-impact-analyzer.md b/agents/tp-impact-analyzer.md index 5f37197..b0a60d3 100644 --- a/agents/tp-impact-analyzer.md +++ b/agents/tp-impact-analyzer.md @@ -12,7 +12,7 @@ tools: - mcp__token-pilot__read_symbols - Read model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incident-timeline.md b/agents/tp-incident-timeline.md index f43f7e5..54f8ac4 100644 --- a/agents/tp-incident-timeline.md +++ b/agents/tp-incident-timeline.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__read_symbol - Bash model: inherit -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incremental-builder.md b/agents/tp-incremental-builder.md index 1db1643..6770b07 100644 --- a/agents/tp-incremental-builder.md +++ b/agents/tp-incremental-builder.md @@ -13,7 +13,7 @@ tools: - Edit - Bash model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 375a824d0d847bb5453ec594c7a62ad566ee7e4d92717b0473f771f1a0477c60 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-migration-scout.md b/agents/tp-migration-scout.md index e8a01c3..0b84d71 100644 --- a/agents/tp-migration-scout.md +++ b/agents/tp-migration-scout.md @@ -11,7 +11,7 @@ tools: - Grep - Glob model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 0334de1bf99b431b65359637d125cda7c44c6f780eb92c57cc538715b1939536 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-onboard.md b/agents/tp-onboard.md index 964a9cd..4de7003 100644 --- a/agents/tp-onboard.md +++ b/agents/tp-onboard.md @@ -10,7 +10,7 @@ tools: - mcp__token-pilot__smart_read - mcp__token-pilot__smart_read_many - mcp__token-pilot__read_section -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-performance-profiler.md b/agents/tp-performance-profiler.md index 1141250..8f1d173 100644 --- a/agents/tp-performance-profiler.md +++ b/agents/tp-performance-profiler.md @@ -11,7 +11,7 @@ tools: - Bash - Read model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: b61f06380d80798fa2e49d37bcba0653495bee04dd6bdbc1feff9a75607b0508 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-pr-reviewer.md b/agents/tp-pr-reviewer.md index 462f4d3..111253b 100644 --- a/agents/tp-pr-reviewer.md +++ b/agents/tp-pr-reviewer.md @@ -11,7 +11,7 @@ tools: - mcp__token-pilot__read_for_edit - Read model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-refactor-planner.md b/agents/tp-refactor-planner.md index 9afdbdc..3826566 100644 --- a/agents/tp-refactor-planner.md +++ b/agents/tp-refactor-planner.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__outline - mcp__token-pilot__read_symbol model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-review-impact.md b/agents/tp-review-impact.md index a21c0ce..102dfd2 100644 --- a/agents/tp-review-impact.md +++ b/agents/tp-review-impact.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__module_info - Bash model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c requiredMcpServers: - "token-pilot" diff --git a/agents/tp-run.md b/agents/tp-run.md index fbbfec0..b711e47 100644 --- a/agents/tp-run.md +++ b/agents/tp-run.md @@ -16,7 +16,7 @@ tools: - Glob - Bash model: haiku -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 2b08618d34a61f00aafccbda9fed6d83243296dedb83440edbd2d5c28bb6dbc4 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-session-restorer.md b/agents/tp-session-restorer.md index 880ebbc..61f2470 100644 --- a/agents/tp-session-restorer.md +++ b/agents/tp-session-restorer.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__session_budget - Bash - Read -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 529374ed728f5eed5b758b3be3da65624783c0bf0c1a253d7d661a843eb5f767 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-ship-coordinator.md b/agents/tp-ship-coordinator.md index 12ffd1e..268a043 100644 --- a/agents/tp-ship-coordinator.md +++ b/agents/tp-ship-coordinator.md @@ -11,7 +11,7 @@ tools: - Read - Grep model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: a60f6ae110eb3138064bce074e8ba26fa0ce5f4659df1624a9d9d3646803391b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-spec-writer.md b/agents/tp-spec-writer.md index 9746a4f..5596114 100644 --- a/agents/tp-spec-writer.md +++ b/agents/tp-spec-writer.md @@ -9,7 +9,7 @@ tools: - Read - Write model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: c7a4e8b39228fd5158528f389c924c5ff2d98c4b9b05ee0106d54a26c5dc1350 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-coverage-gapper.md b/agents/tp-test-coverage-gapper.md index 980083c..4bfc888 100644 --- a/agents/tp-test-coverage-gapper.md +++ b/agents/tp-test-coverage-gapper.md @@ -10,7 +10,7 @@ tools: - mcp__token-pilot__test_summary - Glob - Grep -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-triage.md b/agents/tp-test-triage.md index c56965e..03708ae 100644 --- a/agents/tp-test-triage.md +++ b/agents/tp-test-triage.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__find_usages - mcp__token-pilot__read_symbol model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-writer.md b/agents/tp-test-writer.md index 2b84fda..2649d18 100644 --- a/agents/tp-test-writer.md +++ b/agents/tp-test-writer.md @@ -13,7 +13,7 @@ tools: - Edit - Bash model: sonnet -token_pilot_version: "0.45.0" +token_pilot_version: "0.45.1" token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9 requiredMcpServers: - "token-pilot" diff --git a/package-lock.json b/package-lock.json index f12c5b8..01bcfd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "token-pilot", - "version": "0.45.0", + "version": "0.45.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "token-pilot", - "version": "0.45.0", + "version": "0.45.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1540d50..a842034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.45.0", + "version": "0.45.1", "description": "Save up to 80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window", "type": "module", "main": "dist/index.js", diff --git a/src/core/validation.ts b/src/core/validation.ts index 0fd321a..10203d1 100644 --- a/src/core/validation.ts +++ b/src/core/validation.ts @@ -1,4 +1,5 @@ import { resolve, relative } from "node:path"; +import { existsSync, readdirSync } from "node:fs"; /** * v0.33.0 (B9) — coerce an `unknown` argument value to an integer. @@ -851,3 +852,40 @@ export function isDangerousRoot(root: string): boolean { if (/^[A-Z]:\\(?:Users)?$/i.test(normalized)) return true; return false; } + +/** + * Detect a non-git workspace parent that nests multiple sibling git + * repos. Handing such a directory to ast-index would index every + * sibling into one index, bleeding symbols across unrelated projects + * (find_usages / read_symbol returning matches from the wrong repo). + * + * Returns true only when BOTH hold: + * - `root` itself is NOT a git repo (no `.git` entry), AND + * - `root` has >= 2 immediate child directories that each contain a + * `.git` entry — a directory for a normal repo, a file for a + * submodule / worktree. + * + * Fail-open: a missing path, an unreadable directory, a single child + * repo, or a root that is itself a repo all return false, so legitimate + * single-project and monorepo layouts are never disabled. + */ +export function isMultiRepoParent(root: string): boolean { + if (!root) return false; + let entries; + try { + // A root that is itself a git repo is a single project — vendored + // repos or submodules underneath are intentional, not a parent. + if (existsSync(resolve(root, ".git"))) return false; + entries = readdirSync(root, { withFileTypes: true }); + } catch { + return false; + } + let repoChildren = 0; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (existsSync(resolve(root, entry.name, ".git"))) { + if (++repoChildren >= 2) return true; + } + } + return false; +} diff --git a/src/index.ts b/src/index.ts index dff03d2..0ab68d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ import { isNewerVersion, } from "./ast-index/binary-manager.js"; import { loadConfig } from "./config/loader.js"; -import { isDangerousRoot } from "./core/validation.js"; +import { isDangerousRoot, isMultiRepoParent } from "./core/validation.js"; import type { HookMode } from "./types.js"; import { runSummaryPipeline } from "./hooks/summary-pipeline.js"; import { formatDenyMessage } from "./hooks/format-deny-message.js"; @@ -775,6 +775,22 @@ export async function startServer(cliArgs: string[] = process.argv.slice(2)) { ); } + // Guard: refuse a non-git workspace parent that nests multiple sibling + // git repos. start.sh always passes an explicit root, so the git-detect + // narrowing above is skipped; without this guard a parent like + // `/work/loom` (holding several project repos) would be indexed whole, + // bleeding symbols across unrelated projects. + const multiRepoParent = + !isDangerousRoot(projectRoot) && isMultiRepoParent(projectRoot); + if (multiRepoParent) { + console.error( + `[token-pilot] WARNING: project root "${projectRoot}" contains multiple git repos.\n` + + ` ast-index will be disabled to avoid cross-project index bleed.\n` + + ` Fix: set CLAUDE_PROJECT_DIR to the specific project, or\n` + + ` configure mcpServers with "args": ["/path/to/project"].`, + ); + } + // Non-blocking update check for all components (logs to stderr, never blocks startup) const config = await loadConfig(projectRoot); const binaryStatus = await findBinary(config.astIndex.binaryPath); @@ -831,7 +847,7 @@ export async function startServer(cliArgs: string[] = process.argv.slice(2)) { }); const server = await createServer(projectRoot, { - skipAstIndex: isDangerousRoot(projectRoot), + skipAstIndex: isDangerousRoot(projectRoot) || multiRepoParent, enforcementMode: parseEnforcementMode(process.env.TOKEN_PILOT_MODE), }); const transport = new StdioServerTransport(); diff --git a/src/server.ts b/src/server.ts index 4d446e8..e7679fb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,7 +21,7 @@ import { loadConfig } from "./config/loader.js"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { execFile } from "node:child_process"; -import { isDangerousRoot } from "./core/validation.js"; +import { isDangerousRoot, isMultiRepoParent } from "./core/validation.js"; import { promisify } from "node:util"; import { GitWatcher } from "./git/watcher.js"; @@ -213,7 +213,11 @@ export async function createServer( for (const root of roots) { if (root.uri.startsWith("file://")) { const rootPath = decodeURIComponent(new URL(root.uri).pathname); - if (rootPath && !isDangerousRoot(rootPath)) { + if ( + rootPath && + !isDangerousRoot(rootPath) && + !isMultiRepoParent(rootPath) + ) { await applyDetectedRoot(rootPath, "MCP roots"); return; } diff --git a/tests/core/multi-repo-parent.test.ts b/tests/core/multi-repo-parent.test.ts new file mode 100644 index 0000000..d1b2fee --- /dev/null +++ b/tests/core/multi-repo-parent.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { isMultiRepoParent } from "../../src/core/validation"; + +/** + * Guard against the cross-project index-bleed bug: when token-pilot is + * handed a non-git workspace parent that contains several sibling git + * repos, ast-index would index all of them into one index. The guard + * detects that shape so the caller can fail safe (disable ast-index + + * warn) instead of bleeding symbols across projects. + */ +describe("isMultiRepoParent", () => { + let root: string; + + function repo(name: string, gitAsFile = false) { + const dir = join(root, name); + mkdirSync(dir, { recursive: true }); + const git = join(dir, ".git"); + if (gitAsFile) { + // submodule / worktree style: .git is a file, not a dir + writeFileSync(git, "gitdir: /elsewhere\n"); + } else { + mkdirSync(git); + } + return dir; + } + + function plainDir(name: string) { + mkdirSync(join(root, name), { recursive: true }); + } + + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "tp-mrp-")); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + it("returns true for a non-git dir with >=2 child git repos", () => { + repo("token-pilot"); + repo("loom-host"); + repo("aimux"); + expect(isMultiRepoParent(root)).toBe(true); + }); + + it("returns false for a dir with exactly one child git repo", () => { + repo("token-pilot"); + plainDir("docs"); + expect(isMultiRepoParent(root)).toBe(false); + }); + + it("returns false for a dir with no child git repos", () => { + plainDir("src"); + plainDir("docs"); + expect(isMultiRepoParent(root)).toBe(false); + }); + + it("returns false when the dir itself is a git repo (single project)", () => { + // root/.git present → root is one project, not a multi-repo parent, + // even if it nests child repos (submodules). + mkdirSync(join(root, ".git")); + repo("vendored-a"); + repo("vendored-b"); + expect(isMultiRepoParent(root)).toBe(false); + }); + + it("counts .git-as-file (submodule/worktree) child repos", () => { + repo("a", true); + repo("b", true); + expect(isMultiRepoParent(root)).toBe(true); + }); + + it("returns false for a non-existent path", () => { + expect(isMultiRepoParent(join(root, "does-not-exist"))).toBe(false); + }); + + it("returns false for an empty string", () => { + expect(isMultiRepoParent("")).toBe(false); + }); +});