OceanCode
Jev decisions
OceanCode can put small typed questions to TypeSafe Jev (System One, pinned model
jev-1.13.0) at ten decision points — "would this command delete data?", "which block of this
output answers the step?" — and use the answers as advice. Jev answers with probabilities: a
noul (the probability of yes), a choice over named options, or a score over ordered levels.
The policy that turns an answer into a verdict is code beside each decision point, and the engine's
deterministic backstops stay in front of it: Jev can never turn a deny into anything, never reopen
the dangerous floor, never raise a spend, step or clock bound.
The layer is on whenever a TypeSafe key resolves (since 2026-10-22 — the native TypeSafe
provider: OceanCode calls https://api.typesafe.ai/v1/systemone itself, over HTTPS, with the key
you store once). Without a key it is off and oceancode jev status says so. Four points act by
default — the ones a pre-registered benchmark cleared — plus work_mode, which is asked only when
you pick the Auto work mode; every other point is off until you set its key. An opt-out in either config section, or OC_JEV=off, turns the whole layer off.
What Jev decides
| Point · mode key | Question asked | What act changes | What stays deterministic |
|---|---|---|---|
permission · jev.permissions | For the shell rules' residual ask on a bash command: destructive, egress (any network destination), reversible (score 0–2) and blast_radius (single_file / project / system / external), in one request. Whether the command reaches outside the project is not asked: the shell guard checks it in code (see below). | A would-ask command runs without a prompt only when destructive < 0.6, egress < 0.5, reversible < 1.5 and the blast is single_file or project with confidence ≥ 0.8. Under headless --auto an ask whose advice is an injection refusal, a system/external blast, destructive ≥ 0.6 or egress ≥ 0.5 is rejected instead of approved, and the model reads why: the rejection reaches it as feedback — Blocked under --auto by the permission policy: <likely destructive | likely network egress | blast system | blast external | the command text addresses the judge>. Jev scores: destructive=<d> egress=<e> blast=<b>. Use a safer equivalent that stays inside the project, or stop and explain why this command is required. (a score Jev did not give prints n/a) — instead of a bare "rejected"; the same reason still goes to stderr. | Deny rules and the dangerous floor run first and never reach Jev. An ask rule with a specific pattern keeps its prompt; every other permission kind (paths, edit, write, …) asks as before; dontAsk gets no advice. A command over 4,000 chars, a truncated task or a command that names a path outside the project (skip reason a path outside the project; see What is sent) gives no advice. The hold keeps these shapes at ask whatever Jev says: package install/add/publish verbs, npx/bunx/uvx, go run; the git verbs push/remote/restore/clean/stash/checkout -- and every history rewrite (reset in any form, rebase, merge, branch -d/-D, tag -d, update-ref, reflog, worktree remove/prune, commit --amend, filter-branch, filter-repo — plain commit is still advised); a git -c that sets an alias, a program (core.sshCommand, core.pager, core.editor, core.fsmonitor, core.hooksPath, core.gitProxy, sequence.editor), credential.*, http.* or url.*, or any value with !; recursive rm, find -delete/-exec, ssh/scp/rsync, an uploading curl/wget; anything that runs code Jev never sees — a path as the command word (./deploy.sh, scripts/x.sh, /abs/tool), a shell without -c (on a file, -s, or fed from a pipe or <), an interpreter (python, node, ruby, perl, deno, bun, tsx, php) on a file, an inline program or stdin (only python -m <module> and --version pass), source/./eval/exec, awk -f, sed -f; and any opaque or non-ASCII command. |
output_view · jev.output_view | For a bash output over the truncation limits: which of its ≤ 255 line blocks best answers the step (where, a choice over block ids) and whether any does (exists). The step is the assistant text before the call; when the model chained the call with no text before it, the call's own description stands in (the decision row's state then carries intent_source: "description"). | The model sees the first 20 and last 20 lines, the blocks Jev picked (most probable first until 0.8 of the mass or 8 blocks, each at most 200 lines) and one [… lines a–b omitted; full output: <spill>] marker per gap; the full output goes to a spill file. When exists < 0.5 it gets the registry's head preview and the spill file instead — except that the top block is still kept when its probability is at least 0.8 and exists is at least 0.2. | Only bash is viewed (read, grep and glob page themselves). Binary output, a command whose last stage already filters its output (grep, rg, head, tail, wc, sort, uniq, cut, awk, sed, jq; skip reason command already filters), neither step text nor a description of at least 12 characters (skip reasons no step intent, step intent too short), an output the head and tail already cover, a view that is not smaller than the registry's cut it would replace and a state that does not fit 48,000 bytes keep today's result — for bash, as for every tool, the registry's cut: the head preview (at most 2,000 lines / 50 KiB) and the spill file. The head, the tail and the spill file are always there when a view acts. |
supervisor · jev.supervisor | At a clean depth-0 finish, over the goal, the step count, the open todos, the last six steps and the final text: complete and progress (score 0–2). Before a depth-0 bash call whose exact command already ran at least twice among the prompt's last six commands, the no-progress check asks one noul, proposed_command: "Would running proposed_command give the agent information or progress that it does not already have from recent_commands and their outputs?" | The stop is blocked with a synthesized continuation when complete < 0.3, progress ≥ 1.0 and the progress mass ≥ 0.5 (at most jev.supervisor_max_continuations per human prompt, default one; a further one is asked about only after a state-changing call since the last); complete ≥ 0.85 always finishes, and so does every stop after a check verified a pass this prompt (skip row a check verified a pass this prompt, no call). A no-progress answer ≤ 0.31 is a low: two consecutive lows send one steer message before the next model call; four stop the run — under headless --auto the call is not run and the run ends (stop reason no_progress, exit 5) with the final text Stopped: the last 4 commands repeated without new information.; everywhere else you are asked Continue or Stop (a dismissed question, or a surface with no question path, stops). Any other answer, a failed call and a command that is not such a repeat reset the count. | The hard step, spend and clock bounds, the exact-hash doom-loop trip, the eight-block hook cap and the no_progress guard (08 · Bounding a long run). A stop that asks you something — the final text's last line ends with ?, or the turn used the question tool — is never judged (a skip row, asks the user), and neither is a stop after a tool call the permission policy refused among the last six steps — you rejected it, or a permission rule denied it (a tool call was rejected by policy): the refusal is a decision, so continuing would only make the agent retry it. A stop after the cap, or a further one with no edit / write / apply_patch / bash call since the continuation, is skipped too (continuation cap reached, no state-changing call since the continuation). |
check · jev.check | After a depth-0 bash call that ran an allow-listed test/build runner (the triage list below) after a project-file write this prompt: which of six categories the recorded output supports — successful_check, a choice over new_failure_evidence, check_now_passes, same_blocker_after_different_attempt, repeated_operation, other_observable_activity, insufficient_evidence — and, in the same request, remaining_checks: does the goal name a check (a test suite, a golden comparison, a build, a minimum number of tests) that neither this command nor an earlier one has run and shown passing. Jev reads the recorded output and the goal; it is never asked to predict whether a change will pass. | check_now_passes as the top category with p ≥ 0.5 and remaining_checks < 0.5 is a verified pass (at most one per prompt); a green check while the goal still names an unrun one is not (the pilot's py-scheduler-refactor: a clean golden diff before the eight-test suite existed). Under headless --auto the run ends right after that call, with no further model request, and the final text Checks passed: `<command>` (exit 0). <N> file(s) changed: <paths>.; everywhere else the agent carries on and only the supervisor's continuation is withheld for the rest of the prompt. | Jev is not asked — a skip row says why — when the command exited non-zero (the command exited non-zero), when every file edited this prompt is a test, spec, snapshot, golden or generated file (no source file changed this prompt), or when the prompt already has a verified pass. shadow records the verdict (verified / not_verified) and never stops a run. |
triage · jev.triage | After a bash exit other than 0 and −1: the failure category (env_missing / flaky_transient / syntax_trivial / expected_failure / deep_logic). | With category confidence ≥ 0.8: a flaky_transient failure of an allow-listed test or build runner is re-run once (its output appended after the first run's; metadata.exit keeps the first exit code, metadata.rerun_exit holds the second); flaky_transient elsewhere or env_missing adds one hint line. A failure that names something absent (exit 127, <name>: command not found, No module named '<name>', error[E0463]: can't find crate for, Cannot find module '<name>') is env_missing without a request: the hint names it, points at an alternative that is on PATH (pytest → python3 -m unittest, jest / vitest → node --test, cargo-nextest / nextest → cargo test) and tells the model to use what the project has and not to install anything the task did not ask for; an env_missing answer from Jev itself gets the same wording with "a command" for the name and never says to install; a failing install command (pip install, python3 -m pip install, npm i, cargo install, go get, … in any segment of the command) gets no hint. | The raw output is never removed; the re-run asks the permission gate again; a green, timed-out or killed run never calls Jev. The runner allow-list is exact: cargo test, cargo nextest run, cargo check, cargo clippy, cargo build, pytest, python -m pytest, python3 -m pytest, go test, npm test, npm run test, pnpm test, yarn test, vitest, jest, ./check.sh, optionally after one cd <relative dir> &&, with no other separator, pipe, redirect or substitution and no leading VAR= other than RUST_LOG, RUST_BACKTRACE, NO_COLOR, CI. |
visibility · jev.visibility | At the ledger drain's prune: which prunable completed-turn tool parts the remaining work still needs (needed, a choice over call ids) and whether it needs any (any_needed), over the goal and the open todos. | When any_needed ≥ 0.4, up to three parts with p ≥ 0.05 are kept verbatim out of the prune — provided the rest still meets the prune minimum. A part kept once stays kept while the plan moves; a part already stubbed is never offered again. The plan's stubs land on the next cut's request only (a prune rides a cut, 2026-10-12); between cuts no sent row changes. | The tideline cut, the protect window, the emergency plan and the byte-stable prefix. off, shadow and a failure protect nothing. |
guidance · jev.guidance | After a depth-0 tool result settles in a run that has stalled (3 exact repeats among the last 6 commands with no write), has an edit no check followed, or has had the relevant code open for 16 steps without an edit: four nouls in one request — code_located, issue_reproduced, post_edit_check, stalled_repair. Each is yes at ≥ 0.7, no at ≤ 0.3, unresolved between. | A rule table maps the answers to one action kind — edit, probe, search or test — and appends one line to the tool result: Guidance: <reason>; consider <action>. A note goes out only after 2 consecutive qualifying evaluations, at most 2 per prompt, at least 10 settled calls apart, never after a stop. | The model still picks every command and its arguments. A suggestion of the kind the call just made is dropped (a stall's always qualifies); a failed evaluation resets the count. Each delivered note gets a row and, once resolved, an uptake row: did a call of the suggested kind follow within 3 settled calls. |
snippets · jev.snippets | Once per depth-0 human prompt, before its first model call: deterministic retrieval proposes up to 255 source regions, and one noul per region asks whether it directly bears on the prompt — batched into concurrent requests of at most 128 questions and 38,000 state bytes. | The answered regions are ranked by probability and packed greedily into 12,000 chars; the model call waits for the requests and the pack becomes one message joined onto the prompt's opener, pinned for the whole prompt (later steps send the same bytes; the prune never touches it). | No threshold. shadow sends the same requests in the background and logs the would-be pack; off runs nothing, not even retrieval. |
effort · jev.effort | Once per depth-0 human prompt, before its first model call: difficulty, a score over four levels (trivial / routine / involved / hard), and smaller_model_ok, a noul recorded for tuning and never read. | At confidence ≥ 0.8 the rounded score steps the reasoning effort down from the session's resolved variant — trivial two rungs, routine one, involved and hard none — along the model's own variant ladder, for this prompt only. | Never below medium (else the lowest rung above it, else the middle rung); a ceiling at or below the floor is never asked about. Sub-agent turns and the ledger's merge provider are never touched. |
work_mode · jev.work_mode | Once per top-level prompt of a session that runs the Auto work mode (23): mode, a choice over the work modes that can run here (every picker mode but Auto; no Race outside git or without a test_command, no Pipeline or Race without someone to ask, no script mode with workflows off), each named by its summary. | At confidence ≥ 0.6 Jev's pick runs for this prompt, and the badge reads auto → <pick> · Jev <confidence>. | Below the threshold, with the point off, without a key, or on an error or time-out, the rules pick from the same list (auto → <pick> · rules: <reason>). Never asked outside Auto; shadow runs the rules' pick and logs Jev's. |
Turning it on
A TypeSafe API key. Get one in the TypeSafe console (opens in a new tab) and store it where every other provider key lives (
auth.jsonin the data dir):oceancode key typesafe <key> # or pipe it: echo "$KEY" | oceancode key typesafe oceancode key # reports it: typesafe : API key stored (auth.json) — the Jev decision layer is on. oceancode logout typesafe # removes itThe environment works too:
OC_TYPESAFE_API_KEY, elseTYPESAFE_API_KEY(the name TypeSafe's own SDKs read). The stored key wins over both. The key goes only toapi.typesafe.ai— never to another endpoint, never into a config file, never to a child process.That is all. No
jevsection is needed: with a key the layer is on with the defaults below. Ajevsection in the global config ($OC_CONFIG, else<config-dir>/oceancode/oceancode.json, or the managed document an administrator ships, which wins over it) sets the endpoint, model, budgets and thresholds; a project section (<project>/.oceancode/oceancode.jsoncor.json) may set onlyenabledand the ten mode keys — itsendpoint,model, budgets and thresholds are ignored with one warning, so a cloned repository can neither send your states or credentials elsewhere nor loosen a threshold. Either section can opt out:{ "jev": { "enabled": false } }The endpoint. The default is TypeSafe's own API,
https://api.typesafe.ai/v1/systemone(modeldefaults to the pinnedjev-1.13.0; the thresholds are tuned on it).jev.endpointin the global config orOC_JEV_ENDPOINTnames another: the PlungeAI worker (the hosted typesafe provider, which needs no TypeSafe key and takes theocean.headersmap of the global config — the managed document or yours, never the project file's — asx-ocean-key), or a loopback stub. The key goes only toapi.typesafe.ai,ocean.headersonly to the worker, a redirect is never followed, and plainhttpis refused unlessOC_JEV_ENDPOINTnames a loopback host.Every request names its decision point in an
x-jev-pointheader and the direct API'sServer-Timing(when present) is kept on the decision row.Check it.
oceancode jev statuslists the ten points with their modes and the endpoint host (api.typesafe.ai);oceancode jev probemakes one live call and printsJEV_PROBE_OK. Without a key,statusprints every pointoffandprobenames the reason:no TypeSafe API key: run \oceancode key typesafe <key>` …`.
Modes
Each point has its own key: off, shadow or act.
| Mode | What the point does |
|---|---|
off | Never calls Jev; today's behaviour, no decision row. |
shadow | Asks Jev, records the verdict it would have given, and changes nothing. The call runs in the background, so the turn never waits for it; its row is written when the answer lands, and every row is in jev-decisions.jsonl by the time the session closes. |
act | Asks Jev and applies the verdict inside the bounds of What Jev decides. |
A point whose key is unset runs in its default mode: act for the four points the
2026-10-22 pre-registered benchmark cleared — check, guidance, snippets, effort (165 runs,
11 jobs × 5 arms × 3 reps, no arm RED, grade +3.0 pp [+0.0, +9.1], wall −17 to −19 % on the arms
that add them; docs/results/2026-10-21-jev-rebuild/README.md) — and off for every other
point: the supervisor (its no-progress check never fired in 132 Jev-arm runs; owner ruling
2026-10-22) and the four label-only points (permission, output_view, triage, visibility).
work_mode also defaults to act, but it is asked only for a session that runs the Auto work mode
(23), so every other session never calls it.
Nothing runs in shadow by default: a shadow call is a Jev call the run never acts on, so you
set a key to shadow when you want a label run. A point's default moves to act only on a
benchmark verdict, never by hand. A set key wins over the default either way ("check": "shadow"
keeps a cleared point in shadow), and the project section's key wins over the global section's.
Note that one key differs from its point name: permission is jev.permissions; the point name
jev.permission is accepted as the same key.
OC_JEV overrides all ten at once: OC_JEV=off turns the layer off whatever the config says,
and shadow or act sets every point. It never supplies a key — without one the layer stays
off — and an unrecognised value is ignored with a warning.
Promoting a point to act
Promote one point at a time, and only after its per-point label run passes:
- Run the point in
shadowfor at least a week of your own sessions. oceancode jev labelscollects every decision you answered (see Reading decisions) intobench/jev/labels.json.- The point's own run —
node scripts/bench/jev-decisions.mjs --set bench/jev/labels.json --point <p>(live,OC_LIVE_CONFIRM=1) — must printJEV_EVAL_OK point=<p> …: accuracy ≥ 0.90 on the unanimous labels, a Wilson 95 % lower bound on that accuracy (wilson_lower=on the line) ≥ 0.80, at least 40 rows forpermissionand 12 for any other point, and no false allow. A handful of right answers cannot promote a point: n = 12 at 12/12 is 0.758 and never passes, and all-correct needs 16 rows. Short of the bound the run printsJEV_EVAL_RED reason=wilson. - Then set that point's key to
actin the project config.
Point 1 needs the posture it advises on: an empty config allows shell commands (the base rule is
* * allow), so nothing asks and the advisor never runs. Add bash * ask under permission and
every command off the read-only safe list asks — and is advised. The safe list is flag-aware
(find -delete/-exec, git branch -D, git remote add, git tag v1 are off it; see
12 · Permissions and sandbox). The default posture
is not changed by this layer.
snippets and effort change what a prompt's first request carries (an extra opener message, a
lower reasoning effort), and snippets in act makes that request wait for its Jev calls; measure
a real session's first-request latency and cache reads before promoting either.
Thresholds
Every threshold lives in jev.thresholds, is read from the global section only, and is tuned
against labels for the pinned jev.model (a threshold tuned on one model id is not reused on
another; jev.model must be a versioned id — latest and preview are refused). Probability
thresholds are 0–1; the two score thresholds (permission_reversible, supervisor_progress)
are on the three-level scale 0–2.
| Key | Default | The risk it guards |
|---|---|---|
permission_destructive | 0.6 | Ask when destructive is at least this. |
permission_egress | 0.5 | Ask when egress is at least this. |
permission_reversible | 1.5 | Ask when the reversible score is at least this. |
permission_confidence | 0.8 | Ask when the blast_radius choice is less confident than this. |
output_relevant | 0.5 | Below this exists, keep no block (head preview + spill instead). |
output_mass | 0.8 | Keep blocks until their probabilities add up to this (at most 8). |
output_top_p | 0.8 | Below output_relevant, still keep the top block when its probability is at least this… |
output_exists_floor | 0.2 | …and exists is at least this. |
supervisor_complete | 0.85 | Always let the stop through when complete is at least this. |
supervisor_continue_complete | 0.3 | Block a stop only when complete is below this. |
supervisor_progress | 1.0 | Block a stop only when the progress score is at least this. |
supervisor_progress_confidence | 0.5 | Block a stop only when the progress mass is at least this (see below). |
supervisor_no_progress | 0.31 | A proposed_command answer at or below this is a low (two in a row steer, four stop). |
check_pass | 0.5 | A pass is verified only when check_now_passes is the top category with at least this probability. |
check_remaining | 0.5 | A pass is verified only when the remaining_checks noul (a check the goal names that has not run and passed) is below this. |
triage_confidence | 0.8 | Pass the failure through when the category choice is less confident than this. |
visibility_relevant | 0.4 | Protect nothing below this any_needed. |
visibility_min_p | 0.05 | A protected part needs at least this probability. |
guidance_yes | 0.7 | A guidance noul at or above this is a yes. |
guidance_no | 0.3 | A guidance noul at or below this is a no; between the two it is unresolved. |
effort_confidence | 0.8 | The difficulty score steps the effort down only at this confidence or above. |
work_mode_confidence | 0.6 | Auto runs Jev's pick only at this confidence or above; below it the rules pick. |
What the numbers mean in practice, from the recorded answers:
supervisor_progress_confidencereads the progress mass, not the API'sconfidence. It is compared with1 − P("0")— the probability above the lowest ("no progress") rung. The Score answer's ownconfidencemeasures how concentrated the answer is on one rung, so it drops whenever the answer spreads over adjacent rungs: a recorded half-done trace split 0.39 / 0.61 between "incremental" and "substantial" progress, nothing on "no progress", and reported confidence 0.42 — a mass of 1.0.relevance_rerankis gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. Inact, the retiredrelevancepoint used to send a second request, a noul per top-three skill over its body excerpt, and it confirmed no candidate in 7 of 8 recorded prompts while adding 0.18–0.86 s at the start of each. The wide choice's rank order stands.supervisor_loopingandsupervisor_loop_progressare gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads them. The every-N-steps loop check they tuned could only ever steer; the no-progress check that replaced it asks one question about the command about to run, and only when that command is a repeat.permission_scopeis gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. Against the task "fix the failing unit test in crates/ocean-llm", Jev scoredcargo nextest run --all-features0.73 andls -la0.57 on the oldoutside_scopequestion (both ask), andcargo nextest run -p ocean-llm0.24 (allow): a question about "the task" punished ordinary project commands, so scope became a code check. Re-recorded without it,cargo nextest run --all-featuresis allow.tools_confidence,tools_min_p,relevance_needsandrelevance_min_pare gone with their points (retired 2026-10-21, see Retired points). A key set in an old config is still accepted (one warning, "no longer read"); nothing reads them.
The other jev keys:
| Key | Default | Meaning |
|---|---|---|
enabled | true | An opt-out: false in either section turns the layer off. |
endpoint | the build default | The POST URL (global section only). |
model | jev-1.13.0 | A versioned model id; the thresholds are tuned on it. |
deadline_ms | 1500 | One decision's deadline, retries and the response body included. |
max_rpm | 150 | HTTP attempts per 60-second window, retries included (per process). |
max_state_chars | 24000 | The state budget of every point but output_view and visibility, in bytes of serialized state. A larger value does not raise the 48,000-byte cap: a state over it is never sent (invalid). |
backoff_seconds | 60 | How long the client stops calling after the endpoint fails (see Failure behaviour). |
prewarm | true | At boot, one GET /health on the endpoint's host opens the TLS connection (HTTP/2 where the server offers it), so the first decision skips the handshake; no credentials are sent and a failure is ignored (global section only). scripts/bench/jev-first-call.sh measures the first-call difference. |
supervisor_every_steps | — | Accepted with one warning; no longer read — the loop check it paced is replaced by the no-progress check, which fires on a repeated command instead of on a cadence. |
supervisor_max_continuations | 1 | How many times the supervisor may block a stop with a continuation per human prompt; 0 never blocks. A continuation after the first is asked about only when an edit, write, apply_patch or bash call ran since the last one. |
permissions, output_view, supervisor, triage, visibility, check, guidance, snippets, effort, work_mode | the point's default (Modes) | The per-point modes (the project section may set them). permission is accepted for permissions. |
tools (alias tool_tiers), relevance | — | Accepted with one warning ("no longer read (point retired 2026-10-21)"), never an error; nothing reads them. |
thresholds | the table above | Global section only. |
What is sent
A state is named fields, never a concatenated prompt, and the questions refer to them by backticked path; nothing from the state is spliced into a question.
| Point | State fields |
|---|---|
permission | task (your latest prompt, at most 4,000 bytes), tool, arguments.command, and arguments.workdir when the call names a working directory. Never the model-written description of the command. |
output_view | step_intent (the last 600 chars of the assistant text before the call), tool, output.blocks[] (id, lines, text ≤ 2,000 chars), trimmed to fit 48,000 bytes. |
supervisor | goal (≤ 1,500 chars), step_count, open_todos, last_steps[≤ 6] (tool, args ≤ 200 chars, result ≤ 300 chars — an error's message for a failed call —, exit for a call that reports one such as bash, and failed: true for a call that ended in an error; both keys only when present), final_text (the last 600 chars). The no-progress check: goal, recent_commands[≤ 5] (command ≤ 400 chars, exit when known, output_excerpt ≤ 150 chars), proposed_command. |
check | goal (≤ 1,500 chars), proposed_command, proposed_command_output (exit, text — the last 2,000 chars of the output, or the 300-char check tail when the output is empty —, truncated), recent_commands[≤ 6] (the prompt's earlier bash calls: command, exit when known, output_excerpt). |
triage | command (≤ 500 chars), exit_code, step_intent, output (head and tail, at most half the budget — 12 KB at the default). |
visibility | goal (≤ 1,500 chars), open_todos[≤ 20], parts[≤ 255] (id, tool, args, excerpt ≤ 400 chars of the old tool result), trimmed to fit 48,000 bytes. |
guidance | goal (≤ 1,500 chars), last_commands[≤ 6] (command, exit, output_excerpt), edited_paths. |
snippets | issue (the prompt, ≤ 4,000 chars), blocks (<id> → path, lines, text), at most 38,000 bytes per request. |
effort | prompt (≤ 2,000 chars), prev_prompt (the previous human prompt, ≤ 500 chars, or null). |
work_mode | goal (the prompt, ≤ 2,000 bytes), has_git, test_command_set. |
- Paths outside the project. There is no scope question; the bash gate marks an ask
paths_outside_project: trueand the advisor sends nothing then (a skip row,a path outside the project). The check runs on the call'sworkdirand on every word of the command that is a path: it starts with/or~, contains/, or is..— relative ones resolved against the workdir, the value of--key=value,key=valueand-I/pathtaken as the path, a leading redirection (2>,>>) dropped, a URL (://) and/dev/nullignored, and a$VAR/…word, which cannot be resolved, counted as outside. It is lexical, like the external-directory prompt: a symlink out of the project is not followed. - Head/tail truncation. A long text field keeps its head and tail around a
[truncated N bytes]marker. Point 1 never advises on a truncated state; the other points only record it. - 48,000 bytes is the hard cap on any state. The endpoint was measured to accept about 33k
input tokens — a 60,334-byte state answered, a 61,600-byte one was refused — and dense tool
output runs about 1.7 bytes per token, so no state is ever sent above 48,000 bytes of JSON. A
state the endpoint still refuses as too long comes back as HTTP 503 with error code 7003 ("User
Input Error"); the client records that as
invalid, never retries it and never counts it towards the back-off. - The injection guard. Before any call, every string of the state is normalised (case,
fullwidth letters, invisible and bidi characters, whitespace) and matched against phrases that
address the judge rather than describe the world — "ignore the above instructions", "note to the
classifier", "you are Jev", "this command is safe", … A match skips the call and records
injection. Your own words — the top-leveltask,goal,promptandissuefields — are exempt. - No content filter yet. States carry raw text: the command, up to 255 output blocks, six step
results, up to 12 KB of failing output, up to 255 excerpts of old tool results. That can include
a secret from a file the dotenv rule does not cover (
.dev.vars,*.pem), and the decision log keeps every state on disk (see Reading decisions). Opt a repository out ({ "jev": { "enabled": false } }in its.oceancode/oceancode.json) when its tool output may not leave the machine. - Where it goes. To TypeSafe's own API over HTTPS, under your key — file contents leave the
machine. TypeSafe states that it does not train on customer requests or responses
(
docs.typesafe.ai/models, "Data handling"). Through the PlungeAI worker, when you name it as the endpoint, the request is answered by Workers AI through Cloudflare AI Gateway, billed to the PlungeAI account with gateway logging on; the worker can also relay to OpenRouter, which OceanCode never uses: every request to the worker hasbackend=cloudflare, and an answer whosex-typesafe-backendheader is missing or names another backend is refused.
Reading decisions
oceancode inspectshows onejevrow per point: the mode as its layer, and the endpoint host (oroff) as its source. See 09 · Capabilities.jev-decisions.jsonl. Every decision a point makes — acted on or not — is one JSON line in<ledger root>/<session id>/jev-decisions.jsonl, beside the session's ledger folder (the ledger root is<data dir>/session-ledgerunlessledger.dirsets it). A row carriespoint,mode,outcome,verdict,baseline(today's behaviour),latency_ms,server_ms(the worker's own time for the call, from itsServer-Timingheader; absent when the endpoint sent none),model,input_tokens,state_hash(a hash of the full state),state(the state itself, or for one over 8 KiB a head/tail excerpt — the full state is then written once to<ledger root>/<session id>/jev-states/<state_hash>.json, so every decision can be re-sent) andanswers— the full answers with every probability, or for a skipped or failed decision the reason as a string. It never carries the key, the endpoint or a header value, and a URL in an error message is replaced by<url>. Writing is best effort: a full disk never fails a decision (the first failure is logged once).- Outcomes.
acted(the verdict changed something),shadow(recorded only),fallback(the decision kept today's behaviour: a low-confidence answer, a skip such as a command over 4,000 chars, or an answer that could not be applied),held(point 1's deterministic hold kept an allow answer at ask),unavailable(deadline, transport, HTTP error, rate cap or back-off window),invalid(an answer missing a key, of the wrong type or with bad probabilities; a state over its budget; the 7003 refusal),injection(the guard skipped the call) andoff(the decider behind the point was off). - The triage re-run note. When point 4 re-runs a command, its decision row is written first,
with the state Jev was asked about, and a second row follows with outcome
actedand the reasonrerun exit=<code>orrerun refused: <why>as itsanswersstring. Only rows whoseanswersis an object are decisions; count and label those. A missing-binary or missing-module failure (theenv_missingsignals in the point 4 row above) is written as one row with the reasondeterministic env_missingas itsanswersstring and no request:acted/hintonce the hint was appended,fallback/passfor a failing install command,shadow/hintinshadow. The hint line is "jev triage:<name>is not available (exit <code>); use what the project already has —<alternative>is available and do not install anything the task did not ask for" (the alternative clause only when one is onPATH; "a command" in place of the name when only the exit code said so, and for anenv_missinganswer Jev gave itself). - Labels. When you answer a permission prompt that Jev advised on, a label row is appended to
the same file —
allowfor once or always,denyfor reject — and joined to its decision. Only a person's answer is a label: the replies headlessoceancode rungives by itself (--auto's once, a Jev-advised reject, the reject without--auto) and the Ocean pager's own answers (its YOLO auto-approve, the queue it approves when you switch YOLO on, a cancelled or failed prompt) write none.oceancode jev labelsexports every labelled decision. - Labels a run writes itself. At the end of a prompt the
effortrow gets its outcome ({"steps", "edited", "checked", "check_passed_after_edit"}) and everysnippetsrow of the prompt its coverage (coverage=<f> selected_coverage=<f> edited=<n>). These are measurements, not your verdict:oceancode jev labelsexports them with sourceimplicit-effortandimplicit-snippets(a person's answer isshadow-user). Aguidanceuptake row is a skip row (itsanswersa string), so no export carries it. - Labelling by hand. Every point but
permissionhas no prompt to answer, so its rows come unlabelled.oceancode jev labels --unlabelled --out to-label.json(optionally--point <p>) writes them with"label": nulland"source": "unlabelled". An annotator fills eachlabelin the grammar ofbench/jev/README.md, setssourcetoannotator-<n>(a second annotator addssecond_label), and the rows are appended tobench/jev/labels.jsonby hand, for examplejq -s add bench/jev/labels.json to-label.json > merged.json && mv merged.json bench/jev/labels.json.oceancode jev labelskeeps every row already in that file and adds only decisions whose point and state hash it does not carry, so a later export never duplicates an annotated row. An unlabelled row may carry"evidence": {"spill_grepped": true}: its turn latergrepped orreada file undertool-output/, so the full output was wanted after all. It is a hint for you, never a label. - Implicit labels.
oceancode jev labels --implicit --out bench/jev/labels.jsonlabels the rows nobody labelled from what the agent did next in the session's transcript: whether it made anedit/write/apply_patch/bashcall after a supervisor continuation (continue, elsefinish,implicit-continuation). Rows follow the same merge as any export. These are the agent's behaviour, not your verdict, so spot-check them before a run counts on them; the rules are inbench/jev/README.md.
The oceancode jev subcommand
Every verb reads the same two sections and environment that boot reads, from the current
directory. Each ends with its sentinel on the last stdout line — except the two --json verbs
and verdicts, which keep stdout to JSON and print one JEV_STATUS_EMITTED points=10,
JEV_QUESTIONS_EMITTED points=10 or JEV_VERDICTS_EMITTED lines=<n> line on stderr, as
oceancode inspect --json does. A failure exits 1.
| Command | What it does |
|---|---|
oceancode jev status [--json] | One point mode endpoint-host line per point (- when no client is configured), then JEV_STATUS_OK points=10. --json prints {points, endpoint, thresholds} — the modes and the resolved thresholds the bench lanes read before they spend. Makes no call. |
oceancode jev probe | Live, one call. Asks one fixed destructive noul about git status --short at the first point that is not off; prints model=… latency_ms=… input_tokens=… destructive=… and JEV_PROBE_OK, or JEV_PROBE_FAIL <error> and exits 1. With every point off it refuses without a call, naming why — JEV_PROBE_FAIL jev is off: <reason>, where the reason is the one that turned the layer off: a missing or disabled section, OC_JEV=off, OC_TYPESAFE_API_KEY absent for the direct API, a refused plain-http endpoint, an endpoint that is not a URL, or every point's mode set to off. |
oceancode jev probe --record --point <p> --case <id> --request <req.json> --out <fixture.json> [--expect <json>] | Live, one call. The alias of scripts/fixtures/jev-record.mjs: sends the request file's {state, questions} through the configured decider — its credential scoping, back-off and backend pin — and merges {state, questions, response, expect} into cases.<id> of the fixture, keeping every other case. Prints JEV_RECORD_OK point=<p> case=<id> cases=<n>, or JEV_RECORD_FAIL reason=<reason> <message> and exits 1 (a fixture that holds another point, or does not parse, is refused and left as it is). |
oceancode jev labels [--root <ledger dir>] [--out <file>] | Every decision row with a label, from every <root>/<session>/jev-decisions.jsonl (default root: the configured ledger.dir, else <data dir>/session-ledger — where boot writes the log), appended to --out (default bench/jev/labels.json). The file's rows are kept exactly as read — their order, their own duplicates and any key you added by hand; a decision is added only when the file does not yet carry its point and state hash. A state over 8 KiB is exported whole from its jev-states/ file. Each row's source is shadow-user for your answers, implicit-effort / implicit-snippets for the labels those points write themselves. Prints JEV_LABELS_OK added=<n> total=<m>; a label file that does not parse is refused (JEV_LABELS_FAIL) and left as it is. |
oceancode jev labels --implicit [--root <ledger dir>] [--out <file>] | The same merge for labels read off the session transcripts (<root>/<session>/transcript/): an acted supervisor continue row continue or finish by whether a mutating call followed (see Reading decisions). Rows a person labelled are left to labels. Prints JEV_LABELS_OK added=<n> total=<m> implicit=<k>, k being the rows it derived. |
oceancode jev labels --unlabelled [--point <p>] [--root <ledger dir>] --out <file> | The same export for the decision rows nobody labelled ("label": null, "source": "unlabelled"), for annotators (see Labelling by hand), each with an evidence object where the transcript shows what the agent did next. --out is required, so unlabelled rows never land in the label set by default. |
oceancode jev questions --json | Each point's question set from the seams' own builders, in the order the seams send them. A choice whose options come from the request is empty and names its source in options_from (state.output.blocks[].id, state.parts[].id); snippets is one noul template under the key {id}, repeated per state.blocks key (options_from: "state.blocks"). The bench generates its question file from this. |
oceancode jev verdicts --in <cases.jsonl> | For each input line {point, state, response}, one output line {point, verdict} from that point's own policy at the global section's thresholds: permission "allow" / "ask" (the hold included; its line also carries held, true when the hold fixes the verdict at ask — the bench leaves such a row out of accuracy), output view {keep}, supervisor {stop, low} (stop as if no check had verified a pass yet; low whether the proposed_command answer is a no-progress low, null when the line has none), triage "pass" / "rerun" / "hint", visibility {protect}, check {verified}, guidance the four yes/no/null judgements, snippets {pack, chars}, effort {rungs_down}, work mode {pick} (Jev's mode id at or above the threshold, else null). Makes no call. |
Money. probe and probe --record are the only verbs that call Jev. So does every decision
in shadow or act: at the rate-card price of jev-1.13.0 ($0.042 per million input tokens,
output free) a permission question is about 550 input tokens and the largest output-view or
visibility state about 30k, so a long session costs cents. Through the PlungeAI worker the calls
are billed to the PlungeAI account. The live bench lanes refuse to run without OC_LIVE_CONFIRM=1.
Retired points
Removed 2026-10-21; their config keys still load with one warning (see Thresholds).
tool_tiers(jev.tools) narrowed the advertised tool list per prompt behind atool_searchtool. Retired 2026-10-21: every tool is advertised again, andtool_searchis gone; thesnippetspoint now spends the opening request's context on source instead.relevance(jev.relevance) narrowed<available_skills>per prompt. Retired 2026-10-21: replaced byskills.roster: names, now the default (one line of skill names;fullrestores the block — 02 · Configuration).
Environment variables
| Variable | Meaning | Default |
|---|---|---|
OC_JEV | off, shadow or act: overrides every Jev decision point's mode (off turns the layer off whatever the config says). It never supplies a key — without one the layer stays off. | unset → the config |
OC_JEV_ENDPOINT | Overrides jev.endpoint (the Jev POST URL). The only way to use plain http, and only to a loopback host (127.0.0.1, localhost, ::1). | unset → jev.endpoint, else https://api.typesafe.ai/v1/systemone |
OC_TYPESAFE_API_KEY | The TypeSafe API key when none is stored (oceancode key typesafe wins), sent only to api.typesafe.ai (never to another host); never in config; stripped from child processes like every *KEY* variable. The direct API without a key keeps the layer off. | unset |
TYPESAFE_API_KEY | The same key under the name TypeSafe's own SDKs read; OC_TYPESAFE_API_KEY wins over it. | unset |
The same rows are in 02 · Configuration, with every other variable.
Failure behaviour
A Jev failure is never a turn failure: the point falls back to today's behaviour and writes one
row. A shadow call runs in the background (see Modes), so its row — an answer or a
failure — is written when the call ends, under the id it was given when it started; closing the
session waits for the calls still open, at most 2.5 seconds, and a process that is killed
before then loses their rows.
| Failure | Behaviour |
|---|---|
| Endpoint unreachable, DNS, TLS | Fallback; unavailable. Counts toward the back-off: the second consecutive deadline or transport failure opens the jev.backoff_seconds window, during which no point calls. |
Deadline exceeded (jev.deadline_ms, the response body included) | Fallback; unavailable; the request is dropped, the turn goes on. One deadline alone never opens the back-off window; a second in a row does. The deadline of a large output_view or visibility state (up to 48,000 bytes) is never counted toward it, so a slow large state cannot silence the other points. |
| HTTP 401 / 403 | Fallback; one warning naming the credential source (auth.json (oceancode key typesafe), OC_TYPESAFE_API_KEY, TYPESAFE_API_KEY or ocean.headers); no further requests until restart. |
| HTTP 422 | Fallback; the row carries the server's message. |
| HTTP 408 / 429 / 5xx | Up to 2 retries, 500 ms → 5 s with 25 % jitter, honouring retry-after up to 60 s, and only when the delay fits what is left of the deadline; then fallback, and a 429 or 5xx that still fails opens the back-off window at once. |
| HTTP 503 with error code 7003 (state too long) | Fallback; invalid; no retry and no back-off. |
| A redirect | Never followed (the headers would follow it off the worker); fallback as an HTTP error. |
Client rate cap (jev.max_rpm HTTP attempts per 60-second window, retries included, per process) | The decision is skipped this time; unavailable. The cap does not bound the shared gateway, whose own 429 and the back-off window are the real protection. |
| Answer missing a key, of the wrong type, a choice outside its probabilities, or probabilities outside 0–1 or not summing to 1 ± 0.02 | Fallback; invalid. |
A 200 from the worker whose x-typesafe-backend header is missing or not cloudflare | Fallback; invalid; one warning; no further requests until restart. |
| State over its budget | Each field is cut head/tail and output blocks are trimmed to fit (24,000 bytes by default for every point but output_view and visibility, 48,000 for those two, never above 48,000); a state that still does not fit is not sent (invalid). Point 1 never advises on a truncated command. |
| The injection guard matches | No call; injection. Point 1 keeps the ask, and under --auto in act rejects it. |
A served model other than jev.model | Answered as usual; one warning per process (the thresholds were tuned on the configured id). |
See also
- 02 · Configuration — the
jevkey and the environment variables. - 07 · Headless and server —
oceancode jevamong the management subcommands. - 08 · Bounding a long run — the bounds the supervisor never changes.
- 12 · Permissions and sandbox — the rules and the floor that run before the permission advisor.