1/1

Stop Prompting
Start Looping

Principles of Loop Engineering

Derrick Bozkurt · SRE Manager, RingCentral

The prompt was the easy part.

prompt→agent writes code→??→ship

The prompt doesn’t decide

  • is the change safe?
  • did it pass?
  • should it retry?
  • should it stop?
  • did it actually solve the problem?

So what controls the system?

Agent proposes.

Loop decides.

Intelligence generates the work. The control system decides whether it counts.

Every Reliable Loop Answers Five Questions

Building the control system around the model, not just the prompt inside it.

What is thesystem?goalanchorspromptagentHow does itgovern itself?guardsafety haltcommitHow does it knowit's working?iteration gatereviewheld-out gateHow does itterminate?doneexhaustedHow does itscale?worktreesrunning many

Goal:Define Done In Observable Terms

A goal is one sentence naming done: not a plan, not a task list — the state the run must reach.

Every tick rebuilds the prompt around it: the context resets, the goal does not.

The rest of the loop points back at it: stops bound it, review judges it, done measures it.

without it

The loop cannot tell finished from stopped. It ends when the agent runs out of things to say.

loopkit.tomlgoal = "… per PROMPT.md, including the bulk pricing rule."

Anchors:The Context Rebuilt Every Tick

Anchors are ordinary files: the task, the conventions, and the plan, versioned with the code.

The loop rebuilds the prompt every tick: goal, anchors, last feedback, rules. No history.

Assume the session is lost: a cold agent must resume from the files on disk alone.

without it

State lives in the conversation. Nothing survives a restart, and what tick 3 learned is gone by tick 4.

loopkit.tomlanchors = ["PROMPT.md", "CLAUDE.md"]

Prompt:One Bounded Attempt

A tick is the unit of work: one prompt, one attempt, one verdict, counted and capped.

Give one bounded objective: a small testable step makes progress and regression both visible.

A fresh context beats a long one: the loop discards history, so quality does not decay.

without it

An unbounded tick gives you nothing to count, cap, retry, or resume.

loopkit.toml[stops] max_iter = 8

Agent:The Only Part That Proposes

The agent is a subroutine, not the system: the loop around it decides whether a change counts.

The agent’s own “done” is not a signal: a finished turn says nothing about a finished task.

Check the agent can act before you judge it: a blocked agent looks like a failing one.

without it

The agent’s own claim of “done” means its turn ended, not that the work met the goal.

loopkit.toml[agent] adapter = "claude-code"

Guard:Reject The Change Before It Lands

Preflight runs before tick one: the loop refuses a dirty tree or a forbidden branch.

The tick guard sits between agent and commit: nothing unscreened ever reaches git history.

It reads the diff, not the agent’s summary: what changed on disk is the only evidence.

without it

Nothing screens the diff, so a bad tick commits exactly like a good one.

loopkit.toml[safety] protected_paths = ["tests/", "grading/"]

Safety Halt:Some Changes End The Run

It fires before the commit: the loop reverts the tick, and nothing lands.

This is policy, not quality: there is no better attempt to ask for, so a person must look.

Protect anything you would not merge unreviewed: tests, CI config, migrations, and lockfiles.

without it

A dangerous diff has no terminal state, so it commits like any other change.

loopkit.toml[safety] protected_paths · allow_branches · require_clean_tree

Commit:Make Safe Progress Durable

Commit every tick: a crash then costs one tick, not the run, and the loop resumes from HEAD.

Commit only what cleared the guard: the loop checks safety before it makes progress durable.

A commit is not done: it preserves progress, and the gate decides completion.

without it

A crash with no per-tick commit loses everything the run has done.

gitone commit per tick, on the loop’s own branch

Iteration Gate:Fast Feedback On Every Tick

Run it every tick: cheap enough to afford always, specific enough to guide the next attempt.

Feed back the failure text, not just the verdict: the diagnostics become the next prompt.

Prove the gate is stable first: two runs on one tree must agree, or every stop is corrupt.

without it

With no error signal, the loop repeats the same attempt, quickly and without correction.

loopkit.toml[gate] iteration = "python3 -m pytest tests/seen -q"

Review:Judgment The Iteration Gate Cannot Encode

Review what tests cannot express: policy, structure, and intent live outside assertions.

Never let the author grade the work: independence comes from a fresh context, not a new model.

A broken judge halts the run: it is an infrastructure failure, never a rejection.

Log whether the check ran: a step that defaults to off never runs at all.

without it

Anything a test cannot express goes unenforced. That is where the loop drifts first.

loopkit.toml[review] enabled = true

