Two passes and a comparison
A coding agent's first few turns usually go to finding files. context-packer does that search up front, and this is how.
Start with keywords
The baseline is BM25 over each file's path and full text. The tokenizer splits cachedContentTokenCount into cached content token count, so a task that says "token count" matches it. In the tests this alone puts 69% of the needed files in the top 10, in about 50 milliseconds, with no model at all. It's the default provider, and anything smarter has to beat it.
Ask a decision model, not a chat model
Jev is TypeSafe's decision model. It doesn't write text. You give it a JSON state and a set of typed questions, and it returns a probability for each. That shape is what makes it cheap here: one request can hold a task and 60 files, each under its own key, with one question per key.
{ "state": { "task": "Add jitter to retry backoff",
"f000": "path: src/jev.ts\n- export class JevClient ...",
"f001": "path: src/files.ts\n- export function collect ..." },
"questions": { "f000": { "type": "noul", "instructions":
"Implementing the change described in `task` requires reading or editing the file in `f000`." }, ... } }
The wording matters. "Requires reading or editing" ranked better than "requires editing" in earlier experiments, because the files an agent needs include ones it only reads.
Two passes
Sending every file's full source to Jev would cost too much on a large repository, so the pipeline narrows first.
- Sketch pass. Every file becomes a sketch of about 300 tokens: its path, package, and declarations with the first line of their doc comments. Jev scores the sketches 60 to a request. BM25 ranks the full text at the same time.
- Pool. The top 60 files by sketch score and the top 60 by BM25 go into one shortlist of up to 120.
- Full-source pass. Jev reads the first 6,000 characters of each pooled file, 6 files to a request. The BM25 half of the pool starts scoring before the sketch pass finishes.
- Fuse. Each file's score is Jev's probability plus
1 / (1 + keyword_rank / 10). The two count equally; that weight was chosen on dev tasks. - Compare. One last request asks a single
choicequestion over the top ten: which file must be edited? The first two passes judge each file alone; this one makes Jev compare them. A parallel request labels files as edit, test, example or dependency, shown only when Jev is at least 50% sure.
All requests run in parallel, up to 48 at once. On the test projects a search took 2.5 seconds and about $0.007 on average.
Failing loudly
A Jev batch that errors costs its files a zero for that pass, and the pack reports how many batches failed. If every batch fails, the pack fails. Quietly returning a keyword ranking labeled as Jev would be worse than an error. The same goes for a missing key: asking for Jev without one is an error, not a fallback.
Laya, locally
Laya is a small encoder that runs on a CPU. Its window is 512 tokens, so each request carries one file excerpt of at most 1,000 characters, and a 60-file keyword shortlist bounds the work. The server handles one request at a time.
That last detail caused the one real bug found while writing this. The packer sends its batches concurrently, which is right for Jev. For Laya every batch is a single file, so a pack opened about 60 connections at once, and the server reset them. Unit tests with a fake server passed; the first run against a real server failed within a second. The scorer now serializes its own requests, and there's a test that counts requests in flight.
Why a hook
An MCP tool only helps if the agent calls it. In earlier testing, headless Claude Code ignored an offered pack_context tool even when told to use it. So the Claude Code plugin also installs a UserPromptSubmit hook. It runs before Claude reads the prompt and adds the ranked files as context.
A hook runs on every prompt, so it has to know when to stay out of the way. It adds nothing for prompts under four words, slash commands, replies such as "thanks" or "commit that", and prompts where no distinctive word occurs anywhere in the project. That last check is a free keyword pass, run before any paid provider.
{"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command",
"command": "npx -y @taanviir/context-packer@latest hook", "timeout": 60}]}]}}
The hook defaults to keywords. Set CONTEXT_PACKER_HOOK_PROVIDER=jev to use the measured pipeline.
Seeing why
--explain prints one line per pick: the model's probability, the keyword rank, the comparison score, the role, and which task words occur in the file and how often.
0.89 src/jev.ts (edit)
jev 0.88 · keyword #3 · compared 0.93 · edit 0.98 · words: jev×41 backoff×12 retries×6