jev-assist

September 20, 2026 · View on GitHub

中文

Starting a task in a 600-file repo, perhaps 8 of those files are worth reading. Finding the 8 often costs more than changing them.

Existing approaches have their limits. Grep needs the target expressed as a pattern that matches, so it reaches only the files naming the feature. Having a model read the whole repo costs time and tokens in proportion to the file count. Neither suits a judgment that has to be applied at scale — the same question asked of every file.

jev-assist delegates that class of judgment to TypeSafe Jev, which answers closed questions and returns typed values with probabilities rather than prose. Three commands follow from it: rerank ranks every file by relevance to a task, drift checks files one by one against your team's conventions, and gate judges whether a diff about to be committed touches something dangerous.

The cost structure is what makes this viable: output tokens are free and the questions inside one call are evaluated in parallel, so asking 10 of them costs about what asking 1 costs. Whole-repo passes only make financial sense under that pricing.

Install

Recommended: let a coding agent do it. Hand it this prompt, with <your-key> replaced:

Install the jev-assist skill with npx skills add glud123/jev-assist, then finish the setup
per its SKILL.md: run jev key with the key <your-key>, write a jev.config.json in this
repo's root, and run jev check plus jev validate 20. Show me the results along with which
conventions you inferred from reading code versus which ones you guessed.

It has read your code, and SKILL.md tells it what to derive from where, so the first draft of the config comes faster from the agent than from you.

Manual install.

npx skills add glud123/jev-assist                        # install the skill
jev key sk-...                                           # key → ~/.config/jev/key, 0600, outside the repo
cp <skill-dir>/jev.config.example.json jev.config.json   # then edit it, in the repo being judged
jev check                                                # verify key and config

Node 18 or newer, because it uses the built-in fetch. Nothing else to install. Any skills-compatible agent works — Claude Code, Codex and Cursor read the same SKILL.md and call the script by path.

Installing as a skill does not put jev on your PATH. If you want the command in your own shell:

git clone https://github.com/glud123/jev-assist && cd jev-assist && npm link

Using it in an agent session

The point of the skill is that the agent reaches for it when it should. You do not memorize commands; you state the intent, with the session's working directory set to the repo being judged.

Use jev-assist to find which files this task touches before you start: add CSV export to
the orders table.
Run jev-assist drift over src/**/*.tsx and rank what it flags by severity.
Before I commit, use jev-assist to check whether the staged changes touch anything dangerous.

The agent calls scripts/jev.mjs as described in SKILL.md and reads the ranking or the flags back to you. If it does not reach for the skill on its own, name jev-assist in the prompt. On a repo it has not used this on before, have it run jev validate 20 first — whether the ranking is accurate is a measurable fact, not a claim you have to take.

What it actually does

jev rerank "<task>" ranks every tracked file against a one-line task description. On a private React app of 705 files, replaying 10 real commits, the top 20 caught 68% of the files those commits touched and the top 40 caught 80%. The average is the boring part: every commit that touched 1 to 4 files scored full marks, while the one that touched 11 got 5 of them. So read the top 20 as the core of a change but never as the whole of it — past the cutoff the ranking is no longer reliable, and following the imports of the top results beats reading further down.

Where it beats grep is files that never mention the feature at all. A shared upload service came back 6th for an image-upload task in a completely unrelated corner of the app. Nobody searching avatar or profile was going to find that.

jev drift [glob] asks your conventions against your files, one call per file. The number of questions barely moves the cost, so ask ten things at once instead of one: untranslated copy, code that routes around the request wrapper you wrote for exactly this purpose, server state living outside the store. The difference from grep is the input: grep needs you to express the violation as a pattern that matches, while here you write down the convention itself and leave judging what breaks it to the model.

jev gate [ref] reads a staged diff and flags risk. It exits nonzero on a hit, so a pre-commit hook needs nothing but the bare command. Measured on four real commits: a decryption feature came back 0.99, a fix for a swallowed error 0.91, a CSS tweak 0.64, a comment-only change 0.03. Somewhere between 0.7 and 1.8 seconds per diff. Your typechecker, your linter and your unit tests will all wave through a change that quietly drops user input, which is the entire reason this command exists.

