For AI agents: a documentation index is available at https://www.plungeai.com/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, to get markdown. Setup instructions for agents are at https://www.plungeai.com/docs/agents.md. One ozk_ key opens every plane, models included.

Documentation Index: fetch the complete documentation index at /llms.txt. Use this file to discover all available pages before exploring further.

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 keyQuestion askedWhat act changesWhat stays deterministic
permission · jev.permissionsFor 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_viewFor 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.supervisorAt 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.checkAfter 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.triageAfter 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.visibilityAt 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.guidanceAfter 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.snippetsOnce 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.effortOnce 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_modeOnce 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

  1. 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.json in 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 it

    The environment works too: OC_TYPESAFE_API_KEY, else TYPESAFE_API_KEY (the name TypeSafe's own SDKs read). The stored key wins over both. The key goes only to api.typesafe.ai — never to another endpoint, never into a config file, never to a child process.

  2. That is all. No jev section is needed: with a key the layer is on with the defaults below. A jev section 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.jsonc or .json) may set only enabled and the ten mode keys — its endpoint, 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 } }
  3. The endpoint. The default is TypeSafe's own API, https://api.typesafe.ai/v1/systemone (model defaults to the pinned jev-1.13.0; the thresholds are tuned on it). jev.endpoint in the global config or OC_JEV_ENDPOINT names another: the PlungeAI worker (the hosted typesafe provider, which needs no TypeSafe key and takes the ocean.headers map of the global config — the managed document or yours, never the project file's — as x-ocean-key), or a loopback stub. The key goes only to api.typesafe.ai, ocean.headers only to the worker, a redirect is never followed, and plain http is refused unless OC_JEV_ENDPOINT names a loopback host.

    Every request names its decision point in an x-jev-point header and the direct API's Server-Timing (when present) is kept on the decision row.

  4. Check it. oceancode jev status lists the ten points with their modes and the endpoint host (api.typesafe.ai); oceancode jev probe makes one live call and prints JEV_PROBE_OK. Without a key, status prints every point off and probe names the reason: no TypeSafe API key: run \oceancode key typesafe <key>` …`.

Modes

Each point has its own key: off, shadow or act.

ModeWhat the point does
offNever calls Jev; today's behaviour, no decision row.
shadowAsks 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.
actAsks 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:

  1. Run the point in shadow for at least a week of your own sessions.
  2. oceancode jev labels collects every decision you answered (see Reading decisions) into bench/jev/labels.json.
  3. The point's own run — node scripts/bench/jev-decisions.mjs --set bench/jev/labels.json --point <p> (live, OC_LIVE_CONFIRM=1) — must print JEV_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 for permission and 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 prints JEV_EVAL_RED reason=wilson.
  4. Then set that point's key to act in 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.

KeyDefaultThe risk it guards
permission_destructive0.6Ask when destructive is at least this.
permission_egress0.5Ask when egress is at least this.
permission_reversible1.5Ask when the reversible score is at least this.
permission_confidence0.8Ask when the blast_radius choice is less confident than this.
output_relevant0.5Below this exists, keep no block (head preview + spill instead).
output_mass0.8Keep blocks until their probabilities add up to this (at most 8).
output_top_p0.8Below output_relevant, still keep the top block when its probability is at least this…
output_exists_floor0.2…and exists is at least this.
supervisor_complete0.85Always let the stop through when complete is at least this.
supervisor_continue_complete0.3Block a stop only when complete is below this.
supervisor_progress1.0Block a stop only when the progress score is at least this.
supervisor_progress_confidence0.5Block a stop only when the progress mass is at least this (see below).
supervisor_no_progress0.31A proposed_command answer at or below this is a low (two in a row steer, four stop).
check_pass0.5A pass is verified only when check_now_passes is the top category with at least this probability.
check_remaining0.5A pass is verified only when the remaining_checks noul (a check the goal names that has not run and passed) is below this.
triage_confidence0.8Pass the failure through when the category choice is less confident than this.
visibility_relevant0.4Protect nothing below this any_needed.
visibility_min_p0.05A protected part needs at least this probability.
guidance_yes0.7A guidance noul at or above this is a yes.
guidance_no0.3A guidance noul at or below this is a no; between the two it is unresolved.
effort_confidence0.8The difficulty score steps the effort down only at this confidence or above.
work_mode_confidence0.6Auto 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_confidence reads the progress mass, not the API's confidence. It is compared with 1 − P("0") — the probability above the lowest ("no progress") rung. The Score answer's own confidence measures 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_rerank is gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. In act, the retired relevance point 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_looping and supervisor_loop_progress are 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_scope is 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 scored cargo nextest run --all-features 0.73 and ls -la 0.57 on the old outside_scope question (both ask), and cargo nextest run -p ocean-llm 0.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-features is allow.
  • tools_confidence, tools_min_p, relevance_needs and relevance_min_p are 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:

