Everything the README leaves out: the full flow format, the stop policy, gates, privacy and known limits.
.jevflow/flow.json. Unknown keys anywhere are rejected, so a typo fails at load time.
{
"schema_version": 1,
"flow_version": "1",
"goal": "Build a CLI todo app in Python with add/list/done commands and tests",
"mode": "warn",
"phases": [
{"id": "scaffold", "name": "Project scaffold", "done_when": "package layout and entry point exist",
"check": "test -f todo/cli.py"},
{"id": "implement", "name": "Implement commands", "done_when": "add, list and done work",
"dynamic": true},
{"id": "docs", "name": "Usage docs", "done_when": "README documents every command",
"depends_on": ["scaffold"], "check": "grep -q 'todo add' README.md"},
{"id": "test", "name": "Tests pass", "done_when": "a test suite exists and passes",
"depends_on": ["implement", "docs"], "check": "python -m pytest -q",
"loop": {"max_iterations": 3, "until": "python -m pytest -q"}, "on_fail": "debug"},
{"id": "debug", "name": "Debug failures", "done_when": "the failing tests' cause is fixed"},
{"id": "publish", "name": "Tag release", "done_when": "v0.1 tag exists",
"depends_on": ["test"], "check": "git tag -l v0.1 | grep -q v0.1", "side_effect": true}
],
"limits": {
"max_blocks_per_session": 6, "max_restarts": 5, "max_total_minutes": 90,
"hang_minutes": 10, "max_jev_calls": 200, "check_timeout_s": 120,
"state_char_budget": 12000,
"confidence": {"auto": 0.80, "review": 0.50, "flag": 0.70, "trust_check": 0.90}
},
"privacy": {"send_diff": false},
"gates": {
"pre_tool": false, "injection_screen": false, "injection_tools": ["WebFetch"],
"subagent_stop": false, "task_completed": false,
"bands": {"deny": 0.80, "ask": 0.50, "regenerable": 0.80, "injection": 0.70}
},
"notify": {"command": "./notify.sh", "on": ["ask_human", "goal_complete", "budget"], "timeout_s": 10}
}Top level:
| Field | Default | Meaning |
|---|---|---|
schema_version |
1 |
Only 1 is supported. |
flow_version |
"1" |
Recorded in state; part of side-effect idempotency keys. Bump it when you change the plan. |
goal |
required | One concrete, checkable sentence. |
title |
"" |
Short human name (at most 80 characters) shown in status, the viewer and the flow list. Claude sets it when it lays out a flow. |
mode |
"enforce" (init writes "warn") |
observe: journal only. warn: never blocks, shows what it would do. enforce: blocks and pauses for humans. |
phases |
required | Non-empty list, see below. |
limits |
see example | All integers >= 1, except max_restarts may be 0 (run once). |
privacy.send_diff |
false |
When false, Jev sees only file names and line counts, never file contents. |
gates |
all off | Optional tool gates, see Conditions. |
notify |
none | Optional command run on human, goal and budget events. |
Phase fields:
| Field | Meaning |
|---|---|
id |
^[a-z][a-z0-9_-]{0,39}$; unclear is reserved. |
name, done_when |
Shown to Claude and to Jev. done_when is what Jev verifies. |
check |
Optional shell command, exit 0 = passes. Runs in the project with the Jev key removed from its environment. Always outranks Jev. |
depends_on |
Omitted: depends on the previous phase (linear). []: a root. A list: explicit DAG edges. Cycles are rejected. |
loop |
{max_iterations, until}: each Stop in the phase runs until once (a "run"). Re-block until it passes, then continue. max_iterations is the total runs allowed, passing or failing; when run N fails, route to on_fail or ask a human. loop_iterations in state counts runs used, so a first-try pass records 1 and an unreached loop stays 0. |
on_fail |
Phase to route to when this phase's check fails after an attempt. A target nothing depends on is branch-only: it runs only when routed to and is not required for goal completion. |
dynamic |
Claude may split this phase into sub-steps via .jevflow/subtasks.json. Sub-steps can only hold an advance, never cause one. |
side_effect |
An external action (publish, deploy, send). Requires a check. Completion is written once to .jevflow/side_effects.jsonl with key flow_version:phase:attempt; it is never re-entered, even after a state reset. |
- SessionStart (startup, resume, compact) injects the goal, the phase table, the current phase and, after resume or compaction, the last Jevflow instruction. Compaction is where agents usually lose the plot.
- Stop runs the checks (current phase, loop condition, and every already-done phase for regression), builds a curated state within
state_char_budget(goal, phase table, check results with failing output tail, last message tail, git change summary, last 5 decisions), and asks Jev one parallel request: current phase (withunclear), phase-done verify nouls, next action, stuck, off-goal, claims-done, progress. - The pure
policy.decide()returnsALLOW_STOP,BLOCK(reason)orADVANCE(to_phase). Every decision is journaled to.jevflow/state.jsonbefore the hook returns, so a restart resumes from the journal. - The supervisor (
jevflow run) launchesclaude -p ... --plugin-dir ..., and after each exit reads state: done ends the run, otherwise it relaunches with--resumeand a prompt built from the current phase and last block reason, within the restart and time budgets. It holds a single-runner lease, kills a hung session, and backs off on API errors without charging a restart.
Advance needs three things together: Jev's current_phase winner is this phase at auto confidence, the phase_done verify noul is at auto, and the phase check passes when one is defined. Below the review band Jev is ignored and only checks count.
Supervisor exit codes: 0 goal complete, 2 limit reached, 3 configuration error, 4 waiting on a human (.jevflow/NEEDS_HUMAN.md), 5 another supervisor is running, 130 interrupted.
Evaluated in fixed priority order on each Stop; the first match wins. The condition tag is in every journal entry.
| Condition | Trigger | Result |
|---|---|---|
already_done |
flow already complete | allow stop |
budget_time, budget_blocks, budget_jev, hook_cap |
a limit hit, or 7 consecutive blocks (Claude Code's own cap is 8) | allow stop with report, notify budget |
regression |
a done phase's check now fails | back to that phase with the failing output |
side_effect_regression, side_effect_on_fail |
a done side-effect phase fails, or on_fail routes into one | ask human, never re-run |
loop_continue, loop_pass, loop_exhausted, loop_exhausted_on_fail |
bounded loop phase | re-block, advance, route to on_fail, or ask human |
degraded_check_pass/fail, degraded_no_check |
Jev unreachable, no key, error or bad answer | checks only; never blocks without evidence |
premature_completion, final_check_fail |
Claude claims done while a check fails | block with the failing check output |
stuck, stuck_escalate, stuck_ask_human |
stuck >= flag twice in a row, or the same block reason 3 times | "change approach", then ask human |
off_goal |
off_goal >= flag | re-read the goal |
ask_human |
Jev next_action ask_human at auto |
write NEEDS_HUMAN.md, stop, supervisor exits 4 |
advance, subtask_pending |
advance rule above; a dynamic phase's named sub-step still open | advance, or hold |
review_band, review_check_pass |
confidence between review and auto |
keep blocking with a note; 2 review stops with the check passing advance |
check_and_phase_done |
phase check passes and Jev's phase_done for it is >= limits.confidence.trust_check (0.90) |
advance, even if the current-phase confidence is lower |
budget_blocks, hook_cap, budget_time, budget_jev |
a budget or cap is hit | first mark every phase whose check already passes as done (in DAG order); if that finishes the flow it ends goal_complete, otherwise stop |
drop_band, phase_mismatch, continue |
low confidence or disagreement | keep the current phase |
dag_deadlock, bad_state |
no eligible phase, or unreadable state | ask human |
goal_complete |
every required phase done, all checks pass, Jev agrees | allow stop, notify goal_complete |
Optional gates (gates.*, all off by default, all fail open to Claude Code's own permission system when Jev is unavailable):
| Gate | Hook | What it does |
|---|---|---|
pre_tool |
PreToolUse on Bash | Nouls destructive, remote_code, prod_scope, privileged, regenerable_artifacts, composed in code to ask or deny. Never emits allow, so it can only tighten permissions. Plain read-only commands skip Jev. |
injection_screen |
PostToolUse on WebFetch (and Read if listed and send_diff is true) |
Adds a "treat as data" warning when content looks like instructions aimed at the agent. Never blocks. |
subagent_stop, task_completed |
SubagentStop, TaskCompleted | Holds only a confident premature "done" claim on unfinished work, at most twice per subtask. |
All three modes apply to gates too: observe journals, warn shows a system message, enforce acts.
- With
send_diff: false(default), Jev receives: goal, phase names anddone_when, check exit codes and output tails, the tail (4k chars) of Claude's last message, changed file names with line counts, and recent decisions. No file contents. - Check output and Claude's messages can still contain code or data; write checks that print little.
- The pre-tool gate sends Bash command lines. Credential-looking values (password, token, api_key, Bearer, AWS key ids) are redacted by pattern before sending and journaling, but redaction is best effort.
- Jev is an external service. Do not use Jevflow on projects whose data may not leave your environment.
- Not a sandbox. The hooks need the key, so the key sits in the plugin's secure
jev_api_keyoption and is passed to hook processes only (check and notify commands have it scrubbed). If you exportJEV_API_KEYinstead, the Claude process and its Bash tool inherit it. flow.jsonis trusted code.checkandnotify.commandare shell commands, and the agent can edit the file. The skill tells Claude not to, but nothing enforces it. Review flow changes like code.- Do not enable
pre_toolwhere secrets appear on command lines (redaction is pattern based). - Sub-step titles in
subtasks.jsonare agent-written text sent to Jev. They can only hold an advance, never cause one. - TaskCompleted reads the transcript tail, which can lag the live turn; then the gate allows.
- Keep projects outside the plugin directory: a project nested inside the
--plugin-dirtree had every file write refused by Claude Code's edit safety check. With the project outside it, a real interactive Claude Code session ran the todo flow end to end (6 stops: 2 review-band blocks, 3 advances, goal complete in about 1.5 minutes). - The
on_failbranch is covered by unit tests but was not taken in the demo. - Jev is asked only what it measured well on: phase detection, done verification, stuck, off-goal, claims-done. It is never asked why something failed; root cause comes from check output.
- Linux and macOS only (uses
fcntland process groups).
.claude-plugin/plugin.json plugin manifest
hooks/hooks.json, jevflow hook registration and the Python launcher
commands/ /jevflow:init, /jevflow:status
skills/jevflow/SKILL.md how Claude should behave in a tracked session
jevflow/ the package: flow, state, judge, policy, hooks, gates,
subgates, regions, notify, supervisor, status, jev_client
tests/ unittest suite (python -m unittest discover -s tests)
docs/ SPEC, RESEARCH (spikes, measured Jev probabilities), DEMO
examples/ toy projects used by the demo