Skip to content

Commit 0b41bc0

Browse files
committed
feat(sync): per-file staleness banner — MCP + CLI (closes #66 phase-1)
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.
1 parent af53566 commit 0b41bc0

12 files changed

Lines changed: 1823 additions & 15 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
> **Before an AI writes a new class/id/function, CodeLens must be checked. This is not optional.**
44
5-
CodeLens is an AI-native code intelligence platform that gives AI agents **full visibility** into a codebase before they write any code. It prevents collision, overwrite of existing logic, security vulnerabilities, and dead code through 70 CLI commands, an MCP server with 68 tools (54 static + 14 dynamic), AST-based taint analysis, live CVE/OSV scanning, a plugin system with OWASP Top 10 + Compliance rule packs, a true graph data model (nodes + edges) for structural code queries, and token-efficient `--format compact` output for high-volume agent workflows (issue #17).
5+
CodeLens is an AI-native code intelligence platform that gives AI agents **full visibility** into a codebase before they write any code. It prevents collision, overwrite of existing logic, security vulnerabilities, and dead code through 71 CLI commands, an MCP server with 69 tools (54 static + 15 dynamic), AST-based taint analysis, live CVE/OSV scanning, a plugin system with OWASP Top 10 + Compliance rule packs, a true graph data model (nodes + edges) for structural code queries, and token-efficient `--format compact` output for high-volume agent workflows (issue #17).
66

77
## Features
88

9-
- **70 CLI Commands** — From basic scan/query to AST taint analysis, CVE scanning, plugin management, auto-fix, dashboards, CI/CD quality gates, and `graph-schema` for cheap graph-shape introspection
10-
- **MCP Server (68 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 54 statically-defined tools + 14 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`)
9+
- **71 CLI Commands** — From basic scan/query to AST taint analysis, CVE scanning, plugin management, auto-fix, dashboards, CI/CD quality gates, and `graph-schema` for cheap graph-shape introspection
10+
- **MCP Server (69 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 54 statically-defined tools + 15 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`)
1111
- **Token-Efficient Compact Output (v8.2, issue #17)**`--format compact` produces single-char-key JSON with abbreviated types, omitted null fields, and relative paths — ~50% smaller than `json` on real trace output. Combined with `--limit`/`--offset` pagination, 5 structural queries now cost <5k tokens (down from 30-80k)
1212
- **AST Taint Engine** — Tree-sitter based taint analysis with return-value propagation, scope hierarchy, and branch condition refinement
1313
- **Live CVE/OSV Scanning** — Real-time vulnerability data from OSV.dev API with SQLite cache, 9 ecosystems (PyPI, npm, crates.io, Go, Maven, NuGet, RubyGems, Pub, Hex)
@@ -225,8 +225,8 @@ codelens/
225225
│ ├── changelog.md # Older changelog (per-version highlights)
226226
│ └── agent-integration.md # AI agent integration guide
227227
├── scripts/
228-
│ ├── codelens.py # CLI entry point (70 commands registered)
229-
│ ├── mcp_server.py # MCP JSON-RPC server (68 tools)
228+
│ ├── codelens.py # CLI entry point (71 commands registered)
229+
│ ├── mcp_server.py # MCP JSON-RPC server (69 tools)
230230
│ ├── registry.py # Registry read/write/build
231231
│ ├── persistent_registry.py # SQLite persistent storage (WAL mode)
232232
│ ├── base_parser.py # Base tree-sitter parser

SKILL-QUICK.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co
114114
| "Cross-file taint" | `dataflow` | `taint` (taint is single-file, AST-deep) |
115115
| "Auto-fix issues" | `fix` | `check` (check just gates, doesn't fix) |
116116

117-
## All 70 Commands
117+
## All 71 Commands
118118

119119
### Setup & Lifecycle (8+)
120120
`init` · `scan [--incremental] [--max-files N] [--full]` · `registry-validate` · `detect` · `watch [--debounce SECS] [--git-mode] [--interval SECS]` · `git-status` · `migrate` · `serve` · `lsp-status` (issue #33: `codelens --lsp-status` top-level flag is an alias of `codelens lsp-status` — both delegate to `hybrid_engine.get_lsp_status()` and return the identical payload)
@@ -146,19 +146,19 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co
146146
### Tooling (1)
147147
`plugin <install|list|search|update|info|validate>`
148148

149-
**Total: 70 commands** (auto-registered via `commands/__init__.py`; rerun `python3 scripts/sync_command_count.py --apply` after adding/removing a command)
149+
**Total: 71 commands** (auto-registered via `commands/__init__.py`; rerun `python3 scripts/sync_command_count.py --apply` after adding/removing a command)
150150

151-
## MCP Server (68 Tools)
151+
## MCP Server (69 Tools)
152152

153153
Start the MCP server for AI agent integration:
154154

155155
```bash
156156
python3 scripts/codelens.py serve
157157
```
158158

159-
Exposes 68 tools as `codelens_<command>` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`):
159+
Exposes 69 tools as `codelens_<command>` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`):
160160
- 50 statically-defined tools (full JSON schemas in `mcp_server.py`)
161-
- 14 dynamically-discovered tools (auto-discovered from `COMMAND_REGISTRY`; long-running `watch` and `serve` are excluded)
161+
- 15 dynamically-discovered tools (auto-discovered from `COMMAND_REGISTRY`; long-running `watch` and `serve` are excluded)
162162
- Every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`). Use `format: "compact"` for token-efficient responses (~50% smaller than `json`).
163163
- `watch` and `serve` itself are excluded (long-running)
164164

SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
name: codelens
33
description: >
4-
CodeLens — AI-Native Code Intelligence. 70 commands for AI-powered code analysis,
4+
CodeLens — AI-Native Code Intelligence. 71 commands for AI-powered code analysis,
55
security auditing, quality scoring, AST-based taint analysis, live CVE scanning,
66
and pre-write safety checks. Supports 28+ languages with tree-sitter + regex
7-
fallback parsing. MCP server exposes 68 tools for AI agent integration.
7+
fallback parsing. MCP server exposes 69 tools for AI agent integration.
88
For quick command reference with validated output schemas, see SKILL-QUICK.md.
99
For version history, see CHANGELOG.md.
1010
---

docs/sync/staleness-banner.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Per-File Staleness Banner (issue #66 Phase 1)
2+
3+
> **Status:** Phase 1 shipped. Phases 2–5 tracked as follow-up issues.
4+
> **Last updated:** 2026-07-02
5+
6+
## Alasan Dibuat
7+
8+
After a `codelens scan`, the index is a snapshot. If the user edits a
9+
file after the scan, queries against the index may return outdated
10+
symbol locations, dead-code verdicts, or stale call graphs. Before
11+
Phase 1, the MCP server happily served stale results with no warning —
12+
agents acted on outdated data without knowing.
13+
14+
Phase 1 adds a **per-file staleness banner** that detects when indexed
15+
files have been edited since the last scan and surfaces a warning to
16+
the agent before the tool's actual output. The banner is prepended to
17+
every read-tool response (suppressed on `scan`/`init` — those are the
18+
fix path, not analysis calls).
19+
20+
## Arsitektur
21+
22+
```
23+
scripts/sync/
24+
├── __init__.py # Re-exports public API
25+
└── pending.py # StaleFileDetector + detect_stale_files()
26+
# + format_staleness_banner()
27+
# In-memory Dict[str, float] cache, thread-safe
28+
# via threading.Lock, 5s TTL per workspace.
29+
30+
scripts/commands/
31+
└── staleness.py # `codelens staleness` CLI command — manual check
32+
# + full list when MCP banner truncates to 10
33+
34+
scripts/mcp_server.py # MCPServer._staleness_detector (lazy)
35+
# + _attach_staleness_banner() — prepends banner
36+
# + _invalidate_staleness_cache() — after scan
37+
38+
tests/
39+
└── test_staleness.py # 41 tests — detection, cache, thread safety,
40+
# banner formatting, CLI, MCP integration
41+
```
42+
43+
## Detection algorithm
44+
45+
```
46+
1. Load stored mtimes from .codelens/mtimes.json
47+
(source of truth — written by incremental.save_mtimes() on every scan)
48+
2. For each indexed file:
49+
a. os.stat() — if file gone, skip (deletion is Phase 2's concern)
50+
b. If |current_mtime - stored_mtime| <= 0.001s, skip (filesystem noise)
51+
c. If confirm_with_hash=True (default):
52+
- Compute SHA-256 of current content
53+
- Load stored hash from SQLite `files` table (if available)
54+
- If hashes match, skip (mtime changed but content identical — e.g. `touch`)
55+
- If hashes differ or stored hash unavailable, flag as stale
56+
d. Else (confirm_with_hash=False): flag on mtime change alone
57+
3. Sort by edit_age ascending (most recent edit first)
58+
4. Return tuple of StaleFile records
59+
```
60+
61+
**Why mtimes.json (not SQLite `files` table) as the source of truth?**
62+
`mtimes.json` is written by every scan, including workspaces that use
63+
the legacy JSON registry (pre-v8.2). The SQLite `files` table is only
64+
populated when the persistent registry is active. Using `mtimes.json`
65+
keeps Phase 1 working on every workspace configuration.
66+
67+
**Why 0.001s mtime tolerance?**
68+
Filesystems with coarse mtime resolution (FAT32, some network shares)
69+
can report mtimes that differ by sub-millisecond even when content is
70+
identical. The stored mtime comes from `os.path.getmtime()` which
71+
returns a float; comparing with a small epsilon avoids false positives
72+
from filesystem noise.
73+
74+
## Cache
75+
76+
`StaleFileDetector` caches results per workspace for 5 seconds
77+
(`DETECTOR_CACHE_TTL_SECONDS`). The cache is:
78+
79+
- **Thread-safe** — protected by `threading.Lock`. The MCP server
80+
dispatches tool calls in a thread pool, so concurrent calls must not
81+
race or duplicate work.
82+
- **Per-workspace** — keyed by absolute workspace path. Multiple
83+
workspaces don't interfere.
84+
- **Invalidated on scan**`MCPServer._invalidate_staleness_cache()`
85+
is called after a successful `scan` command. The scan refreshes the
86+
index, so any cached staleness verdict is now stale itself.
87+
88+
The cache exists because walking 10k+ files on every tool call would
89+
add ~50 ms of latency. A 5-second TTL keeps the banner fresh enough
90+
for interactive use (a user editing a file should see the banner
91+
within seconds) without re-stat-ing the whole tree on every query.
92+
93+
## Banner shape
94+
95+
```
96+
⚠️ Some files referenced below were edited since the last index sync.
97+
The index may be stale for these files — re-run `codelens scan` to refresh.
98+
Stale files (showing 3 of 3, most recent first):
99+
• path/to/file.py (edited 2.3s ago, content differs)
100+
• other.js (edited 1m 12s ago, content differs)
101+
• third.ts (edited 5m 0s ago, size/mtime differ)
102+
```
103+
104+
- **Plain text** (not markdown) — renders correctly in both terminal
105+
output and MCP tool-response content blocks.
106+
- **⚠️ marker** — agents can pattern-match on it.
107+
- **"content differs"** vs **"size/mtime differ"** — distinguishes
108+
hash-confirmed staleness from mtime-only staleness.
109+
- **Most recent first** — the agent sees the most relevant context first.
110+
- **Truncates to 10 files** with "and N more" — keeps the banner
111+
actionable; the full list is available via `codelens staleness`.
112+
113+
## MCP integration
114+
115+
`MCPServer._handle_tools_call` calls `_attach_staleness_banner()` on
116+
three response paths:
117+
118+
1. **Cached response** — banner attached (the workspace's staleness is
119+
independent of whether the tool result was cached).
120+
2. **Fresh success** — banner attached, unless the command is `scan` or
121+
`init` (those are the remediation path).
122+
3. **Error response** — banner attached, unless `scan`/`init`. If the
123+
user is in a stale workspace, that context is more useful than the
124+
error itself — the error is almost certainly caused by the stale
125+
index.
126+
127+
The banner is prepended to the first content block's `text` field AND
128+
attached as a structured `response["_staleness"]` field. Both paths
129+
ensure the warning surfaces — agents that pattern-match on JSON keys
130+
see the structured field; agents that read only the text see the
131+
prepended banner.
132+
133+
After a successful `scan`, `_invalidate_staleness_cache(workspace)` is
134+
called so the next read tool re-probes against the fresh index.
135+
136+
## CLI command
137+
138+
```bash
139+
# Check staleness (text output, default)
140+
codelens staleness [workspace]
141+
142+
# JSON output for scripts
143+
codelens staleness [workspace] --format json
144+
145+
# Skip SHA-256 confirmation (faster, false-positive on `touch`)
146+
codelens staleness [workspace] --no-confirm-hash
147+
148+
# Show more files in the banner (default 10)
149+
codelens staleness [workspace] --limit 50
150+
```
151+
152+
## Definition of Done (Phase 1, dari issue)
153+
154+
- [x] In-memory `Dict[str, float]` (path → edit_timestamp), thread-safe via `threading.Lock`
155+
- [x] Walk indexed file list with `os.stat(path)` to compare `(st_size, st_mtime_ns)`
156+
- [x] Re-compute content-hash only when size/mtime changed
157+
- [x] Prepend `⚠️ Some files referenced below were edited since the last index sync…` banner to MCP responses
158+
- [x] Surface non-referenced pending files as small footer (the "and N more" line + `codelens staleness` for full list)
159+
- [x] New file: `scripts/sync/pending.py`
160+
161+
Phase 2 (connect-time catch-up), Phase 3 (native file watcher), and
162+
Phase 5 (anonymous telemetry) are deferred to follow-up issues.
163+
164+
## Design decisions
165+
166+
1. **Why a separate `scripts/sync/` subpackage?**
167+
Staleness and worktree mismatch (Phase 4, separate PR #154) are
168+
independent concerns that share only the "index vs working tree"
169+
theme. A package keeps them discoverable without forcing them into
170+
one file (single-responsibility rule).
171+
172+
2. **Why lazy construction of the detector in MCPServer?**
173+
Keeps the import out of the server's startup path. If the sync
174+
subpackage ever fails to import (e.g. a missing dependency in a
175+
stripped-down install), the server still starts and only staleness
176+
detection is degraded.
177+
178+
3. **Why prepend (not append) the banner?**
179+
Agents read tool output top-to-bottom. If the banner is at the
180+
bottom, the agent may have already acted on stale data before
181+
reaching it. Prepending ensures the warning is the first thing the
182+
agent sees.
183+
184+
4. **Why both structured `_staleness` field AND prepended text?**
185+
Different agents consume tool output differently. Some
186+
pattern-match on JSON keys (those see the structured field). Others
187+
read only the text content (those see the prepended banner). Both
188+
paths ensure the warning surfaces without requiring agents to
189+
change.
190+
191+
5. **Why is `content_hash_changed` a tri-state (True/False/None)?**
192+
- `True` — size/mtime differ AND content hash differs (definitely stale)
193+
- `False` — size/mtime differ BUT content hash matches (not stale, e.g. `touch`)
194+
- `None` — size/mtime differ, no stored hash available to confirm
195+
(probably stale, but can't be sure). The banner says "size/mtime
196+
differ" rather than "content differs" in this case.
197+
198+
6. **Why sort ascending by edit_age (smallest first)?**
199+
`edit_age = now - current_mtime`. A file edited 1s ago has age=1;
200+
a file edited 10s ago has age=10. Ascending puts age=1 first →
201+
most recent first, matching the banner text.
202+
203+
## Testing
204+
205+
```
206+
PYTHONUTF8=1 PYTHONPATH=scripts python3 -m pytest tests/test_staleness.py -v
207+
```
208+
209+
41 tests, all network-free and filesystem-light. Tests create small
210+
temporary workspaces with synthetic `mtimes.json` files — no real
211+
CodeLens scan is needed. Coverage:
212+
213+
- Basic detection (mtime change, deleted files, empty workspace)
214+
- Content-hash confirmation (touch without content change)
215+
- Cache (TTL, invalidation, all-workspace invalidation)
216+
- Thread safety (20 concurrent calls)
217+
- Banner formatting (single file, truncation, age format)
218+
- CLI command (registration, JSON/text output, --no-confirm-hash)
219+
- MCP integration (prepend on read tools, suppress on scan/init,
220+
invalidate after scan, init failure isolation)
221+
222+
## Phases 2–5 (deferred)
223+
224+
| Phase | Scope | Status |
225+
|-------|----------------------------------------------------------|----------------|
226+
| 2 | Connect-time catch-up — content-hash reconciliation on MCP reconnect | Not started |
227+
| 3 | Native file watcher (FSEvents/inotify/ReadDirectoryChangesW) | Not started |
228+
| 4 | Worktree mismatch detection (PR #154) | PR ready |
229+
| 5 | Anonymous opt-in telemetry | Not started |
230+
231+
Phase 2 will extend `StaleFileDetector` to also run content-hash
232+
reconciliation on MCP reconnect, blocking the first query until
233+
catch-up finishes (or 5s timeout, then proceed with stale + banner).
234+
The `StaleFileDetector` cache and `StaleFile` data structure are
235+
designed to accommodate this without API change.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "codelens"
77
version = "8.2.0"
8-
description = "Live Codebase Reference Intelligence — 70 commands for AI-powered code analysis, security auditing, and quality scoring"
8+
description = "Live Codebase Reference Intelligence — 71 commands for AI-powered code analysis, security auditing, and quality scoring"
99
readme = "README.md"
1010
license = {text = "MIT"}
1111
requires-python = ">=3.8"

0 commit comments

Comments
 (0)