KeyDefaultMeaning
enabledtrueAn opt-out: false in either section turns the layer off.
endpointthe build defaultThe POST URL (global section only).
modeljev-1.13.0A versioned model id; the thresholds are tuned on it.
deadline_ms1500One decision's deadline, retries and the response body included.
max_rpm150HTTP attempts per 60-second window, retries included (per process).
max_state_chars24000The 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_seconds60How long the client stops calling after the endpoint fails (see Failure behaviour).
prewarmtrueAt 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_continuations1How 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_modethe 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.
thresholdsthe table aboveGlobal 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.

PointState fields
permissiontask (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_viewstep_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.
supervisorgoal (≤ 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.
checkgoal (≤ 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).
triagecommand (≤ 500 chars), exit_code, step_intent, output (head and tail, at most half the budget — 12 KB at the default).
visibilitygoal (≤ 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.
guidancegoal (≤ 1,500 chars), last_commands[≤ 6] (command, exit, output_excerpt), edited_paths.
snippetsissue (the prompt, ≤ 4,000 chars), blocks (<id> → path, lines, text), at most 38,000 bytes per request.
effortprompt (≤ 2,000 chars), prev_prompt (the previous human prompt, ≤ 500 chars, or null).
work_modegoal (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: true and the advisor sends nothing then (a skip row, a path outside the project). The check runs on the call's workdir and 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=value and -I/path taken as the path, a leading redirection (2>, >>) dropped, a URL (://) and /dev/null ignored, 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-level task, goal, prompt and issue fields — 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 has backend=cloudflare, and an answer whose x-typesafe-backend header is missing or names another backend is refused.

Reading decisions

  • oceancode inspect shows one jev row per point: the mode as its layer, and the endpoint host (or off) 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-ledger unless ledger.dir sets it). A row carries point, mode, outcome, verdict, baseline (today's behaviour), latency_ms, server_ms (the worker's own time for the call, from its Server-Timing header; 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) and answers — 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) and off (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 acted and the reason rerun exit=<code> or rerun refused: <why> as its answers string. Only rows whose answers is an object are decisions; count and label those. A missing-binary or missing-module failure (the env_missing signals in the point 4 row above) is written as one row with the reason deterministic env_missing as its answers string and no request: acted / hint once the hint was appended, fallback / pass for a failing install command, shadow / hint in shadow. 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 on PATH; "a command" in place of the name when only the exit code said so, and for an env_missing answer Jev gave itself).
  • Labels. When you answer a permission prompt that Jev advised on, a label row is appended to the same file — allow for once or always, deny for reject — and joined to its decision. Only a person's answer is a label: the replies headless oceancode run gives 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 labels exports every labelled decision.
  • Labels a run writes itself. At the end of a prompt the effort row gets its outcome ({"steps", "edited", "checked", "check_passed_after_edit"}) and every snippets row of the prompt its coverage (coverage=<f> selected_coverage=<f> edited=<n>). These are measurements, not your verdict: oceancode jev labels exports them with source implicit-effort and implicit-snippets (a person's answer is shadow-user). A guidance uptake row is a skip row (its answers a string), so no export carries it.
  • Labelling by hand. Every point but permission has no prompt to answer, so its rows come unlabelled. oceancode jev labels --unlabelled --out to-label.json (optionally --point <p>) writes them with "label": null and "source": "unlabelled". An annotator fills each label in the grammar of bench/jev/README.md, sets source to annotator-<n> (a second annotator adds second_label), and the rows are appended to bench/jev/labels.json by hand, for example jq -s add bench/jev/labels.json to-label.json > merged.json && mv merged.json bench/jev/labels.json. oceancode jev labels keeps 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 later grepped or read a file under tool-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.json labels the rows nobody labelled from what the agent did next in the session's transcript: whether it made an edit / write / apply_patch / bash call after a supervisor continuation (continue, else finish, 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 in bench/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.

CommandWhat 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 probeLive, 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 --jsonEach 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 a tool_search tool. Retired 2026-10-21: every tool is advertised again, and tool_search is gone; the snippets point now spends the opening request's context on source instead.
  • relevance (jev.relevance) narrowed <available_skills> per prompt. Retired 2026-10-21: replaced by skills.roster: names, now the default (one line of skill names; full restores the block — 02 · Configuration).

Environment variables

VariableMeaningDefault
OC_JEVoff, 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_ENDPOINTOverrides 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_KEYThe 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_KEYThe 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.

FailureBehaviour
Endpoint unreachable, DNS, TLSFallback; 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 / 403Fallback; 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 422Fallback; the row carries the server's message.
HTTP 408 / 429 / 5xxUp 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 redirectNever 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.02Fallback; invalid.
A 200 from the worker whose x-typesafe-backend header is missing or not cloudflareFallback; invalid; one warning; no further requests until restart.
State over its budgetEach 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 matchesNo call; injection. Point 1 keeps the ask, and under --auto in act rejects it.
A served model other than jev.modelAnswered as usual; one warning per process (the thresholds were tuned on the configured id).

See also

Planned: TI-34

The docs assistant is not available yet. You can hand these docs to your own assistant instead.