Held-Out Gate:The Check It Never Optimizes Against

It comes from the failure you already have: the bug report, the ticket, the incident.

The iteration gate steers, the held-out gate decides: two checks with two different jobs.

The agent fits any check it can rerun: passing is not the same as solving.

Feed back the result, not the checks: it learns what failed, never how it is graded.

without it

Every pass looks the same. You cannot tell a solved problem from a fitted one.

measured10 runs · the agent shipped a fitted answer in 7 · held-out caught 7 of 7

Exhausted:Cap, Budget, No Progress

Stops need an order, not a set: budget first, because spent money cannot be unspent.

Catch stuck early, not at the cap: an unchanged tree for N ticks is a spinning loop.

An agent that keeps editing never looks stuck: count rejected reviews, not file edits.

Record which stop fired: done, halted, and exhausted each need a different answer.

without it

A wedged loop spends the whole budget reaching the cap, and giving up reads like success.

stop precedencemax_cost_usd → no_progress_after → review_stall_after → max_iter

Held-Out Gate:The Check It Never Optimizes Against

It comes from the failure you already have: the bug report, the ticket, the incident.

The iteration gate steers, the held-out gate decides: two checks with two different jobs.

The agent fits any check it can rerun: passing is not the same as solving.

Feed back the result, not the checks: it learns what failed, never how it is graded.

without it

Every pass looks the same. You cannot tell a solved problem from a fitted one.

loopkit.toml[gate] acceptance = "python3 -m pytest tests/holdout -q"

Done:Earned, Not Announced

Done is a verdict, not a feeling: the held-out gate passed, and that is the entire claim.

Verify the claim once, by hand: read the diff and run the held-out gate yourself.

A passing gate is not a shipped change: check the branch and the PR, not the exit code.

without it

If the loop decides its own completion, nothing outside it confirms whether the work is actually done.

evidencethe branch, the diff, and the gate output, not the summary

Worktrees:One Repository, Separate Working Directories

.git one history · one set of gates work/task-1 branch loopkit/task-1 one loop, alone in here work/task-2 branch loopkit/task-2 one loop, alone in here work/task-3 branch loopkit/task-3 one loop, alone in here separate files · separate branches · shared history

One history, many working directories: each checks out its own branch from one .git.

One task, one branch, one directory: two agents in one tree corrupt each other.

An isolated workspace holds only tracked files: you must reproduce everything else.

without it

Parallelism without isolation adds conflicting edits, not throughput. A human still merges them.

gitworktree add .work/task-1 loopkit/task-1

Running Many:Batch, Serialize, Don't Trip

wave 1 task-1auth.py task-2billing.py task-3search.py wave 2 task-4auth.py task-5billing.py wave 3 task-6 auth.py · billing.py

Batch by file overlap: tasks that share no files start in the same wave.

A shared file means waiting: task-4 touches auth.py, so it runs after task-1 lands.

Scale only what you can isolate and verify: supervision cost grows with every loop.

without it

Concurrency multiplies whatever one loop already does, including any gaps in its gates or guards.

loopkit.tomlbatch --tasks tasks.toml --jobs 3

Beyond The Loop:Scaling In Three Dimensions

These aren’t new systems. They’re new ways of applying the same loop.

MEASUREMENTPASS@K / PASS^Kcapability vs reliabilitySELECTIONEVOLUTIONHow does it improve?KNOWLEDGESKILLSHow does it remember?pass@kpass^kanyallof k attemptsof k attemptscan it do this?can I walk away?candidate loopsheld-out gatekeep bestrepeatverified successextract proceduresave skillfuture loops inherit
measured10 runs, 30% right each · pass@10 97% · pass^10 1 in 169,000 · with the gate, 100%

All Of It:Five Questions, One Loop

What is thesystem?goalanchorspromptagentHow does itgovern itself?guardsafety haltcommitHow does it knowit's working?iteration gatereviewheld-out gateHow does itterminate?doneexhaustedHow does itscale?worktreesrunning many

Settle these before tick one: the goal, the anchors, the gates, and the bounds.

Every outcome is evidence: a pass advances, a failure feeds the next prompt, a violation halts.

Add them in order: goal and gates first, then the stops, before the first unattended run.

without it

The system around the model is the part you actually write.

The future isn't better prompts.

It's better control systems.

Where To Start:Clone Loopkit, Mold To Your Repo

github.com/debozkurt/loop-kit

Derrick Bozkurt

RingCentral · debozkurt@gmail.com

getting startedask your coding agent to mold loopkit to your repo — config, both gates, safety envelope, for you to review