feat(sync): worktree index mismatch detection — doctor check + MCP banner (closes #66 phase-4)#154
Merged
Merged
Conversation
…nner (closes #66 phase-4) Add scripts/sync/worktree.py with detect_worktree_index_mismatch(), format_worktree_warning(), and format_worktree_banner(). The detector shells out to 'git rev-parse --show-toplevel' and 'git rev-parse --git-common-dir' to identify worktrees, then walks up from the project root to find the .codelens/ that CodeLens would actually load. A mismatch is reported when: - workspace is a git worktree (worktree_root != main_checkout_root) - AND no .codelens/ exists in the worktree - AND a .codelens/ does exist in the main checkout In that scenario CodeLens silently reads the main checkout's index — which was built from a different branch. Every query/trace/dataflow answer is then grounded in the wrong file set. Wiring: - doctor: new check 'workspace.worktree_index_mismatch' (status=warning on mismatch). Runs BEFORE _check_codelens_writable because the writable check creates .codelens/ as a side effect, which would mask the mismatch. - mcp_server: read-tool responses get a top-level _worktree_warning field (banner + mismatch record) when a mismatch is detected. Mutating commands (scan, init) skip the banner. The mismatch verdict is cached per workspace (probed once, reused) and invalidated when scan runs. - The early-probe happens BEFORE _execute_command because read commands like 'list' / 'query' trigger ensure_codelens_dir() as a side effect of loading the registry. Tests: 30 new tests across 3 files. - tests/test_worktree.py: 19 tests for the detector + formatters. - tests/test_doctor.py: 6 tests for the doctor integration, including an ordering test that enforces 'mismatch before writable'. - tests/test_mcp_hooks.py: 5 tests for the MCP integration, including cache-once-per-workspace and detection-never-breaks-tool-call. Pre-existing failure unrelated to this PR: tests/test_codelensignore.py::TestBackwardCompat::test_actual_target_dir_is_ignored (was failing on origin/main before this branch)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
6 tasks
|
6 tasks
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed files have been edited since the last scan and surfaces a warning to the agent before the tool's actual output. New files: - scripts/sync/__init__.py — re-exports public API - scripts/sync/pending.py — StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner() - scripts/commands/staleness.py — 'codelens staleness' CLI command (manual check + full list when MCP banner truncates to 10) - tests/test_staleness.py — 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration) - docs/sync/staleness-banner.md — architecture + design decisions Modified: - scripts/mcp_server.py — MCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan - README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 -> 71) Detection algorithm: 1. Load stored mtimes from .codelens/mtimes.json 2. For each indexed file: os.stat() compare (st_size, st_mtime_ns) 3. If mtime differs, re-compute SHA-256 (only when needed) to confirm content actually changed — skips false positives from / M README.md M SKILL-QUICK.md M SKILL.md A docs/sync/staleness-banner.md M pyproject.toml A scripts/commands/staleness.py M scripts/graph_model.py M scripts/mcp_server.py A scripts/sync/__init__.py A scripts/sync/pending.py M skill.json A tests/test_staleness.py of identical content 4. Sort by edit_age ascending (most recent edit first) 5. Return tuple of StaleFile records Why mtimes.json (not SQLite files table) as source of truth? mtimes.json is written by every scan, including workspaces that use the legacy JSON registry (pre-v8.2). The SQLite files table is only populated when the persistent registry is active. Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Banner: plain text (not markdown), prepended to first content block's text + structured response['_staleness'] field. Both paths ensure the warning surfaces regardless of how agents consume tool output. Verified: - tests/test_staleness.py: 41 passed - tests/test_staleness.py + test_command_count.py + test_doctor.py + test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed - sync_command_count.py --check: clean - 'codelens staleness' smoke-tested end-to-end (text + json output) Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec. StaleFileDetector cache + StaleFile data structure designed to accommodate Phase 2 without API change. Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges first, this PR will need a small rebase to resolve the __init__.py overlap (both add the same package marker) and the mcp_server.py overlap (both add methods to MCPServer — different method names, no conflict). BOS will resolve at merge time.
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed files have been edited since the last scan and surfaces a warning to the agent before the tool's actual output. New files: - scripts/sync/__init__.py — re-exports public API - scripts/sync/pending.py — StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner() - scripts/commands/staleness.py — 'codelens staleness' CLI command (manual check + full list when MCP banner truncates to 10) - tests/test_staleness.py — 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration) - docs/sync/staleness-banner.md — architecture + design decisions Modified: - scripts/mcp_server.py — MCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan - README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 -> 71) Detection algorithm: 1. Load stored mtimes from .codelens/mtimes.json 2. For each indexed file: os.stat() compare (st_size, st_mtime_ns) 3. If mtime differs, re-compute SHA-256 (only when needed) to confirm content actually changed — skips false positives from / M README.md M SKILL-QUICK.md M SKILL.md A docs/sync/staleness-banner.md M pyproject.toml A scripts/commands/staleness.py M scripts/graph_model.py M scripts/mcp_server.py A scripts/sync/__init__.py A scripts/sync/pending.py M skill.json A tests/test_staleness.py of identical content 4. Sort by edit_age ascending (most recent edit first) 5. Return tuple of StaleFile records Why mtimes.json (not SQLite files table) as source of truth? mtimes.json is written by every scan, including workspaces that use the legacy JSON registry (pre-v8.2). The SQLite files table is only populated when the persistent registry is active. Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Banner: plain text (not markdown), prepended to first content block's text + structured response['_staleness'] field. Both paths ensure the warning surfaces regardless of how agents consume tool output. Verified: - tests/test_staleness.py: 41 passed - tests/test_staleness.py + test_command_count.py + test_doctor.py + test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed - sync_command_count.py --check: clean - 'codelens staleness' smoke-tested end-to-end (text + json output) Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec. StaleFileDetector cache + StaleFile data structure designed to accommodate Phase 2 without API change. Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges first, this PR will need a small rebase to resolve the __init__.py overlap (both add the same package marker) and the mcp_server.py overlap (both add methods to MCPServer — different method names, no conflict). BOS will resolve at merge time.
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed files have been edited since the last scan and surfaces a warning to the agent before the tool's actual output. New files: - scripts/sync/__init__.py — re-exports public API - scripts/sync/pending.py — StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner() - scripts/commands/staleness.py — 'codelens staleness' CLI command (manual check + full list when MCP banner truncates to 10) - tests/test_staleness.py — 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration) - docs/sync/staleness-banner.md — architecture + design decisions Modified: - scripts/mcp_server.py — MCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan - README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 -> 71) Detection algorithm: 1. Load stored mtimes from .codelens/mtimes.json 2. For each indexed file: os.stat() compare (st_size, st_mtime_ns) 3. If mtime differs, re-compute SHA-256 (only when needed) to confirm content actually changed — skips false positives from / M README.md M SKILL-QUICK.md M SKILL.md A docs/sync/staleness-banner.md M pyproject.toml A scripts/commands/staleness.py M scripts/graph_model.py M scripts/mcp_server.py A scripts/sync/__init__.py A scripts/sync/pending.py M skill.json A tests/test_staleness.py of identical content 4. Sort by edit_age ascending (most recent edit first) 5. Return tuple of StaleFile records Why mtimes.json (not SQLite files table) as source of truth? mtimes.json is written by every scan, including workspaces that use the legacy JSON registry (pre-v8.2). The SQLite files table is only populated when the persistent registry is active. Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Banner: plain text (not markdown), prepended to first content block's text + structured response['_staleness'] field. Both paths ensure the warning surfaces regardless of how agents consume tool output. Verified: - tests/test_staleness.py: 41 passed - tests/test_staleness.py + test_command_count.py + test_doctor.py + test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed - sync_command_count.py --check: clean - 'codelens staleness' smoke-tested end-to-end (text + json output) Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec. StaleFileDetector cache + StaleFile data structure designed to accommodate Phase 2 without API change. Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges first, this PR will need a small rebase to resolve the __init__.py overlap (both add the same package marker) and the mcp_server.py overlap (both add methods to MCPServer — different method names, no conflict). BOS will resolve at merge time.
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed files have been edited since the last scan and surfaces a warning to the agent before the tool's actual output. New files: - scripts/sync/__init__.py — re-exports public API - scripts/sync/pending.py — StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner() - scripts/commands/staleness.py — 'codelens staleness' CLI command (manual check + full list when MCP banner truncates to 10) - tests/test_staleness.py — 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration) - docs/sync/staleness-banner.md — architecture + design decisions Modified: - scripts/mcp_server.py — MCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan - README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 -> 71) Detection algorithm: 1. Load stored mtimes from .codelens/mtimes.json 2. For each indexed file: os.stat() compare (st_size, st_mtime_ns) 3. If mtime differs, re-compute SHA-256 (only when needed) to confirm content actually changed — skips false positives from / M README.md M SKILL-QUICK.md M SKILL.md A docs/sync/staleness-banner.md M pyproject.toml A scripts/commands/staleness.py M scripts/graph_model.py M scripts/mcp_server.py A scripts/sync/__init__.py A scripts/sync/pending.py M skill.json A tests/test_staleness.py of identical content 4. Sort by edit_age ascending (most recent edit first) 5. Return tuple of StaleFile records Why mtimes.json (not SQLite files table) as source of truth? mtimes.json is written by every scan, including workspaces that use the legacy JSON registry (pre-v8.2). The SQLite files table is only populated when the persistent registry is active. Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Banner: plain text (not markdown), prepended to first content block's text + structured response['_staleness'] field. Both paths ensure the warning surfaces regardless of how agents consume tool output. Verified: - tests/test_staleness.py: 41 passed - tests/test_staleness.py + test_command_count.py + test_doctor.py + test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed - sync_command_count.py --check: clean - 'codelens staleness' smoke-tested end-to-end (text + json output) Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec. StaleFileDetector cache + StaleFile data structure designed to accommodate Phase 2 without API change. Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges first, this PR will need a small rebase to resolve the __init__.py overlap (both add the same package marker) and the mcp_server.py overlap (both add methods to MCPServer — different method names, no conflict). BOS will resolve at merge time.
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed files have been edited since the last scan and surfaces a warning to the agent before the tool's actual output. New files: - scripts/sync/__init__.py — re-exports public API - scripts/sync/pending.py — StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner() - scripts/commands/staleness.py — 'codelens staleness' CLI command (manual check + full list when MCP banner truncates to 10) - tests/test_staleness.py — 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration) - docs/sync/staleness-banner.md — architecture + design decisions Modified: - scripts/mcp_server.py — MCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan - README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 -> 71) Detection algorithm: 1. Load stored mtimes from .codelens/mtimes.json 2. For each indexed file: os.stat() compare (st_size, st_mtime_ns) 3. If mtime differs, re-compute SHA-256 (only when needed) to confirm content actually changed — skips false positives from / M README.md M SKILL-QUICK.md M SKILL.md A docs/sync/staleness-banner.md M pyproject.toml A scripts/commands/staleness.py M scripts/graph_model.py M scripts/mcp_server.py A scripts/sync/__init__.py A scripts/sync/pending.py M skill.json A tests/test_staleness.py of identical content 4. Sort by edit_age ascending (most recent edit first) 5. Return tuple of StaleFile records Why mtimes.json (not SQLite files table) as source of truth? mtimes.json is written by every scan, including workspaces that use the legacy JSON registry (pre-v8.2). The SQLite files table is only populated when the persistent registry is active. Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Banner: plain text (not markdown), prepended to first content block's text + structured response['_staleness'] field. Both paths ensure the warning surfaces regardless of how agents consume tool output. Verified: - tests/test_staleness.py: 41 passed - tests/test_staleness.py + test_command_count.py + test_doctor.py + test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed - sync_command_count.py --check: clean - 'codelens staleness' smoke-tested end-to-end (text + json output) Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec. StaleFileDetector cache + StaleFile data structure designed to accommodate Phase 2 without API change. Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges first, this PR will need a small rebase to resolve the __init__.py overlap (both add the same package marker) and the mcp_server.py overlap (both add methods to MCPServer — different method names, no conflict). BOS will resolve at merge time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #66 (phase 4 only — phases 1, 2, 3, 5 remain open for other workers).
What
Add git worktree ↔ CodeLens index mismatch detection. When a user runs CodeLens inside a git worktree that does not have its own
.codelens/index, CodeLens's workspace auto-detection silently walks up and loads the main checkout's index — which was built from a different branch. Every subsequentquery/trace/dataflow/taintanswer is then grounded in the wrong file set, with no warning to the user or the agent.Why
Issue #66 Phase 4 explicitly calls for this:
This is a real correctness bug. A worktree user gets stale data with no indication anything is wrong.
How
New module
scripts/sync/worktree.pywith three public functions:detect_worktree_index_mismatch(project_root)→ structured recordformat_worktree_warning(mismatch)→ multi-line CLI warningformat_worktree_banner(mismatch)→ single-line MCP bannerThe detector shells out to
git rev-parse --show-toplevelandgit rev-parse --git-common-dir(max 2 subprocess calls per probe, 5s timeout each, never raises). Returns 5 reason codes:not_a_git_reponot_a_worktreeworktree_has_own_index.codelens/(correct setup)no_index_found.codelens/anywhere up the treeworktree_uses_main_indexWiring
codelens doctor— new checkworkspace.worktree_index_mismatch(status=warning on mismatch). Runs BEFORE_check_codelens_writablebecause the writable check creates.codelens/as a side effect of probing write permissions, which would mask the mismatch.MCP server — read-tool responses get a top-level
_worktree_warningfield when a mismatch is detected:{ "content": [...], "isError": false, "_worktree_warning": { "banner": "⚠️ WORKTREE INDEX MISMATCH: ...", "mismatch": { "mismatch": true, "reason": "worktree_uses_main_index", ... } } }Mutating commands (
scan,init) skip the banner — they're the user's remediation path. The mismatch verdict is cached per workspace for the server's lifetime (probed once, reused). Cache is invalidated whenscanruns (the user may have just runcodelens init -ito fix the mismatch).Why a top-level field, not a content prepend: Prepending text to the
contentarray would corrupt the JSON payload agents parse out of the response. A top-level field keeps the JSON intact while still surfacing the warning prominently.Critical implementation detail: early-probe
The mismatch detection runs BEFORE
_execute_command, not after. Why: read commands likelist/querytriggerensure_codelens_dir(workspace)as a side effect of loading the registry, which creates.codelens/in the worktree. If we probed after execution, the worktree would appear to have its own index and the mismatch would never fire. Probing early caches the pre-execution state so the banner reflects what the user actually configured.Tests
30 new tests across 3 files (all green):
tests/test_worktree.py(19 tests) — detector + formatters, covers all 5 reason codes plus robustness (None input, file path, empty string, subdirectory of worktree).tests/test_doctor.py(6 new tests inTestWorktreeMismatchCheckclass) — doctor integration, including an ordering test that enforces "mismatch check runs BEFORE writable check".tests/test_mcp_hooks.py(5 new tests inTestWorktreeBannerAttachmentclass) — MCP integration, including cache-once-per-workspace and detection-never-breaks-tool-call contracts.Test results
The 1 failure is pre-existing and unrelated to this PR:
This test was failing on
origin/mainbefore this branch was created.Files
scripts/sync/__init__.pyscripts/sync/worktree.pyscripts/commands/doctor.py_check_worktree_mismatch, reordered_run_all_checksscripts/mcp_server.py_worktree_mismatch_cache,_get_worktree_mismatch,_attach_worktree_banner; wired into_handle_tools_call(read tools only)tests/test_worktree.pytests/test_doctor.pyTestWorktreeMismatchCheckclass (6 tests)tests/test_mcp_hooks.pyTestWorktreeBannerAttachmentclass (5 tests)CHANGELOG.md[8.2.0]section for issue #66 Phase 4Acceptance criteria (from issue #66)
codelens doctoroutput_worktree_warningfield)codelens init -iin the worktree to build its own indexOut of scope (other phases of issue #66)
scripts/sync/pending.py)These phases remain open for other workers — the issue is not closed by this PR.
Findings
While implementing this, I noticed that
_check_codelens_writableindoctor.pycreates.codelens/as a side effect of probing write permissions. This is intentional (it's the only way to verify write access), but it means any doctor check that depends on the pre-doctor state of.codelens/must run BEFORE the writable check. The_check_worktree_mismatchcheck does this correctly, but future checks should be aware of this ordering constraint. I've added a comment in_run_all_checksdocumenting it.