Skip to content
PKResources
Guides

Loop engineering: stop prompting, start building the loop

How to go from prompting a coding agent by hand to engineering a bounded loop that prompts it until a goal is met, with gates, memory, budgets and a runnable bash sketch.

by Patrick Kamtchueng Kom · Published

Watch yourself work with a coding agent for an hour. You type a request, wait, read the output, run the tests, paste the failure back, type “try again, but don’t touch the migration”, wait again. Most of that is not judgment. It’s you acting as a very slow while loop.

Loop engineering is the habit of noticing that, and moving the repetitive part into a script. You write the goal, the checks and the limits once. The loop does the prompting. You come back to a green branch, or to a clear note saying why it stopped.

  1. DiscoverRead the goal, the repo, the memory file and the last failure
  2. PlanPick one small step toward the goal
  3. ExecuteThe agent edits code, headless
  4. VerifyThe loop runs the gates, not the agent

↺ Not done yet? Record what happened, then go around again, until the goal is met or the budget runs out

One iteration of a closed loop. The rest of this guide is about making each box trustworthy.

Prompting vs looping

Prompting is a conversation. Looping is a system. Both are useful; they fit different work.

Prompting by hand Engineering a loop
Who decides “try again”? You, after reading the output A script, based on check results
What counts as done? “Looks good to me” A command exits with code 0
Where does context live? In your head and the chat history In files the loop reads every time
What you spend time on Babysitting each turn Writing goals, gates and limits
Best for Exploring, unclear problems, design Well-defined work with a checkable finish line

Open loops vs closed loops

“Just let the agent keep going” is also a loop. It’s an open one: nothing outside the agent decides whether the work is good, and nothing decides when to stop. That’s how you get a Monday-morning diff of 90 files and a surprising invoice.

A closed loop has a feedback path and a boundary. The result of each run is measured against a goal, and the loop ends on purpose.

✕ Open loop

  • –Runs until someone notices it's still running
  • –"Done" means the agent said it's done
  • –No cap on iterations, tokens or time
  • –The agent grades its own homework
  • –Every run starts from zero

✓ Closed (bounded) loop

  • +Starts from a written goal with checkable completion criteria
  • +Verification runs outside the agent, in the loop script
  • +Hard caps on iterations, spend and wall-clock time
  • +Exits on success, on budget, or when it's stuck
  • +Writes down what it learned for the next iteration

Anatomy of a closed loop

Every closed loop I’ve built has the same five parts. If one is missing, the loop still runs, it just can’t be trusted.

BUDGETiterations · tokens · time · $not done: iterateDiscoverrepo, memory, last runPlanpick one small stepExecuteagent edits the codeVerifytests · types · lint · evalsGoaldone = every checkexits with 0Exitcriteria met · budget spent · same failure 3× → humanMemoryprogress.md · lessons.md · git history
The closed loop. The goal sits in the middle, the budget is the box around everything, and memory is what lets iteration 6 be smarter than iteration 1.

🃏 The five parts, one line each

Tap a card to flip it

Write a goal the loop can check

The goal is the part people rush, and the part that decides everything else. “Improve the search page” gives the loop nothing to measure. “The test file search.spec.ts passes and the bench:search script reports p95 under 200 ms” does.

A good test: could a shell script decide, with no LLM involved, whether you’re done? If yes, you have completion criteria. If no, you have a wish, and you need a human checkpoint somewhere.

🎮 Can the loop check this on its own?

1 / 9 · Score: 0

Sort each goal: can a script verify it, or does it need a human checkpoint?

Quality gates: cheap first, expensive last

Gates are the loop’s eyes. Run them in order of cost so the fast ones fail first and you don’t pay for a 10-minute test suite to discover a syntax error.

  1. 1

    every turn

    Format and lint

    Seconds. Catches noise so later gates see real problems.

  2. 2

    every turn

    Types

    Catches invented APIs and wrong shapes, the most common agent slip.

  3. 3

    every turn

    Targeted tests

    The tests closest to the change. Fast feedback on the actual goal.

  4. 4

    when cheap gates pass

    Full suite and acceptance check

    The command that defines done. Only run when the cheap gates are green.

  5. 5

    LLM features

    Evals

    For prompts, agents or ranking code: a fixed dataset scored the same way each time.

  6. 6

    at the boundary

    Human checkpoint

    Before merge, before anything irreversible, and whenever the loop exits for a reason other than success.

