Node CLI · TypeScript · BM25F · Apache-2.0 · GitHub public · v0.1.0
prepass
Stop the agent searching from zero.
Every coding agent starts cold. Ask it why notifications fire twice in a repo it doesn't know and it burns its first turns on grep and glob, working out where the relevant code lives before it starts on the actual question. prepass runs the moment you press enter, ranks the repository against your request, and hands the agent a shortlist of paths — never file contents. Everything is local and deterministic: no model call, no embedding, no index to build, nothing to sign up for.
- TypeScript (strict)
- Node.js 22+
- BM25F ranking
- ripgrep
- SWE-bench Lite (evaluation)
- Claude Code hook + skill
- Codex CLI · AGENTS.md
- Apache-2.0
Origin
How it started
The expensive part of agentic coding is the part before the work starts. A cold agent spends its opening moves orienting — and orienting is a retrieval problem, which is a solved field with fifty years of literature behind it. The question worth answering was not whether a shortlist helps in principle, but whether a deterministic local pass could actually find the right file often enough to matter, and that question only has an honest answer if somebody else writes both the questions and the answers.
Features
What it does
Paths only — a safety property, not a token optimisation
prepass hands over file paths and never file contents. The agent has to open a file to use it, and opening it is also what disproves a bad suggestion. Tested directly: given a deliberately wrong map labelled confidence="high", the agent still edited the correct file — across all four arms, none touched a planted file.
One ripgrep pass, then BM25F
git ls-files honours .gitignore exactly; a single ripgrep pass over the whole tree collects term frequencies; BM25F scores three fields — contents, filename, directory — each with its own model of how rare a term is. 71 MB scanned in about ten milliseconds, the whole pipeline in a median 105ms over a 1,866-file pool.
A user-editable glossary the ranker consumes
Ranking can only find words that are in the file. You say "arriving"; CoreLocation says didEnterRegion. Nothing lexical crosses that gap, so the bridge gets written down in a plain JSON file. Expansions carry reduced weight and never displace a word you typed. As far as the research could establish, a user-editable glossary consumed by the ranker — rather than fed to the model — doesn't appear in any comparable tool or paper.
explain --why — arithmetic you can argue with
A ranker nobody can question is a ranker nobody trusts. explain itemises every score term by term and field by field, including the column that matters most: how many files share each term. A term in 3 files of 442 is evidence; the same term in 108 of them is barely a hint. It's also how you tell whether a glossary entry is earning its place or distorting things.
doctor — because the failure mode is silence
prepass has no UI and degrades quietly rather than erroring, so a misconfiguration looks exactly like "it ran and found little". That silence is its worst property — it's what let it return zero files on every non-Node project for two weeks. Every check in doctor exists because it silently went wrong for somebody, including the one that cost the most time: an agent started one directory above the repo it was meant to be reading.
Per project, never globally
prepass earns its keep by saving the agent a search. Where there's no search to save it is pure overhead — measured at +27% cost on a 51-file project. It stands aside below 100 files, and the docs say plainly not to register it globally rather than letting the default quietly cost people money.
Under the hood
Engineering
Benchmarked on work nobody here wrote
240 real GitHub issues from SWE-bench Lite across six repositories the author has never opened, with no glossary. The query is the issue text as filed; the correct answer is whichever files the accepted patch touched. Right file first 35.4%, top 5 64.2%, top 20 77.9%, MRR 0.475, median 105ms. The gold file was in the candidate pool 100% of the time, so every miss is a ranking failure rather than a file that was never scored. Picking 20 files at random from the same pools hits 1.52% — roughly 51x a coin flip. These figures come from a re-run against the exact shipped build, and the raw per-instance data ships in the repo: reproduce it with node bench/swebench.mjs, which costs nothing and calls no model.
The number that keeps the other numbers honest
A hand-written test set — where the same person wrote the questions, the answers, and the glossary entries — scores MRR 0.841. The same ranker on issues nobody here wrote scores 0.475. That 0.37 gap is the measured cost of grading your own homework, and it is the reason the published figure is 0.475. Two times in three the top file is still wrong: prepass narrows the haystack, it does not hand you the needle. Which turns out to be enough, because narrowing is what saves the search — a rank-15 hit still cuts 6,000 files to 20.
What the benchmark made visible
Two fixes, neither of them clever, both invisible without measurement. Django has 6,712 files of which 66% share a basename with another (628 __init__.py, 209 tests.py) against sympy's 18% — so "matched the filename" means almost nothing in one repo and a great deal in the other, and each field needed its own rarity model. That plus a properly weighted directory took MRR from 0.305 to 0.383. The larger one came next: Django ships its documentation in-tree, and a GitHub issue is prose describing a feature — so is docs/topics/forms/media.txt, in the same words. Docs are 8% of that tree and were taking 60% of the shortlist. Discounting prose by score rather than removing it from the corpus — pruning measured worse, because dropping files shifts every statistic IDF is computed from — took MRR 0.383 to 0.475 and Django from 67% to 80% on hit@20.
The things that didn't work, kept on purpose
Local embeddings scored MRR 0.247 against the BM25F baseline of the time, 0.383 — the literature says dense beats lexical for natural-language-to-code, and here it didn't; a hybrid won by 3 points of hit@20 and cost 259 MB of dependencies plus an 18-second first-run index, so it isn't shipped. Pseudo-relevance feedback lost in all seven configurations tried, to textbook query drift: it assumes the top results are right, and when the top result is wrong two times in three, that assumption is fatal. Capping query terms was monotonically harmful — 15 terms scored 0.238 against unlimited at 0.347 — because BM25's IDF already down-weights common terms. Extracting paths from stack traces was killed before it was built: the gold path appears in 17% of issues but in only 3 of 70 misses, a 4% ceiling. Each is documented in the code at the point where someone would try it again.
Read the tie, not the win
One A/B on a public sympy issue — same repo, same commit, same model, one setting different — came out 36 tool calls without prepass against 37 with. That near-tie is the honest result. The whole difference is in the opening: without prepass the agent greps, opens the right file, then opens another before editing anything; with prepass its first action is opening the file that needed changing and its second is editing it. Calls before the right edit went 5 to 2, wall clock fell 25% and cost 12%. After that both sessions do the same long tail of work, because that part isn't finding a file. prepass pays for the search phase, not the work phase — and at n=1 this illustrates the mechanism rather than measuring it. The measurement is the 240-issue benchmark.