Loop Engineering
Loop engineering is the discipline of turning an agent that runs into a system that converges. It is the shift from prompting a model and hoping, to building a bounded, observable, self-repeating machine that stops on an external truth, records why, and can be resumed.
A prompt produces an answer. A loop produces a verdict - and a receipt that lets someone else trust that verdict without re-running the agent. The difference is not scale. It is structure.
loopexec is a deterministic runtime for this discipline. It is not an agent, a model, or a task runner. It governs how work is executed, recorded, and resumed.
A loop is not a prompt
Section titled “A loop is not a prompt”A prompt is a single shot. You phrase the request, the model samples a continuation, you read the result. If it is wrong, you re-phrase. The control system is you, in the chair, at the keyboard. Nothing bounds cost, nothing records why, nothing stops on its own.
People then wrap that shot in a while loop and call it autonomy. It is not. A naive loop inherits every failure of the single prompt and compounds it across iterations:
- Runaway - nothing bounds total spend, so a “$10 job” spends $200 across twenty turns.
- Silent death - a non-zero agent exit kills the loop with no record, and a heartbeat nobody reads cannot catch it.
- Random walk - the loop fixes test A while breaking test B, accepts the diff, and drifts forever without converging.
- Understanding debt - diffs merge faster than any human reads them, and no one can say what changed.
A loop is not a prompt repeated. A loop is an engineered control system with four parts a prompt does not have: an external check it converges toward, durable state it carries between iterations, isolated execution it cannot escape, and explicit stop conditions that compute a halt reason instead of trusting a claim. Remove any one and you are back to a while loop around a guess.
The convergence problem
Section titled “The convergence problem”The single hardest problem in loop engineering is convergence: how does the loop know it is done, and how does it move toward done rather than wandering near it?
The wrong answer is the obvious one - ask the agent. An agent’s self-assessment is an echo. A model that grades its own output grades its own blind spots; a loop built on self-grading does not converge, it ratifies. It will report success the moment it is confident, which is exactly the moment it is most likely to be confidently wrong. This is the first principle of the discipline:
Breaking the echo requires two distinct things, and conflating them is the most common error in the field:
- An external oracle - a process outside the agent that returns a verdict the agent cannot author.
- A force that pulls state toward the oracle’s fixpoint - because an oracle tells you when you have arrived, but exerts zero force getting you there. A fixpoint check by itself permits an infinite random walk that never reaches the fixpoint. The pulling force is the monotone ratchet (property five).
A check answers “are we there?” A ratchet answers “are we closer than last time?” You need both. A loop with only the first is a thermometer; a loop with both is a thermostat.
The seven properties of a real loop
Section titled “The seven properties of a real loop”A loop that is safe to leave running while you sleep has seven properties. Each one closes a specific death named above. Together they are the difference between a wrapper and an engine.
| # | Property | Closes |
|---|---|---|
| 1 | External oracle | the echo / self-grading |
| 2 | Stateless iteration | context rot, quadratic cost |
| 3 | Narrow, budgeted context | blind edits, silent eviction |
| 4 | Guards dominate success | reward hacking |
| 5 | Monotone acceptance / ratchet | the random walk |
| 6 | Isolation | exfiltration, sandbox escape |
| 7 | Receipts | unverifiable, unauditable runs |
1. External oracle
Section titled “1. External oracle”The check is an external process returning an exit code, independent of the agent (SPEC O1). The agent’s opinion is input; the exit code is truth.
An oracle is only trustworthy if it is deterministic, and determinism is not a one-time probe - it is a maintained confidence bound. Ten clean runs do not certify a stop condition; zero failures in ten runs only bounds the flake rate to roughly 30% at 95% confidence. So the contract specifies that probe-check derives its run count from a target flake rate and reports an achieved confidence bound, and that every in-loop check invocation is treated as a free Bernoulli sample feeding a sequential lower bound that halts check_not_deterministic the moment stability drops below target.
Determinism is also not adequacy. A check that returns 0 regardless of the diff is perfectly deterministic and perfectly useless - the false fixpoint, green on code the check never ran. The contract’s doctor gate requires a coverage delta on every changed line plus a mutation canary that must turn the check red, halting check_inadequate otherwise.
Status: the probe-check confidence bound is Shipped (core) - the adversarial perturbation pass and the in-loop sequential monitor are Planned. The doctor gate ships its determinism, isolation preflight, and the mutation-canary adequacy gate (doctor --mutate-cmd); hermeticity and the coverage-delta tier are Planned. See Determinism.
2. Stateless iteration
Section titled “2. Stateless iteration”Progress lives on disk and in git, not in the context window. Each iteration starts from a fresh, narrow context and reads its state from .loop_state.json and the repository - never from chat memory.
This is the load-bearing idea. Carrying the full transcript forward makes cost quadratic and re-introduces context rot: the window fills with stale reasoning until the signal drowns. Externalizing progress to durable state keeps cost roughly linear and makes the loop resumable - on resume, the runtime revalidates its determinism and metric-integrity snapshots before the first agent call and halts on drift.
Status: typed durable state per SPEC section 8 is Shipped.
3. Narrow, budgeted context
Section titled “3. Narrow, budgeted context”Each iteration builds a bounded, relevant slice of the repository - not the whole tree. The token budget is a true ceiling, measured with a code-calibrated estimator (code tokenizes denser than prose), and a slice that cannot fit even after widening its relevance tiers MUST emit context_budget_unsatisfiable rather than silently evicting a file the iteration needs.
The failure this closes is the symptom-file random walk: when the root cause sits outside the stack trace, a naive relevance heuristic edits the visible symptom forever. The contract specifies escalation tiers - stack trace, then test-import closure, then a one-hop dependency graph - that widen before the loop declares itself stuck.
Status: build-context is Shipped (core) - the stack-trace, last-diff, and untracked relevance tiers ship; the import_closure and dep_graph tiers are Planned. The contract requires the budget to be a ceiling that fails loudly, never a target it silently overruns.
4. Guards dominate success
Section titled “4. Guards dominate success”Guards are evaluated against an immutable baseline t0 captured at loop start, before any green branch can fire. This ordering - guards dominate success, G-before-S - is not a detail. It is the difference between a defense and a sieve.
The canonical bug it fixes: a loop that checks the green exit before the integrity gate lets an agent weaken a test, pass the suite, and exit as success - the gate it was supposed to hit is never reached. The defense is bypassed by the exact event it exists to catch. So the iteration is one ordered transaction:
each iteration: 1. evaluate guards against immutable t0 2. if a guard is violated -> classify as the guard reason, revert, halt 3. ONLY THEN may a green check declare success_condition_metA check that went green because a guard was violated classifies as the guard reason, never success_condition_met. The guard itself is the metric-integrity gate: it supersedes the naive git diff --quiet -- test/ and instead holds monotonic invariants over the full test-determining surface - the collected-test set may never lose a member, test-count and AST-level assertion-count may never decrease, a protected-manifest hash over runner config, CI yaml, and the test-dep lockfile stays frozen, and coverage may not fall below the t0 floor. Detection covers working-tree, staged, committed, and untracked changes, because the cheapest cheats hide in exactly the places a working-tree diff is blind to.
Status: computed halt reasons (replacing the --halt-reason flag) are Shipped; the metric-integrity gate is Shipped (core) - collected-set monotonicity ships via run --integrity-cmd, while the assertion-count, manifest-hash, and coverage-floor layers are Planned. See Halt reasons.
5. Monotone acceptance / ratchet
Section titled “5. Monotone acceptance / ratchet”This is the pulling force the convergence problem demands - the Lyapunov function that makes “progress” definable and termination provable for a feasible target.
The rule: accept an iteration’s diff only if its set of passing tests is a superset of the last accepted set. A diff that fixes one test while regressing another is reverted to the last accepted commit, and the loop re-prompts. A binary green/red signal cannot encode “this diff regressed 8 previously-passing tests”; the ratchet records 48/50 -> 40/50 and refuses it. A strict numeric score >= best_so_far ratchet is specified as an opt-in with an acceptance band, because legitimate refactors sometimes dip transiently.
Progress is defined over the failing-test-ID set F_i parsed from a structured reporter, not a grepped line. The loop halts on no strict decrease in |F_i| over K iterations, on a previously-passing test re-entering F (oscillation), or on a contradictory pair across cycles (unsatisfiable_constraints) - and it distinguishes “still improving at the cutoff” (raise the limit) from “stalled / infeasible” (do not retry). That distinction is the one thing the operator woken at 3am actually needs.
Status: the no-regression ratchet is Shipped (core) - set-based progress with oscillation, no-progress, and regression halts ships inside run via --failures-cmd; git revert-to-best is Planned. The contract requires the superset gate as the hard guarantee; the numeric ratchet is the opt-in band on top of it.
6. Isolation
Section titled “6. Isolation”A single --network none container cannot both run a cloud agent and isolate untrusted code - the agent needs egress to reach its model endpoint, and that same egress is the exfiltration path. The contract therefore specifies two-zone isolation:
- Agent zone - the reasoning surface. Network is default-deny with an egress allowlist to the model endpoint only, through an auditing proxy. Credentials are exactly one per-run minted, short-TTL, spend-capped key - never a
~/.claudebind-mount, which:roprotects from writes but not from reads. - Exec zone - untrusted code execution. Network
none, hermetic, reset every run.
The two zones share only a work volume that is a detached clone, not a git worktree. A worktree is ergonomic, not a security boundary: it shares the object DB, refs, remotes, hooks, and credentials, so a confused or injected loop can push to main, rewrite refs, or plant a host hook. The detached clone strips the credential helper, points core.hooksPath at /dev/null, and routes any promotion through a reviewed bot, with server-side branch protection as the backstop.
Status: two-zone isolation and the per-run minted key are Shipped (core) via isolate - the detached-clone sandbox, the per-run minted/revoked credential, and the rendered exec and agent zones ship, while the container engine, the auditing egress proxy, and the provider key API are operator-provided hooks (--runtime, --egress-proxy, --mint-cmd / --revoke-cmd). doctor fails closed - rejecting network: none for a cloud model, rejecting any ~/.claude mount, requiring the egress allowlist - before a loop may start. See Isolation.
7. Receipts
Section titled “7. Receipts”Every halt emits a computed reason and a replayable receipt. The receipt pins everything that determines output, or fails closed (workspace_invalid): the model-identity tuple {provider, model_id, version/build, quantization, runtime, endpoint}, the recorded sampling params, a context manifest of [{path, content_sha256}], parsed cost actuals, and - per iteration - the check command, exit code, commit, computed halt reason, and achieved determinism confidence.
This is where the discipline earns its honesty. A live-LLM trajectory is not reproducible - the model samples, drifts, and tools are nondeterministic. Only the verdict is. So two verbs must never blur:
replay= VERIFY. Re-run the deterministic check against the recorded end-state and confirm the fingerprint matches the receipt. Agent-free, budget-free, deterministic. Answers “does this receipt’s verdict hold?”reexecute= RE-RUN. Run the live agent loop again. Non-deterministic; reports a statistical match, never byte identity. Budget-burning,--confirm-gated.
The marketing line is therefore replayable verdicts, never “replayable runs.” The credibility of the whole system rests on that one word.
Status: the typed JSONL receipt and durable state are Shipped; replay, reexecute, and attest are Shipped. Receipt pinning records the model-identity tuple, sampling params, context manifest, and check fingerprint today; live cost metering is the Planned sub-part. See Receipts and replay.
Two topologies: check_fixpoint and task_list
Section titled “Two topologies: check_fixpoint and task_list”loopexec hosts two control machines. The mode is explicit and recorded in every receipt, because leaving it implicit silently forks the product and makes receipts non-replayable.
loop.mode | Stop condition | Gate |
|---|---|---|
check_fixpoint | external check returns success_exit_code | the application oracle: test, build, lint, or score |
task_list | all plan tasks completed | state-hygiene validation plus a per-task acceptance oracle |
loop: name: fix-failing-tests mode: check_fixpoint # the degenerate one-task plan max_iterations: 20 # the only true bound on total spend stateless: trueThe relationship between them is exact. check_fixpoint is the degenerate one-task plan whose acceptance criterion is the external check. A task_list loop must attach a real per-task acceptance oracle - state-hygiene validation alone cannot decide application correctness, and a task_list loop without a per-task oracle MUST refuse to mark tasks completed and halt check_inadequate. State hygiene that grades its own completion is the echo wearing a different hat.
Both modes obey the same invariant: the receipt records, per iteration, which oracle was evaluated at which commit, under which pinned model, having spent how much, with which guards intact. A receipt that cannot answer that is not a halt record.
Capability status
Section titled “Capability status”Loop engineering is the discipline; loopexec is the engine that enforces it. As of v0.2.0 the binary ships the engine through Slice 7 - every capability below has a Shipped core, with named sub-parts still Planned where noted. This table is the contract between the binary and this site, and it is normative in SPEC section 11. No capability is presented here as shipping before it does.
| Capability | Status |
|---|---|
CLI contract: --json, stable exit codes, deterministic output | Shipped |
init / status / check / step commands | Shipped |
run as a real iterating loop | Shipped |
Computed halt reasons replacing --halt-reason | Shipped |
| Typed JSONL receipt + durable state | Shipped |
probe-check confidence bound | Shipped (core) |
doctor precondition gate | Shipped (core) |
build-context, ratchet, replay / reexecute, escalate / watch, attest / ack | Shipped |
| Two-zone isolation + per-run minted key | Shipped (core) |
| Metric-integrity gate | Shipped (core) |
Where to go next
Section titled “Where to go next”The discipline becomes practice in the guides:
- Determinism - turning a check into a trustworthy oracle, and the confidence bound that keeps it one.
- Halt reasons - the computed reasons and their stable exit-code classes.
- Receipts and replay - what gets pinned, and why verdicts replay but runs do not.
- Isolation - the two zones, the detached clone, and the per-run minted key.
- Getting started - install the binary and run what ships today.