jev validate [n] is the one to run first and the one everybody skips. It replays your own commits as tasks and grades the ranking against what each commit really changed. Accuracy does not survive the trip between two codebases, so the numbers above are ours. Your git history is free labelled data, already sitting there.

Commands

cd /path/to/your/repo
jev check                                    # key + config, before anything else
jev validate 20                              # start here: is it accurate?
jev rerank "add CSV export to the orders table"
jev drift 'src/**/*.tsx'
jev gate                                     # staged changes
jev gate HEAD~1                              # a past commit
CommandQuestion it answersCost
jev rerank "<task>" [--json]Which files matter for this task?1 call / batchSize files (default 60)
jev validate [n]Is rerank accurate enough on my repo?n × rerank
jev drift [glob]Which files drifted from our conventions?1 call / file
jev gate [ref]Does this diff touch something dangerous?1 call / diff
jev checkIs my key and config well-formed?free, no network
jev key <k>Store an API key outside the repofree, no network

rerank prints the top N and nothing else. Pass --json when something needs to read past the cutoff and it also writes the full ranking to .jev-rerank.json in the repo root; add that file to .gitignore.

Read the printed list by the gap in the scores, not by the row count. topN is a fixed number of rows, not a filter — a task that genuinely touches 3 files still prints 20, and the bottom 17 are merely the least unrelated files in the repo. Scores are a distribution over four levels, from "no reason to open this file" to "likely must be read or edited", so a 0.3 means background, not fourth place.

Wiring up pre-commit

One line for an agent:

Wire jev gate into this repo's pre-commit hook. If a hook already exists, append a line
instead of overwriting it.

By hand:

h=.git/hooks/pre-commit
[ -e "$h" ] && echo "$h exists — add a 'jev gate' line to it yourself" ||
  { printf '#!/bin/sh\njev gate\n' > "$h" && chmod +x "$h"; }

It writes a hook containing just jev gate and marks it executable when there is no hook, and only prints a note when one already exists — which it will if Husky or pre-commit is installed.

Configure

The config is per project: jev.config.json lives in the root of the repo being judged, one per repo, never shared and never in the jev-assist repo itself. The reason is that the phrasing of a question depends on the codebase — wording that catches untranslated copy in an i18next app is not even meaningful in an app with no i18n, where it would flag every file.

{
  "description": "A React admin dashboard built with TanStack Query and i18next.",
  "include": ["src/**/*.ts", "src/**/*.tsx"],

  "conventions": {
    "hardcoded_copy": {
      "ask": "Does this file contain user-visible UI text inline instead of i18next keys?",
      "drift": "has hardcoded user-visible copy",
      "ok": "all copy is translated, or the file has no UI text. Mock data and fixtures do not count."
    }
  },

  "gates": {
    "data_loss": {
      "ask": "Could this change lose or silently discard user-entered data?",
      "risk": "a realistic path exists where user input is lost",
      "ok": "user data is preserved on every path"
    }
  },

  "exemptions": {
    "hardcoded_copy": ["/mocks/", ".test."]
  },

  "batchSize": 60,
  "topN": 20,
  "threshold": 0.7,
  "validateK": [20, 40]
}

Top-level fields:

FieldRequiredDefaultMeaning
descriptionyesOne line on what the app is and what it is built with. Every question carries it as context, so name the libraries the conventions below refer to
includeyesGlobs defining the file pool for rerank and drift; only git-tracked files are considered. Confirm it matches something with git ls-files '<glob>' | wc -l
conventionsnoThe rules drift checks. Keys are yours to name. Only rules this codebase actually follows
gatesnoThe risks gate checks. Same shape as conventions, asked against a diff
exemptionsnoPer convention, a list of path substrings; a file whose path contains one skips that check
batchSizeno60Files scored per rerank call
topNno20Rows rerank prints. A fixed count, not a relevance filter
thresholdno0.7Probability at or above which something counts as a hit, shared by drift and gate
validateKno[20, 40]Cutoffs validate computes recall at

Inside each entry of conventions and gates:

