#The review paradigm — what lazy does by default, and why
Lazy reviews every task before you merge it, without being asked. What changed is how: the default review is fast — one session, self-reviewed, with the diff still in context — and the separate reviewer is the optimization you opt into when quality is worth three to four times the wall-clock and the tokens.
This page is the reasoning behind that. For the mechanics of a single review — the verdicts, what the reviewer may touch, how to stop one — see agent reviews.
#What a review is for
A review does three jobs, and the trade-offs later hang on which one you care about.
It is a merge gate: a second reader who catches what the writer missed. A writer that has just spent eight turns on a change is the worst possible auditor of it.
It is a substitute for your own reading: the reason you can accept without opening every file. Someone went through this branch, swept it for security and data-integrity problems, and said so in writing.
And it is a feedback loop: what turns a cheaper writer into acceptable output over a bounded number of rounds. A model told precisely what it got wrong can land work it would never have produced in one pass.
#Three modes
[review] mode in lazy.toml picks how a task is reviewed, and every task
carries its own. The default is low_high.
| mode | who reads the work | gates accept | relative cost |
|---|---|---|---|
low_high (default) |
the writer, in its own session | no | 1x |
separate |
a reviewer in a new session | yes | 3–4x |
off |
nobody | no | 0 |
The "gates accept" column is the DEFAULT answer — what lazy's own automatic
review does in each mode. A review you asked for yourself gates in all three,
and [review] gate overrides
the column either way.
low_high — the writer reviews itself. The work turn runs at
draft_effort (low), then the same session is resumed for a hostile
read-only self-review at review_effort (xhigh, raised to match the draft
when a task's own effort puts it higher — a review never runs weaker than the
work it reviews), then one revise pass
applies what that review found. The diff is already in context, so nothing is
re-read, and no second container starts. Its outcome is recorded as a review
— the same verdict / sweeps / findings shape every other review has, so you read
it the same way — and under the default gate it does not hold your accept. Set
gate = "always" and it does.
See the low-high loop for the phases in detail.
separate — a reviewer in its own session. After the task declares done, a
new agent in a short-lived container of its own reads the branch cold,
read-only, and files a verdict that gates your accept. It is never weaker
than the writer: it runs on the task's own model and effort. This is what every
task used to do.
off — nothing. A final triggers no review, and accept is not gated on
one, because there is none.
Set it per project:
[review]
mode = "low_high"
or per task, which wins and sticks for that task's whole life:
lazy create --review separate --goal "Rotate the signing keys"
lazy edit <task> --review low-high
#Settings are inherited, not just defaulted
[review] is the project's default, and a task can say otherwise. There are
three levels, and each setting resolves through them independently:
project config → parent task → task
A task that says nothing inherits its parent task's value; a top-level task inherits the project's. That is what makes a cluster cheap to configure: the driver sets a mode once on itself and every child follows, unless a child overrides — and a child that overrides only the mode still inherits its parent's gate.
Only a choice is inherited. A task picks up what somebody explicitly set
on an ancestor — --review* on create, start or edit, the same arguments over
MCP, or the web form. It does not pick up a value that ancestor merely ended up
with, such as the project default its own launch pinned on it. So setting a mode
on a cluster driver still configures every child, while a parent task that was never
configured leaves its children on the project default, which is what you would
expect of a default.
A choice travels the whole tree, not one generation: configure a parent task and its grandchildren follow too, and lazy keeps naming the task where the choice was actually made rather than whichever task passed it on.
The resolved values are pinned on the task the first time it launches, the way
--model and --effort are. A project default you change later moves new
tasks and leaves running ones exactly where they were. lazy show <task> prints
the effective values, so you never have to reconstruct them.
#Reading the Review line
lazy show <task> and the task page both carry one line for this:
Review: low-high, gate auto, auto-fix off
mode low-high — the writer reviews its own work in its own session, then revises (project default)
gate auto — the mode decides whether a review holds the merge; a review you asked for always does (project default)
auto-fix off — a review that finds something parks the task instead of starting a fix round (project default)
Three values — the mode, the gate and the auto-fix switch — and, for each one, where it came from:
| provenance | what it means |
|---|---|
project default |
nothing was set for this task or any task above it; [review] in lazy.toml decided |
inherited from <task> |
somebody set it on that task — a parent or a further ancestor — and nothing below it overrode |
set on this task |
somebody set it here, with --review*, the MCP tools or the web form |
already recorded on this task, with no choice on record |
the task was launched by a version of lazy that pinned the value without recording who decided it |
recorded on this task before [review] existed |
the task predates these settings and keeps the arm it was running under (see migrating) |
On the task page the same clauses are the tooltip on the Review: item, and the
item links here. Over MCP, lazy_show returns them as review.explanations
with the machine-readable review.sources.
If a value is not what you expect, the provenance says which level to change:
the project's [review] section, the parent task, or this task.
#The gate: when a review actually holds a merge
The mode says who reads the work. [review] gate says whether what they found
can refuse a merge, and it is the master switch over the mode rule.
| gate | what holds a merge |
|---|---|
auto (default) |
the mode decides for a review lazy dispatched — plus any review you asked for, in any mode |
always |
every recorded review, the low-high self-review's own outcome included |
never |
nothing, in any mode, manual reviews included |
A review you asked for always counts, under the default. lazy review and
a driver's lazy_review are deliberate acts — nobody spends a review turn they
did not want — so ignoring what one found because the task is in the fast mode
would make the command a no-op at exactly the moment you reached for it. Only
lazy's own automatic dispatch follows the mode.
always is for the project that wants the fast shape and still wants a bad
self-review to stop a merge: findings above medium, or a sweep that found
something, hold the accept until addressed or overridden. It does not hold
on findings the revise pass already applied — a self-review that found
something and fixed it is a self-review that worked, and a setting that stranded
those tasks would be unusable. never is the one
setting that also switches off a manual review's gate, because a human who
writes never has said exactly that.
Nothing is disregarded silently. When your settings let a merge through over a review that found something, accept says so in one line and points you at the report. A review that gates refuses instead, which is its own notice.
#Why the default is the fast one
Because the slow one can stop the work from advancing. With many tasks running at once under the separate reviewer, each can take three or more rounds of review-then-fix at roughly half an hour each — enough to exhaust an organisation's spend limit, at which point everything running stops.
The reasoning that produced the old default was "your time is the scarce resource, tokens are not". It was half right. Money is limited the same way time and attention are, and when the spend runs out the work does not merely get more expensive — it stops, and you wait. Performance per token is what actually decides how much gets done, and a second agent re-reading a diff that is already warm in the writer's own context is the worst deal on that axis.
So: fast first, ponderously slow as an optimization on quality. The escalation is per task, because that is where you can see whether the stakes justify it.
#What still holds, in every mode
The declaration is still the trigger. An agent declares its work done with
lazy_final, and that is the only thing the declaration does: accept does not
need one, so a task that parked for a decision — or simply stopped — is yours to
merge whenever you have read it.
Whenever a review COUNTS — in separate mode, or because you asked for it, or
because the gate is always — everything it can conclude still gates your
accept: a verdict that needs work, a blocking decision, a review that could not
be parsed, one that crashed, one that never started. "Nobody knows what it
concluded" may not read as a pass. lazy accept <task> --allow-review-issues is
your override, deliberately CLI-only: an agent may not overrule a review on your
behalf.
What no longer gates: findings at medium or below. A finding is a reviewer's opinion about work you are about to read anyway. Holding the merge on every style nit cost a full agent turn or a person per finding, and left a cluster's driver — told to accept liberally — unable to accept anything, since the override is CLI-only. Critical and high findings still hold; so do a failed review, an outstanding blocking raise, and a report whose own security or data-integrity sweep names something no finding covers.
Fixing is its own switch. auto_fix is false by default: a
separate review that finds something parks the task with the findings
attached instead of spending another round on the daemon's own initiative.
Whether that round is worth half an hour is a judgement made by whoever can see
the whole board — you, or a cluster's driver. Turn it on to get the old
behaviour:
[review]
mode = "separate"
auto_fix = true
The rounds that are started stay bounded: at most two review-then-fix rounds
before the task parks with its findings, and [cluster] max_child_fix_rounds
(default 3) per child for a cluster.
Automatic reviews still come out of [daemon] auto_react_daily_budget, the
shared daily allowance for every turn the daemon starts on its own (default 50,
reset at local midnight). Lower it and reviews arrive late rather than never.
Setting it to 0 is blunter than it looks: it stops every self-started turn,
including the recovery of a crashed one. A separate task whose review was
never dispatched does not quietly become acceptable — the skip is recorded, with
the reason, and it holds acceptance exactly as an unclean review does.
#Migrating from [agent] low_high_loop
The low-high loop used to be an experiment under [agent], off by default. Its
three keys moved into [review] and are still honoured meanwhile, with
lazy doctor naming the replacement for each:
| old | new |
|---|---|
[agent] low_high_loop = true |
[review] mode = "low_high" |
[agent] low_high_loop = false |
[review] mode = "separate" |
[agent] low_high_loop_draft_effort |
[review] draft_effort |
[agent] low_high_loop_review_effort |
[review] review_effort |
Note the second row. false never meant "no review" — it meant "no in-session
loop, and the daemon dispatches a reviewer of its own", which is exactly
separate. Reading it as the new default would switch your existing projects
into a different mode on upgrade, so it does not. Setting both spellings to
things that disagree is refused at load rather than guessed.
Tasks already in flight keep the mode they were running under, for the same reason: the mode is written onto a task the first time it launches, so a project default you change later moves new tasks and leaves running ones alone.
Their children do not. low_high_loop = false was recorded on every task
alive, whether or not anyone had an opinion, so it says what that task was
doing and nothing about what its subtasks should do: a task created under an
existing parent task takes the project default. Escalate the ones that deserve it with
lazy edit <task> --review separate.
lazy start --low-high-loop on|off is gone. Use --review low-high for what
on did, and --review separate for what off did.
The effort you chose for a task is not discarded. low_high never
silently downgrades a task somebody set to high: an --effort on THIS TASK wins,
for the draft and the revise pass alike.
A project-wide [agent] effort is the fallback the other phases resolve from
and does not outrank draft_effort for the draft — otherwise the setting would
do nothing on any project that states an effort, which is most of them.
#When tokens are still the constraint
Every lever below is real, and every one trades something away.
Run a cheaper writer. lazy create/start/edit take --model, and
--agent <name> picks an agent profile
with its own model, harness and credential. What it trades: in separate
mode the review inherits the task's own model and effort, so a cheaper
writer is also a cheaper reviewer. lazy review <task> --model <strong> --effort high buys back a strong reader for one review.
Use --review off for work you are going to read yourself. A one-line
config change, a doc fix, a revert. What it trades: everything — you are the
reviewer, and nothing records that anyone read it.
Lower [cluster] max_child_fix_rounds. Fewer rounds per child before the
cluster must accept it, close it, or ask you. Lower means a smaller number — 0
is not a floor, it removes the bound and is the most expensive setting there is.
What it trades: the cluster hands you more decisions.
Keep briefs small. One coherent goal makes a small diff, and a small diff is a cheap review — the reviewer walks the change in the regions the writing agent grouped itself. What it trades: more tasks to create and accept.
Accept with --allow-review-issues when you have read it yourself.
lazy accept <task> --allow-review-issues merges over a review that left
issues outstanding or could not produce a verdict. What it trades: everything
above — you are the reviewer now.
#What lazy records, so you can judge the trade
Two surfaces answer this, and they count different things — expect two numbers, and know which one you are reading.
The task page's Stats tab is what the agent reported, per turn, for the task or its whole subtree (what it means). Its caveat: usage is recorded only when the harness reports it. Claude Code, Pi, Codex and Cursor measurements are retained with their turns; a turn that reported none is left out rather than drawn as a free turn.
lazy stats tokens is what went over the wire: one record per request the
lazy proxy forwarded, rolled up by role, task and model. Every launch is
proxied, so nothing escapes the trail — but token counts are read off the wire,
and only the Anthropic and OpenAI wires are read. A Cursor task is
forwarded opaquely, so its requests appear with no tokens and no model against
them. Its other caveat: the trail is a bounded rotating window, so old
traffic ages out. lazy stats tools <task> breaks it down by tool.