Loop Engineering: Your Agent Should Not Grade Its Own Homework
Ciprian · · 14 min read Prompt engineering taught you to write better instructions. Context engineering taught you to feed the model the right information. Loop engineering is blunter than both: it says the thing you should be designing is not the prompt but the loop the agent runs in. You decide what gets picked up, what builds it, what checks it, what gets written down, and when it stops.
The term comes from a June 2026 paper by Addy Osmani, Peter Steinberger and Boris Cherny. And this is not a thought experiment: Stripe runs a pipeline that merges over 1,300 machine-written pull requests per week. At that volume, nobody is reading every diff. Something else has to say no.
The one idea to keep
The paper decomposes a single turn of a loop into five moves: discovery, handoff, verification, persistence, scheduling. I’ll get to all of them. But one idea sits above the rest: the agent that writes must not be the agent that judges.
Ask an agent to grade its own output and it praises it. Not occasionally; it’s the default behavior. It wrote the code for reasons, it remembers the reasons, and the reasons are persuasive. The authors’ finding is that tuning a separate, skeptical evaluator is far more tractable than making a generator critical of its own work.
Banks solved this decades ago and called it maker-checker: the person who enters the transfer is not the person who approves it. Loop engineering ports that split to agents. You spawn a fresh evaluator that carries none of the author’s context. It never saw the justifications, so it can’t be persuaded by them. It defaults to “broken until proven otherwise” and it runs the real checks: build it, lint it, run the tests, paste the output. It judges whether the code runs right, not whether it looks right.
That split is the difference between confident garbage and real progress. Everything else in the paper serves it.
Why you should at least try this
I’m not pushing fully autonomous loops. You don’t need to want an agent running unattended overnight to get something out of this. The reason to try loop engineering is the mechanism: building and verifying as structurally separate steps, with a ledger in between. That mechanism is useful at whatever scale you apply it.
At the micro level it’s a single ticket. Build the change, hand it to a fresh evaluator that assumes it’s broken, fix what comes back, done. You’ve replaced “the agent said it works” with a second opinion that ran the tests. At the macro level it’s a fifty-unit migration where the same cycle repeats until the list is empty. Same five moves, different loop count. The mechanism doesn’t care whether a human triggers each turn or a scheduler does.
The five moves make one turn concrete:
- Discovery: pick the next unit of work.
- Handoff: give it to an agent to build.
- Verification: hand the result to an independent evaluator.
- Persistence: record what happened so the next turn knows.
- Scheduling: decide whether to continue, and when.
Drop any one of these and the loop fails in a way the paper names. No verification and you get the Nodding loop, where output approves itself and the evaluator has never said no. No persistence and you get the Amnesiac loop, redoing yesterday’s work and conflicting with it. No scheduling and you get the Manual loop, which stalls until you nudge it, a loop in name only. Unbounded discovery gives you the Blind loop, inventing work nobody asked for. And parallel agents sharing a working tree gives you the Tangled loop, stepping on each other’s files. When a loop feels off, it’s almost always one skipped move. Name it, restore it.
The paper is equally specific about the four silent costs of a self-running loop, each with a one-line guard:
- Token blowout: cap before you ship. Set max turns and retry limits before the loop runs unattended, not after the first surprising bill.
- Verification debt: the independent evaluator. Unverified-but-merged code is debt that detonates on some quiet morning.
- Comprehension rot: read a sample, every turn. If you can’t explain what the loop produced, your map has fallen behind the code.
- Cognitive surrender: keep one door open. One checkpoint where a human can say no, even if they usually won’t.
The skill: /epic-loop
I distilled the paper into a Claude Code skill. It’s deliberately the tame version of a loop: goal-driven, not autonomous. You hand it an epic (“migrate these twelve endpoints”, “get this test suite green”, “work through this refactor list”) and it drives the work to a stated done-condition. It never hunts for its own work; that’s a riskier loop the skill refuses to be.
The shape follows the paper directly. Before turn one, it pins a contract: the goal in one sentence, a checkable done-condition, and a stop posture. The default posture is stop-at-pr. Work lands on a branch, a PR opens, the loop stops, a human merges. That posture is the door. auto-merge exists, but only if you asked for it by name.
Then it decomposes the epic into small units, each buildable and verifiable in a single turn, and works them one at a time: pick, build, verify, record, continue. The verify step is the heart. Every unit goes to a fresh evaluator spawned with no memory of why the code was written, briefed to assume the change is broken, to default to REJECT, and to run the actual build, linter, and test suite rather than skim the diff. REJECT means fix and re-verify. It never ships a rejected unit, and it never waters down the judge to get a PASS; a lenient evaluator defeats the entire exercise. If your loop’s evaluator has never said no, you don’t have an evaluator.
I run it two ways: standalone for a bounded epic, or wrapped in /loop so each iteration advances one unit and feeds the next, with a turn cap set up front.
Here’s the whole thing. Drop it in ~/.claude/skills/epic-loop/SKILL.md and invoke it with /epic-loop <goal>. The evaluator brief references my projects’ build commands (Go, Biome, a tool called preflight), so swap in your own.
---
name: epic-loop
description: Work a large, given goal or epic as a disciplined loop. Use when the user hands you a multi-step task/epic and wants you to drive it to completion turn-by-turn — decompose, build, hand each unit to a FRESH independent evaluator that defaults to "broken", record progress, and continue until a stated done-condition holds. Pairs with /loop (self-paced) or runs once. Not for autonomous work-hunting; the goal is given.
---
# Epic Loop — Drive a Given Goal as a Verified Loop
Distilled from *Loop Engineering* (Osmani/Steinberger/Cherny, 2026). The one idea worth
keeping above all others: **the agent that writes must not be the agent that judges.** An
agent grading its own work praises it. A separate, skeptical evaluator that *defaults to
broken* and *runs the actual checks* is the difference between confident garbage and real
progress. Everything else here serves that split.
This is **goal-driven**, not autonomous work-hunting. The user gives you a goal/epic; the
loop decomposes it, works each unit, verifies each unit independently, and continues until a
stated done-condition holds. You do not invent work.
## When to use
- The user hands you a large/multi-step task or epic and says "work through this" / "drive
this to done" — directly, via `/epic-loop <goal>`, or wrapped in `/loop`.
- There is a checkable done-condition (tests pass, a list is exhausted, a behavior works).
Do **not** use for a single quick edit (just do it), or for open-ended "find me work to do"
(that's a different, riskier loop this skill deliberately avoids).
## The contract — pin these before turn 1
Read the invocation. If any of the first three are missing, infer a sensible default, **state
it in one line, and proceed** (don't interrogate the user):
1. **Goal** — the epic, in one sentence.
2. **Done-condition** — the checkable thing that ends the loop. The `/goal` stop-condition.
Default if unstated: "every decomposed unit is evaluator-PASSED and the full build/lint/test
suite is green."
3. **Stop posture** (configurable — this is the "one door"):
- `stop-at-pr` *(default)* — land work on a branch, open a PR, **stop**. Human merges.
- `auto-merge` — when the independent evaluator fully passes, merge and continue to the
next unit. Only choose this if the user said so for this run (e.g. "auto-merge validated
work"). Honors the user's standing autonomy preference when they ask for it.
- `branch-only` — commit to a branch, no PR. For exploratory epics.
4. **Caps** (set BEFORE running unattended — see Guards): max turns, and what stops it.
## Turn 0 — Plan once, cheaply
Before looping, decompose the goal into an **ordered list of small units**, each independently
buildable and verifiable. Keep it lightweight — a `TaskCreate` list (preferred; you already
have it) or a short checklist. This list *is* the loop's memory; it survives context
compaction. No state-file/inbox ceremony beyond this.
- Order by dependency, then by "cuts risk earliest."
- Each unit should be small enough that one turn can finish AND verify it.
- Write the done-condition down explicitly. If you can't state how the loop ends, stop and ask.
## The turn — five moves, adapted to a given goal
Each turn does ONE unit, concretely, and feeds the next. Drop any move and the loop fails in a
named way (see Anti-patterns).
1. **Pick** *(discovery, bounded)* — take the next unit off the plan. Not "find work" —
"advance the given goal." If the plan needs revising because reality differed, revise it,
say so, move on.
2. **Build** *(handoff)* — do the unit. If multiple units are genuinely parallel and touch
different files, spawn them in **separate worktrees** (`Agent` with `isolation: "worktree"`,
or `--worktree`) so they don't collide. If serial, just do it in place — don't add worktree
ceremony you don't need.
3. **Verify** *(the move you must never skip — see below).*
4. **Record** *(persistence, minimal)* — mark the unit done/failed in the plan with one line of
outcome. That's it. PR/merge happens per the stop posture, not per unit unless asked.
5. **Continue** *(scheduling)* — check the done-condition. If met → finish per stop posture. If
caps hit → stop and report. Otherwise → next turn. Under `/loop`, this is the next iteration;
standalone, just continue the list.
## Verification — the independent evaluator (the heart)
After building a unit, **do not grade it yourself.** Spawn a FRESH evaluator with the `Agent`
tool. It carries none of your context — it never saw the reasons the code was written the way it
was, so it can't be persuaded by them. The maker-checker split, ported from banking: whoever
enters the transfer is not who approves it.
The evaluator's brief, every time:
```
ROLE: Adversarial reviewer. You did NOT write this code.
ASSUME: this change is BROKEN until proven otherwise. Default to REJECT. Do not praise.
ACT, don't just read — judge whether it RUNS right, not whether it LOOKS right:
1. Build it. (octokraft: go build ./... | frontend: tsc/biome)
2. Lint it. (golangci-lint run ./... | npx biome check src/)
3. Run the tests. (go test ./...) — paste real output, not a summary.
4. Run preflight. (preflight analyze --json) — check verdict + blockers.
5. Edge cases the author skipped; does behavior match the unit's intent?
6. Octokraft rules: no silent failure, no workaround/fallback, no stub/TODO,
no v1/v2 split, errors wrapped, repository pattern, prod==dev.
VERDICT: PASS only if every check holds. Otherwise REJECT + list each concrete reason.
```
- Prefer a **different/fresh agent** over self-review. Use `preflight review --analyzer
claude-code` and/or `/code-review` as part of (not instead of) the independent pass.
- **REJECT → fix → re-verify.** Loop the unit until PASS or until you've burned its retry cap,
then stop loudly and report — never ship a unit the evaluator rejected, never water down the
evaluator to get a PASS. Tuning the judge to be lenient defeats the entire skill.
- A loop whose evaluator has "never said no" across many units has no real check. That's the
Nodding Loop. Expect and welcome rejections.
## Stop condition & posture
The loop ends when the **done-condition holds**, judged against real check output (a fresh
build/test run), not against your own assertion that it's done. Then:
- `stop-at-pr` *(default)*: open the PR with a summary of units + evaluator outcomes, **stop**.
- `auto-merge` *(only if requested)*: merge the fully-validated work, continue. Still stop on any
evaluator REJECT you couldn't resolve, or on uncertainty — route those to the human, don't
paper over them.
- `branch-only`: leave it on the branch, report.
If caps hit before the done-condition: **stop and report what's done, what's not, and why.**
Partial progress reported honestly beats a forced finish.
## Guards — set these before running unattended
The four silent costs of a self-running loop, and the one-line guard for each:
- **Token blowout** → **Cap before you ship.** Set max turns / per-unit retry limit BEFORE the
loop runs unattended, not after the first surprising bill. A cap turns open-ended risk into
bounded risk. Under `/loop`, set a turn ceiling.
- **Verification debt** → the independent evaluator above. Unverified-but-merged is debt that
blows up on some quiet morning. Don't accrue it.
- **Comprehension rot** → **Read a sample, every turn.** Actually read one representative chunk
of what the loop produced and be able to explain what it did and why. If you can't explain it,
your map has fallen behind the code — that's the signal, catch it cheap.
- **Cognitive surrender** → **Keep one door open.** The default `stop-at-pr` posture IS the door.
Keep at least one checkpoint where a human *can* say no, even if they usually won't. Don't weld
every door shut for speed.
## Self-check against the five anti-patterns
If the loop feels off, it's almost always one move skipped. Name it, fix that move:
- **Nodding** (no real verification) — output self-approved; evaluator never says no. → Restore
the independent, default-to-broken evaluator.
- **Amnesiac** (no persistence) — redoing/conflicting work each turn. → Keep the plan ledger
current; mark units done.
- **Manual** (no continuation) — stalls waiting for you to nudge each step. → Let `/loop` or the
unit list drive; don't hand-crank.
- **Blind** (you're choosing each unit by hand) — fine for a given goal, but if you're inventing
*new* work mid-loop, stop: that's scope creep, not the epic.
- **Tangled** (parallel agents colliding) — only when running units in parallel. → One worktree
per parallel unit.
## Running it
- **Once, standalone:** invoke `/epic-loop <goal> [done: <condition>] [posture: stop-at-pr|
auto-merge|branch-only] [max-turns: N]`. Plan, then work the list to the done-condition.
- **Self-paced over many turns:** wrap in `/loop` so each iteration advances one unit and feeds
the next. Pass a turn cap.
- **Toward a strict stop-condition:** if the harness offers `/goal`, the done-condition is its
stop check, judged by a fresh model run — same idea as the evaluator, applied to the finish.
The discipline in one line: **build the loop, but build it like someone who intends to stay the
engineer** — generation is cheap, judgment is the scarce thing, so spend it on the evaluator and
the door, not on watching the loop type.
Start smaller than Stripe
You don’t need a 1,300-PR-a-week pipeline to get value from this. Pick one epic you’d normally babysit (a dependency upgrade, a lint-rule migration, a test backfill) and run it as a loop with a real evaluator and a stop-at-pr door. Set the turn cap first. Then count how many times the evaluator says REJECT. Mine rejects often, and every rejection is a bug I didn’t have to find in review.
The paper’s closing line is the one I keep coming back to, so I put it in the skill verbatim: build the loop like someone who intends to stay the engineer. Generation is cheap. Judgment is the scarce thing. Spend it on the evaluator and the door, not on watching the loop type.
Further reading: Addy Osmani’s Loop Engineering post (also on his Substack), and the O’Reilly Radar piece covering the same ground.