Memory between iterations

A headless agent starts each run with a blank slate. Without memory, iteration 5 repeats the mistake from iteration 2. The fix is boring and works: plain files the loop reads before every turn and the agent appends to after every turn.

I keep two. A progress file for this task, and a lessons file that outlives it.

# progress.md  (this task only, the loop feeds the last ~60 lines back in)
## iter 3
- Added cursor param to OrderRepository.list; typecheck green.
- orders.spec.ts: 2 failures, both "expected nextCursor to be null on last page".
- Next: handle the last page in the service, not the controller.

## iter 4
- Fixed last-page case in OrderService. All order tests green.
- Lint fails: unused import in orders.controller.ts. Next: remove it.
# lessons.md  (survives across tasks, reviewed by a human weekly)
- Integration tests need `docker compose up db` first; unit tests don't.
- Never mock OrderRepository in service tests, use the in-memory fake.
- Dates in the API are ISO strings in UTC. Don't convert to local time.

Git is memory too. Committing at every green step gives the loop checkpoints to roll back to, and gives you a readable history of how it got there.

A minimal loop you can run

Here’s a small bash sketch of a closed loop. It isn’t a framework, and that’s the point: about 50 lines you can read, own and change. It uses Claude Code in headless mode (claude -p); the comment shows where Codex’s codex exec would go instead.

#!/usr/bin/env bash
# loop.sh: prompt a coding agent until the gates pass, the budget runs out,
# or it gets stuck. Run it on a fresh branch from a clean working tree.
set -uo pipefail

MAX_ITER="${MAX_ITER:-8}"                      # hard cap on turns
GATES="${GATES:-npm run lint && npm run typecheck && npm test}"
PROTECTED="tests/"                             # files that define "done"
TASK=task.md; PROGRESS=progress.md; LOGS=.loop
mkdir -p "$LOGS"; touch "$PROGRESS"

git diff --quiet && git diff --cached --quiet || { echo "Start from a clean tree."; exit 1; }

last_sig=""; same=0
for i in $(seq 1 "$MAX_ITER"); do
  echo "== iteration $i/$MAX_ITER"

  prompt="$(cat "$TASK")

## Progress so far
$(tail -n 60 "$PROGRESS")

## Output of the last gate run
$(tail -n 80 "$LOGS/gates.log" 2>/dev/null)

Make ONE small step toward the goal. Do not edit anything under $PROTECTED.
Then append a short '## iter $i' entry to $PROGRESS: what you changed,
what you learned, what the next step should be."

  # Swap this line for: codex exec --sandbox workspace-write "$prompt"
  claude -p "$prompt" --permission-mode acceptEdits --max-budget-usd 1 \
    > "$LOGS/agent-$i.log" 2>&1

  # Guard: undo any edit to the files that define "done".
  if git status --porcelain -- "$PROTECTED" | grep -q .; then
    git checkout -- "$PROTECTED"; git clean -fdq -- "$PROTECTED"
    echo "- iter $i: agent edited $PROTECTED, changes reverted." >> "$PROGRESS"
  fi

  # Verify: the loop runs the gates, not the agent.
  if bash -c "$GATES" > "$LOGS/gates.log" 2>&1; then
    git add -A && git commit -qm "loop: goal met at iteration $i"
    echo "Done in $i iterations. Review the branch."; exit 0
  fi

  # Stuck detection: same failure signature three times in a row.
  sig="$(grep -iE 'error|fail' "$LOGS/gates.log" | sort | cksum)"
  if [ "$sig" = "$last_sig" ]; then same=$((same + 1)); else same=1; fi
  last_sig="$sig"
  if [ "$same" -ge 3 ]; then
    echo "Stuck: same failure 3 times. Handing over to a human."; exit 3
  fi
