DEV Community

Cover image for Anatomy of an Agent Loop: 5 Building Blocks and One Memory Spine
ShipWithAI
ShipWithAI

Posted on Originally published at shipwithai.io

Anatomy of an Agent Loop: 5 Building Blocks and One Memory Spine

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After iteration 2:

# no output, exit 0
Enter fullscreen mode Exit fullscreen mode

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 openfixed, 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)