FieldMeaning
askThe closed question put to the model. Anchor it to the concrete module ("the request wrapper in src/services"), never to a principle
drift / riskThe line printed on a hit. conventions use drift, gates use risk
okWhat counts as passing. Exclusions go here; this is the main place you tune

jev check rejects a config missing description or include, and also catches the mechanical faults — placeholders left in, empty groups, an exemption naming a convention that does not exist. What it cannot do is read your code, so it cannot tell whether a question holds in this repo at all: a convention asking about a shared request wrapper you never had passes check and then flags every file. Ask the agent which conventions it found by reading code and which ones it guessed at — the guesses are where the problems are.

Tuning happens in the ok text. Nearly all the noise you will see is a judgment that is literally correct and not worth acting on — a deliberate catch {}, copy inside mock chart data. Say so in ok. Raising threshold does not help: the noise scores high too — that deliberate catch {} came back at 0.91 — so a bar at 0.95 keeps it and drops the genuine findings sitting between 0.75 and 0.9 instead.

Key and endpoint

jev key <API_KEY> writes the key to ~/.config/jev/key at mode 0600 — or to $XDG_CONFIG_HOME/jev/key when that variable is set. Never in the repo.

The endpoint is derived from the key's prefix, since both providers take the same request body:

Key prefixProviderEndpointModel
sk-or-…OpenRouterhttps://openrouter.ai/api/alpha/decisions~typesafe/jev-latest
anything elseTypeSafe directhttps://api.typesafe.ai/v1/systemonejev-latest

Three environment variables override that:

VariableEffect
JEV_API_KEYThe key to use. Takes precedence over the stored file, which is then not read
JEV_API_URLThe endpoint to call, replacing the prefix-derived one. Use for a self-hosted gateway
JEV_MODELThe model to request, replacing the prefix-derived one. Use to pin a version

The two URL/model variables are independent: set JEV_MODEL alone and the endpoint still comes from the key prefix. jev check prints the key masked along with the provider, endpoint and model actually in play — worth reading before debugging a call, since a key sent to the wrong provider comes back as a plain 401.

Validate before trusting

Typed output guarantees the shape of an answer. Nothing about the answer.

jev validate 20

A commit subject is a task and the files that commit changed are the answer. validate walks back through non-merge commits until it has n that touched something inside include, ranks the whole repo against each subject line, and prints recall at each cutoff.

  #  task                                 truth   @20   @40
  1  fix avatar upload failing on Safari      7     5     6
  2  add CSV export to the orders table       4     4     4
  3  bump deps and fix lint                  12     2     3

  recall@20 0.48   recall@40 0.57   (23 files over 3 commits)
  worst: "bump deps and fix lint" — 2/12

That last line deserves as much attention as the first. Dependency bumps and sweeping renames carry no semantic signal and will always score badly, which is a fact about the task rather than a defect in the tool. It tells you when to skip rerank and just grep.

Two adjustments a hand-rolled comparison gets wrong, if you were thinking of writing this yourself with git show --name-only. Files a commit added are excluded, since they did not exist when the task was written and counting them inflates recall. Files it deleted are gone from today's tree and cannot be ranked at all. Commits that touched nothing rankable get skipped rather than scored as zero, so validate 20 may look further back than twenty commits.

Run it before trusting drift or gate on a new repo too. It only grades rerank, but a repo where rerank reads low is a repo where the config's phrasing needs work first.

Known limits

  • rerank cannot see structural coupling. A file that changed only because something it imports changed has no semantic signal, and one such file landed at 121 out of 705. Follow the imports of the top results.
  • Correct is not the same as actionable. Validation turned up a 0.91 silent_catch on an intentional empty catch that had a comment above it explaining why the failure was safe. Budget time for tuning criteria. An audit that cries wolf gets muted, and a muted gate costs money while buying a false sense of coverage.
  • No retry, no concurrency control. Sequential calls, no 429 backoff. Fine up to a few thousand files. Past that, add p-limit and exponential backoff.
  • Do not judge candidates you just generated. If the state and the options both come out of one model, the probability measures its self-consistency and nothing else. It will look like validation.

License

MIT