done

echo "Budget spent after $MAX_ITER iterations. See $PROGRESS."; exit 2

A few choices worth copying:

  • The loop runs the gates. The agent never gets to report “all green”. Exit codes do.
  • Three distinct exits. Code 0 is success, 2 is budget, 3 is stuck. Whatever wraps this script (you, cron, CI) can react differently to each.
  • One step per turn. Small turns mean small diffs, and a failure points at one change instead of ten.
  • Two budgets. MAX_ITER caps the number of turns, and the per-run spending cap (check your agent’s docs for the equivalent flag) caps each turn.

From your laptop to CI

Once a loop works locally, the same script runs anywhere a headless agent can: a cron job, a container, a CI runner. The pattern that works well is “loop on a branch, human on the pull request”.

# Sketch of a scheduled CI job. Adapt names and secrets to your platform.
on:
  schedule: [{ cron: "0 3 * * 1-5" }]
jobs:
  dependency-loop:
    runs-on: ubuntu-latest
    timeout-minutes: 45                  # wall-clock budget
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm i -g @anthropic-ai/claude-code
      - run: git switch -c loop/deps-$(date +%F)
      - run: MAX_ITER=6 ./loop.sh        # exits 0, 2 or 3
      - run: gh pr create --fill --draft  # a human reviews every loop PR
        if: success()

Keep the runner’s permissions tight: a scoped token that can push branches and open draft PRs, not merge to main. The CI timeout is one more budget, and a cheap one to set.

One loop or a fleet?

Once one loop works, it’s tempting to run ten. Parallel loops are great when the tasks are truly independent: one loop per failing package in a monorepo, one per lint rule to adopt, one per module to migrate. Give each its own branch and working directory (git worktrees are perfect for this) so they never step on each other.

Single loop Fleet of parallel loops
Good for One goal with a clear finish line Many similar, independent goals
Isolation A branch A branch and a worktree per loop
Main risk Getting stuck quietly Merge conflicts, runaway total spend
Real bottleneck Your gates Your review capacity

How loops fail

Loops fail in predictable ways. Flip each card for the symptom and the guard that prevents it.

🃏 Loop failure modes

Tap a card to flip it

From operator to engineer

When you prompt by hand, your value is in each turn: reading, nudging, correcting. When you build loops, your value moves up a level. You write goals precise enough to check, gates strict enough to trust, and limits tight enough to sleep on. Then you improve the loop itself, the same way you’d improve a build pipeline.

That’s a real shift in how you spend a day. Less typing “try again”. More asking “why did the loop need three tries for this, and what would make it one?” Usually the answer is a better test, a clearer goal or a new line in the instructions file. All of those help humans too.

Is your repo ready for a loop?

Be honest. A loop amplifies whatever you already have, including weak tests.

📊 Loop readiness

  1. 1. Test, typecheck and lint each run with one command from a clean checkout.

  2. 2. The fast checks finish in under a couple of minutes.

  3. 3. Our tests fail when behavior breaks, not only when code doesn't compile.

  4. 4. We can write the goal of a typical task as a command that exits 0 when it's done.

  5. 5. Our agent instructions file lists the commands, conventions and no-go areas.

  6. 6. We have a place to run agents headless with scoped credentials (a container, a runner, a sandbox).

  7. 7. Someone owns reviewing loop output within a day or two.

Answer every question to see your result

Check yourself

🧠 Loop engineering: quick check

Score: 0 / 5

  1. 1. What makes a loop "closed"?

  2. 2. Who should decide that the tests passed?

  3. 3. The same test has failed with the same error for three iterations. Best move?

  4. 4. Why keep a progress file between iterations?

  5. 5. Your team runs 12 parallel loops, and PRs wait a week for review. What's the fix?

Build your first loop this week

✅ Your first closed loop

0 / 9 completed

✅ Before you leave a loop running alone

0 / 6 completed

Sources

  1. Claude Code docs: Run Claude Code programmatically
  2. OpenAI Codex docs: Non-interactive mode (codex exec)