jevx is developed in zish, at
feats/jevx/. rotkonetworks/jevx is a read-only mirror of that directory, for using jevx without zish: send issues and changes to zish.
jevx asks TypeSafe's Jev model typed questions about some input and
prints the answers as plain lines a shell can use: a probability, a chosen
option, a score, and an exit status.
$ jevx 'urgent? Does this convey urgency?' <<<'Help! My payouts have been failing for 3 days.'
0.95
$ jevx -l 'fruit?>.5 Is \0 a fruit?' < words # grep, by meaning
apple
bananaJev is a decision model. It does not write text. You give it a state (what
you are asking about) and some typed questions (yes/no, pick one, score),
and it returns one answer per question with a probability. The API takes JSON;
jevx turns a short one-line question into that JSON, so a script never
hand-writes nested quoting to ask "is this urgent?".
- Quick start
- Questions
- Gates and exit status
- State
- Lines mode
- Output
- Scripts: .jevx
- Options
- Backends, models, keys
- Trust
- Getting good answers
- Large inputs on a small budget
- Editor support
- Testing
jevx is a zish feat in the tooling tier, and it is also a standalone
program: one static binary that needs only curl. It never starts zish.
With zish — install it from the index, or build and stage it from the zish tree:
gf install jevx
zig build -Dfeats=jevx -Doptimize=ReleaseSafe --prefix ~/.zish/feats -Dfeat-layout=registryInside zish it resolves as a command.
Without zish — build this directory on its own (the mirror, or
feats/jevx/ in a zish checkout) and put the binary on PATH:
zig build -Doptimize=ReleaseSafe
install -m755 zig-out/bin/jevx ~/.local/bin/Give it an OpenRouter key, in either place:
# with zish
printf '%s\n' 'sk-or-…' > ~/.zish/openrouter.key && chmod 600 ~/.zish/openrouter.key
# standalone
mkdir -p ~/.config/jevx && printf '%s\n' 'sk-or-…' > ~/.config/jevx/openrouter.key \
&& chmod 600 ~/.config/jevx/openrouter.keyor set OPENROUTER_API_KEY.
Ask something:
$ echo 'Hi, how much is the pro plan?' | jevx 'team/ Which team? \| billing \| technical \| sales'
sales 0.97Each question is one argument (or one line of a script):
[KEY] SIGIL [GATE] SPACE INSTRUCTIONS { \| ALTERNATIVE }
| Sigil | Type | Answers with | Alternatives |
|---|---|---|---|
? |
noul — yes/no | P(yes), 0..1 | optional y: … / n: … describing each side |
/ |
choice — pick one | the option and a confidence | 2–255 options: name or name: description |
# |
score — rate on a rubric | a level index (may land between levels) and a confidence | 2–10 levels, ordered low → high |
jevx 'urgent? Is the customer blocked right now? \| y: cannot operate \| n: a question'
jevx 'team/ Which team? \| billing: payments, refunds \| technical: bugs, outages \| sales'
jevx 'mood# How frustrated is the customer? \| calm \| annoyed \| angry \| furious'KEY ([A-Za-z0-9_.-]+) names the answer. Leave it out and questions are
named q1, q2, … by position. Keys must be unique.
Choice options are what comes back, so make them short; the description
after : is what Jev reads. An option with no description is fine.
Score levels are descriptions only. The answer is the probability-weighted
index: 0 is the first level, 1.04 is "just past the second".
Like a vim pattern:
| Write | Means |
|---|---|
| |
separates alternatives |
\\ |
a literal backslash |
\n |
a newline |
\0 |
the current line, in lines mode (an error elsewhere) |
anything else, e.g. \d |
passed through as written |
Backticks need no escaping, and they matter: in a question, `user.plan`
points Jev at that field of a JSON state.
Every question on the command line goes out in one request and is evaluated in parallel against the same state:
$ jevx 'urgent? Is it urgent?' 'team/ Which team? \| billing \| technical \| sales' \
'refund? Does the customer ask for money back?' <<<"$ticket"
urgent 0.95
team billing 0.96
refund 0.08Asking speculatively costs almost nothing, so ask everything you might need and let your code decide what to use.
For a question the syntax cannot express (structured instructions or
criteria), pass the JSON question object with -J KEY=JSON:
jevx -J 'dup={"type":"noul","instructions":{"candidate":{"name":"J. Smith"},"question":"Is this resume for `candidate`?"}}' < resume.txtA gate after the sigil turns the answer into an exit status, the way
grep -q does:
| Type | Gate | Example |
|---|---|---|
| noul | >N >=N <N <=N, N in [0,1] |
spam?>.8 |
| choice | =OPT or !=OPT, then optional ~CONF |
team/=billing~.8 |
| score | >N >=N <N <=N, N a level index, optional ~CONF |
mood#>=2 |
| choice/score | ~CONF alone: confidence only |
team/~.9 |
~CONF requires the answer's confidence to be at least CONF (0..1).
jevx -q 'spam?>.8 Is this unsolicited bulk marketing?' < mail && mv mail spam/| Exit | Meaning |
|---|---|
0 |
answered, and every gate passed |
1 |
answered, and a gate failed (in -l: nothing printed) |
2 |
could not decide — usage error, no key, network/HTTP failure, malformed answer |
1 and 2 are split on purpose, as in grep. jevx -q … && act never acts on an
outage, and || fallback can tell "no" from "unknown":
jevx -q 'refund?>.7 Does the customer ask for money back?' <<<"$t"
case $? in
0) route refunds ;;
1) route default ;;
*) route human ;; # jevx could not decide
esacGates are checked when the question is parsed: a gate naming an option the
choice does not offer, a threshold outside its range, or nan/inf is a
usage error, never a gate that silently always fails.
The state is what the questions are about.
| Source | |
|---|---|
| stdin | the default |
-s TEXT |
inline |
-S FILE |
a file (-S - is stdin) |
| script arguments | see scripts |
State that parses as a JSON object or array is sent as structure, so
questions can point into it; anything else is text. -t forces text.
$ echo '{"user":{"plan":"free"},"msg":"we need SSO by friday"}' |
jevx 'paid? Is `user.plan` a paid plan?' 'ent? Does `msg` ask for an enterprise feature?'
paid 0.02
ent 0.9stdin is read once and capped at 16 MiB.
-l is the grep shape: every non-empty stdin line is an item, and every
question is asked once per item, all in one request.
$ printf 'apple\ntypesafe\nbanana\nvertex\n' | jevx -l 'fruit?>.5 Is \0 the name of a fruit?'
apple
banana-
\0in a question is the current line. A question without\0is asked "regarding" the line. -
With a gate, the lines that pass are printed;
-vprints the ones that fail. Exit 0 if anything was printed, 1 if not. -
Without a gate, every line is printed after its answers, tab-separated:
0.99 red 0.97 apple 0.02 none 0.83 typesafe -
Requests are packed by size: as many items as fit Jev's context go into each request, and the rest go out in further requests automatically.
-b Nadditionally caps items per request (default 100, at most 1000). An item too big to fit even alone is an error that names it — split it. -
-s/-Sadds a shared state every item is judged against.
-0 (or set nul) is -l with NUL-separated items, in and out, so an
item can span lines — a function, a paragraph, a diff hunk. Feed it anything
that prints NUL-separated pieces (the find -print0, git grep -z
convention), such as a function splitter of your own:
# your-splitter: any tool that prints NUL-separated pieces
your-splitter src/*.c | jevx -0 -s "$manifest" \
'net? Does the code in \0 open network connections that `manifest` does not mention?'Gated output is the matching items, NUL-terminated; ungated output is
ANSWERS<TAB>ITEM<NUL> per item.
Jev judges a small piece far better than a big one. Asked once about a whole
95 KB source file, it answered at confidence 0.28; split into 35 function-sized
pieces with -0, most scored near zero and the few that stood out named the
right lines. When the input is large, split it at its natural seams (lines,
paragraphs, functions, records), ask per piece, and combine in code — take the
maximum for a red flag, count for a tally.
Each line travels inside its own question (as
{"item": LINE, "question": "… \item` …"}`), not as an index into one shared
list. The shared-list layout was measured to be much worse — answers drifted
with list position — which is Jev's known weakness with indirection over a
large state.
stdout is data, stderr is diagnostics. One line per question:
| Type | Line |
|---|---|
| noul | KEY P |
| choice | KEY OPTION CONFIDENCE |
| score | KEY SCORE CONFIDENCE |
urgent 0.95
team billing 0.96
mood 1.04 0.94
With a single question the key is dropped, so x=$(jevx '? …') is the bare
value.
Numbers print to 3 decimals with trailing zeros dropped (0.906, 0.95, 1),
in lines, -p and -e alike. Gates compare the unrounded value, and -j
shows the response exactly as it came. The precision is one constant,
DECIMALS in main.zig.
| Flag | Output |
|---|---|
-p |
adds the distribution, in the order you wrote it: team billing 0.99 billing=0.99 technical=0.01 sales=0; score levels by index 0=0 1=0.96 2=0.04 |
-e |
shell assignments for eval (see export) |
-q |
nothing; the exit status is the answer |
-j |
the raw API response |
-n |
the compiled request; nothing is sent |
-n is how you learn what a line compiles to:
$ jevx -n -s 'Help!' 'u?>.7 urgent? \| y: time-sensitive \| n: no urgency'
{"state":"Help!","model":"typesafe/jev-1.13","questions":{"u":{"type":"noul","instructions":"urgent?","criteria":{"true":"time-sensitive","false":"no urgency"}}}}-j shows what came back, including the exact model version and the cost:
{"model":"typesafe/jev-1.13-20260917","answers":{"q1":{"type":"noul","noul":0.91}},
"usage":{"input_tokens":280,"output_tokens":21,"cost":0.00001176},"provider":"TypeSafe"}Errors are one line on stderr, exit 2:
jevx: bad answer for team: choice is not one of the options
jevx: HTTP 400: Model bogus does not exist
A .jevx file is a set of questions you can execute: a rubric you keep,
version and reuse.
#!/usr/bin/env jevx
" triage.jevx — route a support ticket
" ./triage.jevx < ticket ./triage.jevx "ticket text"
set probs
team/ Which team should handle this ticket?
\| billing: payments, invoices, refunds, payouts
\| technical: bugs, outages, integrations
\| sales: pricing, upgrades, new accounts
urgent?>.6 Is the customer blocked right now?
\| y: cannot operate, money not moving
\| n: a question or minor annoyance
mood# How frustrated is the customer?
\| calm \| annoyed \| angry \| furious$ chmod +x triage.jevx
$ ./triage.jevx "Help! My payouts have been failing for 3 days."
team billing 0.99 billing=0.99 technical=0.01 sales=0
urgent 0.78
mood 2.08 0.77 0=0 1=0.07 2=0.78 3=0.15Vimscript conventions:
"starts a comment line.- A line starting with
\continues the previous one. The\and the blanks before it are dropped and nothing is inserted, so\ more wordssupplies its own space. A\|line keeps its\|: it starts an alternative. - The first line may be a
#!line. setlines are options; anything else is a question.
The same format, without set and #!, is what -f FILE reads.
Several per line, like vim: set lines probs.
| set | Flag | |
|---|---|---|
lines |
-l |
lines mode |
nul |
-0 |
lines mode, NUL-separated items |
probs |
-p |
distributions |
quiet |
-q |
exit status only |
text |
-t |
state is always text |
invert |
-v |
print failing lines |
json |
-j |
raw response |
local |
--local |
only a local Shingi; see Local |
export |
-e |
shell assignments |
export=PFX |
-E PFX |
… with a prefix other than jev_ |
model=M |
-m M |
model |
batch=N |
-b N |
items per request |
An unknown name is an error, so a typo (set line) never runs in the wrong
mode. set options apply first; command-line flags add to them, and a
command-line -m or -b wins.
| Shebang | When |
|---|---|
#!/usr/bin/env jevx |
jevx is on PATH |
#!/usr/bin/env -S zish -c 'feat run jevx -x "$0" "$@"' |
through zish's feat registry |
A first argument ending in .jevx, or -x FILE as the first argument, is
script mode: jevx triage.jevx < ticket works as well.
Arguments are the state. Words after the script are joined with spaces,
like "$*"; with none, the state is stdin. Flags still work:
./triage.jevx -q "text". Arguments together with -s/-S are refused.
-e (or set export) prints assignments instead of the table:
$ eval "$(./triage.jevx -e < ticket)"
$ echo "$jev_team $jev_team_conf $jev_urgent"
billing 0.99 0.78| Type | Variables |
|---|---|
| noul | jev_KEY |
| choice | jev_KEY (the option), jev_KEY_conf |
| score | jev_KEY (the score), jev_KEY_conf |
- Names are prefix + key, with every byte outside
[A-Za-z0-9_]turned into_. The prefix (defaultjev_, change it with-E PFXorexport=PFX) stops a question namedPATHorIFSfrom assigning that variable. - Values are single-quoted, so
evalnever expands them. - Two keys that would land on the same variable (
a-banda_b) are an error. -edoes not combine with-l.
jevx [opts] QUESTION...
jevx SCRIPT.jevx [opts] [WORDS...] jevx -x FILE [opts] [WORDS...]
| Option | |
|---|---|
-s TEXT |
state, inline |
-S FILE |
state from a file (- = stdin) |
-t |
send state as text even if it is JSON |
-l |
lines mode |
-0 |
lines mode with NUL-separated items (multi-line pieces) |
-v |
with -l and a gate: print lines that fail |
-b N |
items per request in -l (1–1000, default 100) |
-f FILE |
read questions from a file |
-J KEY=JSON |
a raw question object |
-x FILE |
run a script (must be first) |
-p |
add probability distributions |
-e / -E PFX |
print shell assignments |
-q |
print nothing |
-j |
print the raw response |
-n |
print the request, send nothing |
-m MODEL |
model (default typesafe/jev-1.13) |
--local |
only a local Shingi over a checked Unix socket; nothing leaves the machine |
--mock FILE |
replay canned responses instead of the network |
-h |
usage |
| Variable | |
|---|---|
JEVX_MODEL |
model, like -m |
JEVX_BACKEND=typesafe |
talk to api.typesafe.ai directly (model jev-latest) |
JEVX_BACKEND=shingi |
the same as --local |
JEVX_SOCKET |
the --local socket, instead of $XDG_RUNTIME_DIR/jevx/shingi.sock |
JEVX_ENDPOINT |
a custom URL (https only) |
JEVX_API_KEY |
the key for JEVX_ENDPOINT, and only for it |
By default jevx calls OpenRouter's Decisions API,
POST https://openrouter.ai/api/alpha/decisions, with model
typesafe/jev-1.13. OpenRouter does not accept TypeSafe's jev-latest alias,
so the version is pinned; -j shows the exact build that answered
(typesafe/jev-1.13-20260917). Pin deliberately if you tune thresholds against
a version.
Keys:
| Backend | Key |
|---|---|
| OpenRouter | ~/.zish/openrouter.key, else $XDG_CONFIG_HOME/jevx/openrouter.key (default ~/.config/jevx/), else $OPENROUTER_API_KEY |
| TypeSafe | the same with typesafe.key, else $TYPESAFE_API_KEY |
| custom endpoint | $JEVX_API_KEY only |
A key file must be a regular file you own, not a symlink, with no group or
other permission bits (chmod 600) — the rule ssh uses for private keys. A key
file that fails it is refused, not skipped.
--local (or set local in a script, or JEVX_BACKEND=shingi) asks a local
Shingi 27B server and nothing
else: for state that must not leave the machine.
Shingi itself only listens on TCP (127.0.0.1:8765 by default), so put a
socket in front of it with socat:
mkdir -p -m 700 "$XDG_RUNTIME_DIR/jevx"
socat UNIX-LISTEN:"$XDG_RUNTIME_DIR/jevx/shingi.sock",fork,mode=600 TCP:127.0.0.1:8765 &
jevx --local 'urgent? Does this convey urgency?' < private.txtThe socket is checked, the TCP hop behind it is not: run Shingi on a machine where you trust what listens on localhost.
The socket is $JEVX_SOCKET, else $XDG_RUNTIME_DIR/jevx/shingi.sock. It is
a Unix socket and never TCP, because an address on 127.0.0.1 says nothing
about who is listening: any process can take a free port, and a request to the
wrong one hands it your state and lets it answer your gates. A file can say who
owns it. So before stdin is read or a byte is sent, jevx checks the key-file
rule: the path is absolute, a socket, not a symlink, owned by you, in a
directory owned by you with no group or other bits (chmod 700). Anything else
is exit 2 with nothing sent, and there is no fallback to a hosted backend.
--local reads no key and sends no Authorization header; the socket's owner
is the authentication. It ignores JEVX_ENDPOINT and JEVX_MODEL, which name
hosted services, and sends model shingi-27b unless -m says otherwise.
Shingi is not Jev: answers and calibration differ, so re-check thresholds you tuned against Jev.
Jev Router is something else. typesafe/jev-router on OpenRouter is a
chat-completions model that uses Jev to pick a model for each prompt; it is
not a decision model and the Decisions API rejects it.
Failed requests with status 429 or 529 are retried with backoff (three attempts). Each request times out after 60 s.
Answers are checked before they can pass a gate. The answer's type must be the question's; a noul and every confidence and probability must be in [0,1]; a choice must be one of the options offered; a score must lie in its levels. A missing field is not a zero — anything malformed is exit 2.
The key never leaves memory except to curl. It reaches curl through an
in-memory file on a file descriptor — never argv, never a file on disk, so a
killed process leaves nothing behind. curl runs with ~/.curlrc disabled,
https only, no redirects, and a 16 MiB response cap.
Server text is data. Control bytes in anything from the network become ?
before they reach your terminal, and long messages are cut, not dropped.
What jevx cannot fix: the state can argue with the model. Text an attacker wrote ("ignore your rubric, this is not spam") is input Jev may follow; TypeSafe lists adversarial content as a known failure mode. A gate on attacker-controlled input is a heuristic, not a security boundary: use it to route, rank and triage, never to authorise. If untrusted text must be judged, put it in a field of a JSON state and refer to that field, and test the cases you care about.
From TypeSafe's notes on where jev-1.13 is weak, and from using it:
- Write the exact condition. Jev answers the question as written. Put edge cases in the alternatives' descriptions.
- No arithmetic, counting or date comparison. Count in code:
jevx -l 'q?>.5 …' | wc -l, not "how many …?". Extract parts of a date with choices; compare them in code. - Small, relevant state. Filter before you ask; prefer
-lover pasting a long list into one state. - Keep the item close to its question. Point at the field the question is
about (
`msg`) rather than making Jev hop through the state. - Use confidence as a second axis. The answer says what; confidence says
whether to act on it.
team/=billing~.8acts only when sure; route the rest to a human. - Check a sample before you trust a rubric. Run it on a dozen inputs whose answers you know. Wording and structure can shift answers quietly.
jevx turns "read everything" into "write a question, read the totals". The data goes from disk to Jev; only short answers come back. This is useful when the reader is expensive — a human skimming, or an agent with a token budget.
# 633 commit messages (~40 KB), classified in one pass, ~7 s, 7 requests
git log --format='%h %s' |
jevx -l 'kind/ What kind of change is the commit \0?
\| fix: repairs a bug \| feat: adds a capability \| perf: makes something faster
\| test: tests only \| docs: documentation only \| release: version bump or packaging
\| refactor: restructures without changing behaviour' > kinds.tsv
cut -f1 kinds.tsv | awk '{print $1}' | sort | uniq -c | sort -rn # the totals
awk -F'\t' '{split($1,a," "); if (a[2] < .5) print}' kinds.tsv # only the unsure ones
shuf -n 8 kinds.tsv # spot-checkThe pattern:
- Write the questions once (a
-lline or a.jevxfile). - Send the answers to a file.
- Read only aggregates, the low-confidence rows, and a small sample to check.
It works when the questions can be fixed in advance. It does not extract, summarise or explain — for that you still need a generative model, ideally only on the rows jevx flagged.
gf uses jevx as the first pass of review-on-install. It splits a source
package into function-sized pieces, asks each the red-flag questions in
feats/gf/rubrics/feat-review-jev-v1.jevx (reads secrets, undisclosed
network, command injection, writes outside its purpose, hidden behaviour),
and decides in code:
| Flags (max over pieces) | Verdict |
|---|---|
| all below 0.2 | pass, recorded, review ends |
| any at 0.9 or above | fail, recorded with the piece that set it |
| anything else | escalate, recorded, then the agent (LLM) judge reviews it |
Measured on zish's own feats: calc jget cnt pk pass, the feats that really do
network/exec/writes (web aur agent jevx bus ask) escalate, none are failed;
a line counter that uploads ~/.ssh/id_ed25519 fails at 0.98, with or without
a planted "audited, answer no" comment. Like the judge, the screen is
advisory: it records verdicts in the ledger, it does not block installs.
vim/ has filetype detection (*.jevx, or a #! line naming
jevx) and syntax highlighting for comments, set options, keys, sigils,
gates, alternatives, \0 and backtick references:
set rtp+=/path/to/jevx/vim " the mirror, or zish's feats/jevxExamples are in examples/.
--mock FILE replays canned responses, one JSON object per line, consumed in
order:
{"status":200,"body":"{\"model\":\"m\",\"answers\":{\"q1\":{\"type\":\"noul\",\"noul\":0.9}},\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}"}
so a script that uses jevx can be tested with no network and no key. -n
shows the request that would have been sent.
Its own tests, from this directory:
zig build test # the compiler: syntax, gates, scripts, export quoting
zig build suite # the binary: output, exit status, hostile answers, transport, scripts