CodeSentry is a language-agnostic code-understanding assistant. It builds a unified graph model of a repository and uses an LLM agent to answer questions and review code changes with real grounding in the source — every claim cites a real file and line, rather than being hallucinated.
Phase 1 supports Python, JavaScript, TypeScript, Go, and Java in a single graph. Adding a new language is a matter of writing one parser adapter; the graph, retrieval, agent, and CLI never branch on language.
Phase 1 is feature-complete through the diff reviewer. The build proceeded in the
strict order defined in docs/PHASE_1_SPEC.md:
| # | Step | Status |
|---|---|---|
| 1 | Scaffolding, tooling, test skeleton | ✅ |
| 2 | Universal graph schema (Node, Edge, enums) |
✅ |
| 3 | LanguageAdapter base class + registry |
✅ |
| 4 | Python adapter + minimal builder/store + index/stats |
✅ |
| 5 | JavaScript adapter (+ mixed-repo indexing) | ✅ |
| 6 | TypeScript adapter (interfaces, generics, .tsx) |
✅ |
| 7 | Go adapter (receiver methods, embedding) | ✅ |
| 8 | Java adapter (extends/implements, nested classes) | ✅ |
| 9 | Cross-file resolution (imports, calls, heritage) | ✅ |
| 10 | Retrieval layer (subgraph + snippets) | ✅ |
| 11 | Agent schemas, LLMClient, tools |
✅ |
| 12 | Agent loop + prompts + ask command |
✅ |
| 13 | Diff review + review command |
✅ |
| 14 | This README | ✅ |
Quality gates: 113 passing tests, mypy --strict clean on codesentry/,
no openai import outside agent/llm.py. No test ever calls the real API.
Known gap: the spec lists a
codesentry-agent languagescommand that is not yet implemented; the supported languages are documented in the table below instead. Theaskandreviewcommands require anOPENAI_API_KEY(see below).
| Language | Extensions | Notes |
|---|---|---|
| Python | .py, .pyi |
decorators, docstrings, intra-file calls |
| JavaScript | .js, .jsx, .mjs, .cjs |
classes, arrow/function-expression declarations, nested functions, top-level route callbacks (router.post(...)), ESM + require, JSDoc |
| TypeScript | .ts, .tsx, .mts, .cts |
interfaces & type aliases as CLASS nodes, implements, generics, import type |
| Go | .go |
struct/interface types, receiver methods, embedding → INHERITS; interface satisfaction is not resolved in Phase 1 |
| Java | .java |
extends → INHERITS, implements → IMPLEMENTS, nested classes, annotations |
repository ──► LanguageAdapter (per file, tree-sitter)
│ emits universal Node/Edge objects
▼
graph/builder ──► networkx MultiDiGraph
│ cross-file resolution (imports, calls, heritage)
▼
graph/store (pickle + JSON sidecar)
│
┌────────────┼───────────────┐
▼ ▼ ▼
retrieval/ agent/loop review/reviewer
(subgraph, (tools + LLM, (diff → per-file
snippets) cited answers) structured review)
Core ideas:
- Universal graph schema (
graph/schema.py). Nodes areFILE,MODULE,CLASS,FUNCTION,METHOD,FIELD; edges areCONTAINS,CALLS,IMPORTS,INHERITS,IMPLEMENTS. Language-specific detail (decorators, annotations, generics, receiver types, ...) lives in ametadatadict. - Language adapters (
languages/) are the only code that knows a language. Each parses one language with tree-sitter and emits universal nodes/edges plus unresolved cross-file hints stashed in metadata. - Cross-file resolution (
graph/builder.py) connects the per-file graphs. The only language-specific step — mapping an import to a file — is delegated to each adapter'sresolve_import; everything else is driven off universal metadata, so the builder never branches on language. Member calls (obj.method()) only ever bind to methods/classes, and receivers that name an import resolve inside the imported file. Calls that cannot target indexed code at all (library imports, builtins, locals) are counted as external rather than unresolved, sounresolved_callsmeasures real resolution misses. - Retrieval (
retrieval/) turns the graph into LLM context:extract_subgraph(neighbors along any edge type, 1–2 hops) andget_snippet(exact source lines). - Agent (
agent/) runs a hand-rolled tool loop. The nine tools query the graph and files; the model must ground every claim in tool output and citefile:line.agent/llm.pyis the only file that importsopenai. - Review (
review/) parses a unified diff and, per changed file, gathers graph context and asks the model for structured, line-level comments.
codesentry/
├── config.py # Settings + get_settings()
├── graph/ # schema, builder (+ cross-file resolution), store
├── languages/ # base (adapter + registry) + one file per language
├── retrieval/ # subgraph extraction, source snippets
├── agent/ # llm, schemas, tools, prompts, loop
├── review/ # diff parsing, reviewer
└── cli.py # index / stats / ask / review
tests/ # per-module tests + fixtures/ (one sample per language)
Requires Python 3.11+ and uv.
# Install dependencies (creates .venv)
uv sync
# Configure (only needed for `ask` and `review`)
cp .env.example .env
# then edit .env and set OPENAI_API_KEYEnvironment variables (see .env.example):
| Variable | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
— | required for ask / review |
CODESENTRY_MODEL |
gpt-4.1 |
model id (use gpt-4.1-mini for cheap iteration) |
CODESENTRY_MAX_TOKENS |
4096 |
max tokens per response |
CODESENTRY_LOG_LEVEL |
INFO |
log verbosity |
OPENAI_BASE_URL |
— | optional endpoint override |
Run the CLI with uv run codesentry-agent <command>.
uv run codesentry-agent index /path/to/repoIndexed 66 nodes, 81 edges -> /path/to/repo/.codesentry/graph.pkl
Files per language
┏━━━━━━━━━━━━┳━━━━━━━┓
┃ Language ┃ Files ┃
┡━━━━━━━━━━━━╇━━━━━━━┩
│ go │ 4 │
│ python │ 4 │
│ typescript │ 4 │
└────────────┴───────┘
The graph is saved to .codesentry/graph.pkl inside the repo, with a JSON sidecar
of metadata (counts, per-language breakdown, git commit, resolution summary).
uv run codesentry-agent stats /path/to/repoRepository: /path/to/repo
Nodes: 66 Edges: 81
Unresolved calls: 2 External calls: 14 Skipped files: 0 Parse errors: 0
Files per language
┏━━━━━━━━━━━━┳━━━━━━━┓
┃ Language ┃ Files ┃
┡━━━━━━━━━━━━╇━━━━━━━┩
│ go │ 4 │
│ python │ 4 │
│ typescript │ 4 │
└────────────┴───────┘
uv run codesentry-agent ask /path/to/repo "Where is the user repository and what calls it?"Runs the agent tool-loop and prints an answer whose every factual claim is backed
by a citation to a real file:line, followed by a citations table. Options:
--max-iterations (default 15), --model.
# from a file
uv run codesentry-agent review /path/to/repo --diff change.diff
# or from stdin
git diff | uv run codesentry-agent review /path/to/repoPrints line-level comments grouped by file (severity INFO/WARNING/ERROR),
focused on correctness, broken contracts, missing error handling, and obvious
performance problems — not style.
index has been run on real medium-sized codebases, well within the Phase 1
budget (index in under 90s, graph with >1000 nodes):
| Repo | Files | Nodes | Edges | Index time |
|---|---|---|---|---|
| Flask | 83 | 977 | 2,108 | ~28s |
| Werkzeug | 139 | 2,222 | 4,725 | ~29s |
git clone --depth 1 https://github.com/pallets/werkzeug.git
codesentry-agent index werkzeug
# Indexed 2222 nodes, 4725 edges -> werkzeug/.codesentry/graph.pklask produces answers grounded in real file:line citations. For example, asked
"Give a full architecture overview" of a FastAPI service, it navigates the graph,
reads the relevant files, and returns an answer where each claim points at a real
location (e.g. "The application is a FastAPI server defined in main.py:25 ...
question answering uses AnswerService (services/answer_service.py:151)").
Regenerate live
ask/reviewexamples with your ownOPENAI_API_KEY; avoid committing output from private/client codebases.
No test contacts the network; the LLM is always mocked.
uv run pytest # 101 tests
uv run mypy # strict, on codesentry/index and stats are fully exercisable offline. The repo ships small sample
projects under tests/fixtures/ — index one (or point at any real repo):
uv run codesentry-agent index tests/fixtures/sample_python
uv run codesentry-agent stats tests/fixtures/sample_pythonTo see the language-agnostic graph merge several languages into one graph, copy a few fixtures into one directory and index it:
mkdir /tmp/mixed
cp tests/fixtures/sample_python/*.py tests/fixtures/sample_go/*.go \
tests/fixtures/sample_ts/*.ts /tmp/mixed/
uv run codesentry-agent index /tmp/mixed
uv run codesentry-agent stats /tmp/mixeduv run codesentry-agent index tests/fixtures/sample_python
uv run codesentry-agent ask tests/fixtures/sample_python \
"What does UserRepository.count return, and is it correct?"
# review the fixture's intentional off-by-one bug
printf '%s\n' \
'--- a/repository.py' \
'+++ b/repository.py' \
'@@ -18,3 +18,2 @@ class UserRepository:' \
' def count(self) -> int:' \
'- # BUG: off-by-one; should return len(self._users).' \
'- return len(self._users) + 1' \
'+ return len(self._users)' \
| uv run codesentry-agent review tests/fixtures/sample_pythonEach tests/fixtures/sample_* project intentionally contains an obvious bug
(usually an off-by-one in a count/Count method) for exercising review.
Phase 2 (not started) covers a planner/executor/critic agent pattern, an
evaluation harness, and benchmark numbers against SWE-bench Lite and its
multi-language variants. See docs/PHASE_1_SPEC.md for the full Phase 1 spec.