TL;DR — An agent loop is five blocks doing work plus a memory spine that makes the work cumulative. Drop the spine and you have automation that forgets every morning. Below: the map, the primitive each block maps to in Claude Code, and a real lint-sweeper loop that cleared 3 structural findings in 2 iterations / 124 seconds.
Part 2 of the Loop Engineering series on ShipWithAI. Read the full article →
Part 1 argued why you should stop prompting turn by turn. This one is the parts list.
Working definition, unchanged: a loop is a recursive goal — you define a purpose and a verifiable stop condition, and the system iterates agents against it until the condition holds, without you prompting each turn.
The map
1. Scheduled automation (heartbeat)
2. Worktrees 3. Skills
\ /
[ MEMORY SPINE ] <- state on disk, survives runs
/ \
5. Sub-agents 4. Plugins / connectors
The framing is Addy Osmani's. Each block maps to something you already have:
| # | Block | Claude Code primitive | What it's for |
|---|---|---|---|
| 1 | Scheduled automation |
/loop [interval]; Routines for cloud cron |
Re-running a check on a cadence |
| 2 | Worktrees |
isolation: worktree in agent frontmatter |
Parallel work without file collisions |
| 3 | Skills | SKILL.md |
Externalizing project intent |
| 4 | Plugins / connectors | Plugins and MCP servers | Reaching real tools (GitHub, CI) |
| 5 | Sub-agents | Files in .claude/agents/
|
The maker/checker split |
On block 1 — /loop re-runs a prompt on an interval while your session is open. Routines run cloud-scheduled on cron with the laptop closed (there's a per-account daily cap). Different tools, same job.
Nothing here is exotic. The skill is assembly: picking which blocks a given job needs and wiring them around one state file.
The spine is the part people skip
The five blocks do the work. The spine is what makes the work cumulative.
Concretely: a small file on disk that survives between runs. Greppable, versioned, boring. snarktank/ralph (14.3k stars) does it with git plus a progress.txt and a prd.json.
Without it, iteration 4 has no idea what iterations 1 through 3 already tried. That's not a loop, that's a cron job with amnesia.
A real, minimal loop you can copy
The spec uses the standard kickoff template (from the loops.elorm.xyz card format), which fits the /goal contract:
# lint-sweeper.loop
goal: "flake8 --extend-ignore=E501 scripts/*.py exits 0"
max_iterations: 5
between_iterations_check: "flake8 --extend-ignore=E501 scripts/*.py"
exit_when: "check exits 0 OR iteration == max_iterations"
one_step: "fix one batch of findings (batch = one file), then re-run the check"
self_pace: "stop as soon as exit_when holds; do not keep going"
state_file: "drafts/lint-sweeper-state.md"
And the spine it writes to, at iteration 0:
goal: flake8 --extend-ignore=E501 scripts/*.py exits 0
cap: 5 iterations
iteration: 0
## open
- gen-agent-map.py:19 F401 'os' imported but unused
- gen-dist-metadata.py:23 E301 expected 1 blank line
- gen-dist-metadata.py:27 E301 expected 1 blank line
## fixed
## skipped
Note what goal is: a shell command and its exit code, not an English sentence about quality. As the article puts it — the check is the stop signal, so it must be machine-checkable.
What actually happened when it ran
Two honest caveats from the author before the numbers. This ran on the Python scripts in ShipWithAI's content-agent plugin repo, because shipwithai.io has no lint command — a substitution he flags himself. And flake8 actually reported 41 findings; 38 were E501 (line-too-long), which is noisy and subjective, so the verifier was scoped to the 3 real structural defects.
Baseline:
$ flake8 --extend-ignore=E501 scripts/*.py
scripts/gen-agent-map.py:19:1: F401 'os' imported but unused
scripts/gen-dist-metadata.py:23:5: E301 expected 1 blank line, found 0
scripts/gen-dist-metadata.py:27:5: E301 expected 1 blank line, found 0
After iteration 1 — one file's worth of findings gone:
scripts/gen-dist-metadata.py:23:5: E301 expected 1 blank line, found 0
scripts/gen-dist-metadata.py:27:5: E301 expected 1 blank line, found 0
After iteration 2:
# no output, exit 0
Final tally: 3 structural findings → 0, in 2 of 5 allowed iterations, 124 seconds wall-clock. Clean exit — it never touched the cap. Total damage: 2 files changed, 2 insertions, 1 deletion.
Reading the trace
This is the part worth internalizing. Each block leaves a fingerprint:
- Heartbeat — fired the check 3 times (baseline + 2 iterations)
- Verifier — gated each iteration on flake8's exit code. Exit 1 → continue. Exit 0 → stop.
-
Memory spine — recorded each finding moving
open→fixed, tagged by iteration - Maker — produced the diff, handed it to the verifier
- Worktrees and plugins/connectors — never used
The article is careful here, and so should you be: this run used a single agent with the lint exit code as the checker. The real sub-agent maker/checker split — where a separate agent grades the work — is Part 3 and Part 5 material. It's named in the map, not claimed in the trace. Four pieces exercised, two honestly skipped.
The trace is how you debug a loop. Each block leaves a fingerprint, and the verifier's exit code, not the agent's confidence, is what ends the run.
What breaks when a block is missing
| Missing | Symptom |
|---|---|
| Verifiable stop condition | The loop declares false victory |
| Memory spine | Every run starts from zero, no resume |
| Worktrees / maker-checker | Parallel agents collide on the same files |
| A real heartbeat | You have a script you ran once, not a loop |
Each of those gets its own part later in the series — the heartbeat one is Part 6.
Before you try this
- A working harness — CLAUDE.md, at least one skill, a hook or two
- Claude Code v2.1.139+ for
/goal - A repo with a lint command that exits non-zero when it should
And no, you don't need worktrees for a single-agent loop. They earn their place once more than one agent touches the repo. The lint sweeper ran fine without one.
Try it this week
Pick the smallest thing in your repo where "done" is already an exit code — lint, a type-check, one failing test. Write the spec (goal, cap, check command, exit condition, one-step, self-pace). Add the state file. Cap it at 5.
If it exits clean before the cap, you've built a loop. If it declares victory while the check still fails, your stop condition isn't verifiable — which is exactly what Part 3 is about.
This is a condensed summary. The full article walks the setup step by step, shows the full diff, and answers the /loop vs Routines and do-I-need-worktrees questions:
👉 Anatomy of a Loop: Five Building Blocks and One Spine — Part 2, ShipWithAI
Missed Part 1? Loop Engineering: Why You Should Stop Prompting. Part 3 — Stop Conditions: Making "Done" Mean Something — ships next.
Top comments (0)