jev-assist
Jev answers closed questions about code as probabilities, not prose. It applies one judgment across
a pool too large to read, in one of two shapes: ranking the pool against a task (rerank —
reach for it when grep has no literal to narrow with, or narrowed to a flood you cannot order), or
classifying every file in it one at a time (drift — when grep narrowed but cannot tell the
real hits from the noise). Neither is a second opinion on your own reasoning; it does not reason
better than the agent driving it. Use it where the pool is large; decide yourself where the pool is
small.
Commands
| Command | Answers | Cost | Run it |
|---|---|---|---|
jev rerank "<task>" |
Which files does this task touch? | 1 call / 60 files; ~94k input tokens and 5–16s at ~705 files | Once per task |
jev drift [glob] |
Which files broke a convention? | 1 call / file; ~250ms at 2KB, ~950ms at 18KB | On demand |
jev gate [ref] |
Does this diff touch something dangerous? | 1 call / diff; 745–1770ms | Pre-commit |
jev validate [n] |
Is rerank accurate on this repo? |
n × a full rerank |
Once per repo |
jev check |
Is the key and config well-formed? | Free, offline | After editing config |
jev key <API_KEY> |
— stores a key at 0600 | Free, offline | Once per machine |
Exit codes: gate returns 1 on any flag, check returns 1 on any config problem or a missing
key. Every command returns 1 on a usage, config or API error, so a non-zero exit is not by itself
a finding — read the output.
Which command — route on the probe, not the wording
Always grep first. One probe, and its outcome picks the command. Nothing here runs on a task's phrasing: a vague question whose words match the code is still grep's job, and a precise question whose words don't match is not.
| The probe came back | You cannot | Use |
|---|---|---|
| Converging on one neighborhood | — | Neither. Read the files. |
| Empty — no literal to search | find the entry point | rerank |
| Flooded — hundreds of hits | rank them: which matter most for this task | rerank |
| Flooded — hundreds of hits | classify them: which are real instances vs noise | drift |
| A diff, about to be committed | see what a linter cannot | gate |
Rank vs classify is the distinction that gets missed. Both start from a flood. Ask which question the flood failed to answer:
- "Which of these 200 files matter for the change I'm making?" → one task, many candidates,
needs an ordering →
rerank. - "Which of these 200 hits are actually the thing I'm looking for?" → one question, many files,
needs a yes/no per file →
drift.
The tell is what the exclusion asks, not how many you wrote. Iterating on a pattern is normal —
fixing a word boundary, narrowing a path, changing the output format. Those stay in grep. The
signal is an exclusion that needs to know what a file is or what a match means: is this string
user-facing copy or a comment, is this file a fixture or the real thing, is this catch {}
deliberate. A pattern cannot carry that context, so each such exclusion is a judgment you are
hand-coding against a proxy. Those belong in a convention's ok field, as prose, evaluated per
file.
Narrow before judging. drift is the usual place the two compose: when a literal search can
cheaply rule out most of the repo, run it first and pass the survivors as a glob or path list
rather than scanning everything at 1 call per file. (The reverse order, grep after rerank, is
Combining with grep.)
Invoking
jev is on PATH only after npm link. Installed as a skill it is not, so call the script by
absolute path. Nothing sets $SKILL_DIR for you — substitute the real path, quoted if it
contains a space:
node "/absolute/path/to/jev-assist/scripts/jev.mjs" validate 20
Below, jev is shorthand for that command. The working directory is always the repo being
judged, never the skill directory.
What the script touches and sends
scripts/jev.mjs is the only code this skill runs — one ~370-line file, zero npm
dependencies, maintained at github.com/glud123/jev-assist where every change sits in the
git history. Its side-effect surface is greppable in a minute: exactly one fetch (in
ask(), to the endpoint the key prefix selects — the provider table in Setup — or
JEV_API_URL); execFile spawns only git; no eval, no dynamic imports. The only
writes are the key file, stored outside every repo at 0600, and .jev-rerank.json in the
repo root when rerank --json opts into it.
What leaves the machine: rerank sends file paths, the config's description and the
task text; drift sends each file's contents; gate sends the diff. Every call carries
the API key. That is the tool working, not a leak — if a repo must not leave the machine,
do not point jev at it.
Setup
Requires an API key plus jev.config.json in the repo root.
Key first, and stop for it. If the user has not given you a key and none is stored
(jev check says no API key), ask for it and wait. Do not write the config, do not run
validate, do not proceed with the surrounding task — every command that calls the API fails
without a key, so any work you do first is work you did not need to do yet. One question up
front beats a finished task that ends in "now paste your key".
Everything else you can derive by reading the repo. The key you cannot.
Key
jev key sk-or-v1-...
- Store it for the user; do not ask them to run
export. - Lands in
~/.config/jev/keyat 0600 ($XDG_CONFIG_HOMEhonoured), outside the repo — a key in the working tree eventually gets committed. JEV_API_KEYin the environment wins when set.- Never write a key into
jev.config.json, a shell rc, or any tracked file. Never echo it in full;jev keyprints it masked.
Provider is derived from the key prefix. The {state, model, questions} body and {answers}
response are identical either way, so nothing else changes:
| Key prefix | Endpoint | Model |
|---|---|---|
sk-or- |
OpenRouter Decisions | ~typesafe/jev-latest |
| anything else | TypeSafe direct | jev-latest |
JEV_API_URL / JEV_MODEL override both, for a self-hosted gateway or a pinned version. Read
jev check before debugging a call: a wrong-provider key fails as a plain 401.
Config
If the repo has no jev.config.json, write one, then hand it to the user to review. Do not
copy the example across unedited — its conventions name i18next, TanStack Query and
src/services, so in a repo without those it asks three questions that cannot be true.
Read jev.config.example.json in this skill's directory for the shape. Derive content from the
repo:
| Field | Required | Content | Failure mode if wrong |
|---|---|---|---|
description |
yes | One line: what the app is, which libraries the conventions name. Read package.json / go.mod / pyproject.toml rather than guessing. |
Model lacks context for every question |
include |
yes | Globs covering source the repo actually has. Confirm each with git ls-files <glob> | wc -l. |
A glob matching nothing makes rerank silently score zero files |
conventions |
yes | Only rules this codebase actually follows, found by reading it. Anchor each ask to a concrete module (the wrapper in src/services), never a principle. Three or four real ones beat ten aspirational ones. |
A convention the repo never adopted flags every file, teaching the user to ignore output |
gates |
yes | Risks that fit this repo. auth_or_crypto and data_loss travel well; name the actual surface (which module holds payments, what counts as PII here). |
Generic gates flag nothing useful |
exemptions |
no | Path fragments where a convention is legitimately violated: mocks, fixtures, the wrapper module itself. | Known-good files flag forever |
Optional numbers, all with working defaults — leave them alone unless something measured says otherwise:
| Knob | Default | Meaning |
|---|---|---|
batchSize |
60 |
Files scored per rerank call |
topN |
20 |
Rows rerank prints (a fixed count, not a relevance filter) |
threshold |
0.7 |
Probability at or above which drift and gate count a hit |
validateK |
[20, 40] |
Cutoffs validate reports recall at |
When a flag is noisy, fix the ask/ok text, not threshold — the noise scores high too.
Hook
Once per machine, at setup. Run it without asking — it is additive, idempotent and backs the file up first:
node "/absolute/path/to/jev-assist/scripts/install-hook.mjs"
This registers a SessionStart hook in ~/.claude/settings.json that puts hooks/preflight.md
in context at the start of every session — a short reminder to consider this skill before the
first search, covering both shapes. It is unrelated to jev gate, which judges a diff. Without it
the skill is reachable but rarely reached: the description alone loses to the reflex to start
grepping, and three sessions died that way. The installer only copies the skill directory, so
nothing else registers this for you.
It refuses to write if the settings file is missing or unparseable, keeps any other SessionStart
hooks, and leaves a .jev-bak beside the file. It takes effect in the next session, so do not
expect the preflight in the one that ran it. Report that to the user rather than letting them
conclude it failed.
Verify
Once per repo at setup, not at the start of each session:
jev check # shape: placeholders, empty groups, exemptions naming no convention
jev drift # a first pass — read the flags yourself before showing anyone
jev validate 20 # is rerank accurate on this repo at all?
jev check is mechanical and passes a config that asks confidently wrong questions: it gates the
handoff, it does not substitute for it. Run drift against a handful of files whose answer you
already know — a convention flagging nearly everything or nothing is miscalibrated. Hand the user
the config plus what that pass found, and state which conventions you inferred from reading code
versus which you guessed. The guesses are what they should check first.
Uninstall
Read this when the user asks to remove, uninstall or disable jev-assist. Deleting the skill
directory is not enough: everything below was written outside it, and two hooks name the script
by path — the pre-commit hook breaks every commit once it is gone, the SessionStart hook breaks
every session.
Work the list in order. Report what you removed and what you found nothing of.
- The pre-commit hook, first. A hook calling
jev gatefails every commit once the script is gone. Check.git/hooks/pre-commitand any Husky or pre-commit config (.husky/,.pre-commit-config.yaml). Delete the hook only ifjev gateis all it does; otherwise remove that line and leave the rest. - The
SessionStarthook, before the skill directory goes: it names a script by absolute path, so once that path is gone every session starts with a failing hook. Runnode "/absolute/path/to/jev-assist/scripts/install-hook.mjs" --removewhile the script still exists. It leaves~/.claude/settings.json.jev-bakbehind — mention it; it is a copy of their settings, theirs to keep or delete. If the skill directory is already gone, edit~/.claude/settings.jsonby hand and drop theSessionStartentry whose command containssession-start.mjs. - The key.
rm -f ~/.config/jev/key($XDG_CONFIG_HOME/jev/keywhen set), thenrmdir ~/.config/jevif empty. It is a credential — deleting it is the point, not a courtesy. If the key is live and used elsewhere, say so rather than silently dropping it. - Environment variables. Grep the user's shell rc files and any
.envforJEV_API_KEY,JEV_API_URL,JEV_MODEL. AJEV_API_KEYleft in a rc file is an undeleted credential. Show the lines and let the user remove them — do not edit a shell rc yourself. - Per-repo output, in every repo the skill ran against:
rm -f .jev-rerank.json, andjev.config.jsonafter showing it to the user. The config is hand-tuned and worth keeping if they may reinstall. .gitignore. Drop thejev.config.jsonand.jev-rerank.jsonlines the setup added.- The command on PATH, if
npm linkwas ever run:npm unlink -g jev-assist. Check withwhich jev— a dangling symlink is a confusing failure later. - The skill directory itself, plus the entry in whatever registry installed it.
jev rerank "<task>"
Scores every tracked file for relevance, prints the top N. Decide per task, not per session: nothing runs on its own, and ranking 705 files is wasted on a task whose scope you already know.
Skip it when:
- The task names the file, or an exact symbol, string or route grep finds in a second.
- The repo is small enough to read.
validate'sworst:line shows this class of task has no semantic signal (dependency bumps, sweeping renames).
Reach for it when:
- The task's words and the code's don't overlap: a different natural language, UI copy that lives only in i18n keys, a concept the code names differently, a behavior described with no shared noun.
- The files that matter likely share no token with the task.
- The probe flooded — grep returned hundreds of hits with no way to rank them.
Grep first — one probe, three outcomes: convergence (done), empty, or a flood you cannot rank. Only the last two are rerank's business, and the probe settles most cases in seconds.
Measured on one private 705-file React app, 10 consecutive commits as tasks, ground truth from what each commit changed: recall@20 0.68, recall@40 0.80 over 41 files.
Where it wins: files sharing no keyword with the task. A shared upload service ranked 6th for
an image-upload task in an unrelated feature — grep on either the feature name or upload would
have missed or buried it.
Where it fails: files changed only because a reference changed rank low (one ground-truth file landed at 121/705). Semantic scoring cannot see structural coupling, which shows up as a size effect: every task touching 1–4 files scored full marks, while an 11-file change reached 5/11. Treat the top 20 as the core of a change, never the whole of it — see Combining with grep for closing the rest.
Reading the list: topN is a fixed row count. A task touching 3 files still prints 20 rows,
and the rest are the least unrelated files in the repo rather than candidates.
Scores are levels, not probabilities: 0–3, where 3 is "likely must be read or edited", 2 is "shows the existing pattern to follow", 1 is background and 0 is unrelated. Read everything at 2.5 or above. Cut at a gap below that if there is one, but do not wait for a gap — a top 20 spanning 0.4 means nothing stood out, not that all 20 are candidates.
Ranking is per file, not per symbol. A file holding both a live and a stale declaration of the same name ranks once, on the strength of the file; you still have to disambiguate by reading. A high rank is not evidence that the thing you are looking for in that file is the live one.
Output: stdout only, nothing written to disk. Add --json only when something must read past
the cutoff; it also dumps every row to .jev-rerank.json in the repo root, so tell the user to
gitignore it if they do not already.
Run once per task, not per edit. The cost that rules out a loop is ~94k input tokens per run, not the latency — it can come back in under 5s, which makes rerunning it feel cheaper than it is.
Combining with grep
rerank finds the entry point. grep closes the change. Neither does the other's job, and the
failure mode of using only rerank is not a missed file in the ranking — it is a ranked file you
read and then under-changed.
rerankonce, with the task phrased concretely — what gets added or changed. Use the repo's terms where you know them and the user's words where you don't; rerank matches meaning, not tokens. Read everything at 2.5 or above.- Name the symbol you are about to add or change: the prop, the option field, the exported type, the component.
git grep -n <that symbol>, and again for the component callers actually import. This is the classrerankstructurally cannot rank — files that change only because a reference changed. The two that bite every time are the shared type definition and the compatibility wrapper the rest of the app still calls.- Done means every public surface a caller touches accepts the new option. A hook taking an option that no exported prop type declares is unreachable code: it typechecks, it passes tests, and no caller can reach it.
Skip step 1 when the task already names a file or an exact symbol. Never skip step 3.
Measured, one task, same repo and model, rerank-led against grep-only: the rerank arm spent
14% fewer tokens and 24% less wall clock, ranked all four ground-truth files inside the top 20 —
and still shipped the option unreachable, because two of those four ranked files (the shared type
at #3, the wrapper at #16) were read but not edited. Recall is not completeness. Steps 2–4 are
where the grep-only arm spent its extra tokens, and they are not optional.
jev validate [n]
Replays recent commits as tasks and scores rerank against what each commit actually changed.
Run once per repo, before relying on a ranking you cannot check.
jev validate 20
n is how many commits it must score, not how far back it looks. It reads up to 3n, skips
merges and any commit touching no file in include, and stops once n qualify — so with narrow
include globs the sample can reach much further back than n commits.
Recall does not transfer between repos. A number from a 705-file React app says nothing about a Go monolith, and a confident 0.87 on a file is unfalsifiable alone; this makes it falsifiable using history the repo already has.
Read the worst: line as carefully as the recall figure. Dependency bumps and sweeping renames
have no semantic signal and score low by nature — a fact about the task, not a defect. That line
is what tells you when to skip rerank entirely.
Two adjustments a hand-rolled comparison misses, so do not reimplement this with
git show --name-only:
- Files a commit added are excluded — they did not exist when the task was written, and counting them inflates recall.
- Files it deleted cannot be ranked against today's tree at all.
jev drift [glob]
Asks every convention in the config against every file. Catches drift grep cannot, because grep only finds what you already thought to look for.
Reach for it when:
- The user asks a sweep: "which files still do X", "what hasn't been migrated to Y yet", "is Z consistent across the app". One question, every file, a yes/no each.
- A literal search floods and you cannot classify the hits — the pattern matches, but it answers a
different question than you asked. The tell is an exclusion that needs context a pattern cannot
carry: is this string user-facing copy or a code comment, is this file a fixture or the real
thing, is this
catch {}deliberate.
Skip it when:
- A linter, type checker or test can decide it. Those are cheaper and actually authoritative.
- The literal search already converged — the hits are the answer, with no noise to separate.
- No convention in
jev.config.jsoncovers the question. Add one first (they cost nearly nothing to add) or you are running the wrong questions against the right files.
Scope it. drift is 1 call per file, so the glob is the cost knob: jev drift 'src/features/**'
beats a bare jev drift when you already know where the answer lives. When a literal search can
cheaply exclude most of the repo, run it first and pass the survivors.
Question count barely affects cost or latency — output tokens are free and questions evaluate in parallel. Ask everything you care about in one pass: ten conventions cost about what one does.
Probabilities read as magnitude, not just against a threshold. A file importing a UI library two different ways scored 0.62 while a single-style file scored 0.97 — the middling value correctly described a mixed state rather than indecision. Treat 0.4–0.7 as "partially true" and go look.
jev gate [ref]
Judges a staged diff, or a given ref, against the config's gates. Exits 1 on any flag so it hooks into pre-commit.
Measured on four real commits:
| Change | Result |
|---|---|
| Decryption feature | auth_or_crypto 0.99 |
| Double-submit fix | silent_catch 0.91 |
| CSS tweak | clean (≤0.64) |
| Comment-only | clean (≤0.03) |
At 745–1770ms per diff it fits in a commit hook without being felt. It catches a class nothing
else in a typical toolchain does: tsc, ESLint and unit tests all pass a change that quietly
touches decryption or drops user input.
Reading the output
Flags are prompts for a human look, never verdicts. Two failure modes matter more than false positives.
Correct but not actionable. silent_catch fired at 0.91 on a deliberate catch {} that
carried a comment explaining why the failure was safe — literally right and useless. Fix the
criteria, not the threshold: that gate's ok text now ends with "OR the catch is accompanied by
a comment explaining why the failure is safe to ignore." Same for hardcoded copy firing on mock
chart data, fixed by excluding fixtures in the ok text and exemptions. Tune for this early —
an audit that cries wolf gets ignored, and an ignored gate is worse than no gate, costing money
while buying a false sense of coverage.
Typed output guarantees shape, not correctness. A confident number can be confidently wrong.
Run jev validate before trusting rerank on a new repo, and check a convention or gate against
files whose answer you verified by hand. Without that step you are reading probabilities you
cannot calibrate.
Why the config is per-project
The flow transfers across repos; the questions do not. Every convention needs its own exemptions,
and the same question needs different phrasing in different codebases — hardcoded_copy criteria
that works in an i18next app is wrong in an app with no i18n. Keep jev.config.json in the repo
it describes and tune it there.
Validation is per-project for the same reason: a repo with meaningful commit history carries its
own ground truth. A shallow clone or squashed history gives validate nothing to work with, and
there you are guessing at recall.
When not to use this
- Scope you already know. A named file, an exact symbol, a repo small enough to read: grep or
just open it.
rerankcosts ~94k input tokens to tell you what you knew. - One-off judgments where you are the one waiting. Three options in front of you: decide. A round trip adds latency and no information.
- Judging candidates you just generated. If state and options both come from the agent, the probability measures the agent's own self-consistency, not an outside opinion. It will look like validation and will not be.
- Anything a compiler, type checker, or test can decide. Those are cheaper, faster and actually authoritative. Use Jev for what they structurally cannot see.